Merge branch 'akkoma-fixes-1014-1018' into 'develop'

Status visibility checks for post interactions, stop leaking internal Activity representation (Akkoma PR 1014 and 1018)

Closes #3383

See merge request pleroma/pleroma!4400
This commit is contained in:
lain 2025-12-23 13:55:18 +00:00
commit 2f48544937
27 changed files with 1259 additions and 71 deletions

View file

@ -0,0 +1 @@
AP C2S: Reject interactions with statuses not visible to Actor

View file

@ -0,0 +1 @@
MastodonAPI: Reject interactions with statuses not visible to user

View file

@ -0,0 +1 @@
ObjectView: Do not leak unsanitized internal representation of non-Create/non-Undo Activities on fetches

View file

@ -21,7 +21,8 @@ defmodule Pleroma.Constants do
"pleroma_internal", "pleroma_internal",
"generator", "generator",
"rules", "rules",
"language" "language",
"voters"
] ]
) )

View file

@ -126,7 +126,7 @@ defmodule Pleroma.Object do
Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}") Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
end end
def normalize(_, options \\ [fetch: false, id_only: false]) def normalize(_, options \\ [fetch: false])
# If we pass an Activity to Object.normalize(), we can try to use the preloaded object. # If we pass an Activity to Object.normalize(), we can try to use the preloaded object.
# Use this whenever possible, especially when walking graphs in an O(N) loop! # Use this whenever possible, especially when walking graphs in an O(N) loop!
@ -155,9 +155,6 @@ defmodule Pleroma.Object do
def normalize(ap_id, options) when is_binary(ap_id) do def normalize(ap_id, options) when is_binary(ap_id) do
cond do cond do
Keyword.get(options, :id_only) ->
ap_id
Keyword.get(options, :fetch) -> Keyword.get(options, :fetch) ->
case Fetcher.fetch_object_from_id(ap_id, options) do case Fetcher.fetch_object_from_id(ap_id, options) do
{:ok, object} -> object {:ok, object} -> object

View file

@ -482,6 +482,42 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
{:ok, activity} {:ok, activity}
end end
# We currently lack a Flag ObjectValidator since both CommonAPI and Transmogrifier
# both send it straight to ActivityPub.flag and C2S currently has to go through
# the normal pipeline which requires an ObjectValidator.
# TODO: Add a Flag Activity ObjectValidator
defp check_allowed_action(_, %{"type" => "Flag"}) do
{:error, "Flag activities aren't currently supported in C2S"}
end
# It would respond with 201 and silently fail with:
# Could not decode featured collection at fetch #{user.ap_id} \
# {:error, "Trying to fetch local resource"}
defp check_allowed_action(%{ap_id: ap_id}, %{"type" => "Update", "object" => %{"id" => ap_id}}),
do: {:error, "Updating profile is not currently supported in C2S"}
defp check_allowed_action(_, activity), do: {:ok, activity}
defp validate_visibility(%User{} = user, %{"type" => type, "object" => object} = activity) do
with {_, %Object{} = normalized_object} <-
{:normalize, Object.normalize(object, fetch: false)},
{_, true} <- {:visibility, Visibility.visible_for_user?(normalized_object, user)} do
{:ok, activity}
else
{:normalize, _} ->
if type in ["Create", "Listen"] do
# Creating new object via C2S; user is local and authenticated
# via the :authenticate Plug pipeline.
{:ok, activity}
else
{:error, "No such object found"}
end
{:visibility, _} ->
{:forbidden, "You can't interact with this object"}
end
end
def update_outbox( def update_outbox(
%{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn, %{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn,
%{"nickname" => nickname} = params %{"nickname" => nickname} = params
@ -493,6 +529,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|> Map.put("actor", actor) |> Map.put("actor", actor)
with {:ok, params} <- fix_user_message(user, params), with {:ok, params} <- fix_user_message(user, params),
{:ok, params} <- check_allowed_action(user, params),
{:ok, params} <- validate_visibility(user, params),
{:ok, activity, _} <- Pipeline.common_pipeline(params, local: true), {:ok, activity, _} <- Pipeline.common_pipeline(params, local: true),
%Activity{data: activity_data} <- Activity.normalize(activity) do %Activity{data: activity_data} <- Activity.normalize(activity) do
conn conn

View file

@ -873,6 +873,27 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, data} {:ok, data}
end end
def prepare_outgoing(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data)
when objtype in Pleroma.Constants.actor_types() do
object =
object
|> maybe_fix_user_object()
|> strip_internal_fields()
data =
data
|> Map.put("object", object)
|> strip_internal_fields()
|> Map.merge(Utils.make_json_ld_header(object))
|> Map.delete("bcc")
{:ok, data}
end
def prepare_outgoing(%{"type" => "Update", "object" => %{}} = data) do
raise "Requested to serve an Update for non-updateable object type: #{inspect(data)}"
end
def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
object = object =
object_id object_id

View file

@ -15,26 +15,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
Map.merge(base, additional) Map.merge(base, additional)
end end
def render("object.json", %{object: %Activity{data: %{"type" => activity_type}} = activity})
when activity_type in ["Create", "Listen"] do
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header(activity.data)
object = Object.normalize(activity, fetch: false)
additional =
Transmogrifier.prepare_object(activity.data)
|> Map.put("object", Transmogrifier.prepare_object(object.data))
Map.merge(base, additional)
end
def render("object.json", %{object: %Activity{} = activity}) do def render("object.json", %{object: %Activity{} = activity}) do
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header(activity.data) {:ok, ap_data} = Transmogrifier.prepare_outgoing(activity.data)
object_id = Object.normalize(activity, id_only: true) ap_data
additional =
Transmogrifier.prepare_object(activity.data)
|> Map.put("object", object_id)
Map.merge(base, additional)
end end
end end

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
alias OpenApiSpex.Schema alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Account alias Pleroma.Web.ApiSpec.Schemas.Account
alias Pleroma.Web.ApiSpec.Schemas.ApiError alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.ApiNotFoundError
alias Pleroma.Web.ApiSpec.Schemas.FlakeID alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.Status alias Pleroma.Web.ApiSpec.Schemas.Status
@ -35,7 +36,8 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
security: [%{"oAuth" => ["read:statuses"]}], security: [%{"oAuth" => ["read:statuses"]}],
operationId: "EmojiReactionController.index", operationId: "EmojiReactionController.index",
responses: %{ responses: %{
200 => array_of_reactions_response() 200 => array_of_reactions_response(),
404 => Operation.response("Access denied", "application/json", ApiNotFoundError)
} }
} }
end end
@ -54,7 +56,8 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
operationId: "EmojiReactionController.create", operationId: "EmojiReactionController.create",
responses: %{ responses: %{
200 => Operation.response("Status", "application/json", Status), 200 => Operation.response("Status", "application/json", Status),
400 => Operation.response("Bad Request", "application/json", ApiError) 400 => Operation.response("Bad Request", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiNotFoundError)
} }
} }
end end

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do
alias OpenApiSpex.Schema alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers alias Pleroma.Web.ApiSpec.Helpers
alias Pleroma.Web.ApiSpec.Schemas.ApiError alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.ApiNotFoundError
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
def open_api_operation(action) do def open_api_operation(action) do
@ -24,7 +25,8 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do
requestBody: Helpers.request_body("Parameters", create_request(), required: true), requestBody: Helpers.request_body("Parameters", create_request(), required: true),
responses: %{ responses: %{
200 => Operation.response("Report", "application/json", create_response()), 200 => Operation.response("Report", "application/json", create_response()),
400 => Operation.response("Report", "application/json", ApiError) 400 => Operation.response("Report", "application/json", ApiError),
404 => Operation.response("Report", "application/json", ApiNotFoundError)
} }
} }
end end

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
alias Pleroma.Web.ApiSpec.AccountOperation alias Pleroma.Web.ApiSpec.AccountOperation
alias Pleroma.Web.ApiSpec.Schemas.Account alias Pleroma.Web.ApiSpec.Schemas.Account
alias Pleroma.Web.ApiSpec.Schemas.ApiError alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.ApiNotFoundError
alias Pleroma.Web.ApiSpec.Schemas.Attachment alias Pleroma.Web.ApiSpec.Schemas.Attachment
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
alias Pleroma.Web.ApiSpec.Schemas.Emoji alias Pleroma.Web.ApiSpec.Schemas.Emoji
@ -177,6 +178,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
parameters: [id_param()], parameters: [id_param()],
responses: %{ responses: %{
200 => status_response(), 200 => status_response(),
400 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError) 404 => Operation.response("Not Found", "application/json", ApiError)
} }
} }
@ -242,14 +244,19 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
"error" => "You have already pinned the maximum number of statuses" "error" => "You have already pinned the maximum number of statuses"
} }
}), }),
404 => 404 => Operation.response("Not found", "application/json", ApiNotFoundError),
Operation.response("Not found", "application/json", %Schema{ 422 =>
allOf: [ApiError], Operation.response(
title: "Unprocessable Entity", "Unprocessable Entity",
example: %{ "application/json",
"error" => "Record not found" %Schema{
allOf: [ApiError],
title: "Unprocessable Entity",
example: %{
"error" => "Someone else's status cannot be unpinned"
}
} }
}) )
} }
} }
end end
@ -275,7 +282,8 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
} }
}), }),
responses: %{ responses: %{
200 => status_response() 200 => status_response(),
404 => Operation.response("Not found", "application/json", ApiNotFoundError)
} }
} }
end end
@ -289,7 +297,8 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
operationId: "StatusController.unbookmark", operationId: "StatusController.unbookmark",
parameters: [id_param()], parameters: [id_param()],
responses: %{ responses: %{
200 => status_response() 200 => status_response(),
404 => Operation.response("Not found", "application/json", ApiNotFoundError)
} }
} }
end end
@ -324,7 +333,8 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
], ],
responses: %{ responses: %{
200 => status_response(), 200 => status_response(),
400 => Operation.response("Error", "application/json", ApiError) 400 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Not found", "application/json", ApiNotFoundError)
} }
} }
end end
@ -340,7 +350,8 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
parameters: [id_param()], parameters: [id_param()],
responses: %{ responses: %{
200 => status_response(), 200 => status_response(),
400 => Operation.response("Error", "application/json", ApiError) 400 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiNotFoundError)
} }
} }
end end

View file

@ -0,0 +1,19 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.ApiNotFoundError do
alias OpenApiSpex.Schema
require OpenApiSpex
OpenApiSpex.schema(%{
title: "Not Found",
description: "Response schema for 404 API errors",
type: :object,
properties: %{error: %Schema{type: :string}},
example: %{
"error" => "Record not found"
}
})
end

View file

@ -269,6 +269,7 @@ defmodule Pleroma.Web.CommonAPI do
defp favorite_helper(user, id) do defp favorite_helper(user, id) do
with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)}, with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
{_, true} <- {:visibility_error, activity_visible_to_actor(object, user)},
{_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)}, {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
{_, {:ok, %Activity{} = activity, _meta}} <- {_, {:ok, %Activity{} = activity, _meta}} <-
{:common_pipeline, {:common_pipeline,
@ -278,6 +279,9 @@ defmodule Pleroma.Web.CommonAPI do
{:find_object, _} -> {:find_object, _} ->
{:error, :not_found} {:error, :not_found}
{:visibility_error, _} ->
{:error, :not_found}
{:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e -> {:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->
if {:object, {"already liked by this actor", []}} in changeset.errors do if {:object, {"already liked by this actor", []}} in changeset.errors do
{:ok, :already_liked} {:ok, :already_liked}
@ -296,6 +300,7 @@ defmodule Pleroma.Web.CommonAPI do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <- with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)}, {:find_activity, Activity.get_by_id(id)},
%Object{} = note <- Object.normalize(activity, fetch: false), %Object{} = note <- Object.normalize(activity, fetch: false),
{_, true} <- {:visibility_error, activity_visible_to_actor(note, user)},
%Activity{} = like <- Utils.get_existing_like(user.ap_id, note), %Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
{_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(like)}, {_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(like)},
{:ok, undo, _} <- Builder.undo(user, like), {:ok, undo, _} <- Builder.undo(user, like),
@ -303,6 +308,7 @@ defmodule Pleroma.Web.CommonAPI do
{:ok, activity} {:ok, activity}
else else
{:find_activity, _} -> {:error, :not_found} {:find_activity, _} -> {:error, :not_found}
{:visibility_error, _} -> {:error, :not_found}
_ -> {:error, dgettext("errors", "Could not unfavorite")} _ -> {:error, dgettext("errors", "Could not unfavorite")}
end end
end end
@ -311,11 +317,15 @@ defmodule Pleroma.Web.CommonAPI do
{:ok, Activity.t()} | {:error, String.t()} {:ok, Activity.t()} | {:error, String.t()}
def react_with_emoji(id, user, emoji) do def react_with_emoji(id, user, emoji) do
with %Activity{} = activity <- Activity.get_by_id(id), with %Activity{} = activity <- Activity.get_by_id(id),
{_, true} <- {:visibility_error, activity_visible_to_actor(activity, user)},
object <- Object.normalize(activity, fetch: false), object <- Object.normalize(activity, fetch: false),
{:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji), {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
{:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
{:ok, activity} {:ok, activity}
else else
{:visibility_error, _} ->
{:error, :not_found}
_ -> _ ->
{:error, dgettext("errors", "Could not add reaction emoji")} {:error, dgettext("errors", "Could not add reaction emoji")}
end end
@ -506,6 +516,7 @@ defmodule Pleroma.Web.CommonAPI do
@spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors() @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
def pin(id, %User{} = user) do def pin(id, %User{} = user) do
with %Activity{} = activity <- create_activity_by_id(id), with %Activity{} = activity <- create_activity_by_id(id),
true <- activity_visible_to_actor(activity, user),
true <- activity_belongs_to_actor(activity, user.ap_id), true <- activity_belongs_to_actor(activity, user.ap_id),
true <- object_type_is_allowed_for_pin(activity.object), true <- object_type_is_allowed_for_pin(activity.object),
true <- activity_is_public(activity), true <- activity_is_public(activity),
@ -531,6 +542,14 @@ defmodule Pleroma.Web.CommonAPI do
defp activity_belongs_to_actor(%{actor: actor}, actor), do: true defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error} defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
defp activity_visible_to_actor(activity, %User{} = user) do
if Visibility.visible_for_user?(activity, user) do
true
else
{:error, :visibility_error}
end
end
defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
with false <- type in ["Note", "Article", "Question"] do with false <- type in ["Note", "Article", "Question"] do
{:error, :not_allowed} {:error, :not_allowed}
@ -539,13 +558,18 @@ defmodule Pleroma.Web.CommonAPI do
defp activity_is_public(activity) do defp activity_is_public(activity) do
with false <- Visibility.public?(activity) do with false <- Visibility.public?(activity) do
{:error, :visibility_error} {:error, :non_public_error}
end end
end end
@spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors() @spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
def unpin(id, user) do def unpin(id, user) do
# Order of visibility/belonging matters for MastoAPI responses.
# post not visible -> 404
# post visible, not owned -> 422
with %Activity{} = activity <- create_activity_by_id(id), with %Activity{} = activity <- create_activity_by_id(id),
true <- activity_visible_to_actor(activity, user),
true <- activity_belongs_to_actor(activity, user.ap_id),
{:ok, unpin_data, _} <- Builder.unpin(user, activity.object), {:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
{:ok, _unpin, _} <- {:ok, _unpin, _} <-
Pipeline.common_pipeline(unpin_data, Pipeline.common_pipeline(unpin_data,
@ -562,7 +586,8 @@ defmodule Pleroma.Web.CommonAPI do
def add_mute(activity, user, params \\ %{}) do def add_mute(activity, user, params \\ %{}) do
expires_in = Map.get(params, :expires_in, 0) expires_in = Map.get(params, :expires_in, 0)
with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]), with true <- activity_visible_to_actor(activity, user),
{:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
_ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do _ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
if expires_in > 0 do if expires_in > 0 do
Pleroma.Workers.MuteExpireWorker.new( Pleroma.Workers.MuteExpireWorker.new(
@ -574,14 +599,21 @@ defmodule Pleroma.Web.CommonAPI do
{:ok, activity} {:ok, activity}
else else
{:error, :visibility_error} -> {:error, :visibility_error}
{:error, _} -> {:error, dgettext("errors", "conversation is already muted")} {:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
end end
end end
@spec remove_mute(Activity.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()} @spec remove_mute(Activity.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
def remove_mute(%Activity{} = activity, %User{} = user) do def remove_mute(%Activity{} = activity, %User{} = user) do
ThreadMute.remove_mute(user.id, activity.data["context"]) case activity_visible_to_actor(activity, user) do
{:ok, activity} true ->
ThreadMute.remove_mute(user.id, activity.data["context"])
{:ok, activity}
error ->
error
end
end end
@spec remove_mute(String.t(), String.t()) :: {:ok, Activity.t()} | {:error, any()} @spec remove_mute(String.t(), String.t()) :: {:ok, Activity.t()} | {:error, any()}
@ -612,6 +644,7 @@ defmodule Pleroma.Web.CommonAPI do
with {:ok, account} <- get_reported_account(data.account_id), with {:ok, account} <- get_reported_account(data.account_id),
{:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]), {:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
{:ok, statuses} <- get_report_statuses(account, data), {:ok, statuses} <- get_report_statuses(account, data),
true <- check_statuses_visibility(user, statuses),
rules <- get_report_rules(Map.get(data, :rule_ids, nil)) do rules <- get_report_rules(Map.get(data, :rule_ids, nil)) do
ActivityPub.flag(%{ ActivityPub.flag(%{
context: Utils.generate_context_id(), context: Utils.generate_context_id(),
@ -622,9 +655,27 @@ defmodule Pleroma.Web.CommonAPI do
forward: Map.get(data, :forward, false), forward: Map.get(data, :forward, false),
rules: rules rules: rules
}) })
else
false ->
{:error, :visibility_error}
error ->
error
end end
end end
defp check_statuses_visibility(user, statuses) when is_list(statuses) do
visibility = for status <- statuses, do: Visibility.visible_for_user?(status, user)
case Enum.all?(visibility) do
true -> true
_ -> false
end
end
# There are no statuses associated with the report, pass!
defp check_statuses_visibility(_, status) when status == nil, do: true
defp get_reported_account(account_id) do defp get_reported_account(account_id) do
case User.get_cached_by_id(account_id) do case User.get_cached_by_id(account_id) do
%User{} = account -> {:ok, account} %User{} = account -> {:ok, account}

View file

@ -136,22 +136,34 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft
defp in_reply_to(%{params: %{in_reply_to_status_id: :deleted}} = draft) do defp in_reply_to(%{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do
add_error(draft, dgettext("errors", "Cannot reply to a deleted status")) # If a post was deleted all its activities (except the newly added Delete) are purged too,
end # thus lookup by Create db ID will yield nil just as if it never existed in the first place.
#
# We allow replying to Announce here, due to a Pleroma-FE quirk where if presented with
# an Announce id it will render it as if it was just the normal referenced post, but
# use the Announce id for replies in the in_reply_to_id key of a POST request to
# /api/v1/statuses, or as an :id in /api/v1/statuses/:id/*.
# TODO: Fix this quirk in FE and remove here and other affected places
with %Activity{} = activity <- Activity.get_by_id(id),
true <- Visibility.visible_for_user?(activity, draft.user),
{_, type} when type in ["Create", "Announce"] <- {:type, activity.data["type"]} do
%__MODULE__{draft | in_reply_to: activity}
else
nil ->
add_error(draft, dgettext("errors", "Cannot reply to a deleted status"))
defp in_reply_to(%{params: %{in_reply_to_status_id: id} = params} = draft) when is_binary(id) do false ->
activity = Activity.get_by_id(id) add_error(draft, dgettext("errors", "Record not found"))
params = {:type, type} ->
if is_nil(activity) do add_error(
# Deleted activities are returned as nil draft,
Map.put(params, :in_reply_to_status_id, :deleted) dgettext("errors", "Can only reply to posts, not %{type} activities",
else type: inspect(type)
Map.put(params, :in_reply_to_status_id, activity) )
end )
end
in_reply_to(%{draft | params: params})
end end
defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do

View file

@ -16,6 +16,12 @@ defmodule Pleroma.Web.MastodonAPI.ReportController do
def create(%{assigns: %{user: user}, body_params: params} = conn, _) do def create(%{assigns: %{user: user}, body_params: params} = conn, _) do
with {:ok, activity} <- Pleroma.Web.CommonAPI.report(user, params) do with {:ok, activity} <- Pleroma.Web.CommonAPI.report(user, params) do
render(conn, "show.json", activity: activity) render(conn, "show.json", activity: activity)
else
{:error, :visibility_error} ->
{:error, :not_found, "Record not found"}
error ->
error
end end
end end
end end

View file

@ -319,6 +319,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
@doc "DELETE /api/v1/statuses/:id" @doc "DELETE /api/v1/statuses/:id"
def delete(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do def delete(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id), with %Activity{} = activity <- Activity.get_by_id_with_object(id),
# CommonAPI already checks whether user is allowed to delete
{:ok, %Activity{}} <- CommonAPI.delete(id, user) do {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
try_render(conn, "show.json", try_render(conn, "show.json",
activity: activity, activity: activity,
@ -340,6 +341,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
_ _
) do ) do
with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params), with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
# CommonAPI already checks whether user is allowed to reblog
%Activity{} = announce <- Activity.normalize(announce.data) do %Activity{} = announce <- Activity.normalize(announce.data) do
try_render(conn, "show.json", %{activity: announce, for: user, as: :activity}) try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
end end
@ -364,6 +366,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
_ _
) do ) do
with {:ok, _fav} <- CommonAPI.favorite(activity_id, user), with {:ok, _fav} <- CommonAPI.favorite(activity_id, user),
# CommonAPI already checks whether user is allowed to reblog
%Activity{} = activity <- Activity.get_by_id(activity_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
@ -390,6 +393,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity) try_render(conn, "show.json", activity: activity, for: user, as: :activity)
else else
# Order matters, if status is not owned by user and is not visible to user
# return 404 just like other endpoints
{:error, :pinned_statuses_limit_reached} -> {:error, :pinned_statuses_limit_reached} ->
{:error, "You have already pinned the maximum number of statuses"} {:error, "You have already pinned the maximum number of statuses"}
@ -397,6 +402,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
{:error, :unprocessable_entity, "Someone else's status cannot be pinned"} {:error, :unprocessable_entity, "Someone else's status cannot be pinned"}
{:error, :visibility_error} -> {:error, :visibility_error} ->
{:error, :not_found, "Record not found"}
{:error, :non_public_error} ->
{:error, :unprocessable_entity, "Non-public status cannot be pinned"} {:error, :unprocessable_entity, "Non-public status cannot be pinned"}
error -> error ->
@ -410,8 +418,20 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
conn, conn,
_ _
) do ) do
# CommonAPI already checks whether user can unpin
with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity) try_render(conn, "show.json", activity: activity, for: user, as: :activity)
else
# Order matters, if status is not owned by user and is not visible to user
# return 404 just like other endpoints
{:error, :visibility_error} ->
{:error, :not_found, "Record not found"}
{:error, :ownership_error} ->
{:error, :unprocessable_entity, "Someone else's status cannot be unpinned"}
error ->
error
end end
end end
@ -434,6 +454,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
), ),
{:ok, _bookmark} <- Bookmark.create(user.id, activity.id, folder_id) do {:ok, _bookmark} <- Bookmark.create(user.id, activity.id, folder_id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity) try_render(conn, "show.json", activity: activity, for: user, as: :activity)
else
false ->
{:error, :not_found, "Record not found"}
error ->
error
end end
end end
@ -447,6 +473,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
true <- Visibility.visible_for_user?(activity, user), true <- Visibility.visible_for_user?(activity, user),
{:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do {:ok, _bookmark} <- Bookmark.destroy(user.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)
else
false ->
{:error, :not_found, "Record not found"}
error ->
error
end end
end end
@ -459,8 +491,15 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
_ _
) do ) do
with %Activity{} = activity <- Activity.get_by_id(id), with %Activity{} = activity <- Activity.get_by_id(id),
# CommonAPI already checks whether user is allowed to mute
{:ok, activity} <- CommonAPI.add_mute(activity, user, params) do {:ok, activity} <- CommonAPI.add_mute(activity, user, params) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity) try_render(conn, "show.json", activity: activity, for: user, as: :activity)
else
{:error, :visibility_error} ->
{:error, :not_found, "Record not found"}
error ->
error
end end
end end
@ -473,8 +512,15 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
_ _
) do ) do
with %Activity{} = activity <- Activity.get_by_id(id), with %Activity{} = activity <- Activity.get_by_id(id),
# CommonAPI already checks whether user is allowed to unmute
{:ok, activity} <- CommonAPI.remove_mute(activity, user) do {:ok, activity} <- CommonAPI.remove_mute(activity, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity) try_render(conn, "show.json", activity: activity, for: user, as: :activity)
else
{:error, :visibility_error} ->
{:error, :not_found, "Record not found"}
error ->
error
end end
end end

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.OAuthScopesPlug
@ -28,6 +29,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
def index(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do def index(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do
with true <- Pleroma.Config.get([:instance, :show_reactions]), with true <- Pleroma.Config.get([:instance, :show_reactions]),
%Activity{} = activity <- Activity.get_by_id_with_object(activity_id), %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
{_, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
%Object{} = object <- Object.normalize(activity, fetch: false), %Object{} = object <- Object.normalize(activity, fetch: false),
reactions <- Object.get_emoji_reactions(object) do reactions <- Object.get_emoji_reactions(object) do
reactions = reactions =
@ -37,6 +39,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
render(conn, "index.json", emoji_reactions: reactions, user: user) render(conn, "index.json", emoji_reactions: reactions, user: user)
else else
{:visible, _} -> {:error, :not_found}
_e -> json(conn, []) _e -> json(conn, [])
end end
end end
@ -76,6 +79,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
|> Pleroma.Emoji.fully_qualify_emoji() |> Pleroma.Emoji.fully_qualify_emoji()
|> Pleroma.Emoji.maybe_quote() |> Pleroma.Emoji.maybe_quote()
# CommonAPI checks if allowed to react
with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji) do with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji) do
activity = Activity.get_by_id(activity_id) activity = Activity.get_by_id(activity_id)
@ -91,6 +95,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
|> Pleroma.Emoji.fully_qualify_emoji() |> Pleroma.Emoji.fully_qualify_emoji()
|> Pleroma.Emoji.maybe_quote() |> Pleroma.Emoji.maybe_quote()
# CommonAPI checks only author can revoke reactions
with {:ok, _activity} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji) do with {:ok, _activity} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji) do
activity = Activity.get_by_id(activity_id) activity = Activity.get_by_id(activity_id)

View file

@ -332,7 +332,7 @@ defmodule Pleroma.Conversation.ParticipationTest do
# When it's a reply from the blocked user # When it's a reply from the blocked user
{:ok, _direct2} = {:ok, _direct2} =
CommonAPI.post(blocked, %{ CommonAPI.post(blocked, %{
status: "reply", status: "@#{third_user.nickname}, #{blocker.nickname} reply",
visibility: "direct", visibility: "direct",
in_reply_to_conversation_id: blocked_participation.id in_reply_to_conversation_id: blocked_participation.id
}) })

View file

@ -66,8 +66,10 @@ defmodule Pleroma.ConversationTest do
jafnhar = insert(:user, local: false) jafnhar = insert(:user, local: false)
tridi = insert(:user) tridi = insert(:user)
to = [har.nickname, jafnhar.nickname, tridi.nickname]
{:ok, activity} = {:ok, activity} =
CommonAPI.post(har, %{status: "Hey @#{jafnhar.nickname}", visibility: "direct"}) CommonAPI.post(har, %{status: "Hey @#{jafnhar.nickname}", visibility: "direct", to: to})
object = Pleroma.Object.normalize(activity, fetch: false) object = Pleroma.Object.normalize(activity, fetch: false)
context = object.data["context"] context = object.data["context"]
@ -88,7 +90,8 @@ defmodule Pleroma.ConversationTest do
CommonAPI.post(jafnhar, %{ CommonAPI.post(jafnhar, %{
status: "Hey @#{har.nickname}", status: "Hey @#{har.nickname}",
visibility: "direct", visibility: "direct",
in_reply_to_status_id: activity.id in_reply_to_status_id: activity.id,
to: to
}) })
object = Pleroma.Object.normalize(activity, fetch: false) object = Pleroma.Object.normalize(activity, fetch: false)
@ -112,7 +115,8 @@ defmodule Pleroma.ConversationTest do
CommonAPI.post(tridi, %{ CommonAPI.post(tridi, %{
status: "Hey @#{har.nickname}", status: "Hey @#{har.nickname}",
visibility: "direct", visibility: "direct",
in_reply_to_status_id: activity.id in_reply_to_status_id: activity.id,
to: to
}) })
object = Pleroma.Object.normalize(activity, fetch: false) object = Pleroma.Object.normalize(activity, fetch: false)

View file

@ -1580,6 +1580,41 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert object["content"] == activity["object"]["content"] assert object["content"] == activity["object"]["content"]
end end
test "it inserts an incoming reply create activity into the database", %{conn: conn} do
user = insert(:user)
replying_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "cofe"})
data = %{
type: "Create",
object: %{
to: [Pleroma.Constants.as_public(), user.ap_id],
cc: [replying_user.follower_address],
inReplyTo: activity.object.data["id"],
content: "green tea",
type: "Note"
}
}
result =
conn
|> assign(:user, replying_user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{replying_user.nickname}/outbox", data)
|> json_response(201)
updated_object = Object.normalize(activity.object.data["id"], fetch: false)
assert Activity.get_by_ap_id(result["id"])
assert result["object"]
assert %Object{data: object} = Object.normalize(result["object"], fetch: false)
assert object["content"] == data.object.content
assert Pleroma.Web.ActivityPub.Visibility.public?(object)
assert object["inReplyTo"] == activity.object.data["id"]
assert updated_object.data["repliesCount"] == 1
end
test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do
user = insert(:user) user = insert(:user)
@ -1706,6 +1741,289 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert note_object == Object.normalize(note_activity, fetch: false) assert note_object == Object.normalize(note_activity, fetch: false)
end end
test "it rejects Add to other user's collection", %{conn: conn} do
user = insert(:user)
target_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Post"})
object = Object.normalize(activity, fetch: false)
object_id = object.data["id"]
data = %{
type: "Add",
target:
"#{Pleroma.Web.Endpoint.url()}/users/#{target_user.nickname}/collections/featured",
object: object_id
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 400)
end
test "it rejects Remove to other user's collection", %{conn: conn} do
user = insert(:user)
target_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Post"})
object = Object.normalize(activity, fetch: false)
object_id = object.data["id"]
data = %{
type: "Remove",
target:
"#{Pleroma.Web.Endpoint.url()}/users/#{target_user.nickname}/collections/featured",
object: object_id
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 400)
end
test "it rejects updating Actor's profile", %{conn: conn} do
user = insert(:user, local: true)
user_object = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
user_object_new = Map.put(user_object, "name", "lain")
data = %{
type: "Update",
object: user_object_new
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/json")
|> post("/users/#{user.nickname}/outbox", data)
updated_user_object = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
assert updated_user_object == user_object
assert json_response(conn, 400)
end
# Actor publicKey tests are redundant with above test,
# left here for the case that Updating Actors is ever supported
test "it rejects updating Actor's publicKey", %{conn: conn} do
user = insert(:user, local: true)
{:ok, pem} = Pleroma.Keys.generate_rsa_pem()
{:ok, _, public_key} = Pleroma.Keys.keys_from_pem(pem)
# Taken from UserView
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
user_object = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
user_object_public_key = Map.fetch!(user_object, "publicKey")
user_object_public_key = Map.put(user_object_public_key, "publicKeyPem", public_key)
user_object_new = Map.put(user_object, "publicKey", user_object_public_key)
refute user_object == user_object_new
data = %{
type: "Update",
object: user_object_new
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/json")
|> post("/users/#{user.nickname}/outbox", data)
new_user_object = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
assert user_object == new_user_object
assert json_response(conn, 400)
end
test "it rejects updating Actor's publicKey of another user", %{conn: conn} do
user = insert(:user)
target_user = insert(:user, local: true)
{:ok, pem} = Pleroma.Keys.generate_rsa_pem()
{:ok, _, public_key} = Pleroma.Keys.keys_from_pem(pem)
# Taken from UserView
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
target_user_object =
Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: target_user})
target_user_object_public_key = Map.fetch!(target_user_object, "publicKey")
target_user_object_public_key =
Map.put(target_user_object_public_key, "publicKeyPem", public_key)
target_user_object_new =
Map.put(target_user_object, "publicKey", target_user_object_public_key)
refute target_user_object == target_user_object_new
data = %{
type: "Update",
object: target_user_object_new
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/json")
|> post("/users/#{target_user.nickname}/outbox", data)
new_target_user_object =
Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: target_user})
assert target_user_object == new_target_user_object
assert json_response(conn, 403)
end
test "it rejects creating Actors of type Application", %{conn: conn} do
user = insert(:user, local: true)
data = %{
type: "Create",
object: %{
type: "Application"
}
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 400)
end
test "it rejects creating Actors of type Person", %{conn: conn} do
user = insert(:user, local: true)
data = %{
type: "Create",
object: %{
type: "Person"
}
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 400)
end
test "it rejects creating Actors of type Service", %{conn: conn} do
user = insert(:user, local: true)
data = %{
type: "Create",
object: %{
type: "Service"
}
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 400)
end
test "it rejects like activity to object invisible to actor", %{conn: conn} do
user = insert(:user)
stranger = insert(:user, local: true)
{:ok, post} = CommonAPI.post(user, %{status: "cofe", visibility: "private"})
assert Pleroma.Web.ActivityPub.Visibility.private?(post)
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(post, stranger)
post_object = Object.normalize(post, fetch: false)
data = %{
type: "Like",
object: %{
id: post_object.data["id"]
}
}
conn =
conn
|> assign(:user, stranger)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{stranger.nickname}/outbox", data)
assert json_response(conn, 403)
end
test "it rejects announce activity to object invisible to actor", %{conn: conn} do
user = insert(:user)
stranger = insert(:user, local: true)
{:ok, post} = CommonAPI.post(user, %{status: "cofe", visibility: "private"})
assert Pleroma.Web.ActivityPub.Visibility.private?(post)
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(post, stranger)
post_object = Object.normalize(post, fetch: false)
data = %{
type: "Announce",
object: %{
id: post_object.data["id"]
}
}
conn =
conn
|> assign(:user, stranger)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{stranger.nickname}/outbox", data)
assert json_response(conn, 403)
end
test "it rejects emojireact activity to object invisible to actor", %{conn: conn} do
user = insert(:user)
stranger = insert(:user, local: true)
{:ok, post} = CommonAPI.post(user, %{status: "cofe", visibility: "private"})
assert Pleroma.Web.ActivityPub.Visibility.private?(post)
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(post, stranger)
post_object = Object.normalize(post, fetch: false)
data = %{
type: "EmojiReact",
object: %{
id: post_object.data["id"]
},
content: "😀"
}
conn =
conn
|> assign(:user, stranger)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{stranger.nickname}/outbox", data)
assert json_response(conn, 403)
end
test "it increases like count when receiving a like action", %{conn: conn} do test "it increases like count when receiving a like action", %{conn: conn} do
note_activity = insert(:note_activity) note_activity = insert(:note_activity)
note_object = Object.normalize(note_activity, fetch: false) note_object = Object.normalize(note_activity, fetch: false)

View file

@ -9,7 +9,10 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.UserView
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
@ -530,6 +533,9 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert is_nil(modified["object"]["announcements"]) assert is_nil(modified["object"]["announcements"])
assert is_nil(modified["object"]["announcement_count"]) assert is_nil(modified["object"]["announcement_count"])
assert is_nil(modified["object"]["generator"]) assert is_nil(modified["object"]["generator"])
assert is_nil(modified["object"]["rules"])
assert is_nil(modified["object"]["language"])
assert is_nil(modified["object"]["voters"])
end end
test "it strips internal fields of article" do test "it strips internal fields of article" do
@ -587,6 +593,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
test "it can handle Listen activities" do test "it can handle Listen activities" do
listen_activity = insert(:listen) listen_activity = insert(:listen)
# This has an inlined object as in ObjectView
{:ok, modified} = Transmogrifier.prepare_outgoing(listen_activity.data) {:ok, modified} = Transmogrifier.prepare_outgoing(listen_activity.data)
assert modified["type"] == "Listen" assert modified["type"] == "Listen"
@ -595,7 +602,36 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
{:ok, activity} = CommonAPI.listen(user, %{"title" => "lain radio episode 1"}) {:ok, activity} = CommonAPI.listen(user, %{"title" => "lain radio episode 1"})
{:ok, _modified} = Transmogrifier.prepare_outgoing(activity.data) user_ap_id = user.ap_id
activity_ap_id = activity.data["id"]
activity_to = activity.data["to"]
activity_cc = activity.data["cc"]
object_ap_id = activity.data["object"]
object_type = activity.object.data["type"]
# This does not have an inlined object
{:ok, modified2} = Transmogrifier.prepare_outgoing(activity.data)
assert match?(
%{
"@context" => [_ | _],
"type" => "Listen",
"actor" => ^user_ap_id,
"to" => ^activity_to,
"cc" => ^activity_cc,
"context" => "http://localhost" <> _,
"id" => ^activity_ap_id,
"object" => %{
"actor" => ^user_ap_id,
"attributedTo" => ^user_ap_id,
"id" => ^object_ap_id,
"type" => ^object_type,
"to" => ^activity_to,
"cc" => ^activity_cc
}
},
modified2
)
end end
test "custom emoji urls are URI encoded" do test "custom emoji urls are URI encoded" do
@ -635,6 +671,94 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
} = prepared["object"] } = prepared["object"]
end end
test "Updates of Actors are handled" do
user = insert(:user, local: true)
changeset = User.update_changeset(user, %{name: "new name"})
{:ok, unpersisted_user} = Ecto.Changeset.apply_action(changeset, :update)
updated_object =
UserView.render("user.json", user: unpersisted_user)
|> Map.delete("@context")
{:ok, update_data, []} = Builder.update(user, updated_object)
{:ok, activity, _} =
Pipeline.common_pipeline(update_data,
local: true,
user_update_changeset: changeset
)
assert {:ok, prepared} = Transmogrifier.prepare_outgoing(activity.data)
assert prepared["type"] == "Update"
assert prepared["@context"]
assert prepared["object"]["type"] == user.actor_type
end
test "Correctly handles Undo activities" do
blocked = insert(:user)
blocker = insert(:user, local: true)
blocked_ap_id = blocked.ap_id
blocker_ap_id = blocker.ap_id
{:ok, %Activity{} = block_activity} = CommonAPI.block(blocked, blocker)
{:ok, %Activity{} = undo_activity} = CommonAPI.unblock(blocked, blocker)
{:ok, data} = Transmogrifier.prepare_outgoing(undo_activity.data)
block_ap_id = block_activity.data["id"]
assert is_binary(block_ap_id)
assert match?(
%{
"@context" => [_ | _],
"type" => "Undo",
"id" => "http://localhost" <> _,
"actor" => ^blocker_ap_id,
"object" => ^block_ap_id,
"to" => [^blocked_ap_id],
"cc" => [],
"bto" => [],
"bcc" => []
},
data
)
end
test "Correctly handles EmojiReact activities" do
user = insert(:user, local: true)
note_activity = insert(:note_activity)
user_ap_id = user.ap_id
user_followers = user.follower_address
note_author = note_activity.data["actor"]
note_ap_id = note_activity.data["object"]
assert is_binary(note_author)
assert is_binary(note_ap_id)
{:ok, react_activity} = CommonAPI.react_with_emoji(note_activity.id, user, "🐈")
{:ok, data} = Transmogrifier.prepare_outgoing(react_activity.data)
assert match?(
%{
"@context" => [_ | _],
"type" => "EmojiReact",
"actor" => ^user_ap_id,
"to" => [^user_followers, ^note_author],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"bto" => [],
"bcc" => [],
"content" => "🐈",
"context" => "2hu",
"id" => "http://localhost" <> _,
"object" => ^note_ap_id,
"tag" => []
},
data
)
end
test "it prepares a quote post" do test "it prepares a quote post" do
user = insert(:user) user = insert(:user)

View file

@ -95,4 +95,23 @@ defmodule Pleroma.Web.ActivityPub.ObjectViewTest do
assert result["object"] == announce.data["id"] assert result["object"] == announce.data["id"]
assert result["type"] == "Undo" assert result["type"] == "Undo"
end end
test "renders a listen activity" do
audio = insert(:audio)
user = insert(:user)
{:ok, listen_activity} = CommonAPI.listen(user, audio.data)
result = ObjectView.render("object.json", %{object: listen_activity})
assert result["id"] == listen_activity.data["id"]
assert result["to"] == listen_activity.data["to"]
assert result["type"] == "Listen"
assert result["object"]["album"] == listen_activity.data["album"]
assert result["object"]["artist"] == listen_activity.data["artist"]
assert result["object"]["length"] == listen_activity.data["length"]
assert result["object"]["title"] == listen_activity.data["title"]
assert result["object"]["type"] == "Audio"
assert result["@context"]
end
end end

View file

@ -1086,7 +1086,7 @@ defmodule Pleroma.Web.CommonAPITest do
test "only public can be pinned", %{user: user} do test "only public can be pinned", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "private status", visibility: "private"}) {:ok, activity} = CommonAPI.post(user, %{status: "private status", visibility: "private"})
{:error, :visibility_error} = CommonAPI.pin(activity.id, user) {:error, :non_public_error} = CommonAPI.pin(activity.id, user)
end end
test "unpin status", %{user: user, activity: activity} do test "unpin status", %{user: user, activity: activity} do
@ -1300,6 +1300,47 @@ defmodule Pleroma.Web.CommonAPITest do
} = flag_activity } = flag_activity
end end
test "doesn't create a report when post is not visible to user" do
reporter = insert(:user)
target_user = insert(:user)
{:ok, post} = CommonAPI.post(target_user, %{status: "Eric", visibility: "private"})
assert Pleroma.Web.ActivityPub.Visibility.private?(post)
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(post, reporter)
# Fails when all status are invisible
report_data = %{
account_id: target_user.id,
comment: "foobar",
status_ids: [post.id]
}
assert {:error, :visibility_error} = CommonAPI.report(reporter, report_data)
end
test "doesn't create a report when some posts are not visible to user" do
reporter = insert(:user)
target_user = insert(:user)
{:ok, visible_activity} = CommonAPI.post(target_user, %{status: "cofe"})
{:ok, invisibile_activity} =
CommonAPI.post(target_user, %{status: "cawfee", visibility: "private"})
assert Pleroma.Web.ActivityPub.Visibility.private?(invisibile_activity)
assert Pleroma.Web.ActivityPub.Visibility.public?(visible_activity)
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(invisibile_activity, reporter)
# Fails when some statuses are invisible
report_data_partial = %{
account_id: target_user.id,
comment: "foobar",
status_ids: [visible_activity.id, invisibile_activity.id]
}
assert {:error, :visibility_error} = CommonAPI.report(reporter, report_data_partial)
end
test "updates report state" do test "updates report state" do
[reporter, target_user] = insert_pair(:user) [reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user) activity = insert(:note_activity, user: target_user)

View file

@ -316,6 +316,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
user = insert(:user) user = insert(:user)
%{user: other_user, conn: conn} = oauth_access(["read:notifications"]) %{user: other_user, conn: conn} = oauth_access(["read:notifications"])
{:ok, _, _, %{data: %{"state" => "accept"}}} = CommonAPI.follow(other_user, user)
{:ok, _, _, %{data: %{"state" => "accept"}}} = CommonAPI.follow(user, other_user)
{:ok, public_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "public"}) {:ok, public_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "public"})
{:ok, direct_activity} = {:ok, direct_activity} =

View file

@ -147,7 +147,7 @@ defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do
|> json_response_and_validate_schema(400) |> json_response_and_validate_schema(400)
end end
test "returns error when account is not exist", %{ test "returns error when account does not exist", %{
conn: conn, conn: conn,
activity: activity activity: activity
} do } do
@ -159,6 +159,51 @@ defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do
assert json_response_and_validate_schema(conn, 400) == %{"error" => "Account not found"} assert json_response_and_validate_schema(conn, 400) == %{"error" => "Account not found"}
end end
test "returns not found when post isn't visible to reporter", %{user: target_user} do
%{conn: conn, user: reporter} = oauth_access(["write:reports"])
{:ok, invisible_activity} =
CommonAPI.post(target_user, %{status: "Invisible!", visibility: "private"})
assert Pleroma.Web.ActivityPub.Visibility.private?(invisible_activity)
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(invisible_activity, reporter)
assert %{"error" => "Record not found"} =
conn
|> put_req_header("content-type", "application/json")
|> post(
"/api/v1/reports",
%{"account_id" => target_user.id, "status_ids" => [invisible_activity.id]}
)
|> json_response_and_validate_schema(404)
end
test "returns not found when some post aren't visible to reporter", %{
activity: activity,
user: target_user
} do
%{conn: conn, user: reporter} = oauth_access(["write:reports"])
{:ok, invisible_activity} =
CommonAPI.post(target_user, %{status: "Invisible!", visibility: "private"})
assert Pleroma.Web.ActivityPub.Visibility.private?(invisible_activity)
assert Pleroma.Web.ActivityPub.Visibility.visible_for_user?(activity, reporter)
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(invisible_activity, reporter)
assert %{"error" => "Record not found"} =
conn
|> put_req_header("content-type", "application/json")
|> post(
"/api/v1/reports",
%{
"account_id" => target_user.id,
"status_ids" => [activity.id, invisible_activity.id]
}
)
|> json_response_and_validate_schema(404)
end
test "doesn't fail if an admin has no email", %{conn: conn, target_user: target_user} do test "doesn't fail if an admin has no email", %{conn: conn, target_user: target_user} do
insert(:user, %{is_admin: true, email: nil}) insert(:user, %{is_admin: true, email: nil})

View file

@ -17,6 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
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
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Workers.ScheduledActivityWorker alias Pleroma.Workers.ScheduledActivityWorker
@ -267,6 +268,73 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
end) end)
end end
test "replying to a post the current user can't access fails", %{user: user, conn: conn} do
stranger = insert(:user)
{:ok, priv_post_act} =
CommonAPI.post(stranger, %{status: "forbidden knowledge", visibility: "private"})
assert Visibility.visible_for_user?(priv_post_act, stranger)
refute Visibility.visible_for_user?(priv_post_act, user)
resp =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "@#{stranger.nickname} :peek:",
"in_reply_to_id" => priv_post_act.id,
"visibility" => "private"
})
|> json_response_and_validate_schema(422)
assert match?(%{"error" => _}, resp)
end
test "replying to own DM succeeds", %{user: user, conn: conn} do
# this is an "edge" case for visibility: replying user is not
# part of addressed users (but is the author)
stranger = insert(:user)
{:ok, %{id: dm_id} = dm_post_act} =
CommonAPI.post(user, %{
status: "@#{stranger.nickname} wanna lose your mind to forbidden knowledge?",
visibility: "direct"
})
assert Visibility.visible_for_user?(dm_post_act, stranger)
assert Visibility.visible_for_user?(dm_post_act, user)
resp =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "@#{stranger.nickname} :peek:",
"in_reply_to_id" => dm_id,
"visibility" => "direct"
})
|> json_response_and_validate_schema(200)
assert match?(%{"in_reply_to_id" => ^dm_id}, resp)
end
test "replying to a non-post activity fails", %{conn: conn, user: user} do
other_user = insert(:user)
{:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user)
assert Visibility.visible_for_user?(follow_activity, user)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "hiiii!",
"in_reply_to_id" => to_string(follow_activity.id)
})
assert %{"error" => "Can only reply to posts, not \"Follow\" activities"} =
json_response_and_validate_schema(conn, 422)
end
test "posting a status with an invalid in_reply_to_id", %{conn: conn} do test "posting a status with an invalid in_reply_to_id", %{conn: conn} do
conn = conn =
conn conn
@ -1416,6 +1484,24 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert to_string(activity.id) == id assert to_string(activity.id) == id
end end
test "cannot reblog private status of others (even if visible)", %{conn: conn, user: user} do
followed = insert(:user, local: true)
{:ok, _, _, %{data: %{"state" => "accept"}}} = CommonAPI.follow(followed, user)
{:ok, activity} = CommonAPI.post(followed, %{status: "cofe", visibility: "private"})
assert Visibility.visible_for_user?(activity, user)
resp =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/reblog")
|> json_response_and_validate_schema(404)
assert match?(%{"error" => _}, resp)
end
end end
describe "unreblogging" do describe "unreblogging" do
@ -1445,6 +1531,34 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end end
test "can't unreblog someone else's reblog", %{user: user, conn: conn} do
activity = insert(:note_activity)
other_user = insert(:user)
{:ok, %{id: reblog_id}} = CommonAPI.repeat(activity.id, other_user)
# unreblog by base post
resp1 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unreblog")
|> json_response(400)
assert match?(%{"error" => _}, resp1)
# unreblog by reblog ID (reblog IDs are accepted by some APIs;
# ensure it fails here one way or another)
resp2 =
build_conn()
|> assign(:user, user)
|> assign(:token, insert(:oauth_token, user: user, scopes: ["write", "read"]))
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{reblog_id}/unreblog")
|> json_response_and_validate_schema(404)
assert match?(%{"error" => _}, resp2)
end
end end
describe "favoriting" do describe "favoriting" do
@ -1477,13 +1591,23 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|> json_response_and_validate_schema(200) |> json_response_and_validate_schema(200)
end end
test "returns 404 error for a wrong id", %{conn: conn} do test "a status you cannot see fails", %{conn: conn} do
conn = stranger = insert(:user)
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/1/favourite")
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"} {:ok, activity} =
CommonAPI.post(stranger, %{status: "it can eternal lie", visibility: "private"})
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/favourite")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
test "returns 404 error for a wrong id", %{conn: conn} do
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/1/favourite")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end end
end end
@ -1506,6 +1630,54 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert to_string(activity.id) == id assert to_string(activity.id) == id
end end
test "can't unfavourite post that isn't visible to user" do
user = insert(:user)
%{conn: conn, user: stranger} = oauth_access(["write:favourites"])
{:ok, activity} = CommonAPI.post(user, %{status: "invisible", visibility: "private"})
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(activity, stranger)
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unfavourite")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
test "can't unfavourite post that isn't favourited", %{conn: conn} do
activity = insert(:note_activity)
# using base post ID
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unfavourite")
|> json_response_and_validate_schema(400) == %{"error" => "Could not unfavorite"}
end
test "can't unfavourite other user's favs", %{conn: conn} do
activity = insert(:note_activity)
other = insert(:user)
{:ok, _} = CommonAPI.favorite(activity.id, other)
# using base post ID
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unfavourite")
|> json_response_and_validate_schema(400) == %{"error" => "Could not unfavorite"}
end
test "can't unfavourite other user's favs using their activity", %{conn: conn} do
activity = insert(:note_activity)
other = insert(:user)
{:ok, fav_activity} = CommonAPI.favorite(activity.id, other)
# some APIs (used to) take IDs of any activity type, make sure this fails one way or another
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{fav_activity.id}/unfavourite")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
test "returns 404 error for a wrong id", %{conn: conn} do test "returns 404 error for a wrong id", %{conn: conn} do
conn = conn =
conn conn
@ -1516,6 +1688,19 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
end end
end end
test "can't favourite post that isn't visible to user" do
user = insert(:user)
%{conn: conn, user: stranger} = oauth_access(["write:favourites"])
{:ok, activity} = CommonAPI.post(user, %{status: "invisible", visibility: "private"})
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(activity, stranger)
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/favourite")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
describe "pinned statuses" do describe "pinned statuses" do
setup do: oauth_access(["write:accounts"]) setup do: oauth_access(["write:accounts"])
@ -1549,7 +1734,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|> json_response(403) == %{"error" => "Invalid credentials."} |> json_response(403) == %{"error" => "Invalid credentials."}
end end
test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do test "/pin: returns 422 error when activity is not public", %{conn: conn, user: user} do
{:ok, dm} = CommonAPI.post(user, %{status: "test", visibility: "direct"}) {:ok, dm} = CommonAPI.post(user, %{status: "test", visibility: "direct"})
conn = conn =
@ -1562,6 +1747,18 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
} }
end end
test "/pin: returns 404 error when activity not visible to user", %{user: user} do
%{conn: conn, user: stranger} = oauth_access(["write:accounts"])
{:ok, activity} = CommonAPI.post(user, %{status: "invisible", visibility: "private"})
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(activity, stranger)
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/pin")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
test "pin by another user", %{activity: activity} do test "pin by another user", %{activity: activity} do
%{conn: conn} = oauth_access(["write:accounts"]) %{conn: conn} = oauth_access(["write:accounts"])
@ -1596,6 +1793,32 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"} |> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end end
test "/unpin: returns 404 error when activity not visible to user", %{user: user} do
%{conn: conn, user: stranger} = oauth_access(["write:accounts"])
{:ok, activity} = CommonAPI.post(user, %{status: "yumi", visibility: "private"})
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(activity, stranger)
assert conn
|> assign(:user, stranger)
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unpin")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
test "/unpin: returns 422 error when activity not owned by user", %{activity: activity} do
%{conn: conn, user: user} = oauth_access(["write:accounts"])
assert Pleroma.Web.ActivityPub.Visibility.visible_for_user?(activity, user)
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unpin")
|> json_response_and_validate_schema(422) == %{
"error" => "Someone else's status cannot be unpinned"
}
end
test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
{:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"}) {:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"})
@ -1707,6 +1930,28 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
json_response_and_validate_schema(bookmarks, 200) json_response_and_validate_schema(bookmarks, 200)
end end
test "cannot bookmark invisible post" do
user = insert(:user)
%{conn: conn, user: stranger} = oauth_access(["write:bookmarks"])
{:ok, activity} = CommonAPI.post(user, %{status: "mocha", visibility: "private"})
refute Pleroma.Web.ActivityPub.Visibility.visible_for_user?(activity, stranger)
resp1 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/bookmark")
assert json_response_and_validate_schema(resp1, 404) == %{"error" => "Record not found"}
resp2 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unbookmark")
assert json_response_and_validate_schema(resp2, 404) == %{"error" => "Record not found"}
end
test "bookmark folders" do test "bookmark folders" do
%{conn: conn, user: user} = oauth_access(["write:bookmarks", "read:bookmarks"]) %{conn: conn, user: user} = oauth_access(["write:bookmarks", "read:bookmarks"])
@ -1804,6 +2049,30 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|> post("/api/v1/statuses/#{activity.id}/unmute") |> post("/api/v1/statuses/#{activity.id}/unmute")
|> json_response_and_validate_schema(200) |> json_response_and_validate_schema(200)
end end
test "cannot mute not visible conversation", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "Invisible!", visibility: "private"})
%{conn: conn} = oauth_access(["write:mutes"])
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/mute")
|> json_response_and_validate_schema(404) == %{
"error" => "Record not found"
}
end
test "cannot unmute not visible conversation", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "Invisible!", visibility: "private"})
%{conn: conn} = oauth_access(["write:mutes"])
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unmute")
|> json_response_and_validate_schema(404) == %{
"error" => "Record not found"
}
end
end end
test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do
@ -1970,6 +2239,22 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert id == other_user.id assert id == other_user.id
end end
test "fails when base post not visible to current user", %{user: user} do
other_user = insert(:user, local: true)
%{conn: conn} = oauth_access(["read:accounts"])
{:ok, activity} =
CommonAPI.post(user, %{
status: "craving tea and mochi rn",
visibility: "private"
})
assert conn
|> assign(:user, other_user)
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
test "returns empty array when :show_reactions is disabled", %{conn: conn, activity: activity} do test "returns empty array when :show_reactions is disabled", %{conn: conn, activity: activity} do
clear_config([:instance, :show_reactions], false) clear_config([:instance, :show_reactions], false)
@ -2088,6 +2373,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert [] == response assert [] == response
end end
test "does fail when requesting for a non-visible status", %{user: user} do
other_user = insert(:user, local: true)
{:ok, activity} =
CommonAPI.post(user, %{
status: "deep below it sleeps and mustn't wake",
visibility: "private"
})
response =
build_conn()
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read"]))
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response_and_validate_schema(404)
assert match?(%{"error" => _}, response)
end
end end
test "context" do test "context" do
@ -2110,6 +2414,34 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
} = response } = response
end end
test "context doesn't leak priv posts" do
%{user: user, conn: conn} = oauth_access(["read:statuses"])
stranger = insert(:user)
{:ok, %{id: id1}} = CommonAPI.post(stranger, %{status: "1", visibility: "public"})
{:ok, %{id: id2}} =
CommonAPI.post(stranger, %{status: "2", visibility: "unlisted", in_reply_to_status_id: id1})
{:ok, %{id: _id_boo} = act_boo} =
CommonAPI.post(stranger, %{status: "boo", visibility: "private", in_reply_to_status_id: id1})
refute Visibility.visible_for_user?(act_boo, user)
response =
conn
|> get("/api/v1/statuses/#{id1}/context")
|> json_response_and_validate_schema(:ok)
assert match?(
%{
"ancestors" => [],
"descendants" => [%{"id" => ^id2}]
},
response
)
end
test "favorites paginate correctly" do test "favorites paginate correctly" do
%{user: user, conn: conn} = oauth_access(["read:favourites"]) %{user: user, conn: conn} = oauth_access(["read:favourites"])
other_user = insert(:user) other_user = insert(:user)

View file

@ -9,10 +9,38 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Tests.ObanHelpers alias Pleroma.Tests.ObanHelpers
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
import Pleroma.Factory import Pleroma.Factory
defp prepare_reacted_post(visibility \\ "private") do
unrelated_user = insert(:user, local: true)
poster = insert(:user, local: true)
follower = insert(:user, local: true)
{:ok, _, _, %{data: %{"state" => "accept"}}} = CommonAPI.follow(poster, follower)
{:ok, post_activity} = CommonAPI.post(poster, %{status: "miaow!", visibility: visibility})
if visibility != "direct" do
assert Visibility.visible_for_user?(post_activity, follower)
end
if visibility in ["direct", "private"] do
refute Visibility.visible_for_user?(post_activity, unrelated_user)
end
{:ok, _react_activity} = CommonAPI.react_with_emoji(post_activity.id, follower, "🐾")
{post_activity, poster, follower, unrelated_user}
end
defp prepare_conn_of_user(conn, user) do
conn
|> assign(:user, user)
|> assign(:token, insert(:oauth_token, user: user, scopes: ["write", "read"]))
end
setup do setup do
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig) Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok :ok
@ -137,6 +165,28 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
|> json_response_and_validate_schema(400) |> json_response_and_validate_schema(400)
end end
test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji not allowed for non-visible posts", %{
conn: conn
} do
{%{id: activity_id} = _activity, _author, follower, stranger} = prepare_reacted_post()
# Works for follower
resp =
prepare_conn_of_user(conn, follower)
|> put("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐈")
|> json_response_and_validate_schema(200)
assert match?(%{"id" => ^activity_id}, resp)
# Fails for stranger
resp =
prepare_conn_of_user(conn, stranger)
|> put("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐈")
|> json_response_and_validate_schema(404)
assert match?(%{"error" => "Record not found"}, resp)
end
test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
@ -211,6 +261,26 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
|> json_response(400) |> json_response(400)
end end
test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji only allows original reacter to revoke",
%{conn: conn} do
{%{id: activity_id} = _activity, author, follower, unrelated} = prepare_reacted_post("public")
# Works for original reacter
prepare_conn_of_user(conn, follower)
|> delete("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐾")
|> json_response_and_validate_schema(200)
# Fails for anyone else
for u <- [author, unrelated] do
resp =
prepare_conn_of_user(conn, u)
|> delete("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐾")
|> json_response(400)
assert match?(%{"error" => _}, resp)
end
end
test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
@ -324,6 +394,25 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
assert [%{"name" => "🎅", "count" => 2}] = result assert [%{"name" => "🎅", "count" => 2}] = result
end end
test "GET /api/v1/pleroma/statuses/:id/reactions not allowed for non-visible posts", %{
conn: conn
} do
{%{id: activity_id} = _activity, _author, follower, stranger} = prepare_reacted_post()
# Works for follower
resp =
prepare_conn_of_user(conn, follower)
|> get("/api/v1/pleroma/statuses/#{activity_id}/reactions")
|> json_response_and_validate_schema(200)
assert match?([%{"name" => _, "count" => _} | _], resp)
# Fails for stranger
assert prepare_conn_of_user(conn, stranger)
|> get("/api/v1/pleroma/statuses/#{activity_id}/reactions")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
test "GET /api/v1/pleroma/statuses/:id/reactions with :show_reactions disabled", %{conn: conn} do test "GET /api/v1/pleroma/statuses/:id/reactions with :show_reactions disabled", %{conn: conn} do
clear_config([:instance, :show_reactions], false) clear_config([:instance, :show_reactions], false)
@ -372,4 +461,20 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
assert represented_user["id"] == other_user.id assert represented_user["id"] == other_user.id
end end
test "GET /api/v1/pleroma/statuses/:id/reactions/:emoji not allowed for non-visible posts", %{
conn: conn
} do
{%{id: activity_id} = _activity, _author, follower, stranger} = prepare_reacted_post()
# Works for follower
assert prepare_conn_of_user(conn, follower)
|> get("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐈")
|> json_response_and_validate_schema(200)
# Fails for stranger
assert prepare_conn_of_user(conn, stranger)
|> get("/api/v1/pleroma/statuses/#{activity_id}/reactions/🐈")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
end end