commit: 7de21ec9910ef5d2fa36cb6335c25bed67ebc3a4
parent b5266097a1e73bd749d8176e327c14fcb9735f95
Author: Haelwenn <contact+git.pleroma.social@hacktivis.me>
Date: Sun, 31 Jul 2022 22:00:40 +0000
Merge branch 'from/develop/tusooa/emit-move' into 'develop'
Implement moving account
See merge request pleroma/pleroma!3524
Diffstat:
15 files changed, 856 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
@@ -32,6 +32,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- PleromaAPI: Add `GET /api/v1/pleroma/birthdays` API endpoint
- Make backend-rendered pages translatable. This includes emails. Pages returned as a HTTP response are translated using the language specified in the `userLanguage` cookie, or the `Accept-Language` header. Emails are translated using the `language` field when registering. This language can be changed by `PATCH /api/v1/accounts/update_credentials` with the `language` field.
- Uploadfilter `Pleroma.Upload.Filter.Exiftool.ReadDescription` returns description values to the FE so they can pre fill the image description field
+- Added move account API
### Fixed
- Subscription(Bell) Notifications: Don't create from Pipeline Ingested replies
diff --git 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
@@ -194,12 +194,13 @@ defmodule Pleroma.FollowingRelationship 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
@@ -2398,6 +2398,38 @@ defmodule Pleroma.User 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
@@ -413,7 +413,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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
@@ -214,6 +214,146 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation 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
@@ -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
@@ -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 @@ defmodule Pleroma.Web.TwitterAPI.UtilController 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
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Subject>acct:mewmew@lm.kazv.moe</Subject><Alias>https://lm.kazv.moe/users/mewmew</Alias><Alias>https://lm.kazv.moe/users/tester</Alias><Alias>https://lm.kazv.moe/users/testuser</Alias><Link href="https://lm.kazv.moe/users/mewmew" rel="http://webfinger.net/rel/profile-page" type="text/html" /><Link href="https://lm.kazv.moe/users/mewmew" rel="self" type="application/activity+json" /><Link href="https://lm.kazv.moe/users/mewmew" rel="self" type="application/ld+json; profile="https://www.w3.org/ns/activitystreams"" /><Link rel="http://ostatus.org/schema/1.0/subscribe" template="https://lm.kazv.moe/ostatus_subscribe?acct={uri}" /></XRD>
diff --git a/test/fixtures/tesla_mock/lm.kazv.moe_host_meta b/test/fixtures/tesla_mock/lm.kazv.moe_host_meta
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" template="https://lm.kazv.moe/.well-known/webfinger?resource={uri}" type="application/xrd+xml" /></XRD>
diff --git a/test/fixtures/tesla_mock/mewmew@lm.kazv.moe.json 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
@@ -2602,6 +2602,82 @@ defmodule Pleroma.UserTest do
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
+
describe "account endorsements" do
test "it pins people" do
user = insert(:user)
diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs
@@ -1836,9 +1836,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest 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,
@@ -1869,6 +1872,42 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest 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
@@ -516,4 +516,371 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
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 it",
+ %{
+ 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 it 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 it and local cache does not aliases it",
+ %{} 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 aliases it and local cache aliases it",
+ %{
+ 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 it", %{
+ 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
@@ -725,6 +725,15 @@ defmodule HttpRequestMock do
}}
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 @@ defmodule HttpRequestMock do
}}
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