2021-07-10 21:16:00 +00:00
|
|
|
defmodule MatrixServer.RoomServer do
|
|
|
|
use GenServer
|
|
|
|
|
2021-07-25 12:57:52 +00:00
|
|
|
import Ecto.Query
|
|
|
|
|
2021-07-28 15:23:48 +00:00
|
|
|
alias MatrixServer.{Repo, Room, Event, StateResolution}
|
2021-07-10 21:16:00 +00:00
|
|
|
alias MatrixServerWeb.API.CreateRoom
|
2021-07-28 15:23:48 +00:00
|
|
|
alias MatrixServer.StateResolution.Authorization
|
2021-07-10 21:16:00 +00:00
|
|
|
|
2021-07-23 19:00:01 +00:00
|
|
|
@registry MatrixServer.RoomServer.Registry
|
|
|
|
@supervisor MatrixServer.RoomServer.Supervisor
|
|
|
|
|
2021-07-28 15:23:48 +00:00
|
|
|
### Interface
|
2021-07-10 21:16:00 +00:00
|
|
|
|
2021-07-25 15:39:22 +00:00
|
|
|
def start_link(opts) do
|
|
|
|
{name, opts} = Keyword.pop(opts, :name)
|
|
|
|
GenServer.start_link(__MODULE__, opts, name: name)
|
|
|
|
end
|
|
|
|
|
2021-07-28 15:23:48 +00:00
|
|
|
# Get room server pid, or spin one up for the room.
|
|
|
|
# If the room does not exist, return an error.
|
|
|
|
def get_room_server(room_id) do
|
|
|
|
case Repo.one(from r in Room, where: r.id == ^room_id) do
|
|
|
|
nil ->
|
|
|
|
{:error, :not_found}
|
|
|
|
|
|
|
|
%Room{} ->
|
|
|
|
case Registry.lookup(@registry, room_id) do
|
|
|
|
[{pid, _}] ->
|
|
|
|
{:ok, pid}
|
|
|
|
|
|
|
|
[] ->
|
|
|
|
opts = [
|
|
|
|
name: {:via, Registry, {@registry, room_id}},
|
|
|
|
room_id: room_id
|
|
|
|
]
|
|
|
|
|
|
|
|
DynamicSupervisor.start_child(@supervisor, {__MODULE__, opts})
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def create_room(pid, account, input) do
|
|
|
|
GenServer.call(pid, {:create_room, account, input})
|
|
|
|
end
|
|
|
|
|
|
|
|
### Implementation
|
|
|
|
|
2021-07-10 21:16:00 +00:00
|
|
|
@impl true
|
2021-07-23 19:00:01 +00:00
|
|
|
def init(opts) do
|
2021-07-28 15:23:48 +00:00
|
|
|
room_id = Keyword.fetch!(opts, :room_id)
|
|
|
|
|
|
|
|
{:ok, %{room_id: room_id, state_set: %{}}}
|
|
|
|
end
|
|
|
|
|
|
|
|
@impl true
|
|
|
|
def handle_call({:create_room, account, input}, _from, %{room_id: room_id} = state) do
|
|
|
|
result =
|
|
|
|
Repo.transaction(fn ->
|
|
|
|
# TODO: power_level_content_override, initial_state, invite, invite_3pid
|
|
|
|
with room <- Repo.one!(from r in Room, where: r.id == ^room_id),
|
|
|
|
{:ok, create_room_id, state_set, room} <-
|
|
|
|
room_creation_create_room(account, input, room),
|
|
|
|
{:ok, join_creator_id, state_set, room} <-
|
|
|
|
room_creation_join_creator(account, room, state_set, [create_room_id]),
|
|
|
|
{:ok, pl_id, state_set, room} <-
|
|
|
|
room_creation_power_levels(
|
|
|
|
account,
|
|
|
|
room,
|
|
|
|
state_set,
|
|
|
|
[create_room_id, join_creator_id]
|
|
|
|
),
|
|
|
|
{:ok, _, state_set, room} <-
|
|
|
|
room_creation_preset(account, input, room, state_set, [
|
|
|
|
create_room_id,
|
|
|
|
join_creator_id,
|
|
|
|
pl_id
|
|
|
|
]),
|
|
|
|
{:ok, _, state_set, room} <-
|
|
|
|
room_creation_name(account, input, room, state_set, [
|
|
|
|
create_room_id,
|
|
|
|
join_creator_id,
|
|
|
|
pl_id
|
|
|
|
]),
|
|
|
|
{:ok, _, state_set, _} <-
|
|
|
|
room_creation_topic(account, input, room, state_set, [
|
|
|
|
create_room_id,
|
|
|
|
join_creator_id,
|
|
|
|
pl_id
|
|
|
|
]) do
|
|
|
|
state_set
|
|
|
|
else
|
|
|
|
reason ->
|
|
|
|
Repo.rollback(reason)
|
|
|
|
end
|
|
|
|
end)
|
|
|
|
|
|
|
|
case result do
|
|
|
|
{:ok, state_set} -> {:reply, :ok, %{state | state_set: state_set}}
|
|
|
|
{:error, reason} -> {:reply, {:error, reason}, state}
|
|
|
|
end
|
2021-07-23 19:00:01 +00:00
|
|
|
end
|
|
|
|
|
2021-07-27 10:55:36 +00:00
|
|
|
defp room_creation_create_room(account, %CreateRoom{room_version: room_version}, room) do
|
|
|
|
Event.create_room(room, account, room_version)
|
2021-07-26 21:42:35 +00:00
|
|
|
|> verify_and_insert_event(%{}, room)
|
2021-07-26 09:50:18 +00:00
|
|
|
end
|
2021-07-24 20:54:03 +00:00
|
|
|
|
2021-07-27 10:55:36 +00:00
|
|
|
defp room_creation_join_creator(account, room, state_set, auth_events) do
|
|
|
|
Event.join(room, account)
|
2021-07-26 21:42:35 +00:00
|
|
|
|> Map.put(:auth_events, auth_events)
|
|
|
|
|> verify_and_insert_event(state_set, room)
|
2021-07-26 17:47:38 +00:00
|
|
|
end
|
|
|
|
|
2021-07-27 10:55:36 +00:00
|
|
|
defp room_creation_power_levels(account, room, state_set, auth_events) do
|
|
|
|
Event.power_levels(room, account)
|
|
|
|
|> Map.put(:auth_events, auth_events)
|
|
|
|
|> verify_and_insert_event(state_set, room)
|
|
|
|
end
|
|
|
|
|
|
|
|
# TODO: trusted_private_chat:
|
|
|
|
# All invitees are given the same power level as the room creator.
|
|
|
|
defp room_creation_preset(
|
|
|
|
account,
|
|
|
|
%CreateRoom{preset: nil},
|
|
|
|
%Room{visibility: visibility} = room,
|
2021-07-26 17:47:38 +00:00
|
|
|
state_set,
|
2021-07-26 21:42:35 +00:00
|
|
|
auth_events
|
2021-07-26 17:47:38 +00:00
|
|
|
) do
|
2021-07-27 10:55:36 +00:00
|
|
|
preset =
|
|
|
|
case visibility do
|
|
|
|
:public -> "public_chat"
|
|
|
|
:private -> "private_chat"
|
|
|
|
end
|
|
|
|
|
|
|
|
room_creation_preset(account, preset, room, state_set, auth_events)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp room_creation_preset(account, %CreateRoom{preset: preset}, room, state_set, auth_events) do
|
|
|
|
room_creation_preset(account, preset, room, state_set, auth_events)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp room_creation_preset(account, preset, room, state_set, auth_events) do
|
|
|
|
{join_rule, his_vis, guest_access} =
|
|
|
|
case preset do
|
|
|
|
"private_chat" -> {"invite", "shared", "can_join"}
|
|
|
|
"trusted_private_chat" -> {"invite", "shared", "can_join"}
|
|
|
|
"public_chat" -> {"public", "shared", "forbidden"}
|
|
|
|
end
|
|
|
|
|
|
|
|
with {:ok, _, _, _} <-
|
|
|
|
room_creation_join_rules(account, join_rule, room, state_set, auth_events),
|
|
|
|
{:ok, _, _, _} <- room_creation_his_vis(account, his_vis, room, state_set, auth_events) do
|
|
|
|
room_creation_guest_access(account, guest_access, room, state_set, auth_events)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp room_creation_join_rules(account, join_rule, room, state_set, auth_events) do
|
|
|
|
Event.join_rules(room, account, join_rule)
|
|
|
|
|> Map.put(:auth_events, auth_events)
|
|
|
|
|> verify_and_insert_event(state_set, room)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp room_creation_his_vis(account, his_vis, room, state_set, auth_events) do
|
|
|
|
Event.history_visibility(room, account, his_vis)
|
|
|
|
|> Map.put(:auth_events, auth_events)
|
|
|
|
|> verify_and_insert_event(state_set, room)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp room_creation_guest_access(account, guest_access, room, state_set, auth_events) do
|
|
|
|
Event.guest_access(room, account, guest_access)
|
2021-07-26 21:42:35 +00:00
|
|
|
|> Map.put(:auth_events, auth_events)
|
|
|
|
|> verify_and_insert_event(state_set, room)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp room_creation_name(_, %CreateRoom{name: nil}, room, state_set, _) do
|
|
|
|
{:ok, nil, state_set, room}
|
2021-07-24 20:08:01 +00:00
|
|
|
end
|
2021-07-23 22:13:12 +00:00
|
|
|
|
2021-07-27 10:55:36 +00:00
|
|
|
defp room_creation_name(account, %CreateRoom{name: name}, room, state_set, auth_events) do
|
|
|
|
Event.name(room, account, name)
|
2021-07-26 21:42:35 +00:00
|
|
|
|> Map.put(:auth_events, auth_events)
|
|
|
|
|> verify_and_insert_event(state_set, room)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp room_creation_topic(_, %CreateRoom{topic: nil}, room, state_set, _) do
|
|
|
|
{:ok, nil, state_set, room}
|
|
|
|
end
|
|
|
|
|
2021-07-27 10:55:36 +00:00
|
|
|
defp room_creation_topic(account, %CreateRoom{topic: topic}, room, state_set, auth_events) do
|
|
|
|
Event.topic(room, account, topic)
|
2021-07-26 21:42:35 +00:00
|
|
|
|> Map.put(:auth_events, auth_events)
|
|
|
|
|> verify_and_insert_event(state_set, room)
|
|
|
|
end
|
|
|
|
|
|
|
|
defp verify_and_insert_event(
|
|
|
|
%Event{event_id: event_id} = event,
|
|
|
|
current_state_set,
|
|
|
|
%Room{forward_extremities: forward_extremities} = room
|
|
|
|
) do
|
2021-07-24 20:08:01 +00:00
|
|
|
# Check the following things:
|
|
|
|
# 1. TODO: Is a valid event, otherwise it is dropped.
|
|
|
|
# 2. TODO: Passes signature checks, otherwise it is dropped.
|
|
|
|
# 3. TODO: Passes hash checks, otherwise it is redacted before being processed further.
|
|
|
|
# 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.
|
2021-07-25 15:39:22 +00:00
|
|
|
# 6. Passes authorization rules based on the current state of the room, otherwise it is "soft failed".
|
2021-07-26 21:42:35 +00:00
|
|
|
event = %Event{event | prev_events: forward_extremities}
|
|
|
|
|
2021-07-26 17:47:38 +00:00
|
|
|
if Event.prevalidate(event) do
|
2021-07-28 15:23:48 +00:00
|
|
|
if Authorization.authorized_by_auth_events?(event) do
|
2021-07-26 17:47:38 +00:00
|
|
|
state_set = StateResolution.resolve(event, false)
|
|
|
|
|
2021-07-28 15:23:48 +00:00
|
|
|
if Authorization.authorized?(event, state_set) do
|
|
|
|
if Authorization.authorized?(event, current_state_set) do
|
2021-07-26 17:47:38 +00:00
|
|
|
# We assume here that the event is always a forward extremity.
|
2021-07-26 21:42:35 +00:00
|
|
|
room = Room.update_forward_extremities(event, room)
|
2021-07-26 17:47:38 +00:00
|
|
|
{:ok, event} = Repo.insert(event)
|
|
|
|
state_set = StateResolution.resolve_forward_extremities(event)
|
2021-07-26 21:42:35 +00:00
|
|
|
{:ok, event_id, state_set, room}
|
2021-07-26 17:47:38 +00:00
|
|
|
else
|
|
|
|
{:error, :soft_failed}
|
|
|
|
end
|
2021-07-25 15:39:22 +00:00
|
|
|
else
|
2021-07-26 17:47:38 +00:00
|
|
|
{:error, :rejected}
|
2021-07-25 15:39:22 +00:00
|
|
|
end
|
2021-07-25 12:57:52 +00:00
|
|
|
else
|
|
|
|
{:error, :rejected}
|
|
|
|
end
|
2021-07-24 20:08:01 +00:00
|
|
|
else
|
2021-07-26 17:47:38 +00:00
|
|
|
{:error, :invalid}
|
2021-07-24 20:08:01 +00:00
|
|
|
end
|
2021-07-10 21:16:00 +00:00
|
|
|
end
|
|
|
|
end
|