From 3e1a377226ae0215bcf522f058a77c2d4e417642 Mon Sep 17 00:00:00 2001 From: Pim Kunis Date: Wed, 8 Sep 2021 16:27:31 +0200 Subject: [PATCH] Implement client get/set avatar URL endpoint --- README.md | 2 + lib/architex.ex | 4 ++ lib/architex/schema/account.ex | 1 + .../client/controllers/account_controller.ex | 3 +- .../client/controllers/profile_controller.ex | 64 +++++++++++++++++++ lib/architex_web/router.ex | 2 + .../20210830160818_create_initial_tables.exs | 11 ++-- 7 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 lib/architex_web/client/controllers/profile_controller.ex diff --git a/README.md b/README.md index bec33ba..78fed79 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ Here, implemented and some unimplemented features are listed. - GET /_matrix/client/r0/directory/list/room/{roomId} - PUT /_matrix/client/r0/directory/list/room/{roomId} - GET /_matrix/client/r0/capabilities +- GET /_matrix/client/r0/profile/{userId}/avatar_url: Except federation. +- PUT /_matrix/client/r0/profile/{userId}/avatar_url #### Federation API diff --git a/lib/architex.ex b/lib/architex.ex index 67c9608..09657d8 100644 --- a/lib/architex.ex +++ b/lib/architex.ex @@ -263,6 +263,10 @@ defmodule Architex do end # https://stackoverflow.com/a/45754361 + @doc """ + Validate whether the given fields are not nil for the changeset. + """ + @spec validate_not_nil(Ecto.Changeset.t(), [atom()]) :: Ecto.Changeset.t() def validate_not_nil(changeset, fields) do Enum.reduce(fields, changeset, fn field, changeset -> if Ecto.Changeset.get_field(changeset, field) == nil do diff --git a/lib/architex/schema/account.ex b/lib/architex/schema/account.ex index 5468d9b..3801ecb 100644 --- a/lib/architex/schema/account.ex +++ b/lib/architex/schema/account.ex @@ -16,6 +16,7 @@ defmodule Architex.Account do schema "accounts" do field :localpart, :string field :password_hash, :string, redact: true + field :avatar_url has_many :devices, Device many_to_many :joined_rooms, Room, diff --git a/lib/architex_web/client/controllers/account_controller.ex b/lib/architex_web/client/controllers/account_controller.ex index ec2ec39..63b9511 100644 --- a/lib/architex_web/client/controllers/account_controller.ex +++ b/lib/architex_web/client/controllers/account_controller.ex @@ -1,7 +1,6 @@ defmodule ArchitexWeb.Client.AccountController do use ArchitexWeb, :controller - import Architex import ArchitexWeb.Error alias Architex.{Account, Repo} @@ -32,7 +31,7 @@ defmodule ArchitexWeb.Client.AccountController do Action for GET /_matrix/client/r0/account/whoami. """ def whoami(%Conn{assigns: %{account: %Account{localpart: localpart}}} = conn, _params) do - data = %{user_id: get_mxid(localpart)} + data = %{user_id: Architex.get_mxid(localpart)} conn |> put_status(200) diff --git a/lib/architex_web/client/controllers/profile_controller.ex b/lib/architex_web/client/controllers/profile_controller.ex new file mode 100644 index 0000000..796d91b --- /dev/null +++ b/lib/architex_web/client/controllers/profile_controller.ex @@ -0,0 +1,64 @@ +defmodule ArchitexWeb.Client.ProfileController do + use ArchitexWeb, :controller + + import ArchitexWeb.Error + import Ecto.Query + + alias Architex.{Repo, Account} + alias Architex.Types.UserId + alias Plug.Conn + alias Ecto.Changeset + + @doc """ + Get the user's avatar URL. + + Action for GET /_matrix/client/r0/profile/{userId}/avatar_url. + """ + def get_avatar_url(conn, %{"user_id" => user_id}) do + case UserId.cast(user_id) do + {:ok, %UserId{localpart: localpart, domain: domain}} -> + if domain == Architex.server_name() do + case Repo.one(from a in Account, where: a.localpart == ^localpart) do + %Account{avatar_url: avatar_url} -> + data = if avatar_url, do: %{avatar_url: avatar_url}, else: %{} + + conn + |> put_status(200) + |> json(data) + + nil -> + put_error(conn, :not_found, "User was not found.") + end + else + # TODO: Use federation to lookup avatar URL. + put_error(conn, :not_found, "User was not found.") + end + + :error -> + put_error(conn, :not_found, "User ID is invalid.") + end + end + + @doc """ + This API sets the given user's avatar URL. + + Action for PUT /_matrix/client/r0/profile/{userId}/avatar_url. + """ + def set_avatar_url(%Conn{assigns: %{account: account}} = conn, %{"user_id" => user_id} = params) do + if Account.get_mxid(account) == user_id do + avatar_url = Map.get(params, "avatar_url") + + if not is_nil(avatar_url) do + account + |> Changeset.change(avatar_url: avatar_url) + |> Repo.update() + end + + conn + |> send_resp(200, []) + |> halt() + else + put_error(conn, :unauthorized, "User ID does not match access token.") + end + end +end diff --git a/lib/architex_web/router.ex b/lib/architex_web/router.ex index eb99026..eca45d9 100644 --- a/lib/architex_web/router.ex +++ b/lib/architex_web/router.ex @@ -31,6 +31,7 @@ defmodule ArchitexWeb.Router do get "/login", LoginController, :login_types post "/login", LoginController, :login get "/directory/list/room/:room_id", RoomDirectoryController, :get_visibility + get "/profile/:user_id/avatar_url", ProfileController, :get_avatar_url end get "/versions", InfoController, :versions @@ -55,6 +56,7 @@ defmodule ArchitexWeb.Router do get "/joined_rooms", RoomController, :joined_rooms get "/capabilities", InfoController, :capabilities get "/sync", SyncController, :sync + put "/profile/:user_id/avatar_url", ProfileController, :set_avatar_url scope "/directory" do put "/room/:alias", AliasesController, :create diff --git a/priv/repo/migrations/20210830160818_create_initial_tables.exs b/priv/repo/migrations/20210830160818_create_initial_tables.exs index e8a7062..2b643a2 100644 --- a/priv/repo/migrations/20210830160818_create_initial_tables.exs +++ b/priv/repo/migrations/20210830160818_create_initial_tables.exs @@ -5,13 +5,14 @@ defmodule Architex.Repo.Migrations.CreateInitialTables do create table(:accounts) do add :localpart, :string, null: false add :password_hash, :string, size: 60, null: false + add :avatar_url, :string, null: true timestamps(updated_at: false) end create index(:accounts, [:localpart], unique: true) create table(:rooms, primary_key: false) do - add :id, :string, primary_key: true, null: false + add :id, :string, primary_key: true add :state, {:array, {:array, :string}}, default: [], null: false add :forward_extremities, {:array, :string}, default: [], null: false add :visibility, :string, null: false, default: "public" @@ -43,7 +44,7 @@ defmodule Architex.Repo.Migrations.CreateInitialTables do create table(:server_key_info, primary_key: false) do add :valid_until, :bigint, default: 0, null: false - add :server_name, :string, primary_key: true, null: false + add :server_name, :string, primary_key: true end create table(:signing_keys, primary_key: false) do @@ -51,12 +52,12 @@ defmodule Architex.Repo.Migrations.CreateInitialTables do references(:server_key_info, column: :server_name, type: :string, on_delete: :delete_all), null: false - add :signing_key_id, :string, primary_key: true, null: false + add :signing_key_id, :string, primary_key: true add :signing_key, :binary, null: false end create table(:aliases, primary_key: false) do - add :alias, :string, primary_key: true, null: false + add :alias, :string, primary_key: true add :room_id, references(:rooms, type: :string, on_delete: :delete_all), null: false end @@ -76,7 +77,7 @@ defmodule Architex.Repo.Migrations.CreateInitialTables do create index(:devices, [:access_token], unique: true) create table(:device_transactions, primary_key: false) do - add :txn_id, :string, primary_key: true, null: false + add :txn_id, :string, primary_key: true add :device_nid, references(:devices, column: :nid, on_delete: :delete_all), primary_key: true