track hashtags usage

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2023-01-16 21:08:03 +01:00
parent 98b7b701fd
commit bdd5509b79
7 changed files with 200 additions and 5 deletions

View file

@ -0,0 +1,76 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UserHashtag do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias Pleroma.Hashtag
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.UserHashtag
schema "user_hashtags" do
field(:use_count, :integer, default: 0)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:hashtag, Hashtag)
timestamps()
end
def changeset(%UserHashtag{} = user_hashtag, params \\ %{}) do
user_hashtag
|> cast(params, [:user_id, :hashtag_id, :use_count])
|> validate_required([:user_id, :hashtag_id, :use_count])
end
def get(%User{} = user, %Hashtag{} = hashtag) do
Repo.get_by(UserHashtag, user_id: user.id, hashtag_id: hashtag.id)
end
def increase_use_count(%User{} = user, %Hashtag{} = hashtag) do
case get(user, hashtag) do
%UserHashtag{use_count: use_count} = user_hashtag ->
user_hashtag
|> changeset(%{use_count: use_count + 1})
|> Repo.update!()
nil ->
%UserHashtag{}
|> changeset(%{user_id: user.id, hashtag_id: hashtag.id, use_count: 1})
|> Repo.insert!()
end
end
def decrease_use_count(%User{} = user, %Hashtag{} = hashtag) do
case get(user, hashtag) do
%UserHashtag{use_count: use_count} = user_hashtag ->
user_hashtag
|> changeset(%{use_count: max(use_count - 1, 0)})
|> Repo.update!()
nil -> nil
end
end
def all() do
Hashtag
|> group_by([h], [h.id, h.name])
|> join(:inner, [h], uh in UserHashtag, on: h.id == uh.hashtag_id)
|> select([h, uh], %{hashtag: h, use_count: sum(uh.use_count)})
|> order_by([_, uh], desc: sum(uh.use_count))
|> Repo.all()
end
def for_user(%User{id: user_id}) do
UserHashtag
|> where(user_id: ^user_id)
|> order_by(desc: :use_count)
|> Repo.all()
|> Repo.preload(:hashtag)
end
end

View file

@ -111,6 +111,15 @@ defp increase_quotes_count_if_quote(%{
defp increase_quotes_count_if_quote(_create_data), do: :noop
defp increase_hashtag_count(actor, %{object: %{hashtags: hashtags} = object})
when is_list(hashtags) do
if is_public?(object) do
Enum.each(hashtags, fn hashtag -> Pleroma.UserHashtag.increase_use_count(actor, hashtag) end)
end
end
defp increase_hashtag_count(_, _), 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
@ -318,6 +327,7 @@ defp do_create(%{to: to, actor: actor, context: context, object: object} = param
{:fake, false, activity} <- {:fake, fake, activity},
_ <- increase_replies_count_if_reply(create_data),
_ <- increase_quotes_count_if_quote(create_data),
_ <- increase_hashtag_count(actor, activity),
{: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),

View file

@ -21,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Push
alias Pleroma.Web.Streamer
alias Pleroma.Workers.EventReminderWorker
@ -292,8 +293,9 @@ def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do
@impl true
def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do
deleted_object =
Object.normalize(deleted_object, fetch: false) ||
User.get_cached_by_ap_id(deleted_object)
(Object.normalize(deleted_object, fetch: false) ||
User.get_cached_by_ap_id(deleted_object))
|> Repo.preload(:hashtags)
result =
case deleted_object do
@ -313,6 +315,10 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object,
Object.decrease_quotes_count(quote_url)
end
if Visibility.is_public?(deleted_object) do
Enum.each(deleted_object.hashtags, fn hashtag -> Pleroma.UserHashtag.decrease_use_count(user, hashtag) end)
end
MessageReference.delete_for_object(deleted_object)
ap_streamer().stream_out(object)

View file

@ -141,10 +141,61 @@ def birthdays_operation do
}
end
defp id_param do
def hashtags_operation do
%Operation{
tags: ["Retrieve account information"],
summary: "Hashtags",
description: "Most used hashtags",
operationId: "PleromaAPI.AccountController.hashtags",
parameters: [id_param()],
responses: %{
200 =>
Operation.response(
"Array of Hashtags",
"application/json",
array_of_hashtags()
),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def all_hashtags_operation do
%Operation{
tags: ["Retrieve account information"],
summary: "Hashtags",
description: "Most used hashtags",
operationId: "PleromaAPI.AccountController.all_hashtags",
parameters: [],
responses: %{
200 =>
Operation.response(
"Array of Hashtags",
"application/json",
array_of_hashtags()
),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
defp id_param() do
Operation.parameter(:id, :path, FlakeID.schema(), "Account ID",
example: "9umDrYheeY451cQnEe",
required: true
)
end
defp array_of_hashtags() do
%Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
name: %Schema{type: :string},
use_count: %Schema{type: :integer}
}
}
}
end
end

View file

@ -14,6 +14,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
]
alias Pleroma.User
alias Pleroma.UserHashtag
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.Plugs.OAuthScopesPlug
@ -48,7 +49,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
when action == :endorsements
when action in [:endorsements, :hashtags, :all_hashtags]
)
plug(
@ -60,7 +61,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
plug(
:assign_account_by_id
when action in [:favourites, :endorsements, :subscribe, :unsubscribe]
when action in [:favourites, :endorsements, :subscribe, :unsubscribe, :hashtags]
)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaAccountOperation
@ -156,4 +157,34 @@ def birthdays(%{assigns: %{user: %User{} = user}} = conn, %{day: day, month: mon
as: :user
)
end
@doc "POST /api/v1/pleroma/accounts/:id/hashtags"
def hashtags(%{assigns: %{account: user}} = conn, _params) do
with hashtags <- UserHashtag.for_user(user) do
conn
|> json(
hashtags
|> Enum.map(fn %{hashtag: hashtag, use_count: use_count} ->
%{name: hashtag.name, use_count: use_count}
end)
)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "POST /api/v1/pleroma/accounts/hashtags"
def all_hashtags(conn, _params) do
with hashtags <- UserHashtag.all() do
conn
|> json(
hashtags
|> Enum.map(fn %{hashtag: hashtag, use_count: use_count} ->
%{name: hashtag.name, use_count: use_count}
end)
)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
end

View file

@ -687,6 +687,8 @@ defmodule Pleroma.Web.Router do
pipe_through(:api)
get("/accounts/:id/favourites", AccountController, :favourites)
get("/accounts/:id/endorsements", AccountController, :endorsements)
get("/accounts/:id/hashtags", AccountController, :hashtags)
get("/accounts/hashtags", AccountController, :all_hashtags)
get("/statuses/:id/quotes", StatusController, :quotes)
end

View file

@ -0,0 +1,19 @@
# 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.CreateUserHashtags do
use Ecto.Migration
def change do
create_if_not_exists table(:user_hashtags) do
add(:user_id, references(:users, type: :uuid, on_delete: :delete_all))
add(:hashtag_id, references(:hashtags, on_delete: :delete_all))
add(:use_count, :integer, default: 0)
timestamps()
end
create_if_not_exists(unique_index(:user_hashtags, [:user_id, :hashtag_id]))
end
end