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",
"generator",
"rules",
"language"
"language",
"voters"
]
)

View file

@ -126,7 +126,7 @@ defmodule Pleroma.Object do
Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
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.
# 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
cond do
Keyword.get(options, :id_only) ->
ap_id
Keyword.get(options, :fetch) ->
case Fetcher.fetch_object_from_id(ap_id, options) do
{:ok, object} -> object

View file

@ -482,6 +482,42 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
{:ok, activity}
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(
%{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn,
%{"nickname" => nickname} = params
@ -493,6 +529,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|> Map.put("actor", actor)
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),
%Activity{data: activity_data} <- Activity.normalize(activity) do
conn

View file

@ -873,6 +873,27 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, data}
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
object =
object_id

View file

@ -15,26 +15,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
Map.merge(base, additional)
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
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header(activity.data)
object_id = Object.normalize(activity, id_only: true)
additional =
Transmogrifier.prepare_object(activity.data)
|> Map.put("object", object_id)
Map.merge(base, additional)
{:ok, ap_data} = Transmogrifier.prepare_outgoing(activity.data)
ap_data
end
end

View file

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

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.ApiNotFoundError
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
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),
responses: %{
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

View file

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

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
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, %Activity{} = activity, _meta}} <-
{:common_pipeline,
@ -278,6 +279,9 @@ defmodule Pleroma.Web.CommonAPI do
{:find_object, _} ->
{:error, :not_found}
{:visibility_error, _} ->
{:error, :not_found}
{:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->
if {:object, {"already liked by this actor", []}} in changeset.errors do
{:ok, :already_liked}
@ -296,6 +300,7 @@ defmodule Pleroma.Web.CommonAPI do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)},
%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),
{_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(like)},
{:ok, undo, _} <- Builder.undo(user, like),
@ -303,6 +308,7 @@ defmodule Pleroma.Web.CommonAPI do
{:ok, activity}
else
{:find_activity, _} -> {:error, :not_found}
{:visibility_error, _} -> {:error, :not_found}
_ -> {:error, dgettext("errors", "Could not unfavorite")}
end
end
@ -311,11 +317,15 @@ defmodule Pleroma.Web.CommonAPI do
{:ok, Activity.t()} | {:error, String.t()}
def react_with_emoji(id, user, emoji) do
with %Activity{} = activity <- Activity.get_by_id(id),
{_, true} <- {:visibility_error, activity_visible_to_actor(activity, user)},
object <- Object.normalize(activity, fetch: false),
{:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
{:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
{:ok, activity}
else
{:visibility_error, _} ->
{:error, :not_found}
_ ->
{:error, dgettext("errors", "Could not add reaction emoji")}
end
@ -506,6 +516,7 @@ defmodule Pleroma.Web.CommonAPI do
@spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
def pin(id, %User{} = user) do
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 <- object_type_is_allowed_for_pin(activity.object),
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(_, _), 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
with false <- type in ["Note", "Article", "Question"] do
{:error, :not_allowed}
@ -539,13 +558,18 @@ defmodule Pleroma.Web.CommonAPI do
defp activity_is_public(activity) do
with false <- Visibility.public?(activity) do
{:error, :visibility_error}
{:error, :non_public_error}
end
end
@spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
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),
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, _} <-
Pipeline.common_pipeline(unpin_data,
@ -562,7 +586,8 @@ defmodule Pleroma.Web.CommonAPI do
def add_mute(activity, user, params \\ %{}) do
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
if expires_in > 0 do
Pleroma.Workers.MuteExpireWorker.new(
@ -574,14 +599,21 @@ defmodule Pleroma.Web.CommonAPI do
{:ok, activity}
else
{:error, :visibility_error} -> {:error, :visibility_error}
{:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
end
end
@spec remove_mute(Activity.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()}
def remove_mute(%Activity{} = activity, %User{} = user) do
ThreadMute.remove_mute(user.id, activity.data["context"])
{:ok, activity}
case activity_visible_to_actor(activity, user) do
true ->
ThreadMute.remove_mute(user.id, activity.data["context"])
{:ok, activity}
error ->
error
end
end
@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),
{:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
{:ok, statuses} <- get_report_statuses(account, data),
true <- check_statuses_visibility(user, statuses),
rules <- get_report_rules(Map.get(data, :rule_ids, nil)) do
ActivityPub.flag(%{
context: Utils.generate_context_id(),
@ -622,9 +655,27 @@ defmodule Pleroma.Web.CommonAPI do
forward: Map.get(data, :forward, false),
rules: rules
})
else
false ->
{:error, :visibility_error}
error ->
error
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
case User.get_cached_by_id(account_id) do
%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: :deleted}} = draft) do
add_error(draft, dgettext("errors", "Cannot reply to a deleted status"))
end
defp in_reply_to(%{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do
# If a post was deleted all its activities (except the newly added Delete) are purged too,
# 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
activity = Activity.get_by_id(id)
false ->
add_error(draft, dgettext("errors", "Record not found"))
params =
if is_nil(activity) do
# Deleted activities are returned as nil
Map.put(params, :in_reply_to_status_id, :deleted)
else
Map.put(params, :in_reply_to_status_id, activity)
end
in_reply_to(%{draft | params: params})
{:type, type} ->
add_error(
draft,
dgettext("errors", "Can only reply to posts, not %{type} activities",
type: inspect(type)
)
)
end
end
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
with {:ok, activity} <- Pleroma.Web.CommonAPI.report(user, params) do
render(conn, "show.json", activity: activity)
else
{:error, :visibility_error} ->
{:error, :not_found, "Record not found"}
error ->
error
end
end
end

View file

@ -319,6 +319,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
@doc "DELETE /api/v1/statuses/:id"
def delete(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
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
try_render(conn, "show.json",
activity: activity,
@ -340,6 +341,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
_
) do
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
try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
end
@ -364,6 +366,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
_
) do
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
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
@ -390,6 +393,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
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, :pinned_statuses_limit_reached} ->
{: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, :visibility_error} ->
{:error, :not_found, "Record not found"}
{:error, :non_public_error} ->
{:error, :unprocessable_entity, "Non-public status cannot be pinned"}
error ->
@ -410,8 +418,20 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
conn,
_
) do
# CommonAPI already checks whether user can unpin
with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
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
@ -434,6 +454,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
),
{:ok, _bookmark} <- Bookmark.create(user.id, activity.id, folder_id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
else
false ->
{:error, :not_found, "Record not found"}
error ->
error
end
end
@ -447,6 +473,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
true <- Visibility.visible_for_user?(activity, user),
{:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
else
false ->
{:error, :not_found, "Record not found"}
error ->
error
end
end
@ -459,8 +491,15 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
_
) do
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
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
@ -473,8 +512,15 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
_
) do
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
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

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.StatusView
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
with true <- Pleroma.Config.get([:instance, :show_reactions]),
%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),
reactions <- Object.get_emoji_reactions(object) do
reactions =
@ -37,6 +39,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
render(conn, "index.json", emoji_reactions: reactions, user: user)
else
{:visible, _} -> {:error, :not_found}
_e -> json(conn, [])
end
end
@ -76,6 +79,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
|> Pleroma.Emoji.fully_qualify_emoji()
|> Pleroma.Emoji.maybe_quote()
# CommonAPI checks if allowed to react
with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji) do
activity = Activity.get_by_id(activity_id)
@ -91,6 +95,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
|> Pleroma.Emoji.fully_qualify_emoji()
|> Pleroma.Emoji.maybe_quote()
# CommonAPI checks only author can revoke reactions
with {:ok, _activity} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji) do
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
{:ok, _direct2} =
CommonAPI.post(blocked, %{
status: "reply",
status: "@#{third_user.nickname}, #{blocker.nickname} reply",
visibility: "direct",
in_reply_to_conversation_id: blocked_participation.id
})

View file

@ -66,8 +66,10 @@ defmodule Pleroma.ConversationTest do
jafnhar = insert(:user, local: false)
tridi = insert(:user)
to = [har.nickname, jafnhar.nickname, tridi.nickname]
{: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)
context = object.data["context"]
@ -88,7 +90,8 @@ defmodule Pleroma.ConversationTest do
CommonAPI.post(jafnhar, %{
status: "Hey @#{har.nickname}",
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)
@ -112,7 +115,8 @@ defmodule Pleroma.ConversationTest do
CommonAPI.post(tridi, %{
status: "Hey @#{har.nickname}",
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)

View file

@ -1580,6 +1580,41 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert object["content"] == activity["object"]["content"]
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
user = insert(:user)
@ -1706,6 +1741,289 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert note_object == Object.normalize(note_activity, fetch: false)
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
note_activity = insert(:note_activity)
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.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView
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"]["announcement_count"])
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
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
listen_activity = insert(:listen)
# This has an inlined object as in ObjectView
{:ok, modified} = Transmogrifier.prepare_outgoing(listen_activity.data)
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, _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
test "custom emoji urls are URI encoded" do
@ -635,6 +671,94 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
} = prepared["object"]
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
user = insert(:user)

View file

@ -95,4 +95,23 @@ defmodule Pleroma.Web.ActivityPub.ObjectViewTest do
assert result["object"] == announce.data["id"]
assert result["type"] == "Undo"
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

View file

@ -1086,7 +1086,7 @@ defmodule Pleroma.Web.CommonAPITest do
test "only public can be pinned", %{user: user} do
{: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
test "unpin status", %{user: user, activity: activity} do
@ -1300,6 +1300,47 @@ defmodule Pleroma.Web.CommonAPITest do
} = flag_activity
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
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)

View file

@ -316,6 +316,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
user = insert(:user)
%{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, direct_activity} =

View file

@ -147,7 +147,7 @@ defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do
|> json_response_and_validate_schema(400)
end
test "returns error when account is not exist", %{
test "returns error when account does not exist", %{
conn: conn,
activity: activity
} do
@ -159,6 +159,51 @@ defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do
assert json_response_and_validate_schema(conn, 400) == %{"error" => "Account not found"}
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
insert(:user, %{is_admin: true, email: nil})

View file

@ -17,6 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
alias Pleroma.Workers.ScheduledActivityWorker
@ -267,6 +268,73 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
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
conn =
conn
@ -1416,6 +1484,24 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert to_string(activity.id) == id
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
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"}
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
describe "favoriting" do
@ -1477,13 +1591,23 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|> json_response_and_validate_schema(200)
end
test "returns 404 error for a wrong id", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/1/favourite")
test "a status you cannot see fails", %{conn: conn} do
stranger = insert(:user)
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
@ -1506,6 +1630,54 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert to_string(activity.id) == id
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
conn =
conn
@ -1516,6 +1688,19 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
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
setup do: oauth_access(["write:accounts"])
@ -1549,7 +1734,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
|> json_response(403) == %{"error" => "Invalid credentials."}
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"})
conn =
@ -1562,6 +1747,18 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
}
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
%{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"}
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
{: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)
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
%{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")
|> json_response_and_validate_schema(200)
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
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
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
clear_config([:instance, :show_reactions], false)
@ -2088,6 +2373,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert [] == response
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
test "context" do
@ -2110,6 +2414,34 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
} = response
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
%{user: user, conn: conn} = oauth_access(["read:favourites"])
other_user = insert(:user)

View file

@ -9,10 +9,38 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
alias Pleroma.Object
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
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
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok
@ -137,6 +165,28 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
|> json_response_and_validate_schema(400)
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
user = insert(:user)
other_user = insert(:user)
@ -211,6 +261,26 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
|> json_response(400)
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
user = insert(:user)
other_user = insert(:user)
@ -324,6 +394,25 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
assert [%{"name" => "🎅", "count" => 2}] = result
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
clear_config([:instance, :show_reactions], false)
@ -372,4 +461,20 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
assert represented_user["id"] == other_user.id
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