Implement client leave endpoint

Add documentation and type specs
This commit is contained in:
Pim Kunis 2021-08-26 11:01:19 +02:00
parent a1475c1a46
commit ae860a768c
9 changed files with 140 additions and 28 deletions

View file

@ -114,6 +114,14 @@ defmodule MatrixServer.RoomServer do
GenServer.call(pid, {:join, account}) GenServer.call(pid, {:join, account})
end 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 ### Implementation
@impl true @impl true
@ -218,6 +226,29 @@ defmodule MatrixServer.RoomServer do
end end
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. # 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()}) @spec join_insert_event(Room.t(), t(), Account.t()) :: (() -> {:ok, t()} | {:error, atom()})
defp join_insert_event(room, state_set, account) do defp join_insert_event(room, state_set, account) do

View file

@ -5,7 +5,7 @@ defmodule MatrixServer.Account do
alias MatrixServer.{Repo, Account, Device, Room, JoinedRoom} alias MatrixServer.{Repo, Account, Device, Room, JoinedRoom}
alias MatrixServerWeb.Client.Request.{Register, Login} alias MatrixServerWeb.Client.Request.{Register, Login}
alias Ecto.Multi alias Ecto.{Multi, Changeset}
@type t :: %__MODULE__{ @type t :: %__MODULE__{
password_hash: String.t() password_hash: String.t()
@ -25,6 +25,10 @@ defmodule MatrixServer.Account do
timestamps(updated_at: false) timestamps(updated_at: false)
end 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 def available?(localpart) when is_binary(localpart) do
if Regex.match?(MatrixServer.localpart_regex(), localpart) and if Regex.match?(MatrixServer.localpart_regex(), localpart) and
String.length(localpart) <= localpart_length() do String.length(localpart) <= localpart_length() do
@ -42,6 +46,10 @@ defmodule MatrixServer.Account do
end end
end end
@doc """
Return an multi to register a new user.
"""
@spec register(Register.t()) :: Multi.t()
def register(%Register{} = input) do def register(%Register{} = input) do
account_params = %{ account_params = %{
localpart: input.username || MatrixServer.random_string(10, ?a..?z), 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) |> Multi.run(:device_with_access_token, &Device.insert_new_access_token/2)
end 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 def login(%Login{} = input) do
localpart = try_get_localpart(input.identifier.user) localpart = try_get_localpart(input.identifier.user)
@ -86,6 +98,10 @@ defmodule MatrixServer.Account do
end end
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 def by_access_token(access_token) do
Device Device
|> where([d], d.access_token == ^access_token) |> where([d], d.access_token == ^access_token)
@ -94,6 +110,7 @@ defmodule MatrixServer.Account do
|> Repo.one() |> Repo.one()
end end
@spec changeset(map(), map()) :: Changeset.t()
def changeset(account, params \\ %{}) do def changeset(account, params \\ %{}) do
# TODO: fix password_hash in params # TODO: fix password_hash in params
account account
@ -105,11 +122,13 @@ defmodule MatrixServer.Account do
|> unique_constraint(:localpart, name: :accounts_pkey) |> unique_constraint(:localpart, name: :accounts_pkey)
end end
@spec localpart_length :: integer()
defp localpart_length do defp localpart_length do
# Subtract the "@" and ":" in the MXID. # Subtract the "@" and ":" in the MXID.
@max_mxid_length - 2 - String.length(MatrixServer.server_name()) @max_mxid_length - 2 - String.length(MatrixServer.server_name())
end end
@spec try_get_localpart(String.t()) :: String.t()
defp try_get_localpart("@" <> rest = user_id) do defp try_get_localpart("@" <> rest = user_id) do
case String.split(rest, ":", parts: 2) do case String.split(rest, ":", parts: 2) do
[localpart, _] -> localpart [localpart, _] -> localpart
@ -118,4 +137,12 @@ defmodule MatrixServer.Account do
end end
defp try_get_localpart(localpart), do: localpart 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 end

View file

@ -6,6 +6,13 @@ defmodule MatrixServer.Device do
alias MatrixServer.{Account, Device, Repo} alias MatrixServer.{Account, Device, Repo}
alias MatrixServerWeb.Client.Request.Login 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 @primary_key false
schema "devices" do schema "devices" do
field :device_id, :string, primary_key: true field :device_id, :string, primary_key: true

View file

@ -201,6 +201,18 @@ defmodule MatrixServer.Event do
} }
end 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() @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.power_levels", state_key: ""}), do: true
def is_control_event(%Event{type: "m.room.join_rules", state_key: ""}), do: true def is_control_event(%Event{type: "m.room.join_rules", state_key: ""}), do: true

View file

@ -76,19 +76,6 @@ defmodule MatrixServer.StateResolution.Authorization do
end end
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?( def authorized?(
%Event{ %Event{
type: "m.room.member", type: "m.room.member",
@ -99,24 +86,30 @@ defmodule MatrixServer.StateResolution.Authorization do
state_set state_set
) do ) do
sender_membership = get_membership(to_string(sender), state_set) 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 if to_string(sender) == state_key do
cond do # Check rule: 5.4.1
sender_membership != "join" -> sender_membership in ["invite", "join"]
false 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) -> # Check rules: 5.4.2, 5.4.3, 5.4.4
false cond do
sender_membership != "join" ->
false
has_power_level?(to_string(sender), power_levels, :kick) and target_pl < sender_pl -> target_membership == "ban" and not has_power_level?(to_string(sender), power_levels, :ban) ->
true false
true -> has_power_level?(to_string(sender), power_levels, :kick) and target_pl < sender_pl ->
false true
true ->
false
end
end end
end end
@ -312,6 +305,7 @@ defmodule MatrixServer.StateResolution.Authorization do
|> Repo.all() |> Repo.all()
|> Enum.reduce(%{}, &update_state_set/2) |> Enum.reduce(%{}, &update_state_set/2)
IO.inspect(event)
authorized?(event, state_set) authorized?(event, state_set)
end end
end end

View file

@ -112,4 +112,29 @@ defmodule MatrixServerWeb.Client.RoomController do
end end
def join(conn, _), do: put_error(conn, :missing_param) 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 end

View file

@ -3,6 +3,13 @@ defmodule MatrixServerWeb.Client.Request.Login do
import Ecto.Changeset 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 @primary_key false
embedded_schema do embedded_schema do
field :type, :string field :type, :string

View file

@ -5,6 +5,14 @@ defmodule MatrixServerWeb.Client.Request.Register do
alias Ecto.Changeset 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 @primary_key false
embedded_schema do embedded_schema do
field :device_id, :string field :device_id, :string

View file

@ -60,6 +60,7 @@ defmodule MatrixServerWeb.Router do
scope "/rooms/:room_id" do scope "/rooms/:room_id" do
post "/invite", RoomController, :invite post "/invite", RoomController, :invite
post "/join", RoomController, :join post "/join", RoomController, :join
post "/leave", RoomController, :leave
end end
end end
end end