diff options
| -rw-r--r-- | changelog.d/quotes-count.skip | 0 | ||||
| -rw-r--r-- | lib/pleroma/object.ex | 46 | ||||
| -rw-r--r-- | lib/pleroma/web/activity_pub/activity_pub.ex | 21 | ||||
| -rw-r--r-- | lib/pleroma/web/activity_pub/object_validators/common_fields.ex | 1 | ||||
| -rw-r--r-- | lib/pleroma/web/activity_pub/side_effects.ex | 8 | ||||
| -rw-r--r-- | lib/pleroma/web/api_spec/operations/pleroma_status_operation.ex | 45 | ||||
| -rw-r--r-- | lib/pleroma/web/api_spec/schemas/status.ex | 7 | ||||
| -rw-r--r-- | lib/pleroma/web/mastodon_api/views/status_view.ex | 3 | ||||
| -rw-r--r-- | lib/pleroma/web/pleroma_api/controllers/status_controller.ex | 66 | ||||
| -rw-r--r-- | lib/pleroma/web/router.ex | 2 | ||||
| -rw-r--r-- | priv/repo/migrations/20220527134341_add_quote_url_index_to_objects.exs | 11 | ||||
| -rw-r--r-- | test/pleroma/web/activity_pub/activity_pub_test.exs | 28 | ||||
| -rw-r--r-- | test/pleroma/web/mastodon_api/views/status_view_test.exs | 3 | ||||
| -rw-r--r-- | test/pleroma/web/pleroma_api/controllers/status_controller_test.exs | 54 | 
14 files changed, 292 insertions, 3 deletions
diff --git a/changelog.d/quotes-count.skip b/changelog.d/quotes-count.skip new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/changelog.d/quotes-count.skip diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index aa137d250..fa5baf1a4 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -328,6 +328,52 @@ defmodule Pleroma.Object do      end    end +  def increase_quotes_count(ap_id) do +    Object +    |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id))) +    |> update([o], +      set: [ +        data: +          fragment( +            """ +            safe_jsonb_set(?, '{quotesCount}', +              (coalesce((?->>'quotesCount')::int, 0) + 1)::varchar::jsonb, true) +            """, +            o.data, +            o.data +          ) +      ] +    ) +    |> Repo.update_all([]) +    |> case do +      {1, [object]} -> set_cache(object) +      _ -> {:error, "Not found"} +    end +  end + +  def decrease_quotes_count(ap_id) do +    Object +    |> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id))) +    |> update([o], +      set: [ +        data: +          fragment( +            """ +            safe_jsonb_set(?, '{quotesCount}', +              (greatest(0, (?->>'quotesCount')::int - 1))::varchar::jsonb, true) +            """, +            o.data, +            o.data +          ) +      ] +    ) +    |> Repo.update_all([]) +    |> case do +      {1, [object]} -> set_cache(object) +      _ -> {:error, "Not found"} +    end +  end +    def increase_vote_count(ap_id, name, actor) do      with %Object{} = object <- Object.normalize(ap_id, fetch: false),           "Question" <- object.data["type"] do diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 54a77a228..32d1a1037 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -96,6 +96,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp increase_replies_count_if_reply(_create_data), do: :noop +  defp increase_quotes_count_if_quote(%{ +         "object" => %{"quoteUrl" => quote_ap_id} = object, +         "type" => "Create" +       }) do +    if is_public?(object) do +      Object.increase_quotes_count(quote_ap_id) +    end +  end + +  defp increase_quotes_count_if_quote(_create_data), do: :noop +    @object_types ~w[ChatMessage Question Answer Audio Video Image Event Article Note Page]    @impl true    def persist(%{"type" => type} = object, meta) when type in @object_types do @@ -302,6 +313,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      with {:ok, activity} <- insert(create_data, local, fake),           {:fake, false, activity} <- {:fake, fake, activity},           _ <- increase_replies_count_if_reply(create_data), +         _ <- increase_quotes_count_if_quote(create_data),           {:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},           {:ok, _actor} <- increase_note_count_if_public(actor, activity),           {:ok, _actor} <- update_last_status_at_if_public(actor, activity), @@ -1240,6 +1252,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp restrict_unauthenticated(query, _), do: query +  defp restrict_quote_url(query, %{quote_url: quote_url}) do +    from([_activity, object] in query, +      where: fragment("(?)->'quoteUrl' = ?", object.data, ^quote_url) +    ) +  end + +  defp restrict_quote_url(query, _), do: query +    defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query    defp exclude_poll_votes(query, _) do @@ -1402,6 +1422,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        |> restrict_instance(opts)        |> restrict_announce_object_actor(opts)        |> restrict_filtered(opts) +      |> restrict_quote_url(opts)        |> maybe_restrict_deactivated_users(opts)        |> exclude_poll_votes(opts)        |> exclude_chat_messages(opts) diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex index 835ed97b7..1a5d02601 100644 --- a/lib/pleroma/web/activity_pub/object_validators/common_fields.ex +++ b/lib/pleroma/web/activity_pub/object_validators/common_fields.ex @@ -57,6 +57,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do        field(:replies_count, :integer, default: 0)        field(:like_count, :integer, default: 0)        field(:announcement_count, :integer, default: 0) +      field(:quotes_count, :integer, default: 0)        field(:inReplyTo, ObjectValidators.ObjectID)        field(:quoteUrl, ObjectValidators.ObjectID)        field(:url, ObjectValidators.BareUri) diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 7a28a7c97..10f268f05 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -210,6 +210,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do          Object.increase_replies_count(in_reply_to)        end +      if quote_url = object.data["quoteUrl"] do +        Object.increase_quotes_count(quote_url) +      end +        reply_depth = (meta[:depth] || 0) + 1        # FIXME: Force inReplyTo to replies @@ -309,6 +313,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do                Object.decrease_replies_count(in_reply_to)              end +            if quote_url = deleted_object.data["quoteUrl"] do +              Object.decrease_quotes_count(quote_url) +            end +              MessageReference.delete_for_object(deleted_object)              ap_streamer().stream_out(object) diff --git a/lib/pleroma/web/api_spec/operations/pleroma_status_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_status_operation.ex new file mode 100644 index 000000000..6e69c5269 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/pleroma_status_operation.ex @@ -0,0 +1,45 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.PleromaStatusOperation do +  alias OpenApiSpex.Operation +  alias Pleroma.Web.ApiSpec.Schemas.ApiError +  alias Pleroma.Web.ApiSpec.Schemas.FlakeID +  alias Pleroma.Web.ApiSpec.StatusOperation + +  import Pleroma.Web.ApiSpec.Helpers + +  def open_api_operation(action) do +    operation = String.to_existing_atom("#{action}_operation") +    apply(__MODULE__, operation, []) +  end + +  def quotes_operation do +    %Operation{ +      tags: ["Retrieve status information"], +      summary: "Quoted by", +      description: "View quotes for a given status", +      operationId: "PleromaAPI.StatusController.quotes", +      parameters: [id_param() | pagination_params()], +      security: [%{"oAuth" => ["read:statuses"]}], +      responses: %{ +        200 => +          Operation.response( +            "Array of Status", +            "application/json", +            StatusOperation.array_of_statuses() +          ), +        403 => Operation.response("Forbidden", "application/json", ApiError), +        404 => Operation.response("Not Found", "application/json", ApiError) +      } +    } +  end + +  def id_param do +    Operation.parameter(:id, :path, FlakeID, "Status ID", +      example: "9umDrYheeY451cQnEe", +      required: true +    ) +  end +end diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index 07f03134a..a4052803b 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -213,6 +213,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do              type: :boolean,              description: "`true` if the quoted post is visible to the user"            }, +          quotes_count: %Schema{ +            type: :integer, +            description: "How many statuses quoted this status" +          },            local: %Schema{              type: :boolean,              description: "`true` if the post was made on the local instance" @@ -367,7 +371,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do          "in_reply_to_account_acct" => nil,          "local" => true,          "spoiler_text" => %{"text/plain" => ""}, -        "thread_muted" => false +        "thread_muted" => false, +        "quotes_count" => 0        },        "poll" => nil,        "reblog" => nil, diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index d070262cc..e3b5760fa 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -447,7 +447,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do          thread_muted: thread_muted?,          emoji_reactions: emoji_reactions,          parent_visible: visible_for_user?(reply_to, opts[:for]), -        pinned_at: pinned_at +        pinned_at: pinned_at, +        quotes_count: object.data["quotesCount"] || 0        }      }    end diff --git a/lib/pleroma/web/pleroma_api/controllers/status_controller.ex b/lib/pleroma/web/pleroma_api/controllers/status_controller.ex new file mode 100644 index 000000000..482662fdd --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/status_controller.ex @@ -0,0 +1,66 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.StatusController do +  use Pleroma.Web, :controller + +  import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] + +  require Ecto.Query +  require Pleroma.Constants + +  alias Pleroma.Activity +  alias Pleroma.User +  alias Pleroma.Web.ActivityPub.ActivityPub +  alias Pleroma.Web.ActivityPub.Visibility +  alias Pleroma.Web.MastodonAPI.StatusView +  alias Pleroma.Web.Plugs.OAuthScopesPlug + +  plug(Pleroma.Web.ApiSpec.CastAndValidate) + +  action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + +  plug( +    OAuthScopesPlug, +    %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated} when action == :quotes +  ) + +  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaStatusOperation + +  @doc "GET /api/v1/pleroma/statuses/:id/quotes" +  def quotes(%{assigns: %{user: user}} = conn, %{id: id} = params) do +    with %Activity{object: object} = activity <- Activity.get_by_id_with_object(id), +         true <- Visibility.visible_for_user?(activity, user) do +      params = +        params +        |> Map.put(:type, "Create") +        |> Map.put(:blocking_user, user) +        |> Map.put(:quote_url, object.data["id"]) + +      recipients = +        if user do +          [Pleroma.Constants.as_public()] ++ [user.ap_id | User.following(user)] +        else +          [Pleroma.Constants.as_public()] +        end + +      activities = +        recipients +        |> ActivityPub.fetch_activities(params) +        |> Enum.reverse() + +      conn +      |> add_link_headers(activities) +      |> put_view(StatusView) +      |> render("index.json", +        activities: activities, +        for: user, +        as: :activity +      ) +    else +      nil -> {:error, :not_found} +      false -> {:error, :not_found} +    end +  end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index a6e180c4c..9abad65b0 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -578,6 +578,8 @@ defmodule Pleroma.Web.Router do        pipe_through(:api)        get("/accounts/:id/favourites", AccountController, :favourites)        get("/accounts/:id/endorsements", AccountController, :endorsements) + +      get("/statuses/:id/quotes", StatusController, :quotes)      end      scope [] do diff --git a/priv/repo/migrations/20220527134341_add_quote_url_index_to_objects.exs b/priv/repo/migrations/20220527134341_add_quote_url_index_to_objects.exs new file mode 100644 index 000000000..c746f12a1 --- /dev/null +++ b/priv/repo/migrations/20220527134341_add_quote_url_index_to_objects.exs @@ -0,0 +1,11 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.AddQuoteUrlIndexToObjects do +  use Ecto.Migration + +  def change do +    create_if_not_exists(index(:objects, ["(data->'quoteUrl')"], name: :objects_quote_url)) +  end +end diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs index 1e8c14043..40482fef0 100644 --- a/test/pleroma/web/activity_pub/activity_pub_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_test.exs @@ -770,6 +770,34 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do        assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id)        assert object.data["repliesCount"] == 2      end + +    test "increates quotes count", %{user: user} do +      user2 = insert(:user) + +      {:ok, activity} = CommonAPI.post(user, %{status: "1", visibility: "public"}) +      ap_id = activity.data["id"] +      quote_data = %{status: "1", quote_id: activity.id} + +      # public +      {:ok, _} = CommonAPI.post(user2, Map.put(quote_data, :visibility, "public")) +      assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id) +      assert object.data["quotesCount"] == 1 + +      # unlisted +      {:ok, _} = CommonAPI.post(user2, Map.put(quote_data, :visibility, "unlisted")) +      assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id) +      assert object.data["quotesCount"] == 2 + +      # private +      {:ok, _} = CommonAPI.post(user2, Map.put(quote_data, :visibility, "private")) +      assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id) +      assert object.data["quotesCount"] == 2 + +      # direct +      {:ok, _} = CommonAPI.post(user2, Map.put(quote_data, :visibility, "direct")) +      assert %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id) +      assert object.data["quotesCount"] == 2 +    end    end    describe "fetch activities for recipients" do diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs index baa9b32f5..57b81798d 100644 --- a/test/pleroma/web/mastodon_api/views/status_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs @@ -337,7 +337,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do          thread_muted: false,          emoji_reactions: [],          parent_visible: false, -        pinned_at: nil +        pinned_at: nil, +        quotes_count: 0        }      } diff --git a/test/pleroma/web/pleroma_api/controllers/status_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/status_controller_test.exs new file mode 100644 index 000000000..f942f0556 --- /dev/null +++ b/test/pleroma/web/pleroma_api/controllers/status_controller_test.exs @@ -0,0 +1,54 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.StatusControllerTest do +  use Pleroma.Web.ConnCase + +  alias Pleroma.Web.CommonAPI + +  import Pleroma.Factory + +  describe "getting quotes of a specified post" do +    setup do +      [current_user, user] = insert_pair(:user) +      %{user: current_user, conn: conn} = oauth_access(["read:statuses"], user: current_user) +      [current_user: current_user, user: user, conn: conn] +    end + +    test "shows quotes of a post", %{conn: conn} do +      user = insert(:user) +      activity = insert(:note_activity) + +      {:ok, quote_post} = CommonAPI.post(user, %{status: "quoat", quote_id: activity.id}) + +      response = +        conn +        |> get("/api/v1/pleroma/statuses/#{activity.id}/quotes") +        |> json_response_and_validate_schema(:ok) + +      [status] = response + +      assert length(response) == 1 +      assert status["id"] == quote_post.id +    end + +    test "returns 404 error when a post can't be seen", %{conn: conn} do +      activity = insert(:direct_note_activity) + +      response = +        conn +        |> get("/api/v1/pleroma/statuses/#{activity.id}/quotes") + +      assert json_response_and_validate_schema(response, 404) == %{"error" => "Record not found"} +    end + +    test "returns 404 error when a post does not exist", %{conn: conn} do +      response = +        conn +        |> get("/api/v1/pleroma/statuses/idontexist/quotes") + +      assert json_response_and_validate_schema(response, 404) == %{"error" => "Record not found"} +    end +  end +end  | 
