WIP account endorsements

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2022-01-10 21:35:55 +01:00
parent 4f249b2397
commit 0f90fd5805
11 changed files with 107 additions and 51 deletions

View File

@ -258,7 +258,8 @@ config :pleroma, :instance,
show_reactions: true, show_reactions: true,
password_reset_token_validity: 60 * 60 * 24, password_reset_token_validity: 60 * 60 * 24,
profile_directory: true, profile_directory: true,
privileged_staff: false privileged_staff: false,
max_endorsed_users: 20
config :pleroma, :welcome, config :pleroma, :welcome,
direct_message: [ direct_message: [

View File

@ -742,6 +742,16 @@ config :pleroma, :config_description, [
3 3
] ]
}, },
%{
key: :max_endorsed_users,
type: :integer,
description: "The maximum number of recommended accounts. 0 will disable the feature.",
suggestions: [
0,
1,
3
]
},
%{ %{
key: :autofollowed_nicknames, key: :autofollowed_nicknames,
type: {:list, :string}, type: {:list, :string},

View File

@ -377,12 +377,6 @@ Pleroma is generally compatible with the Mastodon 2.7.2 API, but some newer feat
- `GET /api/v1/identity_proofs`: Returns an empty array, `[]` - `GET /api/v1/identity_proofs`: Returns an empty array, `[]`
### Endorsements
*Added in Mastodon 2.5.0*
- `GET /api/v1/endorsements`: Returns an empty array, `[]`
### Featured tags ### Featured tags
*Added in Mastodon 3.0.0* *Added in Mastodon 3.0.0*

View File

@ -94,8 +94,7 @@ defmodule Pleroma.Pagination do
offset: :integer, offset: :integer,
limit: :integer, limit: :integer,
skip_extra_order: :boolean, skip_extra_order: :boolean,
skip_order: :boolean, skip_order: :boolean
shuffle: :boolean,
} }
changeset = cast({%{}, param_types}, params, Map.keys(param_types)) changeset = cast({%{}, param_types}, params, Map.keys(param_types))
@ -114,10 +113,6 @@ defmodule Pleroma.Pagination do
where(query, [{q, table_position(query, table_binding)}], q.id < ^max_id) where(query, [{q, table_position(query, table_binding)}], q.id < ^max_id)
end end
defp restrict(query, :order, %{shuffle: true}, _) do
order_by(query, [u], fragment("RANDOM()"))
end
defp restrict(query, :order, %{skip_order: true}, _), do: query defp restrict(query, :order, %{skip_order: true}, _), do: query
defp restrict(%{order_bys: [_ | _]} = query, :order, %{skip_extra_order: true}, _), do: query defp restrict(%{order_bys: [_ | _]} = query, :order, %{skip_extra_order: true}, _), do: query

View File

@ -82,7 +82,7 @@ defmodule Pleroma.User do
endorsement: [ endorsement: [
endorser_endorsements: :endorsed_users, endorser_endorsements: :endorsed_users,
endorsee_endorsements: :endorser_users endorsee_endorsements: :endorser_users
], ]
] ]
@cachex Pleroma.Config.get([:cachex, :provider], Cachex) @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@ -1522,12 +1522,22 @@ defmodule Pleroma.User do
end end
def endorse(%User{} = endorser, %User{} = target) do def endorse(%User{} = endorser, %User{} = target) do
if not following?(endorser, target) do with max_endorsed_users <- Pleroma.Config.get([:instance, :max_endorsed_users], 0),
endorsed_users <-
User.endorsed_users_relation(endorser)
|> Pleroma.Repo.all() do
cond do
Enum.count(endorsed_users) >= max_endorsed_users ->
{:error, "You have already pinned the maximum number of users"}
not following?(endorser, target) ->
{:error, "Could not endorse: You are not following #{target.nickname}"} {:error, "Could not endorse: You are not following #{target.nickname}"}
else
true ->
UserRelationship.create_endorsement(endorser, target) UserRelationship.create_endorsement(endorser, target)
end end
end end
end
def endorse(%User{} = endorser, %{ap_id: ap_id}) do def endorse(%User{} = endorser, %{ap_id: ap_id}) do
with %User{} = endorsed <- get_cached_by_ap_id(ap_id) do with %User{} = endorsed <- get_cached_by_ap_id(ap_id) do

View File

@ -343,7 +343,15 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
description: "Addds the given account to endorsed accounts list.", description: "Addds the given account to endorsed accounts list.",
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}], parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
responses: %{ responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship) 200 => Operation.response("Relationship", "application/json", AccountRelationship),
400 =>
Operation.response("Bad Request", "application/json", %Schema{
allOf: [ApiError],
title: "Unprocessable Entity",
example: %{
"error" => "You have already pinned the maximum number of users"
}
})
} }
} }
end end
@ -453,10 +461,10 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
tags: ["Retrieve account information"], tags: ["Retrieve account information"],
summary: "Endorsements", summary: "Endorsements",
operationId: "AccountController.endorsements", operationId: "AccountController.endorsements",
description: "Not implemented", description: "Returns endorsed accounts",
security: [%{"oAuth" => ["read:accounts"]}], security: [%{"oAuth" => ["read:accounts"]}],
responses: %{ responses: %{
200 => empty_array_response() 200 => Operation.response("Array of Accounts", "application/json", array_of_accounts())
} }
} }
end end

View File

@ -4,10 +4,10 @@
defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
alias OpenApiSpex.Operation alias OpenApiSpex.Operation
alias Pleroma.Web.ApiSpec.AccountOperation
alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
alias Pleroma.Web.ApiSpec.Schemas.ApiError alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.FlakeID alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.AccountOperation
alias Pleroma.Web.ApiSpec.StatusOperation alias Pleroma.Web.ApiSpec.StatusOperation
import Pleroma.Web.ApiSpec.Helpers import Pleroma.Web.ApiSpec.Helpers
@ -69,17 +69,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
summary: "Endorsements", summary: "Endorsements",
description: "Returns endorsed accounts", description: "Returns endorsed accounts",
operationId: "PleromaAPI.AccountController.endorsements", operationId: "PleromaAPI.AccountController.endorsements",
parameters: parameters: [with_relationships_param(), id_param()],
[
Operation.parameter(
:shuffle,
:query,
:boolean,
"Show endorsed accounts in random order"
),
id_param()
] ++ pagination_params(),
security: [%{"oAuth" => ["read:account"]}],
responses: %{ responses: %{
200 => 200 =>
Operation.response( Operation.response(
@ -87,7 +77,6 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
"application/json", "application/json",
AccountOperation.array_of_accounts() AccountOperation.array_of_accounts()
), ),
403 => Operation.response("Forbidden", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError) 404 => Operation.response("Not Found", "application/json", ApiError)
} }
} }

View File

@ -117,7 +117,8 @@ defmodule Pleroma.Web.CommonAPI do
def unfollow(follower, unfollowed) do def unfollow(follower, unfollowed) do
with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed), with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
{:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed), {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
{:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do {:ok, _subscription} <- User.unsubscribe(follower, unfollowed),
{:ok, _endorsement} <- User.unendorse(follower, unfollowed) do
{:ok, follower} {:ok, follower}
end end
end end

View File

@ -84,7 +84,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute]) plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
@relationship_actions [:follow, :unfollow] @relationship_actions [:follow, :unfollow]
@needs_account ~W(followers following lists follow unfollow mute unmute block unblock endorse unendorse endorse unendorse)a @needs_account ~W(
followers following lists follow unfollow mute unmute block unblock note endorse unendorse
)a
plug( plug(
RateLimiter, RateLimiter,
@ -450,16 +452,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end end
end end
@doc "POST /api/v1/accounts/:id/mute" @doc "POST /api/v1/accounts/:id/pin"
def endorse(%{assigns: %{user: endorser, account: endorsed}} = conn, _params) do def endorse(%{assigns: %{user: endorser, account: endorsed}} = conn, _params) do
with {:ok, _user_relationships} <- User.endorse(endorser, endorsed) do with {:ok, _user_relationships} <- User.endorse(endorser, endorsed) do
render(conn, "relationship.json", user: endorser, target: endorsed) render(conn, "relationship.json", user: endorser, target: endorsed)
else else
{:error, message} -> json_response(conn, :forbidden, %{error: message}) {:error, message} -> json_response(conn, :bad_request, %{error: message})
end end
end end
@doc "POST /api/v1/accounts/:id/unmute" @doc "POST /api/v1/accounts/:id/unpin"
def unendorse(%{assigns: %{user: endorser, account: endorsed}} = conn, _params) do def unendorse(%{assigns: %{user: endorser, account: endorsed}} = conn, _params) do
with {:ok, _user_relationships} <- User.unendorse(endorser, endorsed) do with {:ok, _user_relationships} <- User.unendorse(endorser, endorsed) do
render(conn, "relationship.json", user: endorser, target: endorsed) render(conn, "relationship.json", user: endorser, target: endorsed)

View File

@ -53,7 +53,10 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend) plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend)
plug(:assign_account_by_id when action in [:favourites, :endorsements, :subscribe, :unsubscribe]) plug(
:assign_account_by_id
when action in [:favourites, :endorsements, :subscribe, :unsubscribe]
)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaAccountOperation defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaAccountOperation
@ -106,7 +109,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
users = users =
user user
|> User.endorsed_users_relation(_restrict_deactivated = true) |> User.endorsed_users_relation(_restrict_deactivated = true)
|> fetch_paginated_endorsements(params) |> Pleroma.Repo.all()
conn conn
|> add_link_headers(users) |> add_link_headers(users)
@ -118,16 +121,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
) )
end end
defp fetch_paginated_endorsements(user, %{shuffle: true} = params) do
user
|> Pleroma.Pagination.fetch_paginated(Map.put(params, :shuffle, true))
end
defp fetch_paginated_endorsements(user, params) do
user
|> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
end
@doc "POST /api/v1/pleroma/accounts/:id/subscribe" @doc "POST /api/v1/pleroma/accounts/:id/subscribe"
def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do
with {:ok, _subscription} <- User.subscribe(user, subscription_target) do with {:ok, _subscription} <- User.subscribe(user, subscription_target) do

View File

@ -1838,4 +1838,57 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
|> get("/api/v1/accounts/relationships?id=#{other_user.id}") |> get("/api/v1/accounts/relationships?id=#{other_user.id}")
|> json_response_and_validate_schema(200) |> json_response_and_validate_schema(200)
end end
describe "account endorsements" do
setup do: oauth_access(["read:accounts", "write:accounts", "write:follows"])
setup do: clear_config([:instance, :max_endorsed_users], 1)
test "pin account", %{user: user, conn: conn} do
%{id: id1} = insert(:user)
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/accounts/#{id1}/follow")
|> json_response_and_validate_schema(200)
assert %{"id" => ^id1, "endorsed" => true} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/accounts/#{id1}/pin")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id1}] =
conn
|> put_req_header("content-type", "application/json")
|> get("/api/v1/endorsements")
|> json_response_and_validate_schema(200)
end
test "max pinned accounts", %{user: user, conn: conn} do
%{id: id1} = insert(:user)
%{id: id2} = insert(:user)
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/accounts/#{id1}/follow")
|> json_response_and_validate_schema(200)
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/accounts/#{id2}/follow")
|> json_response_and_validate_schema(200)
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/accounts/#{id1}/pin")
|> json_response_and_validate_schema(200)
assert %{"error" => "You have already pinned the maximum number of users"} =
conn
|> assign(:user, user)
|> post("/api/v1/accounts/#{id2}/pin")
|> json_response_and_validate_schema(400)
end
end
end end