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,
password_reset_token_validity: 60 * 60 * 24,
profile_directory: true,
privileged_staff: false
privileged_staff: false,
max_endorsed_users: 20
config :pleroma, :welcome,
direct_message: [

View file

@ -742,6 +742,16 @@ config :pleroma, :config_description, [
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,
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, `[]`
### Endorsements
*Added in Mastodon 2.5.0*
- `GET /api/v1/endorsements`: Returns an empty array, `[]`
### Featured tags
*Added in Mastodon 3.0.0*

View file

@ -94,8 +94,7 @@ defmodule Pleroma.Pagination do
offset: :integer,
limit: :integer,
skip_extra_order: :boolean,
skip_order: :boolean,
shuffle: :boolean,
skip_order: :boolean
}
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)
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(%{order_bys: [_ | _]} = query, :order, %{skip_extra_order: true}, _), do: query

View file

@ -82,7 +82,7 @@ defmodule Pleroma.User do
endorsement: [
endorser_endorsements: :endorsed_users,
endorsee_endorsements: :endorser_users
],
]
]
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@ -1522,10 +1522,20 @@ defmodule Pleroma.User do
end
def endorse(%User{} = endorser, %User{} = target) do
if not following?(endorser, target) do
{:error, "Could not endorse: You are not following #{target.nickname}"}
else
UserRelationship.create_endorsement(endorser, target)
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}"}
true ->
UserRelationship.create_endorsement(endorser, target)
end
end
end

View file

@ -343,7 +343,15 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
description: "Addds the given account to endorsed accounts list.",
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
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
@ -453,10 +461,10 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
tags: ["Retrieve account information"],
summary: "Endorsements",
operationId: "AccountController.endorsements",
description: "Not implemented",
description: "Returns endorsed accounts",
security: [%{"oAuth" => ["read:accounts"]}],
responses: %{
200 => empty_array_response()
200 => Operation.response("Array of Accounts", "application/json", array_of_accounts())
}
}
end

View file

@ -4,10 +4,10 @@
defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
alias OpenApiSpex.Operation
alias Pleroma.Web.ApiSpec.AccountOperation
alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.AccountOperation
alias Pleroma.Web.ApiSpec.StatusOperation
import Pleroma.Web.ApiSpec.Helpers
@ -69,17 +69,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
summary: "Endorsements",
description: "Returns endorsed accounts",
operationId: "PleromaAPI.AccountController.endorsements",
parameters:
[
Operation.parameter(
:shuffle,
:query,
:boolean,
"Show endorsed accounts in random order"
),
id_param()
] ++ pagination_params(),
security: [%{"oAuth" => ["read:account"]}],
parameters: [with_relationships_param(), id_param()],
responses: %{
200 =>
Operation.response(
@ -87,7 +77,6 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
"application/json",
AccountOperation.array_of_accounts()
),
403 => Operation.response("Forbidden", "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
with {:ok, follower, _follow_activity} <- User.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}
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])
@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(
RateLimiter,
@ -450,16 +452,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
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
with {:ok, _user_relationships} <- User.endorse(endorser, endorsed) do
render(conn, "relationship.json", user: endorser, target: endorsed)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
{:error, message} -> json_response(conn, :bad_request, %{error: message})
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
with {:ok, _user_relationships} <- User.unendorse(endorser, endorsed) do
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(: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
@ -106,7 +109,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
users =
user
|> User.endorsed_users_relation(_restrict_deactivated = true)
|> fetch_paginated_endorsements(params)
|> Pleroma.Repo.all()
conn
|> add_link_headers(users)
@ -118,16 +121,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
)
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"
def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) 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}")
|> json_response_and_validate_schema(200)
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