diff --git a/README.md b/README.md index 21c0220..8b1044f 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Here, implemented and some unimplemented features are listed. - GET /_matrix/client/r0/login - POST /_matrix/client/r0/login: Only with password flow. - POST /_matrix/client/r0/register: Only with dummy flow. -- POST /_matrix/client/r0/createRoom: Except with option invite_3pid. +- POST /_matrix/client/r0/createRoom: Only with optional parameters name, topic, preset and invite. - GET /_matrix/client/r0/joined_rooms - POST /_matrix/client/r0/rooms/{roomId}/invite - POST /_matrix/client/r0/rooms/{roomId}/join: Except with third party invite. @@ -61,8 +61,6 @@ Here, implemented and some unimplemented features are listed. - PUT /_matrix/client/r0/rooms/{roomId}/state/{eventType}/{stateKey} - PUT /_matrix/client/r0/rooms/{roomId}/send/{eventType}/{txnId} - GET /_matrix/client/r0/rooms/{roomId}/messages: Except filtering. -- GET /_matrix/client/r0/rooms/{roomId}/state -- GET /_matrix/client/r0/rooms/{roomId}/state/{eventType}/{stateKey} - GET /_matrix/client/r0/directory/list/room/{roomId} - PUT /_matrix/client/r0/directory/list/room/{roomId} - GET /_matrix/client/r0/capabilities @@ -78,7 +76,7 @@ Here, implemented and some unimplemented features are listed. - GET /_matrix/federation/v1/state/{roomId} - GET /_matrix/federation/v1/state_ids/{roomId} - GET /_matrix/key/v2/server/{keyId} -- GET /_matrix/federation/v1/query/profile +- GET /_matrix/federation/v1/query/profile: Except displayname and avatar_url is not implemented. ### Major unimplemented features diff --git a/lib/architex/room_server.ex b/lib/architex/room_server.ex index 5214ccc..fe25ee4 100644 --- a/lib/architex/room_server.ex +++ b/lib/architex/room_server.ex @@ -6,6 +6,8 @@ defmodule Architex.RoomServer do The RoomServers are supervised by a DynamicSupervisor RoomServer.Supervisor. """ + @typep t :: map() + use GenServer import Ecto.Query @@ -19,12 +21,9 @@ defmodule Architex.RoomServer do Account, Device, DeviceTransaction, - Membership, - Alias + Membership } - alias Architex.Types.{UserId, StateSet} - alias Architex.StateResolution.Authorization alias ArchitexWeb.Client.Request.{CreateRoom, Kick, Ban} @@ -47,13 +46,8 @@ defmodule Architex.RoomServer do @spec get_room_server(String.t()) :: {:error, :not_found} | DynamicSupervisor.on_start_child() def get_room_server(room_id) do # TODO: Might be wise to use a transaction here to prevent race conditions. - query = - Room - |> where([r], r.id == ^room_id) - |> select([:id, :forward_extremities, :state_set, :visibility]) - - case Repo.one(query) do - %Room{} = room -> + case Repo.one(from r in Room, where: r.id == ^room_id) do + %Room{state: serialized_state_set} = room -> case Registry.lookup(@registry, room_id) do [{pid, _}] -> {:ok, pid} @@ -61,7 +55,8 @@ defmodule Architex.RoomServer do [] -> opts = [ name: {:via, Registry, {@registry, room_id}}, - room: room + room: room, + serialized_state_set: serialized_state_set ] DynamicSupervisor.start_child(@supervisor, {__MODULE__, opts}) @@ -116,10 +111,9 @@ defmodule Architex.RoomServer do @doc """ Invite the a user to this room. """ - @spec invite(pid(), Account.t(), String.t(), String.t() | nil, String.t() | nil) :: - :ok | {:error, atom()} - def invite(pid, account, user_id, avatar_url, displayname) do - GenServer.call(pid, {:invite, account, user_id, avatar_url, displayname}) + @spec invite(pid(), Account.t(), String.t()) :: :ok | {:error, atom()} + def invite(pid, account, user_id) do + GenServer.call(pid, {:invite, account, user_id}) end @doc """ @@ -141,28 +135,25 @@ defmodule Architex.RoomServer do @doc """ Kick a user from this room. """ - @spec kick(pid(), Account.t(), Kick.t(), String.t() | nil, String.t() | nil) :: - :ok | {:error, atom()} - def kick(pid, account, request, avatar_url, displayname) do - GenServer.call(pid, {:kick, account, request, avatar_url, displayname}) + @spec kick(pid(), Account.t(), Kick.t()) :: :ok | {:error, atom()} + def kick(pid, account, request) do + GenServer.call(pid, {:kick, account, request}) end @doc """ Ban a user from this room. """ - @spec ban(pid(), Account.t(), Ban.t(), String.t() | nil, String.t() | nil) :: - :ok | {:error, atom()} - def ban(pid, account, request, avatar_url, displayname) do - GenServer.call(pid, {:ban, account, request, avatar_url, displayname}) + @spec ban(pid(), Account.t(), Ban.t()) :: :ok | {:error, atom()} + def ban(pid, account, request) do + GenServer.call(pid, {:ban, account, request}) end @doc """ Unban a user from this room. """ - @spec unban(pid(), Account.t(), String.t(), String.t() | nil, String.t() | nil) :: - :ok | {:error, atom()} - def unban(pid, account, user_id, avatar_url, displayname) do - GenServer.call(pid, {:unban, account, user_id, avatar_url, displayname}) + @spec unban(pid(), Account.t(), String.t()) :: :ok | {:error, atom()} + def unban(pid, account, user_id) do + GenServer.call(pid, {:unban, account, user_id}) end @doc """ @@ -191,64 +182,44 @@ defmodule Architex.RoomServer do GenServer.call(pid, {:send_state_event, account, event_type, content, state_key}) end - @doc """ - Get the current state of a room. - If the requesting user is not a member of the room, - get the state when the user left the room. - If the user has never been in the room, return an error. - """ - @spec get_current_state(pid(), Account.t()) :: {:ok, [Event.t()]} | :error - def get_current_state(pid, account) do - GenServer.call(pid, {:get_current_state, account}) - end - - @spec get_state_event(pid(), Account.t(), String.t(), String.t()) :: - {:ok, map()} | {:error, :unauthorized} | {:error, :not_found} - def get_state_event(pid, account, event_type, state_key) do - GenServer.call(pid, {:get_state_event, account, event_type, state_key}) - end - ### Implementation @impl true def init(opts) do - {:ok, %{room: Keyword.fetch!(opts, :room)}} + room = Keyword.fetch!(opts, :room) + serialized_state_set = Keyword.fetch!(opts, :serialized_state_set) + state_event_ids = Enum.map(serialized_state_set, fn [_, _, event_id] -> event_id end) + + state_set = + Event + |> where([e], e.id in ^state_event_ids) + |> Repo.all() + |> Enum.into(%{}, fn %Event{type: type, state_key: state_key} = event -> + {{type, state_key}, event} + end) + + {:ok, %{room: room, state_set: state_set}} end @impl true def handle_call( - {:create_room, account, %CreateRoom{room_alias_name: room_alias_name} = request}, + {:create_room, account, request}, _from, %{room: %Room{id: room_id} = room} = state ) do - create_alias_result = - if room_alias_name do - Alias.create(room_alias_name, room_id) - else - {:ok, nil} - end + case Repo.transaction(create_room_insert_events(room, account, request)) do + {:ok, {state_set, room}} -> + {:reply, {:ok, room_id}, %{state | state_set: state_set, room: room}} - case create_alias_result do - {:ok, alias_} -> - events = create_room_events(room, account, request, alias_) + {:error, reason} -> + {:reply, {:error, reason}, state} - case Repo.transaction(process_events(room, events)) do - {:ok, room} -> - {:reply, {:ok, room_id}, %{state | room: room}} - - {:error, reason} -> - {:reply, {:error, reason}, state} - - _ -> - {:reply, {:error, :unknown}, state} - end - - {:error, _} -> - {:reply, {:error, :alias}, state} + _ -> + {:reply, {:error, :unknown}, state} end end - def handle_call({:server_in_room?, domain}, _from, %{room: %Room{state_set: state_set}} = state) do + def handle_call({:server_in_room?, domain}, _from, %{state_set: state_set} = state) do result = Enum.any?(state_set, fn {{"m.room.member", user_id}, %Event{content: %{"membership" => "join"}}} -> @@ -301,15 +272,11 @@ defmodule Architex.RoomServer do {:reply, {state_events, auth_chain}, state} end - def handle_call( - {:invite, account, user_id, avatar_url, displayname}, - _from, - %{room: room} = state - ) do - invite_event = Event.Invite.new(room, account, user_id, avatar_url, displayname) + def handle_call({:invite, account, user_id}, _from, %{room: room, state_set: state_set} = state) do + invite_event = Event.Invite.new(room, account, user_id) - case Repo.transaction(process_event(room, invite_event)) do - {:ok, {room, _}} -> {:reply, :ok, %{state | room: room}} + case Repo.transaction(insert_single_event(room, state_set, invite_event)) do + {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} {:error, reason} -> {:reply, {:error, reason}, state} end end @@ -317,64 +284,59 @@ defmodule Architex.RoomServer do def handle_call( {:join, account}, _from, - %{room: %Room{id: room_id} = room} = state + %{room: %Room{id: room_id} = room, state_set: state_set} = state ) do join_event = Event.Join.new(room, account) - case Repo.transaction(process_event(room, join_event)) do - {:ok, {room, _}} -> - {:reply, {:ok, room_id}, %{state | room: room}} + case Repo.transaction(insert_single_event(room, state_set, join_event)) do + {:ok, {state_set, room, _}} -> + {:reply, {:ok, room_id}, %{state | state_set: state_set, room: room}} {:error, reason} -> {:reply, {:error, reason}, state} end end - def handle_call({:leave, account}, _from, %{room: room} = state) do + def handle_call({:leave, account}, _from, %{room: room, state_set: state_set} = state) do leave_event = Event.Leave.new(room, account) - case Repo.transaction(process_event(room, leave_event)) do - {:ok, {room, _}} -> {:reply, :ok, %{state | room: room}} + case Repo.transaction(insert_single_event(room, state_set, leave_event)) do + {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} {:error, reason} -> {:reply, {:error, reason}, state} end end def handle_call( - {:kick, account, %Kick{user_id: user_id, reason: reason}, avatar_url, displayname}, + {:kick, account, %Kick{user_id: user_id, reason: reason}}, _from, - %{room: room} = state + %{room: room, state_set: state_set} = state ) do - kick_event = - Event.Kick.new(room, account, to_string(user_id), avatar_url, displayname, reason) + kick_event = Event.Kick.new(room, account, user_id, reason) - case Repo.transaction(process_event(room, kick_event)) do - {:ok, {room, _}} -> {:reply, :ok, %{state | room: room}} + case Repo.transaction(insert_single_event(room, state_set, kick_event)) do + {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} {:error, reason} -> {:reply, {:error, reason}, state} end end def handle_call( - {:ban, account, %Ban{user_id: user_id, reason: reason}, avatar_url, displayname}, + {:ban, account, %Kick{user_id: user_id, reason: reason}}, _from, - %{room: room} = state + %{room: room, state_set: state_set} = state ) do - ban_event = Event.Ban.new(room, account, to_string(user_id), avatar_url, displayname, reason) + ban_event = Event.Ban.new(room, account, user_id, reason) - case Repo.transaction(process_event(room, ban_event)) do - {:ok, {room, _}} -> {:reply, :ok, %{state | room: room}} + case Repo.transaction(insert_single_event(room, state_set, ban_event)) do + {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} {:error, reason} -> {:reply, {:error, reason}, state} end end - def handle_call( - {:unban, account, user_id, avatar_url, displayname}, - _from, - %{room: room} = state - ) do - unban_event = Event.Unban.new(room, account, user_id, avatar_url, displayname) + def handle_call({:unban, account, user_id}, _from, %{room: room, state_set: state_set} = state) do + unban_event = Event.Unban.new(room, account, user_id) - case Repo.transaction(process_event(room, unban_event)) do - {:ok, {room, _}} -> {:reply, :ok, %{state | room: room}} + case Repo.transaction(insert_single_event(room, state_set, unban_event)) do + {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} {:error, reason} -> {:reply, {:error, reason}, state} end end @@ -382,7 +344,7 @@ defmodule Architex.RoomServer do def handle_call( {:set_visibility, account, visibility}, _from, - %{room: %Room{state_set: state_set} = room} = state + %{room: room, state_set: state_set} = state ) do case state_set do %{{"m.room.create", ""} => %Event{content: %{"creator" => creator}}} -> @@ -402,13 +364,13 @@ defmodule Architex.RoomServer do def handle_call( {:send_message_event, account, device, event_type, content, txn_id}, _from, - %{room: room} = state + %{room: room, state_set: state_set} = state ) do message_event = Event.custom_event(room, account, event_type, content) - case Repo.transaction(process_event_with_txn(room, device, message_event, txn_id)) do - {:ok, {room, event_id}} -> - {:reply, {:ok, event_id}, %{state | room: room}} + case Repo.transaction(insert_event_with_txn(state_set, room, device, message_event, txn_id)) do + {:ok, {state_set, room, event_id}} -> + {:reply, {:ok, event_id}, %{state | state_set: state_set, room: room}} {:error, reason} -> {:reply, {:error, reason}, state} @@ -418,76 +380,23 @@ defmodule Architex.RoomServer do def handle_call( {:send_state_event, account, event_type, content, state_key}, _from, - %{room: room} = state + %{room: room, state_set: state_set} = state ) do state_event = Event.custom_state_event(room, account, event_type, content, state_key) - case Repo.transaction(process_event(room, state_event)) do - {:ok, {room, %Event{id: event_id}}} -> - {:reply, {:ok, event_id}, %{state | room: room}} + case Repo.transaction(insert_single_event(room, state_set, state_event)) do + {:ok, {state_set, room, %Event{id: event_id}}} -> + {:reply, {:ok, event_id}, %{state | state_set: state_set, room: room}} {:error, reason} -> {:reply, {:error, reason}, state} end end - def handle_call( - {:get_current_state, account}, - _from, - %{room: %Room{state_set: state_set}} = state - ) do - mxid = Account.get_mxid(account) - - case state_set[{"m.room.member", mxid}] do - %Event{content: %{"membership" => "join"}} -> - # Get the current state of the room. - {:reply, {:ok, Map.values(state_set)}, state} - - %Event{content: %{"membership" => "leave"}} = event -> - # Get the state of the room, after leaving. - # TODO: This does not work properly, as a user's membership can change to "leave" - # even after they left/are banned. - # I think it is best to seperately keep track when a user left, maybe in the - # Membership table. - state_set = StateResolution.resolve(event) - {:reply, {:ok, Map.values(state_set)}, state} - - _ -> - {:reply, :error, state} - end - end - - def handle_call( - {:get_state_event, account, event_type, state_key}, - _from, - %{room: %Room{state_set: state_set}} = state - ) do - mxid = Account.get_mxid(account) - - case state_set[{"m.room.member", mxid}] do - %Event{content: %{"membership" => "join"}} -> - case state_set[{event_type, state_key}] do - %Event{content: content} -> {:reply, {:ok, content}, state} - nil -> {:reply, {:error, :not_found}, state} - end - - %Event{content: %{"membership" => "leave"}} = event -> - # TODO: See get_current_state. - state_set = StateResolution.resolve(event) - - case state_set[{event_type, state_key}] do - %Event{content: content} -> {:reply, {:ok, content}, state} - nil -> {:reply, {:error, :not_found}, state} - end - - _ -> - {:reply, {:error, :unauthorized}, state} - end - end - - @spec process_event_with_txn(Room.t(), Device.t(), %Event{}, String.t()) :: - (() -> {Room.t(), String.t()} | {:error, atom()}) - defp process_event_with_txn( + @spec insert_event_with_txn(t(), Room.t(), Device.t(), %Event{}, String.t()) :: + (() -> {t(), Room.t(), String.t()} | {:error, atom()}) + defp insert_event_with_txn( + state_set, room, %Device{nid: device_nid} = device, message_event, @@ -500,107 +409,98 @@ defmodule Architex.RoomServer do where: dt.txn_id == ^txn_id and dt.device_nid == ^device_nid ) do %DeviceTransaction{event_id: event_id} -> - {room, event_id} + {state_set, room, event_id} nil -> - with {room, %Event{id: event_id}} <- process_event(room, message_event).() do + with {state_set, room, %Event{id: event_id}} <- + insert_single_event(room, state_set, message_event).() do # Mark this transaction as done. Ecto.build_assoc(device, :device_transactions, txn_id: txn_id, event_id: event_id) |> Repo.insert!() - {room, event_id} + {state_set, room, event_id} end end end end - @spec process_event(Room.t(), %Event{}) :: (() -> {Room.t(), Event.t()} | {:error, atom()}) - defp process_event(room, event) do + @spec insert_single_event(Room.t(), t(), %Event{}) :: + (() -> {t(), Room.t(), Event.t()} | {:error, atom()}) + defp insert_single_event(room, state_set, event) do fn -> - case finalize_and_process_event(event, room) do - {:ok, room, event} -> {room, event} - {:error, reason} -> Repo.rollback(reason) + case finalize_and_insert_event(event, state_set, room) do + {:ok, state_set, room, event} -> + _ = update_room_state_set(room, state_set) + {state_set, room, event} + + {:error, reason} -> + Repo.rollback(reason) end end end - @spec process_events(Room.t(), [%Event{}]) :: (() -> Room.t() | {:error, atom()}) - defp process_events(room, events) do + # Get a function that inserts all events for room creation. + @spec create_room_insert_events(Room.t(), Account.t(), CreateRoom.t()) :: + (() -> {:ok, t(), Room.t()} | {:error, atom()}) + defp create_room_insert_events(room, account, %CreateRoom{ + room_version: room_version, + preset: preset, + name: name, + topic: topic, + invite: invite, + power_level_content_override: power_level_content_override + }) do + + events = + ([ + Event.CreateRoom.new(room, account, room_version), + Event.Join.new(room, account), + Event.PowerLevels.create_room_new(room, account, power_level_content_override) + ] ++ + room_creation_preset(account, preset, room) ++ + [ + if(name, do: Event.Name.new(room, account, name)), + if(topic, do: Event.Topic.new(room, account, topic)) + ] ++ room_creation_invite_events(account, invite, room)) + |> Enum.reject(&Kernel.is_nil/1) + fn -> - Enum.reduce_while(events, room, fn event, room -> - case finalize_and_process_event(event, room) do - {:ok, room, _} -> {:cont, room} - {:error, reason} -> {:halt, {:error, reason}} - end - end) - |> then(fn - {:error, reason} -> Repo.rollback(reason) - room -> room - end) + result = + Enum.reduce_while(events, {%{}, room}, fn event, {state_set, room} -> + case finalize_and_insert_event(event, state_set, room) do + {:ok, state_set, room, _} -> {:cont, {state_set, room}} + {:error, reason} -> {:halt, {:error, reason}} + end + end) + + case result do + {:error, reason} -> + Repo.rollback(reason) + + {state_set, room} -> + _ = update_room_state_set(room, state_set) + {state_set, room} + end end end - @spec create_room_events(Room.t(), Account.t(), CreateRoom.t(), Alias.t() | nil) :: [%Event{}] - defp create_room_events( - room, - account, - %CreateRoom{ - room_version: room_version, - preset: preset, - name: name, - topic: topic, - invite: invite, - power_level_content_override: power_level_content_override, - is_direct: is_direct, - creation_content: creation_content, - initial_state: initial_state - }, - alias_ - ) do - invite_events = room_creation_invite_events(account, invite, room, is_direct) - - # Spec doesn't specify where to insert canonical_alias event, do it after topic event. - name_and_topic_events = - Enum.reject( - [ - if(name, do: Event.Name.new(room, account, name)), - if(topic, do: Event.Topic.new(room, account, topic)), - if(alias_, do: Event.CanonicalAlias.new(room, account, alias_.alias)) - ], - &Kernel.is_nil/1 - ) - - initial_state_pairs = - if initial_state, do: Enum.map(initial_state, &{&1.type, &1.state_key}), else: [] - - initial_state_events = - room_creation_initial_state_events(account, initial_state, room) - |> Enum.reject(fn %Event{type: type, state_key: state_key} -> - ({type, state_key} == {"m.room.name", ""} and name) || - ({type, state_key} == {"m.room.topic", ""} and topic) + # Update the given room in the database with the given state set. + @spec update_room_state_set(Room.t(), t()) :: Room.t() + defp update_room_state_set(room, state_set) do + # TODO: We might as well hold state in the Room struct, + # instead of the state_set state. + # Create custom type for this. + serialized_state_set = + Enum.map(state_set, fn {{type, state_key}, %Event{id: event_id}} -> + [type, state_key, event_id] end) - preset_events = - room_creation_preset(account, preset, room) - |> Enum.reject(&({&1.type, &1.state_key} in initial_state_pairs)) - - basic_events = [ - Event.CreateRoom.new(room, account, room_version, creation_content), - Event.Join.new(room, account), - Event.PowerLevels.create_room_new( - room, - account, - power_level_content_override, - invite, - preset - ) - ] - - basic_events ++ - preset_events ++ initial_state_events ++ name_and_topic_events ++ invite_events + Repo.update!(change(room, state: serialized_state_set)) end # Get the events for room creation as dictated by the given preset. + # TODO: trusted_private_chat: + # All invitees are given the same power level as the room creator. @spec room_creation_preset(Account.t(), String.t() | nil, Room.t()) :: [%Event{}] defp room_creation_preset(account, nil, %Room{visibility: visibility} = room) do preset = @@ -628,24 +528,11 @@ defmodule Architex.RoomServer do end # Get the events for room creation for inviting other users. - @spec room_creation_invite_events(Account.t(), [UserId.t()] | nil, Room.t(), boolean() | nil) :: - [%Event{}] - defp room_creation_invite_events(_, nil, _, _), do: [] + @spec room_creation_invite_events(Account.t(), [String.t()] | nil, Room.t()) :: [%Event{}] + defp room_creation_invite_events(_, nil, _), do: [] - defp room_creation_invite_events(account, invite_user_ids, room, is_direct) do - Enum.map(invite_user_ids, fn user_id -> - {avatar_url, displayname} = UserId.try_get_user_information(user_id) - - Event.Invite.new(room, account, to_string(user_id), avatar_url, displayname, is_direct) - end) - end - - defp room_creation_initial_state_events(_, nil, _), do: [] - - defp room_creation_initial_state_events(account, initial_state, room) do - Enum.map(initial_state, fn %{type: type, content: content, state_key: state_key} -> - Event.custom_state_event(room, account, type, content, state_key) - end) + defp room_creation_invite_events(account, invite_user_ids, room) do + Enum.map(invite_user_ids, &Event.Invite.new(room, account, &1)) end # Finalize the event struct and insert it into the room's state using state resolution. @@ -655,11 +542,12 @@ defmodule Architex.RoomServer do # - Content hash # - Event ID # - Signature - @spec finalize_and_process_event(%Event{}, Room.t()) :: - {:ok, Room.t(), Event.t()} | {:error, atom()} - defp finalize_and_process_event( + @spec finalize_and_insert_event(%Event{}, t(), Room.t()) :: + {:ok, t(), Room.t(), Event.t()} | {:error, atom()} + defp finalize_and_insert_event( event, - %Room{forward_extremities: forward_extremities, state_set: state_set} = room + state_set, + %Room{forward_extremities: forward_extremities} = room ) do event = event @@ -668,7 +556,7 @@ defmodule Architex.RoomServer do |> Map.put(:depth, get_depth(forward_extremities)) case Event.post_process(event) do - {:ok, event} -> authenticate_and_process_event(event, room) + {:ok, event} -> authenticate_and_insert_event(event, state_set, room) _ -> {:error, :event_creation} end end @@ -683,7 +571,7 @@ defmodule Architex.RoomServer do end # Get the auth events for an events. - @spec auth_events_for_event(%Event{}, StateSet.t()) :: [Event.t()] + @spec auth_events_for_event(%Event{}, t()) :: [Event.t()] defp auth_events_for_event(%Event{type: "m.room.create"}, _), do: [] defp auth_events_for_event( @@ -735,9 +623,9 @@ defmodule Architex.RoomServer do # Authenticate and insert a new event using state resolution. # Implements the checks as described in the # [Matrix docs](https://matrix.org/docs/spec/server_server/latest#checks-performed-on-receipt-of-a-pdu). - @spec authenticate_and_process_event(Event.t(), Room.t()) :: - {:ok, Room.t(), Event.t()} | {:error, atom()} - defp authenticate_and_process_event(event, %Room{state_set: current_state_set} = room) do + @spec authenticate_and_insert_event(Event.t(), t(), Room.t()) :: + {:ok, t(), Room.t(), Event.t()} | {:error, atom()} + defp authenticate_and_insert_event(event, current_state_set, room) do # TODO: Correctly handle soft fails. # Check the following things: # 1. TODO: Is a valid event, otherwise it is dropped. @@ -755,9 +643,8 @@ defmodule Architex.RoomServer do event = Repo.insert!(event) state_set = StateResolution.resolve_forward_extremities(event) :ok = update_membership(room, state_set) - room = Repo.update!(change(room, state_set: state_set)) - {:ok, room, event} + {:ok, state_set, room, event} else _ -> {:error, :authorization} end @@ -768,7 +655,7 @@ defmodule Architex.RoomServer do # could access rooms they are not allowed to. Then again, maybe we should perform # the "normal" authorization flow for local users as well, and treat the Membership # table only as informational. - @spec update_membership(Room.t(), StateSet.t()) :: :ok + @spec update_membership(Room.t(), t()) :: :ok defp update_membership(%Room{id: room_id}, state_set) do server_name = Architex.server_name() diff --git a/lib/architex/schema/alias.ex b/lib/architex/schema/alias.ex index 74ba186..f82c921 100644 --- a/lib/architex/schema/alias.ex +++ b/lib/architex/schema/alias.ex @@ -6,18 +6,13 @@ defmodule Architex.Alias do alias Architex.{Repo, Alias, Room} alias Ecto.Changeset - @type t :: %__MODULE__{ - alias: String.t(), - room_id: String.t() - } - @primary_key {:alias, :string, []} schema "aliases" do belongs_to :room, Room, foreign_key: :room_id, references: :id, type: :string end - def create(alias_, room_id) do - change(%Alias{}, alias: alias_, room_id: room_id) + def create(alias, room_id) do + change(%Alias{}, alias: alias, room_id: room_id) |> assoc_constraint(:room) |> unique_constraint(:alias, name: :aliases_pkey) |> Repo.insert() diff --git a/lib/architex/schema/event.ex b/lib/architex/schema/event.ex index 38bf8da..174e97c 100644 --- a/lib/architex/schema/event.ex +++ b/lib/architex/schema/event.ex @@ -314,15 +314,4 @@ defmodule Architex.Event do {:ok, :crypto.hash(:sha256, json)} end end - - @spec default_membership_content(String.t() | nil, String.t() | nil) :: %{ - optional(String.t()) => String.t() - } - def default_membership_content(avatar_url, displayname) do - content = %{} - content = if avatar_url, do: Map.put(content, "avatar_url", avatar_url), else: content - content = if displayname, do: Map.put(content, "displayname", displayname), else: content - - content - end end diff --git a/lib/architex/schema/event/formatters.ex b/lib/architex/schema/event/formatters.ex index 6d96bcc..84b7ea1 100644 --- a/lib/architex/schema/event/formatters.ex +++ b/lib/architex/schema/event/formatters.ex @@ -4,10 +4,33 @@ defmodule Architex.Event.Formatters do """ alias Architex.Event - @doc """ - Event format with keys that all formats have in common. - """ - def base_client_response(%Event{ + @spec messages_response(Event.t()) :: map() + def messages_response(%Event{ + content: content, + type: type, + id: event_id, + sender: sender, + origin_server_ts: origin_server_ts, + unsigned: unsigned, + room_id: room_id, + state_key: state_key + }) do + data = %{ + content: content, + type: type, + event_id: event_id, + sender: to_string(sender), + origin_server_ts: origin_server_ts, + room_id: room_id + } + + data = if unsigned, do: Map.put(data, :unsigned, unsigned), else: data + data = if state_key, do: Map.put(data, :state_key, state_key), else: data + + data + end + + def sync_response(%Event{ content: content, type: type, id: event_id, @@ -23,37 +46,11 @@ defmodule Architex.Event.Formatters do origin_server_ts: origin_server_ts } - if unsigned, do: Map.put(data, :unsigned, unsigned), else: data + data = if unsigned, do: Map.put(data, :unsigned, unsigned), else: data + + data end - @doc """ - The base event format, with room_id and state_key added. - Used in the client /messages response. - """ - @spec messages_response(Event.t()) :: map() - def messages_response(%Event{room_id: room_id, state_key: state_key} = event) do - data = - base_client_response(event) - |> Map.put(:room_id, room_id) - - if state_key, do: Map.put(data, :state_key, state_key), else: data - end - - @doc """ - The event format used in the client /state response. - TODO: prev_content - """ - def state_response(event), do: messages_response(event) - - @doc """ - The base event format, used in the client /sync response. - """ - @spec sync_response(Event.t()) :: map() - def sync_response(event), do: base_client_response(event) - - @doc """ - The PDU format used for federation. - """ @spec as_pdu(Event.t()) :: map() def as_pdu(%Event{ auth_events: auth_events, diff --git a/lib/architex/schema/event/generators.ex b/lib/architex/schema/event/generators.ex index 11429db..24e40aa 100644 --- a/lib/architex/schema/event/generators.ex +++ b/lib/architex/schema/event/generators.ex @@ -2,21 +2,16 @@ defmodule Architex.Event.Join do alias Architex.{Event, Account, Room} @spec new(Room.t(), Account.t()) :: %Event{} - def new( - room, - %Account{localpart: localpart, avatar_url: avatar_url, displayname: displayname} = sender - ) do + def new(room, %Account{localpart: localpart} = sender) do mxid = Architex.get_mxid(localpart) - content = - Event.default_membership_content(avatar_url, displayname) - |> Map.put("membership", "join") - %Event{ Event.new(room, sender) | type: "m.room.member", state_key: mxid, - content: content + content: %{ + "membership" => "join" + } } end end @@ -24,38 +19,24 @@ end defmodule Architex.Event.CreateRoom do alias Architex.{Event, Account, Room} - @clobber_content_keys ["creator", "room_version"] - - @spec new(Room.t(), Account.t(), String.t(), %{optional(String.t()) => any()} | nil) :: %Event{} - def new(room, %Account{localpart: localpart} = creator, room_version, creation_content) do + @spec new(Room.t(), Account.t(), String.t()) :: %Event{} + def new(room, %Account{localpart: localpart} = creator, room_version) do mxid = Architex.get_mxid(localpart) - content = %{ - "creator" => mxid, - "room_version" => room_version || Architex.default_room_version() - } - - content = - if creation_content do - creation_content - |> Map.drop(@clobber_content_keys) - |> Map.merge(content) - else - content - end - %Event{ Event.new(room, creator) | type: "m.room.create", state_key: "", - content: content + content: %{ + "creator" => mxid, + "room_version" => room_version || Architex.default_room_version() + } } end end defmodule Architex.Event.PowerLevels do alias Architex.{Event, Account, Room} - alias Architex.Types.UserId alias ArchitexWeb.Client.Request.CreateRoom alias ArchitexWeb.Client.Request.CreateRoom.PowerLevelContentOverride @@ -69,49 +50,26 @@ defmodule Architex.Event.PowerLevels do @users_default 0 @notifications_room 50 - @spec create_room_new( - Room.t(), - Account.t(), - CreateRoom.PowerLevelContentOverride.t(), - [UserId.t()] | nil, - String.t() | nil - ) :: %Event{} - def create_room_new(room, sender, nil, invite_ids, preset) do - create_room_new(room, sender, %PowerLevelContentOverride{}, invite_ids, preset) + @spec create_room_new(Room.t(), Account.t(), CreateRoom.plco_t()) :: %Event{} + def create_room_new(room, sender, nil) do + create_room_new(room, sender, %PowerLevelContentOverride{}) end - def create_room_new( - room, - %Account{localpart: localpart} = sender, - %PowerLevelContentOverride{ - ban: ban_override, - events: events_override, - events_default: events_default_override, - invite: invite_override, - kick: kick_override, - redact: redact_override, - state_default: state_default_override, - users: users_override, - users_default: users_default_override, - notifications: notifications_override - }, - invite_ids, - preset - ) do + def create_room_new(room, %Account{localpart: localpart} = sender, %PowerLevelContentOverride{ + ban: ban_override, + events: events_override, + events_default: events_default_override, + invite: invite_override, + kick: kick_override, + redact: redact_override, + state_default: state_default_override, + users: users_override, + users_default: users_default_override, + notifications: notifications_override + }) do mxid = Architex.get_mxid(localpart) users = %{mxid => @creator} users = if users_override, do: Map.merge(users, users_override), else: users - creator_pl = users[mxid] - - # Give each invitee the same power level as the creator. - # This overrides the content override, but the spec is not clear on this. - users = - if preset == "trusted_private_chat" and invite_ids do - invite_users_pls = Enum.into(invite_ids, %{}, &{to_string(&1), creator_pl}) - Map.merge(users, invite_users_pls) - else - users - end notifications = case notifications_override do @@ -222,26 +180,15 @@ end defmodule Architex.Event.Invite do alias Architex.{Event, Account, Room} - @spec new( - Room.t(), - Account.t(), - String.t(), - String.t() | nil, - String.t() | nil, - boolean() | nil - ) :: %Event{} - def new(room, sender, user_id, avatar_url, displayname, is_direct \\ nil) do - content = - Event.default_membership_content(avatar_url, displayname) - |> Map.put("membership", "invite") - - content = if is_direct != nil, do: Map.put(content, "is_direct", is_direct), else: content - + @spec new(Room.t(), Account.t(), String.t()) :: %Event{} + def new(room, sender, user_id) do %Event{ Event.new(room, sender) | type: "m.room.member", state_key: user_id, - content: content + content: %{ + "membership" => "invite" + } } end end @@ -250,16 +197,14 @@ defmodule Architex.Event.Leave do alias Architex.{Event, Account, Room} @spec new(Room.t(), Account.t()) :: %Event{} - def new(room, %Account{avatar_url: avatar_url, displayname: displayname} = sender) do - content = - Event.default_membership_content(avatar_url, displayname) - |> Map.put("membership", "leave") - + def new(room, sender) do %Event{ Event.new(room, sender) | type: "m.room.member", state_key: Account.get_mxid(sender), - content: content + content: %{ + "membership" => "leave" + } } end end @@ -267,19 +212,9 @@ end defmodule Architex.Event.Kick do alias Architex.{Event, Account, Room} - @spec new( - Room.t(), - Account.t(), - String.t(), - String.t() | nil, - String.t() | nil, - String.t() | nil - ) :: %Event{} - def new(room, sender, user_id, avatar_url, displayname, reason \\ nil) do - content = - Event.default_membership_content(avatar_url, displayname) - |> Map.put("membership", "leave") - + @spec new(Room.t(), Account.t(), String.t(), String.t() | nil) :: %Event{} + def new(room, sender, user_id, reason \\ nil) do + content = %{"membership" => "leave"} content = if reason, do: Map.put(content, "reason", reason), else: content %Event{ @@ -294,19 +229,9 @@ end defmodule Architex.Event.Ban do alias Architex.{Event, Account, Room} - @spec new( - Room.t(), - Account.t(), - String.t(), - String.t() | nil, - String.t() | nil, - String.t() | nil - ) :: %Event{} - def new(room, sender, user_id, avatar_url, displayname, reason \\ nil) do - content = - Event.default_membership_content(avatar_url, displayname) - |> Map.put("membership", "ban") - + @spec new(Room.t(), Account.t(), String.t(), String.t() | nil) :: %Event{} + def new(room, sender, user_id, reason \\ nil) do + content = %{"membership" => "ban"} content = if reason, do: Map.put(content, "reason", reason), else: content %Event{ @@ -320,36 +245,15 @@ end defmodule Architex.Event.Unban do alias Architex.{Event, Account, Room} - - @spec new(Room.t(), Account.t(), String.t(), String.t() | nil, String.t() | nil) :: %Event{} - def new(room, sender, avatar_url, displayname, user_id) do - content = - Event.default_membership_content(avatar_url, displayname) - |> Map.put("membership", "leave") - + @spec new(Room.t(), Account.t(), String.t()) :: %Event{} + def new(room, sender, user_id) do %Event{ Event.new(room, sender) | type: "m.room.member", state_key: user_id, - content: content - } - end -end - -defmodule Architex.Event.CanonicalAlias do - alias Architex.{Event, Account, Room} - - @spec new(Room.t(), Account.t(), String.t() | nil, [String.t()] | nil) :: %Event{} - def new(room, sender, alias_ \\ nil, alt_aliases \\ nil) do - content = %{} - content = if alias_, do: Map.put(content, "alias", alias_), else: content - content = if alt_aliases, do: Map.put(content, "alt_aliases", alt_aliases), else: content - - %Event{ - Event.new(room, sender) - | type: "m.room.canonical_alias", - state_key: "", - content: content + content: %{ + "membership" => "leave" + } } end end diff --git a/lib/architex/schema/room.ex b/lib/architex/schema/room.ex index e552b43..d07413f 100644 --- a/lib/architex/schema/room.ex +++ b/lib/architex/schema/room.ex @@ -5,19 +5,18 @@ defmodule Architex.Room do import Ecto.Query alias Architex.{Repo, Room, Event, Alias, RoomServer, Account} - alias Architex.Types.StateSet alias ArchitexWeb.Client.Request.{CreateRoom, Messages} @type t :: %__MODULE__{ visibility: :public | :private, - state_set: StateSet.t(), + state: list(list(String.t())), forward_extremities: list(String.t()) } @primary_key {:id, :string, []} schema "rooms" do field :visibility, Ecto.Enum, values: [:public, :private] - field :state_set, StateSet, load_in_query: false + field :state, {:array, {:array, :string}} field :forward_extremities, {:array, :string} has_many :events, Event, foreign_key: :room_id has_many :aliases, Alias, foreign_key: :room_id diff --git a/lib/architex/types/state_set.ex b/lib/architex/types/state_set.ex deleted file mode 100644 index 6eab0c8..0000000 --- a/lib/architex/types/state_set.ex +++ /dev/null @@ -1,45 +0,0 @@ -defmodule Architex.Types.StateSet do - use Ecto.Type - - import Ecto.Query - - alias Architex.{Repo, Event} - - @type t :: %{optional({String.t(), String.t()}) => Event.t()} - - def type(), do: {:array, :string} - - def cast(_), do: :error - - def load(event_ids) when is_list(event_ids) do - events = - Event - |> where([e], e.id in ^event_ids) - |> Repo.all() - |> IO.inspect() - - if length(events) == length(event_ids) do - state_set = - Enum.into(events, %{}, fn %Event{type: type, state_key: state_key} = event -> - {{type, state_key}, event} - end) - - {:ok, state_set} - else - :error - end - end - - def load(_), do: :error - - def dump(state_set) when is_map(state_set) do - dumped = - Enum.map(state_set, fn {_, %Event{id: event_id}} -> - event_id - end) - - {:ok, dumped} - end - - def dump(_), do: :error -end diff --git a/lib/architex/types/user_id.ex b/lib/architex/types/user_id.ex index 93ddc9b..d4754ef 100644 --- a/lib/architex/types/user_id.ex +++ b/lib/architex/types/user_id.ex @@ -1,11 +1,7 @@ defmodule Architex.Types.UserId do use Ecto.Type - import Ecto.Query - - alias Architex.{Repo, Account} alias Architex.Types.UserId - alias ArchitexWeb.Federation.HTTPClient @type t :: %__MODULE__{ localpart: String.t(), @@ -57,37 +53,4 @@ defmodule Architex.Types.UserId do def dump(%UserId{} = user_id), do: {:ok, to_string(user_id)} def dump(_), do: :error - - # TODO: Seems out of place here. - @spec try_get_user_information(UserId.t()) :: {String.t() | nil, String.t() | nil} - def try_get_user_information(%UserId{localpart: localpart, domain: domain} = user_id) do - if domain == Architex.server_name() do - # Get information about a user on this homeserver. - query = - Account - |> where([a], a.localpart == ^localpart) - |> select([a], {a.avatar_url, a.displayname}) - - case Repo.one(query) do - nil -> {nil, nil} - info -> info - end - else - # Get information about a user on another homeserver. - client = HTTPClient.client(domain) - - case HTTPClient.query_profile(client, to_string(user_id)) do - {:ok, response} -> - avatar_url = Map.get(response, "avatar_url") - avatar_url = if is_binary(avatar_url), do: avatar_url - displayname = Map.get(response, "displayname") - displayname = if is_binary(displayname), do: displayname - - {avatar_url, displayname} - - {:error, _, _} -> - {nil, nil} - end - end - end end diff --git a/lib/architex_web/api_schemas/client/request/ban.ex b/lib/architex_web/api_schemas/client/request/ban.ex index 1749e6b..694667f 100644 --- a/lib/architex_web/api_schemas/client/request/ban.ex +++ b/lib/architex_web/api_schemas/client/request/ban.ex @@ -1,16 +1,14 @@ defmodule ArchitexWeb.Client.Request.Ban do use ArchitexWeb.APIRequest - alias Architex.Types.UserId - @type t :: %__MODULE__{ - user_id: UserId.t(), + user_id: String.t(), reason: String.t() | nil } @primary_key false embedded_schema do - field :user_id, UserId + field :user_id, :string field :reason, :string end diff --git a/lib/architex_web/api_schemas/client/request/create_room.ex b/lib/architex_web/api_schemas/client/request/create_room.ex index 19f782f..4a57ec3 100644 --- a/lib/architex_web/api_schemas/client/request/create_room.ex +++ b/lib/architex_web/api_schemas/client/request/create_room.ex @@ -1,43 +1,45 @@ defmodule ArchitexWeb.Client.Request.CreateRoom do use ArchitexWeb.APIRequest - alias Architex.Types.UserId + @type t :: %__MODULE__{ + visibility: String.t() | nil, + room_alias_name: String.t() | nil, + name: String.t() | nil, + topic: String.t() | nil, + invite: list(String.t()) | nil, + room_version: String.t() | nil, + preset: String.t() | nil, + power_level_content_override: plco_t() | nil + } - defmodule PowerLevelContentOverride do - use Ecto.Schema + @type plco_t :: %__MODULE__.PowerLevelContentOverride{ + ban: integer() | nil, + events: %{optional(String.t()) => integer()} | nil, + events_default: integer() | nil, + invite: integer() | nil, + kick: integer() | nil, + redact: integer() | nil, + state_default: integer() | nil, + users: %{optional(String.t()) => integer()} | nil, + users_default: integer() | nil, + notifications: plco_n_t() | nil + } - defmodule Notifications do - use Ecto.Schema + @type plco_n_t :: %__MODULE__.PowerLevelContentOverride.Notifications{ + room: integer() | nil + } - @type t :: %__MODULE__{ - room: integer() | nil - } + @primary_key false + embedded_schema do + field :visibility, :string + field :room_alias_name, :string + field :name, :string + field :topic, :string + field :invite, {:array, :string} + field :room_version, :string + field :preset, :string - @primary_key false - embedded_schema do - field :room, :integer - end - - def changeset(data, params) do - cast(data, params, [:room]) - end - end - - @type t :: %__MODULE__{ - ban: integer() | nil, - events: %{optional(String.t()) => integer()} | nil, - events_default: integer() | nil, - invite: integer() | nil, - kick: integer() | nil, - redact: integer() | nil, - state_default: integer() | nil, - users: %{optional(String.t()) => integer()} | nil, - users_default: integer() | nil, - notifications: Notifications.t() | nil - } - - @primary_key false - embedded_schema do + embeds_one :power_level_content_override, PowerLevelContentOverride, primary_key: false do field :ban, :integer field :events, {:map, :integer} field :events_default, :integer @@ -48,81 +50,13 @@ defmodule ArchitexWeb.Client.Request.CreateRoom do field :users, {:map, :integer} field :users_default, :integer - embeds_one :notifications, Notifications + embeds_one :notifications, Notifications, primary_key: false do + field :room, :integer + end end - def changeset(data, params) do - data - |> cast(params, [ - :ban, - :events, - :events_default, - :invite, - :kick, - :redact, - :state_default, - :users, - :users_default - ]) - |> cast_embed(:notifications, - with: &Notifications.changeset/2, - required: false - ) - end - end - - defmodule StateEvent do - use Ecto.Schema - - @type t :: %__MODULE__{ - type: String.t(), - state_key: String.t(), - content: %{optional(String.t()) => any()} - } - - @primary_key false - embedded_schema do - field :type, :string - field :state_key, :string, default: "" - field :content, :map - end - - def changeset(data, params) do - data - |> cast(params, [:type, :state_key, :content]) - |> validate_required([:type, :content]) - end - end - - @type t :: %__MODULE__{ - visibility: String.t() | nil, - room_alias_name: String.t() | nil, - name: String.t() | nil, - topic: String.t() | nil, - invite: [UserId.t()] | nil, - room_version: String.t() | nil, - preset: String.t() | nil, - is_direct: boolean() | nil, - creation_content: %{optional(String.t()) => any()} | nil, - power_level_content_override: PowerLevelContentOverride.t() | nil, - initial_state: [StateEvent.t()] | nil - } - - @primary_key false - embedded_schema do - # TODO: unimplemented: invite_3pid and room_alias_name - field :visibility, :string - field :room_alias_name, :string - field :name, :string - field :topic, :string - field :invite, {:array, UserId} - field :room_version, :string - field :preset, :string - field :is_direct, :boolean - field :creation_content, :map - - embeds_many :initial_state, StateEvent - embeds_one :power_level_content_override, PowerLevelContentOverride + # TODO: unimplemented: + # creation_content, initial_state, invite_3pid, initial_state, is_direct end def changeset(data, params) do @@ -134,15 +68,35 @@ defmodule ArchitexWeb.Client.Request.CreateRoom do :topic, :invite, :room_version, - :preset, - :is_direct, - :creation_content + :preset ]) |> cast_embed(:power_level_content_override, - with: &PowerLevelContentOverride.changeset/2, + with: &power_level_content_override_changeset/2, required: false ) - |> cast_embed(:initial_state, with: &StateEvent.changeset/2, required: false) |> validate_inclusion(:preset, ["private_chat", "public_chat", "trusted_private_chat"]) end + + def power_level_content_override_changeset(data, params) do + data + |> cast(params, [ + :ban, + :events, + :events_default, + :invite, + :kick, + :redact, + :state_default, + :users, + :users_default + ]) + |> cast_embed(:notifications, + with: &power_level_content_override_notifications_changeset/2, + required: false + ) + end + + def power_level_content_override_notifications_changeset(data, params) do + cast(data, params, [:room]) + end end diff --git a/lib/architex_web/api_schemas/client/request/kick.ex b/lib/architex_web/api_schemas/client/request/kick.ex index 48b9630..0d2bc81 100644 --- a/lib/architex_web/api_schemas/client/request/kick.ex +++ b/lib/architex_web/api_schemas/client/request/kick.ex @@ -1,16 +1,14 @@ defmodule ArchitexWeb.Client.Request.Kick do use ArchitexWeb.APIRequest - alias Architex.Types.UserId - @type t :: %__MODULE__{ - user_id: UserId.t(), + user_id: String.t(), reason: String.t() | nil } @primary_key false embedded_schema do - field :user_id, UserId + field :user_id, :string field :reason, :string end diff --git a/lib/architex_web/client/controllers/room_controller.ex b/lib/architex_web/client/controllers/room_controller.ex index 95aa10f..c694482 100644 --- a/lib/architex_web/client/controllers/room_controller.ex +++ b/lib/architex_web/client/controllers/room_controller.ex @@ -25,9 +25,6 @@ defmodule ArchitexWeb.Client.RoomController do {:error, :authorization} -> put_error(conn, :invalid_room_state) - {:error, :alias} -> - put_error(conn, :room_in_use, "The requested alias is already in use.") - {:error, :unknown} -> put_error(conn, :unknown) end @@ -63,11 +60,9 @@ defmodule ArchitexWeb.Client.RoomController do "room_id" => room_id, "user_id" => user_id }) do - with {:ok, user_id_struct} <- UserId.cast(user_id), + with {:ok, _} <- UserId.cast(user_id), {:ok, pid} <- RoomServer.get_room_server(room_id) do - {avatar_url, displayname} = UserId.try_get_user_information(user_id_struct) - - case RoomServer.invite(pid, account, user_id, avatar_url, displayname) do + case RoomServer.invite(pid, account, user_id) do :ok -> conn |> send_resp(200, []) @@ -137,11 +132,9 @@ defmodule ArchitexWeb.Client.RoomController do Action for POST /_matrix/client/r0/rooms/{roomId}/kick. """ def kick(%Conn{assigns: %{account: account}} = conn, %{"room_id" => room_id} = params) do - with {:ok, %Kick{user_id: user_id} = request} <- Kick.parse(params), + with {:ok, request} <- Kick.parse(params), {:ok, pid} <- RoomServer.get_room_server(room_id) do - {avatar_url, displayname} = UserId.try_get_user_information(user_id) - - case RoomServer.kick(pid, account, request, avatar_url, displayname) do + case RoomServer.kick(pid, account, request) do :ok -> conn |> send_resp(200, []) @@ -162,11 +155,9 @@ defmodule ArchitexWeb.Client.RoomController do Action for POST /_matrix/client/r0/rooms/{roomId}/ban. """ def ban(%Conn{assigns: %{account: account}} = conn, %{"room_id" => room_id} = params) do - with {:ok, %Ban{user_id: user_id} = request} <- Ban.parse(params), + with {:ok, request} <- Ban.parse(params), {:ok, pid} <- RoomServer.get_room_server(room_id) do - {avatar_url, displayname} = UserId.try_get_user_information(user_id) - - case RoomServer.ban(pid, account, request, avatar_url, displayname) do + case RoomServer.ban(pid, account, request) do :ok -> conn |> send_resp(200, []) @@ -190,22 +181,20 @@ defmodule ArchitexWeb.Client.RoomController do "room_id" => room_id, "user_id" => user_id }) do - with {:ok, user_id_struct} <- UserId.cast(user_id), - {:ok, pid} <- RoomServer.get_room_server(room_id) do - {avatar_url, displayname} = UserId.try_get_user_information(user_id_struct) + case RoomServer.get_room_server(room_id) do + {:ok, pid} -> + case RoomServer.unban(pid, account, user_id) do + :ok -> + conn + |> send_resp(200, []) + |> halt() - case RoomServer.unban(pid, account, user_id, avatar_url, displayname) do - :ok -> - conn - |> send_resp(200, []) - |> halt() + {:error, _} -> + put_error(conn, :unknown) + end - {:error, _} -> - put_error(conn, :unknown) - end - else - :error -> put_error(conn, :invalid_param, "Given user ID is invalid.") - {:error, :not_found} -> put_error(conn, :not_found, "The given room was not found.") + {:error, :not_found} -> + put_error(conn, :not_found, "The given room was not found.") end end @@ -311,75 +300,4 @@ defmodule ArchitexWeb.Client.RoomController do {:error, _} -> put_error(conn, :bad_json) end end - - @doc """ - Get the state events for the current state of a room. - - Action for GET /_matrix/client/r0/rooms/{roomId}/state. - """ - def get_state(%Conn{assigns: %{account: account}} = conn, %{"room_id" => room_id}) do - case RoomServer.get_room_server(room_id) do - {:ok, pid} -> - case RoomServer.get_current_state(pid, account) do - {:ok, events} -> - events = Enum.map(events, &Event.Formatters.state_response/1) - - conn - |> put_status(200) - |> json(events) - - :error -> - put_error( - conn, - :forbidden, - "You aren't a member of the room and weren't previously a member of the room." - ) - end - - {:error, :not_found} -> - put_error(conn, :not_found, "The given room was not found.") - end - end - - @doc """ - Looks up the contents of a state event in a room. - - Action for GET /_matrix/client/r0/rooms/{roomId}/state/{eventType}/{stateKey}. - """ - def get_state_event(conn, %{"state_key" => [state_key | _]} = params) do - do_get_state_event(conn, params, state_key) - end - - def get_state_event(conn, params) do - do_get_state_event(conn, params, "") - end - - defp do_get_state_event( - %Conn{assigns: %{account: account}} = conn, - %{"room_id" => room_id, "event_type" => event_type}, - state_key - ) do - case RoomServer.get_room_server(room_id) do - {:ok, pid} -> - case RoomServer.get_state_event(pid, account, event_type, state_key) do - {:ok, content} -> - conn - |> put_status(200) - |> json(content) - - {:error, :unauthorized} -> - put_error( - conn, - :forbidden, - "You aren't a member of the room and weren't previously a member of the room." - ) - - {:error, :not_found} -> - put_error(conn, :not_found, "The room has no state with the given type or key.") - end - - {:error, :not_found} -> - put_error(conn, :not_found, "The given room was not found.") - end - end end diff --git a/lib/architex_web/error.ex b/lib/architex_web/error.ex index 7a4bdb3..b8abc5e 100644 --- a/lib/architex_web/error.ex +++ b/lib/architex_web/error.ex @@ -14,7 +14,6 @@ defmodule ArchitexWeb.Error do unauthorized: {400, "M_UNAUTHORIZED", "The request was unauthorized."}, invalid_param: {400, "M_INVALID_PARAM", "A request parameter was invalid."}, missing_param: {400, "M_MISSING_PARAM", "A request parameter is missing."}, - room_in_use: {400, "M_ROOM_IN_USE", "The room is already in use."}, unknown_token: {401, "M_UNKNOWN_TOKEN", "Invalid access token."}, missing_token: {401, "M_MISSING_TOKEN", "Access token required."}, not_found: {404, "M_NOT_FOUND", "The requested resource was not found."}, diff --git a/lib/architex_web/federation/http_client.ex b/lib/architex_web/federation/http_client.ex index ce35b16..b77c7a2 100644 --- a/lib/architex_web/federation/http_client.ex +++ b/lib/architex_web/federation/http_client.ex @@ -33,8 +33,6 @@ defmodule ArchitexWeb.Federation.HTTPClient do """ @spec client(String.t()) :: Tesla.Client.t() def client(server_name) do - # TODO: When implementing resolving homeservers, probably create - # a homeserver struct instead of using domain names directly. Tesla.client( [ {Tesla.Middleware.Opts, [server_name: server_name]}, diff --git a/lib/architex_web/router.ex b/lib/architex_web/router.ex index 44f4cd8..5827b21 100644 --- a/lib/architex_web/router.ex +++ b/lib/architex_web/router.ex @@ -61,16 +61,13 @@ defmodule ArchitexWeb.Router do scope "/r0" do get "/account/whoami", AccountController, :whoami + post "/logout", AccountController, :logout + post "/logout/all", AccountController, :logout_all post "/createRoom", RoomController, :create get "/joined_rooms", RoomController, :joined_rooms get "/capabilities", InfoController, :capabilities get "/sync", SyncController, :sync - scope "/logout" do - post "/", AccountController, :logout - post "/all", AccountController, :logout_all - end - scope "/profile/:user_id" do put "/avatar_url", ProfileController, :set_avatar_url put "/displayname", ProfileController, :set_displayname @@ -90,15 +87,7 @@ defmodule ArchitexWeb.Router do post "/unban", RoomController, :unban put "/send/:event_type/:txn_id", RoomController, :send_message_event get "/messages", RoomController, :messages - - scope "/state" do - get "/", RoomController, :get_state - - scope "/:event_type/*state_key" do - get "/", RoomController, :get_state_event - put "/", RoomController, :send_state_event - end - end + put "/state/:event_type/*state_key", RoomController, :send_state_event end end end diff --git a/priv/repo/migrations/20210830160818_create_initial_tables.exs b/priv/repo/migrations/20210830160818_create_initial_tables.exs index d9e2e84..c8fae73 100644 --- a/priv/repo/migrations/20210830160818_create_initial_tables.exs +++ b/priv/repo/migrations/20210830160818_create_initial_tables.exs @@ -14,7 +14,7 @@ defmodule Architex.Repo.Migrations.CreateInitialTables do create table(:rooms, primary_key: false) do add :id, :string, primary_key: true - add :state_set, {:array, :string}, default: [], null: false + add :state, {:array, {:array, :string}}, default: [], null: false add :forward_extremities, {:array, :string}, default: [], null: false add :visibility, :string, null: false, default: "public" end