Page MenuHomePhorge

No OneTemporary

Size
86 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index 57ac40b42..d423b1139 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -1,560 +1,571 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPubController do
use Pleroma.Web, :controller
alias Pleroma.Activity
alias Pleroma.Delivery
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.ControllerHelper
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Federator
alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
alias Pleroma.Web.Plugs.FederatingPlug
require Logger
action_fallback(:errors)
@federating_only_actions [:internal_fetch, :relay, :relay_following, :relay_followers]
plug(FederatingPlug when action in @federating_only_actions)
plug(
EnsureAuthenticatedPlug,
[unless_func: &FederatingPlug.federating?/1] when action not in @federating_only_actions
)
# Note: :following and :followers must be served even without authentication (as via :api)
plug(
EnsureAuthenticatedPlug
when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
)
plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media])
plug(
Pleroma.Web.Plugs.Cache,
[query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
when action in [:activity, :object]
)
plug(:set_requester_reachable when action in [:inbox])
plug(:relay_active? when action in [:relay])
defp relay_active?(conn, _) do
if Pleroma.Config.get([:instance, :allow_relay]) do
conn
else
conn
|> render_error(:not_found, "not found")
|> halt()
end
end
def user(conn, %{"nickname" => nickname}) do
with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("user.json", %{user: user})
else
nil -> {:error, :not_found}
%{local: false} -> {:error, :not_found}
end
end
def object(%{assigns: assigns} = conn, _) do
with ap_id <- Endpoint.url() <> conn.request_path,
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
user <- Map.get(assigns, :user, nil),
{_, true} <- {:visible?, Visibility.visible_for_user?(object, user)} do
conn
+ |> maybe_skip_cache(user)
|> assign(:tracking_fun_data, object.id)
|> set_cache_ttl_for(object)
|> put_resp_content_type("application/activity+json")
|> put_view(ObjectView)
|> render("object.json", object: object)
else
{:visible?, false} -> {:error, :not_found}
nil -> {:error, :not_found}
end
end
def track_object_fetch(conn, nil), do: conn
def track_object_fetch(conn, object_id) do
with %{assigns: %{user: %User{id: user_id}}} <- conn do
Delivery.create(object_id, user_id)
end
conn
end
def activity(%{assigns: assigns} = conn, _) do
with ap_id <- Endpoint.url() <> conn.request_path,
%Activity{} = activity <- Activity.normalize(ap_id),
{_, true} <- {:local?, activity.local},
user <- Map.get(assigns, :user, nil),
{_, true} <- {:visible?, Visibility.visible_for_user?(activity, user)} do
conn
+ |> maybe_skip_cache(user)
|> maybe_set_tracking_data(activity)
|> set_cache_ttl_for(activity)
|> put_resp_content_type("application/activity+json")
|> put_view(ObjectView)
|> render("object.json", object: activity)
else
{:visible?, false} -> {:error, :not_found}
{:local?, false} -> {:error, :not_found}
nil -> {:error, :not_found}
end
end
defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
object_id = Object.normalize(activity, fetch: false).id
assign(conn, :tracking_fun_data, object_id)
end
defp maybe_set_tracking_data(conn, _activity), do: conn
defp set_cache_ttl_for(conn, %Activity{object: object}) do
set_cache_ttl_for(conn, object)
end
defp set_cache_ttl_for(conn, entity) do
ttl =
case entity do
%Object{data: %{"type" => "Question"}} ->
Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
%Object{} ->
Pleroma.Config.get([:web_cache_ttl, :activity_pub])
_ ->
nil
end
assign(conn, :cache_ttl, ttl)
end
+ def maybe_skip_cache(conn, user) do
+ if user do
+ conn
+ |> assign(:skip_cache, true)
+ else
+ conn
+ end
+ end
+
# GET /relay/following
def relay_following(conn, _params) do
with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("following.json", %{user: Relay.get_actor()})
end
end
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
{:show_follows, true} <-
{:show_follows, (for_user && for_user == user) || !user.hide_follows} do
{page, _} = Integer.parse(page)
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("following.json", %{user: user, page: page, for: for_user})
else
{:show_follows, _} ->
conn
|> put_resp_content_type("application/activity+json")
|> send_resp(403, "")
end
end
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("following.json", %{user: user, for: for_user})
end
end
# GET /relay/followers
def relay_followers(conn, _params) do
with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("followers.json", %{user: Relay.get_actor()})
end
end
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
{:show_followers, true} <-
{:show_followers, (for_user && for_user == user) || !user.hide_followers} do
{page, _} = Integer.parse(page)
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("followers.json", %{user: user, page: page, for: for_user})
else
{:show_followers, _} ->
conn
|> put_resp_content_type("application/activity+json")
|> send_resp(403, "")
end
end
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("followers.json", %{user: user, for: for_user})
end
end
def outbox(
%{assigns: %{user: for_user}} = conn,
%{"nickname" => nickname, "page" => page?} = params
)
when page? in [true, "true"] do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do
# "include_poll_votes" is a hack because postgres generates inefficient
# queries when filtering by 'Answer', poll votes will be hidden by the
# visibility filter in this case anyway
params =
params
|> Map.drop(["nickname", "page"])
|> Map.put("include_poll_votes", true)
|> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
activities = ActivityPub.fetch_user_activities(user, for_user, params)
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("activity_collection_page.json", %{
activities: activities,
pagination: ControllerHelper.get_pagination_fields(conn, activities),
iri: "#{user.ap_id}/outbox"
})
end
end
def outbox(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
end
end
def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
with %User{} = recipient <- User.get_cached_by_nickname(nickname),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
true <- Utils.recipient_in_message(recipient, actor, params),
params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
Federator.incoming_ap_doc(params)
json(conn, "ok")
end
end
def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
Federator.incoming_ap_doc(params)
json(conn, "ok")
end
# POST /relay/inbox -or- POST /internal/fetch/inbox
def inbox(conn, params) do
if params["type"] == "Create" && FederatingPlug.federating?() do
post_inbox_relayed_create(conn, params)
else
post_inbox_fallback(conn, params)
end
end
defp post_inbox_relayed_create(conn, params) do
Logger.debug(
"Signature missing or not from author, relayed Create message, fetching object from source"
)
Fetcher.fetch_object_from_id(params["object"]["id"])
json(conn, "ok")
end
defp post_inbox_fallback(conn, params) do
headers = Enum.into(conn.req_headers, %{})
if headers["signature"] && params["actor"] &&
String.contains?(headers["signature"], params["actor"]) do
Logger.debug(
"Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!"
)
Logger.debug(inspect(conn.req_headers))
end
conn
|> put_status(:bad_request)
|> json(dgettext("errors", "error"))
end
defp represent_service_actor(%User{} = user, conn) do
with {:ok, user} <- User.ensure_keys_present(user) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("user.json", %{user: user})
else
nil -> {:error, :not_found}
end
end
defp represent_service_actor(nil, _), do: {:error, :not_found}
def relay(conn, _params) do
Relay.get_actor()
|> represent_service_actor(conn)
end
def internal_fetch(conn, _params) do
InternalFetchActor.get_actor()
|> represent_service_actor(conn)
end
@doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("user.json", %{user: user})
end
def read_inbox(
%{assigns: %{user: %User{nickname: nickname} = user}} = conn,
%{"nickname" => nickname, "page" => page?} = params
)
when page? in [true, "true"] do
params =
params
|> Map.drop(["nickname", "page"])
|> Map.put("blocking_user", user)
|> Map.put("user", user)
|> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
activities =
[user.ap_id | User.following(user)]
|> ActivityPub.fetch_activities(params)
|> Enum.reverse()
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("activity_collection_page.json", %{
activities: activities,
pagination: ControllerHelper.get_pagination_fields(conn, activities),
iri: "#{user.ap_id}/inbox"
})
end
def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
"nickname" => nickname
}) do
with {:ok, user} <- User.ensure_keys_present(user) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
end
end
def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
"nickname" => nickname
}) do
err =
dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
nickname: nickname,
as_nickname: as_nickname
)
conn
|> put_status(:forbidden)
|> json(err)
end
defp fix_user_message(%User{ap_id: actor}, %{"type" => "Create", "object" => object} = activity)
when is_map(object) do
length =
[object["content"], object["summary"], object["name"]]
|> Enum.filter(&is_binary(&1))
|> Enum.join("")
|> String.length()
limit = Pleroma.Config.get([:instance, :limit])
if length < limit do
object =
object
|> Transmogrifier.strip_internal_fields()
|> Map.put("attributedTo", actor)
|> Map.put("actor", actor)
|> Map.put("id", Utils.generate_object_id())
{:ok, Map.put(activity, "object", object)}
else
{:error,
dgettext(
"errors",
"Character limit (%{limit} characters) exceeded, contains %{length} characters",
limit: limit,
length: length
)}
end
end
defp fix_user_message(
%User{ap_id: actor} = user,
%{"type" => "Delete", "object" => object} = activity
) do
with {_, %Object{data: object_data}} <- {:normalize, Object.normalize(object, fetch: false)},
{_, true} <- {:permission, user.is_moderator || actor == object_data["actor"]} do
{:ok, activity}
else
{:normalize, _} ->
{:error, "No such object found"}
{:permission, _} ->
{:forbidden, "You can't delete this object"}
end
end
defp fix_user_message(%User{}, activity) do
{:ok, activity}
end
def update_outbox(
%{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn,
%{"nickname" => nickname} = params
) do
params =
params
|> Map.drop(["nickname"])
|> Map.put("id", Utils.generate_activity_id())
|> Map.put("actor", actor)
with {:ok, params} <- fix_user_message(user, params),
{:ok, activity, _} <- Pipeline.common_pipeline(params, local: true),
%Activity{data: activity_data} <- Activity.normalize(activity) do
conn
|> put_status(:created)
|> put_resp_header("location", activity_data["id"])
|> json(activity_data)
else
{:forbidden, message} ->
conn
|> put_status(:forbidden)
|> json(message)
{:error, message} ->
conn
|> put_status(:bad_request)
|> json(message)
e ->
Logger.warn(fn -> "AP C2S: #{inspect(e)}" end)
conn
|> put_status(:bad_request)
|> json("Bad Request")
end
end
def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
err =
dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
nickname: nickname,
as_nickname: user.nickname
)
conn
|> put_status(:forbidden)
|> json(err)
end
defp errors(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> json(dgettext("errors", "Not found"))
end
defp errors(conn, _e) do
conn
|> put_status(:internal_server_error)
|> json(dgettext("errors", "error"))
end
defp set_requester_reachable(%Plug.Conn{} = conn, _) do
with actor <- conn.params["actor"],
true <- is_binary(actor) do
Pleroma.Instances.set_reachable(actor)
end
conn
end
defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
{:ok, new_user} = User.ensure_keys_present(user)
for_user =
if new_user != user and match?(%User{}, for_user) do
User.get_cached_by_nickname(for_user.nickname)
else
for_user
end
{new_user, for_user}
end
def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
with {:ok, object} <-
ActivityPub.upload(
file,
actor: User.ap_id(user),
description: Map.get(data, "description")
) do
Logger.debug(inspect(object))
conn
|> put_status(:created)
|> json(object.data)
end
end
def pinned(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
conn
|> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("featured.json", %{user: user}))
end
end
end
diff --git a/lib/pleroma/web/plugs/cache.ex b/lib/pleroma/web/plugs/cache.ex
index e0467f107..935b2d834 100644
--- a/lib/pleroma/web/plugs/cache.ex
+++ b/lib/pleroma/web/plugs/cache.ex
@@ -1,143 +1,146 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.Cache do
@moduledoc """
Caches successful GET responses.
To enable the cache add the plug to a router pipeline or controller:
plug(Pleroma.Web.Plugs.Cache)
## Configuration
To configure the plug you need to pass settings as the second argument to the `plug/2` macro:
plug(Pleroma.Web.Plugs.Cache, [ttl: nil, query_params: true])
Available options:
- `ttl`: An expiration time (time-to-live). This value should be in milliseconds or `nil` to disable expiration. Defaults to `nil`.
- `query_params`: Take URL query string into account (`true`), ignore it (`false`) or limit to specific params only (list). Defaults to `true`.
- `tracking_fun`: A function that is called on successfull responses, no matter if the request is cached or not. It should accept a conn as the first argument and the value assigned to `tracking_fun_data` as the second.
Additionally, you can overwrite the TTL inside a controller action by assigning `cache_ttl` to the connection struct:
def index(conn, _params) do
ttl = 60_000 # one minute
conn
|> assign(:cache_ttl, ttl)
|> render("index.html")
end
"""
import Phoenix.Controller, only: [current_path: 1, json: 2]
import Plug.Conn
@behaviour Plug
@defaults %{ttl: nil, query_params: true}
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@impl true
def init([]), do: @defaults
def init(opts) do
opts = Map.new(opts)
Map.merge(@defaults, opts)
end
@impl true
def call(%{method: "GET"} = conn, opts) do
key = cache_key(conn, opts)
case @cachex.get(:web_resp_cache, key) do
{:ok, nil} ->
cache_resp(conn, opts)
{:ok, {content_type, body, tracking_fun_data}} ->
conn = opts.tracking_fun.(conn, tracking_fun_data)
send_cached(conn, {content_type, body})
{:ok, record} ->
send_cached(conn, record)
{atom, message} when atom in [:ignore, :error] ->
render_error(conn, message)
end
end
def call(conn, _), do: conn
# full path including query params
defp cache_key(conn, %{query_params: true}), do: current_path(conn)
# request path without query params
defp cache_key(conn, %{query_params: false}), do: conn.request_path
# request path with specific query params
defp cache_key(conn, %{query_params: query_params}) when is_list(query_params) do
query_string =
conn.params
|> Map.take(query_params)
|> URI.encode_query()
conn.request_path <> "?" <> query_string
end
defp cache_resp(conn, opts) do
register_before_send(conn, fn
%{status: 200, resp_body: body} = conn ->
ttl = Map.get(conn.assigns, :cache_ttl, opts.ttl)
key = cache_key(conn, opts)
content_type = content_type(conn)
- conn =
- cond do
- Map.get(conn.assigns, :skip_cache, false) ->
- conn
+ should_cache = not Map.get(conn.assigns, :skip_cache, false)
- !opts[:tracking_fun] ->
+ conn =
+ unless opts[:tracking_fun] do
+ if should_cache do
@cachex.put(:web_resp_cache, key, {content_type, body}, ttl: ttl)
- conn
+ end
+
+ conn
+ else
+ tracking_fun_data = Map.get(conn.assigns, :tracking_fun_data, nil)
- true ->
- tracking_fun_data = Map.get(conn.assigns, :tracking_fun_data, nil)
+ if should_cache do
@cachex.put(:web_resp_cache, key, {content_type, body, tracking_fun_data}, ttl: ttl)
+ end
- opts.tracking_fun.(conn, tracking_fun_data)
+ opts.tracking_fun.(conn, tracking_fun_data)
end
put_resp_header(conn, "x-cache", "MISS from Pleroma")
conn ->
conn
end)
end
defp content_type(conn) do
conn
|> Plug.Conn.get_resp_header("content-type")
|> hd()
end
defp send_cached(conn, {content_type, body}) do
conn
|> put_resp_content_type(content_type, nil)
|> put_resp_header("x-cache", "HIT from Pleroma")
|> send_resp(:ok, body)
|> halt()
end
defp render_error(conn, message) do
conn
|> put_status(:internal_server_error)
|> json(%{error: message})
|> halt()
end
end
diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
index 50315e21f..511405624 100644
--- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
@@ -1,2001 +1,2025 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
alias Pleroma.Activity
alias Pleroma.Delivery
alias Pleroma.Instances
alias Pleroma.Object
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Endpoint
alias Pleroma.Workers.ReceiverWorker
import Pleroma.Factory
require Pleroma.Constants
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup do: clear_config([:instance, :federating], true)
describe "/relay" do
setup do: clear_config([:instance, :allow_relay])
test "with the relay active, it returns the relay user", %{conn: conn} do
res =
conn
|> get(activity_pub_path(conn, :relay))
|> json_response(200)
assert res["id"] =~ "/relay"
end
test "with the relay disabled, it returns 404", %{conn: conn} do
clear_config([:instance, :allow_relay], false)
conn
|> get(activity_pub_path(conn, :relay))
|> json_response(404)
end
test "on non-federating instance, it returns 404", %{conn: conn} do
clear_config([:instance, :federating], false)
user = insert(:user)
conn
|> assign(:user, user)
|> get(activity_pub_path(conn, :relay))
|> json_response(404)
end
end
describe "/internal/fetch" do
test "it returns the internal fetch user", %{conn: conn} do
res =
conn
|> get(activity_pub_path(conn, :internal_fetch))
|> json_response(200)
assert res["id"] =~ "/fetch"
end
test "on non-federating instance, it returns 404", %{conn: conn} do
clear_config([:instance, :federating], false)
user = insert(:user)
conn
|> assign(:user, user)
|> get(activity_pub_path(conn, :internal_fetch))
|> json_response(404)
end
end
describe "/users/:nickname" do
test "it returns a json representation of the user with accept application/json", %{
conn: conn
} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/users/#{user.nickname}")
user = User.get_cached_by_id(user.id)
assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
end
test "it returns a json representation of the user with accept application/activity+json", %{
conn: conn
} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}")
user = User.get_cached_by_id(user.id)
assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
end
test "it returns a json representation of the user with accept application/ld+json", %{
conn: conn
} do
user = insert(:user)
conn =
conn
|> put_req_header(
"accept",
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
)
|> get("/users/#{user.nickname}")
user = User.get_cached_by_id(user.id)
assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
end
test "it returns 404 for remote users", %{
conn: conn
} do
user = insert(:user, local: false, nickname: "remoteuser@example.com")
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/users/#{user.nickname}.json")
assert json_response(conn, 404)
end
test "it returns error when user is not found", %{conn: conn} do
response =
conn
|> put_req_header("accept", "application/json")
|> get("/users/jimm")
|> json_response(404)
assert response == "Not found"
end
end
describe "mastodon compatibility routes" do
test "it returns a json representation of the object with accept application/json", %{
conn: conn
} do
{:ok, object} =
%{
"type" => "Note",
"content" => "hey",
"id" => Endpoint.url() <> "/users/raymoo/statuses/999999999",
"actor" => Endpoint.url() <> "/users/raymoo",
"to" => [Pleroma.Constants.as_public()]
}
|> Object.create()
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/users/raymoo/statuses/999999999")
assert json_response(conn, 200) == ObjectView.render("object.json", %{object: object})
end
test "it returns a json representation of the activity with accept application/json", %{
conn: conn
} do
{:ok, object} =
%{
"type" => "Note",
"content" => "hey",
"id" => Endpoint.url() <> "/users/raymoo/statuses/999999999",
"actor" => Endpoint.url() <> "/users/raymoo",
"to" => [Pleroma.Constants.as_public()]
}
|> Object.create()
{:ok, activity, _} =
%{
"id" => object.data["id"] <> "/activity",
"type" => "Create",
"object" => object.data["id"],
"actor" => object.data["actor"],
"to" => object.data["to"]
}
|> ActivityPub.persist(local: true)
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/users/raymoo/statuses/999999999/activity")
assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
end
end
describe "/objects/:uuid" do
test "it doesn't return a local-only object", %{conn: conn} do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
object = Object.normalize(post, fetch: false)
uuid = String.split(object.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/objects/#{uuid}")
assert json_response(conn, 404)
end
test "returns local-only objects when authenticated", %{conn: conn} do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
object = Object.normalize(post, fetch: false)
uuid = String.split(object.data["id"], "/") |> List.last()
assert response =
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(response, 200) == ObjectView.render("object.json", %{object: object})
end
test "it returns a json representation of the object with accept application/json", %{
conn: conn
} do
note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/objects/#{uuid}")
assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
end
test "it returns a json representation of the object with accept application/activity+json",
%{conn: conn} do
note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
end
test "it returns a json representation of the object with accept application/ld+json", %{
conn: conn
} do
note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header(
"accept",
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
)
|> get("/objects/#{uuid}")
assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
end
+ test "does not cache authenticated response", %{conn: conn} do
+ user = insert(:user)
+ reader = insert(:user)
+
+ {:ok, post} =
+ CommonAPI.post(user, %{status: "test @#{reader.nickname}", visibility: "local"})
+
+ object = Object.normalize(post, fetch: false)
+ uuid = String.split(object.data["id"], "/") |> List.last()
+
+ assert response =
+ conn
+ |> assign(:user, reader)
+ |> put_req_header("accept", "application/activity+json")
+ |> get("/objects/#{uuid}")
+
+ json_response(response, 200)
+
+ conn
+ |> put_req_header("accept", "application/activity+json")
+ |> get("/objects/#{uuid}")
+ |> json_response(404)
+ end
+
test "it returns 404 for non-public messages", %{conn: conn} do
note = insert(:direct_note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn, 404)
end
test "returns visible non-public messages when authenticated", %{conn: conn} do
note = insert(:direct_note)
uuid = String.split(note.data["id"], "/") |> List.last()
user = User.get_by_ap_id(note.data["actor"])
marisa = insert(:user)
assert conn
|> assign(:user, marisa)
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
|> json_response(404)
assert response =
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
|> json_response(200)
assert response == ObjectView.render("object.json", %{object: note})
end
test "it returns 404 for tombstone objects", %{conn: conn} do
tombstone = insert(:tombstone)
uuid = String.split(tombstone.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn, 404)
end
test "it caches a response", %{conn: conn} do
note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn1 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn1, :ok)
assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
conn2 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn1, :ok) == json_response(conn2, :ok)
assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
end
test "cached purged after object deletion", %{conn: conn} do
note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn1 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn1, :ok)
assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
Object.delete(note)
conn2 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert "Not found" == json_response(conn2, :not_found)
end
end
describe "/activities/:uuid" do
test "it doesn't return a local-only activity", %{conn: conn} do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
uuid = String.split(post.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/activities/#{uuid}")
assert json_response(conn, 404)
end
test "returns local-only activities when authenticated", %{conn: conn} do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
uuid = String.split(post.data["id"], "/") |> List.last()
assert response =
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert json_response(response, 200) == ObjectView.render("object.json", %{object: post})
end
test "it returns a json representation of the activity", %{conn: conn} do
activity = insert(:note_activity)
uuid = String.split(activity.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert json_response(conn, 200) == ObjectView.render("object.json", %{object: activity})
end
test "it returns 404 for non-public activities", %{conn: conn} do
activity = insert(:direct_note_activity)
uuid = String.split(activity.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert json_response(conn, 404)
end
test "returns visible non-public messages when authenticated", %{conn: conn} do
note = insert(:direct_note_activity)
uuid = String.split(note.data["id"], "/") |> List.last()
user = User.get_by_ap_id(note.data["actor"])
marisa = insert(:user)
assert conn
|> assign(:user, marisa)
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
|> json_response(404)
assert response =
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
|> json_response(200)
assert response == ObjectView.render("object.json", %{object: note})
end
test "it caches a response", %{conn: conn} do
activity = insert(:note_activity)
uuid = String.split(activity.data["id"], "/") |> List.last()
conn1 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert json_response(conn1, :ok)
assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
conn2 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert json_response(conn1, :ok) == json_response(conn2, :ok)
assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
end
test "cached purged after activity deletion", %{conn: conn} do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "cofe"})
uuid = String.split(activity.data["id"], "/") |> List.last()
conn1 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert json_response(conn1, :ok)
assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
Activity.delete_all_by_object_ap_id(activity.object.data["id"])
conn2 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert "Not found" == json_response(conn2, :not_found)
end
end
describe "/inbox" do
test "it inserts an incoming activity into the database", %{conn: conn} do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
assert "ok" == json_response(conn, 200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
end
@tag capture_log: true
test "it inserts an incoming activity into the database" <>
"even if we can't fetch the user but have it in our db",
%{conn: conn} do
user =
insert(:user,
ap_id: "https://mastodon.example.org/users/raymoo",
ap_enabled: true,
local: false,
last_refreshed_at: nil
)
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Jason.decode!()
|> Map.put("actor", user.ap_id)
|> put_in(["object", "attributedTo"], user.ap_id)
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
assert "ok" == json_response(conn, 200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
end
test "it clears `unreachable` federation status of the sender", %{conn: conn} do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
sender_url = data["actor"]
Instances.set_consistently_unreachable(sender_url)
refute Instances.reachable?(sender_url)
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
assert "ok" == json_response(conn, 200)
assert Instances.reachable?(sender_url)
end
test "accept follow activity", %{conn: conn} do
clear_config([:instance, :federating], true)
relay = Relay.get_actor()
assert {:ok, %Activity{} = activity} = Relay.follow("https://relay.mastodon.host/actor")
followed_relay = Pleroma.User.get_by_ap_id("https://relay.mastodon.host/actor")
relay = refresh_record(relay)
accept =
File.read!("test/fixtures/relay/accept-follow.json")
|> String.replace("{{ap_id}}", relay.ap_id)
|> String.replace("{{activity_id}}", activity.data["id"])
assert "ok" ==
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", accept)
|> json_response(200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Pleroma.FollowingRelationship.following?(
relay,
followed_relay
)
Mix.shell(Mix.Shell.Process)
on_exit(fn ->
Mix.shell(Mix.Shell.IO)
end)
:ok = Mix.Tasks.Pleroma.Relay.run(["list"])
assert_receive {:mix_shell, :info, ["https://relay.mastodon.host/actor"]}
end
@tag capture_log: true
test "without valid signature, " <>
"it only accepts Create activities and requires enabled federation",
%{conn: conn} do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Jason.decode!()
conn = put_req_header(conn, "content-type", "application/activity+json")
clear_config([:instance, :federating], false)
conn
|> post("/inbox", data)
|> json_response(403)
conn
|> post("/inbox", non_create_data)
|> json_response(403)
clear_config([:instance, :federating], true)
ret_conn = post(conn, "/inbox", data)
assert "ok" == json_response(ret_conn, 200)
conn
|> post("/inbox", non_create_data)
|> json_response(400)
end
test "accepts Add/Remove activities", %{conn: conn} do
object_id = "c61d6733-e256-4fe1-ab13-1e369789423f"
status =
File.read!("test/fixtures/statuses/note.json")
|> String.replace("{{nickname}}", "lain")
|> String.replace("{{object_id}}", object_id)
object_url = "https://example.com/objects/#{object_id}"
user =
File.read!("test/fixtures/users_mock/user.json")
|> String.replace("{{nickname}}", "lain")
actor = "https://example.com/users/lain"
Tesla.Mock.mock(fn
%{
method: :get,
url: ^object_url
} ->
%Tesla.Env{
status: 200,
body: status,
headers: [{"content-type", "application/activity+json"}]
}
%{
method: :get,
url: ^actor
} ->
%Tesla.Env{
status: 200,
body: user,
headers: [{"content-type", "application/activity+json"}]
}
%{method: :get, url: "https://example.com/users/lain/collections/featured"} ->
%Tesla.Env{
status: 200,
body:
"test/fixtures/users_mock/masto_featured.json"
|> File.read!()
|> String.replace("{{domain}}", "example.com")
|> String.replace("{{nickname}}", "lain"),
headers: [{"content-type", "application/activity+json"}]
}
end)
data = %{
"id" => "https://example.com/objects/d61d6733-e256-4fe1-ab13-1e369789423f",
"actor" => actor,
"object" => object_url,
"target" => "https://example.com/users/lain/collections/featured",
"type" => "Add",
"to" => [Pleroma.Constants.as_public()]
}
assert "ok" ==
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
|> json_response(200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
user = User.get_cached_by_ap_id(data["actor"])
assert user.pinned_objects[data["object"]]
data = %{
"id" => "https://example.com/objects/d61d6733-e256-4fe1-ab13-1e369789423d",
"actor" => actor,
"object" => object_url,
"target" => "https://example.com/users/lain/collections/featured",
"type" => "Remove",
"to" => [Pleroma.Constants.as_public()]
}
assert "ok" ==
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
|> json_response(200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
user = refresh_record(user)
refute user.pinned_objects[data["object"]]
end
test "mastodon pin/unpin", %{conn: conn} do
status_id = "105786274556060421"
status =
File.read!("test/fixtures/statuses/masto-note.json")
|> String.replace("{{nickname}}", "lain")
|> String.replace("{{status_id}}", status_id)
status_url = "https://example.com/users/lain/statuses/#{status_id}"
user =
File.read!("test/fixtures/users_mock/user.json")
|> String.replace("{{nickname}}", "lain")
actor = "https://example.com/users/lain"
Tesla.Mock.mock(fn
%{
method: :get,
url: ^status_url
} ->
%Tesla.Env{
status: 200,
body: status,
headers: [{"content-type", "application/activity+json"}]
}
%{
method: :get,
url: ^actor
} ->
%Tesla.Env{
status: 200,
body: user,
headers: [{"content-type", "application/activity+json"}]
}
%{method: :get, url: "https://example.com/users/lain/collections/featured"} ->
%Tesla.Env{
status: 200,
body:
"test/fixtures/users_mock/masto_featured.json"
|> File.read!()
|> String.replace("{{domain}}", "example.com")
|> String.replace("{{nickname}}", "lain"),
headers: [{"content-type", "application/activity+json"}]
}
end)
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"actor" => actor,
"object" => status_url,
"target" => "https://example.com/users/lain/collections/featured",
"type" => "Add"
}
assert "ok" ==
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
|> json_response(200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_object_ap_id_with_object(data["object"])
user = User.get_cached_by_ap_id(data["actor"])
assert user.pinned_objects[data["object"]]
data = %{
"actor" => actor,
"object" => status_url,
"target" => "https://example.com/users/lain/collections/featured",
"type" => "Remove"
}
assert "ok" ==
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
|> json_response(200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_object_ap_id_with_object(data["object"])
user = refresh_record(user)
refute user.pinned_objects[data["object"]]
end
end
describe "/users/:nickname/inbox" do
setup do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Jason.decode!()
[data: data]
end
test "it inserts an incoming activity into the database", %{conn: conn, data: data} do
user = insert(:user)
data =
data
|> Map.put("bcc", [user.ap_id])
|> Kernel.put_in(["object", "bcc"], [user.ap_id])
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
assert "ok" == json_response(conn, 200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
end
test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do
user = insert(:user)
data =
data
|> Map.put("to", user.ap_id)
|> Map.put("cc", [])
|> Kernel.put_in(["object", "to"], user.ap_id)
|> Kernel.put_in(["object", "cc"], [])
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
assert "ok" == json_response(conn, 200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
end
test "it accepts messages with cc as string instead of array", %{conn: conn, data: data} do
user = insert(:user)
data =
data
|> Map.put("to", [])
|> Map.put("cc", user.ap_id)
|> Kernel.put_in(["object", "to"], [])
|> Kernel.put_in(["object", "cc"], user.ap_id)
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
assert "ok" == json_response(conn, 200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
%Activity{} = activity = Activity.get_by_ap_id(data["id"])
assert user.ap_id in activity.recipients
end
test "it accepts messages with bcc as string instead of array", %{conn: conn, data: data} do
user = insert(:user)
data =
data
|> Map.put("to", [])
|> Map.put("cc", [])
|> Map.put("bcc", user.ap_id)
|> Kernel.put_in(["object", "to"], [])
|> Kernel.put_in(["object", "cc"], [])
|> Kernel.put_in(["object", "bcc"], user.ap_id)
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
assert "ok" == json_response(conn, 200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
end
test "it accepts announces with to as string instead of array", %{conn: conn} do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "hey"})
announcer = insert(:user, local: false)
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"actor" => announcer.ap_id,
"id" => "#{announcer.ap_id}/statuses/19512778738411822/activity",
"object" => post.data["object"],
"to" => "https://www.w3.org/ns/activitystreams#Public",
"cc" => [user.ap_id],
"type" => "Announce"
}
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
assert "ok" == json_response(conn, 200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
%Activity{} = activity = Activity.get_by_ap_id(data["id"])
assert "https://www.w3.org/ns/activitystreams#Public" in activity.recipients
end
test "it accepts messages from actors that are followed by the user", %{
conn: conn,
data: data
} do
recipient = insert(:user)
actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"})
{:ok, recipient, actor} = User.follow(recipient, actor)
object =
data["object"]
|> Map.put("attributedTo", actor.ap_id)
data =
data
|> Map.put("actor", actor.ap_id)
|> Map.put("object", object)
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{recipient.nickname}/inbox", data)
assert "ok" == json_response(conn, 200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Activity.get_by_ap_id(data["id"])
end
test "it rejects reads from other users", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
conn =
conn
|> assign(:user, other_user)
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/inbox")
assert json_response(conn, 403)
end
test "it returns a note activity in a collection", %{conn: conn} do
note_activity = insert(:direct_note_activity)
note_object = Object.normalize(note_activity, fetch: false)
user = User.get_cached_by_ap_id(hd(note_activity.data["to"]))
conn =
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/inbox?page=true")
assert response(conn, 200) =~ note_object.data["content"]
end
test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do
user = insert(:user)
data = Map.put(data, "bcc", [user.ap_id])
sender_host = URI.parse(data["actor"]).host
Instances.set_consistently_unreachable(sender_host)
refute Instances.reachable?(sender_host)
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
assert "ok" == json_response(conn, 200)
assert Instances.reachable?(sender_host)
end
@tag capture_log: true
test "it removes all follower collections but actor's", %{conn: conn} do
[actor, recipient] = insert_pair(:user)
to = [
recipient.ap_id,
recipient.follower_address,
"https://www.w3.org/ns/activitystreams#Public"
]
cc = [recipient.follower_address, actor.follower_address]
data = %{
"@context" => ["https://www.w3.org/ns/activitystreams"],
"type" => "Create",
"id" => Utils.generate_activity_id(),
"to" => to,
"cc" => cc,
"actor" => actor.ap_id,
"object" => %{
"type" => "Note",
"to" => to,
"cc" => cc,
"content" => "It's a note",
"attributedTo" => actor.ap_id,
"id" => Utils.generate_object_id()
}
}
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{recipient.nickname}/inbox", data)
|> json_response(200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert activity = Activity.get_by_ap_id(data["id"])
assert activity.id
assert actor.follower_address in activity.recipients
assert actor.follower_address in activity.data["cc"]
refute recipient.follower_address in activity.recipients
refute recipient.follower_address in activity.data["cc"]
refute recipient.follower_address in activity.data["to"]
end
test "it requires authentication", %{conn: conn} do
user = insert(:user)
conn = put_req_header(conn, "accept", "application/activity+json")
ret_conn = get(conn, "/users/#{user.nickname}/inbox")
assert json_response(ret_conn, 403)
ret_conn =
conn
|> assign(:user, user)
|> get("/users/#{user.nickname}/inbox")
assert json_response(ret_conn, 200)
end
@tag capture_log: true
test "forwarded report", %{conn: conn} do
admin = insert(:user, is_admin: true)
actor = insert(:user, local: false)
remote_domain = URI.parse(actor.ap_id).host
reported_user = insert(:user)
note = insert(:note_activity, user: reported_user)
data = %{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"https://#{remote_domain}/schemas/litepub-0.1.jsonld",
%{
"@language" => "und"
}
],
"actor" => actor.ap_id,
"cc" => [
reported_user.ap_id
],
"content" => "test",
"context" => "context",
"id" => "http://#{remote_domain}/activities/02be56cf-35e3-46b4-b2c6-47ae08dfee9e",
"nickname" => reported_user.nickname,
"object" => [
reported_user.ap_id,
%{
"actor" => %{
"actor_type" => "Person",
"approval_pending" => false,
"avatar" => "",
"confirmation_pending" => false,
"deactivated" => false,
"display_name" => "test user",
"id" => reported_user.id,
"local" => false,
"nickname" => reported_user.nickname,
"registration_reason" => nil,
"roles" => %{
"admin" => false,
"moderator" => false
},
"tags" => [],
"url" => reported_user.ap_id
},
"content" => "",
"id" => note.data["id"],
"published" => note.data["published"],
"type" => "Note"
}
],
"published" => note.data["published"],
"state" => "open",
"to" => [],
"type" => "Flag"
}
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{reported_user.nickname}/inbox", data)
|> json_response(200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
assert Pleroma.Repo.aggregate(Activity, :count, :id) == 2
ObanHelpers.perform_all()
Swoosh.TestAssertions.assert_email_sent(
to: {admin.name, admin.email},
html_body: ~r/Reported Account:/i
)
end
@tag capture_log: true
test "forwarded report from mastodon", %{conn: conn} do
admin = insert(:user, is_admin: true)
actor = insert(:user, local: false)
remote_domain = URI.parse(actor.ap_id).host
remote_actor = "https://#{remote_domain}/actor"
[reported_user, another] = insert_list(2, :user)
note = insert(:note_activity, user: reported_user)
Pleroma.Web.CommonAPI.favorite(another, note.id)
mock_json_body =
"test/fixtures/mastodon/application_actor.json"
|> File.read!()
|> String.replace("{{DOMAIN}}", remote_domain)
Tesla.Mock.mock(fn %{url: ^remote_actor} ->
%Tesla.Env{
status: 200,
body: mock_json_body,
headers: [{"content-type", "application/activity+json"}]
}
end)
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"actor" => remote_actor,
"content" => "test report",
"id" => "https://#{remote_domain}/e3b12fd1-948c-446e-b93b-a5e67edbe1d8",
"object" => [
reported_user.ap_id,
note.data["object"]
],
"type" => "Flag"
}
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{reported_user.nickname}/inbox", data)
|> json_response(200)
ObanHelpers.perform(all_enqueued(worker: ReceiverWorker))
flag_activity = "Flag" |> Pleroma.Activity.Queries.by_type() |> Pleroma.Repo.one()
reported_user_ap_id = reported_user.ap_id
[^reported_user_ap_id, flag_data] = flag_activity.data["object"]
Enum.each(~w(actor content id published type), &Map.has_key?(flag_data, &1))
ObanHelpers.perform_all()
Swoosh.TestAssertions.assert_email_sent(
to: {admin.name, admin.email},
html_body: ~r/#{note.data["object"]}/i
)
end
end
describe "GET /users/:nickname/outbox" do
test "it paginates correctly", %{conn: conn} do
user = insert(:user)
conn = assign(conn, :user, user)
outbox_endpoint = user.ap_id <> "/outbox"
_posts =
for i <- 0..25 do
{:ok, activity} = CommonAPI.post(user, %{status: "post #{i}"})
activity
end
result =
conn
|> put_req_header("accept", "application/activity+json")
|> get(outbox_endpoint <> "?page=true")
|> json_response(200)
result_ids = Enum.map(result["orderedItems"], fn x -> x["id"] end)
assert length(result["orderedItems"]) == 20
assert length(result_ids) == 20
assert result["next"]
assert String.starts_with?(result["next"], outbox_endpoint)
result_next =
conn
|> put_req_header("accept", "application/activity+json")
|> get(result["next"])
|> json_response(200)
result_next_ids = Enum.map(result_next["orderedItems"], fn x -> x["id"] end)
assert length(result_next["orderedItems"]) == 6
assert length(result_next_ids) == 6
refute Enum.find(result_next_ids, fn x -> x in result_ids end)
refute Enum.find(result_ids, fn x -> x in result_next_ids end)
assert String.starts_with?(result["id"], outbox_endpoint)
result_next_again =
conn
|> put_req_header("accept", "application/activity+json")
|> get(result_next["id"])
|> json_response(200)
assert result_next == result_next_again
end
test "it returns 200 even if there're no activities", %{conn: conn} do
user = insert(:user)
outbox_endpoint = user.ap_id <> "/outbox"
conn =
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get(outbox_endpoint)
result = json_response(conn, 200)
assert outbox_endpoint == result["id"]
end
test "it returns a note activity in a collection", %{conn: conn} do
note_activity = insert(:note_activity)
note_object = Object.normalize(note_activity, fetch: false)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
conn =
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/outbox?page=true")
assert response(conn, 200) =~ note_object.data["content"]
end
test "it returns an announce activity in a collection", %{conn: conn} do
announce_activity = insert(:announce_activity)
user = User.get_cached_by_ap_id(announce_activity.data["actor"])
conn =
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/outbox?page=true")
assert response(conn, 200) =~ announce_activity.data["object"]
end
test "It returns poll Answers when authenticated", %{conn: conn} do
poller = insert(:user)
voter = insert(:user)
{:ok, activity} =
CommonAPI.post(poller, %{
status: "suya...",
poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10}
})
assert question = Object.normalize(activity, fetch: false)
{:ok, [activity], _object} = CommonAPI.vote(voter, question, [1])
assert outbox_get =
conn
|> assign(:user, voter)
|> put_req_header("accept", "application/activity+json")
|> get(voter.ap_id <> "/outbox?page=true")
|> json_response(200)
assert [answer_outbox] = outbox_get["orderedItems"]
assert answer_outbox["id"] == activity.data["id"]
end
end
describe "POST /users/:nickname/outbox (C2S)" do
setup do: clear_config([:instance, :limit])
setup do
[
activity: %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"object" => %{
"type" => "Note",
"content" => "AP C2S test",
"to" => "https://www.w3.org/ns/activitystreams#Public",
"cc" => []
}
}
]
end
test "it rejects posts from other users / unauthenticated users", %{
conn: conn,
activity: activity
} do
user = insert(:user)
other_user = insert(:user)
conn = put_req_header(conn, "content-type", "application/activity+json")
conn
|> post("/users/#{user.nickname}/outbox", activity)
|> json_response(403)
conn
|> assign(:user, other_user)
|> post("/users/#{user.nickname}/outbox", activity)
|> json_response(403)
end
test "it inserts an incoming create activity into the database", %{
conn: conn,
activity: activity
} do
user = insert(:user)
result =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", activity)
|> json_response(201)
assert Activity.get_by_ap_id(result["id"])
assert result["object"]
assert %Object{data: object} = Object.normalize(result["object"], fetch: false)
assert object["content"] == activity["object"]["content"]
end
test "it rejects anything beyond 'Note' creations", %{conn: conn, activity: activity} do
user = insert(:user)
activity =
activity
|> put_in(["object", "type"], "Benis")
_result =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", activity)
|> json_response(400)
end
test "it inserts an incoming sensitive activity into the database", %{
conn: conn,
activity: activity
} do
user = insert(:user)
conn = assign(conn, :user, user)
object = Map.put(activity["object"], "sensitive", true)
activity = Map.put(activity, "object", object)
response =
conn
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", activity)
|> json_response(201)
assert Activity.get_by_ap_id(response["id"])
assert response["object"]
assert %Object{data: response_object} = Object.normalize(response["object"], fetch: false)
assert response_object["sensitive"] == true
assert response_object["content"] == activity["object"]["content"]
representation =
conn
|> put_req_header("accept", "application/activity+json")
|> get(response["id"])
|> json_response(200)
assert representation["object"]["sensitive"] == true
end
test "it rejects an incoming activity with bogus type", %{conn: conn, activity: activity} do
user = insert(:user)
activity = Map.put(activity, "type", "BadType")
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", activity)
assert json_response(conn, 400)
end
test "it erects a tombstone when receiving a delete activity", %{conn: conn} do
note_activity = insert(:note_activity)
note_object = Object.normalize(note_activity, fetch: false)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
data = %{
"type" => "Delete",
"object" => %{
"id" => note_object.data["id"]
}
}
result =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
|> json_response(201)
assert Activity.get_by_ap_id(result["id"])
assert object = Object.get_by_ap_id(note_object.data["id"])
assert object.data["type"] == "Tombstone"
end
test "it rejects delete activity of object from other actor", %{conn: conn} do
note_activity = insert(:note_activity)
note_object = Object.normalize(note_activity, fetch: false)
user = insert(:user)
data = %{
type: "Delete",
object: %{
id: note_object.data["id"]
}
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.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)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
data = %{
type: "Like",
object: %{
id: note_object.data["id"]
}
}
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
result = json_response(conn, 201)
assert Activity.get_by_ap_id(result["id"])
assert object = Object.get_by_ap_id(note_object.data["id"])
assert object.data["like_count"] == 1
end
test "it doesn't spreads faulty attributedTo or actor fields", %{
conn: conn,
activity: activity
} do
reimu = insert(:user, nickname: "reimu")
cirno = insert(:user, nickname: "cirno")
assert reimu.ap_id
assert cirno.ap_id
activity =
activity
|> put_in(["object", "actor"], reimu.ap_id)
|> put_in(["object", "attributedTo"], reimu.ap_id)
|> put_in(["actor"], reimu.ap_id)
|> put_in(["attributedTo"], reimu.ap_id)
_reimu_outbox =
conn
|> assign(:user, cirno)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{reimu.nickname}/outbox", activity)
|> json_response(403)
cirno_outbox =
conn
|> assign(:user, cirno)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{cirno.nickname}/outbox", activity)
|> json_response(201)
assert cirno_outbox["attributedTo"] == nil
assert cirno_outbox["actor"] == cirno.ap_id
assert cirno_object = Object.normalize(cirno_outbox["object"], fetch: false)
assert cirno_object.data["actor"] == cirno.ap_id
assert cirno_object.data["attributedTo"] == cirno.ap_id
end
test "Character limitation", %{conn: conn, activity: activity} do
clear_config([:instance, :limit], 5)
user = insert(:user)
result =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", activity)
|> json_response(400)
assert result == "Character limit (5 characters) exceeded, contains 11 characters"
end
end
describe "/relay/followers" do
test "it returns relay followers", %{conn: conn} do
relay_actor = Relay.get_actor()
user = insert(:user)
User.follow(user, relay_actor)
result =
conn
|> get("/relay/followers")
|> json_response(200)
assert result["first"]["orderedItems"] == [user.ap_id]
end
test "on non-federating instance, it returns 404", %{conn: conn} do
clear_config([:instance, :federating], false)
user = insert(:user)
conn
|> assign(:user, user)
|> get("/relay/followers")
|> json_response(404)
end
end
describe "/relay/following" do
test "it returns relay following", %{conn: conn} do
result =
conn
|> get("/relay/following")
|> json_response(200)
assert result["first"]["orderedItems"] == []
end
test "on non-federating instance, it returns 404", %{conn: conn} do
clear_config([:instance, :federating], false)
user = insert(:user)
conn
|> assign(:user, user)
|> get("/relay/following")
|> json_response(404)
end
end
describe "/users/:nickname/followers" do
test "it returns the followers in a collection", %{conn: conn} do
user = insert(:user)
user_two = insert(:user)
User.follow(user, user_two)
result =
conn
|> assign(:user, user_two)
|> get("/users/#{user_two.nickname}/followers")
|> json_response(200)
assert result["first"]["orderedItems"] == [user.ap_id]
end
test "it returns a uri if the user has 'hide_followers' set", %{conn: conn} do
user = insert(:user)
user_two = insert(:user, hide_followers: true)
User.follow(user, user_two)
result =
conn
|> assign(:user, user)
|> get("/users/#{user_two.nickname}/followers")
|> json_response(200)
assert is_binary(result["first"])
end
test "it returns a 403 error on pages, if the user has 'hide_followers' set and the request is from another user",
%{conn: conn} do
user = insert(:user)
other_user = insert(:user, hide_followers: true)
result =
conn
|> assign(:user, user)
|> get("/users/#{other_user.nickname}/followers?page=1")
assert result.status == 403
assert result.resp_body == ""
end
test "it renders the page, if the user has 'hide_followers' set and the request is authenticated with the same user",
%{conn: conn} do
user = insert(:user, hide_followers: true)
other_user = insert(:user)
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
result =
conn
|> assign(:user, user)
|> get("/users/#{user.nickname}/followers?page=1")
|> json_response(200)
assert result["totalItems"] == 1
assert result["orderedItems"] == [other_user.ap_id]
end
test "it works for more than 10 users", %{conn: conn} do
user = insert(:user)
Enum.each(1..15, fn _ ->
other_user = insert(:user)
User.follow(other_user, user)
end)
result =
conn
|> assign(:user, user)
|> get("/users/#{user.nickname}/followers")
|> json_response(200)
assert length(result["first"]["orderedItems"]) == 10
assert result["first"]["totalItems"] == 15
assert result["totalItems"] == 15
result =
conn
|> assign(:user, user)
|> get("/users/#{user.nickname}/followers?page=2")
|> json_response(200)
assert length(result["orderedItems"]) == 5
assert result["totalItems"] == 15
end
test "does not require authentication", %{conn: conn} do
user = insert(:user)
conn
|> get("/users/#{user.nickname}/followers")
|> json_response(200)
end
end
describe "/users/:nickname/following" do
test "it returns the following in a collection", %{conn: conn} do
user = insert(:user)
user_two = insert(:user)
User.follow(user, user_two)
result =
conn
|> assign(:user, user)
|> get("/users/#{user.nickname}/following")
|> json_response(200)
assert result["first"]["orderedItems"] == [user_two.ap_id]
end
test "it returns a uri if the user has 'hide_follows' set", %{conn: conn} do
user = insert(:user)
user_two = insert(:user, hide_follows: true)
User.follow(user, user_two)
result =
conn
|> assign(:user, user)
|> get("/users/#{user_two.nickname}/following")
|> json_response(200)
assert is_binary(result["first"])
end
test "it returns a 403 error on pages, if the user has 'hide_follows' set and the request is from another user",
%{conn: conn} do
user = insert(:user)
user_two = insert(:user, hide_follows: true)
result =
conn
|> assign(:user, user)
|> get("/users/#{user_two.nickname}/following?page=1")
assert result.status == 403
assert result.resp_body == ""
end
test "it renders the page, if the user has 'hide_follows' set and the request is authenticated with the same user",
%{conn: conn} do
user = insert(:user, hide_follows: true)
other_user = insert(:user)
{:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
result =
conn
|> assign(:user, user)
|> get("/users/#{user.nickname}/following?page=1")
|> json_response(200)
assert result["totalItems"] == 1
assert result["orderedItems"] == [other_user.ap_id]
end
test "it works for more than 10 users", %{conn: conn} do
user = insert(:user)
Enum.each(1..15, fn _ ->
user = User.get_cached_by_id(user.id)
other_user = insert(:user)
User.follow(user, other_user)
end)
result =
conn
|> assign(:user, user)
|> get("/users/#{user.nickname}/following")
|> json_response(200)
assert length(result["first"]["orderedItems"]) == 10
assert result["first"]["totalItems"] == 15
assert result["totalItems"] == 15
result =
conn
|> assign(:user, user)
|> get("/users/#{user.nickname}/following?page=2")
|> json_response(200)
assert length(result["orderedItems"]) == 5
assert result["totalItems"] == 15
end
test "does not require authentication", %{conn: conn} do
user = insert(:user)
conn
|> get("/users/#{user.nickname}/following")
|> json_response(200)
end
end
describe "delivery tracking" do
test "it tracks a signed object fetch", %{conn: conn} do
user = insert(:user, local: false)
activity = insert(:note_activity)
object = Object.normalize(activity, fetch: false)
object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
conn
|> put_req_header("accept", "application/activity+json")
|> assign(:user, user)
|> get(object_path)
|> json_response(200)
assert Delivery.get(object.id, user.id)
end
test "it tracks a signed activity fetch", %{conn: conn} do
user = insert(:user, local: false)
activity = insert(:note_activity)
object = Object.normalize(activity, fetch: false)
activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
conn
|> put_req_header("accept", "application/activity+json")
|> assign(:user, user)
|> get(activity_path)
|> json_response(200)
assert Delivery.get(object.id, user.id)
end
test "it tracks a signed object fetch when the json is cached", %{conn: conn} do
user = insert(:user, local: false)
other_user = insert(:user, local: false)
activity = insert(:note_activity)
object = Object.normalize(activity, fetch: false)
object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
conn
|> put_req_header("accept", "application/activity+json")
|> assign(:user, user)
|> get(object_path)
|> json_response(200)
build_conn()
|> put_req_header("accept", "application/activity+json")
|> assign(:user, other_user)
|> get(object_path)
|> json_response(200)
assert Delivery.get(object.id, user.id)
assert Delivery.get(object.id, other_user.id)
end
test "it tracks a signed activity fetch when the json is cached", %{conn: conn} do
user = insert(:user, local: false)
other_user = insert(:user, local: false)
activity = insert(:note_activity)
object = Object.normalize(activity, fetch: false)
activity_path = String.trim_leading(activity.data["id"], Pleroma.Web.Endpoint.url())
conn
|> put_req_header("accept", "application/activity+json")
|> assign(:user, user)
|> get(activity_path)
|> json_response(200)
build_conn()
|> put_req_header("accept", "application/activity+json")
|> assign(:user, other_user)
|> get(activity_path)
|> json_response(200)
assert Delivery.get(object.id, user.id)
assert Delivery.get(object.id, other_user.id)
end
end
describe "Additional ActivityPub C2S endpoints" do
test "GET /api/ap/whoami", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> get("/api/ap/whoami")
user = User.get_cached_by_id(user.id)
assert UserView.render("user.json", %{user: user}) == json_response(conn, 200)
conn
|> get("/api/ap/whoami")
|> json_response(403)
end
setup do: clear_config([:media_proxy])
setup do: clear_config([Pleroma.Upload])
test "POST /api/ap/upload_media", %{conn: conn} do
user = insert(:user)
desc = "Description of the image"
image = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
object =
conn
|> assign(:user, user)
|> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
|> json_response(:created)
assert object["name"] == desc
assert object["type"] == "Document"
assert object["actor"] == user.ap_id
assert [%{"href" => object_href, "mediaType" => object_mediatype}] = object["url"]
assert is_binary(object_href)
assert object_mediatype == "image/jpeg"
assert String.ends_with?(object_href, ".jpg")
activity_request = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"object" => %{
"type" => "Note",
"content" => "AP C2S test, attachment",
"attachment" => [object],
"to" => "https://www.w3.org/ns/activitystreams#Public",
"cc" => []
}
}
activity_response =
conn
|> assign(:user, user)
|> post("/users/#{user.nickname}/outbox", activity_request)
|> json_response(:created)
assert activity_response["id"]
assert activity_response["object"]
assert activity_response["actor"] == user.ap_id
assert %Object{data: %{"attachment" => [attachment]}} =
Object.normalize(activity_response["object"], fetch: false)
assert attachment["type"] == "Document"
assert attachment["name"] == desc
assert [
%{
"href" => ^object_href,
"type" => "Link",
"mediaType" => ^object_mediatype
}
] = attachment["url"]
# Fails if unauthenticated
conn
|> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
|> json_response(403)
end
end
test "pinned collection", %{conn: conn} do
clear_config([:instance, :max_pinned_statuses], 2)
user = insert(:user)
objects = insert_list(2, :note, user: user)
Enum.reduce(objects, user, fn %{data: %{"id" => object_id}}, user ->
{:ok, updated} = User.add_pinned_object_id(user, object_id)
updated
end)
%{nickname: nickname, featured_address: featured_address, pinned_objects: pinned_objects} =
refresh_record(user)
%{"id" => ^featured_address, "orderedItems" => items, "totalItems" => 2} =
conn
|> get("/users/#{nickname}/collections/featured")
|> json_response(200)
object_ids = Enum.map(items, & &1["id"])
assert Enum.all?(pinned_objects, fn {obj_id, _} ->
obj_id in object_ids
end)
end
end

File Metadata

Mime Type
text/x-diff
Expires
Fri, Aug 28, 12:20 PM (8 h, 34 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1736394
Default Alt Text
(86 KB)

Event Timeline