Remerge of hashtag following (#341)

this time with less idiot

Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/341
Signed-off-by: mkljczk <git@mkljczk.pl>
This commit is contained in:
floatingghost 2022-12-05 12:58:48 +00:00 committed by mkljczk
commit c94c6eac22
17 changed files with 564 additions and 3 deletions

View file

@ -924,6 +924,31 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
)
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(:left, [activity, object], hto in "hashtags_objects",
on: hto.object_id == object.id,
as: :hto
)
|> where(
[activity, object, hto: hto],
(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
from(activity in query, where: activity.local == true)
end
@ -1414,7 +1439,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> maybe_preload_report_notes(opts)
|> maybe_set_thread_muted_field(opts)
|> maybe_order(opts)
|> restrict_recipients(recipients, opts[:user])
|> restrict_recipients_or_hashtags(recipients, opts[:user], opts[:followed_hashtags])
|> restrict_replies(opts)
|> restrict_since(opts)
|> restrict_local(opts)

View 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

View file

@ -17,11 +17,16 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Tag do
type: :string,
format: :uri,
description: "A link to the hashtag on the instance"
},
following: %Schema{
type: :boolean,
description: "Whether the authenticated user is following the hashtag"
}
},
example: %{
name: "cofe",
url: "https://lain.com/tag/cofe"
url: "https://lain.com/tag/cofe",
following: false
}
})
end

View 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

View file

@ -40,6 +40,11 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
# GET /api/v1/timelines/home
def home(%{assigns: %{user: user}} = conn, params) do
followed_hashtags =
user
|> User.followed_hashtags()
|> Enum.map(& &1.id)
params =
params
|> Map.put(:type, ["Create", "Announce"])
@ -49,6 +54,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> Map.put(:announce_filtering_user, user)
|> Map.put(:user, user)
|> Map.put(:local_only, params[:local])
|> Map.put(:followed_hashtags, followed_hashtags)
|> Map.delete(:local)
activities =

View 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

View file

@ -755,6 +755,10 @@ defmodule Pleroma.Web.Router do
get("/announcements", AnnouncementController, :index)
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
scope "/api/v1", Pleroma.Web.MastodonAPI do

View file

@ -19,6 +19,7 @@ defmodule Pleroma.Web.Streamer do
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.StreamerView
require Pleroma.Constants
@registry Pleroma.Web.StreamerRegistry
@ -305,7 +306,17 @@ defmodule Pleroma.Web.Streamer do
User.get_recipients_from_activity(item)
|> 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)
end)
end