@@ -184,6 +184,7 @@ Additional parameters can be added to the JSON body/Form data: | |||
- `pleroma_settings_store` - Opaque user settings to be saved on the backend. | |||
- `skip_thread_containment` - if true, skip filtering out broken threads | |||
- `allow_following_move` - if true, allows automatically follow moved following accounts | |||
- `also_known_as` - array of ActivityPub IDs, needed for following move | |||
- `pleroma_background_image` - sets the background image of the user. Can be set to "" (an empty string) to reset. | |||
- `discoverable` - if true, discovery of this account in search results and other services is allowed. | |||
- `actor_type` - the type of this account. | |||
@@ -570,23 +570,3 @@ 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 |
@@ -96,7 +96,6 @@ 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) | |||
@@ -486,6 +485,7 @@ defmodule Pleroma.User do | |||
:hide_follows_count, | |||
:hide_favorites, | |||
:allow_following_move, | |||
:also_known_as, | |||
:background, | |||
:show_role, | |||
:skip_thread_containment, | |||
@@ -494,12 +494,12 @@ defmodule Pleroma.User do | |||
:pleroma_settings_store, | |||
:discoverable, | |||
:actor_type, | |||
:also_known_as, | |||
:accepts_chat_messages | |||
] | |||
) | |||
|> unique_constraint(:nickname) | |||
|> validate_format(:nickname, local_nickname_regex()) | |||
|> validate_also_known_as() | |||
|> validate_length(:bio, max: bio_limit) | |||
|> validate_length(:name, min: 1, max: name_limit) | |||
|> validate_inclusion(:actor_type, ["Person", "Service"]) | |||
@@ -2387,36 +2387,11 @@ defmodule Pleroma.User do | |||
|> 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}) | |||
|> validate_ap_aliases() | |||
|> 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}) | |||
|> validate_ap_aliases() | |||
|> Repo.update() | |||
end | |||
defp validate_ap_aliases(changeset) do | |||
validate_change(changeset, :ap_aliases, fn :ap_aliases, ap_aliases -> | |||
case Enum.all?(ap_aliases, fn a -> Regex.match?(@url_regex, a) end) do | |||
defp validate_also_known_as(changeset) do | |||
validate_change(changeset, :also_known_as, fn :also_known_as, also_known_as -> | |||
case Enum.all?(also_known_as, fn a -> Regex.match?(@url_regex, a) end) do | |||
true -> [] | |||
false -> [ap_aliases: "Invalid ap_id format. Must be a URL."] | |||
false -> [also_known_as: "Invalid ap_id format. Must be a URL."] | |||
end | |||
end) | |||
end | |||
@@ -597,6 +597,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do | |||
nullable: true, | |||
description: "Allows automatically follow moved following accounts" | |||
}, | |||
also_known_as: %Schema{ | |||
type: :array, | |||
items: %Schema{type: :string}, | |||
nullable: true, | |||
description: "List of alternate ActivityPub IDs" | |||
}, | |||
pleroma_background_image: %Schema{ | |||
type: :string, | |||
nullable: true, | |||
@@ -627,6 +633,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do | |||
pleroma_settings_store: %{"pleroma-fe" => %{"key" => "val"}}, | |||
skip_thread_containment: false, | |||
allow_following_move: false, | |||
also_known_as: ["https://foo.bar/users/foo"], | |||
discoverable: false, | |||
actor_type: "Person" | |||
} | |||
@@ -4,8 +4,6 @@ | |||
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 | |||
@@ -89,54 +87,10 @@ 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 |
@@ -41,7 +41,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do | |||
type: :object, | |||
properties: %{ | |||
ap_id: %Schema{type: :string}, | |||
ap_aliases: %Schema{type: :array, items: %Schema{type: :string}}, | |||
also_known_as: %Schema{type: :array, items: %Schema{type: :string}}, | |||
allow_following_move: %Schema{ | |||
type: :boolean, | |||
description: "whether the user allows automatically follow moved following accounts" | |||
@@ -186,6 +186,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do | |||
:show_role, | |||
:skip_thread_containment, | |||
:allow_following_move, | |||
:also_known_as, | |||
:discoverable, | |||
:accepts_chat_messages | |||
] | |||
@@ -210,6 +211,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do | |||
if bot, do: {:ok, "Service"}, else: {:ok, "Person"} | |||
end) | |||
|> Maps.put_if_present(:actor_type, params[:actor_type]) | |||
|> Maps.put_if_present(:also_known_as, params[:also_known_as]) | |||
# What happens here: | |||
# | |||
@@ -267,7 +267,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do | |||
# Pleroma extension | |||
pleroma: %{ | |||
ap_id: user.ap_id, | |||
ap_aliases: user.ap_aliases, | |||
also_known_as: user.also_known_as, | |||
confirmation_pending: user.confirmation_pending, | |||
tags: user.tags, | |||
hide_followers_count: user.hide_followers_count, | |||
@@ -39,11 +39,6 @@ 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]) | |||
@@ -112,24 +107,4 @@ 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 |
@@ -345,9 +345,6 @@ 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) | |||
@@ -59,10 +59,7 @@ defmodule Pleroma.Web.WebFinger do | |||
end | |||
defp gather_aliases(%User{} = user) do | |||
user.ap_aliases | |||
|> MapSet.new() | |||
|> MapSet.put(user.ap_id) | |||
|> MapSet.to_list() | |||
[user.ap_id] ++ user.also_known_as | |||
end | |||
def represent_user(user, "JSON") do | |||
@@ -78,6 +75,10 @@ defmodule Pleroma.Web.WebFinger do | |||
def represent_user(user, "XML") do | |||
{:ok, user} = User.ensure_keys_present(user) | |||
aliases = | |||
gather_aliases(user) | |||
|> Enum.map(fn the_alias -> {:Alias, the_alias} end) | |||
links = | |||
gather_links(user) | |||
|> Enum.map(fn link -> {:Link, link} end) | |||
@@ -86,9 +87,8 @@ defmodule Pleroma.Web.WebFinger do | |||
:XRD, | |||
%{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"}, | |||
[ | |||
{:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"}, | |||
{:Alias, user.ap_id} | |||
] ++ links | |||
{:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"} | |||
] ++ aliases ++ links | |||
} | |||
|> XmlBuilder.to_doc() | |||
end | |||
@@ -1,9 +0,0 @@ | |||
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 |
@@ -2024,48 +2024,4 @@ 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 "add_aliases/2 with invalid alias" do | |||
user = insert(:user) | |||
{:error, _} = User.add_aliases(user, ["invalid_alias"]) | |||
{:error, _} = User.add_aliases(user, ["http://still_invalid"]) | |||
{:error, _} = User.add_aliases(user, ["http://validalias.com/users/dude", "invalid_alias"]) | |||
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 |
@@ -216,6 +216,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do | |||
assert user_data["display_name"] == "markorepairs" | |||
end | |||
test "updates the user's AKAs", %{conn: conn} do | |||
conn = | |||
patch(conn, "/api/v1/accounts/update_credentials", %{ | |||
"also_known_as" => ["https://mushroom.kingdom/users/mario"] | |||
}) | |||
assert user_data = json_response_and_validate_schema(conn, 200) | |||
assert user_data["pleroma"]["also_known_as"] == ["https://mushroom.kingdom/users/mario"] | |||
end | |||
test "updates the user's avatar", %{user: user, conn: conn} do | |||
new_avatar = %Plug.Upload{ | |||
content_type: "image/jpg", | |||
@@ -38,7 +38,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do | |||
inserted_at: ~N[2017-08-15 15:47:06.597036], | |||
emoji: %{"karjalanpiirakka" => "/file.png"}, | |||
raw_bio: "valid html. a\nb\nc\nd\nf '&<>\"", | |||
ap_aliases: ["https://shitposter.zone/users/shp"] | |||
also_known_as: ["https://shitposter.zone/users/shp"] | |||
}) | |||
expected = %{ | |||
@@ -78,7 +78,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do | |||
}, | |||
pleroma: %{ | |||
ap_id: user.ap_id, | |||
ap_aliases: ["https://shitposter.zone/users/shp"], | |||
also_known_as: ["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", | |||
@@ -174,7 +174,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do | |||
}, | |||
pleroma: %{ | |||
ap_id: user.ap_id, | |||
ap_aliases: [], | |||
also_known_as: [], | |||
background_image: nil, | |||
favicon: | |||
"https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png", | |||
@@ -281,33 +281,4 @@ 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 |
@@ -33,7 +33,7 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do | |||
user = | |||
insert(:user, | |||
ap_id: "https://hyrule.world/users/zelda", | |||
ap_aliases: ["https://mushroom.kingdom/users/toad"] | |||
also_known_as: ["https://mushroom.kingdom/users/toad"] | |||
) | |||
response = | |||
@@ -61,14 +61,20 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do | |||
end | |||
test "Webfinger XML" do | |||
user = insert(:user) | |||
user = | |||
insert(:user, | |||
ap_id: "https://hyrule.world/users/zelda", | |||
also_known_as: ["https://mushroom.kingdom/users/toad"] | |||
) | |||
response = | |||
build_conn() | |||
|> put_req_header("accept", "application/xrd+xml") | |||
|> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") | |||
|> response(200) | |||
assert response(response, 200) | |||
assert response =~ "<Alias>https://hyrule.world/users/zelda</Alias>" | |||
assert response =~ "<Alias>https://mushroom.kingdom/users/toad</Alias>" | |||
end | |||
test "it returns 404 when user isn't found (XML)" do | |||