Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into remake-remodel-dms

This commit is contained in:
lain 2020-05-08 13:13:37 +02:00
commit 7637ef4203
50 changed files with 1415 additions and 896 deletions

View file

@ -156,6 +156,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: `pleroma.thread_muted` to the Status entity - Mastodon API: `pleroma.thread_muted` to the Status entity
- Mastodon API: Mark the direct conversation as read for the author when they send a new direct message - Mastodon API: Mark the direct conversation as read for the author when they send a new direct message
- Mastodon API, streaming: Add `pleroma.direct_conversation_id` to the `conversation` stream event payload. - Mastodon API, streaming: Add `pleroma.direct_conversation_id` to the `conversation` stream event payload.
- Mastodon API: Add `pleroma.unread_count` to the Marker entity
- Admin API: Render whole status in grouped reports - Admin API: Render whole status in grouped reports
- Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise). - Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise).
- Mastodon API: Favoriting / Repeating a post multiple times will now return the identical response every time. Before, executing that action twice would return an error ("already favorited") on the second try. - Mastodon API: Favoriting / Repeating a post multiple times will now return the identical response every time. Before, executing that action twice would return an error ("already favorited") on the second try.

View file

@ -61,6 +61,7 @@ Has these additional fields under the `pleroma` object:
- `deactivated`: boolean, true when the user is deactivated - `deactivated`: boolean, true when the user is deactivated
- `allow_following_move`: boolean, true when the user allows automatically follow moved following accounts - `allow_following_move`: boolean, true when the user allows automatically follow moved following accounts
- `unread_conversation_count`: The count of unread conversations. Only returned to the account owner. - `unread_conversation_count`: The count of unread conversations. Only returned to the account owner.
- `unread_notifications_count`: The count of unread notifications. Only returned to the account owner.
### Source ### Source
@ -218,3 +219,9 @@ Has theses additional parameters (which are the same as in Pleroma-API):
- `pleroma.metadata.features`: A list of supported features - `pleroma.metadata.features`: A list of supported features
- `pleroma.metadata.federation`: The federation restrictions of this instance - `pleroma.metadata.federation`: The federation restrictions of this instance
- `vapid_public_key`: The public key needed for push messages - `vapid_public_key`: The public key needed for push messages
## Markers
Has these additional fields under the `pleroma` object:
- `unread_count`: contains number unread notifications

View file

@ -9,24 +9,34 @@ defmodule Pleroma.Marker do
import Ecto.Query import Ecto.Query
alias Ecto.Multi alias Ecto.Multi
alias Pleroma.Notification
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias __MODULE__
@timelines ["notifications"] @timelines ["notifications"]
@type t :: %__MODULE__{}
schema "markers" do schema "markers" do
field(:last_read_id, :string, default: "") field(:last_read_id, :string, default: "")
field(:timeline, :string, default: "") field(:timeline, :string, default: "")
field(:lock_version, :integer, default: 0) field(:lock_version, :integer, default: 0)
field(:unread_count, :integer, default: 0, virtual: true)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType) belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
timestamps() timestamps()
end end
@doc "Gets markers by user and timeline."
@spec get_markers(User.t(), list(String)) :: list(t())
def get_markers(user, timelines \\ []) do def get_markers(user, timelines \\ []) do
Repo.all(get_query(user, timelines)) user
|> get_query(timelines)
|> unread_count_query()
|> Repo.all()
end end
@spec upsert(User.t(), map()) :: {:ok | :error, any()}
def upsert(%User{} = user, attrs) do def upsert(%User{} = user, attrs) do
attrs attrs
|> Map.take(@timelines) |> Map.take(@timelines)
@ -45,6 +55,27 @@ def upsert(%User{} = user, attrs) do
|> Repo.transaction() |> Repo.transaction()
end end
@spec multi_set_last_read_id(Multi.t(), User.t(), String.t()) :: Multi.t()
def multi_set_last_read_id(multi, %User{} = user, "notifications") do
multi
|> Multi.run(:counters, fn _repo, _changes ->
{:ok, %{last_read_id: Repo.one(Notification.last_read_query(user))}}
end)
|> Multi.insert(
:marker,
fn %{counters: attrs} ->
%Marker{timeline: "notifications", user_id: user.id}
|> struct(attrs)
|> Ecto.Changeset.change()
end,
returning: true,
on_conflict: {:replace, [:last_read_id]},
conflict_target: [:user_id, :timeline]
)
end
def multi_set_last_read_id(multi, _, _), do: multi
defp get_marker(user, timeline) do defp get_marker(user, timeline) do
case Repo.find_resource(get_query(user, timeline)) do case Repo.find_resource(get_query(user, timeline)) do
{:ok, marker} -> %__MODULE__{marker | user: user} {:ok, marker} -> %__MODULE__{marker | user: user}
@ -71,4 +102,16 @@ defp get_query(user, timelines) do
|> by_user_id(user.id) |> by_user_id(user.id)
|> by_timeline(timelines) |> by_timeline(timelines)
end end
defp unread_count_query(query) do
from(
q in query,
left_join: n in "notifications",
on: n.user_id == q.user_id and n.seen == false,
group_by: [:id],
select_merge: %{
unread_count: fragment("count(?)", n.id)
}
)
end
end end

View file

@ -5,8 +5,10 @@
defmodule Pleroma.Notification do defmodule Pleroma.Notification do
use Ecto.Schema use Ecto.Schema
alias Ecto.Multi
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.FollowingRelationship alias Pleroma.FollowingRelationship
alias Pleroma.Marker
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Pagination alias Pleroma.Pagination
@ -34,11 +36,30 @@ defmodule Pleroma.Notification do
timestamps() timestamps()
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
def changeset(%Notification{} = notification, attrs) do def changeset(%Notification{} = notification, attrs) do
notification notification
|> cast(attrs, [:seen]) |> cast(attrs, [:seen])
end 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: [desc: :id]
)
end
defp for_user_query_ap_id_opts(user, opts) do defp for_user_query_ap_id_opts(user, opts) do
ap_id_relationships = ap_id_relationships =
[:block] ++ [:block] ++
@ -185,25 +206,23 @@ def for_user_since(user, date) do
|> Repo.all() |> Repo.all()
end end
def set_read_up_to(%{id: user_id} = _user, id) do def set_read_up_to(%{id: user_id} = user, id) do
query = query =
from( from(
n in Notification, n in Notification,
where: n.user_id == ^user_id, where: n.user_id == ^user_id,
where: n.id <= ^id, where: n.id <= ^id,
where: n.seen == false, where: n.seen == false,
update: [
set: [
seen: true,
updated_at: ^NaiveDateTime.utc_now()
]
],
# Ideally we would preload object and activities here # Ideally we would preload object and activities here
# but Ecto does not support preloads in update_all # but Ecto does not support preloads in update_all
select: n.id select: n.id
) )
{_, notification_ids} = Repo.update_all(query, []) {:ok, %{ids: {_, notification_ids}}} =
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()
Notification Notification
|> where([n], n.id in ^notification_ids) |> where([n], n.id in ^notification_ids)
@ -220,11 +239,18 @@ def set_read_up_to(%{id: user_id} = _user, id) do
|> Repo.all() |> Repo.all()
end end
@spec read_one(User.t(), String.t()) ::
{:ok, Notification.t()} | {:error, Ecto.Changeset.t()} | nil
def read_one(%User{} = user, notification_id) do def read_one(%User{} = user, notification_id) do
with {:ok, %Notification{} = notification} <- get(user, notification_id) do with {:ok, %Notification{} = notification} <- get(user, notification_id) do
notification Multi.new()
|> changeset(%{seen: true}) |> Multi.update(:update, changeset(notification, %{seen: true}))
|> Repo.update() |> Marker.multi_set_last_read_id(user, "notifications")
|> Repo.transaction()
|> case do
{:ok, %{update: notification}} -> {:ok, notification}
{:error, :update, changeset, _} -> {:error, changeset}
end
end end
end end
@ -316,8 +342,11 @@ defp do_create_notifications(%Activity{} = activity) do
# TODO move to sql, too. # TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do
unless skip?(activity, user) do unless skip?(activity, user) do
notification = %Notification{user_id: user.id, activity: activity} {:ok, %{notification: notification}} =
{:ok, notification} = Repo.insert(notification) Multi.new()
|> Multi.insert(:notification, %Notification{user_id: user.id, activity: activity})
|> Marker.multi_set_last_read_id(user, "notifications")
|> Repo.transaction()
if do_send do if do_send do
Streamer.stream(["user", "user:notification"], notification) Streamer.stream(["user", "user:notification"], notification)

View file

@ -1557,23 +1557,13 @@ def delete_user_activities(%User{ap_id: ap_id} = user) do
defp delete_activity(%{data: %{"type" => "Create", "object" => object}}, user) do defp delete_activity(%{data: %{"type" => "Create", "object" => object}}, user) do
{:ok, delete_data, _} = Builder.delete(user, object) {:ok, delete_data, _} = Builder.delete(user, object)
Pipeline.common_pipeline(delete_data, local: true) Pipeline.common_pipeline(delete_data, local: user.local)
end end
defp delete_activity(%{data: %{"type" => "Like"}} = activity, _user) do defp delete_activity(%{data: %{"type" => type}} = activity, user)
object = Object.normalize(activity) when type in ["Like", "Announce"] do
{:ok, undo, _} = Builder.undo(user, activity)
activity.actor Pipeline.common_pipeline(undo, local: user.local)
|> get_cached_by_ap_id()
|> ActivityPub.unlike(object)
end
defp delete_activity(%{data: %{"type" => "Announce"}} = activity, _user) do
object = Object.normalize(activity)
activity.actor
|> get_cached_by_ap_id()
|> ActivityPub.unannounce(object)
end end
defp delete_activity(_activity, _user), do: "Doing nothing" defp delete_activity(_activity, _user), do: "Doing nothing"

View file

@ -363,81 +363,6 @@ def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
end end
end end
@spec react_with_emoji(User.t(), Object.t(), String.t(), keyword()) ::
{:ok, Activity.t(), Object.t()} | {:error, any()}
def react_with_emoji(user, object, emoji, options \\ []) do
with {:ok, result} <-
Repo.transaction(fn -> do_react_with_emoji(user, object, emoji, options) end) do
result
end
end
defp do_react_with_emoji(user, object, emoji, options) do
with local <- Keyword.get(options, :local, true),
activity_id <- Keyword.get(options, :activity_id, nil),
true <- Pleroma.Emoji.is_unicode_emoji?(emoji),
reaction_data <- make_emoji_reaction_data(user, object, emoji, activity_id),
{:ok, activity} <- insert(reaction_data, local),
{:ok, object} <- add_emoji_reaction_to_object(activity, object),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
false -> {:error, false}
{:error, error} -> Repo.rollback(error)
end
end
@spec unreact_with_emoji(User.t(), String.t(), keyword()) ::
{:ok, Activity.t(), Object.t()} | {:error, any()}
def unreact_with_emoji(user, reaction_id, options \\ []) do
with {:ok, result} <-
Repo.transaction(fn -> do_unreact_with_emoji(user, reaction_id, options) end) do
result
end
end
defp do_unreact_with_emoji(user, reaction_id, options) do
with local <- Keyword.get(options, :local, true),
activity_id <- Keyword.get(options, :activity_id, nil),
user_ap_id <- user.ap_id,
%Activity{actor: ^user_ap_id} = reaction_activity <- Activity.get_by_ap_id(reaction_id),
object <- Object.normalize(reaction_activity),
unreact_data <- make_undo_data(user, reaction_activity, activity_id),
{:ok, activity} <- insert(unreact_data, local),
{:ok, object} <- remove_emoji_reaction_from_object(reaction_activity, object),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
{:error, error} -> Repo.rollback(error)
end
end
@spec unlike(User.t(), Object.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t(), Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()}
def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) do
with {:ok, result} <-
Repo.transaction(fn -> do_unlike(actor, object, activity_id, local) end) do
result
end
end
defp do_unlike(actor, object, activity_id, local) do
with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object),
unlike_data <- make_unlike_data(actor, like_activity, activity_id),
{:ok, unlike_activity} <- insert(unlike_data, local),
{:ok, _activity} <- Repo.delete(like_activity),
{:ok, object} <- remove_like_from_object(like_activity, object),
_ <- notify_and_stream(unlike_activity),
:ok <- maybe_federate(unlike_activity) do
{:ok, unlike_activity, like_activity, object}
else
nil -> {:ok, object}
{:error, error} -> Repo.rollback(error)
end
end
@spec announce(User.t(), Object.t(), String.t() | nil, boolean(), boolean()) :: @spec announce(User.t(), Object.t(), String.t() | nil, boolean(), boolean()) ::
{:ok, Activity.t(), Object.t()} | {:error, any()} {:ok, Activity.t(), Object.t()} | {:error, any()}
def announce( def announce(
@ -468,35 +393,6 @@ defp do_announce(user, object, activity_id, local, public) do
end end
end end
@spec unannounce(User.t(), Object.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()}
def unannounce(
%User{} = actor,
%Object{} = object,
activity_id \\ nil,
local \\ true
) do
with {:ok, result} <-
Repo.transaction(fn -> do_unannounce(actor, object, activity_id, local) end) do
result
end
end
defp do_unannounce(actor, object, activity_id, local) do
with %Activity{} = announce_activity <- get_existing_announce(actor.ap_id, object),
unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
{:ok, unannounce_activity} <- insert(unannounce_data, local),
_ <- notify_and_stream(unannounce_activity),
:ok <- maybe_federate(unannounce_activity),
{:ok, _activity} <- Repo.delete(announce_activity),
{:ok, object} <- remove_announce_from_object(announce_activity, object) do
{:ok, unannounce_activity, object}
else
nil -> {:ok, object}
{:error, error} -> Repo.rollback(error)
end
end
@spec follow(User.t(), User.t(), String.t() | nil, boolean()) :: @spec follow(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | {:error, any()} {:ok, Activity.t()} | {:error, any()}
def follow(follower, followed, activity_id \\ nil, local \\ true) do def follow(follower, followed, activity_id \\ nil, local \\ true) do
@ -569,28 +465,6 @@ defp do_block(blocker, blocked, activity_id, local) do
end end
end end
@spec unblock(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | {:error, any()} | nil
def unblock(blocker, blocked, activity_id \\ nil, local \\ true) do
with {:ok, result} <-
Repo.transaction(fn -> do_unblock(blocker, blocked, activity_id, local) end) do
result
end
end
defp do_unblock(blocker, blocked, activity_id, local) do
with %Activity{} = block_activity <- fetch_latest_block(blocker, blocked),
unblock_data <- make_unblock_data(blocker, blocked, block_activity, activity_id),
{:ok, activity} <- insert(unblock_data, local),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
nil -> nil
{:error, error} -> Repo.rollback(error)
end
end
@spec flag(map()) :: {:ok, Activity.t()} | {:error, any()} @spec flag(map()) :: {:ok, Activity.t()} | {:error, any()}
def flag( def flag(
%{ %{

View file

@ -396,7 +396,10 @@ def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
|> json(err) |> json(err)
end end
defp handle_user_activity(%User{} = user, %{"type" => "Create"} = params) do defp handle_user_activity(
%User{} = user,
%{"type" => "Create", "object" => %{"type" => "Note"}} = params
) do
object = object =
params["object"] params["object"]
|> Map.merge(Map.take(params, ["to", "cc"])) |> Map.merge(Map.take(params, ["to", "cc"]))

View file

@ -11,6 +11,31 @@ defmodule Pleroma.Web.ActivityPub.Builder do
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
@spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
def emoji_react(actor, object, emoji) do
with {:ok, data, meta} <- object_action(actor, object) do
data =
data
|> Map.put("content", emoji)
|> Map.put("type", "EmojiReact")
{:ok, data, meta}
end
end
@spec undo(User.t(), Activity.t()) :: {:ok, map(), keyword()}
def undo(actor, object) do
{:ok,
%{
"id" => Utils.generate_activity_id(),
"actor" => actor.ap_id,
"type" => "Undo",
"object" => object.data["id"],
"to" => object.data["to"] || [],
"cc" => object.data["cc"] || []
}, []}
end
@spec delete(User.t(), String.t()) :: {:ok, map(), keyword()} @spec delete(User.t(), String.t()) :: {:ok, map(), keyword()}
def delete(actor, object_id) do def delete(actor, object_id) do
object = Object.normalize(object_id, false) object = Object.normalize(object_id, false)
@ -76,6 +101,17 @@ def chat_message(actor, recipient, content, opts \\ []) do
@spec like(User.t(), Object.t()) :: {:ok, map(), keyword()} @spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
def like(actor, object) do def like(actor, object) do
with {:ok, data, meta} <- object_action(actor, object) do
data =
data
|> Map.put("type", "Like")
{:ok, data, meta}
end
end
@spec object_action(User.t(), Object.t()) :: {:ok, map(), keyword()}
defp object_action(actor, object) do
object_actor = User.get_cached_by_ap_id(object.data["actor"]) object_actor = User.get_cached_by_ap_id(object.data["actor"])
# Address the actor of the object, and our actor's follower collection if the post is public. # Address the actor of the object, and our actor's follower collection if the post is public.
@ -97,7 +133,6 @@ def like(actor, object) do
%{ %{
"id" => Utils.generate_activity_id(), "id" => Utils.generate_activity_id(),
"actor" => actor.ap_id, "actor" => actor.ap_id,
"type" => "Like",
"object" => object.data["id"], "object" => object.data["id"],
"to" => to, "to" => to,
"cc" => cc, "cc" => cc,

View file

@ -14,12 +14,24 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.Types alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
@spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()} @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
def validate(object, meta) def validate(object, meta)
def validate(%{"type" => "Undo"} = object, meta) do
with {:ok, object} <-
object
|> UndoValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
end
end
def validate(%{"type" => "Delete"} = object, meta) do def validate(%{"type" => "Delete"} = object, meta) do
with cng <- DeleteValidator.cast_and_validate(object), with cng <- DeleteValidator.cast_and_validate(object),
do_not_federate <- DeleteValidator.do_not_federate?(cng), do_not_federate <- DeleteValidator.do_not_federate?(cng),
@ -50,6 +62,16 @@ def validate(%{"type" => "ChatMessage"} = object, meta) do
end end
end end
def validate(%{"type" => "EmojiReact"} = object, meta) do
with {:ok, object} <-
object
|> EmojiReactValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object |> Map.from_struct())
{:ok, object, meta}
end
end
def validate(%{"type" => "Create", "object" => object} = create_activity, meta) do def validate(%{"type" => "Create", "object" => object} = create_activity, meta) do
with {:ok, object_data} <- cast_and_apply(object), with {:ok, object_data} <- cast_and_apply(object),
meta = Keyword.put(meta, :object_data, object_data |> stringify_keys), meta = Keyword.put(meta, :object_data, object_data |> stringify_keys),

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
import Ecto.Changeset import Ecto.Changeset
alias Pleroma.Activity
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
@ -47,7 +48,7 @@ def validate_object_presence(cng, options \\ []) do
cng cng
|> validate_change(field_name, fn field_name, object_id -> |> validate_change(field_name, fn field_name, object_id ->
object = Object.get_cached_by_ap_id(object_id) object = Object.get_cached_by_ap_id(object_id) || Activity.get_by_ap_id(object_id)
cond do cond do
!object -> !object ->

View file

@ -0,0 +1,81 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
use Ecto.Schema
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@primary_key false
embedded_schema do
field(:id, Types.ObjectID, primary_key: true)
field(:type, :string)
field(:object, Types.ObjectID)
field(:actor, Types.ObjectID)
field(:context, :string)
field(:content, :string)
field(:to, {:array, :string}, default: [])
field(:cc, {:array, :string}, default: [])
end
def cast_and_validate(data) do
data
|> cast_data()
|> validate_data()
end
def cast_data(data) do
%__MODULE__{}
|> changeset(data)
end
def changeset(struct, data) do
struct
|> cast(data, __schema__(:fields))
|> fix_after_cast()
end
def fix_after_cast(cng) do
cng
|> fix_context()
end
def fix_context(cng) do
object = get_field(cng, :object)
with nil <- get_field(cng, :context),
%Object{data: %{"context" => context}} <- Object.get_cached_by_ap_id(object) do
cng
|> put_change(:context, context)
else
_ ->
cng
end
end
def validate_emoji(cng) do
content = get_field(cng, :content)
if Pleroma.Emoji.is_unicode_emoji?(content) do
cng
else
cng
|> add_error(:content, "must be a single character emoji")
end
end
def validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["EmojiReact"])
|> validate_required([:id, :type, :object, :actor, :context, :to, :cc, :content])
|> validate_actor_presence()
|> validate_object_presence()
|> validate_emoji()
end
end

View file

@ -0,0 +1,62 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator do
use Ecto.Schema
alias Pleroma.Activity
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@primary_key false
embedded_schema do
field(:id, Types.ObjectID, primary_key: true)
field(:type, :string)
field(:object, Types.ObjectID)
field(:actor, Types.ObjectID)
field(:to, {:array, :string}, default: [])
field(:cc, {:array, :string}, default: [])
end
def cast_and_validate(data) do
data
|> cast_data()
|> validate_data()
end
def cast_data(data) do
%__MODULE__{}
|> changeset(data)
end
def changeset(struct, data) do
struct
|> cast(data, __schema__(:fields))
end
def validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Undo"])
|> validate_required([:id, :type, :object, :actor, :to, :cc])
|> validate_actor_presence()
|> validate_object_presence()
|> validate_undo_rights()
end
def validate_undo_rights(cng) do
actor = get_field(cng, :actor)
object = get_field(cng, :object)
with %Activity{data: %{"actor" => object_actor}} <- Activity.get_by_ap_id(object),
true <- object_actor != actor do
cng
|> add_error(:actor, "not the same as object actor")
else
_ -> cng
end
end
end

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
collection, and so on. collection, and so on.
""" """
alias Pleroma.Chat alias Pleroma.Chat
alias Pleroma.Activity
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
@ -41,6 +42,25 @@ def handle(%{data: %{"type" => "Create"}} = activity, meta) do
end end
end end
def handle(%{data: %{"type" => "Undo", "object" => undone_object}} = object, meta) do
with undone_object <- Activity.get_by_ap_id(undone_object),
:ok <- handle_undoing(undone_object) do
{:ok, object, meta}
end
end
# Tasks this handles:
# - Add reaction to object
# - Set up notification
def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do
reacted_object = Object.get_by_ap_id(object.data["object"])
Utils.add_emoji_reaction_to_object(object, reacted_object)
Notification.create_notifications(object)
{:ok, object, meta}
end
# Tasks this handles: # Tasks this handles:
# - Delete and unpins the create activity # - Delete and unpins the create activity
# - Replace object with Tombstone # - Replace object with Tombstone
@ -109,4 +129,41 @@ def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do
def handle_object_creation(object) do def handle_object_creation(object) do
{:ok, object} {:ok, object}
end end
def handle_undoing(%{data: %{"type" => "Like"}} = object) do
with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
{:ok, _} <- Utils.remove_like_from_object(object, liked_object),
{:ok, _} <- Repo.delete(object) do
:ok
end
end
def handle_undoing(%{data: %{"type" => "EmojiReact"}} = object) do
with %Object{} = reacted_object <- Object.get_by_ap_id(object.data["object"]),
{:ok, _} <- Utils.remove_emoji_reaction_from_object(object, reacted_object),
{:ok, _} <- Repo.delete(object) do
:ok
end
end
def handle_undoing(%{data: %{"type" => "Announce"}} = object) do
with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
{:ok, _} <- Utils.remove_announce_from_object(object, liked_object),
{:ok, _} <- Repo.delete(object) do
:ok
end
end
def handle_undoing(
%{data: %{"type" => "Block", "actor" => blocker, "object" => blocked}} = object
) do
with %User{} = blocker <- User.get_cached_by_ap_id(blocker),
%User{} = blocked <- User.get_cached_by_ap_id(blocked),
{:ok, _} <- User.unblock(blocker, blocked),
{:ok, _} <- Repo.delete(object) do
:ok
end
end
def handle_undoing(object), do: {:error, ["don't know how to handle", object]}
end end

View file

@ -666,7 +666,7 @@ def handle_incoming(
end end
end end
def handle_incoming(%{"type" => "Like"} = data, _options) do def handle_incoming(%{"type" => type} = data, _options) when type in ["Like", "EmojiReact"] do
with :ok <- ObjectValidator.fetch_actor_and_object(data), with :ok <- ObjectValidator.fetch_actor_and_object(data),
{:ok, activity, _meta} <- {:ok, activity, _meta} <-
Pipeline.common_pipeline(data, local: false) do Pipeline.common_pipeline(data, local: false) do
@ -676,27 +676,6 @@ def handle_incoming(%{"type" => "Like"} = data, _options) do
end end
end end
def handle_incoming(
%{
"type" => "EmojiReact",
"object" => object_id,
"actor" => _actor,
"id" => id,
"content" => emoji
} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _object} <-
ActivityPub.react_with_emoji(actor, object, emoji, activity_id: id, local: false) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming( def handle_incoming(
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data, %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
_options _options
@ -754,25 +733,6 @@ def handle_incoming(
end end
end end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Announce", "object" => object_id},
"actor" => _actor,
"id" => id
} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming( def handle_incoming(
%{ %{
"type" => "Undo", "type" => "Undo",
@ -795,75 +755,13 @@ def handle_incoming(
def handle_incoming( def handle_incoming(
%{ %{
"type" => "Undo", "type" => "Undo",
"object" => %{"type" => "EmojiReact", "id" => reaction_activity_id}, "object" => %{"type" => type}
"actor" => _actor,
"id" => id
} = data, } = data,
_options _options
) do )
with actor <- Containment.get_actor(data), when type in ["Like", "EmojiReact", "Announce", "Block"] do
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
{:ok, activity, _} <-
ActivityPub.unreact_with_emoji(actor, reaction_activity_id,
activity_id: id,
local: false
) do
{:ok, activity} {:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Block", "object" => blocked},
"actor" => blocker,
"id" => id
} = _data,
_options
) do
with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
{:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
User.unblock(blocker, blocked)
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
_options
) do
with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
{:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
User.unfollow(blocker, blocked)
User.block(blocker, blocked)
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Like", "object" => object_id},
"actor" => _actor,
"id" => id
} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
{:ok, activity}
else
_e -> :error
end end
end end
@ -885,6 +783,21 @@ def handle_incoming(
end end
end end
def handle_incoming(
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
_options
) do
with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
{:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
User.unfollow(blocker, blocked)
User.block(blocker, blocked)
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming( def handle_incoming(
%{ %{
"type" => "Move", "type" => "Move",

View file

@ -562,45 +562,6 @@ def make_announce_data(
|> maybe_put("id", activity_id) |> maybe_put("id", activity_id)
end end
@doc """
Make unannounce activity data for the given actor and object
"""
def make_unannounce_data(
%User{ap_id: ap_id} = user,
%Activity{data: %{"context" => context, "object" => object}} = activity,
activity_id
) do
object = Object.normalize(object)
%{
"type" => "Undo",
"actor" => ap_id,
"object" => activity.data,
"to" => [user.follower_address, object.data["actor"]],
"cc" => [Pleroma.Constants.as_public()],
"context" => context
}
|> maybe_put("id", activity_id)
end
def make_unlike_data(
%User{ap_id: ap_id} = user,
%Activity{data: %{"context" => context, "object" => object}} = activity,
activity_id
) do
object = Object.normalize(object)
%{
"type" => "Undo",
"actor" => ap_id,
"object" => activity.data,
"to" => [user.follower_address, object.data["actor"]],
"cc" => [Pleroma.Constants.as_public()],
"context" => context
}
|> maybe_put("id", activity_id)
end
def make_undo_data( def make_undo_data(
%User{ap_id: actor, follower_address: follower_address}, %User{ap_id: actor, follower_address: follower_address},
%Activity{ %Activity{
@ -688,16 +649,6 @@ def make_block_data(blocker, blocked, activity_id) do
|> maybe_put("id", activity_id) |> maybe_put("id", activity_id)
end end
def make_unblock_data(blocker, blocked, block_activity, activity_id) do
%{
"type" => "Undo",
"actor" => blocker.ap_id,
"to" => [blocked.ap_id],
"object" => block_activity.data
}
|> maybe_put("id", activity_id)
end
#### Create-related helpers #### Create-related helpers
def make_create_data(params, additional) do def make_create_data(params, additional) do

View file

@ -556,11 +556,12 @@ defp update_creadentials_request do
} }
end end
defp array_of_accounts do def array_of_accounts do
%Schema{ %Schema{
title: "ArrayOfAccounts", title: "ArrayOfAccounts",
type: :array, type: :array,
items: Account items: Account,
example: [Account.schema().example]
} }
end end

View file

@ -0,0 +1,207 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.SearchOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.AccountOperation
alias Pleroma.Web.ApiSpec.Schemas.Account
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.Status
alias Pleroma.Web.ApiSpec.Schemas.Tag
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def account_search_operation do
%Operation{
tags: ["Search"],
summary: "Search for matching accounts by username or display name",
operationId: "SearchController.account_search",
parameters: [
Operation.parameter(:q, :query, %Schema{type: :string}, "What to search for",
required: true
),
Operation.parameter(
:limit,
:query,
%Schema{type: :integer, default: 40},
"Maximum number of results"
),
Operation.parameter(
:resolve,
:query,
%Schema{allOf: [BooleanLike], default: false},
"Attempt WebFinger lookup. Use this when `q` is an exact address."
),
Operation.parameter(
:following,
:query,
%Schema{allOf: [BooleanLike], default: false},
"Only include accounts that the user is following"
)
],
responses: %{
200 =>
Operation.response(
"Array of Account",
"application/json",
AccountOperation.array_of_accounts()
)
}
}
end
def search_operation do
%Operation{
tags: ["Search"],
summary: "Search results",
security: [%{"oAuth" => ["read:search"]}],
operationId: "SearchController.search",
deprecated: true,
parameters: [
Operation.parameter(
:account_id,
:query,
FlakeID,
"If provided, statuses returned will be authored only by this account"
),
Operation.parameter(
:type,
:query,
%Schema{type: :string, enum: ["accounts", "hashtags", "statuses"]},
"Search type"
),
Operation.parameter(:q, :query, %Schema{type: :string}, "The search query", required: true),
Operation.parameter(
:resolve,
:query,
%Schema{allOf: [BooleanLike], default: false},
"Attempt WebFinger lookup"
),
Operation.parameter(
:following,
:query,
%Schema{allOf: [BooleanLike], default: false},
"Only include accounts that the user is following"
),
Operation.parameter(
:offset,
:query,
%Schema{type: :integer},
"Offset"
)
| pagination_params()
],
responses: %{
200 => Operation.response("Results", "application/json", results())
}
}
end
def search2_operation do
%Operation{
tags: ["Search"],
summary: "Search results",
security: [%{"oAuth" => ["read:search"]}],
operationId: "SearchController.search2",
parameters: [
Operation.parameter(
:account_id,
:query,
FlakeID,
"If provided, statuses returned will be authored only by this account"
),
Operation.parameter(
:type,
:query,
%Schema{type: :string, enum: ["accounts", "hashtags", "statuses"]},
"Search type"
),
Operation.parameter(:q, :query, %Schema{type: :string}, "What to search for",
required: true
),
Operation.parameter(
:resolve,
:query,
%Schema{allOf: [BooleanLike], default: false},
"Attempt WebFinger lookup"
),
Operation.parameter(
:following,
:query,
%Schema{allOf: [BooleanLike], default: false},
"Only include accounts that the user is following"
)
| pagination_params()
],
responses: %{
200 => Operation.response("Results", "application/json", results2())
}
}
end
defp results2 do
%Schema{
title: "SearchResults",
type: :object,
properties: %{
accounts: %Schema{
type: :array,
items: Account,
description: "Accounts which match the given query"
},
statuses: %Schema{
type: :array,
items: Status,
description: "Statuses which match the given query"
},
hashtags: %Schema{
type: :array,
items: Tag,
description: "Hashtags which match the given query"
}
},
example: %{
"accounts" => [Account.schema().example],
"statuses" => [Status.schema().example],
"hashtags" => [Tag.schema().example]
}
}
end
defp results do
%Schema{
title: "SearchResults",
type: :object,
properties: %{
accounts: %Schema{
type: :array,
items: Account,
description: "Accounts which match the given query"
},
statuses: %Schema{
type: :array,
items: Status,
description: "Statuses which match the given query"
},
hashtags: %Schema{
type: :array,
items: %Schema{type: :string},
description: "Hashtags which match the given query"
}
},
example: %{
"accounts" => [Account.schema().example],
"statuses" => [Status.schema().example],
"hashtags" => ["cofe"]
}
}
end
end

View file

@ -9,6 +9,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
alias Pleroma.Web.ApiSpec.Schemas.Emoji alias Pleroma.Web.ApiSpec.Schemas.Emoji
alias Pleroma.Web.ApiSpec.Schemas.FlakeID alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.Poll alias Pleroma.Web.ApiSpec.Schemas.Poll
alias Pleroma.Web.ApiSpec.Schemas.Tag
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
require OpenApiSpex require OpenApiSpex
@ -106,16 +107,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
replies_count: %Schema{type: :integer}, replies_count: %Schema{type: :integer},
sensitive: %Schema{type: :boolean}, sensitive: %Schema{type: :boolean},
spoiler_text: %Schema{type: :string}, spoiler_text: %Schema{type: :string},
tags: %Schema{ tags: %Schema{type: :array, items: Tag},
type: :array,
items: %Schema{
type: :object,
properties: %{
name: %Schema{type: :string},
url: %Schema{type: :string, format: :uri}
}
}
},
uri: %Schema{type: :string, format: :uri}, uri: %Schema{type: :string, format: :uri},
url: %Schema{type: :string, nullable: true, format: :uri}, url: %Schema{type: :string, nullable: true, format: :uri},
visibility: VisibilityScope visibility: VisibilityScope

View file

@ -0,0 +1,27 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.Tag do
alias OpenApiSpex.Schema
require OpenApiSpex
OpenApiSpex.schema(%{
title: "Tag",
description: "Represents a hashtag used within the content of a status",
type: :object,
properties: %{
name: %Schema{type: :string, description: "The value of the hashtag after the # sign"},
url: %Schema{
type: :string,
format: :uri,
description: "A link to the hashtag on the instance"
}
},
example: %{
name: "cofe",
url: "https://lain.com/tag/cofe"
}
})
end

View file

@ -55,6 +55,14 @@ defp validate_chat_content_length(content) do
end end
end end
def unblock(blocker, blocked) do
with %Activity{} = 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}
end
end
def follow(follower, followed) do def follow(follower, followed) do
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout]) timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
@ -138,9 +146,12 @@ def repeat(id, user, params \\ %{}) do
def unrepeat(id, user) do def unrepeat(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <- with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)} do {:find_activity, Activity.get_by_id(id)},
object = Object.normalize(activity) %Object{} = note <- Object.normalize(activity, false),
ActivityPub.unannounce(user, object) %Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
{:ok, undo, _} <- Builder.undo(user, announce),
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
{:ok, activity}
else else
{:find_activity, _} -> {:error, :not_found} {:find_activity, _} -> {:error, :not_found}
_ -> {:error, dgettext("errors", "Could not unrepeat")} _ -> {:error, dgettext("errors", "Could not unrepeat")}
@ -197,9 +208,12 @@ def favorite_helper(user, id) do
def unfavorite(id, user) do def unfavorite(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <- with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)} do {:find_activity, Activity.get_by_id(id)},
object = Object.normalize(activity) %Object{} = note <- Object.normalize(activity, false),
ActivityPub.unlike(user, object) %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
{:ok, undo, _} <- Builder.undo(user, like),
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
{:ok, activity}
else else
{:find_activity, _} -> {:error, :not_found} {:find_activity, _} -> {:error, :not_found}
_ -> {:error, dgettext("errors", "Could not unfavorite")} _ -> {:error, dgettext("errors", "Could not unfavorite")}
@ -208,8 +222,10 @@ def unfavorite(id, user) do
def react_with_emoji(id, user, emoji) do def react_with_emoji(id, user, emoji) do
with %Activity{} = activity <- Activity.get_by_id(id), with %Activity{} = activity <- Activity.get_by_id(id),
object <- Object.normalize(activity) do object <- Object.normalize(activity),
ActivityPub.react_with_emoji(user, object, emoji) {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
{:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
{:ok, activity}
else else
_ -> _ ->
{:error, dgettext("errors", "Could not add reaction emoji")} {:error, dgettext("errors", "Could not add reaction emoji")}
@ -217,8 +233,10 @@ def react_with_emoji(id, user, emoji) do
end end
def unreact_with_emoji(id, user, emoji) do def unreact_with_emoji(id, user, emoji) do
with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"]) {:ok, undo, _} <- Builder.undo(user, reaction_activity),
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
{:ok, activity}
else else
_ -> _ ->
{:error, dgettext("errors", "Could not remove reaction emoji")} {:error, dgettext("errors", "Could not remove reaction emoji")}

View file

@ -356,8 +356,7 @@ def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
@doc "POST /api/v1/accounts/:id/unblock" @doc "POST /api/v1/accounts/:id/unblock"
def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
with {:ok, _user_block} <- User.unblock(blocker, blocked), with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
render(conn, "relationship.json", user: blocker, target: blocked) render(conn, "relationship.json", user: blocker, target: blocked)
else else
{:error, message} -> json_response(conn, :forbidden, %{error: message}) {:error, message} -> json_response(conn, :forbidden, %{error: message})

View file

@ -5,7 +5,7 @@
defmodule Pleroma.Web.MastodonAPI.SearchController do defmodule Pleroma.Web.MastodonAPI.SearchController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [fetch_integer_param: 2, skip_relationships?: 1] import Pleroma.Web.ControllerHelper, only: [skip_relationships?: 1]
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
@ -18,6 +18,8 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
require Logger require Logger
plug(Pleroma.Web.ApiSpec.CastAndValidate)
# Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search) # Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search)
plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated}) plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated})
@ -25,7 +27,9 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search]) plug(RateLimiter, [name: :search] when action in [:search, :search2, :account_search])
def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.SearchOperation
def account_search(%{assigns: %{user: user}} = conn, %{q: query} = params) do
accounts = User.search(query, search_options(params, user)) accounts = User.search(query, search_options(params, user))
conn conn
@ -36,7 +40,7 @@ def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) d
def search2(conn, params), do: do_search(:v2, conn, params) def search2(conn, params), do: do_search(:v2, conn, params)
def search(conn, params), do: do_search(:v1, conn, params) def search(conn, params), do: do_search(:v1, conn, params)
defp do_search(version, %{assigns: %{user: user}} = conn, %{"q" => query} = params) do defp do_search(version, %{assigns: %{user: user}} = conn, %{q: query} = params) do
options = search_options(params, user) options = search_options(params, user)
timeout = Keyword.get(Repo.config(), :timeout, 15_000) timeout = Keyword.get(Repo.config(), :timeout, 15_000)
default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []} default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []}
@ -44,7 +48,7 @@ defp do_search(version, %{assigns: %{user: user}} = conn, %{"q" => query} = para
result = result =
default_values default_values
|> Enum.map(fn {resource, default_value} -> |> Enum.map(fn {resource, default_value} ->
if params["type"] in [nil, resource] do if params[:type] in [nil, resource] do
{resource, fn -> resource_search(version, resource, query, options) end} {resource, fn -> resource_search(version, resource, query, options) end}
else else
{resource, fn -> default_value end} {resource, fn -> default_value end}
@ -68,11 +72,11 @@ defp do_search(version, %{assigns: %{user: user}} = conn, %{"q" => query} = para
defp search_options(params, user) do defp search_options(params, user) do
[ [
skip_relationships: skip_relationships?(params), skip_relationships: skip_relationships?(params),
resolve: params["resolve"] == "true", resolve: params[:resolve],
following: params["following"] == "true", following: params[:following],
limit: fetch_integer_param(params, "limit"), limit: params[:limit],
offset: fetch_integer_param(params, "offset"), offset: params[:offset],
type: params["type"], type: params[:type],
author: get_author(params), author: get_author(params),
for_user: user for_user: user
] ]
@ -135,7 +139,7 @@ defp with_fallback(f, fallback \\ []) do
end end
end end
defp get_author(%{"account_id" => account_id}) when is_binary(account_id), defp get_author(%{account_id: account_id}) when is_binary(account_id),
do: User.get_cached_by_id(account_id) do: User.get_cached_by_id(account_id)
defp get_author(_params), do: nil defp get_author(_params), do: nil

View file

@ -206,9 +206,9 @@ def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id} = params) do
end end
@doc "POST /api/v1/statuses/:id/unreblog" @doc "POST /api/v1/statuses/:id/unreblog"
def unreblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do def unreblog(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user), with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
%Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do %Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", %{activity: activity, for: user, as: :activity}) try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
end end
end end
@ -222,9 +222,9 @@ def favourite(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
end end
@doc "POST /api/v1/statuses/:id/unfavourite" @doc "POST /api/v1/statuses/:id/unfavourite"
def unfavourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do def unfavourite(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user), with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do %Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity) try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end end
end end

View file

@ -36,9 +36,11 @@ def render("index.json", %{users: users} = opts) do
end end
def render("show.json", %{user: user} = opts) do def render("show.json", %{user: user} = opts) do
if User.visible_for?(user, opts[:for]), if User.visible_for?(user, opts[:for]) do
do: do_render("show.json", opts), do_render("show.json", opts)
else: %{} else
%{}
end
end end
def render("mention.json", %{user: user}) do def render("mention.json", %{user: user}) do
@ -221,7 +223,7 @@ defp do_render("show.json", %{user: user} = opts) do
fields: user.fields, fields: user.fields,
bot: bot, bot: bot,
source: %{ source: %{
note: (user.bio || "") |> String.replace(~r(<br */?>), "\n") |> Pleroma.HTML.strip_tags(), note: prepare_user_bio(user),
sensitive: false, sensitive: false,
fields: user.raw_fields, fields: user.raw_fields,
pleroma: %{ pleroma: %{
@ -254,8 +256,17 @@ defp do_render("show.json", %{user: user} = opts) do
|> maybe_put_follow_requests_count(user, opts[:for]) |> maybe_put_follow_requests_count(user, opts[:for])
|> maybe_put_allow_following_move(user, opts[:for]) |> maybe_put_allow_following_move(user, opts[:for])
|> maybe_put_unread_conversation_count(user, opts[:for]) |> maybe_put_unread_conversation_count(user, opts[:for])
|> maybe_put_unread_notification_count(user, opts[:for])
end end
defp prepare_user_bio(%User{bio: ""}), do: ""
defp prepare_user_bio(%User{bio: bio}) when is_binary(bio) do
bio |> String.replace(~r(<br */?>), "\n") |> Pleroma.HTML.strip_tags()
end
defp prepare_user_bio(_), do: ""
defp username_from_nickname(string) when is_binary(string) do defp username_from_nickname(string) when is_binary(string) do
hd(String.split(string, "@")) hd(String.split(string, "@"))
end end
@ -351,6 +362,16 @@ defp maybe_put_unread_conversation_count(data, %User{id: user_id} = user, %User{
defp maybe_put_unread_conversation_count(data, _, _), do: data defp maybe_put_unread_conversation_count(data, _, _), do: data
defp maybe_put_unread_notification_count(data, %User{id: user_id}, %User{id: user_id} = user) do
Kernel.put_in(
data,
[:pleroma, :unread_notifications_count],
Pleroma.Notification.unread_notifications_count(user)
)
end
defp maybe_put_unread_notification_count(data, _, _), do: data
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil defp image_url(_), do: nil
end end

View file

@ -11,7 +11,10 @@ def render("markers.json", %{markers: markers}) do
%{ %{
last_read_id: m.last_read_id, last_read_id: m.last_read_id,
version: m.lock_version, version: m.lock_version,
updated_at: NaiveDateTime.to_iso8601(m.updated_at) updated_at: NaiveDateTime.to_iso8601(m.updated_at),
pleroma: %{
unread_count: m.unread_count
}
}} }}
end) end)
end end

View file

@ -78,7 +78,7 @@ def websocket_info({:render_with_user, view, template, item}, state) do
user = %User{} = User.get_cached_by_ap_id(state.user.ap_id) user = %User{} = User.get_cached_by_ap_id(state.user.ap_id)
unless Streamer.filtered_by_user?(user, item) do unless Streamer.filtered_by_user?(user, item) do
websocket_info({:text, view.render(template, user, item)}, %{state | user: user}) websocket_info({:text, view.render(template, item, user)}, %{state | user: user})
else else
{:ok, state} {:ok, state}
end end

View file

@ -86,7 +86,7 @@ def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id}
end end
def react_with_emoji(%{assigns: %{user: user}} = conn, %{"id" => activity_id, "emoji" => emoji}) do def react_with_emoji(%{assigns: %{user: user}} = conn, %{"id" => activity_id, "emoji" => emoji}) do
with {:ok, _activity, _object} <- CommonAPI.react_with_emoji(activity_id, user, emoji), with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji),
activity <- Activity.get_by_id(activity_id) do activity <- Activity.get_by_id(activity_id) do
conn conn
|> put_view(StatusView) |> put_view(StatusView)
@ -98,7 +98,8 @@ def unreact_with_emoji(%{assigns: %{user: user}} = conn, %{
"id" => activity_id, "id" => activity_id,
"emoji" => emoji "emoji" => emoji
}) do }) do
with {:ok, _activity, _object} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji), with {:ok, _activity} <-
CommonAPI.unreact_with_emoji(activity_id, user, emoji),
activity <- Activity.get_by_id(activity_id) do activity <- Activity.get_by_id(activity_id) do
conn conn
|> put_view(StatusView) |> put_view(StatusView)

View file

@ -106,14 +106,13 @@ def build_content(notification, actor, object, mastodon_type \\ nil)
def build_content( def build_content(
%{ %{
activity: %{data: %{"directMessage" => true}},
user: %{notification_settings: %{privacy_option: true}} user: %{notification_settings: %{privacy_option: true}}
}, } = notification,
actor, _actor,
_object, _object,
_mastodon_type mastodon_type
) do ) do
%{title: "New Direct Message", body: "@#{actor.nickname}"} %{body: format_title(notification, mastodon_type)}
end end
def build_content(notification, actor, object, mastodon_type) do def build_content(notification, actor, object, mastodon_type) do

View file

@ -25,7 +25,7 @@ def render("update.json", %Activity{} = activity, %User{} = user) do
|> Jason.encode!() |> Jason.encode!()
end end
def render("notification.json", %User{} = user, %Notification{} = notify) do def render("notification.json", %Notification{} = notify, %User{} = user) do
%{ %{
event: "notification", event: "notification",
payload: payload:

View file

@ -0,0 +1,40 @@
defmodule Pleroma.Repo.Migrations.UpdateMarkers do
use Ecto.Migration
import Ecto.Query
alias Pleroma.Repo
def up do
update_markers()
end
def down do
:ok
end
defp update_markers do
now = NaiveDateTime.utc_now()
markers_attrs =
from(q in "notifications",
select: %{
timeline: "notifications",
user_id: q.user_id,
last_read_id:
type(fragment("MAX( CASE WHEN seen = true THEN id ELSE null END )"), :string)
},
group_by: [q.user_id]
)
|> Repo.all()
|> Enum.map(fn %{last_read_id: last_read_id} = attrs ->
attrs
|> Map.put(:last_read_id, last_read_id || "")
|> Map.put_new(:inserted_at, now)
|> Map.put_new(:updated_at, now)
end)
Repo.insert_all("markers", markers_attrs,
on_conflict: {:replace, [:last_read_id]},
conflict_target: [:user_id, :timeline]
)
end
end

View file

@ -8,12 +8,39 @@ defmodule Pleroma.MarkerTest do
import Pleroma.Factory import Pleroma.Factory
describe "multi_set_unread_count/3" do
test "returns multi" do
user = insert(:user)
assert %Ecto.Multi{
operations: [marker: {:run, _}, counters: {:run, _}]
} =
Marker.multi_set_last_read_id(
Ecto.Multi.new(),
user,
"notifications"
)
end
test "return empty multi" do
user = insert(:user)
multi = Ecto.Multi.new()
assert Marker.multi_set_last_read_id(multi, user, "home") == multi
end
end
describe "get_markers/2" do describe "get_markers/2" do
test "returns user markers" do test "returns user markers" do
user = insert(:user) user = insert(:user)
marker = insert(:marker, user: user) marker = insert(:marker, user: user)
insert(:notification, user: user)
insert(:notification, user: user)
insert(:marker, timeline: "home", user: user) insert(:marker, timeline: "home", user: user)
assert Marker.get_markers(user, ["notifications"]) == [refresh_record(marker)]
assert Marker.get_markers(
user,
["notifications"]
) == [%Marker{refresh_record(marker) | unread_count: 2}]
end end
end end

View file

@ -24,7 +24,7 @@ test "creates a notification for an emoji reaction" do
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "yeah"}) {:ok, activity} = CommonAPI.post(user, %{"status" => "yeah"})
{:ok, activity, _object} = CommonAPI.react_with_emoji(activity.id, other_user, "") {:ok, activity} = CommonAPI.react_with_emoji(activity.id, other_user, "")
{:ok, [notification]} = Notification.create_notifications(activity) {:ok, [notification]} = Notification.create_notifications(activity)
@ -47,6 +47,9 @@ test "notifies someone when they are directly addressed" do
assert notified_ids == [other_user.id, third_user.id] assert notified_ids == [other_user.id, third_user.id]
assert notification.activity_id == activity.id assert notification.activity_id == activity.id
assert other_notification.activity_id == activity.id assert other_notification.activity_id == activity.id
assert [%Pleroma.Marker{unread_count: 2}] =
Pleroma.Marker.get_markers(other_user, ["notifications"])
end end
test "it creates a notification for subscribed users" do test "it creates a notification for subscribed users" do
@ -466,6 +469,16 @@ test "it sets all notifications as read up to a specified notification ID" do
assert n1.seen == true assert n1.seen == true
assert n2.seen == true assert n2.seen == true
assert n3.seen == false assert n3.seen == false
assert %Pleroma.Marker{} =
m =
Pleroma.Repo.get_by(
Pleroma.Marker,
user_id: other_user.id,
timeline: "notifications"
)
assert m.last_read_id == to_string(n2.id)
end end
end end
@ -728,7 +741,7 @@ test "liking an activity results in 1 notification, then 0 if the activity is un
assert length(Notification.for_user(user)) == 1 assert length(Notification.for_user(user)) == 1
{:ok, _, _, _} = CommonAPI.unfavorite(activity.id, other_user) {:ok, _} = CommonAPI.unfavorite(activity.id, other_user)
assert Enum.empty?(Notification.for_user(user)) assert Enum.empty?(Notification.for_user(user))
end end
@ -762,7 +775,7 @@ test "repeating an activity results in 1 notification, then 0 if the activity is
assert length(Notification.for_user(user)) == 1 assert length(Notification.for_user(user)) == 1
{:ok, _, _} = CommonAPI.unrepeat(activity.id, other_user) {:ok, _} = CommonAPI.unrepeat(activity.id, other_user)
assert Enum.empty?(Notification.for_user(user)) assert Enum.empty?(Notification.for_user(user))
end end

View file

@ -815,6 +815,21 @@ test "it inserts an incoming create activity into the database", %{
assert object["content"] == activity["object"]["content"] assert object["content"] == activity["object"]["content"]
end end
test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do
user = insert(:user)
activity =
activity
|> put_in(["object", "type"], "Benis")
_result =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", activity)
|> json_response(400)
end
test "it inserts an incoming sensitive activity into the database", %{ test "it inserts an incoming sensitive activity into the database", %{
conn: conn, conn: conn,
activity: activity activity: activity

View file

@ -16,7 +16,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Federator
import ExUnit.CaptureLog import ExUnit.CaptureLog
import Mock import Mock
@ -874,187 +873,6 @@ test "returns reblogs for users for whom reblogs have not been muted" do
end end
end end
describe "react to an object" do
test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
Config.put([:instance, :federating], true)
user = insert(:user)
reactor = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
assert object = Object.normalize(activity)
{:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
assert called(Federator.publish(reaction_activity))
end
test "adds an emoji reaction activity to the db" do
user = insert(:user)
reactor = insert(:user)
third_user = insert(:user)
fourth_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
assert object = Object.normalize(activity)
{:ok, reaction_activity, object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
assert reaction_activity
assert reaction_activity.data["actor"] == reactor.ap_id
assert reaction_activity.data["type"] == "EmojiReact"
assert reaction_activity.data["content"] == "🔥"
assert reaction_activity.data["object"] == object.data["id"]
assert reaction_activity.data["to"] == [User.ap_followers(reactor), activity.data["actor"]]
assert reaction_activity.data["context"] == object.data["context"]
assert object.data["reaction_count"] == 1
assert object.data["reactions"] == [["🔥", [reactor.ap_id]]]
{:ok, _reaction_activity, object} = ActivityPub.react_with_emoji(third_user, object, "")
assert object.data["reaction_count"] == 2
assert object.data["reactions"] == [["🔥", [reactor.ap_id]], ["", [third_user.ap_id]]]
{:ok, _reaction_activity, object} = ActivityPub.react_with_emoji(fourth_user, object, "🔥")
assert object.data["reaction_count"] == 3
assert object.data["reactions"] == [
["🔥", [fourth_user.ap_id, reactor.ap_id]],
["", [third_user.ap_id]]
]
end
test "reverts emoji reaction on error" do
[user, reactor] = insert_list(2, :user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "Status"})
object = Object.normalize(activity)
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.react_with_emoji(reactor, object, "😀")
end
object = Object.get_by_ap_id(object.data["id"])
refute object.data["reaction_count"]
refute object.data["reactions"]
end
end
describe "unreacting to an object" do
test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
Config.put([:instance, :federating], true)
user = insert(:user)
reactor = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
assert object = Object.normalize(activity)
{:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
assert called(Federator.publish(reaction_activity))
{:ok, unreaction_activity, _object} =
ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
assert called(Federator.publish(unreaction_activity))
end
test "adds an undo activity to the db" do
user = insert(:user)
reactor = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
assert object = Object.normalize(activity)
{:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
{:ok, unreaction_activity, _object} =
ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
assert unreaction_activity.actor == reactor.ap_id
assert unreaction_activity.data["object"] == reaction_activity.data["id"]
object = Object.get_by_ap_id(object.data["id"])
assert object.data["reaction_count"] == 0
assert object.data["reactions"] == []
end
test "reverts emoji unreact on error" do
[user, reactor] = insert_list(2, :user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "Status"})
object = Object.normalize(activity)
{:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "😀")
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} =
ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
end
object = Object.get_by_ap_id(object.data["id"])
assert object.data["reaction_count"] == 1
assert object.data["reactions"] == [["😀", [reactor.ap_id]]]
end
end
describe "unliking" do
test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
Config.put([:instance, :federating], true)
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
{:ok, object} = ActivityPub.unlike(user, object)
refute called(Federator.publish())
{:ok, _like_activity} = CommonAPI.favorite(user, note_activity.id)
object = Object.get_by_id(object.id)
assert object.data["like_count"] == 1
{:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0
assert called(Federator.publish(unlike_activity))
end
test "reverts unliking on error" do
note_activity = insert(:note_activity)
user = insert(:user)
{:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)
object = Object.normalize(note_activity)
assert object.data["like_count"] == 1
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.unlike(user, object)
end
assert Object.get_by_ap_id(object.data["id"]) == object
assert object.data["like_count"] == 1
assert Activity.get_by_id(like_activity.id)
end
test "unliking a previously liked object" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
# Unliking something that hasn't been liked does nothing
{:ok, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0
{:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)
object = Object.get_by_id(object.id)
assert object.data["like_count"] == 1
{:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0
assert Activity.get_by_id(like_activity.id) == nil
assert note_activity.actor in unlike_activity.recipients
end
end
describe "announcing an object" do describe "announcing an object" do
test "adds an announce activity to the db" do test "adds an announce activity to the db" do
note_activity = insert(:note_activity) note_activity = insert(:note_activity)
@ -1124,52 +942,6 @@ test "does not add an announce activity to the db if the announcer is not the au
end end
end end
describe "unannouncing an object" do
test "unannouncing a previously announced object" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
# Unannouncing an object that is not announced does nothing
{:ok, object} = ActivityPub.unannounce(user, object)
refute object.data["announcement_count"]
{:ok, announce_activity, object} = ActivityPub.announce(user, object)
assert object.data["announcement_count"] == 1
{:ok, unannounce_activity, object} = ActivityPub.unannounce(user, object)
assert object.data["announcement_count"] == 0
assert unannounce_activity.data["to"] == [
User.ap_followers(user),
object.data["actor"]
]
assert unannounce_activity.data["type"] == "Undo"
assert unannounce_activity.data["object"] == announce_activity.data
assert unannounce_activity.data["actor"] == user.ap_id
assert unannounce_activity.data["context"] == announce_activity.data["context"]
assert Activity.get_by_id(announce_activity.id) == nil
end
test "reverts unannouncing on error" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
{:ok, _announce_activity, object} = ActivityPub.announce(user, object)
assert object.data["announcement_count"] == 1
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.unannounce(user, object)
end
object = Object.get_by_ap_id(object.data["id"])
assert object.data["announcement_count"] == 1
end
end
describe "uploading files" do describe "uploading files" do
test "copies the file to the configured folder" do test "copies the file to the configured folder" do
file = %Plug.Upload{ file = %Plug.Upload{
@ -1276,7 +1048,7 @@ test "creates an undo activity for a pending follow request" do
end end
end end
describe "blocking / unblocking" do describe "blocking" do
test "reverts block activity on error" do test "reverts block activity on error" do
[blocker, blocked] = insert_list(2, :user) [blocker, blocked] = insert_list(2, :user)
@ -1298,38 +1070,6 @@ test "creates a block activity" do
assert activity.data["actor"] == blocker.ap_id assert activity.data["actor"] == blocker.ap_id
assert activity.data["object"] == blocked.ap_id assert activity.data["object"] == blocked.ap_id
end end
test "reverts unblock activity on error" do
[blocker, blocked] = insert_list(2, :user)
{:ok, block_activity} = ActivityPub.block(blocker, blocked)
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.unblock(blocker, blocked)
end
assert block_activity.data["type"] == "Block"
assert block_activity.data["actor"] == blocker.ap_id
assert Repo.aggregate(Activity, :count, :id) == 1
assert Repo.aggregate(Object, :count, :id) == 1
end
test "creates an undo activity for the last block" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, block_activity} = ActivityPub.block(blocker, blocked)
{:ok, activity} = ActivityPub.unblock(blocker, blocked)
assert activity.data["type"] == "Undo"
assert activity.data["actor"] == blocker.ap_id
embedded_object = activity.data["object"]
assert is_map(embedded_object)
assert embedded_object["type"] == "Block"
assert embedded_object["object"] == blocked.ap_id
assert embedded_object["id"] == block_activity.data["id"]
end
end end
describe "timeline post-processing" do describe "timeline post-processing" do

View file

@ -156,6 +156,86 @@ test "does not validate if it doesn't concern local users" do
end end
end end
describe "EmojiReacts" do
setup do
user = insert(:user)
{:ok, post_activity} = CommonAPI.post(user, %{"status" => "uguu"})
object = Pleroma.Object.get_by_ap_id(post_activity.data["object"])
{:ok, valid_emoji_react, []} = Builder.emoji_react(user, object, "👌")
%{user: user, post_activity: post_activity, valid_emoji_react: valid_emoji_react}
end
test "it validates a valid EmojiReact", %{valid_emoji_react: valid_emoji_react} do
assert {:ok, _, _} = ObjectValidator.validate(valid_emoji_react, [])
end
test "it is not valid without a 'content' field", %{valid_emoji_react: valid_emoji_react} do
without_content =
valid_emoji_react
|> Map.delete("content")
{:error, cng} = ObjectValidator.validate(without_content, [])
refute cng.valid?
assert {:content, {"can't be blank", [validation: :required]}} in cng.errors
end
test "it is not valid with a non-emoji content field", %{valid_emoji_react: valid_emoji_react} do
without_emoji_content =
valid_emoji_react
|> Map.put("content", "x")
{:error, cng} = ObjectValidator.validate(without_emoji_content, [])
refute cng.valid?
assert {:content, {"must be a single character emoji", []}} in cng.errors
end
end
describe "Undos" do
setup do
user = insert(:user)
{:ok, post_activity} = CommonAPI.post(user, %{"status" => "uguu"})
{:ok, like} = CommonAPI.favorite(user, post_activity.id)
{:ok, valid_like_undo, []} = Builder.undo(user, like)
%{user: user, like: like, valid_like_undo: valid_like_undo}
end
test "it validates a basic like undo", %{valid_like_undo: valid_like_undo} do
assert {:ok, _, _} = ObjectValidator.validate(valid_like_undo, [])
end
test "it does not validate if the actor of the undo is not the actor of the object", %{
valid_like_undo: valid_like_undo
} do
other_user = insert(:user, ap_id: "https://gensokyo.2hu/users/raymoo")
bad_actor =
valid_like_undo
|> Map.put("actor", other_user.ap_id)
{:error, cng} = ObjectValidator.validate(bad_actor, [])
assert {:actor, {"not the same as object actor", []}} in cng.errors
end
test "it does not validate if the object is missing", %{valid_like_undo: valid_like_undo} do
missing_object =
valid_like_undo
|> Map.put("object", "https://gensokyo.2hu/objects/1")
{:error, cng} = ObjectValidator.validate(missing_object, [])
assert {:object, {"can't find object", []}} in cng.errors
assert length(cng.errors) == 1
end
end
describe "deletes" do describe "deletes" do
setup do setup do
user = insert(:user) user = insert(:user)

View file

@ -73,6 +73,133 @@ test "it handles user deletions", %{delete_user: delete, user: user} do
end end
end end
describe "EmojiReact objects" do
setup do
poster = insert(:user)
user = insert(:user)
{:ok, post} = CommonAPI.post(poster, %{"status" => "hey"})
{:ok, emoji_react_data, []} = Builder.emoji_react(user, post.object, "👌")
{:ok, emoji_react, _meta} = ActivityPub.persist(emoji_react_data, local: true)
%{emoji_react: emoji_react, user: user, poster: poster}
end
test "adds the reaction to the object", %{emoji_react: emoji_react, user: user} do
{:ok, emoji_react, _} = SideEffects.handle(emoji_react)
object = Object.get_by_ap_id(emoji_react.data["object"])
assert object.data["reaction_count"] == 1
assert ["👌", [user.ap_id]] in object.data["reactions"]
end
test "creates a notification", %{emoji_react: emoji_react, poster: poster} do
{:ok, emoji_react, _} = SideEffects.handle(emoji_react)
assert Repo.get_by(Notification, user_id: poster.id, activity_id: emoji_react.id)
end
end
describe "Undo objects" do
setup do
poster = insert(:user)
user = insert(:user)
{:ok, post} = CommonAPI.post(poster, %{"status" => "hey"})
{:ok, like} = CommonAPI.favorite(user, post.id)
{:ok, reaction} = CommonAPI.react_with_emoji(post.id, user, "👍")
{:ok, announce, _} = CommonAPI.repeat(post.id, user)
{:ok, block} = ActivityPub.block(user, poster)
User.block(user, poster)
{:ok, undo_data, _meta} = Builder.undo(user, like)
{:ok, like_undo, _meta} = ActivityPub.persist(undo_data, local: true)
{:ok, undo_data, _meta} = Builder.undo(user, reaction)
{:ok, reaction_undo, _meta} = ActivityPub.persist(undo_data, local: true)
{:ok, undo_data, _meta} = Builder.undo(user, announce)
{:ok, announce_undo, _meta} = ActivityPub.persist(undo_data, local: true)
{:ok, undo_data, _meta} = Builder.undo(user, block)
{:ok, block_undo, _meta} = ActivityPub.persist(undo_data, local: true)
%{
like_undo: like_undo,
post: post,
like: like,
reaction_undo: reaction_undo,
reaction: reaction,
announce_undo: announce_undo,
announce: announce,
block_undo: block_undo,
block: block,
poster: poster,
user: user
}
end
test "deletes the original block", %{block_undo: block_undo, block: block} do
{:ok, _block_undo, _} = SideEffects.handle(block_undo)
refute Activity.get_by_id(block.id)
end
test "unblocks the blocked user", %{block_undo: block_undo, block: block} do
blocker = User.get_by_ap_id(block.data["actor"])
blocked = User.get_by_ap_id(block.data["object"])
{:ok, _block_undo, _} = SideEffects.handle(block_undo)
refute User.blocks?(blocker, blocked)
end
test "an announce undo removes the announce from the object", %{
announce_undo: announce_undo,
post: post
} do
{:ok, _announce_undo, _} = SideEffects.handle(announce_undo)
object = Object.get_by_ap_id(post.data["object"])
assert object.data["announcement_count"] == 0
assert object.data["announcements"] == []
end
test "deletes the original announce", %{announce_undo: announce_undo, announce: announce} do
{:ok, _announce_undo, _} = SideEffects.handle(announce_undo)
refute Activity.get_by_id(announce.id)
end
test "a reaction undo removes the reaction from the object", %{
reaction_undo: reaction_undo,
post: post
} do
{:ok, _reaction_undo, _} = SideEffects.handle(reaction_undo)
object = Object.get_by_ap_id(post.data["object"])
assert object.data["reaction_count"] == 0
assert object.data["reactions"] == []
end
test "deletes the original reaction", %{reaction_undo: reaction_undo, reaction: reaction} do
{:ok, _reaction_undo, _} = SideEffects.handle(reaction_undo)
refute Activity.get_by_id(reaction.id)
end
test "a like undo removes the like from the object", %{like_undo: like_undo, post: post} do
{:ok, _like_undo, _} = SideEffects.handle(like_undo)
object = Object.get_by_ap_id(post.data["object"])
assert object.data["like_count"] == 0
assert object.data["likes"] == []
end
test "deletes the original like", %{like_undo: like_undo, like: like} do
{:ok, _like_undo, _} = SideEffects.handle(like_undo)
refute Activity.get_by_id(like.id)
end
end
describe "like objects" do describe "like objects" do
setup do setup do
poster = insert(:user) poster = insert(:user)

View file

@ -0,0 +1,61 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiReactHandlingTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
test "it works for incoming emoji reactions" do
user = insert(:user)
other_user = insert(:user, local: false)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
data =
File.read!("test/fixtures/emoji-reaction.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
|> Map.put("actor", other_user.ap_id)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == other_user.ap_id
assert data["type"] == "EmojiReact"
assert data["id"] == "http://mastodon.example.org/users/admin#reactions/2"
assert data["object"] == activity.data["object"]
assert data["content"] == "👌"
object = Object.get_by_ap_id(data["object"])
assert object.data["reaction_count"] == 1
assert match?([["👌", _]], object.data["reactions"])
end
test "it reject invalid emoji reactions" do
user = insert(:user)
other_user = insert(:user, local: false)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
data =
File.read!("test/fixtures/emoji-reaction-too-long.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
|> Map.put("actor", other_user.ap_id)
assert {:error, _} = Transmogrifier.handle_incoming(data)
data =
File.read!("test/fixtures/emoji-reaction-no-emoji.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
|> Map.put("actor", other_user.ap_id)
assert {:error, _} = Transmogrifier.handle_incoming(data)
end
end

View file

@ -0,0 +1,185 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.UndoHandlingTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
test "it works for incoming emoji reaction undos" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
{:ok, reaction_activity} = CommonAPI.react_with_emoji(activity.id, user, "👌")
data =
File.read!("test/fixtures/mastodon-undo-like.json")
|> Poison.decode!()
|> Map.put("object", reaction_activity.data["id"])
|> Map.put("actor", user.ap_id)
{:ok, activity} = Transmogrifier.handle_incoming(data)
assert activity.actor == user.ap_id
assert activity.data["id"] == data["id"]
assert activity.data["type"] == "Undo"
end
test "it returns an error for incoming unlikes wihout a like activity" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
data =
File.read!("test/fixtures/mastodon-undo-like.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
assert Transmogrifier.handle_incoming(data) == :error
end
test "it works for incoming unlikes with an existing like activity" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
like_data =
File.read!("test/fixtures/mastodon-like.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
_liker = insert(:user, ap_id: like_data["actor"], local: false)
{:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data)
data =
File.read!("test/fixtures/mastodon-undo-like.json")
|> Poison.decode!()
|> Map.put("object", like_data)
|> Map.put("actor", like_data["actor"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Undo"
assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo"
assert data["object"] == "http://mastodon.example.org/users/admin#likes/2"
note = Object.get_by_ap_id(like_data["object"])
assert note.data["like_count"] == 0
assert note.data["likes"] == []
end
test "it works for incoming unlikes with an existing like activity and a compact object" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
like_data =
File.read!("test/fixtures/mastodon-like.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
_liker = insert(:user, ap_id: like_data["actor"], local: false)
{:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data)
data =
File.read!("test/fixtures/mastodon-undo-like.json")
|> Poison.decode!()
|> Map.put("object", like_data["id"])
|> Map.put("actor", like_data["actor"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Undo"
assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo"
assert data["object"] == "http://mastodon.example.org/users/admin#likes/2"
end
test "it works for incoming unannounces with an existing notice" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
announce_data =
File.read!("test/fixtures/mastodon-announce.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
_announcer = insert(:user, ap_id: announce_data["actor"], local: false)
{:ok, %Activity{data: announce_data, local: false}} =
Transmogrifier.handle_incoming(announce_data)
data =
File.read!("test/fixtures/mastodon-undo-announce.json")
|> Poison.decode!()
|> Map.put("object", announce_data)
|> Map.put("actor", announce_data["actor"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Undo"
assert data["object"] ==
"http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
end
test "it works for incomming unfollows with an existing follow" do
user = insert(:user)
follow_data =
File.read!("test/fixtures/mastodon-follow-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
_follower = insert(:user, ap_id: follow_data["actor"], local: false)
{:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data)
data =
File.read!("test/fixtures/mastodon-unfollow-activity.json")
|> Poison.decode!()
|> Map.put("object", follow_data)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Undo"
assert data["object"]["type"] == "Follow"
assert data["object"]["object"] == user.ap_id
assert data["actor"] == "http://mastodon.example.org/users/admin"
refute User.following?(User.get_cached_by_ap_id(data["actor"]), user)
end
test "it works for incoming unblocks with an existing block" do
user = insert(:user)
block_data =
File.read!("test/fixtures/mastodon-block-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
_blocker = insert(:user, ap_id: block_data["actor"], local: false)
{:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(block_data)
data =
File.read!("test/fixtures/mastodon-unblock-activity.json")
|> Poison.decode!()
|> Map.put("object", block_data)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Undo"
assert data["object"] == block_data["id"]
blocker = User.get_cached_by_ap_id(data["actor"])
refute User.blocks?(blocker, user)
end
end

View file

@ -325,124 +325,6 @@ test "it cleans up incoming notices which are not really DMs" do
assert object_data["cc"] == to assert object_data["cc"] == to
end end
test "it works for incoming emoji reactions" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
data =
File.read!("test/fixtures/emoji-reaction.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "EmojiReact"
assert data["id"] == "http://mastodon.example.org/users/admin#reactions/2"
assert data["object"] == activity.data["object"]
assert data["content"] == "👌"
end
test "it reject invalid emoji reactions" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
data =
File.read!("test/fixtures/emoji-reaction-too-long.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
assert :error = Transmogrifier.handle_incoming(data)
data =
File.read!("test/fixtures/emoji-reaction-no-emoji.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
assert :error = Transmogrifier.handle_incoming(data)
end
test "it works for incoming emoji reaction undos" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
{:ok, reaction_activity, _object} = CommonAPI.react_with_emoji(activity.id, user, "👌")
data =
File.read!("test/fixtures/mastodon-undo-like.json")
|> Poison.decode!()
|> Map.put("object", reaction_activity.data["id"])
|> Map.put("actor", user.ap_id)
{:ok, activity} = Transmogrifier.handle_incoming(data)
assert activity.actor == user.ap_id
assert activity.data["id"] == data["id"]
assert activity.data["type"] == "Undo"
end
test "it returns an error for incoming unlikes wihout a like activity" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
data =
File.read!("test/fixtures/mastodon-undo-like.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
assert Transmogrifier.handle_incoming(data) == :error
end
test "it works for incoming unlikes with an existing like activity" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
like_data =
File.read!("test/fixtures/mastodon-like.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
{:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data)
data =
File.read!("test/fixtures/mastodon-undo-like.json")
|> Poison.decode!()
|> Map.put("object", like_data)
|> Map.put("actor", like_data["actor"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Undo"
assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo"
assert data["object"]["id"] == "http://mastodon.example.org/users/admin#likes/2"
end
test "it works for incoming unlikes with an existing like activity and a compact object" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
like_data =
File.read!("test/fixtures/mastodon-like.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
{:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data)
data =
File.read!("test/fixtures/mastodon-undo-like.json")
|> Poison.decode!()
|> Map.put("object", like_data["id"])
|> Map.put("actor", like_data["actor"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Undo"
assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo"
assert data["object"]["id"] == "http://mastodon.example.org/users/admin#likes/2"
end
test "it works for incoming announces" do test "it works for incoming announces" do
data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!() data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!()
@ -599,7 +481,7 @@ test "it strips internal likes" do
test "it strips internal reactions" do test "it strips internal reactions" do
user = insert(:user) user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"}) {:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
{:ok, _, _} = CommonAPI.react_with_emoji(activity.id, user, "📢") {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "📢")
%{object: object} = Activity.get_by_id_with_object(activity.id) %{object: object} = Activity.get_by_id_with_object(activity.id)
assert Map.has_key?(object.data, "reactions") assert Map.has_key?(object.data, "reactions")
@ -766,35 +648,6 @@ test "it works for incoming update activities which lock the account" do
assert user.locked == true assert user.locked == true
end end
test "it works for incoming unannounces with an existing notice" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
announce_data =
File.read!("test/fixtures/mastodon-announce.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
{:ok, %Activity{data: announce_data, local: false}} =
Transmogrifier.handle_incoming(announce_data)
data =
File.read!("test/fixtures/mastodon-undo-announce.json")
|> Poison.decode!()
|> Map.put("object", announce_data)
|> Map.put("actor", announce_data["actor"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Undo"
assert object_data = data["object"]
assert object_data["type"] == "Announce"
assert object_data["object"] == activity.data["object"]
assert object_data["id"] ==
"http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
end
test "it works for incomming unfollows with an existing follow" do test "it works for incomming unfollows with an existing follow" do
user = insert(:user) user = insert(:user)
@ -889,32 +742,6 @@ test "incoming blocks successfully tear down any follow relationship" do
refute User.following?(blocked, blocker) refute User.following?(blocked, blocker)
end end
test "it works for incoming unblocks with an existing block" do
user = insert(:user)
block_data =
File.read!("test/fixtures/mastodon-block-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(block_data)
data =
File.read!("test/fixtures/mastodon-unblock-activity.json")
|> Poison.decode!()
|> Map.put("object", block_data)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Undo"
assert data["object"]["type"] == "Block"
assert data["object"]["object"] == user.ap_id
assert data["actor"] == "http://mastodon.example.org/users/admin"
blocker = User.get_cached_by_ap_id(data["actor"])
refute User.blocks?(blocker, user)
end
test "it works for incoming accepts which were pre-accepted" do test "it works for incoming accepts which were pre-accepted" do
follower = insert(:user) follower = insert(:user)
followed = insert(:user) followed = insert(:user)

View file

@ -102,34 +102,6 @@ test "works with an object has tags as map" do
end end
end end
describe "make_unlike_data/3" do
test "returns data for unlike activity" do
user = insert(:user)
like_activity = insert(:like_activity, data_attrs: %{"context" => "test context"})
object = Object.normalize(like_activity.data["object"])
assert Utils.make_unlike_data(user, like_activity, nil) == %{
"type" => "Undo",
"actor" => user.ap_id,
"object" => like_activity.data,
"to" => [user.follower_address, object.data["actor"]],
"cc" => [Pleroma.Constants.as_public()],
"context" => like_activity.data["context"]
}
assert Utils.make_unlike_data(user, like_activity, "9mJEZK0tky1w2xD2vY") == %{
"type" => "Undo",
"actor" => user.ap_id,
"object" => like_activity.data,
"to" => [user.follower_address, object.data["actor"]],
"cc" => [Pleroma.Constants.as_public()],
"context" => like_activity.data["context"],
"id" => "9mJEZK0tky1w2xD2vY"
}
end
end
describe "make_like_data" do describe "make_like_data" do
setup do setup do
user = insert(:user) user = insert(:user)

View file

@ -408,7 +408,7 @@ test "reacting to a status with an emoji" do
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"}) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
{:ok, reaction, _} = CommonAPI.react_with_emoji(activity.id, user, "👍") {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
assert reaction.data["actor"] == user.ap_id assert reaction.data["actor"] == user.ap_id
assert reaction.data["content"] == "👍" assert reaction.data["content"] == "👍"
@ -423,12 +423,13 @@ test "unreacting to a status with an emoji" do
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"}) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
{:ok, reaction, _} = CommonAPI.react_with_emoji(activity.id, user, "👍") {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
{:ok, unreaction, _} = CommonAPI.unreact_with_emoji(activity.id, user, "👍") {:ok, unreaction} = CommonAPI.unreact_with_emoji(activity.id, user, "👍")
assert unreaction.data["type"] == "Undo" assert unreaction.data["type"] == "Undo"
assert unreaction.data["object"] == reaction.data["id"] assert unreaction.data["object"] == reaction.data["id"]
assert unreaction.local
end end
test "repeating a status" do test "repeating a status" do

View file

@ -1196,12 +1196,15 @@ test "returns lists to which the account belongs" do
describe "verify_credentials" do describe "verify_credentials" do
test "verify_credentials" do test "verify_credentials" do
%{user: user, conn: conn} = oauth_access(["read:accounts"]) %{user: user, conn: conn} = oauth_access(["read:accounts"])
[notification | _] = insert_list(7, :notification, user: user)
Pleroma.Notification.set_read_up_to(user, notification.id)
conn = get(conn, "/api/v1/accounts/verify_credentials") conn = get(conn, "/api/v1/accounts/verify_credentials")
response = json_response_and_validate_schema(conn, 200) response = json_response_and_validate_schema(conn, 200)
assert %{"id" => id, "source" => %{"privacy" => "public"}} = response assert %{"id" => id, "source" => %{"privacy" => "public"}} = response
assert response["pleroma"]["chat_token"] assert response["pleroma"]["chat_token"]
assert response["pleroma"]["unread_notifications_count"] == 6
assert id == to_string(user.id) assert id == to_string(user.id)
end end

View file

@ -11,6 +11,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do
test "gets markers with correct scopes", %{conn: conn} do test "gets markers with correct scopes", %{conn: conn} do
user = insert(:user) user = insert(:user)
token = insert(:oauth_token, user: user, scopes: ["read:statuses"]) token = insert(:oauth_token, user: user, scopes: ["read:statuses"])
insert_list(7, :notification, user: user)
{:ok, %{"notifications" => marker}} = {:ok, %{"notifications" => marker}} =
Pleroma.Marker.upsert( Pleroma.Marker.upsert(
@ -29,7 +30,8 @@ test "gets markers with correct scopes", %{conn: conn} do
"notifications" => %{ "notifications" => %{
"last_read_id" => "69420", "last_read_id" => "69420",
"updated_at" => NaiveDateTime.to_iso8601(marker.updated_at), "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at),
"version" => 0 "version" => 0,
"pleroma" => %{"unread_count" => 7}
} }
} }
end end
@ -71,7 +73,8 @@ test "creates a marker with correct scopes", %{conn: conn} do
"notifications" => %{ "notifications" => %{
"last_read_id" => "69420", "last_read_id" => "69420",
"updated_at" => _, "updated_at" => _,
"version" => 0 "version" => 0,
"pleroma" => %{"unread_count" => 0}
} }
} = response } = response
end end
@ -101,7 +104,8 @@ test "updates exist marker", %{conn: conn} do
"notifications" => %{ "notifications" => %{
"last_read_id" => "69888", "last_read_id" => "69888",
"updated_at" => NaiveDateTime.to_iso8601(marker.updated_at), "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at),
"version" => 0 "version" => 0,
"pleroma" => %{"unread_count" => 0}
} }
} }
end end

View file

@ -27,8 +27,8 @@ test "it returns empty result if user or status search return undefined error",
capture_log(fn -> capture_log(fn ->
results = results =
conn conn
|> get("/api/v2/search", %{"q" => "2hu"}) |> get("/api/v2/search?q=2hu")
|> json_response(200) |> json_response_and_validate_schema(200)
assert results["accounts"] == [] assert results["accounts"] == []
assert results["statuses"] == [] assert results["statuses"] == []
@ -54,8 +54,8 @@ test "search", %{conn: conn} do
results = results =
conn conn
|> get("/api/v2/search", %{"q" => "2hu #private"}) |> get("/api/v2/search?#{URI.encode_query(%{q: "2hu #private"})}")
|> json_response(200) |> json_response_and_validate_schema(200)
[account | _] = results["accounts"] [account | _] = results["accounts"]
assert account["id"] == to_string(user_three.id) assert account["id"] == to_string(user_three.id)
@ -68,8 +68,8 @@ test "search", %{conn: conn} do
assert status["id"] == to_string(activity.id) assert status["id"] == to_string(activity.id)
results = results =
get(conn, "/api/v2/search", %{"q" => "天子"}) get(conn, "/api/v2/search?q=天子")
|> json_response(200) |> json_response_and_validate_schema(200)
[status] = results["statuses"] [status] = results["statuses"]
assert status["id"] == to_string(activity.id) assert status["id"] == to_string(activity.id)
@ -89,8 +89,8 @@ test "excludes a blocked users from search results", %{conn: conn} do
conn conn
|> assign(:user, user) |> assign(:user, user)
|> assign(:token, insert(:oauth_token, user: user, scopes: ["read"])) |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"]))
|> get("/api/v2/search", %{"q" => "Agent"}) |> get("/api/v2/search?q=Agent")
|> json_response(200) |> json_response_and_validate_schema(200)
status_ids = Enum.map(results["statuses"], fn g -> g["id"] end) status_ids = Enum.map(results["statuses"], fn g -> g["id"] end)
@ -107,8 +107,8 @@ test "account search", %{conn: conn} do
results = results =
conn conn
|> get("/api/v1/accounts/search", %{"q" => "shp"}) |> get("/api/v1/accounts/search?q=shp")
|> json_response(200) |> json_response_and_validate_schema(200)
result_ids = for result <- results, do: result["acct"] result_ids = for result <- results, do: result["acct"]
@ -117,8 +117,8 @@ test "account search", %{conn: conn} do
results = results =
conn conn
|> get("/api/v1/accounts/search", %{"q" => "2hu"}) |> get("/api/v1/accounts/search?q=2hu")
|> json_response(200) |> json_response_and_validate_schema(200)
result_ids = for result <- results, do: result["acct"] result_ids = for result <- results, do: result["acct"]
@ -130,8 +130,8 @@ test "returns account if query contains a space", %{conn: conn} do
results = results =
conn conn
|> get("/api/v1/accounts/search", %{"q" => "shp@shitposter.club xxx "}) |> get("/api/v1/accounts/search?q=shp@shitposter.club xxx")
|> json_response(200) |> json_response_and_validate_schema(200)
assert length(results) == 1 assert length(results) == 1
end end
@ -146,8 +146,8 @@ test "it returns empty result if user or status search return undefined error",
capture_log(fn -> capture_log(fn ->
results = results =
conn conn
|> get("/api/v1/search", %{"q" => "2hu"}) |> get("/api/v1/search?q=2hu")
|> json_response(200) |> json_response_and_validate_schema(200)
assert results["accounts"] == [] assert results["accounts"] == []
assert results["statuses"] == [] assert results["statuses"] == []
@ -173,8 +173,8 @@ test "search", %{conn: conn} do
results = results =
conn conn
|> get("/api/v1/search", %{"q" => "2hu"}) |> get("/api/v1/search?q=2hu")
|> json_response(200) |> json_response_and_validate_schema(200)
[account | _] = results["accounts"] [account | _] = results["accounts"]
assert account["id"] == to_string(user_three.id) assert account["id"] == to_string(user_three.id)
@ -194,8 +194,8 @@ test "search fetches remote statuses and prefers them over other results", %{con
results = results =
conn conn
|> get("/api/v1/search", %{"q" => "https://shitposter.club/notice/2827873"}) |> get("/api/v1/search?q=https://shitposter.club/notice/2827873")
|> json_response(200) |> json_response_and_validate_schema(200)
[status, %{"id" => ^activity_id}] = results["statuses"] [status, %{"id" => ^activity_id}] = results["statuses"]
@ -212,10 +212,12 @@ test "search doesn't show statuses that it shouldn't", %{conn: conn} do
}) })
capture_log(fn -> capture_log(fn ->
q = Object.normalize(activity).data["id"]
results = results =
conn conn
|> get("/api/v1/search", %{"q" => Object.normalize(activity).data["id"]}) |> get("/api/v1/search?q=#{q}")
|> json_response(200) |> json_response_and_validate_schema(200)
[] = results["statuses"] [] = results["statuses"]
end) end)
@ -228,8 +230,8 @@ test "search fetches remote accounts", %{conn: conn} do
conn conn
|> assign(:user, user) |> assign(:user, user)
|> assign(:token, insert(:oauth_token, user: user, scopes: ["read"])) |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"]))
|> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "true"}) |> get("/api/v1/search?q=mike@osada.macgirvin.com&resolve=true")
|> json_response(200) |> json_response_and_validate_schema(200)
[account] = results["accounts"] [account] = results["accounts"]
assert account["acct"] == "mike@osada.macgirvin.com" assert account["acct"] == "mike@osada.macgirvin.com"
@ -238,8 +240,8 @@ test "search fetches remote accounts", %{conn: conn} do
test "search doesn't fetch remote accounts if resolve is false", %{conn: conn} do test "search doesn't fetch remote accounts if resolve is false", %{conn: conn} do
results = results =
conn conn
|> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "false"}) |> get("/api/v1/search?q=mike@osada.macgirvin.com&resolve=false")
|> json_response(200) |> json_response_and_validate_schema(200)
assert [] == results["accounts"] assert [] == results["accounts"]
end end
@ -254,16 +256,16 @@ test "search with limit and offset", %{conn: conn} do
result = result =
conn conn
|> get("/api/v1/search", %{"q" => "2hu", "limit" => 1}) |> get("/api/v1/search?q=2hu&limit=1")
assert results = json_response(result, 200) assert results = json_response_and_validate_schema(result, 200)
assert [%{"id" => activity_id1}] = results["statuses"] assert [%{"id" => activity_id1}] = results["statuses"]
assert [_] = results["accounts"] assert [_] = results["accounts"]
results = results =
conn conn
|> get("/api/v1/search", %{"q" => "2hu", "limit" => 1, "offset" => 1}) |> get("/api/v1/search?q=2hu&limit=1&offset=1")
|> json_response(200) |> json_response_and_validate_schema(200)
assert [%{"id" => activity_id2}] = results["statuses"] assert [%{"id" => activity_id2}] = results["statuses"]
assert [] = results["accounts"] assert [] = results["accounts"]
@ -279,13 +281,13 @@ test "search returns results only for the given type", %{conn: conn} do
assert %{"statuses" => [_activity], "accounts" => [], "hashtags" => []} = assert %{"statuses" => [_activity], "accounts" => [], "hashtags" => []} =
conn conn
|> get("/api/v1/search", %{"q" => "2hu", "type" => "statuses"}) |> get("/api/v1/search?q=2hu&type=statuses")
|> json_response(200) |> json_response_and_validate_schema(200)
assert %{"statuses" => [], "accounts" => [_user_two], "hashtags" => []} = assert %{"statuses" => [], "accounts" => [_user_two], "hashtags" => []} =
conn conn
|> get("/api/v1/search", %{"q" => "2hu", "type" => "accounts"}) |> get("/api/v1/search?q=2hu&type=accounts")
|> json_response(200) |> json_response_and_validate_schema(200)
end end
test "search uses account_id to filter statuses by the author", %{conn: conn} do test "search uses account_id to filter statuses by the author", %{conn: conn} do
@ -297,8 +299,8 @@ test "search uses account_id to filter statuses by the author", %{conn: conn} do
results = results =
conn conn
|> get("/api/v1/search", %{"q" => "2hu", "account_id" => user.id}) |> get("/api/v1/search?q=2hu&account_id=#{user.id}")
|> json_response(200) |> json_response_and_validate_schema(200)
assert [%{"id" => activity_id1}] = results["statuses"] assert [%{"id" => activity_id1}] = results["statuses"]
assert activity_id1 == activity1.id assert activity_id1 == activity1.id
@ -306,8 +308,8 @@ test "search uses account_id to filter statuses by the author", %{conn: conn} do
results = results =
conn conn
|> get("/api/v1/search", %{"q" => "2hu", "account_id" => user_two.id}) |> get("/api/v1/search?q=2hu&account_id=#{user_two.id}")
|> json_response(200) |> json_response_and_validate_schema(200)
assert [%{"id" => activity_id2}] = results["statuses"] assert [%{"id" => activity_id2}] = results["statuses"]
assert activity_id2 == activity2.id assert activity_id2 == activity2.id

View file

@ -469,6 +469,24 @@ test "shows unread_conversation_count only to the account owner" do
:unread_conversation_count :unread_conversation_count
] == 1 ] == 1
end end
test "shows unread_count only to the account owner" do
user = insert(:user)
insert_list(7, :notification, user: user)
other_user = insert(:user)
user = User.get_cached_by_ap_id(user.ap_id)
assert AccountView.render(
"show.json",
%{user: user, for: other_user}
)[:pleroma][:unread_notifications_count] == nil
assert AccountView.render(
"show.json",
%{user: user, for: user}
)[:pleroma][:unread_notifications_count] == 7
end
end end
describe "follow requests counter" do describe "follow requests counter" do

View file

@ -8,19 +8,21 @@ defmodule Pleroma.Web.MastodonAPI.MarkerViewTest do
import Pleroma.Factory import Pleroma.Factory
test "returns markers" do test "returns markers" do
marker1 = insert(:marker, timeline: "notifications", last_read_id: "17") marker1 = insert(:marker, timeline: "notifications", last_read_id: "17", unread_count: 5)
marker2 = insert(:marker, timeline: "home", last_read_id: "42") marker2 = insert(:marker, timeline: "home", last_read_id: "42")
assert MarkerView.render("markers.json", %{markers: [marker1, marker2]}) == %{ assert MarkerView.render("markers.json", %{markers: [marker1, marker2]}) == %{
"home" => %{ "home" => %{
last_read_id: "42", last_read_id: "42",
updated_at: NaiveDateTime.to_iso8601(marker2.updated_at), updated_at: NaiveDateTime.to_iso8601(marker2.updated_at),
version: 0 version: 0,
pleroma: %{unread_count: 0}
}, },
"notifications" => %{ "notifications" => %{
last_read_id: "17", last_read_id: "17",
updated_at: NaiveDateTime.to_iso8601(marker1.updated_at), updated_at: NaiveDateTime.to_iso8601(marker1.updated_at),
version: 0 version: 0,
pleroma: %{unread_count: 5}
} }
} }
end end

View file

@ -182,7 +182,7 @@ test "EmojiReact notification" do
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"}) {:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
{:ok, _activity, _} = CommonAPI.react_with_emoji(activity.id, other_user, "") {:ok, _activity} = CommonAPI.react_with_emoji(activity.id, other_user, "")
activity = Repo.get(Activity, activity.id) activity = Repo.get(Activity, activity.id)

View file

@ -32,9 +32,9 @@ test "has an emoji reaction list" do
third_user = insert(:user) third_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "dae cofe??"}) {:ok, activity} = CommonAPI.post(user, %{"status" => "dae cofe??"})
{:ok, _, _} = CommonAPI.react_with_emoji(activity.id, user, "") {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "")
{:ok, _, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵") {:ok, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵")
{:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "") {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "")
activity = Repo.get(Activity, activity.id) activity = Repo.get(Activity, activity.id)
status = StatusView.render("show.json", activity: activity) status = StatusView.render("show.json", activity: activity)

View file

@ -3,12 +3,14 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
@ -41,7 +43,9 @@ test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"}) {:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
{:ok, activity, _object} = CommonAPI.react_with_emoji(activity.id, other_user, "") {:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, "")
ObanHelpers.perform_all()
result = result =
conn conn
@ -52,7 +56,9 @@ test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
assert %{"id" => id} = json_response(result, 200) assert %{"id" => id} = json_response(result, 200)
assert to_string(activity.id) == id assert to_string(activity.id) == id
object = Object.normalize(activity) ObanHelpers.perform_all()
object = Object.get_by_ap_id(activity.data["object"])
assert object.data["reaction_count"] == 0 assert object.data["reaction_count"] == 0
end end
@ -71,8 +77,8 @@ test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do
assert result == [] assert result == []
{:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
{:ok, _, _} = CommonAPI.react_with_emoji(activity.id, doomed_user, "🎅") {:ok, _} = CommonAPI.react_with_emoji(activity.id, doomed_user, "🎅")
User.perform(:delete, doomed_user) User.perform(:delete, doomed_user)
@ -109,8 +115,8 @@ test "GET /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
assert result == [] assert result == []
{:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
{:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "") {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "")
result = result =
conn conn

View file

@ -193,7 +193,7 @@ test "renders title for create activity with direct visibility" do
end end
describe "build_content/3" do describe "build_content/3" do
test "returns info content for direct message with enabled privacy option" do test "hides details for notifications when privacy option enabled" do
user = insert(:user, nickname: "Bob") user = insert(:user, nickname: "Bob")
user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: true}) user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: true})
@ -209,12 +209,37 @@ test "returns info content for direct message with enabled privacy option" do
object = Object.normalize(activity) object = Object.normalize(activity)
assert Impl.build_content(notif, actor, object) == %{ assert Impl.build_content(notif, actor, object) == %{
body: "@Bob", body: "New Direct Message"
title: "New Direct Message" }
{:ok, activity} =
CommonAPI.post(user, %{
"visibility" => "public",
"status" => "<Lorem ipsum dolor sit amet."
})
notif = insert(:notification, user: user2, activity: activity)
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
object = Object.normalize(activity)
assert Impl.build_content(notif, actor, object) == %{
body: "New Mention"
}
{:ok, activity} = CommonAPI.favorite(user, activity.id)
notif = insert(:notification, user: user2, activity: activity)
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
object = Object.normalize(activity)
assert Impl.build_content(notif, actor, object) == %{
body: "New Favorite"
} }
end end
test "returns regular content for direct message with disabled privacy option" do test "returns regular content for notifications with privacy option disabled" do
user = insert(:user, nickname: "Bob") user = insert(:user, nickname: "Bob")
user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: false}) user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: false})
@ -235,6 +260,36 @@ test "returns regular content for direct message with disabled privacy option" d
"@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini...", "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini...",
title: "New Direct Message" title: "New Direct Message"
} }
{:ok, activity} =
CommonAPI.post(user, %{
"visibility" => "public",
"status" =>
"<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis."
})
notif = insert(:notification, user: user2, activity: activity)
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
object = Object.normalize(activity)
assert Impl.build_content(notif, actor, object) == %{
body:
"@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini...",
title: "New Mention"
}
{:ok, activity} = CommonAPI.favorite(user, activity.id)
notif = insert(:notification, user: user2, activity: activity)
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
object = Object.normalize(activity)
assert Impl.build_content(notif, actor, object) == %{
body: "@Bob has favorited your post",
title: "New Favorite"
}
end end
end end
end end