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 @@
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 @@
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 @@ defp cast_params(params) 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 @@ defp restrict(query, :max_id, %{max_id: max_id}, table_binding) 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,10 +1522,20 @@ def unblock(%User{} = blocker, %{ap_id: ap_id}) 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),
{:error, "Could not endorse: You are not following #{target.nickname}"} endorsed_users <-
else User.endorsed_users_relation(endorser)
UserRelationship.create_endorsement(endorser, target) |> 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}"}
true ->
UserRelationship.create_endorsement(endorser, target)
end
end end
end end

View File

@ -343,7 +343,15 @@ def endorse_operation 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 @@ def endorsements_operation 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 @@ def endorsements_operation 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 @@ def endorsements_operation 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 @@ def follow(follower, followed) 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 @@ def note(
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 @@ def endorsements(%{assigns: %{user: for_user, account: user}} = conn, params) 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 @@ def endorsements(%{assigns: %{user: for_user, account: user}} = conn, params) 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 @@ test "create a note on a user" 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