diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2119c8e21..22d8b0db1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
+### Added
+- Added move account API
+
### Removed
- SSH frontend, to be potentially re-enabled via a bridge rather than wired into the main system
- Gopher frontend, as above
diff --git a/docs/development/API/pleroma_api.md b/docs/development/API/pleroma_api.md
index 2304291e5..1c4c959f0 100644
--- a/docs/development/API/pleroma_api.md
+++ b/docs/development/API/pleroma_api.md
@@ -342,6 +342,36 @@ See [Admin-API](admin_api.md)
* Response: JSON. Returns `{"status": "success"}` if the change was successful, `{"error": "[error message]"}` otherwise
* Note: Currently, Mastodon has no API for changing email. If they add it in future it might be incompatible with Pleroma.
+## `/api/pleroma/move_account`
+### Move account
+* Method `POST`
+* Authentication: required
+* Params:
+ * `password`: user's password
+ * `target_account`: the nickname of the target account (e.g. `foo@example.org`)
+* Response: JSON. Returns `{"status": "success"}` if the change was successful, `{"error": "[error message]"}` otherwise
+* Note: This endpoint emits a `Move` activity to all followers of the current account. Some remote servers will automatically unfollow the current account and follow the target account upon seeing this, but this depends on the remote server implementation and cannot be guaranteed. For local followers , they will automatically unfollow and follow if and only if they have set the `allow_following_move` preference ("Allow auto-follow when following account moves").
+
+## `/api/pleroma/aliases`
+### Get aliases of the current account
+* Method `GET`
+* Authentication: required
+* Response: JSON. Returns `{"aliases": [alias, ...]}`, where `alias` is the nickname of an alias, e.g. `foo@example.org`.
+
+### Add alias to the current account
+* Method `PUT`
+* Authentication: required
+* Params:
+ * `alias`: the nickname of the alias to add, e.g. `foo@example.org`.
+* Response: JSON. Returns `{"status": "success"}` if the change was successful, `{"error": "[error message]"}` otherwise
+
+### Delete alias from the current account
+* Method `DELETE`
+* Authentication: required
+* Params:
+ * `alias`: the nickname of the alias to delete, e.g. `foo@example.org`.
+* Response: JSON. Returns `{"status": "success"}` if the change was successful, `{"error": "[error message]"}` otherwise
+
# Pleroma Conversations
Pleroma Conversations have the same general structure that Mastodon Conversations have. The behavior differs in the following ways when using these endpoints:
diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex
index a0c7e6e39..b101b9ee7 100644
--- a/lib/pleroma/following_relationship.ex
+++ b/lib/pleroma/following_relationship.ex
@@ -194,12 +194,13 @@ def move_following(origin, target) do
|> join(:inner, [r], f in assoc(r, :follower))
|> where(following_id: ^origin.id)
|> where([r, f], f.allow_following_move == true)
+ |> where([r, f], f.local == true)
|> limit(50)
|> preload([:follower])
|> Repo.all()
|> Enum.map(fn following_relationship ->
- Repo.delete(following_relationship)
Pleroma.Web.CommonAPI.follow(following_relationship.follower, target)
+ Pleroma.Web.CommonAPI.unfollow(following_relationship.follower, origin)
end)
|> case do
[] ->
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index dc6c661ea..b256c87e1 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -2288,6 +2288,38 @@ def change_email(user, email) do
|> update_and_set_cache()
end
+ def alias_users(user) do
+ user.also_known_as
+ |> Enum.map(&User.get_cached_by_ap_id/1)
+ |> Enum.filter(fn user -> user != nil end)
+ end
+
+ def add_alias(user, new_alias_user) do
+ current_aliases = user.also_known_as || []
+ new_alias_ap_id = new_alias_user.ap_id
+
+ if new_alias_ap_id in current_aliases do
+ {:ok, user}
+ else
+ user
+ |> cast(%{also_known_as: current_aliases ++ [new_alias_ap_id]}, [:also_known_as])
+ |> update_and_set_cache()
+ end
+ end
+
+ def delete_alias(user, alias_user) do
+ current_aliases = user.also_known_as || []
+ alias_ap_id = alias_user.ap_id
+
+ if alias_ap_id in current_aliases do
+ user
+ |> cast(%{also_known_as: current_aliases -- [alias_ap_id]}, [:also_known_as])
+ |> update_and_set_cache()
+ else
+ {:error, :no_such_alias}
+ end
+ end
+
# Internal function; public one is `deactivate/2`
defp set_activation_status(user, status) do
user
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 77f38f9f1..236181b07 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -417,7 +417,8 @@ def move(%User{} = origin, %User{} = target, local \\ true) do
"type" => "Move",
"actor" => origin.ap_id,
"object" => origin.ap_id,
- "target" => target.ap_id
+ "target" => target.ap_id,
+ "to" => [origin.follower_address]
}
with true <- origin.ap_id in target.also_known_as,
diff --git a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex b/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex
index 2a701066d..4a2a246f5 100644
--- a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex
@@ -214,6 +214,146 @@ def captcha_operation do
}
end
+ def move_account_operation do
+ %Operation{
+ tags: ["Account credentials"],
+ summary: "Move account",
+ security: [%{"oAuth" => ["write:accounts"]}],
+ operationId: "UtilController.move_account",
+ requestBody: request_body("Parameters", move_account_request(), required: true),
+ responses: %{
+ 200 =>
+ Operation.response("Success", "application/json", %Schema{
+ type: :object,
+ properties: %{status: %Schema{type: :string, example: "success"}}
+ }),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 403 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp move_account_request do
+ %Schema{
+ title: "MoveAccountRequest",
+ description: "POST body for moving the account",
+ type: :object,
+ required: [:password, :target_account],
+ properties: %{
+ password: %Schema{type: :string, description: "Current password"},
+ target_account: %Schema{
+ type: :string,
+ description: "The nickname of the target account to move to"
+ }
+ }
+ }
+ end
+
+ def list_aliases_operation do
+ %Operation{
+ tags: ["Account credentials"],
+ summary: "List account aliases",
+ security: [%{"oAuth" => ["read:accounts"]}],
+ operationId: "UtilController.list_aliases",
+ responses: %{
+ 200 =>
+ Operation.response("Success", "application/json", %Schema{
+ type: :object,
+ properties: %{
+ aliases: %Schema{
+ type: :array,
+ items: %Schema{type: :string},
+ example: ["foo@example.org"]
+ }
+ }
+ }),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 403 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def add_alias_operation do
+ %Operation{
+ tags: ["Account credentials"],
+ summary: "Add an alias to this account",
+ security: [%{"oAuth" => ["write:accounts"]}],
+ operationId: "UtilController.add_alias",
+ requestBody: request_body("Parameters", add_alias_request(), required: true),
+ responses: %{
+ 200 =>
+ Operation.response("Success", "application/json", %Schema{
+ type: :object,
+ properties: %{
+ status: %Schema{
+ type: :string,
+ example: "success"
+ }
+ }
+ }),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 403 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp add_alias_request do
+ %Schema{
+ title: "AddAliasRequest",
+ description: "PUT body for adding aliases",
+ type: :object,
+ required: [:alias],
+ properties: %{
+ alias: %Schema{
+ type: :string,
+ description: "The nickname of the account to add to aliases"
+ }
+ }
+ }
+ end
+
+ def delete_alias_operation do
+ %Operation{
+ tags: ["Account credentials"],
+ summary: "Delete an alias from this account",
+ security: [%{"oAuth" => ["write:accounts"]}],
+ operationId: "UtilController.delete_alias",
+ requestBody: request_body("Parameters", delete_alias_request(), required: true),
+ responses: %{
+ 200 =>
+ Operation.response("Success", "application/json", %Schema{
+ type: :object,
+ properties: %{
+ status: %Schema{
+ type: :string,
+ example: "success"
+ }
+ }
+ }),
+ 400 => Operation.response("Error", "application/json", ApiError),
+ 403 => Operation.response("Error", "application/json", ApiError),
+ 404 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp delete_alias_request do
+ %Schema{
+ title: "DeleteAliasRequest",
+ description: "PUT body for deleting aliases",
+ type: :object,
+ required: [:alias],
+ properties: %{
+ alias: %Schema{
+ type: :string,
+ description: "The nickname of the account to delete from aliases"
+ }
+ }
+ }
+ end
+
def healthcheck_operation do
%Operation{
tags: ["Accounts"],
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 7977fa619..b1817ef8b 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -349,6 +349,11 @@ defmodule Pleroma.Web.Router do
post("/delete_account", UtilController, :delete_account)
put("/notification_settings", UtilController, :update_notificaton_settings)
post("/disable_account", UtilController, :disable_account)
+ post("/move_account", UtilController, :move_account)
+
+ put("/aliases", UtilController, :add_alias)
+ get("/aliases", UtilController, :list_aliases)
+ delete("/aliases", UtilController, :delete_alias)
end
scope "/api/pleroma", Pleroma.Web.PleromaAPI do
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index ccbef6d9f..b8abc666e 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
alias Pleroma.Emoji
alias Pleroma.Healthcheck
alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.WebFinger
@@ -26,7 +27,18 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
:change_password,
:delete_account,
:update_notificaton_settings,
- :disable_account
+ :disable_account,
+ :move_account,
+ :add_alias,
+ :delete_alias
+ ]
+ )
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:accounts"]}
+ when action in [
+ :list_aliases
]
)
@@ -158,6 +170,91 @@ def disable_account(%{assigns: %{user: user}} = conn, params) do
end
end
+ def move_account(%{assigns: %{user: user}, body_params: body_params} = conn, %{}) do
+ case CommonAPI.Utils.confirm_current_password(user, body_params.password) do
+ {:ok, user} ->
+ with {:ok, target_user} <- find_or_fetch_user_by_nickname(body_params.target_account),
+ {:ok, _user} <- ActivityPub.move(user, target_user) do
+ json(conn, %{status: "success"})
+ else
+ {:not_found, _} ->
+ conn
+ |> put_status(404)
+ |> json(%{error: "Target account not found."})
+
+ {:error, error} ->
+ json(conn, %{error: error})
+ end
+
+ {:error, msg} ->
+ json(conn, %{error: msg})
+ end
+ end
+
+ def add_alias(%{assigns: %{user: user}, body_params: body_params} = conn, _) do
+ with {:ok, alias_user} <- find_user_by_nickname(body_params.alias),
+ {:ok, _user} <- user |> User.add_alias(alias_user) do
+ json(conn, %{status: "success"})
+ else
+ {:not_found, _} ->
+ conn
+ |> put_status(404)
+ |> json(%{error: "Target account does not exist."})
+
+ {:error, error} ->
+ json(conn, %{error: error})
+ end
+ end
+
+ def delete_alias(%{assigns: %{user: user}, body_params: body_params} = conn, _) do
+ with {:ok, alias_user} <- find_user_by_nickname(body_params.alias),
+ {:ok, _user} <- user |> User.delete_alias(alias_user) do
+ json(conn, %{status: "success"})
+ else
+ {:error, :no_such_alias} ->
+ conn
+ |> put_status(404)
+ |> json(%{error: "Account has no such alias."})
+
+ {:error, error} ->
+ json(conn, %{error: error})
+ end
+ end
+
+ def list_aliases(%{assigns: %{user: user}} = conn, %{}) do
+ alias_nicks =
+ user
+ |> User.alias_users()
+ |> Enum.map(&User.full_nickname/1)
+
+ json(conn, %{aliases: alias_nicks})
+ end
+
+ defp find_user_by_nickname(nickname) do
+ user = User.get_cached_by_nickname(nickname)
+
+ if user == nil do
+ {:not_found, nil}
+ else
+ {:ok, user}
+ end
+ end
+
+ defp find_or_fetch_user_by_nickname(nickname) do
+ user = User.get_by_nickname(nickname)
+
+ if user != nil and user.local do
+ {:ok, user}
+ else
+ with {:ok, user} <- User.fetch_by_nickname(nickname) do
+ {:ok, user}
+ else
+ _ ->
+ {:not_found, nil}
+ end
+ end
+ end
+
def captcha(conn, _params) do
json(conn, Pleroma.Captcha.new())
end
diff --git a/test/fixtures/tesla_mock/https___lm.kazv.moe_users_mewmew.xml b/test/fixtures/tesla_mock/https___lm.kazv.moe_users_mewmew.xml
new file mode 100644
index 000000000..b9e8dbbf5
--- /dev/null
+++ b/test/fixtures/tesla_mock/https___lm.kazv.moe_users_mewmew.xml
@@ -0,0 +1 @@
+acct:mewmew@lm.kazv.moehttps://lm.kazv.moe/users/mewmewhttps://lm.kazv.moe/users/testerhttps://lm.kazv.moe/users/testuser
diff --git a/test/fixtures/tesla_mock/lm.kazv.moe_host_meta b/test/fixtures/tesla_mock/lm.kazv.moe_host_meta
new file mode 100644
index 000000000..02e6f055e
--- /dev/null
+++ b/test/fixtures/tesla_mock/lm.kazv.moe_host_meta
@@ -0,0 +1 @@
+
diff --git a/test/fixtures/tesla_mock/mewmew@lm.kazv.moe.json b/test/fixtures/tesla_mock/mewmew@lm.kazv.moe.json
new file mode 100644
index 000000000..8d2c3e1e7
--- /dev/null
+++ b/test/fixtures/tesla_mock/mewmew@lm.kazv.moe.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://lm.kazv.moe/schemas/litepub-0.1.jsonld",{"@language":"und"}],"alsoKnownAs":["https://lm.kazv.moe/users/tester","https://lm.kazv.moe/users/testuser"],"attachment":[],"capabilities":{"acceptsChatMessages":true},"discoverable":false,"endpoints":{"oauthAuthorizationEndpoint":"https://lm.kazv.moe/oauth/authorize","oauthRegistrationEndpoint":"https://lm.kazv.moe/api/v1/apps","oauthTokenEndpoint":"https://lm.kazv.moe/oauth/token","sharedInbox":"https://lm.kazv.moe/inbox","uploadMedia":"https://lm.kazv.moe/api/ap/upload_media"},"featured":"https://lm.kazv.moe/users/mewmew/collections/featured","followers":"https://lm.kazv.moe/users/mewmew/followers","following":"https://lm.kazv.moe/users/mewmew/following","id":"https://lm.kazv.moe/users/mewmew","inbox":"https://lm.kazv.moe/users/mewmew/inbox","manuallyApprovesFollowers":false,"name":"mew","outbox":"https://lm.kazv.moe/users/mewmew/outbox","preferredUsername":"mewmew","publicKey":{"id":"https://lm.kazv.moe/users/mewmew#main-key","owner":"https://lm.kazv.moe/users/mewmew","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0nT3IVUwx799FSJyJEOY\n5D2c5zgtt2Z+BD9417eVLmVQF5fJlWgcKS4pbFc76zkYoBkZtV7XbzvN9KTNulpa\nUGNOM0/UdEoQLB8xbVCMm0ABUU8vbTWoMTxp93bfVHBz+33FPYdH1JHX4TCU/mJF\nX4UJMvFmMn5BFjSQm9GG6Eq2j6SAUsaTa8+Rrd8FzS6zb/dk3N/Llz0tfsZYS0sq\nEy9OYhsKOQ6eegULFJOF3Hz04vzwftmeXFsbb3aO2zKz3uAMYZglWHNBYJAePBtJ\ng362kqdJwgT14TFnZ0K2ziDPbkRULG1Kke/lsqw2rPF6Q6P4PeO1shCEDthoDoID\newIDAQAB\n-----END PUBLIC KEY-----\n\n"},"summary":"","tag":[],"type":"Person","url":"https://lm.kazv.moe/users/mewmew"}
diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs
index 756281a46..8486987fd 100644
--- a/test/pleroma/user_test.exs
+++ b/test/pleroma/user_test.exs
@@ -2521,4 +2521,80 @@ defp object_id_from_created_activity(user) do
%{object: %{data: %{"id" => object_id}}} = Activity.get_by_id_with_object(id)
object_id
end
+
+ describe "add_alias/2" do
+ test "should add alias for another user" do
+ user = insert(:user)
+ user2 = insert(:user)
+
+ assert {:ok, user_updated} = user |> User.add_alias(user2)
+
+ assert user_updated.also_known_as |> length() == 1
+ assert user2.ap_id in user_updated.also_known_as
+ end
+
+ test "should add multiple aliases" do
+ user = insert(:user)
+ user2 = insert(:user)
+ user3 = insert(:user)
+
+ assert {:ok, user} = user |> User.add_alias(user2)
+ assert {:ok, user_updated} = user |> User.add_alias(user3)
+
+ assert user_updated.also_known_as |> length() == 2
+ assert user2.ap_id in user_updated.also_known_as
+ assert user3.ap_id in user_updated.also_known_as
+ end
+
+ test "should not add duplicate aliases" do
+ user = insert(:user)
+ user2 = insert(:user)
+
+ assert {:ok, user} = user |> User.add_alias(user2)
+
+ assert {:ok, user_updated} = user |> User.add_alias(user2)
+
+ assert user_updated.also_known_as |> length() == 1
+ assert user2.ap_id in user_updated.also_known_as
+ end
+ end
+
+ describe "alias_users/1" do
+ test "should get aliases for a user" do
+ user = insert(:user)
+ user2 = insert(:user, also_known_as: [user.ap_id])
+
+ aliases = user2 |> User.alias_users()
+
+ assert aliases |> length() == 1
+
+ alias_user = aliases |> Enum.at(0)
+
+ assert alias_user.ap_id == user.ap_id
+ end
+ end
+
+ describe "delete_alias/2" do
+ test "should delete existing alias" do
+ user = insert(:user)
+ user2 = insert(:user, also_known_as: [user.ap_id])
+
+ assert {:ok, user_updated} = user2 |> User.delete_alias(user)
+
+ assert user_updated.also_known_as == []
+ end
+
+ test "should report error on non-existing alias" do
+ user = insert(:user)
+ user2 = insert(:user)
+ user3 = insert(:user, also_known_as: [user.ap_id])
+
+ assert {:error, :no_such_alias} = user3 |> User.delete_alias(user2)
+
+ user3_updated = User.get_cached_by_ap_id(user3.ap_id)
+
+ assert user3_updated.also_known_as |> length() == 1
+ assert user.ap_id in user3_updated.also_known_as
+ end
+ end
end
diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs
index 2b65f59e0..a38d8a3fb 100644
--- a/test/pleroma/web/activity_pub/activity_pub_test.exs
+++ b/test/pleroma/web/activity_pub/activity_pub_test.exs
@@ -1777,9 +1777,12 @@ test "create" do
"target" => ^new_ap_id,
"type" => "Move"
},
- local: true
+ local: true,
+ recipients: recipients
} = activity
+ assert old_user.follower_address in recipients
+
params = %{
"op" => "move_following",
"origin_id" => old_user.id,
@@ -1810,6 +1813,42 @@ test "old user must be in the new user's `also_known_as` list" do
assert {:error, "Target account must have the origin in `alsoKnownAs`"} =
ActivityPub.move(old_user, new_user)
end
+
+ test "do not move remote user following relationships" do
+ %{ap_id: old_ap_id} = old_user = insert(:user)
+ %{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id])
+ follower_remote = insert(:user, local: false)
+
+ User.follow(follower_remote, old_user)
+
+ assert User.following?(follower_remote, old_user)
+
+ assert {:ok, activity} = ActivityPub.move(old_user, new_user)
+
+ assert %Activity{
+ actor: ^old_ap_id,
+ data: %{
+ "actor" => ^old_ap_id,
+ "object" => ^old_ap_id,
+ "target" => ^new_ap_id,
+ "type" => "Move"
+ },
+ local: true
+ } = activity
+
+ params = %{
+ "op" => "move_following",
+ "origin_id" => old_user.id,
+ "target_id" => new_user.id
+ }
+
+ assert_enqueued(worker: Pleroma.Workers.BackgroundWorker, args: params)
+
+ Pleroma.Workers.BackgroundWorker.perform(%Oban.Job{args: params})
+
+ assert User.following?(follower_remote, old_user)
+ refute User.following?(follower_remote, new_user)
+ end
end
test "doesn't retrieve replies activities with exclude_replies" do
diff --git a/test/pleroma/web/twitter_api/util_controller_test.exs b/test/pleroma/web/twitter_api/util_controller_test.exs
index ee658ddf6..fb7da93f8 100644
--- a/test/pleroma/web/twitter_api/util_controller_test.exs
+++ b/test/pleroma/web/twitter_api/util_controller_test.exs
@@ -516,4 +516,371 @@ test "with proper permissions and valid password (JSON body)", %{conn: conn, use
assert user.password_hash == nil
end
end
+
+ describe "POST /api/pleroma/move_account" do
+ setup do: oauth_access(["write:accounts"])
+
+ test "without permissions", %{conn: conn} do
+ target_user = insert(:user)
+ target_nick = target_user |> User.full_nickname()
+
+ conn =
+ conn
+ |> assign(:token, nil)
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/pleroma/move_account", %{
+ "password" => "hi",
+ "target_account" => target_nick
+ })
+
+ assert json_response_and_validate_schema(conn, 403) == %{
+ "error" => "Insufficient permissions: write:accounts."
+ }
+ end
+
+ test "with proper permissions and invalid password", %{conn: conn} do
+ target_user = insert(:user)
+ target_nick = target_user |> User.full_nickname()
+
+ conn =
+ conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/pleroma/move_account", %{
+ "password" => "hi",
+ "target_account" => target_nick
+ })
+
+ assert json_response_and_validate_schema(conn, 200) == %{"error" => "Invalid password."}
+ end
+
+ test "with proper permissions, valid password and target account does not alias this",
+ %{
+ conn: conn
+ } do
+ target_user = insert(:user)
+ target_nick = target_user |> User.full_nickname()
+
+ conn =
+ conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/pleroma/move_account", %{
+ "password" => "test",
+ "target_account" => target_nick
+ })
+
+ assert json_response_and_validate_schema(conn, 200) == %{
+ "error" => "Target account must have the origin in `alsoKnownAs`"
+ }
+ end
+
+ test "with proper permissions, valid password and target account does not exist",
+ %{
+ conn: conn
+ } do
+ target_nick = "not_found@mastodon.social"
+
+ conn =
+ conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/pleroma/move_account", %{
+ "password" => "test",
+ "target_account" => target_nick
+ })
+
+ assert json_response_and_validate_schema(conn, 404) == %{
+ "error" => "Target account not found."
+ }
+ end
+
+ test "with proper permissions, valid password, remote target account aliases this and local cache does not exist",
+ %{} do
+ user = insert(:user, ap_id: "https://lm.kazv.moe/users/testuser")
+ %{user: _user, conn: conn} = oauth_access(["write:accounts"], user: user)
+
+ target_nick = "mewmew@lm.kazv.moe"
+
+ conn =
+ conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/pleroma/move_account", %{
+ "password" => "test",
+ "target_account" => target_nick
+ })
+
+ assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
+ end
+
+ test "with proper permissions, valid password, remote target account aliases this and local cache does not alias this",
+ %{} do
+ user = insert(:user, ap_id: "https://lm.kazv.moe/users/testuser")
+ %{user: _user, conn: conn} = oauth_access(["write:accounts"], user: user)
+
+ target_user =
+ insert(
+ :user,
+ ap_id: "https://lm.kazv.moe/users/mewmew",
+ nickname: "mewmew@lm.kazv.moe",
+ local: false
+ )
+
+ target_nick = target_user |> User.full_nickname()
+
+ conn =
+ conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/pleroma/move_account", %{
+ "password" => "test",
+ "target_account" => target_nick
+ })
+
+ assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
+ end
+
+ test "with proper permissions, valid password, remote target account does not alias this and local cache aliases this",
+ %{
+ user: user,
+ conn: conn
+ } do
+ target_user =
+ insert(
+ :user,
+ ap_id: "https://lm.kazv.moe/users/mewmew",
+ nickname: "mewmew@lm.kazv.moe",
+ local: false,
+ also_known_as: [user.ap_id]
+ )
+
+ target_nick = target_user |> User.full_nickname()
+
+ conn =
+ conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post("/api/pleroma/move_account", %{
+ "password" => "test",
+ "target_account" => target_nick
+ })
+
+ assert json_response_and_validate_schema(conn, 200) == %{
+ "error" => "Target account must have the origin in `alsoKnownAs`"
+ }
+ end
+
+ test "with proper permissions, valid password and target account aliases this", %{
+ conn: conn,
+ user: user
+ } do
+ target_user = insert(:user, also_known_as: [user.ap_id])
+ target_nick = target_user |> User.full_nickname()
+ follower = insert(:user)
+
+ User.follow(follower, user)
+
+ assert User.following?(follower, user)
+
+ conn =
+ conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post(
+ "/api/pleroma/move_account",
+ %{
+ password: "test",
+ target_account: target_nick
+ }
+ )
+
+ assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
+
+ params = %{
+ "op" => "move_following",
+ "origin_id" => user.id,
+ "target_id" => target_user.id
+ }
+
+ assert_enqueued(worker: Pleroma.Workers.BackgroundWorker, args: params)
+
+ Pleroma.Workers.BackgroundWorker.perform(%Oban.Job{args: params})
+
+ refute User.following?(follower, user)
+ assert User.following?(follower, target_user)
+ end
+
+ test "prefix nickname by @ should work", %{
+ conn: conn,
+ user: user
+ } do
+ target_user = insert(:user, also_known_as: [user.ap_id])
+ target_nick = target_user |> User.full_nickname()
+ follower = insert(:user)
+
+ User.follow(follower, user)
+
+ assert User.following?(follower, user)
+
+ conn =
+ conn
+ |> put_req_header("content-type", "multipart/form-data")
+ |> post(
+ "/api/pleroma/move_account",
+ %{
+ password: "test",
+ target_account: "@" <> target_nick
+ }
+ )
+
+ assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
+
+ params = %{
+ "op" => "move_following",
+ "origin_id" => user.id,
+ "target_id" => target_user.id
+ }
+
+ assert_enqueued(worker: Pleroma.Workers.BackgroundWorker, args: params)
+
+ Pleroma.Workers.BackgroundWorker.perform(%Oban.Job{args: params})
+
+ refute User.following?(follower, user)
+ assert User.following?(follower, target_user)
+ end
+ end
+
+ describe "GET /api/pleroma/aliases" do
+ setup do: oauth_access(["read:accounts"])
+
+ test "without permissions", %{conn: conn} do
+ conn =
+ conn
+ |> assign(:token, nil)
+ |> get("/api/pleroma/aliases")
+
+ assert json_response_and_validate_schema(conn, 403) == %{
+ "error" => "Insufficient permissions: read:accounts."
+ }
+ end
+
+ test "with permissions", %{
+ conn: conn
+ } do
+ assert %{"aliases" => []} =
+ conn
+ |> get("/api/pleroma/aliases")
+ |> json_response_and_validate_schema(200)
+ end
+
+ test "with permissions and aliases", %{} do
+ user = insert(:user)
+ user2 = insert(:user)
+
+ assert {:ok, user} = user |> User.add_alias(user2)
+
+ %{user: _user, conn: conn} = oauth_access(["read:accounts"], user: user)
+
+ assert %{"aliases" => aliases} =
+ conn
+ |> get("/api/pleroma/aliases")
+ |> json_response_and_validate_schema(200)
+
+ assert aliases == [user2 |> User.full_nickname()]
+ end
+ end
+
+ describe "PUT /api/pleroma/aliases" do
+ setup do: oauth_access(["write:accounts"])
+
+ test "without permissions", %{conn: conn} do
+ conn =
+ conn
+ |> assign(:token, nil)
+ |> put_req_header("content-type", "application/json")
+ |> put("/api/pleroma/aliases", %{alias: "none"})
+
+ assert json_response_and_validate_schema(conn, 403) == %{
+ "error" => "Insufficient permissions: write:accounts."
+ }
+ end
+
+ test "with permissions, no alias param", %{
+ conn: conn
+ } do
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> put("/api/pleroma/aliases", %{})
+
+ assert %{"error" => "Missing field: alias."} = json_response_and_validate_schema(conn, 400)
+ end
+
+ test "with permissions, with alias param", %{
+ conn: conn
+ } do
+ user2 = insert(:user)
+
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> put("/api/pleroma/aliases", %{alias: user2 |> User.full_nickname()})
+
+ assert json_response_and_validate_schema(conn, 200) == %{
+ "status" => "success"
+ }
+ end
+ end
+
+ describe "DELETE /api/pleroma/aliases" do
+ setup do
+ alias_user = insert(:user)
+ non_alias_user = insert(:user)
+ user = insert(:user, also_known_as: [alias_user.ap_id])
+
+ oauth_access(["write:accounts"], user: user)
+ |> Map.put(:alias_user, alias_user)
+ |> Map.put(:non_alias_user, non_alias_user)
+ end
+
+ test "without permissions", %{conn: conn} do
+ conn =
+ conn
+ |> assign(:token, nil)
+ |> put_req_header("content-type", "application/json")
+ |> delete("/api/pleroma/aliases", %{alias: "none"})
+
+ assert json_response_and_validate_schema(conn, 403) == %{
+ "error" => "Insufficient permissions: write:accounts."
+ }
+ end
+
+ test "with permissions, no alias param", %{conn: conn} do
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> delete("/api/pleroma/aliases", %{})
+
+ assert %{"error" => "Missing field: alias."} = json_response_and_validate_schema(conn, 400)
+ end
+
+ test "with permissions, account does not have such alias", %{
+ conn: conn,
+ non_alias_user: non_alias_user
+ } do
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> delete("/api/pleroma/aliases", %{alias: non_alias_user |> User.full_nickname()})
+
+ assert %{"error" => "Account has no such alias."} =
+ json_response_and_validate_schema(conn, 404)
+ end
+
+ test "with permissions, account does have such alias", %{
+ conn: conn,
+ alias_user: alias_user
+ } do
+ conn =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> delete("/api/pleroma/aliases", %{alias: alias_user |> User.full_nickname()})
+
+ assert %{"status" => "success"} = json_response_and_validate_schema(conn, 200)
+ end
+ end
end
diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex
index 94900dc14..dfac773de 100644
--- a/test/support/http_request_mock.ex
+++ b/test/support/http_request_mock.ex
@@ -725,6 +725,15 @@ def get(
}}
end
+ def get(
+ "https://mastodon.social/.well-known/webfinger?resource=acct:not_found@mastodon.social",
+ _,
+ _,
+ [{"accept", "application/xrd+xml,application/jrd+json"}]
+ ) do
+ {:ok, %Tesla.Env{status: 404}}
+ end
+
def get("http://gs.example.org/.well-known/host-meta", _, _, _) do
{:ok,
%Tesla.Env{
@@ -1124,6 +1133,57 @@ def get(
}}
end
+ def get("http://lm.kazv.moe/.well-known/host-meta", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/lm.kazv.moe_host_meta")
+ }}
+ end
+
+ def get("https://lm.kazv.moe/.well-known/host-meta", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/lm.kazv.moe_host_meta")
+ }}
+ end
+
+ def get(
+ "https://lm.kazv.moe/.well-known/webfinger?resource=acct:mewmew@lm.kazv.moe",
+ _,
+ _,
+ [{"accept", "application/xrd+xml,application/jrd+json"}]
+ ) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/https___lm.kazv.moe_users_mewmew.xml"),
+ headers: [{"content-type", "application/xrd+xml"}]
+ }}
+ end
+
+ def get("https://lm.kazv.moe/users/mewmew", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/mewmew@lm.kazv.moe.json"),
+ headers: activitypub_object_headers()
+ }}
+ end
+
+ def get("https://lm.kazv.moe/users/mewmew/collections/featured", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body:
+ File.read!("test/fixtures/users_mock/masto_featured.json")
+ |> String.replace("{{domain}}", "lm.kazv.moe")
+ |> String.replace("{{nickname}}", "mewmew"),
+ headers: [{"content-type", "application/activity+json"}]
+ }}
+ end
+
def get("https://info.pleroma.site/activity.json", _, _, [
{"accept", "application/activity+json"}
]) do