end
end
+ @unpersisted_activity_types ~w[Undo Delete Remove Accept Reject]
+ @impl true
+ def persist(%{"type" => type} = object, [local: false] = meta)
+ when type in @unpersisted_activity_types do
+ {:ok, object, meta}
+ {recipients, _, _} = get_recipients(object)
+
+ unpersisted = %Activity{
+ data: object,
+ local: false,
+ recipients: recipients,
+ actor: object["actor"]
+ }
+
+ {:ok, unpersisted, meta}
+ end
+
@impl true
def persist(object, meta) do
with local <- Keyword.fetch!(meta, :local),
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
- IO.inspect(Repo.to_sql(:all, query))
opts = Map.put(opts, :skip_extra_order, true)
Pagination.fetch_paginated(query, opts, pagination)
|> fetch_activities(params, :offset)
end
- defp user_activities_recipients(%{godmode: true}), do: []
+ def user_activities_recipients(%{godmode: true}), do: []
- defp user_activities_recipients(%{reading_user: reading_user}) do
+ def user_activities_recipients(%{reading_user: reading_user}) do
if not is_nil(reading_user) and reading_user.local do
[
Constants.as_public(),
)
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
end
end
+ defp exclude_invisible_actors(query, %{type: "Flag"}), do: query
defp exclude_invisible_actors(query, %{invisible_actors: true}), do: query
defp exclude_invisible_actors(query, _opts) do
- invisible_ap_ids =
- User.Query.build(%{invisible: true, select: [:ap_id]})
- |> Repo.all()
- |> Enum.map(fn %{ap_id: ap_id} -> ap_id end)
-
- from([activity] in query, where: activity.actor not in ^invisible_ap_ids)
+ query
+ |> join(:inner, [activity], u in User,
+ as: :u,
+ on: activity.actor == u.ap_id and u.invisible == false
+ )
end
defp exclude_id(query, %{exclude_id: id}) when is_binary(id) 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)
|> restrict_instance(opts)
|> restrict_announce_object_actor(opts)
|> restrict_filtered(opts)
- |> Activity.restrict_deactivated_users()
+ |> maybe_restrict_deactivated_users(opts)
|> exclude_poll_votes(opts)
|> exclude_invisible_actors(opts)
|> exclude_visibility(opts)
@spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()}
def upload(file, opts \\ []) do
- with {:ok, data} <- Upload.store(file, opts) do
+ with {:ok, data} <- Upload.store(sanitize_upload_file(file), opts) do
obj_data = Maps.put_if_present(data, "actor", opts[:actor])
Repo.insert(%Object{data: obj_data})
end
end
+ defp sanitize_upload_file(%Plug.Upload{filename: filename} = upload) when is_binary(filename) do
+ %Plug.Upload{
+ upload
+ | filename: Path.basename(filename)
+ }
+ end
+
+ defp sanitize_upload_file(upload), do: upload
+
@spec get_actor_url(any()) :: binary() | nil
defp get_actor_url(url) when is_binary(url), do: url
defp get_actor_url(%{"href" => href}) when is_binary(href), do: href
defp normalize_image(urls) when is_list(urls), do: urls |> List.first() |> normalize_image()
defp normalize_image(_), do: nil
+ defp normalize_also_known_as(aka) when is_list(aka), do: aka
+ defp normalize_also_known_as(aka) when is_binary(aka), do: [aka]
+ defp normalize_also_known_as(nil), do: []
+
defp object_to_user_data(data, additional) do
fields =
data
# we request WebFinger here
nickname = additional[:nickname_from_acct] || generate_nickname(data)
+ # also_known_as must be a URL
+ also_known_as =
+ data
+ |> Map.get("alsoKnownAs", [])
+ |> normalize_also_known_as()
+ |> Enum.filter(fn url ->
+ case URI.parse(url) do
+ %URI{scheme: "http"} -> true
+ %URI{scheme: "https"} -> true
+ _ -> false
+ end
+ end)
+
%{
ap_id: data["id"],
uri: get_actor_url(data["url"]),
featured_address: featured_address,
bio: data["summary"] || "",
actor_type: actor_type,
- also_known_as: Map.get(data, "alsoKnownAs", []),
+ also_known_as: also_known_as,
public_key: public_key,
inbox: data["inbox"],
shared_inbox: shared_inbox,
{:ok, maybe_update_follow_information(data)}
else
# If this has been deleted, only log a debug and not an error
- {:error, "Object has been deleted" = e} ->
+ {:error, {"Object has been deleted", _, _} = e} ->
Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
{:error, e}
{:error, {:reject, reason} = e} ->
- Logger.info("Rejected user #{ap_id}: #{inspect(reason)}")
+ Logger.debug("Rejected user #{ap_id}: #{inspect(reason)}")
{:error, e}
{:error, e} ->
|> restrict_visibility(%{visibility: "direct"})
|> order_by([activity], asc: activity.id)
end
+
+ defp maybe_restrict_deactivated_users(activity, %{type: "Flag"}), do: activity
+
+ defp maybe_restrict_deactivated_users(activity, _opts),
+ do: Activity.restrict_deactivated_users(activity)
end