Put avatar_url and displayname on m.room.member events

This commit is contained in:
Pim Kunis 2021-09-13 12:42:56 +02:00
parent 064a398a37
commit 739c496ac6
9 changed files with 193 additions and 70 deletions

View file

@ -25,6 +25,8 @@ defmodule Architex.RoomServer do
Alias Alias
} }
alias Architex.Types.UserId
alias Architex.StateResolution.Authorization alias Architex.StateResolution.Authorization
alias ArchitexWeb.Client.Request.{CreateRoom, Kick, Ban} alias ArchitexWeb.Client.Request.{CreateRoom, Kick, Ban}
@ -112,9 +114,10 @@ defmodule Architex.RoomServer do
@doc """ @doc """
Invite the a user to this room. Invite the a user to this room.
""" """
@spec invite(pid(), Account.t(), String.t()) :: :ok | {:error, atom()} @spec invite(pid(), Account.t(), String.t(), String.t() | nil, String.t() | nil) ::
def invite(pid, account, user_id) do :ok | {:error, atom()}
GenServer.call(pid, {:invite, account, user_id}) def invite(pid, account, user_id, avatar_url, displayname) do
GenServer.call(pid, {:invite, account, user_id, avatar_url, displayname})
end end
@doc """ @doc """
@ -136,25 +139,28 @@ defmodule Architex.RoomServer do
@doc """ @doc """
Kick a user from this room. Kick a user from this room.
""" """
@spec kick(pid(), Account.t(), Kick.t()) :: :ok | {:error, atom()} @spec kick(pid(), Account.t(), Kick.t(), String.t() | nil, String.t() | nil) ::
def kick(pid, account, request) do :ok | {:error, atom()}
GenServer.call(pid, {:kick, account, request}) def kick(pid, account, request, avatar_url, displayname) do
GenServer.call(pid, {:kick, account, request, avatar_url, displayname})
end end
@doc """ @doc """
Ban a user from this room. Ban a user from this room.
""" """
@spec ban(pid(), Account.t(), Ban.t()) :: :ok | {:error, atom()} @spec ban(pid(), Account.t(), Ban.t(), String.t() | nil, String.t() | nil) ::
def ban(pid, account, request) do :ok | {:error, atom()}
GenServer.call(pid, {:ban, account, request}) def ban(pid, account, request, avatar_url, displayname) do
GenServer.call(pid, {:ban, account, request, avatar_url, displayname})
end end
@doc """ @doc """
Unban a user from this room. Unban a user from this room.
""" """
@spec unban(pid(), Account.t(), String.t()) :: :ok | {:error, atom()} @spec unban(pid(), Account.t(), String.t(), String.t() | nil, String.t() | nil) ::
def unban(pid, account, user_id) do :ok | {:error, atom()}
GenServer.call(pid, {:unban, account, user_id}) def unban(pid, account, user_id, avatar_url, displayname) do
GenServer.call(pid, {:unban, account, user_id, avatar_url, displayname})
end end
@doc """ @doc """
@ -288,8 +294,12 @@ defmodule Architex.RoomServer do
{:reply, {state_events, auth_chain}, state} {:reply, {state_events, auth_chain}, state}
end end
def handle_call({:invite, account, user_id}, _from, %{room: room, state_set: state_set} = state) do def handle_call(
invite_event = Event.Invite.new(room, account, user_id) {:invite, account, user_id, avatar_url, displayname},
_from,
%{room: room, state_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 case Repo.transaction(process_event(room, state_set, invite_event)) do
{:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}}
@ -323,11 +333,12 @@ defmodule Architex.RoomServer do
end end
def handle_call( def handle_call(
{:kick, account, %Kick{user_id: user_id, reason: reason}}, {:kick, account, %Kick{user_id: user_id, reason: reason}, avatar_url, displayname},
_from, _from,
%{room: room, state_set: state_set} = state %{room: room, state_set: state_set} = state
) do ) do
kick_event = Event.Kick.new(room, account, user_id, reason) kick_event =
Event.Kick.new(room, account, to_string(user_id), avatar_url, displayname, reason)
case Repo.transaction(process_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}} {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}}
@ -336,11 +347,11 @@ defmodule Architex.RoomServer do
end end
def handle_call( def handle_call(
{:ban, account, %Kick{user_id: user_id, reason: reason}}, {:ban, account, %Ban{user_id: user_id, reason: reason}, avatar_url, displayname},
_from, _from,
%{room: room, state_set: state_set} = state %{room: room, state_set: state_set} = state
) do ) do
ban_event = Event.Ban.new(room, account, user_id, reason) ban_event = Event.Ban.new(room, account, to_string(user_id), avatar_url, displayname, reason)
case Repo.transaction(process_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}} {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}}
@ -348,8 +359,12 @@ defmodule Architex.RoomServer do
end end
end end
def handle_call({:unban, account, user_id}, _from, %{room: room, state_set: state_set} = state) do def handle_call(
unban_event = Event.Unban.new(room, account, user_id) {:unban, account, user_id, avatar_url, displayname},
_from,
%{room: room, state_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 case Repo.transaction(process_event(room, state_set, unban_event)) do
{:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}} {:ok, {state_set, room, _}} -> {:reply, :ok, %{state | state_set: state_set, room: room}}
@ -578,12 +593,16 @@ defmodule Architex.RoomServer do
end end
# Get the events for room creation for inviting other users. # Get the events for room creation for inviting other users.
@spec room_creation_invite_events(Account.t(), [String.t()] | nil, Room.t(), boolean() | nil) :: @spec room_creation_invite_events(Account.t(), [UserId.t()] | nil, Room.t(), boolean() | nil) ::
[%Event{}] [%Event{}]
defp room_creation_invite_events(_, nil, _, _), do: [] defp room_creation_invite_events(_, nil, _, _), do: []
defp room_creation_invite_events(account, invite_user_ids, room, is_direct) 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 end
defp room_creation_initial_state_events(_, nil, _), do: [] defp room_creation_initial_state_events(_, nil, _), do: []

View file

@ -314,4 +314,15 @@ defmodule Architex.Event do
{:ok, :crypto.hash(:sha256, json)} {:ok, :crypto.hash(:sha256, json)}
end end
end end
@spec default_membership_content(String.t() | nil, String.t() | nil) :: %{
optional(String.t()) => String.t()
}
def default_membership_content(avatar_url, displayname) do
content = %{}
content = if avatar_url, do: Map.put(content, "avatar_url", avatar_url), else: content
content = if displayname, do: Map.put(content, "displayname", displayname), else: content
content
end
end end

View file

@ -2,16 +2,21 @@ defmodule Architex.Event.Join do
alias Architex.{Event, Account, Room} alias Architex.{Event, Account, Room}
@spec new(Room.t(), Account.t()) :: %Event{} @spec new(Room.t(), Account.t()) :: %Event{}
def new(room, %Account{localpart: localpart} = sender) do def new(
room,
%Account{localpart: localpart, avatar_url: avatar_url, displayname: displayname} = sender
) do
mxid = Architex.get_mxid(localpart) mxid = Architex.get_mxid(localpart)
content =
Event.default_membership_content(avatar_url, displayname)
|> Map.put("membership", "join")
%Event{ %Event{
Event.new(room, sender) Event.new(room, sender)
| type: "m.room.member", | type: "m.room.member",
state_key: mxid, state_key: mxid,
content: %{ content: content
"membership" => "join"
}
} }
end end
end end
@ -50,6 +55,7 @@ end
defmodule Architex.Event.PowerLevels do defmodule Architex.Event.PowerLevels do
alias Architex.{Event, Account, Room} alias Architex.{Event, Account, Room}
alias Architex.Types.UserId
alias ArchitexWeb.Client.Request.CreateRoom alias ArchitexWeb.Client.Request.CreateRoom
alias ArchitexWeb.Client.Request.CreateRoom.PowerLevelContentOverride alias ArchitexWeb.Client.Request.CreateRoom.PowerLevelContentOverride
@ -67,7 +73,7 @@ defmodule Architex.Event.PowerLevels do
Room.t(), Room.t(),
Account.t(), Account.t(),
CreateRoom.PowerLevelContentOverride.t(), CreateRoom.PowerLevelContentOverride.t(),
[String.t()] | nil, [UserId.t()] | nil,
String.t() | nil String.t() | nil
) :: %Event{} ) :: %Event{}
def create_room_new(room, sender, nil, invite_ids, preset) do 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. # This overrides the content override, but the spec is not clear on this.
users = users =
if preset == "trusted_private_chat" and invite_ids do 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) Map.merge(users, invite_users_pls)
else else
users users
@ -216,9 +222,19 @@ end
defmodule Architex.Event.Invite do defmodule Architex.Event.Invite do
alias Architex.{Event, Account, Room} alias Architex.{Event, Account, Room}
@spec new(Room.t(), Account.t(), String.t(), boolean() | nil) :: %Event{} @spec new(
def new(room, sender, user_id, is_direct \\ nil) do Room.t(),
content = %{"membership" => "invite"} 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 content = if is_direct != nil, do: Map.put(content, "is_direct", is_direct), else: content
%Event{ %Event{
@ -234,14 +250,16 @@ defmodule Architex.Event.Leave do
alias Architex.{Event, Account, Room} alias Architex.{Event, Account, Room}
@spec new(Room.t(), Account.t()) :: %Event{} @spec new(Room.t(), Account.t()) :: %Event{}
def new(room, sender) do def new(room, %Account{avatar_url: avatar_url, displayname: displayname} = sender) do
content =
Event.default_membership_content(avatar_url, displayname)
|> Map.put("membership", "leave")
%Event{ %Event{
Event.new(room, sender) Event.new(room, sender)
| type: "m.room.member", | type: "m.room.member",
state_key: Account.get_mxid(sender), state_key: Account.get_mxid(sender),
content: %{ content: content
"membership" => "leave"
}
} }
end end
end end
@ -249,9 +267,19 @@ end
defmodule Architex.Event.Kick do defmodule Architex.Event.Kick do
alias Architex.{Event, Account, Room} alias Architex.{Event, Account, Room}
@spec new(Room.t(), Account.t(), String.t(), String.t() | nil) :: %Event{} @spec new(
def new(room, sender, user_id, reason \\ nil) do Room.t(),
content = %{"membership" => "leave"} Account.t(),
String.t(),
String.t() | nil,
String.t() | nil,
String.t() | nil
) :: %Event{}
def new(room, sender, user_id, avatar_url, displayname, reason \\ nil) do
content =
Event.default_membership_content(avatar_url, displayname)
|> Map.put("membership", "leave")
content = if reason, do: Map.put(content, "reason", reason), else: content content = if reason, do: Map.put(content, "reason", reason), else: content
%Event{ %Event{
@ -266,9 +294,19 @@ end
defmodule Architex.Event.Ban do defmodule Architex.Event.Ban do
alias Architex.{Event, Account, Room} alias Architex.{Event, Account, Room}
@spec new(Room.t(), Account.t(), String.t(), String.t() | nil) :: %Event{} @spec new(
def new(room, sender, user_id, reason \\ nil) do Room.t(),
content = %{"membership" => "ban"} Account.t(),
String.t(),
String.t() | nil,
String.t() | nil,
String.t() | nil
) :: %Event{}
def new(room, sender, user_id, avatar_url, displayname, reason \\ nil) do
content =
Event.default_membership_content(avatar_url, displayname)
|> Map.put("membership", "ban")
content = if reason, do: Map.put(content, "reason", reason), else: content content = if reason, do: Map.put(content, "reason", reason), else: content
%Event{ %Event{
@ -283,15 +321,17 @@ end
defmodule Architex.Event.Unban do defmodule Architex.Event.Unban do
alias Architex.{Event, Account, Room} alias Architex.{Event, Account, Room}
@spec new(Room.t(), Account.t(), String.t()) :: %Event{} @spec new(Room.t(), Account.t(), String.t(), String.t() | nil, String.t() | nil) :: %Event{}
def new(room, sender, user_id) do def new(room, sender, avatar_url, displayname, user_id) do
content =
Event.default_membership_content(avatar_url, displayname)
|> Map.put("membership", "leave")
%Event{ %Event{
Event.new(room, sender) Event.new(room, sender)
| type: "m.room.member", | type: "m.room.member",
state_key: user_id, state_key: user_id,
content: %{ content: content
"membership" => "leave"
}
} }
end end
end end

View file

@ -1,7 +1,11 @@
defmodule Architex.Types.UserId do defmodule Architex.Types.UserId do
use Ecto.Type use Ecto.Type
import Ecto.Query
alias Architex.{Repo, Account}
alias Architex.Types.UserId alias Architex.Types.UserId
alias ArchitexWeb.Federation.HTTPClient
@type t :: %__MODULE__{ @type t :: %__MODULE__{
localpart: String.t(), localpart: String.t(),
@ -53,4 +57,37 @@ defmodule Architex.Types.UserId do
def dump(%UserId{} = user_id), do: {:ok, to_string(user_id)} def dump(%UserId{} = user_id), do: {:ok, to_string(user_id)}
def dump(_), do: :error def dump(_), do: :error
# TODO: Seems out of place here.
@spec try_get_user_information(UserId.t()) :: {String.t() | nil, String.t() | nil}
def try_get_user_information(%UserId{localpart: localpart, domain: domain} = user_id) do
if domain == Architex.server_name() do
# Get information about a user on this homeserver.
query =
Account
|> where([a], a.localpart == ^localpart)
|> select([a], {a.avatar_url, a.displayname})
case Repo.one(query) do
nil -> {nil, nil}
info -> info
end
else
# Get information about a user on another homeserver.
client = HTTPClient.client(domain)
case HTTPClient.query_profile(client, to_string(user_id)) do
{:ok, response} ->
avatar_url = Map.get(response, "avatar_url")
avatar_url = if is_binary(avatar_url), do: avatar_url
displayname = Map.get(response, "displayname")
displayname = if is_binary(displayname), do: displayname
{avatar_url, displayname}
{:error, _, _} ->
{nil, nil}
end
end
end
end end

View file

@ -1,14 +1,16 @@
defmodule ArchitexWeb.Client.Request.Ban do defmodule ArchitexWeb.Client.Request.Ban do
use ArchitexWeb.APIRequest use ArchitexWeb.APIRequest
alias Architex.Types.UserId
@type t :: %__MODULE__{ @type t :: %__MODULE__{
user_id: String.t(), user_id: UserId.t(),
reason: String.t() | nil reason: String.t() | nil
} }
@primary_key false @primary_key false
embedded_schema do embedded_schema do
field :user_id, :string field :user_id, UserId
field :reason, :string field :reason, :string
end end

View file

@ -1,6 +1,8 @@
defmodule ArchitexWeb.Client.Request.CreateRoom do defmodule ArchitexWeb.Client.Request.CreateRoom do
use ArchitexWeb.APIRequest use ArchitexWeb.APIRequest
alias Architex.Types.UserId
defmodule PowerLevelContentOverride do defmodule PowerLevelContentOverride do
use Ecto.Schema use Ecto.Schema
@ -97,7 +99,7 @@ defmodule ArchitexWeb.Client.Request.CreateRoom do
room_alias_name: String.t() | nil, room_alias_name: String.t() | nil,
name: String.t() | nil, name: String.t() | nil,
topic: String.t() | nil, topic: String.t() | nil,
invite: list(String.t()) | nil, invite: [UserId.t()] | nil,
room_version: String.t() | nil, room_version: String.t() | nil,
preset: String.t() | nil, preset: String.t() | nil,
is_direct: boolean() | nil, is_direct: boolean() | nil,
@ -113,7 +115,7 @@ defmodule ArchitexWeb.Client.Request.CreateRoom do
field :room_alias_name, :string field :room_alias_name, :string
field :name, :string field :name, :string
field :topic, :string field :topic, :string
field :invite, {:array, :string} field :invite, {:array, UserId}
field :room_version, :string field :room_version, :string
field :preset, :string field :preset, :string
field :is_direct, :boolean field :is_direct, :boolean

View file

@ -1,14 +1,16 @@
defmodule ArchitexWeb.Client.Request.Kick do defmodule ArchitexWeb.Client.Request.Kick do
use ArchitexWeb.APIRequest use ArchitexWeb.APIRequest
alias Architex.Types.UserId
@type t :: %__MODULE__{ @type t :: %__MODULE__{
user_id: String.t(), user_id: UserId.t(),
reason: String.t() | nil reason: String.t() | nil
} }
@primary_key false @primary_key false
embedded_schema do embedded_schema do
field :user_id, :string field :user_id, UserId
field :reason, :string field :reason, :string
end end

View file

@ -63,9 +63,11 @@ defmodule ArchitexWeb.Client.RoomController do
"room_id" => room_id, "room_id" => room_id,
"user_id" => user_id "user_id" => user_id
}) do }) do
with {:ok, _} <- UserId.cast(user_id), with {:ok, user_id_struct} <- UserId.cast(user_id),
{:ok, pid} <- RoomServer.get_room_server(room_id) do {:ok, pid} <- RoomServer.get_room_server(room_id) do
case RoomServer.invite(pid, account, user_id) do {avatar_url, displayname} = UserId.try_get_user_information(user_id_struct)
case RoomServer.invite(pid, account, user_id, avatar_url, displayname) do
:ok -> :ok ->
conn conn
|> send_resp(200, []) |> send_resp(200, [])
@ -135,9 +137,11 @@ defmodule ArchitexWeb.Client.RoomController do
Action for POST /_matrix/client/r0/rooms/{roomId}/kick. Action for POST /_matrix/client/r0/rooms/{roomId}/kick.
""" """
def kick(%Conn{assigns: %{account: account}} = conn, %{"room_id" => room_id} = params) do def kick(%Conn{assigns: %{account: account}} = conn, %{"room_id" => room_id} = params) do
with {:ok, request} <- Kick.parse(params), with {:ok, %Kick{user_id: user_id} = request} <- Kick.parse(params),
{:ok, pid} <- RoomServer.get_room_server(room_id) do {:ok, pid} <- RoomServer.get_room_server(room_id) do
case RoomServer.kick(pid, account, request) do {avatar_url, displayname} = UserId.try_get_user_information(user_id)
case RoomServer.kick(pid, account, request, avatar_url, displayname) do
:ok -> :ok ->
conn conn
|> send_resp(200, []) |> send_resp(200, [])
@ -158,9 +162,11 @@ defmodule ArchitexWeb.Client.RoomController do
Action for POST /_matrix/client/r0/rooms/{roomId}/ban. Action for POST /_matrix/client/r0/rooms/{roomId}/ban.
""" """
def ban(%Conn{assigns: %{account: account}} = conn, %{"room_id" => room_id} = params) do def ban(%Conn{assigns: %{account: account}} = conn, %{"room_id" => room_id} = params) do
with {:ok, request} <- Ban.parse(params), with {:ok, %Ban{user_id: user_id} = request} <- Ban.parse(params),
{:ok, pid} <- RoomServer.get_room_server(room_id) do {:ok, pid} <- RoomServer.get_room_server(room_id) do
case RoomServer.ban(pid, account, request) do {avatar_url, displayname} = UserId.try_get_user_information(user_id)
case RoomServer.ban(pid, account, request, avatar_url, displayname) do
:ok -> :ok ->
conn conn
|> send_resp(200, []) |> send_resp(200, [])
@ -184,20 +190,22 @@ defmodule ArchitexWeb.Client.RoomController do
"room_id" => room_id, "room_id" => room_id,
"user_id" => user_id "user_id" => user_id
}) do }) do
case RoomServer.get_room_server(room_id) do with {:ok, user_id_struct} <- UserId.cast(user_id),
{:ok, pid} -> {:ok, pid} <- RoomServer.get_room_server(room_id) do
case RoomServer.unban(pid, account, user_id) do {avatar_url, displayname} = UserId.try_get_user_information(user_id_struct)
:ok ->
conn
|> send_resp(200, [])
|> halt()
{:error, _} -> case RoomServer.unban(pid, account, user_id, avatar_url, displayname) do
put_error(conn, :unknown) :ok ->
end conn
|> send_resp(200, [])
|> halt()
{:error, :not_found} -> {:error, _} ->
put_error(conn, :not_found, "The given room was not found.") put_error(conn, :unknown)
end
else
:error -> put_error(conn, :invalid_param, "Given user ID is invalid.")
{:error, :not_found} -> put_error(conn, :not_found, "The given room was not found.")
end end
end end

View file

@ -33,6 +33,8 @@ defmodule ArchitexWeb.Federation.HTTPClient do
""" """
@spec client(String.t()) :: Tesla.Client.t() @spec client(String.t()) :: Tesla.Client.t()
def client(server_name) do def client(server_name) do
# TODO: When implementing resolving homeservers, probably create
# a homeserver struct instead of using domain names directly.
Tesla.client( Tesla.client(
[ [
{Tesla.Middleware.Opts, [server_name: server_name]}, {Tesla.Middleware.Opts, [server_name: server_name]},