From 739c496ac6c209c9bf2b8256c28d6c88fda705ea Mon Sep 17 00:00:00 2001 From: Pim Kunis Date: Mon, 13 Sep 2021 12:42:56 +0200 Subject: [PATCH] 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]},