diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md index 5bd38ad36..8a937fdfd 100644 --- a/docs/API/pleroma_api.md +++ b/docs/API/pleroma_api.md @@ -570,3 +570,23 @@ Emoji reactions work a lot like favourites do. They make it possible to react to {"name": "😀", "count": 2, "me": true, "accounts": [{"id" => "xyz.."...}, {"id" => "zyx..."}]} ] ``` + +# Account aliases + +Set and delete ActivityPub aliases for follower move. + +## `POST /api/v1/pleroma/accounts/ap_aliases` +### Add account aliases +* Method: `POST` +* Authentication: required +* Params: + * `aliases`: array of ActivityPub IDs to add +* Response: JSON, the user's account + +## `DELETE /api/v1/pleroma/accounts/ap_aliases` +### Delete account aliases +* Method: `DELETE` +* Authentication: required +* Params: + * `aliases`: array of ActivityPub IDs to delete +* Response: JSON, the user's account diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 9240e912d..9b756c9a0 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -89,6 +89,7 @@ defmodule Pleroma.User do field(:keys, :string) field(:public_key, :string) field(:ap_id, :string) + field(:ap_aliases, {:array, :string}, default: []) field(:avatar, :map, default: %{}) field(:local, :boolean, default: true) field(:follower_address, :string) @@ -2268,4 +2269,27 @@ def sanitize_html(%User{} = user, filter) do |> Map.put(:bio, HTML.filter_tags(user.bio, filter)) |> Map.put(:fields, fields) end + + def add_aliases(%User{} = user, aliases) when is_list(aliases) do + alias_set = + (user.ap_aliases ++ aliases) + |> MapSet.new() + |> MapSet.to_list() + + user + |> change(%{ap_aliases: alias_set}) + |> Repo.update() + end + + def delete_aliases(%User{} = user, aliases) when is_list(aliases) do + alias_set = + user.ap_aliases + |> MapSet.new() + |> MapSet.difference(MapSet.new(aliases)) + |> MapSet.to_list() + + user + |> change(%{ap_aliases: alias_set}) + |> Repo.update() + end end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex index 97836b2eb..1040f6e20 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex @@ -4,6 +4,8 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.Account alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship alias Pleroma.Web.ApiSpec.Schemas.ApiError alias Pleroma.Web.ApiSpec.Schemas.FlakeID @@ -87,10 +89,54 @@ def unsubscribe_operation do } end + def add_aliases_operation do + %Operation{ + tags: ["Accounts"], + summary: "Add ActivityPub aliases", + operationId: "PleromaAPI.AccountController.add_aliases", + requestBody: request_body("Parameters", alias_request(), required: true), + security: [%{"oAuth" => ["write:accounts"]}], + responses: %{ + 200 => Operation.response("Account", "application/json", Account), + 403 => Operation.response("Forbidden", "application/json", ApiError) + } + } + end + + def delete_aliases_operation do + %Operation{ + tags: ["Accounts"], + summary: "Delete ActivityPub aliases", + operationId: "PleromaAPI.AccountController.delete_aliases", + requestBody: request_body("Parameters", alias_request(), required: true), + security: [%{"oAuth" => ["write:accounts"]}], + responses: %{ + 200 => Operation.response("Account", "application/json", Account) + } + } + end + defp id_param do Operation.parameter(:id, :path, FlakeID, "Account ID", example: "9umDrYheeY451cQnEe", required: true ) end + + defp alias_request do + %Schema{ + title: "AccountAliasRequest", + description: "POST body for adding/deleting AP aliases", + type: :object, + properties: %{ + aliases: %Schema{ + type: :array, + items: %Schema{type: :string} + } + }, + example: %{ + "aliases" => ["https://beepboop.social/users/beep", "https://mushroom.kingdom/users/toad"] + } + } + end end diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex index ca79f0747..4fd27edf5 100644 --- a/lib/pleroma/web/api_spec/schemas/account.ex +++ b/lib/pleroma/web/api_spec/schemas/account.ex @@ -40,6 +40,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do pleroma: %Schema{ type: :object, properties: %{ + ap_id: %Schema{type: :string}, + ap_aliases: %Schema{type: :array, items: %Schema{type: :string}}, allow_following_move: %Schema{ type: :boolean, description: "whether the user allows automatically follow moved following accounts" diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index bc9745044..e2912031a 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -248,6 +248,7 @@ defp do_render("show.json", %{user: user} = opts) do # Pleroma extension pleroma: %{ ap_id: user.ap_id, + ap_aliases: user.ap_aliases, confirmation_pending: user.confirmation_pending, tags: user.tags, hide_followers_count: user.hide_followers_count, diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index 563edded7..03e5781a3 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -39,6 +39,11 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do %{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites ) + plug( + OAuthScopesPlug, + %{scopes: ["write:accounts"]} when action in [:add_aliases, :delete_aliases] + ) + plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend) plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe]) @@ -107,4 +112,24 @@ def unsubscribe(%{assigns: %{user: user, account: subscription_target}} = conn, {:error, message} -> json_response(conn, :forbidden, %{error: message}) end end + + @doc "POST /api/v1/pleroma/accounts/ap_aliases" + def add_aliases(%{assigns: %{user: user}, body_params: %{aliases: aliases}} = conn, _params) + when is_list(aliases) do + with {:ok, user} <- User.add_aliases(user, aliases) do + render(conn, "show.json", user: user) + else + {:error, message} -> json_response(conn, :forbidden, %{error: message}) + end + end + + @doc "DELETE /api/v1/pleroma/accounts/ap_aliases" + def delete_aliases(%{assigns: %{user: user}, body_params: %{aliases: aliases}} = conn, _params) + when is_list(aliases) do + with {:ok, user} <- User.delete_aliases(user, aliases) do + render(conn, "show.json", user: user) + else + {:error, message} -> json_response(conn, :forbidden, %{error: message}) + end + end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 386308362..dea95cd77 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -344,6 +344,9 @@ defmodule Pleroma.Web.Router do post("/accounts/:id/subscribe", AccountController, :subscribe) post("/accounts/:id/unsubscribe", AccountController, :unsubscribe) + + post("/accounts/ap_aliases", AccountController, :add_aliases) + delete("/accounts/ap_aliases", AccountController, :delete_aliases) end post("/accounts/confirmation_resend", AccountController, :confirmation_resend) diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex index 71ccf251a..fb142ce8d 100644 --- a/lib/pleroma/web/web_finger/web_finger.ex +++ b/lib/pleroma/web/web_finger/web_finger.ex @@ -58,12 +58,19 @@ defp gather_links(%User{} = user) do ] ++ Publisher.gather_webfinger_links(user) end + defp gather_aliases(%User{} = user) do + user.ap_aliases + |> MapSet.new() + |> MapSet.put(user.ap_id) + |> MapSet.to_list() + end + def represent_user(user, "JSON") do {:ok, user} = User.ensure_keys_present(user) %{ "subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}", - "aliases" => [user.ap_id], + "aliases" => gather_aliases(user), "links" => gather_links(user) } end diff --git a/priv/repo/migrations/20200717025041_add_aliases_to_users.exs b/priv/repo/migrations/20200717025041_add_aliases_to_users.exs new file mode 100644 index 000000000..a6ace6e0f --- /dev/null +++ b/priv/repo/migrations/20200717025041_add_aliases_to_users.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.AddAliasesToUsers do + use Ecto.Migration + + def change do + alter table(:users) do + add(:ap_aliases, {:array, :string}, default: []) + end + end +end diff --git a/test/user_test.exs b/test/user_test.exs index 9788e09d9..db6e4872e 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -1858,4 +1858,41 @@ test "avatar fallback" do assert User.avatar_url(user, no_default: true) == nil end + + test "add_aliases/2" do + user = insert(:user) + + aliases = [ + "https://gleasonator.com/users/alex", + "https://gleasonator.com/users/alex", + "https://animalliberation.social/users/alex" + ] + + {:ok, user} = User.add_aliases(user, aliases) + + assert user.ap_aliases == [ + "https://animalliberation.social/users/alex", + "https://gleasonator.com/users/alex" + ] + end + + test "delete_aliases/2" do + user = + insert(:user, + ap_aliases: [ + "https://animalliberation.social/users/alex", + "https://benis.social/users/benis", + "https://gleasonator.com/users/alex" + ] + ) + + aliases = ["https://benis.social/users/benis"] + + {:ok, user} = User.delete_aliases(user, aliases) + + assert user.ap_aliases == [ + "https://animalliberation.social/users/alex", + "https://gleasonator.com/users/alex" + ] + end end diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs index a83bf90a3..4a0512e68 100644 --- a/test/web/mastodon_api/views/account_view_test.exs +++ b/test/web/mastodon_api/views/account_view_test.exs @@ -37,7 +37,8 @@ test "Represent a user account" do "valid html. a
b
c
d
f '&<>\"", inserted_at: ~N[2017-08-15 15:47:06.597036], emoji: %{"karjalanpiirakka" => "/file.png"}, - raw_bio: "valid html. a\nb\nc\nd\nf '&<>\"" + raw_bio: "valid html. a\nb\nc\nd\nf '&<>\"", + ap_aliases: ["https://shitposter.zone/users/shp"] }) expected = %{ @@ -77,6 +78,7 @@ test "Represent a user account" do }, pleroma: %{ ap_id: user.ap_id, + ap_aliases: ["https://shitposter.zone/users/shp"], background_image: "https://example.com/images/asuka_hospital.png", favicon: "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png", @@ -171,6 +173,7 @@ test "Represent a Service(bot) account" do }, pleroma: %{ ap_id: user.ap_id, + ap_aliases: [], background_image: nil, favicon: "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png", diff --git a/test/web/pleroma_api/controllers/account_controller_test.exs b/test/web/pleroma_api/controllers/account_controller_test.exs index 07909d48b..da01a8218 100644 --- a/test/web/pleroma_api/controllers/account_controller_test.exs +++ b/test/web/pleroma_api/controllers/account_controller_test.exs @@ -281,4 +281,33 @@ test "returns 404 when subscription_target not found" do assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404) end end + + describe "aliases controllers" do + setup do: oauth_access(["write:accounts"]) + + test "adds aliases", %{conn: conn} do + aliases = ["https://gleasonator.com/users/alex"] + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/pleroma/accounts/ap_aliases", %{"aliases" => aliases}) + + assert %{"pleroma" => %{"ap_aliases" => res}} = json_response_and_validate_schema(conn, 200) + assert Enum.count(res) == 1 + end + + test "deletes aliases", %{conn: conn, user: user} do + aliases = ["https://gleasonator.com/users/alex"] + User.add_aliases(user, aliases) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/v1/pleroma/accounts/ap_aliases", %{"aliases" => aliases}) + + assert %{"pleroma" => %{"ap_aliases" => res}} = json_response_and_validate_schema(conn, 200) + assert Enum.count(res) == 0 + end + end end diff --git a/test/web/web_finger/web_finger_controller_test.exs b/test/web/web_finger/web_finger_controller_test.exs index 0023f1e81..50b6c4b3e 100644 --- a/test/web/web_finger/web_finger_controller_test.exs +++ b/test/web/web_finger/web_finger_controller_test.exs @@ -30,14 +30,24 @@ test "GET host-meta" do end test "Webfinger JRD" do - user = insert(:user) + user = + insert(:user, + ap_id: "https://hyrule.world/users/zelda", + ap_aliases: ["https://mushroom.kingdom/users/toad"] + ) response = build_conn() |> put_req_header("accept", "application/jrd+json") |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") + |> json_response(200) - assert json_response(response, 200)["subject"] == "acct:#{user.nickname}@localhost" + assert response["subject"] == "acct:#{user.nickname}@localhost" + + assert response["aliases"] == [ + "https://hyrule.world/users/zelda", + "https://mushroom.kingdom/users/toad" + ] end test "it returns 404 when user isn't found (JSON)" do