From bb7c3b07e962ac7983cbcb889eb73c3cdba9245d Mon Sep 17 00:00:00 2001 From: Pim Kunis Date: Thu, 26 Aug 2021 14:32:24 +0200 Subject: [PATCH] Implement client kick endpoint --- lib/matrix_server/room_server.ex | 56 +++++++++++++++---- lib/matrix_server/schema/event.ex | 13 +++++ .../state_resolution/authorization.ex | 1 - .../client/controllers/room_controller.ex | 27 +++++++-- lib/matrix_server_web/client/request/kick.ex | 20 +++++++ lib/matrix_server_web/request.ex | 26 +++++++++ lib/matrix_server_web/router.ex | 1 + 7 files changed, 127 insertions(+), 17 deletions(-) create mode 100644 lib/matrix_server_web/client/request/kick.ex create mode 100644 lib/matrix_server_web/request.ex diff --git a/lib/matrix_server/room_server.ex b/lib/matrix_server/room_server.ex index 96c0773..6693d85 100644 --- a/lib/matrix_server/room_server.ex +++ b/lib/matrix_server/room_server.ex @@ -15,7 +15,7 @@ defmodule MatrixServer.RoomServer do alias MatrixServer.{Repo, Room, Event, StateResolution, Account, JoinedRoom} alias MatrixServer.StateResolution.Authorization - alias MatrixServerWeb.Client.Request.CreateRoom + alias MatrixServerWeb.Client.Request.{CreateRoom, Kick} @registry MatrixServer.RoomServer.Registry @supervisor MatrixServer.RoomServer.Supervisor @@ -122,6 +122,14 @@ defmodule MatrixServer.RoomServer do GenServer.call(pid, {:leave, account}) end + @doc """ + Kick a user from this room. + """ + @spec kick(pid(), Account.t(), Kick.t()) :: :ok | {:error, atom()} + def kick(pid, account, request) do + GenServer.call(pid, {:kick, account, request}) + end + ### Implementation @impl true @@ -149,9 +157,14 @@ defmodule MatrixServer.RoomServer do ) do # TODO: power_level_content_override, initial_state, invite, invite_3pid 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} - _ -> {:reply, {:error, :unknown}, state} + {:ok, {state_set, room}} -> + {:reply, {:ok, room_id}, %{state | state_set: state_set, room: room}} + + {:error, reason} -> + {:reply, {:error, reason}, state} + + _ -> + {:reply, {:error, :unknown}, state} end end @@ -212,7 +225,7 @@ defmodule MatrixServer.RoomServer do invite_event = Event.invite(room, account, user_id) case insert_single_event(room, state_set, invite_event) do - {:ok, state_set} -> {:reply, :ok, %{state | state_set: state_set}} + {:ok, {state_set, room}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} {:error, reason} -> {:reply, {:error, reason}, state} end end @@ -225,8 +238,11 @@ defmodule MatrixServer.RoomServer do join_event = Event.join(room, account) case insert_single_event(room, state_set, join_event) do - {:ok, state_set} -> {:reply, {:ok, room_id}, %{state | state_set: state_set}} - {:error, reason} -> {:reply, {:error, reason}, state} + {:ok, {state_set, room}} -> + {:reply, {:ok, room_id}, %{state | state_set: state_set, room: room}} + + {:error, reason} -> + {:reply, {:error, reason}, state} end end @@ -234,18 +250,31 @@ defmodule MatrixServer.RoomServer do leave_event = Event.leave(room, account) case insert_single_event(room, state_set, leave_event) do - {:ok, state_set} -> {:reply, :ok, %{state | state_set: state_set}} + {:ok, {state_set, room}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} {:error, reason} -> {:reply, {:error, reason}, state} end end - @spec insert_single_event(Room.t(), t(), Event.t()) :: {:ok, t()} | {:error, atom()} + def handle_call( + {:kick, account, %Kick{user_id: user_id, reason: reason}}, + _from, + %{room: room, state_set: state_set} = state + ) do + kick_event = Event.kick(room, account, user_id, reason) + + case 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 + + @spec insert_single_event(Room.t(), t(), Event.t()) :: {:ok, t(), Room.t()} | {:error, atom()} defp insert_single_event(room, state_set, event) do Repo.transaction(fn -> case finalize_and_insert_event(event, state_set, room) do {:ok, state_set, room} -> _ = update_room_state_set(room, state_set) - state_set + {state_set, room} {:error, reason} -> Repo.rollback(reason) @@ -255,7 +284,7 @@ defmodule MatrixServer.RoomServer do # Get a function that inserts all events for room creation. @spec create_room_insert_events(Room.t(), Account.t(), CreateRoom.t()) :: - (() -> {:ok, t()} | {:error, atom()}) + (() -> {:ok, t(), Room.t()} | {:error, atom()}) defp create_room_insert_events(room, account, %CreateRoom{ room_version: room_version, preset: preset, @@ -290,7 +319,7 @@ defmodule MatrixServer.RoomServer do {state_set, room} -> _ = update_room_state_set(room, state_set) - state_set + {state_set, room} end end end @@ -298,6 +327,9 @@ defmodule MatrixServer.RoomServer do # 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} -> [type, state_key, event.event_id] diff --git a/lib/matrix_server/schema/event.ex b/lib/matrix_server/schema/event.ex index 2e3ee7a..eec5e6c 100644 --- a/lib/matrix_server/schema/event.ex +++ b/lib/matrix_server/schema/event.ex @@ -213,6 +213,19 @@ defmodule MatrixServer.Event do } end + @spec kick(Room.t(), Account.t(), String.t(), String.t() | nil) :: t() + def kick(room, sender, user_id, reason \\ nil) do + content = %{"membership" => "leave"} + content = if reason, do: Map.put(content, "reason", reason), else: content + + %Event{ + new(room, sender) + | type: "m.room.member", + state_key: user_id, + content: content + } + end + @spec is_control_event(t()) :: boolean() def is_control_event(%Event{type: "m.room.power_levels", state_key: ""}), do: true def is_control_event(%Event{type: "m.room.join_rules", state_key: ""}), do: true diff --git a/lib/matrix_server/state_resolution/authorization.ex b/lib/matrix_server/state_resolution/authorization.ex index 3fc970b..b8e83b5 100644 --- a/lib/matrix_server/state_resolution/authorization.ex +++ b/lib/matrix_server/state_resolution/authorization.ex @@ -305,7 +305,6 @@ defmodule MatrixServer.StateResolution.Authorization do |> Repo.all() |> Enum.reduce(%{}, &update_state_set/2) - IO.inspect(event) authorized?(event, state_set) end end diff --git a/lib/matrix_server_web/client/controllers/room_controller.ex b/lib/matrix_server_web/client/controllers/room_controller.ex index 0870d7f..8b0ea8d 100644 --- a/lib/matrix_server_web/client/controllers/room_controller.ex +++ b/lib/matrix_server_web/client/controllers/room_controller.ex @@ -6,7 +6,7 @@ defmodule MatrixServerWeb.Client.RoomController do alias MatrixServer.{Repo, Room, RoomServer} alias MatrixServer.Types.UserId - alias MatrixServerWeb.Client.Request.CreateRoom + alias MatrixServerWeb.Client.Request.{CreateRoom, Kick} alias Ecto.Changeset alias Plug.Conn @@ -111,8 +111,6 @@ defmodule MatrixServerWeb.Client.RoomController do end end - def join(conn, _), do: put_error(conn, :missing_param) - @doc """ This API stops a user participating in a particular room. @@ -136,5 +134,26 @@ defmodule MatrixServerWeb.Client.RoomController do end end - def leave(conn, _), do: put_error(conn, :missing_param) + @doc """ + Kick a user from the room. + + Action for POST /_matrix/client/r0/rooms/{roomId}/kick. + """ + def kick(%Conn{assigns: %{account: account}} = conn, %{"room_id" => room_id} = params) do + with {:ok, request} <- Kick.parse(params), + {:ok, pid} <- RoomServer.get_room_server(room_id) do + case RoomServer.kick(pid, account, request) do + :ok -> + conn + |> send_resp(200, []) + |> halt() + + {:error, _} -> + put_error(conn, :unknown) + end + else + {:error, %Ecto.Changeset{}} -> put_error(conn, :bad_json) + {:error, :not_found} -> put_error(conn, :not_found, "Room not found.") + end + end end diff --git a/lib/matrix_server_web/client/request/kick.ex b/lib/matrix_server_web/client/request/kick.ex new file mode 100644 index 0000000..18fea6e --- /dev/null +++ b/lib/matrix_server_web/client/request/kick.ex @@ -0,0 +1,20 @@ +defmodule MatrixServerWeb.Client.Request.Kick do + use MatrixServerWeb.Request + + @type t :: %__MODULE__{ + user_id: String.t(), + reason: String.t() | nil + } + + @primary_key false + embedded_schema do + field :user_id, :string + field :reason, :string + end + + def changeset(data, params) do + data + |> cast(params, [:user_id, :reason]) + |> validate_required([:user_id]) + end +end diff --git a/lib/matrix_server_web/request.ex b/lib/matrix_server_web/request.ex new file mode 100644 index 0000000..bf8a38e --- /dev/null +++ b/lib/matrix_server_web/request.ex @@ -0,0 +1,26 @@ +defmodule MatrixServerWeb.Request do + import Ecto.Changeset + + alias Ecto.Changeset + + @spec parse(module(), map()) :: {:ok, struct()} | {:error, Changeset.t()} + def parse(module, params) do + case apply(module, :changeset, [struct(module), params]) do + %Ecto.Changeset{valid?: true} = cs -> {:ok, apply_changes(cs)} + cs -> {:error, cs} + end + end + + defmacro __using__(_opts) do + quote do + use Ecto.Schema + + import Ecto.Changeset + + @spec parse(map()) :: {:ok, struct()} | {:error, Changeset.t()} + def parse(params) do + MatrixServerWeb.Request.parse(__MODULE__, params) + end + end + end +end diff --git a/lib/matrix_server_web/router.ex b/lib/matrix_server_web/router.ex index 6f1075d..10a10e1 100644 --- a/lib/matrix_server_web/router.ex +++ b/lib/matrix_server_web/router.ex @@ -61,6 +61,7 @@ defmodule MatrixServerWeb.Router do post "/invite", RoomController, :invite post "/join", RoomController, :join post "/leave", RoomController, :leave + post "/kick", RoomController, :kick end end end