From bcc6cbb24bbddc9a92a0d2527f042e46098fd0ab Mon Sep 17 00:00:00 2001 From: Pim Kunis Date: Wed, 25 Aug 2021 01:27:03 +0200 Subject: [PATCH] Implement client invite endpoint Refactor room server to automatically determine auth events Add documentation in various places --- lib/matrix_server/room_server.ex | 243 +++++++++++++----- lib/matrix_server/schema/event.ex | 71 +++-- lib/matrix_server/schema/joined_room.ex | 5 + .../state_resolution/authorization.ex | 12 +- .../client/controllers/account_controller.ex | 21 ++ .../client/controllers/aliases_controller.ex | 5 + .../client/controllers/info_controller.ex | 5 + .../client/controllers/login_controller.ex | 11 + .../client/controllers/register_controller.ex | 5 + .../client/controllers/room_controller.ex | 50 +++- .../controllers/event_controller.ex | 15 ++ .../federation/controllers/key_controller.ex | 5 + .../controllers/query_controller.ex | 6 + lib/matrix_server_web/router.ex | 4 + priv/repo/seeds.exs | 96 ------- 15 files changed, 345 insertions(+), 209 deletions(-) diff --git a/lib/matrix_server/room_server.ex b/lib/matrix_server/room_server.ex index 9779847..f04c789 100644 --- a/lib/matrix_server/room_server.ex +++ b/lib/matrix_server/room_server.ex @@ -6,12 +6,15 @@ defmodule MatrixServer.RoomServer do The RoomServers are supervised by a DynamicSupervisor RoomServer.Supervisor. """ + @typep t :: map() + use GenServer import Ecto.Query import Ecto.Changeset - alias MatrixServer.{Repo, Room, Event, StateResolution, JoinedRoom, Account} + alias MatrixServer.{Repo, Room, Event, StateResolution, Account, JoinedRoom} + alias MatrixServer.Types.UserId alias MatrixServer.StateResolution.Authorization alias MatrixServerWeb.Client.Request.CreateRoom @@ -35,7 +38,7 @@ defmodule MatrixServer.RoomServer do def get_room_server(room_id) do # TODO: Might be wise to use a transaction here to prevent race conditions. case Repo.one(from r in Room, where: r.id == ^room_id) do - %Room{state: serialized_state_set} -> + %Room{state: serialized_state_set} = room -> case Registry.lookup(@registry, room_id) do [{pid, _}] -> {:ok, pid} @@ -43,7 +46,7 @@ defmodule MatrixServer.RoomServer do [] -> opts = [ name: {:via, Registry, {@registry, room_id}}, - room_id: room_id, + room: room, serialized_state_set: serialized_state_set ] @@ -62,11 +65,7 @@ defmodule MatrixServer.RoomServer do Events are inserted into the new room depending on the input `input` and according to the [Matrix documentation](https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-createroom). """ - @spec create_room( - pid(), - MatrixServer.Account.t(), - MatrixServerWeb.Client.Request.CreateRoom.t() - ) :: {:ok, String.t()} | {:error, atom()} + @spec create_room(pid(), Account.t(), CreateRoom.t()) :: {:ok, String.t()} | {:error, atom()} def create_room(pid, account, input) do GenServer.call(pid, {:create_room, account, input}) end @@ -100,11 +99,19 @@ defmodule MatrixServer.RoomServer do GenServer.call(pid, {:get_state_ids_at_event, event}) end + @doc """ + Invite the a user to this room. + """ + @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 + ### Implementation @impl true def init(opts) do - room_id = Keyword.fetch!(opts, :room_id) + 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) @@ -116,14 +123,16 @@ defmodule MatrixServer.RoomServer do {{type, state_key}, event} end) - {:ok, %{room_id: room_id, state_set: state_set}} + {:ok, %{room: room, state_set: state_set}} end @impl true - def handle_call({:create_room, account, input}, _from, %{room_id: room_id} = state) do + def handle_call( + {:create_room, account, input}, + _from, + %{room: %Room{id: room_id} = room} = state + ) do # TODO: power_level_content_override, initial_state, invite, invite_3pid - room = Repo.one!(from r in Room, where: r.id == ^room_id) - case Repo.transaction(create_room_insert_events(room, account, input)) do {:ok, state_set} -> {:reply, {:ok, room_id}, %{state | state_set: state_set}} {:error, reason} -> {:reply, {:error, reason}, state} @@ -184,76 +193,95 @@ defmodule MatrixServer.RoomServer do {:reply, {state_events, auth_chain}, state} end + def handle_call({:invite, account, user_id}, _from, %{room: room, state_set: state_set} = state) do + case Repo.transaction(invite_insert_event(room, state_set, account, user_id)) do + {:ok, state_set} -> {:reply, :ok, %{state | state_set: state_set}} + {:error, reason} -> {:reply, {:error, reason}, state} + end + end + + @spec invite_insert_event(Room.t(), t(), Account.t(), String.t()) :: + (() -> {:ok, t()} | {:error, atom()}) + defp invite_insert_event(room, state_set, account, user_id) do + invite_event = Event.invite(room, account, user_id) + + fn -> + case finalize_and_insert_event(invite_event, state_set, room) do + {:ok, state_set, room} -> + _ = update_room_state_set(room, state_set) + state_set + + {:error, reason} -> + Repo.rollback(reason) + end + end + end + + @spec create_room_insert_events(Room.t(), Account.t(), CreateRoom.t()) :: + (() -> {:ok, t()} | {:error, atom()}) defp create_room_insert_events(room, account, %CreateRoom{ room_version: room_version, preset: preset, name: name, topic: topic }) do + events = + ([ + Event.create_room(room, account, room_version), + Event.join(room, account), + Event.power_levels(room, account) + ] ++ + room_creation_preset(account, preset, room) ++ + [ + if(name, do: Event.name(room, account, name)), + if(topic, do: Event.topic(room, account, topic)) + ]) + |> Enum.reject(&Kernel.is_nil/1) + fn -> - state_set = %{} + 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) - with create_room <- Event.create_room(room, account, room_version), - {:ok, state_set, create_room, room} <- - verify_and_insert_event(create_room, state_set, room), - join_creator <- Event.join(room, account, [create_room]), - {:ok, state_set, join_creator, room} <- - verify_and_insert_event(join_creator, state_set, room), - {:ok, _} <- insert_joined_room_assoc(account, room), - pls <- Event.power_levels(room, account, [create_room, join_creator]), - {:ok, state_set, pls, room} <- verify_and_insert_event(pls, state_set, room) do - auth_events = [create_room, join_creator, pls] - preset_events = room_creation_preset(account, preset, room, auth_events) - name_event = if name, do: Event.name(room, account, name, auth_events) - topic_event = if topic, do: Event.topic(room, account, topic, auth_events) + case result do + {:error, reason} -> + Repo.rollback(reason) - remaining_events = - Enum.reject(preset_events ++ [name_event, topic_event], &Kernel.is_nil/1) - - result = - Enum.reduce_while(remaining_events, {state_set, room}, fn event, {state_set, room} -> - case verify_and_insert_event(event, state_set, room) do - {:ok, state_set, _event, room} -> {:cont, {state_set, room}} - {:error, reason} -> {:halt, {:error, reason}} - end - end) - - case result do - {:error, reason} -> - Repo.rollback(reason) - - {state_set, room} -> - serialized_state_set = - Enum.map(state_set, fn {{type, state_key}, event} -> - [type, state_key, event.event_id] - end) - - Repo.update!(change(room, state: serialized_state_set)) - state_set - end - else - _ -> Repo.rollback(:event_creation) + {state_set, room} -> + _ = update_room_state_set(room, state_set) + state_set end end end - defp insert_joined_room_assoc(%Account{localpart: localpart}, %Room{id: room_id}) do - Repo.insert(%JoinedRoom{localpart: localpart, room_id: room_id}) + @spec update_room_state_set(Room.t(), t()) :: Room.t() + defp update_room_state_set(room, state_set) do + serialized_state_set = + Enum.map(state_set, fn {{type, state_key}, event} -> + [type, state_key, event.event_id] + end) + + Repo.update!(change(room, state: serialized_state_set)) end # TODO: trusted_private_chat: # All invitees are given the same power level as the room creator. - defp room_creation_preset(account, nil, %Room{visibility: visibility} = room, auth_events) do + @spec room_creation_preset(Account.t(), String.t() | nil, Room.t()) :: [Event.t()] + defp room_creation_preset(account, nil, %Room{visibility: visibility} = room) do preset = case visibility do :public -> "public_chat" :private -> "private_chat" end - room_creation_preset(account, preset, room, auth_events) + room_creation_preset(account, preset, room) end - defp room_creation_preset(account, preset, room, auth_events) do + defp room_creation_preset(account, preset, room) do {join_rule, his_vis, guest_access} = case preset do "private_chat" -> {"invite", "shared", "can_join"} @@ -262,17 +290,80 @@ defmodule MatrixServer.RoomServer do end [ - Event.join_rules(room, account, join_rule, auth_events), - Event.history_visibility(room, account, his_vis, auth_events), - Event.guest_access(room, account, guest_access, auth_events) + Event.join_rules(room, account, join_rule), + Event.history_visibility(room, account, his_vis), + Event.guest_access(room, account, guest_access) ] end - defp verify_and_insert_event( + @spec finalize_and_insert_event(Event.t(), t(), Room.t()) :: + {:ok, t(), Room.t()} | {:error, atom()} + defp finalize_and_insert_event( event, - current_state_set, + state_set, %Room{forward_extremities: forward_extremities} = room ) do + event = + event + |> Map.put(:auth_events, auth_events_for_event(event, state_set)) + |> Map.put(:prev_events, forward_extremities) + + case Event.post_process(event) do + {:ok, event} -> verify_and_insert_event(event, state_set, room) + _ -> {:error, :event_creation} + end + end + + @spec auth_events_for_event(Event.t(), t()) :: [{String.t(), String.t()}] + defp auth_events_for_event(%Event{type: "m.room.create"}, _), do: [] + + defp auth_events_for_event( + %Event{sender: sender} = event, + state_set + ) do + state_pairs = + [{"m.room.create", ""}, {"m.room.power_levels", ""}, {"m.room.member", to_string(sender)}] ++ + auth_events_for_member_event(event) + + state_set + |> Map.take(state_pairs) + |> Map.values() + |> Enum.map(& &1.event_id) + end + + @spec auth_events_for_member_event(Event.t()) :: [{String.t(), String.t()}] + defp auth_events_for_member_event( + %Event{ + type: "m.room.member", + state_key: state_key, + content: %{"membership" => membership} + } = event + ) do + [ + {"m.room.member", state_key}, + if(membership in ["join", "invite"], do: {"m.room.join_rules", ""}), + third_party_invite_state_pair(event) + ] + |> Enum.reject(&Kernel.is_nil/1) + end + + defp auth_events_for_member_event(_), do: [] + + @spec third_party_invite_state_pair(Event.t()) :: {String.t(), String.t()} | nil + defp third_party_invite_state_pair(%Event{ + content: %{ + "membership" => "invite", + "third_party_invite" => %{"signed" => %{"token" => token}} + } + }) do + {"m.room.third_party_invite", token} + end + + defp third_party_invite_state_pair(_), do: nil + + @spec verify_and_insert_event(Event.t(), t(), Room.t()) :: + {:ok, t(), Room.t()} | {:error, atom()} + defp verify_and_insert_event(event, current_state_set, room) do # TODO: Correct error values. # Check the following things: # 1. TODO: Is a valid event, otherwise it is dropped. @@ -281,10 +372,7 @@ defmodule MatrixServer.RoomServer do # 4. Passes authorization rules based on the event's auth events, otherwise it is rejected. # 5. Passes authorization rules based on the state at the event, otherwise it is rejected. # 6. Passes authorization rules based on the current state of the room, otherwise it is "soft failed". - event = %Event{event | prev_events: forward_extremities} - - with {:ok, event} <- Event.post_process(event), - true <- Event.prevalidate(event), + with true <- Event.prevalidate(event), true <- Authorization.authorized_by_auth_events?(event), state_set <- StateResolution.resolve(event, false), true <- Authorization.authorized?(event, state_set), @@ -292,9 +380,28 @@ defmodule MatrixServer.RoomServer do room = Room.update_forward_extremities(event, room) event = Repo.insert!(event) state_set = StateResolution.resolve_forward_extremities(event) - {:ok, state_set, event, room} + _ = update_joined_rooms(event, room) + + {:ok, state_set, room} else _ -> {:error, :authorization} end end + + @spec update_joined_rooms(Event.t(), Room.t()) :: JoinedRoom.t() | nil + defp update_joined_rooms( + %Event{ + type: "m.room.member", + sender: %UserId{localpart: localpart, domain: domain}, + content: %{"membership" => "join"} + }, + %Room{id: room_id} + ) do + # TODO: Also remove joined rooms. + if domain == MatrixServer.server_name() do + Repo.insert(%JoinedRoom{localpart: localpart, room_id: room_id}) + end + end + + defp update_joined_rooms(_, _), do: nil end diff --git a/lib/matrix_server/schema/event.ex b/lib/matrix_server/schema/event.ex index 7f5eeda..62a6ffb 100644 --- a/lib/matrix_server/schema/event.ex +++ b/lib/matrix_server/schema/event.ex @@ -74,11 +74,7 @@ defmodule MatrixServer.Event do end @spec create_room(Room.t(), Account.t(), String.t()) :: t() - def create_room( - room, - %Account{localpart: localpart} = creator, - room_version - ) do + def create_room(room, %Account{localpart: localpart} = creator, room_version) do mxid = MatrixServer.get_mxid(localpart) %Event{ @@ -92,8 +88,8 @@ defmodule MatrixServer.Event do } end - @spec join(Room.t(), Account.t(), [t()]) :: t() - def join(room, %Account{localpart: localpart} = sender, auth_events) do + @spec join(Room.t(), Account.t()) :: t() + def join(room, %Account{localpart: localpart} = sender) do mxid = MatrixServer.get_mxid(localpart) %Event{ @@ -102,17 +98,12 @@ defmodule MatrixServer.Event do state_key: mxid, content: %{ "membership" => "join" - }, - auth_events: Enum.map(auth_events, & &1.event_id) + } } end - @spec power_levels(Room.t(), Account.t(), [t()]) :: t() - def power_levels( - room, - %Account{localpart: localpart} = sender, - auth_events - ) do + @spec power_levels(Room.t(), Account.t()) :: t() + def power_levels(room, %Account{localpart: localpart} = sender) do mxid = MatrixServer.get_mxid(localpart) %Event{ @@ -134,73 +125,79 @@ defmodule MatrixServer.Event do "notifications" => %{ "room" => 50 } - }, - auth_events: Enum.map(auth_events, & &1.event_id) + } } end - @spec name(Room.t(), Account.t(), String.t(), [t()]) :: %Event{} - def name(room, sender, name, auth_events) do + @spec name(Room.t(), Account.t(), String.t()) :: %Event{} + def name(room, sender, name) do %Event{ new(room, sender) | type: "m.room.name", state_key: "", content: %{ "name" => name - }, - auth_events: Enum.map(auth_events, & &1.event_id) + } } end - @spec topic(Room.t(), Account.t(), String.t(), [t()]) :: t() - def topic(room, sender, topic, auth_events) do + @spec topic(Room.t(), Account.t(), String.t()) :: t() + def topic(room, sender, topic) do %Event{ new(room, sender) | type: "m.room.topic", state_key: "", content: %{ "topic" => topic - }, - auth_events: Enum.map(auth_events, & &1.event_id) + } } end - @spec join_rules(Room.t(), Account.t(), String.t(), [t()]) :: t() - def join_rules(room, sender, join_rule, auth_events) do + @spec join_rules(Room.t(), Account.t(), String.t()) :: t() + def join_rules(room, sender, join_rule) do %Event{ new(room, sender) | type: "m.room.join_rules", state_key: "", content: %{ "join_rule" => join_rule - }, - auth_events: Enum.map(auth_events, & &1.event_id) + } } end - @spec history_visibility(Room.t(), Account.t(), String.t(), [t()]) :: t() - def history_visibility(room, sender, history_visibility, auth_events) do + @spec history_visibility(Room.t(), Account.t(), String.t()) :: t() + def history_visibility(room, sender, history_visibility) do %Event{ new(room, sender) | type: "m.room.history_visibility", state_key: "", content: %{ "history_visibility" => history_visibility - }, - auth_events: Enum.map(auth_events, & &1.event_id) + } } end - @spec guest_access(Room.t(), Account.t(), String.t(), [t()]) :: t() - def guest_access(room, sender, guest_access, auth_events) do + @spec guest_access(Room.t(), Account.t(), String.t()) :: t() + def guest_access(room, sender, guest_access) do %Event{ new(room, sender) | type: "m.room.guest_access", state_key: "", content: %{ "guest_access" => guest_access - }, - auth_events: Enum.map(auth_events, & &1.event_id) + } + } + end + + @spec invite(Room.t(), Account.t(), String.t()) :: t() + def invite(room, sender, user_id) do + %Event{ + new(room, sender) + | type: "m.room.member", + state_key: user_id, + content: %{ + "membership" => "invite" + } } end diff --git a/lib/matrix_server/schema/joined_room.ex b/lib/matrix_server/schema/joined_room.ex index 044b5a8..b2e3584 100644 --- a/lib/matrix_server/schema/joined_room.ex +++ b/lib/matrix_server/schema/joined_room.ex @@ -3,6 +3,11 @@ defmodule MatrixServer.JoinedRoom do alias MatrixServer.{Account, Room} + @type t :: %__MODULE__{ + localpart: String.t(), + room_id: String.t() + } + @primary_key false schema "joined_rooms" do belongs_to :account, Account, diff --git a/lib/matrix_server/state_resolution/authorization.ex b/lib/matrix_server/state_resolution/authorization.ex index e61d4cb..68ec447 100644 --- a/lib/matrix_server/state_resolution/authorization.ex +++ b/lib/matrix_server/state_resolution/authorization.ex @@ -20,9 +20,12 @@ defmodule MatrixServer.StateResolution.Authorization do do: prev_events == [] # Check rule: 5.2.1 - def authorized?(%Event{type: "m.room.member", state_key: state_key}, %{ - {"m.room.create", ""} => %Event{content: %{"creator" => creator}} - }), + def authorized?( + %Event{type: "m.room.member", state_key: state_key, prev_events: [create_id]}, + %{ + {"m.room.create", ""} => %Event{event_id: create_id, content: %{"creator" => creator}} + } + ), do: state_key == creator def authorized?( @@ -309,6 +312,9 @@ defmodule MatrixServer.StateResolution.Authorization do |> Repo.all() |> Enum.reduce(%{}, &update_state_set/2) + IO.inspect(event) + IO.inspect(state_set) + authorized?(event, state_set) end end diff --git a/lib/matrix_server_web/client/controllers/account_controller.ex b/lib/matrix_server_web/client/controllers/account_controller.ex index 178efa3..cb91764 100644 --- a/lib/matrix_server_web/client/controllers/account_controller.ex +++ b/lib/matrix_server_web/client/controllers/account_controller.ex @@ -7,6 +7,11 @@ defmodule MatrixServerWeb.Client.AccountController do alias MatrixServer.{Account, Repo} alias Plug.Conn + @doc """ + Checks to see if a username is available, and valid, for the server. + + Action for GET /_matrix/client/r0/register/available. + """ def available(conn, params) do localpart = Map.get(params, "username", "") @@ -21,6 +26,11 @@ defmodule MatrixServerWeb.Client.AccountController do end end + @doc """ + Gets information about the owner of a given access token. + + Action for GET /_matrix/client/r0/account/whoami. + """ def whoami(%Conn{assigns: %{account: %Account{localpart: localpart}}} = conn, _params) do data = %{user_id: get_mxid(localpart)} @@ -29,6 +39,11 @@ defmodule MatrixServerWeb.Client.AccountController do |> json(data) end + @doc """ + Invalidates an existing access token, so that it can no longer be used for authorization. + + Action for POST /_matrix/client/r0/logout. + """ def logout(%Conn{assigns: %{device: device}} = conn, _params) do case Repo.delete(device) do {:ok, _} -> @@ -41,6 +56,12 @@ defmodule MatrixServerWeb.Client.AccountController do end end + @doc """ + Invalidates all access tokens for a user, so that they can no longer be used + for authorization. + + Action for POST /_matrix/client/r0/logout/all. + """ def logout_all(%Conn{assigns: %{account: account}} = conn, _params) do Repo.delete_all(Ecto.assoc(account, :devices)) diff --git a/lib/matrix_server_web/client/controllers/aliases_controller.ex b/lib/matrix_server_web/client/controllers/aliases_controller.ex index 3cae10a..ce89096 100644 --- a/lib/matrix_server_web/client/controllers/aliases_controller.ex +++ b/lib/matrix_server_web/client/controllers/aliases_controller.ex @@ -5,6 +5,11 @@ defmodule MatrixServerWeb.Client.AliasesController do alias MatrixServer.Alias + @doc """ + Create a new mapping from room alias to room ID. + + Action for PUT /_matrix/client/r0/directory/room/{roomAlias}. + """ def create(conn, %{"alias" => alias, "room_id" => room_id}) do case Alias.create(alias, room_id) do {:ok, _} -> diff --git a/lib/matrix_server_web/client/controllers/info_controller.ex b/lib/matrix_server_web/client/controllers/info_controller.ex index 7ccc491..1e8bee4 100644 --- a/lib/matrix_server_web/client/controllers/info_controller.ex +++ b/lib/matrix_server_web/client/controllers/info_controller.ex @@ -5,6 +5,11 @@ defmodule MatrixServerWeb.Client.InfoController do @supported_versions ["r0.6.1"] + @doc """ + Gets the versions of the specification supported by the server. + + Action for GET /_matrix/client/versions. + """ def versions(conn, _params) do data = %{versions: @supported_versions} diff --git a/lib/matrix_server_web/client/controllers/login_controller.ex b/lib/matrix_server_web/client/controllers/login_controller.ex index 09c8f97..da47d51 100644 --- a/lib/matrix_server_web/client/controllers/login_controller.ex +++ b/lib/matrix_server_web/client/controllers/login_controller.ex @@ -10,6 +10,11 @@ defmodule MatrixServerWeb.Client.LoginController do @login_type "m.login.password" + @doc """ + Gets the homeserver's supported login types to authenticate users. + + Action for GET /_matrix/client/r0/login. + """ def login_types(conn, _params) do data = %{flows: [%{type: @login_type}]} @@ -18,6 +23,12 @@ defmodule MatrixServerWeb.Client.LoginController do |> json(data) end + @doc """ + Authenticates the user, and issues an access token they can use to + authorize themself in subsequent requests. + + Action for POST /_matrix/client/r0/login. + """ def login( conn, %{"type" => @login_type, "identifier" => %{"type" => "m.id.user"}} = params diff --git a/lib/matrix_server_web/client/controllers/register_controller.ex b/lib/matrix_server_web/client/controllers/register_controller.ex index 1c2d3e6..48bc5a6 100644 --- a/lib/matrix_server_web/client/controllers/register_controller.ex +++ b/lib/matrix_server_web/client/controllers/register_controller.ex @@ -10,6 +10,11 @@ defmodule MatrixServerWeb.Client.RegisterController do @register_type "m.login.dummy" + @doc """ + Register for an account on this homeserver. + + Action for POST /_matrix/client/r0/register. + """ def register(conn, %{"auth" => %{"type" => @register_type}} = params) do case Register.changeset(params) do %Changeset{valid?: true} = cs -> diff --git a/lib/matrix_server_web/client/controllers/room_controller.ex b/lib/matrix_server_web/client/controllers/room_controller.ex index e6a49b7..0b537ac 100644 --- a/lib/matrix_server_web/client/controllers/room_controller.ex +++ b/lib/matrix_server_web/client/controllers/room_controller.ex @@ -4,11 +4,17 @@ defmodule MatrixServerWeb.Client.RoomController do import MatrixServerWeb.Error import Ecto.{Changeset, Query} - alias MatrixServer.{Repo, Room} + alias MatrixServer.{Repo, Room, RoomServer} + alias MatrixServer.Types.UserId alias MatrixServerWeb.Client.Request.CreateRoom alias Ecto.Changeset alias Plug.Conn + @doc """ + Create a new room with various configuration options. + + Action for POST /_matrix/client/r0/createRoom. + """ def create(%Conn{assigns: %{account: account}} = conn, params) do case CreateRoom.changeset(params) do %Changeset{valid?: true} = cs -> @@ -32,11 +38,17 @@ defmodule MatrixServerWeb.Client.RoomController do end end + @doc """ + This API returns a list of the user's current rooms. + + Action for GET /_matrix/client/r0/joined_rooms. + """ def joined_rooms(%Conn{assigns: %{account: account}} = conn, _params) do - joined_room_ids = account - |> Ecto.assoc(:joined_rooms) - |> select([jr], jr.id) - |> Repo.all() + joined_room_ids = + account + |> Ecto.assoc(:joined_rooms) + |> select([jr], jr.id) + |> Repo.all() data = %{ joined_rooms: joined_room_ids @@ -46,4 +58,32 @@ defmodule MatrixServerWeb.Client.RoomController do |> put_status(200) |> json(data) end + + @doc """ + This API invites a user to participate in a particular room. + + Action for POST /_matrix/client/r0/rooms/{roomId}/invite. + """ + def invite(%Conn{assigns: %{account: account}} = conn, %{ + "room_id" => room_id, + "user_id" => user_id + }) do + with {:ok, _} <- UserId.cast(user_id), + {:ok, pid} <- RoomServer.get_room_server(room_id) do + case RoomServer.invite(pid, account, user_id) do + :ok -> + conn + |> send_resp(200, []) + |> halt() + + {: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.") + end + end + + def invite(conn, _), do: put_error(conn, :missing_param) end diff --git a/lib/matrix_server_web/federation/controllers/event_controller.ex b/lib/matrix_server_web/federation/controllers/event_controller.ex index 3f1ed6f..4779b20 100644 --- a/lib/matrix_server_web/federation/controllers/event_controller.ex +++ b/lib/matrix_server_web/federation/controllers/event_controller.ex @@ -8,6 +8,11 @@ defmodule MatrixServerWeb.Federation.EventController do alias MatrixServer.{Repo, Event, RoomServer} alias MatrixServerWeb.Federation.Transaction + @doc """ + Retrieves a single event. + + Action for GET /_matrix/federation/v1/event/{eventId}. + """ def event(%Plug.Conn{assigns: %{origin: origin}} = conn, %{"event_id" => event_id}) do query = Event @@ -39,6 +44,11 @@ defmodule MatrixServerWeb.Federation.EventController do def event(conn, _), do: put_error(conn, :missing_param) + @doc """ + Retrieves a snapshot of a room's state at a given event. + + Action for GET /_matrix/federation/v1/state/{roomId}. + """ def state(%Plug.Conn{assigns: %{origin: origin}} = conn, %{ "event_id" => event_id, "room_id" => room_id @@ -48,6 +58,11 @@ defmodule MatrixServerWeb.Federation.EventController do def state(conn, _), do: put_error(conn, :missing_param) + @doc """ + Retrieves a snapshot of a room's state at a given event, in the form of event IDs. + + Action for GET /_matrix/federation/v1/state_ids/{roomId}. + """ def state_ids(%Plug.Conn{assigns: %{origin: origin}} = conn, %{ "event_id" => event_id, "room_id" => room_id diff --git a/lib/matrix_server_web/federation/controllers/key_controller.ex b/lib/matrix_server_web/federation/controllers/key_controller.ex index 243e44b..1185706 100644 --- a/lib/matrix_server_web/federation/controllers/key_controller.ex +++ b/lib/matrix_server_web/federation/controllers/key_controller.ex @@ -5,6 +5,11 @@ defmodule MatrixServerWeb.Federation.KeyController do alias MatrixServer.KeyServer + @doc """ + Gets the homeserver's published signing keys. + + Action for GET /_matrix/key/v2/server/{keyId}. + """ def get_signing_keys(conn, _params) do keys = KeyServer.get_own_signing_keys() diff --git a/lib/matrix_server_web/federation/controllers/query_controller.ex b/lib/matrix_server_web/federation/controllers/query_controller.ex index 84b9754..60df951 100644 --- a/lib/matrix_server_web/federation/controllers/query_controller.ex +++ b/lib/matrix_server_web/federation/controllers/query_controller.ex @@ -31,6 +31,12 @@ defmodule MatrixServerWeb.Federation.QueryController do end end + @doc """ + Performs a query to get profile information, such as a display name or avatar, + for a given user. + + Action for GET /_matrix/federation/v1/query/profile. + """ def profile(conn, params) do with {:ok, %ProfileRequest{user_id: %UserId{localpart: localpart, domain: domain}}} <- ProfileRequest.validate(params) do diff --git a/lib/matrix_server_web/router.ex b/lib/matrix_server_web/router.ex index a105cc8..f82fc7d 100644 --- a/lib/matrix_server_web/router.ex +++ b/lib/matrix_server_web/router.ex @@ -56,6 +56,10 @@ defmodule MatrixServerWeb.Router do scope "/directory/room" do put "/:alias", AliasesController, :create end + + scope "/rooms/:room_id" do + post "/invite", RoomController, :invite + end end end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index a5fada9..f492e90 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -12,99 +12,3 @@ Repo.insert(%Device{ display_name: "My Android", localpart: "chuck" }) - -# Auth difference example from here: -# https://matrix.org/docs/guides/implementing-stateres#auth-differences - -alice = - Repo.insert!(%Account{ - localpart: "alice", - password_hash: Bcrypt.hash_pwd_salt("sneed") - }) - -bob = - Repo.insert!(%Account{ - localpart: "bob", - password_hash: Bcrypt.hash_pwd_salt("sneed") - }) - -charlie = - Repo.insert!(%Account{ - localpart: "charlie", - password_hash: Bcrypt.hash_pwd_salt("sneed") - }) - -room = - Repo.insert!(%Room{ - id: "room1", - visibility: :public - }) - -create_room = - Repo.insert!( - Event.create_room(room, alice, "v1", false) - |> Map.put(:origin_server_ts, timestamp.(0)) - |> Event.post_process() - |> elem(1) - ) - -Repo.insert!( - Event.join(room, alice, false) - |> Map.put(:prev_events, ["create"]) - |> Map.put(:auth_events, ["create"]) - |> Map.put(:origin_server_ts, timestamp.(1)) - |> Map.put(:event_id, "join_alice") -) - -Repo.insert!( - Event.join(room, bob) - |> elem(1) - |> Map.put(:prev_events, ["join_alice"]) - |> Map.put(:auth_events, ["create"]) - |> Map.put(:origin_server_ts, timestamp.(2)) - |> Map.put(:event_id, "join_bob") -) - -Repo.insert!( - Event.join(room, charlie) - |> elem(1) - |> Map.put(:prev_events, ["join_bob"]) - |> Map.put(:auth_events, ["create"]) - |> Map.put(:origin_server_ts, timestamp.(3)) - |> Map.put(:event_id, "join_charlie") -) - -%Event{content: content} = event = Event.power_levels(room, alice) |> elem(1) -event = %Event{event | content: %{content | "users" => %{"alice" => 100, "bob" => 100}}} - -Repo.insert!( - event - |> Map.put(:prev_events, ["join_alice"]) - |> Map.put(:auth_events, ["create", "join_alice"]) - |> Map.put(:origin_server_ts, timestamp.(4)) - |> Map.put(:event_id, "a") -) - -%Event{content: content} = event = Event.power_levels(room, bob) |> elem(1) - -event = %Event{ - event - | content: %{content | "users" => %{"alice" => 100, "bob" => 100, "charlie" => 100}} -} - -Repo.insert!( - event - |> Map.put(:prev_events, ["a"]) - |> Map.put(:auth_events, ["create", "join_bob", "a"]) - |> Map.put(:origin_server_ts, timestamp.(5)) - |> Map.put(:event_id, "b") -) - -Repo.insert!( - Event.topic(room, alice, "sneed") - |> elem(1) - |> Map.put(:prev_events, ["a"]) - |> Map.put(:auth_events, ["create", "join_alice", "a"]) - |> Map.put(:origin_server_ts, timestamp.(5)) - |> Map.put(:event_id, "fork") -)