Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
124 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index e4edbd5ee..e2fc2640d 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -1,955 +1,961 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 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.Config
alias Pleroma.EctoType.ActivityPub.ObjectValidators.ObjectID
alias Pleroma.Maps
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router.Helpers
import Ecto.Query
require Logger
require Pleroma.Constants
@supported_object_types [
"Article",
"Note",
"Event",
"Video",
"Page",
"Question",
"Answer",
"Audio",
"Image"
]
@strip_status_report_states ~w(closed resolved)
@supported_report_states ~w(open closed resolved)
@valid_visibilities ~w(public unlisted private direct)
def as_local_public, do: Endpoint.url() <> "/#Public"
# 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
@spec determine_explicit_mentions(map()) :: [any]
def determine_explicit_mentions(%{"tag" => tag}) when is_list(tag) do
Enum.flat_map(tag, fn
%{"type" => "Mention", "href" => href} -> [href]
_ -> []
end)
end
def determine_explicit_mentions(%{"tag" => tag} = object) when is_map(tag) do
object
|> Map.put("tag", [tag])
|> determine_explicit_mentions()
end
def determine_explicit_mentions(_), do: []
@spec label_in_collection?(any(), any()) :: boolean()
defp label_in_collection?(ap_id, coll) when is_binary(coll), do: ap_id == coll
defp label_in_collection?(ap_id, coll) when is_list(coll), do: ap_id in coll
defp label_in_collection?(_, _), do: false
@spec label_in_message?(String.t(), map()) :: boolean()
def label_in_message?(label, params),
do:
[params["to"], params["cc"], params["bto"], params["bcc"]]
|> Enum.any?(&label_in_collection?(label, &1))
@spec unaddressed_message?(map()) :: boolean()
def unaddressed_message?(params),
do:
[params["to"], params["cc"], params["bto"], params["bcc"]]
|> Enum.all?(&is_nil(&1))
@spec recipient_in_message(User.t(), User.t(), map()) :: boolean()
def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params),
do:
label_in_message?(ap_id, params) || unaddressed_message?(params) ||
User.following?(recipient, actor)
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? =
!label_in_collection?(ap_id, params["to"]) &&
!label_in_collection?(ap_id, params["cc"])
if need_splice? do
cc = [ap_id | extract_list(params["cc"])]
params
|> Map.put("cc", cc)
|> Maps.safe_put_in(["object", "cc"], cc)
else
params
end
end
def make_json_ld_header do
%{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"#{Endpoint.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
"#{Endpoint.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
}
get_notified_from_object(fake_create_activity)
end
def get_notified_from_object(object) do
Notification.get_notified_from_activity(%Activity{data: object}, false)
end
def maybe_create_context(context), do: context || generate_id("contexts")
@doc """
Enqueues an activity for federation if it's local
"""
@spec maybe_federate(any()) :: :ok
def maybe_federate(%Activity{local: true, data: %{"type" => type}} = activity) do
outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
with true <- Config.get!([:instance, :federating]),
true <- type != "Block" || outgoing_blocks,
false <- Visibility.is_local_public?(activity) do
Pleroma.Web.Federator.publish(activity)
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
"""
@spec lazy_put_activity_defaults(map(), boolean) :: map()
def lazy_put_activity_defaults(map, fake? \\ false)
def lazy_put_activity_defaults(map, true) do
map
|> Map.put_new("id", "pleroma:fakeid")
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", "pleroma:fakecontext")
|> lazy_put_object_defaults(true)
end
def lazy_put_activity_defaults(map, _fake?) do
context = maybe_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)
|> lazy_put_object_defaults(false)
end
# Adds an id and published date if they aren't there.
#
@spec lazy_put_object_defaults(map(), boolean()) :: map()
defp lazy_put_object_defaults(%{"object" => map} = activity, true)
when is_map(map) do
object =
map
|> Map.put_new("id", "pleroma:fake_object_id")
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", activity["context"])
|> Map.put_new("fake", true)
%{activity | "object" => object}
end
defp lazy_put_object_defaults(%{"object" => map} = activity, _)
when is_map(map) do
object =
map
|> Map.put_new_lazy("id", &generate_object_id/0)
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", activity["context"])
%{activity | "object" => object}
end
defp lazy_put_object_defaults(activity, _), do: activity
@doc """
Inserts a full object if it is contained in an activity.
"""
def insert_full_object(%{"object" => %{"type" => type} = object_data} = map)
when type in @supported_object_types do
with {:ok, object} <- Object.create(object_data) do
map = Map.put(map, "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
actor
|> Activity.Queries.by_actor()
|> Activity.Queries.by_object_id(id)
|> Activity.Queries.by_type("Like")
|> limit(1)
|> Repo.one()
end
@doc """
Returns like activities targeting an object
"""
def get_object_likes(%{data: %{"id" => id}}) do
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)
%{
"type" => "Like",
"actor" => ap_id,
"object" => id,
"to" => to,
"cc" => cc,
"context" => object.data["context"]
}
|> Maps.put_if_present("id", activity_id)
end
def make_emoji_reaction_data(user, object, emoji, activity_id) do
make_like_data(user, object, activity_id)
|> Map.put("type", "EmojiReact")
|> Map.put("content", emoji)
end
@spec update_element_in_object(String.t(), list(any), Object.t(), integer() | nil) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def update_element_in_object(property, element, object, count \\ nil) do
length =
count ||
length(element)
data =
Map.merge(
object.data,
%{"#{property}_count" => length, "#{property}s" => element}
)
object
|> Changeset.change(data: data)
|> Object.update_and_set_cache()
end
@spec add_emoji_reaction_to_object(Activity.t(), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def add_emoji_reaction_to_object(
%Activity{data: %{"content" => emoji, "actor" => actor}} = activity,
object
) do
reactions = get_cached_emoji_reactions(object)
emoji = Pleroma.Emoji.maybe_strip_name(emoji)
url = maybe_emoji_url(emoji, activity)
new_reactions =
case Enum.find_index(reactions, fn [candidate, _, candidate_url] ->
if is_nil(candidate_url) do
emoji == candidate
else
url == candidate_url
end
end) do
nil ->
reactions ++ [[emoji, [actor], url]]
index ->
List.update_at(
reactions,
index,
fn [emoji, users, url] -> [emoji, Enum.uniq([actor | users]), url] end
)
end
count = emoji_count(new_reactions)
update_element_in_object("reaction", new_reactions, object, count)
end
defp maybe_emoji_url(
name,
%Activity{
data: %{
"tag" => [
%{"type" => "Emoji", "name" => name, "icon" => %{"url" => url}}
]
}
}
),
do: url
defp maybe_emoji_url(_, _), do: nil
def emoji_count(reactions_list) do
Enum.reduce(reactions_list, 0, fn [_, users, _], acc -> acc + length(users) end)
end
def remove_emoji_reaction_from_object(
%Activity{data: %{"content" => emoji, "actor" => actor}} = activity,
object
) do
emoji = Pleroma.Emoji.maybe_strip_name(emoji)
reactions = get_cached_emoji_reactions(object)
url = maybe_emoji_url(emoji, activity)
new_reactions =
case Enum.find_index(reactions, fn [candidate, _, candidate_url] ->
if is_nil(candidate_url) do
emoji == candidate
else
url == candidate_url
end
end) do
nil ->
reactions
index ->
List.update_at(
reactions,
index,
fn [emoji, users, url] -> [emoji, List.delete(users, actor), url] end
)
|> Enum.reject(fn [_, users, _] -> Enum.empty?(users) end)
end
count = emoji_count(new_reactions)
update_element_in_object("reaction", new_reactions, object, count)
end
def get_cached_emoji_reactions(object) do
Object.get_emoji_reactions(object)
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
[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
object
|> fetch_likes()
|> List.delete(actor)
|> update_likes_in_object(object)
end
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).
"""
@spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity | nil}
def update_follow_state_for_all(
%Activity{data: %{"actor" => actor, "object" => object}} = activity,
state
) do
"Follow"
|> Activity.Queries.by_type()
|> Activity.Queries.by_actor(actor)
|> Activity.Queries.by_object_id(object)
|> where(fragment("data->>'state' = 'pending'") or fragment("data->>'state' = 'accept'"))
|> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
|> Repo.update_all([])
activity = Activity.get_by_id(activity.id)
{:ok, activity}
end
def update_follow_state(
%Activity{} = activity,
state
) do
new_data = Map.put(activity.data, "state", state)
changeset = Changeset.change(activity, data: new_data)
with {:ok, activity} <- Repo.update(changeset) 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
%{
"type" => "Follow",
"actor" => follower_id,
"to" => [followed_id],
"cc" => [Pleroma.Constants.as_public()],
"object" => followed_id,
"state" => "pending"
}
|> Maps.put_if_present("id", activity_id)
end
def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
"Follow"
|> Activity.Queries.by_type()
|> where(actor: ^follower_id)
# this is to use the index
|> Activity.Queries.by_object_id(followed_id)
|> order_by([activity], fragment("? desc nulls last", activity.id))
|> limit(1)
|> Repo.one()
end
def fetch_latest_undo(%User{ap_id: ap_id}) do
"Undo"
|> Activity.Queries.by_type()
|> where(actor: ^ap_id)
|> order_by([activity], fragment("? desc nulls last", activity.id))
|> limit(1)
|> Repo.one()
end
def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do
%{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id)
emoji = Pleroma.Emoji.maybe_quote(emoji)
"EmojiReact"
|> Activity.Queries.by_type()
|> where(actor: ^ap_id)
|> custom_emoji_discriminator(emoji)
|> Activity.Queries.by_object_id(object_ap_id)
|> order_by([activity], fragment("? desc nulls last", activity.id))
|> limit(1)
|> Repo.one()
end
defp custom_emoji_discriminator(query, emoji) do
if String.contains?(emoji, "@") do
stripped = Pleroma.Emoji.maybe_strip_name(emoji)
[name, domain] = String.split(stripped, "@")
domain_pattern = "%/" <> domain <> "/%"
emoji_pattern = Pleroma.Emoji.maybe_quote(name)
query
|> where([activity], fragment("?->>'content' = ?
AND EXISTS (
SELECT FROM jsonb_array_elements(?->'tag') elem
WHERE elem->>'id' ILIKE ?
)", activity.data, ^emoji_pattern, activity.data, ^domain_pattern))
else
query
|> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji))
end
end
#### Announce-related helpers
@doc """
Returns an existing announce activity if the notice has already been announced
"""
@spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do
"Announce"
|> Activity.Queries.by_type()
|> where(actor: ^actor)
# this is to use the index
|> Activity.Queries.by_object_id(ap_id)
|> Repo.one()
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
%{
"type" => "Announce",
"actor" => ap_id,
"object" => id,
"to" => [user.follower_address],
"cc" => [],
"context" => object.data["context"]
}
|> Maps.put_if_present("id", activity_id)
end
def make_announce_data(
%User{ap_id: ap_id} = user,
%Object{data: %{"id" => id}} = object,
activity_id,
true
) do
%{
"type" => "Announce",
"actor" => ap_id,
"object" => id,
"to" => [user.follower_address, object.data["actor"]],
"cc" => [Pleroma.Constants.as_public()],
"context" => object.data["context"]
}
|> Maps.put_if_present("id", activity_id)
end
def make_undo_data(
%User{ap_id: actor, follower_address: follower_address},
%Activity{
data: %{"id" => undone_activity_id, "context" => context},
actor: undone_activity_actor
},
activity_id \\ nil
) do
%{
"type" => "Undo",
"actor" => actor,
"object" => undone_activity_id,
"to" => [follower_address, undone_activity_actor],
"cc" => [Pleroma.Constants.as_public()],
"context" => context
}
|> Maps.put_if_present("id", activity_id)
end
@spec add_announce_to_object(Activity.t(), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def add_announce_to_object(
%Activity{data: %{"actor" => actor}},
object
) do
unless actor |> User.get_cached_by_ap_id() |> User.invisible?() do
announcements = take_announcements(object)
with announcements <- Enum.uniq([actor | announcements]) do
update_element_in_object("announcement", announcements, object)
end
else
{:ok, object}
end
end
def add_announce_to_object(_, object), do: {:ok, object}
@spec remove_announce_from_object(Activity.t(), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
with announcements <- List.delete(take_announcements(object), actor) do
update_element_in_object("announcement", announcements, object)
end
end
defp take_announcements(%{data: %{"announcements" => announcements}} = _)
when is_list(announcements),
do: announcements
defp take_announcements(_), do: []
#### Unfollow-related helpers
def make_unfollow_data(follower, followed, follow_activity, activity_id) do
%{
"type" => "Undo",
"actor" => follower.ap_id,
"to" => [followed.ap_id],
"object" => follow_activity.data
}
|> Maps.put_if_present("id", activity_id)
end
#### Block-related helpers
@spec fetch_latest_block(User.t(), User.t()) :: Activity.t() | nil
def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
"Block"
|> Activity.Queries.by_type()
|> where(actor: ^blocker_id)
# this is to use the index
|> Activity.Queries.by_object_id(blocked_id)
|> order_by([activity], fragment("? desc nulls last", activity.id))
|> limit(1)
|> Repo.one()
end
def make_block_data(blocker, blocked, activity_id) do
%{
"type" => "Block",
"actor" => blocker.ap_id,
"to" => [blocked.ap_id],
"object" => blocked.ap_id
}
|> Maps.put_if_present("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
#### Listen-related helpers
def make_listen_data(params, additional) do
published = params.published || make_date()
%{
"type" => "Listen",
"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
@spec make_flag_data(map(), map()) :: map()
def make_flag_data(%{actor: actor, context: context, content: content} = params, additional) do
%{
"type" => "Flag",
"actor" => actor.ap_id,
"content" => content,
"object" => build_flag_object(params),
"context" => context,
"state" => "open"
}
|> Map.merge(additional)
end
def make_flag_data(_, _), do: %{}
defp build_flag_object(%{account: account, statuses: statuses}) do
[account.ap_id | build_flag_object(%{statuses: statuses})]
end
defp build_flag_object(%{statuses: statuses}) do
Enum.map(statuses || [], &build_flag_object/1)
end
defp build_flag_object(%Activity{} = activity) do
object = Object.normalize(activity, fetch: false)
# Do not allow people to report Creates. Instead, report the Object that is Created.
if activity.data["type"] != "Create" do
build_flag_object_with_actor_and_id(
object,
User.get_by_ap_id(activity.data["actor"]),
activity.data["id"]
)
else
build_flag_object(object)
end
end
defp build_flag_object(%Object{} = object) do
actor = User.get_by_ap_id(object.data["actor"])
build_flag_object_with_actor_and_id(object, actor, object.data["id"])
end
defp build_flag_object(act) when is_map(act) or is_binary(act) do
id =
case act do
%Activity{} = act -> act.data["id"]
act when is_map(act) -> act["id"]
act when is_binary(act) -> act
end
case Activity.get_by_ap_id_with_object(id) do
%Activity{object: object} = _ ->
build_flag_object(object)
nil ->
if %Object{} = object = Object.get_by_ap_id(id) do
build_flag_object(object)
else
%{"id" => id, "deleted" => true}
end
end
end
defp build_flag_object(_), do: []
defp build_flag_object_with_actor_and_id(%Object{data: data}, actor, id) do
%{
"type" => "Note",
"id" => id,
"content" => data["content"],
"published" => data["published"],
"actor" =>
AccountView.render(
"show.json",
%{user: actor, skip_visibility_check: true}
)
}
end
#### Report-related helpers
def get_reports(params, page, page_size) do
params =
params
|> Map.put(:type, "Flag")
|> Map.put(:skip_preload, true)
|> Map.put(:preload_report_notes, true)
|> Map.put(:total, true)
|> Map.put(:limit, page_size)
|> Map.put(:offset, (page - 1) * page_size)
ActivityPub.fetch_activities([], params, :offset)
end
defp maybe_strip_report_status(data, state) do
with true <- Config.get([:instance, :report_strip_status]),
true <- state in @strip_status_report_states,
{:ok, stripped_activity} = strip_report_status_data(%Activity{data: data}) do
data |> Map.put("object", stripped_activity.data["object"])
else
_ -> data
end
end
def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
new_data =
activity.data
|> Map.put("state", state)
|> maybe_strip_report_status(state)
activity
|> Changeset.change(data: new_data)
|> Repo.update()
end
def update_report_state(activity_ids, state) when state in @supported_report_states do
activities_num = length(activity_ids)
from(a in Activity, where: a.id in ^activity_ids)
|> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
|> Repo.update_all([])
|> case do
{^activities_num, _} -> :ok
_ -> {:error, activity_ids}
end
end
def update_report_state(_, _), do: {:error, "Unsupported state"}
def strip_report_status_data(activity) do
[actor | reported_activities] = activity.data["object"]
stripped_activities =
Enum.reduce(reported_activities, [], fn act, acc ->
case ObjectID.cast(act) do
{:ok, act} -> [act | acc]
_ -> acc
end
end)
new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
{:ok, %{activity | data: new_data}}
end
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
actor
|> Activity.Queries.by_actor()
|> Activity.Queries.by_type("Create")
|> Activity.with_preloaded_object()
|> where([a, object: o], fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(id)))
|> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
|> Repo.all()
end
def maybe_handle_group_posts(activity) do
+ poster = User.get_cached_by_ap_id(activity.actor)
+
mentions =
activity.data["to"]
|> Enum.filter(&(&1 != activity.actor))
mentioned_local_groups =
User.get_all_by_ap_id(mentions)
- |> Enum.filter(&(&1.actor_type == "Group" and &1.local))
+ |> Enum.filter(fn user ->
+ user.actor_type == "Group" and
+ user.local and
+ not User.blocks?(user, poster)
+ end)
mentioned_local_groups
|> Enum.each(fn group ->
Pleroma.Web.CommonAPI.repeat(activity.id, group)
end)
:ok
end
end
diff --git a/test/pleroma/web/activity_pub/side_effects_test.exs b/test/pleroma/web/activity_pub/side_effects_test.exs
index 2b1d414b9..94cc80b76 100644
--- a/test/pleroma/web/activity_pub/side_effects_test.exs
+++ b/test/pleroma/web/activity_pub/side_effects_test.exs
@@ -1,988 +1,1007 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Chat
alias Pleroma.Chat.MessageReference
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.SideEffects
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.ActivityDraft
import Mock
import Pleroma.Factory
defp get_announces_of_object(%{data: %{"id" => id}} = _object) do
Pleroma.Activity.Queries.by_type("Announce")
|> Pleroma.Activity.Queries.by_object_id(id)
|> Pleroma.Repo.all()
end
describe "handle_after_transaction" do
test "it streams out notifications and streams" do
author = insert(:user, local: true)
recipient = insert(:user, local: true)
{:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
{:ok, create_activity_data, _meta} =
Builder.create(author, chat_message_data["id"], [recipient.ap_id])
{:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
{:ok, _create_activity, meta} =
SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
assert [notification] = meta[:notifications]
with_mocks([
{
Pleroma.Web.Streamer,
[],
[
stream: fn _, _ -> nil end
]
},
{
Pleroma.Web.Push,
[],
[
send: fn _ -> nil end
]
}
]) do
SideEffects.handle_after_transaction(meta)
assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification))
assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_))
assert called(Pleroma.Web.Push.send(notification))
end
end
end
describe "blocking users" do
setup do
user = insert(:user)
blocked = insert(:user)
User.follow(blocked, user)
User.follow(user, blocked)
{:ok, block_data, []} = Builder.block(user, blocked)
{:ok, block, _meta} = ActivityPub.persist(block_data, local: true)
%{user: user, blocked: blocked, block: block}
end
test "it unfollows and blocks", %{user: user, blocked: blocked, block: block} do
assert User.following?(user, blocked)
assert User.following?(blocked, user)
{:ok, _, _} = SideEffects.handle(block)
refute User.following?(user, blocked)
refute User.following?(blocked, user)
assert User.blocks?(user, blocked)
end
test "it updates following relationship", %{user: user, blocked: blocked, block: block} do
{:ok, _, _} = SideEffects.handle(block)
refute Pleroma.FollowingRelationship.get(user, blocked)
assert User.get_follow_state(user, blocked) == nil
assert User.get_follow_state(blocked, user) == nil
assert User.get_follow_state(user, blocked, nil) == nil
assert User.get_follow_state(blocked, user, nil) == nil
end
test "it blocks but does not unfollow if the relevant setting is set", %{
user: user,
blocked: blocked,
block: block
} do
clear_config([:activitypub, :unfollow_blocked], false)
assert User.following?(user, blocked)
assert User.following?(blocked, user)
{:ok, _, _} = SideEffects.handle(block)
refute User.following?(user, blocked)
assert User.following?(blocked, user)
assert User.blocks?(user, blocked)
end
end
describe "update users" do
setup do
user = insert(:user, local: false)
{:ok, update_data, []} =
Builder.update(user, %{"id" => user.ap_id, "type" => "Person", "name" => "new name!"})
{:ok, update, _meta} = ActivityPub.persist(update_data, local: true)
%{user: user, update_data: update_data, update: update}
end
test "it updates the user", %{user: user, update: update} do
{:ok, _, _} = SideEffects.handle(update)
user = User.get_by_id(user.id)
assert user.name == "new name!"
end
test "it uses a given changeset to update", %{user: user, update: update} do
changeset = Ecto.Changeset.change(user, %{default_scope: "direct"})
assert user.default_scope == "public"
{:ok, _, _} = SideEffects.handle(update, user_update_changeset: changeset)
user = User.get_by_id(user.id)
assert user.default_scope == "direct"
end
end
describe "update notes" do
setup do
make_time = fn ->
Pleroma.Web.ActivityPub.Utils.make_date()
end
user = insert(:user)
note = insert(:note, user: user, data: %{"published" => make_time.()})
_note_activity = insert(:note_activity, note: note)
updated_note =
note.data
|> Map.put("summary", "edited summary")
|> Map.put("content", "edited content")
|> Map.put("updated", make_time.())
{:ok, update_data, []} = Builder.update(user, updated_note)
{:ok, update, _meta} = ActivityPub.persist(update_data, local: true)
%{
user: user,
note: note,
object_id: note.id,
update_data: update_data,
update: update,
updated_note: updated_note
}
end
test "it updates the note", %{
object_id: object_id,
update: update,
updated_note: updated_note
} do
{:ok, _, _} = SideEffects.handle(update, object_data: updated_note)
updated_time = updated_note["updated"]
new_note = Pleroma.Object.get_by_id(object_id)
assert %{
"summary" => "edited summary",
"content" => "edited content",
"updated" => ^updated_time
} = new_note.data
end
test "it rejects updates with no updated attribute in object", %{
object_id: object_id,
update: update,
updated_note: updated_note
} do
old_note = Pleroma.Object.get_by_id(object_id)
updated_note = Map.drop(updated_note, ["updated"])
{:ok, _, _} = SideEffects.handle(update, object_data: updated_note)
new_note = Pleroma.Object.get_by_id(object_id)
assert old_note.data == new_note.data
end
test "it rejects updates with updated attribute older than what we have in the original object",
%{
object_id: object_id,
update: update,
updated_note: updated_note
} do
old_note = Pleroma.Object.get_by_id(object_id)
{:ok, creation_time, _} = DateTime.from_iso8601(old_note.data["published"])
updated_note =
Map.put(updated_note, "updated", DateTime.to_iso8601(DateTime.add(creation_time, -10)))
{:ok, _, _} = SideEffects.handle(update, object_data: updated_note)
new_note = Pleroma.Object.get_by_id(object_id)
assert old_note.data == new_note.data
end
test "it rejects updates with updated attribute older than the last Update", %{
object_id: object_id,
update: update,
updated_note: updated_note
} do
old_note = Pleroma.Object.get_by_id(object_id)
{:ok, creation_time, _} = DateTime.from_iso8601(old_note.data["published"])
updated_note =
Map.put(updated_note, "updated", DateTime.to_iso8601(DateTime.add(creation_time, +10)))
{:ok, _, _} = SideEffects.handle(update, object_data: updated_note)
old_note = Pleroma.Object.get_by_id(object_id)
{:ok, update_time, _} = DateTime.from_iso8601(old_note.data["updated"])
updated_note =
Map.put(updated_note, "updated", DateTime.to_iso8601(DateTime.add(update_time, -5)))
{:ok, _, _} = SideEffects.handle(update, object_data: updated_note)
new_note = Pleroma.Object.get_by_id(object_id)
assert old_note.data == new_note.data
end
test "it updates using object_data", %{
object_id: object_id,
update: update,
updated_note: updated_note
} do
updated_note = Map.put(updated_note, "summary", "mew mew")
{:ok, _, _} = SideEffects.handle(update, object_data: updated_note)
new_note = Pleroma.Object.get_by_id(object_id)
assert %{"summary" => "mew mew", "content" => "edited content"} = new_note.data
end
test "it records the original note in formerRepresentations", %{
note: note,
object_id: object_id,
update: update,
updated_note: updated_note
} do
{:ok, _, _} = SideEffects.handle(update, object_data: updated_note)
%{data: new_note} = Pleroma.Object.get_by_id(object_id)
assert %{"summary" => "edited summary", "content" => "edited content"} = new_note
assert [Map.drop(note.data, ["id", "formerRepresentations"])] ==
new_note["formerRepresentations"]["orderedItems"]
assert new_note["formerRepresentations"]["totalItems"] == 1
end
test "it puts the original note at the front of formerRepresentations", %{
user: user,
note: note,
object_id: object_id,
update: update,
updated_note: updated_note
} do
{:ok, _, _} = SideEffects.handle(update, object_data: updated_note)
%{data: first_edit} = Pleroma.Object.get_by_id(object_id)
second_updated_note =
note.data
|> Map.put("summary", "edited summary 2")
|> Map.put("content", "edited content 2")
|> Map.put(
"updated",
first_edit["updated"]
|> DateTime.from_iso8601()
|> elem(1)
|> DateTime.add(10)
|> DateTime.to_iso8601()
)
{:ok, second_update_data, []} = Builder.update(user, second_updated_note)
{:ok, update, _meta} = ActivityPub.persist(second_update_data, local: true)
{:ok, _, _} = SideEffects.handle(update, object_data: second_updated_note)
%{data: new_note} = Pleroma.Object.get_by_id(object_id)
assert %{"summary" => "edited summary 2", "content" => "edited content 2"} = new_note
original_version = Map.drop(note.data, ["id", "formerRepresentations"])
first_edit = Map.drop(first_edit, ["id", "formerRepresentations"])
assert [first_edit, original_version] ==
new_note["formerRepresentations"]["orderedItems"]
assert new_note["formerRepresentations"]["totalItems"] == 2
end
test "it does not prepend to formerRepresentations if no actual changes are made", %{
note: note,
object_id: object_id,
update: update,
updated_note: updated_note
} do
{:ok, _, _} = SideEffects.handle(update, object_data: updated_note)
%{data: first_edit} = Pleroma.Object.get_by_id(object_id)
updated_note =
updated_note
|> Map.put(
"updated",
first_edit["updated"]
|> DateTime.from_iso8601()
|> elem(1)
|> DateTime.add(10)
|> DateTime.to_iso8601()
)
{:ok, _, _} = SideEffects.handle(update, object_data: updated_note)
%{data: new_note} = Pleroma.Object.get_by_id(object_id)
assert %{"summary" => "edited summary", "content" => "edited content"} = new_note
original_version = Map.drop(note.data, ["id", "formerRepresentations"])
assert [original_version] ==
new_note["formerRepresentations"]["orderedItems"]
assert new_note["formerRepresentations"]["totalItems"] == 1
end
end
describe "update questions" do
setup do
user = insert(:user)
question =
insert(:question,
user: user,
data: %{"published" => Pleroma.Web.ActivityPub.Utils.make_date()}
)
%{user: user, data: question.data, id: question.id}
end
test "allows updating choice count without generating edit history", %{
user: user,
data: data,
id: id
} do
new_choices =
data["oneOf"]
|> Enum.map(fn choice -> put_in(choice, ["replies", "totalItems"], 5) end)
updated_question =
data
|> Map.put("oneOf", new_choices)
|> Map.put("updated", Pleroma.Web.ActivityPub.Utils.make_date())
{:ok, update_data, []} = Builder.update(user, updated_question)
{:ok, update, _meta} = ActivityPub.persist(update_data, local: true)
{:ok, _, _} = SideEffects.handle(update, object_data: updated_question)
%{data: new_question} = Pleroma.Object.get_by_id(id)
assert [%{"replies" => %{"totalItems" => 5}}, %{"replies" => %{"totalItems" => 5}}] =
new_question["oneOf"]
refute Map.has_key?(new_question, "formerRepresentations")
end
test "allows updating choice count without updated field", %{
user: user,
data: data,
id: id
} do
new_choices =
data["oneOf"]
|> Enum.map(fn choice -> put_in(choice, ["replies", "totalItems"], 5) end)
updated_question =
data
|> Map.put("oneOf", new_choices)
{:ok, update_data, []} = Builder.update(user, updated_question)
{:ok, update, _meta} = ActivityPub.persist(update_data, local: true)
{:ok, _, _} = SideEffects.handle(update, object_data: updated_question)
%{data: new_question} = Pleroma.Object.get_by_id(id)
assert [%{"replies" => %{"totalItems" => 5}}, %{"replies" => %{"totalItems" => 5}}] =
new_question["oneOf"]
refute Map.has_key?(new_question, "formerRepresentations")
end
test "allows updating choice count with updated field same as the creation date", %{
user: user,
data: data,
id: id
} do
new_choices =
data["oneOf"]
|> Enum.map(fn choice -> put_in(choice, ["replies", "totalItems"], 5) end)
updated_question =
data
|> Map.put("oneOf", new_choices)
|> Map.put("updated", data["published"])
{:ok, update_data, []} = Builder.update(user, updated_question)
{:ok, update, _meta} = ActivityPub.persist(update_data, local: true)
{:ok, _, _} = SideEffects.handle(update, object_data: updated_question)
%{data: new_question} = Pleroma.Object.get_by_id(id)
assert [%{"replies" => %{"totalItems" => 5}}, %{"replies" => %{"totalItems" => 5}}] =
new_question["oneOf"]
refute Map.has_key?(new_question, "formerRepresentations")
end
end
describe "EmojiReact objects" do
setup do
poster = insert(:user)
user = insert(:user)
{:ok, post} = CommonAPI.post(poster, %{status: "hey"})
{:ok, emoji_react_data, []} = Builder.emoji_react(user, post.object, "👌")
{:ok, emoji_react, _meta} = ActivityPub.persist(emoji_react_data, local: true)
%{emoji_react: emoji_react, user: user, poster: poster}
end
test "adds the reaction to the object", %{emoji_react: emoji_react, user: user} do
{:ok, emoji_react, _} = SideEffects.handle(emoji_react)
object = Object.get_by_ap_id(emoji_react.data["object"])
assert object.data["reaction_count"] == 1
assert ["👌", [user.ap_id], nil] in object.data["reactions"]
end
test "creates a notification", %{emoji_react: emoji_react, poster: poster} do
{:ok, emoji_react, _} = SideEffects.handle(emoji_react)
assert Repo.get_by(Notification, user_id: poster.id, activity_id: emoji_react.id)
end
end
describe "Question objects" do
setup do
user = insert(:user)
question = build(:question, user: user)
question_activity = build(:question_activity, question: question)
activity_data = Map.put(question_activity.data, "object", question.data["id"])
meta = [object_data: question.data, local: false]
{:ok, activity, meta} = ActivityPub.persist(activity_data, meta)
%{activity: activity, meta: meta}
end
test "enqueues the poll end", %{activity: activity, meta: meta} do
{:ok, activity, meta} = SideEffects.handle(activity, meta)
assert_enqueued(
worker: Pleroma.Workers.PollWorker,
args: %{op: "poll_end", activity_id: activity.id},
scheduled_at: NaiveDateTime.from_iso8601!(meta[:object_data]["closed"])
)
end
end
describe "delete users with confirmation pending" do
setup do
user = insert(:user, is_confirmed: false)
{:ok, delete_user_data, _meta} = Builder.delete(user, user.ap_id)
{:ok, delete_user, _meta} = ActivityPub.persist(delete_user_data, local: true)
{:ok, delete: delete_user, user: user}
end
test "when activation is required", %{delete: delete, user: user} do
clear_config([:instance, :account_activation_required], true)
{:ok, _, _} = SideEffects.handle(delete)
ObanHelpers.perform_all()
refute User.get_cached_by_id(user.id)
end
end
describe "Undo objects" do
setup do
poster = insert(:user)
user = insert(:user)
{:ok, post} = CommonAPI.post(poster, %{status: "hey"})
{:ok, like} = CommonAPI.favorite(user, post.id)
{:ok, reaction} = CommonAPI.react_with_emoji(post.id, user, "👍")
{:ok, announce} = CommonAPI.repeat(post.id, user)
{:ok, block} = CommonAPI.block(user, poster)
{:ok, undo_data, _meta} = Builder.undo(user, like)
{:ok, like_undo, _meta} = ActivityPub.persist(undo_data, local: true)
{:ok, undo_data, _meta} = Builder.undo(user, reaction)
{:ok, reaction_undo, _meta} = ActivityPub.persist(undo_data, local: true)
{:ok, undo_data, _meta} = Builder.undo(user, announce)
{:ok, announce_undo, _meta} = ActivityPub.persist(undo_data, local: true)
{:ok, undo_data, _meta} = Builder.undo(user, block)
{:ok, block_undo, _meta} = ActivityPub.persist(undo_data, local: true)
%{
like_undo: like_undo,
post: post,
like: like,
reaction_undo: reaction_undo,
reaction: reaction,
announce_undo: announce_undo,
announce: announce,
block_undo: block_undo,
block: block,
poster: poster,
user: user
}
end
test "deletes the original block", %{
block_undo: block_undo,
block: block
} do
{:ok, _block_undo, _meta} = SideEffects.handle(block_undo)
refute Activity.get_by_id(block.id)
end
test "unblocks the blocked user", %{block_undo: block_undo, block: block} do
blocker = User.get_by_ap_id(block.data["actor"])
blocked = User.get_by_ap_id(block.data["object"])
{:ok, _block_undo, _} = SideEffects.handle(block_undo)
refute User.blocks?(blocker, blocked)
end
test "an announce undo removes the announce from the object", %{
announce_undo: announce_undo,
post: post
} do
{:ok, _announce_undo, _} = SideEffects.handle(announce_undo)
object = Object.get_by_ap_id(post.data["object"])
assert object.data["announcement_count"] == 0
assert object.data["announcements"] == []
end
test "deletes the original announce", %{announce_undo: announce_undo, announce: announce} do
{:ok, _announce_undo, _} = SideEffects.handle(announce_undo)
refute Activity.get_by_id(announce.id)
end
test "a reaction undo removes the reaction from the object", %{
reaction_undo: reaction_undo,
post: post
} do
{:ok, _reaction_undo, _} = SideEffects.handle(reaction_undo)
object = Object.get_by_ap_id(post.data["object"])
assert object.data["reaction_count"] == 0
assert object.data["reactions"] == []
end
test "deletes the original reaction", %{reaction_undo: reaction_undo, reaction: reaction} do
{:ok, _reaction_undo, _} = SideEffects.handle(reaction_undo)
refute Activity.get_by_id(reaction.id)
end
test "a like undo removes the like from the object", %{like_undo: like_undo, post: post} do
{:ok, _like_undo, _} = SideEffects.handle(like_undo)
object = Object.get_by_ap_id(post.data["object"])
assert object.data["like_count"] == 0
assert object.data["likes"] == []
end
test "deletes the original like", %{like_undo: like_undo, like: like} do
{:ok, _like_undo, _} = SideEffects.handle(like_undo)
refute Activity.get_by_id(like.id)
end
end
describe "like objects" do
setup do
poster = insert(:user)
user = insert(:user)
{:ok, post} = CommonAPI.post(poster, %{status: "hey"})
{:ok, like_data, _meta} = Builder.like(user, post.object)
{:ok, like, _meta} = ActivityPub.persist(like_data, local: true)
%{like: like, user: user, poster: poster}
end
test "add the like to the original object", %{like: like, user: user} do
{:ok, like, _} = SideEffects.handle(like)
object = Object.get_by_ap_id(like.data["object"])
assert object.data["like_count"] == 1
assert user.ap_id in object.data["likes"]
end
test "creates a notification", %{like: like, poster: poster} do
{:ok, like, _} = SideEffects.handle(like)
assert Repo.get_by(Notification, user_id: poster.id, activity_id: like.id)
end
end
describe "creation of ChatMessages" do
test "notifies the recipient" do
author = insert(:user, local: false)
recipient = insert(:user, local: true)
{:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
{:ok, create_activity_data, _meta} =
Builder.create(author, chat_message_data["id"], [recipient.ap_id])
{:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
{:ok, _create_activity, _meta} =
SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
assert Repo.get_by(Notification, user_id: recipient.id, activity_id: create_activity.id)
end
test "it streams the created ChatMessage" do
author = insert(:user, local: true)
recipient = insert(:user, local: true)
{:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
{:ok, create_activity_data, _meta} =
Builder.create(author, chat_message_data["id"], [recipient.ap_id])
{:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
{:ok, _create_activity, meta} =
SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
assert [_, _] = meta[:streamables]
end
test "it creates a Chat and MessageReferences for the local users and bumps the unread count, except for the author" do
author = insert(:user, local: true)
recipient = insert(:user, local: true)
{:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
{:ok, create_activity_data, _meta} =
Builder.create(author, chat_message_data["id"], [recipient.ap_id])
{:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
with_mocks([
{
Pleroma.Web.Streamer,
[],
[
stream: fn _, _ -> nil end
]
},
{
Pleroma.Web.Push,
[],
[
send: fn _ -> nil end
]
}
]) do
{:ok, _create_activity, meta} =
SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
# The notification gets created
assert [notification] = meta[:notifications]
assert notification.activity_id == create_activity.id
# But it is not sent out
refute called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification))
refute called(Pleroma.Web.Push.send(notification))
# Same for the user chat stream
assert [{topics, _}, _] = meta[:streamables]
assert topics == ["user", "user:pleroma_chat"]
refute called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_))
chat = Chat.get(author.id, recipient.ap_id)
[cm_ref] = MessageReference.for_chat_query(chat) |> Repo.all()
assert cm_ref.object.data["content"] == "hey"
assert cm_ref.unread == false
chat = Chat.get(recipient.id, author.ap_id)
[cm_ref] = MessageReference.for_chat_query(chat) |> Repo.all()
assert cm_ref.object.data["content"] == "hey"
assert cm_ref.unread == true
end
end
test "it creates a Chat for the local users and bumps the unread count" do
author = insert(:user, local: false)
recipient = insert(:user, local: true)
{:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
{:ok, create_activity_data, _meta} =
Builder.create(author, chat_message_data["id"], [recipient.ap_id])
{:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
{:ok, _create_activity, _meta} =
SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
# An object is created
assert Object.get_by_ap_id(chat_message_data["id"])
# The remote user won't get a chat
chat = Chat.get(author.id, recipient.ap_id)
refute chat
# The local user will get a chat
chat = Chat.get(recipient.id, author.ap_id)
assert chat
author = insert(:user, local: true)
recipient = insert(:user, local: true)
{:ok, chat_message_data, _meta} = Builder.chat_message(author, recipient.ap_id, "hey")
{:ok, create_activity_data, _meta} =
Builder.create(author, chat_message_data["id"], [recipient.ap_id])
{:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
{:ok, _create_activity, _meta} =
SideEffects.handle(create_activity, local: false, object_data: chat_message_data)
# Both users are local and get the chat
chat = Chat.get(author.id, recipient.ap_id)
assert chat
chat = Chat.get(recipient.id, author.ap_id)
assert chat
end
end
describe "announce objects" do
setup do
poster = insert(:user)
user = insert(:user)
{:ok, post} = CommonAPI.post(poster, %{status: "hey"})
{:ok, private_post} = CommonAPI.post(poster, %{status: "hey", visibility: "private"})
{:ok, announce_data, _meta} = Builder.announce(user, post.object, public: true)
{:ok, private_announce_data, _meta} =
Builder.announce(user, private_post.object, public: false)
{:ok, relay_announce_data, _meta} =
Builder.announce(Pleroma.Web.ActivityPub.Relay.get_actor(), post.object, public: true)
{:ok, announce, _meta} = ActivityPub.persist(announce_data, local: true)
{:ok, private_announce, _meta} = ActivityPub.persist(private_announce_data, local: true)
{:ok, relay_announce, _meta} = ActivityPub.persist(relay_announce_data, local: true)
%{
announce: announce,
user: user,
poster: poster,
private_announce: private_announce,
relay_announce: relay_announce
}
end
test "adds the announce to the original object", %{announce: announce, user: user} do
{:ok, announce, _} = SideEffects.handle(announce)
object = Object.get_by_ap_id(announce.data["object"])
assert object.data["announcement_count"] == 1
assert user.ap_id in object.data["announcements"]
end
test "does not add the announce to the original object if the actor is a service actor", %{
relay_announce: announce
} do
{:ok, announce, _} = SideEffects.handle(announce)
object = Object.get_by_ap_id(announce.data["object"])
assert object.data["announcement_count"] == nil
end
test "creates a notification", %{announce: announce, poster: poster} do
{:ok, announce, _} = SideEffects.handle(announce)
assert Repo.get_by(Notification, user_id: poster.id, activity_id: announce.id)
end
test "it streams out the announce", %{announce: announce} do
with_mocks([
{
Pleroma.Web.Streamer,
[],
[
stream: fn _, _ -> nil end
]
},
{
Pleroma.Web.Push,
[],
[
send: fn _ -> nil end
]
}
]) do
{:ok, announce, _} = SideEffects.handle(announce)
assert called(Pleroma.Web.Streamer.stream(["user", "list"], announce))
assert called(Pleroma.Web.Push.send(:_))
end
end
end
describe "removing a follower" do
setup do
user = insert(:user)
followed = insert(:user)
{:ok, _, _, follow_activity} = CommonAPI.follow(user, followed)
{:ok, reject_data, []} = Builder.reject(followed, follow_activity)
{:ok, reject, _meta} = ActivityPub.persist(reject_data, local: true)
%{user: user, followed: followed, reject: reject}
end
test "", %{user: user, followed: followed, reject: reject} do
assert User.following?(user, followed)
assert Pleroma.FollowingRelationship.get(user, followed)
{:ok, _, _} = SideEffects.handle(reject)
refute User.following?(user, followed)
refute Pleroma.FollowingRelationship.get(user, followed)
assert User.get_follow_state(user, followed) == nil
assert User.get_follow_state(user, followed, nil) == nil
end
end
describe "removing a follower from remote" do
setup do
user = insert(:user)
followed = insert(:user, local: false)
# Mock a local-to-remote follow
{:ok, follow_data, []} = Builder.follow(user, followed)
follow_data =
follow_data
|> Map.put("state", "accept")
{:ok, follow, _meta} = ActivityPub.persist(follow_data, local: true)
{:ok, _, _} = SideEffects.handle(follow)
# Mock a remote-to-local accept
{:ok, accept_data, _} = Builder.accept(followed, follow)
{:ok, accept, _} = ActivityPub.persist(accept_data, local: false)
{:ok, _, _} = SideEffects.handle(accept)
# Mock a remote-to-local reject
{:ok, reject_data, []} = Builder.reject(followed, follow)
{:ok, reject, _meta} = ActivityPub.persist(reject_data, local: false)
%{user: user, followed: followed, reject: reject}
end
test "", %{user: user, followed: followed, reject: reject} do
assert User.following?(user, followed)
assert Pleroma.FollowingRelationship.get(user, followed)
{:ok, _, _} = SideEffects.handle(reject)
refute User.following?(user, followed)
refute Pleroma.FollowingRelationship.get(user, followed)
assert Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(user, followed).data["state"] ==
"reject"
assert User.get_follow_state(user, followed) == nil
assert User.get_follow_state(user, followed, nil) == nil
end
end
describe "Group actors" do
setup do
poster =
insert(:user,
local: false,
nickname: "poster@example.com",
ap_id: "https://example.com/users/poster"
)
group = insert(:user, actor_type: "Group")
make_create = fn mentioned_users ->
mentions = mentioned_users |> Enum.map(fn u -> "@#{u.nickname}" end) |> Enum.join(" ")
{:ok, draft} = ActivityDraft.create(poster, %{status: "#{mentions} hey"})
create_activity_data =
Utils.make_create_data(draft.changes |> Map.put(:published, nil), %{})
|> put_in(["object", "id"], "https://example.com/object")
|> put_in(["id"], "https://example.com/activity")
assert Enum.all?(mentioned_users, fn u -> u.ap_id in create_activity_data["to"] end)
create_activity_data
end
%{poster: poster, group: group, make_create: make_create}
end
test "group should boost it", %{make_create: make_create, group: group} do
create_activity_data = make_create.([group])
{:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
{:ok, _create_activity, _meta} =
SideEffects.handle(create_activity,
local: false,
object_data: create_activity_data["object"]
)
object = Object.normalize(create_activity, fetch: false)
assert [announce] = get_announces_of_object(object)
assert announce.actor == group.ap_id
end
test "remote group should not boost it", %{make_create: make_create, group: group} do
remote_group =
insert(:user, actor_type: "Group", local: false, nickname: "remotegroup@example.com")
create_activity_data = make_create.([group, remote_group])
{:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
{:ok, _create_activity, _meta} =
SideEffects.handle(create_activity,
local: false,
object_data: create_activity_data["object"]
)
object = Object.normalize(create_activity, fetch: false)
assert [announce] = get_announces_of_object(object)
assert announce.actor == group.ap_id
end
+
+ test "group should not boost it if group is blocking poster", %{
+ make_create: make_create,
+ group: group,
+ poster: poster
+ } do
+ {:ok, _} = CommonAPI.block(group, poster)
+ create_activity_data = make_create.([group])
+ {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
+
+ {:ok, _create_activity, _meta} =
+ SideEffects.handle(create_activity,
+ local: false,
+ object_data: create_activity_data["object"]
+ )
+
+ object = Object.normalize(create_activity, fetch: false)
+ assert [] = get_announces_of_object(object)
+ end
end
end
diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs
index 275a633c2..f002172c5 100644
--- a/test/pleroma/web/common_api_test.exs
+++ b/test/pleroma/web/common_api_test.exs
@@ -1,1887 +1,1895 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPITest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase, async: false
alias Pleroma.Activity
alias Pleroma.Chat
alias Pleroma.Conversation.Participation
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.UnstubbedConfigMock, as: ConfigMock
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
alias Pleroma.Workers.PollWorker
import Ecto.Query, only: [from: 2]
import Mock
import Mox
import Pleroma.Factory
require Pleroma.Activity.Queries
require Pleroma.Constants
defp get_announces_of_object(%{data: %{"id" => id}} = _object) do
Pleroma.Activity.Queries.by_type("Announce")
|> Pleroma.Activity.Queries.by_object_id(id)
|> Pleroma.Repo.all()
end
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup do
ConfigMock
|> stub_with(Pleroma.Test.StaticConfig)
:ok
end
setup do: clear_config([:instance, :safe_dm_mentions])
setup do: clear_config([:instance, :limit])
setup do: clear_config([:instance, :max_pinned_statuses])
describe "posting polls" do
test "it posts a poll" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "who is the best",
poll: %{expires_in: 600, options: ["reimu", "marisa"]}
})
object = Object.normalize(activity, fetch: false)
assert object.data["type"] == "Question"
assert object.data["oneOf"] |> length() == 2
assert_enqueued(
worker: PollWorker,
args: %{op: "poll_end", activity_id: activity.id},
scheduled_at: NaiveDateTime.from_iso8601!(object.data["closed"])
)
end
end
describe "blocking" do
setup do
blocker = insert(:user)
blocked = insert(:user, local: false)
CommonAPI.follow(blocker, blocked)
CommonAPI.follow(blocked, blocker)
CommonAPI.accept_follow_request(blocker, blocked)
CommonAPI.accept_follow_request(blocked, blocked)
%{blocker: blocker, blocked: blocked}
end
test "it blocks and federates", %{blocker: blocker, blocked: blocked} do
clear_config([:instance, :federating], true)
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
assert User.get_follow_state(blocker, blocked) == :follow_accept
refute is_nil(Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(blocker, blocked))
assert {:ok, block} = CommonAPI.block(blocker, blocked)
assert block.local
assert User.blocks?(blocker, blocked)
refute User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
refute User.get_follow_state(blocker, blocked)
assert %{data: %{"state" => "reject"}} =
Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(blocker, blocked)
assert called(Pleroma.Web.Federator.publish(block))
end
end
test "it blocks and does not federate if outgoing blocks are disabled", %{
blocker: blocker,
blocked: blocked
} do
clear_config([:instance, :federating], true)
clear_config([:activitypub, :outgoing_blocks], false)
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
assert {:ok, block} = CommonAPI.block(blocker, blocked)
assert block.local
assert User.blocks?(blocker, blocked)
refute User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
refute called(Pleroma.Web.Federator.publish(block))
end
end
end
describe "posting chat messages" do
setup do: clear_config([:instance, :chat_limit])
test "it posts a self-chat" do
author = insert(:user)
recipient = author
{:ok, activity} =
CommonAPI.post_chat_message(
author,
recipient,
"remember to buy milk when milk truk arive"
)
assert activity.data["type"] == "Create"
end
test "it posts a chat message without content but with an attachment" do
author = insert(:user)
recipient = insert(:user)
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, upload} = ActivityPub.upload(file, actor: author.ap_id)
with_mocks([
{
Pleroma.Web.Streamer,
[],
[
stream: fn _, _ ->
nil
end
]
},
{
Pleroma.Web.Push,
[],
[
send: fn _ -> nil end
]
}
]) do
{:ok, activity} =
CommonAPI.post_chat_message(
author,
recipient,
nil,
media_id: upload.id
)
notification =
Notification.for_user_and_activity(recipient, activity)
|> Repo.preload(:activity)
assert called(Pleroma.Web.Push.send(notification))
assert called(Pleroma.Web.Streamer.stream(["user", "user:notification"], notification))
assert called(Pleroma.Web.Streamer.stream(["user", "user:pleroma_chat"], :_))
assert activity
end
end
test "it adds html newlines" do
author = insert(:user)
recipient = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post_chat_message(
author,
recipient,
"uguu\nuguuu"
)
assert other_user.ap_id not in activity.recipients
object = Object.normalize(activity, fetch: false)
assert object.data["content"] == "uguu<br/>uguuu"
end
test "it linkifies" do
author = insert(:user)
recipient = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post_chat_message(
author,
recipient,
"https://example.org is the site of @#{other_user.nickname} #2hu"
)
assert other_user.ap_id not in activity.recipients
object = Object.normalize(activity, fetch: false)
assert object.data["content"] ==
"<a href=\"https://example.org\" rel=\"ugc\">https://example.org</a> is the site of <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{other_user.id}\" href=\"#{other_user.ap_id}\" rel=\"ugc\">@<span>#{other_user.nickname}</span></a></span> <a class=\"hashtag\" data-tag=\"2hu\" href=\"http://localhost:4001/tag/2hu\">#2hu</a>"
end
test "it posts a chat message" do
author = insert(:user)
recipient = insert(:user)
{:ok, activity} =
CommonAPI.post_chat_message(
author,
recipient,
"a test message <script>alert('uuu')</script> :firefox:"
)
assert activity.data["type"] == "Create"
assert activity.local
object = Object.normalize(activity, fetch: false)
assert object.data["type"] == "ChatMessage"
assert object.data["to"] == [recipient.ap_id]
assert object.data["content"] ==
"a test message &lt;script&gt;alert(&#39;uuu&#39;)&lt;/script&gt; :firefox:"
assert object.data["emoji"] == %{
"firefox" => "http://localhost:4001/emoji/Firefox.gif"
}
assert Chat.get(author.id, recipient.ap_id)
assert Chat.get(recipient.id, author.ap_id)
assert :ok == Pleroma.Web.Federator.perform(:publish, activity)
end
test "it reject messages over the local limit" do
clear_config([:instance, :chat_limit], 2)
author = insert(:user)
recipient = insert(:user)
{:error, message} =
CommonAPI.post_chat_message(
author,
recipient,
"123"
)
assert message == :content_too_long
end
test "it reject messages via MRF" do
clear_config([:mrf_keyword, :reject], ["GNO"])
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
author = insert(:user)
recipient = insert(:user)
assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} ==
CommonAPI.post_chat_message(author, recipient, "GNO/Linux")
end
test "it reject messages with attachments not belonging to user" do
author = insert(:user)
not_author = insert(:user)
recipient = author
attachment = insert(:attachment, %{user: not_author})
{:error, message} =
CommonAPI.post_chat_message(
author,
recipient,
"123",
media_id: attachment.id
)
assert message == :forbidden
end
end
describe "unblocking" do
test "it works even without an existing block activity" do
blocked = insert(:user)
blocker = insert(:user)
User.block(blocker, blocked)
assert User.blocks?(blocker, blocked)
assert {:ok, :no_activity} == CommonAPI.unblock(blocker, blocked)
refute User.blocks?(blocker, blocked)
end
end
describe "deletion" do
test "it works with pruned objects" do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
clear_config([:instance, :federating], true)
Object.normalize(post, fetch: false)
|> Object.prune()
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
assert {:ok, delete} = CommonAPI.delete(post.id, user)
assert delete.local
assert called(Pleroma.Web.Federator.publish(delete))
end
refute Activity.get_by_id(post.id)
end
test "it allows users to delete their posts" do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
clear_config([:instance, :federating], true)
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
assert {:ok, delete} = CommonAPI.delete(post.id, user)
assert delete.local
assert called(Pleroma.Web.Federator.publish(delete))
end
refute Activity.get_by_id(post.id)
end
test "it does not allow a user to delete posts from another user" do
user = insert(:user)
other_user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
assert {:error, "Could not delete"} = CommonAPI.delete(post.id, other_user)
assert Activity.get_by_id(post.id)
end
test "it allows privileged users to delete other user's posts" do
clear_config([:instance, :moderator_privileges], [:messages_delete])
user = insert(:user)
moderator = insert(:user, is_moderator: true)
{:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
assert delete.local
refute Activity.get_by_id(post.id)
end
test "it doesn't allow unprivileged mods or admins to delete other user's posts" do
clear_config([:instance, :admin_privileges], [])
clear_config([:instance, :moderator_privileges], [])
user = insert(:user)
moderator = insert(:user, is_moderator: true, is_admin: true)
{:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
assert {:error, "Could not delete"} = CommonAPI.delete(post.id, moderator)
assert Activity.get_by_id(post.id)
end
test "privileged users deleting non-local posts won't federate the delete" do
clear_config([:instance, :admin_privileges], [:messages_delete])
# This is the user of the ingested activity
_user =
insert(:user,
local: false,
ap_id: "http://mastodon.example.org/users/admin",
last_refreshed_at: NaiveDateTime.utc_now()
)
admin = insert(:user, is_admin: true)
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Jason.decode!()
{:ok, post} = Transmogrifier.handle_incoming(data)
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
assert {:ok, delete} = CommonAPI.delete(post.id, admin)
assert delete.local
refute called(Pleroma.Web.Federator.publish(:_))
end
refute Activity.get_by_id(post.id)
end
test "it allows privileged users to delete banned user's posts" do
clear_config([:instance, :moderator_privileges], [:messages_delete])
user = insert(:user)
moderator = insert(:user, is_moderator: true)
{:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
User.set_activation(user, false)
assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
assert delete.local
refute Activity.get_by_id(post.id)
end
end
test "favoriting race condition" do
user = insert(:user)
users_serial = insert_list(10, :user)
users = insert_list(10, :user)
{:ok, activity} = CommonAPI.post(user, %{status: "."})
users_serial
|> Enum.map(fn user ->
CommonAPI.favorite(user, activity.id)
end)
object = Object.get_by_ap_id(activity.data["object"])
assert object.data["like_count"] == 10
users
|> Enum.map(fn user ->
Task.async(fn ->
CommonAPI.favorite(user, activity.id)
end)
end)
|> Enum.map(&Task.await/1)
object = Object.get_by_ap_id(activity.data["object"])
assert object.data["like_count"] == 20
end
test "repeating race condition" do
user = insert(:user)
users_serial = insert_list(10, :user)
users = insert_list(10, :user)
{:ok, activity} = CommonAPI.post(user, %{status: "."})
users_serial
|> Enum.map(fn user ->
CommonAPI.repeat(activity.id, user)
end)
object = Object.get_by_ap_id(activity.data["object"])
assert object.data["announcement_count"] == 10
users
|> Enum.map(fn user ->
Task.async(fn ->
CommonAPI.repeat(activity.id, user)
end)
end)
|> Enum.map(&Task.await/1)
object = Object.get_by_ap_id(activity.data["object"])
assert object.data["announcement_count"] == 20
end
test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"})
[participation] = Participation.for_user(user)
{:ok, convo_reply} =
CommonAPI.post(user, %{status: ".", in_reply_to_conversation_id: participation.id})
assert Visibility.is_direct?(convo_reply)
assert activity.data["context"] == convo_reply.data["context"]
end
test "when replying to a conversation / participation, it only mentions the recipients explicitly declared in the participation" do
har = insert(:user)
jafnhar = insert(:user)
tridi = insert(:user)
{:ok, activity} =
CommonAPI.post(har, %{
status: "@#{jafnhar.nickname} hey",
visibility: "direct"
})
assert har.ap_id in activity.recipients
assert jafnhar.ap_id in activity.recipients
[participation] = Participation.for_user(har)
{:ok, activity} =
CommonAPI.post(har, %{
status: "I don't really like @#{tridi.nickname}",
visibility: "direct",
in_reply_to_status_id: activity.id,
in_reply_to_conversation_id: participation.id
})
assert har.ap_id in activity.recipients
assert jafnhar.ap_id in activity.recipients
refute tridi.ap_id in activity.recipients
end
test "with the safe_dm_mention option set, it does not mention people beyond the initial tags" do
har = insert(:user)
jafnhar = insert(:user)
tridi = insert(:user)
clear_config([:instance, :safe_dm_mentions], true)
{:ok, activity} =
CommonAPI.post(har, %{
status: "@#{jafnhar.nickname} hey, i never want to see @#{tridi.nickname} again",
visibility: "direct"
})
refute tridi.ap_id in activity.recipients
assert jafnhar.ap_id in activity.recipients
end
test "it de-duplicates tags" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU"})
object = Object.normalize(activity, fetch: false)
assert Object.tags(object) == ["2hu"]
end
test "zwnj is treated as word character" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "#ساٴين‌س"})
object = Object.normalize(activity, fetch: false)
assert Object.tags(object) == ["ساٴين‌س"]
end
test "allows lang attribute" do
user = insert(:user)
text = ~s{<span lang="en">something</span><p lang="diaetuitech_rpyhpgc">random</p>}
{:ok, activity} = CommonAPI.post(user, %{status: text, content_type: "text/html"})
object = Object.normalize(activity, fetch: false)
assert object.data["content"] == text
end
test "double dot in link is allowed" do
user = insert(:user)
text = "https://example.to/something..mp3"
{:ok, activity} = CommonAPI.post(user, %{status: text})
object = Object.normalize(activity, fetch: false)
assert object.data["content"] == "<a href=\"#{text}\" rel=\"ugc\">#{text}</a>"
end
test "it adds emoji in the object" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: ":firefox:"})
assert Object.normalize(activity, fetch: false).data["emoji"]["firefox"]
end
describe "posting" do
test "it adds an emoji on an external site" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey :external_emoji:"})
assert %{"external_emoji" => url} = Object.normalize(activity).data["emoji"]
assert url == "https://example.com/emoji.png"
{:ok, activity} = CommonAPI.post(user, %{status: "hey :blank:"})
assert %{"blank" => url} = Object.normalize(activity).data["emoji"]
assert url == "#{Pleroma.Web.Endpoint.url()}/emoji/blank.png"
end
test "it copies emoji from the subject of the parent post" do
%Object{} =
object =
Object.normalize("https://patch.cx/objects/a399c28e-c821-4820-bc3e-4afeb044c16f",
fetch: true
)
activity = Activity.get_create_by_object_ap_id(object.data["id"])
user = insert(:user)
{:ok, reply_activity} =
CommonAPI.post(user, %{
in_reply_to_id: activity.id,
status: ":joker_disapprove:",
spoiler_text: ":joker_smile:"
})
assert Object.normalize(reply_activity).data["emoji"]["joker_smile"]
refute Object.normalize(reply_activity).data["emoji"]["joker_disapprove"]
end
test "deactivated users can't post" do
user = insert(:user, is_active: false)
assert {:error, _} = CommonAPI.post(user, %{status: "ye"})
end
test "it supports explicit addressing" do
user = insert(:user)
user_two = insert(:user)
user_three = insert(:user)
user_four = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status:
"Hey, I think @#{user_three.nickname} is ugly. @#{user_four.nickname} is alright though.",
to: [user_two.nickname, user_four.nickname, "nonexistent"]
})
assert user.ap_id in activity.recipients
assert user_two.ap_id in activity.recipients
assert user_four.ap_id in activity.recipients
refute user_three.ap_id in activity.recipients
end
test "it filters out obviously bad tags when accepting a post as HTML" do
user = insert(:user)
post = "<p><b>2hu</b></p><script>alert('xss')</script>"
{:ok, activity} =
CommonAPI.post(user, %{
status: post,
content_type: "text/html"
})
object = Object.normalize(activity, fetch: false)
assert object.data["content"] == "<p><b>2hu</b></p>alert(&#39;xss&#39;)"
assert object.data["source"]["content"] == post
end
test "it filters out obviously bad tags when accepting a post as Markdown" do
user = insert(:user)
post = "<p><b>2hu</b></p><script>alert('xss')</script>"
{:ok, activity} =
CommonAPI.post(user, %{
status: post,
content_type: "text/markdown"
})
object = Object.normalize(activity, fetch: false)
assert object.data["content"] == "<p><b>2hu</b></p>"
assert object.data["source"]["content"] == post
end
test "it does not allow replies to direct messages that are not direct messages themselves" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"})
assert {:ok, _} =
CommonAPI.post(user, %{
status: "suya..",
visibility: "direct",
in_reply_to_status_id: activity.id
})
Enum.each(["public", "private", "unlisted"], fn visibility ->
assert {:error, "The message visibility must be direct"} =
CommonAPI.post(user, %{
status: "suya..",
visibility: visibility,
in_reply_to_status_id: activity.id
})
end)
end
test "replying with a direct message will NOT auto-add the author of the reply to the recipient list" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "I'm stupid"})
{:ok, open_answer} =
CommonAPI.post(other_user, %{status: "No ur smart", in_reply_to_status_id: post.id})
# The OP is implicitly added
assert user.ap_id in open_answer.recipients
{:ok, secret_answer} =
CommonAPI.post(other_user, %{
status: "lol, that guy really is stupid, right, @#{third_user.nickname}?",
in_reply_to_status_id: post.id,
visibility: "direct"
})
assert third_user.ap_id in secret_answer.recipients
# The OP is not added
refute user.ap_id in secret_answer.recipients
end
test "it allows to address a list" do
user = insert(:user)
{:ok, list} = Pleroma.List.create("foo", user)
{:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"})
assert activity.data["bcc"] == [list.ap_id]
assert activity.recipients == [list.ap_id, user.ap_id]
assert activity.data["listMessage"] == list.ap_id
end
test "it returns error when status is empty and no attachments" do
user = insert(:user)
assert {:error, "Cannot post an empty status without attachments"} =
CommonAPI.post(user, %{status: ""})
end
test "it validates character limits are correctly enforced" do
clear_config([:instance, :limit], 5)
user = insert(:user)
assert {:error, "The status is over the character limit"} =
CommonAPI.post(user, %{status: "foobar"})
assert {:ok, _activity} = CommonAPI.post(user, %{status: "12345"})
end
test "it validates media attachment limits are correctly enforced" do
clear_config([:instance, :max_media_attachments], 4)
user = insert(:user)
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
assert {:error, "Too many attachments"} =
CommonAPI.post(user, %{
status: "",
media_ids: List.duplicate(upload.id, 5)
})
assert {:ok, _activity} =
CommonAPI.post(user, %{
status: "",
media_ids: [upload.id]
})
end
test "it can handle activities that expire" do
user = insert(:user)
expires_at = DateTime.add(DateTime.utc_now(), 1_000_000)
assert {:ok, activity} = CommonAPI.post(user, %{status: "chai", expires_in: 1_000_000})
assert_enqueued(
worker: Pleroma.Workers.PurgeExpiredActivity,
args: %{activity_id: activity.id},
scheduled_at: expires_at
)
end
test "it allows quote posting" do
user = insert(:user)
{:ok, quoted} = CommonAPI.post(user, %{status: "Hello world"})
{:ok, quote_post} = CommonAPI.post(user, %{status: "nice post", quote_id: quoted.id})
quoted = Object.normalize(quoted)
quote_post = Object.normalize(quote_post)
assert quote_post.data["quoteUrl"] == quoted.data["id"]
# The OP is not mentioned
refute quoted.data["actor"] in quote_post.data["to"]
end
test "quote posting with explicit addressing doesn't mention the OP" do
user = insert(:user)
{:ok, quoted} = CommonAPI.post(user, %{status: "Hello world"})
{:ok, quote_post} =
CommonAPI.post(user, %{status: "nice post", quote_id: quoted.id, to: []})
assert Object.normalize(quote_post).data["to"] == [Pleroma.Constants.as_public()]
end
test "quote posting visibility" do
user = insert(:user)
another_user = insert(:user)
{:ok, direct} = CommonAPI.post(user, %{status: ".", visibility: "direct"})
{:ok, private} = CommonAPI.post(user, %{status: ".", visibility: "private"})
{:ok, unlisted} = CommonAPI.post(user, %{status: ".", visibility: "unlisted"})
{:ok, local} = CommonAPI.post(user, %{status: ".", visibility: "local"})
{:ok, public} = CommonAPI.post(user, %{status: ".", visibility: "public"})
{:error, _} = CommonAPI.post(user, %{status: "nice", quote_id: direct.id})
{:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: private.id})
{:error, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: private.id})
{:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: unlisted.id})
{:ok, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: unlisted.id})
{:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: local.id})
{:ok, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: local.id})
{:ok, _} = CommonAPI.post(user, %{status: "nice", quote_id: public.id})
{:ok, _} = CommonAPI.post(another_user, %{status: "nice", quote_id: public.id})
end
test "it properly mentions punycode domain" do
user = insert(:user)
_mentioned_user =
insert(:user, ap_id: "https://xn--i2raa.com/users/yyy", nickname: "yyy@xn--i2raa.com")
{:ok, activity} =
CommonAPI.post(user, %{status: "hey @yyy@xn--i2raa.com", content_type: "text/markdown"})
assert "https://xn--i2raa.com/users/yyy" in Object.normalize(activity).data["to"]
end
end
describe "reactions" do
test "reacting to a status with an emoji" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
assert reaction.data["actor"] == user.ap_id
assert reaction.data["content"] == "👍"
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:error, _} = CommonAPI.react_with_emoji(activity.id, user, ".")
end
test "unreacting to a status with an emoji" do
user = insert(:user)
other_user = insert(:user)
clear_config([:instance, :federating], true)
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end do
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
{:ok, unreaction} = CommonAPI.unreact_with_emoji(activity.id, user, "👍")
assert unreaction.data["type"] == "Undo"
assert unreaction.data["object"] == reaction.data["id"]
assert unreaction.local
# On federation, it contains the undone (and deleted) object
unreaction_with_object = %{
unreaction
| data: Map.put(unreaction.data, "object", reaction.data)
}
assert called(Pleroma.Web.Federator.publish(unreaction_with_object))
end
end
test "repeating a status" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, user)
assert Visibility.is_public?(announce_activity)
end
test "can't repeat a repeat" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, %Activity{} = announce} = CommonAPI.repeat(activity.id, other_user)
refute match?({:ok, %Activity{}}, CommonAPI.repeat(announce.id, user))
end
test "repeating a status privately" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, %Activity{} = announce_activity} =
CommonAPI.repeat(activity.id, user, %{visibility: "private"})
assert Visibility.is_private?(announce_activity)
refute Visibility.visible_for_user?(announce_activity, nil)
end
test "author can repeat own private statuses" do
author = insert(:user)
follower = insert(:user)
CommonAPI.follow(follower, author)
{:ok, activity} = CommonAPI.post(author, %{status: "cofe", visibility: "private"})
{:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, author)
assert Visibility.is_private?(announce_activity)
refute Visibility.visible_for_user?(announce_activity, nil)
assert Visibility.visible_for_user?(activity, follower)
assert {:error, :not_found} = CommonAPI.repeat(activity.id, follower)
end
test "favoriting a status" do
user = insert(:user)
other_user = insert(:user)
{:ok, post_activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, %Activity{data: data}} = CommonAPI.favorite(user, post_activity.id)
assert data["type"] == "Like"
assert data["actor"] == user.ap_id
assert data["object"] == post_activity.data["object"]
end
test "retweeting a status twice returns the status" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, %Activity{} = announce} = CommonAPI.repeat(activity.id, user)
{:ok, ^announce} = CommonAPI.repeat(activity.id, user)
end
test "favoriting a status twice returns ok, but without the like activity" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, %Activity{}} = CommonAPI.favorite(user, activity.id)
assert {:ok, :already_liked} = CommonAPI.favorite(user, activity.id)
end
end
describe "pinned statuses" do
setup do
clear_config([:instance, :max_pinned_statuses], 1)
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"})
[user: user, activity: activity]
end
test "activity not found error", %{user: user} do
assert {:error, :not_found} = CommonAPI.pin("id", user)
end
test "pin status", %{user: user, activity: activity} do
assert {:ok, ^activity} = CommonAPI.pin(activity.id, user)
%{data: %{"id" => object_id}} = Object.normalize(activity)
user = refresh_record(user)
assert user.pinned_objects |> Map.keys() == [object_id]
end
test "pin poll", %{user: user} do
{:ok, activity} =
CommonAPI.post(user, %{
status: "How is fediverse today?",
poll: %{options: ["Absolutely outstanding", "Not good"], expires_in: 20}
})
assert {:ok, ^activity} = CommonAPI.pin(activity.id, user)
%{data: %{"id" => object_id}} = Object.normalize(activity)
user = refresh_record(user)
assert user.pinned_objects |> Map.keys() == [object_id]
end
test "unlisted statuses can be pinned", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "HI!!!", visibility: "unlisted"})
assert {:ok, ^activity} = CommonAPI.pin(activity.id, user)
end
test "only self-authored can be pinned", %{activity: activity} do
user = insert(:user)
assert {:error, :ownership_error} = CommonAPI.pin(activity.id, user)
end
test "max pinned statuses", %{user: user, activity: activity_one} do
{:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"})
assert {:ok, ^activity_one} = CommonAPI.pin(activity_one.id, user)
user = refresh_record(user)
assert {:error, :pinned_statuses_limit_reached} = CommonAPI.pin(activity_two.id, user)
end
test "only public can be pinned", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "private status", visibility: "private"})
{:error, :visibility_error} = CommonAPI.pin(activity.id, user)
end
test "unpin status", %{user: user, activity: activity} do
{:ok, activity} = CommonAPI.pin(activity.id, user)
user = refresh_record(user)
id = activity.id
assert match?({:ok, %{id: ^id}}, CommonAPI.unpin(activity.id, user))
user = refresh_record(user)
assert user.pinned_objects == %{}
end
test "should unpin when deleting a status", %{user: user, activity: activity} do
{:ok, activity} = CommonAPI.pin(activity.id, user)
user = refresh_record(user)
assert {:ok, _} = CommonAPI.delete(activity.id, user)
user = refresh_record(user)
assert user.pinned_objects == %{}
end
test "ephemeral activity won't be deleted if was pinned", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "Hello!", expires_in: 601})
assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id)
{:ok, _activity} = CommonAPI.pin(activity.id, user)
refute Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id)
user = refresh_record(user)
{:ok, _} = CommonAPI.unpin(activity.id, user)
# recreates expiration job on unpin
assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id)
end
test "ephemeral activity deletion job won't be deleted on pinning error", %{
user: user,
activity: activity
} do
clear_config([:instance, :max_pinned_statuses], 1)
{:ok, _activity} = CommonAPI.pin(activity.id, user)
{:ok, activity2} = CommonAPI.post(user, %{status: "another status", expires_in: 601})
assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity2.id)
user = refresh_record(user)
{:error, :pinned_statuses_limit_reached} = CommonAPI.pin(activity2.id, user)
assert Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity2.id)
end
end
describe "mute tests" do
setup do
user = insert(:user)
activity = insert(:note_activity)
[user: user, activity: activity]
end
test "marks notifications as read after mute" do
author = insert(:user)
activity = insert(:note_activity, user: author)
friend1 = insert(:user)
friend2 = insert(:user)
{:ok, reply_activity} =
CommonAPI.post(
friend2,
%{
status: "@#{author.nickname} @#{friend1.nickname} test reply",
in_reply_to_status_id: activity.id
}
)
{:ok, favorite_activity} = CommonAPI.favorite(friend2, activity.id)
{:ok, repeat_activity} = CommonAPI.repeat(activity.id, friend1)
assert Repo.aggregate(
from(n in Notification, where: n.seen == false and n.user_id == ^friend1.id),
:count
) == 1
unread_notifications =
Repo.all(from(n in Notification, where: n.seen == false, where: n.user_id == ^author.id))
assert Enum.any?(unread_notifications, fn n ->
n.type == "favourite" && n.activity_id == favorite_activity.id
end)
assert Enum.any?(unread_notifications, fn n ->
n.type == "reblog" && n.activity_id == repeat_activity.id
end)
assert Enum.any?(unread_notifications, fn n ->
n.type == "mention" && n.activity_id == reply_activity.id
end)
{:ok, _} = CommonAPI.add_mute(author, activity)
assert CommonAPI.thread_muted?(author, activity)
assert Repo.aggregate(
from(n in Notification, where: n.seen == false and n.user_id == ^friend1.id),
:count
) == 1
read_notifications =
Repo.all(from(n in Notification, where: n.seen == true, where: n.user_id == ^author.id))
assert Enum.any?(read_notifications, fn n ->
n.type == "favourite" && n.activity_id == favorite_activity.id
end)
assert Enum.any?(read_notifications, fn n ->
n.type == "reblog" && n.activity_id == repeat_activity.id
end)
assert Enum.any?(read_notifications, fn n ->
n.type == "mention" && n.activity_id == reply_activity.id
end)
end
test "add mute", %{user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(user, activity)
assert CommonAPI.thread_muted?(user, activity)
end
test "add expiring mute", %{user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(user, activity, %{expires_in: 60})
assert CommonAPI.thread_muted?(user, activity)
worker = Pleroma.Workers.MuteExpireWorker
args = %{"op" => "unmute_conversation", "user_id" => user.id, "activity_id" => activity.id}
assert_enqueued(
worker: worker,
args: args
)
assert :ok = perform_job(worker, args)
refute CommonAPI.thread_muted?(user, activity)
end
test "remove mute", %{user: user, activity: activity} do
CommonAPI.add_mute(user, activity)
{:ok, _} = CommonAPI.remove_mute(user, activity)
refute CommonAPI.thread_muted?(user, activity)
end
test "remove mute by ids", %{user: user, activity: activity} do
CommonAPI.add_mute(user, activity)
{:ok, _} = CommonAPI.remove_mute(user.id, activity.id)
refute CommonAPI.thread_muted?(user, activity)
end
test "check that mutes can't be duplicate", %{user: user, activity: activity} do
CommonAPI.add_mute(user, activity)
{:error, _} = CommonAPI.add_mute(user, activity)
end
end
describe "reports" do
test "creates a report" do
reporter = insert(:user)
target_user = insert(:user)
{:ok, activity} = CommonAPI.post(target_user, %{status: "foobar"})
activity = Activity.normalize(activity)
reporter_ap_id = reporter.ap_id
target_ap_id = target_user.ap_id
reported_object_ap_id = activity.object.data["id"]
comment = "foobar"
report_data = %{
account_id: target_user.id,
comment: comment,
status_ids: [activity.id]
}
note_obj = %{
"type" => "Note",
"id" => reported_object_ap_id,
"content" => "foobar",
"published" => activity.object.data["published"],
"actor" => AccountView.render("show.json", %{user: target_user})
}
assert {:ok, flag_activity} = CommonAPI.report(reporter, report_data)
assert %Activity{
actor: ^reporter_ap_id,
data: %{
"type" => "Flag",
"content" => ^comment,
"object" => [^target_ap_id, ^note_obj],
"state" => "open"
}
} = flag_activity
end
test "updates report state" do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
object = Object.normalize(activity)
{:ok, %Activity{id: report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I feel offended",
status_ids: [activity.id]
})
{:ok, report} = CommonAPI.update_report_state(report_id, "resolved")
assert report.data["state"] == "resolved"
[reported_user, object_id] = report.data["object"]
assert reported_user == target_user.ap_id
assert object_id == object.data["id"]
end
test "updates report state, don't strip when report_strip_status is false" do
clear_config([:instance, :report_strip_status], false)
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %Activity{id: report_id, data: report_data}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I feel offended",
status_ids: [activity.id]
})
{:ok, report} = CommonAPI.update_report_state(report_id, "resolved")
assert report.data["state"] == "resolved"
[reported_user, reported_activity] = report.data["object"]
assert reported_user == target_user.ap_id
assert is_map(reported_activity)
assert reported_activity["content"] ==
report_data["object"] |> Enum.at(1) |> Map.get("content")
end
test "does not update report state when state is unsupported" do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %Activity{id: report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I feel offended",
status_ids: [activity.id]
})
assert CommonAPI.update_report_state(report_id, "test") == {:error, "Unsupported state"}
end
test "updates state of multiple reports" do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %Activity{id: first_report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I feel offended",
status_ids: [activity.id]
})
{:ok, %Activity{id: second_report_id}} =
CommonAPI.report(reporter, %{
account_id: target_user.id,
comment: "I feel very offended!",
status_ids: [activity.id]
})
{:ok, report_ids} =
CommonAPI.update_report_state([first_report_id, second_report_id], "resolved")
first_report = Activity.get_by_id(first_report_id)
second_report = Activity.get_by_id(second_report_id)
assert report_ids -- [first_report_id, second_report_id] == []
assert first_report.data["state"] == "resolved"
assert second_report.data["state"] == "resolved"
end
end
describe "reblog muting" do
setup do
muter = insert(:user)
muted = insert(:user)
[muter: muter, muted: muted]
end
test "add a reblog mute", %{muter: muter, muted: muted} do
{:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted)
assert User.showing_reblogs?(muter, muted) == false
end
test "remove a reblog mute", %{muter: muter, muted: muted} do
{:ok, _reblog_mute} = CommonAPI.hide_reblogs(muter, muted)
{:ok, _reblog_mute} = CommonAPI.show_reblogs(muter, muted)
assert User.showing_reblogs?(muter, muted) == true
end
end
describe "follow/2" do
test "directly follows a non-locked local user" do
[follower, followed] = insert_pair(:user)
{:ok, follower, followed, _} = CommonAPI.follow(follower, followed)
assert User.following?(follower, followed)
end
end
describe "unfollow/2" do
test "also unsubscribes a user" do
[follower, followed] = insert_pair(:user)
{:ok, follower, followed, _} = CommonAPI.follow(follower, followed)
{:ok, _subscription} = User.subscribe(follower, followed)
assert User.subscribed_to?(follower, followed)
{:ok, follower} = CommonAPI.unfollow(follower, followed)
refute User.subscribed_to?(follower, followed)
end
test "also unpins a user" do
[follower, followed] = insert_pair(:user)
{:ok, follower, followed, _} = CommonAPI.follow(follower, followed)
{:ok, _endorsement} = User.endorse(follower, followed)
assert User.endorses?(follower, followed)
{:ok, follower} = CommonAPI.unfollow(follower, followed)
refute User.endorses?(follower, followed)
end
test "cancels a pending follow for a local user" do
follower = insert(:user)
followed = insert(:user, is_locked: true)
assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} =
CommonAPI.follow(follower, followed)
assert User.get_follow_state(follower, followed) == :follow_pending
assert {:ok, follower} = CommonAPI.unfollow(follower, followed)
assert User.get_follow_state(follower, followed) == nil
assert %{id: ^activity_id, data: %{"state" => "cancelled"}} =
Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed)
assert %{
data: %{
"type" => "Undo",
"object" => %{"type" => "Follow", "state" => "cancelled"}
}
} = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower)
end
test "cancels a pending follow for a remote user" do
follower = insert(:user)
followed = insert(:user, is_locked: true, local: false)
assert {:ok, follower, followed, %{id: activity_id, data: %{"state" => "pending"}}} =
CommonAPI.follow(follower, followed)
assert User.get_follow_state(follower, followed) == :follow_pending
assert {:ok, follower} = CommonAPI.unfollow(follower, followed)
assert User.get_follow_state(follower, followed) == nil
assert %{id: ^activity_id, data: %{"state" => "cancelled"}} =
Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(follower, followed)
assert %{
data: %{
"type" => "Undo",
"object" => %{"type" => "Follow", "state" => "cancelled"}
}
} = Pleroma.Web.ActivityPub.Utils.fetch_latest_undo(follower)
end
end
describe "accept_follow_request/2" do
test "after acceptance, it sets all existing pending follow request states to 'accept'" do
user = insert(:user, is_locked: true)
follower = insert(:user)
follower_two = insert(:user)
{:ok, _, _, follow_activity} = CommonAPI.follow(follower, user)
{:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user)
{:ok, _, _, follow_activity_three} = CommonAPI.follow(follower_two, user)
assert follow_activity.data["state"] == "pending"
assert follow_activity_two.data["state"] == "pending"
assert follow_activity_three.data["state"] == "pending"
{:ok, _follower} = CommonAPI.accept_follow_request(follower, user)
assert Repo.get(Activity, follow_activity.id).data["state"] == "accept"
assert Repo.get(Activity, follow_activity_two.id).data["state"] == "accept"
assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending"
end
test "after rejection, it sets all existing pending follow request states to 'reject'" do
user = insert(:user, is_locked: true)
follower = insert(:user)
follower_two = insert(:user)
{:ok, _, _, follow_activity} = CommonAPI.follow(follower, user)
{:ok, _, _, follow_activity_two} = CommonAPI.follow(follower, user)
{:ok, _, _, follow_activity_three} = CommonAPI.follow(follower_two, user)
assert follow_activity.data["state"] == "pending"
assert follow_activity_two.data["state"] == "pending"
assert follow_activity_three.data["state"] == "pending"
{:ok, _follower} = CommonAPI.reject_follow_request(follower, user)
assert Repo.get(Activity, follow_activity.id).data["state"] == "reject"
assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject"
assert Repo.get(Activity, follow_activity_three.id).data["state"] == "pending"
end
test "doesn't create a following relationship if the corresponding follow request doesn't exist" do
user = insert(:user, is_locked: true)
not_follower = insert(:user)
CommonAPI.accept_follow_request(not_follower, user)
assert Pleroma.FollowingRelationship.following?(not_follower, user) == false
end
end
describe "vote/3" do
test "does not allow to vote twice" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "Am I cute?",
poll: %{options: ["Yes", "No"], expires_in: 20}
})
object = Object.normalize(activity, fetch: false)
{:ok, _, object} = CommonAPI.vote(other_user, object, [0])
assert {:error, "Already voted"} == CommonAPI.vote(other_user, object, [1])
end
end
describe "listen/2" do
test "returns a valid activity" do
user = insert(:user)
{:ok, activity} =
CommonAPI.listen(user, %{
title: "lain radio episode 1",
album: "lain radio",
artist: "lain",
length: 180_000
})
object = Object.normalize(activity, fetch: false)
assert object.data["title"] == "lain radio episode 1"
assert Visibility.get_visibility(activity) == "public"
end
test "respects visibility=private" do
user = insert(:user)
{:ok, activity} =
CommonAPI.listen(user, %{
title: "lain radio episode 1",
album: "lain radio",
artist: "lain",
length: 180_000,
visibility: "private"
})
object = Object.normalize(activity, fetch: false)
assert object.data["title"] == "lain radio episode 1"
assert Visibility.get_visibility(activity) == "private"
end
end
describe "get_user/1" do
test "gets user by ap_id" do
user = insert(:user)
assert CommonAPI.get_user(user.ap_id) == user
end
test "gets user by guessed nickname" do
user = insert(:user, ap_id: "", nickname: "mario@mushroom.kingdom")
assert CommonAPI.get_user("https://mushroom.kingdom/users/mario") == user
end
test "fallback" do
assert %User{
name: "",
ap_id: "",
nickname: "erroruser@example.com"
} = CommonAPI.get_user("")
end
end
describe "with `local` visibility" do
setup do: clear_config([:instance, :federating], true)
test "post" do
user = insert(:user)
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
{:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
assert Visibility.is_local_public?(activity)
assert_not_called(Pleroma.Web.Federator.publish(activity))
end
end
test "delete" do
user = insert(:user)
{:ok, %Activity{id: activity_id}} =
CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, %Activity{data: %{"deleted_activity_id" => ^activity_id}} = activity} =
CommonAPI.delete(activity_id, user)
assert Visibility.is_local_public?(activity)
assert_not_called(Pleroma.Web.Federator.publish(activity))
end
end
test "repeat" do
user = insert(:user)
other_user = insert(:user)
{:ok, %Activity{id: activity_id}} =
CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, %Activity{data: %{"type" => "Announce"}} = activity} =
CommonAPI.repeat(activity_id, user)
assert Visibility.is_local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
test "unrepeat" do
user = insert(:user)
other_user = insert(:user)
{:ok, %Activity{id: activity_id}} =
CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
assert {:ok, _} = CommonAPI.repeat(activity_id, user)
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, %Activity{data: %{"type" => "Undo"}} = activity} =
CommonAPI.unrepeat(activity_id, user)
assert Visibility.is_local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
test "favorite" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, %Activity{data: %{"type" => "Like"}} = activity} =
CommonAPI.favorite(user, activity.id)
assert Visibility.is_local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
test "unfavorite" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
{:ok, %Activity{}} = CommonAPI.favorite(user, activity.id)
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, activity} = CommonAPI.unfavorite(activity.id, user)
assert Visibility.is_local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
test "react_with_emoji" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, %Activity{data: %{"type" => "EmojiReact"}} = activity} =
CommonAPI.react_with_emoji(activity.id, user, "👍")
assert Visibility.is_local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
test "unreact_with_emoji" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
{:ok, _reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, %Activity{data: %{"type" => "Undo"}} = activity} =
CommonAPI.unreact_with_emoji(activity.id, user, "👍")
assert Visibility.is_local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
end
describe "update/3" do
test "updates a post" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1"})
{:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2"})
updated_object = Object.normalize(updated)
assert updated_object.data["content"] == "updated 2"
assert Map.get(updated_object.data, "summary", "") == ""
assert Map.has_key?(updated_object.data, "updated")
end
test "does not change visibility" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1", visibility: "private"})
{:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2"})
updated_object = Object.normalize(updated)
assert updated_object.data["content"] == "updated 2"
assert Map.get(updated_object.data, "summary", "") == ""
assert Visibility.get_visibility(updated_object) == "private"
assert Visibility.get_visibility(updated) == "private"
end
test "updates a post with emoji" do
[{emoji1, _}, {emoji2, _} | _] = Pleroma.Emoji.get_all()
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1 :#{emoji1}:"})
{:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2 :#{emoji2}:"})
updated_object = Object.normalize(updated)
assert updated_object.data["content"] == "updated 2 :#{emoji2}:"
assert %{^emoji2 => _} = updated_object.data["emoji"]
end
test "updates a post with emoji and federate properly" do
[{emoji1, _}, {emoji2, _} | _] = Pleroma.Emoji.get_all()
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{status: "foo1", spoiler_text: "title 1 :#{emoji1}:"})
clear_config([:instance, :federating], true)
with_mock Pleroma.Web.Federator,
publish: fn _p -> nil end do
{:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2 :#{emoji2}:"})
assert updated.data["object"]["content"] == "updated 2 :#{emoji2}:"
assert %{^emoji2 => _} = updated.data["object"]["emoji"]
assert called(Pleroma.Web.Federator.publish(updated))
end
end
test "editing a post that copied a remote title with remote emoji should keep that emoji" do
remote_emoji_uri = "https://remote.org/emoji.png"
note =
insert(
:note,
data: %{
"summary" => ":remoteemoji:",
"emoji" => %{
"remoteemoji" => remote_emoji_uri
},
"tag" => [
%{
"type" => "Emoji",
"name" => "remoteemoji",
"icon" => %{"url" => remote_emoji_uri}
}
]
}
)
note_activity = insert(:note_activity, note: note)
user = insert(:user)
{:ok, reply} =
CommonAPI.post(user, %{
status: "reply",
spoiler_text: ":remoteemoji:",
in_reply_to_id: note_activity.id
})
assert reply.object.data["emoji"]["remoteemoji"] == remote_emoji_uri
{:ok, edit} =
CommonAPI.update(user, reply, %{status: "reply mew mew", spoiler_text: ":remoteemoji:"})
edited_note = Pleroma.Object.normalize(edit)
assert edited_note.data["emoji"]["remoteemoji"] == remote_emoji_uri
end
test "respects MRF" do
user = insert(:user)
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
clear_config([:mrf_keyword, :replace], [{"updated", "mewmew"}])
{:ok, activity} = CommonAPI.post(user, %{status: "foo1", spoiler_text: "updated 1"})
assert Object.normalize(activity).data["summary"] == "mewmew 1"
{:ok, updated} = CommonAPI.update(user, activity, %{status: "updated 2"})
updated_object = Object.normalize(updated)
assert updated_object.data["content"] == "mewmew 2"
assert Map.get(updated_object.data, "summary", "") == ""
assert Map.has_key?(updated_object.data, "updated")
end
end
describe "Group actors" do
setup do
poster = insert(:user)
group = insert(:user, actor_type: "Group")
other_group = insert(:user, actor_type: "Group")
%{poster: poster, group: group, other_group: other_group}
end
test "it boosts public posts", %{poster: poster, group: group} do
{:ok, post} = CommonAPI.post(poster, %{status: "hey @#{group.nickname}"})
announces = get_announces_of_object(post.object)
assert [_] = announces
end
test "it does not boost private posts", %{poster: poster, group: group} do
{:ok, private_post} =
CommonAPI.post(poster, %{status: "hey @#{group.nickname}", visibility: "private"})
assert [] = get_announces_of_object(private_post.object)
end
test "remote groups do not boost any posts", %{poster: poster} do
remote_group =
insert(:user, actor_type: "Group", local: false, nickname: "remote@example.com")
{:ok, post} = CommonAPI.post(poster, %{status: "hey @#{User.full_nickname(remote_group)}"})
assert remote_group.ap_id in post.data["to"]
announces = get_announces_of_object(post.object)
assert [] = announces
end
test "multiple groups mentioned", %{poster: poster, group: group, other_group: other_group} do
{:ok, post} =
CommonAPI.post(poster, %{status: "hey @#{group.nickname} @#{other_group.nickname}"})
announces = get_announces_of_object(post.object)
assert [_, _] = announces
end
+
+ test "it does not boost if group is blocking poster", %{poster: poster, group: group} do
+ {:ok, _} = CommonAPI.block(group, poster)
+ {:ok, post} = CommonAPI.post(poster, %{status: "hey @#{group.nickname}"})
+
+ announces = get_announces_of_object(post.object)
+ assert [] = announces
+ end
end
end

File Metadata

Mime Type
text/x-diff
Expires
Fri, Oct 30, 2:21 PM (17 h, 44 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1800201
Default Alt Text
(124 KB)

Event Timeline