Implement client invite endpoint

Refactor room server to automatically determine auth events
Add documentation in various places
This commit is contained in:
Pim Kunis 2021-08-25 01:27:03 +02:00
parent 6f8c224d50
commit bcc6cbb24b
15 changed files with 345 additions and 209 deletions

View file

@ -6,12 +6,15 @@ defmodule MatrixServer.RoomServer do
The RoomServers are supervised by a DynamicSupervisor RoomServer.Supervisor. The RoomServers are supervised by a DynamicSupervisor RoomServer.Supervisor.
""" """
@typep t :: map()
use GenServer use GenServer
import Ecto.Query import Ecto.Query
import Ecto.Changeset 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 MatrixServer.StateResolution.Authorization
alias MatrixServerWeb.Client.Request.CreateRoom alias MatrixServerWeb.Client.Request.CreateRoom
@ -35,7 +38,7 @@ defmodule MatrixServer.RoomServer do
def get_room_server(room_id) do def get_room_server(room_id) do
# TODO: Might be wise to use a transaction here to prevent race conditions. # 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 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 case Registry.lookup(@registry, room_id) do
[{pid, _}] -> [{pid, _}] ->
{:ok, pid} {:ok, pid}
@ -43,7 +46,7 @@ defmodule MatrixServer.RoomServer do
[] -> [] ->
opts = [ opts = [
name: {:via, Registry, {@registry, room_id}}, name: {:via, Registry, {@registry, room_id}},
room_id: room_id, room: room,
serialized_state_set: serialized_state_set 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 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). to the [Matrix documentation](https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-createroom).
""" """
@spec create_room( @spec create_room(pid(), Account.t(), CreateRoom.t()) :: {:ok, String.t()} | {:error, atom()}
pid(),
MatrixServer.Account.t(),
MatrixServerWeb.Client.Request.CreateRoom.t()
) :: {:ok, String.t()} | {:error, atom()}
def create_room(pid, account, input) do def create_room(pid, account, input) do
GenServer.call(pid, {:create_room, account, input}) GenServer.call(pid, {:create_room, account, input})
end end
@ -100,11 +99,19 @@ defmodule MatrixServer.RoomServer do
GenServer.call(pid, {:get_state_ids_at_event, event}) GenServer.call(pid, {:get_state_ids_at_event, event})
end 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 ### Implementation
@impl true @impl true
def init(opts) do 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) serialized_state_set = Keyword.fetch!(opts, :serialized_state_set)
state_event_ids = Enum.map(serialized_state_set, fn [_, _, event_id] -> event_id end) 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} {{type, state_key}, event}
end) end)
{:ok, %{room_id: room_id, state_set: state_set}} {:ok, %{room: room, state_set: state_set}}
end end
@impl true @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 # 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 case Repo.transaction(create_room_insert_events(room, account, input)) do
{:ok, state_set} -> {:reply, {:ok, room_id}, %{state | state_set: state_set}} {:ok, state_set} -> {:reply, {:ok, room_id}, %{state | state_set: state_set}}
{:error, reason} -> {:reply, {:error, reason}, state} {:error, reason} -> {:reply, {:error, reason}, state}
@ -184,36 +193,56 @@ defmodule MatrixServer.RoomServer do
{:reply, {state_events, auth_chain}, state} {:reply, {state_events, auth_chain}, state}
end 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{ defp create_room_insert_events(room, account, %CreateRoom{
room_version: room_version, room_version: room_version,
preset: preset, preset: preset,
name: name, name: name,
topic: topic topic: topic
}) do }) 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 -> fn ->
state_set = %{}
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)
remaining_events =
Enum.reject(preset_events ++ [name_event, topic_event], &Kernel.is_nil/1)
result = result =
Enum.reduce_while(remaining_events, {state_set, room}, fn event, {state_set, room} -> Enum.reduce_while(events, {%{}, room}, fn event, {state_set, room} ->
case verify_and_insert_event(event, state_set, room) do case finalize_and_insert_event(event, state_set, room) do
{:ok, state_set, _event, room} -> {:cont, {state_set, room}} {:ok, state_set, room} -> {:cont, {state_set, room}}
{:error, reason} -> {:halt, {:error, reason}} {:error, reason} -> {:halt, {:error, reason}}
end end
end) end)
@ -223,37 +252,36 @@ defmodule MatrixServer.RoomServer do
Repo.rollback(reason) Repo.rollback(reason)
{state_set, room} -> {state_set, room} ->
_ = update_room_state_set(room, state_set)
state_set
end
end
end
@spec update_room_state_set(Room.t(), t()) :: Room.t()
defp update_room_state_set(room, state_set) do
serialized_state_set = serialized_state_set =
Enum.map(state_set, fn {{type, state_key}, event} -> Enum.map(state_set, fn {{type, state_key}, event} ->
[type, state_key, event.event_id] [type, state_key, event.event_id]
end) end)
Repo.update!(change(room, state: serialized_state_set)) Repo.update!(change(room, state: serialized_state_set))
state_set
end
else
_ -> Repo.rollback(:event_creation)
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})
end end
# TODO: trusted_private_chat: # TODO: trusted_private_chat:
# All invitees are given the same power level as the room creator. # 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 = preset =
case visibility do case visibility do
:public -> "public_chat" :public -> "public_chat"
:private -> "private_chat" :private -> "private_chat"
end end
room_creation_preset(account, preset, room, auth_events) room_creation_preset(account, preset, room)
end 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} = {join_rule, his_vis, guest_access} =
case preset do case preset do
"private_chat" -> {"invite", "shared", "can_join"} "private_chat" -> {"invite", "shared", "can_join"}
@ -262,17 +290,80 @@ defmodule MatrixServer.RoomServer do
end end
[ [
Event.join_rules(room, account, join_rule, auth_events), Event.join_rules(room, account, join_rule),
Event.history_visibility(room, account, his_vis, auth_events), Event.history_visibility(room, account, his_vis),
Event.guest_access(room, account, guest_access, auth_events) Event.guest_access(room, account, guest_access)
] ]
end 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, event,
current_state_set, state_set,
%Room{forward_extremities: forward_extremities} = room %Room{forward_extremities: forward_extremities} = room
) do ) 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. # TODO: Correct error values.
# Check the following things: # Check the following things:
# 1. TODO: Is a valid event, otherwise it is dropped. # 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. # 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. # 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". # 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 true <- Event.prevalidate(event),
with {:ok, event} <- Event.post_process(event),
true <- Event.prevalidate(event),
true <- Authorization.authorized_by_auth_events?(event), true <- Authorization.authorized_by_auth_events?(event),
state_set <- StateResolution.resolve(event, false), state_set <- StateResolution.resolve(event, false),
true <- Authorization.authorized?(event, state_set), true <- Authorization.authorized?(event, state_set),
@ -292,9 +380,28 @@ defmodule MatrixServer.RoomServer do
room = Room.update_forward_extremities(event, room) room = Room.update_forward_extremities(event, room)
event = Repo.insert!(event) event = Repo.insert!(event)
state_set = StateResolution.resolve_forward_extremities(event) state_set = StateResolution.resolve_forward_extremities(event)
{:ok, state_set, event, room} _ = update_joined_rooms(event, room)
{:ok, state_set, room}
else else
_ -> {:error, :authorization} _ -> {:error, :authorization}
end end
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 end

View file

@ -74,11 +74,7 @@ defmodule MatrixServer.Event do
end end
@spec create_room(Room.t(), Account.t(), String.t()) :: t() @spec create_room(Room.t(), Account.t(), String.t()) :: t()
def create_room( def create_room(room, %Account{localpart: localpart} = creator, room_version) do
room,
%Account{localpart: localpart} = creator,
room_version
) do
mxid = MatrixServer.get_mxid(localpart) mxid = MatrixServer.get_mxid(localpart)
%Event{ %Event{
@ -92,8 +88,8 @@ defmodule MatrixServer.Event do
} }
end end
@spec join(Room.t(), Account.t(), [t()]) :: t() @spec join(Room.t(), Account.t()) :: t()
def join(room, %Account{localpart: localpart} = sender, auth_events) do def join(room, %Account{localpart: localpart} = sender) do
mxid = MatrixServer.get_mxid(localpart) mxid = MatrixServer.get_mxid(localpart)
%Event{ %Event{
@ -102,17 +98,12 @@ defmodule MatrixServer.Event do
state_key: mxid, state_key: mxid,
content: %{ content: %{
"membership" => "join" "membership" => "join"
}, }
auth_events: Enum.map(auth_events, & &1.event_id)
} }
end end
@spec power_levels(Room.t(), Account.t(), [t()]) :: t() @spec power_levels(Room.t(), Account.t()) :: t()
def power_levels( def power_levels(room, %Account{localpart: localpart} = sender) do
room,
%Account{localpart: localpart} = sender,
auth_events
) do
mxid = MatrixServer.get_mxid(localpart) mxid = MatrixServer.get_mxid(localpart)
%Event{ %Event{
@ -134,73 +125,79 @@ defmodule MatrixServer.Event do
"notifications" => %{ "notifications" => %{
"room" => 50 "room" => 50
} }
}, }
auth_events: Enum.map(auth_events, & &1.event_id)
} }
end end
@spec name(Room.t(), Account.t(), String.t(), [t()]) :: %Event{} @spec name(Room.t(), Account.t(), String.t()) :: %Event{}
def name(room, sender, name, auth_events) do def name(room, sender, name) do
%Event{ %Event{
new(room, sender) new(room, sender)
| type: "m.room.name", | type: "m.room.name",
state_key: "", state_key: "",
content: %{ content: %{
"name" => name "name" => name
}, }
auth_events: Enum.map(auth_events, & &1.event_id)
} }
end end
@spec topic(Room.t(), Account.t(), String.t(), [t()]) :: t() @spec topic(Room.t(), Account.t(), String.t()) :: t()
def topic(room, sender, topic, auth_events) do def topic(room, sender, topic) do
%Event{ %Event{
new(room, sender) new(room, sender)
| type: "m.room.topic", | type: "m.room.topic",
state_key: "", state_key: "",
content: %{ content: %{
"topic" => topic "topic" => topic
}, }
auth_events: Enum.map(auth_events, & &1.event_id)
} }
end end
@spec join_rules(Room.t(), Account.t(), String.t(), [t()]) :: t() @spec join_rules(Room.t(), Account.t(), String.t()) :: t()
def join_rules(room, sender, join_rule, auth_events) do def join_rules(room, sender, join_rule) do
%Event{ %Event{
new(room, sender) new(room, sender)
| type: "m.room.join_rules", | type: "m.room.join_rules",
state_key: "", state_key: "",
content: %{ content: %{
"join_rule" => join_rule "join_rule" => join_rule
}, }
auth_events: Enum.map(auth_events, & &1.event_id)
} }
end end
@spec history_visibility(Room.t(), Account.t(), String.t(), [t()]) :: t() @spec history_visibility(Room.t(), Account.t(), String.t()) :: t()
def history_visibility(room, sender, history_visibility, auth_events) do def history_visibility(room, sender, history_visibility) do
%Event{ %Event{
new(room, sender) new(room, sender)
| type: "m.room.history_visibility", | type: "m.room.history_visibility",
state_key: "", state_key: "",
content: %{ content: %{
"history_visibility" => history_visibility "history_visibility" => history_visibility
}, }
auth_events: Enum.map(auth_events, & &1.event_id)
} }
end end
@spec guest_access(Room.t(), Account.t(), String.t(), [t()]) :: t() @spec guest_access(Room.t(), Account.t(), String.t()) :: t()
def guest_access(room, sender, guest_access, auth_events) do def guest_access(room, sender, guest_access) do
%Event{ %Event{
new(room, sender) new(room, sender)
| type: "m.room.guest_access", | type: "m.room.guest_access",
state_key: "", state_key: "",
content: %{ content: %{
"guest_access" => guest_access "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 end

View file

@ -3,6 +3,11 @@ defmodule MatrixServer.JoinedRoom do
alias MatrixServer.{Account, Room} alias MatrixServer.{Account, Room}
@type t :: %__MODULE__{
localpart: String.t(),
room_id: String.t()
}
@primary_key false @primary_key false
schema "joined_rooms" do schema "joined_rooms" do
belongs_to :account, Account, belongs_to :account, Account,

View file

@ -20,9 +20,12 @@ defmodule MatrixServer.StateResolution.Authorization do
do: prev_events == [] do: prev_events == []
# Check rule: 5.2.1 # Check rule: 5.2.1
def authorized?(%Event{type: "m.room.member", state_key: state_key}, %{ def authorized?(
{"m.room.create", ""} => %Event{content: %{"creator" => creator}} %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 do: state_key == creator
def authorized?( def authorized?(
@ -309,6 +312,9 @@ defmodule MatrixServer.StateResolution.Authorization do
|> Repo.all() |> Repo.all()
|> Enum.reduce(%{}, &update_state_set/2) |> Enum.reduce(%{}, &update_state_set/2)
IO.inspect(event)
IO.inspect(state_set)
authorized?(event, state_set) authorized?(event, state_set)
end end
end end

View file

@ -7,6 +7,11 @@ defmodule MatrixServerWeb.Client.AccountController do
alias MatrixServer.{Account, Repo} alias MatrixServer.{Account, Repo}
alias Plug.Conn 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 def available(conn, params) do
localpart = Map.get(params, "username", "") localpart = Map.get(params, "username", "")
@ -21,6 +26,11 @@ defmodule MatrixServerWeb.Client.AccountController do
end end
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 def whoami(%Conn{assigns: %{account: %Account{localpart: localpart}}} = conn, _params) do
data = %{user_id: get_mxid(localpart)} data = %{user_id: get_mxid(localpart)}
@ -29,6 +39,11 @@ defmodule MatrixServerWeb.Client.AccountController do
|> json(data) |> json(data)
end 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 def logout(%Conn{assigns: %{device: device}} = conn, _params) do
case Repo.delete(device) do case Repo.delete(device) do
{:ok, _} -> {:ok, _} ->
@ -41,6 +56,12 @@ defmodule MatrixServerWeb.Client.AccountController do
end end
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 def logout_all(%Conn{assigns: %{account: account}} = conn, _params) do
Repo.delete_all(Ecto.assoc(account, :devices)) Repo.delete_all(Ecto.assoc(account, :devices))

View file

@ -5,6 +5,11 @@ defmodule MatrixServerWeb.Client.AliasesController do
alias MatrixServer.Alias 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 def create(conn, %{"alias" => alias, "room_id" => room_id}) do
case Alias.create(alias, room_id) do case Alias.create(alias, room_id) do
{:ok, _} -> {:ok, _} ->

View file

@ -5,6 +5,11 @@ defmodule MatrixServerWeb.Client.InfoController do
@supported_versions ["r0.6.1"] @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 def versions(conn, _params) do
data = %{versions: @supported_versions} data = %{versions: @supported_versions}

View file

@ -10,6 +10,11 @@ defmodule MatrixServerWeb.Client.LoginController do
@login_type "m.login.password" @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 def login_types(conn, _params) do
data = %{flows: [%{type: @login_type}]} data = %{flows: [%{type: @login_type}]}
@ -18,6 +23,12 @@ defmodule MatrixServerWeb.Client.LoginController do
|> json(data) |> json(data)
end 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( def login(
conn, conn,
%{"type" => @login_type, "identifier" => %{"type" => "m.id.user"}} = params %{"type" => @login_type, "identifier" => %{"type" => "m.id.user"}} = params

View file

@ -10,6 +10,11 @@ defmodule MatrixServerWeb.Client.RegisterController do
@register_type "m.login.dummy" @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 def register(conn, %{"auth" => %{"type" => @register_type}} = params) do
case Register.changeset(params) do case Register.changeset(params) do
%Changeset{valid?: true} = cs -> %Changeset{valid?: true} = cs ->

View file

@ -4,11 +4,17 @@ defmodule MatrixServerWeb.Client.RoomController do
import MatrixServerWeb.Error import MatrixServerWeb.Error
import Ecto.{Changeset, Query} import Ecto.{Changeset, Query}
alias MatrixServer.{Repo, Room} alias MatrixServer.{Repo, Room, RoomServer}
alias MatrixServer.Types.UserId
alias MatrixServerWeb.Client.Request.CreateRoom alias MatrixServerWeb.Client.Request.CreateRoom
alias Ecto.Changeset alias Ecto.Changeset
alias Plug.Conn 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 def create(%Conn{assigns: %{account: account}} = conn, params) do
case CreateRoom.changeset(params) do case CreateRoom.changeset(params) do
%Changeset{valid?: true} = cs -> %Changeset{valid?: true} = cs ->
@ -32,8 +38,14 @@ defmodule MatrixServerWeb.Client.RoomController do
end end
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 def joined_rooms(%Conn{assigns: %{account: account}} = conn, _params) do
joined_room_ids = account joined_room_ids =
account
|> Ecto.assoc(:joined_rooms) |> Ecto.assoc(:joined_rooms)
|> select([jr], jr.id) |> select([jr], jr.id)
|> Repo.all() |> Repo.all()
@ -46,4 +58,32 @@ defmodule MatrixServerWeb.Client.RoomController do
|> put_status(200) |> put_status(200)
|> json(data) |> json(data)
end 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 end

View file

@ -8,6 +8,11 @@ defmodule MatrixServerWeb.Federation.EventController do
alias MatrixServer.{Repo, Event, RoomServer} alias MatrixServer.{Repo, Event, RoomServer}
alias MatrixServerWeb.Federation.Transaction 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 def event(%Plug.Conn{assigns: %{origin: origin}} = conn, %{"event_id" => event_id}) do
query = query =
Event Event
@ -39,6 +44,11 @@ defmodule MatrixServerWeb.Federation.EventController do
def event(conn, _), do: put_error(conn, :missing_param) 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, %{ def state(%Plug.Conn{assigns: %{origin: origin}} = conn, %{
"event_id" => event_id, "event_id" => event_id,
"room_id" => room_id "room_id" => room_id
@ -48,6 +58,11 @@ defmodule MatrixServerWeb.Federation.EventController do
def state(conn, _), do: put_error(conn, :missing_param) 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, %{ def state_ids(%Plug.Conn{assigns: %{origin: origin}} = conn, %{
"event_id" => event_id, "event_id" => event_id,
"room_id" => room_id "room_id" => room_id

View file

@ -5,6 +5,11 @@ defmodule MatrixServerWeb.Federation.KeyController do
alias MatrixServer.KeyServer 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 def get_signing_keys(conn, _params) do
keys = keys =
KeyServer.get_own_signing_keys() KeyServer.get_own_signing_keys()

View file

@ -31,6 +31,12 @@ defmodule MatrixServerWeb.Federation.QueryController do
end end
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 def profile(conn, params) do
with {:ok, %ProfileRequest{user_id: %UserId{localpart: localpart, domain: domain}}} <- with {:ok, %ProfileRequest{user_id: %UserId{localpart: localpart, domain: domain}}} <-
ProfileRequest.validate(params) do ProfileRequest.validate(params) do

View file

@ -56,6 +56,10 @@ defmodule MatrixServerWeb.Router do
scope "/directory/room" do scope "/directory/room" do
put "/:alias", AliasesController, :create put "/:alias", AliasesController, :create
end end
scope "/rooms/:room_id" do
post "/invite", RoomController, :invite
end
end end
end end

View file

@ -12,99 +12,3 @@ Repo.insert(%Device{
display_name: "My Android", display_name: "My Android",
localpart: "chuck" 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")
)