From d23a42e29cdeb7c94bffbc5ec0cbd45ee99ffd4f Mon Sep 17 00:00:00 2001 From: Pim Kunis Date: Sat, 11 Sep 2021 17:38:25 +0200 Subject: [PATCH 01/10] Implement is_direct option for client room creation --- README.md | 2 +- lib/architex/room_server.ex | 15 ++++++++------- lib/architex/schema/event/generators.ex | 11 ++++++----- .../api_schemas/client/request/create_room.ex | 7 +++++-- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 8b1044f..317a3c7 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Here, implemented and some unimplemented features are listed. - GET /_matrix/client/r0/login - POST /_matrix/client/r0/login: Only with password flow. - POST /_matrix/client/r0/register: Only with dummy flow. -- POST /_matrix/client/r0/createRoom: Only with optional parameters name, topic, preset and invite. +- POST /_matrix/client/r0/createRoom: Except creation_content, initial_state, invite_3pid and initial_state. - GET /_matrix/client/r0/joined_rooms - POST /_matrix/client/r0/rooms/{roomId}/invite - POST /_matrix/client/r0/rooms/{roomId}/join: Except with third party invite. diff --git a/lib/architex/room_server.ex b/lib/architex/room_server.ex index fe25ee4..c7a6c32 100644 --- a/lib/architex/room_server.ex +++ b/lib/architex/room_server.ex @@ -448,9 +448,9 @@ defmodule Architex.RoomServer do name: name, topic: topic, invite: invite, - power_level_content_override: power_level_content_override + power_level_content_override: power_level_content_override, + is_direct: is_direct }) do - events = ([ Event.CreateRoom.new(room, account, room_version), @@ -461,7 +461,7 @@ defmodule Architex.RoomServer do [ 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)) + ] ++ room_creation_invite_events(account, invite, room, is_direct)) |> Enum.reject(&Kernel.is_nil/1) fn -> @@ -528,11 +528,12 @@ defmodule Architex.RoomServer do end # Get the events for room creation for inviting other users. - @spec room_creation_invite_events(Account.t(), [String.t()] | nil, Room.t()) :: [%Event{}] - defp room_creation_invite_events(_, nil, _), do: [] + @spec room_creation_invite_events(Account.t(), [String.t()] | nil, Room.t(), boolean() | nil) :: + [%Event{}] + defp room_creation_invite_events(_, nil, _, _), do: [] - defp room_creation_invite_events(account, invite_user_ids, room) do - Enum.map(invite_user_ids, &Event.Invite.new(room, account, &1)) + defp room_creation_invite_events(account, invite_user_ids, room, is_direct) do + Enum.map(invite_user_ids, &Event.Invite.new(room, account, &1, is_direct)) end # Finalize the event struct and insert it into the room's state using state resolution. diff --git a/lib/architex/schema/event/generators.ex b/lib/architex/schema/event/generators.ex index 24e40aa..4edb6fd 100644 --- a/lib/architex/schema/event/generators.ex +++ b/lib/architex/schema/event/generators.ex @@ -180,15 +180,16 @@ end defmodule Architex.Event.Invite do 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(), boolean() | nil) :: %Event{} + def new(room, sender, user_id, is_direct \\ nil) do + content = %{"membership" => "invite"} + content = if is_direct != nil, do: Map.put(content, "is_direct", is_direct), else: content + %Event{ Event.new(room, sender) | type: "m.room.member", state_key: user_id, - content: %{ - "membership" => "invite" - } + content: content } end end diff --git a/lib/architex_web/api_schemas/client/request/create_room.ex b/lib/architex_web/api_schemas/client/request/create_room.ex index 4a57ec3..f84ec95 100644 --- a/lib/architex_web/api_schemas/client/request/create_room.ex +++ b/lib/architex_web/api_schemas/client/request/create_room.ex @@ -9,6 +9,7 @@ defmodule ArchitexWeb.Client.Request.CreateRoom do invite: list(String.t()) | nil, room_version: String.t() | nil, preset: String.t() | nil, + is_direct: boolean() | nil, power_level_content_override: plco_t() | nil } @@ -38,6 +39,7 @@ defmodule ArchitexWeb.Client.Request.CreateRoom do field :invite, {:array, :string} field :room_version, :string field :preset, :string + field :is_direct, :boolean embeds_one :power_level_content_override, PowerLevelContentOverride, primary_key: false do field :ban, :integer @@ -56,7 +58,7 @@ defmodule ArchitexWeb.Client.Request.CreateRoom do end # TODO: unimplemented: - # creation_content, initial_state, invite_3pid, initial_state, is_direct + # creation_content, initial_state, invite_3pid, initial_state end def changeset(data, params) do @@ -68,7 +70,8 @@ defmodule ArchitexWeb.Client.Request.CreateRoom do :topic, :invite, :room_version, - :preset + :preset, + :is_direct ]) |> cast_embed(:power_level_content_override, with: &power_level_content_override_changeset/2, From 8dfd770fd00f357fbb38350e615c82a8260d51f9 Mon Sep 17 00:00:00 2001 From: Pim Kunis Date: Sat, 11 Sep 2021 18:23:51 +0200 Subject: [PATCH 02/10] Implement creation_content option for client room creation --- README.md | 2 +- lib/architex/room_server.ex | 5 ++-- lib/architex/schema/event/generators.ex | 25 ++++++++++++++----- .../api_schemas/client/request/create_room.ex | 8 +++--- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 317a3c7..5290f5c 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Here, implemented and some unimplemented features are listed. - GET /_matrix/client/r0/login - POST /_matrix/client/r0/login: Only with password flow. - POST /_matrix/client/r0/register: Only with dummy flow. -- POST /_matrix/client/r0/createRoom: Except creation_content, initial_state, invite_3pid and initial_state. +- POST /_matrix/client/r0/createRoom: Except with options invite_3pid, initial_state and room_alias_name. - GET /_matrix/client/r0/joined_rooms - POST /_matrix/client/r0/rooms/{roomId}/invite - POST /_matrix/client/r0/rooms/{roomId}/join: Except with third party invite. diff --git a/lib/architex/room_server.ex b/lib/architex/room_server.ex index c7a6c32..c74a8a0 100644 --- a/lib/architex/room_server.ex +++ b/lib/architex/room_server.ex @@ -449,11 +449,12 @@ defmodule Architex.RoomServer do topic: topic, invite: invite, power_level_content_override: power_level_content_override, - is_direct: is_direct + is_direct: is_direct, + creation_content: creation_content }) do events = ([ - Event.CreateRoom.new(room, account, room_version), + 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) ] ++ diff --git a/lib/architex/schema/event/generators.ex b/lib/architex/schema/event/generators.ex index 4edb6fd..37bb9c6 100644 --- a/lib/architex/schema/event/generators.ex +++ b/lib/architex/schema/event/generators.ex @@ -19,18 +19,31 @@ end defmodule Architex.Event.CreateRoom do alias Architex.{Event, Account, Room} - @spec new(Room.t(), Account.t(), String.t()) :: %Event{} - def new(room, %Account{localpart: localpart} = creator, room_version) do + @clobber_content_keys ["creator", "room_version"] + + @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) + 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.new(room, creator) | type: "m.room.create", state_key: "", - content: %{ - "creator" => mxid, - "room_version" => room_version || Architex.default_room_version() - } + content: content } end end diff --git a/lib/architex_web/api_schemas/client/request/create_room.ex b/lib/architex_web/api_schemas/client/request/create_room.ex index f84ec95..95a4f85 100644 --- a/lib/architex_web/api_schemas/client/request/create_room.ex +++ b/lib/architex_web/api_schemas/client/request/create_room.ex @@ -10,6 +10,7 @@ defmodule ArchitexWeb.Client.Request.CreateRoom do room_version: String.t() | nil, preset: String.t() | nil, is_direct: boolean() | nil, + creation_content: %{optional(String.t()) => any()} | nil, power_level_content_override: plco_t() | nil } @@ -40,6 +41,7 @@ defmodule ArchitexWeb.Client.Request.CreateRoom do field :room_version, :string field :preset, :string field :is_direct, :boolean + field :creation_content, :map embeds_one :power_level_content_override, PowerLevelContentOverride, primary_key: false do field :ban, :integer @@ -57,8 +59,7 @@ defmodule ArchitexWeb.Client.Request.CreateRoom do end end - # TODO: unimplemented: - # creation_content, initial_state, invite_3pid, initial_state + # TODO: unimplemented: invite_3pid, initial_state, room_alias_name end def changeset(data, params) do @@ -71,7 +72,8 @@ defmodule ArchitexWeb.Client.Request.CreateRoom do :invite, :room_version, :preset, - :is_direct + :is_direct, + :creation_content ]) |> cast_embed(:power_level_content_override, with: &power_level_content_override_changeset/2, From 56be8ba301ce35aed5f1dec5db1e57bf2114022e Mon Sep 17 00:00:00 2001 From: Pim Kunis Date: Sat, 11 Sep 2021 22:31:00 +0200 Subject: [PATCH 03/10] Give invitees the same power level as the creator during client room creation --- lib/architex/room_server.ex | 10 +++-- lib/architex/schema/event/generators.ex | 53 ++++++++++++++++++------- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/lib/architex/room_server.ex b/lib/architex/room_server.ex index c74a8a0..d20b1a4 100644 --- a/lib/architex/room_server.ex +++ b/lib/architex/room_server.ex @@ -456,7 +456,13 @@ defmodule Architex.RoomServer do ([ 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) + Event.PowerLevels.create_room_new( + room, + account, + power_level_content_override, + invite, + preset + ) ] ++ room_creation_preset(account, preset, room) ++ [ @@ -500,8 +506,6 @@ defmodule Architex.RoomServer do end # 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{}] defp room_creation_preset(account, nil, %Room{visibility: visibility} = room) do preset = diff --git a/lib/architex/schema/event/generators.ex b/lib/architex/schema/event/generators.ex index 37bb9c6..8c8a3c5 100644 --- a/lib/architex/schema/event/generators.ex +++ b/lib/architex/schema/event/generators.ex @@ -63,26 +63,49 @@ defmodule Architex.Event.PowerLevels do @users_default 0 @notifications_room 50 - @spec create_room_new(Room.t(), Account.t(), CreateRoom.plco_t()) :: %Event{} - def create_room_new(room, sender, nil) do - create_room_new(room, sender, %PowerLevelContentOverride{}) + @spec create_room_new( + Room.t(), + Account.t(), + CreateRoom.plco_t(), + [String.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 - def create_room_new(room, %Account{localpart: localpart} = sender, %PowerLevelContentOverride{ - ban: ban_override, - events: events_override, - events_default: events_default_override, - invite: invite_override, - kick: kick_override, - redact: redact_override, - state_default: state_default_override, - users: users_override, - users_default: users_default_override, - notifications: notifications_override - }) do + def create_room_new( + room, + %Account{localpart: localpart} = sender, + %PowerLevelContentOverride{ + ban: ban_override, + events: events_override, + events_default: events_default_override, + invite: invite_override, + kick: kick_override, + redact: redact_override, + state_default: state_default_override, + users: users_override, + users_default: users_default_override, + notifications: notifications_override + }, + invite_ids, + preset + ) do mxid = Architex.get_mxid(localpart) users = %{mxid => @creator} 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, %{}, &{&1, creator_pl}) + Map.merge(users, invite_users_pls) + else + users + end notifications = case notifications_override do From b34fd58cf748cf4de907dcb074f7bb18c6887968 Mon Sep 17 00:00:00 2001 From: Pim Kunis Date: Sun, 12 Sep 2021 11:54:05 +0200 Subject: [PATCH 04/10] Implement initial_state option for client room creation --- lib/architex/room_server.ex | 67 ++++++-- lib/architex/schema/event/generators.ex | 2 +- .../api_schemas/client/request/create_room.ex | 157 +++++++++++------- 3 files changed, 148 insertions(+), 78 deletions(-) diff --git a/lib/architex/room_server.ex b/lib/architex/room_server.ex index d20b1a4..45747f2 100644 --- a/lib/architex/room_server.ex +++ b/lib/architex/room_server.ex @@ -450,26 +450,49 @@ defmodule Architex.RoomServer do invite: invite, power_level_content_override: power_level_content_override, is_direct: is_direct, - creation_content: creation_content + creation_content: creation_content, + initial_state: initial_state }) do + invite_events = room_creation_invite_events(account, invite, room, is_direct) + + name_and_topic_events = + Enum.reject( + [ + if(name, do: Event.Name.new(room, account, name)), + if(topic, do: Event.Topic.new(room, account, topic)) + ], + &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) + + 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 + ) + ] + 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 - ) - ] ++ - 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, is_direct)) - |> Enum.reject(&Kernel.is_nil/1) + basic_events ++ + preset_events ++ initial_state_events ++ name_and_topic_events ++ invite_events fn -> result = @@ -541,6 +564,14 @@ defmodule Architex.RoomServer do Enum.map(invite_user_ids, &Event.Invite.new(room, account, &1, is_direct)) 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 + # Finalize the event struct and insert it into the room's state using state resolution. # The values that are automatically added are: # - Auth events diff --git a/lib/architex/schema/event/generators.ex b/lib/architex/schema/event/generators.ex index 8c8a3c5..0a91216 100644 --- a/lib/architex/schema/event/generators.ex +++ b/lib/architex/schema/event/generators.ex @@ -66,7 +66,7 @@ defmodule Architex.Event.PowerLevels do @spec create_room_new( Room.t(), Account.t(), - CreateRoom.plco_t(), + CreateRoom.PowerLevelContentOverride.t(), [String.t()] | nil, String.t() | nil ) :: %Event{} diff --git a/lib/architex_web/api_schemas/client/request/create_room.ex b/lib/architex_web/api_schemas/client/request/create_room.ex index 95a4f85..046f84f 100644 --- a/lib/architex_web/api_schemas/client/request/create_room.ex +++ b/lib/architex_web/api_schemas/client/request/create_room.ex @@ -1,6 +1,97 @@ defmodule ArchitexWeb.Client.Request.CreateRoom do use ArchitexWeb.APIRequest + defmodule PowerLevelContentOverride do + use Ecto.Schema + + defmodule Notifications do + use Ecto.Schema + + @type t :: %__MODULE__{ + room: integer() | nil + } + + @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 :events, {:map, :integer} + field :events_default, :integer + field :invite, :integer + field :kick, :integer + field :redact, :integer + field :state_default, :integer + field :users, {:map, :integer} + field :users_default, :integer + + embeds_one :notifications, Notifications + end + + def changeset(data, params) do + 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, @@ -11,28 +102,13 @@ defmodule ArchitexWeb.Client.Request.CreateRoom do preset: String.t() | nil, is_direct: boolean() | nil, creation_content: %{optional(String.t()) => any()} | nil, - power_level_content_override: plco_t() | nil - } - - @type plco_t :: %__MODULE__.PowerLevelContentOverride{ - 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: plco_n_t() | nil - } - - @type plco_n_t :: %__MODULE__.PowerLevelContentOverride.Notifications{ - room: integer() | 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 @@ -43,23 +119,8 @@ defmodule ArchitexWeb.Client.Request.CreateRoom do field :is_direct, :boolean field :creation_content, :map - embeds_one :power_level_content_override, PowerLevelContentOverride, primary_key: false do - field :ban, :integer - field :events, {:map, :integer} - field :events_default, :integer - field :invite, :integer - field :kick, :integer - field :redact, :integer - field :state_default, :integer - field :users, {:map, :integer} - field :users_default, :integer - - embeds_one :notifications, Notifications, primary_key: false do - field :room, :integer - end - end - - # TODO: unimplemented: invite_3pid, initial_state, room_alias_name + embeds_many :initial_state, StateEvent + embeds_one :power_level_content_override, PowerLevelContentOverride end def changeset(data, params) do @@ -76,32 +137,10 @@ defmodule ArchitexWeb.Client.Request.CreateRoom do :creation_content ]) |> cast_embed(:power_level_content_override, - with: &power_level_content_override_changeset/2, + with: &PowerLevelContentOverride.changeset/2, required: false ) + |> cast_embed(:initial_state, with: &StateEvent.changeset/2, required: false) |> validate_inclusion(:preset, ["private_chat", "public_chat", "trusted_private_chat"]) 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 From 064a398a37a32c1e8c73da579e298ef039df85c6 Mon Sep 17 00:00:00 2001 From: Pim Kunis Date: Sun, 12 Sep 2021 13:33:06 +0200 Subject: [PATCH 05/10] Implement room_alias_name option for client room creation --- README.md | 2 +- lib/architex/room_server.ex | 152 ++++++++++-------- lib/architex/schema/alias.ex | 9 +- lib/architex/schema/event/generators.ex | 19 +++ .../client/controllers/room_controller.ex | 3 + lib/architex_web/error.ex | 1 + 6 files changed, 118 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 5290f5c..a85079d 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Here, implemented and some unimplemented features are listed. - GET /_matrix/client/r0/login - POST /_matrix/client/r0/login: Only with password flow. - POST /_matrix/client/r0/register: Only with dummy flow. -- POST /_matrix/client/r0/createRoom: Except with options invite_3pid, initial_state and room_alias_name. +- POST /_matrix/client/r0/createRoom: Except with option invite_3pid. - GET /_matrix/client/r0/joined_rooms - POST /_matrix/client/r0/rooms/{roomId}/invite - POST /_matrix/client/r0/rooms/{roomId}/join: Except with third party invite. diff --git a/lib/architex/room_server.ex b/lib/architex/room_server.ex index 45747f2..f6eda8a 100644 --- a/lib/architex/room_server.ex +++ b/lib/architex/room_server.ex @@ -21,7 +21,8 @@ defmodule Architex.RoomServer do Account, Device, DeviceTransaction, - Membership + Membership, + Alias } alias Architex.StateResolution.Authorization @@ -203,19 +204,34 @@ defmodule Architex.RoomServer do @impl true def handle_call( - {:create_room, account, request}, + {:create_room, account, %CreateRoom{room_alias_name: room_alias_name} = request}, _from, %{room: %Room{id: room_id} = room} = state ) do - case Repo.transaction(create_room_insert_events(room, account, request)) do - {:ok, {state_set, room}} -> - {:reply, {:ok, room_id}, %{state | state_set: state_set, room: room}} + create_alias_result = + if room_alias_name do + Alias.create(room_alias_name, room_id) + else + {:ok, nil} + end - {:error, reason} -> - {:reply, {:error, reason}, state} + case create_alias_result do + {:ok, alias_} -> + events = create_room_events(room, account, request, alias_) - _ -> - {:reply, {:error, :unknown}, state} + case Repo.transaction(process_events(room, %{}, events)) do + {:ok, {state_set, room}} -> + {:reply, {:ok, room_id}, %{state | state_set: state_set, room: room}} + + {:error, reason} -> + {:reply, {:error, reason}, state} + + _ -> + {:reply, {:error, :unknown}, state} + end + + {:error, _} -> + {:reply, {:error, :alias}, state} end end @@ -275,7 +291,7 @@ defmodule Architex.RoomServer do def handle_call({:invite, account, user_id}, _from, %{room: room, state_set: state_set} = state) do invite_event = Event.Invite.new(room, account, user_id) - case Repo.transaction(insert_single_event(room, state_set, invite_event)) do + case Repo.transaction(process_event(room, state_set, invite_event)) do {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} {:error, reason} -> {:reply, {:error, reason}, state} end @@ -288,7 +304,7 @@ defmodule Architex.RoomServer do ) do join_event = Event.Join.new(room, account) - case Repo.transaction(insert_single_event(room, state_set, join_event)) do + case Repo.transaction(process_event(room, state_set, join_event)) do {:ok, {state_set, room, _}} -> {:reply, {:ok, room_id}, %{state | state_set: state_set, room: room}} @@ -300,7 +316,7 @@ defmodule Architex.RoomServer do def handle_call({:leave, account}, _from, %{room: room, state_set: state_set} = state) do leave_event = Event.Leave.new(room, account) - case Repo.transaction(insert_single_event(room, state_set, leave_event)) do + case Repo.transaction(process_event(room, state_set, leave_event)) do {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} {:error, reason} -> {:reply, {:error, reason}, state} end @@ -313,7 +329,7 @@ defmodule Architex.RoomServer do ) do kick_event = Event.Kick.new(room, account, user_id, reason) - case Repo.transaction(insert_single_event(room, state_set, kick_event)) do + case Repo.transaction(process_event(room, state_set, kick_event)) do {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} {:error, reason} -> {:reply, {:error, reason}, state} end @@ -326,7 +342,7 @@ defmodule Architex.RoomServer do ) do ban_event = Event.Ban.new(room, account, user_id, reason) - case Repo.transaction(insert_single_event(room, state_set, ban_event)) do + case Repo.transaction(process_event(room, state_set, ban_event)) do {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} {:error, reason} -> {:reply, {:error, reason}, state} end @@ -335,7 +351,7 @@ defmodule Architex.RoomServer do def handle_call({:unban, account, user_id}, _from, %{room: room, state_set: state_set} = state) do unban_event = Event.Unban.new(room, account, user_id) - case Repo.transaction(insert_single_event(room, state_set, unban_event)) do + case Repo.transaction(process_event(room, state_set, unban_event)) do {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} {:error, reason} -> {:reply, {:error, reason}, state} end @@ -368,7 +384,7 @@ defmodule Architex.RoomServer do ) do message_event = Event.custom_event(room, account, event_type, content) - case Repo.transaction(insert_event_with_txn(state_set, room, device, message_event, txn_id)) do + case Repo.transaction(process_event_with_txn(state_set, room, device, message_event, txn_id)) do {:ok, {state_set, room, event_id}} -> {:reply, {:ok, event_id}, %{state | state_set: state_set, room: room}} @@ -384,7 +400,7 @@ defmodule Architex.RoomServer do ) do state_event = Event.custom_state_event(room, account, event_type, content, state_key) - case Repo.transaction(insert_single_event(room, state_set, state_event)) do + case Repo.transaction(process_event(room, state_set, state_event)) do {:ok, {state_set, room, %Event{id: event_id}}} -> {:reply, {:ok, event_id}, %{state | state_set: state_set, room: room}} @@ -393,9 +409,9 @@ defmodule Architex.RoomServer do end end - @spec insert_event_with_txn(t(), Room.t(), Device.t(), %Event{}, String.t()) :: + @spec process_event_with_txn(t(), Room.t(), Device.t(), %Event{}, String.t()) :: (() -> {t(), Room.t(), String.t()} | {:error, atom()}) - defp insert_event_with_txn( + defp process_event_with_txn( state_set, room, %Device{nid: device_nid} = device, @@ -413,7 +429,7 @@ defmodule Architex.RoomServer do nil -> with {state_set, room, %Event{id: event_id}} <- - insert_single_event(room, state_set, message_event).() do + process_event(room, state_set, message_event).() do # Mark this transaction as done. Ecto.build_assoc(device, :device_transactions, txn_id: txn_id, event_id: event_id) |> Repo.insert!() @@ -424,11 +440,11 @@ defmodule Architex.RoomServer do end end - @spec insert_single_event(Room.t(), t(), %Event{}) :: + @spec process_event(Room.t(), t(), %Event{}) :: (() -> {t(), Room.t(), Event.t()} | {:error, atom()}) - defp insert_single_event(room, state_set, event) do + defp process_event(room, state_set, event) do fn -> - case finalize_and_insert_event(event, state_set, room) do + case finalize_and_process_event(event, state_set, room) do {:ok, state_set, room, event} -> _ = update_room_state_set(room, state_set) {state_set, room, event} @@ -439,27 +455,53 @@ defmodule Architex.RoomServer do end end - # Get a function that inserts all events for room creation. - @spec create_room_insert_events(Room.t(), Account.t(), CreateRoom.t()) :: - (() -> {:ok, t(), Room.t()} | {:error, atom()}) - defp create_room_insert_events(room, account, %CreateRoom{ - room_version: room_version, - preset: preset, - name: name, - topic: topic, - invite: invite, - power_level_content_override: power_level_content_override, - is_direct: is_direct, - creation_content: creation_content, - initial_state: initial_state - }) do + @spec process_events(Room.t(), t(), [%Event{}]) :: + (() -> {t(), Room.t()} | {:error, atom()}) + defp process_events(room, state_set, events) do + fn -> + Enum.reduce_while(events, {state_set, room}, fn event, {state_set, room} -> + case finalize_and_process_event(event, state_set, room) do + {:ok, state_set, room, _} -> {:cont, {state_set, room}} + {:error, reason} -> {:halt, {:error, reason}} + end + end) + |> then(fn + {:error, reason} -> + Repo.rollback(reason) + + {state_set, room} -> + _ = update_room_state_set(room, state_set) + {state_set, room} + end) + end + end + + @spec create_room_events(Room.t(), Account.t(), CreateRoom.t(), Alias.t() | nil) :: [%Event{}] + defp create_room_events( + room, + account, + %CreateRoom{ + room_version: room_version, + preset: preset, + name: name, + 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(topic, do: Event.Topic.new(room, account, topic)), + if(alias_, do: Event.CanonicalAlias.new(room, account, alias_.alias)) ], &Kernel.is_nil/1 ) @@ -490,28 +532,8 @@ defmodule Architex.RoomServer do ) ] - events = - basic_events ++ - preset_events ++ initial_state_events ++ name_and_topic_events ++ invite_events - - fn -> - result = - Enum.reduce_while(events, {%{}, room}, fn event, {state_set, room} -> - case finalize_and_insert_event(event, state_set, room) do - {:ok, state_set, room, _} -> {:cont, {state_set, room}} - {:error, reason} -> {:halt, {:error, reason}} - end - end) - - case result do - {:error, reason} -> - Repo.rollback(reason) - - {state_set, room} -> - _ = update_room_state_set(room, state_set) - {state_set, room} - end - end + basic_events ++ + preset_events ++ initial_state_events ++ name_and_topic_events ++ invite_events end # Update the given room in the database with the given state set. @@ -579,9 +601,9 @@ defmodule Architex.RoomServer do # - Content hash # - Event ID # - Signature - @spec finalize_and_insert_event(%Event{}, t(), Room.t()) :: + @spec finalize_and_process_event(%Event{}, t(), Room.t()) :: {:ok, t(), Room.t(), Event.t()} | {:error, atom()} - defp finalize_and_insert_event( + defp finalize_and_process_event( event, state_set, %Room{forward_extremities: forward_extremities} = room @@ -593,7 +615,7 @@ defmodule Architex.RoomServer do |> Map.put(:depth, get_depth(forward_extremities)) case Event.post_process(event) do - {:ok, event} -> authenticate_and_insert_event(event, state_set, room) + {:ok, event} -> authenticate_and_process_event(event, state_set, room) _ -> {:error, :event_creation} end end @@ -660,9 +682,9 @@ defmodule Architex.RoomServer do # Authenticate and insert a new event using state resolution. # Implements the checks as described in the # [Matrix docs](https://matrix.org/docs/spec/server_server/latest#checks-performed-on-receipt-of-a-pdu). - @spec authenticate_and_insert_event(Event.t(), t(), Room.t()) :: + @spec authenticate_and_process_event(Event.t(), t(), Room.t()) :: {:ok, t(), Room.t(), Event.t()} | {:error, atom()} - defp authenticate_and_insert_event(event, current_state_set, room) do + defp authenticate_and_process_event(event, current_state_set, room) do # TODO: Correctly handle soft fails. # Check the following things: # 1. TODO: Is a valid event, otherwise it is dropped. diff --git a/lib/architex/schema/alias.ex b/lib/architex/schema/alias.ex index f82c921..74ba186 100644 --- a/lib/architex/schema/alias.ex +++ b/lib/architex/schema/alias.ex @@ -6,13 +6,18 @@ defmodule Architex.Alias do alias Architex.{Repo, Alias, Room} alias Ecto.Changeset + @type t :: %__MODULE__{ + alias: String.t(), + room_id: String.t() + } + @primary_key {:alias, :string, []} schema "aliases" do belongs_to :room, Room, foreign_key: :room_id, references: :id, type: :string end - def create(alias, room_id) do - change(%Alias{}, alias: alias, room_id: room_id) + def create(alias_, room_id) do + change(%Alias{}, alias: alias_, room_id: room_id) |> assoc_constraint(:room) |> unique_constraint(:alias, name: :aliases_pkey) |> Repo.insert() diff --git a/lib/architex/schema/event/generators.ex b/lib/architex/schema/event/generators.ex index 0a91216..9b1df1a 100644 --- a/lib/architex/schema/event/generators.ex +++ b/lib/architex/schema/event/generators.ex @@ -282,6 +282,7 @@ end defmodule Architex.Event.Unban do alias Architex.{Event, Account, Room} + @spec new(Room.t(), Account.t(), String.t()) :: %Event{} def new(room, sender, user_id) do %Event{ @@ -294,3 +295,21 @@ defmodule Architex.Event.Unban do } end end + +defmodule Architex.Event.CanonicalAlias do + alias Architex.{Event, Account, Room} + + @spec new(Room.t(), Account.t(), String.t() | nil, [String.t()] | nil) :: %Event{} + def new(room, sender, alias_ \\ nil, alt_aliases \\ nil) do + content = %{} + content = if alias_, do: Map.put(content, "alias", alias_), else: content + content = if alt_aliases, do: Map.put(content, "alt_aliases", alt_aliases), else: content + + %Event{ + Event.new(room, sender) + | type: "m.room.canonical_alias", + state_key: "", + content: content + } + end +end diff --git a/lib/architex_web/client/controllers/room_controller.ex b/lib/architex_web/client/controllers/room_controller.ex index c694482..8047cd1 100644 --- a/lib/architex_web/client/controllers/room_controller.ex +++ b/lib/architex_web/client/controllers/room_controller.ex @@ -25,6 +25,9 @@ defmodule ArchitexWeb.Client.RoomController do {:error, :authorization} -> put_error(conn, :invalid_room_state) + {:error, :alias} -> + put_error(conn, :room_in_use, "The requested alias is already in use.") + {:error, :unknown} -> put_error(conn, :unknown) end diff --git a/lib/architex_web/error.ex b/lib/architex_web/error.ex index b8abc5e..7a4bdb3 100644 --- a/lib/architex_web/error.ex +++ b/lib/architex_web/error.ex @@ -14,6 +14,7 @@ defmodule ArchitexWeb.Error do unauthorized: {400, "M_UNAUTHORIZED", "The request was unauthorized."}, invalid_param: {400, "M_INVALID_PARAM", "A request parameter was invalid."}, missing_param: {400, "M_MISSING_PARAM", "A request parameter is missing."}, + room_in_use: {400, "M_ROOM_IN_USE", "The room is already in use."}, unknown_token: {401, "M_UNKNOWN_TOKEN", "Invalid access token."}, missing_token: {401, "M_MISSING_TOKEN", "Access token required."}, not_found: {404, "M_NOT_FOUND", "The requested resource was not found."}, From 739c496ac6c209c9bf2b8256c28d6c88fda705ea Mon Sep 17 00:00:00 2001 From: Pim Kunis Date: Mon, 13 Sep 2021 12:42:56 +0200 Subject: [PATCH 06/10] Put avatar_url and displayname on m.room.member events --- lib/architex/room_server.ex | 63 ++++++++----- lib/architex/schema/event.ex | 11 +++ lib/architex/schema/event/generators.ex | 88 ++++++++++++++----- lib/architex/types/user_id.ex | 37 ++++++++ .../api_schemas/client/request/ban.ex | 6 +- .../api_schemas/client/request/create_room.ex | 6 +- .../api_schemas/client/request/kick.ex | 6 +- .../client/controllers/room_controller.ex | 44 ++++++---- lib/architex_web/federation/http_client.ex | 2 + 9 files changed, 193 insertions(+), 70 deletions(-) diff --git a/lib/architex/room_server.ex b/lib/architex/room_server.ex index f6eda8a..f97ee37 100644 --- a/lib/architex/room_server.ex +++ b/lib/architex/room_server.ex @@ -25,6 +25,8 @@ defmodule Architex.RoomServer do Alias } + alias Architex.Types.UserId + alias Architex.StateResolution.Authorization alias ArchitexWeb.Client.Request.{CreateRoom, Kick, Ban} @@ -112,9 +114,10 @@ defmodule Architex.RoomServer do @doc """ Invite the a user to this room. """ - @spec invite(pid(), Account.t(), String.t()) :: :ok | {:error, atom()} - def invite(pid, account, user_id) do - GenServer.call(pid, {:invite, account, user_id}) + @spec invite(pid(), Account.t(), String.t(), String.t() | nil, String.t() | nil) :: + :ok | {:error, atom()} + def invite(pid, account, user_id, avatar_url, displayname) do + GenServer.call(pid, {:invite, account, user_id, avatar_url, displayname}) end @doc """ @@ -136,25 +139,28 @@ defmodule Architex.RoomServer do @doc """ Kick a user from this room. """ - @spec kick(pid(), Account.t(), Kick.t()) :: :ok | {:error, atom()} - def kick(pid, account, request) do - GenServer.call(pid, {:kick, account, request}) + @spec kick(pid(), Account.t(), Kick.t(), String.t() | nil, String.t() | nil) :: + :ok | {:error, atom()} + def kick(pid, account, request, avatar_url, displayname) do + GenServer.call(pid, {:kick, account, request, avatar_url, displayname}) end @doc """ Ban a user from this room. """ - @spec ban(pid(), Account.t(), Ban.t()) :: :ok | {:error, atom()} - def ban(pid, account, request) do - GenServer.call(pid, {:ban, account, request}) + @spec ban(pid(), Account.t(), Ban.t(), String.t() | nil, String.t() | nil) :: + :ok | {:error, atom()} + def ban(pid, account, request, avatar_url, displayname) do + GenServer.call(pid, {:ban, account, request, avatar_url, displayname}) end @doc """ Unban a user from this room. """ - @spec unban(pid(), Account.t(), String.t()) :: :ok | {:error, atom()} - def unban(pid, account, user_id) do - GenServer.call(pid, {:unban, account, user_id}) + @spec unban(pid(), Account.t(), String.t(), String.t() | nil, String.t() | nil) :: + :ok | {:error, atom()} + def unban(pid, account, user_id, avatar_url, displayname) do + GenServer.call(pid, {:unban, account, user_id, avatar_url, displayname}) end @doc """ @@ -288,8 +294,12 @@ defmodule Architex.RoomServer do {:reply, {state_events, auth_chain}, state} end - def handle_call({:invite, account, user_id}, _from, %{room: room, state_set: state_set} = state) do - invite_event = Event.Invite.new(room, account, user_id) + def handle_call( + {:invite, account, user_id, avatar_url, displayname}, + _from, + %{room: room, state_set: state_set} = state + ) do + invite_event = Event.Invite.new(room, account, user_id, avatar_url, displayname) case Repo.transaction(process_event(room, state_set, invite_event)) do {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} @@ -323,11 +333,12 @@ defmodule Architex.RoomServer do end 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, %{room: room, state_set: state_set} = state ) 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(process_event(room, state_set, kick_event)) do {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} @@ -336,11 +347,11 @@ defmodule Architex.RoomServer do end 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, %{room: room, state_set: state_set} = state ) 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(process_event(room, state_set, ban_event)) do {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} @@ -348,8 +359,12 @@ defmodule Architex.RoomServer do end end - def handle_call({:unban, account, user_id}, _from, %{room: room, state_set: state_set} = state) do - unban_event = Event.Unban.new(room, account, user_id) + def handle_call( + {:unban, account, user_id, avatar_url, displayname}, + _from, + %{room: room, state_set: state_set} = state + ) do + unban_event = Event.Unban.new(room, account, user_id, avatar_url, displayname) case Repo.transaction(process_event(room, state_set, unban_event)) do {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} @@ -578,12 +593,16 @@ defmodule Architex.RoomServer do end # Get the events for room creation for inviting other users. - @spec room_creation_invite_events(Account.t(), [String.t()] | nil, Room.t(), boolean() | nil) :: + @spec room_creation_invite_events(Account.t(), [UserId.t()] | nil, Room.t(), boolean() | nil) :: [%Event{}] defp room_creation_invite_events(_, nil, _, _), 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, is_direct)) + 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: [] diff --git a/lib/architex/schema/event.ex b/lib/architex/schema/event.ex index 174e97c..38bf8da 100644 --- a/lib/architex/schema/event.ex +++ b/lib/architex/schema/event.ex @@ -314,4 +314,15 @@ defmodule Architex.Event do {:ok, :crypto.hash(:sha256, json)} 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 diff --git a/lib/architex/schema/event/generators.ex b/lib/architex/schema/event/generators.ex index 9b1df1a..11429db 100644 --- a/lib/architex/schema/event/generators.ex +++ b/lib/architex/schema/event/generators.ex @@ -2,16 +2,21 @@ defmodule Architex.Event.Join do alias Architex.{Event, Account, Room} @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) + content = + Event.default_membership_content(avatar_url, displayname) + |> Map.put("membership", "join") + %Event{ Event.new(room, sender) | type: "m.room.member", state_key: mxid, - content: %{ - "membership" => "join" - } + content: content } end end @@ -50,6 +55,7 @@ end defmodule Architex.Event.PowerLevels do alias Architex.{Event, Account, Room} + alias Architex.Types.UserId alias ArchitexWeb.Client.Request.CreateRoom alias ArchitexWeb.Client.Request.CreateRoom.PowerLevelContentOverride @@ -67,7 +73,7 @@ defmodule Architex.Event.PowerLevels do Room.t(), Account.t(), CreateRoom.PowerLevelContentOverride.t(), - [String.t()] | nil, + [UserId.t()] | nil, String.t() | nil ) :: %Event{} def create_room_new(room, sender, nil, invite_ids, preset) do @@ -101,7 +107,7 @@ defmodule Architex.Event.PowerLevels do # 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, %{}, &{&1, creator_pl}) + invite_users_pls = Enum.into(invite_ids, %{}, &{to_string(&1), creator_pl}) Map.merge(users, invite_users_pls) else users @@ -216,9 +222,19 @@ end defmodule Architex.Event.Invite do alias Architex.{Event, Account, Room} - @spec new(Room.t(), Account.t(), String.t(), boolean() | nil) :: %Event{} - def new(room, sender, user_id, is_direct \\ nil) do - content = %{"membership" => "invite"} + @spec new( + 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{ @@ -234,14 +250,16 @@ defmodule Architex.Event.Leave do alias Architex.{Event, Account, Room} @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.new(room, sender) | type: "m.room.member", state_key: Account.get_mxid(sender), - content: %{ - "membership" => "leave" - } + content: content } end end @@ -249,9 +267,19 @@ end defmodule Architex.Event.Kick do alias Architex.{Event, Account, Room} - @spec new(Room.t(), Account.t(), String.t(), String.t() | nil) :: %Event{} - def new(room, sender, user_id, reason \\ nil) do - content = %{"membership" => "leave"} + @spec new( + Room.t(), + 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 %Event{ @@ -266,9 +294,19 @@ end defmodule Architex.Event.Ban do alias Architex.{Event, Account, Room} - @spec new(Room.t(), Account.t(), String.t(), String.t() | nil) :: %Event{} - def new(room, sender, user_id, reason \\ nil) do - content = %{"membership" => "ban"} + @spec new( + Room.t(), + 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 %Event{ @@ -283,15 +321,17 @@ end defmodule Architex.Event.Unban do alias Architex.{Event, Account, Room} - @spec new(Room.t(), Account.t(), String.t()) :: %Event{} - def new(room, sender, user_id) do + @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.new(room, sender) | type: "m.room.member", state_key: user_id, - content: %{ - "membership" => "leave" - } + content: content } end end diff --git a/lib/architex/types/user_id.ex b/lib/architex/types/user_id.ex index d4754ef..93ddc9b 100644 --- a/lib/architex/types/user_id.ex +++ b/lib/architex/types/user_id.ex @@ -1,7 +1,11 @@ defmodule Architex.Types.UserId do use Ecto.Type + import Ecto.Query + + alias Architex.{Repo, Account} alias Architex.Types.UserId + alias ArchitexWeb.Federation.HTTPClient @type t :: %__MODULE__{ 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(_), 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 diff --git a/lib/architex_web/api_schemas/client/request/ban.ex b/lib/architex_web/api_schemas/client/request/ban.ex index 694667f..1749e6b 100644 --- a/lib/architex_web/api_schemas/client/request/ban.ex +++ b/lib/architex_web/api_schemas/client/request/ban.ex @@ -1,14 +1,16 @@ defmodule ArchitexWeb.Client.Request.Ban do use ArchitexWeb.APIRequest + alias Architex.Types.UserId + @type t :: %__MODULE__{ - user_id: String.t(), + user_id: UserId.t(), reason: String.t() | nil } @primary_key false embedded_schema do - field :user_id, :string + field :user_id, UserId field :reason, :string end diff --git a/lib/architex_web/api_schemas/client/request/create_room.ex b/lib/architex_web/api_schemas/client/request/create_room.ex index 046f84f..19f782f 100644 --- a/lib/architex_web/api_schemas/client/request/create_room.ex +++ b/lib/architex_web/api_schemas/client/request/create_room.ex @@ -1,6 +1,8 @@ defmodule ArchitexWeb.Client.Request.CreateRoom do use ArchitexWeb.APIRequest + alias Architex.Types.UserId + defmodule PowerLevelContentOverride do use Ecto.Schema @@ -97,7 +99,7 @@ defmodule ArchitexWeb.Client.Request.CreateRoom do room_alias_name: String.t() | nil, name: String.t() | nil, topic: String.t() | nil, - invite: list(String.t()) | nil, + invite: [UserId.t()] | nil, room_version: String.t() | nil, preset: String.t() | nil, is_direct: boolean() | nil, @@ -113,7 +115,7 @@ defmodule ArchitexWeb.Client.Request.CreateRoom do field :room_alias_name, :string field :name, :string field :topic, :string - field :invite, {:array, :string} + field :invite, {:array, UserId} field :room_version, :string field :preset, :string field :is_direct, :boolean diff --git a/lib/architex_web/api_schemas/client/request/kick.ex b/lib/architex_web/api_schemas/client/request/kick.ex index 0d2bc81..48b9630 100644 --- a/lib/architex_web/api_schemas/client/request/kick.ex +++ b/lib/architex_web/api_schemas/client/request/kick.ex @@ -1,14 +1,16 @@ defmodule ArchitexWeb.Client.Request.Kick do use ArchitexWeb.APIRequest + alias Architex.Types.UserId + @type t :: %__MODULE__{ - user_id: String.t(), + user_id: UserId.t(), reason: String.t() | nil } @primary_key false embedded_schema do - field :user_id, :string + field :user_id, UserId field :reason, :string end diff --git a/lib/architex_web/client/controllers/room_controller.ex b/lib/architex_web/client/controllers/room_controller.ex index 8047cd1..70beeb5 100644 --- a/lib/architex_web/client/controllers/room_controller.ex +++ b/lib/architex_web/client/controllers/room_controller.ex @@ -63,9 +63,11 @@ defmodule ArchitexWeb.Client.RoomController do "room_id" => room_id, "user_id" => user_id }) 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 - 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 -> conn |> send_resp(200, []) @@ -135,9 +137,11 @@ defmodule ArchitexWeb.Client.RoomController do Action for POST /_matrix/client/r0/rooms/{roomId}/kick. """ 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 - 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 -> conn |> send_resp(200, []) @@ -158,9 +162,11 @@ defmodule ArchitexWeb.Client.RoomController do Action for POST /_matrix/client/r0/rooms/{roomId}/ban. """ 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 - 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 -> conn |> send_resp(200, []) @@ -184,20 +190,22 @@ defmodule ArchitexWeb.Client.RoomController do "room_id" => room_id, "user_id" => user_id }) do - case RoomServer.get_room_server(room_id) do - {:ok, pid} -> - case RoomServer.unban(pid, account, user_id) do - :ok -> - conn - |> send_resp(200, []) - |> halt() + with {:ok, user_id_struct} <- UserId.cast(user_id), + {:ok, pid} <- RoomServer.get_room_server(room_id) do + {avatar_url, displayname} = UserId.try_get_user_information(user_id_struct) - {:error, _} -> - put_error(conn, :unknown) - end + case RoomServer.unban(pid, account, user_id, avatar_url, displayname) do + :ok -> + conn + |> send_resp(200, []) + |> halt() - {:error, :not_found} -> - put_error(conn, :not_found, "The given room was not found.") + {:error, _} -> + 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 diff --git a/lib/architex_web/federation/http_client.ex b/lib/architex_web/federation/http_client.ex index b77c7a2..ce35b16 100644 --- a/lib/architex_web/federation/http_client.ex +++ b/lib/architex_web/federation/http_client.ex @@ -33,6 +33,8 @@ defmodule ArchitexWeb.Federation.HTTPClient do """ @spec client(String.t()) :: Tesla.Client.t() def client(server_name) do + # TODO: When implementing resolving homeservers, probably create + # a homeserver struct instead of using domain names directly. Tesla.client( [ {Tesla.Middleware.Opts, [server_name: server_name]}, From 222b6a309aef1cee0e7b9d6f7bedb8cd9b41bded Mon Sep 17 00:00:00 2001 From: Pim Kunis Date: Tue, 14 Sep 2021 14:39:22 +0200 Subject: [PATCH 07/10] Implement client get room stat endpoint --- README.md | 1 + lib/architex/room_server.ex | 29 +++++++++ lib/architex/schema/event/formatters.ex | 63 ++++++++++--------- .../client/controllers/room_controller.ex | 29 +++++++++ lib/architex_web/router.ex | 6 +- 5 files changed, 97 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index a85079d..d0553d9 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ Here, implemented and some unimplemented features are listed. - PUT /_matrix/client/r0/rooms/{roomId}/state/{eventType}/{stateKey} - PUT /_matrix/client/r0/rooms/{roomId}/send/{eventType}/{txnId} - GET /_matrix/client/r0/rooms/{roomId}/messages: Except filtering. +- GET /_matrix/client/r0/rooms/{roomId}/state - GET /_matrix/client/r0/directory/list/room/{roomId} - PUT /_matrix/client/r0/directory/list/room/{roomId} - GET /_matrix/client/r0/capabilities diff --git a/lib/architex/room_server.ex b/lib/architex/room_server.ex index f97ee37..b4a029e 100644 --- a/lib/architex/room_server.ex +++ b/lib/architex/room_server.ex @@ -189,6 +189,17 @@ defmodule Architex.RoomServer do GenServer.call(pid, {:send_state_event, account, event_type, content, state_key}) 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 + ### Implementation @impl true @@ -424,6 +435,24 @@ defmodule Architex.RoomServer do end end + def handle_call({:get_current_state, account}, _from, %{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. + state_set = StateResolution.resolve(event) + {:reply, {:ok, Map.values(state_set)}, state} + + _ -> + {:reply, :error, state} + end + end + @spec process_event_with_txn(t(), Room.t(), Device.t(), %Event{}, String.t()) :: (() -> {t(), Room.t(), String.t()} | {:error, atom()}) defp process_event_with_txn( diff --git a/lib/architex/schema/event/formatters.ex b/lib/architex/schema/event/formatters.ex index 84b7ea1..6d96bcc 100644 --- a/lib/architex/schema/event/formatters.ex +++ b/lib/architex/schema/event/formatters.ex @@ -4,33 +4,10 @@ defmodule Architex.Event.Formatters do """ alias Architex.Event - @spec messages_response(Event.t()) :: map() - def messages_response(%Event{ - content: content, - type: type, - 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{ + @doc """ + Event format with keys that all formats have in common. + """ + def base_client_response(%Event{ content: content, type: type, id: event_id, @@ -46,11 +23,37 @@ defmodule Architex.Event.Formatters do origin_server_ts: origin_server_ts } - data = if unsigned, do: Map.put(data, :unsigned, unsigned), else: data - - data + if unsigned, do: Map.put(data, :unsigned, unsigned), else: data 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() def as_pdu(%Event{ auth_events: auth_events, diff --git a/lib/architex_web/client/controllers/room_controller.ex b/lib/architex_web/client/controllers/room_controller.ex index 70beeb5..f713010 100644 --- a/lib/architex_web/client/controllers/room_controller.ex +++ b/lib/architex_web/client/controllers/room_controller.ex @@ -311,4 +311,33 @@ defmodule ArchitexWeb.Client.RoomController do {:error, _} -> put_error(conn, :bad_json) end end + + @doc """ + Get the state events for the current state of a room. + + Action for GET /_matrix/client/r0/rooms/{roomId}/state. + """ + def 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 end diff --git a/lib/architex_web/router.ex b/lib/architex_web/router.ex index 5827b21..757d557 100644 --- a/lib/architex_web/router.ex +++ b/lib/architex_web/router.ex @@ -87,7 +87,11 @@ defmodule ArchitexWeb.Router do post "/unban", RoomController, :unban put "/send/:event_type/:txn_id", RoomController, :send_message_event get "/messages", RoomController, :messages - put "/state/:event_type/*state_key", RoomController, :send_state_event + + scope "/state" do + get "/", RoomController, :state + put "/:event_type/*state_key", RoomController, :send_state_event + end end end end From f700be5dbee3bd0d6c7f2241bec30b7648cb6fa1 Mon Sep 17 00:00:00 2001 From: Pim Kunis Date: Tue, 14 Sep 2021 15:49:14 +0200 Subject: [PATCH 08/10] Implement client get state event endpoint --- README.md | 1 + lib/architex/room_server.ex | 38 ++++++++++++++++ .../client/controllers/room_controller.ex | 44 ++++++++++++++++++- lib/architex_web/router.ex | 15 +++++-- 4 files changed, 93 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d0553d9..00d5c62 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ Here, implemented and some unimplemented features are listed. - PUT /_matrix/client/r0/rooms/{roomId}/send/{eventType}/{txnId} - 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} - PUT /_matrix/client/r0/directory/list/room/{roomId} - GET /_matrix/client/r0/capabilities diff --git a/lib/architex/room_server.ex b/lib/architex/room_server.ex index b4a029e..31c8d6f 100644 --- a/lib/architex/room_server.ex +++ b/lib/architex/room_server.ex @@ -200,6 +200,12 @@ defmodule Architex.RoomServer 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 @impl true @@ -445,6 +451,10 @@ defmodule Architex.RoomServer do %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} @@ -453,6 +463,34 @@ defmodule Architex.RoomServer do end end + def handle_call( + {:get_state_event, account, event_type, state_key}, + _from, + %{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(t(), Room.t(), Device.t(), %Event{}, String.t()) :: (() -> {t(), Room.t(), String.t()} | {:error, atom()}) defp process_event_with_txn( diff --git a/lib/architex_web/client/controllers/room_controller.ex b/lib/architex_web/client/controllers/room_controller.ex index f713010..95aa10f 100644 --- a/lib/architex_web/client/controllers/room_controller.ex +++ b/lib/architex_web/client/controllers/room_controller.ex @@ -317,7 +317,7 @@ defmodule ArchitexWeb.Client.RoomController do Action for GET /_matrix/client/r0/rooms/{roomId}/state. """ - def state(%Conn{assigns: %{account: account}} = conn, %{"room_id" => room_id}) do + 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 @@ -340,4 +340,46 @@ defmodule ArchitexWeb.Client.RoomController do 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 diff --git a/lib/architex_web/router.ex b/lib/architex_web/router.ex index 757d557..44f4cd8 100644 --- a/lib/architex_web/router.ex +++ b/lib/architex_web/router.ex @@ -61,13 +61,16 @@ defmodule ArchitexWeb.Router do scope "/r0" do get "/account/whoami", AccountController, :whoami - post "/logout", AccountController, :logout - post "/logout/all", AccountController, :logout_all post "/createRoom", RoomController, :create get "/joined_rooms", RoomController, :joined_rooms get "/capabilities", InfoController, :capabilities get "/sync", SyncController, :sync + scope "/logout" do + post "/", AccountController, :logout + post "/all", AccountController, :logout_all + end + scope "/profile/:user_id" do put "/avatar_url", ProfileController, :set_avatar_url put "/displayname", ProfileController, :set_displayname @@ -89,8 +92,12 @@ defmodule ArchitexWeb.Router do get "/messages", RoomController, :messages scope "/state" do - get "/", RoomController, :state - put "/:event_type/*state_key", RoomController, :send_state_event + get "/", RoomController, :get_state + + scope "/:event_type/*state_key" do + get "/", RoomController, :get_state_event + put "/", RoomController, :send_state_event + end end end end From 731143775d8f64f4ef6f6e87bdcf04894cf1d190 Mon Sep 17 00:00:00 2001 From: pizzapim Date: Tue, 14 Sep 2021 13:51:23 +0000 Subject: [PATCH 09/10] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 00d5c62..21c0220 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ Here, implemented and some unimplemented features are listed. - GET /_matrix/federation/v1/state/{roomId} - GET /_matrix/federation/v1/state_ids/{roomId} - 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 From f94c156f950ddea675cf7d592974b82ff1b63ff1 Mon Sep 17 00:00:00 2001 From: Pim Kunis Date: Tue, 14 Sep 2021 21:10:57 +0200 Subject: [PATCH 10/10] Create custom database type for state sets --- lib/architex/room_server.ex | 184 ++++++++---------- lib/architex/schema/room.ex | 5 +- lib/architex/types/state_set.ex | 45 +++++ .../20210830160818_create_initial_tables.exs | 2 +- 4 files changed, 125 insertions(+), 111 deletions(-) create mode 100644 lib/architex/types/state_set.ex diff --git a/lib/architex/room_server.ex b/lib/architex/room_server.ex index 31c8d6f..5214ccc 100644 --- a/lib/architex/room_server.ex +++ b/lib/architex/room_server.ex @@ -6,8 +6,6 @@ defmodule Architex.RoomServer do The RoomServers are supervised by a DynamicSupervisor RoomServer.Supervisor. """ - @typep t :: map() - use GenServer import Ecto.Query @@ -25,7 +23,7 @@ defmodule Architex.RoomServer do Alias } - alias Architex.Types.UserId + alias Architex.Types.{UserId, StateSet} alias Architex.StateResolution.Authorization alias ArchitexWeb.Client.Request.{CreateRoom, Kick, Ban} @@ -49,8 +47,13 @@ defmodule Architex.RoomServer do @spec get_room_server(String.t()) :: {:error, :not_found} | DynamicSupervisor.on_start_child() def get_room_server(room_id) do # TODO: Might be wise to use a transaction here to prevent race conditions. - case Repo.one(from r in Room, where: r.id == ^room_id) do - %Room{state: serialized_state_set} = room -> + query = + 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 [{pid, _}] -> {:ok, pid} @@ -58,8 +61,7 @@ defmodule Architex.RoomServer do [] -> opts = [ name: {:via, Registry, {@registry, room_id}}, - room: room, - serialized_state_set: serialized_state_set + room: room ] DynamicSupervisor.start_child(@supervisor, {__MODULE__, opts}) @@ -210,19 +212,7 @@ defmodule Architex.RoomServer do @impl true def init(opts) do - 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}} + {:ok, %{room: Keyword.fetch!(opts, :room)}} end @impl true @@ -242,9 +232,9 @@ defmodule Architex.RoomServer do {:ok, alias_} -> events = create_room_events(room, account, request, alias_) - case Repo.transaction(process_events(room, %{}, events)) do - {:ok, {state_set, room}} -> - {:reply, {:ok, room_id}, %{state | state_set: state_set, room: room}} + case Repo.transaction(process_events(room, events)) do + {:ok, room} -> + {:reply, {:ok, room_id}, %{state | room: room}} {:error, reason} -> {:reply, {:error, reason}, state} @@ -258,7 +248,7 @@ defmodule Architex.RoomServer do 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 = Enum.any?(state_set, fn {{"m.room.member", user_id}, %Event{content: %{"membership" => "join"}}} -> @@ -314,12 +304,12 @@ defmodule Architex.RoomServer do def handle_call( {:invite, account, user_id, avatar_url, displayname}, _from, - %{room: room, state_set: state_set} = state + %{room: room} = state ) do invite_event = Event.Invite.new(room, account, user_id, avatar_url, displayname) - case Repo.transaction(process_event(room, state_set, invite_event)) do - {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} + case Repo.transaction(process_event(room, invite_event)) do + {:ok, {room, _}} -> {:reply, :ok, %{state | room: room}} {:error, reason} -> {:reply, {:error, reason}, state} end end @@ -327,24 +317,24 @@ defmodule Architex.RoomServer do def handle_call( {:join, account}, _from, - %{room: %Room{id: room_id} = room, state_set: state_set} = state + %{room: %Room{id: room_id} = room} = state ) do join_event = Event.Join.new(room, account) - case Repo.transaction(process_event(room, state_set, join_event)) do - {:ok, {state_set, room, _}} -> - {:reply, {:ok, room_id}, %{state | state_set: state_set, room: room}} + case Repo.transaction(process_event(room, join_event)) do + {:ok, {room, _}} -> + {:reply, {:ok, room_id}, %{state | room: room}} {:error, reason} -> {:reply, {:error, reason}, state} 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) - case Repo.transaction(process_event(room, state_set, leave_event)) do - {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} + case Repo.transaction(process_event(room, leave_event)) do + {:ok, {room, _}} -> {:reply, :ok, %{state | room: room}} {:error, reason} -> {:reply, {:error, reason}, state} end end @@ -352,13 +342,13 @@ defmodule Architex.RoomServer do def handle_call( {:kick, account, %Kick{user_id: user_id, reason: reason}, avatar_url, displayname}, _from, - %{room: room, state_set: state_set} = state + %{room: room} = state ) do kick_event = Event.Kick.new(room, account, to_string(user_id), avatar_url, displayname, reason) - case Repo.transaction(process_event(room, state_set, kick_event)) do - {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} + case Repo.transaction(process_event(room, kick_event)) do + {:ok, {room, _}} -> {:reply, :ok, %{state | room: room}} {:error, reason} -> {:reply, {:error, reason}, state} end end @@ -366,12 +356,12 @@ defmodule Architex.RoomServer do def handle_call( {:ban, account, %Ban{user_id: user_id, reason: reason}, avatar_url, displayname}, _from, - %{room: room, state_set: state_set} = state + %{room: room} = state ) do ban_event = Event.Ban.new(room, account, to_string(user_id), avatar_url, displayname, reason) - case Repo.transaction(process_event(room, state_set, ban_event)) do - {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} + case Repo.transaction(process_event(room, ban_event)) do + {:ok, {room, _}} -> {:reply, :ok, %{state | room: room}} {:error, reason} -> {:reply, {:error, reason}, state} end end @@ -379,12 +369,12 @@ defmodule Architex.RoomServer do def handle_call( {:unban, account, user_id, avatar_url, displayname}, _from, - %{room: room, state_set: state_set} = state + %{room: room} = state ) do unban_event = Event.Unban.new(room, account, user_id, avatar_url, displayname) - case Repo.transaction(process_event(room, state_set, unban_event)) do - {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} + case Repo.transaction(process_event(room, unban_event)) do + {:ok, {room, _}} -> {:reply, :ok, %{state | room: room}} {:error, reason} -> {:reply, {:error, reason}, state} end end @@ -392,7 +382,7 @@ defmodule Architex.RoomServer do def handle_call( {:set_visibility, account, visibility}, _from, - %{room: room, state_set: state_set} = state + %{room: %Room{state_set: state_set} = room} = state ) do case state_set do %{{"m.room.create", ""} => %Event{content: %{"creator" => creator}}} -> @@ -412,13 +402,13 @@ defmodule Architex.RoomServer do def handle_call( {:send_message_event, account, device, event_type, content, txn_id}, _from, - %{room: room, state_set: state_set} = state + %{room: room} = state ) do message_event = Event.custom_event(room, account, event_type, content) - case Repo.transaction(process_event_with_txn(state_set, room, device, message_event, txn_id)) do - {:ok, {state_set, room, event_id}} -> - {:reply, {:ok, event_id}, %{state | state_set: state_set, room: room}} + case Repo.transaction(process_event_with_txn(room, device, message_event, txn_id)) do + {:ok, {room, event_id}} -> + {:reply, {:ok, event_id}, %{state | room: room}} {:error, reason} -> {:reply, {:error, reason}, state} @@ -428,20 +418,24 @@ defmodule Architex.RoomServer do def handle_call( {:send_state_event, account, event_type, content, state_key}, _from, - %{room: room, state_set: state_set} = state + %{room: room} = state ) do state_event = Event.custom_state_event(room, account, event_type, content, state_key) - case Repo.transaction(process_event(room, state_set, state_event)) do - {:ok, {state_set, room, %Event{id: event_id}}} -> - {:reply, {:ok, event_id}, %{state | state_set: state_set, room: room}} + case Repo.transaction(process_event(room, state_event)) do + {:ok, {room, %Event{id: event_id}}} -> + {:reply, {:ok, event_id}, %{state | room: room}} {:error, reason} -> {:reply, {:error, reason}, state} end end - def handle_call({:get_current_state, account}, _from, %{state_set: state_set} = state) do + def handle_call( + {:get_current_state, account}, + _from, + %{room: %Room{state_set: state_set}} = state + ) do mxid = Account.get_mxid(account) case state_set[{"m.room.member", mxid}] do @@ -466,7 +460,7 @@ defmodule Architex.RoomServer do def handle_call( {:get_state_event, account, event_type, state_key}, _from, - %{state_set: state_set} = state + %{room: %Room{state_set: state_set}} = state ) do mxid = Account.get_mxid(account) @@ -491,10 +485,9 @@ defmodule Architex.RoomServer do end end - @spec process_event_with_txn(t(), Room.t(), Device.t(), %Event{}, String.t()) :: - (() -> {t(), Room.t(), String.t()} | {:error, atom()}) + @spec process_event_with_txn(Room.t(), Device.t(), %Event{}, String.t()) :: + (() -> {Room.t(), String.t()} | {:error, atom()}) defp process_event_with_txn( - state_set, room, %Device{nid: device_nid} = device, message_event, @@ -507,53 +500,42 @@ defmodule Architex.RoomServer do where: dt.txn_id == ^txn_id and dt.device_nid == ^device_nid ) do %DeviceTransaction{event_id: event_id} -> - {state_set, room, event_id} + {room, event_id} nil -> - with {state_set, room, %Event{id: event_id}} <- - process_event(room, state_set, message_event).() do + with {room, %Event{id: event_id}} <- process_event(room, message_event).() do # Mark this transaction as done. Ecto.build_assoc(device, :device_transactions, txn_id: txn_id, event_id: event_id) |> Repo.insert!() - {state_set, room, event_id} + {room, event_id} end end end end - @spec process_event(Room.t(), t(), %Event{}) :: - (() -> {t(), Room.t(), Event.t()} | {:error, atom()}) - defp process_event(room, state_set, event) do + @spec process_event(Room.t(), %Event{}) :: (() -> {Room.t(), Event.t()} | {:error, atom()}) + defp process_event(room, event) do fn -> - case finalize_and_process_event(event, state_set, room) do - {:ok, state_set, room, event} -> - _ = update_room_state_set(room, state_set) - {state_set, room, event} - - {:error, reason} -> - Repo.rollback(reason) + case finalize_and_process_event(event, room) do + {:ok, room, event} -> {room, event} + {:error, reason} -> Repo.rollback(reason) end end end - @spec process_events(Room.t(), t(), [%Event{}]) :: - (() -> {t(), Room.t()} | {:error, atom()}) - defp process_events(room, state_set, events) do + @spec process_events(Room.t(), [%Event{}]) :: (() -> Room.t() | {:error, atom()}) + defp process_events(room, events) do fn -> - Enum.reduce_while(events, {state_set, room}, fn event, {state_set, room} -> - case finalize_and_process_event(event, state_set, room) do - {:ok, state_set, room, _} -> {:cont, {state_set, room}} + Enum.reduce_while(events, room, fn event, room -> + case finalize_and_process_event(event, room) do + {:ok, room, _} -> {:cont, room} {:error, reason} -> {:halt, {:error, reason}} end end) |> then(fn - {:error, reason} -> - Repo.rollback(reason) - - {state_set, room} -> - _ = update_room_state_set(room, state_set) - {state_set, room} + {:error, reason} -> Repo.rollback(reason) + room -> room end) end end @@ -618,20 +600,6 @@ defmodule Architex.RoomServer do preset_events ++ initial_state_events ++ name_and_topic_events ++ invite_events end - # Update the given room in the database with the given state set. - @spec update_room_state_set(Room.t(), t()) :: Room.t() - defp update_room_state_set(room, state_set) do - # TODO: We might as well hold state in the Room struct, - # instead of the state_set state. - # Create custom type for this. - serialized_state_set = - Enum.map(state_set, fn {{type, state_key}, %Event{id: event_id}} -> - [type, state_key, event_id] - end) - - Repo.update!(change(room, state: serialized_state_set)) - end - # Get the events for room creation as dictated by the given preset. @spec room_creation_preset(Account.t(), String.t() | nil, Room.t()) :: [%Event{}] defp room_creation_preset(account, nil, %Room{visibility: visibility} = room) do @@ -687,12 +655,11 @@ defmodule Architex.RoomServer do # - Content hash # - Event ID # - Signature - @spec finalize_and_process_event(%Event{}, t(), Room.t()) :: - {:ok, t(), Room.t(), Event.t()} | {:error, atom()} + @spec finalize_and_process_event(%Event{}, Room.t()) :: + {:ok, Room.t(), Event.t()} | {:error, atom()} defp finalize_and_process_event( event, - state_set, - %Room{forward_extremities: forward_extremities} = room + %Room{forward_extremities: forward_extremities, state_set: state_set} = room ) do event = event @@ -701,7 +668,7 @@ defmodule Architex.RoomServer do |> Map.put(:depth, get_depth(forward_extremities)) case Event.post_process(event) do - {:ok, event} -> authenticate_and_process_event(event, state_set, room) + {:ok, event} -> authenticate_and_process_event(event, room) _ -> {:error, :event_creation} end end @@ -716,7 +683,7 @@ defmodule Architex.RoomServer do end # 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( @@ -768,9 +735,9 @@ defmodule Architex.RoomServer do # Authenticate and insert a new event using state resolution. # Implements the checks as described in the # [Matrix docs](https://matrix.org/docs/spec/server_server/latest#checks-performed-on-receipt-of-a-pdu). - @spec authenticate_and_process_event(Event.t(), t(), Room.t()) :: - {:ok, t(), Room.t(), Event.t()} | {:error, atom()} - defp authenticate_and_process_event(event, current_state_set, room) do + @spec authenticate_and_process_event(Event.t(), Room.t()) :: + {:ok, Room.t(), Event.t()} | {:error, atom()} + defp authenticate_and_process_event(event, %Room{state_set: current_state_set} = room) do # TODO: Correctly handle soft fails. # Check the following things: # 1. TODO: Is a valid event, otherwise it is dropped. @@ -788,8 +755,9 @@ defmodule Architex.RoomServer do event = Repo.insert!(event) state_set = StateResolution.resolve_forward_extremities(event) :ok = update_membership(room, state_set) + room = Repo.update!(change(room, state_set: state_set)) - {:ok, state_set, room, event} + {:ok, room, event} else _ -> {:error, :authorization} end @@ -800,7 +768,7 @@ defmodule Architex.RoomServer do # 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 # 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 server_name = Architex.server_name() diff --git a/lib/architex/schema/room.ex b/lib/architex/schema/room.ex index d07413f..e552b43 100644 --- a/lib/architex/schema/room.ex +++ b/lib/architex/schema/room.ex @@ -5,18 +5,19 @@ defmodule Architex.Room do import Ecto.Query alias Architex.{Repo, Room, Event, Alias, RoomServer, Account} + alias Architex.Types.StateSet alias ArchitexWeb.Client.Request.{CreateRoom, Messages} @type t :: %__MODULE__{ visibility: :public | :private, - state: list(list(String.t())), + state_set: StateSet.t(), forward_extremities: list(String.t()) } @primary_key {:id, :string, []} schema "rooms" do 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} has_many :events, Event, foreign_key: :room_id has_many :aliases, Alias, foreign_key: :room_id diff --git a/lib/architex/types/state_set.ex b/lib/architex/types/state_set.ex new file mode 100644 index 0000000..6eab0c8 --- /dev/null +++ b/lib/architex/types/state_set.ex @@ -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 diff --git a/priv/repo/migrations/20210830160818_create_initial_tables.exs b/priv/repo/migrations/20210830160818_create_initial_tables.exs index c8fae73..d9e2e84 100644 --- a/priv/repo/migrations/20210830160818_create_initial_tables.exs +++ b/priv/repo/migrations/20210830160818_create_initial_tables.exs @@ -14,7 +14,7 @@ defmodule Architex.Repo.Migrations.CreateInitialTables do create table(:rooms, primary_key: false) do 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 :visibility, :string, null: false, default: "public" end