Add account aliases
authorAlex Gleason <alex@alexgleason.me>
Fri, 17 Jul 2020 03:19:17 +0000 (22:19 -0500)
committerAlex Gleason <alex@alexgleason.me>
Fri, 17 Jul 2020 21:17:49 +0000 (16:17 -0500)
13 files changed:
docs/API/pleroma_api.md
lib/pleroma/user.ex
lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex
lib/pleroma/web/api_spec/schemas/account.ex
lib/pleroma/web/mastodon_api/views/account_view.ex
lib/pleroma/web/pleroma_api/controllers/account_controller.ex
lib/pleroma/web/router.ex
lib/pleroma/web/web_finger/web_finger.ex
priv/repo/migrations/20200717025041_add_aliases_to_users.exs [new file with mode: 0644]
test/user_test.exs
test/web/mastodon_api/views/account_view_test.exs
test/web/pleroma_api/controllers/account_controller_test.exs
test/web/web_finger/web_finger_controller_test.exs

index 5bd38ad364d9db7be00fbb0750a9de3ddbe1e1e8..8a937fdfdcf91781acbfc52582be6fd6d1967e18 100644 (file)
@@ -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
index 9240e912d9a3db676b6f19a2a95e2b932a4bf661..9b756c9a07b23dbf50bab96074cf42e5877affc9 100644 (file)
@@ -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 @@ defmodule Pleroma.User 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
index 97836b2eb570023531c63c7be7ccebeb19940b43..1040f6e205c53f6ab8f9a1c1a845aed2e9e6c9da 100644 (file)
@@ -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 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation 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
index ca79f0747861b93bb63e5d40022878369ffad247..4fd27edf58e5c0e4da2162e9a60143f09f80e897 100644 (file)
@@ -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"
index bc9745044a4bef61f2b0ae00786797b37f987858..e2912031ad6a9dc795f5fc41f77966d51825d12d 100644 (file)
@@ -248,6 +248,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView 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,
index 563edded70d0bcf32513fbbaab293faa3488e844..03e5781a31532d4ab2da7a8e176dbc915a073e89 100644 (file)
@@ -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 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
       {: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
index 386308362b6baaf295b70932e3270846cfce2b2f..dea95cd7790f0c641626b689db297be67ccdc1bc 100644 (file)
@@ -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)
index 71ccf251a8c79cdaf540e6a28193f60e87b6cbc1..fb142ce8d819a53299960ae6cff4ac12523560a3 100644 (file)
@@ -58,12 +58,19 @@ defmodule Pleroma.Web.WebFinger 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 (file)
index 0000000..a6ace6e
--- /dev/null
@@ -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
index 9788e09d9b24f5d74bace103993e53c6c6d5797a..db6e4872ef33f160ddd6f714e43326f8b9cf2b2c 100644 (file)
@@ -1858,4 +1858,41 @@ defmodule Pleroma.UserTest 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
index a83bf90a31cdc57ee0612d515dfe07edeb86a57e..4a0512e6855bdacc115ff2fc3044aa9eb77f0352 100644 (file)
@@ -37,7 +37,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
           "<script src=\"invalid-html\"></script><span>valid html</span>. a<br>b<br/>c<br >d<br />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 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest 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 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
       },
       pleroma: %{
         ap_id: user.ap_id,
+        ap_aliases: [],
         background_image: nil,
         favicon:
           "https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png",
index 07909d48bd806f8c2c1b65e88cabe8563326f665..da01a8218acb7bc15f97adcc192653b9ca314816 100644 (file)
@@ -281,4 +281,33 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest 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
index 0023f1e810e4cee2bed55d0cc49f73b51fe9f334..50b6c4b3e657e83f3752f53dcc24e4ba7b260f40 100644 (file)
@@ -30,14 +30,24 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest 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 response["subject"] == "acct:#{user.nickname}@localhost"
 
-    assert json_response(response, 200)["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