Page MenuHomePhorge

No OneTemporary

Size
148 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/pleroma/activity/queries.ex b/lib/pleroma/activity/queries.ex
new file mode 100644
index 000000000..aa5b29566
--- /dev/null
+++ b/lib/pleroma/activity/queries.ex
@@ -0,0 +1,49 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Activity.Queries do
+ @moduledoc """
+ Contains queries for Activity.
+ """
+
+ import Ecto.Query, only: [from: 2]
+
+ @type query :: Ecto.Queryable.t() | Activity.t()
+
+ alias Pleroma.Activity
+
+ @spec by_actor(query, String.t()) :: query
+ def by_actor(query \\ Activity, actor) do
+ from(
+ activity in query,
+ where: fragment("(?)->>'actor' = ?", activity.data, ^actor)
+ )
+ end
+
+ @spec by_object_id(query, String.t()) :: query
+ def by_object_id(query \\ Activity, object_id) do
+ from(activity in query,
+ where:
+ fragment(
+ "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
+ activity.data,
+ activity.data,
+ ^object_id
+ )
+ )
+ end
+
+ @spec by_type(query, String.t()) :: query
+ def by_type(query \\ Activity, activity_type) do
+ from(
+ activity in query,
+ where: fragment("(?)->>'type' = ?", activity.data, ^activity_type)
+ )
+ end
+
+ @spec limit(query, pos_integer()) :: query
+ def limit(query \\ Activity, limit) do
+ from(activity in query, limit: ^limit)
+ end
+end
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index c8d339c19..d58eb7f7d 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -1,233 +1,231 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Object do
use Ecto.Schema
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.ObjectTombstone
alias Pleroma.Repo
alias Pleroma.User
import Ecto.Query
import Ecto.Changeset
require Logger
schema "objects" do
field(:data, :map)
timestamps()
end
def create(data) do
Object.change(%Object{}, %{data: data})
|> Repo.insert()
end
def change(struct, params \\ %{}) do
struct
|> cast(params, [:data])
|> validate_required([:data])
|> unique_constraint(:ap_id, name: :objects_unique_apid_index)
end
def get_by_id(nil), do: nil
def get_by_id(id), do: Repo.get(Object, id)
def get_by_ap_id(nil), do: nil
def get_by_ap_id(ap_id) do
Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
end
defp warn_on_no_object_preloaded(ap_id) do
"Object.normalize() called without preloaded object (#{ap_id}). Consider preloading the object"
|> Logger.debug()
Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
end
def normalize(_, fetch_remote \\ true, options \\ [])
# If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
# Use this whenever possible, especially when walking graphs in an O(N) loop!
def normalize(%Object{} = object, _, _), do: object
def normalize(%Activity{object: %Object{} = object}, _, _), do: object
# A hack for fake activities
def normalize(%Activity{data: %{"object" => %{"fake" => true} = data}}, _, _) do
%Object{id: "pleroma:fake_object_id", data: data}
end
# No preloaded object
def normalize(%Activity{data: %{"object" => %{"id" => ap_id}}}, fetch_remote, _) do
warn_on_no_object_preloaded(ap_id)
normalize(ap_id, fetch_remote)
end
# No preloaded object
def normalize(%Activity{data: %{"object" => ap_id}}, fetch_remote, _) do
warn_on_no_object_preloaded(ap_id)
normalize(ap_id, fetch_remote)
end
# Old way, try fetching the object through cache.
def normalize(%{"id" => ap_id}, fetch_remote, _), do: normalize(ap_id, fetch_remote)
def normalize(ap_id, false, _) when is_binary(ap_id), do: get_cached_by_ap_id(ap_id)
def normalize(ap_id, true, options) when is_binary(ap_id) do
Fetcher.fetch_object_from_id!(ap_id, options)
end
def normalize(_, _, _), do: nil
# Owned objects can only be mutated by their owner
def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}),
do: actor == ap_id
# Legacy objects can be mutated by anybody
def authorize_mutation(%Object{}, %User{}), do: true
def get_cached_by_ap_id(ap_id) do
key = "object:#{ap_id}"
Cachex.fetch!(:object_cache, key, fn _ ->
object = get_by_ap_id(ap_id)
if object do
{:commit, object}
else
{:ignore, object}
end
end)
end
def context_mapping(context) do
Object.change(%Object{}, %{data: %{"id" => context}})
end
def make_tombstone(%Object{data: %{"id" => id, "type" => type}}, deleted \\ DateTime.utc_now()) do
%ObjectTombstone{
id: id,
formerType: type,
deleted: deleted
}
|> Map.from_struct()
end
def swap_object_with_tombstone(object) do
tombstone = make_tombstone(object)
object
|> Object.change(%{data: tombstone})
|> Repo.update()
end
def delete(%Object{data: %{"id" => id}} = object) do
with {:ok, _obj} = swap_object_with_tombstone(object),
deleted_activity = Activity.delete_by_ap_id(id),
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
{:ok, object, deleted_activity}
end
end
def prune(%Object{data: %{"id" => id}} = object) do
with {:ok, object} <- Repo.delete(object),
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
{:ok, object}
end
end
def set_cache(%Object{data: %{"id" => ap_id}} = object) do
Cachex.put(:object_cache, "object:#{ap_id}", object)
{:ok, object}
end
def update_and_set_cache(changeset) do
with {:ok, object} <- Repo.update(changeset) do
set_cache(object)
- else
- e -> e
end
end
def increase_replies_count(ap_id) do
Object
|> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
|> update([o],
set: [
data:
fragment(
"""
jsonb_set(?, '{repliesCount}',
(coalesce((?->>'repliesCount')::int, 0) + 1)::varchar::jsonb, true)
""",
o.data,
o.data
)
]
)
|> Repo.update_all([])
|> case do
{1, [object]} -> set_cache(object)
_ -> {:error, "Not found"}
end
end
def decrease_replies_count(ap_id) do
Object
|> where([o], fragment("?->>'id' = ?::text", o.data, ^to_string(ap_id)))
|> update([o],
set: [
data:
fragment(
"""
jsonb_set(?, '{repliesCount}',
(greatest(0, (?->>'repliesCount')::int - 1))::varchar::jsonb, true)
""",
o.data,
o.data
)
]
)
|> Repo.update_all([])
|> case do
{1, [object]} -> set_cache(object)
_ -> {:error, "Not found"}
end
end
def increase_vote_count(ap_id, name) do
with %Object{} = object <- Object.normalize(ap_id),
"Question" <- object.data["type"] do
multiple = Map.has_key?(object.data, "anyOf")
options =
(object.data["anyOf"] || object.data["oneOf"] || [])
|> Enum.map(fn
%{"name" => ^name} = option ->
Kernel.update_in(option["replies"]["totalItems"], &(&1 + 1))
option ->
option
end)
data =
if multiple do
Map.put(object.data, "anyOf", options)
else
Map.put(object.data, "oneOf", options)
end
object
|> Object.change(%{data: data})
|> update_and_set_cache()
else
_ -> :noop
end
end
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 172c952d4..eeb826814 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -1,1186 +1,1181 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Conversation
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Object.Fetcher
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.Upload
alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.WebFinger
import Ecto.Query
import Pleroma.Web.ActivityPub.Utils
import Pleroma.Web.ActivityPub.Visibility
require Logger
require Pleroma.Constants
# For Announce activities, we filter the recipients based on following status for any actors
# that match actual users. See issue #164 for more information about why this is necessary.
defp get_recipients(%{"type" => "Announce"} = data) do
to = Map.get(data, "to", [])
cc = Map.get(data, "cc", [])
bcc = Map.get(data, "bcc", [])
actor = User.get_cached_by_ap_id(data["actor"])
recipients =
Enum.filter(Enum.concat([to, cc, bcc]), fn recipient ->
case User.get_cached_by_ap_id(recipient) do
nil -> true
user -> User.following?(user, actor)
end
end)
{recipients, to, cc}
end
defp get_recipients(%{"type" => "Create"} = data) do
to = Map.get(data, "to", [])
cc = Map.get(data, "cc", [])
bcc = Map.get(data, "bcc", [])
actor = Map.get(data, "actor", [])
recipients = [to, cc, bcc, [actor]] |> Enum.concat() |> Enum.uniq()
{recipients, to, cc}
end
defp get_recipients(data) do
to = Map.get(data, "to", [])
cc = Map.get(data, "cc", [])
bcc = Map.get(data, "bcc", [])
recipients = Enum.concat([to, cc, bcc])
{recipients, to, cc}
end
defp check_actor_is_active(actor) do
if not is_nil(actor) do
with user <- User.get_cached_by_ap_id(actor),
false <- user.info.deactivated do
true
else
_e -> false
end
else
true
end
end
defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(content) do
limit = Config.get([:instance, :remote_limit])
String.length(content) <= limit
end
defp check_remote_limit(_), do: true
def increase_note_count_if_public(actor, object) do
if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
end
def decrease_note_count_if_public(actor, object) do
if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
end
def increase_replies_count_if_reply(%{
"object" => %{"inReplyTo" => reply_ap_id} = object,
"type" => "Create"
}) do
if is_public?(object) do
Object.increase_replies_count(reply_ap_id)
end
end
def increase_replies_count_if_reply(_create_data), do: :noop
def decrease_replies_count_if_reply(%Object{
data: %{"inReplyTo" => reply_ap_id} = object
}) do
if is_public?(object) do
Object.decrease_replies_count(reply_ap_id)
end
end
def decrease_replies_count_if_reply(_object), do: :noop
def increase_poll_votes_if_vote(%{
"object" => %{"inReplyTo" => reply_ap_id, "name" => name},
"type" => "Create"
}) do
Object.increase_vote_count(reply_ap_id, name)
end
def increase_poll_votes_if_vote(_create_data), do: :noop
def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do
with nil <- Activity.normalize(map),
map <- lazy_put_activity_defaults(map, fake),
true <- bypass_actor_check || check_actor_is_active(map["actor"]),
{_, true} <- {:remote_limit_error, check_remote_limit(map)},
{:ok, map} <- MRF.filter(map),
{recipients, _, _} = get_recipients(map),
{:fake, false, map, recipients} <- {:fake, fake, map, recipients},
:ok <- Containment.contain_child(map),
{:ok, map, object} <- insert_full_object(map) do
{:ok, activity} =
Repo.insert(%Activity{
data: map,
local: local,
actor: map["actor"],
recipients: recipients
})
# Splice in the child object if we have one.
activity =
- if !is_nil(object) do
+ if not is_nil(object) do
Map.put(activity, :object, object)
else
activity
end
PleromaJobQueue.enqueue(:background, Pleroma.Web.RichMedia.Helpers, [:fetch, activity])
Notification.create_notifications(activity)
participations =
activity
|> Conversation.create_or_bump_for()
|> get_participations()
stream_out(activity)
stream_out_participations(participations)
{:ok, activity}
else
%Activity{} = activity ->
{:ok, activity}
{:fake, true, map, recipients} ->
activity = %Activity{
data: map,
local: local,
actor: map["actor"],
recipients: recipients,
id: "pleroma:fakeid"
}
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
{:ok, activity}
error ->
{:error, error}
end
end
defp get_participations({:ok, %{participations: participations}}), do: participations
defp get_participations(_), do: []
def stream_out_participations(participations) do
participations =
participations
|> Repo.preload(:user)
Enum.each(participations, fn participation ->
Pleroma.Web.Streamer.stream("participation", participation)
end)
end
def stream_out_participations(%Object{data: %{"context" => context}}, user) do
with %Conversation{} = conversation <- Conversation.get_for_ap_id(context),
conversation = Repo.preload(conversation, :participations),
last_activity_id =
fetch_latest_activity_id_for_context(conversation.ap_id, %{
"user" => user,
"blocking_user" => user
}) do
if last_activity_id do
stream_out_participations(conversation.participations)
end
end
end
def stream_out_participations(_, _), do: :noop
def stream_out(activity) do
if activity.data["type"] in ["Create", "Announce", "Delete"] do
object = Object.normalize(activity)
# Do not stream out poll replies
unless object.data["type"] == "Answer" do
Pleroma.Web.Streamer.stream("user", activity)
Pleroma.Web.Streamer.stream("list", activity)
if get_visibility(activity) == "public" do
Pleroma.Web.Streamer.stream("public", activity)
if activity.local do
Pleroma.Web.Streamer.stream("public:local", activity)
end
if activity.data["type"] in ["Create"] do
object.data
|> Map.get("tag", [])
|> Enum.filter(fn tag -> is_bitstring(tag) end)
|> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end)
if object.data["attachment"] != [] do
Pleroma.Web.Streamer.stream("public:media", activity)
if activity.local do
Pleroma.Web.Streamer.stream("public:local:media", activity)
end
end
end
else
if get_visibility(activity) == "direct",
do: Pleroma.Web.Streamer.stream("direct", activity)
end
end
end
end
def create(%{to: to, actor: actor, context: context, object: object} = params, fake \\ false) do
additional = params[:additional] || %{}
# only accept false as false value
local = !(params[:local] == false)
published = params[:published]
with create_data <-
make_create_data(
%{to: to, actor: actor, published: published, context: context, object: object},
additional
),
{:ok, activity} <- insert(create_data, local, fake),
{:fake, false, activity} <- {:fake, fake, activity},
_ <- increase_replies_count_if_reply(create_data),
_ <- increase_poll_votes_if_vote(create_data),
# Changing note count prior to enqueuing federation task in order to avoid
# race conditions on updating user.info
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
{:fake, true, activity} ->
{:ok, activity}
{:error, message} ->
{:error, message}
end
end
def accept(%{to: to, actor: actor, object: object} = params) do
# only accept false as false value
local = !(params[:local] == false)
with data <- %{"to" => to, "type" => "Accept", "actor" => actor.ap_id, "object" => object},
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def reject(%{to: to, actor: actor, object: object} = params) do
# only accept false as false value
local = !(params[:local] == false)
with data <- %{"to" => to, "type" => "Reject", "actor" => actor.ap_id, "object" => object},
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
# only accept false as false value
local = !(params[:local] == false)
with data <- %{
"to" => to,
"cc" => cc,
"type" => "Update",
"actor" => actor,
"object" => object
},
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
# TODO: This is weird, maybe we shouldn't check here if we can make the activity.
def like(
%User{ap_id: ap_id} = user,
%Object{data: %{"id" => _}} = object,
activity_id \\ nil,
local \\ true
) do
with nil <- get_existing_like(ap_id, object),
like_data <- make_like_data(user, object, activity_id),
{:ok, activity} <- insert(like_data, local),
{:ok, object} <- add_like_to_object(activity, object),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
%Activity{} = activity -> {:ok, activity, object}
error -> {:error, error}
end
end
- def unlike(
- %User{} = actor,
- %Object{} = object,
- activity_id \\ nil,
- local \\ true
- ) do
+ def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) do
with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object),
unlike_data <- make_unlike_data(actor, like_activity, activity_id),
{:ok, unlike_activity} <- insert(unlike_data, local),
{:ok, _activity} <- Repo.delete(like_activity),
{:ok, object} <- remove_like_from_object(like_activity, object),
:ok <- maybe_federate(unlike_activity) do
{:ok, unlike_activity, like_activity, object}
else
_e -> {:ok, object}
end
end
def announce(
%User{ap_id: _} = user,
%Object{data: %{"id" => _}} = object,
activity_id \\ nil,
local \\ true,
public \\ true
) do
with true <- is_public?(object),
announce_data <- make_announce_data(user, object, activity_id, public),
{:ok, activity} <- insert(announce_data, local),
{:ok, object} <- add_announce_to_object(activity, object),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
error -> {:error, error}
end
end
def unannounce(
%User{} = actor,
%Object{} = object,
activity_id \\ nil,
local \\ true
) do
with %Activity{} = announce_activity <- get_existing_announce(actor.ap_id, object),
unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
{:ok, unannounce_activity} <- insert(unannounce_data, local),
:ok <- maybe_federate(unannounce_activity),
{:ok, _activity} <- Repo.delete(announce_activity),
{:ok, object} <- remove_announce_from_object(announce_activity, object) do
{:ok, unannounce_activity, object}
else
_e -> {:ok, object}
end
end
def follow(follower, followed, activity_id \\ nil, local \\ true) do
with data <- make_follow_data(follower, followed, activity_id),
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity),
_ <- User.set_follow_state_cache(follower.ap_id, followed.ap_id, activity.data["state"]) do
{:ok, activity}
end
end
def unfollow(follower, followed, activity_id \\ nil, local \\ true) do
with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
{:ok, activity} <- insert(unfollow_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def delete(%User{ap_id: ap_id, follower_address: follower_address} = user) do
with data <- %{
"to" => [follower_address],
"type" => "Delete",
"actor" => ap_id,
"object" => %{"type" => "Person", "id" => ap_id}
},
{:ok, activity} <- insert(data, true, true, true),
:ok <- maybe_federate(activity) do
{:ok, user}
end
end
def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do
user = User.get_cached_by_ap_id(actor)
to = (object.data["to"] || []) ++ (object.data["cc"] || [])
with {:ok, object, activity} <- Object.delete(object),
data <- %{
"type" => "Delete",
"actor" => actor,
"object" => id,
"to" => to,
"deleted_activity_id" => activity && activity.id
},
{:ok, activity} <- insert(data, local, false),
stream_out_participations(object, user),
_ <- decrease_replies_count_if_reply(object),
# Changing note count prior to enqueuing federation task in order to avoid
# race conditions on updating user.info
{:ok, _actor} <- decrease_note_count_if_public(user, object),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def block(blocker, blocked, activity_id \\ nil, local \\ true) do
outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
if unfollow_blocked do
follow_activity = fetch_latest_follow(blocker, blocked)
if follow_activity, do: unfollow(blocker, blocked, nil, local)
end
with true <- outgoing_blocks,
block_data <- make_block_data(blocker, blocked, activity_id),
{:ok, activity} <- insert(block_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
_e -> {:ok, nil}
end
end
def unblock(blocker, blocked, activity_id \\ nil, local \\ true) do
with %Activity{} = block_activity <- fetch_latest_block(blocker, blocked),
unblock_data <- make_unblock_data(blocker, blocked, block_activity, activity_id),
{:ok, activity} <- insert(unblock_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def flag(
%{
actor: actor,
context: context,
account: account,
statuses: statuses,
content: content
} = params
) do
# only accept false as false value
local = !(params[:local] == false)
forward = !(params[:forward] == false)
additional = params[:additional] || %{}
params = %{
actor: actor,
context: context,
account: account,
statuses: statuses,
content: content
}
additional =
if forward do
Map.merge(additional, %{"to" => [], "cc" => [account.ap_id]})
else
Map.merge(additional, %{"to" => [], "cc" => []})
end
with flag_data <- make_flag_data(params, additional),
{:ok, activity} <- insert(flag_data, local),
:ok <- maybe_federate(activity) do
Enum.each(User.all_superusers(), fn superuser ->
superuser
|> Pleroma.Emails.AdminEmail.report(actor, account, statuses, content)
|> Pleroma.Emails.Mailer.deliver_async()
end)
{:ok, activity}
end
end
defp fetch_activities_for_context_query(context, opts) do
public = [Pleroma.Constants.as_public()]
recipients =
if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public
from(activity in Activity)
|> maybe_preload_objects(opts)
|> maybe_preload_bookmarks(opts)
|> maybe_set_thread_muted_field(opts)
|> restrict_blocked(opts)
|> restrict_recipients(recipients, opts["user"])
|> where(
[activity],
fragment(
"?->>'type' = ? and ?->>'context' = ?",
activity.data,
"Create",
activity.data,
^context
)
)
|> exclude_poll_votes(opts)
|> exclude_id(opts)
|> order_by([activity], desc: activity.id)
end
@spec fetch_activities_for_context(String.t(), keyword() | map()) :: [Activity.t()]
def fetch_activities_for_context(context, opts \\ %{}) do
context
|> fetch_activities_for_context_query(opts)
|> Repo.all()
end
@spec fetch_latest_activity_id_for_context(String.t(), keyword() | map()) ::
Pleroma.FlakeId.t() | nil
def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
context
|> fetch_activities_for_context_query(Map.merge(%{"skip_preload" => true}, opts))
|> limit(1)
|> select([a], a.id)
|> Repo.one()
end
def fetch_public_activities(opts \\ %{}) do
q = fetch_activities_query([Pleroma.Constants.as_public()], opts)
q
|> restrict_unlisted()
|> Pagination.fetch_paginated(opts)
|> Enum.reverse()
end
@valid_visibilities ~w[direct unlisted public private]
defp restrict_visibility(query, %{visibility: visibility})
when is_list(visibility) do
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
query =
from(
a in query,
where:
fragment(
"activity_visibility(?, ?, ?) = ANY (?)",
a.actor,
a.recipients,
a.data,
^visibility
)
)
query
else
Logger.error("Could not restrict visibility to #{visibility}")
end
end
defp restrict_visibility(query, %{visibility: visibility})
when visibility in @valid_visibilities do
from(
a in query,
where:
fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility)
)
end
defp restrict_visibility(_query, %{visibility: visibility})
when visibility not in @valid_visibilities do
Logger.error("Could not restrict visibility to #{visibility}")
end
defp restrict_visibility(query, _visibility), do: query
defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _),
do: query
defp restrict_thread_visibility(
query,
%{"user" => %User{info: %{skip_thread_containment: true}}},
_
),
do: query
defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}, _) do
from(
a in query,
where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data)
)
end
defp restrict_thread_visibility(query, _, _), do: query
def fetch_user_activities(user, reading_user, params \\ %{}) do
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("user", reading_user)
|> Map.put("actor_id", user.ap_id)
|> Map.put("whole_db", true)
|> Map.put("pinned_activity_ids", user.info.pinned_activities)
recipients =
user_activities_recipients(%{
"godmode" => params["godmode"],
"reading_user" => reading_user
})
fetch_activities(recipients, params)
|> Enum.reverse()
end
defp user_activities_recipients(%{"godmode" => true}) do
[]
end
defp user_activities_recipients(%{"reading_user" => reading_user}) do
if reading_user do
[Pleroma.Constants.as_public()] ++ [reading_user.ap_id | reading_user.following]
else
[Pleroma.Constants.as_public()]
end
end
defp restrict_since(query, %{"since_id" => ""}), do: query
defp restrict_since(query, %{"since_id" => since_id}) do
from(activity in query, where: activity.id > ^since_id)
end
defp restrict_since(query, _), do: query
defp restrict_tag_reject(_query, %{"tag_reject" => _tag_reject, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_tag_reject(query, %{"tag_reject" => tag_reject})
when is_list(tag_reject) and tag_reject != [] do
from(
[_activity, object] in query,
where: fragment("not (?)->'tag' \\?| (?)", object.data, ^tag_reject)
)
end
defp restrict_tag_reject(query, _), do: query
defp restrict_tag_all(_query, %{"tag_all" => _tag_all, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_tag_all(query, %{"tag_all" => tag_all})
when is_list(tag_all) and tag_all != [] do
from(
[_activity, object] in query,
where: fragment("(?)->'tag' \\?& (?)", object.data, ^tag_all)
)
end
defp restrict_tag_all(query, _), do: query
defp restrict_tag(_query, %{"tag" => _tag, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do
from(
[_activity, object] in query,
where: fragment("(?)->'tag' \\?| (?)", object.data, ^tag)
)
end
defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do
from(
[_activity, object] in query,
where: fragment("(?)->'tag' \\? (?)", object.data, ^tag)
)
end
defp restrict_tag(query, _), do: query
defp restrict_recipients(query, [], _user), do: query
defp restrict_recipients(query, recipients, nil) do
from(activity in query, where: fragment("? && ?", ^recipients, activity.recipients))
end
defp restrict_recipients(query, recipients, user) do
from(
activity in query,
where: fragment("? && ?", ^recipients, activity.recipients),
or_where: activity.actor == ^user.ap_id
)
end
defp restrict_local(query, %{"local_only" => true}) do
from(activity in query, where: activity.local == true)
end
defp restrict_local(query, _), do: query
defp restrict_actor(query, %{"actor_id" => actor_id}) do
from(activity in query, where: activity.actor == ^actor_id)
end
defp restrict_actor(query, _), do: query
defp restrict_type(query, %{"type" => type}) when is_binary(type) do
from(activity in query, where: fragment("?->>'type' = ?", activity.data, ^type))
end
defp restrict_type(query, %{"type" => type}) do
from(activity in query, where: fragment("?->>'type' = ANY(?)", activity.data, ^type))
end
defp restrict_type(query, _), do: query
defp restrict_state(query, %{"state" => state}) do
from(activity in query, where: fragment("?->>'state' = ?", activity.data, ^state))
end
defp restrict_state(query, _), do: query
defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do
from(
[_activity, object] in query,
where: fragment("(?)->'likes' \\? (?)", object.data, ^ap_id)
)
end
defp restrict_favorited_by(query, _), do: query
defp restrict_media(_query, %{"only_media" => _val, "skip_preload" => true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do
from(
[_activity, object] in query,
where: fragment("not (?)->'attachment' = (?)", object.data, ^[])
)
end
defp restrict_media(query, _), do: query
defp restrict_replies(query, %{"exclude_replies" => val}) when val == "true" or val == "1" do
from(
activity in query,
where: fragment("?->'object'->>'inReplyTo' is null", activity.data)
)
end
defp restrict_replies(query, _), do: query
defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val == "true" or val == "1" do
from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data))
end
defp restrict_reblogs(query, _), do: query
defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query
defp restrict_muted(query, %{"muting_user" => %User{info: info}} = opts) do
mutes = info.mutes
query =
from([activity] in query,
where: fragment("not (? = ANY(?))", activity.actor, ^mutes),
where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes)
)
unless opts["skip_preload"] do
from([thread_mute: tm] in query, where: is_nil(tm))
else
query
end
end
defp restrict_muted(query, _), do: query
defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
blocks = info.blocks || []
domain_blocks = info.domain_blocks || []
query =
if has_named_binding?(query, :object), do: query, else: Activity.with_joined_object(query)
from(
[activity, object: o] in query,
where: fragment("not (? = ANY(?))", activity.actor, ^blocks),
where: fragment("not (? && ?)", activity.recipients, ^blocks),
where:
fragment(
"not (?->>'type' = 'Announce' and ?->'to' \\?| ?)",
activity.data,
activity.data,
^blocks
),
where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks),
where: fragment("not (split_part(?->>'actor', '/', 3) = ANY(?))", o.data, ^domain_blocks)
)
end
defp restrict_blocked(query, _), do: query
defp restrict_unlisted(query) do
from(
activity in query,
where:
fragment(
"not (coalesce(?->'cc', '{}'::jsonb) \\?| ?)",
activity.data,
^[Pleroma.Constants.as_public()]
)
)
end
defp restrict_pinned(query, %{"pinned" => "true", "pinned_activity_ids" => ids}) do
from(activity in query, where: activity.id in ^ids)
end
defp restrict_pinned(query, _), do: query
defp restrict_muted_reblogs(query, %{"muting_user" => %User{info: info}}) do
muted_reblogs = info.muted_reblogs || []
from(
activity in query,
where:
fragment(
"not ( ?->>'type' = 'Announce' and ? = ANY(?))",
activity.data,
activity.actor,
^muted_reblogs
)
)
end
defp restrict_muted_reblogs(query, _), do: query
defp exclude_poll_votes(query, %{"include_poll_votes" => "true"}), do: query
defp exclude_poll_votes(query, _) do
if has_named_binding?(query, :object) do
from([activity, object: o] in query,
where: fragment("not(?->>'type' = ?)", o.data, "Answer")
)
else
query
end
end
defp exclude_id(query, %{"exclude_id" => id}) when is_binary(id) do
from(activity in query, where: activity.id != ^id)
end
defp exclude_id(query, _), do: query
defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query
defp maybe_preload_objects(query, _) do
query
|> Activity.with_preloaded_object()
end
defp maybe_preload_bookmarks(query, %{"skip_preload" => true}), do: query
defp maybe_preload_bookmarks(query, opts) do
query
|> Activity.with_preloaded_bookmark(opts["user"])
end
defp maybe_set_thread_muted_field(query, %{"skip_preload" => true}), do: query
defp maybe_set_thread_muted_field(query, opts) do
query
|> Activity.with_set_thread_muted_field(opts["muting_user"] || opts["user"])
end
defp maybe_order(query, %{order: :desc}) do
query
|> order_by(desc: :id)
end
defp maybe_order(query, %{order: :asc}) do
query
|> order_by(asc: :id)
end
defp maybe_order(query, _), do: query
def fetch_activities_query(recipients, opts \\ %{}) do
config = %{
skip_thread_containment: Config.get([:instance, :skip_thread_containment])
}
Activity
|> maybe_preload_objects(opts)
|> maybe_preload_bookmarks(opts)
|> maybe_set_thread_muted_field(opts)
|> maybe_order(opts)
|> restrict_recipients(recipients, opts["user"])
|> restrict_tag(opts)
|> restrict_tag_reject(opts)
|> restrict_tag_all(opts)
|> restrict_since(opts)
|> restrict_local(opts)
|> restrict_actor(opts)
|> restrict_type(opts)
|> restrict_state(opts)
|> restrict_favorited_by(opts)
|> restrict_blocked(opts)
|> restrict_muted(opts)
|> restrict_media(opts)
|> restrict_visibility(opts)
|> restrict_thread_visibility(opts, config)
|> restrict_replies(opts)
|> restrict_reblogs(opts)
|> restrict_pinned(opts)
|> restrict_muted_reblogs(opts)
|> Activity.restrict_deactivated_users()
|> exclude_poll_votes(opts)
end
def fetch_activities(recipients, opts \\ %{}) do
list_memberships = Pleroma.List.memberships(opts["user"])
fetch_activities_query(recipients ++ list_memberships, opts)
|> Pagination.fetch_paginated(opts)
|> Enum.reverse()
|> maybe_update_cc(list_memberships, opts["user"])
end
defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id})
when is_list(list_memberships) and length(list_memberships) > 0 do
Enum.map(activities, fn
%{data: %{"bcc" => bcc}} = activity when is_list(bcc) and length(bcc) > 0 ->
if Enum.any?(bcc, &(&1 in list_memberships)) do
update_in(activity.data["cc"], &[user_ap_id | &1])
else
activity
end
activity ->
activity
end)
end
defp maybe_update_cc(activities, _, _), do: activities
def fetch_activities_bounded_query(query, recipients, recipients_with_public) do
from(activity in query,
where:
fragment("? && ?", activity.recipients, ^recipients) or
(fragment("? && ?", activity.recipients, ^recipients_with_public) and
^Pleroma.Constants.as_public() in activity.recipients)
)
end
def fetch_activities_bounded(recipients, recipients_with_public, opts \\ %{}) do
fetch_activities_query([], opts)
|> fetch_activities_bounded_query(recipients, recipients_with_public)
|> Pagination.fetch_paginated(opts)
|> Enum.reverse()
end
def upload(file, opts \\ []) do
with {:ok, data} <- Upload.store(file, opts) do
obj_data =
if opts[:actor] do
Map.put(data, "actor", opts[:actor])
else
data
end
Repo.insert(%Object{data: obj_data})
end
end
defp object_to_user_data(data) do
avatar =
data["icon"]["url"] &&
%{
"type" => "Image",
"url" => [%{"href" => data["icon"]["url"]}]
}
banner =
data["image"]["url"] &&
%{
"type" => "Image",
"url" => [%{"href" => data["image"]["url"]}]
}
fields =
data
|> Map.get("attachment", [])
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
locked = data["manuallyApprovesFollowers"] || false
data = Transmogrifier.maybe_fix_user_object(data)
user_data = %{
ap_id: data["id"],
info: %{
ap_enabled: true,
source_data: data,
banner: banner,
fields: fields,
locked: locked
},
avatar: avatar,
name: data["name"],
follower_address: data["followers"],
following_address: data["following"],
bio: data["summary"]
}
# nickname can be nil because of virtual actors
user_data =
if data["preferredUsername"] do
Map.put(
user_data,
:nickname,
"#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}"
)
else
Map.put(user_data, :nickname, nil)
end
{:ok, user_data}
end
def fetch_follow_information_for_user(user) do
with {:ok, following_data} <-
Fetcher.fetch_and_contain_remote_object_from_id(user.following_address),
following_count when is_integer(following_count) <- following_data["totalItems"],
{:ok, hide_follows} <- collection_private(following_data),
{:ok, followers_data} <-
Fetcher.fetch_and_contain_remote_object_from_id(user.follower_address),
followers_count when is_integer(followers_count) <- followers_data["totalItems"],
{:ok, hide_followers} <- collection_private(followers_data) do
{:ok,
%{
hide_follows: hide_follows,
follower_count: followers_count,
following_count: following_count,
hide_followers: hide_followers
}}
else
{:error, _} = e ->
e
e ->
{:error, e}
end
end
defp maybe_update_follow_information(data) do
with {:enabled, true} <-
{:enabled, Pleroma.Config.get([:instance, :external_user_synchronization])},
{:ok, info} <- fetch_follow_information_for_user(data) do
info = Map.merge(data.info, info)
Map.put(data, :info, info)
else
{:enabled, false} ->
data
e ->
Logger.error(
"Follower/Following counter update for #{data.ap_id} failed.\n" <> inspect(e)
)
data
end
end
defp collection_private(data) do
if is_map(data["first"]) and
data["first"]["type"] in ["CollectionPage", "OrderedCollectionPage"] do
{:ok, false}
else
with {:ok, %{"type" => type}} when type in ["CollectionPage", "OrderedCollectionPage"] <-
Fetcher.fetch_and_contain_remote_object_from_id(data["first"]) do
{:ok, false}
else
{:error, {:ok, %{status: code}}} when code in [401, 403] ->
{:ok, true}
{:error, _} = e ->
e
e ->
{:error, e}
end
end
end
def user_data_from_user_object(data) do
with {:ok, data} <- MRF.filter(data),
{:ok, data} <- object_to_user_data(data) do
{:ok, data}
else
e -> {:error, e}
end
end
def fetch_and_prepare_user_from_ap_id(ap_id) do
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id),
{:ok, data} <- user_data_from_user_object(data),
data <- maybe_update_follow_information(data) do
{:ok, data}
else
e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
end
end
def make_user_from_ap_id(ap_id) do
if _user = User.get_cached_by_ap_id(ap_id) do
Transmogrifier.upgrade_user_from_ap_id(ap_id)
else
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do
User.insert_or_update_user(data)
else
e -> {:error, e}
end
end
end
def make_user_from_nickname(nickname) do
with {:ok, %{"ap_id" => ap_id}} when not is_nil(ap_id) <- WebFinger.finger(nickname) do
make_user_from_ap_id(ap_id)
else
_e -> {:error, "No AP id in WebFinger"}
end
end
# filter out broken threads
def contain_broken_threads(%Activity{} = activity, %User{} = user) do
entire_thread_visible_for_user?(activity, user)
end
# do post-processing on a specific activity
def contain_activity(%Activity{} = activity, %User{} = user) do
contain_broken_threads(activity, user)
end
def fetch_direct_messages_query do
Activity
|> restrict_type(%{"type" => "Create"})
|> restrict_visibility(%{visibility: "direct"})
|> order_by([activity], asc: activity.id)
end
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index ed801a7ae..08bf1c752 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -1,381 +1,381 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPubController do
use Pleroma.Web, :controller
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator
require Logger
action_fallback(:errors)
plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay])
plug(:set_requester_reachable when action in [:inbox])
plug(:relay_active? when action in [:relay])
def relay_active?(conn, _) do
if Pleroma.Config.get([:instance, :allow_relay]) do
conn
else
conn
|> render_error(:not_found, "not found")
|> halt()
end
end
def user(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("user.json", %{user: user}))
else
nil -> {:error, :not_found}
end
end
def object(conn, %{"uuid" => uuid}) do
with ap_id <- o_status_url(conn, :object, uuid),
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
{_, true} <- {:public?, Visibility.is_public?(object)} do
conn
|> put_resp_content_type("application/activity+json")
|> json(ObjectView.render("object.json", %{object: object}))
else
{:public?, false} ->
{:error, :not_found}
end
end
def object_likes(conn, %{"uuid" => uuid, "page" => page}) do
with ap_id <- o_status_url(conn, :object, uuid),
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
{_, true} <- {:public?, Visibility.is_public?(object)},
likes <- Utils.get_object_likes(object) do
{page, _} = Integer.parse(page)
conn
|> put_resp_content_type("application/activity+json")
|> json(ObjectView.render("likes.json", ap_id, likes, page))
else
{:public?, false} ->
{:error, :not_found}
end
end
def object_likes(conn, %{"uuid" => uuid}) do
with ap_id <- o_status_url(conn, :object, uuid),
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
{_, true} <- {:public?, Visibility.is_public?(object)},
likes <- Utils.get_object_likes(object) do
conn
|> put_resp_content_type("application/activity+json")
|> json(ObjectView.render("likes.json", ap_id, likes))
else
{:public?, false} ->
{:error, :not_found}
end
end
def activity(conn, %{"uuid" => uuid}) do
with ap_id <- o_status_url(conn, :activity, uuid),
%Activity{} = activity <- Activity.normalize(ap_id),
{_, true} <- {:public?, Visibility.is_public?(activity)} do
conn
|> put_resp_content_type("application/activity+json")
|> json(ObjectView.render("object.json", %{object: activity}))
else
{:public?, false} ->
{:error, :not_found}
end
end
# GET /relay/following
def following(%{assigns: %{relay: true}} = conn, _params) do
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("following.json", %{user: Relay.get_actor()}))
end
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
{:show_follows, true} <-
{:show_follows, (for_user && for_user == user) || !user.info.hide_follows} do
{page, _} = Integer.parse(page)
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("following.json", %{user: user, page: page, for: for_user}))
else
{:show_follows, _} ->
conn
|> put_resp_content_type("application/activity+json")
|> send_resp(403, "")
end
end
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("following.json", %{user: user, for: for_user}))
end
end
# GET /relay/followers
def followers(%{assigns: %{relay: true}} = conn, _params) do
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("followers.json", %{user: Relay.get_actor()}))
end
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
{:show_followers, true} <-
{:show_followers, (for_user && for_user == user) || !user.info.hide_followers} do
{page, _} = Integer.parse(page)
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("followers.json", %{user: user, page: page, for: for_user}))
else
{:show_followers, _} ->
conn
|> put_resp_content_type("application/activity+json")
|> send_resp(403, "")
end
end
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("followers.json", %{user: user, for: for_user}))
end
end
def outbox(conn, %{"nickname" => nickname} = params) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("outbox.json", %{user: user, max_id: params["max_id"]}))
end
end
def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
with %User{} = recipient <- User.get_cached_by_nickname(nickname),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
true <- Utils.recipient_in_message(recipient, actor, params),
params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
Federator.incoming_ap_doc(params)
json(conn, "ok")
end
end
def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
Federator.incoming_ap_doc(params)
json(conn, "ok")
end
# only accept relayed Creates
def inbox(conn, %{"type" => "Create"} = params) do
Logger.info(
"Signature missing or not from author, relayed Create message, fetching object from source"
)
Fetcher.fetch_object_from_id(params["object"]["id"])
json(conn, "ok")
end
def inbox(conn, params) do
headers = Enum.into(conn.req_headers, %{})
if String.contains?(headers["signature"], params["actor"]) do
Logger.info(
"Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
)
Logger.info(inspect(conn.req_headers))
end
json(conn, dgettext("errors", "error"))
end
defp represent_service_actor(%User{} = user, conn) do
with {:ok, user} <- User.ensure_keys_present(user) do
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("user.json", %{user: user}))
else
nil -> {:error, :not_found}
end
end
defp represent_service_actor(nil, _), do: {:error, :not_found}
def relay(conn, _params) do
Relay.get_actor()
|> represent_service_actor(conn)
end
def internal_fetch(conn, _params) do
InternalFetchActor.get_actor()
|> represent_service_actor(conn)
end
def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("user.json", %{user: user}))
end
def whoami(_conn, _params), do: {:error, :not_found}
def read_inbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = params) do
if nickname == user.nickname do
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("inbox.json", %{user: user, max_id: params["max_id"]}))
else
err =
dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
nickname: nickname,
as_nickname: user.nickname
)
conn
|> put_status(:forbidden)
|> json(err)
end
end
def handle_user_activity(user, %{"type" => "Create"} = params) do
object =
params["object"]
|> Map.merge(Map.take(params, ["to", "cc"]))
|> Map.put("attributedTo", user.ap_id())
|> Transmogrifier.fix_object()
ActivityPub.create(%{
to: params["to"],
actor: user,
context: object["context"],
object: object,
additional: Map.take(params, ["cc"])
})
end
def handle_user_activity(user, %{"type" => "Delete"} = params) do
with %Object{} = object <- Object.normalize(params["object"]),
true <- user.info.is_moderator || user.ap_id == object.data["actor"],
{:ok, delete} <- ActivityPub.delete(object) do
{:ok, delete}
else
_ -> {:error, dgettext("errors", "Can't delete object")}
end
end
def handle_user_activity(user, %{"type" => "Like"} = params) do
with %Object{} = object <- Object.normalize(params["object"]),
{:ok, activity, _object} <- ActivityPub.like(user, object) do
{:ok, activity}
else
_ -> {:error, dgettext("errors", "Can't like object")}
end
end
def handle_user_activity(_, _) do
{:error, dgettext("errors", "Unhandled activity type")}
end
def update_outbox(
- %{assigns: %{user: user}} = conn,
+ %{assigns: %{user: %User{nickname: nickname} = user}} = conn,
%{"nickname" => nickname} = params
) do
- if nickname == user.nickname do
- actor = user.ap_id()
-
- params =
- params
- |> Map.drop(["id"])
- |> Map.put("actor", actor)
- |> Transmogrifier.fix_addressing()
+ actor = user.ap_id()
- with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
- conn
- |> put_status(:created)
- |> put_resp_header("location", activity.data["id"])
- |> json(activity.data)
- else
- {:error, message} ->
- conn
- |> put_status(:bad_request)
- |> json(message)
- end
- else
- err =
- dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
- nickname: nickname,
- as_nickname: user.nickname
- )
+ params =
+ params
+ |> Map.drop(["id"])
+ |> Map.put("actor", actor)
+ |> Transmogrifier.fix_addressing()
+ with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
conn
- |> put_status(:forbidden)
- |> json(err)
+ |> put_status(:created)
+ |> put_resp_header("location", activity.data["id"])
+ |> json(activity.data)
+ else
+ {:error, message} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(message)
end
end
+ def update_outbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = _) do
+ err =
+ dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
+ nickname: nickname,
+ as_nickname: user.nickname
+ )
+
+ conn
+ |> put_status(:forbidden)
+ |> json(err)
+ end
+
def errors(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> json(dgettext("errors", "Not found"))
end
def errors(conn, _e) do
conn
|> put_status(:internal_server_error)
|> json(dgettext("errors", "error"))
end
defp set_requester_reachable(%Plug.Conn{} = conn, _) do
with actor <- conn.params["actor"],
true <- is_binary(actor) do
Pleroma.Instances.set_reachable(actor)
end
conn
end
defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
{:ok, new_user} = User.ensure_keys_present(user)
for_user =
if new_user != user and match?(%User{}, for_user) do
User.get_cached_by_nickname(for_user.nickname)
else
for_user
end
{new_user, for_user}
end
end
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 1c3058658..c9c0c3763 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -1,802 +1,788 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Utils do
alias Ecto.Changeset
alias Ecto.UUID
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router.Helpers
import Ecto.Query
require Logger
require Pleroma.Constants
@supported_object_types ["Article", "Note", "Video", "Page", "Question", "Answer"]
@supported_report_states ~w(open closed resolved)
@valid_visibilities ~w(public unlisted private direct)
# Some implementations send the actor URI as the actor field, others send the entire actor object,
# so figure out what the actor's URI is based on what we have.
def get_ap_id(%{"id" => id} = _), do: id
def get_ap_id(id), do: id
def normalize_params(params) do
Map.put(params, "actor", get_ap_id(params["actor"]))
end
def determine_explicit_mentions(%{"tag" => tag} = _object) when is_list(tag) do
tag
|> Enum.filter(fn x -> is_map(x) end)
|> Enum.filter(fn x -> x["type"] == "Mention" end)
|> Enum.map(fn x -> x["href"] end)
end
def determine_explicit_mentions(%{"tag" => tag} = object) when is_map(tag) do
Map.put(object, "tag", [tag])
|> determine_explicit_mentions()
end
def determine_explicit_mentions(_), do: []
defp recipient_in_collection(ap_id, coll) when is_binary(coll), do: ap_id == coll
defp recipient_in_collection(ap_id, coll) when is_list(coll), do: ap_id in coll
defp recipient_in_collection(_, _), do: false
def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params) do
cond do
recipient_in_collection(ap_id, params["to"]) ->
true
recipient_in_collection(ap_id, params["cc"]) ->
true
recipient_in_collection(ap_id, params["bto"]) ->
true
recipient_in_collection(ap_id, params["bcc"]) ->
true
# if the message is unaddressed at all, then assume it is directly addressed
# to the recipient
!params["to"] && !params["cc"] && !params["bto"] && !params["bcc"] ->
true
# if the message is sent from somebody the user is following, then assume it
# is addressed to the recipient
User.following?(recipient, actor) ->
true
true ->
false
end
end
defp extract_list(target) when is_binary(target), do: [target]
defp extract_list(lst) when is_list(lst), do: lst
defp extract_list(_), do: []
def maybe_splice_recipient(ap_id, params) do
need_splice =
!recipient_in_collection(ap_id, params["to"]) &&
!recipient_in_collection(ap_id, params["cc"])
cc_list = extract_list(params["cc"])
if need_splice do
params
|> Map.put("cc", [ap_id | cc_list])
else
params
end
end
def make_json_ld_header do
%{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"#{Web.base_url()}/schemas/litepub-0.1.jsonld",
%{
"@language" => "und"
}
]
}
end
def make_date do
DateTime.utc_now() |> DateTime.to_iso8601()
end
def generate_activity_id do
generate_id("activities")
end
def generate_context_id do
generate_id("contexts")
end
def generate_object_id do
Helpers.o_status_url(Endpoint, :object, UUID.generate())
end
def generate_id(type) do
"#{Web.base_url()}/#{type}/#{UUID.generate()}"
end
def get_notified_from_object(%{"type" => type} = object) when type in @supported_object_types do
fake_create_activity = %{
"to" => object["to"],
"cc" => object["cc"],
"type" => "Create",
"object" => object
}
Notification.get_notified_from_activity(%Activity{data: fake_create_activity}, false)
end
def get_notified_from_object(object) do
Notification.get_notified_from_activity(%Activity{data: object}, false)
end
def create_context(context) do
context = context || generate_id("contexts")
# Ecto has problems accessing the constraint inside the jsonb,
# so we explicitly check for the existed object before insert
object = Object.get_cached_by_ap_id(context)
with true <- is_nil(object),
changeset <- Object.context_mapping(context),
{:ok, inserted_object} <- Repo.insert(changeset) do
inserted_object
else
_ ->
object
end
end
@doc """
Enqueues an activity for federation if it's local
"""
+ @spec maybe_federate(any()) :: :ok
def maybe_federate(%Activity{local: true} = activity) do
if Pleroma.Config.get!([:instance, :federating]) do
priority =
case activity.data["type"] do
"Delete" -> 10
"Create" -> 1
_ -> 5
end
Pleroma.Web.Federator.publish(activity, priority)
end
:ok
end
def maybe_federate(_), do: :ok
@doc """
Adds an id and a published data if they aren't there,
also adds it to an included object
"""
def lazy_put_activity_defaults(map, fake \\ false) do
map =
unless fake do
%{data: %{"id" => context}, id: context_id} = create_context(map["context"])
map
|> Map.put_new_lazy("id", &generate_activity_id/0)
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", context)
|> Map.put_new("context_id", context_id)
else
map
|> Map.put_new("id", "pleroma:fakeid")
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", "pleroma:fakecontext")
|> Map.put_new("context_id", -1)
end
if is_map(map["object"]) do
object = lazy_put_object_defaults(map["object"], map, fake)
%{map | "object" => object}
else
map
end
end
@doc """
Adds an id and published date if they aren't there.
"""
def lazy_put_object_defaults(map, activity \\ %{}, fake)
def lazy_put_object_defaults(map, activity, true = _fake) do
map
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("id", "pleroma:fake_object_id")
|> Map.put_new("context", activity["context"])
|> Map.put_new("fake", true)
|> Map.put_new("context_id", activity["context_id"])
end
def lazy_put_object_defaults(map, activity, _fake) do
map
|> Map.put_new_lazy("id", &generate_object_id/0)
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", activity["context"])
|> Map.put_new("context_id", activity["context_id"])
end
@doc """
Inserts a full object if it is contained in an activity.
"""
def insert_full_object(%{"object" => %{"type" => type} = object_data} = map)
when is_map(object_data) and type in @supported_object_types do
with {:ok, object} <- Object.create(object_data) do
map =
map
|> Map.put("object", object.data["id"])
{:ok, map, object}
end
end
def insert_full_object(map), do: {:ok, map, nil}
#### Like-related helpers
@doc """
Returns an existing like if a user already liked an object
"""
+ @spec get_existing_like(String.t(), map()) :: Activity.t() | nil
def get_existing_like(actor, %{data: %{"id" => id}}) do
- query =
- from(
- activity in Activity,
- where: fragment("(?)->>'actor' = ?", activity.data, ^actor),
- # this is to use the index
- where:
- fragment(
- "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
- activity.data,
- activity.data,
- ^id
- ),
- where: fragment("(?)->>'type' = 'Like'", activity.data)
- )
-
- Repo.one(query)
+ actor
+ |> Activity.Queries.by_actor()
+ |> Activity.Queries.by_object_id(id)
+ |> Activity.Queries.by_type("Like")
+ |> Activity.Queries.limit(1)
+ |> Repo.one()
end
@doc """
Returns like activities targeting an object
"""
def get_object_likes(%{data: %{"id" => id}}) do
- query =
- from(
- activity in Activity,
- # this is to use the index
- where:
- fragment(
- "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
- activity.data,
- activity.data,
- ^id
- ),
- where: fragment("(?)->>'type' = 'Like'", activity.data)
- )
-
- Repo.all(query)
+ id
+ |> Activity.Queries.by_object_id()
+ |> Activity.Queries.by_type("Like")
+ |> Repo.all()
end
+ @spec make_like_data(User.t(), map(), String.t()) :: map()
def make_like_data(
%User{ap_id: ap_id} = actor,
%{data: %{"actor" => object_actor_id, "id" => id}} = object,
activity_id
) do
object_actor = User.get_cached_by_ap_id(object_actor_id)
to =
if Visibility.is_public?(object) do
[actor.follower_address, object.data["actor"]]
else
[object.data["actor"]]
end
cc =
(object.data["to"] ++ (object.data["cc"] || []))
|> List.delete(actor.ap_id)
|> List.delete(object_actor.follower_address)
- data = %{
+ %{
"type" => "Like",
"actor" => ap_id,
"object" => id,
"to" => to,
"cc" => cc,
"context" => object.data["context"]
}
-
- if activity_id, do: Map.put(data, "id", activity_id), else: data
+ |> maybe_put("id", activity_id)
end
+ @spec update_element_in_object(String.t(), list(any), Object.t()) ::
+ {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def update_element_in_object(property, element, object) do
- with new_data <-
- object.data
- |> Map.put("#{property}_count", length(element))
- |> Map.put("#{property}s", element),
- changeset <- Changeset.change(object, data: new_data),
- {:ok, object} <- Object.update_and_set_cache(changeset) do
- {:ok, object}
- end
- end
+ data =
+ Map.merge(
+ object.data,
+ %{"#{property}_count" => length(element), "#{property}s" => element}
+ )
- def update_likes_in_object(likes, object) do
- update_element_in_object("like", likes, object)
+ object
+ |> Changeset.change(data: data)
+ |> Object.update_and_set_cache()
end
+ @spec add_like_to_object(Activity.t(), Object.t()) ::
+ {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
- likes = if is_list(object.data["likes"]), do: object.data["likes"], else: []
-
- with likes <- [actor | likes] |> Enum.uniq() do
- update_likes_in_object(likes, object)
- end
+ [actor | fetch_likes(object)]
+ |> Enum.uniq()
+ |> update_likes_in_object(object)
end
+ @spec remove_like_from_object(Activity.t(), Object.t()) ::
+ {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
- likes = if is_list(object.data["likes"]), do: object.data["likes"], else: []
+ object
+ |> fetch_likes()
+ |> List.delete(actor)
+ |> update_likes_in_object(object)
+ end
- with likes <- likes |> List.delete(actor) do
- update_likes_in_object(likes, object)
+ defp update_likes_in_object(likes, object) do
+ update_element_in_object("like", likes, object)
+ end
+
+ defp fetch_likes(object) do
+ if is_list(object.data["likes"]) do
+ object.data["likes"]
+ else
+ []
end
end
#### Follow-related helpers
@doc """
Updates a follow activity's state (for locked accounts).
"""
def update_follow_state_for_all(
%Activity{data: %{"actor" => actor, "object" => object}} = activity,
state
) do
try do
Ecto.Adapters.SQL.query!(
Repo,
"UPDATE activities SET data = jsonb_set(data, '{state}', $1) WHERE data->>'type' = 'Follow' AND data->>'actor' = $2 AND data->>'object' = $3 AND data->>'state' = 'pending'",
[state, actor, object]
)
User.set_follow_state_cache(actor, object, state)
activity = Activity.get_by_id(activity.id)
{:ok, activity}
rescue
e ->
{:error, e}
end
end
def update_follow_state(
%Activity{data: %{"actor" => actor, "object" => object}} = activity,
state
) do
with new_data <-
activity.data
|> Map.put("state", state),
changeset <- Changeset.change(activity, data: new_data),
{:ok, activity} <- Repo.update(changeset),
_ <- User.set_follow_state_cache(actor, object, state) do
{:ok, activity}
end
end
@doc """
Makes a follow activity data for the given follower and followed
"""
def make_follow_data(
%User{ap_id: follower_id},
%User{ap_id: followed_id} = _followed,
activity_id
) do
- data = %{
+ %{
"type" => "Follow",
"actor" => follower_id,
"to" => [followed_id],
"cc" => [Pleroma.Constants.as_public()],
"object" => followed_id,
"state" => "pending"
}
-
- data = if activity_id, do: Map.put(data, "id", activity_id), else: data
-
- data
+ |> maybe_put("id", activity_id)
end
def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
query =
from(
activity in Activity,
where:
fragment(
"? ->> 'type' = 'Follow'",
activity.data
),
where: activity.actor == ^follower_id,
# this is to use the index
where:
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
activity.data,
^followed_id
),
order_by: [fragment("? desc nulls last", activity.id)],
limit: 1
)
Repo.one(query)
end
#### Announce-related helpers
@doc """
Retruns an existing announce activity if the notice has already been announced
"""
def get_existing_announce(actor, %{data: %{"id" => id}}) do
query =
from(
activity in Activity,
where: activity.actor == ^actor,
# this is to use the index
where:
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
activity.data,
^id
),
where: fragment("(?)->>'type' = 'Announce'", activity.data)
)
Repo.one(query)
end
@doc """
Make announce activity data for the given actor and object
"""
# for relayed messages, we only want to send to subscribers
def make_announce_data(
%User{ap_id: ap_id} = user,
%Object{data: %{"id" => id}} = object,
activity_id,
false
) do
- data = %{
+ %{
"type" => "Announce",
"actor" => ap_id,
"object" => id,
"to" => [user.follower_address],
"cc" => [],
"context" => object.data["context"]
}
-
- if activity_id, do: Map.put(data, "id", activity_id), else: data
+ |> maybe_put("id", activity_id)
end
def make_announce_data(
%User{ap_id: ap_id} = user,
%Object{data: %{"id" => id}} = object,
activity_id,
true
) do
- data = %{
+ %{
"type" => "Announce",
"actor" => ap_id,
"object" => id,
"to" => [user.follower_address, object.data["actor"]],
"cc" => [Pleroma.Constants.as_public()],
"context" => object.data["context"]
}
-
- if activity_id, do: Map.put(data, "id", activity_id), else: data
+ |> maybe_put("id", activity_id)
end
@doc """
Make unannounce activity data for the given actor and object
"""
def make_unannounce_data(
%User{ap_id: ap_id} = user,
%Activity{data: %{"context" => context}} = activity,
activity_id
) do
- data = %{
+ %{
"type" => "Undo",
"actor" => ap_id,
"object" => activity.data,
"to" => [user.follower_address, activity.data["actor"]],
"cc" => [Pleroma.Constants.as_public()],
"context" => context
}
-
- if activity_id, do: Map.put(data, "id", activity_id), else: data
+ |> maybe_put("id", activity_id)
end
def make_unlike_data(
%User{ap_id: ap_id} = user,
%Activity{data: %{"context" => context}} = activity,
activity_id
) do
- data = %{
+ %{
"type" => "Undo",
"actor" => ap_id,
"object" => activity.data,
"to" => [user.follower_address, activity.data["actor"]],
"cc" => [Pleroma.Constants.as_public()],
"context" => context
}
-
- if activity_id, do: Map.put(data, "id", activity_id), else: data
+ |> maybe_put("id", activity_id)
end
def add_announce_to_object(
%Activity{
data: %{"actor" => actor, "cc" => [Pleroma.Constants.as_public()]}
},
object
) do
announcements =
if is_list(object.data["announcements"]), do: object.data["announcements"], else: []
with announcements <- [actor | announcements] |> Enum.uniq() do
update_element_in_object("announcement", announcements, object)
end
end
def add_announce_to_object(_, object), do: {:ok, object}
def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
announcements =
if is_list(object.data["announcements"]), do: object.data["announcements"], else: []
with announcements <- announcements |> List.delete(actor) do
update_element_in_object("announcement", announcements, object)
end
end
#### Unfollow-related helpers
def make_unfollow_data(follower, followed, follow_activity, activity_id) do
- data = %{
+ %{
"type" => "Undo",
"actor" => follower.ap_id,
"to" => [followed.ap_id],
"object" => follow_activity.data
}
-
- if activity_id, do: Map.put(data, "id", activity_id), else: data
+ |> maybe_put("id", activity_id)
end
#### Block-related helpers
def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
query =
from(
activity in Activity,
where:
fragment(
"? ->> 'type' = 'Block'",
activity.data
),
where: activity.actor == ^blocker_id,
# this is to use the index
where:
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
activity.data,
^blocked_id
),
order_by: [fragment("? desc nulls last", activity.id)],
limit: 1
)
Repo.one(query)
end
def make_block_data(blocker, blocked, activity_id) do
- data = %{
+ %{
"type" => "Block",
"actor" => blocker.ap_id,
"to" => [blocked.ap_id],
"object" => blocked.ap_id
}
-
- if activity_id, do: Map.put(data, "id", activity_id), else: data
+ |> maybe_put("id", activity_id)
end
def make_unblock_data(blocker, blocked, block_activity, activity_id) do
- data = %{
+ %{
"type" => "Undo",
"actor" => blocker.ap_id,
"to" => [blocked.ap_id],
"object" => block_activity.data
}
-
- if activity_id, do: Map.put(data, "id", activity_id), else: data
+ |> maybe_put("id", activity_id)
end
#### Create-related helpers
def make_create_data(params, additional) do
published = params.published || make_date()
%{
"type" => "Create",
"to" => params.to |> Enum.uniq(),
"actor" => params.actor.ap_id,
"object" => params.object,
"published" => published,
"context" => params.context
}
|> Map.merge(additional)
end
#### Flag-related helpers
def make_flag_data(params, additional) do
status_ap_ids =
Enum.map(params.statuses || [], fn
%Activity{} = act -> act.data["id"]
act when is_map(act) -> act["id"]
act when is_binary(act) -> act
end)
object = [params.account.ap_id] ++ status_ap_ids
%{
"type" => "Flag",
"actor" => params.actor.ap_id,
"content" => params.content,
"object" => object,
"context" => params.context,
"state" => "open"
}
|> Map.merge(additional)
end
@doc """
Fetches the OrderedCollection/OrderedCollectionPage from `from`, limiting the amount of pages fetched after
the first one to `pages_left` pages.
If the amount of pages is higher than the collection has, it returns whatever was there.
"""
def fetch_ordered_collection(from, pages_left, acc \\ []) do
with {:ok, response} <- Tesla.get(from),
{:ok, collection} <- Jason.decode(response.body) do
case collection["type"] do
"OrderedCollection" ->
# If we've encountered the OrderedCollection and not the page,
# just call the same function on the page address
fetch_ordered_collection(collection["first"], pages_left)
"OrderedCollectionPage" ->
if pages_left > 0 do
# There are still more pages
if Map.has_key?(collection, "next") do
# There are still more pages, go deeper saving what we have into the accumulator
fetch_ordered_collection(
collection["next"],
pages_left - 1,
acc ++ collection["orderedItems"]
)
else
# No more pages left, just return whatever we already have
acc ++ collection["orderedItems"]
end
else
# Got the amount of pages needed, add them all to the accumulator
acc ++ collection["orderedItems"]
end
_ ->
{:error, "Not an OrderedCollection or OrderedCollectionPage"}
end
end
end
#### Report-related helpers
def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
with new_data <- Map.put(activity.data, "state", state),
changeset <- Changeset.change(activity, data: new_data),
{:ok, activity} <- Repo.update(changeset) do
{:ok, activity}
end
end
def update_report_state(_, _), do: {:error, "Unsupported state"}
def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
[to, cc, recipients] =
activity
|> get_updated_targets(visibility)
|> Enum.map(&Enum.uniq/1)
object_data =
activity.object.data
|> Map.put("to", to)
|> Map.put("cc", cc)
{:ok, object} =
activity.object
|> Object.change(%{data: object_data})
|> Object.update_and_set_cache()
activity_data =
activity.data
|> Map.put("to", to)
|> Map.put("cc", cc)
activity
|> Map.put(:object, object)
|> Activity.change(%{data: activity_data, recipients: recipients})
|> Repo.update()
end
def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}
defp get_updated_targets(
%Activity{data: %{"to" => to} = data, recipients: recipients},
visibility
) do
cc = Map.get(data, "cc", [])
follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
public = Pleroma.Constants.as_public()
case visibility do
"public" ->
to = [public | List.delete(to, follower_address)]
cc = [follower_address | List.delete(cc, public)]
recipients = [public | recipients]
[to, cc, recipients]
"private" ->
to = [follower_address | List.delete(to, public)]
cc = List.delete(cc, public)
recipients = List.delete(recipients, public)
[to, cc, recipients]
"unlisted" ->
to = [follower_address | List.delete(to, public)]
cc = [public | List.delete(cc, follower_address)]
recipients = recipients ++ [follower_address, public]
[to, cc, recipients]
_ ->
[to, cc, recipients]
end
end
def get_existing_votes(actor, %{data: %{"id" => id}}) do
query =
from(
[activity, object: object] in Activity.with_preloaded_object(Activity),
where: fragment("(?)->>'type' = 'Create'", activity.data),
where: fragment("(?)->>'actor' = ?", activity.data, ^actor),
where:
fragment(
"(?)->>'inReplyTo' = ?",
object.data,
^to_string(id)
),
where: fragment("(?)->>'type' = 'Answer'", object.data)
)
Repo.all(query)
end
+
+ defp maybe_put(map, _key, nil), do: map
+ defp maybe_put(map, key, value), do: Map.put(map, key, value)
end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index 62d1de717..719115003 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -1,356 +1,358 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Factory do
use ExMachina.Ecto, repo: Pleroma.Repo
alias Pleroma.Object
alias Pleroma.User
def participation_factory do
conversation = insert(:conversation)
user = insert(:user)
%Pleroma.Conversation.Participation{
conversation: conversation,
user: user,
read: false
}
end
def conversation_factory do
%Pleroma.Conversation{
ap_id: sequence(:ap_id, &"https://some_conversation/#{&1}")
}
end
def user_factory do
user = %User{
name: sequence(:name, &"Test テスト User #{&1}"),
email: sequence(:email, &"user#{&1}@example.com"),
nickname: sequence(:nickname, &"nick#{&1}"),
password_hash: Comeonin.Pbkdf2.hashpwsalt("test"),
bio: sequence(:bio, &"Tester Number #{&1}"),
info: %{},
last_digest_emailed_at: NaiveDateTime.utc_now()
}
%{
user
| ap_id: User.ap_id(user),
follower_address: User.ap_followers(user),
following_address: User.ap_following(user),
following: [User.ap_id(user)]
}
end
def note_factory(attrs \\ %{}) do
text = sequence(:text, &"This is :moominmamma: note #{&1}")
user = attrs[:user] || insert(:user)
data = %{
"type" => "Note",
"content" => text,
"id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(),
"actor" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"likes" => [],
"like_count" => 0,
"context" => "2hu",
"summary" => "2hu",
"tag" => ["2hu"],
"emoji" => %{
"2hu" => "corndog.png"
}
}
%Pleroma.Object{
data: merge_attributes(data, Map.get(attrs, :data, %{}))
}
end
def direct_note_factory do
user2 = insert(:user)
%Pleroma.Object{data: data} = note_factory()
%Pleroma.Object{data: Map.merge(data, %{"to" => [user2.ap_id]})}
end
def article_factory do
note_factory()
|> Map.put("type", "Article")
end
def tombstone_factory do
data = %{
"type" => "Tombstone",
"id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(),
"formerType" => "Note",
"deleted" => DateTime.utc_now() |> DateTime.to_iso8601()
}
%Pleroma.Object{
data: data
}
end
def direct_note_activity_factory do
dm = insert(:direct_note)
data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"type" => "Create",
"actor" => dm.data["actor"],
"to" => dm.data["to"],
"object" => dm.data,
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"context" => dm.data["context"]
}
%Pleroma.Activity{
data: data,
actor: data["actor"],
recipients: data["to"]
}
end
def note_activity_factory(attrs \\ %{}) do
user = attrs[:user] || insert(:user)
note = attrs[:note] || insert(:note, user: user)
data_attrs = attrs[:data_attrs] || %{}
attrs = Map.drop(attrs, [:user, :note, :data_attrs])
data =
%{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"type" => "Create",
"actor" => note.data["actor"],
"to" => note.data["to"],
"object" => note.data["id"],
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"context" => note.data["context"]
}
|> Map.merge(data_attrs)
%Pleroma.Activity{
data: data,
actor: data["actor"],
recipients: data["to"]
}
|> Map.merge(attrs)
end
defp expiration_offset_by_minutes(attrs, minutes) do
scheduled_at =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(minutes), :millisecond)
|> NaiveDateTime.truncate(:second)
%Pleroma.ActivityExpiration{}
|> Map.merge(attrs)
|> Map.put(:scheduled_at, scheduled_at)
end
def expiration_in_the_past_factory(attrs \\ %{}) do
expiration_offset_by_minutes(attrs, -60)
end
def expiration_in_the_future_factory(attrs \\ %{}) do
expiration_offset_by_minutes(attrs, 61)
end
def article_activity_factory do
article = insert(:article)
data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"type" => "Create",
"actor" => article.data["actor"],
"to" => article.data["to"],
"object" => article.data,
"published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"context" => article.data["context"]
}
%Pleroma.Activity{
data: data,
actor: data["actor"],
recipients: data["to"]
}
end
def announce_activity_factory(attrs \\ %{}) do
note_activity = attrs[:note_activity] || insert(:note_activity)
user = attrs[:user] || insert(:user)
data = %{
"type" => "Announce",
"actor" => note_activity.actor,
"object" => note_activity.data["id"],
"to" => [user.follower_address, note_activity.data["actor"]],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"context" => note_activity.data["context"]
}
%Pleroma.Activity{
data: data,
actor: user.ap_id,
recipients: data["to"]
}
end
def like_activity_factory(attrs \\ %{}) do
note_activity = attrs[:note_activity] || insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
- data = %{
- "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
- "actor" => user.ap_id,
- "type" => "Like",
- "object" => object.data["id"],
- "published_at" => DateTime.utc_now() |> DateTime.to_iso8601()
- }
+ data =
+ %{
+ "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
+ "actor" => user.ap_id,
+ "type" => "Like",
+ "object" => object.data["id"],
+ "published_at" => DateTime.utc_now() |> DateTime.to_iso8601()
+ }
+ |> Map.merge(attrs[:data_attrs] || %{})
%Pleroma.Activity{
data: data
}
end
def follow_activity_factory do
follower = insert(:user)
followed = insert(:user)
data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"actor" => follower.ap_id,
"type" => "Follow",
"object" => followed.ap_id,
"published_at" => DateTime.utc_now() |> DateTime.to_iso8601()
}
%Pleroma.Activity{
data: data,
actor: follower.ap_id
}
end
def websub_subscription_factory do
%Pleroma.Web.Websub.WebsubServerSubscription{
topic: "http://example.org",
callback: "http://example.org/callback",
secret: "here's a secret",
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 100),
state: "requested"
}
end
def websub_client_subscription_factory do
%Pleroma.Web.Websub.WebsubClientSubscription{
topic: "http://example.org",
secret: "here's a secret",
valid_until: nil,
state: "requested",
subscribers: []
}
end
def oauth_app_factory do
%Pleroma.Web.OAuth.App{
client_name: "Some client",
redirect_uris: "https://example.com/callback",
scopes: ["read", "write", "follow", "push"],
website: "https://example.com",
client_id: Ecto.UUID.generate(),
client_secret: "aaa;/&bbb"
}
end
def instance_factory do
%Pleroma.Instances.Instance{
host: "domain.com",
unreachable_since: nil
}
end
def oauth_token_factory do
oauth_app = insert(:oauth_app)
%Pleroma.Web.OAuth.Token{
token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(),
refresh_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(),
user: build(:user),
app_id: oauth_app.id,
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)
}
end
def oauth_authorization_factory do
%Pleroma.Web.OAuth.Authorization{
token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false),
scopes: ["read", "write", "follow", "push"],
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10),
user: build(:user),
app: build(:oauth_app)
}
end
def push_subscription_factory do
%Pleroma.Web.Push.Subscription{
user: build(:user),
token: build(:oauth_token),
endpoint: "https://example.com/example/1234",
key_auth: "8eDyX_uCN0XRhSbY5hs7Hg==",
key_p256dh:
"BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA=",
data: %{}
}
end
def notification_factory do
%Pleroma.Notification{
user: build(:user)
}
end
def scheduled_activity_factory do
%Pleroma.ScheduledActivity{
user: build(:user),
scheduled_at: NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(60), :millisecond),
params: build(:note) |> Map.from_struct() |> Map.get(:data)
}
end
def registration_factory do
user = insert(:user)
%Pleroma.Registration{
user: user,
provider: "twitter",
uid: "171799000",
info: %{
"name" => "John Doe",
"email" => "john@doe.com",
"nickname" => "johndoe",
"description" => "My bio"
}
}
end
def config_factory do
%Pleroma.Web.AdminAPI.Config{
key: sequence(:key, &"some_key_#{&1}"),
group: "pleroma",
value:
sequence(
:value,
fn key ->
:erlang.term_to_binary(%{another_key: "#{key}somevalue", another: "#{key}somevalue"})
end
)
}
end
end
diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs
index 1515f4eb6..f72b44aed 100644
--- a/test/web/activity_pub/activity_pub_test.exs
+++ b/test/web/activity_pub/activity_pub_test.exs
@@ -1,1209 +1,1253 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Builders.ActivityBuilder
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
import Tesla.Mock
import Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
+ clear_config([:instance, :federating])
+
describe "streaming out participations" do
test "it streams them out" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
{:ok, conversation} = Pleroma.Conversation.create_or_bump_for(activity)
participations =
conversation.participations
|> Repo.preload(:user)
with_mock Pleroma.Web.Streamer,
stream: fn _, _ -> nil end do
ActivityPub.stream_out_participations(conversation.participations)
Enum.each(participations, fn participation ->
assert called(Pleroma.Web.Streamer.stream("participation", participation))
end)
end
end
end
describe "fetching restricted by visibility" do
test "it restricts by the appropriate visibility" do
user = insert(:user)
{:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
{:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
{:ok, unlisted_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"})
{:ok, private_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
activities =
ActivityPub.fetch_activities([], %{:visibility => "direct", "actor_id" => user.ap_id})
assert activities == [direct_activity]
activities =
ActivityPub.fetch_activities([], %{:visibility => "unlisted", "actor_id" => user.ap_id})
assert activities == [unlisted_activity]
activities =
ActivityPub.fetch_activities([], %{:visibility => "private", "actor_id" => user.ap_id})
assert activities == [private_activity]
activities =
ActivityPub.fetch_activities([], %{:visibility => "public", "actor_id" => user.ap_id})
assert activities == [public_activity]
activities =
ActivityPub.fetch_activities([], %{
:visibility => ~w[private public],
"actor_id" => user.ap_id
})
assert activities == [public_activity, private_activity]
end
end
describe "building a user from his ap id" do
test "it returns a user" do
user_id = "http://mastodon.example.org/users/admin"
{:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
assert user.ap_id == user_id
assert user.nickname == "admin@mastodon.example.org"
assert user.info.source_data
assert user.info.ap_enabled
assert user.follower_address == "http://mastodon.example.org/users/admin/followers"
end
test "it fetches the appropriate tag-restricted posts" do
user = insert(:user)
{:ok, status_one} = CommonAPI.post(user, %{"status" => ". #test"})
{:ok, status_two} = CommonAPI.post(user, %{"status" => ". #essais"})
{:ok, status_three} = CommonAPI.post(user, %{"status" => ". #test #reject"})
fetch_one = ActivityPub.fetch_activities([], %{"type" => "Create", "tag" => "test"})
fetch_two =
ActivityPub.fetch_activities([], %{"type" => "Create", "tag" => ["test", "essais"]})
fetch_three =
ActivityPub.fetch_activities([], %{
"type" => "Create",
"tag" => ["test", "essais"],
"tag_reject" => ["reject"]
})
fetch_four =
ActivityPub.fetch_activities([], %{
"type" => "Create",
"tag" => ["test"],
"tag_all" => ["test", "reject"]
})
assert fetch_one == [status_one, status_three]
assert fetch_two == [status_one, status_two, status_three]
assert fetch_three == [status_one, status_two]
assert fetch_four == [status_three]
end
end
describe "insertion" do
test "drops activities beyond a certain limit" do
limit = Pleroma.Config.get([:instance, :remote_limit])
random_text =
:crypto.strong_rand_bytes(limit + 1)
|> Base.encode64()
|> binary_part(0, limit + 1)
data = %{
"ok" => true,
"object" => %{
"content" => random_text
}
}
assert {:error, {:remote_limit_error, _}} = ActivityPub.insert(data)
end
test "doesn't drop activities with content being null" do
user = insert(:user)
data = %{
"actor" => user.ap_id,
"to" => [],
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => nil
}
}
assert {:ok, _} = ActivityPub.insert(data)
end
test "returns the activity if one with the same id is already in" do
activity = insert(:note_activity)
{:ok, new_activity} = ActivityPub.insert(activity.data)
assert activity.id == new_activity.id
end
test "inserts a given map into the activity database, giving it an id if it has none." do
user = insert(:user)
data = %{
"actor" => user.ap_id,
"to" => [],
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => "hey"
}
}
{:ok, %Activity{} = activity} = ActivityPub.insert(data)
assert activity.data["ok"] == data["ok"]
assert is_binary(activity.data["id"])
given_id = "bla"
data = %{
"id" => given_id,
"actor" => user.ap_id,
"to" => [],
"context" => "blabla",
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => "hey"
}
}
{:ok, %Activity{} = activity} = ActivityPub.insert(data)
assert activity.data["ok"] == data["ok"]
assert activity.data["id"] == given_id
assert activity.data["context"] == "blabla"
assert activity.data["context_id"]
end
test "adds a context when none is there" do
user = insert(:user)
data = %{
"actor" => user.ap_id,
"to" => [],
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => "hey"
}
}
{:ok, %Activity{} = activity} = ActivityPub.insert(data)
object = Pleroma.Object.normalize(activity)
assert is_binary(activity.data["context"])
assert is_binary(object.data["context"])
assert activity.data["context_id"]
assert object.data["context_id"]
end
test "adds an id to a given object if it lacks one and is a note and inserts it to the object database" do
user = insert(:user)
data = %{
"actor" => user.ap_id,
"to" => [],
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => "hey"
}
}
{:ok, %Activity{} = activity} = ActivityPub.insert(data)
assert object = Object.normalize(activity)
assert is_binary(object.data["id"])
end
end
describe "create activities" do
test "removes doubled 'to' recipients" do
user = insert(:user)
{:ok, activity} =
ActivityPub.create(%{
to: ["user1", "user1", "user2"],
actor: user,
context: "",
object: %{
"to" => ["user1", "user1", "user2"],
"type" => "Note",
"content" => "testing"
}
})
assert activity.data["to"] == ["user1", "user2"]
assert activity.actor == user.ap_id
assert activity.recipients == ["user1", "user2", user.ap_id]
end
test "increases user note count only for public activities" do
user = insert(:user)
{:ok, _} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "1",
"visibility" => "public"
})
{:ok, _} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "2",
"visibility" => "unlisted"
})
{:ok, _} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "2",
"visibility" => "private"
})
{:ok, _} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "3",
"visibility" => "direct"
})
user = User.get_cached_by_id(user.id)
assert user.info.note_count == 2
end
test "increases replies count" do
user = insert(:user)
user2 = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "1", "visibility" => "public"})
ap_id = activity.data["id"]
reply_data = %{"status" => "1", "in_reply_to_status_id" => activity.id}
# public
{:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "public"))
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 1
# unlisted
{:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "unlisted"))
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
# private
{:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "private"))
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
# direct
{:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "direct"))
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
end
end
describe "fetch activities for recipients" do
test "retrieve the activities for certain recipients" do
{:ok, activity_one} = ActivityBuilder.insert(%{"to" => ["someone"]})
{:ok, activity_two} = ActivityBuilder.insert(%{"to" => ["someone_else"]})
{:ok, _activity_three} = ActivityBuilder.insert(%{"to" => ["noone"]})
activities = ActivityPub.fetch_activities(["someone", "someone_else"])
assert length(activities) == 2
assert activities == [activity_one, activity_two]
end
end
describe "fetch activities in context" do
test "retrieves activities that have a given context" do
{:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
{:ok, activity_two} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
{:ok, _activity_three} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"})
{:ok, _activity_four} = ActivityBuilder.insert(%{"type" => "Announce", "context" => "2hu"})
activity_five = insert(:note_activity)
user = insert(:user)
{:ok, user} = User.block(user, %{ap_id: activity_five.data["actor"]})
activities = ActivityPub.fetch_activities_for_context("2hu", %{"blocking_user" => user})
assert activities == [activity_two, activity]
end
end
test "doesn't return blocked activities" do
activity_one = insert(:note_activity)
activity_two = insert(:note_activity)
activity_three = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, user} = User.block(user, %{ap_id: activity_one.data["actor"]})
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
refute Enum.member?(activities, activity_one)
{:ok, user} = User.unblock(user, %{ap_id: activity_one.data["actor"]})
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
{:ok, user} = User.block(user, %{ap_id: activity_three.data["actor"]})
{:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster)
%Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id)
activity_three = Activity.get_by_id(activity_three.id)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
refute Enum.member?(activities, activity_three)
refute Enum.member?(activities, boost_activity)
assert Enum.member?(activities, activity_one)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => nil, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, boost_activity)
assert Enum.member?(activities, activity_one)
end
test "doesn't return transitive interactions concerning blocked users" do
blocker = insert(:user)
blockee = insert(:user)
friend = insert(:user)
{:ok, blocker} = User.block(blocker, blockee)
{:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"})
{:ok, activity_two} = CommonAPI.post(friend, %{"status" => "hey! @#{blockee.nickname}"})
{:ok, activity_three} = CommonAPI.post(blockee, %{"status" => "hey! @#{friend.nickname}"})
{:ok, activity_four} = CommonAPI.post(blockee, %{"status" => "hey! @#{blocker.nickname}"})
activities = ActivityPub.fetch_activities([], %{"blocking_user" => blocker})
assert Enum.member?(activities, activity_one)
refute Enum.member?(activities, activity_two)
refute Enum.member?(activities, activity_three)
refute Enum.member?(activities, activity_four)
end
test "doesn't return announce activities concerning blocked users" do
blocker = insert(:user)
blockee = insert(:user)
friend = insert(:user)
{:ok, blocker} = User.block(blocker, blockee)
{:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"})
{:ok, activity_two} = CommonAPI.post(blockee, %{"status" => "hey! @#{friend.nickname}"})
{:ok, activity_three, _} = CommonAPI.repeat(activity_two.id, friend)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => blocker})
|> Enum.map(fn act -> act.id end)
assert Enum.member?(activities, activity_one.id)
refute Enum.member?(activities, activity_two.id)
refute Enum.member?(activities, activity_three.id)
end
test "doesn't return activities from blocked domains" do
domain = "dogwhistle.zone"
domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"})
note = insert(:note, %{data: %{"actor" => domain_user.ap_id}})
activity = insert(:note_activity, %{note: note})
user = insert(:user)
{:ok, user} = User.block_domain(user, domain)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
refute activity in activities
followed_user = insert(:user)
ActivityPub.follow(user, followed_user)
{:ok, repeat_activity, _} = CommonAPI.repeat(activity.id, followed_user)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
refute repeat_activity in activities
end
test "doesn't return muted activities" do
activity_one = insert(:note_activity)
activity_two = insert(:note_activity)
activity_three = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, user} = User.mute(user, %User{ap_id: activity_one.data["actor"]})
activities =
ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
refute Enum.member?(activities, activity_one)
# Calling with 'with_muted' will deliver muted activities, too.
activities =
ActivityPub.fetch_activities([], %{
"muting_user" => user,
"with_muted" => true,
"skip_preload" => true
})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
{:ok, user} = User.unmute(user, %User{ap_id: activity_one.data["actor"]})
activities =
ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
{:ok, user} = User.mute(user, %User{ap_id: activity_three.data["actor"]})
{:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster)
%Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id)
activity_three = Activity.get_by_id(activity_three.id)
activities =
ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
refute Enum.member?(activities, activity_three)
refute Enum.member?(activities, boost_activity)
assert Enum.member?(activities, activity_one)
activities = ActivityPub.fetch_activities([], %{"muting_user" => nil, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, boost_activity)
assert Enum.member?(activities, activity_one)
end
test "doesn't return thread muted activities" do
user = insert(:user)
_activity_one = insert(:note_activity)
note_two = insert(:note, data: %{"context" => "suya.."})
activity_two = insert(:note_activity, note: note_two)
{:ok, _activity_two} = CommonAPI.add_mute(user, activity_two)
assert [_activity_one] = ActivityPub.fetch_activities([], %{"muting_user" => user})
end
test "returns thread muted activities when with_muted is set" do
user = insert(:user)
_activity_one = insert(:note_activity)
note_two = insert(:note, data: %{"context" => "suya.."})
activity_two = insert(:note_activity, note: note_two)
{:ok, _activity_two} = CommonAPI.add_mute(user, activity_two)
assert [_activity_two, _activity_one] =
ActivityPub.fetch_activities([], %{"muting_user" => user, "with_muted" => true})
end
test "does include announces on request" do
activity_three = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, user} = User.follow(user, booster)
{:ok, announce, _object} = CommonAPI.repeat(activity_three.id, booster)
[announce_activity] = ActivityPub.fetch_activities([user.ap_id | user.following])
assert announce_activity.id == announce.id
end
test "excludes reblogs on request" do
user = insert(:user)
{:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user})
{:ok, _} = ActivityBuilder.insert(%{"type" => "Announce"}, %{:user => user})
[activity] = ActivityPub.fetch_user_activities(user, nil, %{"exclude_reblogs" => "true"})
assert activity == expected_activity
end
describe "public fetch activities" do
test "doesn't retrieve unlisted activities" do
user = insert(:user)
{:ok, _unlisted_activity} =
CommonAPI.post(user, %{"status" => "yeah", "visibility" => "unlisted"})
{:ok, listed_activity} = CommonAPI.post(user, %{"status" => "yeah"})
[activity] = ActivityPub.fetch_public_activities()
assert activity == listed_activity
end
test "retrieves public activities" do
_activities = ActivityPub.fetch_public_activities()
%{public: public} = ActivityBuilder.public_and_non_public()
activities = ActivityPub.fetch_public_activities()
assert length(activities) == 1
assert Enum.at(activities, 0) == public
end
test "retrieves a maximum of 20 activities" do
activities = ActivityBuilder.insert_list(30)
last_expected = List.last(activities)
activities = ActivityPub.fetch_public_activities()
last = List.last(activities)
assert length(activities) == 20
assert last == last_expected
end
test "retrieves ids starting from a since_id" do
activities = ActivityBuilder.insert_list(30)
later_activities = ActivityBuilder.insert_list(10)
since_id = List.last(activities).id
last_expected = List.last(later_activities)
activities = ActivityPub.fetch_public_activities(%{"since_id" => since_id})
last = List.last(activities)
assert length(activities) == 10
assert last == last_expected
end
test "retrieves ids up to max_id" do
_first_activities = ActivityBuilder.insert_list(10)
activities = ActivityBuilder.insert_list(20)
later_activities = ActivityBuilder.insert_list(10)
max_id = List.first(later_activities).id
last_expected = List.last(activities)
activities = ActivityPub.fetch_public_activities(%{"max_id" => max_id})
last = List.last(activities)
assert length(activities) == 20
assert last == last_expected
end
test "doesn't return reblogs for users for whom reblogs have been muted" do
activity = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, user} = CommonAPI.hide_reblogs(user, booster)
{:ok, activity, _} = CommonAPI.repeat(activity.id, booster)
activities = ActivityPub.fetch_activities([], %{"muting_user" => user})
refute Enum.any?(activities, fn %{id: id} -> id == activity.id end)
end
test "returns reblogs for users for whom reblogs have not been muted" do
activity = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, user} = CommonAPI.hide_reblogs(user, booster)
{:ok, user} = CommonAPI.show_reblogs(user, booster)
{:ok, activity, _} = CommonAPI.repeat(activity.id, booster)
activities = ActivityPub.fetch_activities([], %{"muting_user" => user})
assert Enum.any?(activities, fn %{id: id} -> id == activity.id end)
end
end
describe "like an object" do
+ test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
+ Pleroma.Config.put([:instance, :federating], true)
+ note_activity = insert(:note_activity)
+ assert object_activity = Object.normalize(note_activity)
+
+ user = insert(:user)
+
+ {:ok, like_activity, _object} = ActivityPub.like(user, object_activity)
+ assert called(Pleroma.Web.Federator.publish(like_activity, 5))
+ end
+
+ test "returns exist activity if object already liked" do
+ note_activity = insert(:note_activity)
+ assert object_activity = Object.normalize(note_activity)
+
+ user = insert(:user)
+
+ {:ok, like_activity, _object} = ActivityPub.like(user, object_activity)
+
+ {:ok, like_activity_exist, _object} = ActivityPub.like(user, object_activity)
+ assert like_activity == like_activity_exist
+ end
+
test "adds a like activity to the db" do
note_activity = insert(:note_activity)
assert object = Object.normalize(note_activity)
user = insert(:user)
user_two = insert(:user)
{:ok, like_activity, object} = ActivityPub.like(user, object)
assert like_activity.data["actor"] == user.ap_id
assert like_activity.data["type"] == "Like"
assert like_activity.data["object"] == object.data["id"]
assert like_activity.data["to"] == [User.ap_followers(user), note_activity.data["actor"]]
assert like_activity.data["context"] == object.data["context"]
assert object.data["like_count"] == 1
assert object.data["likes"] == [user.ap_id]
# Just return the original activity if the user already liked it.
{:ok, same_like_activity, object} = ActivityPub.like(user, object)
assert like_activity == same_like_activity
assert object.data["likes"] == [user.ap_id]
assert object.data["like_count"] == 1
{:ok, _like_activity, object} = ActivityPub.like(user_two, object)
assert object.data["like_count"] == 2
end
end
describe "unliking" do
+ test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do
+ Pleroma.Config.put([:instance, :federating], true)
+
+ note_activity = insert(:note_activity)
+ object = Object.normalize(note_activity)
+ user = insert(:user)
+
+ {:ok, object} = ActivityPub.unlike(user, object)
+ refute called(Pleroma.Web.Federator.publish())
+
+ {:ok, _like_activity, object} = ActivityPub.like(user, object)
+ assert object.data["like_count"] == 1
+
+ {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
+ assert object.data["like_count"] == 0
+
+ assert called(Pleroma.Web.Federator.publish(unlike_activity, 5))
+ end
+
test "unliking a previously liked object" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
# Unliking something that hasn't been liked does nothing
{:ok, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0
{:ok, like_activity, object} = ActivityPub.like(user, object)
assert object.data["like_count"] == 1
{:ok, _, _, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0
assert Activity.get_by_id(like_activity.id) == nil
end
end
describe "announcing an object" do
test "adds an announce activity to the db" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
{:ok, announce_activity, object} = ActivityPub.announce(user, object)
assert object.data["announcement_count"] == 1
assert object.data["announcements"] == [user.ap_id]
assert announce_activity.data["to"] == [
User.ap_followers(user),
note_activity.data["actor"]
]
assert announce_activity.data["object"] == object.data["id"]
assert announce_activity.data["actor"] == user.ap_id
assert announce_activity.data["context"] == object.data["context"]
end
end
describe "unannouncing an object" do
test "unannouncing a previously announced object" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
# Unannouncing an object that is not announced does nothing
# {:ok, object} = ActivityPub.unannounce(user, object)
# assert object.data["announcement_count"] == 0
{:ok, announce_activity, object} = ActivityPub.announce(user, object)
assert object.data["announcement_count"] == 1
{:ok, unannounce_activity, object} = ActivityPub.unannounce(user, object)
assert object.data["announcement_count"] == 0
assert unannounce_activity.data["to"] == [
User.ap_followers(user),
announce_activity.data["actor"]
]
assert unannounce_activity.data["type"] == "Undo"
assert unannounce_activity.data["object"] == announce_activity.data
assert unannounce_activity.data["actor"] == user.ap_id
assert unannounce_activity.data["context"] == announce_activity.data["context"]
assert Activity.get_by_id(announce_activity.id) == nil
end
end
describe "uploading files" do
test "copies the file to the configured folder" do
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, %Object{} = object} = ActivityPub.upload(file)
assert object.data["name"] == "an_image.jpg"
end
test "works with base64 encoded images" do
file = %{
"img" => data_uri()
}
{:ok, %Object{}} = ActivityPub.upload(file)
end
end
describe "fetch the latest Follow" do
test "fetches the latest Follow activity" do
%Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity)
follower = Repo.get_by(User, ap_id: activity.data["actor"])
followed = Repo.get_by(User, ap_id: activity.data["object"])
assert activity == Utils.fetch_latest_follow(follower, followed)
end
end
describe "following / unfollowing" do
test "creates a follow activity" do
follower = insert(:user)
followed = insert(:user)
{:ok, activity} = ActivityPub.follow(follower, followed)
assert activity.data["type"] == "Follow"
assert activity.data["actor"] == follower.ap_id
assert activity.data["object"] == followed.ap_id
end
test "creates an undo activity for the last follow" do
follower = insert(:user)
followed = insert(:user)
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
{:ok, activity} = ActivityPub.unfollow(follower, followed)
assert activity.data["type"] == "Undo"
assert activity.data["actor"] == follower.ap_id
embedded_object = activity.data["object"]
assert is_map(embedded_object)
assert embedded_object["type"] == "Follow"
assert embedded_object["object"] == followed.ap_id
assert embedded_object["id"] == follow_activity.data["id"]
end
end
describe "blocking / unblocking" do
test "creates a block activity" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, activity} = ActivityPub.block(blocker, blocked)
assert activity.data["type"] == "Block"
assert activity.data["actor"] == blocker.ap_id
assert activity.data["object"] == blocked.ap_id
end
test "creates an undo activity for the last block" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, block_activity} = ActivityPub.block(blocker, blocked)
{:ok, activity} = ActivityPub.unblock(blocker, blocked)
assert activity.data["type"] == "Undo"
assert activity.data["actor"] == blocker.ap_id
embedded_object = activity.data["object"]
assert is_map(embedded_object)
assert embedded_object["type"] == "Block"
assert embedded_object["object"] == blocked.ap_id
assert embedded_object["id"] == block_activity.data["id"]
end
end
describe "deletion" do
test "it creates a delete activity and deletes the original object" do
note = insert(:note_activity)
object = Object.normalize(note)
{:ok, delete} = ActivityPub.delete(object)
assert delete.data["type"] == "Delete"
assert delete.data["actor"] == note.data["actor"]
assert delete.data["object"] == object.data["id"]
assert Activity.get_by_id(delete.id) != nil
assert Repo.get(Object, object.id).data["type"] == "Tombstone"
end
test "decrements user note count only for public activities" do
user = insert(:user, info: %{note_count: 10})
{:ok, a1} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "public"
})
{:ok, a2} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "unlisted"
})
{:ok, a3} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "private"
})
{:ok, a4} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "direct"
})
{:ok, _} = Object.normalize(a1) |> ActivityPub.delete()
{:ok, _} = Object.normalize(a2) |> ActivityPub.delete()
{:ok, _} = Object.normalize(a3) |> ActivityPub.delete()
{:ok, _} = Object.normalize(a4) |> ActivityPub.delete()
user = User.get_cached_by_id(user.id)
assert user.info.note_count == 10
end
test "it creates a delete activity and checks that it is also sent to users mentioned by the deleted object" do
user = insert(:user)
note = insert(:note_activity)
object = Object.normalize(note)
{:ok, object} =
object
|> Object.change(%{
data: %{
"actor" => object.data["actor"],
"id" => object.data["id"],
"to" => [user.ap_id],
"type" => "Note"
}
})
|> Object.update_and_set_cache()
{:ok, delete} = ActivityPub.delete(object)
assert user.ap_id in delete.data["to"]
end
test "decreases reply count" do
user = insert(:user)
user2 = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "1", "visibility" => "public"})
reply_data = %{"status" => "1", "in_reply_to_status_id" => activity.id}
ap_id = activity.data["id"]
{:ok, public_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "public"))
{:ok, unlisted_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "unlisted"))
{:ok, private_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "private"))
{:ok, direct_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "direct"))
_ = CommonAPI.delete(direct_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
_ = CommonAPI.delete(private_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
_ = CommonAPI.delete(public_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 1
_ = CommonAPI.delete(unlisted_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 0
end
end
describe "timeline post-processing" do
test "it filters broken threads" do
user1 = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
{:ok, user1} = User.follow(user1, user3)
assert User.following?(user1, user3)
{:ok, user2} = User.follow(user2, user3)
assert User.following?(user2, user3)
{:ok, user3} = User.follow(user3, user2)
assert User.following?(user3, user2)
{:ok, public_activity} = CommonAPI.post(user3, %{"status" => "hi 1"})
{:ok, private_activity_1} =
CommonAPI.post(user3, %{"status" => "hi 2", "visibility" => "private"})
{:ok, private_activity_2} =
CommonAPI.post(user2, %{
"status" => "hi 3",
"visibility" => "private",
"in_reply_to_status_id" => private_activity_1.id
})
{:ok, private_activity_3} =
CommonAPI.post(user3, %{
"status" => "hi 4",
"visibility" => "private",
"in_reply_to_status_id" => private_activity_2.id
})
activities =
ActivityPub.fetch_activities([user1.ap_id | user1.following])
|> Enum.map(fn a -> a.id end)
private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"])
assert [public_activity.id, private_activity_1.id, private_activity_3.id] == activities
assert length(activities) == 3
activities =
ActivityPub.fetch_activities([user1.ap_id | user1.following], %{"user" => user1})
|> Enum.map(fn a -> a.id end)
assert [public_activity.id, private_activity_1.id] == activities
assert length(activities) == 2
end
end
describe "update" do
test "it creates an update activity with the new user data" do
user = insert(:user)
{:ok, user} = User.ensure_keys_present(user)
user_data = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
{:ok, update} =
ActivityPub.update(%{
actor: user_data["id"],
to: [user.follower_address],
cc: [],
object: user_data
})
assert update.data["actor"] == user.ap_id
assert update.data["to"] == [user.follower_address]
assert embedded_object = update.data["object"]
assert embedded_object["id"] == user_data["id"]
assert embedded_object["type"] == user_data["type"]
end
end
test "returned pinned statuses" do
Pleroma.Config.put([:instance, :max_pinned_statuses], 3)
user = insert(:user)
{:ok, activity_one} = CommonAPI.post(user, %{"status" => "HI!!!"})
{:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
{:ok, activity_three} = CommonAPI.post(user, %{"status" => "HI!!!"})
CommonAPI.pin(activity_one.id, user)
user = refresh_record(user)
CommonAPI.pin(activity_two.id, user)
user = refresh_record(user)
CommonAPI.pin(activity_three.id, user)
user = refresh_record(user)
activities = ActivityPub.fetch_user_activities(user, nil, %{"pinned" => "true"})
assert 3 = length(activities)
end
test "it can create a Flag activity" do
reporter = insert(:user)
target_account = insert(:user)
{:ok, activity} = CommonAPI.post(target_account, %{"status" => "foobar"})
context = Utils.generate_context_id()
content = "foobar"
reporter_ap_id = reporter.ap_id
target_ap_id = target_account.ap_id
activity_ap_id = activity.data["id"]
assert {:ok, activity} =
ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [activity],
content: content
})
assert %Activity{
actor: ^reporter_ap_id,
data: %{
"type" => "Flag",
"content" => ^content,
"context" => ^context,
"object" => [^target_ap_id, ^activity_ap_id]
}
} = activity
end
test "fetch_activities/2 returns activities addressed to a list " do
user = insert(:user)
member = insert(:user)
{:ok, list} = Pleroma.List.create("foo", user)
{:ok, list} = Pleroma.List.follow(list, member)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
activity = Repo.preload(activity, :bookmark)
activity = %Activity{activity | thread_muted?: !!activity.thread_muted?}
assert ActivityPub.fetch_activities([], %{"user" => user}) == [activity]
end
def data_uri do
File.read!("test/fixtures/avatar_data_uri")
end
describe "fetch_activities_bounded" do
test "fetches private posts for followed users" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "thought I looked cute might delete later :3",
"visibility" => "private"
})
[result] = ActivityPub.fetch_activities_bounded([user.follower_address], [])
assert result.id == activity.id
end
test "fetches only public posts for other users" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe", "visibility" => "public"})
{:ok, _private_activity} =
CommonAPI.post(user, %{
"status" => "why is tenshi eating a corndog so cute?",
"visibility" => "private"
})
[result] = ActivityPub.fetch_activities_bounded([], [user.follower_address])
assert result.id == activity.id
end
end
describe "fetch_follow_information_for_user" do
test "syncronizes following/followers counters" do
user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/fuser2/followers",
following_address: "http://localhost:4001/users/fuser2/following"
)
{:ok, info} = ActivityPub.fetch_follow_information_for_user(user)
assert info.follower_count == 527
assert info.following_count == 267
end
test "detects hidden followers" do
mock(fn env ->
case env.url do
"http://localhost:4001/users/masto_closed/followers?page=1" ->
%Tesla.Env{status: 403, body: ""}
_ ->
apply(HttpRequestMock, :request, [env])
end
end)
user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
following_address: "http://localhost:4001/users/masto_closed/following"
)
{:ok, info} = ActivityPub.fetch_follow_information_for_user(user)
assert info.hide_followers == true
assert info.hide_follows == false
end
test "detects hidden follows" do
mock(fn env ->
case env.url do
"http://localhost:4001/users/masto_closed/following?page=1" ->
%Tesla.Env{status: 403, body: ""}
_ ->
apply(HttpRequestMock, :request, [env])
end
end)
user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
following_address: "http://localhost:4001/users/masto_closed/following"
)
{:ok, info} = ActivityPub.fetch_follow_information_for_user(user)
assert info.hide_followers == false
assert info.hide_follows == true
end
end
end
diff --git a/test/web/activity_pub/utils_test.exs b/test/web/activity_pub/utils_test.exs
index ca5f057a7..eb429b2c4 100644
--- a/test/web/activity_pub/utils_test.exs
+++ b/test/web/activity_pub/utils_test.exs
@@ -1,302 +1,404 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.UtilsTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
+ require Pleroma.Constants
+
describe "fetch the latest Follow" do
test "fetches the latest Follow activity" do
%Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity)
follower = User.get_cached_by_ap_id(activity.data["actor"])
followed = User.get_cached_by_ap_id(activity.data["object"])
assert activity == Utils.fetch_latest_follow(follower, followed)
end
end
describe "fetch the latest Block" do
test "fetches the latest Block activity" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, activity} = ActivityPub.block(blocker, blocked)
assert activity == Utils.fetch_latest_block(blocker, blocked)
end
end
describe "determine_explicit_mentions()" do
test "works with an object that has mentions" do
object = %{
"tag" => [
%{
"type" => "Mention",
"href" => "https://example.com/~alyssa",
"name" => "Alyssa P. Hacker"
}
]
}
assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"]
end
test "works with an object that does not have mentions" do
object = %{
"tag" => [
%{"type" => "Hashtag", "href" => "https://example.com/tag/2hu", "name" => "2hu"}
]
}
assert Utils.determine_explicit_mentions(object) == []
end
test "works with an object that has mentions and other tags" do
object = %{
"tag" => [
%{
"type" => "Mention",
"href" => "https://example.com/~alyssa",
"name" => "Alyssa P. Hacker"
},
%{"type" => "Hashtag", "href" => "https://example.com/tag/2hu", "name" => "2hu"}
]
}
assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"]
end
test "works with an object that has no tags" do
object = %{}
assert Utils.determine_explicit_mentions(object) == []
end
test "works with an object that has only IR tags" do
object = %{"tag" => ["2hu"]}
assert Utils.determine_explicit_mentions(object) == []
end
end
+ describe "make_unlike_data/3" do
+ test "returns data for unlike activity" do
+ user = insert(:user)
+ like_activity = insert(:like_activity, data_attrs: %{"context" => "test context"})
+
+ assert Utils.make_unlike_data(user, like_activity, nil) == %{
+ "type" => "Undo",
+ "actor" => user.ap_id,
+ "object" => like_activity.data,
+ "to" => [user.follower_address, like_activity.data["actor"]],
+ "cc" => [Pleroma.Constants.as_public()],
+ "context" => like_activity.data["context"]
+ }
+
+ assert Utils.make_unlike_data(user, like_activity, "9mJEZK0tky1w2xD2vY") == %{
+ "type" => "Undo",
+ "actor" => user.ap_id,
+ "object" => like_activity.data,
+ "to" => [user.follower_address, like_activity.data["actor"]],
+ "cc" => [Pleroma.Constants.as_public()],
+ "context" => like_activity.data["context"],
+ "id" => "9mJEZK0tky1w2xD2vY"
+ }
+ end
+ end
+
describe "make_like_data" do
setup do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
[user: user, other_user: other_user, third_user: third_user]
end
test "addresses actor's follower address if the activity is public", %{
user: user,
other_user: other_user,
third_user: third_user
} do
expected_to = Enum.sort([user.ap_id, other_user.follower_address])
expected_cc = Enum.sort(["https://www.w3.org/ns/activitystreams#Public", third_user.ap_id])
{:ok, activity} =
CommonAPI.post(user, %{
"status" =>
"hey @#{other_user.nickname}, @#{third_user.nickname} how about beering together this weekend?"
})
%{"to" => to, "cc" => cc} = Utils.make_like_data(other_user, activity, nil)
assert Enum.sort(to) == expected_to
assert Enum.sort(cc) == expected_cc
end
test "does not adress actor's follower address if the activity is not public", %{
user: user,
other_user: other_user,
third_user: third_user
} do
expected_to = Enum.sort([user.ap_id])
expected_cc = [third_user.ap_id]
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "@#{other_user.nickname} @#{third_user.nickname} bought a new swimsuit!",
"visibility" => "private"
})
%{"to" => to, "cc" => cc} = Utils.make_like_data(other_user, activity, nil)
assert Enum.sort(to) == expected_to
assert Enum.sort(cc) == expected_cc
end
end
describe "fetch_ordered_collection" do
import Tesla.Mock
test "fetches the first OrderedCollectionPage when an OrderedCollection is encountered" do
mock(fn
%{method: :get, url: "http://mastodon.com/outbox"} ->
json(%{"type" => "OrderedCollection", "first" => "http://mastodon.com/outbox?page=true"})
%{method: :get, url: "http://mastodon.com/outbox?page=true"} ->
json(%{"type" => "OrderedCollectionPage", "orderedItems" => ["ok"]})
end)
assert Utils.fetch_ordered_collection("http://mastodon.com/outbox", 1) == ["ok"]
end
test "fetches several pages in the right order one after another, but only the specified amount" do
mock(fn
%{method: :get, url: "http://example.com/outbox"} ->
json(%{
"type" => "OrderedCollectionPage",
"orderedItems" => [0],
"next" => "http://example.com/outbox?page=1"
})
%{method: :get, url: "http://example.com/outbox?page=1"} ->
json(%{
"type" => "OrderedCollectionPage",
"orderedItems" => [1],
"next" => "http://example.com/outbox?page=2"
})
%{method: :get, url: "http://example.com/outbox?page=2"} ->
json(%{"type" => "OrderedCollectionPage", "orderedItems" => [2]})
end)
assert Utils.fetch_ordered_collection("http://example.com/outbox", 0) == [0]
assert Utils.fetch_ordered_collection("http://example.com/outbox", 1) == [0, 1]
end
test "returns an error if the url doesn't have an OrderedCollection/Page" do
mock(fn
%{method: :get, url: "http://example.com/not-an-outbox"} ->
json(%{"type" => "NotAnOutbox"})
end)
assert {:error, _} = Utils.fetch_ordered_collection("http://example.com/not-an-outbox", 1)
end
test "returns the what was collected if there are less pages than specified" do
mock(fn
%{method: :get, url: "http://example.com/outbox"} ->
json(%{
"type" => "OrderedCollectionPage",
"orderedItems" => [0],
"next" => "http://example.com/outbox?page=1"
})
%{method: :get, url: "http://example.com/outbox?page=1"} ->
json(%{"type" => "OrderedCollectionPage", "orderedItems" => [1]})
end)
assert Utils.fetch_ordered_collection("http://example.com/outbox", 5) == [0, 1]
end
end
test "make_json_ld_header/0" do
assert Utils.make_json_ld_header() == %{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"http://localhost:4001/schemas/litepub-0.1.jsonld",
%{
"@language" => "und"
}
]
}
end
describe "get_existing_votes" do
test "fetches existing votes" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "How do I pronounce LaTeX?",
"poll" => %{
"options" => ["laytekh", "lahtekh", "latex"],
"expires_in" => 20,
"multiple" => true
}
})
object = Object.normalize(activity)
{:ok, votes, object} = CommonAPI.vote(other_user, object, [0, 1])
assert Enum.sort(Utils.get_existing_votes(other_user.ap_id, object)) == Enum.sort(votes)
end
test "fetches only Create activities" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "Are we living in a society?",
"poll" => %{
"options" => ["yes", "no"],
"expires_in" => 20
}
})
object = Object.normalize(activity)
{:ok, [vote], object} = CommonAPI.vote(other_user, object, [0])
vote_object = Object.normalize(vote)
{:ok, _activity, _object} = ActivityPub.like(user, vote_object)
[fetched_vote] = Utils.get_existing_votes(other_user.ap_id, object)
assert fetched_vote.id == vote.id
end
end
describe "update_follow_state_for_all/2" do
test "updates the state of all Follow activities with the same actor and object" do
user = insert(:user, info: %{locked: true})
follower = insert(:user)
{:ok, follow_activity} = ActivityPub.follow(follower, user)
{:ok, follow_activity_two} = ActivityPub.follow(follower, user)
data =
follow_activity_two.data
|> Map.put("state", "accept")
cng = Ecto.Changeset.change(follow_activity_two, data: data)
{:ok, follow_activity_two} = Repo.update(cng)
{:ok, follow_activity_two} =
Utils.update_follow_state_for_all(follow_activity_two, "accept")
assert Repo.get(Activity, follow_activity.id).data["state"] == "accept"
assert Repo.get(Activity, follow_activity_two.id).data["state"] == "accept"
end
end
describe "update_follow_state/2" do
test "updates the state of the given follow activity" do
user = insert(:user, info: %{locked: true})
follower = insert(:user)
{:ok, follow_activity} = ActivityPub.follow(follower, user)
{:ok, follow_activity_two} = ActivityPub.follow(follower, user)
data =
follow_activity_two.data
|> Map.put("state", "accept")
cng = Ecto.Changeset.change(follow_activity_two, data: data)
{:ok, follow_activity_two} = Repo.update(cng)
{:ok, follow_activity_two} = Utils.update_follow_state(follow_activity_two, "reject")
assert Repo.get(Activity, follow_activity.id).data["state"] == "pending"
assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject"
end
end
+
+ describe "update_element_in_object/3" do
+ test "updates likes" do
+ user = insert(:user)
+ activity = insert(:note_activity)
+ object = Object.normalize(activity)
+
+ assert {:ok, updated_object} =
+ Utils.update_element_in_object(
+ "like",
+ [user.ap_id],
+ object
+ )
+
+ assert updated_object.data["likes"] == [user.ap_id]
+ assert updated_object.data["like_count"] == 1
+ end
+ end
+
+ describe "add_like_to_object/2" do
+ test "add actor to likes" do
+ user = insert(:user)
+ user2 = insert(:user)
+ object = insert(:note)
+
+ assert {:ok, updated_object} =
+ Utils.add_like_to_object(
+ %Activity{data: %{"actor" => user.ap_id}},
+ object
+ )
+
+ assert updated_object.data["likes"] == [user.ap_id]
+ assert updated_object.data["like_count"] == 1
+
+ assert {:ok, updated_object2} =
+ Utils.add_like_to_object(
+ %Activity{data: %{"actor" => user2.ap_id}},
+ updated_object
+ )
+
+ assert updated_object2.data["likes"] == [user2.ap_id, user.ap_id]
+ assert updated_object2.data["like_count"] == 2
+ end
+ end
+
+ describe "remove_like_from_object/2" do
+ test "removes ap_id from likes" do
+ user = insert(:user)
+ user2 = insert(:user)
+ object = insert(:note, data: %{"likes" => [user.ap_id, user2.ap_id], "like_count" => 2})
+
+ assert {:ok, updated_object} =
+ Utils.remove_like_from_object(
+ %Activity{data: %{"actor" => user.ap_id}},
+ object
+ )
+
+ assert updated_object.data["likes"] == [user2.ap_id]
+ assert updated_object.data["like_count"] == 1
+ end
+ end
+
+ describe "get_existing_like/2" do
+ test "fetches existing like" do
+ note_activity = insert(:note_activity)
+ assert object = Object.normalize(note_activity)
+
+ user = insert(:user)
+ refute Utils.get_existing_like(user.ap_id, object)
+ {:ok, like_activity, _object} = ActivityPub.like(user, object)
+
+ assert ^like_activity = Utils.get_existing_like(user.ap_id, object)
+ end
+ end
end

File Metadata

Mime Type
text/x-diff
Expires
Wed, May 14, 7:29 AM (1 d, 15 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
166749
Default Alt Text
(148 KB)

Event Timeline