alias Pleroma.Conversation
alias Pleroma.Conversation.Participation
alias Pleroma.Filter
+ alias Pleroma.Hashtag
alias Pleroma.Maps
alias Pleroma.Notification
alias Pleroma.Object
:ok <-
maybe_federate(stripped_activity) do
User.all_superusers()
+ |> Enum.filter(fn user -> user.ap_id != actor end)
|> Enum.filter(fn user -> not is_nil(user.email) end)
|> Enum.each(fn superuser ->
superuser
|> Repo.one()
end
+ defp fetch_paginated_optimized(query, opts, pagination) do
+ # Note: tag-filtering funcs may apply "ORDER BY objects.id DESC",
+ # and extra sorting on "activities.id DESC NULLS LAST" would worse the query plan
+ opts = Map.put(opts, :skip_extra_order, true)
+
+ Pagination.fetch_paginated(query, opts, pagination)
+ end
+
+ def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
+ list_memberships = Pleroma.List.memberships(opts[:user])
+
+ fetch_activities_query(recipients ++ list_memberships, opts)
+ |> fetch_paginated_optimized(opts, pagination)
+ |> Enum.reverse()
+ |> maybe_update_cc(list_memberships, opts[:user])
+ end
+
@spec fetch_public_or_unlisted_activities(map(), Pagination.type()) :: [Activity.t()]
def fetch_public_or_unlisted_activities(opts \\ %{}, pagination \\ :keyset) do
opts = Map.delete(opts, :user)
[Constants.as_public()]
|> fetch_activities_query(opts)
|> restrict_unlisted(opts)
- |> Pagination.fetch_paginated(opts, pagination)
+ |> fetch_paginated_optimized(opts, pagination)
end
@spec fetch_public_activities(map(), Pagination.type()) :: [Activity.t()]
|> Enum.reverse()
end
- def fetch_user_activities(user, reading_user, params \\ %{}) do
+ def fetch_user_activities(user, reading_user, params \\ %{})
+
+ def fetch_user_activities(user, reading_user, %{total: true} = params) do
+ result = fetch_activities_for_user(user, reading_user, params)
+
+ Keyword.put(result, :items, Enum.reverse(result[:items]))
+ end
+
+ def fetch_user_activities(user, reading_user, params) do
+ user
+ |> fetch_activities_for_user(reading_user, params)
+ |> Enum.reverse()
+ end
+
+ defp fetch_activities_for_user(user, reading_user, params) do
params =
params
|> Map.put(:type, ["Create", "Announce"])
}
|> user_activities_recipients()
|> fetch_activities(params, pagination_type)
- |> Enum.reverse()
+ end
+
+ def fetch_statuses(reading_user, %{total: true} = params) do
+ result = fetch_activities_for_reading_user(reading_user, params)
+ Keyword.put(result, :items, Enum.reverse(result[:items]))
end
def fetch_statuses(reading_user, params) do
+ reading_user
+ |> fetch_activities_for_reading_user(params)
+ |> Enum.reverse()
+ end
+
+ defp fetch_activities_for_reading_user(reading_user, params) do
params = Map.put(params, :type, ["Create", "Announce"])
%{
}
|> user_activities_recipients()
|> fetch_activities(params, :offset)
- |> Enum.reverse()
end
defp user_activities_recipients(%{godmode: true}), do: []
raise_on_missing_preload()
end
- defp restrict_embedded_tag_all(query, %{tag_all: tag_all}) when is_list(tag_all) do
+ defp restrict_embedded_tag_all(query, %{tag_all: [_ | _] = tag_all}) do
from(
[_activity, object] in query,
where: fragment("(?)->'tag' \\?& (?)", object.data, ^tag_all)
raise_on_missing_preload()
end
- defp restrict_embedded_tag_any(query, %{tag: tag}) when is_list(tag) do
+ defp restrict_embedded_tag_any(query, %{tag: [_ | _] = tag_any}) do
from(
[_activity, object] in query,
- where: fragment("(?)->'tag' \\?| (?)", object.data, ^tag)
+ where: fragment("(?)->'tag' \\?| (?)", object.data, ^tag_any)
)
end
raise_on_missing_preload()
end
- defp restrict_embedded_tag_reject_any(query, %{tag_reject: tag_reject})
- when is_list(tag_reject) do
+ defp restrict_embedded_tag_reject_any(query, %{tag_reject: [_ | _] = tag_reject}) do
from(
[_activity, object] in query,
where: fragment("not (?)->'tag' \\?| (?)", object.data, ^tag_reject)
defp restrict_embedded_tag_reject_any(query, _), do: query
+ defp object_ids_query_for_tags(tags) do
+ from(hto in "hashtags_objects")
+ |> join(:inner, [hto], ht in Pleroma.Hashtag, on: hto.hashtag_id == ht.id)
+ |> where([hto, ht], ht.name in ^tags)
+ |> select([hto], hto.object_id)
+ |> distinct([hto], true)
+ end
+
defp restrict_hashtag_all(_query, %{tag_all: _tag, skip_preload: true}) do
raise_on_missing_preload()
end
- defp restrict_hashtag_all(query, %{tag_all: tags}) when is_list(tags) do
+ defp restrict_hashtag_all(query, %{tag_all: [single_tag]}) do
+ restrict_hashtag_any(query, %{tag: single_tag})
+ end
+
+ defp restrict_hashtag_all(query, %{tag_all: [_ | _] = tags}) do
from(
[_activity, object] in query,
where:
end
defp restrict_hashtag_all(query, %{tag_all: tag}) when is_binary(tag) do
- restrict_hashtag_any(query, %{tag: tag})
+ restrict_hashtag_all(query, %{tag_all: [tag]})
end
defp restrict_hashtag_all(query, _), do: query
raise_on_missing_preload()
end
- defp restrict_hashtag_any(query, %{tag: tags}) when is_list(tags) do
+ defp restrict_hashtag_any(query, %{tag: [_ | _] = tags}) do
+ hashtag_ids =
+ from(ht in Hashtag, where: ht.name in ^tags, select: ht.id)
+ |> Repo.all()
+
+ # Note: NO extra ordering should be done on "activities.id desc nulls last" for optimal plan
from(
[_activity, object] in query,
- where:
- fragment(
- """
- EXISTS (SELECT 1 FROM hashtags JOIN hashtags_objects
- ON hashtags_objects.hashtag_id = hashtags.id WHERE hashtags.name = ANY(?)
- AND hashtags_objects.object_id = ? LIMIT 1)
- """,
- ^tags,
- object.id
- )
+ join: hto in "hashtags_objects",
+ on: hto.object_id == object.id,
+ where: hto.hashtag_id in ^hashtag_ids,
+ distinct: [desc: object.id],
+ order_by: [desc: object.id]
)
end
raise_on_missing_preload()
end
- defp restrict_hashtag_reject_any(query, %{tag_reject: tags_reject}) when is_list(tags_reject) do
+ defp restrict_hashtag_reject_any(query, %{tag_reject: [_ | _] = tags_reject}) do
from(
[_activity, object] in query,
- where:
- fragment(
- """
- NOT EXISTS (SELECT 1 FROM hashtags JOIN hashtags_objects
- ON hashtags_objects.hashtag_id = hashtags.id WHERE hashtags.name = ANY(?)
- AND hashtags_objects.object_id = ? LIMIT 1)
- """,
- ^tags_reject,
- object.id
- )
+ where: object.id not in subquery(object_ids_query_for_tags(tags_reject))
)
end
defp restrict_local(query, _), do: query
+ defp restrict_remote(query, %{remote: true}) do
+ from(activity in query, where: activity.local == false)
+ end
+
+ defp restrict_remote(query, _), do: query
+
defp restrict_actor(query, %{actor_id: actor_id}) do
from(activity in query, where: activity.actor == ^actor_id)
end
defp maybe_order(query, _), do: query
+ defp normalize_fetch_activities_query_opts(opts) do
+ Enum.reduce([:tag, :tag_all, :tag_reject], opts, fn key, opts ->
+ case opts[key] do
+ value when is_bitstring(value) ->
+ Map.put(opts, key, Hashtag.normalize_name(value))
+
+ value when is_list(value) ->
+ normalized_value =
+ value
+ |> Enum.map(&Hashtag.normalize_name/1)
+ |> Enum.uniq()
+
+ Map.put(opts, key, normalized_value)
+
+ _ ->
+ opts
+ end
+ end)
+ end
+
defp fetch_activities_query_ap_ids_ops(opts) do
source_user = opts[:muting_user]
ap_id_relationships = if source_user, do: [:mute, :reblog_mute], else: []
end
def fetch_activities_query(recipients, opts \\ %{}) do
+ opts = normalize_fetch_activities_query_opts(opts)
+
{restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts} =
fetch_activities_query_ap_ids_ops(opts)
|> restrict_replies(opts)
|> restrict_since(opts)
|> restrict_local(opts)
+ |> restrict_remote(opts)
|> restrict_actor(opts)
|> restrict_type(opts)
|> restrict_state(opts)
|> exclude_invisible_actors(opts)
|> exclude_visibility(opts)
- if Config.improved_hashtag_timeline() do
+ if Config.feature_enabled?(:improved_hashtag_timeline) do
query
|> restrict_hashtag_any(opts)
|> restrict_hashtag_all(opts)
end
end
- def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
- list_memberships = Pleroma.List.memberships(opts[:user])
-
- fetch_activities_query(recipients ++ list_memberships, opts)
- |> Pagination.fetch_paginated(opts, pagination)
- |> Enum.reverse()
- |> maybe_update_cc(list_memberships, opts[:user])
- end
-
@doc """
Fetch favorites activities of user with order by sort adds to favorites
"""
defp get_actor_url(_url), do: nil
- defp object_to_user_data(data) do
- avatar =
- data["icon"]["url"] &&
- %{
- "type" => "Image",
- "url" => [%{"href" => data["icon"]["url"]}]
- }
+ defp normalize_image(%{"url" => url}) do
+ %{
+ "type" => "Image",
+ "url" => [%{"href" => url}]
+ }
+ end
- banner =
- data["image"]["url"] &&
- %{
- "type" => "Image",
- "url" => [%{"href" => data["image"]["url"]}]
- }
+ defp normalize_image(urls) when is_list(urls), do: urls |> List.first() |> normalize_image()
+ defp normalize_image(_), do: nil
+ defp object_to_user_data(data) do
fields =
data
|> Map.get("attachment", [])
ap_id: data["id"],
uri: get_actor_url(data["url"]),
ap_enabled: true,
- banner: banner,
+ banner: normalize_image(data["image"]),
fields: fields,
emoji: emojis,
is_locked: is_locked,
is_discoverable: is_discoverable,
invisible: invisible,
- avatar: avatar,
+ avatar: normalize_image(data["icon"]),
name: data["name"],
follower_address: data["followers"],
following_address: data["following"],