diff --git a/lib/matrix_server/room_server.ex b/lib/matrix_server/room_server.ex index 0338727..304c18c 100644 --- a/lib/matrix_server/room_server.ex +++ b/lib/matrix_server/room_server.ex @@ -114,6 +114,14 @@ defmodule MatrixServer.RoomServer do GenServer.call(pid, {:join, account}) end + @doc """ + Leave a room. + """ + @spec leave(pid(), Account.t()) :: :ok | {:error, atom()} + def leave(pid, account) do + GenServer.call(pid, {:leave, account}) + end + ### Implementation @impl true @@ -218,6 +226,29 @@ defmodule MatrixServer.RoomServer do end end + def handle_call({:leave, account}, _from, %{room: room, state_set: state_set} = state) do + case Repo.transaction(leave_insert_event(room, state_set, account)) do + {:ok, state_set} -> {:reply, :ok, %{state | state_set: state_set}} + {:error, reason} -> {:reply, {:error, reason}, state} + end + end + + @spec leave_insert_event(Room.t(), t(), Account.t()) :: (() -> {:ok, t()} | {:error, atom()}) + defp leave_insert_event(room, state_set, account) do + leave_event = Event.leave(room, account) + + fn -> + case finalize_and_insert_event(leave_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 + # Get a function that inserts a join event into the room for the given account. @spec join_insert_event(Room.t(), t(), Account.t()) :: (() -> {:ok, t()} | {:error, atom()}) defp join_insert_event(room, state_set, account) do diff --git a/lib/matrix_server/schema/account.ex b/lib/matrix_server/schema/account.ex index 9fc4ce6..b91f2bf 100644 --- a/lib/matrix_server/schema/account.ex +++ b/lib/matrix_server/schema/account.ex @@ -5,7 +5,7 @@ defmodule MatrixServer.Account do alias MatrixServer.{Repo, Account, Device, Room, JoinedRoom} alias MatrixServerWeb.Client.Request.{Register, Login} - alias Ecto.Multi + alias Ecto.{Multi, Changeset} @type t :: %__MODULE__{ password_hash: String.t() @@ -25,6 +25,10 @@ defmodule MatrixServer.Account do timestamps(updated_at: false) end + @doc """ + Reports whether the given user localpart is available on this server. + """ + @spec available?(String.t()) :: :ok | {:error, :user_in_use | :invalid_username} def available?(localpart) when is_binary(localpart) do if Regex.match?(MatrixServer.localpart_regex(), localpart) and String.length(localpart) <= localpart_length() do @@ -42,6 +46,10 @@ defmodule MatrixServer.Account do end end + @doc """ + Return an multi to register a new user. + """ + @spec register(Register.t()) :: Multi.t() def register(%Register{} = input) do account_params = %{ localpart: input.username || MatrixServer.random_string(10, ?a..?z), @@ -62,6 +70,10 @@ defmodule MatrixServer.Account do |> Multi.run(:device_with_access_token, &Device.insert_new_access_token/2) end + @doc """ + Return a function to log a user in. + """ + @spec login(Login.t()) :: (Ecto.Repo.t() -> {:error, any()} | {:ok, Device.t()}) def login(%Login{} = input) do localpart = try_get_localpart(input.identifier.user) @@ -86,6 +98,10 @@ defmodule MatrixServer.Account do end end + @doc """ + Get a device and its associated account using the device's access token. + """ + @spec by_access_token(String.t()) :: {Account.t(), Device.t()} | nil def by_access_token(access_token) do Device |> where([d], d.access_token == ^access_token) @@ -94,6 +110,7 @@ defmodule MatrixServer.Account do |> Repo.one() end + @spec changeset(map(), map()) :: Changeset.t() def changeset(account, params \\ %{}) do # TODO: fix password_hash in params account @@ -105,11 +122,13 @@ defmodule MatrixServer.Account do |> unique_constraint(:localpart, name: :accounts_pkey) end + @spec localpart_length :: integer() defp localpart_length do # Subtract the "@" and ":" in the MXID. @max_mxid_length - 2 - String.length(MatrixServer.server_name()) end + @spec try_get_localpart(String.t()) :: String.t() defp try_get_localpart("@" <> rest = user_id) do case String.split(rest, ":", parts: 2) do [localpart, _] -> localpart @@ -118,4 +137,12 @@ defmodule MatrixServer.Account do end defp try_get_localpart(localpart), do: localpart + + @doc """ + Get the matrix user ID of an account. + """ + @spec get_mxid(Account.t()) :: String.t() + def get_mxid(%Account{localpart: localpart}) do + "@" <> localpart <> ":" <> MatrixServer.server_name() + end end diff --git a/lib/matrix_server/schema/device.ex b/lib/matrix_server/schema/device.ex index 99d919a..1099f17 100644 --- a/lib/matrix_server/schema/device.ex +++ b/lib/matrix_server/schema/device.ex @@ -6,6 +6,13 @@ defmodule MatrixServer.Device do alias MatrixServer.{Account, Device, Repo} alias MatrixServerWeb.Client.Request.Login + @type t :: %__MODULE__{ + device_id: String.t(), + access_token: String.t(), + display_name: String.t(), + localpart: String.t() + } + @primary_key false schema "devices" do field :device_id, :string, primary_key: true diff --git a/lib/matrix_server/schema/event.ex b/lib/matrix_server/schema/event.ex index 62a6ffb..2e3ee7a 100644 --- a/lib/matrix_server/schema/event.ex +++ b/lib/matrix_server/schema/event.ex @@ -201,6 +201,18 @@ defmodule MatrixServer.Event do } end + @spec leave(Room.t(), Account.t()) :: t() + def leave(room, sender) do + %Event{ + new(room, sender) + | type: "m.room.member", + state_key: Account.get_mxid(sender), + content: %{ + "membership" => "leave" + } + } + 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 c643d1d..3fc970b 100644 --- a/lib/matrix_server/state_resolution/authorization.ex +++ b/lib/matrix_server/state_resolution/authorization.ex @@ -76,19 +76,6 @@ defmodule MatrixServer.StateResolution.Authorization do end end - def authorized?( - %Event{ - type: "m.room.member", - sender: sender, - content: %{"membership" => "leave"}, - state_key: sender - }, - state_set - ) do - # Check rule: 5.4.1 - get_membership(to_string(sender), state_set) in ["invite", "join"] - end - def authorized?( %Event{ type: "m.room.member", @@ -99,24 +86,30 @@ defmodule MatrixServer.StateResolution.Authorization do state_set ) do sender_membership = get_membership(to_string(sender), state_set) - target_membership = get_membership(state_key, state_set) - power_levels = get_power_levels(state_set) - sender_pl = get_user_power_level(to_string(sender), power_levels) - target_pl = get_user_power_level(state_key, power_levels) - # Check rules: 5.4.2, 5.4.3, 5.4.4 - cond do - sender_membership != "join" -> - false + if to_string(sender) == state_key do + # Check rule: 5.4.1 + sender_membership in ["invite", "join"] + else + target_membership = get_membership(state_key, state_set) + power_levels = get_power_levels(state_set) + sender_pl = get_user_power_level(to_string(sender), power_levels) + target_pl = get_user_power_level(state_key, power_levels) - target_membership == "ban" and not has_power_level?(to_string(sender), power_levels, :ban) -> - false + # Check rules: 5.4.2, 5.4.3, 5.4.4 + cond do + sender_membership != "join" -> + false - has_power_level?(to_string(sender), power_levels, :kick) and target_pl < sender_pl -> - true + target_membership == "ban" and not has_power_level?(to_string(sender), power_levels, :ban) -> + false - true -> - false + has_power_level?(to_string(sender), power_levels, :kick) and target_pl < sender_pl -> + true + + true -> + false + end end end @@ -312,6 +305,7 @@ 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 bb94754..0870d7f 100644 --- a/lib/matrix_server_web/client/controllers/room_controller.ex +++ b/lib/matrix_server_web/client/controllers/room_controller.ex @@ -112,4 +112,29 @@ defmodule MatrixServerWeb.Client.RoomController do end def join(conn, _), do: put_error(conn, :missing_param) + + @doc """ + This API stops a user participating in a particular room. + + Action for POST /_matrix/client/r0/rooms/{roomId}/leave. + """ + def leave(%Conn{assigns: %{account: account}} = conn, %{"room_id" => room_id}) do + case RoomServer.get_room_server(room_id) do + {:ok, pid} -> + case RoomServer.leave(pid, account) do + :ok -> + conn + |> send_resp(200, []) + |> halt() + + {:error, _} -> + put_error(conn, :unknown) + end + + {:error, :not_found} -> + put_error(conn, :not_found, "The given room was not found.") + end + end + + def leave(conn, _), do: put_error(conn, :missing_param) end diff --git a/lib/matrix_server_web/client/request/login.ex b/lib/matrix_server_web/client/request/login.ex index 5e5d5c1..e824a8d 100644 --- a/lib/matrix_server_web/client/request/login.ex +++ b/lib/matrix_server_web/client/request/login.ex @@ -3,6 +3,13 @@ defmodule MatrixServerWeb.Client.Request.Login do import Ecto.Changeset + @type t :: %__MODULE__{ + type: String.t(), + password: String.t(), + device_id: String.t(), + initial_device_display_name: String.t() + } + @primary_key false embedded_schema do field :type, :string diff --git a/lib/matrix_server_web/client/request/register.ex b/lib/matrix_server_web/client/request/register.ex index 2969a76..545f138 100644 --- a/lib/matrix_server_web/client/request/register.ex +++ b/lib/matrix_server_web/client/request/register.ex @@ -5,6 +5,14 @@ defmodule MatrixServerWeb.Client.Request.Register do alias Ecto.Changeset + @type t :: %__MODULE__{ + device_id: String.t(), + initial_device_display_name: String.t(), + password: String.t(), + username: String.t(), + inhibit_login: boolean() + } + @primary_key false embedded_schema do field :device_id, :string diff --git a/lib/matrix_server_web/router.ex b/lib/matrix_server_web/router.ex index bb60f87..6f1075d 100644 --- a/lib/matrix_server_web/router.ex +++ b/lib/matrix_server_web/router.ex @@ -60,6 +60,7 @@ defmodule MatrixServerWeb.Router do scope "/rooms/:room_id" do post "/invite", RoomController, :invite post "/join", RoomController, :join + post "/leave", RoomController, :leave end end end