Compare commits
10 commits
8ab147190e
...
f94c156f95
Author | SHA1 | Date | |
---|---|---|---|
f94c156f95 | |||
731143775d | |||
f700be5dbe | |||
222b6a309a | |||
739c496ac6 | |||
064a398a37 | |||
b34fd58cf7 | |||
56be8ba301 | |||
8dfd770fd0 | |||
d23a42e29c |
17 changed files with 793 additions and 334 deletions
|
@ -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: Only with optional parameters name, topic, preset and invite.
|
- POST /_matrix/client/r0/createRoom: Except with option invite_3pid.
|
||||||
- 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,6 +61,8 @@ 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
|
||||||
|
@ -76,7 +78,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: Except displayname and avatar_url is not implemented.
|
- GET /_matrix/federation/v1/query/profile
|
||||||
|
|
||||||
### Major unimplemented features
|
### Major unimplemented features
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,6 @@ 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
|
||||||
|
@ -21,9 +19,12 @@ 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}
|
||||||
|
|
||||||
|
@ -46,8 +47,13 @@ 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.
|
||||||
case Repo.one(from r in Room, where: r.id == ^room_id) do
|
query =
|
||||||
%Room{state: serialized_state_set} = room ->
|
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}
|
||||||
|
@ -55,8 +61,7 @@ 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})
|
||||||
|
@ -111,9 +116,10 @@ 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()) :: :ok | {:error, atom()}
|
@spec invite(pid(), Account.t(), String.t(), String.t() | nil, String.t() | nil) ::
|
||||||
def invite(pid, account, user_id) do
|
:ok | {:error, atom()}
|
||||||
GenServer.call(pid, {:invite, account, user_id})
|
def invite(pid, account, user_id, avatar_url, displayname) do
|
||||||
|
GenServer.call(pid, {:invite, account, user_id, avatar_url, displayname})
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -135,25 +141,28 @@ 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()) :: :ok | {:error, atom()}
|
@spec kick(pid(), Account.t(), Kick.t(), String.t() | nil, String.t() | nil) ::
|
||||||
def kick(pid, account, request) do
|
:ok | {:error, atom()}
|
||||||
GenServer.call(pid, {:kick, account, request})
|
def kick(pid, account, request, avatar_url, displayname) do
|
||||||
|
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()) :: :ok | {:error, atom()}
|
@spec ban(pid(), Account.t(), Ban.t(), String.t() | nil, String.t() | nil) ::
|
||||||
def ban(pid, account, request) do
|
:ok | {:error, atom()}
|
||||||
GenServer.call(pid, {:ban, account, request})
|
def ban(pid, account, request, avatar_url, displayname) do
|
||||||
|
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()) :: :ok | {:error, atom()}
|
@spec unban(pid(), Account.t(), String.t(), String.t() | nil, String.t() | nil) ::
|
||||||
def unban(pid, account, user_id) do
|
:ok | {:error, atom()}
|
||||||
GenServer.call(pid, {:unban, account, user_id})
|
def unban(pid, account, user_id, avatar_url, displayname) do
|
||||||
|
GenServer.call(pid, {:unban, account, user_id, avatar_url, displayname})
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -182,44 +191,64 @@ 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
|
||||||
room = Keyword.fetch!(opts, :room)
|
{:ok, %{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, request},
|
{:create_room, account, %CreateRoom{room_alias_name: room_alias_name} = request},
|
||||||
_from,
|
_from,
|
||||||
%{room: %Room{id: room_id} = room} = state
|
%{room: %Room{id: room_id} = room} = state
|
||||||
) do
|
) do
|
||||||
case Repo.transaction(create_room_insert_events(room, account, request)) do
|
create_alias_result =
|
||||||
{:ok, {state_set, room}} ->
|
if room_alias_name do
|
||||||
{:reply, {:ok, room_id}, %{state | state_set: state_set, room: room}}
|
Alias.create(room_alias_name, room_id)
|
||||||
|
else
|
||||||
|
{:ok, nil}
|
||||||
|
end
|
||||||
|
|
||||||
{:error, reason} ->
|
case create_alias_result do
|
||||||
{:reply, {:error, reason}, state}
|
{:ok, alias_} ->
|
||||||
|
events = create_room_events(room, account, request, alias_)
|
||||||
|
|
||||||
_ ->
|
case Repo.transaction(process_events(room, events)) do
|
||||||
{:reply, {:error, :unknown}, state}
|
{:ok, room} ->
|
||||||
|
{: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, %{state_set: state_set} = state) do
|
def handle_call({:server_in_room?, domain}, _from, %{room: %Room{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"}}} ->
|
||||||
|
@ -272,11 +301,15 @@ defmodule Architex.RoomServer do
|
||||||
{:reply, {state_events, auth_chain}, state}
|
{:reply, {state_events, auth_chain}, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_call({:invite, account, user_id}, _from, %{room: room, state_set: state_set} = state) do
|
def handle_call(
|
||||||
invite_event = Event.Invite.new(room, account, user_id)
|
{:invite, account, user_id, avatar_url, displayname},
|
||||||
|
_from,
|
||||||
|
%{room: room} = state
|
||||||
|
) do
|
||||||
|
invite_event = Event.Invite.new(room, account, user_id, avatar_url, displayname)
|
||||||
|
|
||||||
case Repo.transaction(insert_single_event(room, state_set, invite_event)) do
|
case Repo.transaction(process_event(room, invite_event)) do
|
||||||
{:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}}
|
{:ok, {room, _}} -> {:reply, :ok, %{state | room: room}}
|
||||||
{:error, reason} -> {:reply, {:error, reason}, state}
|
{:error, reason} -> {:reply, {:error, reason}, state}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -284,59 +317,64 @@ defmodule Architex.RoomServer do
|
||||||
def handle_call(
|
def handle_call(
|
||||||
{:join, account},
|
{:join, account},
|
||||||
_from,
|
_from,
|
||||||
%{room: %Room{id: room_id} = room, state_set: state_set} = state
|
%{room: %Room{id: room_id} = room} = state
|
||||||
) do
|
) do
|
||||||
join_event = Event.Join.new(room, account)
|
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, join_event)) do
|
||||||
{:ok, {state_set, room, _}} ->
|
{:ok, {room, _}} ->
|
||||||
{:reply, {:ok, room_id}, %{state | state_set: state_set, room: room}}
|
{:reply, {:ok, room_id}, %{state | 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_set: state_set} = state) do
|
def handle_call({:leave, account}, _from, %{room: room} = state) do
|
||||||
leave_event = Event.Leave.new(room, account)
|
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, leave_event)) do
|
||||||
{:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}}
|
{:ok, {room, _}} -> {:reply, :ok, %{state | 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}},
|
{:kick, account, %Kick{user_id: user_id, reason: reason}, avatar_url, displayname},
|
||||||
_from,
|
_from,
|
||||||
%{room: room, state_set: state_set} = state
|
%{room: room} = state
|
||||||
) do
|
) do
|
||||||
kick_event = Event.Kick.new(room, account, user_id, reason)
|
kick_event =
|
||||||
|
Event.Kick.new(room, account, to_string(user_id), avatar_url, displayname, reason)
|
||||||
|
|
||||||
case Repo.transaction(insert_single_event(room, state_set, kick_event)) do
|
case Repo.transaction(process_event(room, kick_event)) do
|
||||||
{:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}}
|
{:ok, {room, _}} -> {:reply, :ok, %{state | 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, %Kick{user_id: user_id, reason: reason}},
|
{:ban, account, %Ban{user_id: user_id, reason: reason}, avatar_url, displayname},
|
||||||
_from,
|
_from,
|
||||||
%{room: room, state_set: state_set} = state
|
%{room: room} = state
|
||||||
) do
|
) do
|
||||||
ban_event = Event.Ban.new(room, account, user_id, reason)
|
ban_event = Event.Ban.new(room, account, to_string(user_id), avatar_url, displayname, reason)
|
||||||
|
|
||||||
case Repo.transaction(insert_single_event(room, state_set, ban_event)) do
|
case Repo.transaction(process_event(room, ban_event)) do
|
||||||
{:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}}
|
{:ok, {room, _}} -> {:reply, :ok, %{state | room: room}}
|
||||||
{:error, reason} -> {:reply, {:error, reason}, state}
|
{:error, reason} -> {:reply, {:error, reason}, state}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_call({:unban, account, user_id}, _from, %{room: room, state_set: state_set} = state) do
|
def handle_call(
|
||||||
unban_event = Event.Unban.new(room, account, user_id)
|
{:unban, account, user_id, avatar_url, displayname},
|
||||||
|
_from,
|
||||||
|
%{room: room} = state
|
||||||
|
) do
|
||||||
|
unban_event = Event.Unban.new(room, account, user_id, avatar_url, displayname)
|
||||||
|
|
||||||
case Repo.transaction(insert_single_event(room, state_set, unban_event)) do
|
case Repo.transaction(process_event(room, unban_event)) do
|
||||||
{:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}}
|
{:ok, {room, _}} -> {:reply, :ok, %{state | room: room}}
|
||||||
{:error, reason} -> {:reply, {:error, reason}, state}
|
{:error, reason} -> {:reply, {:error, reason}, state}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -344,7 +382,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} = state
|
%{room: %Room{state_set: state_set} = room} = 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}}} ->
|
||||||
|
@ -364,13 +402,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_set: state_set} = state
|
%{room: room} = 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(insert_event_with_txn(state_set, room, device, message_event, txn_id)) do
|
case Repo.transaction(process_event_with_txn(room, device, message_event, txn_id)) do
|
||||||
{:ok, {state_set, room, event_id}} ->
|
{:ok, {room, event_id}} ->
|
||||||
{:reply, {:ok, event_id}, %{state | state_set: state_set, room: room}}
|
{:reply, {:ok, event_id}, %{state | room: room}}
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
{:reply, {:error, reason}, state}
|
{:reply, {:error, reason}, state}
|
||||||
|
@ -380,23 +418,76 @@ 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_set: state_set} = state
|
%{room: room} = 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(insert_single_event(room, state_set, state_event)) do
|
case Repo.transaction(process_event(room, state_event)) do
|
||||||
{:ok, {state_set, room, %Event{id: event_id}}} ->
|
{:ok, {room, %Event{id: event_id}}} ->
|
||||||
{:reply, {:ok, event_id}, %{state | state_set: state_set, room: room}}
|
{:reply, {:ok, event_id}, %{state | room: room}}
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
{:reply, {:error, reason}, state}
|
{:reply, {:error, reason}, state}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec insert_event_with_txn(t(), Room.t(), Device.t(), %Event{}, String.t()) ::
|
def handle_call(
|
||||||
(() -> {t(), Room.t(), String.t()} | {:error, atom()})
|
{:get_current_state, account},
|
||||||
defp insert_event_with_txn(
|
_from,
|
||||||
state_set,
|
%{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"}} ->
|
||||||
|
# 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,
|
||||||
|
@ -409,98 +500,107 @@ 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} ->
|
||||||
{state_set, room, event_id}
|
{room, event_id}
|
||||||
|
|
||||||
nil ->
|
nil ->
|
||||||
with {state_set, room, %Event{id: event_id}} <-
|
with {room, %Event{id: event_id}} <- process_event(room, message_event).() do
|
||||||
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!()
|
||||||
|
|
||||||
{state_set, room, event_id}
|
{room, event_id}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec insert_single_event(Room.t(), t(), %Event{}) ::
|
@spec process_event(Room.t(), %Event{}) :: (() -> {Room.t(), Event.t()} | {:error, atom()})
|
||||||
(() -> {t(), Room.t(), Event.t()} | {:error, atom()})
|
defp process_event(room, event) do
|
||||||
defp insert_single_event(room, state_set, event) do
|
|
||||||
fn ->
|
fn ->
|
||||||
case finalize_and_insert_event(event, state_set, room) do
|
case finalize_and_process_event(event, room) do
|
||||||
{:ok, state_set, room, event} ->
|
{:ok, room, event} -> {room, event}
|
||||||
_ = update_room_state_set(room, state_set)
|
{:error, reason} -> Repo.rollback(reason)
|
||||||
{state_set, room, event}
|
|
||||||
|
|
||||||
{:error, reason} ->
|
|
||||||
Repo.rollback(reason)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Get a function that inserts all events for room creation.
|
@spec process_events(Room.t(), [%Event{}]) :: (() -> Room.t() | {:error, atom()})
|
||||||
@spec create_room_insert_events(Room.t(), Account.t(), CreateRoom.t()) ::
|
defp process_events(room, events) do
|
||||||
(() -> {: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 ->
|
||||||
result =
|
Enum.reduce_while(events, room, fn event, room ->
|
||||||
Enum.reduce_while(events, {%{}, room}, fn event, {state_set, room} ->
|
case finalize_and_process_event(event, room) do
|
||||||
case finalize_and_insert_event(event, state_set, room) do
|
{:ok, room, _} -> {:cont, room}
|
||||||
{:ok, state_set, room, _} -> {:cont, {state_set, room}}
|
{:error, reason} -> {:halt, {:error, reason}}
|
||||||
{:error, reason} -> {:halt, {:error, reason}}
|
end
|
||||||
end
|
end)
|
||||||
end)
|
|> then(fn
|
||||||
|
{:error, reason} -> Repo.rollback(reason)
|
||||||
case result do
|
room -> room
|
||||||
{:error, reason} ->
|
end)
|
||||||
Repo.rollback(reason)
|
|
||||||
|
|
||||||
{state_set, room} ->
|
|
||||||
_ = update_room_state_set(room, state_set)
|
|
||||||
{state_set, room}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Update the given room in the database with the given state set.
|
@spec create_room_events(Room.t(), Account.t(), CreateRoom.t(), Alias.t() | nil) :: [%Event{}]
|
||||||
@spec update_room_state_set(Room.t(), t()) :: Room.t()
|
defp create_room_events(
|
||||||
defp update_room_state_set(room, state_set) do
|
room,
|
||||||
# TODO: We might as well hold state in the Room struct,
|
account,
|
||||||
# instead of the state_set state.
|
%CreateRoom{
|
||||||
# Create custom type for this.
|
room_version: room_version,
|
||||||
serialized_state_set =
|
preset: preset,
|
||||||
Enum.map(state_set, fn {{type, state_key}, %Event{id: event_id}} ->
|
name: name,
|
||||||
[type, state_key, event_id]
|
topic: topic,
|
||||||
|
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)
|
||||||
|
|
||||||
Repo.update!(change(room, state: serialized_state_set))
|
preset_events =
|
||||||
|
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 =
|
||||||
|
@ -528,11 +628,24 @@ 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(), [String.t()] | nil, Room.t()) :: [%Event{}]
|
@spec room_creation_invite_events(Account.t(), [UserId.t()] | nil, Room.t(), boolean() | nil) ::
|
||||||
defp room_creation_invite_events(_, nil, _), do: []
|
[%Event{}]
|
||||||
|
defp room_creation_invite_events(_, nil, _, _), do: []
|
||||||
|
|
||||||
defp room_creation_invite_events(account, invite_user_ids, room) do
|
defp room_creation_invite_events(account, invite_user_ids, room, is_direct) do
|
||||||
Enum.map(invite_user_ids, &Event.Invite.new(room, account, &1))
|
Enum.map(invite_user_ids, fn user_id ->
|
||||||
|
{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.
|
||||||
|
@ -542,12 +655,11 @@ defmodule Architex.RoomServer do
|
||||||
# - Content hash
|
# - Content hash
|
||||||
# - Event ID
|
# - Event ID
|
||||||
# - Signature
|
# - Signature
|
||||||
@spec finalize_and_insert_event(%Event{}, t(), Room.t()) ::
|
@spec finalize_and_process_event(%Event{}, Room.t()) ::
|
||||||
{:ok, t(), Room.t(), Event.t()} | {:error, atom()}
|
{:ok, Room.t(), Event.t()} | {:error, atom()}
|
||||||
defp finalize_and_insert_event(
|
defp finalize_and_process_event(
|
||||||
event,
|
event,
|
||||||
state_set,
|
%Room{forward_extremities: forward_extremities, state_set: state_set} = room
|
||||||
%Room{forward_extremities: forward_extremities} = room
|
|
||||||
) do
|
) do
|
||||||
event =
|
event =
|
||||||
event
|
event
|
||||||
|
@ -556,7 +668,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_insert_event(event, state_set, room)
|
{:ok, event} -> authenticate_and_process_event(event, room)
|
||||||
_ -> {:error, :event_creation}
|
_ -> {:error, :event_creation}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -571,7 +683,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{}, t()) :: [Event.t()]
|
@spec auth_events_for_event(%Event{}, StateSet.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(
|
||||||
|
@ -623,9 +735,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_insert_event(Event.t(), t(), Room.t()) ::
|
@spec authenticate_and_process_event(Event.t(), Room.t()) ::
|
||||||
{:ok, t(), Room.t(), Event.t()} | {:error, atom()}
|
{:ok, Room.t(), Event.t()} | {:error, atom()}
|
||||||
defp authenticate_and_insert_event(event, current_state_set, room) do
|
defp authenticate_and_process_event(event, %Room{state_set: 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.
|
||||||
|
@ -643,8 +755,9 @@ 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, state_set, room, event}
|
{:ok, room, event}
|
||||||
else
|
else
|
||||||
_ -> {:error, :authorization}
|
_ -> {:error, :authorization}
|
||||||
end
|
end
|
||||||
|
@ -655,7 +768,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(), t()) :: :ok
|
@spec update_membership(Room.t(), StateSet.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()
|
||||||
|
|
||||||
|
|
|
@ -6,13 +6,18 @@ 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()
|
||||||
|
|
|
@ -314,4 +314,15 @@ 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
|
||||||
|
|
|
@ -4,33 +4,10 @@ defmodule Architex.Event.Formatters do
|
||||||
"""
|
"""
|
||||||
alias Architex.Event
|
alias Architex.Event
|
||||||
|
|
||||||
@spec messages_response(Event.t()) :: map()
|
@doc """
|
||||||
def messages_response(%Event{
|
Event format with keys that all formats have in common.
|
||||||
content: content,
|
"""
|
||||||
type: type,
|
def base_client_response(%Event{
|
||||||
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,
|
||||||
|
@ -46,11 +23,37 @@ defmodule Architex.Event.Formatters do
|
||||||
origin_server_ts: origin_server_ts
|
origin_server_ts: origin_server_ts
|
||||||
}
|
}
|
||||||
|
|
||||||
data = if unsigned, do: Map.put(data, :unsigned, unsigned), else: 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,
|
||||||
|
|
|
@ -2,16 +2,21 @@ 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(room, %Account{localpart: localpart} = sender) do
|
def new(
|
||||||
|
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
|
||||||
|
@ -19,24 +24,38 @@ end
|
||||||
defmodule Architex.Event.CreateRoom do
|
defmodule Architex.Event.CreateRoom do
|
||||||
alias Architex.{Event, Account, Room}
|
alias Architex.{Event, Account, Room}
|
||||||
|
|
||||||
@spec new(Room.t(), Account.t(), String.t()) :: %Event{}
|
@clobber_content_keys ["creator", "room_version"]
|
||||||
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
|
||||||
|
|
||||||
|
@ -50,26 +69,49 @@ defmodule Architex.Event.PowerLevels do
|
||||||
@users_default 0
|
@users_default 0
|
||||||
@notifications_room 50
|
@notifications_room 50
|
||||||
|
|
||||||
@spec create_room_new(Room.t(), Account.t(), CreateRoom.plco_t()) :: %Event{}
|
@spec create_room_new(
|
||||||
def create_room_new(room, sender, nil) do
|
Room.t(),
|
||||||
create_room_new(room, sender, %PowerLevelContentOverride{})
|
Account.t(),
|
||||||
|
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(room, %Account{localpart: localpart} = sender, %PowerLevelContentOverride{
|
def create_room_new(
|
||||||
ban: ban_override,
|
room,
|
||||||
events: events_override,
|
%Account{localpart: localpart} = sender,
|
||||||
events_default: events_default_override,
|
%PowerLevelContentOverride{
|
||||||
invite: invite_override,
|
ban: ban_override,
|
||||||
kick: kick_override,
|
events: events_override,
|
||||||
redact: redact_override,
|
events_default: events_default_override,
|
||||||
state_default: state_default_override,
|
invite: invite_override,
|
||||||
users: users_override,
|
kick: kick_override,
|
||||||
users_default: users_default_override,
|
redact: redact_override,
|
||||||
notifications: notifications_override
|
state_default: state_default_override,
|
||||||
}) do
|
users: users_override,
|
||||||
|
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
|
||||||
|
@ -180,15 +222,26 @@ end
|
||||||
defmodule Architex.Event.Invite do
|
defmodule Architex.Event.Invite do
|
||||||
alias Architex.{Event, Account, Room}
|
alias Architex.{Event, Account, Room}
|
||||||
|
|
||||||
@spec new(Room.t(), Account.t(), String.t()) :: %Event{}
|
@spec new(
|
||||||
def new(room, sender, user_id) do
|
Room.t(),
|
||||||
|
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
|
||||||
|
@ -197,14 +250,16 @@ 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, sender) do
|
def new(room, %Account{avatar_url: avatar_url, displayname: displayname} = 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
|
||||||
|
@ -212,9 +267,19 @@ end
|
||||||
defmodule Architex.Event.Kick do
|
defmodule Architex.Event.Kick do
|
||||||
alias Architex.{Event, Account, Room}
|
alias Architex.{Event, Account, Room}
|
||||||
|
|
||||||
@spec new(Room.t(), Account.t(), String.t(), String.t() | nil) :: %Event{}
|
@spec new(
|
||||||
def new(room, sender, user_id, reason \\ nil) do
|
Room.t(),
|
||||||
content = %{"membership" => "leave"}
|
Account.t(),
|
||||||
|
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{
|
||||||
|
@ -229,9 +294,19 @@ end
|
||||||
defmodule Architex.Event.Ban do
|
defmodule Architex.Event.Ban do
|
||||||
alias Architex.{Event, Account, Room}
|
alias Architex.{Event, Account, Room}
|
||||||
|
|
||||||
@spec new(Room.t(), Account.t(), String.t(), String.t() | nil) :: %Event{}
|
@spec new(
|
||||||
def new(room, sender, user_id, reason \\ nil) do
|
Room.t(),
|
||||||
content = %{"membership" => "ban"}
|
Account.t(),
|
||||||
|
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{
|
||||||
|
@ -245,15 +320,36 @@ 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{}
|
|
||||||
def new(room, sender, user_id) do
|
@spec new(Room.t(), Account.t(), String.t(), String.t() | nil, String.t() | nil) :: %Event{}
|
||||||
|
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
|
||||||
|
|
|
@ -5,18 +5,19 @@ 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: list(list(String.t())),
|
state_set: StateSet.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, {:array, {:array, :string}}
|
field :state_set, StateSet, load_in_query: false
|
||||||
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
|
||||||
|
|
45
lib/architex/types/state_set.ex
Normal file
45
lib/architex/types/state_set.ex
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
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
|
|
@ -1,7 +1,11 @@
|
||||||
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(),
|
||||||
|
@ -53,4 +57,37 @@ 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
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
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: String.t(),
|
user_id: UserId.t(),
|
||||||
reason: String.t() | nil
|
reason: String.t() | nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@primary_key false
|
@primary_key false
|
||||||
embedded_schema do
|
embedded_schema do
|
||||||
field :user_id, :string
|
field :user_id, UserId
|
||||||
field :reason, :string
|
field :reason, :string
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,45 +1,43 @@
|
||||||
defmodule ArchitexWeb.Client.Request.CreateRoom do
|
defmodule ArchitexWeb.Client.Request.CreateRoom do
|
||||||
use ArchitexWeb.APIRequest
|
use ArchitexWeb.APIRequest
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
alias Architex.Types.UserId
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
@type plco_t :: %__MODULE__.PowerLevelContentOverride{
|
defmodule PowerLevelContentOverride do
|
||||||
ban: integer() | nil,
|
use Ecto.Schema
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
@type plco_n_t :: %__MODULE__.PowerLevelContentOverride.Notifications{
|
defmodule Notifications do
|
||||||
room: integer() | nil
|
use Ecto.Schema
|
||||||
}
|
|
||||||
|
|
||||||
@primary_key false
|
@type t :: %__MODULE__{
|
||||||
embedded_schema do
|
room: integer() | nil
|
||||||
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
|
|
||||||
|
|
||||||
embeds_one :power_level_content_override, PowerLevelContentOverride, primary_key: false do
|
@primary_key false
|
||||||
|
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
|
||||||
|
@ -50,13 +48,81 @@ 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, primary_key: false do
|
embeds_one :notifications, Notifications
|
||||||
field :room, :integer
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: unimplemented:
|
def changeset(data, params) do
|
||||||
# creation_content, initial_state, invite_3pid, initial_state, is_direct
|
data
|
||||||
|
|> 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
|
||||||
|
@ -68,35 +134,15 @@ 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: &power_level_content_override_changeset/2,
|
with: &PowerLevelContentOverride.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
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
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: String.t(),
|
user_id: UserId.t(),
|
||||||
reason: String.t() | nil
|
reason: String.t() | nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@primary_key false
|
@primary_key false
|
||||||
embedded_schema do
|
embedded_schema do
|
||||||
field :user_id, :string
|
field :user_id, UserId
|
||||||
field :reason, :string
|
field :reason, :string
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,9 @@ 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
|
||||||
|
@ -60,9 +63,11 @@ 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, _} <- UserId.cast(user_id),
|
with {:ok, user_id_struct} <- UserId.cast(user_id),
|
||||||
{:ok, pid} <- RoomServer.get_room_server(room_id) do
|
{:ok, pid} <- RoomServer.get_room_server(room_id) do
|
||||||
case RoomServer.invite(pid, account, user_id) do
|
{avatar_url, displayname} = UserId.try_get_user_information(user_id_struct)
|
||||||
|
|
||||||
|
case RoomServer.invite(pid, account, user_id, avatar_url, displayname) do
|
||||||
:ok ->
|
:ok ->
|
||||||
conn
|
conn
|
||||||
|> send_resp(200, [])
|
|> send_resp(200, [])
|
||||||
|
@ -132,9 +137,11 @@ 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, request} <- Kick.parse(params),
|
with {:ok, %Kick{user_id: user_id} = request} <- Kick.parse(params),
|
||||||
{:ok, pid} <- RoomServer.get_room_server(room_id) do
|
{:ok, pid} <- RoomServer.get_room_server(room_id) do
|
||||||
case RoomServer.kick(pid, account, request) do
|
{avatar_url, displayname} = UserId.try_get_user_information(user_id)
|
||||||
|
|
||||||
|
case RoomServer.kick(pid, account, request, avatar_url, displayname) do
|
||||||
:ok ->
|
:ok ->
|
||||||
conn
|
conn
|
||||||
|> send_resp(200, [])
|
|> send_resp(200, [])
|
||||||
|
@ -155,9 +162,11 @@ 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, request} <- Ban.parse(params),
|
with {:ok, %Ban{user_id: user_id} = request} <- Ban.parse(params),
|
||||||
{:ok, pid} <- RoomServer.get_room_server(room_id) do
|
{:ok, pid} <- RoomServer.get_room_server(room_id) do
|
||||||
case RoomServer.ban(pid, account, request) do
|
{avatar_url, displayname} = UserId.try_get_user_information(user_id)
|
||||||
|
|
||||||
|
case RoomServer.ban(pid, account, request, avatar_url, displayname) do
|
||||||
:ok ->
|
:ok ->
|
||||||
conn
|
conn
|
||||||
|> send_resp(200, [])
|
|> send_resp(200, [])
|
||||||
|
@ -181,20 +190,22 @@ defmodule ArchitexWeb.Client.RoomController do
|
||||||
"room_id" => room_id,
|
"room_id" => room_id,
|
||||||
"user_id" => user_id
|
"user_id" => user_id
|
||||||
}) do
|
}) do
|
||||||
case RoomServer.get_room_server(room_id) do
|
with {:ok, user_id_struct} <- UserId.cast(user_id),
|
||||||
{:ok, pid} ->
|
{:ok, pid} <- RoomServer.get_room_server(room_id) do
|
||||||
case RoomServer.unban(pid, account, user_id) do
|
{avatar_url, displayname} = UserId.try_get_user_information(user_id_struct)
|
||||||
:ok ->
|
|
||||||
conn
|
|
||||||
|> send_resp(200, [])
|
|
||||||
|> halt()
|
|
||||||
|
|
||||||
{:error, _} ->
|
case RoomServer.unban(pid, account, user_id, avatar_url, displayname) do
|
||||||
put_error(conn, :unknown)
|
:ok ->
|
||||||
end
|
conn
|
||||||
|
|> send_resp(200, [])
|
||||||
|
|> halt()
|
||||||
|
|
||||||
{:error, :not_found} ->
|
{:error, _} ->
|
||||||
put_error(conn, :not_found, "The given room was not found.")
|
put_error(conn, :unknown)
|
||||||
|
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
|
||||||
|
|
||||||
|
@ -300,4 +311,75 @@ 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
|
||||||
|
|
|
@ -14,6 +14,7 @@ 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."},
|
||||||
|
|
|
@ -33,6 +33,8 @@ 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]},
|
||||||
|
|
|
@ -61,13 +61,16 @@ 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
|
||||||
|
@ -87,7 +90,15 @@ 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
|
||||||
|
|
|
@ -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, {:array, {:array, :string}}, default: [], null: false
|
add :state_set, {: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
|
||||||
|
|
Loading…
Reference in a new issue