track hashtags usage
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
98b7b701fd
commit
bdd5509b79
7 changed files with 200 additions and 5 deletions
76
lib/pleroma/user_hashtag.ex
Normal file
76
lib/pleroma/user_hashtag.ex
Normal 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
|
|
@ -111,6 +111,15 @@ defp increase_quotes_count_if_quote(%{
|
||||||
|
|
||||||
defp increase_quotes_count_if_quote(_create_data), do: :noop
|
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]
|
@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
|
||||||
|
@ -318,6 +327,7 @@ defp do_create(%{to: to, actor: actor, context: context, object: object} = param
|
||||||
{: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),
|
_ <- increase_quotes_count_if_quote(create_data),
|
||||||
|
_ <- increase_hashtag_count(actor, activity),
|
||||||
{: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),
|
||||||
|
|
|
@ -21,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
||||||
alias Pleroma.Web.ActivityPub.Builder
|
alias Pleroma.Web.ActivityPub.Builder
|
||||||
alias Pleroma.Web.ActivityPub.Pipeline
|
alias Pleroma.Web.ActivityPub.Pipeline
|
||||||
alias Pleroma.Web.ActivityPub.Utils
|
alias Pleroma.Web.ActivityPub.Utils
|
||||||
|
alias Pleroma.Web.ActivityPub.Visibility
|
||||||
alias Pleroma.Web.Push
|
alias Pleroma.Web.Push
|
||||||
alias Pleroma.Web.Streamer
|
alias Pleroma.Web.Streamer
|
||||||
alias Pleroma.Workers.EventReminderWorker
|
alias Pleroma.Workers.EventReminderWorker
|
||||||
|
@ -292,8 +293,9 @@ def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do
|
||||||
@impl true
|
@impl true
|
||||||
def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do
|
def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do
|
||||||
deleted_object =
|
deleted_object =
|
||||||
Object.normalize(deleted_object, fetch: false) ||
|
(Object.normalize(deleted_object, fetch: false) ||
|
||||||
User.get_cached_by_ap_id(deleted_object)
|
User.get_cached_by_ap_id(deleted_object))
|
||||||
|
|> Repo.preload(:hashtags)
|
||||||
|
|
||||||
result =
|
result =
|
||||||
case deleted_object do
|
case deleted_object do
|
||||||
|
@ -313,6 +315,10 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object,
|
||||||
Object.decrease_quotes_count(quote_url)
|
Object.decrease_quotes_count(quote_url)
|
||||||
end
|
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)
|
MessageReference.delete_for_object(deleted_object)
|
||||||
|
|
||||||
ap_streamer().stream_out(object)
|
ap_streamer().stream_out(object)
|
||||||
|
|
|
@ -141,10 +141,61 @@ def birthdays_operation do
|
||||||
}
|
}
|
||||||
end
|
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",
|
Operation.parameter(:id, :path, FlakeID.schema(), "Account ID",
|
||||||
example: "9umDrYheeY451cQnEe",
|
example: "9umDrYheeY451cQnEe",
|
||||||
required: true
|
required: true
|
||||||
)
|
)
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -14,6 +14,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
|
||||||
]
|
]
|
||||||
|
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
|
alias Pleroma.UserHashtag
|
||||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||||
alias Pleroma.Web.MastodonAPI.StatusView
|
alias Pleroma.Web.MastodonAPI.StatusView
|
||||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||||
|
@ -48,7 +49,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
|
||||||
plug(
|
plug(
|
||||||
OAuthScopesPlug,
|
OAuthScopesPlug,
|
||||||
%{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
|
%{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
|
||||||
when action == :endorsements
|
when action in [:endorsements, :hashtags, :all_hashtags]
|
||||||
)
|
)
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
|
@ -60,7 +61,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
:assign_account_by_id
|
: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
|
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
|
as: :user
|
||||||
)
|
)
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -687,6 +687,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("/accounts/:id/hashtags", AccountController, :hashtags)
|
||||||
|
get("/accounts/hashtags", AccountController, :all_hashtags)
|
||||||
|
|
||||||
get("/statuses/:id/quotes", StatusController, :quotes)
|
get("/statuses/:id/quotes", StatusController, :quotes)
|
||||||
end
|
end
|
||||||
|
|
19
priv/repo/migrations/20230111000000_create_user_hashtags.exs
Normal file
19
priv/repo/migrations/20230111000000_create_user_hashtags.exs
Normal 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
|
Loading…
Reference in a new issue