Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into feature/emojireactvalidator

This commit is contained in:
lain 2020-05-07 18:53:34 +02:00
commit ef55d24054
30 changed files with 863 additions and 616 deletions

View File

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

View File

@ -356,56 +356,6 @@ def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
end end
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(
@ -436,35 +386,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
@ -537,28 +458,6 @@ defp do_block(blocker, blocked, activity_id, local) do
end end
end end
@spec unblock(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | {:error, any()} | nil
def unblock(blocker, blocked, activity_id \\ nil, local \\ true) do
with {:ok, result} <-
Repo.transaction(fn -> do_unblock(blocker, blocked, activity_id, local) end) do
result
end
end
defp do_unblock(blocker, blocked, activity_id, local) do
with %Activity{} = block_activity <- fetch_latest_block(blocker, blocked),
unblock_data <- make_unblock_data(blocker, blocked, block_activity, activity_id),
{:ok, activity} <- insert(unblock_data, local),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
nil -> nil
{:error, error} -> Repo.rollback(error)
end
end
@spec flag(map()) :: {:ok, Activity.t()} | {:error, any()} @spec flag(map()) :: {:ok, Activity.t()} | {:error, any()}
def flag( def flag(
%{ %{

View File

@ -22,6 +22,19 @@ def emoji_react(actor, object, emoji) do
end end
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)

View File

@ -15,10 +15,21 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator 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),

View File

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

View File

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

View File

@ -5,8 +5,10 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
liked object, a `Follow` activity will add the user to the follower liked object, a `Follow` activity will add the user to the follower
collection, and so on. collection, and so on.
""" """
alias Pleroma.Activity
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
@ -25,6 +27,13 @@ def handle(%{data: %{"type" => "Like"}} = object, meta) do
{:ok, object, meta} {:ok, object, meta}
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: # Tasks this handles:
# - Add reaction to object # - Add reaction to object
# - Set up notification # - Set up notification
@ -84,4 +93,41 @@ def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object,
def handle(object, meta) do def handle(object, meta) do
{:ok, object, meta} {:ok, object, meta}
end end
def handle_undoing(%{data: %{"type" => "Like"}} = object) do
with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
{:ok, _} <- Utils.remove_like_from_object(object, liked_object),
{:ok, _} <- Repo.delete(object) do
:ok
end
end
def handle_undoing(%{data: %{"type" => "EmojiReact"}} = object) do
with %Object{} = reacted_object <- Object.get_by_ap_id(object.data["object"]),
{:ok, _} <- Utils.remove_emoji_reaction_from_object(object, reacted_object),
{:ok, _} <- Repo.delete(object) do
:ok
end
end
def handle_undoing(%{data: %{"type" => "Announce"}} = object) do
with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
{:ok, _} <- Utils.remove_announce_from_object(object, liked_object),
{:ok, _} <- Repo.delete(object) do
:ok
end
end
def handle_undoing(
%{data: %{"type" => "Block", "actor" => blocker, "object" => blocked}} = object
) do
with %User{} = blocker <- User.get_cached_by_ap_id(blocker),
%User{} = blocked <- User.get_cached_by_ap_id(blocked),
{:ok, _} <- User.unblock(blocker, blocked),
{:ok, _} <- Repo.delete(object) do
:ok
end
end
def handle_undoing(object), do: {:error, ["don't know how to handle", object]}
end end

View File

@ -723,25 +723,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",
@ -764,75 +745,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
@ -854,6 +773,21 @@ def handle_incoming(
end end
end end
def handle_incoming(
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
_options
) do
with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
{:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
User.unfollow(blocker, blocked)
User.block(blocker, blocked)
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming( def handle_incoming(
%{ %{
"type" => "Move", "type" => "Move",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,6 +24,14 @@ defmodule Pleroma.Web.CommonAPI do
require Pleroma.Constants require Pleroma.Constants
require Logger require Logger
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])
@ -107,9 +115,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")}
@ -166,9 +177,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")}
@ -188,8 +202,10 @@ def react_with_emoji(id, user, emoji) do
end end
def unreact_with_emoji(id, user, emoji) do def unreact_with_emoji(id, user, emoji) do
with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"]) {:ok, undo, _} <- Builder.undo(user, reaction_activity),
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
{:ok, activity}
else else
_ -> _ ->
{:error, dgettext("errors", "Could not remove reaction emoji")} {:error, dgettext("errors", "Could not remove reaction emoji")}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -728,7 +728,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 +762,7 @@ test "repeating an activity results in 1 notification, then 0 if the activity is
assert length(Notification.for_user(user)) == 1 assert length(Notification.for_user(user)) == 1
{:ok, _, _} = CommonAPI.unrepeat(activity.id, other_user) {:ok, _} = CommonAPI.unrepeat(activity.id, other_user)
assert Enum.empty?(Notification.for_user(user)) assert Enum.empty?(Notification.for_user(user))
end end

View File

@ -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,122 +873,6 @@ test "returns reblogs for users for whom reblogs have not been muted" do
end end
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} = CommonAPI.react_with_emoji(activity.id, reactor, "🔥")
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} = CommonAPI.react_with_emoji(activity.id, reactor, "🔥")
{: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} = CommonAPI.react_with_emoji(activity.id, reactor, "😀")
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)
@ -1059,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{
@ -1211,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)
@ -1233,38 +1070,6 @@ test "creates a block activity" do
assert activity.data["actor"] == blocker.ap_id assert activity.data["actor"] == blocker.ap_id
assert activity.data["object"] == blocked.ap_id assert activity.data["object"] == blocked.ap_id
end end
test "reverts unblock activity on error" do
[blocker, blocked] = insert_list(2, :user)
{:ok, block_activity} = ActivityPub.block(blocker, blocked)
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.unblock(blocker, blocked)
end
assert block_activity.data["type"] == "Block"
assert block_activity.data["actor"] == blocker.ap_id
assert Repo.aggregate(Activity, :count, :id) == 1
assert Repo.aggregate(Object, :count, :id) == 1
end
test "creates an undo activity for the last block" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, block_activity} = ActivityPub.block(blocker, blocked)
{:ok, activity} = ActivityPub.unblock(blocker, blocked)
assert activity.data["type"] == "Undo"
assert activity.data["actor"] == blocker.ap_id
embedded_object = activity.data["object"]
assert is_map(embedded_object)
assert embedded_object["type"] == "Block"
assert embedded_object["object"] == blocked.ap_id
assert embedded_object["id"] == block_activity.data["id"]
end
end end
describe "timeline post-processing" do describe "timeline post-processing" do

View File

@ -50,6 +50,46 @@ test "it is not valid with a non-emoji content field", %{valid_emoji_react: vali
end end
end end
describe "Undos" do
setup do
user = insert(:user)
{:ok, post_activity} = CommonAPI.post(user, %{"status" => "uguu"})
{:ok, like} = CommonAPI.favorite(user, post_activity.id)
{:ok, valid_like_undo, []} = Builder.undo(user, like)
%{user: user, like: like, valid_like_undo: valid_like_undo}
end
test "it validates a basic like undo", %{valid_like_undo: valid_like_undo} do
assert {:ok, _, _} = ObjectValidator.validate(valid_like_undo, [])
end
test "it does not validate if the actor of the undo is not the actor of the object", %{
valid_like_undo: valid_like_undo
} do
other_user = insert(:user, ap_id: "https://gensokyo.2hu/users/raymoo")
bad_actor =
valid_like_undo
|> Map.put("actor", other_user.ap_id)
{:error, cng} = ObjectValidator.validate(bad_actor, [])
assert {:actor, {"not the same as object actor", []}} in cng.errors
end
test "it does not validate if the object is missing", %{valid_like_undo: valid_like_undo} do
missing_object =
valid_like_undo
|> Map.put("object", "https://gensokyo.2hu/objects/1")
{:error, cng} = ObjectValidator.validate(missing_object, [])
assert {:object, {"can't find object", []}} in cng.errors
assert length(cng.errors) == 1
end
end
describe "deletes" do describe "deletes" do
setup do setup do
user = insert(:user) user = insert(:user)

View File

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

View File

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

View File

@ -378,7 +378,7 @@ test "it works for incoming unlikes with an existing like activity" do
assert data["actor"] == "http://mastodon.example.org/users/admin" assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Undo" assert data["type"] == "Undo"
assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/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" assert data["object"] == "http://mastodon.example.org/users/admin#likes/2"
end end
test "it works for incoming unlikes with an existing like activity and a compact object" do test "it works for incoming unlikes with an existing like activity and a compact object" do
@ -403,7 +403,44 @@ test "it works for incoming unlikes with an existing like activity and a compact
assert data["actor"] == "http://mastodon.example.org/users/admin" assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Undo" assert data["type"] == "Undo"
assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/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" assert data["object"] == "http://mastodon.example.org/users/admin#likes/2"
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 end
test "it works for incoming announces" do test "it works for incoming announces" do
@ -729,35 +766,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)
@ -852,32 +860,6 @@ test "incoming blocks successfully tear down any follow relationship" do
refute User.following?(blocked, blocker) refute User.following?(blocked, blocker)
end end
test "it works for incoming unblocks with an existing block" do
user = insert(:user)
block_data =
File.read!("test/fixtures/mastodon-block-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(block_data)
data =
File.read!("test/fixtures/mastodon-unblock-activity.json")
|> Poison.decode!()
|> Map.put("object", block_data)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Undo"
assert data["object"]["type"] == "Block"
assert data["object"]["object"] == user.ap_id
assert data["actor"] == "http://mastodon.example.org/users/admin"
blocker = User.get_cached_by_ap_id(data["actor"])
refute User.blocks?(blocker, user)
end
test "it works for incoming accepts which were pre-accepted" do test "it works for incoming accepts which were pre-accepted" do
follower = insert(:user) follower = insert(:user)
followed = insert(:user) followed = insert(:user)

View File

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

View File

@ -375,10 +375,11 @@ test "unreacting 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, "👍")
{:ok, unreaction, _} = CommonAPI.unreact_with_emoji(activity.id, user, "👍") {:ok, unreaction} = CommonAPI.unreact_with_emoji(activity.id, user, "👍")
assert unreaction.data["type"] == "Undo" assert unreaction.data["type"] == "Undo"
assert unreaction.data["object"] == reaction.data["id"] assert unreaction.data["object"] == reaction.data["id"]
assert unreaction.local
end end
test "repeating a status" do test "repeating a status" do

View File

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

View File

@ -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} = 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