Merge branch 'feature/1482-activity_pub_transactions' into 'develop'

ActivityPub actions & side-effects in transaction

Closes #1482

See merge request pleroma/pleroma!2089
This commit is contained in:
Haelwenn 2020-03-02 07:58:01 +00:00
commit 764a50f8a6
8 changed files with 392 additions and 64 deletions

2
.gitignore vendored
View file

@ -47,3 +47,5 @@ docs/generated_config.md
.idea .idea
pleroma.iml pleroma.iml
# asdf
.tool-versions

View file

@ -5,6 +5,9 @@
defmodule Pleroma.Object do defmodule Pleroma.Object do
use Ecto.Schema use Ecto.Schema
import Ecto.Query
import Ecto.Changeset
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Object.Fetcher alias Pleroma.Object.Fetcher
@ -12,9 +15,6 @@ defmodule Pleroma.Object do
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
import Ecto.Query
import Ecto.Changeset
require Logger require Logger
@type t() :: %__MODULE__{} @type t() :: %__MODULE__{}
@ -145,18 +145,18 @@ def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}),
# Legacy objects can be mutated by anybody # Legacy objects can be mutated by anybody
def authorize_mutation(%Object{}, %User{}), do: true def authorize_mutation(%Object{}, %User{}), do: true
@spec get_cached_by_ap_id(String.t()) :: Object.t() | nil
def get_cached_by_ap_id(ap_id) do def get_cached_by_ap_id(ap_id) do
key = "object:#{ap_id}" key = "object:#{ap_id}"
Cachex.fetch!(:object_cache, key, fn _ -> with {:ok, nil} <- Cachex.get(:object_cache, key),
object = get_by_ap_id(ap_id) object when not is_nil(object) <- get_by_ap_id(ap_id),
{:ok, true} <- Cachex.put(:object_cache, key, object) do
if object do object
{:commit, object}
else else
{:ignore, object} {:ok, object} -> object
nil -> nil
end end
end)
end end
def context_mapping(context) do def context_mapping(context) do

View file

@ -12,12 +12,15 @@ defmodule Pleroma.Pagination do
alias Pleroma.Repo alias Pleroma.Repo
@type type :: :keyset | :offset
@default_limit 20 @default_limit 20
@max_limit 40 @max_limit 40
@page_keys ["max_id", "min_id", "limit", "since_id", "order"] @page_keys ["max_id", "min_id", "limit", "since_id", "order"]
def page_keys, do: @page_keys def page_keys, do: @page_keys
@spec fetch_paginated(Ecto.Query.t(), map(), type(), atom() | nil) :: [Ecto.Schema.t()]
def fetch_paginated(query, params, type \\ :keyset, table_binding \\ nil) def fetch_paginated(query, params, type \\ :keyset, table_binding \\ nil)
def fetch_paginated(query, %{"total" => true} = params, :keyset, table_binding) do def fetch_paginated(query, %{"total" => true} = params, :keyset, table_binding) do
@ -58,6 +61,7 @@ def fetch_paginated(query, params, :offset, table_binding) do
|> Repo.all() |> Repo.all()
end end
@spec paginate(Ecto.Query.t(), map(), type(), atom() | nil) :: [Ecto.Schema.t()]
def paginate(query, options, method \\ :keyset, table_binding \\ nil) def paginate(query, options, method \\ :keyset, table_binding \\ nil)
def paginate(query, options, :keyset, table_binding) do def paginate(query, options, :keyset, table_binding) do

View file

@ -37,6 +37,7 @@ defmodule Pleroma.Upload do
Plug.Upload.t() Plug.Upload.t()
| (data_uri_string :: String.t()) | (data_uri_string :: String.t())
| {:from_local, name :: String.t(), id :: String.t(), path :: String.t()} | {:from_local, name :: String.t(), id :: String.t(), path :: String.t()}
| map()
@type option :: @type option ::
{:type, :avatar | :banner | :background} {:type, :avatar | :banner | :background}

View file

@ -749,9 +749,18 @@ def invalidate_cache(user) do
Cachex.del(:user_cache, "nickname:#{user.nickname}") Cachex.del(:user_cache, "nickname:#{user.nickname}")
end end
@spec get_cached_by_ap_id(String.t()) :: User.t() | nil
def get_cached_by_ap_id(ap_id) do def get_cached_by_ap_id(ap_id) do
key = "ap_id:#{ap_id}" key = "ap_id:#{ap_id}"
Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
with {:ok, nil} <- Cachex.get(:user_cache, key),
user when not is_nil(user) <- get_by_ap_id(ap_id),
{:ok, true} <- Cachex.put(:user_cache, key, user) do
user
else
{:ok, user} -> user
nil -> nil
end
end end
def get_cached_by_id(id) do def get_cached_by_id(id) do

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Activity.Ir.Topics alias Pleroma.Activity.Ir.Topics
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Constants
alias Pleroma.Conversation alias Pleroma.Conversation
alias Pleroma.Conversation.Participation alias Pleroma.Conversation.Participation
alias Pleroma.Notification alias Pleroma.Notification
@ -124,6 +125,7 @@ def increase_poll_votes_if_vote(%{
def increase_poll_votes_if_vote(_create_data), do: :noop def increase_poll_votes_if_vote(_create_data), do: :noop
@spec insert(map(), boolean(), boolean(), boolean()) :: {:ok, Activity.t()} | {:error, any()}
def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do
with nil <- Activity.normalize(map), with nil <- Activity.normalize(map),
map <- lazy_put_activity_defaults(map, fake), map <- lazy_put_activity_defaults(map, fake),
@ -231,12 +233,19 @@ def stream_out(_activity) do
:noop :noop
end end
def create(%{to: to, actor: actor, context: context, object: object} = params, fake \\ false) do @spec create(map(), boolean()) :: {:ok, Activity.t()} | {:error, any()}
def create(params, fake \\ false) do
with {:ok, result} <- Repo.transaction(fn -> do_create(params, fake) end) do
result
end
end
defp do_create(%{to: to, actor: actor, context: context, object: object} = params, fake) do
additional = params[:additional] || %{} additional = params[:additional] || %{}
# only accept false as false value # only accept false as false value
local = !(params[:local] == false) local = !(params[:local] == false)
published = params[:published] published = params[:published]
quick_insert? = Pleroma.Config.get([:env]) == :benchmark quick_insert? = Config.get([:env]) == :benchmark
with create_data <- with create_data <-
make_create_data( make_create_data(
@ -259,10 +268,11 @@ def create(%{to: to, actor: actor, context: context, object: object} = params, f
{:ok, activity} {:ok, activity}
{:error, message} -> {:error, message} ->
{:error, message} Repo.rollback(message)
end end
end end
@spec listen(map()) :: {:ok, Activity.t()} | {:error, any()}
def listen(%{to: to, actor: actor, context: context, object: object} = params) do def listen(%{to: to, actor: actor, context: context, object: object} = params) do
additional = params[:additional] || %{} additional = params[:additional] || %{}
# only accept false as false value # only accept false as false value
@ -277,20 +287,20 @@ def listen(%{to: to, actor: actor, context: context, object: object} = params) d
{:ok, activity} <- insert(listen_data, local), {:ok, activity} <- insert(listen_data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
else
{:error, message} ->
{:error, message}
end end
end end
@spec accept(map()) :: {:ok, Activity.t()} | {:error, any()}
def accept(params) do def accept(params) do
accept_or_reject("Accept", params) accept_or_reject("Accept", params)
end end
@spec reject(map()) :: {:ok, Activity.t()} | {:error, any()}
def reject(params) do def reject(params) do
accept_or_reject("Reject", params) accept_or_reject("Reject", params)
end end
@spec accept_or_reject(String.t(), map()) :: {:ok, Activity.t()} | {:error, any()}
def accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do def accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do
local = Map.get(params, :local, true) local = Map.get(params, :local, true)
activity_id = Map.get(params, :activity_id, nil) activity_id = Map.get(params, :activity_id, nil)
@ -304,6 +314,7 @@ def accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do
end end
end end
@spec update(map()) :: {:ok, Activity.t()} | {:error, any()}
def update(%{to: to, cc: cc, actor: actor, object: object} = params) do def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
local = !(params[:local] == false) local = !(params[:local] == false)
activity_id = params[:activity_id] activity_id = params[:activity_id]
@ -322,7 +333,16 @@ 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 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), with local <- Keyword.get(options, :local, true),
activity_id <- Keyword.get(options, :activity_id, nil), activity_id <- Keyword.get(options, :activity_id, nil),
true <- Pleroma.Emoji.is_unicode_emoji?(emoji), true <- Pleroma.Emoji.is_unicode_emoji?(emoji),
@ -332,11 +352,21 @@ def react_with_emoji(user, object, emoji, options \\ []) do
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity, object} {:ok, activity, object}
else else
e -> {:error, e} false -> {:error, false}
{:error, error} -> Repo.rollback(error)
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 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), with local <- Keyword.get(options, :local, true),
activity_id <- Keyword.get(options, :activity_id, nil), activity_id <- Keyword.get(options, :activity_id, nil),
user_ap_id <- user.ap_id, user_ap_id <- user.ap_id,
@ -348,16 +378,24 @@ def unreact_with_emoji(user, reaction_id, options \\ []) do
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity, object} {:ok, activity, object}
else else
e -> {:error, e} {:error, error} -> Repo.rollback(error)
end end
end end
# TODO: This is weird, maybe we shouldn't check here if we can make the activity. # TODO: This is weird, maybe we shouldn't check here if we can make the activity.
def like( @spec like(User.t(), Object.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t(), Object.t()} | {:error, any()}
def like(user, object, activity_id \\ nil, local \\ true) do
with {:ok, result} <- Repo.transaction(fn -> do_like(user, object, activity_id, local) end) do
result
end
end
defp do_like(
%User{ap_id: ap_id} = user, %User{ap_id: ap_id} = user,
%Object{data: %{"id" => _}} = object, %Object{data: %{"id" => _}} = object,
activity_id \\ nil, activity_id,
local \\ true local
) do ) do
with nil <- get_existing_like(ap_id, object), with nil <- get_existing_like(ap_id, object),
like_data <- make_like_data(user, object, activity_id), like_data <- make_like_data(user, object, activity_id),
@ -366,12 +404,24 @@ def like(
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity, object} {:ok, activity, object}
else else
%Activity{} = activity -> {:ok, activity, object} %Activity{} = activity ->
error -> {:error, error} {:ok, activity, object}
{:error, error} ->
Repo.rollback(error)
end end
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 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), with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object),
unlike_data <- make_unlike_data(actor, like_activity, activity_id), unlike_data <- make_unlike_data(actor, like_activity, activity_id),
{:ok, unlike_activity} <- insert(unlike_data, local), {:ok, unlike_activity} <- insert(unlike_data, local),
@ -380,10 +430,13 @@ def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ tru
:ok <- maybe_federate(unlike_activity) do :ok <- maybe_federate(unlike_activity) do
{:ok, unlike_activity, like_activity, object} {:ok, unlike_activity, like_activity, object}
else else
_e -> {:ok, object} nil -> {:ok, object}
{:error, error} -> Repo.rollback(error)
end end
end end
@spec announce(User.t(), Object.t(), String.t() | nil, boolean(), boolean()) ::
{:ok, Activity.t(), Object.t()} | {:error, any()}
def announce( def announce(
%User{ap_id: _} = user, %User{ap_id: _} = user,
%Object{data: %{"id" => _}} = object, %Object{data: %{"id" => _}} = object,
@ -391,6 +444,13 @@ def announce(
local \\ true, local \\ true,
public \\ true public \\ true
) do ) do
with {:ok, result} <-
Repo.transaction(fn -> do_announce(user, object, activity_id, local, public) end) do
result
end
end
defp do_announce(user, object, activity_id, local, public) do
with true <- is_announceable?(object, user, public), with true <- is_announceable?(object, user, public),
announce_data <- make_announce_data(user, object, activity_id, public), announce_data <- make_announce_data(user, object, activity_id, public),
{:ok, activity} <- insert(announce_data, local), {:ok, activity} <- insert(announce_data, local),
@ -398,16 +458,26 @@ def announce(
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity, object} {:ok, activity, object}
else else
error -> {:error, error} false -> {:error, false}
{:error, error} -> Repo.rollback(error)
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( def unannounce(
%User{} = actor, %User{} = actor,
%Object{} = object, %Object{} = object,
activity_id \\ nil, activity_id \\ nil,
local \\ true local \\ true
) do ) 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), with %Activity{} = announce_activity <- get_existing_announce(actor.ap_id, object),
unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id), unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
{:ok, unannounce_activity} <- insert(unannounce_data, local), {:ok, unannounce_activity} <- insert(unannounce_data, local),
@ -416,30 +486,61 @@ def unannounce(
{:ok, object} <- remove_announce_from_object(announce_activity, object) do {:ok, object} <- remove_announce_from_object(announce_activity, object) do
{:ok, unannounce_activity, object} {:ok, unannounce_activity, object}
else else
_e -> {:ok, object} nil -> {:ok, object}
{:error, error} -> Repo.rollback(error)
end end
end end
@spec follow(User.t(), User.t(), String.t() | nil, boolean()) ::
{: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
with {:ok, result} <-
Repo.transaction(fn -> do_follow(follower, followed, activity_id, local) end) do
result
end
end
defp do_follow(follower, followed, activity_id, local) do
with data <- make_follow_data(follower, followed, activity_id), with data <- make_follow_data(follower, followed, activity_id),
{:ok, activity} <- insert(data, local), {:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity), :ok <- maybe_federate(activity),
_ <- User.set_follow_state_cache(follower.ap_id, followed.ap_id, activity.data["state"]) do _ <- User.set_follow_state_cache(follower.ap_id, followed.ap_id, activity.data["state"]) do
{:ok, activity} {:ok, activity}
else
{:error, error} -> Repo.rollback(error)
end end
end end
@spec unfollow(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | nil | {:error, any()}
def unfollow(follower, followed, activity_id \\ nil, local \\ true) do def unfollow(follower, followed, activity_id \\ nil, local \\ true) do
with {:ok, result} <-
Repo.transaction(fn -> do_unfollow(follower, followed, activity_id, local) end) do
result
end
end
defp do_unfollow(follower, followed, activity_id, local) do
with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed), with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"), {:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id), unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
{:ok, activity} <- insert(unfollow_data, local), {:ok, activity} <- insert(unfollow_data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
else
nil -> nil
{:error, error} -> Repo.rollback(error)
end end
end end
def delete(%User{ap_id: ap_id, follower_address: follower_address} = user) do @spec delete(User.t() | Object.t(), keyword()) :: {:ok, User.t() | Object.t()} | {:error, any()}
def delete(entity, options \\ []) do
with {:ok, result} <- Repo.transaction(fn -> do_delete(entity, options) end) do
result
end
end
defp do_delete(%User{ap_id: ap_id, follower_address: follower_address} = user, _) do
with data <- %{ with data <- %{
"to" => [follower_address], "to" => [follower_address],
"type" => "Delete", "type" => "Delete",
@ -452,7 +553,7 @@ def delete(%User{ap_id: ap_id, follower_address: follower_address} = user) do
end end
end end
def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options \\ []) do defp do_delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options) do
local = Keyword.get(options, :local, true) local = Keyword.get(options, :local, true)
activity_id = Keyword.get(options, :activity_id, nil) activity_id = Keyword.get(options, :activity_id, nil)
actor = Keyword.get(options, :actor, actor) actor = Keyword.get(options, :actor, actor)
@ -477,11 +578,22 @@ def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options \\ [
{:ok, _actor} <- decrease_note_count_if_public(user, object), {:ok, _actor} <- decrease_note_count_if_public(user, object),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
else
{:error, error} ->
Repo.rollback(error)
end end
end end
@spec block(User.t(), User.t(), String.t() | nil, boolean) :: {:ok, Activity.t() | nil} @spec block(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | {:error, any()}
def block(blocker, blocked, activity_id \\ nil, local \\ true) do def block(blocker, blocked, activity_id \\ nil, local \\ true) do
with {:ok, result} <-
Repo.transaction(fn -> do_block(blocker, blocked, activity_id, local) end) do
result
end
end
defp do_block(blocker, blocked, activity_id, local) do
outgoing_blocks = Config.get([:activitypub, :outgoing_blocks]) outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
unfollow_blocked = Config.get([:activitypub, :unfollow_blocked]) unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
@ -496,20 +608,32 @@ def block(blocker, blocked, activity_id \\ nil, local \\ true) do
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
else else
_e -> {:ok, nil} {:error, error} -> Repo.rollback(error)
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 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), with %Activity{} = block_activity <- fetch_latest_block(blocker, blocked),
unblock_data <- make_unblock_data(blocker, blocked, block_activity, activity_id), unblock_data <- make_unblock_data(blocker, blocked, block_activity, activity_id),
{:ok, activity} <- insert(unblock_data, local), {:ok, activity} <- insert(unblock_data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
else
nil -> nil
{:error, error} -> Repo.rollback(error)
end end
end end
@spec flag(map()) :: {:ok, Activity.t()} | any @spec flag(map()) :: {:ok, Activity.t()} | {:error, any()}
def flag( def flag(
%{ %{
actor: actor, actor: actor,
@ -546,6 +670,7 @@ def flag(
end end
end end
@spec move(User.t(), User.t(), boolean()) :: {:ok, Activity.t()} | {:error, any()}
def move(%User{} = origin, %User{} = target, local \\ true) do def move(%User{} = origin, %User{} = target, local \\ true) do
params = %{ params = %{
"type" => "Move", "type" => "Move",
@ -571,7 +696,7 @@ def move(%User{} = origin, %User{} = target, local \\ true) do
end end
defp fetch_activities_for_context_query(context, opts) do defp fetch_activities_for_context_query(context, opts) do
public = [Pleroma.Constants.as_public()] public = [Constants.as_public()]
recipients = recipients =
if opts["user"], if opts["user"],
@ -616,10 +741,11 @@ def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
|> Repo.one() |> Repo.one()
end end
@spec fetch_public_activities(map(), Pagination.type()) :: [Activity.t()]
def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do
opts = Map.drop(opts, ["user"]) opts = Map.drop(opts, ["user"])
[Pleroma.Constants.as_public()] [Constants.as_public()]
|> fetch_activities_query(opts) |> fetch_activities_query(opts)
|> restrict_unlisted() |> restrict_unlisted()
|> Pagination.fetch_paginated(opts, pagination) |> Pagination.fetch_paginated(opts, pagination)
@ -791,9 +917,9 @@ defp user_activities_recipients(%{"godmode" => true}) do
defp user_activities_recipients(%{"reading_user" => reading_user}) do defp user_activities_recipients(%{"reading_user" => reading_user}) do
if reading_user do if reading_user do
[Pleroma.Constants.as_public()] ++ [reading_user.ap_id | User.following(reading_user)] [Constants.as_public()] ++ [reading_user.ap_id | User.following(reading_user)]
else else
[Pleroma.Constants.as_public()] [Constants.as_public()]
end end
end end
@ -1000,7 +1126,7 @@ defp restrict_unlisted(query) do
fragment( fragment(
"not (coalesce(?->'cc', '{}'::jsonb) \\?| ?)", "not (coalesce(?->'cc', '{}'::jsonb) \\?| ?)",
activity.data, activity.data,
^[Pleroma.Constants.as_public()] ^[Constants.as_public()]
) )
) )
end end
@ -1174,7 +1300,7 @@ def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
@doc """ @doc """
Fetch favorites activities of user with order by sort adds to favorites Fetch favorites activities of user with order by sort adds to favorites
""" """
@spec fetch_favourites(User.t(), map(), atom()) :: list(Activity.t()) @spec fetch_favourites(User.t(), map(), Pagination.type()) :: list(Activity.t())
def fetch_favourites(user, params \\ %{}, pagination \\ :keyset) do def fetch_favourites(user, params \\ %{}, pagination \\ :keyset) do
user.ap_id user.ap_id
|> Activity.Queries.by_actor() |> Activity.Queries.by_actor()
@ -1212,7 +1338,7 @@ def fetch_activities_bounded_query(query, recipients, recipients_with_public) do
where: where:
fragment("? && ?", activity.recipients, ^recipients) or fragment("? && ?", activity.recipients, ^recipients) or
(fragment("? && ?", activity.recipients, ^recipients_with_public) and (fragment("? && ?", activity.recipients, ^recipients_with_public) and
^Pleroma.Constants.as_public() in activity.recipients) ^Constants.as_public() in activity.recipients)
) )
end end
@ -1228,6 +1354,7 @@ def fetch_activities_bounded(
|> Enum.reverse() |> Enum.reverse()
end end
@spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()}
def upload(file, opts \\ []) do def upload(file, opts \\ []) do
with {:ok, data} <- Upload.store(file, opts) do with {:ok, data} <- Upload.store(file, opts) do
obj_data = obj_data =
@ -1325,8 +1452,7 @@ defp normalize_counter(counter) when is_integer(counter), do: counter
defp normalize_counter(_), do: 0 defp normalize_counter(_), do: 0
defp maybe_update_follow_information(data) do defp maybe_update_follow_information(data) do
with {:enabled, true} <- with {:enabled, true} <- {:enabled, Config.get([:instance, :external_user_synchronization])},
{:enabled, Pleroma.Config.get([:instance, :external_user_synchronization])},
{:ok, info} <- fetch_follow_information_for_user(data) do {:ok, info} <- fetch_follow_information_for_user(data) do
info = Map.merge(data[:info] || %{}, info) info = Map.merge(data[:info] || %{}, info)
Map.put(data, :info, info) Map.put(data, :info, info)

View file

@ -110,3 +110,4 @@
"web_push_encryption": {:hex, :web_push_encryption, "0.2.3", "a0ceab85a805a30852f143d22d71c434046fbdbafbc7292e7887cec500826a80", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "9315c8f37c108835cf3f8e9157d7a9b8f420a34f402d1b1620a31aed5b93ecdf"}, "web_push_encryption": {:hex, :web_push_encryption, "0.2.3", "a0ceab85a805a30852f143d22d71c434046fbdbafbc7292e7887cec500826a80", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "9315c8f37c108835cf3f8e9157d7a9b8f420a34f402d1b1620a31aed5b93ecdf"},
"websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []}, "websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []},
} }

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Builders.ActivityBuilder alias Pleroma.Builders.ActivityBuilder
alias Pleroma.Config
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
@ -15,6 +16,7 @@ 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 Pleroma.Factory import Pleroma.Factory
import Tesla.Mock import Tesla.Mock
@ -224,7 +226,7 @@ test "it fetches the appropriate tag-restricted posts" do
describe "insertion" do describe "insertion" do
test "drops activities beyond a certain limit" do test "drops activities beyond a certain limit" do
limit = Pleroma.Config.get([:instance, :remote_limit]) limit = Config.get([:instance, :remote_limit])
random_text = random_text =
:crypto.strong_rand_bytes(limit + 1) :crypto.strong_rand_bytes(limit + 1)
@ -385,6 +387,27 @@ test "can be fetched into a timeline" do
end end
describe "create activities" do describe "create activities" do
test "it reverts create" do
user = insert(:user)
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} =
ActivityPub.create(%{
to: ["user1", "user2"],
actor: user,
context: "",
object: %{
"to" => ["user1", "user2"],
"type" => "Note",
"content" => "testing"
}
})
end
assert Repo.aggregate(Activity, :count, :id) == 0
assert Repo.aggregate(Object, :count, :id) == 0
end
test "removes doubled 'to' recipients" do test "removes doubled 'to' recipients" do
user = insert(:user) user = insert(:user)
@ -852,8 +875,8 @@ test "returns reblogs for users for whom reblogs have not been muted" do
end end
describe "react to an object" do describe "react to an object" do
test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
Pleroma.Config.put([:instance, :federating], true) Config.put([:instance, :federating], true)
user = insert(:user) user = insert(:user)
reactor = insert(:user) reactor = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"}) {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
@ -861,7 +884,7 @@ test "returns reblogs for users for whom reblogs have not been muted" do
{:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥") {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
assert called(Pleroma.Web.Federator.publish(reaction_activity)) assert called(Federator.publish(reaction_activity))
end end
test "adds an emoji reaction activity to the db" do test "adds an emoji reaction activity to the db" do
@ -899,11 +922,26 @@ test "adds an emoji reaction activity to the db" do
["", [third_user.ap_id]] ["", [third_user.ap_id]]
] ]
end 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 end
describe "unreacting to an object" do describe "unreacting to an object" do
test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
Pleroma.Config.put([:instance, :federating], true) Config.put([:instance, :federating], true)
user = insert(:user) user = insert(:user)
reactor = insert(:user) reactor = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"}) {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
@ -911,12 +949,12 @@ test "adds an emoji reaction activity to the db" do
{:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥") {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
assert called(Pleroma.Web.Federator.publish(reaction_activity)) assert called(Federator.publish(reaction_activity))
{:ok, unreaction_activity, _object} = {:ok, unreaction_activity, _object} =
ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"]) ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
assert called(Pleroma.Web.Federator.publish(unreaction_activity)) assert called(Federator.publish(unreaction_activity))
end end
test "adds an undo activity to the db" do test "adds an undo activity to the db" do
@ -937,18 +975,36 @@ test "adds an undo activity to the db" do
assert object.data["reaction_count"] == 0 assert object.data["reaction_count"] == 0
assert object.data["reactions"] == [] assert object.data["reactions"] == []
end 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 end
describe "like an object" do describe "like an object" do
test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
Pleroma.Config.put([:instance, :federating], true) Config.put([:instance, :federating], true)
note_activity = insert(:note_activity) note_activity = insert(:note_activity)
assert object_activity = Object.normalize(note_activity) assert object_activity = Object.normalize(note_activity)
user = insert(:user) user = insert(:user)
{:ok, like_activity, _object} = ActivityPub.like(user, object_activity) {:ok, like_activity, _object} = ActivityPub.like(user, object_activity)
assert called(Pleroma.Web.Federator.publish(like_activity)) assert called(Federator.publish(like_activity))
end end
test "returns exist activity if object already liked" do test "returns exist activity if object already liked" do
@ -963,6 +1019,19 @@ test "returns exist activity if object already liked" do
assert like_activity == like_activity_exist assert like_activity == like_activity_exist
end end
test "reverts like activity on error" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.like(user, object)
end
assert Repo.aggregate(Activity, :count, :id) == 1
assert Repo.get(Object, object.id) == object
end
test "adds a like activity to the db" do test "adds a like activity to the db" do
note_activity = insert(:note_activity) note_activity = insert(:note_activity)
assert object = Object.normalize(note_activity) assert object = Object.normalize(note_activity)
@ -993,15 +1062,15 @@ test "adds a like activity to the db" do
end end
describe "unliking" do describe "unliking" do
test_with_mock "sends an activity to federation", Pleroma.Web.Federator, [:passthrough], [] do test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
Pleroma.Config.put([:instance, :federating], true) Config.put([:instance, :federating], true)
note_activity = insert(:note_activity) note_activity = insert(:note_activity)
object = Object.normalize(note_activity) object = Object.normalize(note_activity)
user = insert(:user) user = insert(:user)
{:ok, object} = ActivityPub.unlike(user, object) {:ok, object} = ActivityPub.unlike(user, object)
refute called(Pleroma.Web.Federator.publish()) refute called(Federator.publish())
{:ok, _like_activity, object} = ActivityPub.like(user, object) {:ok, _like_activity, object} = ActivityPub.like(user, object)
assert object.data["like_count"] == 1 assert object.data["like_count"] == 1
@ -1009,7 +1078,24 @@ test "adds a like activity to the db" do
{:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object) {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0 assert object.data["like_count"] == 0
assert called(Pleroma.Web.Federator.publish(unlike_activity)) assert called(Federator.publish(unlike_activity))
end
test "reverts unliking on error" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
{:ok, like_activity, object} = ActivityPub.like(user, object)
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 end
test "unliking a previously liked object" do test "unliking a previously liked object" do
@ -1051,6 +1137,21 @@ test "adds an announce activity to the db" do
assert announce_activity.data["actor"] == user.ap_id assert announce_activity.data["actor"] == user.ap_id
assert announce_activity.data["context"] == object.data["context"] assert announce_activity.data["context"] == object.data["context"]
end end
test "reverts annouce from object on error" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.announce(user, object)
end
reloaded_object = Object.get_by_ap_id(object.data["id"])
assert reloaded_object == object
refute reloaded_object.data["announcement_count"]
refute reloaded_object.data["announcements"]
end
end end
describe "announcing a private object" do describe "announcing a private object" do
@ -1093,8 +1194,8 @@ test "unannouncing a previously announced object" do
user = insert(:user) user = insert(:user)
# Unannouncing an object that is not announced does nothing # Unannouncing an object that is not announced does nothing
# {:ok, object} = ActivityPub.unannounce(user, object) {:ok, object} = ActivityPub.unannounce(user, object)
# assert object.data["announcement_count"] == 0 refute object.data["announcement_count"]
{:ok, announce_activity, object} = ActivityPub.announce(user, object) {:ok, announce_activity, object} = ActivityPub.announce(user, object)
assert object.data["announcement_count"] == 1 assert object.data["announcement_count"] == 1
@ -1114,6 +1215,22 @@ test "unannouncing a previously announced object" do
assert Activity.get_by_id(announce_activity.id) == nil assert Activity.get_by_id(announce_activity.id) == nil
end 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 end
describe "uploading files" do describe "uploading files" do
@ -1148,6 +1265,35 @@ test "fetches the latest Follow activity" do
end end
describe "following / unfollowing" do describe "following / unfollowing" do
test "it reverts follow activity" do
follower = insert(:user)
followed = insert(:user)
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.follow(follower, followed)
end
assert Repo.aggregate(Activity, :count, :id) == 0
assert Repo.aggregate(Object, :count, :id) == 0
end
test "it reverts unfollow activity" do
follower = insert(:user)
followed = insert(:user)
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.unfollow(follower, followed)
end
activity = Activity.get_by_id(follow_activity.id)
assert activity.data["type"] == "Follow"
assert activity.data["actor"] == follower.ap_id
assert activity.data["object"] == followed.ap_id
end
test "creates a follow activity" do test "creates a follow activity" do
follower = insert(:user) follower = insert(:user)
followed = insert(:user) followed = insert(:user)
@ -1194,6 +1340,17 @@ test "creates an undo activity for a pending follow request" do
end end
describe "blocking / unblocking" do describe "blocking / unblocking" do
test "reverts block activity on error" do
[blocker, blocked] = insert_list(2, :user)
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.block(blocker, blocked)
end
assert Repo.aggregate(Activity, :count, :id) == 0
assert Repo.aggregate(Object, :count, :id) == 0
end
test "creates a block activity" do test "creates a block activity" do
blocker = insert(:user) blocker = insert(:user)
blocked = insert(:user) blocked = insert(:user)
@ -1205,6 +1362,21 @@ test "creates a block activity" do
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 test "creates an undo activity for the last block" do
blocker = insert(:user) blocker = insert(:user)
blocked = insert(:user) blocked = insert(:user)
@ -1226,6 +1398,19 @@ test "creates an undo activity for the last block" do
describe "deletion" do describe "deletion" do
clear_config([:instance, :rewrite_policy]) clear_config([:instance, :rewrite_policy])
test "it reverts deletion on error" do
note = insert(:note_activity)
object = Object.normalize(note)
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.delete(object)
end
assert Repo.aggregate(Activity, :count, :id) == 1
assert Repo.get(Object, object.id) == object
assert Activity.get_by_id(note.id) == note
end
test "it creates a delete activity and deletes the original object" do test "it creates a delete activity and deletes the original object" do
note = insert(:note_activity) note = insert(:note_activity)
object = Object.normalize(note) object = Object.normalize(note)
@ -1419,7 +1604,7 @@ test "it creates an update activity with the new user data" do
end end
test "returned pinned statuses" do test "returned pinned statuses" do
Pleroma.Config.put([:instance, :max_pinned_statuses], 3) Config.put([:instance, :max_pinned_statuses], 3)
user = insert(:user) user = insert(:user)
{:ok, activity_one} = CommonAPI.post(user, %{"status" => "HI!!!"}) {:ok, activity_one} = CommonAPI.post(user, %{"status" => "HI!!!"})