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
|
||||||
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
|
def increase_vote_count(ap_id, name, actor) do
|
||||||
with %Object{} = object <- Object.normalize(ap_id, fetch: false),
|
with %Object{} = object <- Object.normalize(ap_id, fetch: false),
|
||||||
"Question" <- object.data["type"] do
|
"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_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]
|
@object_types ~w[ChatMessage Question Answer Audio Video Image Event Article Note Page]
|
||||||
@impl true
|
@impl true
|
||||||
def persist(%{"type" => type} = object, meta) when type in @object_types do
|
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),
|
with {:ok, activity} <- insert(create_data, local, fake),
|
||||||
{:fake, false, activity} <- {:fake, fake, activity},
|
{:fake, false, activity} <- {:fake, fake, activity},
|
||||||
_ <- increase_replies_count_if_reply(create_data),
|
_ <- increase_replies_count_if_reply(create_data),
|
||||||
|
_ <- increase_quotes_count_if_quote(create_data),
|
||||||
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
|
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
|
||||||
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
|
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
|
||||||
{:ok, _actor} <- update_last_status_at_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_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, %{include_poll_votes: true}), do: query
|
||||||
|
|
||||||
defp exclude_poll_votes(query, _) do
|
defp exclude_poll_votes(query, _) do
|
||||||
|
@ -1402,6 +1422,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|
||||||
|> restrict_instance(opts)
|
|> restrict_instance(opts)
|
||||||
|> restrict_announce_object_actor(opts)
|
|> restrict_announce_object_actor(opts)
|
||||||
|> restrict_filtered(opts)
|
|> restrict_filtered(opts)
|
||||||
|
|> restrict_quote_url(opts)
|
||||||
|> maybe_restrict_deactivated_users(opts)
|
|> maybe_restrict_deactivated_users(opts)
|
||||||
|> exclude_poll_votes(opts)
|
|> exclude_poll_votes(opts)
|
||||||
|> exclude_chat_messages(opts)
|
|> exclude_chat_messages(opts)
|
||||||
|
|
|
@ -57,6 +57,7 @@ defmacro status_object_fields do
|
||||||
field(:replies_count, :integer, default: 0)
|
field(:replies_count, :integer, default: 0)
|
||||||
field(:like_count, :integer, default: 0)
|
field(:like_count, :integer, default: 0)
|
||||||
field(:announcement_count, :integer, default: 0)
|
field(:announcement_count, :integer, default: 0)
|
||||||
|
field(:quotes_count, :integer, default: 0)
|
||||||
field(:inReplyTo, ObjectValidators.ObjectID)
|
field(:inReplyTo, ObjectValidators.ObjectID)
|
||||||
field(:quoteUrl, ObjectValidators.ObjectID)
|
field(:quoteUrl, ObjectValidators.ObjectID)
|
||||||
field(:url, ObjectValidators.BareUri)
|
field(:url, ObjectValidators.BareUri)
|
||||||
|
|
|
@ -210,6 +210,10 @@ def handle(%{data: %{"type" => "Create"}} = activity, meta) do
|
||||||
Object.increase_replies_count(in_reply_to)
|
Object.increase_replies_count(in_reply_to)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if quote_url = object.data["quoteUrl"] do
|
||||||
|
Object.increase_quotes_count(quote_url)
|
||||||
|
end
|
||||||
|
|
||||||
reply_depth = (meta[:depth] || 0) + 1
|
reply_depth = (meta[:depth] || 0) + 1
|
||||||
|
|
||||||
# FIXME: Force inReplyTo to replies
|
# 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)
|
Object.decrease_replies_count(in_reply_to)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if quote_url = deleted_object.data["quoteUrl"] do
|
||||||
|
Object.decrease_quotes_count(quote_url)
|
||||||
|
end
|
||||||
|
|
||||||
MessageReference.delete_for_object(deleted_object)
|
MessageReference.delete_for_object(deleted_object)
|
||||||
|
|
||||||
ap_streamer().stream_out(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,
|
type: :boolean,
|
||||||
description: "`true` if the quoted post is visible to the user"
|
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{
|
local: %Schema{
|
||||||
type: :boolean,
|
type: :boolean,
|
||||||
description: "`true` if the post was made on the local instance"
|
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,
|
"in_reply_to_account_acct" => nil,
|
||||||
"local" => true,
|
"local" => true,
|
||||||
"spoiler_text" => %{"text/plain" => ""},
|
"spoiler_text" => %{"text/plain" => ""},
|
||||||
"thread_muted" => false
|
"thread_muted" => false,
|
||||||
|
"quotes_count" => 0
|
||||||
},
|
},
|
||||||
"poll" => nil,
|
"poll" => nil,
|
||||||
"reblog" => nil,
|
"reblog" => nil,
|
||||||
|
|
|
@ -447,7 +447,8 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
|
||||||
thread_muted: thread_muted?,
|
thread_muted: thread_muted?,
|
||||||
emoji_reactions: emoji_reactions,
|
emoji_reactions: emoji_reactions,
|
||||||
parent_visible: visible_for_user?(reply_to, opts[:for]),
|
parent_visible: visible_for_user?(reply_to, opts[:for]),
|
||||||
pinned_at: pinned_at
|
pinned_at: pinned_at,
|
||||||
|
quotes_count: object.data["quotesCount"] || 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
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)
|
pipe_through(:api)
|
||||||
get("/accounts/:id/favourites", AccountController, :favourites)
|
get("/accounts/:id/favourites", AccountController, :favourites)
|
||||||
get("/accounts/:id/endorsements", AccountController, :endorsements)
|
get("/accounts/:id/endorsements", AccountController, :endorsements)
|
||||||
|
|
||||||
|
get("/statuses/:id/quotes", StatusController, :quotes)
|
||||||
end
|
end
|
||||||
|
|
||||||
scope [] do
|
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 %{data: _data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
|
||||||
assert object.data["repliesCount"] == 2
|
assert object.data["repliesCount"] == 2
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "fetch activities for recipients" do
|
describe "fetch activities for recipients" do
|
||||||
|
|
|
@ -337,7 +337,8 @@ test "a note activity" do
|
||||||
thread_muted: false,
|
thread_muted: false,
|
||||||
emoji_reactions: [],
|
emoji_reactions: [],
|
||||||
parent_visible: false,
|
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