Compare commits

..

10 commits

17 changed files with 793 additions and 334 deletions

View file

@ -50,7 +50,7 @@ Here, implemented and some unimplemented features are listed.
- GET /_matrix/client/r0/login - GET /_matrix/client/r0/login
- POST /_matrix/client/r0/login: Only with password flow. - POST /_matrix/client/r0/login: Only with password flow.
- POST /_matrix/client/r0/register: Only with dummy flow. - POST /_matrix/client/r0/register: Only with dummy flow.
- POST /_matrix/client/r0/createRoom: Only with optional parameters name, topic, preset and invite. - POST /_matrix/client/r0/createRoom: Except with option invite_3pid.
- GET /_matrix/client/r0/joined_rooms - GET /_matrix/client/r0/joined_rooms
- POST /_matrix/client/r0/rooms/{roomId}/invite - POST /_matrix/client/r0/rooms/{roomId}/invite
- POST /_matrix/client/r0/rooms/{roomId}/join: Except with third party invite. - POST /_matrix/client/r0/rooms/{roomId}/join: Except with third party invite.
@ -61,6 +61,8 @@ Here, implemented and some unimplemented features are listed.
- PUT /_matrix/client/r0/rooms/{roomId}/state/{eventType}/{stateKey} - PUT /_matrix/client/r0/rooms/{roomId}/state/{eventType}/{stateKey}
- PUT /_matrix/client/r0/rooms/{roomId}/send/{eventType}/{txnId} - PUT /_matrix/client/r0/rooms/{roomId}/send/{eventType}/{txnId}
- GET /_matrix/client/r0/rooms/{roomId}/messages: Except filtering. - GET /_matrix/client/r0/rooms/{roomId}/messages: Except filtering.
- GET /_matrix/client/r0/rooms/{roomId}/state
- GET /_matrix/client/r0/rooms/{roomId}/state/{eventType}/{stateKey}
- GET /_matrix/client/r0/directory/list/room/{roomId} - GET /_matrix/client/r0/directory/list/room/{roomId}
- PUT /_matrix/client/r0/directory/list/room/{roomId} - PUT /_matrix/client/r0/directory/list/room/{roomId}
- GET /_matrix/client/r0/capabilities - GET /_matrix/client/r0/capabilities
@ -76,7 +78,7 @@ Here, implemented and some unimplemented features are listed.
- GET /_matrix/federation/v1/state/{roomId} - GET /_matrix/federation/v1/state/{roomId}
- GET /_matrix/federation/v1/state_ids/{roomId} - GET /_matrix/federation/v1/state_ids/{roomId}
- GET /_matrix/key/v2/server/{keyId} - GET /_matrix/key/v2/server/{keyId}
- GET /_matrix/federation/v1/query/profile: Except displayname and avatar_url is not implemented. - GET /_matrix/federation/v1/query/profile
### Major unimplemented features ### Major unimplemented features

View file

@ -6,8 +6,6 @@ defmodule Architex.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
@ -21,9 +19,12 @@ defmodule Architex.RoomServer do
Account, Account,
Device, Device,
DeviceTransaction, DeviceTransaction,
Membership Membership,
Alias
} }
alias Architex.Types.{UserId, StateSet}
alias Architex.StateResolution.Authorization alias Architex.StateResolution.Authorization
alias ArchitexWeb.Client.Request.{CreateRoom, Kick, Ban} alias ArchitexWeb.Client.Request.{CreateRoom, Kick, Ban}
@ -46,8 +47,13 @@ defmodule Architex.RoomServer do
@spec get_room_server(String.t()) :: {:error, :not_found} | DynamicSupervisor.on_start_child() @spec get_room_server(String.t()) :: {:error, :not_found} | DynamicSupervisor.on_start_child()
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 query =
%Room{state: serialized_state_set} = room -> Room
|> where([r], r.id == ^room_id)
|> select([:id, :forward_extremities, :state_set, :visibility])
case Repo.one(query) do
%Room{} = room ->
case Registry.lookup(@registry, room_id) do case Registry.lookup(@registry, room_id) do
[{pid, _}] -> [{pid, _}] ->
{:ok, pid} {:ok, pid}
@ -55,8 +61,7 @@ defmodule Architex.RoomServer do
[] -> [] ->
opts = [ opts = [
name: {:via, Registry, {@registry, room_id}}, name: {:via, Registry, {@registry, room_id}},
room: room, room: room
serialized_state_set: serialized_state_set
] ]
DynamicSupervisor.start_child(@supervisor, {__MODULE__, opts}) DynamicSupervisor.start_child(@supervisor, {__MODULE__, opts})
@ -111,9 +116,10 @@ defmodule Architex.RoomServer do
@doc """ @doc """
Invite the a user to this room. Invite the a user to this room.
""" """
@spec invite(pid(), Account.t(), String.t()) :: :ok | {:error, atom()} @spec invite(pid(), Account.t(), String.t(), String.t() | nil, String.t() | nil) ::
def invite(pid, account, user_id) do :ok | {:error, atom()}
GenServer.call(pid, {:invite, account, user_id}) def invite(pid, account, user_id, avatar_url, displayname) do
GenServer.call(pid, {:invite, account, user_id, avatar_url, displayname})
end end
@doc """ @doc """
@ -135,25 +141,28 @@ defmodule Architex.RoomServer do
@doc """ @doc """
Kick a user from this room. Kick a user from this room.
""" """
@spec kick(pid(), Account.t(), Kick.t()) :: :ok | {:error, atom()} @spec kick(pid(), Account.t(), Kick.t(), String.t() | nil, String.t() | nil) ::
def kick(pid, account, request) do :ok | {:error, atom()}
GenServer.call(pid, {:kick, account, request}) def kick(pid, account, request, avatar_url, displayname) do
GenServer.call(pid, {:kick, account, request, avatar_url, displayname})
end end
@doc """ @doc """
Ban a user from this room. Ban a user from this room.
""" """
@spec ban(pid(), Account.t(), Ban.t()) :: :ok | {:error, atom()} @spec ban(pid(), Account.t(), Ban.t(), String.t() | nil, String.t() | nil) ::
def ban(pid, account, request) do :ok | {:error, atom()}
GenServer.call(pid, {:ban, account, request}) def ban(pid, account, request, avatar_url, displayname) do
GenServer.call(pid, {:ban, account, request, avatar_url, displayname})
end end
@doc """ @doc """
Unban a user from this room. Unban a user from this room.
""" """
@spec unban(pid(), Account.t(), String.t()) :: :ok | {:error, atom()} @spec unban(pid(), Account.t(), String.t(), String.t() | nil, String.t() | nil) ::
def unban(pid, account, user_id) do :ok | {:error, atom()}
GenServer.call(pid, {:unban, account, user_id}) def unban(pid, account, user_id, avatar_url, displayname) do
GenServer.call(pid, {:unban, account, user_id, avatar_url, displayname})
end end
@doc """ @doc """
@ -182,44 +191,64 @@ defmodule Architex.RoomServer do
GenServer.call(pid, {:send_state_event, account, event_type, content, state_key}) GenServer.call(pid, {:send_state_event, account, event_type, content, state_key})
end end
@doc """
Get the current state of a room.
If the requesting user is not a member of the room,
get the state when the user left the room.
If the user has never been in the room, return an error.
"""
@spec get_current_state(pid(), Account.t()) :: {:ok, [Event.t()]} | :error
def get_current_state(pid, account) do
GenServer.call(pid, {:get_current_state, account})
end
@spec get_state_event(pid(), Account.t(), String.t(), String.t()) ::
{:ok, map()} | {:error, :unauthorized} | {:error, :not_found}
def get_state_event(pid, account, event_type, state_key) do
GenServer.call(pid, {:get_state_event, account, event_type, state_key})
end
### Implementation ### Implementation
@impl true @impl true
def init(opts) do def init(opts) do
room = Keyword.fetch!(opts, :room) {:ok, %{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)
state_set =
Event
|> where([e], e.id in ^state_event_ids)
|> Repo.all()
|> Enum.into(%{}, fn %Event{type: type, state_key: state_key} = event ->
{{type, state_key}, event}
end)
{:ok, %{room: room, state_set: state_set}}
end end
@impl true @impl true
def handle_call( def handle_call(
{:create_room, account, request}, {:create_room, account, %CreateRoom{room_alias_name: room_alias_name} = request},
_from, _from,
%{room: %Room{id: room_id} = room} = state %{room: %Room{id: room_id} = room} = state
) do ) do
case Repo.transaction(create_room_insert_events(room, account, request)) do create_alias_result =
{:ok, {state_set, room}} -> if room_alias_name do
{:reply, {:ok, room_id}, %{state | state_set: state_set, room: room}} Alias.create(room_alias_name, room_id)
else
{:ok, nil}
end
{:error, reason} -> case create_alias_result do
{:reply, {:error, reason}, state} {:ok, alias_} ->
events = create_room_events(room, account, request, alias_)
_ -> case Repo.transaction(process_events(room, events)) do
{:reply, {:error, :unknown}, state} {:ok, room} ->
{:reply, {:ok, room_id}, %{state | room: room}}
{:error, reason} ->
{:reply, {:error, reason}, state}
_ ->
{:reply, {:error, :unknown}, state}
end
{:error, _} ->
{:reply, {:error, :alias}, state}
end end
end end
def handle_call({:server_in_room?, domain}, _from, %{state_set: state_set} = state) do def handle_call({:server_in_room?, domain}, _from, %{room: %Room{state_set: state_set}} = state) do
result = result =
Enum.any?(state_set, fn Enum.any?(state_set, fn
{{"m.room.member", user_id}, %Event{content: %{"membership" => "join"}}} -> {{"m.room.member", user_id}, %Event{content: %{"membership" => "join"}}} ->
@ -272,11 +301,15 @@ defmodule Architex.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 def handle_call(
invite_event = Event.Invite.new(room, account, user_id) {:invite, account, user_id, avatar_url, displayname},
_from,
%{room: room} = state
) do
invite_event = Event.Invite.new(room, account, user_id, avatar_url, displayname)
case Repo.transaction(insert_single_event(room, state_set, invite_event)) do case Repo.transaction(process_event(room, invite_event)) do
{:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} {:ok, {room, _}} -> {:reply, :ok, %{state | room: room}}
{:error, reason} -> {:reply, {:error, reason}, state} {:error, reason} -> {:reply, {:error, reason}, state}
end end
end end
@ -284,59 +317,64 @@ defmodule Architex.RoomServer do
def handle_call( def handle_call(
{:join, account}, {:join, account},
_from, _from,
%{room: %Room{id: room_id} = room, state_set: state_set} = state %{room: %Room{id: room_id} = room} = state
) do ) do
join_event = Event.Join.new(room, account) join_event = Event.Join.new(room, account)
case Repo.transaction(insert_single_event(room, state_set, join_event)) do case Repo.transaction(process_event(room, join_event)) do
{:ok, {state_set, room, _}} -> {:ok, {room, _}} ->
{:reply, {:ok, room_id}, %{state | state_set: state_set, room: room}} {:reply, {:ok, room_id}, %{state | room: room}}
{:error, reason} -> {:error, reason} ->
{:reply, {:error, reason}, state} {:reply, {:error, reason}, state}
end end
end end
def handle_call({:leave, account}, _from, %{room: room, state_set: state_set} = state) do def handle_call({:leave, account}, _from, %{room: room} = state) do
leave_event = Event.Leave.new(room, account) leave_event = Event.Leave.new(room, account)
case Repo.transaction(insert_single_event(room, state_set, leave_event)) do case Repo.transaction(process_event(room, leave_event)) do
{:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} {:ok, {room, _}} -> {:reply, :ok, %{state | room: room}}
{:error, reason} -> {:reply, {:error, reason}, state} {:error, reason} -> {:reply, {:error, reason}, state}
end end
end end
def handle_call( def handle_call(
{:kick, account, %Kick{user_id: user_id, reason: reason}}, {:kick, account, %Kick{user_id: user_id, reason: reason}, avatar_url, displayname},
_from, _from,
%{room: room, state_set: state_set} = state %{room: room} = state
) do ) do
kick_event = Event.Kick.new(room, account, user_id, reason) kick_event =
Event.Kick.new(room, account, to_string(user_id), avatar_url, displayname, reason)
case Repo.transaction(insert_single_event(room, state_set, kick_event)) do case Repo.transaction(process_event(room, kick_event)) do
{:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} {:ok, {room, _}} -> {:reply, :ok, %{state | room: room}}
{:error, reason} -> {:reply, {:error, reason}, state} {:error, reason} -> {:reply, {:error, reason}, state}
end end
end end
def handle_call( def handle_call(
{:ban, account, %Kick{user_id: user_id, reason: reason}}, {:ban, account, %Ban{user_id: user_id, reason: reason}, avatar_url, displayname},
_from, _from,
%{room: room, state_set: state_set} = state %{room: room} = state
) do ) do
ban_event = Event.Ban.new(room, account, user_id, reason) ban_event = Event.Ban.new(room, account, to_string(user_id), avatar_url, displayname, reason)
case Repo.transaction(insert_single_event(room, state_set, ban_event)) do case Repo.transaction(process_event(room, ban_event)) do
{:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} {:ok, {room, _}} -> {:reply, :ok, %{state | room: room}}
{:error, reason} -> {:reply, {:error, reason}, state} {:error, reason} -> {:reply, {:error, reason}, state}
end end
end end
def handle_call({:unban, account, user_id}, _from, %{room: room, state_set: state_set} = state) do def handle_call(
unban_event = Event.Unban.new(room, account, user_id) {:unban, account, user_id, avatar_url, displayname},
_from,
%{room: room} = state
) do
unban_event = Event.Unban.new(room, account, user_id, avatar_url, displayname)
case Repo.transaction(insert_single_event(room, state_set, unban_event)) do case Repo.transaction(process_event(room, unban_event)) do
{:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} {:ok, {room, _}} -> {:reply, :ok, %{state | room: room}}
{:error, reason} -> {:reply, {:error, reason}, state} {:error, reason} -> {:reply, {:error, reason}, state}
end end
end end
@ -344,7 +382,7 @@ defmodule Architex.RoomServer do
def handle_call( def handle_call(
{:set_visibility, account, visibility}, {:set_visibility, account, visibility},
_from, _from,
%{room: room, state_set: state_set} = state %{room: %Room{state_set: state_set} = room} = state
) do ) do
case state_set do case state_set do
%{{"m.room.create", ""} => %Event{content: %{"creator" => creator}}} -> %{{"m.room.create", ""} => %Event{content: %{"creator" => creator}}} ->
@ -364,13 +402,13 @@ defmodule Architex.RoomServer do
def handle_call( def handle_call(
{:send_message_event, account, device, event_type, content, txn_id}, {:send_message_event, account, device, event_type, content, txn_id},
_from, _from,
%{room: room, state_set: state_set} = state %{room: room} = state
) do ) do
message_event = Event.custom_event(room, account, event_type, content) message_event = Event.custom_event(room, account, event_type, content)
case Repo.transaction(insert_event_with_txn(state_set, room, device, message_event, txn_id)) do case Repo.transaction(process_event_with_txn(room, device, message_event, txn_id)) do
{:ok, {state_set, room, event_id}} -> {:ok, {room, event_id}} ->
{:reply, {:ok, event_id}, %{state | state_set: state_set, room: room}} {:reply, {:ok, event_id}, %{state | room: room}}
{:error, reason} -> {:error, reason} ->
{:reply, {:error, reason}, state} {:reply, {:error, reason}, state}
@ -380,23 +418,76 @@ defmodule Architex.RoomServer do
def handle_call( def handle_call(
{:send_state_event, account, event_type, content, state_key}, {:send_state_event, account, event_type, content, state_key},
_from, _from,
%{room: room, state_set: state_set} = state %{room: room} = state
) do ) do
state_event = Event.custom_state_event(room, account, event_type, content, state_key) state_event = Event.custom_state_event(room, account, event_type, content, state_key)
case Repo.transaction(insert_single_event(room, state_set, state_event)) do case Repo.transaction(process_event(room, state_event)) do
{:ok, {state_set, room, %Event{id: event_id}}} -> {:ok, {room, %Event{id: event_id}}} ->
{:reply, {:ok, event_id}, %{state | state_set: state_set, room: room}} {:reply, {:ok, event_id}, %{state | room: room}}
{:error, reason} -> {:error, reason} ->
{:reply, {:error, reason}, state} {:reply, {:error, reason}, state}
end end
end end
@spec insert_event_with_txn(t(), Room.t(), Device.t(), %Event{}, String.t()) :: def handle_call(
(() -> {t(), Room.t(), String.t()} | {:error, atom()}) {:get_current_state, account},
defp insert_event_with_txn( _from,
state_set, %{room: %Room{state_set: state_set}} = state
) do
mxid = Account.get_mxid(account)
case state_set[{"m.room.member", mxid}] do
%Event{content: %{"membership" => "join"}} ->
# Get the current state of the room.
{:reply, {:ok, Map.values(state_set)}, state}
%Event{content: %{"membership" => "leave"}} = event ->
# Get the state of the room, after leaving.
# TODO: This does not work properly, as a user's membership can change to "leave"
# even after they left/are banned.
# I think it is best to seperately keep track when a user left, maybe in the
# Membership table.
state_set = StateResolution.resolve(event)
{:reply, {:ok, Map.values(state_set)}, state}
_ ->
{:reply, :error, state}
end
end
def handle_call(
{:get_state_event, account, event_type, state_key},
_from,
%{room: %Room{state_set: state_set}} = state
) do
mxid = Account.get_mxid(account)
case state_set[{"m.room.member", mxid}] do
%Event{content: %{"membership" => "join"}} ->
case state_set[{event_type, state_key}] do
%Event{content: content} -> {:reply, {:ok, content}, state}
nil -> {:reply, {:error, :not_found}, state}
end
%Event{content: %{"membership" => "leave"}} = event ->
# TODO: See get_current_state.
state_set = StateResolution.resolve(event)
case state_set[{event_type, state_key}] do
%Event{content: content} -> {:reply, {:ok, content}, state}
nil -> {:reply, {:error, :not_found}, state}
end
_ ->
{:reply, {:error, :unauthorized}, state}
end
end
@spec process_event_with_txn(Room.t(), Device.t(), %Event{}, String.t()) ::
(() -> {Room.t(), String.t()} | {:error, atom()})
defp process_event_with_txn(
room, room,
%Device{nid: device_nid} = device, %Device{nid: device_nid} = device,
message_event, message_event,
@ -409,98 +500,107 @@ defmodule Architex.RoomServer do
where: dt.txn_id == ^txn_id and dt.device_nid == ^device_nid where: dt.txn_id == ^txn_id and dt.device_nid == ^device_nid
) do ) do
%DeviceTransaction{event_id: event_id} -> %DeviceTransaction{event_id: event_id} ->
{state_set, room, event_id} {room, event_id}
nil -> nil ->
with {state_set, room, %Event{id: event_id}} <- with {room, %Event{id: event_id}} <- process_event(room, message_event).() do
insert_single_event(room, state_set, message_event).() do
# Mark this transaction as done. # Mark this transaction as done.
Ecto.build_assoc(device, :device_transactions, txn_id: txn_id, event_id: event_id) Ecto.build_assoc(device, :device_transactions, txn_id: txn_id, event_id: event_id)
|> Repo.insert!() |> Repo.insert!()
{state_set, room, event_id} {room, event_id}
end end
end end
end end
end end
@spec insert_single_event(Room.t(), t(), %Event{}) :: @spec process_event(Room.t(), %Event{}) :: (() -> {Room.t(), Event.t()} | {:error, atom()})
(() -> {t(), Room.t(), Event.t()} | {:error, atom()}) defp process_event(room, event) do
defp insert_single_event(room, state_set, event) do
fn -> fn ->
case finalize_and_insert_event(event, state_set, room) do case finalize_and_process_event(event, room) do
{:ok, state_set, room, event} -> {:ok, room, event} -> {room, event}
_ = update_room_state_set(room, state_set) {:error, reason} -> Repo.rollback(reason)
{state_set, room, event}
{:error, reason} ->
Repo.rollback(reason)
end end
end end
end end
# Get a function that inserts all events for room creation. @spec process_events(Room.t(), [%Event{}]) :: (() -> Room.t() | {:error, atom()})
@spec create_room_insert_events(Room.t(), Account.t(), CreateRoom.t()) :: defp process_events(room, events) do
(() -> {:ok, t(), Room.t()} | {:error, atom()})
defp create_room_insert_events(room, account, %CreateRoom{
room_version: room_version,
preset: preset,
name: name,
topic: topic,
invite: invite,
power_level_content_override: power_level_content_override
}) do
events =
([
Event.CreateRoom.new(room, account, room_version),
Event.Join.new(room, account),
Event.PowerLevels.create_room_new(room, account, power_level_content_override)
] ++
room_creation_preset(account, preset, room) ++
[
if(name, do: Event.Name.new(room, account, name)),
if(topic, do: Event.Topic.new(room, account, topic))
] ++ room_creation_invite_events(account, invite, room))
|> Enum.reject(&Kernel.is_nil/1)
fn -> fn ->
result = Enum.reduce_while(events, room, fn event, room ->
Enum.reduce_while(events, {%{}, room}, fn event, {state_set, room} -> case finalize_and_process_event(event, room) do
case finalize_and_insert_event(event, state_set, room) do {:ok, room, _} -> {:cont, room}
{:ok, state_set, room, _} -> {:cont, {state_set, room}} {:error, reason} -> {:halt, {:error, reason}}
{:error, reason} -> {:halt, {:error, reason}} end
end end)
end) |> then(fn
{:error, reason} -> Repo.rollback(reason)
case result do room -> room
{:error, reason} -> end)
Repo.rollback(reason)
{state_set, room} ->
_ = update_room_state_set(room, state_set)
{state_set, room}
end
end end
end end
# Update the given room in the database with the given state set. @spec create_room_events(Room.t(), Account.t(), CreateRoom.t(), Alias.t() | nil) :: [%Event{}]
@spec update_room_state_set(Room.t(), t()) :: Room.t() defp create_room_events(
defp update_room_state_set(room, state_set) do room,
# TODO: We might as well hold state in the Room struct, account,
# instead of the state_set state. %CreateRoom{
# Create custom type for this. room_version: room_version,
serialized_state_set = preset: preset,
Enum.map(state_set, fn {{type, state_key}, %Event{id: event_id}} -> name: name,
[type, state_key, event_id] topic: topic,
invite: invite,
power_level_content_override: power_level_content_override,
is_direct: is_direct,
creation_content: creation_content,
initial_state: initial_state
},
alias_
) do
invite_events = room_creation_invite_events(account, invite, room, is_direct)
# Spec doesn't specify where to insert canonical_alias event, do it after topic event.
name_and_topic_events =
Enum.reject(
[
if(name, do: Event.Name.new(room, account, name)),
if(topic, do: Event.Topic.new(room, account, topic)),
if(alias_, do: Event.CanonicalAlias.new(room, account, alias_.alias))
],
&Kernel.is_nil/1
)
initial_state_pairs =
if initial_state, do: Enum.map(initial_state, &{&1.type, &1.state_key}), else: []
initial_state_events =
room_creation_initial_state_events(account, initial_state, room)
|> Enum.reject(fn %Event{type: type, state_key: state_key} ->
({type, state_key} == {"m.room.name", ""} and name) ||
({type, state_key} == {"m.room.topic", ""} and topic)
end) end)
Repo.update!(change(room, state: serialized_state_set)) preset_events =
room_creation_preset(account, preset, room)
|> Enum.reject(&({&1.type, &1.state_key} in initial_state_pairs))
basic_events = [
Event.CreateRoom.new(room, account, room_version, creation_content),
Event.Join.new(room, account),
Event.PowerLevels.create_room_new(
room,
account,
power_level_content_override,
invite,
preset
)
]
basic_events ++
preset_events ++ initial_state_events ++ name_and_topic_events ++ invite_events
end end
# Get the events for room creation as dictated by the given preset. # Get the events for room creation as dictated by the given preset.
# TODO: trusted_private_chat:
# All invitees are given the same power level as the room creator.
@spec room_creation_preset(Account.t(), String.t() | nil, Room.t()) :: [%Event{}] @spec room_creation_preset(Account.t(), String.t() | nil, Room.t()) :: [%Event{}]
defp room_creation_preset(account, nil, %Room{visibility: visibility} = room) do defp room_creation_preset(account, nil, %Room{visibility: visibility} = room) do
preset = preset =
@ -528,11 +628,24 @@ defmodule Architex.RoomServer do
end end
# Get the events for room creation for inviting other users. # Get the events for room creation for inviting other users.
@spec room_creation_invite_events(Account.t(), [String.t()] | nil, Room.t()) :: [%Event{}] @spec room_creation_invite_events(Account.t(), [UserId.t()] | nil, Room.t(), boolean() | nil) ::
defp room_creation_invite_events(_, nil, _), do: [] [%Event{}]
defp room_creation_invite_events(_, nil, _, _), do: []
defp room_creation_invite_events(account, invite_user_ids, room) do defp room_creation_invite_events(account, invite_user_ids, room, is_direct) do
Enum.map(invite_user_ids, &Event.Invite.new(room, account, &1)) Enum.map(invite_user_ids, fn user_id ->
{avatar_url, displayname} = UserId.try_get_user_information(user_id)
Event.Invite.new(room, account, to_string(user_id), avatar_url, displayname, is_direct)
end)
end
defp room_creation_initial_state_events(_, nil, _), do: []
defp room_creation_initial_state_events(account, initial_state, room) do
Enum.map(initial_state, fn %{type: type, content: content, state_key: state_key} ->
Event.custom_state_event(room, account, type, content, state_key)
end)
end end
# Finalize the event struct and insert it into the room's state using state resolution. # Finalize the event struct and insert it into the room's state using state resolution.
@ -542,12 +655,11 @@ defmodule Architex.RoomServer do
# - Content hash # - Content hash
# - Event ID # - Event ID
# - Signature # - Signature
@spec finalize_and_insert_event(%Event{}, t(), Room.t()) :: @spec finalize_and_process_event(%Event{}, Room.t()) ::
{:ok, t(), Room.t(), Event.t()} | {:error, atom()} {:ok, Room.t(), Event.t()} | {:error, atom()}
defp finalize_and_insert_event( defp finalize_and_process_event(
event, event,
state_set, %Room{forward_extremities: forward_extremities, state_set: state_set} = room
%Room{forward_extremities: forward_extremities} = room
) do ) do
event = event =
event event
@ -556,7 +668,7 @@ defmodule Architex.RoomServer do
|> Map.put(:depth, get_depth(forward_extremities)) |> Map.put(:depth, get_depth(forward_extremities))
case Event.post_process(event) do case Event.post_process(event) do
{:ok, event} -> authenticate_and_insert_event(event, state_set, room) {:ok, event} -> authenticate_and_process_event(event, room)
_ -> {:error, :event_creation} _ -> {:error, :event_creation}
end end
end end
@ -571,7 +683,7 @@ defmodule Architex.RoomServer do
end end
# Get the auth events for an events. # Get the auth events for an events.
@spec auth_events_for_event(%Event{}, t()) :: [Event.t()] @spec auth_events_for_event(%Event{}, StateSet.t()) :: [Event.t()]
defp auth_events_for_event(%Event{type: "m.room.create"}, _), do: [] defp auth_events_for_event(%Event{type: "m.room.create"}, _), do: []
defp auth_events_for_event( defp auth_events_for_event(
@ -623,9 +735,9 @@ defmodule Architex.RoomServer do
# Authenticate and insert a new event using state resolution. # Authenticate and insert a new event using state resolution.
# Implements the checks as described in the # Implements the checks as described in the
# [Matrix docs](https://matrix.org/docs/spec/server_server/latest#checks-performed-on-receipt-of-a-pdu). # [Matrix docs](https://matrix.org/docs/spec/server_server/latest#checks-performed-on-receipt-of-a-pdu).
@spec authenticate_and_insert_event(Event.t(), t(), Room.t()) :: @spec authenticate_and_process_event(Event.t(), Room.t()) ::
{:ok, t(), Room.t(), Event.t()} | {:error, atom()} {:ok, Room.t(), Event.t()} | {:error, atom()}
defp authenticate_and_insert_event(event, current_state_set, room) do defp authenticate_and_process_event(event, %Room{state_set: current_state_set} = room) do
# TODO: Correctly handle soft fails. # TODO: Correctly handle soft fails.
# 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.
@ -643,8 +755,9 @@ defmodule Architex.RoomServer do
event = Repo.insert!(event) event = Repo.insert!(event)
state_set = StateResolution.resolve_forward_extremities(event) state_set = StateResolution.resolve_forward_extremities(event)
:ok = update_membership(room, state_set) :ok = update_membership(room, state_set)
room = Repo.update!(change(room, state_set: state_set))
{:ok, state_set, room, event} {:ok, room, event}
else else
_ -> {:error, :authorization} _ -> {:error, :authorization}
end end
@ -655,7 +768,7 @@ defmodule Architex.RoomServer do
# could access rooms they are not allowed to. Then again, maybe we should perform # could access rooms they are not allowed to. Then again, maybe we should perform
# the "normal" authorization flow for local users as well, and treat the Membership # the "normal" authorization flow for local users as well, and treat the Membership
# table only as informational. # table only as informational.
@spec update_membership(Room.t(), t()) :: :ok @spec update_membership(Room.t(), StateSet.t()) :: :ok
defp update_membership(%Room{id: room_id}, state_set) do defp update_membership(%Room{id: room_id}, state_set) do
server_name = Architex.server_name() server_name = Architex.server_name()

View file

@ -6,13 +6,18 @@ defmodule Architex.Alias do
alias Architex.{Repo, Alias, Room} alias Architex.{Repo, Alias, Room}
alias Ecto.Changeset alias Ecto.Changeset
@type t :: %__MODULE__{
alias: String.t(),
room_id: String.t()
}
@primary_key {:alias, :string, []} @primary_key {:alias, :string, []}
schema "aliases" do schema "aliases" do
belongs_to :room, Room, foreign_key: :room_id, references: :id, type: :string belongs_to :room, Room, foreign_key: :room_id, references: :id, type: :string
end end
def create(alias, room_id) do def create(alias_, room_id) do
change(%Alias{}, alias: alias, room_id: room_id) change(%Alias{}, alias: alias_, room_id: room_id)
|> assoc_constraint(:room) |> assoc_constraint(:room)
|> unique_constraint(:alias, name: :aliases_pkey) |> unique_constraint(:alias, name: :aliases_pkey)
|> Repo.insert() |> Repo.insert()

View file

@ -314,4 +314,15 @@ defmodule Architex.Event do
{:ok, :crypto.hash(:sha256, json)} {:ok, :crypto.hash(:sha256, json)}
end end
end end
@spec default_membership_content(String.t() | nil, String.t() | nil) :: %{
optional(String.t()) => String.t()
}
def default_membership_content(avatar_url, displayname) do
content = %{}
content = if avatar_url, do: Map.put(content, "avatar_url", avatar_url), else: content
content = if displayname, do: Map.put(content, "displayname", displayname), else: content
content
end
end end

View file

@ -4,33 +4,10 @@ defmodule Architex.Event.Formatters do
""" """
alias Architex.Event alias Architex.Event
@spec messages_response(Event.t()) :: map() @doc """
def messages_response(%Event{ Event format with keys that all formats have in common.
content: content, """
type: type, def base_client_response(%Event{
id: event_id,
sender: sender,
origin_server_ts: origin_server_ts,
unsigned: unsigned,
room_id: room_id,
state_key: state_key
}) do
data = %{
content: content,
type: type,
event_id: event_id,
sender: to_string(sender),
origin_server_ts: origin_server_ts,
room_id: room_id
}
data = if unsigned, do: Map.put(data, :unsigned, unsigned), else: data
data = if state_key, do: Map.put(data, :state_key, state_key), else: data
data
end
def sync_response(%Event{
content: content, content: content,
type: type, type: type,
id: event_id, id: event_id,
@ -46,11 +23,37 @@ defmodule Architex.Event.Formatters do
origin_server_ts: origin_server_ts origin_server_ts: origin_server_ts
} }
data = if unsigned, do: Map.put(data, :unsigned, unsigned), else: data if unsigned, do: Map.put(data, :unsigned, unsigned), else: data
data
end end
@doc """
The base event format, with room_id and state_key added.
Used in the client /messages response.
"""
@spec messages_response(Event.t()) :: map()
def messages_response(%Event{room_id: room_id, state_key: state_key} = event) do
data =
base_client_response(event)
|> Map.put(:room_id, room_id)
if state_key, do: Map.put(data, :state_key, state_key), else: data
end
@doc """
The event format used in the client /state response.
TODO: prev_content
"""
def state_response(event), do: messages_response(event)
@doc """
The base event format, used in the client /sync response.
"""
@spec sync_response(Event.t()) :: map()
def sync_response(event), do: base_client_response(event)
@doc """
The PDU format used for federation.
"""
@spec as_pdu(Event.t()) :: map() @spec as_pdu(Event.t()) :: map()
def as_pdu(%Event{ def as_pdu(%Event{
auth_events: auth_events, auth_events: auth_events,

View file

@ -2,16 +2,21 @@ defmodule Architex.Event.Join do
alias Architex.{Event, Account, Room} alias Architex.{Event, Account, Room}
@spec new(Room.t(), Account.t()) :: %Event{} @spec new(Room.t(), Account.t()) :: %Event{}
def new(room, %Account{localpart: localpart} = sender) do def new(
room,
%Account{localpart: localpart, avatar_url: avatar_url, displayname: displayname} = sender
) do
mxid = Architex.get_mxid(localpart) mxid = Architex.get_mxid(localpart)
content =
Event.default_membership_content(avatar_url, displayname)
|> Map.put("membership", "join")
%Event{ %Event{
Event.new(room, sender) Event.new(room, sender)
| type: "m.room.member", | type: "m.room.member",
state_key: mxid, state_key: mxid,
content: %{ content: content
"membership" => "join"
}
} }
end end
end end
@ -19,24 +24,38 @@ end
defmodule Architex.Event.CreateRoom do defmodule Architex.Event.CreateRoom do
alias Architex.{Event, Account, Room} alias Architex.{Event, Account, Room}
@spec new(Room.t(), Account.t(), String.t()) :: %Event{} @clobber_content_keys ["creator", "room_version"]
def new(room, %Account{localpart: localpart} = creator, room_version) do
@spec new(Room.t(), Account.t(), String.t(), %{optional(String.t()) => any()} | nil) :: %Event{}
def new(room, %Account{localpart: localpart} = creator, room_version, creation_content) do
mxid = Architex.get_mxid(localpart) mxid = Architex.get_mxid(localpart)
content = %{
"creator" => mxid,
"room_version" => room_version || Architex.default_room_version()
}
content =
if creation_content do
creation_content
|> Map.drop(@clobber_content_keys)
|> Map.merge(content)
else
content
end
%Event{ %Event{
Event.new(room, creator) Event.new(room, creator)
| type: "m.room.create", | type: "m.room.create",
state_key: "", state_key: "",
content: %{ content: content
"creator" => mxid,
"room_version" => room_version || Architex.default_room_version()
}
} }
end end
end end
defmodule Architex.Event.PowerLevels do defmodule Architex.Event.PowerLevels do
alias Architex.{Event, Account, Room} alias Architex.{Event, Account, Room}
alias Architex.Types.UserId
alias ArchitexWeb.Client.Request.CreateRoom alias ArchitexWeb.Client.Request.CreateRoom
alias ArchitexWeb.Client.Request.CreateRoom.PowerLevelContentOverride alias ArchitexWeb.Client.Request.CreateRoom.PowerLevelContentOverride
@ -50,26 +69,49 @@ defmodule Architex.Event.PowerLevels do
@users_default 0 @users_default 0
@notifications_room 50 @notifications_room 50
@spec create_room_new(Room.t(), Account.t(), CreateRoom.plco_t()) :: %Event{} @spec create_room_new(
def create_room_new(room, sender, nil) do Room.t(),
create_room_new(room, sender, %PowerLevelContentOverride{}) Account.t(),
CreateRoom.PowerLevelContentOverride.t(),
[UserId.t()] | nil,
String.t() | nil
) :: %Event{}
def create_room_new(room, sender, nil, invite_ids, preset) do
create_room_new(room, sender, %PowerLevelContentOverride{}, invite_ids, preset)
end end
def create_room_new(room, %Account{localpart: localpart} = sender, %PowerLevelContentOverride{ def create_room_new(
ban: ban_override, room,
events: events_override, %Account{localpart: localpart} = sender,
events_default: events_default_override, %PowerLevelContentOverride{
invite: invite_override, ban: ban_override,
kick: kick_override, events: events_override,
redact: redact_override, events_default: events_default_override,
state_default: state_default_override, invite: invite_override,
users: users_override, kick: kick_override,
users_default: users_default_override, redact: redact_override,
notifications: notifications_override state_default: state_default_override,
}) do users: users_override,
users_default: users_default_override,
notifications: notifications_override
},
invite_ids,
preset
) do
mxid = Architex.get_mxid(localpart) mxid = Architex.get_mxid(localpart)
users = %{mxid => @creator} users = %{mxid => @creator}
users = if users_override, do: Map.merge(users, users_override), else: users users = if users_override, do: Map.merge(users, users_override), else: users
creator_pl = users[mxid]
# Give each invitee the same power level as the creator.
# This overrides the content override, but the spec is not clear on this.
users =
if preset == "trusted_private_chat" and invite_ids do
invite_users_pls = Enum.into(invite_ids, %{}, &{to_string(&1), creator_pl})
Map.merge(users, invite_users_pls)
else
users
end
notifications = notifications =
case notifications_override do case notifications_override do
@ -180,15 +222,26 @@ end
defmodule Architex.Event.Invite do defmodule Architex.Event.Invite do
alias Architex.{Event, Account, Room} alias Architex.{Event, Account, Room}
@spec new(Room.t(), Account.t(), String.t()) :: %Event{} @spec new(
def new(room, sender, user_id) do Room.t(),
Account.t(),
String.t(),
String.t() | nil,
String.t() | nil,
boolean() | nil
) :: %Event{}
def new(room, sender, user_id, avatar_url, displayname, is_direct \\ nil) do
content =
Event.default_membership_content(avatar_url, displayname)
|> Map.put("membership", "invite")
content = if is_direct != nil, do: Map.put(content, "is_direct", is_direct), else: content
%Event{ %Event{
Event.new(room, sender) Event.new(room, sender)
| type: "m.room.member", | type: "m.room.member",
state_key: user_id, state_key: user_id,
content: %{ content: content
"membership" => "invite"
}
} }
end end
end end
@ -197,14 +250,16 @@ defmodule Architex.Event.Leave do
alias Architex.{Event, Account, Room} alias Architex.{Event, Account, Room}
@spec new(Room.t(), Account.t()) :: %Event{} @spec new(Room.t(), Account.t()) :: %Event{}
def new(room, sender) do def new(room, %Account{avatar_url: avatar_url, displayname: displayname} = sender) do
content =
Event.default_membership_content(avatar_url, displayname)
|> Map.put("membership", "leave")
%Event{ %Event{
Event.new(room, sender) Event.new(room, sender)
| type: "m.room.member", | type: "m.room.member",
state_key: Account.get_mxid(sender), state_key: Account.get_mxid(sender),
content: %{ content: content
"membership" => "leave"
}
} }
end end
end end
@ -212,9 +267,19 @@ end
defmodule Architex.Event.Kick do defmodule Architex.Event.Kick do
alias Architex.{Event, Account, Room} alias Architex.{Event, Account, Room}
@spec new(Room.t(), Account.t(), String.t(), String.t() | nil) :: %Event{} @spec new(
def new(room, sender, user_id, reason \\ nil) do Room.t(),
content = %{"membership" => "leave"} Account.t(),
String.t(),
String.t() | nil,
String.t() | nil,
String.t() | nil
) :: %Event{}
def new(room, sender, user_id, avatar_url, displayname, reason \\ nil) do
content =
Event.default_membership_content(avatar_url, displayname)
|> Map.put("membership", "leave")
content = if reason, do: Map.put(content, "reason", reason), else: content content = if reason, do: Map.put(content, "reason", reason), else: content
%Event{ %Event{
@ -229,9 +294,19 @@ end
defmodule Architex.Event.Ban do defmodule Architex.Event.Ban do
alias Architex.{Event, Account, Room} alias Architex.{Event, Account, Room}
@spec new(Room.t(), Account.t(), String.t(), String.t() | nil) :: %Event{} @spec new(
def new(room, sender, user_id, reason \\ nil) do Room.t(),
content = %{"membership" => "ban"} Account.t(),
String.t(),
String.t() | nil,
String.t() | nil,
String.t() | nil
) :: %Event{}
def new(room, sender, user_id, avatar_url, displayname, reason \\ nil) do
content =
Event.default_membership_content(avatar_url, displayname)
|> Map.put("membership", "ban")
content = if reason, do: Map.put(content, "reason", reason), else: content content = if reason, do: Map.put(content, "reason", reason), else: content
%Event{ %Event{
@ -245,15 +320,36 @@ end
defmodule Architex.Event.Unban do defmodule Architex.Event.Unban do
alias Architex.{Event, Account, Room} alias Architex.{Event, Account, Room}
@spec new(Room.t(), Account.t(), String.t()) :: %Event{}
def new(room, sender, user_id) do @spec new(Room.t(), Account.t(), String.t(), String.t() | nil, String.t() | nil) :: %Event{}
def new(room, sender, avatar_url, displayname, user_id) do
content =
Event.default_membership_content(avatar_url, displayname)
|> Map.put("membership", "leave")
%Event{ %Event{
Event.new(room, sender) Event.new(room, sender)
| type: "m.room.member", | type: "m.room.member",
state_key: user_id, state_key: user_id,
content: %{ content: content
"membership" => "leave" }
} end
end
defmodule Architex.Event.CanonicalAlias do
alias Architex.{Event, Account, Room}
@spec new(Room.t(), Account.t(), String.t() | nil, [String.t()] | nil) :: %Event{}
def new(room, sender, alias_ \\ nil, alt_aliases \\ nil) do
content = %{}
content = if alias_, do: Map.put(content, "alias", alias_), else: content
content = if alt_aliases, do: Map.put(content, "alt_aliases", alt_aliases), else: content
%Event{
Event.new(room, sender)
| type: "m.room.canonical_alias",
state_key: "",
content: content
} }
end end
end end

View file

@ -5,18 +5,19 @@ defmodule Architex.Room do
import Ecto.Query import Ecto.Query
alias Architex.{Repo, Room, Event, Alias, RoomServer, Account} alias Architex.{Repo, Room, Event, Alias, RoomServer, Account}
alias Architex.Types.StateSet
alias ArchitexWeb.Client.Request.{CreateRoom, Messages} alias ArchitexWeb.Client.Request.{CreateRoom, Messages}
@type t :: %__MODULE__{ @type t :: %__MODULE__{
visibility: :public | :private, visibility: :public | :private,
state: list(list(String.t())), state_set: StateSet.t(),
forward_extremities: list(String.t()) forward_extremities: list(String.t())
} }
@primary_key {:id, :string, []} @primary_key {:id, :string, []}
schema "rooms" do schema "rooms" do
field :visibility, Ecto.Enum, values: [:public, :private] field :visibility, Ecto.Enum, values: [:public, :private]
field :state, {:array, {:array, :string}} field :state_set, StateSet, load_in_query: false
field :forward_extremities, {:array, :string} field :forward_extremities, {:array, :string}
has_many :events, Event, foreign_key: :room_id has_many :events, Event, foreign_key: :room_id
has_many :aliases, Alias, foreign_key: :room_id has_many :aliases, Alias, foreign_key: :room_id

View file

@ -0,0 +1,45 @@
defmodule Architex.Types.StateSet do
use Ecto.Type
import Ecto.Query
alias Architex.{Repo, Event}
@type t :: %{optional({String.t(), String.t()}) => Event.t()}
def type(), do: {:array, :string}
def cast(_), do: :error
def load(event_ids) when is_list(event_ids) do
events =
Event
|> where([e], e.id in ^event_ids)
|> Repo.all()
|> IO.inspect()
if length(events) == length(event_ids) do
state_set =
Enum.into(events, %{}, fn %Event{type: type, state_key: state_key} = event ->
{{type, state_key}, event}
end)
{:ok, state_set}
else
:error
end
end
def load(_), do: :error
def dump(state_set) when is_map(state_set) do
dumped =
Enum.map(state_set, fn {_, %Event{id: event_id}} ->
event_id
end)
{:ok, dumped}
end
def dump(_), do: :error
end

View file

@ -1,7 +1,11 @@
defmodule Architex.Types.UserId do defmodule Architex.Types.UserId do
use Ecto.Type use Ecto.Type
import Ecto.Query
alias Architex.{Repo, Account}
alias Architex.Types.UserId alias Architex.Types.UserId
alias ArchitexWeb.Federation.HTTPClient
@type t :: %__MODULE__{ @type t :: %__MODULE__{
localpart: String.t(), localpart: String.t(),
@ -53,4 +57,37 @@ defmodule Architex.Types.UserId do
def dump(%UserId{} = user_id), do: {:ok, to_string(user_id)} def dump(%UserId{} = user_id), do: {:ok, to_string(user_id)}
def dump(_), do: :error def dump(_), do: :error
# TODO: Seems out of place here.
@spec try_get_user_information(UserId.t()) :: {String.t() | nil, String.t() | nil}
def try_get_user_information(%UserId{localpart: localpart, domain: domain} = user_id) do
if domain == Architex.server_name() do
# Get information about a user on this homeserver.
query =
Account
|> where([a], a.localpart == ^localpart)
|> select([a], {a.avatar_url, a.displayname})
case Repo.one(query) do
nil -> {nil, nil}
info -> info
end
else
# Get information about a user on another homeserver.
client = HTTPClient.client(domain)
case HTTPClient.query_profile(client, to_string(user_id)) do
{:ok, response} ->
avatar_url = Map.get(response, "avatar_url")
avatar_url = if is_binary(avatar_url), do: avatar_url
displayname = Map.get(response, "displayname")
displayname = if is_binary(displayname), do: displayname
{avatar_url, displayname}
{:error, _, _} ->
{nil, nil}
end
end
end
end end

View file

@ -1,14 +1,16 @@
defmodule ArchitexWeb.Client.Request.Ban do defmodule ArchitexWeb.Client.Request.Ban do
use ArchitexWeb.APIRequest use ArchitexWeb.APIRequest
alias Architex.Types.UserId
@type t :: %__MODULE__{ @type t :: %__MODULE__{
user_id: String.t(), user_id: UserId.t(),
reason: String.t() | nil reason: String.t() | nil
} }
@primary_key false @primary_key false
embedded_schema do embedded_schema do
field :user_id, :string field :user_id, UserId
field :reason, :string field :reason, :string
end end

View file

@ -1,45 +1,43 @@
defmodule ArchitexWeb.Client.Request.CreateRoom do defmodule ArchitexWeb.Client.Request.CreateRoom do
use ArchitexWeb.APIRequest use ArchitexWeb.APIRequest
@type t :: %__MODULE__{ alias Architex.Types.UserId
visibility: String.t() | nil,
room_alias_name: String.t() | nil,
name: String.t() | nil,
topic: String.t() | nil,
invite: list(String.t()) | nil,
room_version: String.t() | nil,
preset: String.t() | nil,
power_level_content_override: plco_t() | nil
}
@type plco_t :: %__MODULE__.PowerLevelContentOverride{ defmodule PowerLevelContentOverride do
ban: integer() | nil, use Ecto.Schema
events: %{optional(String.t()) => integer()} | nil,
events_default: integer() | nil,
invite: integer() | nil,
kick: integer() | nil,
redact: integer() | nil,
state_default: integer() | nil,
users: %{optional(String.t()) => integer()} | nil,
users_default: integer() | nil,
notifications: plco_n_t() | nil
}
@type plco_n_t :: %__MODULE__.PowerLevelContentOverride.Notifications{ defmodule Notifications do
room: integer() | nil use Ecto.Schema
}
@primary_key false @type t :: %__MODULE__{
embedded_schema do room: integer() | nil
field :visibility, :string }
field :room_alias_name, :string
field :name, :string
field :topic, :string
field :invite, {:array, :string}
field :room_version, :string
field :preset, :string
embeds_one :power_level_content_override, PowerLevelContentOverride, primary_key: false do @primary_key false
embedded_schema do
field :room, :integer
end
def changeset(data, params) do
cast(data, params, [:room])
end
end
@type t :: %__MODULE__{
ban: integer() | nil,
events: %{optional(String.t()) => integer()} | nil,
events_default: integer() | nil,
invite: integer() | nil,
kick: integer() | nil,
redact: integer() | nil,
state_default: integer() | nil,
users: %{optional(String.t()) => integer()} | nil,
users_default: integer() | nil,
notifications: Notifications.t() | nil
}
@primary_key false
embedded_schema do
field :ban, :integer field :ban, :integer
field :events, {:map, :integer} field :events, {:map, :integer}
field :events_default, :integer field :events_default, :integer
@ -50,13 +48,81 @@ defmodule ArchitexWeb.Client.Request.CreateRoom do
field :users, {:map, :integer} field :users, {:map, :integer}
field :users_default, :integer field :users_default, :integer
embeds_one :notifications, Notifications, primary_key: false do embeds_one :notifications, Notifications
field :room, :integer
end
end end
# TODO: unimplemented: def changeset(data, params) do
# creation_content, initial_state, invite_3pid, initial_state, is_direct data
|> cast(params, [
:ban,
:events,
:events_default,
:invite,
:kick,
:redact,
:state_default,
:users,
:users_default
])
|> cast_embed(:notifications,
with: &Notifications.changeset/2,
required: false
)
end
end
defmodule StateEvent do
use Ecto.Schema
@type t :: %__MODULE__{
type: String.t(),
state_key: String.t(),
content: %{optional(String.t()) => any()}
}
@primary_key false
embedded_schema do
field :type, :string
field :state_key, :string, default: ""
field :content, :map
end
def changeset(data, params) do
data
|> cast(params, [:type, :state_key, :content])
|> validate_required([:type, :content])
end
end
@type t :: %__MODULE__{
visibility: String.t() | nil,
room_alias_name: String.t() | nil,
name: String.t() | nil,
topic: String.t() | nil,
invite: [UserId.t()] | nil,
room_version: String.t() | nil,
preset: String.t() | nil,
is_direct: boolean() | nil,
creation_content: %{optional(String.t()) => any()} | nil,
power_level_content_override: PowerLevelContentOverride.t() | nil,
initial_state: [StateEvent.t()] | nil
}
@primary_key false
embedded_schema do
# TODO: unimplemented: invite_3pid and room_alias_name
field :visibility, :string
field :room_alias_name, :string
field :name, :string
field :topic, :string
field :invite, {:array, UserId}
field :room_version, :string
field :preset, :string
field :is_direct, :boolean
field :creation_content, :map
embeds_many :initial_state, StateEvent
embeds_one :power_level_content_override, PowerLevelContentOverride
end end
def changeset(data, params) do def changeset(data, params) do
@ -68,35 +134,15 @@ defmodule ArchitexWeb.Client.Request.CreateRoom do
:topic, :topic,
:invite, :invite,
:room_version, :room_version,
:preset :preset,
:is_direct,
:creation_content
]) ])
|> cast_embed(:power_level_content_override, |> cast_embed(:power_level_content_override,
with: &power_level_content_override_changeset/2, with: &PowerLevelContentOverride.changeset/2,
required: false required: false
) )
|> cast_embed(:initial_state, with: &StateEvent.changeset/2, required: false)
|> validate_inclusion(:preset, ["private_chat", "public_chat", "trusted_private_chat"]) |> validate_inclusion(:preset, ["private_chat", "public_chat", "trusted_private_chat"])
end end
def power_level_content_override_changeset(data, params) do
data
|> cast(params, [
:ban,
:events,
:events_default,
:invite,
:kick,
:redact,
:state_default,
:users,
:users_default
])
|> cast_embed(:notifications,
with: &power_level_content_override_notifications_changeset/2,
required: false
)
end
def power_level_content_override_notifications_changeset(data, params) do
cast(data, params, [:room])
end
end end

View file

@ -1,14 +1,16 @@
defmodule ArchitexWeb.Client.Request.Kick do defmodule ArchitexWeb.Client.Request.Kick do
use ArchitexWeb.APIRequest use ArchitexWeb.APIRequest
alias Architex.Types.UserId
@type t :: %__MODULE__{ @type t :: %__MODULE__{
user_id: String.t(), user_id: UserId.t(),
reason: String.t() | nil reason: String.t() | nil
} }
@primary_key false @primary_key false
embedded_schema do embedded_schema do
field :user_id, :string field :user_id, UserId
field :reason, :string field :reason, :string
end end

View file

@ -25,6 +25,9 @@ defmodule ArchitexWeb.Client.RoomController do
{:error, :authorization} -> {:error, :authorization} ->
put_error(conn, :invalid_room_state) put_error(conn, :invalid_room_state)
{:error, :alias} ->
put_error(conn, :room_in_use, "The requested alias is already in use.")
{:error, :unknown} -> {:error, :unknown} ->
put_error(conn, :unknown) put_error(conn, :unknown)
end end
@ -60,9 +63,11 @@ defmodule ArchitexWeb.Client.RoomController do
"room_id" => room_id, "room_id" => room_id,
"user_id" => user_id "user_id" => user_id
}) do }) do
with {:ok, _} <- UserId.cast(user_id), with {:ok, user_id_struct} <- UserId.cast(user_id),
{:ok, pid} <- RoomServer.get_room_server(room_id) do {:ok, pid} <- RoomServer.get_room_server(room_id) do
case RoomServer.invite(pid, account, user_id) do {avatar_url, displayname} = UserId.try_get_user_information(user_id_struct)
case RoomServer.invite(pid, account, user_id, avatar_url, displayname) do
:ok -> :ok ->
conn conn
|> send_resp(200, []) |> send_resp(200, [])
@ -132,9 +137,11 @@ defmodule ArchitexWeb.Client.RoomController do
Action for POST /_matrix/client/r0/rooms/{roomId}/kick. Action for POST /_matrix/client/r0/rooms/{roomId}/kick.
""" """
def kick(%Conn{assigns: %{account: account}} = conn, %{"room_id" => room_id} = params) do def kick(%Conn{assigns: %{account: account}} = conn, %{"room_id" => room_id} = params) do
with {:ok, request} <- Kick.parse(params), with {:ok, %Kick{user_id: user_id} = request} <- Kick.parse(params),
{:ok, pid} <- RoomServer.get_room_server(room_id) do {:ok, pid} <- RoomServer.get_room_server(room_id) do
case RoomServer.kick(pid, account, request) do {avatar_url, displayname} = UserId.try_get_user_information(user_id)
case RoomServer.kick(pid, account, request, avatar_url, displayname) do
:ok -> :ok ->
conn conn
|> send_resp(200, []) |> send_resp(200, [])
@ -155,9 +162,11 @@ defmodule ArchitexWeb.Client.RoomController do
Action for POST /_matrix/client/r0/rooms/{roomId}/ban. Action for POST /_matrix/client/r0/rooms/{roomId}/ban.
""" """
def ban(%Conn{assigns: %{account: account}} = conn, %{"room_id" => room_id} = params) do def ban(%Conn{assigns: %{account: account}} = conn, %{"room_id" => room_id} = params) do
with {:ok, request} <- Ban.parse(params), with {:ok, %Ban{user_id: user_id} = request} <- Ban.parse(params),
{:ok, pid} <- RoomServer.get_room_server(room_id) do {:ok, pid} <- RoomServer.get_room_server(room_id) do
case RoomServer.ban(pid, account, request) do {avatar_url, displayname} = UserId.try_get_user_information(user_id)
case RoomServer.ban(pid, account, request, avatar_url, displayname) do
:ok -> :ok ->
conn conn
|> send_resp(200, []) |> send_resp(200, [])
@ -181,20 +190,22 @@ defmodule ArchitexWeb.Client.RoomController do
"room_id" => room_id, "room_id" => room_id,
"user_id" => user_id "user_id" => user_id
}) do }) do
case RoomServer.get_room_server(room_id) do with {:ok, user_id_struct} <- UserId.cast(user_id),
{:ok, pid} -> {:ok, pid} <- RoomServer.get_room_server(room_id) do
case RoomServer.unban(pid, account, user_id) do {avatar_url, displayname} = UserId.try_get_user_information(user_id_struct)
:ok ->
conn
|> send_resp(200, [])
|> halt()
{:error, _} -> case RoomServer.unban(pid, account, user_id, avatar_url, displayname) do
put_error(conn, :unknown) :ok ->
end conn
|> send_resp(200, [])
|> halt()
{:error, :not_found} -> {:error, _} ->
put_error(conn, :not_found, "The given room was not found.") 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
end end
@ -300,4 +311,75 @@ defmodule ArchitexWeb.Client.RoomController do
{:error, _} -> put_error(conn, :bad_json) {:error, _} -> put_error(conn, :bad_json)
end end
end end
@doc """
Get the state events for the current state of a room.
Action for GET /_matrix/client/r0/rooms/{roomId}/state.
"""
def get_state(%Conn{assigns: %{account: account}} = conn, %{"room_id" => room_id}) do
case RoomServer.get_room_server(room_id) do
{:ok, pid} ->
case RoomServer.get_current_state(pid, account) do
{:ok, events} ->
events = Enum.map(events, &Event.Formatters.state_response/1)
conn
|> put_status(200)
|> json(events)
:error ->
put_error(
conn,
:forbidden,
"You aren't a member of the room and weren't previously a member of the room."
)
end
{:error, :not_found} ->
put_error(conn, :not_found, "The given room was not found.")
end
end
@doc """
Looks up the contents of a state event in a room.
Action for GET /_matrix/client/r0/rooms/{roomId}/state/{eventType}/{stateKey}.
"""
def get_state_event(conn, %{"state_key" => [state_key | _]} = params) do
do_get_state_event(conn, params, state_key)
end
def get_state_event(conn, params) do
do_get_state_event(conn, params, "")
end
defp do_get_state_event(
%Conn{assigns: %{account: account}} = conn,
%{"room_id" => room_id, "event_type" => event_type},
state_key
) do
case RoomServer.get_room_server(room_id) do
{:ok, pid} ->
case RoomServer.get_state_event(pid, account, event_type, state_key) do
{:ok, content} ->
conn
|> put_status(200)
|> json(content)
{:error, :unauthorized} ->
put_error(
conn,
:forbidden,
"You aren't a member of the room and weren't previously a member of the room."
)
{:error, :not_found} ->
put_error(conn, :not_found, "The room has no state with the given type or key.")
end
{:error, :not_found} ->
put_error(conn, :not_found, "The given room was not found.")
end
end
end end

View file

@ -14,6 +14,7 @@ defmodule ArchitexWeb.Error do
unauthorized: {400, "M_UNAUTHORIZED", "The request was unauthorized."}, unauthorized: {400, "M_UNAUTHORIZED", "The request was unauthorized."},
invalid_param: {400, "M_INVALID_PARAM", "A request parameter was invalid."}, invalid_param: {400, "M_INVALID_PARAM", "A request parameter was invalid."},
missing_param: {400, "M_MISSING_PARAM", "A request parameter is missing."}, missing_param: {400, "M_MISSING_PARAM", "A request parameter is missing."},
room_in_use: {400, "M_ROOM_IN_USE", "The room is already in use."},
unknown_token: {401, "M_UNKNOWN_TOKEN", "Invalid access token."}, unknown_token: {401, "M_UNKNOWN_TOKEN", "Invalid access token."},
missing_token: {401, "M_MISSING_TOKEN", "Access token required."}, missing_token: {401, "M_MISSING_TOKEN", "Access token required."},
not_found: {404, "M_NOT_FOUND", "The requested resource was not found."}, not_found: {404, "M_NOT_FOUND", "The requested resource was not found."},

View file

@ -33,6 +33,8 @@ defmodule ArchitexWeb.Federation.HTTPClient do
""" """
@spec client(String.t()) :: Tesla.Client.t() @spec client(String.t()) :: Tesla.Client.t()
def client(server_name) do def client(server_name) do
# TODO: When implementing resolving homeservers, probably create
# a homeserver struct instead of using domain names directly.
Tesla.client( Tesla.client(
[ [
{Tesla.Middleware.Opts, [server_name: server_name]}, {Tesla.Middleware.Opts, [server_name: server_name]},

View file

@ -61,13 +61,16 @@ defmodule ArchitexWeb.Router do
scope "/r0" do scope "/r0" do
get "/account/whoami", AccountController, :whoami get "/account/whoami", AccountController, :whoami
post "/logout", AccountController, :logout
post "/logout/all", AccountController, :logout_all
post "/createRoom", RoomController, :create post "/createRoom", RoomController, :create
get "/joined_rooms", RoomController, :joined_rooms get "/joined_rooms", RoomController, :joined_rooms
get "/capabilities", InfoController, :capabilities get "/capabilities", InfoController, :capabilities
get "/sync", SyncController, :sync get "/sync", SyncController, :sync
scope "/logout" do
post "/", AccountController, :logout
post "/all", AccountController, :logout_all
end
scope "/profile/:user_id" do scope "/profile/:user_id" do
put "/avatar_url", ProfileController, :set_avatar_url put "/avatar_url", ProfileController, :set_avatar_url
put "/displayname", ProfileController, :set_displayname put "/displayname", ProfileController, :set_displayname
@ -87,7 +90,15 @@ defmodule ArchitexWeb.Router do
post "/unban", RoomController, :unban post "/unban", RoomController, :unban
put "/send/:event_type/:txn_id", RoomController, :send_message_event put "/send/:event_type/:txn_id", RoomController, :send_message_event
get "/messages", RoomController, :messages get "/messages", RoomController, :messages
put "/state/:event_type/*state_key", RoomController, :send_state_event
scope "/state" do
get "/", RoomController, :get_state
scope "/:event_type/*state_key" do
get "/", RoomController, :get_state_event
put "/", RoomController, :send_state_event
end
end
end end
end end
end end

View file

@ -14,7 +14,7 @@ defmodule Architex.Repo.Migrations.CreateInitialTables do
create table(:rooms, primary_key: false) do create table(:rooms, primary_key: false) do
add :id, :string, primary_key: true add :id, :string, primary_key: true
add :state, {:array, {:array, :string}}, default: [], null: false add :state_set, {:array, :string}, default: [], null: false
add :forward_extremities, {:array, :string}, default: [], null: false add :forward_extremities, {:array, :string}, default: [], null: false
add :visibility, :string, null: false, default: "public" add :visibility, :string, null: false, default: "public"
end end