From a9b0027071e3edf4d6a899ae5772e37806b4fc7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 29 Nov 2021 12:43:29 +0100 Subject: [PATCH 1/3] Account endorsements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- lib/pleroma/ecto_enums.ex | 3 +- lib/pleroma/user.ex | 49 ++++++++++++++++--- lib/pleroma/user_relationship.ex | 9 ++-- .../api_spec/operations/account_operation.ex | 29 +++++++++++ .../operations/pleroma_account_operation.ex | 22 +++++++++ .../controllers/account_controller.ex | 41 ++++++++++++++-- .../web/mastodon_api/views/account_view.ex | 9 +++- .../controllers/account_controller.ex | 32 +++++++++++- lib/pleroma/web/router.ex | 3 ++ 9 files changed, 179 insertions(+), 18 deletions(-) diff --git a/lib/pleroma/ecto_enums.ex b/lib/pleroma/ecto_enums.ex index 2a9addabc..3b6361555 100644 --- a/lib/pleroma/ecto_enums.ex +++ b/lib/pleroma/ecto_enums.ex @@ -9,7 +9,8 @@ mute: 2, reblog_mute: 3, notification_mute: 4, - inverse_subscription: 5 + inverse_subscription: 5, + endorsement: 6 ) defenum(Pleroma.FollowingRelationship.State, diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 3b4e49176..a8b9a53eb 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -78,7 +78,11 @@ defmodule Pleroma.User do inverse_subscription: [ subscribee_subscriptions: :subscriber_users, subscriber_subscriptions: :subscribee_users - ] + ], + endorsement: [ + endorser_endorsements: :endorsed_users, + endorsee_endorsements: :endorser_users + ], ] @cachex Pleroma.Config.get([:cachex, :provider], Cachex) @@ -168,25 +172,25 @@ defmodule Pleroma.User do {incoming_relation, incoming_relation_source} ]} <- @user_relationships_config do # Definitions of `has_many` relations: :blocker_blocks, :muter_mutes, :reblog_muter_mutes, - # :notification_muter_mutes, :subscribee_subscriptions + # :notification_muter_mutes, :subscribee_subscriptions, :endorser_endorsements has_many(outgoing_relation, UserRelationship, foreign_key: :source_id, where: [relationship_type: relationship_type] ) # Definitions of `has_many` relations: :blockee_blocks, :mutee_mutes, :reblog_mutee_mutes, - # :notification_mutee_mutes, :subscriber_subscriptions + # :notification_mutee_mutes, :subscriber_subscriptions, :endorsee_endorsements has_many(incoming_relation, UserRelationship, foreign_key: :target_id, where: [relationship_type: relationship_type] ) # Definitions of `has_many` relations: :blocked_users, :muted_users, :reblog_muted_users, - # :notification_muted_users, :subscriber_users + # :notification_muted_users, :subscriber_users, :endorsed_users has_many(outgoing_relation_target, through: [outgoing_relation, :target]) # Definitions of `has_many` relations: :blocker_users, :muter_users, :reblog_muter_users, - # :notification_muter_users, :subscribee_users + # :notification_muter_users, :subscribee_users, :endorser_users has_many(incoming_relation_source, through: [incoming_relation, :source]) end @@ -214,7 +218,7 @@ defmodule Pleroma.User do @user_relationships_config do # `def blocked_users_relation/2`, `def muted_users_relation/2`, # `def reblog_muted_users_relation/2`, `def notification_muted_users/2`, - # `def subscriber_users/2` + # `def subscriber_users/2`, `def endorsed_users_relation/2` def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do target_users_query = assoc(user, unquote(outgoing_relation_target)) @@ -227,7 +231,7 @@ def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? end # `def blocked_users/2`, `def muted_users/2`, `def reblog_muted_users/2`, - # `def notification_muted_users/2`, `def subscriber_users/2` + # `def notification_muted_users/2`, `def subscriber_users/2`, `def endorsed_users/2` def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do __MODULE__ |> apply(unquote(:"#{outgoing_relation_target}_relation"), [ @@ -238,7 +242,8 @@ def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do end # `def blocked_users_ap_ids/2`, `def muted_users_ap_ids/2`, `def reblog_muted_users_ap_ids/2`, - # `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2` + # `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2`, + # `def endorsed_users_ap_ids/2` def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do __MODULE__ |> apply(unquote(:"#{outgoing_relation_target}_relation"), [ @@ -1514,6 +1519,30 @@ def unblock(%User{} = blocker, %{ap_id: ap_id}) do unblock(blocker, get_cached_by_ap_id(ap_id)) 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) + end + end + + def endorse(%User{} = endorser, %{ap_id: ap_id}) do + with %User{} = endorsed <- get_cached_by_ap_id(ap_id) do + endorse(endorser, endorsed) + end + end + + def unendorse(%User{} = unendorser, %User{} = target) do + UserRelationship.delete_endorsement(unendorser, target) + end + + def unendorse(%User{} = unendorser, %{ap_id: ap_id}) do + with %User{} = user <- get_cached_by_ap_id(ap_id) do + unendorse(unendorser, user) + end + end + def mutes?(nil, _), do: false def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target) @@ -1559,6 +1588,10 @@ def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do end end + def endorses?(%User{} = user, %User{} = target) do + UserRelationship.endorsement_exists?(user, target) + end + @doc """ Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type. E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}` diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex index a467e9b65..8be5acc59 100644 --- a/lib/pleroma/user_relationship.ex +++ b/lib/pleroma/user_relationship.ex @@ -24,17 +24,20 @@ defmodule Pleroma.UserRelationship do for relationship_type <- Keyword.keys(Pleroma.UserRelationship.Type.__enum_map__()) do # `def create_block/2`, `def create_mute/2`, `def create_reblog_mute/2`, - # `def create_notification_mute/2`, `def create_inverse_subscription/2` + # `def create_notification_mute/2`, `def create_inverse_subscription/2`, + # `def endorsement/2` def unquote(:"create_#{relationship_type}")(source, target), do: create(unquote(relationship_type), source, target) # `def delete_block/2`, `def delete_mute/2`, `def delete_reblog_mute/2`, - # `def delete_notification_mute/2`, `def delete_inverse_subscription/2` + # `def delete_notification_mute/2`, `def delete_inverse_subscription/2`, + # `def delete_endorsement/2` def unquote(:"delete_#{relationship_type}")(source, target), do: delete(unquote(relationship_type), source, target) # `def block_exists?/2`, `def mute_exists?/2`, `def reblog_mute_exists?/2`, - # `def notification_mute_exists?/2`, `def inverse_subscription_exists?/2` + # `def notification_mute_exists?/2`, `def inverse_subscription_exists?/2`, + # `def inverse_endorsement?/2` def unquote(:"#{relationship_type}_exists?")(source, target), do: exists?(unquote(relationship_type), source, target) end diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 54e5ebc76..2ee3c4f85 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -328,6 +328,35 @@ def unblock_operation do } end + def endorse_operation do + %Operation{ + tags: ["Account actions"], + summary: "Endorse", + operationId: "AccountController.endorse", + security: [%{"oAuth" => ["follow", "write:accounts"]}], + description: + "Addds the given account to endorsed accounts list.", + parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}], + responses: %{ + 200 => Operation.response("Relationship", "application/json", AccountRelationship) + } + } + end + + def unendorse_operation do + %Operation{ + tags: ["Account actions"], + summary: "Unendorse", + operationId: "AccountController.unendorse", + security: [%{"oAuth" => ["follow", "write:accounts"]}], + description: "Removes the given account from endorsed accounts list.", + parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}], + responses: %{ + 200 => Operation.response("Relationship", "application/json", AccountRelationship) + } + } + end + def follow_by_uri_operation do %Operation{ tags: ["Account actions"], diff --git a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex index ad49f6426..f8e4b3263 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do 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 @@ -62,6 +63,27 @@ def favourites_operation do } end + def endorsements_operation do + %Operation{ + tags: ["Retrieve account information"], + summary: "Endorsements", + description: "Returns endorsed accounts", + operationId: "PleromaAPI.AccountController.endorsements", + parameters: [id_param() | pagination_params()], + security: [%{"oAuth" => ["read:account"]}], + responses: %{ + 200 => + Operation.response( + "Array of Accounts", + "application/json", + AccountOperation.array_of_accounts() + ), + 403 => Operation.response("Forbidden", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + def subscribe_operation do %Operation{ tags: ["Account actions"], diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 5fcbffc34..47911103f 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -53,7 +53,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do when action in [:verify_credentials, :endorsements, :identity_proofs] ) - plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials) + plug( + OAuthScopesPlug, + %{scopes: ["write:accounts"]} when action in [:update_credentials, :endorse, :unendorse] + ) plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists) @@ -79,7 +82,7 @@ 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)a + @needs_account ~W(followers following lists follow unfollow mute unmute block unblock endorse unendorse)a plug( RateLimiter, @@ -435,6 +438,24 @@ def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do end end + @doc "POST /api/v1/accounts/:id/mute" + 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}) + end + end + + @doc "POST /api/v1/accounts/:id/unmute" + 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) + else + {:error, message} -> json_response(conn, :forbidden, %{error: message}) + end + end + @doc "POST /api/v1/follows" def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do case User.get_cached_by_nickname(uri) do @@ -478,7 +499,21 @@ def blocks(%{assigns: %{user: user}} = conn, params) do end @doc "GET /api/v1/endorsements" - def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params) + def endorsements(%{assigns: %{user: user}} = conn, params) do + users = + user + |> User.endorsed_users_relation(_restrict_deactivated = true) + |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true)) + + conn + |> add_link_headers(users) + |> render("index.json", + users: users, + for: user, + as: :user, + embed_relationships: embed_relationships?(params) + ) + end @doc "GET /api/v1/identity_proofs" def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 9e9de33f6..136dcf929 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -156,7 +156,14 @@ def render( target, &User.muting_reblogs?(&1, &2) ), - endorsed: false + endorsed: + UserRelationship.exists?( + user_relationships, + :endorsement, + target, + reading_user, + &User.endorses?(&2, &1) + ), } end diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index 8e4d3e7f7..058286340 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -6,7 +6,12 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do use Pleroma.Web, :controller import Pleroma.Web.ControllerHelper, - only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2] + only: [ + json_response: 3, + add_link_headers: 2, + embed_relationships?: 1, + assign_account_by_id: 2 + ] alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub @@ -40,9 +45,15 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do %{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites ) + plug( + OAuthScopesPlug, + %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]} + when action == :endorsements + ) + plug(RateLimiter, [name: :account_confirmation_resend] when action == :confirmation_resend) - plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe]) + plug(:assign_account_by_id when action in [:favourites, :endorsements, :subscribe, :unsubscribe]) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaAccountOperation @@ -90,6 +101,23 @@ def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do ) end + @doc "GET /api/v1/pleroma/accounts/:id/endorsements" + def endorsements(%{assigns: %{user: for_user, account: user}} = conn, params) do + users = + user + |> User.endorsed_users_relation(_restrict_deactivated = true) + |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true)) + + conn + |> add_link_headers(users) + |> render("index.json", + for: for_user, + users: users, + as: :user, + embed_relationships: embed_relationships?(params) + ) + 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 diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index abb332ec2..bc9d31a24 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -411,6 +411,7 @@ defmodule Pleroma.Web.Router do scope [] do pipe_through(:api) get("/accounts/:id/favourites", AccountController, :favourites) + get("/accounts/:id/endorsements", AccountController, :endorsements) end scope [] do @@ -456,6 +457,8 @@ defmodule Pleroma.Web.Router do post("/accounts/:id/unblock", AccountController, :unblock) post("/accounts/:id/mute", AccountController, :mute) post("/accounts/:id/unmute", AccountController, :unmute) + post("/accounts/:id/pin", AccountController, :endorse) + post("/accounts/:id/unpin", AccountController, :unendorse) get("/conversations", ConversationController, :index) post("/conversations/:id/read", ConversationController, :mark_as_read) From 0f90fd58052aa372aaad63d769cd724046c9f61f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 10 Jan 2022 21:35:55 +0100 Subject: [PATCH 2/3] WIP account endorsements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- config/config.exs | 3 +- config/description.exs | 10 ++++ .../API/differences_in_mastoapi_responses.md | 6 --- lib/pleroma/pagination.ex | 7 +-- lib/pleroma/user.ex | 20 +++++-- .../api_spec/operations/account_operation.ex | 14 +++-- .../operations/pleroma_account_operation.ex | 15 +----- lib/pleroma/web/common_api.ex | 3 +- .../controllers/account_controller.ex | 10 ++-- .../controllers/account_controller.ex | 17 ++---- .../controllers/account_controller_test.exs | 53 +++++++++++++++++++ 11 files changed, 107 insertions(+), 51 deletions(-) diff --git a/config/config.exs b/config/config.exs index 2bde5b826..1385ce5de 100644 --- a/config/config.exs +++ b/config/config.exs @@ -258,7 +258,8 @@ 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: [ diff --git a/config/description.exs b/config/description.exs index ea3f34abe..644c60a63 100644 --- a/config/description.exs +++ b/config/description.exs @@ -742,6 +742,16 @@ 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}, diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md index 518aca114..0e6bcb79b 100644 --- a/docs/development/API/differences_in_mastoapi_responses.md +++ b/docs/development/API/differences_in_mastoapi_responses.md @@ -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* diff --git a/lib/pleroma/pagination.ex b/lib/pleroma/pagination.ex index 2ce243845..33e45a0eb 100644 --- a/lib/pleroma/pagination.ex +++ b/lib/pleroma/pagination.ex @@ -94,8 +94,7 @@ defp cast_params(params) 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 @@ defp restrict(query, :max_id, %{max_id: max_id}, table_binding) 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 diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index ea72af517..1b426c9d7 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -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 @@ def unblock(%User{} = blocker, %{ap_id: ap_id}) 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 diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 35d8609ef..768d3c720 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -343,7 +343,15 @@ def endorse_operation 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 @@ def endorsements_operation 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 diff --git a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex index 9996ff68b..ed0db173e 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex @@ -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 @@ def endorsements_operation 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 @@ def endorsements_operation do "application/json", AccountOperation.array_of_accounts() ), - 403 => Operation.response("Forbidden", "application/json", ApiError), 404 => Operation.response("Not Found", "application/json", ApiError) } } diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index 6f685cb7b..2481e4e16 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -117,7 +117,8 @@ def follow(follower, followed) 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 diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 1e9ce2927..0c0548828 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -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 @@ def note( 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) diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index 805a1d7af..549a08f61 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -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 @@ def endorsements(%{assigns: %{user: for_user, account: user}} = conn, params) 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 @@ def endorsements(%{assigns: %{user: for_user, account: user}} = conn, params) 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 diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index 374e2048a..828ebddd6 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -1838,4 +1838,57 @@ test "create a note on a user" 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 From eedf551eedd7acb854498303259598ad7aa72b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Wed, 12 Jan 2022 21:39:14 +0100 Subject: [PATCH 3/3] Add more tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- lib/pleroma/user.ex | 4 +- .../controllers/account_controller.ex | 3 +- .../controllers/account_controller.ex | 1 - test/pleroma/user_test.exs | 35 ++++++++++++++++ test/pleroma/web/common_api_test.exs | 12 ++++++ .../controllers/account_controller_test.exs | 41 +++++++++++-------- .../controllers/account_controller_test.exs | 25 +++++++++++ 7 files changed, 100 insertions(+), 21 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 1b426c9d7..0a5dfccc9 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1525,9 +1525,9 @@ def endorse(%User{} = endorser, %User{} = 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 + |> Repo.aggregate(:count, :id) do cond do - Enum.count(endorsed_users) >= max_endorsed_users -> + endorsed_users >= max_endorsed_users -> {:error, "You have already pinned the maximum number of users"} not following?(endorser, target) -> diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 0c0548828..a90833bf0 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -529,10 +529,9 @@ def endorsements(%{assigns: %{user: user}} = conn, params) do users = user |> User.endorsed_users_relation(_restrict_deactivated = true) - |> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true)) + |> Pleroma.Repo.all() conn - |> add_link_headers(users) |> render("index.json", users: users, for: user, diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index 549a08f61..66a8d1c1c 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -112,7 +112,6 @@ def endorsements(%{assigns: %{user: for_user, account: user}} = conn, params) do |> Pleroma.Repo.all() conn - |> add_link_headers(users) |> render("index.json", for: for_user, users: users, diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index 6cd93c34c..0345a9290 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -2498,4 +2498,39 @@ defp object_id_from_created_activity(user) do %{object: %{data: %{"id" => object_id}}} = Activity.get_by_id_with_object(id) object_id end + + describe "account endorsements" do + test "it pins people" do + user = insert(:user) + pinned_user = insert(:user) + + {:ok, _pinned_user, _user} = User.follow(user, pinned_user) + + refute User.endorses?(user, pinned_user) + + {:ok, _user_relationship} = User.endorse(user, pinned_user) + + assert User.endorses?(user, pinned_user) + end + + test "it unpins users" do + user = insert(:user) + pinned_user = insert(:user) + + {:ok, _pinned_user, _user} = User.follow(user, pinned_user) + {:ok, _user_relationship} = User.endorse(user, pinned_user) + {:ok, _user_pin} = User.unendorse(user, pinned_user) + + refute User.endorses?(user, pinned_user) + end + + test "it doesn't pin users you do not follow" do + user = insert(:user) + pinned_user = insert(:user) + + assert {:error, _message} = User.endorse(user, pinned_user) + + refute User.endorses?(user, pinned_user) + end + end end diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index ad0b87543..4b186ccfc 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -1207,6 +1207,18 @@ test "also unsubscribes a user" do refute User.subscribed_to?(follower, followed) end + test "also unpins a user" do + [follower, followed] = insert_pair(:user) + {:ok, follower, followed, _} = CommonAPI.follow(follower, followed) + {:ok, _endorsement} = User.endorse(follower, followed) + + assert User.endorses?(follower, followed) + + {:ok, follower} = CommonAPI.unfollow(follower, followed) + + refute User.endorses?(follower, followed) + end + test "cancels a pending follow for a local user" do follower = insert(:user) followed = insert(:user, is_locked: true) diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs index 828ebddd6..bba528d83 100644 --- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs @@ -1845,12 +1845,9 @@ test "create a note on a user" do setup do: clear_config([:instance, :max_endorsed_users], 1) test "pin account", %{user: user, conn: conn} do - %{id: id1} = insert(:user) + %{id: id1} = other_user1 = insert(:user) - conn - |> put_req_header("content-type", "application/json") - |> post("/api/v1/accounts/#{id1}/follow") - |> json_response_and_validate_schema(200) + CommonAPI.follow(user, other_user1) assert %{"id" => ^id1, "endorsed" => true} = conn @@ -1865,19 +1862,31 @@ test "pin account", %{user: user, conn: conn} do |> json_response_and_validate_schema(200) end + test "unpin account", %{user: user, conn: conn} do + %{id: id1} = other_user1 = insert(:user) + + CommonAPI.follow(user, other_user1) + User.endorse(user, other_user1) + + assert %{"id" => ^id1, "endorsed" => false} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/accounts/#{id1}/unpin") + |> json_response_and_validate_schema(200) + + assert [] = + 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) + %{id: id1} = other_user1 = insert(:user) + %{id: id2} = other_user2 = 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) + CommonAPI.follow(user, other_user1) + CommonAPI.follow(user, other_user2) conn |> put_req_header("content-type", "application/json") diff --git a/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs index ad271c31b..d9aa8ce55 100644 --- a/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/account_controller_test.exs @@ -279,4 +279,29 @@ test "returns 404 when subscription_target not found" do assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404) end end + + describe "account endorsements" do + test "returns a list of pinned accounts", %{conn: conn} do + %{id: id1} = user1 = insert(:user) + %{id: id2} = user2 = insert(:user) + %{id: id3} = user3 = insert(:user) + + CommonAPI.follow(user1, user2) + CommonAPI.follow(user1, user3) + + User.endorse(user1, user2) + User.endorse(user1, user3) + + [%{"id" => ^id2}, %{"id" => ^id3}] = + conn + |> get("/api/v1/pleroma/accounts/#{id1}/endorsements") + |> json_response_and_validate_schema(200) + end + + test "returns 404 error when specified user is not exist", %{conn: conn} do + conn = get(conn, "/api/v1/pleroma/accounts/test/endorsements") + + assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} + end + end end