Add ability to follow hashtags (#336)
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk> Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/336
This commit is contained in:
parent
6b882a2c0b
commit
4a94c9a31e
18 changed files with 560 additions and 3 deletions
|
@ -471,9 +471,15 @@ defmodule Mix.Tasks.Pleroma.User do
|
||||||
|
|
||||||
def run(["timeline_query", nickname]) do
|
def run(["timeline_query", nickname]) do
|
||||||
start_pleroma()
|
start_pleroma()
|
||||||
|
|
||||||
params = %{local: true}
|
params = %{local: true}
|
||||||
|
|
||||||
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
|
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
|
||||||
|
followed_hashtags =
|
||||||
|
user
|
||||||
|
|> User.followed_hashtags()
|
||||||
|
|> Enum.map(& &1.id)
|
||||||
|
|
||||||
params =
|
params =
|
||||||
params
|
params
|
||||||
|> Map.put(:type, ["Create", "Announce"])
|
|> Map.put(:type, ["Create", "Announce"])
|
||||||
|
@ -484,6 +490,7 @@ defmodule Mix.Tasks.Pleroma.User do
|
||||||
|> Map.put(:announce_filtering_user, user)
|
|> Map.put(:announce_filtering_user, user)
|
||||||
|> Map.put(:user, user)
|
|> Map.put(:user, user)
|
||||||
|> Map.put(:local_only, params[:local])
|
|> Map.put(:local_only, params[:local])
|
||||||
|
|> Map.put(:hashtags, followed_hashtags)
|
||||||
|> Map.delete(:local)
|
|> Map.delete(:local)
|
||||||
|
|
||||||
_activities =
|
_activities =
|
||||||
|
|
|
@ -10,6 +10,7 @@ defmodule Pleroma.Hashtag do
|
||||||
|
|
||||||
alias Ecto.Multi
|
alias Ecto.Multi
|
||||||
alias Pleroma.Hashtag
|
alias Pleroma.Hashtag
|
||||||
|
alias Pleroma.User.HashtagFollow
|
||||||
alias Pleroma.Object
|
alias Pleroma.Object
|
||||||
alias Pleroma.Repo
|
alias Pleroma.Repo
|
||||||
|
|
||||||
|
@ -27,6 +28,14 @@ defmodule Pleroma.Hashtag do
|
||||||
|> String.trim()
|
|> String.trim()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_by_id(id) do
|
||||||
|
Repo.get(Hashtag, id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_by_name(name) do
|
||||||
|
Repo.get_by(Hashtag, name: normalize_name(name))
|
||||||
|
end
|
||||||
|
|
||||||
def get_or_create_by_name(name) do
|
def get_or_create_by_name(name) do
|
||||||
changeset = changeset(%Hashtag{}, %{name: name})
|
changeset = changeset(%Hashtag{}, %{name: name})
|
||||||
|
|
||||||
|
@ -103,4 +112,22 @@ defmodule Pleroma.Hashtag do
|
||||||
{:ok, deleted_count}
|
{:ok, deleted_count}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_followers(%Hashtag{id: hashtag_id}) do
|
||||||
|
from(hf in HashtagFollow)
|
||||||
|
|> where([hf], hf.hashtag_id == ^hashtag_id)
|
||||||
|
|> join(:inner, [hf], u in assoc(hf, :user))
|
||||||
|
|> select([hf, u], u.id)
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_recipients_for_activity(%Pleroma.Activity{object: %{hashtags: tags}})
|
||||||
|
when is_list(tags) do
|
||||||
|
tags
|
||||||
|
|> Enum.map(&get_followers/1)
|
||||||
|
|> List.flatten()
|
||||||
|
|> Enum.uniq()
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_recipients_for_activity(_activity), do: []
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,6 +18,8 @@ defmodule Pleroma.User do
|
||||||
alias Pleroma.Emoji
|
alias Pleroma.Emoji
|
||||||
alias Pleroma.FollowingRelationship
|
alias Pleroma.FollowingRelationship
|
||||||
alias Pleroma.Formatter
|
alias Pleroma.Formatter
|
||||||
|
alias Pleroma.Hashtag
|
||||||
|
alias Pleroma.User.HashtagFollow
|
||||||
alias Pleroma.HTML
|
alias Pleroma.HTML
|
||||||
alias Pleroma.Keys
|
alias Pleroma.Keys
|
||||||
alias Pleroma.MFA
|
alias Pleroma.MFA
|
||||||
|
@ -168,6 +170,12 @@ defmodule Pleroma.User do
|
||||||
|
|
||||||
has_many(:frontend_profiles, Pleroma.Akkoma.FrontendSettingsProfile)
|
has_many(:frontend_profiles, Pleroma.Akkoma.FrontendSettingsProfile)
|
||||||
|
|
||||||
|
many_to_many(:followed_hashtags, Hashtag,
|
||||||
|
on_replace: :delete,
|
||||||
|
on_delete: :delete_all,
|
||||||
|
join_through: HashtagFollow
|
||||||
|
)
|
||||||
|
|
||||||
for {relationship_type,
|
for {relationship_type,
|
||||||
[
|
[
|
||||||
{outgoing_relation, outgoing_relation_target},
|
{outgoing_relation, outgoing_relation_target},
|
||||||
|
@ -2550,4 +2558,54 @@ defmodule Pleroma.User do
|
||||||
_ -> {:error, user}
|
_ -> {:error, user}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp maybe_load_followed_hashtags(%User{followed_hashtags: follows} = user)
|
||||||
|
when is_list(follows),
|
||||||
|
do: user
|
||||||
|
|
||||||
|
defp maybe_load_followed_hashtags(%User{} = user) do
|
||||||
|
followed_hashtags = HashtagFollow.get_by_user(user)
|
||||||
|
%{user | followed_hashtags: followed_hashtags}
|
||||||
|
end
|
||||||
|
|
||||||
|
def followed_hashtags(%User{followed_hashtags: follows})
|
||||||
|
when is_list(follows),
|
||||||
|
do: follows
|
||||||
|
|
||||||
|
def followed_hashtags(%User{} = user) do
|
||||||
|
{:ok, user} =
|
||||||
|
user
|
||||||
|
|> maybe_load_followed_hashtags()
|
||||||
|
|> set_cache()
|
||||||
|
|
||||||
|
user.followed_hashtags
|
||||||
|
end
|
||||||
|
|
||||||
|
def follow_hashtag(%User{} = user, %Hashtag{} = hashtag) do
|
||||||
|
Logger.debug("Follow hashtag #{hashtag.name} for user #{user.nickname}")
|
||||||
|
user = maybe_load_followed_hashtags(user)
|
||||||
|
|
||||||
|
with {:ok, _} <- HashtagFollow.new(user, hashtag),
|
||||||
|
follows <- HashtagFollow.get_by_user(user),
|
||||||
|
%User{} = user <- user |> Map.put(:followed_hashtags, follows) do
|
||||||
|
user
|
||||||
|
|> set_cache()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def unfollow_hashtag(%User{} = user, %Hashtag{} = hashtag) do
|
||||||
|
Logger.debug("Unfollow hashtag #{hashtag.name} for user #{user.nickname}")
|
||||||
|
user = maybe_load_followed_hashtags(user)
|
||||||
|
|
||||||
|
with {:ok, _} <- HashtagFollow.delete(user, hashtag),
|
||||||
|
follows <- HashtagFollow.get_by_user(user),
|
||||||
|
%User{} = user <- user |> Map.put(:followed_hashtags, follows) do
|
||||||
|
user
|
||||||
|
|> set_cache()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def following_hashtag?(%User{} = user, %Hashtag{} = hashtag) do
|
||||||
|
not is_nil(HashtagFollow.get(user, hashtag))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
49
lib/pleroma/user/hashtag_follow.ex
Normal file
49
lib/pleroma/user/hashtag_follow.ex
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
defmodule Pleroma.User.HashtagFollow do
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Query
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Hashtag
|
||||||
|
alias Pleroma.Repo
|
||||||
|
|
||||||
|
schema "user_follows_hashtag" do
|
||||||
|
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
||||||
|
belongs_to(:hashtag, Hashtag)
|
||||||
|
end
|
||||||
|
|
||||||
|
def changeset(%__MODULE__{} = user_hashtag_follow, attrs) do
|
||||||
|
user_hashtag_follow
|
||||||
|
|> cast(attrs, [:user_id, :hashtag_id])
|
||||||
|
|> unique_constraint(:hashtag_id,
|
||||||
|
name: :user_hashtag_follows_user_id_hashtag_id_index,
|
||||||
|
message: "already following"
|
||||||
|
)
|
||||||
|
|> validate_required([:user_id, :hashtag_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def new(%User{} = user, %Hashtag{} = hashtag) do
|
||||||
|
%__MODULE__{}
|
||||||
|
|> changeset(%{user_id: user.id, hashtag_id: hashtag.id})
|
||||||
|
|> Repo.insert(on_conflict: :nothing)
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete(%User{} = user, %Hashtag{} = hashtag) do
|
||||||
|
with %__MODULE__{} = user_hashtag_follow <- get(user, hashtag) do
|
||||||
|
Repo.delete(user_hashtag_follow)
|
||||||
|
else
|
||||||
|
_ -> {:ok, nil}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get(%User{} = user, %Hashtag{} = hashtag) do
|
||||||
|
from(hf in __MODULE__)
|
||||||
|
|> where([hf], hf.user_id == ^user.id and hf.hashtag_id == ^hashtag.id)
|
||||||
|
|> Repo.one()
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_by_user(%User{} = user) do
|
||||||
|
Ecto.assoc(user, :followed_hashtags)
|
||||||
|
|> Repo.all()
|
||||||
|
end
|
||||||
|
end
|
|
@ -933,6 +933,29 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Essentially, either look for activities addressed to `recipients`, _OR_ ones
|
||||||
|
# that reference a hashtag that the user follows
|
||||||
|
# Firstly, two fallbacks in case there's no hashtag constraint, or the user doesn't
|
||||||
|
# follow any
|
||||||
|
defp restrict_recipients_or_hashtags(query, recipients, user, nil) do
|
||||||
|
restrict_recipients(query, recipients, user)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp restrict_recipients_or_hashtags(query, recipients, user, []) do
|
||||||
|
restrict_recipients(query, recipients, user)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp restrict_recipients_or_hashtags(query, recipients, _user, hashtag_ids) do
|
||||||
|
from(
|
||||||
|
[activity, object] in query,
|
||||||
|
join: hto in "hashtags_objects",
|
||||||
|
on: hto.object_id == object.id,
|
||||||
|
where:
|
||||||
|
(hto.hashtag_id in ^hashtag_ids and ^Constants.as_public() in activity.recipients) or
|
||||||
|
fragment("? && ?", ^recipients, activity.recipients)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
defp restrict_local(query, %{local_only: true}) do
|
defp restrict_local(query, %{local_only: true}) do
|
||||||
from(activity in query, where: activity.local == true)
|
from(activity in query, where: activity.local == true)
|
||||||
end
|
end
|
||||||
|
@ -1380,7 +1403,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||||
|> maybe_preload_report_notes(opts)
|
|> maybe_preload_report_notes(opts)
|
||||||
|> maybe_set_thread_muted_field(opts)
|
|> maybe_set_thread_muted_field(opts)
|
||||||
|> maybe_order(opts)
|
|> maybe_order(opts)
|
||||||
|> restrict_recipients(recipients, opts[:user])
|
|> restrict_recipients_or_hashtags(recipients, opts[:user], opts[:followed_hashtags])
|
||||||
|> restrict_replies(opts)
|
|> restrict_replies(opts)
|
||||||
|> restrict_since(opts)
|
|> restrict_since(opts)
|
||||||
|> restrict_local(opts)
|
|> restrict_local(opts)
|
||||||
|
|
65
lib/pleroma/web/api_spec/operations/tag_operation.ex
Normal file
65
lib/pleroma/web/api_spec/operations/tag_operation.ex
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
defmodule Pleroma.Web.ApiSpec.TagOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.Tag
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Tags"],
|
||||||
|
summary: "Hashtag",
|
||||||
|
description: "View a hashtag",
|
||||||
|
security: [%{"oAuth" => ["read"]}],
|
||||||
|
parameters: [id_param()],
|
||||||
|
operationId: "TagController.show",
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Hashtag", "application/json", Tag),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def follow_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Tags"],
|
||||||
|
summary: "Follow a hashtag",
|
||||||
|
description: "Follow a hashtag",
|
||||||
|
security: [%{"oAuth" => ["write:follows"]}],
|
||||||
|
parameters: [id_param()],
|
||||||
|
operationId: "TagController.follow",
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Hashtag", "application/json", Tag),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def unfollow_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Tags"],
|
||||||
|
summary: "Unfollow a hashtag",
|
||||||
|
description: "Unfollow a hashtag",
|
||||||
|
security: [%{"oAuth" => ["write:follow"]}],
|
||||||
|
parameters: [id_param()],
|
||||||
|
operationId: "TagController.unfollow",
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("Hashtag", "application/json", Tag),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp id_param do
|
||||||
|
Operation.parameter(
|
||||||
|
:id,
|
||||||
|
:path,
|
||||||
|
%Schema{type: :string},
|
||||||
|
"Name of the hashtag"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -17,11 +17,16 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Tag do
|
||||||
type: :string,
|
type: :string,
|
||||||
format: :uri,
|
format: :uri,
|
||||||
description: "A link to the hashtag on the instance"
|
description: "A link to the hashtag on the instance"
|
||||||
|
},
|
||||||
|
following: %Schema{
|
||||||
|
type: :boolean,
|
||||||
|
description: "Whether the authenticated user is following the hashtag"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
example: %{
|
example: %{
|
||||||
name: "cofe",
|
name: "cofe",
|
||||||
url: "https://lain.com/tag/cofe"
|
url: "https://lain.com/tag/cofe",
|
||||||
|
following: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
47
lib/pleroma/web/mastodon_api/controllers/tag_controller.ex
Normal file
47
lib/pleroma/web/mastodon_api/controllers/tag_controller.ex
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
defmodule Pleroma.Web.MastodonAPI.TagController do
|
||||||
|
@moduledoc "Hashtag routes for mastodon API"
|
||||||
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Hashtag
|
||||||
|
|
||||||
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
|
plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["read"]} when action in [:show])
|
||||||
|
|
||||||
|
plug(
|
||||||
|
Pleroma.Web.Plugs.OAuthScopesPlug,
|
||||||
|
%{scopes: ["write:follows"]} when action in [:follow, :unfollow]
|
||||||
|
)
|
||||||
|
|
||||||
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TagOperation
|
||||||
|
|
||||||
|
def show(conn, %{id: id}) do
|
||||||
|
with %Hashtag{} = hashtag <- Hashtag.get_by_name(id) do
|
||||||
|
render(conn, "show.json", tag: hashtag, for_user: conn.assigns.user)
|
||||||
|
else
|
||||||
|
_ -> conn |> render_error(:not_found, "Hashtag not found")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def follow(conn, %{id: id}) do
|
||||||
|
with %Hashtag{} = hashtag <- Hashtag.get_by_name(id),
|
||||||
|
%User{} = user <- conn.assigns.user,
|
||||||
|
{:ok, _} <-
|
||||||
|
User.follow_hashtag(user, hashtag) do
|
||||||
|
render(conn, "show.json", tag: hashtag, for_user: user)
|
||||||
|
else
|
||||||
|
_ -> render_error(conn, :not_found, "Hashtag not found")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def unfollow(conn, %{id: id}) do
|
||||||
|
with %Hashtag{} = hashtag <- Hashtag.get_by_name(id),
|
||||||
|
%User{} = user <- conn.assigns.user,
|
||||||
|
{:ok, _} <-
|
||||||
|
User.unfollow_hashtag(user, hashtag) do
|
||||||
|
render(conn, "show.json", tag: hashtag, for_user: user)
|
||||||
|
else
|
||||||
|
_ -> render_error(conn, :not_found, "Hashtag not found")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -41,6 +41,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|
||||||
|
|
||||||
# GET /api/v1/timelines/home
|
# GET /api/v1/timelines/home
|
||||||
def home(%{assigns: %{user: user}} = conn, params) do
|
def home(%{assigns: %{user: user}} = conn, params) do
|
||||||
|
followed_hashtags =
|
||||||
|
user
|
||||||
|
|> User.followed_hashtags()
|
||||||
|
|> Enum.map(& &1.id)
|
||||||
|
|
||||||
params =
|
params =
|
||||||
params
|
params
|
||||||
|> Map.put(:type, ["Create", "Announce"])
|
|> Map.put(:type, ["Create", "Announce"])
|
||||||
|
@ -50,6 +55,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|
||||||
|> Map.put(:announce_filtering_user, user)
|
|> Map.put(:announce_filtering_user, user)
|
||||||
|> Map.put(:user, user)
|
|> Map.put(:user, user)
|
||||||
|> Map.put(:local_only, params[:local])
|
|> Map.put(:local_only, params[:local])
|
||||||
|
|> Map.put(:followed_hashtags, followed_hashtags)
|
||||||
|> Map.delete(:local)
|
|> Map.delete(:local)
|
||||||
|
|
||||||
activities =
|
activities =
|
||||||
|
|
21
lib/pleroma/web/mastodon_api/views/tag_view.ex
Normal file
21
lib/pleroma/web/mastodon_api/views/tag_view.ex
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
defmodule Pleroma.Web.MastodonAPI.TagView do
|
||||||
|
use Pleroma.Web, :view
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.Router.Helpers
|
||||||
|
|
||||||
|
def render("show.json", %{tag: tag, for_user: user}) do
|
||||||
|
following =
|
||||||
|
with %User{} <- user do
|
||||||
|
User.following_hashtag?(user, tag)
|
||||||
|
else
|
||||||
|
_ -> false
|
||||||
|
end
|
||||||
|
|
||||||
|
%{
|
||||||
|
name: tag.name,
|
||||||
|
url: Helpers.tag_feed_url(Pleroma.Web.Endpoint, :feed, tag.name),
|
||||||
|
history: [],
|
||||||
|
following: following
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
|
@ -598,6 +598,10 @@ defmodule Pleroma.Web.Router do
|
||||||
|
|
||||||
get("/announcements", AnnouncementController, :index)
|
get("/announcements", AnnouncementController, :index)
|
||||||
post("/announcements/:id/dismiss", AnnouncementController, :mark_read)
|
post("/announcements/:id/dismiss", AnnouncementController, :mark_read)
|
||||||
|
|
||||||
|
get("/tags/:id", TagController, :show)
|
||||||
|
post("/tags/:id/follow", TagController, :follow)
|
||||||
|
post("/tags/:id/unfollow", TagController, :unfollow)
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/api/web", Pleroma.Web do
|
scope "/api/web", Pleroma.Web do
|
||||||
|
|
|
@ -17,6 +17,7 @@ defmodule Pleroma.Web.Streamer do
|
||||||
alias Pleroma.Web.OAuth.Token
|
alias Pleroma.Web.OAuth.Token
|
||||||
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||||
alias Pleroma.Web.StreamerView
|
alias Pleroma.Web.StreamerView
|
||||||
|
require Pleroma.Constants
|
||||||
|
|
||||||
@mix_env Mix.env()
|
@mix_env Mix.env()
|
||||||
@registry Pleroma.Web.StreamerRegistry
|
@registry Pleroma.Web.StreamerRegistry
|
||||||
|
@ -252,7 +253,17 @@ defmodule Pleroma.Web.Streamer do
|
||||||
User.get_recipients_from_activity(item)
|
User.get_recipients_from_activity(item)
|
||||||
|> Enum.map(fn %{id: id} -> "user:#{id}" end)
|
|> Enum.map(fn %{id: id} -> "user:#{id}" end)
|
||||||
|
|
||||||
Enum.each(recipient_topics, fn topic ->
|
hashtag_recipients =
|
||||||
|
if Pleroma.Constants.as_public() in item.recipients do
|
||||||
|
Pleroma.Hashtag.get_recipients_for_activity(item)
|
||||||
|
|> Enum.map(fn id -> "user:#{id}" end)
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
all_recipients = Enum.uniq(recipient_topics ++ hashtag_recipients)
|
||||||
|
|
||||||
|
Enum.each(all_recipients, fn topic ->
|
||||||
push_to_socket(topic, item)
|
push_to_socket(topic, item)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
defmodule Pleroma.Repo.Migrations.AddUserFollowsHashtag do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table(:user_follows_hashtag) do
|
||||||
|
add(:hashtag_id, references(:hashtags))
|
||||||
|
add(:user_id, references(:users, type: :uuid, on_delete: :delete_all))
|
||||||
|
end
|
||||||
|
|
||||||
|
create(unique_index(:user_follows_hashtag, [:user_id, :hashtag_id]))
|
||||||
|
end
|
||||||
|
end
|
|
@ -2679,4 +2679,74 @@ defmodule Pleroma.UserTest do
|
||||||
assert user.ap_id in user3_updated.also_known_as
|
assert user.ap_id in user3_updated.also_known_as
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "follow_hashtag/2" do
|
||||||
|
test "should follow a hashtag" do
|
||||||
|
user = insert(:user)
|
||||||
|
hashtag = insert(:hashtag)
|
||||||
|
|
||||||
|
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||||
|
|
||||||
|
user = User.get_cached_by_ap_id(user.ap_id)
|
||||||
|
|
||||||
|
assert user.followed_hashtags |> Enum.count() == 1
|
||||||
|
assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not follow a hashtag twice" do
|
||||||
|
user = insert(:user)
|
||||||
|
hashtag = insert(:hashtag)
|
||||||
|
|
||||||
|
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||||
|
|
||||||
|
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||||
|
|
||||||
|
user = User.get_cached_by_ap_id(user.ap_id)
|
||||||
|
|
||||||
|
assert user.followed_hashtags |> Enum.count() == 1
|
||||||
|
assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can follow multiple hashtags" do
|
||||||
|
user = insert(:user)
|
||||||
|
hashtag = insert(:hashtag)
|
||||||
|
other_hashtag = insert(:hashtag)
|
||||||
|
|
||||||
|
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||||
|
assert {:ok, _} = user |> User.follow_hashtag(other_hashtag)
|
||||||
|
|
||||||
|
user = User.get_cached_by_ap_id(user.ap_id)
|
||||||
|
|
||||||
|
assert user.followed_hashtags |> Enum.count() == 2
|
||||||
|
assert hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
|
||||||
|
assert other_hashtag.name in Enum.map(user.followed_hashtags, fn %{name: name} -> name end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "unfollow_hashtag/2" do
|
||||||
|
test "should unfollow a hashtag" do
|
||||||
|
user = insert(:user)
|
||||||
|
hashtag = insert(:hashtag)
|
||||||
|
|
||||||
|
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||||
|
assert {:ok, _} = user |> User.unfollow_hashtag(hashtag)
|
||||||
|
|
||||||
|
user = User.get_cached_by_ap_id(user.ap_id)
|
||||||
|
|
||||||
|
assert user.followed_hashtags |> Enum.count() == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not error when trying to unfollow a hashtag twice" do
|
||||||
|
user = insert(:user)
|
||||||
|
hashtag = insert(:hashtag)
|
||||||
|
|
||||||
|
assert {:ok, _} = user |> User.follow_hashtag(hashtag)
|
||||||
|
assert {:ok, _} = user |> User.unfollow_hashtag(hashtag)
|
||||||
|
assert {:ok, _} = user |> User.unfollow_hashtag(hashtag)
|
||||||
|
|
||||||
|
user = User.get_cached_by_ap_id(user.ap_id)
|
||||||
|
|
||||||
|
assert user.followed_hashtags |> Enum.count() == 0
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -719,6 +719,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "fetch activities for followed hashtags" do
|
||||||
|
test "it should return public activities that reference a given hashtag" do
|
||||||
|
hashtag = insert(:hashtag, name: "tenshi")
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, public} = CommonAPI.post(user, %{status: "maji #tenshi", visibility: "public"})
|
||||||
|
{:ok, _unrelated} = CommonAPI.post(user, %{status: "dai #tensh", visibility: "public"})
|
||||||
|
{:ok, unlisted} = CommonAPI.post(user, %{status: "maji #tenshi", visibility: "unlisted"})
|
||||||
|
{:ok, _private} = CommonAPI.post(user, %{status: "maji #tenshi", visibility: "private"})
|
||||||
|
|
||||||
|
activities = ActivityPub.fetch_activities([], %{followed_hashtags: [hashtag.id]})
|
||||||
|
assert length(activities) == 2
|
||||||
|
public_id = public.id
|
||||||
|
unlisted_id = unlisted.id
|
||||||
|
assert [%{id: ^public_id}, %{id: ^unlisted_id}] = activities
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "fetch activities in context" do
|
describe "fetch activities in context" do
|
||||||
test "retrieves activities that have a given context" do
|
test "retrieves activities that have a given context" do
|
||||||
{:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
|
{:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
defmodule Pleroma.Web.MastodonAPI.TagControllerTest do
|
||||||
|
use Pleroma.Web.ConnCase
|
||||||
|
|
||||||
|
import Pleroma.Factory
|
||||||
|
import Tesla.Mock
|
||||||
|
|
||||||
|
alias Pleroma.User
|
||||||
|
|
||||||
|
setup do
|
||||||
|
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "GET /api/v1/tags/:id" do
|
||||||
|
test "returns 200 with tag" do
|
||||||
|
%{user: user, conn: conn} = oauth_access(["read"])
|
||||||
|
|
||||||
|
tag = insert(:hashtag, name: "jubjub")
|
||||||
|
{:ok, _user} = User.follow_hashtag(user, tag)
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/tags/jubjub")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert %{
|
||||||
|
"name" => "jubjub",
|
||||||
|
"url" => "http://localhost:4001/tags/jubjub",
|
||||||
|
"history" => [],
|
||||||
|
"following" => true
|
||||||
|
} = response
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns 404 with unknown tag" do
|
||||||
|
%{conn: conn} = oauth_access(["read"])
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/tags/jubjub")
|
||||||
|
|> json_response_and_validate_schema(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "POST /api/v1/tags/:id/follow" do
|
||||||
|
test "should follow a hashtag" do
|
||||||
|
%{user: user, conn: conn} = oauth_access(["write:follows"])
|
||||||
|
hashtag = insert(:hashtag, name: "jubjub")
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> post("/api/v1/tags/jubjub/follow")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert response["following"] == true
|
||||||
|
user = User.get_cached_by_ap_id(user.ap_id)
|
||||||
|
assert User.following_hashtag?(user, hashtag)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should 404 if hashtag doesn't exist" do
|
||||||
|
%{conn: conn} = oauth_access(["write:follows"])
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> post("/api/v1/tags/rubrub/follow")
|
||||||
|
|> json_response_and_validate_schema(404)
|
||||||
|
|
||||||
|
assert response["error"] == "Hashtag not found"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "POST /api/v1/tags/:id/unfollow" do
|
||||||
|
test "should unfollow a hashtag" do
|
||||||
|
%{user: user, conn: conn} = oauth_access(["write:follows"])
|
||||||
|
hashtag = insert(:hashtag, name: "jubjub")
|
||||||
|
{:ok, user} = User.follow_hashtag(user, hashtag)
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> post("/api/v1/tags/jubjub/unfollow")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert response["following"] == false
|
||||||
|
user = User.get_cached_by_ap_id(user.ap_id)
|
||||||
|
refute User.following_hashtag?(user, hashtag)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should 404 if hashtag doesn't exist" do
|
||||||
|
%{conn: conn} = oauth_access(["write:follows"])
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> post("/api/v1/tags/rubrub/unfollow")
|
||||||
|
|> json_response_and_validate_schema(404)
|
||||||
|
|
||||||
|
assert response["error"] == "Hashtag not found"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -410,6 +410,36 @@ defmodule Pleroma.Web.StreamerTest do
|
||||||
assert_receive {:render_with_user, _, "status_update.json", ^create, ^stream}
|
assert_receive {:render_with_user, _, "status_update.json", ^create, ^stream}
|
||||||
refute Streamer.filtered_by_user?(user, edited)
|
refute Streamer.filtered_by_user?(user, edited)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "it streams posts containing followed hashtags on the 'user' stream", %{
|
||||||
|
user: user,
|
||||||
|
token: oauth_token
|
||||||
|
} do
|
||||||
|
hashtag = insert(:hashtag, %{name: "tenshi"})
|
||||||
|
other_user = insert(:user)
|
||||||
|
{:ok, user} = User.follow_hashtag(user, hashtag)
|
||||||
|
|
||||||
|
Streamer.get_topic_and_add_socket("user", user, oauth_token)
|
||||||
|
{:ok, activity} = CommonAPI.post(other_user, %{status: "hey #tenshi"})
|
||||||
|
|
||||||
|
assert_receive {:render_with_user, _, "update.json", ^activity, _}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not stream private posts containing followed hashtags on the 'user' stream", %{
|
||||||
|
user: user,
|
||||||
|
token: oauth_token
|
||||||
|
} do
|
||||||
|
hashtag = insert(:hashtag, %{name: "tenshi"})
|
||||||
|
other_user = insert(:user)
|
||||||
|
{:ok, user} = User.follow_hashtag(user, hashtag)
|
||||||
|
|
||||||
|
Streamer.get_topic_and_add_socket("user", user, oauth_token)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(other_user, %{status: "hey #tenshi", visibility: "private"})
|
||||||
|
|
||||||
|
refute_receive {:render_with_user, _, "update.json", ^activity, _}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "public streams" do
|
describe "public streams" do
|
||||||
|
|
|
@ -716,4 +716,11 @@ defmodule Pleroma.Factory do
|
||||||
user: user
|
user: user
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def hashtag_factory(params \\ %{}) do
|
||||||
|
%Pleroma.Hashtag{
|
||||||
|
name: "test #{sequence(:hashtag_name, & &1)}"
|
||||||
|
}
|
||||||
|
|> Map.merge(params)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue