Merge branch 'quotes-count' into 'develop'
Count and display post quotes See merge request pleroma/pleroma!3956
This commit is contained in:
commit
752bc168f6
|
@ -328,6 +328,52 @@ def decrease_replies_count(ap_id) 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
|
||||
|
|
|
@ -96,6 +96,17 @@ defp increase_replies_count_if_reply(%{
|
|||
|
||||
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 @@ defp do_create(%{to: to, actor: actor, context: context, object: object} = param
|
|||
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 @@ defp restrict_unauthenticated(query, nil) 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 @@ def fetch_activities_query(recipients, opts \\ %{}) 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)
|
||||
|
|
|
@ -57,6 +57,7 @@ defmacro status_object_fields 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)
|
||||
|
|
|
@ -210,6 +210,10 @@ def handle(%{data: %{"type" => "Create"}} = activity, meta) 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 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object,
|
|||
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)
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -447,7 +447,8 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
|
|||
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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -770,6 +770,34 @@ test "increases replies count", %{user: user} 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
|
||||
|
|
|
@ -337,7 +337,8 @@ test "a note activity" do
|
|||
thread_muted: false,
|
||||
emoji_reactions: [],
|
||||
parent_visible: false,
|
||||
pinned_at: nil
|
||||
pinned_at: nil,
|
||||
quotes_count: 0
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue