Implement room_alias_name option for client room creation

This commit is contained in:
Pim Kunis 2021-09-12 13:33:06 +02:00
parent b34fd58cf7
commit 064a398a37
6 changed files with 118 additions and 68 deletions

View file

@ -50,7 +50,7 @@ Here, implemented and some unimplemented features are listed.
- GET /_matrix/client/r0/login
- POST /_matrix/client/r0/login: Only with password flow.
- POST /_matrix/client/r0/register: Only with dummy flow.
- POST /_matrix/client/r0/createRoom: Except with options invite_3pid, initial_state and room_alias_name.
- POST /_matrix/client/r0/createRoom: Except with option invite_3pid.
- GET /_matrix/client/r0/joined_rooms
- POST /_matrix/client/r0/rooms/{roomId}/invite
- POST /_matrix/client/r0/rooms/{roomId}/join: Except with third party invite.

View file

@ -21,7 +21,8 @@ defmodule Architex.RoomServer do
Account,
Device,
DeviceTransaction,
Membership
Membership,
Alias
}
alias Architex.StateResolution.Authorization
@ -203,11 +204,22 @@ defmodule Architex.RoomServer do
@impl true
def handle_call(
{:create_room, account, request},
{:create_room, account, %CreateRoom{room_alias_name: room_alias_name} = request},
_from,
%{room: %Room{id: room_id} = room} = state
) do
case Repo.transaction(create_room_insert_events(room, account, request)) do
create_alias_result =
if room_alias_name do
Alias.create(room_alias_name, room_id)
else
{:ok, nil}
end
case create_alias_result do
{:ok, alias_} ->
events = create_room_events(room, account, request, alias_)
case Repo.transaction(process_events(room, %{}, events)) do
{:ok, {state_set, room}} ->
{:reply, {:ok, room_id}, %{state | state_set: state_set, room: room}}
@ -217,6 +229,10 @@ defmodule Architex.RoomServer do
_ ->
{:reply, {:error, :unknown}, state}
end
{:error, _} ->
{:reply, {:error, :alias}, state}
end
end
def handle_call({:server_in_room?, domain}, _from, %{state_set: state_set} = state) do
@ -275,7 +291,7 @@ defmodule Architex.RoomServer do
def handle_call({:invite, account, user_id}, _from, %{room: room, state_set: state_set} = state) do
invite_event = Event.Invite.new(room, account, user_id)
case Repo.transaction(insert_single_event(room, state_set, invite_event)) do
case Repo.transaction(process_event(room, state_set, invite_event)) do
{:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}}
{:error, reason} -> {:reply, {:error, reason}, state}
end
@ -288,7 +304,7 @@ defmodule Architex.RoomServer do
) do
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, state_set, join_event)) do
{:ok, {state_set, room, _}} ->
{:reply, {:ok, room_id}, %{state | state_set: state_set, room: room}}
@ -300,7 +316,7 @@ defmodule Architex.RoomServer do
def handle_call({:leave, account}, _from, %{room: room, state_set: state_set} = state) do
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, state_set, leave_event)) do
{:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}}
{:error, reason} -> {:reply, {:error, reason}, state}
end
@ -313,7 +329,7 @@ defmodule Architex.RoomServer do
) do
kick_event = Event.Kick.new(room, account, user_id, reason)
case Repo.transaction(insert_single_event(room, state_set, kick_event)) do
case Repo.transaction(process_event(room, state_set, kick_event)) do
{:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}}
{:error, reason} -> {:reply, {:error, reason}, state}
end
@ -326,7 +342,7 @@ defmodule Architex.RoomServer do
) do
ban_event = Event.Ban.new(room, account, user_id, reason)
case Repo.transaction(insert_single_event(room, state_set, ban_event)) do
case Repo.transaction(process_event(room, state_set, ban_event)) do
{:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}}
{:error, reason} -> {:reply, {:error, reason}, state}
end
@ -335,7 +351,7 @@ defmodule Architex.RoomServer do
def handle_call({:unban, account, user_id}, _from, %{room: room, state_set: state_set} = state) do
unban_event = Event.Unban.new(room, account, user_id)
case Repo.transaction(insert_single_event(room, state_set, unban_event)) do
case Repo.transaction(process_event(room, state_set, unban_event)) do
{:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}}
{:error, reason} -> {:reply, {:error, reason}, state}
end
@ -368,7 +384,7 @@ defmodule Architex.RoomServer do
) do
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(state_set, room, device, message_event, txn_id)) do
{:ok, {state_set, room, event_id}} ->
{:reply, {:ok, event_id}, %{state | state_set: state_set, room: room}}
@ -384,7 +400,7 @@ defmodule Architex.RoomServer do
) do
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_set, state_event)) do
{:ok, {state_set, room, %Event{id: event_id}}} ->
{:reply, {:ok, event_id}, %{state | state_set: state_set, room: room}}
@ -393,9 +409,9 @@ defmodule Architex.RoomServer do
end
end
@spec insert_event_with_txn(t(), Room.t(), Device.t(), %Event{}, String.t()) ::
@spec process_event_with_txn(t(), Room.t(), Device.t(), %Event{}, String.t()) ::
(() -> {t(), Room.t(), String.t()} | {:error, atom()})
defp insert_event_with_txn(
defp process_event_with_txn(
state_set,
room,
%Device{nid: device_nid} = device,
@ -413,7 +429,7 @@ defmodule Architex.RoomServer do
nil ->
with {state_set, room, %Event{id: event_id}} <-
insert_single_event(room, state_set, message_event).() do
process_event(room, state_set, message_event).() do
# Mark this transaction as done.
Ecto.build_assoc(device, :device_transactions, txn_id: txn_id, event_id: event_id)
|> Repo.insert!()
@ -424,11 +440,11 @@ defmodule Architex.RoomServer do
end
end
@spec insert_single_event(Room.t(), t(), %Event{}) ::
@spec process_event(Room.t(), t(), %Event{}) ::
(() -> {t(), Room.t(), Event.t()} | {:error, atom()})
defp insert_single_event(room, state_set, event) do
defp process_event(room, state_set, event) do
fn ->
case finalize_and_insert_event(event, state_set, room) do
case finalize_and_process_event(event, state_set, room) do
{:ok, state_set, room, event} ->
_ = update_room_state_set(room, state_set)
{state_set, room, event}
@ -439,10 +455,32 @@ defmodule Architex.RoomServer do
end
end
# Get a function that inserts all events for room creation.
@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{
@spec process_events(Room.t(), t(), [%Event{}]) ::
(() -> {t(), Room.t()} | {:error, atom()})
defp process_events(room, state_set, events) do
fn ->
Enum.reduce_while(events, {state_set, room}, fn event, {state_set, room} ->
case finalize_and_process_event(event, state_set, room) do
{:ok, state_set, room, _} -> {:cont, {state_set, room}}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
|> then(fn
{:error, reason} ->
Repo.rollback(reason)
{state_set, room} ->
_ = update_room_state_set(room, state_set)
{state_set, room}
end)
end
end
@spec create_room_events(Room.t(), Account.t(), CreateRoom.t(), Alias.t() | nil) :: [%Event{}]
defp create_room_events(
room,
account,
%CreateRoom{
room_version: room_version,
preset: preset,
name: name,
@ -452,14 +490,18 @@ defmodule Architex.RoomServer do
is_direct: is_direct,
creation_content: creation_content,
initial_state: initial_state
}) do
},
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(topic, do: Event.Topic.new(room, account, topic)),
if(alias_, do: Event.CanonicalAlias.new(room, account, alias_.alias))
],
&Kernel.is_nil/1
)
@ -490,28 +532,8 @@ defmodule Architex.RoomServer do
)
]
events =
basic_events ++
preset_events ++ initial_state_events ++ name_and_topic_events ++ invite_events
fn ->
result =
Enum.reduce_while(events, {%{}, room}, fn event, {state_set, room} ->
case finalize_and_insert_event(event, state_set, room) do
{:ok, state_set, room, _} -> {:cont, {state_set, room}}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
case result do
{:error, reason} ->
Repo.rollback(reason)
{state_set, room} ->
_ = update_room_state_set(room, state_set)
{state_set, room}
end
end
end
# Update the given room in the database with the given state set.
@ -579,9 +601,9 @@ defmodule Architex.RoomServer do
# - Content hash
# - Event ID
# - Signature
@spec finalize_and_insert_event(%Event{}, t(), Room.t()) ::
@spec finalize_and_process_event(%Event{}, t(), Room.t()) ::
{:ok, t(), Room.t(), Event.t()} | {:error, atom()}
defp finalize_and_insert_event(
defp finalize_and_process_event(
event,
state_set,
%Room{forward_extremities: forward_extremities} = room
@ -593,7 +615,7 @@ defmodule Architex.RoomServer do
|> Map.put(:depth, get_depth(forward_extremities))
case Event.post_process(event) do
{:ok, event} -> authenticate_and_insert_event(event, state_set, room)
{:ok, event} -> authenticate_and_process_event(event, state_set, room)
_ -> {:error, :event_creation}
end
end
@ -660,9 +682,9 @@ defmodule Architex.RoomServer do
# Authenticate and insert a new event using state resolution.
# Implements the checks as described in the
# [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(), t(), Room.t()) ::
{:ok, t(), Room.t(), Event.t()} | {:error, atom()}
defp authenticate_and_insert_event(event, current_state_set, room) do
defp authenticate_and_process_event(event, current_state_set, room) do
# TODO: Correctly handle soft fails.
# Check the following things:
# 1. TODO: Is a valid event, otherwise it is dropped.

View file

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

View file

@ -282,6 +282,7 @@ end
defmodule Architex.Event.Unban do
alias Architex.{Event, Account, Room}
@spec new(Room.t(), Account.t(), String.t()) :: %Event{}
def new(room, sender, user_id) do
%Event{
@ -294,3 +295,21 @@ defmodule Architex.Event.Unban do
}
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

View file

@ -25,6 +25,9 @@ defmodule ArchitexWeb.Client.RoomController do
{:error, :authorization} ->
put_error(conn, :invalid_room_state)
{:error, :alias} ->
put_error(conn, :room_in_use, "The requested alias is already in use.")
{:error, :unknown} ->
put_error(conn, :unknown)
end

View file

@ -14,6 +14,7 @@ defmodule ArchitexWeb.Error do
unauthorized: {400, "M_UNAUTHORIZED", "The request was unauthorized."},
invalid_param: {400, "M_INVALID_PARAM", "A request parameter was invalid."},
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."},
missing_token: {401, "M_MISSING_TOKEN", "Access token required."},
not_found: {404, "M_NOT_FOUND", "The requested resource was not found."},