Implement client invite endpoint
Refactor room server to automatically determine auth events Add documentation in various places
This commit is contained in:
parent
6f8c224d50
commit
bcc6cbb24b
15 changed files with 345 additions and 209 deletions
|
@ -6,12 +6,15 @@ defmodule MatrixServer.RoomServer do
|
|||
The RoomServers are supervised by a DynamicSupervisor RoomServer.Supervisor.
|
||||
"""
|
||||
|
||||
@typep t :: map()
|
||||
|
||||
use GenServer
|
||||
|
||||
import Ecto.Query
|
||||
import Ecto.Changeset
|
||||
|
||||
alias MatrixServer.{Repo, Room, Event, StateResolution, JoinedRoom, Account}
|
||||
alias MatrixServer.{Repo, Room, Event, StateResolution, Account, JoinedRoom}
|
||||
alias MatrixServer.Types.UserId
|
||||
alias MatrixServer.StateResolution.Authorization
|
||||
alias MatrixServerWeb.Client.Request.CreateRoom
|
||||
|
||||
|
@ -35,7 +38,7 @@ defmodule MatrixServer.RoomServer do
|
|||
def get_room_server(room_id) do
|
||||
# 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
|
||||
%Room{state: serialized_state_set} ->
|
||||
%Room{state: serialized_state_set} = room ->
|
||||
case Registry.lookup(@registry, room_id) do
|
||||
[{pid, _}] ->
|
||||
{:ok, pid}
|
||||
|
@ -43,7 +46,7 @@ defmodule MatrixServer.RoomServer do
|
|||
[] ->
|
||||
opts = [
|
||||
name: {:via, Registry, {@registry, room_id}},
|
||||
room_id: room_id,
|
||||
room: room,
|
||||
serialized_state_set: serialized_state_set
|
||||
]
|
||||
|
||||
|
@ -62,11 +65,7 @@ defmodule MatrixServer.RoomServer do
|
|||
Events are inserted into the new room depending on the input `input` and according
|
||||
to the [Matrix documentation](https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-createroom).
|
||||
"""
|
||||
@spec create_room(
|
||||
pid(),
|
||||
MatrixServer.Account.t(),
|
||||
MatrixServerWeb.Client.Request.CreateRoom.t()
|
||||
) :: {:ok, String.t()} | {:error, atom()}
|
||||
@spec create_room(pid(), Account.t(), CreateRoom.t()) :: {:ok, String.t()} | {:error, atom()}
|
||||
def create_room(pid, account, input) do
|
||||
GenServer.call(pid, {:create_room, account, input})
|
||||
end
|
||||
|
@ -100,11 +99,19 @@ defmodule MatrixServer.RoomServer do
|
|||
GenServer.call(pid, {:get_state_ids_at_event, event})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Invite the a user to this room.
|
||||
"""
|
||||
@spec invite(pid(), Account.t(), String.t()) :: :ok | {:error, atom()}
|
||||
def invite(pid, account, user_id) do
|
||||
GenServer.call(pid, {:invite, account, user_id})
|
||||
end
|
||||
|
||||
### Implementation
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
room_id = Keyword.fetch!(opts, :room_id)
|
||||
room = Keyword.fetch!(opts, :room)
|
||||
serialized_state_set = Keyword.fetch!(opts, :serialized_state_set)
|
||||
state_event_ids = Enum.map(serialized_state_set, fn [_, _, event_id] -> event_id end)
|
||||
|
||||
|
@ -116,14 +123,16 @@ defmodule MatrixServer.RoomServer do
|
|||
{{type, state_key}, event}
|
||||
end)
|
||||
|
||||
{:ok, %{room_id: room_id, state_set: state_set}}
|
||||
{:ok, %{room: room, state_set: state_set}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:create_room, account, input}, _from, %{room_id: room_id} = state) do
|
||||
def handle_call(
|
||||
{:create_room, account, input},
|
||||
_from,
|
||||
%{room: %Room{id: room_id} = room} = state
|
||||
) do
|
||||
# TODO: power_level_content_override, initial_state, invite, invite_3pid
|
||||
room = Repo.one!(from r in Room, where: r.id == ^room_id)
|
||||
|
||||
case Repo.transaction(create_room_insert_events(room, account, input)) do
|
||||
{:ok, state_set} -> {:reply, {:ok, room_id}, %{state | state_set: state_set}}
|
||||
{:error, reason} -> {:reply, {:error, reason}, state}
|
||||
|
@ -184,76 +193,95 @@ defmodule MatrixServer.RoomServer do
|
|||
{:reply, {state_events, auth_chain}, state}
|
||||
end
|
||||
|
||||
def handle_call({:invite, account, user_id}, _from, %{room: room, state_set: state_set} = state) do
|
||||
case Repo.transaction(invite_insert_event(room, state_set, account, user_id)) do
|
||||
{:ok, state_set} -> {:reply, :ok, %{state | state_set: state_set}}
|
||||
{:error, reason} -> {:reply, {:error, reason}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@spec invite_insert_event(Room.t(), t(), Account.t(), String.t()) ::
|
||||
(() -> {:ok, t()} | {:error, atom()})
|
||||
defp invite_insert_event(room, state_set, account, user_id) do
|
||||
invite_event = Event.invite(room, account, user_id)
|
||||
|
||||
fn ->
|
||||
case finalize_and_insert_event(invite_event, state_set, room) do
|
||||
{:ok, state_set, room} ->
|
||||
_ = update_room_state_set(room, state_set)
|
||||
state_set
|
||||
|
||||
{:error, reason} ->
|
||||
Repo.rollback(reason)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@spec create_room_insert_events(Room.t(), Account.t(), CreateRoom.t()) ::
|
||||
(() -> {:ok, t()} | {:error, atom()})
|
||||
defp create_room_insert_events(room, account, %CreateRoom{
|
||||
room_version: room_version,
|
||||
preset: preset,
|
||||
name: name,
|
||||
topic: topic
|
||||
}) do
|
||||
events =
|
||||
([
|
||||
Event.create_room(room, account, room_version),
|
||||
Event.join(room, account),
|
||||
Event.power_levels(room, account)
|
||||
] ++
|
||||
room_creation_preset(account, preset, room) ++
|
||||
[
|
||||
if(name, do: Event.name(room, account, name)),
|
||||
if(topic, do: Event.topic(room, account, topic))
|
||||
])
|
||||
|> Enum.reject(&Kernel.is_nil/1)
|
||||
|
||||
fn ->
|
||||
state_set = %{}
|
||||
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)
|
||||
|
||||
with create_room <- Event.create_room(room, account, room_version),
|
||||
{:ok, state_set, create_room, room} <-
|
||||
verify_and_insert_event(create_room, state_set, room),
|
||||
join_creator <- Event.join(room, account, [create_room]),
|
||||
{:ok, state_set, join_creator, room} <-
|
||||
verify_and_insert_event(join_creator, state_set, room),
|
||||
{:ok, _} <- insert_joined_room_assoc(account, room),
|
||||
pls <- Event.power_levels(room, account, [create_room, join_creator]),
|
||||
{:ok, state_set, pls, room} <- verify_and_insert_event(pls, state_set, room) do
|
||||
auth_events = [create_room, join_creator, pls]
|
||||
preset_events = room_creation_preset(account, preset, room, auth_events)
|
||||
name_event = if name, do: Event.name(room, account, name, auth_events)
|
||||
topic_event = if topic, do: Event.topic(room, account, topic, auth_events)
|
||||
case result do
|
||||
{:error, reason} ->
|
||||
Repo.rollback(reason)
|
||||
|
||||
remaining_events =
|
||||
Enum.reject(preset_events ++ [name_event, topic_event], &Kernel.is_nil/1)
|
||||
|
||||
result =
|
||||
Enum.reduce_while(remaining_events, {state_set, room}, fn event, {state_set, room} ->
|
||||
case verify_and_insert_event(event, state_set, room) do
|
||||
{:ok, state_set, _event, room} -> {:cont, {state_set, room}}
|
||||
{:error, reason} -> {:halt, {:error, reason}}
|
||||
end
|
||||
end)
|
||||
|
||||
case result do
|
||||
{:error, reason} ->
|
||||
Repo.rollback(reason)
|
||||
|
||||
{state_set, room} ->
|
||||
serialized_state_set =
|
||||
Enum.map(state_set, fn {{type, state_key}, event} ->
|
||||
[type, state_key, event.event_id]
|
||||
end)
|
||||
|
||||
Repo.update!(change(room, state: serialized_state_set))
|
||||
state_set
|
||||
end
|
||||
else
|
||||
_ -> Repo.rollback(:event_creation)
|
||||
{state_set, room} ->
|
||||
_ = update_room_state_set(room, state_set)
|
||||
state_set
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp insert_joined_room_assoc(%Account{localpart: localpart}, %Room{id: room_id}) do
|
||||
Repo.insert(%JoinedRoom{localpart: localpart, room_id: room_id})
|
||||
@spec update_room_state_set(Room.t(), t()) :: Room.t()
|
||||
defp update_room_state_set(room, state_set) do
|
||||
serialized_state_set =
|
||||
Enum.map(state_set, fn {{type, state_key}, event} ->
|
||||
[type, state_key, event.event_id]
|
||||
end)
|
||||
|
||||
Repo.update!(change(room, state: serialized_state_set))
|
||||
end
|
||||
|
||||
# TODO: trusted_private_chat:
|
||||
# All invitees are given the same power level as the room creator.
|
||||
defp room_creation_preset(account, nil, %Room{visibility: visibility} = room, auth_events) do
|
||||
@spec room_creation_preset(Account.t(), String.t() | nil, Room.t()) :: [Event.t()]
|
||||
defp room_creation_preset(account, nil, %Room{visibility: visibility} = room) do
|
||||
preset =
|
||||
case visibility do
|
||||
:public -> "public_chat"
|
||||
:private -> "private_chat"
|
||||
end
|
||||
|
||||
room_creation_preset(account, preset, room, auth_events)
|
||||
room_creation_preset(account, preset, room)
|
||||
end
|
||||
|
||||
defp room_creation_preset(account, preset, room, auth_events) do
|
||||
defp room_creation_preset(account, preset, room) do
|
||||
{join_rule, his_vis, guest_access} =
|
||||
case preset do
|
||||
"private_chat" -> {"invite", "shared", "can_join"}
|
||||
|
@ -262,17 +290,80 @@ defmodule MatrixServer.RoomServer do
|
|||
end
|
||||
|
||||
[
|
||||
Event.join_rules(room, account, join_rule, auth_events),
|
||||
Event.history_visibility(room, account, his_vis, auth_events),
|
||||
Event.guest_access(room, account, guest_access, auth_events)
|
||||
Event.join_rules(room, account, join_rule),
|
||||
Event.history_visibility(room, account, his_vis),
|
||||
Event.guest_access(room, account, guest_access)
|
||||
]
|
||||
end
|
||||
|
||||
defp verify_and_insert_event(
|
||||
@spec finalize_and_insert_event(Event.t(), t(), Room.t()) ::
|
||||
{:ok, t(), Room.t()} | {:error, atom()}
|
||||
defp finalize_and_insert_event(
|
||||
event,
|
||||
current_state_set,
|
||||
state_set,
|
||||
%Room{forward_extremities: forward_extremities} = room
|
||||
) do
|
||||
event =
|
||||
event
|
||||
|> Map.put(:auth_events, auth_events_for_event(event, state_set))
|
||||
|> Map.put(:prev_events, forward_extremities)
|
||||
|
||||
case Event.post_process(event) do
|
||||
{:ok, event} -> verify_and_insert_event(event, state_set, room)
|
||||
_ -> {:error, :event_creation}
|
||||
end
|
||||
end
|
||||
|
||||
@spec auth_events_for_event(Event.t(), t()) :: [{String.t(), String.t()}]
|
||||
defp auth_events_for_event(%Event{type: "m.room.create"}, _), do: []
|
||||
|
||||
defp auth_events_for_event(
|
||||
%Event{sender: sender} = event,
|
||||
state_set
|
||||
) do
|
||||
state_pairs =
|
||||
[{"m.room.create", ""}, {"m.room.power_levels", ""}, {"m.room.member", to_string(sender)}] ++
|
||||
auth_events_for_member_event(event)
|
||||
|
||||
state_set
|
||||
|> Map.take(state_pairs)
|
||||
|> Map.values()
|
||||
|> Enum.map(& &1.event_id)
|
||||
end
|
||||
|
||||
@spec auth_events_for_member_event(Event.t()) :: [{String.t(), String.t()}]
|
||||
defp auth_events_for_member_event(
|
||||
%Event{
|
||||
type: "m.room.member",
|
||||
state_key: state_key,
|
||||
content: %{"membership" => membership}
|
||||
} = event
|
||||
) do
|
||||
[
|
||||
{"m.room.member", state_key},
|
||||
if(membership in ["join", "invite"], do: {"m.room.join_rules", ""}),
|
||||
third_party_invite_state_pair(event)
|
||||
]
|
||||
|> Enum.reject(&Kernel.is_nil/1)
|
||||
end
|
||||
|
||||
defp auth_events_for_member_event(_), do: []
|
||||
|
||||
@spec third_party_invite_state_pair(Event.t()) :: {String.t(), String.t()} | nil
|
||||
defp third_party_invite_state_pair(%Event{
|
||||
content: %{
|
||||
"membership" => "invite",
|
||||
"third_party_invite" => %{"signed" => %{"token" => token}}
|
||||
}
|
||||
}) do
|
||||
{"m.room.third_party_invite", token}
|
||||
end
|
||||
|
||||
defp third_party_invite_state_pair(_), do: nil
|
||||
|
||||
@spec verify_and_insert_event(Event.t(), t(), Room.t()) ::
|
||||
{:ok, t(), Room.t()} | {:error, atom()}
|
||||
defp verify_and_insert_event(event, current_state_set, room) do
|
||||
# TODO: Correct error values.
|
||||
# Check the following things:
|
||||
# 1. TODO: Is a valid event, otherwise it is dropped.
|
||||
|
@ -281,10 +372,7 @@ defmodule MatrixServer.RoomServer do
|
|||
# 4. Passes authorization rules based on the event's auth events, otherwise it is rejected.
|
||||
# 5. Passes authorization rules based on the state at the event, otherwise it is rejected.
|
||||
# 6. Passes authorization rules based on the current state of the room, otherwise it is "soft failed".
|
||||
event = %Event{event | prev_events: forward_extremities}
|
||||
|
||||
with {:ok, event} <- Event.post_process(event),
|
||||
true <- Event.prevalidate(event),
|
||||
with true <- Event.prevalidate(event),
|
||||
true <- Authorization.authorized_by_auth_events?(event),
|
||||
state_set <- StateResolution.resolve(event, false),
|
||||
true <- Authorization.authorized?(event, state_set),
|
||||
|
@ -292,9 +380,28 @@ defmodule MatrixServer.RoomServer do
|
|||
room = Room.update_forward_extremities(event, room)
|
||||
event = Repo.insert!(event)
|
||||
state_set = StateResolution.resolve_forward_extremities(event)
|
||||
{:ok, state_set, event, room}
|
||||
_ = update_joined_rooms(event, room)
|
||||
|
||||
{:ok, state_set, room}
|
||||
else
|
||||
_ -> {:error, :authorization}
|
||||
end
|
||||
end
|
||||
|
||||
@spec update_joined_rooms(Event.t(), Room.t()) :: JoinedRoom.t() | nil
|
||||
defp update_joined_rooms(
|
||||
%Event{
|
||||
type: "m.room.member",
|
||||
sender: %UserId{localpart: localpart, domain: domain},
|
||||
content: %{"membership" => "join"}
|
||||
},
|
||||
%Room{id: room_id}
|
||||
) do
|
||||
# TODO: Also remove joined rooms.
|
||||
if domain == MatrixServer.server_name() do
|
||||
Repo.insert(%JoinedRoom{localpart: localpart, room_id: room_id})
|
||||
end
|
||||
end
|
||||
|
||||
defp update_joined_rooms(_, _), do: nil
|
||||
end
|
||||
|
|
|
@ -74,11 +74,7 @@ defmodule MatrixServer.Event do
|
|||
end
|
||||
|
||||
@spec create_room(Room.t(), Account.t(), String.t()) :: t()
|
||||
def create_room(
|
||||
room,
|
||||
%Account{localpart: localpart} = creator,
|
||||
room_version
|
||||
) do
|
||||
def create_room(room, %Account{localpart: localpart} = creator, room_version) do
|
||||
mxid = MatrixServer.get_mxid(localpart)
|
||||
|
||||
%Event{
|
||||
|
@ -92,8 +88,8 @@ defmodule MatrixServer.Event do
|
|||
}
|
||||
end
|
||||
|
||||
@spec join(Room.t(), Account.t(), [t()]) :: t()
|
||||
def join(room, %Account{localpart: localpart} = sender, auth_events) do
|
||||
@spec join(Room.t(), Account.t()) :: t()
|
||||
def join(room, %Account{localpart: localpart} = sender) do
|
||||
mxid = MatrixServer.get_mxid(localpart)
|
||||
|
||||
%Event{
|
||||
|
@ -102,17 +98,12 @@ defmodule MatrixServer.Event do
|
|||
state_key: mxid,
|
||||
content: %{
|
||||
"membership" => "join"
|
||||
},
|
||||
auth_events: Enum.map(auth_events, & &1.event_id)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
@spec power_levels(Room.t(), Account.t(), [t()]) :: t()
|
||||
def power_levels(
|
||||
room,
|
||||
%Account{localpart: localpart} = sender,
|
||||
auth_events
|
||||
) do
|
||||
@spec power_levels(Room.t(), Account.t()) :: t()
|
||||
def power_levels(room, %Account{localpart: localpart} = sender) do
|
||||
mxid = MatrixServer.get_mxid(localpart)
|
||||
|
||||
%Event{
|
||||
|
@ -134,73 +125,79 @@ defmodule MatrixServer.Event do
|
|||
"notifications" => %{
|
||||
"room" => 50
|
||||
}
|
||||
},
|
||||
auth_events: Enum.map(auth_events, & &1.event_id)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
@spec name(Room.t(), Account.t(), String.t(), [t()]) :: %Event{}
|
||||
def name(room, sender, name, auth_events) do
|
||||
@spec name(Room.t(), Account.t(), String.t()) :: %Event{}
|
||||
def name(room, sender, name) do
|
||||
%Event{
|
||||
new(room, sender)
|
||||
| type: "m.room.name",
|
||||
state_key: "",
|
||||
content: %{
|
||||
"name" => name
|
||||
},
|
||||
auth_events: Enum.map(auth_events, & &1.event_id)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
@spec topic(Room.t(), Account.t(), String.t(), [t()]) :: t()
|
||||
def topic(room, sender, topic, auth_events) do
|
||||
@spec topic(Room.t(), Account.t(), String.t()) :: t()
|
||||
def topic(room, sender, topic) do
|
||||
%Event{
|
||||
new(room, sender)
|
||||
| type: "m.room.topic",
|
||||
state_key: "",
|
||||
content: %{
|
||||
"topic" => topic
|
||||
},
|
||||
auth_events: Enum.map(auth_events, & &1.event_id)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
@spec join_rules(Room.t(), Account.t(), String.t(), [t()]) :: t()
|
||||
def join_rules(room, sender, join_rule, auth_events) do
|
||||
@spec join_rules(Room.t(), Account.t(), String.t()) :: t()
|
||||
def join_rules(room, sender, join_rule) do
|
||||
%Event{
|
||||
new(room, sender)
|
||||
| type: "m.room.join_rules",
|
||||
state_key: "",
|
||||
content: %{
|
||||
"join_rule" => join_rule
|
||||
},
|
||||
auth_events: Enum.map(auth_events, & &1.event_id)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
@spec history_visibility(Room.t(), Account.t(), String.t(), [t()]) :: t()
|
||||
def history_visibility(room, sender, history_visibility, auth_events) do
|
||||
@spec history_visibility(Room.t(), Account.t(), String.t()) :: t()
|
||||
def history_visibility(room, sender, history_visibility) do
|
||||
%Event{
|
||||
new(room, sender)
|
||||
| type: "m.room.history_visibility",
|
||||
state_key: "",
|
||||
content: %{
|
||||
"history_visibility" => history_visibility
|
||||
},
|
||||
auth_events: Enum.map(auth_events, & &1.event_id)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
@spec guest_access(Room.t(), Account.t(), String.t(), [t()]) :: t()
|
||||
def guest_access(room, sender, guest_access, auth_events) do
|
||||
@spec guest_access(Room.t(), Account.t(), String.t()) :: t()
|
||||
def guest_access(room, sender, guest_access) do
|
||||
%Event{
|
||||
new(room, sender)
|
||||
| type: "m.room.guest_access",
|
||||
state_key: "",
|
||||
content: %{
|
||||
"guest_access" => guest_access
|
||||
},
|
||||
auth_events: Enum.map(auth_events, & &1.event_id)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
@spec invite(Room.t(), Account.t(), String.t()) :: t()
|
||||
def invite(room, sender, user_id) do
|
||||
%Event{
|
||||
new(room, sender)
|
||||
| type: "m.room.member",
|
||||
state_key: user_id,
|
||||
content: %{
|
||||
"membership" => "invite"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -3,6 +3,11 @@ defmodule MatrixServer.JoinedRoom do
|
|||
|
||||
alias MatrixServer.{Account, Room}
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
localpart: String.t(),
|
||||
room_id: String.t()
|
||||
}
|
||||
|
||||
@primary_key false
|
||||
schema "joined_rooms" do
|
||||
belongs_to :account, Account,
|
||||
|
|
|
@ -20,9 +20,12 @@ defmodule MatrixServer.StateResolution.Authorization do
|
|||
do: prev_events == []
|
||||
|
||||
# Check rule: 5.2.1
|
||||
def authorized?(%Event{type: "m.room.member", state_key: state_key}, %{
|
||||
{"m.room.create", ""} => %Event{content: %{"creator" => creator}}
|
||||
}),
|
||||
def authorized?(
|
||||
%Event{type: "m.room.member", state_key: state_key, prev_events: [create_id]},
|
||||
%{
|
||||
{"m.room.create", ""} => %Event{event_id: create_id, content: %{"creator" => creator}}
|
||||
}
|
||||
),
|
||||
do: state_key == creator
|
||||
|
||||
def authorized?(
|
||||
|
@ -309,6 +312,9 @@ defmodule MatrixServer.StateResolution.Authorization do
|
|||
|> Repo.all()
|
||||
|> Enum.reduce(%{}, &update_state_set/2)
|
||||
|
||||
IO.inspect(event)
|
||||
IO.inspect(state_set)
|
||||
|
||||
authorized?(event, state_set)
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue