Compare commits

..

No commits in common. "f94c156f950ddea675cf7d592974b82ff1b63ff1" and "8ab147190e1bc6de644a69bf2672d5da1b025288" have entirely different histories.

17 changed files with 334 additions and 793 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: Except with option invite_3pid. - POST /_matrix/client/r0/createRoom: Only with optional parameters name, topic, preset and invite.
- 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,8 +61,6 @@ 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
@ -78,7 +76,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 - GET /_matrix/federation/v1/query/profile: Except displayname and avatar_url is not implemented.
### Major unimplemented features ### Major unimplemented features

View file

@ -6,6 +6,8 @@ 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
@ -19,12 +21,9 @@ 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}
@ -47,13 +46,8 @@ 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.
query = case Repo.one(from r in Room, where: r.id == ^room_id) do
Room %Room{state: serialized_state_set} = 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}
@ -61,7 +55,8 @@ 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})
@ -116,10 +111,9 @@ 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(), String.t() | nil, String.t() | nil) :: @spec invite(pid(), Account.t(), String.t()) :: :ok | {:error, atom()}
:ok | {:error, atom()} def invite(pid, account, user_id) do
def invite(pid, account, user_id, avatar_url, displayname) do GenServer.call(pid, {:invite, account, user_id})
GenServer.call(pid, {:invite, account, user_id, avatar_url, displayname})
end end
@doc """ @doc """
@ -141,28 +135,25 @@ 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(), String.t() | nil, String.t() | nil) :: @spec kick(pid(), Account.t(), Kick.t()) :: :ok | {:error, atom()}
:ok | {:error, atom()} def kick(pid, account, request) do
def kick(pid, account, request, avatar_url, displayname) do GenServer.call(pid, {:kick, account, request})
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(), String.t() | nil, String.t() | nil) :: @spec ban(pid(), Account.t(), Ban.t()) :: :ok | {:error, atom()}
:ok | {:error, atom()} def ban(pid, account, request) do
def ban(pid, account, request, avatar_url, displayname) do GenServer.call(pid, {:ban, account, request})
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(), String.t() | nil, String.t() | nil) :: @spec unban(pid(), Account.t(), String.t()) :: :ok | {:error, atom()}
:ok | {:error, atom()} def unban(pid, account, user_id) do
def unban(pid, account, user_id, avatar_url, displayname) do GenServer.call(pid, {:unban, account, user_id})
GenServer.call(pid, {:unban, account, user_id, avatar_url, displayname})
end end
@doc """ @doc """
@ -191,64 +182,44 @@ 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
{:ok, %{room: Keyword.fetch!(opts, :room)}} 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, %CreateRoom{room_alias_name: room_alias_name} = request}, {:create_room, account, request},
_from, _from,
%{room: %Room{id: room_id} = room} = state %{room: %Room{id: room_id} = room} = state
) do ) do
create_alias_result = case Repo.transaction(create_room_insert_events(room, account, request)) do
if room_alias_name do {:ok, {state_set, room}} ->
Alias.create(room_alias_name, room_id) {:reply, {:ok, room_id}, %{state | state_set: state_set, room: room}}
else
{:ok, nil}
end
case create_alias_result do {:error, reason} ->
{:ok, alias_} -> {:reply, {:error, reason}, state}
events = create_room_events(room, account, request, alias_)
case Repo.transaction(process_events(room, events)) do _ ->
{:ok, room} -> {:reply, {:error, :unknown}, state}
{: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, %{room: %Room{state_set: state_set}} = state) do def handle_call({:server_in_room?, domain}, _from, %{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"}}} ->
@ -301,15 +272,11 @@ defmodule Architex.RoomServer do
{:reply, {state_events, auth_chain}, state} {:reply, {state_events, auth_chain}, state}
end end
def handle_call( def handle_call({:invite, account, user_id}, _from, %{room: room, state_set: state_set} = state) do
{:invite, account, user_id, avatar_url, displayname}, invite_event = Event.Invite.new(room, account, user_id)
_from,
%{room: room} = state
) do
invite_event = Event.Invite.new(room, account, user_id, avatar_url, displayname)
case Repo.transaction(process_event(room, invite_event)) do case Repo.transaction(insert_single_event(room, state_set, invite_event)) do
{:ok, {room, _}} -> {:reply, :ok, %{state | room: room}} {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}}
{:error, reason} -> {:reply, {:error, reason}, state} {:error, reason} -> {:reply, {:error, reason}, state}
end end
end end
@ -317,64 +284,59 @@ defmodule Architex.RoomServer do
def handle_call( def handle_call(
{:join, account}, {:join, account},
_from, _from,
%{room: %Room{id: room_id} = room} = state %{room: %Room{id: room_id} = room, state_set: state_set} = state
) do ) do
join_event = Event.Join.new(room, account) join_event = Event.Join.new(room, account)
case Repo.transaction(process_event(room, join_event)) do case Repo.transaction(insert_single_event(room, state_set, join_event)) do
{:ok, {room, _}} -> {:ok, {state_set, room, _}} ->
{:reply, {:ok, room_id}, %{state | room: room}} {:reply, {:ok, room_id}, %{state | state_set: state_set, 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) do def handle_call({:leave, account}, _from, %{room: room, state_set: state_set} = state) do
leave_event = Event.Leave.new(room, account) leave_event = Event.Leave.new(room, account)
case Repo.transaction(process_event(room, leave_event)) do case Repo.transaction(insert_single_event(room, state_set, leave_event)) do
{:ok, {room, _}} -> {:reply, :ok, %{state | room: room}} {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, 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}, avatar_url, displayname}, {:kick, account, %Kick{user_id: user_id, reason: reason}},
_from, _from,
%{room: room} = state %{room: room, state_set: state_set} = state
) do ) do
kick_event = kick_event = Event.Kick.new(room, account, user_id, reason)
Event.Kick.new(room, account, to_string(user_id), avatar_url, displayname, reason)
case Repo.transaction(process_event(room, kick_event)) do case Repo.transaction(insert_single_event(room, state_set, kick_event)) do
{:ok, {room, _}} -> {:reply, :ok, %{state | room: room}} {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, 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, %Ban{user_id: user_id, reason: reason}, avatar_url, displayname}, {:ban, account, %Kick{user_id: user_id, reason: reason}},
_from, _from,
%{room: room} = state %{room: room, state_set: state_set} = state
) do ) do
ban_event = Event.Ban.new(room, account, to_string(user_id), avatar_url, displayname, reason) ban_event = Event.Ban.new(room, account, user_id, reason)
case Repo.transaction(process_event(room, ban_event)) do case Repo.transaction(insert_single_event(room, state_set, ban_event)) do
{:ok, {room, _}} -> {:reply, :ok, %{state | room: room}} {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}}
{:error, reason} -> {:reply, {:error, reason}, state} {:error, reason} -> {:reply, {:error, reason}, state}
end end
end end
def handle_call( def handle_call({:unban, account, user_id}, _from, %{room: room, state_set: state_set} = state) do
{:unban, account, user_id, avatar_url, displayname}, unban_event = Event.Unban.new(room, account, user_id)
_from,
%{room: room} = state
) do
unban_event = Event.Unban.new(room, account, user_id, avatar_url, displayname)
case Repo.transaction(process_event(room, unban_event)) do case Repo.transaction(insert_single_event(room, state_set, unban_event)) do
{:ok, {room, _}} -> {:reply, :ok, %{state | room: room}} {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}}
{:error, reason} -> {:reply, {:error, reason}, state} {:error, reason} -> {:reply, {:error, reason}, state}
end end
end end
@ -382,7 +344,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} = room} = state %{room: room, state_set: state_set} = 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}}} ->
@ -402,13 +364,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 %{room: room, state_set: state_set} = 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(process_event_with_txn(room, device, message_event, txn_id)) do case Repo.transaction(insert_event_with_txn(state_set, room, device, message_event, txn_id)) do
{:ok, {room, event_id}} -> {:ok, {state_set, room, event_id}} ->
{:reply, {:ok, event_id}, %{state | room: room}} {:reply, {:ok, event_id}, %{state | state_set: state_set, room: room}}
{:error, reason} -> {:error, reason} ->
{:reply, {:error, reason}, state} {:reply, {:error, reason}, state}
@ -418,76 +380,23 @@ 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 %{room: room, state_set: state_set} = 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(process_event(room, state_event)) do case Repo.transaction(insert_single_event(room, state_set, state_event)) do
{:ok, {room, %Event{id: event_id}}} -> {:ok, {state_set, room, %Event{id: event_id}}} ->
{:reply, {:ok, event_id}, %{state | room: room}} {:reply, {:ok, event_id}, %{state | state_set: state_set, room: room}}
{:error, reason} -> {:error, reason} ->
{:reply, {:error, reason}, state} {:reply, {:error, reason}, state}
end end
end end
def handle_call( @spec insert_event_with_txn(t(), Room.t(), Device.t(), %Event{}, String.t()) ::
{:get_current_state, account}, (() -> {t(), Room.t(), String.t()} | {:error, atom()})
_from, defp insert_event_with_txn(
%{room: %Room{state_set: state_set}} = state state_set,
) 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,
@ -500,107 +409,98 @@ 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} ->
{room, event_id} {state_set, room, event_id}
nil -> nil ->
with {room, %Event{id: event_id}} <- process_event(room, message_event).() do with {state_set, room, %Event{id: event_id}} <-
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!()
{room, event_id} {state_set, room, event_id}
end end
end end
end end
end end
@spec process_event(Room.t(), %Event{}) :: (() -> {Room.t(), Event.t()} | {:error, atom()}) @spec insert_single_event(Room.t(), t(), %Event{}) ::
defp process_event(room, event) do (() -> {t(), Room.t(), Event.t()} | {:error, atom()})
defp insert_single_event(room, state_set, event) do
fn -> fn ->
case finalize_and_process_event(event, room) do case finalize_and_insert_event(event, state_set, room) do
{:ok, room, event} -> {room, event} {:ok, state_set, room, event} ->
{:error, reason} -> Repo.rollback(reason) _ = update_room_state_set(room, state_set)
{state_set, room, event}
{:error, reason} ->
Repo.rollback(reason)
end end
end end
end end
@spec process_events(Room.t(), [%Event{}]) :: (() -> Room.t() | {:error, atom()}) # Get a function that inserts all events for room creation.
defp process_events(room, events) do @spec create_room_insert_events(Room.t(), Account.t(), CreateRoom.t()) ::
(() -> {: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 ->
Enum.reduce_while(events, room, fn event, room -> result =
case finalize_and_process_event(event, room) do Enum.reduce_while(events, {%{}, room}, fn event, {state_set, room} ->
{:ok, room, _} -> {:cont, room} case finalize_and_insert_event(event, state_set, room) do
{:error, reason} -> {:halt, {:error, reason}} {:ok, state_set, room, _} -> {:cont, {state_set, room}}
end {:error, reason} -> {:halt, {:error, reason}}
end) end
|> then(fn end)
{:error, reason} -> Repo.rollback(reason)
room -> room case result do
end) {:error, reason} ->
Repo.rollback(reason)
{state_set, room} ->
_ = update_room_state_set(room, state_set)
{state_set, room}
end
end end
end end
@spec create_room_events(Room.t(), Account.t(), CreateRoom.t(), Alias.t() | nil) :: [%Event{}] # Update the given room in the database with the given state set.
defp create_room_events( @spec update_room_state_set(Room.t(), t()) :: Room.t()
room, defp update_room_state_set(room, state_set) do
account, # TODO: We might as well hold state in the Room struct,
%CreateRoom{ # instead of the state_set state.
room_version: room_version, # Create custom type for this.
preset: preset, serialized_state_set =
name: name, Enum.map(state_set, fn {{type, state_key}, %Event{id: event_id}} ->
topic: topic, [type, state_key, event_id]
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)
preset_events = Repo.update!(change(room, state: serialized_state_set))
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 =
@ -628,24 +528,11 @@ 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(), [UserId.t()] | nil, Room.t(), boolean() | nil) :: @spec room_creation_invite_events(Account.t(), [String.t()] | nil, Room.t()) :: [%Event{}]
[%Event{}] defp room_creation_invite_events(_, nil, _), do: []
defp room_creation_invite_events(_, nil, _, _), do: []
defp room_creation_invite_events(account, invite_user_ids, room, is_direct) do defp room_creation_invite_events(account, invite_user_ids, room) do
Enum.map(invite_user_ids, fn user_id -> Enum.map(invite_user_ids, &Event.Invite.new(room, account, &1))
{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.
@ -655,11 +542,12 @@ defmodule Architex.RoomServer do
# - Content hash # - Content hash
# - Event ID # - Event ID
# - Signature # - Signature
@spec finalize_and_process_event(%Event{}, Room.t()) :: @spec finalize_and_insert_event(%Event{}, t(), Room.t()) ::
{:ok, Room.t(), Event.t()} | {:error, atom()} {:ok, t(), Room.t(), Event.t()} | {:error, atom()}
defp finalize_and_process_event( defp finalize_and_insert_event(
event, event,
%Room{forward_extremities: forward_extremities, state_set: state_set} = room state_set,
%Room{forward_extremities: forward_extremities} = room
) do ) do
event = event =
event event
@ -668,7 +556,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_process_event(event, room) {:ok, event} -> authenticate_and_insert_event(event, state_set, room)
_ -> {:error, :event_creation} _ -> {:error, :event_creation}
end end
end end
@ -683,7 +571,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{}, StateSet.t()) :: [Event.t()] @spec auth_events_for_event(%Event{}, 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(
@ -735,9 +623,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_process_event(Event.t(), Room.t()) :: @spec authenticate_and_insert_event(Event.t(), t(), Room.t()) ::
{:ok, Room.t(), Event.t()} | {:error, atom()} {:ok, t(), Room.t(), Event.t()} | {:error, atom()}
defp authenticate_and_process_event(event, %Room{state_set: current_state_set} = room) do defp authenticate_and_insert_event(event, 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.
@ -755,9 +643,8 @@ 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, room, event} {:ok, state_set, room, event}
else else
_ -> {:error, :authorization} _ -> {:error, :authorization}
end end
@ -768,7 +655,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(), StateSet.t()) :: :ok @spec update_membership(Room.t(), 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,18 +6,13 @@ 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,15 +314,4 @@ 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,10 +4,33 @@ defmodule Architex.Event.Formatters do
""" """
alias Architex.Event alias Architex.Event
@doc """ @spec messages_response(Event.t()) :: map()
Event format with keys that all formats have in common. def messages_response(%Event{
""" content: content,
def base_client_response(%Event{ type: type,
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,
@ -23,37 +46,11 @@ defmodule Architex.Event.Formatters do
origin_server_ts: origin_server_ts origin_server_ts: origin_server_ts
} }
if unsigned, do: Map.put(data, :unsigned, unsigned), else: data 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,21 +2,16 @@ 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( def new(room, %Account{localpart: localpart} = sender) do
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
@ -24,38 +19,24 @@ end
defmodule Architex.Event.CreateRoom do defmodule Architex.Event.CreateRoom do
alias Architex.{Event, Account, Room} alias Architex.{Event, Account, Room}
@clobber_content_keys ["creator", "room_version"] @spec new(Room.t(), Account.t(), String.t()) :: %Event{}
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
@ -69,49 +50,26 @@ defmodule Architex.Event.PowerLevels do
@users_default 0 @users_default 0
@notifications_room 50 @notifications_room 50
@spec create_room_new( @spec create_room_new(Room.t(), Account.t(), CreateRoom.plco_t()) :: %Event{}
Room.t(), def create_room_new(room, sender, nil) do
Account.t(), create_room_new(room, sender, %PowerLevelContentOverride{})
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( def create_room_new(room, %Account{localpart: localpart} = sender, %PowerLevelContentOverride{
room, ban: ban_override,
%Account{localpart: localpart} = sender, events: events_override,
%PowerLevelContentOverride{ events_default: events_default_override,
ban: ban_override, invite: invite_override,
events: events_override, kick: kick_override,
events_default: events_default_override, redact: redact_override,
invite: invite_override, state_default: state_default_override,
kick: kick_override, users: users_override,
redact: redact_override, users_default: users_default_override,
state_default: state_default_override, notifications: notifications_override
users: users_override, }) do
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
@ -222,26 +180,15 @@ end
defmodule Architex.Event.Invite do defmodule Architex.Event.Invite do
alias Architex.{Event, Account, Room} alias Architex.{Event, Account, Room}
@spec new( @spec new(Room.t(), Account.t(), String.t()) :: %Event{}
Room.t(), def new(room, sender, user_id) do
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
@ -250,16 +197,14 @@ 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, %Account{avatar_url: avatar_url, displayname: displayname} = sender) do def new(room, 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
@ -267,19 +212,9 @@ end
defmodule Architex.Event.Kick do defmodule Architex.Event.Kick do
alias Architex.{Event, Account, Room} alias Architex.{Event, Account, Room}
@spec new( @spec new(Room.t(), Account.t(), String.t(), String.t() | nil) :: %Event{}
Room.t(), def new(room, sender, user_id, reason \\ nil) do
Account.t(), content = %{"membership" => "leave"}
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{
@ -294,19 +229,9 @@ end
defmodule Architex.Event.Ban do defmodule Architex.Event.Ban do
alias Architex.{Event, Account, Room} alias Architex.{Event, Account, Room}
@spec new( @spec new(Room.t(), Account.t(), String.t(), String.t() | nil) :: %Event{}
Room.t(), def new(room, sender, user_id, reason \\ nil) do
Account.t(), content = %{"membership" => "ban"}
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{
@ -320,36 +245,15 @@ 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{}
@spec new(Room.t(), Account.t(), String.t(), String.t() | nil, String.t() | nil) :: %Event{} def new(room, sender, user_id) do
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,19 +5,18 @@ 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_set: StateSet.t(), state: list(list(String.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_set, StateSet, load_in_query: false field :state, {:array, {:array, :string}}
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

@ -1,45 +0,0 @@
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,11 +1,7 @@
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(),
@ -57,37 +53,4 @@ 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,16 +1,14 @@
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: UserId.t(), user_id: String.t(),
reason: String.t() | nil reason: String.t() | nil
} }
@primary_key false @primary_key false
embedded_schema do embedded_schema do
field :user_id, UserId field :user_id, :string
field :reason, :string field :reason, :string
end end

View file

@ -1,43 +1,45 @@
defmodule ArchitexWeb.Client.Request.CreateRoom do defmodule ArchitexWeb.Client.Request.CreateRoom do
use ArchitexWeb.APIRequest use ArchitexWeb.APIRequest
alias Architex.Types.UserId @type t :: %__MODULE__{
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
}
defmodule PowerLevelContentOverride do @type plco_t :: %__MODULE__.PowerLevelContentOverride{
use Ecto.Schema 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: plco_n_t() | nil
}
defmodule Notifications do @type plco_n_t :: %__MODULE__.PowerLevelContentOverride.Notifications{
use Ecto.Schema room: integer() | nil
}
@type t :: %__MODULE__{ @primary_key false
room: integer() | nil embedded_schema do
} 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
@primary_key false embeds_one :power_level_content_override, PowerLevelContentOverride, primary_key: false do
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
@ -48,81 +50,13 @@ 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 embeds_one :notifications, Notifications, primary_key: false do
field :room, :integer
end
end end
def changeset(data, params) do # TODO: unimplemented:
data # creation_content, initial_state, invite_3pid, initial_state, is_direct
|> 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
@ -134,15 +68,35 @@ 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: &PowerLevelContentOverride.changeset/2, with: &power_level_content_override_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,16 +1,14 @@
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: UserId.t(), user_id: String.t(),
reason: String.t() | nil reason: String.t() | nil
} }
@primary_key false @primary_key false
embedded_schema do embedded_schema do
field :user_id, UserId field :user_id, :string
field :reason, :string field :reason, :string
end end

View file

@ -25,9 +25,6 @@ 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
@ -63,11 +60,9 @@ 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, user_id_struct} <- UserId.cast(user_id), with {:ok, _} <- UserId.cast(user_id),
{:ok, pid} <- RoomServer.get_room_server(room_id) do {:ok, pid} <- RoomServer.get_room_server(room_id) do
{avatar_url, displayname} = UserId.try_get_user_information(user_id_struct) case RoomServer.invite(pid, account, user_id) do
case RoomServer.invite(pid, account, user_id, avatar_url, displayname) do
:ok -> :ok ->
conn conn
|> send_resp(200, []) |> send_resp(200, [])
@ -137,11 +132,9 @@ 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, %Kick{user_id: user_id} = request} <- Kick.parse(params), with {:ok, request} <- Kick.parse(params),
{:ok, pid} <- RoomServer.get_room_server(room_id) do {:ok, pid} <- RoomServer.get_room_server(room_id) do
{avatar_url, displayname} = UserId.try_get_user_information(user_id) case RoomServer.kick(pid, account, request) do
case RoomServer.kick(pid, account, request, avatar_url, displayname) do
:ok -> :ok ->
conn conn
|> send_resp(200, []) |> send_resp(200, [])
@ -162,11 +155,9 @@ 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, %Ban{user_id: user_id} = request} <- Ban.parse(params), with {:ok, request} <- Ban.parse(params),
{:ok, pid} <- RoomServer.get_room_server(room_id) do {:ok, pid} <- RoomServer.get_room_server(room_id) do
{avatar_url, displayname} = UserId.try_get_user_information(user_id) case RoomServer.ban(pid, account, request) do
case RoomServer.ban(pid, account, request, avatar_url, displayname) do
:ok -> :ok ->
conn conn
|> send_resp(200, []) |> send_resp(200, [])
@ -190,22 +181,20 @@ 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, user_id_struct} <- UserId.cast(user_id), case RoomServer.get_room_server(room_id) do
{:ok, pid} <- RoomServer.get_room_server(room_id) do {:ok, pid} ->
{avatar_url, displayname} = UserId.try_get_user_information(user_id_struct) case RoomServer.unban(pid, account, user_id) do
:ok ->
conn
|> send_resp(200, [])
|> halt()
case RoomServer.unban(pid, account, user_id, avatar_url, displayname) do {:error, _} ->
:ok -> put_error(conn, :unknown)
conn end
|> send_resp(200, [])
|> halt()
{:error, _} -> {:error, :not_found} ->
put_error(conn, :unknown) put_error(conn, :not_found, "The given room was not found.")
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
@ -311,75 +300,4 @@ 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,7 +14,6 @@ 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,8 +33,6 @@ 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,16 +61,13 @@ 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
@ -90,15 +87,7 @@ 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_set, {:array, :string}, default: [], null: false add :state, {:array, {: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