Page MenuHomePhorge

No OneTemporary

Size
252 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index de2508b93..75f4ba503 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -1,768 +1,768 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Notification do
use Ecto.Schema
alias Ecto.Multi
alias Pleroma.Activity
alias Pleroma.FollowingRelationship
alias Pleroma.Marker
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.ThreadMute
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.Push
alias Pleroma.Web.Streamer
import Ecto.Query
import Ecto.Changeset
require Logger
@type t :: %__MODULE__{}
@include_muted_option :with_muted
schema "notifications" do
field(:seen, :boolean, default: false)
# This is an enum type in the database. If you add a new notification type,
# remember to add a migration to add it to the `notifications_type` enum
# as well.
field(:type, :string)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
timestamps()
end
def update_notification_type(user, activity) do
with %__MODULE__{} = notification <-
Repo.get_by(__MODULE__, user_id: user.id, activity_id: activity.id) do
type =
activity
|> type_from_activity()
notification
|> changeset(%{type: type})
|> Repo.update()
end
end
@spec unread_notifications_count(User.t()) :: integer()
def unread_notifications_count(%User{id: user_id}) do
from(q in __MODULE__,
where: q.user_id == ^user_id and q.seen == false
)
|> Repo.aggregate(:count, :id)
end
@notification_types ~w{
favourite
follow
follow_request
mention
move
pleroma:chat_mention
pleroma:emoji_reaction
pleroma:report
reblog
poll
status
}
def changeset(%Notification{} = notification, attrs) do
notification
|> cast(attrs, [:seen, :type])
|> validate_inclusion(:type, @notification_types)
end
@spec last_read_query(User.t()) :: Ecto.Queryable.t()
def last_read_query(user) do
from(q in Pleroma.Notification,
where: q.user_id == ^user.id,
where: q.seen == true,
select: type(q.id, :string),
limit: 1,
order_by: fragment("? desc nulls last", q.id)
)
end
defp for_user_query_ap_id_opts(user, opts) do
ap_id_relationships =
[:block] ++
if opts[@include_muted_option], do: [], else: [:notification_mute]
preloaded_ap_ids = User.outgoing_relationships_ap_ids(user, ap_id_relationships)
exclude_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts)
exclude_notification_muted_opts =
Map.merge(%{notification_muted_users_ap_ids: preloaded_ap_ids[:notification_mute]}, opts)
{exclude_blocked_opts, exclude_notification_muted_opts}
end
def for_user_query(user, opts \\ %{}) do
{exclude_blocked_opts, exclude_notification_muted_opts} =
for_user_query_ap_id_opts(user, opts)
Notification
|> where(user_id: ^user.id)
|> join(:inner, [n], activity in assoc(n, :activity))
|> join(:left, [n, a], object in Object,
on:
fragment(
"(?->>'id') = associated_object_id(?)",
object.data,
a.data
)
)
|> join(:inner, [_n, a], u in User, on: u.ap_id == a.actor, as: :user_actor)
|> preload([n, a, o], activity: {a, object: o})
|> where([user_actor: user_actor], user_actor.is_active)
|> exclude_notification_muted(user, exclude_notification_muted_opts)
|> exclude_blocked(user, exclude_blocked_opts)
|> exclude_blockers(user)
|> exclude_filtered(user)
|> exclude_visibility(opts)
end
# Excludes blocked users and non-followed domain-blocked users
defp exclude_blocked(query, user, opts) do
blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
query
|> where([..., user_actor: user_actor], user_actor.ap_id not in ^blocked_ap_ids)
|> FollowingRelationship.keep_following_or_not_domain_blocked(user)
end
defp exclude_blockers(query, user) do
if Pleroma.Config.get([:activitypub, :blockers_visible]) == true do
query
else
blocker_ap_ids = User.incoming_relationships_ungrouped_ap_ids(user, [:block])
query
|> where([..., user_actor: user_actor], user_actor.ap_id not in ^blocker_ap_ids)
end
end
defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do
query
end
defp exclude_notification_muted(query, user, opts) do
notification_muted_ap_ids =
opts[:notification_muted_users_ap_ids] || User.notification_muted_users_ap_ids(user)
query
|> where([..., user_actor: user_actor], user_actor.ap_id not in ^notification_muted_ap_ids)
|> join(:left, [n, a], tm in ThreadMute,
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data),
as: :thread_mute
)
|> where([thread_mute: thread_mute], is_nil(thread_mute.user_id))
end
defp exclude_filtered(query, user) do
case Pleroma.Filter.compose_regex(user) do
nil ->
query
regex ->
from([_n, a, o] in query,
where:
fragment("not(?->>'content' ~* ?)", o.data, ^regex) or
fragment("?->>'content' is null", o.data) or
fragment("?->>'actor' = ?", o.data, ^user.ap_id)
)
end
end
@valid_visibilities ~w[direct unlisted public private]
defp exclude_visibility(query, %{exclude_visibilities: visibility})
when is_list(visibility) do
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
query
|> join(:left, [n, a], mutated_activity in Pleroma.Activity,
on:
fragment(
"associated_object_id(?)",
a.data
) ==
fragment(
"associated_object_id(?)",
mutated_activity.data
) and
fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
fragment("?->>'type'", mutated_activity.data) == "Create",
as: :mutated_activity
)
|> where(
[n, a, mutated_activity: mutated_activity],
not fragment(
"""
CASE WHEN (?->>'type') = 'Like' or (?->>'type') = 'Announce'
THEN (activity_visibility(?, ?, ?) = ANY (?))
ELSE (activity_visibility(?, ?, ?) = ANY (?)) END
""",
a.data,
a.data,
mutated_activity.actor,
mutated_activity.recipients,
mutated_activity.data,
^visibility,
a.actor,
a.recipients,
a.data,
^visibility
)
)
else
Logger.error("Could not exclude visibility to #{visibility}")
query
end
end
defp exclude_visibility(query, %{exclude_visibilities: visibility})
when visibility in @valid_visibilities do
exclude_visibility(query, [visibility])
end
defp exclude_visibility(query, %{exclude_visibilities: visibility})
when visibility not in @valid_visibilities do
Logger.error("Could not exclude visibility to #{visibility}")
query
end
defp exclude_visibility(query, _visibility), do: query
def for_user(user, opts \\ %{}) do
user
|> for_user_query(opts)
|> Pagination.fetch_paginated(opts)
end
@doc """
Returns notifications for user received since given date.
## Examples
iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-13 11:22:33])
[%Pleroma.Notification{}, %Pleroma.Notification{}]
iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-15 11:22:33])
[]
"""
@spec for_user_since(Pleroma.User.t(), NaiveDateTime.t()) :: [t()]
def for_user_since(user, date) do
from(n in for_user_query(user),
where: n.updated_at > ^date
)
|> Repo.all()
end
def set_read_up_to(%{id: user_id} = user, id) do
query =
from(
n in Notification,
where: n.user_id == ^user_id,
where: n.id <= ^id,
where: n.seen == false,
# Ideally we would preload object and activities here
# but Ecto does not support preloads in update_all
select: n.id
)
Multi.new()
|> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
|> Marker.multi_set_last_read_id(user, "notifications")
|> Repo.transaction()
end
@spec read_one(User.t(), String.t()) ::
{:ok, Notification.t()} | {:error, Ecto.Changeset.t()} | nil
def read_one(%User{} = user, notification_id) do
with {:ok, %Notification{} = notification} <- get(user, notification_id) do
Multi.new()
|> Multi.update(:update, changeset(notification, %{seen: true}))
|> Marker.multi_set_last_read_id(user, "notifications")
|> Repo.transaction()
end
end
def get(%{id: user_id} = _user, id) do
query =
from(
n in Notification,
where: n.id == ^id,
join: activity in assoc(n, :activity),
preload: [activity: activity]
)
notification = Repo.one(query)
case notification do
%{user_id: ^user_id} ->
{:ok, notification}
_ ->
{:error, "Cannot get notification"}
end
end
def clear(user) do
from(n in Notification, where: n.user_id == ^user.id)
|> Repo.delete_all()
end
def destroy_multiple(%{id: user_id} = _user, ids) do
from(n in Notification,
where: n.id in ^ids,
where: n.user_id == ^user_id
)
|> Repo.delete_all()
end
def dismiss(%Pleroma.Activity{} = activity) do
Notification
|> where([n], n.activity_id == ^activity.id)
|> Repo.delete_all()
|> case do
{_, notifications} -> {:ok, notifications}
_ -> {:error, "Cannot dismiss notification"}
end
end
def dismiss(%{id: user_id} = _user, id) do
notification = Repo.get(Notification, id)
case notification do
%{user_id: ^user_id} ->
Repo.delete(notification)
_ ->
{:error, "Cannot dismiss notification"}
end
end
@spec create_notifications(Activity.t()) :: {:ok, [Notification.t()] | []}
def create_notifications(activity)
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
object = Object.normalize(activity, fetch: false)
if object && object.data["type"] == "Answer" do
{:ok, []}
else
do_create_notifications(activity)
end
end
def create_notifications(%Activity{data: %{"type" => type}} = activity)
when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag", "Update"] do
do_create_notifications(activity)
end
def create_notifications(_), do: {:ok, []}
defp do_create_notifications(%Activity{} = activity) do
enabled_receivers = get_notified_from_activity(activity)
enabled_subscribers = get_notified_subscribers_from_activity(activity)
notifications =
(Enum.map(enabled_receivers, fn user ->
create_notification(activity, user)
end) ++
Enum.map(enabled_subscribers -- enabled_receivers, fn user ->
create_notification(activity, user, type: "status")
end))
|> Enum.reject(&is_nil/1)
{:ok, notifications}
end
defp type_from_activity(%{data: %{"type" => type}} = activity) do
case type do
"Follow" ->
if Activity.follow_accepted?(activity) do
"follow"
else
"follow_request"
end
"Announce" ->
"reblog"
"Like" ->
"favourite"
"Move" ->
"move"
"EmojiReact" ->
"pleroma:emoji_reaction"
"Flag" ->
"pleroma:report"
# Compatibility with old reactions
"EmojiReaction" ->
"pleroma:emoji_reaction"
"Create" ->
activity
|> type_from_activity_object()
"Update" ->
"update"
t ->
raise "No notification type for activity type #{t}"
end
end
defp type_from_activity_object(%{data: %{"type" => "Create", "object" => %{}}}), do: "mention"
defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do
object = Object.get_by_ap_id(activity.data["object"])
case object && object.data["type"] do
"ChatMessage" -> "pleroma:chat_mention"
_ -> "mention"
end
end
# TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user, opts \\ []) do
type = Keyword.get(opts, :type, type_from_activity(activity))
unless skip?(activity, user, opts) do
{:ok, %{notification: notification}} =
Multi.new()
|> Multi.insert(:notification, %Notification{
user_id: user.id,
activity: activity,
seen: mark_as_read?(activity, user),
type: type
})
|> Marker.multi_set_last_read_id(user, "notifications")
|> Repo.transaction()
notification
end
end
def create_poll_notifications(%Activity{} = activity) do
with %Object{data: %{"type" => "Question", "actor" => actor} = data} <-
Object.normalize(activity) do
voters =
case data do
%{"voters" => voters} when is_list(voters) -> voters
_ -> []
end
notifications =
Enum.reduce([actor | voters], [], fn ap_id, acc ->
with %User{local: true} = user <- User.get_by_ap_id(ap_id) do
[create_notification(activity, user, type: "poll") | acc]
else
_ -> acc
end
end)
{:ok, notifications}
end
end
@doc """
Returns a tuple with 2 elements:
{notification-enabled receivers, currently disabled receivers (blocking / [thread] muting)}
NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1
"""
@spec get_notified_from_activity(Activity.t(), boolean()) :: list(User.t())
def get_notified_from_activity(activity, local_only \\ true)
def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
when type in [
"Create",
"Like",
"Announce",
"Follow",
"Move",
"EmojiReact",
"Flag",
"Update"
] do
potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
potential_receivers =
User.get_users_from_set(potential_receiver_ap_ids, local_only: local_only)
notification_enabled_ap_ids =
potential_receiver_ap_ids
|> exclude_domain_blocker_ap_ids(activity, potential_receivers)
|> exclude_relationship_restricted_ap_ids(activity)
|> exclude_thread_muter_ap_ids(activity)
Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
end
def get_notified_from_activity(_, _local_only), do: []
def get_notified_subscribers_from_activity(activity, local_only \\ true)
def get_notified_subscribers_from_activity(
%Activity{data: %{"type" => "Create"}} = activity,
local_only
) do
notification_enabled_ap_ids =
[]
|> Utils.maybe_notify_subscribers(activity)
potential_receivers =
User.get_users_from_set(notification_enabled_ap_ids, local_only: local_only)
Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
end
def get_notified_subscribers_from_activity(_, _), do: []
# For some activities, only notify the author of the object
def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_id}})
when type in ~w{Like Announce EmojiReact} do
case Object.get_cached_by_ap_id(object_id) do
%Object{data: %{"actor" => actor}} ->
[actor]
_ ->
[]
end
end
def get_potential_receiver_ap_ids(%{data: %{"type" => "Follow", "object" => object_id}}) do
[object_id]
end
def get_potential_receiver_ap_ids(%{data: %{"type" => "Flag", "actor" => actor}}) do
(User.all_users_with_privilege(:reports_manage_reports)
|> Enum.map(fn user -> user.ap_id end)) --
[actor]
end
# Update activity: notify all who repeated this
def get_potential_receiver_ap_ids(%{data: %{"type" => "Update", "actor" => actor}} = activity) do
with %Object{data: %{"id" => object_id}} <- Object.normalize(activity, fetch: false) do
repeaters =
Activity.Queries.by_type("Announce")
|> Activity.Queries.by_object_id(object_id)
|> Activity.with_joined_user_actor()
|> where([a, u], u.local)
|> select([a, u], u.ap_id)
|> Repo.all()
repeaters -- [actor]
end
end
def get_potential_receiver_ap_ids(activity) do
[]
|> Utils.maybe_notify_to_recipients(activity)
|> Utils.maybe_notify_mentioned_recipients(activity)
|> Utils.maybe_notify_followers(activity)
|> Enum.uniq()
end
@doc "Filters out AP IDs domain-blocking and not following the activity's actor"
def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])
def exclude_domain_blocker_ap_ids([], _activity, _preloaded_users), do: []
def exclude_domain_blocker_ap_ids(ap_ids, %Activity{} = activity, preloaded_users) do
activity_actor_domain = activity.actor && URI.parse(activity.actor).host
users =
ap_ids
|> Enum.map(fn ap_id ->
Enum.find(preloaded_users, &(&1.ap_id == ap_id)) ||
User.get_cached_by_ap_id(ap_id)
end)
|> Enum.filter(& &1)
domain_blocker_ap_ids = for u <- users, activity_actor_domain in u.domain_blocks, do: u.ap_id
domain_blocker_follower_ap_ids =
if Enum.any?(domain_blocker_ap_ids) do
activity
|> Activity.user_actor()
|> FollowingRelationship.followers_ap_ids(domain_blocker_ap_ids)
else
[]
end
ap_ids
|> Kernel.--(domain_blocker_ap_ids)
|> Kernel.++(domain_blocker_follower_ap_ids)
end
@doc "Filters out AP IDs of users basing on their relationships with activity actor user"
def exclude_relationship_restricted_ap_ids([], _activity), do: []
def exclude_relationship_restricted_ap_ids(ap_ids, %Activity{} = activity) do
relationship_restricted_ap_ids =
activity
|> Activity.user_actor()
|> User.incoming_relationships_ungrouped_ap_ids([
:block,
:notification_mute
])
Enum.uniq(ap_ids) -- relationship_restricted_ap_ids
end
@doc "Filters out AP IDs of users who mute activity thread"
def exclude_thread_muter_ap_ids([], _activity), do: []
def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do
thread_muter_ap_ids = ThreadMute.muter_ap_ids(activity.data["context"])
Enum.uniq(ap_ids) -- thread_muter_ap_ids
end
def skip?(activity, user, opts \\ [])
@spec skip?(Activity.t(), User.t(), Keyword.t()) :: boolean()
def skip?(%Activity{} = activity, %User{} = user, opts) do
[
:self,
:internal,
:invisible,
:block_from_strangers,
:recently_followed,
:filtered
]
|> Enum.find(&skip?(&1, activity, user, opts))
end
def skip?(_activity, _user, _opts), do: false
@spec skip?(atom(), Activity.t(), User.t(), Keyword.t()) :: boolean()
def skip?(:self, %Activity{} = activity, %User{} = user, opts) do
cond do
opts[:type] == "poll" -> false
activity.data["actor"] == user.ap_id -> true
true -> false
end
end
def skip?(:internal, %Activity{} = activity, _user, _opts) do
actor = activity.data["actor"]
user = User.get_cached_by_ap_id(actor)
User.internal?(user)
end
def skip?(:invisible, %Activity{} = activity, _user, _opts) do
actor = activity.data["actor"]
user = User.get_cached_by_ap_id(actor)
User.invisible?(user)
end
def skip?(
:block_from_strangers,
%Activity{} = activity,
%User{notification_settings: %{block_from_strangers: true}} = user,
opts
) do
actor = activity.data["actor"]
follower = User.get_cached_by_ap_id(actor)
cond do
opts[:type] == "poll" -> false
user.ap_id == actor -> false
!User.following?(user, follower) -> true
true -> false
end
end
# To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
def skip?(
:recently_followed,
%Activity{data: %{"type" => "Follow"}} = activity,
%User{} = user,
_opts
) do
actor = activity.data["actor"]
Notification.for_user(user)
|> Enum.any?(fn
%{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
_ -> false
end)
end
def skip?(:filtered, %{data: %{"type" => type}}, _user, _opts) when type in ["Follow", "Move"],
do: false
def skip?(:filtered, activity, user, _opts) do
object = Object.normalize(activity, fetch: false)
cond do
is_nil(object) ->
false
object.data["actor"] == user.ap_id ->
false
not is_nil(regex = Pleroma.Filter.compose_regex(user, :re)) ->
Regex.match?(regex, object.data["content"])
true ->
false
end
end
def skip?(_type, _activity, _user, _opts), do: false
def mark_as_read?(activity, target_user) do
user = Activity.user_actor(activity)
- User.mutes_user?(target_user, user) || CommonAPI.thread_muted?(target_user, activity)
+ User.mutes_user?(target_user, user) || CommonAPI.thread_muted?(activity, target_user)
end
def for_user_and_activity(user, activity) do
from(n in __MODULE__,
where: n.user_id == ^user.id,
where: n.activity_id == ^activity.id
)
|> Repo.one()
end
@spec mark_context_as_read(User.t(), String.t()) :: {integer(), nil | [term()]}
def mark_context_as_read(%User{id: id}, context) do
from(
n in Notification,
join: a in assoc(n, :activity),
where: n.user_id == ^id,
where: n.seen == false,
where: fragment("?->>'context'", a.data) == ^context
)
|> Repo.update_all(set: [seen: true])
end
@doc "Streams a list of notifications over websockets and web push"
@spec stream(list(Notification.t())) :: :ok
def stream(notifications) do
Enum.each(notifications, fn notification ->
Streamer.stream(["user", "user:notification"], notification)
Push.send(notification)
end)
end
end
diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex
index b5ba081b8..f26cd8c1e 100644
--- a/lib/pleroma/web/common_api.ex
+++ b/lib/pleroma/web/common_api.ex
@@ -1,726 +1,726 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity
alias Pleroma.Conversation.Participation
alias Pleroma.Formatter
alias Pleroma.ModerationLog
alias Pleroma.Object
alias Pleroma.Rule
alias Pleroma.ThreadMute
alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI.ActivityDraft
import Ecto.Query, only: [where: 3]
import Pleroma.Web.Gettext
import Pleroma.Web.CommonAPI.Utils
require Pleroma.Constants
require Logger
@spec block(User.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
def block(blocker, blocked) do
with {:ok, block_data, _} <- Builder.block(blocker, blocked),
{:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
{:ok, block}
end
end
@spec post_chat_message(User.t(), User.t(), String.t(), list()) ::
{:ok, Activity.t()} | {:error, any()}
def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
:ok <- validate_chat_attachment_attribution(maybe_attachment, user),
:ok <- validate_chat_content_length(content, !!maybe_attachment),
{_, {:ok, chat_message_data, _meta}} <-
{:build_object,
Builder.chat_message(
user,
recipient.ap_id,
content |> format_chat_content,
attachment: maybe_attachment
)},
{_, {:ok, create_activity_data, _meta}} <-
{:build_create_activity, Builder.create(user, chat_message_data, [recipient.ap_id])},
{_, {:ok, %Activity{} = activity, _meta}} <-
{:common_pipeline,
Pipeline.common_pipeline(create_activity_data,
local: true,
idempotency_key: opts[:idempotency_key]
)} do
{:ok, activity}
else
{:common_pipeline, {:reject, _} = e} -> e
e -> e
end
end
defp format_chat_content(nil), do: nil
defp format_chat_content(content) do
{text, _, _} =
content
|> Formatter.html_escape("text/plain")
|> Formatter.linkify()
|> (fn {text, mentions, tags} ->
{String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
end).()
text
end
defp validate_chat_attachment_attribution(nil, _), do: :ok
defp validate_chat_attachment_attribution(attachment, user) do
with :ok <- Object.authorize_access(attachment, user) do
:ok
else
e ->
e
end
end
defp validate_chat_content_length(_, true), do: :ok
defp validate_chat_content_length(nil, false), do: {:error, :no_content}
defp validate_chat_content_length(content, _) do
if String.length(content) <= Pleroma.Config.get([:instance, :chat_limit]) do
:ok
else
{:error, :content_too_long}
end
end
@spec unblock(User.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
def unblock(blocker, blocked) do
with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
{:ok, unblock_data, _} <- Builder.undo(blocker, block),
{:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
{:ok, unblock}
else
{:fetch_block, nil} ->
if User.blocks?(blocker, blocked) do
User.unblock(blocker, blocked)
{:ok, :no_activity}
else
{:error, :not_blocking}
end
e ->
e
end
end
@spec follow(User.t(), User.t()) ::
{:ok, User.t(), User.t(), Activity.t() | Object.t()} | {:error, :rejected}
def follow(follower, followed) do
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
with {:ok, follow_data, _} <- Builder.follow(follower, followed),
{:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true),
{:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
if activity.data["state"] == "reject" do
{:error, :rejected}
else
{:ok, follower, followed, activity}
end
end
end
@spec unfollow(User.t(), User.t()) :: {:ok, User.t()} | {:error, any()}
def unfollow(follower, unfollowed) do
with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
{:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
{:ok, _subscription} <- User.unsubscribe(follower, unfollowed),
{:ok, _endorsement} <- User.unendorse(follower, unfollowed) do
{:ok, follower}
end
end
@spec accept_follow_request(User.t(), User.t()) :: {:ok, User.t()} | {:error, any()}
def accept_follow_request(follower, followed) do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, accept_data, _} <- Builder.accept(followed, follow_activity),
{:ok, _activity, _} <- Pipeline.common_pipeline(accept_data, local: true) do
{:ok, follower}
end
end
@spec reject_follow_request(User.t(), User.t()) :: {:ok, User.t()} | {:error, any()} | nil
def reject_follow_request(follower, followed) do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, reject_data, _} <- Builder.reject(followed, follow_activity),
{:ok, _activity, _} <- Pipeline.common_pipeline(reject_data, local: true) do
{:ok, follower}
end
end
@spec delete(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
def delete(activity_id, user) do
with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(activity_id, filter: [])},
{_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(activity)},
{_, %Object{} = object, _} <-
{:find_object, Object.normalize(activity, fetch: false), activity},
true <- User.privileged?(user, :messages_delete) || user.ap_id == object.data["actor"],
{:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
{:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
if User.privileged?(user, :messages_delete) and user.ap_id != object.data["actor"] do
action =
if object.data["type"] == "ChatMessage" do
"chat_message_delete"
else
"status_delete"
end
ModerationLog.insert_log(%{
action: action,
actor: user,
subject_id: activity_id
})
end
{:ok, delete}
else
{:find_activity, _} ->
{:error, :not_found}
{:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
# We have the create activity, but not the object, it was probably pruned.
# Insert a tombstone and try again
with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
{:ok, _tombstone} <- Object.create(tombstone_data) do
delete(activity_id, user)
else
_ ->
Logger.error(
"Could not insert tombstone for missing object on deletion. Object is #{object}."
)
{:error, dgettext("errors", "Could not delete")}
end
_ ->
{:error, dgettext("errors", "Could not delete")}
end
end
@spec repeat(String.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
def repeat(id, user, params \\ %{}) do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
object = %Object{} <- Object.normalize(activity, fetch: false),
{_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
public = public_announce?(object, params),
{:ok, announce, _} <- Builder.announce(user, object, public: public),
{:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
{:ok, activity}
else
{:existing_announce, %Activity{} = announce} ->
{:ok, announce}
_ ->
{:error, :not_found}
end
end
@spec unrepeat(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
def unrepeat(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)},
%Object{} = note <- Object.normalize(activity, fetch: false),
%Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
{_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(announce)},
{:ok, undo, _} <- Builder.undo(user, announce),
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
{:ok, activity}
else
{:find_activity, _} -> {:error, :not_found}
_ -> {:error, dgettext("errors", "Could not unrepeat")}
end
end
@spec favorite(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
def favorite(id, %User{} = user) do
case favorite_helper(user, id) do
{:ok, _} = res ->
res
{:error, :not_found} = res ->
res
{:error, e} ->
Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
{:error, dgettext("errors", "Could not favorite")}
end
end
defp favorite_helper(user, id) do
with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
{_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
{_, {:ok, %Activity{} = activity, _meta}} <-
{:common_pipeline,
Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
{:ok, activity}
else
{:find_object, _} ->
{:error, :not_found}
{:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->
if {:object, {"already liked by this actor", []}} in changeset.errors do
{:ok, :already_liked}
else
{:error, e}
end
e ->
{:error, e}
end
end
@spec unfavorite(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
def unfavorite(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)},
%Object{} = note <- Object.normalize(activity, fetch: false),
%Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
{_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(like)},
{:ok, undo, _} <- Builder.undo(user, like),
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
{:ok, activity}
else
{:find_activity, _} -> {:error, :not_found}
_ -> {:error, dgettext("errors", "Could not unfavorite")}
end
end
@spec react_with_emoji(String.t(), User.t(), String.t()) ::
{:ok, Activity.t()} | {:error, any()}
def react_with_emoji(id, user, emoji) do
with %Activity{} = activity <- Activity.get_by_id(id),
object <- Object.normalize(activity, fetch: false),
{:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
{:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
{:ok, activity}
else
_ ->
{:error, dgettext("errors", "Could not add reaction emoji")}
end
end
@spec unreact_with_emoji(String.t(), User.t(), String.t()) ::
{:ok, Activity.t()} | {:error, any()}
def unreact_with_emoji(id, user, emoji) do
with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
{_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(reaction_activity)},
{:ok, undo, _} <- Builder.undo(user, reaction_activity),
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
{:ok, activity}
else
_ ->
{:error, dgettext("errors", "Could not remove reaction emoji")}
end
end
@spec vote(User.t(), Object.t(), list()) :: {:ok, list(), Object.t()} | {:error, any()}
def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
with :ok <- validate_not_author(object, user),
:ok <- validate_existing_votes(user, object),
{:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
answer_activities =
Enum.map(choices, fn index ->
{:ok, answer_object, _meta} =
Builder.answer(user, object, Enum.at(options, index)["name"])
{:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
{:ok, activity, _meta} =
activity_data
|> Map.put("cc", answer_object["cc"])
|> Map.put("context", answer_object["context"])
|> Pipeline.common_pipeline(local: true)
# TODO: Do preload of Pleroma.Object in Pipeline
Activity.normalize(activity.data)
end)
object = Object.get_cached_by_ap_id(object.data["id"])
{:ok, answer_activities, object}
end
end
defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
do: {:error, dgettext("errors", "Poll's author can't vote")}
defp validate_not_author(_, _), do: :ok
defp validate_existing_votes(%{ap_id: ap_id}, object) do
if Utils.get_existing_votes(ap_id, object) == [] do
:ok
else
{:error, dgettext("errors", "Already voted")}
end
end
defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
when is_list(any_of) and any_of != [],
do: {any_of, Enum.count(any_of)}
defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
when is_list(one_of) and one_of != [],
do: {one_of, 1}
defp normalize_and_validate_choices(choices, object) do
choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
{options, max_count} = get_options_and_max_count(object)
count = Enum.count(options)
with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
{_, true} <- {:count_check, Enum.count(choices) <= max_count} do
{:ok, options, choices}
else
{:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
{:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
end
end
defp public_announce?(_, %{visibility: visibility})
when visibility in ~w{public unlisted private direct},
do: visibility in ~w(public unlisted)
defp public_announce?(object, _) do
Visibility.public?(object)
end
@spec get_visibility(map(), map() | nil, Participation.t() | nil) ::
{String.t() | nil, String.t() | nil}
def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
def get_visibility(%{visibility: visibility}, in_reply_to, _)
when visibility in ~w{public local unlisted private direct},
do: {visibility, get_replied_to_visibility(in_reply_to)}
def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
visibility = {:list, String.to_integer(list_id)}
{visibility, get_replied_to_visibility(in_reply_to)}
end
def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
visibility = get_replied_to_visibility(in_reply_to)
{visibility, visibility}
end
def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
@spec get_replied_to_visibility(Activity.t() | nil) :: String.t() | nil
def get_replied_to_visibility(nil), do: nil
def get_replied_to_visibility(activity) do
with %Object{} = object <- Object.normalize(activity, fetch: false) do
Visibility.get_visibility(object)
end
end
@spec check_expiry_date({:ok, nil | integer()} | String.t()) ::
{:ok, boolean() | nil} | {:error, String.t()}
def check_expiry_date({:ok, nil} = res), do: res
def check_expiry_date({:ok, in_seconds}) do
expiry = DateTime.add(DateTime.utc_now(), in_seconds)
if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
{:ok, expiry}
else
{:error, "Expiry date is too soon"}
end
end
def check_expiry_date(expiry_str) do
Ecto.Type.cast(:integer, expiry_str)
|> check_expiry_date()
end
@spec listen(User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
def listen(user, data) do
with {:ok, draft} <- ActivityDraft.listen(user, data) do
ActivityPub.listen(draft.changes)
end
end
@spec post(User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
def post(user, %{status: _} = data) do
with {:ok, draft} <- ActivityDraft.create(user, data) do
ActivityPub.create(draft.changes, draft.preview?)
end
end
@spec update(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
def update(orig_activity, %User{} = user, changes) do
with orig_object <- Object.normalize(orig_activity),
{:ok, new_object} <- make_update_data(user, orig_object, changes),
{:ok, update_data, _} <- Builder.update(user, new_object),
{:ok, update, _} <- Pipeline.common_pipeline(update_data, local: true) do
{:ok, update}
else
_ -> {:error, nil}
end
end
defp make_update_data(user, orig_object, changes) do
kept_params = %{
visibility: Visibility.get_visibility(orig_object),
in_reply_to_id:
with replied_id when is_binary(replied_id) <- orig_object.data["inReplyTo"],
%Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(replied_id) do
activity_id
else
_ -> nil
end
}
params = Map.merge(changes, kept_params)
with {:ok, draft} <- ActivityDraft.create(user, params) do
change =
Object.Updater.make_update_object_data(orig_object.data, draft.object, Utils.make_date())
{:ok, change}
else
_ -> {:error, nil}
end
end
@spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
def pin(id, %User{} = user) do
with %Activity{} = activity <- create_activity_by_id(id),
true <- activity_belongs_to_actor(activity, user.ap_id),
true <- object_type_is_allowed_for_pin(activity.object),
true <- activity_is_public(activity),
{:ok, pin_data, _} <- Builder.pin(user, activity.object),
{:ok, _pin, _} <-
Pipeline.common_pipeline(pin_data,
local: true,
activity_id: id
) do
{:ok, activity}
else
{:error, {:side_effects, error}} -> error
error -> error
end
end
defp create_activity_by_id(id) do
with nil <- Activity.create_by_id_with_object(id) do
{:error, :not_found}
end
end
defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
with false <- type in ["Note", "Article", "Question"] do
{:error, :not_allowed}
end
end
defp activity_is_public(activity) do
with false <- Visibility.public?(activity) do
{:error, :visibility_error}
end
end
@spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
def unpin(id, user) do
with %Activity{} = activity <- create_activity_by_id(id),
{:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
{:ok, _unpin, _} <-
Pipeline.common_pipeline(unpin_data,
local: true,
activity_id: activity.id,
expires_at: activity.data["expires_at"],
featured_address: user.featured_address
) do
{:ok, activity}
end
end
@spec add_mute(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
def add_mute(activity, user, params \\ %{}) do
expires_in = Map.get(params, :expires_in, 0)
with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
_ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
if expires_in > 0 do
Pleroma.Workers.MuteExpireWorker.enqueue(
"unmute_conversation",
%{"user_id" => user.id, "activity_id" => activity.id},
schedule_in: expires_in
)
end
{:ok, activity}
else
{:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
end
end
@spec remove_mute(Activity.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
def remove_mute(%Activity{} = activity, %User{} = user) do
ThreadMute.remove_mute(user.id, activity.data["context"])
{:ok, activity}
end
@spec remove_mute(String.t(), String.t()) :: {:ok, Activity.t()} | {:error, any()}
def remove_mute(activity_id, user_id) do
with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
{:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
remove_mute(activity, user)
else
{what, result} = error ->
Logger.warning(
"CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}"
)
{:error, error}
end
end
- @spec thread_muted?(User.t(), Activity.t()) :: boolean()
- def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
+ @spec thread_muted?(Activity.t(), User.t()) :: boolean()
+ def thread_muted?(%{data: %{"context" => context}}, %User{id: user_id})
when is_binary(context) do
ThreadMute.exists?(user_id, context)
end
def thread_muted?(_, _), do: false
@spec report(User.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
def report(user, data) do
with {:ok, account} <- get_reported_account(data.account_id),
{:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
{:ok, statuses} <- get_report_statuses(account, data),
rules <- get_report_rules(Map.get(data, :rule_ids, nil)) do
ActivityPub.flag(%{
context: Utils.generate_context_id(),
actor: user,
account: account,
statuses: statuses,
content: content_html,
forward: Map.get(data, :forward, false),
rules: rules
})
end
end
defp get_reported_account(account_id) do
case User.get_cached_by_id(account_id) do
%User{} = account -> {:ok, account}
_ -> {:error, dgettext("errors", "Account not found")}
end
end
defp get_report_rules(nil) do
nil
end
defp get_report_rules(rule_ids) do
rule_ids
|> Enum.filter(&Rule.exists?/1)
end
@spec update_report_state(String.t() | [String.t()], String.t()) ::
{:ok, any()} | {:error, any()}
def update_report_state(activity_ids, state) when is_list(activity_ids) do
case Utils.update_report_state(activity_ids, state) do
:ok -> {:ok, activity_ids}
_ -> {:error, dgettext("errors", "Could not update state")}
end
end
def update_report_state(activity_id, state) do
with %Activity{} = activity <- Activity.get_by_id(activity_id, filter: []) do
Utils.update_report_state(activity, state)
else
nil -> {:error, :not_found}
end
end
@spec update_activity_scope(String.t(), map()) :: {:ok, any()} | {:error, any()}
def update_activity_scope(activity_id, opts \\ %{}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
{:ok, activity} <- toggle_sensitive(activity, opts) do
set_visibility(activity, opts)
else
nil -> {:error, :not_found}
end
end
defp toggle_sensitive(activity, %{sensitive: sensitive}) when sensitive in ~w(true false) do
toggle_sensitive(activity, %{sensitive: String.to_existing_atom(sensitive)})
end
defp toggle_sensitive(%Activity{object: object} = activity, %{sensitive: sensitive})
when is_boolean(sensitive) do
new_data = Map.put(object.data, "sensitive", sensitive)
{:ok, object} =
object
|> Object.change(%{data: new_data})
|> Object.update_and_set_cache()
{:ok, Map.put(activity, :object, object)}
end
defp toggle_sensitive(activity, _), do: {:ok, activity}
defp set_visibility(activity, %{visibility: visibility}) do
Utils.update_activity_visibility(activity, visibility)
end
defp set_visibility(activity, _), do: {:ok, activity}
@spec hide_reblogs(User.t(), User.t()) :: {:ok, any()} | {:error, any()}
def hide_reblogs(%User{} = user, %User{} = target) do
UserRelationship.create_reblog_mute(user, target)
end
@spec show_reblogs(User.t(), User.t()) :: {:ok, any()} | {:error, any()}
def show_reblogs(%User{} = user, %User{} = target) do
UserRelationship.delete_reblog_mute(user, target)
end
@spec get_user(String.t(), boolean()) :: User.t() | nil
def get_user(ap_id, fake_record_fallback \\ true) do
cond do
user = User.get_cached_by_ap_id(ap_id) ->
user
user = User.get_by_guessed_nickname(ap_id) ->
user
fake_record_fallback ->
# TODO: refactor (fake records is never a good idea)
User.error_user(ap_id)
true ->
nil
end
end
defp maybe_cancel_jobs(%Activity{data: %{"id" => ap_id}}) do
Oban.Job
|> where([j], j.worker == "Pleroma.Workers.PublisherWorker")
|> where([j], j.args["op"] == "publish_one")
|> where([j], j.args["params"]["id"] == ^ap_id)
|> Oban.cancel_all_jobs()
end
defp maybe_cancel_jobs(_), do: {:ok, 0}
end
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index d9d7e516a..747638c53 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -1,850 +1,850 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusView do
use Pleroma.Web, :view
require Pleroma.Constants
alias Pleroma.Activity
alias Pleroma.HTML
alias Pleroma.Maps
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.PollView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.PleromaAPI.EmojiReactionController
alias Pleroma.Web.RichMedia.Card
import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2]
# This is a naive way to do this, just spawning a process per activity
# to fetch the preview. However it should be fine considering
# pagination is restricted to 40 activities at a time
defp fetch_rich_media_for_activities(activities) do
Enum.each(activities, fn activity ->
Card.get_by_activity(activity)
end)
end
# TODO: Add cached version.
defp get_replied_to_activities([]), do: %{}
defp get_replied_to_activities(activities) do
activities
|> Enum.map(fn
%{data: %{"type" => "Create"}} = activity ->
object = Object.normalize(activity, fetch: false)
object && object.data["inReplyTo"] != "" && object.data["inReplyTo"]
_ ->
nil
end)
|> Enum.filter(& &1)
|> Activity.create_by_object_ap_id_with_object()
|> Repo.all()
|> Enum.reduce(%{}, fn activity, acc ->
object = Object.normalize(activity, fetch: false)
if object, do: Map.put(acc, object.data["id"], activity), else: acc
end)
end
defp get_quoted_activities([]), do: %{}
defp get_quoted_activities(activities) do
activities
|> Enum.map(fn
%{data: %{"type" => "Create"}} = activity ->
object = Object.normalize(activity, fetch: false)
object && object.data["quoteUrl"] != "" && object.data["quoteUrl"]
_ ->
nil
end)
|> Enum.filter(& &1)
|> Activity.create_by_object_ap_id_with_object()
|> Repo.all()
|> Enum.reduce(%{}, fn activity, acc ->
object = Object.normalize(activity, fetch: false)
if object, do: Map.put(acc, object.data["id"], activity), else: acc
end)
end
# DEPRECATED This field seems to be a left-over from the StatusNet era.
# If your application uses `pleroma.conversation_id`: this field is deprecated.
# It is currently stubbed instead by doing a CRC32 of the context, and
# clearing the MSB to avoid overflow exceptions with signed integers on the
# different clients using this field (Java/Kotlin code, mostly; see Husky.)
# This should be removed in a future version of Pleroma. Pleroma-FE currently
# depends on this field, as well.
defp get_context_id(%{data: %{"context" => context}}) when is_binary(context) do
import Bitwise
:erlang.crc32(context)
|> band(bnot(0x8000_0000))
end
defp get_context_id(_), do: nil
# Check if the user reblogged this status
defp reblogged?(activity, %User{ap_id: ap_id}) do
with %Object{data: %{"announcements" => announcements}} when is_list(announcements) <-
Object.normalize(activity, fetch: false) do
ap_id in announcements
else
_ -> false
end
end
# False if the user is logged out
defp reblogged?(_activity, _user), do: false
def render("index.json", opts) do
reading_user = opts[:for]
# To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
activities = Enum.filter(opts.activities, & &1)
# Start prefetching rich media before doing anything else
fetch_rich_media_for_activities(activities)
replied_to_activities = get_replied_to_activities(activities)
quoted_activities = get_quoted_activities(activities)
parent_activities =
activities
|> Enum.filter(&(&1.data["type"] == "Announce" && &1.data["object"]))
|> Enum.map(&Object.normalize(&1, fetch: false).data["id"])
|> Activity.create_by_object_ap_id()
|> Activity.with_preloaded_object(:left)
|> Activity.with_preloaded_bookmark(reading_user)
|> Activity.with_set_thread_muted_field(reading_user)
|> Repo.all()
relationships_opt =
cond do
Map.has_key?(opts, :relationships) ->
opts[:relationships]
is_nil(reading_user) ->
UserRelationship.view_relationships_option(nil, [])
true ->
# Note: unresolved users are filtered out
actors =
(activities ++ parent_activities)
|> Enum.map(&CommonAPI.get_user(&1.data["actor"], false))
|> Enum.filter(& &1)
UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes)
end
opts =
opts
|> Map.put(:replied_to_activities, replied_to_activities)
|> Map.put(:quoted_activities, quoted_activities)
|> Map.put(:parent_activities, parent_activities)
|> Map.put(:relationships, relationships_opt)
safe_render_many(activities, StatusView, "show.json", opts)
end
def render(
"show.json",
%{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
) do
user = CommonAPI.get_user(activity.data["actor"])
created_at = Utils.to_masto_date(activity.data["published"])
object = Object.normalize(activity, fetch: false)
reblogged_parent_activity =
if opts[:parent_activities] do
Activity.Queries.find_by_object_ap_id(
opts[:parent_activities],
object.data["id"]
)
else
Activity.create_by_object_ap_id(object.data["id"])
|> Activity.with_preloaded_bookmark(opts[:for])
|> Activity.with_set_thread_muted_field(opts[:for])
|> Repo.one()
end
reblog_rendering_opts = Map.put(opts, :activity, reblogged_parent_activity)
reblogged = render("show.json", reblog_rendering_opts)
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
bookmark = Activity.get_bookmark(reblogged_parent_activity, opts[:for])
bookmark_folder =
if bookmark != nil do
bookmark.folder_id
else
nil
end
mentions =
activity.recipients
|> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
|> Enum.filter(& &1)
|> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
{pinned?, pinned_at} = pin_data(object, user)
%{
id: to_string(activity.id),
uri: object.data["id"],
url: object.data["id"],
account:
AccountView.render("show.json", %{
user: user,
for: opts[:for]
}),
in_reply_to_id: nil,
in_reply_to_account_id: nil,
reblog: reblogged,
content: reblogged[:content] || "",
created_at: created_at,
reblogs_count: 0,
replies_count: 0,
favourites_count: 0,
reblogged: reblogged?(reblogged_parent_activity, opts[:for]),
favourited: present?(favorited),
bookmarked: present?(bookmark),
muted: false,
pinned: pinned?,
sensitive: false,
spoiler_text: "",
visibility: get_visibility(activity),
media_attachments: reblogged[:media_attachments] || [],
mentions: mentions,
tags: reblogged[:tags] || [],
application: build_application(object.data["generator"]),
language: nil,
emojis: [],
pleroma: %{
local: activity.local,
pinned_at: pinned_at,
bookmark_folder: bookmark_folder
}
}
end
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
object = Object.normalize(activity, fetch: false)
user = CommonAPI.get_user(activity.data["actor"])
user_follower_address = user.follower_address
like_count = object.data["like_count"] || 0
announcement_count = object.data["announcement_count"] || 0
hashtags = Object.hashtags(object)
sensitive = object.data["sensitive"] || Enum.member?(hashtags, "nsfw")
tags = Object.tags(object)
tag_mentions =
tags
|> Enum.filter(fn tag -> is_map(tag) and tag["type"] == "Mention" end)
|> Enum.map(fn tag -> tag["href"] end)
mentions =
(object.data["to"] ++ tag_mentions)
|> Enum.uniq()
|> Enum.map(fn
Pleroma.Constants.as_public() -> nil
^user_follower_address -> nil
ap_id -> User.get_cached_by_ap_id(ap_id)
end)
|> Enum.filter(& &1)
|> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
bookmark = Activity.get_bookmark(activity, opts[:for])
bookmark_folder =
if bookmark != nil do
bookmark.folder_id
else
nil
end
client_posted_this_activity = opts[:for] && user.id == opts[:for].id
expires_at =
with true <- client_posted_this_activity,
%Oban.Job{scheduled_at: scheduled_at} <-
Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) do
scheduled_at
else
_ -> nil
end
thread_muted? =
cond do
is_nil(opts[:for]) -> false
is_boolean(activity.thread_muted?) -> activity.thread_muted?
- true -> CommonAPI.thread_muted?(opts[:for], activity)
+ true -> CommonAPI.thread_muted?(activity, opts[:for])
end
attachment_data = object.data["attachment"] || []
attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
created_at = Utils.to_masto_date(object.data["published"])
edited_at =
with %{"updated" => updated} <- object.data,
date <- Utils.to_masto_date(updated),
true <- date != "" do
date
else
_ ->
nil
end
reply_to = get_reply_to(activity, opts)
reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"])
history_len =
1 +
(Object.Updater.history_for(object.data)
|> Map.get("orderedItems")
|> length())
# See render("history.json", ...) for more details
# Here the implicit index of the current content is 0
chrono_order = history_len - 1
quote_activity = get_quote(activity, opts)
quote_id =
case quote_activity do
%Activity{id: id} -> id
_ -> nil
end
quote_post =
if visible_for_user?(quote_activity, opts[:for]) and opts[:show_quote] != false do
quote_rendering_opts = Map.merge(opts, %{activity: quote_activity, show_quote: false})
render("show.json", quote_rendering_opts)
else
nil
end
content =
object
|> render_content()
content_html =
content
|> Activity.HTML.get_cached_scrubbed_html_for_activity(
User.html_filter_policy(opts[:for]),
activity,
"mastoapi:content:#{chrono_order}"
)
content_plaintext =
content
|> Activity.HTML.get_cached_stripped_html_for_activity(
activity,
"mastoapi:content:#{chrono_order}"
)
summary = object.data["summary"] || ""
card =
case Card.get_by_activity(activity) do
%Card{} = result -> render("card.json", result)
_ -> nil
end
url =
if user.local do
Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
else
object.data["url"] || object.data["external_url"] || object.data["id"]
end
direct_conversation_id =
with {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]},
{_, true} <- {:include_id, opts[:with_direct_conversation_id]},
{_, %User{} = for_user} <- {:for_user, opts[:for]} do
Activity.direct_conversation_id(activity, for_user)
else
{:direct_conversation_id, participation_id} when is_integer(participation_id) ->
participation_id
_e ->
nil
end
emoji_reactions =
object
|> Object.get_emoji_reactions()
|> EmojiReactionController.filter_allowed_users(
opts[:for],
Map.get(opts, :with_muted, false)
)
|> Stream.map(fn {emoji, users, url} ->
build_emoji_map(emoji, users, url, opts[:for])
end)
|> Enum.to_list()
# Status muted state (would do 1 request per status unless user mutes are preloaded)
muted =
thread_muted? ||
UserRelationship.exists?(
get_in(opts, [:relationships, :user_relationships]),
:mute,
opts[:for],
user,
fn for_user, user -> User.mutes?(for_user, user) end
)
{pinned?, pinned_at} = pin_data(object, user)
%{
id: to_string(activity.id),
uri: object.data["id"],
url: url,
account:
AccountView.render("show.json", %{
user: user,
for: opts[:for]
}),
in_reply_to_id: reply_to && to_string(reply_to.id),
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
reblog: nil,
card: card,
content: content_html,
text: opts[:with_source] && get_source_text(object.data["source"]),
created_at: created_at,
edited_at: edited_at,
reblogs_count: announcement_count,
replies_count: object.data["repliesCount"] || 0,
favourites_count: like_count,
reblogged: reblogged?(activity, opts[:for]),
favourited: present?(favorited),
bookmarked: present?(bookmark),
muted: muted,
pinned: pinned?,
sensitive: sensitive,
spoiler_text: summary,
visibility: get_visibility(object),
media_attachments: attachments,
poll: render(PollView, "show.json", object: object, for: opts[:for]),
mentions: mentions,
tags: build_tags(tags),
application: build_application(object.data["generator"]),
language: nil,
emojis: build_emojis(object.data["emoji"]),
pleroma: %{
local: activity.local,
conversation_id: get_context_id(activity),
context: object.data["context"],
in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
quote: quote_post,
quote_id: quote_id,
quote_url: object.data["quoteUrl"],
quote_visible: visible_for_user?(quote_activity, opts[:for]),
content: %{"text/plain" => content_plaintext},
spoiler_text: %{"text/plain" => summary},
expires_at: expires_at,
direct_conversation_id: direct_conversation_id,
thread_muted: thread_muted?,
emoji_reactions: emoji_reactions,
parent_visible: visible_for_user?(reply_to, opts[:for]),
pinned_at: pinned_at,
quotes_count: object.data["quotesCount"] || 0,
bookmark_folder: bookmark_folder
}
}
end
def render("show.json", _) do
nil
end
def render("history.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
object = Object.normalize(activity, fetch: false)
hashtags = Object.hashtags(object)
user = CommonAPI.get_user(activity.data["actor"])
past_history =
Object.Updater.history_for(object.data)
|> Map.get("orderedItems")
|> Enum.map(&Map.put(&1, "id", object.data["id"]))
|> Enum.map(&%Object{data: &1, id: object.id})
history =
[object | past_history]
# Mastodon expects the original to be at the first
|> Enum.reverse()
|> Enum.with_index()
|> Enum.map(fn {object, chrono_order} ->
%{
# The history is prepended every time there is a new edit.
# In chrono_order, the oldest item is always at 0, and so on.
# The chrono_order is an invariant kept between edits.
chrono_order: chrono_order,
object: object
}
end)
individual_opts =
opts
|> Map.put(:as, :item)
|> Map.put(:user, user)
|> Map.put(:hashtags, hashtags)
render_many(history, StatusView, "history_item.json", individual_opts)
end
def render(
"history_item.json",
%{
activity: activity,
user: user,
item: %{object: object, chrono_order: chrono_order},
hashtags: hashtags
} = opts
) do
sensitive = object.data["sensitive"] || Enum.member?(hashtags, "nsfw")
attachment_data = object.data["attachment"] || []
attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
created_at = Utils.to_masto_date(object.data["updated"] || object.data["published"])
content =
object
|> render_content()
content_html =
content
|> Activity.HTML.get_cached_scrubbed_html_for_activity(
User.html_filter_policy(opts[:for]),
activity,
"mastoapi:content:#{chrono_order}"
)
summary = object.data["summary"] || ""
%{
account:
AccountView.render("show.json", %{
user: user,
for: opts[:for]
}),
content: content_html,
sensitive: sensitive,
spoiler_text: summary,
created_at: created_at,
media_attachments: attachments,
emojis: build_emojis(object.data["emoji"]),
poll: render(PollView, "show.json", object: object, for: opts[:for])
}
end
def render("source.json", %{activity: %{data: %{"object" => _object}} = activity} = _opts) do
object = Object.normalize(activity, fetch: false)
%{
id: activity.id,
text: get_source_text(Map.get(object.data, "source", "")),
spoiler_text: Map.get(object.data, "summary", ""),
content_type: get_source_content_type(object.data["source"])
}
end
def render("card.json", %Card{fields: rich_media}) do
page_url_data = URI.parse(rich_media["url"])
page_url = page_url_data |> to_string
image_url = proxied_url(rich_media["image"], page_url_data)
audio_url = proxied_url(rich_media["audio"], page_url_data)
video_url = proxied_url(rich_media["video"], page_url_data)
%{
type: "link",
provider_name: page_url_data.host,
provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
url: page_url,
image: image_url,
image_description: rich_media["image:alt"] || "",
title: rich_media["title"] || "",
description: rich_media["description"] || "",
pleroma: %{
opengraph:
rich_media
|> Maps.put_if_present("image", image_url)
|> Maps.put_if_present("audio", audio_url)
|> Maps.put_if_present("video", video_url)
}
}
end
def render("card.json", _), do: nil
def render("attachment.json", %{attachment: attachment}) do
[attachment_url | _] = attachment["url"]
media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
href = attachment_url["href"] |> MediaProxy.url()
href_preview = attachment_url["href"] |> MediaProxy.preview_url()
meta = render("attachment_meta.json", %{attachment: attachment})
type =
cond do
String.contains?(media_type, "image") -> "image"
String.contains?(media_type, "video") -> "video"
String.contains?(media_type, "audio") -> "audio"
true -> "unknown"
end
attachment_id =
with {_, ap_id} when is_binary(ap_id) <- {:ap_id, attachment["id"]},
{_, %Object{data: _object_data, id: object_id}} <-
{:object, Object.get_by_ap_id(ap_id)} do
to_string(object_id)
else
_ ->
<<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
to_string(attachment["id"] || hash_id)
end
description =
if attachment["summary"] do
HTML.strip_tags(attachment["summary"])
else
attachment["name"]
end
name = if attachment["summary"], do: attachment["name"]
pleroma =
%{mime_type: media_type}
|> Maps.put_if_present(:name, name)
%{
id: attachment_id,
url: href,
remote_url: href,
preview_url: href_preview,
text_url: href,
type: type,
description: description,
pleroma: pleroma,
blurhash: attachment["blurhash"]
}
|> Maps.put_if_present(:meta, meta)
end
def render("attachment_meta.json", %{
attachment: %{"url" => [%{"width" => width, "height" => height} | _]}
})
when is_integer(width) and is_integer(height) do
%{
original: %{
width: width,
height: height,
aspect: width / height
}
}
end
def render("attachment_meta.json", _), do: nil
def render("context.json", %{activity: activity, activities: activities, user: user}) do
%{ancestors: ancestors, descendants: descendants} =
activities
|> Enum.reverse()
|> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
|> Map.put_new(:ancestors, [])
|> Map.put_new(:descendants, [])
%{
ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
descendants: render("index.json", for: user, activities: descendants, as: :activity)
}
end
def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
object = Object.normalize(activity, fetch: false)
with nil <- replied_to_activities[object.data["inReplyTo"]] do
# If user didn't participate in the thread
Activity.get_in_reply_to_activity(activity)
end
end
def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
object = Object.normalize(activity, fetch: false)
if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
else
nil
end
end
def get_quote(activity, %{quoted_activities: quoted_activities}) do
object = Object.normalize(activity, fetch: false)
with nil <- quoted_activities[object.data["quoteUrl"]] do
# For when a quote post is inside an Announce
Activity.get_create_by_object_ap_id_with_object(object.data["quoteUrl"])
end
end
def get_quote(%{data: %{"object" => _object}} = activity, _) do
object = Object.normalize(activity, fetch: false)
if object.data["quoteUrl"] && object.data["quoteUrl"] != "" do
Activity.get_create_by_object_ap_id(object.data["quoteUrl"])
else
nil
end
end
def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do
url = object.data["url"] || object.data["id"]
"<p><a href=\"#{url}\">#{name}</a></p>#{object.data["content"]}"
end
def render_content(object), do: object.data["content"] || ""
@doc """
Builds a dictionary tags.
## Examples
iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
[{"name": "fediverse", "url": "/tag/fediverse"},
{"name": "nextcloud", "url": "/tag/nextcloud"}]
"""
@spec build_tags(list(any())) :: list(map())
def build_tags(object_tags) when is_list(object_tags) do
object_tags
|> Enum.filter(&is_binary/1)
|> Enum.map(&%{name: &1, url: "#{Pleroma.Web.Endpoint.url()}/tag/#{URI.encode(&1)}"})
end
def build_tags(_), do: []
@doc """
Builds list emojis.
Arguments: `nil` or list tuple of name and url.
Returns list emojis.
## Examples
iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
[%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
"""
@spec build_emojis(nil | list(tuple())) :: list(map())
def build_emojis(nil), do: []
def build_emojis(emojis) do
emojis
|> Enum.map(fn {name, url} ->
name = HTML.strip_tags(name)
url =
url
|> HTML.strip_tags()
|> MediaProxy.url()
%{shortcode: name, url: url, static_url: url, visible_in_picker: false}
end)
end
defp present?(nil), do: false
defp present?(false), do: false
defp present?(_), do: true
defp pin_data(%Object{data: %{"id" => object_id}}, %User{pinned_objects: pinned_objects}) do
if pinned_at = pinned_objects[object_id] do
{true, Utils.to_masto_date(pinned_at)}
else
{false, nil}
end
end
defp build_emoji_map(emoji, users, url, current_user) do
%{
name: Pleroma.Web.PleromaAPI.EmojiReactionView.emoji_name(emoji, url),
count: length(users),
url: MediaProxy.url(url),
me: !!(current_user && current_user.ap_id in users),
account_ids: Enum.map(users, fn user -> User.get_cached_by_ap_id(user).id end)
}
end
@spec build_application(map() | nil) :: map() | nil
defp build_application(%{"type" => _type, "name" => name, "url" => url}),
do: %{name: name, website: url}
defp build_application(_), do: nil
# Workaround for Elixir issue #10771
# Avoid applying URI.merge unless necessary
# TODO: revert to always attempting URI.merge(image_url_data, page_url_data)
# when Elixir 1.12 is the minimum supported version
@spec build_image_url(struct() | nil, struct()) :: String.t() | nil
defp build_image_url(
%URI{scheme: image_scheme, host: image_host} = image_url_data,
%URI{} = _page_url_data
)
when not is_nil(image_scheme) and not is_nil(image_host) do
image_url_data |> to_string
end
defp build_image_url(%URI{} = image_url_data, %URI{} = page_url_data) do
URI.merge(page_url_data, image_url_data) |> to_string
end
defp get_source_text(%{"content" => content} = _source) do
content
end
defp get_source_text(source) when is_binary(source) do
source
end
defp get_source_text(_) do
""
end
defp get_source_content_type(%{"mediaType" => type} = _source) do
type
end
defp get_source_content_type(_source) do
Utils.get_content_type(nil)
end
defp proxied_url(url, page_url_data) do
if is_binary(url) do
build_image_url(URI.parse(url), page_url_data) |> MediaProxy.url()
else
nil
end
end
end
diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
index 9abdfae30..76dc0f42d 100644
--- a/lib/pleroma/web/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -1,420 +1,420 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Streamer do
require Logger
require Pleroma.Constants
alias Pleroma.Activity
alias Pleroma.Chat.MessageReference
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.StreamerView
@registry Pleroma.Web.StreamerRegistry
def registry, do: @registry
@public_streams Pleroma.Constants.public_streams()
@local_streams ["public:local", "public:local:media"]
@user_streams ["user", "user:notification", "direct", "user:pleroma_chat"]
@doc "Expands and authorizes a stream, and registers the process for streaming."
@spec get_topic_and_add_socket(
stream :: String.t(),
User.t() | nil,
Token.t() | nil,
map() | nil
) ::
{:ok, topic :: String.t()} | {:error, :bad_topic} | {:error, :unauthorized}
def get_topic_and_add_socket(stream, user, oauth_token, params \\ %{}) do
with {:ok, topic} <- get_topic(stream, user, oauth_token, params) do
add_socket(topic, oauth_token)
end
end
defp can_access_stream(user, oauth_token, kind) do
with {_, true} <- {:restrict?, Config.restrict_unauthenticated_access?(:timelines, kind)},
{_, %User{id: user_id}, %Token{user_id: user_id}} <- {:user, user, oauth_token},
{_, true} <-
{:scopes,
OAuthScopesPlug.filter_descendants(["read:statuses"], oauth_token.scopes) != []} do
true
else
{:restrict?, _} ->
true
_ ->
false
end
end
@doc "Expand and authorizes a stream"
@spec get_topic(stream :: String.t() | nil, User.t() | nil, Token.t() | nil, map()) ::
{:ok, topic :: String.t() | nil} | {:error, :bad_topic}
def get_topic(stream, user, oauth_token, params \\ %{})
def get_topic(nil = _stream, _user, _oauth_token, _params) do
{:ok, nil}
end
# Allow all public steams if the instance allows unauthenticated access.
# Otherwise, only allow users with valid oauth tokens.
def get_topic(stream, user, oauth_token, _params) when stream in @public_streams do
kind = if stream in @local_streams, do: :local, else: :federated
if can_access_stream(user, oauth_token, kind) do
{:ok, stream}
else
{:error, :unauthorized}
end
end
# Allow all hashtags streams.
def get_topic("hashtag", _user, _oauth_token, %{"tag" => tag} = _params) do
{:ok, "hashtag:" <> tag}
end
# Allow remote instance streams.
def get_topic("public:remote", user, oauth_token, %{"instance" => instance} = _params) do
if can_access_stream(user, oauth_token, :federated) do
{:ok, "public:remote:" <> instance}
else
{:error, :unauthorized}
end
end
def get_topic("public:remote:media", user, oauth_token, %{"instance" => instance} = _params) do
if can_access_stream(user, oauth_token, :federated) do
{:ok, "public:remote:media:" <> instance}
else
{:error, :unauthorized}
end
end
# Expand user streams.
def get_topic(
stream,
%User{id: user_id} = user,
%Token{user_id: user_id} = oauth_token,
_params
)
when stream in @user_streams do
# Note: "read" works for all user streams (not mentioning it since it's an ancestor scope)
required_scopes =
if stream == "user:notification" do
["read:notifications"]
else
["read:statuses"]
end
if OAuthScopesPlug.filter_descendants(required_scopes, oauth_token.scopes) == [] do
{:error, :unauthorized}
else
{:ok, stream <> ":" <> to_string(user.id)}
end
end
def get_topic(stream, _user, _oauth_token, _params) when stream in @user_streams do
{:error, :unauthorized}
end
# List streams.
def get_topic(
"list",
%User{id: user_id} = user,
%Token{user_id: user_id} = oauth_token,
%{"list" => id}
) do
cond do
OAuthScopesPlug.filter_descendants(["read", "read:lists"], oauth_token.scopes) == [] ->
{:error, :unauthorized}
Pleroma.List.get(id, user) ->
{:ok, "list:" <> to_string(id)}
true ->
{:error, :bad_topic}
end
end
def get_topic("list", _user, _oauth_token, _params) do
{:error, :unauthorized}
end
def get_topic(_stream, _user, _oauth_token, _params) do
{:error, :bad_topic}
end
@doc "Registers the process for streaming. Use `get_topic/3` to get the full authorized topic."
def add_socket(topic, oauth_token) do
if should_env_send?() do
oauth_token_id = if oauth_token, do: oauth_token.id, else: false
Registry.register(@registry, topic, oauth_token_id)
end
{:ok, topic}
end
def remove_socket(topic) do
if should_env_send?(), do: Registry.unregister(@registry, topic)
end
def stream(topics, items) do
if should_env_send?() do
for topic <- List.wrap(topics), item <- List.wrap(items) do
fun = fn -> do_stream(topic, item) end
if Config.get([__MODULE__, :sync_streaming], false) do
fun.()
else
spawn(fun)
end
end
end
end
def filtered_by_user?(user, item, streamed_type \\ :activity)
def filtered_by_user?(%User{} = user, %Activity{} = item, streamed_type) do
%{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} =
User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute])
recipient_blocks = MapSet.new(blocked_ap_ids ++ muted_ap_ids)
recipients = MapSet.new(item.recipients)
domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
with parent <- Object.normalize(item, fetch: false) || item,
true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)),
true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids,
true <-
!(streamed_type == :activity && item.data["type"] == "Announce" &&
parent.data["actor"] == user.ap_id),
true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(parent.data["actor"] not in &1)),
true <- MapSet.disjoint?(recipients, recipient_blocks),
%{host: item_host} <- URI.parse(item.actor),
%{host: parent_host} <- URI.parse(parent.data["actor"]),
false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, item_host),
false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, parent_host),
true <- thread_containment(item, user),
- false <- CommonAPI.thread_muted?(user, parent) do
+ false <- CommonAPI.thread_muted?(parent, user) do
false
else
_ -> true
end
end
def filtered_by_user?(%User{} = user, %Notification{activity: activity}, _) do
filtered_by_user?(user, activity, :notification)
end
defp do_stream("direct", item) do
recipient_topics =
User.get_recipients_from_activity(item)
|> Enum.map(fn %{id: id} -> "direct:#{id}" end)
Enum.each(recipient_topics, fn user_topic ->
Logger.debug("Trying to push direct message to #{user_topic}\n\n")
push_to_socket(user_topic, item)
end)
end
defp do_stream("follow_relationship", item) do
user_topic = "user:#{item.follower.id}"
text = StreamerView.render("follow_relationships_update.json", item, user_topic)
Logger.debug("Trying to push follow relationship update to #{user_topic}\n\n")
Registry.dispatch(@registry, user_topic, fn list ->
Enum.each(list, fn {pid, _auth} ->
send(pid, {:text, text})
end)
end)
end
defp do_stream("participation", participation) do
user_topic = "direct:#{participation.user_id}"
Logger.debug("Trying to push a conversation participation to #{user_topic}\n\n")
push_to_socket(user_topic, participation)
end
defp do_stream("list", item) do
# filter the recipient list if the activity is not public, see #270.
recipient_lists =
case Visibility.public?(item) do
true ->
Pleroma.List.get_lists_from_activity(item)
_ ->
Pleroma.List.get_lists_from_activity(item)
|> Enum.filter(fn list ->
owner = User.get_cached_by_id(list.user_id)
Visibility.visible_for_user?(item, owner)
end)
end
recipient_topics =
recipient_lists
|> Enum.map(fn %{id: id} -> "list:#{id}" end)
Enum.each(recipient_topics, fn list_topic ->
Logger.debug("Trying to push message to #{list_topic}\n\n")
push_to_socket(list_topic, item)
end)
end
defp do_stream(topic, %Notification{} = item)
when topic in ["user", "user:notification"] do
user_topic = "#{topic}:#{item.user_id}"
Registry.dispatch(@registry, user_topic, fn list ->
Enum.each(list, fn {pid, _auth} ->
send(pid, {:render_with_user, StreamerView, "notification.json", item, user_topic})
end)
end)
end
defp do_stream(topic, {user, %MessageReference{} = cm_ref})
when topic in ["user", "user:pleroma_chat"] do
topic = "#{topic}:#{user.id}"
text = StreamerView.render("chat_update.json", %{chat_message_reference: cm_ref}, topic)
Registry.dispatch(@registry, topic, fn list ->
Enum.each(list, fn {pid, _auth} ->
send(pid, {:text, text})
end)
end)
end
defp do_stream("user", item) do
Logger.debug("Trying to push to users")
recipient_topics =
User.get_recipients_from_activity(item)
|> Enum.map(fn %{id: id} -> "user:#{id}" end)
Enum.each(recipient_topics, fn topic ->
push_to_socket(topic, item)
end)
end
defp do_stream(topic, item) do
Logger.debug("Trying to push to #{topic}")
Logger.debug("Pushing item to #{topic}")
push_to_socket(topic, item)
end
defp push_to_socket(topic, %Participation{} = participation) do
rendered = StreamerView.render("conversation.json", participation, topic)
Registry.dispatch(@registry, topic, fn list ->
Enum.each(list, fn {pid, _} ->
send(pid, {:text, rendered})
end)
end)
end
defp push_to_socket(topic, %Activity{
data: %{"type" => "Delete", "deleted_activity_id" => deleted_activity_id}
}) do
rendered = Jason.encode!(%{event: "delete", payload: to_string(deleted_activity_id)})
Registry.dispatch(@registry, topic, fn list ->
Enum.each(list, fn {pid, _} ->
send(pid, {:text, rendered})
end)
end)
end
defp push_to_socket(_topic, %Activity{data: %{"type" => "Delete"}}), do: :noop
defp push_to_socket(topic, %Activity{data: %{"type" => "Update"}} = item) do
create_activity =
Pleroma.Activity.get_create_by_object_ap_id(item.object.data["id"])
|> Map.put(:object, item.object)
anon_render = StreamerView.render("status_update.json", create_activity, topic)
Registry.dispatch(@registry, topic, fn list ->
Enum.each(list, fn {pid, auth?} ->
if auth? do
send(
pid,
{:render_with_user, StreamerView, "status_update.json", create_activity, topic}
)
else
send(pid, {:text, anon_render})
end
end)
end)
end
defp push_to_socket(topic, item) do
anon_render = StreamerView.render("update.json", item, topic)
Registry.dispatch(@registry, topic, fn list ->
Enum.each(list, fn {pid, auth?} ->
if auth? do
send(pid, {:render_with_user, StreamerView, "update.json", item, topic})
else
send(pid, {:text, anon_render})
end
end)
end)
end
defp thread_containment(_activity, %User{skip_thread_containment: true}), do: true
defp thread_containment(activity, user) do
if Config.get([:instance, :skip_thread_containment]) do
true
else
ActivityPub.contain_activity(activity, user)
end
end
def close_streams_by_oauth_token(oauth_token) do
if should_env_send?() do
Registry.select(
@registry,
[
{
{:"$1", :"$2", :"$3"},
[{:==, :"$3", oauth_token.id}],
[:"$2"]
}
]
)
|> Enum.each(fn pid -> send(pid, :close) end)
end
end
# In dev/prod the streamer registry is expected to be started, so return true
# In test it is possible to have the registry started for a test so it will check
# In benchmark it will never find the process alive and return false
def should_env_send? do
if Application.get_env(:pleroma, Pleroma.Application)[:streamer_registry] do
true
else
case Process.whereis(@registry) do
nil ->
false
pid ->
Process.alive?(pid)
end
end
end
end
diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs
index 2dc1b839d..f2a315508 100644
--- a/test/pleroma/user_test.exs
+++ b/test/pleroma/user_test.exs
@@ -1,2907 +1,2907 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UserTest do
alias Pleroma.Activity
alias Pleroma.Builders.UserBuilder
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
use Pleroma.DataCase, async: false
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
import ExUnit.CaptureLog
import Swoosh.TestAssertions
setup do
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
:ok
end
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup do: clear_config([:instance, :account_activation_required])
describe "service actors" do
test "returns updated invisible actor" do
uri = "#{Pleroma.Web.Endpoint.url()}/relay"
followers_uri = "#{uri}/followers"
insert(
:user,
%{
nickname: "relay",
invisible: false,
local: true,
ap_id: uri,
follower_address: followers_uri
}
)
actor = User.get_or_create_service_actor_by_ap_id(uri, "relay")
assert actor.invisible
end
test "returns relay user" do
uri = "#{Pleroma.Web.Endpoint.url()}/relay"
followers_uri = "#{uri}/followers"
assert %User{
nickname: "relay",
invisible: true,
local: true,
ap_id: ^uri,
follower_address: ^followers_uri
} = User.get_or_create_service_actor_by_ap_id(uri, "relay")
assert capture_log(fn ->
refute User.get_or_create_service_actor_by_ap_id("/relay", "relay")
end) =~ "Cannot create service actor:"
end
test "returns invisible actor" do
uri = "#{Pleroma.Web.Endpoint.url()}/internal/fetch-test"
followers_uri = "#{uri}/followers"
user = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test")
assert %User{
nickname: "internal.fetch-test",
invisible: true,
local: true,
ap_id: ^uri,
follower_address: ^followers_uri
} = user
user2 = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test")
assert user.id == user2.id
end
end
describe "AP ID user relationships" do
setup do
{:ok, user: insert(:user)}
end
test "outgoing_relationships_ap_ids/1", %{user: user} do
rel_types = [:block, :mute, :notification_mute, :reblog_mute, :inverse_subscription]
ap_ids_by_rel =
Enum.into(
rel_types,
%{},
fn rel_type ->
rel_records =
insert_list(2, :user_relationship, %{source: user, relationship_type: rel_type})
ap_ids = Enum.map(rel_records, fn rr -> Repo.preload(rr, :target).target.ap_id end)
{rel_type, Enum.sort(ap_ids)}
end
)
assert ap_ids_by_rel[:block] == Enum.sort(User.blocked_users_ap_ids(user))
assert ap_ids_by_rel[:block] == Enum.sort(Enum.map(User.blocked_users(user), & &1.ap_id))
assert ap_ids_by_rel[:mute] == Enum.sort(User.muted_users_ap_ids(user))
assert ap_ids_by_rel[:mute] == Enum.sort(Enum.map(User.muted_users(user), & &1.ap_id))
assert ap_ids_by_rel[:notification_mute] ==
Enum.sort(User.notification_muted_users_ap_ids(user))
assert ap_ids_by_rel[:notification_mute] ==
Enum.sort(Enum.map(User.notification_muted_users(user), & &1.ap_id))
assert ap_ids_by_rel[:reblog_mute] == Enum.sort(User.reblog_muted_users_ap_ids(user))
assert ap_ids_by_rel[:reblog_mute] ==
Enum.sort(Enum.map(User.reblog_muted_users(user), & &1.ap_id))
assert ap_ids_by_rel[:inverse_subscription] == Enum.sort(User.subscriber_users_ap_ids(user))
assert ap_ids_by_rel[:inverse_subscription] ==
Enum.sort(Enum.map(User.subscriber_users(user), & &1.ap_id))
outgoing_relationships_ap_ids = User.outgoing_relationships_ap_ids(user, rel_types)
assert ap_ids_by_rel ==
Enum.into(outgoing_relationships_ap_ids, %{}, fn {k, v} -> {k, Enum.sort(v)} end)
end
end
describe "when tags are nil" do
test "tagging a user" do
user = insert(:user, %{tags: nil})
user = User.tag(user, ["cool", "dude"])
assert "cool" in user.tags
assert "dude" in user.tags
end
test "untagging a user" do
user = insert(:user, %{tags: nil})
user = User.untag(user, ["cool", "dude"])
assert user.tags == []
end
end
test "ap_id returns the activity pub id for the user" do
user = UserBuilder.build()
expected_ap_id = "#{Pleroma.Web.Endpoint.url()}/users/#{user.nickname}"
assert expected_ap_id == User.ap_id(user)
end
test "ap_followers returns the followers collection for the user" do
user = UserBuilder.build()
expected_followers_collection = "#{User.ap_id(user)}/followers"
assert expected_followers_collection == User.ap_followers(user)
end
test "ap_following returns the following collection for the user" do
user = UserBuilder.build()
expected_followers_collection = "#{User.ap_id(user)}/following"
assert expected_followers_collection == User.ap_following(user)
end
test "returns all pending follow requests" do
unlocked = insert(:user)
locked = insert(:user, is_locked: true)
follower = insert(:user)
CommonAPI.follow(follower, unlocked)
CommonAPI.follow(follower, locked)
assert [] = User.get_follow_requests(unlocked)
assert [activity] = User.get_follow_requests(locked)
assert activity
end
test "doesn't return already accepted or duplicate follow requests" do
locked = insert(:user, is_locked: true)
pending_follower = insert(:user)
accepted_follower = insert(:user)
CommonAPI.follow(pending_follower, locked)
CommonAPI.follow(pending_follower, locked)
CommonAPI.follow(accepted_follower, locked)
Pleroma.FollowingRelationship.update(accepted_follower, locked, :follow_accept)
assert [^pending_follower] = User.get_follow_requests(locked)
end
test "doesn't return follow requests for deactivated accounts" do
locked = insert(:user, is_locked: true)
pending_follower = insert(:user, %{is_active: false})
CommonAPI.follow(pending_follower, locked)
refute pending_follower.is_active
assert [] = User.get_follow_requests(locked)
end
test "clears follow requests when requester is blocked" do
followed = insert(:user, is_locked: true)
follower = insert(:user)
CommonAPI.follow(follower, followed)
assert [_activity] = User.get_follow_requests(followed)
{:ok, _user_relationship} = User.block(followed, follower)
assert [] = User.get_follow_requests(followed)
end
test "follow_all follows multiple users" do
user = insert(:user)
followed_zero = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
blocked = insert(:user)
not_followed = insert(:user)
reverse_blocked = insert(:user)
{:ok, _user_relationship} = User.block(user, blocked)
{:ok, _user_relationship} = User.block(reverse_blocked, user)
{:ok, user, followed_zero} = User.follow(user, followed_zero)
{:ok, user} = User.follow_all(user, [followed_one, followed_two, blocked, reverse_blocked])
assert User.following?(user, followed_one)
assert User.following?(user, followed_two)
assert User.following?(user, followed_zero)
refute User.following?(user, not_followed)
refute User.following?(user, blocked)
refute User.following?(user, reverse_blocked)
end
test "follow_all follows multiple users without duplicating" do
user = insert(:user)
followed_zero = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
{:ok, user} = User.follow_all(user, [followed_zero, followed_one])
assert length(User.following(user)) == 3
{:ok, user} = User.follow_all(user, [followed_one, followed_two])
assert length(User.following(user)) == 4
end
test "follow takes a user and another user" do
user = insert(:user)
followed = insert(:user)
{:ok, user, followed} = User.follow(user, followed)
user = User.get_cached_by_id(user.id)
followed = User.get_cached_by_ap_id(followed.ap_id)
assert followed.follower_count == 1
assert user.following_count == 1
assert User.ap_followers(followed) in User.following(user)
end
test "can't follow a deactivated users" do
user = insert(:user)
followed = insert(:user, %{is_active: false})
{:error, _} = User.follow(user, followed)
end
test "can't follow a user who blocked us" do
blocker = insert(:user)
blockee = insert(:user)
{:ok, _user_relationship} = User.block(blocker, blockee)
{:error, _} = User.follow(blockee, blocker)
end
test "can't subscribe to a user who blocked us" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, _user_relationship} = User.block(blocker, blocked)
{:error, _} = User.subscribe(blocked, blocker)
end
test "local users do not automatically follow local locked accounts" do
follower = insert(:user, is_locked: true)
followed = insert(:user, is_locked: true)
{:ok, follower, followed} = User.maybe_direct_follow(follower, followed)
refute User.following?(follower, followed)
end
describe "unfollow/2" do
setup do: clear_config([:instance, :external_user_synchronization])
test "unfollow with synchronizes external user" do
clear_config([:instance, :external_user_synchronization], true)
followed =
insert(:user,
nickname: "fuser1",
follower_address: "http://localhost:4001/users/fuser1/followers",
following_address: "http://localhost:4001/users/fuser1/following",
ap_id: "http://localhost:4001/users/fuser1"
)
user =
insert(:user, %{
local: false,
nickname: "fuser2",
ap_id: "http://localhost:4001/users/fuser2",
follower_address: "http://localhost:4001/users/fuser2/followers",
following_address: "http://localhost:4001/users/fuser2/following"
})
{:ok, user, followed} = User.follow(user, followed, :follow_accept)
{:ok, user, _activity} = User.unfollow(user, followed)
user = User.get_cached_by_id(user.id)
assert User.following(user) == []
end
test "unfollow takes a user and another user" do
followed = insert(:user)
user = insert(:user)
{:ok, user, followed} = User.follow(user, followed, :follow_accept)
assert User.following(user) == [user.follower_address, followed.follower_address]
{:ok, user, _activity} = User.unfollow(user, followed)
assert User.following(user) == [user.follower_address]
end
test "unfollow doesn't unfollow yourself" do
user = insert(:user)
{:error, _} = User.unfollow(user, user)
assert User.following(user) == [user.follower_address]
end
end
test "test if a user is following another user" do
followed = insert(:user)
user = insert(:user)
User.follow(user, followed, :follow_accept)
assert User.following?(user, followed)
refute User.following?(followed, user)
end
test "fetches correct profile for nickname beginning with number" do
# Use old-style integer ID to try to reproduce the problem
user = insert(:user, %{id: 1080})
user_with_numbers = insert(:user, %{nickname: "#{user.id}garbage"})
assert user_with_numbers == User.get_cached_by_nickname_or_id(user_with_numbers.nickname)
end
describe "user registration" do
@full_user_data %{
bio: "A guy",
name: "my name",
nickname: "nick",
password: "test",
password_confirmation: "test",
email: "email@example.com"
}
setup do: clear_config([:instance, :autofollowed_nicknames])
setup do: clear_config([:instance, :autofollowing_nicknames])
setup do: clear_config([:welcome])
setup do: clear_config([:instance, :account_activation_required])
test "it autofollows accounts that are set for it" do
user = insert(:user)
remote_user = insert(:user, %{local: false})
clear_config([:instance, :autofollowed_nicknames], [
user.nickname,
remote_user.nickname
])
cng = User.register_changeset(%User{}, @full_user_data)
{:ok, registered_user} = User.register(cng)
assert User.following?(registered_user, user)
refute User.following?(registered_user, remote_user)
end
test "it adds automatic followers for new registered accounts" do
user1 = insert(:user)
user2 = insert(:user)
clear_config([:instance, :autofollowing_nicknames], [
user1.nickname,
user2.nickname
])
cng = User.register_changeset(%User{}, @full_user_data)
{:ok, registered_user} = User.register(cng)
assert User.following?(user1, registered_user)
assert User.following?(user2, registered_user)
end
test "it sends a welcome message if it is set" do
welcome_user = insert(:user)
clear_config([:welcome, :direct_message, :enabled], true)
clear_config([:welcome, :direct_message, :sender_nickname], welcome_user.nickname)
clear_config([:welcome, :direct_message, :message], "Hello, this is a direct message")
cng = User.register_changeset(%User{}, @full_user_data)
{:ok, registered_user} = User.register(cng)
ObanHelpers.perform_all()
activity = Repo.one(Pleroma.Activity)
assert registered_user.ap_id in activity.recipients
assert Object.normalize(activity, fetch: false).data["content"] =~ "direct message"
assert activity.actor == welcome_user.ap_id
end
test "it sends a welcome chat message if it is set" do
welcome_user = insert(:user)
clear_config([:welcome, :chat_message, :enabled], true)
clear_config([:welcome, :chat_message, :sender_nickname], welcome_user.nickname)
clear_config([:welcome, :chat_message, :message], "Hello, this is a chat message")
cng = User.register_changeset(%User{}, @full_user_data)
{:ok, registered_user} = User.register(cng)
ObanHelpers.perform_all()
activity = Repo.one(Pleroma.Activity)
assert registered_user.ap_id in activity.recipients
assert Object.normalize(activity, fetch: false).data["content"] =~ "chat message"
assert activity.actor == welcome_user.ap_id
end
setup do:
clear_config(:mrf_simple,
media_removal: [],
media_nsfw: [],
federated_timeline_removal: [],
report_removal: [],
reject: [],
followers_only: [],
accept: [],
avatar_removal: [],
banner_removal: [],
reject_deletes: []
)
setup do: clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.SimplePolicy])
test "it sends a welcome chat message when Simple policy applied to local instance" do
clear_config([:mrf_simple, :media_nsfw], [{"localhost", ""}])
welcome_user = insert(:user)
clear_config([:welcome, :chat_message, :enabled], true)
clear_config([:welcome, :chat_message, :sender_nickname], welcome_user.nickname)
clear_config([:welcome, :chat_message, :message], "Hello, this is a chat message")
cng = User.register_changeset(%User{}, @full_user_data)
{:ok, registered_user} = User.register(cng)
ObanHelpers.perform_all()
activity = Repo.one(Pleroma.Activity)
assert registered_user.ap_id in activity.recipients
assert Object.normalize(activity, fetch: false).data["content"] =~ "chat message"
assert activity.actor == welcome_user.ap_id
end
test "it sends a welcome email message if it is set" do
welcome_user = insert(:user)
clear_config([:welcome, :email, :enabled], true)
clear_config([:welcome, :email, :sender], welcome_user.email)
clear_config(
[:welcome, :email, :subject],
"Hello, welcome to cool site: <%= instance_name %>"
)
instance_name = Pleroma.Config.get([:instance, :name])
cng = User.register_changeset(%User{}, @full_user_data)
{:ok, registered_user} = User.register(cng)
ObanHelpers.perform_all()
assert_email_sent(
from: {instance_name, welcome_user.email},
to: {registered_user.name, registered_user.email},
subject: "Hello, welcome to cool site: #{instance_name}",
html_body: "Welcome to #{instance_name}"
)
end
test "it sends a confirm email" do
clear_config([:instance, :account_activation_required], true)
cng = User.register_changeset(%User{}, @full_user_data)
{:ok, registered_user} = User.register(cng)
ObanHelpers.perform_all()
Pleroma.Emails.UserEmail.account_confirmation_email(registered_user)
# temporary hackney fix until hackney max_connections bug is fixed
# https://git.pleroma.social/pleroma/pleroma/-/issues/2101
|> Swoosh.Email.put_private(:hackney_options, ssl_options: [versions: [:"tlsv1.2"]])
|> assert_email_sent()
end
test "sends a pending approval email" do
clear_config([:instance, :account_approval_required], true)
{:ok, user} =
User.register_changeset(%User{}, @full_user_data)
|> User.register()
ObanHelpers.perform_all()
assert_email_sent(
from: Pleroma.Config.Helpers.sender(),
to: {user.name, user.email},
subject: "Your account is awaiting approval"
)
end
test "it sends a registration confirmed email if no others will be sent" do
clear_config([:welcome, :email, :enabled], false)
clear_config([:instance, :account_activation_required], false)
clear_config([:instance, :account_approval_required], false)
{:ok, user} =
User.register_changeset(%User{}, @full_user_data)
|> User.register()
ObanHelpers.perform_all()
instance_name = Pleroma.Config.get([:instance, :name])
sender = Pleroma.Config.get([:instance, :notify_email])
assert_email_sent(
from: {instance_name, sender},
to: {user.name, user.email},
subject: "Account registered on #{instance_name}"
)
end
test "it fails gracefully with invalid email config" do
cng = User.register_changeset(%User{}, @full_user_data)
# Disable the mailer but enable all the things that want to send emails
clear_config([Pleroma.Emails.Mailer, :enabled], false)
clear_config([:instance, :account_activation_required], true)
clear_config([:instance, :account_approval_required], true)
clear_config([:welcome, :email, :enabled], true)
clear_config([:welcome, :email, :sender], "lain@lain.com")
# The user is still created
assert {:ok, %User{nickname: "nick"}} = User.register(cng)
# No emails are sent
ObanHelpers.perform_all()
refute_email_sent()
end
test "it works when the registering user does not provide an email" do
clear_config([Pleroma.Emails.Mailer, :enabled], false)
clear_config([:instance, :account_activation_required], false)
clear_config([:instance, :account_approval_required], true)
cng = User.register_changeset(%User{}, @full_user_data |> Map.put(:email, ""))
# The user is still created
assert {:ok, %User{nickname: "nick"}} = User.register(cng)
# No emails are sent
ObanHelpers.perform_all()
refute_email_sent()
end
test "it requires an email, name, nickname and password, bio is optional when account_activation_required is enabled" do
clear_config([:instance, :account_activation_required], true)
@full_user_data
|> Map.keys()
|> Enum.each(fn key ->
params = Map.delete(@full_user_data, key)
changeset = User.register_changeset(%User{}, params)
assert if key == :bio, do: changeset.valid?, else: not changeset.valid?
end)
end
test "it requires an name, nickname and password, bio and email are optional when account_activation_required is disabled" do
clear_config([:instance, :account_activation_required], false)
@full_user_data
|> Map.keys()
|> Enum.each(fn key ->
params = Map.delete(@full_user_data, key)
changeset = User.register_changeset(%User{}, params)
assert if key in [:bio, :email], do: changeset.valid?, else: not changeset.valid?
end)
end
test "it restricts certain nicknames" do
clear_config([User, :restricted_nicknames], ["about"])
[restricted_name | _] = Pleroma.Config.get([User, :restricted_nicknames])
assert is_binary(restricted_name)
params =
@full_user_data
|> Map.put(:nickname, restricted_name)
changeset = User.register_changeset(%User{}, params)
refute changeset.valid?
end
test "it is case-insensitive when restricting nicknames" do
clear_config([User, :restricted_nicknames], ["about"])
[restricted_name | _] = Pleroma.Config.get([User, :restricted_nicknames])
assert is_binary(restricted_name)
restricted_upcase_name = String.upcase(restricted_name)
params =
@full_user_data
|> Map.put(:nickname, restricted_upcase_name)
changeset = User.register_changeset(%User{}, params)
refute changeset.valid?
end
test "it blocks blacklisted email domains" do
clear_config([User, :email_blacklist], ["trolling.world"])
# Block with match
params = Map.put(@full_user_data, :email, "troll@trolling.world")
changeset = User.register_changeset(%User{}, params)
refute changeset.valid?
# Block with case-insensitive match
params = Map.put(@full_user_data, :email, "troll@TrOlLing.wOrld")
changeset = User.register_changeset(%User{}, params)
refute changeset.valid?
# Block with subdomain match
params = Map.put(@full_user_data, :email, "troll@gnomes.trolling.world")
changeset = User.register_changeset(%User{}, params)
refute changeset.valid?
# Pass with different domains that are similar
params = Map.put(@full_user_data, :email, "troll@gnomestrolling.world")
changeset = User.register_changeset(%User{}, params)
assert changeset.valid?
params = Map.put(@full_user_data, :email, "troll@trolling.world.us")
changeset = User.register_changeset(%User{}, params)
assert changeset.valid?
end
test "it sets the password_hash, ap_id, private key and followers collection address" do
changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid?
assert is_binary(changeset.changes[:password_hash])
assert is_binary(changeset.changes[:keys])
assert changeset.changes[:ap_id] == User.ap_id(%User{nickname: @full_user_data.nickname})
assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers"
end
test "it sets the 'accepts_chat_messages' set to true" do
changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid?
{:ok, user} = Repo.insert(changeset)
assert user.accepts_chat_messages
end
test "it creates a confirmed user" do
changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid?
{:ok, user} = Repo.insert(changeset)
assert user.is_confirmed
end
end
describe "user registration, with :account_activation_required" do
@full_user_data %{
bio: "A guy",
name: "my name",
nickname: "nick",
password: "test",
password_confirmation: "test",
email: "email@example.com"
}
setup do: clear_config([:instance, :account_activation_required], true)
test "it creates unconfirmed user" do
changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid?
{:ok, user} = Repo.insert(changeset)
refute user.is_confirmed
assert user.confirmation_token
end
test "it creates confirmed user if :confirmed option is given" do
changeset = User.register_changeset(%User{}, @full_user_data, confirmed: true)
assert changeset.valid?
{:ok, user} = Repo.insert(changeset)
assert user.is_confirmed
refute user.confirmation_token
end
end
describe "user registration, with :account_approval_required" do
@full_user_data %{
bio: "A guy",
name: "my name",
nickname: "nick",
password: "test",
password_confirmation: "test",
email: "email@example.com",
registration_reason: "I'm a cool guy :)"
}
setup do: clear_config([:instance, :account_approval_required], true)
test "it creates unapproved user" do
changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid?
{:ok, user} = Repo.insert(changeset)
refute user.is_approved
assert user.registration_reason == "I'm a cool guy :)"
end
test "it restricts length of registration reason" do
reason_limit = Pleroma.Config.get([:instance, :registration_reason_length])
assert is_integer(reason_limit)
params =
@full_user_data
|> Map.put(
:registration_reason,
"Quia et nesciunt dolores numquam ipsam nisi sapiente soluta. Ullam repudiandae nisi quam porro officiis officiis ad. Consequatur animi velit ex quia. Odit voluptatem perferendis quia ut nisi. Dignissimos sit soluta atque aliquid dolorem ut dolorum ut. Labore voluptates iste iusto amet voluptatum earum. Ad fugit illum nam eos ut nemo. Pariatur ea fuga non aspernatur. Dignissimos debitis officia corporis est nisi ab et. Atque itaque alias eius voluptas minus. Accusamus numquam tempore occaecati in."
)
changeset = User.register_changeset(%User{}, params)
refute changeset.valid?
end
end
describe "user registration, with :birthday_required and :birthday_min_age" do
@full_user_data %{
bio: "A guy",
name: "my name",
nickname: "nick",
password: "test",
password_confirmation: "test",
email: "email@example.com"
}
setup do
clear_config([:instance, :birthday_required], true)
clear_config([:instance, :birthday_min_age], 18 * 365)
end
test "it passes when correct birth date is provided" do
today = Date.utc_today()
birthday = Date.add(today, -19 * 365)
params =
@full_user_data
|> Map.put(:birthday, birthday)
changeset = User.register_changeset(%User{}, params)
assert changeset.valid?
end
test "it fails when birth date is not provided" do
changeset = User.register_changeset(%User{}, @full_user_data)
refute changeset.valid?
end
test "it fails when provided invalid birth date" do
today = Date.utc_today()
birthday = Date.add(today, -17 * 365)
params =
@full_user_data
|> Map.put(:birthday, birthday)
changeset = User.register_changeset(%User{}, params)
refute changeset.valid?
end
end
describe "get_or_fetch/1" do
test "gets an existing user by nickname" do
user = insert(:user)
{:ok, fetched_user} = User.get_or_fetch(user.nickname)
assert user == fetched_user
end
test "gets an existing user by ap_id" do
ap_id = "http://mastodon.example.org/users/admin"
user =
insert(
:user,
local: false,
nickname: "admin@mastodon.example.org",
ap_id: ap_id
)
{:ok, fetched_user} = User.get_or_fetch(ap_id)
freshed_user = refresh_record(user)
assert freshed_user == fetched_user
end
test "gets an existing user by nickname starting with http" do
user = insert(:user, nickname: "httpssome")
{:ok, fetched_user} = User.get_or_fetch("httpssome")
assert user == fetched_user
end
end
describe "get_or_fetch/1 remote users with tld, while BE is running on a subdomain" do
setup do: clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true)
test "for mastodon" do
ap_id = "a@mastodon.example"
{:ok, fetched_user} = User.get_or_fetch(ap_id)
assert fetched_user.ap_id == "https://sub.mastodon.example/users/a"
assert fetched_user.nickname == "a@mastodon.example"
end
test "for pleroma" do
ap_id = "a@pleroma.example"
{:ok, fetched_user} = User.get_or_fetch(ap_id)
assert fetched_user.ap_id == "https://sub.pleroma.example/users/a"
assert fetched_user.nickname == "a@pleroma.example"
end
end
describe "fetching a user from nickname or trying to build one" do
test "gets an existing user" do
user = insert(:user)
{:ok, fetched_user} = User.get_or_fetch_by_nickname(user.nickname)
assert user == fetched_user
end
test "gets an existing user, case insensitive" do
user = insert(:user, nickname: "nick")
{:ok, fetched_user} = User.get_or_fetch_by_nickname("NICK")
assert user == fetched_user
end
test "gets an existing user by fully qualified nickname" do
user = insert(:user)
{:ok, fetched_user} =
User.get_or_fetch_by_nickname(user.nickname <> "@" <> Pleroma.Web.Endpoint.host())
assert user == fetched_user
end
test "gets an existing user by fully qualified nickname, case insensitive" do
user = insert(:user, nickname: "nick")
casing_altered_fqn = String.upcase(user.nickname <> "@" <> Pleroma.Web.Endpoint.host())
{:ok, fetched_user} = User.get_or_fetch_by_nickname(casing_altered_fqn)
assert user == fetched_user
end
test "returns nil if no user could be fetched" do
{:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistent@social.heldscal.la")
assert fetched_user == "not found nonexistent@social.heldscal.la"
end
test "returns nil for nonexistent local user" do
{:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistent")
assert fetched_user == "not found nonexistent"
end
test "updates an existing user, if stale" do
a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800)
orig_user =
insert(
:user,
local: false,
nickname: "admin@mastodon.example.org",
ap_id: "http://mastodon.example.org/users/admin",
last_refreshed_at: a_week_ago
)
assert orig_user.last_refreshed_at == a_week_ago
{:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin")
# Oban job was generated to refresh the stale user
assert_enqueued(worker: "Pleroma.Workers.UserRefreshWorker", args: %{"ap_id" => user.ap_id})
# Run job to refresh the user; just capture its output instead of fetching it again
assert {:ok, updated_user} =
perform_job(Pleroma.Workers.UserRefreshWorker, %{"ap_id" => user.ap_id})
assert updated_user.inbox
refute updated_user.last_refreshed_at == orig_user.last_refreshed_at
end
test "if nicknames clash, the old user gets a prefix with the old id to the nickname" do
a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800)
orig_user =
insert(
:user,
local: false,
nickname: "admin@mastodon.example.org",
ap_id: "http://mastodon.example.org/users/harinezumigari",
last_refreshed_at: a_week_ago
)
assert orig_user.last_refreshed_at == a_week_ago
{:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin")
assert user.inbox
refute user.id == orig_user.id
orig_user = User.get_by_id(orig_user.id)
assert orig_user.nickname == "#{orig_user.id}.admin@mastodon.example.org"
end
test "it returns the old user if stale, but unfetchable" do
a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800)
orig_user =
insert(
:user,
local: false,
nickname: "admin@mastodon.example.org",
ap_id: "http://mastodon.example.org/users/raymoo",
last_refreshed_at: a_week_ago
)
assert orig_user.last_refreshed_at == a_week_ago
{:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/raymoo")
assert user.last_refreshed_at == orig_user.last_refreshed_at
end
end
test "returns an ap_id for a user" do
user = insert(:user)
assert User.ap_id(user) ==
Pleroma.Web.Router.Helpers.user_feed_url(
Pleroma.Web.Endpoint,
:feed_redirect,
user.nickname
)
end
test "returns an ap_followers link for a user" do
user = insert(:user)
assert User.ap_followers(user) ==
Pleroma.Web.Router.Helpers.user_feed_url(
Pleroma.Web.Endpoint,
:feed_redirect,
user.nickname
) <> "/followers"
end
describe "remote user changeset" do
@valid_remote %{
bio: "hello",
name: "Someone",
nickname: "a@b.de",
ap_id: "http...",
avatar: %{some: "avatar"}
}
setup do: clear_config([:instance, :user_bio_length])
setup do: clear_config([:instance, :user_name_length])
test "it confirms validity" do
cs = User.remote_user_changeset(@valid_remote)
assert cs.valid?
end
test "it sets the follower_address" do
cs = User.remote_user_changeset(@valid_remote)
# remote users get a fake local follower address
assert cs.changes.follower_address ==
User.ap_followers(%User{nickname: @valid_remote[:nickname]})
end
test "it enforces the fqn format for nicknames" do
cs = User.remote_user_changeset(%{@valid_remote | nickname: "bla"})
assert Ecto.Changeset.get_field(cs, :local) == false
assert cs.changes.avatar
refute cs.valid?
end
test "it has required fields" do
[:ap_id]
|> Enum.each(fn field ->
cs = User.remote_user_changeset(Map.delete(@valid_remote, field))
refute cs.valid?
end)
end
test "it is invalid given a local user" do
user = insert(:user)
cs = User.remote_user_changeset(user, %{name: "tom from myspace"})
refute cs.valid?
end
end
describe "followers and friends" do
test "gets all followers for a given user" do
user = insert(:user)
follower_one = insert(:user)
follower_two = insert(:user)
not_follower = insert(:user)
{:ok, follower_one, user} = User.follow(follower_one, user)
{:ok, follower_two, user} = User.follow(follower_two, user)
res = User.get_followers(user)
assert Enum.member?(res, follower_one)
assert Enum.member?(res, follower_two)
refute Enum.member?(res, not_follower)
end
test "gets all friends (followed users) for a given user" do
user = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
not_followed = insert(:user)
{:ok, user, followed_one} = User.follow(user, followed_one)
{:ok, user, followed_two} = User.follow(user, followed_two)
res = User.get_friends(user)
followed_one = User.get_cached_by_ap_id(followed_one.ap_id)
followed_two = User.get_cached_by_ap_id(followed_two.ap_id)
assert Enum.member?(res, followed_one)
assert Enum.member?(res, followed_two)
refute Enum.member?(res, not_followed)
end
end
describe "updating note and follower count" do
test "it sets the note_count property" do
note = insert(:note)
user = User.get_cached_by_ap_id(note.data["actor"])
assert user.note_count == 0
{:ok, user} = User.update_note_count(user)
assert user.note_count == 1
end
test "it increases the note_count property" do
note = insert(:note)
user = User.get_cached_by_ap_id(note.data["actor"])
assert user.note_count == 0
{:ok, user} = User.increase_note_count(user)
assert user.note_count == 1
{:ok, user} = User.increase_note_count(user)
assert user.note_count == 2
end
test "it decreases the note_count property" do
note = insert(:note)
user = User.get_cached_by_ap_id(note.data["actor"])
assert user.note_count == 0
{:ok, user} = User.increase_note_count(user)
assert user.note_count == 1
{:ok, user} = User.decrease_note_count(user)
assert user.note_count == 0
{:ok, user} = User.decrease_note_count(user)
assert user.note_count == 0
end
test "it sets the follower_count property" do
user = insert(:user)
follower = insert(:user)
User.follow(follower, user)
assert user.follower_count == 0
{:ok, user} = User.update_follower_count(user)
assert user.follower_count == 1
end
end
describe "mutes" do
test "it mutes people" do
user = insert(:user)
muted_user = insert(:user)
refute User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
{:ok, _user_relationships} = User.mute(user, muted_user)
assert User.mutes?(user, muted_user)
assert User.muted_notifications?(user, muted_user)
end
test "expiring" do
user = insert(:user)
muted_user = insert(:user)
{:ok, _user_relationships} = User.mute(user, muted_user, %{duration: 60})
assert User.mutes?(user, muted_user)
worker = Pleroma.Workers.MuteExpireWorker
args = %{"op" => "unmute_user", "muter_id" => user.id, "mutee_id" => muted_user.id}
assert_enqueued(
worker: worker,
args: args
)
assert :ok = perform_job(worker, args)
refute User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
end
test "it unmutes users" do
user = insert(:user)
muted_user = insert(:user)
{:ok, _user_relationships} = User.mute(user, muted_user)
{:ok, _user_mute} = User.unmute(user, muted_user)
refute User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
end
test "it unmutes users by id" do
user = insert(:user)
muted_user = insert(:user)
{:ok, _user_relationships} = User.mute(user, muted_user)
{:ok, _user_mute} = User.unmute(user.id, muted_user.id)
refute User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
end
test "it mutes user without notifications" do
user = insert(:user)
muted_user = insert(:user)
refute User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
{:ok, _user_relationships} = User.mute(user, muted_user, %{notifications: false})
assert User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
end
end
describe "blocks" do
test "it blocks people" do
user = insert(:user)
blocked_user = insert(:user)
refute User.blocks?(user, blocked_user)
{:ok, _user_relationship} = User.block(user, blocked_user)
assert User.blocks?(user, blocked_user)
end
test "it unblocks users" do
user = insert(:user)
blocked_user = insert(:user)
{:ok, _user_relationship} = User.block(user, blocked_user)
{:ok, _user_block} = User.unblock(user, blocked_user)
refute User.blocks?(user, blocked_user)
end
test "blocks tear down cyclical follow relationships" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, blocker, blocked} = User.follow(blocker, blocked)
{:ok, blocked, blocker} = User.follow(blocked, blocker)
assert User.following?(blocker, blocked)
assert User.following?(blocked, blocker)
{:ok, _user_relationship} = User.block(blocker, blocked)
blocked = User.get_cached_by_id(blocked.id)
assert User.blocks?(blocker, blocked)
refute User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
end
test "blocks tear down blocker->blocked follow relationships" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, blocker, blocked} = User.follow(blocker, blocked)
assert User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
{:ok, _user_relationship} = User.block(blocker, blocked)
blocked = User.get_cached_by_id(blocked.id)
assert User.blocks?(blocker, blocked)
refute User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
end
test "blocks tear down blocked->blocker follow relationships" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, blocked, blocker} = User.follow(blocked, blocker)
refute User.following?(blocker, blocked)
assert User.following?(blocked, blocker)
{:ok, _user_relationship} = User.block(blocker, blocked)
blocked = User.get_cached_by_id(blocked.id)
assert User.blocks?(blocker, blocked)
refute User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
end
test "blocks tear down blocked->blocker subscription relationships" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, _subscription} = User.subscribe(blocked, blocker)
assert User.subscribed_to?(blocked, blocker)
refute User.subscribed_to?(blocker, blocked)
{:ok, _user_relationship} = User.block(blocker, blocked)
assert User.blocks?(blocker, blocked)
refute User.subscribed_to?(blocker, blocked)
refute User.subscribed_to?(blocked, blocker)
end
end
describe "domain blocking" do
test "blocks domains" do
user = insert(:user)
collateral_user = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"})
{:ok, user} = User.block_domain(user, "awful-and-rude-instance.com")
assert User.blocks?(user, collateral_user)
end
test "does not block domain with same end" do
user = insert(:user)
collateral_user =
insert(:user, %{ap_id: "https://another-awful-and-rude-instance.com/user/bully"})
{:ok, user} = User.block_domain(user, "awful-and-rude-instance.com")
refute User.blocks?(user, collateral_user)
end
test "does not block domain with same end if wildcard added" do
user = insert(:user)
collateral_user =
insert(:user, %{ap_id: "https://another-awful-and-rude-instance.com/user/bully"})
{:ok, user} = User.block_domain(user, "*.awful-and-rude-instance.com")
refute User.blocks?(user, collateral_user)
end
test "blocks domain with wildcard for subdomain" do
user = insert(:user)
user_from_subdomain =
insert(:user, %{ap_id: "https://subdomain.awful-and-rude-instance.com/user/bully"})
user_with_two_subdomains =
insert(:user, %{
ap_id: "https://subdomain.second_subdomain.awful-and-rude-instance.com/user/bully"
})
user_domain = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"})
{:ok, user} = User.block_domain(user, "*.awful-and-rude-instance.com")
assert User.blocks?(user, user_from_subdomain)
assert User.blocks?(user, user_with_two_subdomains)
assert User.blocks?(user, user_domain)
end
test "unblocks domains" do
user = insert(:user)
collateral_user = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"})
{:ok, user} = User.block_domain(user, "awful-and-rude-instance.com")
{:ok, user} = User.unblock_domain(user, "awful-and-rude-instance.com")
refute User.blocks?(user, collateral_user)
end
test "follows take precedence over domain blocks" do
user = insert(:user)
good_eggo = insert(:user, %{ap_id: "https://meanies.social/user/cuteposter"})
{:ok, user} = User.block_domain(user, "meanies.social")
{:ok, user, good_eggo} = User.follow(user, good_eggo)
refute User.blocks?(user, good_eggo)
end
end
describe "get_recipients_from_activity" do
test "works for announces" do
actor = insert(:user)
user = insert(:user, local: true)
{:ok, activity} = CommonAPI.post(actor, %{status: "hello"})
{:ok, announce} = CommonAPI.repeat(activity.id, user)
recipients = User.get_recipients_from_activity(announce)
assert user in recipients
end
test "get recipients" do
actor = insert(:user)
user = insert(:user, local: true)
user_two = insert(:user, local: false)
addressed = insert(:user, local: true)
addressed_remote = insert(:user, local: false)
{:ok, activity} =
CommonAPI.post(actor, %{
status: "hey @#{addressed.nickname} @#{addressed_remote.nickname}"
})
assert Enum.map([actor, addressed], & &1.ap_id) --
Enum.map(User.get_recipients_from_activity(activity), & &1.ap_id) == []
{:ok, user, actor} = User.follow(user, actor)
{:ok, _user_two, _actor} = User.follow(user_two, actor)
recipients = User.get_recipients_from_activity(activity)
assert length(recipients) == 3
assert user in recipients
assert addressed in recipients
end
test "has following" do
actor = insert(:user)
user = insert(:user)
user_two = insert(:user)
addressed = insert(:user, local: true)
{:ok, activity} =
CommonAPI.post(actor, %{
status: "hey @#{addressed.nickname}"
})
assert Enum.map([actor, addressed], & &1.ap_id) --
Enum.map(User.get_recipients_from_activity(activity), & &1.ap_id) == []
{:ok, _actor, _user} = User.follow(actor, user)
{:ok, _actor, _user_two} = User.follow(actor, user_two)
recipients = User.get_recipients_from_activity(activity)
assert length(recipients) == 2
assert addressed in recipients
end
end
describe ".set_activation" do
test "can de-activate then re-activate a user" do
user = insert(:user)
assert user.is_active
{:ok, user} = User.set_activation(user, false)
refute user.is_active
{:ok, user} = User.set_activation(user, true)
assert user.is_active
end
test "hide a user from followers" do
user = insert(:user)
user2 = insert(:user)
{:ok, user, user2} = User.follow(user, user2)
{:ok, _user} = User.set_activation(user, false)
user2 = User.get_cached_by_id(user2.id)
assert user2.follower_count == 0
assert [] = User.get_followers(user2)
end
test "hide a user from friends" do
user = insert(:user)
user2 = insert(:user)
{:ok, user2, user} = User.follow(user2, user)
assert user2.following_count == 1
assert User.following_count(user2) == 1
{:ok, _user} = User.set_activation(user, false)
user2 = User.get_cached_by_id(user2.id)
assert refresh_record(user2).following_count == 0
assert user2.following_count == 0
assert User.following_count(user2) == 0
assert [] = User.get_friends(user2)
end
test "hide a user's statuses from timelines and notifications" do
user = insert(:user)
user2 = insert(:user)
{:ok, user2, user} = User.follow(user2, user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{user2.nickname}"})
activity = Repo.preload(activity, :bookmark)
[notification] = Pleroma.Notification.for_user(user2)
assert notification.activity.id == activity.id
assert [activity] == ActivityPub.fetch_public_activities(%{}) |> Repo.preload(:bookmark)
- assert [%{activity | thread_muted?: CommonAPI.thread_muted?(user2, activity)}] ==
+ assert [%{activity | thread_muted?: CommonAPI.thread_muted?(activity, user2)}] ==
ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{
user: user2
})
{:ok, _user} = User.set_activation(user, false)
assert [] == ActivityPub.fetch_public_activities(%{})
assert [] == Pleroma.Notification.for_user(user2)
assert [] ==
ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{
user: user2
})
end
end
describe "approve" do
test "approves a user" do
user = insert(:user, is_approved: false)
refute user.is_approved
{:ok, user} = User.approve(user)
assert user.is_approved
end
test "approves a list of users" do
unapproved_users = [
insert(:user, is_approved: false),
insert(:user, is_approved: false),
insert(:user, is_approved: false)
]
{:ok, users} = User.approve(unapproved_users)
assert Enum.count(users) == 3
Enum.each(users, fn user ->
assert user.is_approved
end)
end
test "it sends welcome email if it is set" do
clear_config([:welcome, :email, :enabled], true)
clear_config([:welcome, :email, :sender], "tester@test.me")
user = insert(:user, is_approved: false)
welcome_user = insert(:user, email: "tester@test.me")
instance_name = Pleroma.Config.get([:instance, :name])
User.approve(user)
ObanHelpers.perform_all()
assert_email_sent(
from: {instance_name, welcome_user.email},
to: {user.name, user.email},
html_body: "Welcome to #{instance_name}"
)
end
test "approving an approved user does not trigger post-register actions" do
clear_config([:welcome, :email, :enabled], true)
user = insert(:user, is_approved: true)
User.approve(user)
ObanHelpers.perform_all()
assert_no_email_sent()
end
end
describe "confirm" do
test "confirms a user" do
user = insert(:user, is_confirmed: false)
refute user.is_confirmed
{:ok, user} = User.confirm(user)
assert user.is_confirmed
end
test "confirms a list of users" do
unconfirmed_users = [
insert(:user, is_confirmed: false),
insert(:user, is_confirmed: false),
insert(:user, is_confirmed: false)
]
{:ok, users} = User.confirm(unconfirmed_users)
assert Enum.count(users) == 3
Enum.each(users, fn user ->
assert user.is_confirmed
end)
end
test "sends approval emails when `is_approved: false`" do
admin = insert(:user, is_admin: true)
user = insert(:user, is_confirmed: false, is_approved: false)
User.confirm(user)
ObanHelpers.perform_all()
user_email = Pleroma.Emails.UserEmail.approval_pending_email(user)
admin_email = Pleroma.Emails.AdminEmail.new_unapproved_registration(admin, user)
notify_email = Pleroma.Config.get([:instance, :notify_email])
instance_name = Pleroma.Config.get([:instance, :name])
# User approval email
assert_email_sent(
from: {instance_name, notify_email},
to: {user.name, user.email},
html_body: user_email.html_body
)
# Admin email
assert_email_sent(
from: {instance_name, notify_email},
to: {admin.name, admin.email},
html_body: admin_email.html_body
)
end
test "confirming a confirmed user does not trigger post-register actions" do
user = insert(:user, is_confirmed: true, is_approved: false)
User.confirm(user)
ObanHelpers.perform_all()
assert_no_email_sent()
end
end
describe "delete" do
setup do
{:ok, user} = insert(:user) |> User.set_cache()
[user: user]
end
setup do: clear_config([:instance, :federating])
test ".delete_user_activities deletes all create activities", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "2hu"})
User.delete_user_activities(user)
# TODO: Test removal favorites, repeats, delete activities.
refute Activity.get_by_id(activity.id)
end
test "it deactivates a user, all follow relationships and all activities", %{user: user} do
follower = insert(:user)
{:ok, follower, user} = User.follow(follower, user)
locked_user = insert(:user, name: "locked", is_locked: true)
{:ok, _, _} = User.follow(user, locked_user, :follow_pending)
object = insert(:note, user: user)
activity = insert(:note_activity, user: user, note: object)
object_two = insert(:note, user: follower)
activity_two = insert(:note_activity, user: follower, note: object_two)
{:ok, like} = CommonAPI.favorite(activity_two.id, user)
{:ok, like_two} = CommonAPI.favorite(activity.id, follower)
{:ok, repeat} = CommonAPI.repeat(activity_two.id, user)
{:ok, job} = User.delete(user)
{:ok, _user} = ObanHelpers.perform(job)
follower = User.get_cached_by_id(follower.id)
refute User.following?(follower, user)
assert %{is_active: false} = User.get_by_id(user.id)
assert [] == User.get_follow_requests(locked_user)
user_activities =
user.ap_id
|> Activity.Queries.by_actor()
|> Repo.all()
|> Enum.map(fn act -> act.data["type"] end)
assert Enum.all?(user_activities, fn act -> act in ~w(Delete Undo) end)
refute Activity.get_by_id(activity.id)
refute Activity.get_by_id(like.id)
refute Activity.get_by_id(like_two.id)
refute Activity.get_by_id(repeat.id)
end
end
test "delete/1 when confirmation is pending deletes the user" do
clear_config([:instance, :account_activation_required], true)
user = insert(:user, is_confirmed: false)
{:ok, job} = User.delete(user)
{:ok, _} = ObanHelpers.perform(job)
refute User.get_cached_by_id(user.id)
refute User.get_by_id(user.id)
end
test "delete/1 when approval is pending deletes the user" do
user = insert(:user, is_approved: false)
{:ok, job} = User.delete(user)
{:ok, _} = ObanHelpers.perform(job)
refute User.get_cached_by_id(user.id)
refute User.get_by_id(user.id)
end
test "delete/1 purges a user when they wouldn't be fully deleted" do
user =
insert(:user, %{
bio: "eyy lmao",
name: "qqqqqqq",
password_hash: "pdfk2$1b3n159001",
keys: "RSA begin buplic key",
public_key: "--PRIVATE KEYE--",
avatar: %{"a" => "b"},
tags: ["qqqqq"],
banner: %{"a" => "b"},
background: %{"a" => "b"},
note_count: 9,
follower_count: 9,
following_count: 9001,
is_locked: true,
is_confirmed: true,
password_reset_pending: true,
is_approved: true,
registration_reason: "ahhhhh",
confirmation_token: "qqqq",
domain_blocks: ["lain.com"],
is_active: false,
is_moderator: true,
is_admin: true,
mascot: %{"a" => "b"},
emoji: %{"a" => "b"},
pleroma_settings_store: %{"q" => "x"},
fields: [%{"gg" => "qq"}],
raw_fields: [%{"gg" => "qq"}],
is_discoverable: true,
also_known_as: ["https://lol.olo/users/loll"]
})
{:ok, job} = User.delete(user)
{:ok, _} = ObanHelpers.perform(job)
user = User.get_by_id(user.id)
assert %User{
bio: "",
raw_bio: nil,
email: nil,
name: nil,
password_hash: nil,
keys: "RSA begin buplic key",
public_key: "--PRIVATE KEYE--",
avatar: %{},
tags: [],
last_refreshed_at: nil,
last_digest_emailed_at: nil,
banner: %{},
background: %{},
note_count: 0,
follower_count: 0,
following_count: 0,
is_locked: false,
is_confirmed: true,
password_reset_pending: false,
is_approved: true,
registration_reason: nil,
confirmation_token: nil,
domain_blocks: [],
is_active: false,
is_moderator: false,
is_admin: false,
mascot: nil,
emoji: %{},
pleroma_settings_store: %{},
fields: [],
raw_fields: [],
is_discoverable: false,
also_known_as: []
} = user
end
test "delete/1 purges a remote user" do
user =
insert(:user, %{
name: "qqqqqqq",
avatar: %{"a" => "b"},
banner: %{"a" => "b"},
local: false
})
{:ok, job} = User.delete(user)
{:ok, _} = ObanHelpers.perform(job)
user = User.get_by_id(user.id)
assert user.name == nil
assert user.avatar == %{}
assert user.banner == %{}
end
describe "set_suggestion" do
test "suggests a user" do
user = insert(:user, is_suggested: false)
refute user.is_suggested
{:ok, user} = User.set_suggestion(user, true)
assert user.is_suggested
end
test "suggests a list of users" do
unsuggested_users = [
insert(:user, is_suggested: false),
insert(:user, is_suggested: false),
insert(:user, is_suggested: false)
]
{:ok, users} = User.set_suggestion(unsuggested_users, true)
assert Enum.count(users) == 3
Enum.each(users, fn user ->
assert user.is_suggested
end)
end
test "unsuggests a user" do
user = insert(:user, is_suggested: true)
assert user.is_suggested
{:ok, user} = User.set_suggestion(user, false)
refute user.is_suggested
end
end
test "get_public_key_for_ap_id returns correctly for user that's not in the db" do
assert :error = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin")
end
describe "per-user rich-text filtering" do
test "html_filter_policy returns default policies, when rich-text is enabled" do
user = insert(:user)
assert Pleroma.Config.get([:markup, :scrub_policy]) == User.html_filter_policy(user)
end
test "html_filter_policy returns TwitterText scrubber when rich-text is disabled" do
user = insert(:user, no_rich_text: true)
assert Pleroma.HTML.Scrubber.TwitterText == User.html_filter_policy(user)
end
end
describe "caching" do
test "invalidate_cache works" do
user = insert(:user)
User.set_cache(user)
User.invalidate_cache(user)
{:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
{:ok, nil} = Cachex.get(:user_cache, "nickname:#{user.nickname}")
end
test "User.delete() plugs any possible zombie objects" do
user = insert(:user)
{:ok, job} = User.delete(user)
{:ok, _} = ObanHelpers.perform(job)
{:ok, cached_user} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
assert cached_user != user
{:ok, cached_user} = Cachex.get(:user_cache, "nickname:#{user.ap_id}")
assert cached_user != user
end
end
describe "account_status/1" do
setup do: clear_config([:instance, :account_activation_required])
test "return confirmation_pending for unconfirm user" do
clear_config([:instance, :account_activation_required], true)
user = insert(:user, is_confirmed: false)
assert User.account_status(user) == :confirmation_pending
end
test "return active for confirmed user" do
clear_config([:instance, :account_activation_required], true)
user = insert(:user, is_confirmed: true)
assert User.account_status(user) == :active
end
test "return active for remote user" do
user = insert(:user, local: false)
assert User.account_status(user) == :active
end
test "returns :password_reset_pending for user with reset password" do
user = insert(:user, password_reset_pending: true)
assert User.account_status(user) == :password_reset_pending
end
test "returns :deactivated for deactivated user" do
user = insert(:user, local: true, is_confirmed: true, is_active: false)
assert User.account_status(user) == :deactivated
end
test "returns :approval_pending for unapproved user" do
user = insert(:user, local: true, is_approved: false)
assert User.account_status(user) == :approval_pending
user = insert(:user, local: true, is_confirmed: false, is_approved: false)
assert User.account_status(user) == :approval_pending
end
end
describe "privileged?/1" do
setup do
clear_config([:instance, :admin_privileges], [:cofe, :suya])
clear_config([:instance, :moderator_privileges], [:cofe, :suya])
end
test "returns false for unprivileged users" do
user = insert(:user, local: true)
refute User.privileged?(user, :cofe)
end
test "returns false for remote users" do
user = insert(:user, local: false)
remote_admin_user = insert(:user, local: false, is_admin: true)
refute User.privileged?(user, :cofe)
refute User.privileged?(remote_admin_user, :cofe)
end
test "returns true for local moderators if, and only if, they are privileged" do
user = insert(:user, local: true, is_moderator: true)
assert User.privileged?(user, :cofe)
clear_config([:instance, :moderator_privileges], [])
refute User.privileged?(user, :cofe)
end
test "returns true for local admins if, and only if, they are privileged" do
user = insert(:user, local: true, is_admin: true)
assert User.privileged?(user, :cofe)
clear_config([:instance, :admin_privileges], [])
refute User.privileged?(user, :cofe)
end
end
describe "privileges/1" do
setup do
clear_config([:instance, :moderator_privileges], [:cofe, :only_moderator])
clear_config([:instance, :admin_privileges], [:cofe, :only_admin])
end
test "returns empty list for users without roles" do
user = insert(:user, local: true)
assert [] == User.privileges(user)
end
test "returns list of privileges for moderators" do
moderator = insert(:user, is_moderator: true, local: true)
assert [:cofe, :only_moderator] == User.privileges(moderator) |> Enum.sort()
end
test "returns list of privileges for admins" do
admin = insert(:user, is_admin: true, local: true)
assert [:cofe, :only_admin] == User.privileges(admin) |> Enum.sort()
end
test "returns list of unique privileges for users who are both moderator and admin" do
moderator_admin = insert(:user, is_moderator: true, is_admin: true, local: true)
assert [:cofe, :only_admin, :only_moderator] ==
User.privileges(moderator_admin) |> Enum.sort()
end
test "returns empty list for remote users" do
remote_moderator_admin = insert(:user, is_moderator: true, is_admin: true, local: false)
assert [] == User.privileges(remote_moderator_admin)
end
end
describe "invisible?/1" do
test "returns true for an invisible user" do
user = insert(:user, local: true, invisible: true)
assert User.invisible?(user)
end
test "returns false for a non-invisible user" do
user = insert(:user, local: true)
refute User.invisible?(user)
end
end
describe "visible_for/2" do
test "returns true when the account is itself" do
user = insert(:user, local: true)
assert User.visible_for(user, user) == :visible
end
test "returns false when the account is unconfirmed and confirmation is required" do
clear_config([:instance, :account_activation_required], true)
user = insert(:user, local: true, is_confirmed: false)
other_user = insert(:user, local: true)
refute User.visible_for(user, other_user) == :visible
end
test "returns true when the account is unconfirmed and confirmation is required but the account is remote" do
clear_config([:instance, :account_activation_required], true)
user = insert(:user, local: false, is_confirmed: false)
other_user = insert(:user, local: true)
assert User.visible_for(user, other_user) == :visible
end
test "returns true when the account is unconfirmed and being viewed by a privileged account (privilege :users_manage_activation_state, confirmation required)" do
clear_config([:instance, :account_activation_required], true)
clear_config([:instance, :admin_privileges], [:users_manage_activation_state])
user = insert(:user, local: true, is_confirmed: false)
other_user = insert(:user, local: true, is_admin: true)
assert User.visible_for(user, other_user) == :visible
clear_config([:instance, :admin_privileges], [])
refute User.visible_for(user, other_user) == :visible
end
end
describe "all_users_with_privilege/1" do
setup do
%{
user: insert(:user, local: true, is_admin: false, is_moderator: false),
moderator_user: insert(:user, local: true, is_admin: false, is_moderator: true),
admin_user: insert(:user, local: true, is_admin: true, is_moderator: false),
admin_moderator_user: insert(:user, local: true, is_admin: true, is_moderator: true),
remote_user: insert(:user, local: false, is_admin: true, is_moderator: true),
non_active_user:
insert(:user, local: true, is_admin: true, is_moderator: true, is_active: false)
}
end
test "doesn't return any users when there are no privileged roles" do
clear_config([:instance, :admin_privileges], [])
clear_config([:instance, :moderator_privileges], [])
assert [] = User.Query.build(%{is_privileged: :cofe}) |> Repo.all()
end
test "returns moderator users if they are privileged", %{
moderator_user: moderator_user,
admin_moderator_user: admin_moderator_user
} do
clear_config([:instance, :admin_privileges], [])
clear_config([:instance, :moderator_privileges], [:cofe])
assert [_, _] = User.Query.build(%{is_privileged: :cofe}) |> Repo.all()
assert moderator_user in User.all_users_with_privilege(:cofe)
assert admin_moderator_user in User.all_users_with_privilege(:cofe)
end
test "returns admin users if they are privileged", %{
admin_user: admin_user,
admin_moderator_user: admin_moderator_user
} do
clear_config([:instance, :admin_privileges], [:cofe])
clear_config([:instance, :moderator_privileges], [])
assert [_, _] = User.Query.build(%{is_privileged: :cofe}) |> Repo.all()
assert admin_user in User.all_users_with_privilege(:cofe)
assert admin_moderator_user in User.all_users_with_privilege(:cofe)
end
test "returns admin and moderator users if they are both privileged", %{
moderator_user: moderator_user,
admin_user: admin_user,
admin_moderator_user: admin_moderator_user
} do
clear_config([:instance, :admin_privileges], [:cofe])
clear_config([:instance, :moderator_privileges], [:cofe])
assert [_, _, _] = User.Query.build(%{is_privileged: :cofe}) |> Repo.all()
assert admin_user in User.all_users_with_privilege(:cofe)
assert moderator_user in User.all_users_with_privilege(:cofe)
assert admin_moderator_user in User.all_users_with_privilege(:cofe)
end
end
describe "parse_bio/2" do
test "preserves hosts in user links text" do
remote_user = insert(:user, local: false, nickname: "nick@domain.com")
user = insert(:user)
bio = "A.k.a. @nick@domain.com"
expected_text =
~s(A.k.a. <span class="h-card"><a class="u-url mention" data-user="#{remote_user.id}" href="#{remote_user.ap_id}" rel="ugc">@<span>nick@domain.com</span></a></span>)
assert expected_text == User.parse_bio(bio, user)
end
test "Adds rel=me on linkbacked urls" do
user = insert(:user, ap_id: "https://social.example.org/users/lain")
bio = "http://example.com/rel_me/null"
expected_text = "<a href=\"#{bio}\">#{bio}</a>"
assert expected_text == User.parse_bio(bio, user)
bio = "http://example.com/rel_me/link"
expected_text = "<a href=\"#{bio}\" rel=\"me\">#{bio}</a>"
assert expected_text == User.parse_bio(bio, user)
bio = "http://example.com/rel_me/anchor"
expected_text = "<a href=\"#{bio}\" rel=\"me\">#{bio}</a>"
assert expected_text == User.parse_bio(bio, user)
end
end
test "follower count is updated when a follower is blocked" do
user = insert(:user)
follower = insert(:user)
follower2 = insert(:user)
follower3 = insert(:user)
{:ok, follower, user} = User.follow(follower, user)
{:ok, _follower2, _user} = User.follow(follower2, user)
{:ok, _follower3, _user} = User.follow(follower3, user)
{:ok, _user_relationship} = User.block(user, follower)
user = refresh_record(user)
assert user.follower_count == 2
end
describe "list_inactive_users_query/1" do
defp days_ago(days) do
NaiveDateTime.add(
NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second),
-days * 60 * 60 * 24,
:second
)
end
test "Users are inactive by default" do
total = 10
users =
Enum.map(1..total, fn _ ->
insert(:user, last_digest_emailed_at: days_ago(20), is_active: true)
end)
inactive_users_ids =
Pleroma.User.list_inactive_users_query()
|> Pleroma.Repo.all()
|> Enum.map(& &1.id)
Enum.each(users, fn user ->
assert user.id in inactive_users_ids
end)
end
test "Only includes users who has no recent activity" do
total = 10
users =
Enum.map(1..total, fn _ ->
insert(:user, last_digest_emailed_at: days_ago(20), is_active: true)
end)
{inactive, active} = Enum.split(users, trunc(total / 2))
Enum.map(active, fn user ->
to = Enum.random(users -- [user])
{:ok, _} =
CommonAPI.post(user, %{
status: "hey @#{to.nickname}"
})
end)
inactive_users_ids =
Pleroma.User.list_inactive_users_query()
|> Pleroma.Repo.all()
|> Enum.map(& &1.id)
Enum.each(active, fn user ->
refute user.id in inactive_users_ids
end)
Enum.each(inactive, fn user ->
assert user.id in inactive_users_ids
end)
end
test "Only includes users with no read notifications" do
total = 10
users =
Enum.map(1..total, fn _ ->
insert(:user, last_digest_emailed_at: days_ago(20), is_active: true)
end)
[sender | recipients] = users
{inactive, active} = Enum.split(recipients, trunc(total / 2))
Enum.each(recipients, fn to ->
{:ok, _} =
CommonAPI.post(sender, %{
status: "hey @#{to.nickname}"
})
{:ok, _} =
CommonAPI.post(sender, %{
status: "hey again @#{to.nickname}"
})
end)
Enum.each(active, fn user ->
[n1, _n2] = Pleroma.Notification.for_user(user)
{:ok, _} = Pleroma.Notification.read_one(user, n1.id)
end)
inactive_users_ids =
Pleroma.User.list_inactive_users_query()
|> Pleroma.Repo.all()
|> Enum.map(& &1.id)
Enum.each(active, fn user ->
refute user.id in inactive_users_ids
end)
Enum.each(inactive, fn user ->
assert user.id in inactive_users_ids
end)
end
end
describe "get_ap_ids_by_nicknames" do
test "it returns a list of AP ids for a given set of nicknames" do
user = insert(:user)
user_two = insert(:user)
ap_ids = User.get_ap_ids_by_nicknames([user.nickname, user_two.nickname, "nonexistent"])
assert length(ap_ids) == 2
assert user.ap_id in ap_ids
assert user_two.ap_id in ap_ids
end
test "it returns a list of AP ids in the same order" do
user = insert(:user)
user_two = insert(:user)
user_three = insert(:user)
ap_ids =
User.get_ap_ids_by_nicknames([user.nickname, user_three.nickname, user_two.nickname])
assert [user.ap_id, user_three.ap_id, user_two.ap_id] == ap_ids
end
end
describe "sync followers count" do
setup do
user1 = insert(:user, local: false, ap_id: "http://localhost:4001/users/masto_closed")
user2 = insert(:user, local: false, ap_id: "http://localhost:4001/users/fuser2")
insert(:user, local: true)
insert(:user, local: false, is_active: false)
{:ok, user1: user1, user2: user2}
end
test "external_users/1 external active users with limit", %{user1: user1, user2: user2} do
[fdb_user1] = User.external_users(limit: 1)
assert fdb_user1.ap_id
assert fdb_user1.ap_id == user1.ap_id
assert fdb_user1.id == user1.id
[fdb_user2] = User.external_users(max_id: fdb_user1.id, limit: 1)
assert fdb_user2.ap_id
assert fdb_user2.ap_id == user2.ap_id
assert fdb_user2.id == user2.id
assert User.external_users(max_id: fdb_user2.id, limit: 1) == []
end
end
describe "internal?/1" do
test "non-internal user returns false" do
user = insert(:user)
refute User.internal?(user)
end
test "user with no nickname returns true" do
user = insert(:user, %{nickname: nil})
assert User.internal?(user)
end
test "user with internal-prefixed nickname returns true" do
user = insert(:user, %{nickname: "internal.test"})
assert User.internal?(user)
end
end
describe "update_and_set_cache/1" do
test "returns error when user is stale instead Ecto.StaleEntryError" do
user = insert(:user)
changeset = Ecto.Changeset.change(user, bio: "test")
Repo.delete(user)
assert {:error, %Ecto.Changeset{errors: [id: {"is stale", [stale: true]}], valid?: false}} =
User.update_and_set_cache(changeset)
end
test "performs update cache if user updated" do
user = insert(:user)
assert {:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
changeset = Ecto.Changeset.change(user, bio: "test-bio")
assert {:ok, %User{bio: "test-bio"} = user} = User.update_and_set_cache(changeset)
assert {:ok, user} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
assert %User{bio: "test-bio"} = User.get_cached_by_ap_id(user.ap_id)
end
end
describe "following/followers synchronization" do
setup do: clear_config([:instance, :external_user_synchronization])
test "updates the counters normally on following/getting a follow when disabled" do
clear_config([:instance, :external_user_synchronization], false)
user = insert(:user)
other_user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
following_address: "http://localhost:4001/users/masto_closed/following"
)
assert other_user.following_count == 0
assert other_user.follower_count == 0
{:ok, user, other_user} = Pleroma.User.follow(user, other_user)
assert user.following_count == 1
assert other_user.follower_count == 1
end
test "synchronizes the counters with the remote instance for the followed when enabled" do
clear_config([:instance, :external_user_synchronization], false)
user = insert(:user)
other_user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
following_address: "http://localhost:4001/users/masto_closed/following"
)
assert other_user.following_count == 0
assert other_user.follower_count == 0
clear_config([:instance, :external_user_synchronization], true)
{:ok, _user, other_user} = User.follow(user, other_user)
assert other_user.follower_count == 437
end
test "synchronizes the counters with the remote instance for the follower when enabled" do
clear_config([:instance, :external_user_synchronization], false)
user = insert(:user)
other_user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
following_address: "http://localhost:4001/users/masto_closed/following"
)
assert other_user.following_count == 0
assert other_user.follower_count == 0
clear_config([:instance, :external_user_synchronization], true)
{:ok, other_user, _user} = User.follow(other_user, user)
assert other_user.following_count == 152
end
end
describe "change_email/2" do
setup do
[user: insert(:user)]
end
test "blank email returns error if we require an email on registration", %{user: user} do
orig_account_activation_required =
Pleroma.Config.get([:instance, :account_activation_required])
Pleroma.Config.put([:instance, :account_activation_required], true)
on_exit(fn ->
Pleroma.Config.put(
[:instance, :account_activation_required],
orig_account_activation_required
)
end)
assert {:error, %{errors: [email: {"can't be blank", _}]}} = User.change_email(user, "")
assert {:error, %{errors: [email: {"can't be blank", _}]}} = User.change_email(user, nil)
end
test "blank email should be fine if we do not require an email on registration", %{user: user} do
orig_account_activation_required =
Pleroma.Config.get([:instance, :account_activation_required])
Pleroma.Config.put([:instance, :account_activation_required], false)
on_exit(fn ->
Pleroma.Config.put(
[:instance, :account_activation_required],
orig_account_activation_required
)
end)
assert {:ok, %User{email: nil}} = User.change_email(user, "")
assert {:ok, %User{email: nil}} = User.change_email(user, nil)
end
test "non unique email returns error", %{user: user} do
%{email: email} = insert(:user)
assert {:error, %{errors: [email: {"has already been taken", _}]}} =
User.change_email(user, email)
end
test "invalid email returns error", %{user: user} do
assert {:error, %{errors: [email: {"has invalid format", _}]}} =
User.change_email(user, "cofe")
end
test "changes email", %{user: user} do
assert {:ok, %User{email: "cofe@cofe.party"}} = User.change_email(user, "cofe@cofe.party")
end
test "adds email", %{user: user} do
orig_account_activation_required =
Pleroma.Config.get([:instance, :account_activation_required])
Pleroma.Config.put([:instance, :account_activation_required], false)
on_exit(fn ->
Pleroma.Config.put(
[:instance, :account_activation_required],
orig_account_activation_required
)
end)
assert {:ok, _} = User.change_email(user, "")
Pleroma.Config.put([:instance, :account_activation_required], true)
assert {:ok, %User{email: "cofe2@cofe.party"}} = User.change_email(user, "cofe2@cofe.party")
end
end
describe "get_cached_by_nickname_or_id" do
setup do
local_user = insert(:user)
remote_user = insert(:user, nickname: "nickname@example.com", local: false)
[local_user: local_user, remote_user: remote_user]
end
setup do: clear_config([:instance, :limit_to_local_content])
test "allows getting remote users by id no matter what :limit_to_local_content is set to", %{
remote_user: remote_user
} do
clear_config([:instance, :limit_to_local_content], false)
assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id)
clear_config([:instance, :limit_to_local_content], true)
assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id)
clear_config([:instance, :limit_to_local_content], :unauthenticated)
assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id)
end
test "disallows getting remote users by nickname without authentication when :limit_to_local_content is set to :unauthenticated",
%{remote_user: remote_user} do
clear_config([:instance, :limit_to_local_content], :unauthenticated)
assert nil == User.get_cached_by_nickname_or_id(remote_user.nickname)
end
test "allows getting remote users by nickname with authentication when :limit_to_local_content is set to :unauthenticated",
%{remote_user: remote_user, local_user: local_user} do
clear_config([:instance, :limit_to_local_content], :unauthenticated)
assert %User{} = User.get_cached_by_nickname_or_id(remote_user.nickname, for: local_user)
end
test "disallows getting remote users by nickname when :limit_to_local_content is set to true",
%{remote_user: remote_user} do
clear_config([:instance, :limit_to_local_content], true)
assert nil == User.get_cached_by_nickname_or_id(remote_user.nickname)
end
test "allows getting local users by nickname no matter what :limit_to_local_content is set to",
%{local_user: local_user} do
clear_config([:instance, :limit_to_local_content], false)
assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname)
clear_config([:instance, :limit_to_local_content], true)
assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname)
clear_config([:instance, :limit_to_local_content], :unauthenticated)
assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname)
end
end
describe "update_email_notifications/2" do
setup do
user = insert(:user, email_notifications: %{"digest" => true})
{:ok, user: user}
end
test "Notifications are updated", %{user: user} do
true = user.email_notifications["digest"]
assert {:ok, result} = User.update_email_notifications(user, %{"digest" => false})
assert result.email_notifications["digest"] == false
end
end
describe "local_nickname/1" do
test "returns nickname without host" do
assert User.local_nickname("@mentioned") == "mentioned"
assert User.local_nickname("a_local_nickname") == "a_local_nickname"
assert User.local_nickname("nickname@host.com") == "nickname"
end
end
describe "full_nickname/1" do
test "returns fully qualified nickname for local users" do
local_user = insert(:user, nickname: "local_user")
assert User.full_nickname(local_user) == "local_user@localhost"
end
test "returns fully qualified nickname for local users when using different domain for webfinger" do
clear_config([Pleroma.Web.WebFinger, :domain], "plemora.dev")
local_user = insert(:user, nickname: "local_user")
assert User.full_nickname(local_user) == "local_user@plemora.dev"
end
test "returns fully qualified nickname for remote users" do
remote_user = insert(:user, nickname: "remote@host.com", local: false)
assert User.full_nickname(remote_user) == "remote@host.com"
end
test "strips leading @ from mentions" do
assert User.full_nickname("@mentioned") == "mentioned"
assert User.full_nickname("@nickname@host.com") == "nickname@host.com"
end
test "does not modify nicknames" do
assert User.full_nickname("nickname") == "nickname"
assert User.full_nickname("nickname@host.com") == "nickname@host.com"
end
end
test "avatar fallback" do
user = insert(:user)
assert User.avatar_url(user) =~ "/images/avi.png"
clear_config([:assets, :default_user_avatar], "avatar.png")
user = User.get_cached_by_nickname_or_id(user.nickname)
assert User.avatar_url(user) =~ "avatar.png"
assert User.avatar_url(user, no_default: true) == nil
end
test "get_host/1" do
user = insert(:user, ap_id: "https://lain.com/users/lain", nickname: "lain")
assert User.get_host(user) == "lain.com"
end
test "update_last_active_at/1" do
user = insert(:user)
assert is_nil(user.last_active_at)
test_started_at = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
assert {:ok, user} = User.update_last_active_at(user)
assert user.last_active_at >= test_started_at
assert user.last_active_at <= NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
last_active_at =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(-:timer.hours(24), :millisecond)
|> NaiveDateTime.truncate(:second)
assert {:ok, user} =
user
|> cast(%{last_active_at: last_active_at}, [:last_active_at])
|> User.update_and_set_cache()
assert user.last_active_at == last_active_at
assert {:ok, user} = User.update_last_active_at(user)
assert user.last_active_at >= test_started_at
assert user.last_active_at <= NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
end
test "active_user_count/1" do
insert(:user)
insert(:user, %{local: false})
insert(:user, %{last_active_at: NaiveDateTime.utc_now()})
insert(:user, %{last_active_at: Timex.shift(NaiveDateTime.utc_now(), days: -15)})
insert(:user, %{last_active_at: Timex.shift(NaiveDateTime.utc_now(), weeks: -6)})
insert(:user, %{last_active_at: Timex.shift(NaiveDateTime.utc_now(), months: -7)})
insert(:user, %{last_active_at: Timex.shift(NaiveDateTime.utc_now(), years: -2)})
assert User.active_user_count() == 2
assert User.active_user_count(180) == 3
assert User.active_user_count(365) == 4
assert User.active_user_count(1000) == 5
end
describe "pins" do
setup do
user = insert(:user)
[user: user, object_id: object_id_from_created_activity(user)]
end
test "unique pins", %{user: user, object_id: object_id} do
assert {:ok, %{pinned_objects: %{^object_id => pinned_at1} = pins} = updated_user} =
User.add_pinned_object_id(user, object_id)
assert Enum.count(pins) == 1
assert {:ok, %{pinned_objects: %{^object_id => pinned_at2} = pins}} =
User.add_pinned_object_id(updated_user, object_id)
assert pinned_at1 == pinned_at2
assert Enum.count(pins) == 1
end
test "respects max_pinned_statuses limit", %{user: user, object_id: object_id} do
clear_config([:instance, :max_pinned_statuses], 1)
{:ok, updated} = User.add_pinned_object_id(user, object_id)
object_id2 = object_id_from_created_activity(user)
{:error, %{errors: errors}} = User.add_pinned_object_id(updated, object_id2)
assert Keyword.has_key?(errors, :pinned_objects)
end
test "remove_pinned_object_id/2", %{user: user, object_id: object_id} do
assert {:ok, updated} = User.add_pinned_object_id(user, object_id)
{:ok, after_remove} = User.remove_pinned_object_id(updated, object_id)
assert after_remove.pinned_objects == %{}
end
end
defp object_id_from_created_activity(user) do
%{id: id} = insert(:note_activity, user: user)
%{object: %{data: %{"id" => object_id}}} = Activity.get_by_id_with_object(id)
object_id
end
describe "add_alias/2" do
test "should add alias for another user" do
user = insert(:user)
user2 = insert(:user)
assert {:ok, user_updated} = user |> User.add_alias(user2)
assert user_updated.also_known_as |> length() == 1
assert user2.ap_id in user_updated.also_known_as
end
test "should add multiple aliases" do
user = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
assert {:ok, user} = user |> User.add_alias(user2)
assert {:ok, user_updated} = user |> User.add_alias(user3)
assert user_updated.also_known_as |> length() == 2
assert user2.ap_id in user_updated.also_known_as
assert user3.ap_id in user_updated.also_known_as
end
test "should not add duplicate aliases" do
user = insert(:user)
user2 = insert(:user)
assert {:ok, user} = user |> User.add_alias(user2)
assert {:ok, user_updated} = user |> User.add_alias(user2)
assert user_updated.also_known_as |> length() == 1
assert user2.ap_id in user_updated.also_known_as
end
end
describe "alias_users/1" do
test "should get aliases for a user" do
user = insert(:user)
user2 = insert(:user, also_known_as: [user.ap_id])
aliases = user2 |> User.alias_users()
assert aliases |> length() == 1
alias_user = aliases |> Enum.at(0)
assert alias_user.ap_id == user.ap_id
end
end
describe "delete_alias/2" do
test "should delete existing alias" do
user = insert(:user)
user2 = insert(:user, also_known_as: [user.ap_id])
assert {:ok, user_updated} = user2 |> User.delete_alias(user)
assert user_updated.also_known_as == []
end
test "should report error on non-existing alias" do
user = insert(:user)
user2 = insert(:user)
user3 = insert(:user, also_known_as: [user.ap_id])
assert {:error, :no_such_alias} = user3 |> User.delete_alias(user2)
user3_updated = User.get_cached_by_ap_id(user3.ap_id)
assert user3_updated.also_known_as |> length() == 1
assert user.ap_id in user3_updated.also_known_as
end
end
describe "get_familiar_followers/3" do
test "returns familiar followers for a pair of users" do
user1 = insert(:user)
%{id: id2} = user2 = insert(:user)
user3 = insert(:user)
_user4 = insert(:user)
User.follow(user1, user2)
User.follow(user2, user3)
assert [%{id: ^id2}] = User.get_familiar_followers(user3, user1)
end
end
describe "account endorsements" do
test "it pins people" do
user = insert(:user)
pinned_user = insert(:user)
{:ok, _pinned_user, _user} = User.follow(user, pinned_user)
refute User.endorses?(user, pinned_user)
{:ok, _user_relationship} = User.endorse(user, pinned_user)
assert User.endorses?(user, pinned_user)
end
test "it unpins users" do
user = insert(:user)
pinned_user = insert(:user)
{:ok, _pinned_user, _user} = User.follow(user, pinned_user)
{:ok, _user_relationship} = User.endorse(user, pinned_user)
{:ok, _user_pin} = User.unendorse(user, pinned_user)
refute User.endorses?(user, pinned_user)
end
test "it doesn't pin users you do not follow" do
user = insert(:user)
pinned_user = insert(:user)
assert {:error, _message} = User.endorse(user, pinned_user)
refute User.endorses?(user, pinned_user)
end
end
test "it checks fields links for a backlink" do
user = insert(:user, ap_id: "https://social.example.org/users/lain")
fields = [
%{"name" => "Link", "value" => "http://example.com/rel_me/null"},
%{"name" => "Verified link", "value" => "http://example.com/rel_me/link"},
%{"name" => "Not a link", "value" => "i'm not a link"}
]
user
|> User.update_and_set_cache(%{raw_fields: fields})
ObanHelpers.perform_all()
user = User.get_cached_by_id(user.id)
assert [
%{"verified_at" => nil},
%{"verified_at" => verified_at},
%{"verified_at" => nil}
] = user.fields
assert is_binary(verified_at)
end
test "updating fields does not invalidate previously validated links" do
user = insert(:user, ap_id: "https://social.example.org/users/lain")
user
|> User.update_and_set_cache(%{
raw_fields: [%{"name" => "verified link", "value" => "http://example.com/rel_me/link"}]
})
ObanHelpers.perform_all()
%User{fields: [%{"verified_at" => verified_at}]} = user = User.get_cached_by_id(user.id)
user
|> User.update_and_set_cache(%{
raw_fields: [%{"name" => "Verified link", "value" => "http://example.com/rel_me/link"}]
})
user = User.get_cached_by_id(user.id)
assert [%{"verified_at" => ^verified_at}] = user.fields
end
end
diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs
index 7c92084bc..cc4482efe 100644
--- a/test/pleroma/web/common_api_test.exs
+++ b/test/pleroma/web/common_api_test.exs
@@ -1,2137 +1,2137 @@
# 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.Rule
alias Pleroma.Tests.ObanHelpers
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, where: 3]
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(activity.id, user)
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(activity.id, user)
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.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.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.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.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(post_activity.id, user)
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(activity.id, user)
assert {:ok, :already_liked} = CommonAPI.favorite(activity.id, user)
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(activity.id, friend2)
{: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(activity, author)
- assert CommonAPI.thread_muted?(author, activity)
+ assert CommonAPI.thread_muted?(activity, author)
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(activity, user)
- assert CommonAPI.thread_muted?(user, activity)
+ assert CommonAPI.thread_muted?(activity, user)
end
test "add expiring mute", %{user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(activity, user, %{expires_in: 60})
- assert CommonAPI.thread_muted?(user, activity)
+ assert CommonAPI.thread_muted?(activity, user)
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)
+ refute CommonAPI.thread_muted?(activity, user)
end
test "remove mute", %{user: user, activity: activity} do
CommonAPI.add_mute(activity, user)
{:ok, _} = CommonAPI.remove_mute(activity, user)
- refute CommonAPI.thread_muted?(user, activity)
+ refute CommonAPI.thread_muted?(activity, user)
end
test "remove mute by ids", %{user: user, activity: activity} do
CommonAPI.add_mute(activity, user)
{:ok, _} = CommonAPI.remove_mute(activity.id, user.id)
- refute CommonAPI.thread_muted?(user, activity)
+ refute CommonAPI.thread_muted?(activity, user)
end
test "check that mutes can't be duplicate", %{user: user, activity: activity} do
CommonAPI.add_mute(activity, user)
{:error, _} = CommonAPI.add_mute(activity, user)
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
test "creates a report with provided rules" do
reporter = insert(:user)
target_user = insert(:user)
%{id: rule_id} = Rule.create(%{text: "There are no rules"})
reporter_ap_id = reporter.ap_id
target_ap_id = target_user.ap_id
report_data = %{
account_id: target_user.id,
rule_ids: [rule_id]
}
assert {:ok, flag_activity} = CommonAPI.report(reporter, report_data)
assert %Activity{
actor: ^reporter_ap_id,
data: %{
"type" => "Flag",
"object" => [^target_ap_id],
"state" => "open",
"rules" => [^rule_id]
}
} = flag_activity
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.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.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.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.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(activity.id, user)
assert Visibility.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(activity.id, user)
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, activity} = CommonAPI.unfavorite(activity.id, user)
assert Visibility.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.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.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(activity, user, %{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(activity, user, %{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(activity, user, %{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(activity, user, %{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(reply, user, %{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(activity, user, %{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
describe "Oban jobs are cancelled" do
setup do
clear_config([:instance, :federating], true)
local_user = insert(:user)
remote_one =
insert(:user, %{
local: false,
nickname: "nick1@domain.com",
ap_id: "https://domain.com/users/nick1",
inbox: "https://domain.com/users/nick1/inbox",
shared_inbox: "https://domain.com/inbox"
})
remote_two =
insert(:user, %{
local: false,
nickname: "nick2@example.com",
ap_id: "https://example.com/users/nick2",
inbox: "https://example.com/users/nick2/inbox",
shared_inbox: "https://example.com/inbox"
})
%{local_user: local_user, remote_one: remote_one, remote_two: remote_two}
end
test "when deleting posts", %{
local_user: local_user,
remote_one: remote_one,
remote_two: remote_two
} do
{:ok, _, _} = Pleroma.User.follow(remote_one, local_user)
{:ok, _, _} = Pleroma.User.follow(remote_two, local_user)
{:ok, %{data: %{"id" => ap_id}} = activity} =
CommonAPI.post(local_user, %{status: "Happy Friday everyone!"})
# Generate the publish_one jobs
ObanHelpers.perform_all()
publish_one_jobs =
all_enqueued()
|> Enum.filter(fn job ->
match?(
%{
state: "available",
queue: "federator_outgoing",
worker: "Pleroma.Workers.PublisherWorker",
args: %{"op" => "publish_one", "params" => %{"id" => ^ap_id}}
},
job
)
end)
assert length(publish_one_jobs) == 2
# The delete should have triggered cancelling the publish_one jobs
assert {:ok, _delete} = CommonAPI.delete(activity.id, local_user)
# all_enqueued/1 will not return cancelled jobs
cancelled_jobs =
Oban.Job
|> where([j], j.worker == "Pleroma.Workers.PublisherWorker")
|> where([j], j.state == "cancelled")
|> where([j], j.args["op"] == "publish_one")
|> where([j], j.args["params"]["id"] == ^ap_id)
|> Pleroma.Repo.all()
assert length(cancelled_jobs) == 2
end
test "when unfavoriting posts", %{
local_user: local_user,
remote_one: remote_user
} do
{:ok, activity} =
CommonAPI.post(remote_user, %{status: "I like turtles!"})
{:ok, %{data: %{"id" => ap_id}} = _favorite} =
CommonAPI.favorite(activity.id, local_user)
# Generate the publish_one jobs
ObanHelpers.perform_all()
publish_one_jobs =
all_enqueued()
|> Enum.filter(fn job ->
match?(
%{
state: "available",
queue: "federator_outgoing",
worker: "Pleroma.Workers.PublisherWorker",
args: %{"op" => "publish_one", "params" => %{"id" => ^ap_id}}
},
job
)
end)
assert length(publish_one_jobs) == 1
# The unfavorite should have triggered cancelling the publish_one jobs
assert {:ok, _unfavorite} = CommonAPI.unfavorite(activity.id, local_user)
# all_enqueued/1 will not return cancelled jobs
cancelled_jobs =
Oban.Job
|> where([j], j.worker == "Pleroma.Workers.PublisherWorker")
|> where([j], j.state == "cancelled")
|> where([j], j.args["op"] == "publish_one")
|> where([j], j.args["params"]["id"] == ^ap_id)
|> Pleroma.Repo.all()
assert length(cancelled_jobs) == 1
end
test "when unboosting posts", %{
local_user: local_user,
remote_one: remote_one,
remote_two: remote_two
} do
{:ok, _, _} = Pleroma.User.follow(remote_one, local_user)
{:ok, _, _} = Pleroma.User.follow(remote_two, local_user)
{:ok, activity} =
CommonAPI.post(remote_one, %{status: "This is an unpleasant post"})
{:ok, %{data: %{"id" => ap_id}} = _repeat} =
CommonAPI.repeat(activity.id, local_user)
# Generate the publish_one jobs
ObanHelpers.perform_all()
publish_one_jobs =
all_enqueued()
|> Enum.filter(fn job ->
match?(
%{
state: "available",
queue: "federator_outgoing",
worker: "Pleroma.Workers.PublisherWorker",
args: %{"op" => "publish_one", "params" => %{"id" => ^ap_id}}
},
job
)
end)
assert length(publish_one_jobs) == 2
# The unrepeat should have triggered cancelling the publish_one jobs
assert {:ok, _unfavorite} = CommonAPI.unrepeat(activity.id, local_user)
# all_enqueued/1 will not return cancelled jobs
cancelled_jobs =
Oban.Job
|> where([j], j.worker == "Pleroma.Workers.PublisherWorker")
|> where([j], j.state == "cancelled")
|> where([j], j.args["op"] == "publish_one")
|> where([j], j.args["params"]["id"] == ^ap_id)
|> Pleroma.Repo.all()
assert length(cancelled_jobs) == 2
end
test "when unreacting to posts", %{
local_user: local_user,
remote_one: remote_one,
remote_two: remote_two
} do
{:ok, _, _} = Pleroma.User.follow(remote_one, local_user)
{:ok, _, _} = Pleroma.User.follow(remote_two, local_user)
{:ok, activity} =
CommonAPI.post(remote_one, %{status: "Gang gang!!!!"})
{:ok, %{data: %{"id" => ap_id}} = _react} =
CommonAPI.react_with_emoji(activity.id, local_user, "👍")
# Generate the publish_one jobs
ObanHelpers.perform_all()
publish_one_jobs =
all_enqueued()
|> Enum.filter(fn job ->
match?(
%{
state: "available",
queue: "federator_outgoing",
worker: "Pleroma.Workers.PublisherWorker",
args: %{"op" => "publish_one", "params" => %{"id" => ^ap_id}}
},
job
)
end)
assert length(publish_one_jobs) == 2
# The unreact should have triggered cancelling the publish_one jobs
assert {:ok, _unreact} = CommonAPI.unreact_with_emoji(activity.id, local_user, "👍")
# all_enqueued/1 will not return cancelled jobs
cancelled_jobs =
Oban.Job
|> where([j], j.worker == "Pleroma.Workers.PublisherWorker")
|> where([j], j.state == "cancelled")
|> where([j], j.args["op"] == "publish_one")
|> where([j], j.args["params"]["id"] == ^ap_id)
|> Pleroma.Repo.all()
assert length(cancelled_jobs) == 2
end
end
end

File Metadata

Mime Type
text/x-diff
Expires
Tue, Oct 13, 3:25 PM (1 d, 22 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1786809
Default Alt Text
(252 KB)

Event Timeline