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-29 14:59:40 +00:00
|
|
|
import Ecto.Changeset
|
2021-07-25 12:57:52 +00:00
|
|
|
|
2021-07-28 15:23:48 +00:00
|
|
|
alias MatrixServer.{Repo, Room, Event, StateResolution}
|
|
|
|
alias MatrixServer.StateResolution.Authorization
|
2021-08-14 13:20:42 +00:00
|
|
|
alias MatrixServerWeb.Client.Request.CreateRoom
|
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-08-21 09:25:36 +00:00
|
|
|
@spec get_room_server(Room.t()) :: {:error, :not_found} | DynamicSupervisor.on_start_child()
|
|
|
|
def get_room_server(%Room{id: room_id}), do: get_room_server(room_id)
|
|
|
|
|
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.
|
2021-08-18 21:22:04 +00:00
|
|
|
@spec get_room_server(String.t()) :: {:error, :not_found} | DynamicSupervisor.on_start_child()
|
2021-07-28 15:23:48 +00:00
|
|
|
def get_room_server(room_id) do
|
|
|
|
case Repo.one(from r in Room, where: r.id == ^room_id) do
|
|
|
|
nil ->
|
|
|
|
{:error, :not_found}
|
|
|
|
|
2021-07-29 14:59:40 +00:00
|
|
|
%Room{state: serialized_state_set} ->
|
2021-07-28 15:23:48 +00:00
|
|
|
case Registry.lookup(@registry, room_id) do
|
|
|
|
[{pid, _}] ->
|
|
|
|
{:ok, pid}
|
|
|
|
|
|
|
|
[] ->
|
|
|
|
opts = [
|
|
|
|
name: {:via, Registry, {@registry, room_id}},
|
2021-07-29 14:59:40 +00:00
|
|
|
room_id: room_id,
|
|
|
|
serialized_state_set: serialized_state_set
|
2021-07-28 15:23:48 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
DynamicSupervisor.start_child(@supervisor, {__MODULE__, opts})
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-08-19 14:31:03 +00:00
|
|
|
@spec create_room(
|
|
|
|
pid(),
|
|
|
|
MatrixServer.Account.t(),
|
|
|
|
MatrixServerWeb.Client.Request.CreateRoom.t()
|
|
|
|
) :: {:ok, String.t()} | {:error, atom()}
|
2021-07-28 15:23:48 +00:00
|
|
|
def create_room(pid, account, input) do
|
|
|
|
GenServer.call(pid, {:create_room, account, input})
|
|
|
|
end
|
|
|
|
|
2021-08-18 21:22:04 +00:00
|
|
|
@spec server_in_room(pid(), String.t()) :: boolean()
|
2021-08-16 17:30:47 +00:00
|
|
|
def server_in_room(pid, domain) do
|
|
|
|
GenServer.call(pid, {:server_in_room, domain})
|
|
|
|
end
|
|
|
|
|
2021-08-22 10:19:47 +00:00
|
|
|
@spec get_state_at_event(pid(), Event.t()) :: {[Event.t()], [Event.t()]}
|
2021-08-21 19:39:28 +00:00
|
|
|
def get_state_at_event(pid, event) do
|
|
|
|
GenServer.call(pid, {:get_state_at_event, event})
|
|
|
|
end
|
|
|
|
|
2021-08-22 10:19:47 +00:00
|
|
|
@spec get_state_ids_at_event(pid(), Event.t()) :: {[String.t()], [String.t()]}
|
|
|
|
def get_state_ids_at_event(pid, event) do
|
|
|
|
GenServer.call(pid, {:get_state_ids_at_event, event})
|
|
|
|
end
|
|
|
|
|
2021-07-28 15:23:48 +00:00
|
|
|
### 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)
|
2021-07-29 14:59:40 +00:00
|
|
|
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.event_id in ^state_event_ids)
|
|
|
|
|> Repo.all()
|
|
|
|
|> Enum.into(%{}, fn %Event{type: type, state_key: state_key} = event ->
|
|
|
|
{{type, state_key}, event}
|
|
|
|
end)
|
2021-07-28 15:23:48 +00:00
|
|
|
|
2021-07-29 14:59:40 +00:00
|
|
|
{:ok, %{room_id: room_id, state_set: state_set}}
|
2021-07-28 15:23:48 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
@impl true
|
2021-08-18 11:54:18 +00:00
|
|
|
def handle_call({:create_room, account, input}, _from, %{room_id: room_id} = state) do
|
|
|
|
# TODO: power_level_content_override, initial_state, invite, invite_3pid
|
|
|
|
room = Repo.one!(from r in Room, where: r.id == ^room_id)
|
|
|
|
|
2021-08-18 17:04:09 +00:00
|
|
|
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}
|
|
|
|
_ -> {:reply, {:error, :unknown}, state}
|
2021-07-28 15:23:48 +00:00
|
|
|
end
|
2021-07-23 19:00:01 +00:00
|
|
|
end
|
|
|
|
|
2021-08-16 17:30:47 +00:00
|
|
|
def handle_call({:server_in_room, domain}, _from, %{state_set: state_set} = state) do
|
2021-08-18 11:54:18 +00:00
|
|
|
result =
|
|
|
|
Enum.any?(state_set, fn
|
|
|
|
{{"m.room.member", user_id}, %Event{content: %{"membership" => "join"}}} ->
|
|
|
|
MatrixServer.get_domain(user_id) == domain
|
2021-08-16 17:30:47 +00:00
|
|
|
|
2021-08-18 11:54:18 +00:00
|
|
|
_ ->
|
|
|
|
false
|
|
|
|
end)
|
2021-08-16 17:30:47 +00:00
|
|
|
|
|
|
|
{:reply, result, state}
|
|
|
|
end
|
|
|
|
|
2021-08-21 19:39:28 +00:00
|
|
|
def handle_call({:get_state_at_event, %Event{room_id: room_id} = event}, _from, state) do
|
|
|
|
room_events =
|
|
|
|
Event
|
|
|
|
|> where([e], e.room_id == ^room_id)
|
|
|
|
|> select([e], {e.event_id, e})
|
|
|
|
|> Repo.all()
|
|
|
|
|> Enum.into(%{})
|
|
|
|
|
|
|
|
state_set = StateResolution.resolve(event, false)
|
|
|
|
state_events = Map.values(state_set)
|
|
|
|
|
|
|
|
auth_chain =
|
|
|
|
state_set
|
|
|
|
|> Map.values()
|
|
|
|
|> StateResolution.full_auth_chain(room_events)
|
|
|
|
|> Enum.map(&room_events[&1])
|
|
|
|
|
|
|
|
{:reply, {state_events, auth_chain}, state}
|
|
|
|
end
|
|
|
|
|
2021-08-22 10:19:47 +00:00
|
|
|
def handle_call({:get_state_ids_at_event, %Event{room_id: room_id} = event}, _from, state) do
|
|
|
|
room_events =
|
|
|
|
Event
|
|
|
|
|> where([e], e.room_id == ^room_id)
|
|
|
|
|> select([e], {e.event_id, e})
|
|
|
|
|> Repo.all()
|
|
|
|
|> Enum.into(%{})
|
|
|
|
|
|
|
|
state_set = StateResolution.resolve(event, false)
|
|
|
|
state_events = Enum.map(state_set, fn {_, %Event{event_id: event_id}} -> event_id end)
|
|
|
|
|
|
|
|
auth_chain =
|
|
|
|
state_set
|
|
|
|
|> Map.values()
|
|
|
|
|> StateResolution.full_auth_chain(room_events)
|
|
|
|
|> MapSet.to_list()
|
|
|
|
|
|
|
|
{:reply, {state_events, auth_chain}, state}
|
|
|
|
end
|
|
|
|
|
2021-08-18 17:04:09 +00:00
|
|
|
defp create_room_insert_events(room, account, %CreateRoom{
|
2021-08-18 11:54:18 +00:00
|
|
|
room_version: room_version,
|
2021-08-18 17:04:09 +00:00
|
|
|
preset: preset,
|
2021-08-18 11:54:18 +00:00
|
|
|
name: name,
|
2021-08-18 17:04:09 +00:00
|
|
|
topic: topic
|
2021-08-18 11:54:18 +00:00
|
|
|
}) do
|
|
|
|
fn ->
|
2021-08-18 17:04:09 +00:00
|
|
|
state_set = %{}
|
|
|
|
|
|
|
|
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),
|
|
|
|
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)
|
|
|
|
|
|
|
|
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)
|
2021-08-18 11:54:18 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-07-27 10:55:36 +00:00
|
|
|
# TODO: trusted_private_chat:
|
|
|
|
# All invitees are given the same power level as the room creator.
|
2021-07-29 14:59:40 +00:00
|
|
|
defp room_creation_preset(account, nil, %Room{visibility: visibility} = room, auth_events) do
|
2021-07-27 10:55:36 +00:00
|
|
|
preset =
|
|
|
|
case visibility do
|
|
|
|
:public -> "public_chat"
|
|
|
|
:private -> "private_chat"
|
|
|
|
end
|
|
|
|
|
2021-07-29 14:59:40 +00:00
|
|
|
room_creation_preset(account, preset, room, auth_events)
|
2021-07-27 10:55:36 +00:00
|
|
|
end
|
|
|
|
|
2021-07-29 14:59:40 +00:00
|
|
|
defp room_creation_preset(account, preset, room, auth_events) do
|
2021-07-27 10:55:36 +00:00
|
|
|
{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
|
|
|
|
|
2021-08-18 17:04:09 +00:00
|
|
|
[
|
|
|
|
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)
|
|
|
|
]
|
2021-07-26 21:42:35 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
defp verify_and_insert_event(
|
2021-07-29 14:59:40 +00:00
|
|
|
event,
|
2021-07-26 21:42:35 +00:00
|
|
|
current_state_set,
|
|
|
|
%Room{forward_extremities: forward_extremities} = room
|
|
|
|
) do
|
2021-08-18 17:04:09 +00:00
|
|
|
# TODO: Correct error values.
|
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-08-18 17:04:09 +00:00
|
|
|
with {:ok, event} <- Event.post_process(event),
|
|
|
|
true <- Event.prevalidate(event),
|
2021-07-29 20:06:02 +00:00
|
|
|
true <- Authorization.authorized_by_auth_events?(event),
|
|
|
|
state_set <- StateResolution.resolve(event, false),
|
|
|
|
true <- Authorization.authorized?(event, state_set),
|
|
|
|
true <- Authorization.authorized?(event, current_state_set) do
|
|
|
|
room = Room.update_forward_extremities(event, room)
|
|
|
|
event = Repo.insert!(event)
|
|
|
|
state_set = StateResolution.resolve_forward_extremities(event)
|
2021-08-18 17:04:09 +00:00
|
|
|
{:ok, state_set, event, room}
|
2021-07-24 20:08:01 +00:00
|
|
|
else
|
2021-07-29 20:06:02 +00:00
|
|
|
_ -> {:error, :authorization}
|
2021-07-24 20:08:01 +00:00
|
|
|
end
|
2021-07-10 21:16:00 +00:00
|
|
|
end
|
|
|
|
end
|