Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into remake-remodel-dms
This commit is contained in:
commit
7637ef4203
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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(
|
||||||
%{
|
%{
|
||||||
|
|
|
@ -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"]))
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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")}
|
||||||
|
|
|
@ -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})
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue