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