Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
178 KB
Referenced Files
None
Subscribers
None
diff --git a/.formatter.exs b/.formatter.exs
index 7fa95a619..5799ac127 100644
--- a/.formatter.exs
+++ b/.formatter.exs
@@ -1,3 +1,3 @@
[
- inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}", "priv/repo/migrations/*.exs"]
+ inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}", "priv/repo/migrations/*.exs", "priv/scrubbers/*.ex"]
]
diff --git a/lib/web/activity_pub/activity_pub_controller.ex b/lib/federation/activity_pub/controllers/activity_pub_controller.ex
similarity index 98%
rename from lib/web/activity_pub/activity_pub_controller.ex
rename to lib/federation/activity_pub/controllers/activity_pub_controller.ex
index 7ed956e9c..5146158ba 100644
--- a/lib/web/activity_pub/activity_pub_controller.ex
+++ b/lib/federation/activity_pub/controllers/activity_pub_controller.ex
@@ -1,509 +1,509 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.ActivityPub.ActivityPubController do
+defmodule Pleroma.Federation.ActivityPub.ActivityPubController do
use Pleroma.Web, :controller
alias Pleroma.Activity
alias Pleroma.Delivery
alias Pleroma.Federation.ActivityPub
alias Pleroma.Federation.ActivityPub.Federator
alias Pleroma.Federation.ActivityPub.InternalFetchActor
alias Pleroma.Federation.ActivityPub.Relay
alias Pleroma.Federation.ActivityPub.Transmogrifier
alias Pleroma.Federation.ActivityPub.Utils
alias Pleroma.Federation.ActivityPub.Visibility
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ObjectView
- alias Pleroma.Web.ActivityPub.UserView
+ alias Pleroma.Federation.ActivityPub.ObjectView
+ alias Pleroma.Federation.ActivityPub.UserView
require Logger
action_fallback(:errors)
plug(
Pleroma.Web.CachePlug,
[query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
when action in [:activity, :object]
)
plug(Pleroma.Federation.ActivityPub.FederatingPlug when action in [:inbox, :relay])
plug(:set_requester_reachable when action in [:inbox])
plug(:relay_active? when action in [:relay])
def 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(conn, %{"uuid" => uuid}) do
with ap_id <- o_status_url(conn, :object, uuid),
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
{_, true} <- {:public?, Visibility.is_public?(object)} do
conn
|> 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
{:public?, false} ->
{: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(conn, %{"uuid" => uuid}) do
with ap_id <- o_status_url(conn, :activity, uuid),
%Activity{} = activity <- Activity.normalize(ap_id),
{_, true} <- {:public?, Visibility.is_public?(activity)} do
conn
|> 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
{:public?, 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).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
# GET /relay/following
def following(%{assigns: %{relay: true}} = conn, _params) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("following.json", %{user: Relay.get_actor()})
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 followers(%{assigns: %{relay: true}} = conn, _params) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("followers.json", %{user: Relay.get_actor()})
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(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
activities =
if params["max_id"] do
ActivityPub.fetch_user_activities(user, nil, %{
"max_id" => params["max_id"],
# This 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
"include_poll_votes" => true,
"limit" => 10
})
else
ActivityPub.fetch_user_activities(user, nil, %{
"limit" => 10,
"include_poll_votes" => true
})
end
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("activity_collection_page.json", %{
activities: 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
# only accept relayed Creates
def inbox(conn, %{"type" => "Create"} = 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
def inbox(conn, params) do
headers = Enum.into(conn.req_headers, %{})
if 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
json(conn, 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 whoami(_conn, _params), do: {:error, :not_found}
def read_inbox(
%{assigns: %{user: %{nickname: nickname} = user}} = conn,
%{"nickname" => nickname, "page" => page?} = params
)
when page? in [true, "true"] do
activities =
if params["max_id"] do
ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{
"max_id" => params["max_id"],
"limit" => 10
})
else
ActivityPub.fetch_activities([user.ap_id | User.following(user)], %{"limit" => 10})
end
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("activity_collection_page.json", %{
activities: activities,
iri: "#{user.ap_id}/inbox"
})
end
def read_inbox(%{assigns: %{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: nil}} = conn, %{"nickname" => nickname}) do
err = dgettext("errors", "can't read inbox of %{nickname}", nickname: nickname)
conn
|> put_status(:forbidden)
|> json(err)
end
def read_inbox(%{assigns: %{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
def handle_user_activity(user, %{"type" => "Create"} = params) do
object =
params["object"]
|> Map.merge(Map.take(params, ["to", "cc"]))
|> Map.put("attributedTo", user.ap_id())
|> Transmogrifier.fix_object()
ActivityPub.create(%{
to: params["to"],
actor: user,
context: object["context"],
object: object,
additional: Map.take(params, ["cc"])
})
end
def handle_user_activity(user, %{"type" => "Delete"} = params) do
with %Object{} = object <- Object.normalize(params["object"]),
true <- user.is_moderator || user.ap_id == object.data["actor"],
{:ok, delete} <- ActivityPub.delete(object) do
{:ok, delete}
else
_ -> {:error, dgettext("errors", "Can't delete object")}
end
end
def handle_user_activity(user, %{"type" => "Like"} = params) do
with %Object{} = object <- Object.normalize(params["object"]),
{:ok, activity, _object} <- ActivityPub.like(user, object) do
{:ok, activity}
else
_ -> {:error, dgettext("errors", "Can't like object")}
end
end
def handle_user_activity(_, _) do
{:error, dgettext("errors", "Unhandled activity type")}
end
def update_outbox(
%{assigns: %{user: %User{nickname: nickname} = user}} = conn,
%{"nickname" => nickname} = params
) do
actor = user.ap_id()
params =
params
|> Map.drop(["id"])
|> Map.put("actor", actor)
|> Transmogrifier.fix_addressing()
with {:ok, %Activity{} = activity} <- handle_user_activity(user, params) do
conn
|> put_status(:created)
|> put_resp_header("location", activity.data["id"])
|> json(activity.data)
else
{:error, message} ->
conn
|> put_status(:bad_request)
|> json(message)
end
end
def update_outbox(%{assigns: %{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
def errors(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> json(dgettext("errors", "Not found"))
end
def 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
# TODO: Add support for "object" field
@doc """
Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>
Parameters:
- (required) `file`: data of the media
- (optionnal) `description`: description of the media, intended for accessibility
Response:
- HTTP Code: 201 Created
- HTTP Body: ActivityPub object to be inserted into another's `attachment` field
"""
def upload_media(%{assigns: %{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
end
diff --git a/lib/web/activity_pub/views/object_view.ex b/lib/federation/activity_pub/views/object_view.ex
similarity index 95%
rename from lib/web/activity_pub/views/object_view.ex
rename to lib/federation/activity_pub/views/object_view.ex
index 464ad4db4..47e3ae8d1 100644
--- a/lib/web/activity_pub/views/object_view.ex
+++ b/lib/federation/activity_pub/views/object_view.ex
@@ -1,41 +1,41 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.ActivityPub.ObjectView do
+defmodule Pleroma.Federation.ActivityPub.ObjectView do
use Pleroma.Web, :view
alias Pleroma.Activity
alias Pleroma.Federation.ActivityPub.Transmogrifier
alias Pleroma.Federation.ActivityPub.Utils
alias Pleroma.Object
def render("object.json", %{object: %Object{} = object}) do
base = Utils.make_json_ld_header()
additional = Transmogrifier.prepare_object(object.data)
Map.merge(base, additional)
end
def render("object.json", %{object: %Activity{data: %{"type" => activity_type}} = activity})
when activity_type in ["Create", "Listen"] do
base = Utils.make_json_ld_header()
object = Object.normalize(activity)
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 = Utils.make_json_ld_header()
object = Object.normalize(activity)
additional =
Transmogrifier.prepare_object(activity.data)
|> Map.put("object", object.data["id"])
Map.merge(base, additional)
end
end
diff --git a/lib/web/activity_pub/views/user_view.ex b/lib/federation/activity_pub/views/user_view.ex
similarity index 99%
rename from lib/web/activity_pub/views/user_view.ex
rename to lib/federation/activity_pub/views/user_view.ex
index 6c50884b1..b9601882b 100644
--- a/lib/web/activity_pub/views/user_view.ex
+++ b/lib/federation/activity_pub/views/user_view.ex
@@ -1,294 +1,294 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.ActivityPub.UserView do
+defmodule Pleroma.Federation.ActivityPub.UserView do
use Pleroma.Web, :view
alias Pleroma.Crypto.Keys
alias Pleroma.Federation.ActivityPub.Transmogrifier
alias Pleroma.Federation.ActivityPub.Utils
alias Pleroma.HTML
alias Pleroma.Storage.Repo
alias Pleroma.User
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router.Helpers
import Ecto.Query
def render("endpoints.json", %{user: %User{nickname: nil, local: true} = _user}) do
%{"sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox)}
end
def render("endpoints.json", %{user: %User{local: true} = _user}) do
%{
"oauthAuthorizationEndpoint" => Helpers.o_auth_url(Endpoint, :authorize),
"oauthRegistrationEndpoint" => Helpers.app_url(Endpoint, :create),
"oauthTokenEndpoint" => Helpers.o_auth_url(Endpoint, :token_exchange),
"sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox),
"uploadMedia" => Helpers.activity_pub_url(Endpoint, :upload_media)
}
end
def render("endpoints.json", _), do: %{}
def render("service.json", %{user: user}) do
{:ok, user} = User.ensure_keys_present(user)
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
endpoints = render("endpoints.json", %{user: user})
%{
"id" => user.ap_id,
"type" => "Application",
"following" => "#{user.ap_id}/following",
"followers" => "#{user.ap_id}/followers",
"inbox" => "#{user.ap_id}/inbox",
"name" => "Pleroma",
"summary" =>
"An internal service actor for this Pleroma instance. No user-serviceable parts inside.",
"url" => user.ap_id,
"manuallyApprovesFollowers" => false,
"publicKey" => %{
"id" => "#{user.ap_id}#main-key",
"owner" => user.ap_id,
"publicKeyPem" => public_key
},
"endpoints" => endpoints,
"invisible" => User.invisible?(user)
}
|> Map.merge(Utils.make_json_ld_header())
end
# the instance itself is not a Person, but instead an Application
def render("user.json", %{user: %User{nickname: nil} = user}),
do: render("service.json", %{user: user})
def render("user.json", %{user: %User{nickname: "internal." <> _} = user}),
do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname)
def render("user.json", %{user: user}) do
{:ok, user} = User.ensure_keys_present(user)
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
endpoints = render("endpoints.json", %{user: user})
emoji_tags = Transmogrifier.take_emoji_tags(user)
fields =
user
|> User.fields()
|> Enum.map(fn %{"name" => name, "value" => value} ->
%{
"name" => HTML.strip_tags(name),
"value" => HTML.filter_tags(value, HTML.Scrubber.LinksOnly)
}
end)
|> Enum.map(&Map.put(&1, "type", "PropertyValue"))
%{
"id" => user.ap_id,
"type" => user.actor_type,
"following" => "#{user.ap_id}/following",
"followers" => "#{user.ap_id}/followers",
"inbox" => "#{user.ap_id}/inbox",
"outbox" => "#{user.ap_id}/outbox",
"preferredUsername" => user.nickname,
"name" => user.name,
"summary" => user.bio,
"url" => user.ap_id,
"manuallyApprovesFollowers" => user.locked,
"publicKey" => %{
"id" => "#{user.ap_id}#main-key",
"owner" => user.ap_id,
"publicKeyPem" => public_key
},
"endpoints" => endpoints,
"attachment" => fields,
"tag" => (user.source_data["tag"] || []) ++ emoji_tags,
"discoverable" => user.discoverable
}
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
|> Map.merge(Utils.make_json_ld_header())
end
def render("following.json", %{user: user, page: page} = opts) do
showing_items = (opts[:for] && opts[:for] == user) || !user.hide_follows
showing_count = showing_items || !user.hide_follows_count
query = User.get_friends_query(user)
query = from(user in query, select: [:ap_id])
following = Repo.all(query)
total =
if showing_count do
length(following)
else
0
end
collection(following, "#{user.ap_id}/following", page, showing_items, total)
|> Map.merge(Utils.make_json_ld_header())
end
def render("following.json", %{user: user} = opts) do
showing_items = (opts[:for] && opts[:for] == user) || !user.hide_follows
showing_count = showing_items || !user.hide_follows_count
query = User.get_friends_query(user)
query = from(user in query, select: [:ap_id])
following = Repo.all(query)
total =
if showing_count do
length(following)
else
0
end
%{
"id" => "#{user.ap_id}/following",
"type" => "OrderedCollection",
"totalItems" => total,
"first" =>
if showing_items do
collection(following, "#{user.ap_id}/following", 1, !user.hide_follows)
else
"#{user.ap_id}/following?page=1"
end
}
|> Map.merge(Utils.make_json_ld_header())
end
def render("followers.json", %{user: user, page: page} = opts) do
showing_items = (opts[:for] && opts[:for] == user) || !user.hide_followers
showing_count = showing_items || !user.hide_followers_count
query = User.get_followers_query(user)
query = from(user in query, select: [:ap_id])
followers = Repo.all(query)
total =
if showing_count do
length(followers)
else
0
end
collection(followers, "#{user.ap_id}/followers", page, showing_items, total)
|> Map.merge(Utils.make_json_ld_header())
end
def render("followers.json", %{user: user} = opts) do
showing_items = (opts[:for] && opts[:for] == user) || !user.hide_followers
showing_count = showing_items || !user.hide_followers_count
query = User.get_followers_query(user)
query = from(user in query, select: [:ap_id])
followers = Repo.all(query)
total =
if showing_count do
length(followers)
else
0
end
%{
"id" => "#{user.ap_id}/followers",
"type" => "OrderedCollection",
"first" =>
if showing_items do
collection(followers, "#{user.ap_id}/followers", 1, showing_items, total)
else
"#{user.ap_id}/followers?page=1"
end
}
|> maybe_put_total_items(showing_count, total)
|> Map.merge(Utils.make_json_ld_header())
end
def render("activity_collection.json", %{iri: iri}) do
%{
"id" => iri,
"type" => "OrderedCollection",
"first" => "#{iri}?page=true"
}
|> Map.merge(Utils.make_json_ld_header())
end
def render("activity_collection_page.json", %{activities: activities, iri: iri}) do
# this is sorted chronologically, so first activity is the newest (max)
{max_id, min_id, collection} =
if length(activities) > 0 do
{
Enum.at(activities, 0).id,
Enum.at(Enum.reverse(activities), 0).id,
Enum.map(activities, fn act ->
{:ok, data} = Transmogrifier.prepare_outgoing(act.data)
data
end)
}
else
{
0,
0,
[]
}
end
%{
"id" => "#{iri}?max_id=#{max_id}&page=true",
"type" => "OrderedCollectionPage",
"partOf" => iri,
"orderedItems" => collection,
"next" => "#{iri}?max_id=#{min_id}&page=true"
}
|> Map.merge(Utils.make_json_ld_header())
end
defp maybe_put_total_items(map, false, _total), do: map
defp maybe_put_total_items(map, true, total) do
Map.put(map, "totalItems", total)
end
def collection(collection, iri, page, show_items \\ true, total \\ nil) do
offset = (page - 1) * 10
items = Enum.slice(collection, offset, 10)
items = Enum.map(items, fn user -> user.ap_id end)
total = total || length(collection)
map = %{
"id" => "#{iri}?page=#{page}",
"type" => "OrderedCollectionPage",
"partOf" => iri,
"totalItems" => total,
"orderedItems" => if(show_items, do: items, else: [])
}
if offset < total do
Map.put(map, "next", "#{iri}?page=#{page + 1}")
else
map
end
end
defp maybe_make_image(func, key, user) do
if image = func.(user, no_default: true) do
%{
key => %{
"type" => "Image",
"url" => image
}
}
else
%{}
end
end
end
diff --git a/lib/federation/ostatus/ostatus_controller.ex b/lib/federation/ostatus/ostatus_controller.ex
index c81a3641a..5561fa1a5 100644
--- a/lib/federation/ostatus/ostatus_controller.ex
+++ b/lib/federation/ostatus/ostatus_controller.ex
@@ -1,147 +1,147 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Federation.OStatus.OStatusController do
use Pleroma.Web, :controller
alias Pleroma.Activity
alias Pleroma.Federation.ActivityPub.Visibility
alias Pleroma.Object
alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ActivityPubController
+ alias Pleroma.Federation.ActivityPub.ActivityPubController
alias Pleroma.Web.Endpoint
alias Pleroma.Web.FallbackRedirectController
alias Pleroma.Web.Metadata.PlayerView
alias Pleroma.Web.RateLimiterPlug
alias Pleroma.Web.Router
plug(
RateLimiterPlug,
[name: :ap_routes, params: ["uuid"]] when action in [:object, :activity]
)
plug(
Pleroma.Web.SetFormatPlug
when action in [:object, :activity, :notice]
)
action_fallback(:errors)
def object(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid})
when format in ["json", "activity+json"] do
ActivityPubController.call(conn, :object)
end
def object(%{assigns: %{format: format}} = conn, %{"uuid" => uuid}) do
with id <- o_status_url(conn, :object, uuid),
{_, %Activity{} = activity} <-
{:activity, Activity.get_create_by_object_ap_id_with_object(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)} do
case format do
_ -> redirect(conn, to: "/notice/#{activity.id}")
end
else
reason when reason in [{:public?, false}, {:activity, nil}] ->
{:error, :not_found}
e ->
e
end
end
def activity(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid})
when format in ["json", "activity+json"] do
ActivityPubController.call(conn, :activity)
end
def activity(%{assigns: %{format: format}} = conn, %{"uuid" => uuid}) do
with id <- o_status_url(conn, :activity, uuid),
{_, %Activity{} = activity} <- {:activity, Activity.normalize(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)} do
case format do
_ -> redirect(conn, to: "/notice/#{activity.id}")
end
else
reason when reason in [{:public?, false}, {:activity, nil}] ->
{:error, :not_found}
e ->
e
end
end
def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do
with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id_with_object(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)},
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
cond do
format in ["json", "activity+json"] ->
if activity.local do
%{data: %{"id" => redirect_url}} = Object.normalize(activity)
redirect(conn, external: redirect_url)
else
{:error, :not_found}
end
activity.data["type"] == "Create" ->
%Object{} = object = Object.normalize(activity)
FallbackRedirectController.redirector_with_meta(
conn,
%{
activity_id: activity.id,
object: object,
url: Router.Helpers.o_status_url(Endpoint, :notice, activity.id),
user: user
}
)
true ->
FallbackRedirectController.redirector(conn, nil)
end
else
reason when reason in [{:public?, false}, {:activity, nil}] ->
conn
|> put_status(404)
|> FallbackRedirectController.redirector(nil, 404)
e ->
e
end
end
# Returns an HTML embedded <audio> or <video> player suitable for embed iframes.
def notice_player(conn, %{"id" => id}) do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.is_public?(activity),
%Object{} = object <- Object.normalize(activity),
%{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object,
true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do
conn
|> put_layout(:metadata_player)
|> put_resp_header("x-frame-options", "ALLOW")
|> put_resp_header(
"content-security-policy",
"default-src 'none';style-src 'self' 'unsafe-inline';img-src 'self' data: https:; media-src 'self' https:;"
)
|> put_view(PlayerView)
|> render("player.html", url)
else
_error ->
conn
|> put_status(404)
|> FallbackRedirectController.redirector(nil, 404)
end
end
def errors(conn, {:error, :not_found}) do
render_error(conn, :not_found, "Not found")
end
def errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found})
def errors(conn, _) do
render_error(conn, :internal_server_error, "Something went wrong")
end
end
diff --git a/lib/web/common_api/common_api.ex b/lib/web/common_api/common_api.ex
index 95fe63c88..335a1404a 100644
--- a/lib/web/common_api/common_api.ex
+++ b/lib/web/common_api/common_api.ex
@@ -1,442 +1,442 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.Conversation.Participation
alias Pleroma.Federation.ActivityPub
alias Pleroma.Federation.ActivityPub.Utils
alias Pleroma.Federation.ActivityPub.Visibility
alias Pleroma.FollowingRelationship
alias Pleroma.Helpers.Constants
alias Pleroma.Object
alias Pleroma.ThreadMute
alias Pleroma.User
alias Pleroma.UserRelationship
- alias Pleroma.Web.ActivityPub.UserView
+ alias Pleroma.Federation.ActivityPub.UserView
import Pleroma.Web.Gettext
import Pleroma.Web.CommonAPI.Utils
require Pleroma.Helpers.Constants
def follow(follower, followed) do
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, activity} <- ActivityPub.follow(follower, followed),
{:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
{:ok, follower, followed, activity}
end
end
def unfollow(follower, unfollowed) do
with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
{:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
{:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
{:ok, follower}
end
end
def accept_follow_request(follower, followed) do
with {:ok, follower} <- User.follow(follower, followed),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept"),
{:ok, _activity} <-
ActivityPub.accept(%{
to: [follower.ap_id],
actor: followed,
object: follow_activity.data["id"],
type: "Accept"
}) do
{:ok, follower}
end
end
def reject_follow_request(follower, followed) do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"),
{:ok, _activity} <-
ActivityPub.reject(%{
to: [follower.ap_id],
actor: followed,
object: follow_activity.data["id"],
type: "Reject"
}) do
{:ok, follower}
end
end
def delete(activity_id, user) do
with %Activity{data: %{"object" => _}} = activity <-
Activity.get_by_id_with_object(activity_id),
%Object{} = object <- Object.normalize(activity),
true <- User.superuser?(user) || user.ap_id == object.data["actor"],
{:ok, _} <- unpin(activity_id, user),
{:ok, delete} <- ActivityPub.delete(object) do
{:ok, delete}
else
_ -> {:error, dgettext("errors", "Could not delete")}
end
end
def repeat(id_or_ap_id, user, params \\ %{}) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
object <- Object.normalize(activity),
announce_activity <- Utils.get_existing_announce(user.ap_id, object),
public <- public_announce?(object, params) do
if announce_activity do
{:ok, announce_activity, object}
else
ActivityPub.announce(user, object, nil, true, public)
end
else
_ -> {:error, dgettext("errors", "Could not repeat")}
end
end
def unrepeat(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
object = Object.normalize(activity)
ActivityPub.unannounce(user, object)
else
_ -> {:error, dgettext("errors", "Could not unrepeat")}
end
end
def favorite(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
object <- Object.normalize(activity),
like_activity <- Utils.get_existing_like(user.ap_id, object) do
if like_activity do
{:ok, like_activity, object}
else
ActivityPub.like(user, object)
end
else
_ -> {:error, dgettext("errors", "Could not favorite")}
end
end
def unfavorite(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
object = Object.normalize(activity)
ActivityPub.unlike(user, object)
else
_ -> {:error, dgettext("errors", "Could not unfavorite")}
end
end
def react_with_emoji(id, user, emoji) do
with %Activity{} = activity <- Activity.get_by_id(id),
object <- Object.normalize(activity) do
ActivityPub.react_with_emoji(user, object, emoji)
else
_ ->
{:error, dgettext("errors", "Could not add reaction emoji")}
end
end
def unreact_with_emoji(id, user, emoji) do
with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji) do
ActivityPub.unreact_with_emoji(user, reaction_activity.data["id"])
else
_ ->
{:error, dgettext("errors", "Could not remove reaction emoji")}
end
end
def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
with :ok <- validate_not_author(object, user),
:ok <- validate_existing_votes(user, object),
{:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
answer_activities =
Enum.map(choices, fn index ->
answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
{:ok, activity} =
ActivityPub.create(%{
to: answer_data["to"],
actor: user,
context: object.data["context"],
object: answer_data,
additional: %{"cc" => answer_data["cc"]}
})
activity
end)
object = Object.get_cached_by_ap_id(object.data["id"])
{:ok, answer_activities, object}
end
end
defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
do: {:error, dgettext("errors", "Poll's author can't vote")}
defp validate_not_author(_, _), do: :ok
defp validate_existing_votes(%{ap_id: ap_id}, object) do
if Utils.get_existing_votes(ap_id, object) == [] do
:ok
else
{:error, dgettext("errors", "Already voted")}
end
end
defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}
defp normalize_and_validate_choices(choices, object) do
choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
{options, max_count} = get_options_and_max_count(object)
count = Enum.count(options)
with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
{_, true} <- {:count_check, Enum.count(choices) <= max_count} do
{:ok, options, choices}
else
{:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
{:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
end
end
def public_announce?(_, %{"visibility" => visibility})
when visibility in ~w{public unlisted private direct},
do: visibility in ~w(public unlisted)
def public_announce?(object, _) do
Visibility.is_public?(object)
end
def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
when visibility in ~w{public unlisted private direct},
do: {visibility, get_replied_to_visibility(in_reply_to)}
def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
visibility = {:list, String.to_integer(list_id)}
{visibility, get_replied_to_visibility(in_reply_to)}
end
def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
visibility = get_replied_to_visibility(in_reply_to)
{visibility, visibility}
end
def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
def get_replied_to_visibility(nil), do: nil
def get_replied_to_visibility(activity) do
with %Object{} = object <- Object.normalize(activity) do
Visibility.get_visibility(object)
end
end
def check_expiry_date({:ok, nil} = res), do: res
def check_expiry_date({:ok, in_seconds}) do
expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)
if ActivityExpiration.expires_late_enough?(expiry) do
{:ok, expiry}
else
{:error, "Expiry date is too soon"}
end
end
def check_expiry_date(expiry_str) do
Ecto.Type.cast(:integer, expiry_str)
|> check_expiry_date()
end
def listen(user, %{"title" => _} = data) do
with visibility <- data["visibility"] || "public",
{to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
listen_data <-
Map.take(data, ["album", "artist", "title", "length"])
|> Map.put("type", "Audio")
|> Map.put("to", to)
|> Map.put("cc", cc)
|> Map.put("actor", user.ap_id),
{:ok, activity} <-
ActivityPub.listen(%{
actor: user,
to: to,
object: listen_data,
context: Utils.generate_context_id(),
additional: %{"cc" => cc}
}) do
{:ok, activity}
end
end
def post(user, %{"status" => _} = data) do
with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
draft.changes
|> ActivityPub.create(draft.preview?)
|> maybe_create_activity_expiration(draft.expires_at)
end
end
defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
{:ok, activity}
end
end
defp maybe_create_activity_expiration(result, _), do: result
# Updates the emojis for a user based on their profile
def update(user) do
emoji = emoji_from_profile(user)
source_data = Map.put(user.source_data, "tag", emoji)
user =
case User.update_source_data(user, source_data) do
{:ok, user} -> user
_ -> user
end
ActivityPub.update(%{
local: true,
to: [Constants.as_public(), user.follower_address],
cc: [],
actor: user.ap_id,
object: UserView.render("user.json", %{user: user})
})
end
def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
with %Activity{
actor: ^user_ap_id,
data: %{"type" => "Create"},
object: %Object{data: %{"type" => object_type}}
} = activity <- get_by_id_or_ap_id(id_or_ap_id),
true <- object_type in ["Note", "Article", "Question"],
true <- Visibility.is_public?(activity),
{:ok, _user} <- User.add_pinnned_activity(user, activity) do
{:ok, activity}
else
{:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
_ -> {:error, dgettext("errors", "Could not pin")}
end
end
def unpin(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
{:ok, _user} <- User.remove_pinnned_activity(user, activity) do
{:ok, activity}
else
{:error, %{errors: [pinned_activities: {err, _}]}} -> {:error, err}
_ -> {:error, dgettext("errors", "Could not unpin")}
end
end
def add_mute(user, activity) do
with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
{:ok, activity}
else
{:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
end
end
def remove_mute(user, activity) do
ThreadMute.remove_mute(user.id, activity.data["context"])
{:ok, activity}
end
def thread_muted?(%{id: nil} = _user, _activity), do: false
def thread_muted?(user, activity) do
ThreadMute.check_muted(user.id, activity.data["context"]) != []
end
def report(user, %{"account_id" => account_id} = data) do
with {:ok, account} <- get_reported_account(account_id),
{:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
{:ok, statuses} <- get_report_statuses(account, data) do
ActivityPub.flag(%{
context: Utils.generate_context_id(),
actor: user,
account: account,
statuses: statuses,
content: content_html,
forward: data["forward"] || false
})
end
end
def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}
defp get_reported_account(account_id) do
case User.get_cached_by_id(account_id) do
%User{} = account -> {:ok, account}
_ -> {:error, dgettext("errors", "Account not found")}
end
end
def update_report_state(activity_ids, state) when is_list(activity_ids) do
case Utils.update_report_state(activity_ids, state) do
:ok -> {:ok, activity_ids}
_ -> {:error, dgettext("errors", "Could not update state")}
end
end
def update_report_state(activity_id, state) do
with %Activity{} = activity <- Activity.get_by_id(activity_id) do
Utils.update_report_state(activity, state)
else
nil -> {:error, :not_found}
_ -> {:error, dgettext("errors", "Could not update state")}
end
end
def update_activity_scope(activity_id, opts \\ %{}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
{:ok, activity} <- toggle_sensitive(activity, opts) do
set_visibility(activity, opts)
else
nil -> {:error, :not_found}
{:error, reason} -> {:error, reason}
end
end
defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
end
defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
when is_boolean(sensitive) do
new_data = Map.put(object.data, "sensitive", sensitive)
{:ok, object} =
object
|> Object.change(%{data: new_data})
|> Object.update_and_set_cache()
{:ok, Map.put(activity, :object, object)}
end
defp toggle_sensitive(activity, _), do: {:ok, activity}
defp set_visibility(activity, %{"visibility" => visibility}) do
Utils.update_activity_visibility(activity, visibility)
end
defp set_visibility(activity, _), do: {:ok, activity}
def hide_reblogs(%User{} = user, %User{} = target) do
UserRelationship.create_reblog_mute(user, target)
end
def show_reblogs(%User{} = user, %User{} = target) do
UserRelationship.delete_reblog_mute(user, target)
end
end
diff --git a/lib/web/feed/user_controller.ex b/lib/web/feed/user_controller.ex
index 257f4e4f3..7c46ebb66 100644
--- a/lib/web/feed/user_controller.ex
+++ b/lib/web/feed/user_controller.ex
@@ -1,67 +1,67 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Feed.UserController do
use Pleroma.Web, :controller
alias Pleroma.Federation.ActivityPub
alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ActivityPubController
+ alias Pleroma.Federation.ActivityPub.ActivityPubController
alias Pleroma.Web.FallbackRedirectController
alias Pleroma.Web.Feed.FeedView
import Pleroma.Web.ControllerHelper, only: [put_in_if_exist: 3]
plug(Pleroma.Web.SetFormatPlug when action in [:feed_redirect])
action_fallback(:errors)
def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname}) do
with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do
FallbackRedirectController.redirector_with_meta(conn, %{user: user})
end
end
def feed_redirect(%{assigns: %{format: format}} = conn, _params)
when format in ["json", "activity+json"] do
ActivityPubController.call(conn, :user)
end
def feed_redirect(conn, %{"nickname" => nickname}) do
with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
redirect(conn, external: "#{user_feed_url(conn, :feed, user.nickname)}.atom")
end
end
def feed(conn, %{"nickname" => nickname} = params) do
with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
activities =
%{
"type" => ["Create"],
"actor_id" => user.ap_id
}
|> put_in_if_exist("max_id", params["max_id"])
|> ActivityPub.fetch_public_activities()
conn
|> put_resp_content_type("application/atom+xml")
|> put_view(FeedView)
|> render("user.xml",
user: user,
activities: activities,
feed_config: Pleroma.Config.get([:feed])
)
end
end
def errors(conn, {:error, :not_found}) do
render_error(conn, :not_found, "Not found")
end
def errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found})
def errors(conn, _) do
render_error(conn, :internal_server_error, "Something went wrong")
end
end
diff --git a/lib/web/router.ex b/lib/web/router.ex
index 4f9f6b9a6..9f4026af7 100644
--- a/lib/web/router.ex
+++ b/lib/web/router.ex
@@ -1,673 +1,673 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Router do
use Pleroma.Web, :router
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
end
pipeline :oauth do
plug(:fetch_session)
plug(Pleroma.Web.OAuthPlug)
plug(Pleroma.Web.UserEnabledPlug)
end
pipeline :api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(Pleroma.Web.OAuthPlug)
plug(Pleroma.Web.BasicAuthDecoderPlug)
plug(Pleroma.Web.UserFetcherPlug)
plug(Pleroma.Web.SessionAuthenticationPlug)
plug(Pleroma.Web.LegacyAuthenticationPlug)
plug(Pleroma.Web.AuthenticationPlug)
plug(Pleroma.Web.UserEnabledPlug)
plug(Pleroma.Web.SetUserSessionIDPlug)
plug(Pleroma.Web.EnsureUserKeyPlug)
plug(Pleroma.Web.IdempotencyPlug)
end
pipeline :authenticated_api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(Pleroma.Web.OAuthPlug)
plug(Pleroma.Web.BasicAuthDecoderPlug)
plug(Pleroma.Web.UserFetcherPlug)
plug(Pleroma.Web.SessionAuthenticationPlug)
plug(Pleroma.Web.LegacyAuthenticationPlug)
plug(Pleroma.Web.AuthenticationPlug)
plug(Pleroma.Web.UserEnabledPlug)
plug(Pleroma.Web.SetUserSessionIDPlug)
plug(Pleroma.Web.EnsureAuthenticatedPlug)
plug(Pleroma.Web.IdempotencyPlug)
end
pipeline :admin_api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(Pleroma.Web.OAuthPlug)
plug(Pleroma.Web.BasicAuthDecoderPlug)
plug(Pleroma.Web.UserFetcherPlug)
plug(Pleroma.Web.SessionAuthenticationPlug)
plug(Pleroma.Web.LegacyAuthenticationPlug)
plug(Pleroma.Web.AuthenticationPlug)
plug(Pleroma.Web.AdminSecretAuthenticationPlug)
plug(Pleroma.Web.UserEnabledPlug)
plug(Pleroma.Web.SetUserSessionIDPlug)
plug(Pleroma.Web.EnsureAuthenticatedPlug)
plug(Pleroma.Web.UserIsAdminPlug)
plug(Pleroma.Web.IdempotencyPlug)
end
pipeline :mastodon_html do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(Pleroma.Web.OAuthPlug)
plug(Pleroma.Web.BasicAuthDecoderPlug)
plug(Pleroma.Web.UserFetcherPlug)
plug(Pleroma.Web.SessionAuthenticationPlug)
plug(Pleroma.Web.LegacyAuthenticationPlug)
plug(Pleroma.Web.AuthenticationPlug)
plug(Pleroma.Web.UserEnabledPlug)
plug(Pleroma.Web.SetUserSessionIDPlug)
plug(Pleroma.Web.EnsureUserKeyPlug)
end
pipeline :pleroma_html do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(Pleroma.Web.OAuthPlug)
plug(Pleroma.Web.BasicAuthDecoderPlug)
plug(Pleroma.Web.UserFetcherPlug)
plug(Pleroma.Web.SessionAuthenticationPlug)
plug(Pleroma.Web.AuthenticationPlug)
plug(Pleroma.Web.EnsureUserKeyPlug)
end
pipeline :well_known do
plug(:accepts, ["json", "jrd+json", "xml", "xrd+xml"])
end
pipeline :config do
plug(:accepts, ["json", "xml"])
end
pipeline :pleroma_api do
plug(:accepts, ["html", "json"])
end
pipeline :mailbox_preview do
plug(:accepts, ["html"])
plug(:put_secure_browser_headers, %{
"content-security-policy" =>
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' 'unsafe-eval'"
})
end
pipeline :http_signature do
plug(Pleroma.Federation.HTTPSignatures.HTTPSignaturesPlug)
plug(Pleroma.Federation.HTTPSignatures.MappedSignatureToIdentityPlug)
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_api)
get("/password_reset/:token", PasswordController, :reset, as: :reset_password)
post("/password_reset", PasswordController, :do_reset, as: :reset_password)
get("/emoji", UtilController, :emoji)
get("/captcha", UtilController, :captcha)
get("/healthcheck", UtilController, :healthcheck)
end
scope "/api/pleroma", Pleroma.Web do
pipe_through(:pleroma_api)
post("/uploader_callback/:upload_path", UploaderController, :callback)
end
scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:admin_api)
post("/users/follow", AdminAPIController, :user_follow)
post("/users/unfollow", AdminAPIController, :user_unfollow)
delete("/users", AdminAPIController, :user_delete)
post("/users", AdminAPIController, :users_create)
patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
patch("/users/activate", AdminAPIController, :user_activate)
patch("/users/deactivate", AdminAPIController, :user_deactivate)
put("/users/tag", AdminAPIController, :tag_users)
delete("/users/tag", AdminAPIController, :untag_users)
get("/users/:nickname/permission_group", AdminAPIController, :right_get)
get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get)
post("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_add)
delete(
"/users/:nickname/permission_group/:permission_group",
AdminAPIController,
:right_delete
)
post("/users/permission_group/:permission_group", AdminAPIController, :right_add_multiple)
delete(
"/users/permission_group/:permission_group",
AdminAPIController,
:right_delete_multiple
)
get("/relay", AdminAPIController, :relay_list)
post("/relay", AdminAPIController, :relay_follow)
delete("/relay", AdminAPIController, :relay_unfollow)
post("/users/invite_token", AdminAPIController, :create_invite_token)
get("/users/invites", AdminAPIController, :invites)
post("/users/revoke_invite", AdminAPIController, :revoke_invite)
post("/users/email_invite", AdminAPIController, :email_invite)
get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
patch("/users/force_password_reset", AdminAPIController, :force_password_reset)
get("/users", AdminAPIController, :list_users)
get("/users/:nickname", AdminAPIController, :user_show)
get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
get("/instances/:instance/statuses", AdminAPIController, :list_instance_statuses)
patch("/users/confirm_email", AdminAPIController, :confirm_email)
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
get("/reports", AdminAPIController, :list_reports)
get("/grouped_reports", AdminAPIController, :list_grouped_reports)
get("/reports/:id", AdminAPIController, :report_show)
patch("/reports", AdminAPIController, :reports_update)
post("/reports/:id/notes", AdminAPIController, :report_notes_create)
delete("/reports/:report_id/notes/:id", AdminAPIController, :report_notes_delete)
put("/statuses/:id", AdminAPIController, :status_update)
delete("/statuses/:id", AdminAPIController, :status_delete)
get("/config", AdminAPIController, :config_show)
post("/config", AdminAPIController, :config_update)
get("/config/descriptions", AdminAPIController, :config_descriptions)
get("/restart", AdminAPIController, :restart)
get("/moderation_log", AdminAPIController, :list_log)
post("/reload_emoji", AdminAPIController, :reload_emoji)
end
scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
scope "/packs" do
# Modifying packs
pipe_through(:admin_api)
post("/import_from_fs", EmojiAPIController, :import_from_fs)
post("/:pack_name/update_file", EmojiAPIController, :update_file)
post("/:pack_name/update_metadata", EmojiAPIController, :update_metadata)
put("/:name", EmojiAPIController, :create)
delete("/:name", EmojiAPIController, :delete)
post("/download_from", EmojiAPIController, :download_from)
post("/list_from", EmojiAPIController, :list_from)
end
scope "/packs" do
# Pack info / downloading
get("/", EmojiAPIController, :list_packs)
get("/:name/download_shared/", EmojiAPIController, :download_shared)
end
end
scope "/", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_html)
post("/main/ostatus", UtilController, :remote_subscribe)
get("/ostatus_subscribe", RemoteFollowController, :follow)
post("/ostatus_subscribe", RemoteFollowController, :do_follow)
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:authenticated_api)
post("/change_email", UtilController, :change_email)
post("/change_password", UtilController, :change_password)
post("/delete_account", UtilController, :delete_account)
put("/notification_settings", UtilController, :update_notificaton_settings)
post("/disable_account", UtilController, :disable_account)
post("/blocks_import", UtilController, :blocks_import)
post("/follow_import", UtilController, :follow_import)
end
scope "/oauth", Pleroma.Web.OAuth do
scope [] do
pipe_through(:oauth)
get("/authorize", OAuthController, :authorize)
end
post("/authorize", OAuthController, :create_authorization)
post("/token", OAuthController, :token_exchange)
post("/revoke", OAuthController, :token_revoke)
get("/registration_details", OAuthController, :registration_details)
scope [] do
pipe_through(:browser)
get("/prepare_request", OAuthController, :prepare_request)
get("/:provider", OAuthController, :request)
get("/:provider/callback", OAuthController, :callback)
post("/register", OAuthController, :register)
end
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:api)
get("/statuses/:id/emoji_reactions_by", PleromaAPIController, :emoji_reactions_by)
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
scope [] do
pipe_through(:authenticated_api)
get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
get("/conversations/:id", PleromaAPIController, :conversation)
post("/conversations/read", PleromaAPIController, :read_conversations)
end
scope [] do
pipe_through(:authenticated_api)
patch("/conversations/:id", PleromaAPIController, :update_conversation)
post("/statuses/:id/react_with_emoji", PleromaAPIController, :react_with_emoji)
post("/statuses/:id/unreact_with_emoji", PleromaAPIController, :unreact_with_emoji)
post("/notifications/read", PleromaAPIController, :read_notification)
patch("/accounts/update_avatar", AccountController, :update_avatar)
patch("/accounts/update_banner", AccountController, :update_banner)
patch("/accounts/update_background", AccountController, :update_background)
get("/mascot", MascotController, :show)
put("/mascot", MascotController, :update)
post("/scrobble", ScrobbleController, :new_scrobble)
end
scope [] do
pipe_through(:api)
get("/accounts/:id/favourites", AccountController, :favourites)
end
scope [] do
pipe_through(:authenticated_api)
post("/accounts/:id/subscribe", AccountController, :subscribe)
post("/accounts/:id/unsubscribe", AccountController, :unsubscribe)
end
post("/accounts/confirmation_resend", AccountController, :confirmation_resend)
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:api)
get("/accounts/:id/scrobbles", ScrobbleController, :user_scrobbles)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:authenticated_api)
get("/accounts/verify_credentials", AccountController, :verify_credentials)
get("/accounts/relationships", AccountController, :relationships)
get("/accounts/:id/lists", AccountController, :lists)
get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array)
get("/follow_requests", FollowRequestController, :index)
get("/blocks", AccountController, :blocks)
get("/mutes", AccountController, :mutes)
get("/timelines/home", TimelineController, :home)
get("/timelines/direct", TimelineController, :direct)
get("/favourites", StatusController, :favourites)
get("/bookmarks", StatusController, :bookmarks)
get("/notifications", NotificationController, :index)
get("/notifications/:id", NotificationController, :show)
post("/notifications/clear", NotificationController, :clear)
post("/notifications/dismiss", NotificationController, :dismiss)
delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
get("/scheduled_statuses", ScheduledActivityController, :index)
get("/scheduled_statuses/:id", ScheduledActivityController, :show)
get("/lists", ListController, :index)
get("/lists/:id", ListController, :show)
get("/lists/:id/accounts", ListController, :list_accounts)
get("/domain_blocks", DomainBlockController, :index)
get("/filters", FilterController, :index)
get("/suggestions", SuggestionController, :index)
get("/conversations", ConversationController, :index)
post("/conversations/:id/read", ConversationController, :read)
get("/endorsements", AccountController, :endorsements)
patch("/accounts/update_credentials", AccountController, :update_credentials)
post("/statuses", StatusController, :create)
delete("/statuses/:id", StatusController, :delete)
post("/statuses/:id/reblog", StatusController, :reblog)
post("/statuses/:id/unreblog", StatusController, :unreblog)
post("/statuses/:id/favourite", StatusController, :favourite)
post("/statuses/:id/unfavourite", StatusController, :unfavourite)
post("/statuses/:id/pin", StatusController, :pin)
post("/statuses/:id/unpin", StatusController, :unpin)
post("/statuses/:id/bookmark", StatusController, :bookmark)
post("/statuses/:id/unbookmark", StatusController, :unbookmark)
post("/statuses/:id/mute", StatusController, :mute_conversation)
post("/statuses/:id/unmute", StatusController, :unmute_conversation)
put("/scheduled_statuses/:id", ScheduledActivityController, :update)
delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
post("/polls/:id/votes", PollController, :vote)
post("/media", MediaController, :create)
put("/media/:id", MediaController, :update)
delete("/lists/:id", ListController, :delete)
post("/lists", ListController, :create)
put("/lists/:id", ListController, :update)
post("/lists/:id/accounts", ListController, :add_to_list)
delete("/lists/:id/accounts", ListController, :remove_from_list)
post("/filters", FilterController, :create)
get("/filters/:id", FilterController, :show)
put("/filters/:id", FilterController, :update)
delete("/filters/:id", FilterController, :delete)
post("/reports", ReportController, :create)
post("/follows", AccountController, :follows)
post("/accounts/:id/follow", AccountController, :follow)
post("/accounts/:id/unfollow", AccountController, :unfollow)
post("/accounts/:id/block", AccountController, :block)
post("/accounts/:id/unblock", AccountController, :unblock)
post("/accounts/:id/mute", AccountController, :mute)
post("/accounts/:id/unmute", AccountController, :unmute)
post("/follow_requests/:id/authorize", FollowRequestController, :authorize)
post("/follow_requests/:id/reject", FollowRequestController, :reject)
post("/domain_blocks", DomainBlockController, :create)
delete("/domain_blocks", DomainBlockController, :delete)
post("/push/subscription", SubscriptionController, :create)
get("/push/subscription", SubscriptionController, :get)
put("/push/subscription", SubscriptionController, :update)
delete("/push/subscription", SubscriptionController, :delete)
get("/markers", MarkerController, :index)
post("/markers", MarkerController, :upsert)
end
scope "/api/web", Pleroma.Web do
pipe_through(:authenticated_api)
put("/settings", MastoFEController, :put_settings)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:api)
post("/accounts", AccountController, :create)
get("/accounts/search", SearchController, :account_search)
get("/instance", InstanceController, :show)
get("/instance/peers", InstanceController, :peers)
post("/apps", AppController, :create)
get("/apps/verify_credentials", AppController, :verify_credentials)
get("/statuses/:id/card", StatusController, :card)
get("/statuses/:id/favourited_by", StatusController, :favourited_by)
get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
get("/custom_emojis", CustomEmojiController, :index)
get("/trends", MastodonAPIController, :empty_array)
get("/timelines/public", TimelineController, :public)
get("/timelines/tag/:tag", TimelineController, :hashtag)
get("/timelines/list/:list_id", TimelineController, :list)
get("/statuses", StatusController, :index)
get("/statuses/:id", StatusController, :show)
get("/statuses/:id/context", StatusController, :context)
get("/polls/:id", PollController, :show)
get("/accounts/:id/statuses", AccountController, :statuses)
get("/accounts/:id/followers", AccountController, :followers)
get("/accounts/:id/following", AccountController, :following)
get("/accounts/:id", AccountController, :show)
get("/search", SearchController, :search)
end
scope "/api/v2", Pleroma.Web.MastodonAPI do
pipe_through(:api)
get("/search", SearchController, :search2)
end
scope "/api", Pleroma.Web do
pipe_through(:config)
get("/help/test", TwitterAPI.UtilController, :help_test)
post("/help/test", TwitterAPI.UtilController, :help_test)
get("/statusnet/config", TwitterAPI.UtilController, :config)
get("/statusnet/version", TwitterAPI.UtilController, :version)
get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations)
end
scope "/api", Pleroma.Web do
pipe_through(:api)
get(
"/account/confirm_email/:user_id/:token",
TwitterAPI.Controller,
:confirm_email,
as: :confirm_email
)
end
scope "/api", Pleroma.Web, as: :authenticated_twitter_api do
pipe_through(:authenticated_api)
get("/oauth_tokens", TwitterAPI.Controller, :oauth_tokens)
delete("/oauth_tokens/:id", TwitterAPI.Controller, :revoke_token)
post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
end
pipeline :ap_service_actor do
plug(:accepts, ["activity+json", "json"])
end
pipeline :ostatus do
plug(:accepts, ["html", "xml", "atom", "activity+json", "json"])
plug(Pleroma.Web.StaticFEPlug)
end
pipeline :oembed do
plug(:accepts, ["json", "xml"])
end
scope "/", Pleroma.Federation do
pipe_through(:ostatus)
pipe_through(:http_signature)
get("/objects/:uuid", OStatus.OStatusController, :object)
get("/activities/:uuid", OStatus.OStatusController, :activity)
get("/notice/:id", OStatus.OStatusController, :notice)
get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player)
end
scope "/", Pleroma.Web do
pipe_through(:ostatus)
pipe_through(:http_signature)
get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed)
get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed)
get("/tags/:tag", Feed.TagController, :feed, as: :tag_feed)
end
scope "/", Pleroma.Web do
pipe_through(:browser)
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
end
pipeline :activitypub do
plug(:accepts, ["activity+json", "json"])
plug(Pleroma.Federation.HTTPSignatures.HTTPSignaturesPlug)
plug(Pleroma.Federation.HTTPSignatures.MappedSignatureToIdentityPlug)
end
- scope "/", Pleroma.Web.ActivityPub do
+ scope "/", Pleroma.Federation.ActivityPub do
# XXX: not really ostatus
pipe_through(:ostatus)
get("/users/:nickname/outbox", ActivityPubController, :outbox)
end
pipeline :activitypub_client do
plug(:accepts, ["activity+json", "json"])
plug(:fetch_session)
plug(Pleroma.Web.OAuthPlug)
plug(Pleroma.Web.BasicAuthDecoderPlug)
plug(Pleroma.Web.UserFetcherPlug)
plug(Pleroma.Web.SessionAuthenticationPlug)
plug(Pleroma.Web.LegacyAuthenticationPlug)
plug(Pleroma.Web.AuthenticationPlug)
plug(Pleroma.Web.UserEnabledPlug)
plug(Pleroma.Web.SetUserSessionIDPlug)
plug(Pleroma.Web.EnsureUserKeyPlug)
end
- scope "/", Pleroma.Web.ActivityPub do
+ scope "/", Pleroma.Federation.ActivityPub do
pipe_through([:activitypub_client])
get("/api/ap/whoami", ActivityPubController, :whoami)
get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
post("/api/ap/upload_media", ActivityPubController, :upload_media)
get("/users/:nickname/followers", ActivityPubController, :followers)
get("/users/:nickname/following", ActivityPubController, :following)
end
- scope "/", Pleroma.Web.ActivityPub do
+ scope "/", Pleroma.Federation.ActivityPub do
pipe_through(:activitypub)
post("/inbox", ActivityPubController, :inbox)
post("/users/:nickname/inbox", ActivityPubController, :inbox)
end
- scope "/relay", Pleroma.Web.ActivityPub do
+ scope "/relay", Pleroma.Federation.ActivityPub do
pipe_through(:ap_service_actor)
get("/", ActivityPubController, :relay)
scope [] do
pipe_through(:http_signature)
post("/inbox", ActivityPubController, :inbox)
end
get("/following", ActivityPubController, :following, assigns: %{relay: true})
get("/followers", ActivityPubController, :followers, assigns: %{relay: true})
end
- scope "/internal/fetch", Pleroma.Web.ActivityPub do
+ scope "/internal/fetch", Pleroma.Federation.ActivityPub do
pipe_through(:ap_service_actor)
get("/", ActivityPubController, :internal_fetch)
post("/inbox", ActivityPubController, :inbox)
end
scope "/.well-known", Pleroma.Federation do
pipe_through(:well_known)
get("/host-meta", WebFinger.WebFingerController, :host_meta)
get("/webfinger", WebFinger.WebFingerController, :webfinger)
get("/nodeinfo", NodeInfo.NodeInfoController, :schemas)
end
scope "/nodeinfo", Pleroma.Federation do
get("/:version", NodeInfo.NodeInfoController, :nodeinfo)
end
scope "/", Pleroma.Web do
pipe_through(:api)
get("/web/manifest.json", MastoFEController, :manifest)
end
scope "/", Pleroma.Web do
pipe_through(:mastodon_html)
get("/web/login", MastodonAPI.AuthController, :login)
delete("/auth/sign_out", MastodonAPI.AuthController, :logout)
post("/auth/password", MastodonAPI.AuthController, :password_reset)
get("/web/*path", MastoFEController, :index)
end
pipeline :remote_media do
end
scope "/proxy/", Pleroma.Web.MediaProxy do
pipe_through(:remote_media)
get("/:sig/:url", MediaProxyController, :remote)
get("/:sig/:url/:filename", MediaProxyController, :remote)
end
if Pleroma.Config.get(:env) == :dev do
scope "/dev" do
pipe_through([:mailbox_preview])
forward("/mailbox", Plug.Swoosh.MailboxPreview, base_path: "/dev/mailbox")
end
end
scope "/", Pleroma.Web.MongooseIM do
get("/user_exists", MongooseIMController, :user_exists)
get("/check_password", MongooseIMController, :check_password)
end
scope "/", Pleroma.Web do
get("/registration/:token", FallbackRedirectController, :registration_page)
get("/:maybe_nickname_or_id", FallbackRedirectController, :redirector_with_meta)
get("/api*path", FallbackRedirectController, :api_not_implemented)
get("/*path", FallbackRedirectController, :redirector)
options("/*path", FallbackRedirectController, :empty)
end
end
diff --git a/test/federation/activity_pub/activity_pub_test.exs b/test/federation/activity_pub/activity_pub_test.exs
index 046c8cdbb..080c1f6e1 100644
--- a/test/federation/activity_pub/activity_pub_test.exs
+++ b/test/federation/activity_pub/activity_pub_test.exs
@@ -1,1779 +1,1779 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Federation.ActivityPubTest do
use Pleroma.DataCase
use Oban.Testing, repo: Pleroma.Storage.Repo
alias Pleroma.Activity
alias Pleroma.Builders.ActivityBuilder
alias Pleroma.Crypto
alias Pleroma.Federation.ActivityPub
alias Pleroma.Federation.ActivityPub.Federator
alias Pleroma.Federation.ActivityPub.Utils
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.User
- alias Pleroma.Web.ActivityPub.UserView
+ alias Pleroma.Federation.ActivityPub.UserView
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
import Tesla.Mock
import Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
clear_config([:instance, :federating])
describe "streaming out participations" do
test "it streams them out" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
{:ok, conversation} = Pleroma.Conversation.create_or_bump_for(activity)
participations =
conversation.participations
|> Repo.preload(:user)
with_mock Pleroma.Web.Streamer,
stream: fn _, _ -> nil end do
ActivityPub.stream_out_participations(conversation.participations)
assert called(Pleroma.Web.Streamer.stream("participation", participations))
end
end
test "streams them out on activity creation" do
user_one = insert(:user)
user_two = insert(:user)
with_mock Pleroma.Web.Streamer,
stream: fn _, _ -> nil end do
{:ok, activity} =
CommonAPI.post(user_one, %{
"status" => "@#{user_two.nickname}",
"visibility" => "direct"
})
conversation =
activity.data["context"]
|> Pleroma.Conversation.get_for_ap_id()
|> Repo.preload(participations: :user)
assert called(Pleroma.Web.Streamer.stream("participation", conversation.participations))
end
end
end
describe "fetching restricted by visibility" do
test "it restricts by the appropriate visibility" do
user = insert(:user)
{:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
{:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
{:ok, unlisted_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"})
{:ok, private_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
activities =
ActivityPub.fetch_activities([], %{:visibility => "direct", "actor_id" => user.ap_id})
assert activities == [direct_activity]
activities =
ActivityPub.fetch_activities([], %{:visibility => "unlisted", "actor_id" => user.ap_id})
assert activities == [unlisted_activity]
activities =
ActivityPub.fetch_activities([], %{:visibility => "private", "actor_id" => user.ap_id})
assert activities == [private_activity]
activities =
ActivityPub.fetch_activities([], %{:visibility => "public", "actor_id" => user.ap_id})
assert activities == [public_activity]
activities =
ActivityPub.fetch_activities([], %{
:visibility => ~w[private public],
"actor_id" => user.ap_id
})
assert activities == [public_activity, private_activity]
end
end
describe "fetching excluded by visibility" do
test "it excludes by the appropriate visibility" do
user = insert(:user)
{:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
{:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
{:ok, unlisted_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"})
{:ok, private_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
activities =
ActivityPub.fetch_activities([], %{
"exclude_visibilities" => "direct",
"actor_id" => user.ap_id
})
assert public_activity in activities
assert unlisted_activity in activities
assert private_activity in activities
refute direct_activity in activities
activities =
ActivityPub.fetch_activities([], %{
"exclude_visibilities" => "unlisted",
"actor_id" => user.ap_id
})
assert public_activity in activities
refute unlisted_activity in activities
assert private_activity in activities
assert direct_activity in activities
activities =
ActivityPub.fetch_activities([], %{
"exclude_visibilities" => "private",
"actor_id" => user.ap_id
})
assert public_activity in activities
assert unlisted_activity in activities
refute private_activity in activities
assert direct_activity in activities
activities =
ActivityPub.fetch_activities([], %{
"exclude_visibilities" => "public",
"actor_id" => user.ap_id
})
refute public_activity in activities
assert unlisted_activity in activities
assert private_activity in activities
assert direct_activity in activities
end
end
describe "building a user from his ap id" do
test "it returns a user" do
user_id = "http://mastodon.example.org/users/admin"
{:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
assert user.ap_id == user_id
assert user.nickname == "admin@mastodon.example.org"
assert user.source_data
assert user.ap_enabled
assert user.follower_address == "http://mastodon.example.org/users/admin/followers"
end
test "it returns a user that is invisible" do
user_id = "http://mastodon.example.org/users/relay"
{:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
assert User.invisible?(user)
end
test "it fetches the appropriate tag-restricted posts" do
user = insert(:user)
{:ok, status_one} = CommonAPI.post(user, %{"status" => ". #test"})
{:ok, status_two} = CommonAPI.post(user, %{"status" => ". #essais"})
{:ok, status_three} = CommonAPI.post(user, %{"status" => ". #test #reject"})
fetch_one = ActivityPub.fetch_activities([], %{"type" => "Create", "tag" => "test"})
fetch_two =
ActivityPub.fetch_activities([], %{"type" => "Create", "tag" => ["test", "essais"]})
fetch_three =
ActivityPub.fetch_activities([], %{
"type" => "Create",
"tag" => ["test", "essais"],
"tag_reject" => ["reject"]
})
fetch_four =
ActivityPub.fetch_activities([], %{
"type" => "Create",
"tag" => ["test"],
"tag_all" => ["test", "reject"]
})
assert fetch_one == [status_one, status_three]
assert fetch_two == [status_one, status_two, status_three]
assert fetch_three == [status_one, status_two]
assert fetch_four == [status_three]
end
end
describe "insertion" do
test "drops activities beyond a certain limit" do
limit = Pleroma.Config.get([:instance, :remote_limit])
random_text =
(limit + 1)
|> Crypto.random_string()
|> binary_part(0, limit + 1)
data = %{
"ok" => true,
"object" => %{
"content" => random_text
}
}
assert {:error, {:remote_limit_error, _}} = ActivityPub.insert(data)
end
test "doesn't drop activities with content being null" do
user = insert(:user)
data = %{
"actor" => user.ap_id,
"to" => [],
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => nil
}
}
assert {:ok, _} = ActivityPub.insert(data)
end
test "returns the activity if one with the same id is already in" do
activity = insert(:note_activity)
{:ok, new_activity} = ActivityPub.insert(activity.data)
assert activity.id == new_activity.id
end
test "inserts a given map into the activity database, giving it an id if it has none." do
user = insert(:user)
data = %{
"actor" => user.ap_id,
"to" => [],
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => "hey"
}
}
{:ok, %Activity{} = activity} = ActivityPub.insert(data)
assert activity.data["ok"] == data["ok"]
assert is_binary(activity.data["id"])
given_id = "bla"
data = %{
"id" => given_id,
"actor" => user.ap_id,
"to" => [],
"context" => "blabla",
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => "hey"
}
}
{:ok, %Activity{} = activity} = ActivityPub.insert(data)
assert activity.data["ok"] == data["ok"]
assert activity.data["id"] == given_id
assert activity.data["context"] == "blabla"
assert activity.data["context_id"]
end
test "adds a context when none is there" do
user = insert(:user)
data = %{
"actor" => user.ap_id,
"to" => [],
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => "hey"
}
}
{:ok, %Activity{} = activity} = ActivityPub.insert(data)
object = Pleroma.Object.normalize(activity)
assert is_binary(activity.data["context"])
assert is_binary(object.data["context"])
assert activity.data["context_id"]
assert object.data["context_id"]
end
test "adds an id to a given object if it lacks one and is a note and inserts it to the object database" do
user = insert(:user)
data = %{
"actor" => user.ap_id,
"to" => [],
"object" => %{
"actor" => user.ap_id,
"to" => [],
"type" => "Note",
"content" => "hey"
}
}
{:ok, %Activity{} = activity} = ActivityPub.insert(data)
assert object = Object.normalize(activity)
assert is_binary(object.data["id"])
end
end
describe "listen activities" do
test "does not increase user note count" do
user = insert(:user)
{:ok, activity} =
ActivityPub.listen(%{
to: ["https://www.w3.org/ns/activitystreams#Public"],
actor: user,
context: "",
object: %{
"actor" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"artist" => "lain",
"title" => "lain radio episode 1",
"length" => 180_000,
"type" => "Audio"
}
})
assert activity.actor == user.ap_id
user = User.get_cached_by_id(user.id)
assert user.note_count == 0
end
test "can be fetched into a timeline" do
_listen_activity_1 = insert(:listen)
_listen_activity_2 = insert(:listen)
_listen_activity_3 = insert(:listen)
timeline = ActivityPub.fetch_activities([], %{"type" => ["Listen"]})
assert length(timeline) == 3
end
end
describe "create activities" do
test "removes doubled 'to' recipients" do
user = insert(:user)
{:ok, activity} =
ActivityPub.create(%{
to: ["user1", "user1", "user2"],
actor: user,
context: "",
object: %{
"to" => ["user1", "user1", "user2"],
"type" => "Note",
"content" => "testing"
}
})
assert activity.data["to"] == ["user1", "user2"]
assert activity.actor == user.ap_id
assert activity.recipients == ["user1", "user2", user.ap_id]
end
test "increases user note count only for public activities" do
user = insert(:user)
{:ok, _} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "1",
"visibility" => "public"
})
{:ok, _} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "2",
"visibility" => "unlisted"
})
{:ok, _} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "2",
"visibility" => "private"
})
{:ok, _} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "3",
"visibility" => "direct"
})
user = User.get_cached_by_id(user.id)
assert user.note_count == 2
end
test "increases replies count" do
user = insert(:user)
user2 = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "1", "visibility" => "public"})
ap_id = activity.data["id"]
reply_data = %{"status" => "1", "in_reply_to_status_id" => activity.id}
# public
{:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "public"))
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 1
# unlisted
{:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "unlisted"))
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
# private
{:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "private"))
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
# direct
{:ok, _} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "direct"))
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
end
end
describe "fetch activities for recipients" do
test "retrieve the activities for certain recipients" do
{:ok, activity_one} = ActivityBuilder.insert(%{"to" => ["someone"]})
{:ok, activity_two} = ActivityBuilder.insert(%{"to" => ["someone_else"]})
{:ok, _activity_three} = ActivityBuilder.insert(%{"to" => ["noone"]})
activities = ActivityPub.fetch_activities(["someone", "someone_else"])
assert length(activities) == 2
assert activities == [activity_one, activity_two]
end
end
describe "fetch activities in context" do
test "retrieves activities that have a given context" do
{:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
{:ok, activity_two} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
{:ok, _activity_three} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"})
{:ok, _activity_four} = ActivityBuilder.insert(%{"type" => "Announce", "context" => "2hu"})
activity_five = insert(:note_activity)
user = insert(:user)
{:ok, _user_relationship} = User.block(user, %{ap_id: activity_five.data["actor"]})
activities = ActivityPub.fetch_activities_for_context("2hu", %{"blocking_user" => user})
assert activities == [activity_two, activity]
end
end
test "doesn't return blocked activities" do
activity_one = insert(:note_activity)
activity_two = insert(:note_activity)
activity_three = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, _user_relationship} = User.block(user, %{ap_id: activity_one.data["actor"]})
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
refute Enum.member?(activities, activity_one)
{:ok, _user_block} = User.unblock(user, %{ap_id: activity_one.data["actor"]})
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
{:ok, _user_relationship} = User.block(user, %{ap_id: activity_three.data["actor"]})
{:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster)
%Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id)
activity_three = Activity.get_by_id(activity_three.id)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
refute Enum.member?(activities, activity_three)
refute Enum.member?(activities, boost_activity)
assert Enum.member?(activities, activity_one)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => nil, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, boost_activity)
assert Enum.member?(activities, activity_one)
end
test "doesn't return transitive interactions concerning blocked users" do
blocker = insert(:user)
blockee = insert(:user)
friend = insert(:user)
{:ok, _user_relationship} = User.block(blocker, blockee)
{:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"})
{:ok, activity_two} = CommonAPI.post(friend, %{"status" => "hey! @#{blockee.nickname}"})
{:ok, activity_three} = CommonAPI.post(blockee, %{"status" => "hey! @#{friend.nickname}"})
{:ok, activity_four} = CommonAPI.post(blockee, %{"status" => "hey! @#{blocker.nickname}"})
activities = ActivityPub.fetch_activities([], %{"blocking_user" => blocker})
assert Enum.member?(activities, activity_one)
refute Enum.member?(activities, activity_two)
refute Enum.member?(activities, activity_three)
refute Enum.member?(activities, activity_four)
end
test "doesn't return announce activities concerning blocked users" do
blocker = insert(:user)
blockee = insert(:user)
friend = insert(:user)
{:ok, _user_relationship} = User.block(blocker, blockee)
{:ok, activity_one} = CommonAPI.post(friend, %{"status" => "hey!"})
{:ok, activity_two} = CommonAPI.post(blockee, %{"status" => "hey! @#{friend.nickname}"})
{:ok, activity_three, _} = CommonAPI.repeat(activity_two.id, friend)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => blocker})
|> Enum.map(fn act -> act.id end)
assert Enum.member?(activities, activity_one.id)
refute Enum.member?(activities, activity_two.id)
refute Enum.member?(activities, activity_three.id)
end
test "doesn't return activities from blocked domains" do
domain = "dogwhistle.zone"
domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"})
note = insert(:note, %{data: %{"actor" => domain_user.ap_id}})
activity = insert(:note_activity, %{note: note})
user = insert(:user)
{:ok, user} = User.block_domain(user, domain)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
refute activity in activities
followed_user = insert(:user)
ActivityPub.follow(user, followed_user)
{:ok, repeat_activity, _} = CommonAPI.repeat(activity.id, followed_user)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => user, "skip_preload" => true})
refute repeat_activity in activities
end
test "does return activities from followed users on blocked domains" do
domain = "meanies.social"
domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"})
blocker = insert(:user)
{:ok, blocker} = User.follow(blocker, domain_user)
{:ok, blocker} = User.block_domain(blocker, domain)
assert User.following?(blocker, domain_user)
assert User.blocks_domain?(blocker, domain_user)
refute User.blocks?(blocker, domain_user)
note = insert(:note, %{data: %{"actor" => domain_user.ap_id}})
activity = insert(:note_activity, %{note: note})
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => blocker, "skip_preload" => true})
assert activity in activities
# And check that if the guy we DO follow boosts someone else from their domain,
# that should be hidden
another_user = insert(:user, %{ap_id: "https://#{domain}/@meanie2"})
bad_note = insert(:note, %{data: %{"actor" => another_user.ap_id}})
bad_activity = insert(:note_activity, %{note: bad_note})
{:ok, repeat_activity, _} = CommonAPI.repeat(bad_activity.id, domain_user)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => blocker, "skip_preload" => true})
refute repeat_activity in activities
end
test "doesn't return muted activities" do
activity_one = insert(:note_activity)
activity_two = insert(:note_activity)
activity_three = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
activity_one_actor = User.get_by_ap_id(activity_one.data["actor"])
{:ok, _user_relationships} = User.mute(user, activity_one_actor)
activities =
ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
refute Enum.member?(activities, activity_one)
# Calling with 'with_muted' will deliver muted activities, too.
activities =
ActivityPub.fetch_activities([], %{
"muting_user" => user,
"with_muted" => true,
"skip_preload" => true
})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
{:ok, _user_mute} = User.unmute(user, activity_one_actor)
activities =
ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, activity_one)
activity_three_actor = User.get_by_ap_id(activity_three.data["actor"])
{:ok, _user_relationships} = User.mute(user, activity_three_actor)
{:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(activity_three.id, booster)
%Activity{} = boost_activity = Activity.get_create_by_object_ap_id(id)
activity_three = Activity.get_by_id(activity_three.id)
activities =
ActivityPub.fetch_activities([], %{"muting_user" => user, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
refute Enum.member?(activities, activity_three)
refute Enum.member?(activities, boost_activity)
assert Enum.member?(activities, activity_one)
activities = ActivityPub.fetch_activities([], %{"muting_user" => nil, "skip_preload" => true})
assert Enum.member?(activities, activity_two)
assert Enum.member?(activities, activity_three)
assert Enum.member?(activities, boost_activity)
assert Enum.member?(activities, activity_one)
end
test "doesn't return thread muted activities" do
user = insert(:user)
_activity_one = insert(:note_activity)
note_two = insert(:note, data: %{"context" => "suya.."})
activity_two = insert(:note_activity, note: note_two)
{:ok, _activity_two} = CommonAPI.add_mute(user, activity_two)
assert [_activity_one] = ActivityPub.fetch_activities([], %{"muting_user" => user})
end
test "returns thread muted activities when with_muted is set" do
user = insert(:user)
_activity_one = insert(:note_activity)
note_two = insert(:note, data: %{"context" => "suya.."})
activity_two = insert(:note_activity, note: note_two)
{:ok, _activity_two} = CommonAPI.add_mute(user, activity_two)
assert [_activity_two, _activity_one] =
ActivityPub.fetch_activities([], %{"muting_user" => user, "with_muted" => true})
end
test "does include announces on request" do
activity_three = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, user} = User.follow(user, booster)
{:ok, announce, _object} = CommonAPI.repeat(activity_three.id, booster)
[announce_activity] = ActivityPub.fetch_activities([user.ap_id | User.following(user)])
assert announce_activity.id == announce.id
end
test "excludes reblogs on request" do
user = insert(:user)
{:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user})
{:ok, _} = ActivityBuilder.insert(%{"type" => "Announce"}, %{:user => user})
[activity] = ActivityPub.fetch_user_activities(user, nil, %{"exclude_reblogs" => "true"})
assert activity == expected_activity
end
describe "public fetch activities" do
test "doesn't retrieve unlisted activities" do
user = insert(:user)
{:ok, _unlisted_activity} =
CommonAPI.post(user, %{"status" => "yeah", "visibility" => "unlisted"})
{:ok, listed_activity} = CommonAPI.post(user, %{"status" => "yeah"})
[activity] = ActivityPub.fetch_public_activities()
assert activity == listed_activity
end
test "retrieves public activities" do
_activities = ActivityPub.fetch_public_activities()
%{public: public} = ActivityBuilder.public_and_non_public()
activities = ActivityPub.fetch_public_activities()
assert length(activities) == 1
assert Enum.at(activities, 0) == public
end
test "retrieves a maximum of 20 activities" do
ActivityBuilder.insert_list(10)
expected_activities = ActivityBuilder.insert_list(20)
activities = ActivityPub.fetch_public_activities()
assert collect_ids(activities) == collect_ids(expected_activities)
assert length(activities) == 20
end
test "retrieves ids starting from a since_id" do
activities = ActivityBuilder.insert_list(30)
expected_activities = ActivityBuilder.insert_list(10)
since_id = List.last(activities).id
activities = ActivityPub.fetch_public_activities(%{"since_id" => since_id})
assert collect_ids(activities) == collect_ids(expected_activities)
assert length(activities) == 10
end
test "retrieves ids up to max_id" do
ActivityBuilder.insert_list(10)
expected_activities = ActivityBuilder.insert_list(20)
%{id: max_id} =
10
|> ActivityBuilder.insert_list()
|> List.first()
activities = ActivityPub.fetch_public_activities(%{"max_id" => max_id})
assert length(activities) == 20
assert collect_ids(activities) == collect_ids(expected_activities)
end
test "paginates via offset/limit" do
_first_part_activities = ActivityBuilder.insert_list(10)
second_part_activities = ActivityBuilder.insert_list(10)
later_activities = ActivityBuilder.insert_list(10)
activities =
ActivityPub.fetch_public_activities(%{"page" => "2", "page_size" => "20"}, :offset)
assert length(activities) == 20
assert collect_ids(activities) ==
collect_ids(second_part_activities) ++ collect_ids(later_activities)
end
test "doesn't return reblogs for users for whom reblogs have been muted" do
activity = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster)
{:ok, activity, _} = CommonAPI.repeat(activity.id, booster)
activities = ActivityPub.fetch_activities([], %{"muting_user" => user})
refute Enum.any?(activities, fn %{id: id} -> id == activity.id end)
end
test "returns reblogs for users for whom reblogs have not been muted" do
activity = insert(:note_activity)
user = insert(:user)
booster = insert(:user)
{:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, booster)
{:ok, _reblog_mute} = CommonAPI.show_reblogs(user, booster)
{:ok, activity, _} = CommonAPI.repeat(activity.id, booster)
activities = ActivityPub.fetch_activities([], %{"muting_user" => user})
assert Enum.any?(activities, fn %{id: id} -> id == activity.id end)
end
end
describe "react to an object" do
test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
Pleroma.Config.put([:instance, :federating], true)
user = insert(:user)
reactor = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
assert object = Object.normalize(activity)
{:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
assert called(Federator.publish(reaction_activity))
end
test "adds an emoji reaction activity to the db" do
user = insert(:user)
reactor = insert(:user)
third_user = insert(:user)
fourth_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
assert object = Object.normalize(activity)
{:ok, reaction_activity, object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
assert reaction_activity
assert reaction_activity.data["actor"] == reactor.ap_id
assert reaction_activity.data["type"] == "EmojiReaction"
assert reaction_activity.data["content"] == "🔥"
assert reaction_activity.data["object"] == object.data["id"]
assert reaction_activity.data["to"] == [User.ap_followers(reactor), activity.data["actor"]]
assert reaction_activity.data["context"] == object.data["context"]
assert object.data["reaction_count"] == 1
assert object.data["reactions"] == [["🔥", [reactor.ap_id]]]
{:ok, _reaction_activity, object} = ActivityPub.react_with_emoji(third_user, object, "☕")
assert object.data["reaction_count"] == 2
assert object.data["reactions"] == [["🔥", [reactor.ap_id]], ["☕", [third_user.ap_id]]]
{:ok, _reaction_activity, object} = ActivityPub.react_with_emoji(fourth_user, object, "🔥")
assert object.data["reaction_count"] == 3
assert object.data["reactions"] == [
["🔥", [fourth_user.ap_id, reactor.ap_id]],
["☕", [third_user.ap_id]]
]
end
end
describe "unreacting to an object" do
test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
Pleroma.Config.put([:instance, :federating], true)
user = insert(:user)
reactor = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
assert object = Object.normalize(activity)
{:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
assert called(Federator.publish(reaction_activity))
{:ok, unreaction_activity, _object} =
ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
assert called(Federator.publish(unreaction_activity))
end
test "adds an undo activity to the db" do
user = insert(:user)
reactor = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
assert object = Object.normalize(activity)
{:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
{:ok, unreaction_activity, _object} =
ActivityPub.unreact_with_emoji(reactor, reaction_activity.data["id"])
assert unreaction_activity.actor == reactor.ap_id
assert unreaction_activity.data["object"] == reaction_activity.data["id"]
object = Object.get_by_ap_id(object.data["id"])
assert object.data["reaction_count"] == 0
assert object.data["reactions"] == []
end
end
describe "like an object" do
test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
Pleroma.Config.put([:instance, :federating], true)
note_activity = insert(:note_activity)
assert object_activity = Object.normalize(note_activity)
user = insert(:user)
{:ok, like_activity, _object} = ActivityPub.like(user, object_activity)
assert called(Federator.publish(like_activity))
end
test "returns exist activity if object already liked" do
note_activity = insert(:note_activity)
assert object_activity = Object.normalize(note_activity)
user = insert(:user)
{:ok, like_activity, _object} = ActivityPub.like(user, object_activity)
{:ok, like_activity_exist, _object} = ActivityPub.like(user, object_activity)
assert like_activity == like_activity_exist
end
test "adds a like activity to the db" do
note_activity = insert(:note_activity)
assert object = Object.normalize(note_activity)
user = insert(:user)
user_two = insert(:user)
{:ok, like_activity, object} = ActivityPub.like(user, object)
assert like_activity.data["actor"] == user.ap_id
assert like_activity.data["type"] == "Like"
assert like_activity.data["object"] == object.data["id"]
assert like_activity.data["to"] == [User.ap_followers(user), note_activity.data["actor"]]
assert like_activity.data["context"] == object.data["context"]
assert object.data["like_count"] == 1
assert object.data["likes"] == [user.ap_id]
# Just return the original activity if the user already liked it.
{:ok, same_like_activity, object} = ActivityPub.like(user, object)
assert like_activity == same_like_activity
assert object.data["likes"] == [user.ap_id]
assert object.data["like_count"] == 1
{:ok, _like_activity, object} = ActivityPub.like(user_two, object)
assert object.data["like_count"] == 2
end
end
describe "unliking" do
test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
Pleroma.Config.put([:instance, :federating], true)
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
{:ok, object} = ActivityPub.unlike(user, object)
refute called(Federator.publish())
{:ok, _like_activity, object} = ActivityPub.like(user, object)
assert object.data["like_count"] == 1
{:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0
assert called(Federator.publish(unlike_activity))
end
test "unliking a previously liked object" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
# Unliking something that hasn't been liked does nothing
{:ok, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0
{:ok, like_activity, object} = ActivityPub.like(user, object)
assert object.data["like_count"] == 1
{:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0
assert Activity.get_by_id(like_activity.id) == nil
assert note_activity.actor in unlike_activity.recipients
end
end
describe "announcing an object" do
test "adds an announce activity to the db" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
{:ok, announce_activity, object} = ActivityPub.announce(user, object)
assert object.data["announcement_count"] == 1
assert object.data["announcements"] == [user.ap_id]
assert announce_activity.data["to"] == [
User.ap_followers(user),
note_activity.data["actor"]
]
assert announce_activity.data["object"] == object.data["id"]
assert announce_activity.data["actor"] == user.ap_id
assert announce_activity.data["context"] == object.data["context"]
end
end
describe "announcing a private object" do
test "adds an announce activity to the db if the audience is not widened" do
user = insert(:user)
{:ok, note_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
object = Object.normalize(note_activity)
{:ok, announce_activity, object} = ActivityPub.announce(user, object, nil, true, false)
assert announce_activity.data["to"] == [User.ap_followers(user)]
assert announce_activity.data["object"] == object.data["id"]
assert announce_activity.data["actor"] == user.ap_id
assert announce_activity.data["context"] == object.data["context"]
end
test "does not add an announce activity to the db if the audience is widened" do
user = insert(:user)
{:ok, note_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
object = Object.normalize(note_activity)
assert {:error, _} = ActivityPub.announce(user, object, nil, true, true)
end
test "does not add an announce activity to the db if the announcer is not the author" do
user = insert(:user)
announcer = insert(:user)
{:ok, note_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
object = Object.normalize(note_activity)
assert {:error, _} = ActivityPub.announce(announcer, object, nil, true, false)
end
end
describe "unannouncing an object" do
test "unannouncing a previously announced object" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
# Unannouncing an object that is not announced does nothing
# {:ok, object} = ActivityPub.unannounce(user, object)
# assert object.data["announcement_count"] == 0
{:ok, announce_activity, object} = ActivityPub.announce(user, object)
assert object.data["announcement_count"] == 1
{:ok, unannounce_activity, object} = ActivityPub.unannounce(user, object)
assert object.data["announcement_count"] == 0
assert unannounce_activity.data["to"] == [
User.ap_followers(user),
object.data["actor"]
]
assert unannounce_activity.data["type"] == "Undo"
assert unannounce_activity.data["object"] == announce_activity.data
assert unannounce_activity.data["actor"] == user.ap_id
assert unannounce_activity.data["context"] == announce_activity.data["context"]
assert Activity.get_by_id(announce_activity.id) == nil
end
end
describe "uploading files" do
test "copies the file to the configured folder" do
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, %Object{} = object} =
ActivityPub.upload(file, uploader: Pleroma.Upload.Uploader.Local)
assert object.data["name"] == "an_image.jpg"
end
test "works with base64 encoded images" do
file = %{
"img" => data_uri()
}
{:ok, %Object{}} = ActivityPub.upload(file, uploader: Pleroma.Upload.Uploader.Local)
end
end
describe "fetch the latest Follow" do
test "fetches the latest Follow activity" do
%Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity)
follower = Repo.get_by(User, ap_id: activity.data["actor"])
followed = Repo.get_by(User, ap_id: activity.data["object"])
assert activity == Utils.fetch_latest_follow(follower, followed)
end
end
describe "following / unfollowing" do
test "creates a follow activity" do
follower = insert(:user)
followed = insert(:user)
{:ok, activity} = ActivityPub.follow(follower, followed)
assert activity.data["type"] == "Follow"
assert activity.data["actor"] == follower.ap_id
assert activity.data["object"] == followed.ap_id
end
test "creates an undo activity for the last follow" do
follower = insert(:user)
followed = insert(:user)
{:ok, follow_activity} = ActivityPub.follow(follower, followed)
{:ok, activity} = ActivityPub.unfollow(follower, followed)
assert activity.data["type"] == "Undo"
assert activity.data["actor"] == follower.ap_id
embedded_object = activity.data["object"]
assert is_map(embedded_object)
assert embedded_object["type"] == "Follow"
assert embedded_object["object"] == followed.ap_id
assert embedded_object["id"] == follow_activity.data["id"]
end
end
describe "blocking / unblocking" do
test "creates a block activity" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, activity} = ActivityPub.block(blocker, blocked)
assert activity.data["type"] == "Block"
assert activity.data["actor"] == blocker.ap_id
assert activity.data["object"] == blocked.ap_id
end
test "creates an undo activity for the last block" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, block_activity} = ActivityPub.block(blocker, blocked)
{:ok, activity} = ActivityPub.unblock(blocker, blocked)
assert activity.data["type"] == "Undo"
assert activity.data["actor"] == blocker.ap_id
embedded_object = activity.data["object"]
assert is_map(embedded_object)
assert embedded_object["type"] == "Block"
assert embedded_object["object"] == blocked.ap_id
assert embedded_object["id"] == block_activity.data["id"]
end
end
describe "deletion" do
test "it creates a delete activity and deletes the original object" do
note = insert(:note_activity)
object = Object.normalize(note)
{:ok, delete} = ActivityPub.delete(object)
assert delete.data["type"] == "Delete"
assert delete.data["actor"] == note.data["actor"]
assert delete.data["object"] == object.data["id"]
assert Activity.get_by_id(delete.id) != nil
assert Repo.get(Object, object.id).data["type"] == "Tombstone"
end
test "decrements user note count only for public activities" do
user = insert(:user, note_count: 10)
{:ok, a1} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "public"
})
{:ok, a2} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "unlisted"
})
{:ok, a3} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "private"
})
{:ok, a4} =
CommonAPI.post(User.get_cached_by_id(user.id), %{
"status" => "yeah",
"visibility" => "direct"
})
{:ok, _} = Object.normalize(a1) |> ActivityPub.delete()
{:ok, _} = Object.normalize(a2) |> ActivityPub.delete()
{:ok, _} = Object.normalize(a3) |> ActivityPub.delete()
{:ok, _} = Object.normalize(a4) |> ActivityPub.delete()
user = User.get_cached_by_id(user.id)
assert user.note_count == 10
end
test "it creates a delete activity and checks that it is also sent to users mentioned by the deleted object" do
user = insert(:user)
note = insert(:note_activity)
object = Object.normalize(note)
{:ok, object} =
object
|> Object.change(%{
data: %{
"actor" => object.data["actor"],
"id" => object.data["id"],
"to" => [user.ap_id],
"type" => "Note"
}
})
|> Object.update_and_set_cache()
{:ok, delete} = ActivityPub.delete(object)
assert user.ap_id in delete.data["to"]
end
test "decreases reply count" do
user = insert(:user)
user2 = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "1", "visibility" => "public"})
reply_data = %{"status" => "1", "in_reply_to_status_id" => activity.id}
ap_id = activity.data["id"]
{:ok, public_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "public"))
{:ok, unlisted_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "unlisted"))
{:ok, private_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "private"))
{:ok, direct_reply} = CommonAPI.post(user2, Map.put(reply_data, "visibility", "direct"))
_ = CommonAPI.delete(direct_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
_ = CommonAPI.delete(private_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 2
_ = CommonAPI.delete(public_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 1
_ = CommonAPI.delete(unlisted_reply.id, user2)
assert %{data: data, object: object} = Activity.get_by_ap_id_with_object(ap_id)
assert object.data["repliesCount"] == 0
end
test "it passes delete activity through MRF before deleting the object" do
rewrite_policy = Pleroma.Config.get([:instance, :rewrite_policy])
Pleroma.Config.put(
[:instance, :rewrite_policy],
Pleroma.Federation.ActivityPub.MRF.DropPolicy
)
on_exit(fn -> Pleroma.Config.put([:instance, :rewrite_policy], rewrite_policy) end)
note = insert(:note_activity)
object = Object.normalize(note)
{:error, {:reject, _}} = ActivityPub.delete(object)
assert Activity.get_by_id(note.id)
assert Repo.get(Object, object.id).data["type"] == object.data["type"]
end
end
describe "timeline post-processing" do
test "it filters broken threads" do
user1 = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
{:ok, user1} = User.follow(user1, user3)
assert User.following?(user1, user3)
{:ok, user2} = User.follow(user2, user3)
assert User.following?(user2, user3)
{:ok, user3} = User.follow(user3, user2)
assert User.following?(user3, user2)
{:ok, public_activity} = CommonAPI.post(user3, %{"status" => "hi 1"})
{:ok, private_activity_1} =
CommonAPI.post(user3, %{"status" => "hi 2", "visibility" => "private"})
{:ok, private_activity_2} =
CommonAPI.post(user2, %{
"status" => "hi 3",
"visibility" => "private",
"in_reply_to_status_id" => private_activity_1.id
})
{:ok, private_activity_3} =
CommonAPI.post(user3, %{
"status" => "hi 4",
"visibility" => "private",
"in_reply_to_status_id" => private_activity_2.id
})
activities =
ActivityPub.fetch_activities([user1.ap_id | User.following(user1)])
|> Enum.map(fn a -> a.id end)
private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"])
assert [public_activity.id, private_activity_1.id, private_activity_3.id] == activities
assert length(activities) == 3
activities =
ActivityPub.fetch_activities([user1.ap_id | User.following(user1)], %{"user" => user1})
|> Enum.map(fn a -> a.id end)
assert [public_activity.id, private_activity_1.id] == activities
assert length(activities) == 2
end
end
describe "update" do
test "it creates an update activity with the new user data" do
user = insert(:user)
{:ok, user} = User.ensure_keys_present(user)
user_data = UserView.render("user.json", %{user: user})
{:ok, update} =
ActivityPub.update(%{
actor: user_data["id"],
to: [user.follower_address],
cc: [],
object: user_data
})
assert update.data["actor"] == user.ap_id
assert update.data["to"] == [user.follower_address]
assert embedded_object = update.data["object"]
assert embedded_object["id"] == user_data["id"]
assert embedded_object["type"] == user_data["type"]
end
end
test "returned pinned statuses" do
Pleroma.Config.put([:instance, :max_pinned_statuses], 3)
user = insert(:user)
{:ok, activity_one} = CommonAPI.post(user, %{"status" => "HI!!!"})
{:ok, activity_two} = CommonAPI.post(user, %{"status" => "HI!!!"})
{:ok, activity_three} = CommonAPI.post(user, %{"status" => "HI!!!"})
CommonAPI.pin(activity_one.id, user)
user = refresh_record(user)
CommonAPI.pin(activity_two.id, user)
user = refresh_record(user)
CommonAPI.pin(activity_three.id, user)
user = refresh_record(user)
activities = ActivityPub.fetch_user_activities(user, nil, %{"pinned" => "true"})
assert 3 = length(activities)
end
describe "flag/1" do
setup do
reporter = insert(:user)
target_account = insert(:user)
content = "foobar"
{:ok, activity} = CommonAPI.post(target_account, %{"status" => content})
context = Utils.generate_context_id()
reporter_ap_id = reporter.ap_id
target_ap_id = target_account.ap_id
activity_ap_id = activity.data["id"]
activity_with_object = Activity.get_by_ap_id_with_object(activity_ap_id)
{:ok,
%{
reporter: reporter,
context: context,
target_account: target_account,
reported_activity: activity,
content: content,
activity_ap_id: activity_ap_id,
activity_with_object: activity_with_object,
reporter_ap_id: reporter_ap_id,
target_ap_id: target_ap_id
}}
end
test "it can create a Flag activity",
%{
reporter: reporter,
context: context,
target_account: target_account,
reported_activity: reported_activity,
content: content,
activity_ap_id: activity_ap_id,
activity_with_object: activity_with_object,
reporter_ap_id: reporter_ap_id,
target_ap_id: target_ap_id
} do
assert {:ok, activity} =
ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [reported_activity],
content: content
})
note_obj = %{
"type" => "Note",
"id" => activity_ap_id,
"content" => content,
"published" => activity_with_object.object.data["published"],
"actor" => AccountView.render("show.json", %{user: target_account})
}
assert %Activity{
actor: ^reporter_ap_id,
data: %{
"type" => "Flag",
"content" => ^content,
"context" => ^context,
"object" => [^target_ap_id, ^note_obj]
}
} = activity
end
test_with_mock "strips status data from Flag, before federating it",
%{
reporter: reporter,
context: context,
target_account: target_account,
reported_activity: reported_activity,
content: content
},
Utils,
[:passthrough],
[] do
{:ok, activity} =
ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [reported_activity],
content: content
})
new_data =
put_in(activity.data, ["object"], [target_account.ap_id, reported_activity.data["id"]])
assert_called(Utils.maybe_federate(%{activity | data: new_data}))
end
end
test "fetch_activities/2 returns activities addressed to a list " do
user = insert(:user)
member = insert(:user)
{:ok, list} = Pleroma.List.create("foo", user)
{:ok, list} = Pleroma.List.follow(list, member)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
activity = Repo.preload(activity, :bookmark)
activity = %Activity{activity | thread_muted?: !!activity.thread_muted?}
assert ActivityPub.fetch_activities([], %{"user" => user}) == [activity]
end
def data_uri do
File.read!("test/fixtures/avatar_data_uri")
end
describe "fetch_activities_bounded" do
test "fetches private posts for followed users" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "thought I looked cute might delete later :3",
"visibility" => "private"
})
[result] = ActivityPub.fetch_activities_bounded([user.follower_address], [])
assert result.id == activity.id
end
test "fetches only public posts for other users" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe", "visibility" => "public"})
{:ok, _private_activity} =
CommonAPI.post(user, %{
"status" => "why is tenshi eating a corndog so cute?",
"visibility" => "private"
})
[result] = ActivityPub.fetch_activities_bounded([], [user.follower_address])
assert result.id == activity.id
end
end
describe "fetch_follow_information_for_user" do
test "syncronizes following/followers counters" do
user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/fuser2/followers",
following_address: "http://localhost:4001/users/fuser2/following"
)
{:ok, info} = ActivityPub.fetch_follow_information_for_user(user)
assert info.follower_count == 527
assert info.following_count == 267
end
test "detects hidden followers" do
mock(fn env ->
case env.url do
"http://localhost:4001/users/masto_closed/followers?page=1" ->
%Tesla.Env{status: 403, body: ""}
_ ->
apply(HttpRequestMock, :request, [env])
end
end)
user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
following_address: "http://localhost:4001/users/masto_closed/following"
)
{:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
assert follow_info.hide_followers == true
assert follow_info.hide_follows == false
end
test "detects hidden follows" do
mock(fn env ->
case env.url do
"http://localhost:4001/users/masto_closed/following?page=1" ->
%Tesla.Env{status: 403, body: ""}
_ ->
apply(HttpRequestMock, :request, [env])
end
end)
user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
following_address: "http://localhost:4001/users/masto_closed/following"
)
{:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
assert follow_info.hide_followers == false
assert follow_info.hide_follows == true
end
test "detects hidden follows/followers for friendica" do
user =
insert(:user,
local: false,
follower_address: "http://localhost:8080/followers/fuser3",
following_address: "http://localhost:8080/following/fuser3"
)
{:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
assert follow_info.hide_followers == true
assert follow_info.follower_count == 296
assert follow_info.following_count == 32
assert follow_info.hide_follows == true
end
test "doesn't crash when follower and following counters are hidden" do
mock(fn env ->
case env.url do
"http://localhost:4001/users/masto_hidden_counters/following" ->
json(%{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "http://localhost:4001/users/masto_hidden_counters/followers"
})
"http://localhost:4001/users/masto_hidden_counters/following?page=1" ->
%Tesla.Env{status: 403, body: ""}
"http://localhost:4001/users/masto_hidden_counters/followers" ->
json(%{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "http://localhost:4001/users/masto_hidden_counters/following"
})
"http://localhost:4001/users/masto_hidden_counters/followers?page=1" ->
%Tesla.Env{status: 403, body: ""}
end
end)
user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_hidden_counters/followers",
following_address: "http://localhost:4001/users/masto_hidden_counters/following"
)
{:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
assert follow_info.hide_followers == true
assert follow_info.follower_count == 0
assert follow_info.hide_follows == true
assert follow_info.following_count == 0
end
end
describe "fetch_favourites/3" do
test "returns a favourite activities sorted by adds to favorite" do
user = insert(:user)
other_user = insert(:user)
user1 = insert(:user)
user2 = insert(:user)
{:ok, a1} = CommonAPI.post(user1, %{"status" => "bla"})
{:ok, _a2} = CommonAPI.post(user2, %{"status" => "traps are happy"})
{:ok, a3} = CommonAPI.post(user2, %{"status" => "Trees Are "})
{:ok, a4} = CommonAPI.post(user2, %{"status" => "Agent Smith "})
{:ok, a5} = CommonAPI.post(user1, %{"status" => "Red or Blue "})
{:ok, _, _} = CommonAPI.favorite(a4.id, user)
{:ok, _, _} = CommonAPI.favorite(a3.id, other_user)
{:ok, _, _} = CommonAPI.favorite(a3.id, user)
{:ok, _, _} = CommonAPI.favorite(a5.id, other_user)
{:ok, _, _} = CommonAPI.favorite(a5.id, user)
{:ok, _, _} = CommonAPI.favorite(a4.id, other_user)
{:ok, _, _} = CommonAPI.favorite(a1.id, user)
{:ok, _, _} = CommonAPI.favorite(a1.id, other_user)
result = ActivityPub.fetch_favourites(user)
assert Enum.map(result, & &1.id) == [a1.id, a5.id, a3.id, a4.id]
result = ActivityPub.fetch_favourites(user, %{"limit" => 2})
assert Enum.map(result, & &1.id) == [a1.id, a5.id]
end
end
describe "Move activity" do
test "create" do
%{ap_id: old_ap_id} = old_user = insert(:user)
%{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id])
follower = insert(:user)
follower_move_opted_out = insert(:user, allow_following_move: false)
User.follow(follower, old_user)
User.follow(follower_move_opted_out, old_user)
assert User.following?(follower, old_user)
assert User.following?(follower_move_opted_out, old_user)
assert {:ok, activity} = ActivityPub.move(old_user, new_user)
assert %Activity{
actor: ^old_ap_id,
data: %{
"actor" => ^old_ap_id,
"object" => ^old_ap_id,
"target" => ^new_ap_id,
"type" => "Move"
},
local: true
} = activity
params = %{
"op" => "move_following",
"origin_id" => old_user.id,
"target_id" => new_user.id
}
assert_enqueued(worker: Pleroma.Workers.BackgroundWorker, args: params)
Pleroma.Workers.BackgroundWorker.perform(params, nil)
refute User.following?(follower, old_user)
assert User.following?(follower, new_user)
assert User.following?(follower_move_opted_out, old_user)
refute User.following?(follower_move_opted_out, new_user)
activity = %Activity{activity | object: nil}
assert [%Notification{activity: ^activity}] =
Notification.for_user(follower, %{with_move: true})
assert [%Notification{activity: ^activity}] =
Notification.for_user(follower_move_opted_out, %{with_move: true})
end
test "old user must be in the new user's `also_known_as` list" do
old_user = insert(:user)
new_user = insert(:user)
assert {:error, "Target account must have the origin in `alsoKnownAs`"} =
ActivityPub.move(old_user, new_user)
end
end
end
diff --git a/test/web/activity_pub/views/object_view_test.exs b/test/federation/activity_pub/views/object_view_test.exs
similarity index 94%
rename from test/web/activity_pub/views/object_view_test.exs
rename to test/federation/activity_pub/views/object_view_test.exs
index 13447dc29..dc90638a6 100644
--- a/test/web/activity_pub/views/object_view_test.exs
+++ b/test/federation/activity_pub/views/object_view_test.exs
@@ -1,66 +1,66 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.ActivityPub.ObjectViewTest do
+defmodule Pleroma.Federation.ActivityPub.ObjectViewTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Pleroma.Object
- alias Pleroma.Web.ActivityPub.ObjectView
+ alias Pleroma.Federation.ActivityPub.ObjectView
alias Pleroma.Web.CommonAPI
test "renders a note object" do
note = insert(:note)
result = ObjectView.render("object.json", %{object: note})
assert result["id"] == note.data["id"]
assert result["to"] == note.data["to"]
assert result["content"] == note.data["content"]
assert result["type"] == "Note"
assert result["@context"]
end
test "renders a note activity" do
note = insert(:note_activity)
object = Object.normalize(note)
result = ObjectView.render("object.json", %{object: note})
assert result["id"] == note.data["id"]
assert result["to"] == note.data["to"]
assert result["object"]["type"] == "Note"
assert result["object"]["content"] == object.data["content"]
assert result["type"] == "Create"
assert result["@context"]
end
test "renders a like activity" do
note = insert(:note_activity)
object = Object.normalize(note)
user = insert(:user)
{:ok, like_activity, _} = CommonAPI.favorite(note.id, user)
result = ObjectView.render("object.json", %{object: like_activity})
assert result["id"] == like_activity.data["id"]
assert result["object"] == object.data["id"]
assert result["type"] == "Like"
end
test "renders an announce activity" do
note = insert(:note_activity)
object = Object.normalize(note)
user = insert(:user)
{:ok, announce_activity, _} = CommonAPI.repeat(note.id, user)
result = ObjectView.render("object.json", %{object: announce_activity})
assert result["id"] == announce_activity.data["id"]
assert result["object"] == object.data["id"]
assert result["type"] == "Announce"
end
end
diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/federation/activity_pub/views/user_view_test.exs
similarity index 98%
rename from test/web/activity_pub/views/user_view_test.exs
rename to test/federation/activity_pub/views/user_view_test.exs
index 8374b8d23..16a6f09dc 100644
--- a/test/web/activity_pub/views/user_view_test.exs
+++ b/test/federation/activity_pub/views/user_view_test.exs
@@ -1,192 +1,192 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.ActivityPub.UserViewTest do
+defmodule Pleroma.Federation.ActivityPub.UserViewTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Pleroma.User
- alias Pleroma.Web.ActivityPub.UserView
+ alias Pleroma.Federation.ActivityPub.UserView
alias Pleroma.Web.CommonAPI
test "Renders a user, including the public key" do
user = insert(:user)
{:ok, user} = User.ensure_keys_present(user)
result = UserView.render("user.json", %{user: user})
assert result["id"] == user.ap_id
assert result["preferredUsername"] == user.nickname
assert String.contains?(result["publicKey"]["publicKeyPem"], "BEGIN PUBLIC KEY")
end
test "Renders profile fields" do
fields = [
%{"name" => "foo", "value" => "bar"}
]
{:ok, user} =
insert(:user)
|> User.upgrade_changeset(%{fields: fields})
|> User.update_and_set_cache()
assert %{
"attachment" => [%{"name" => "foo", "type" => "PropertyValue", "value" => "bar"}]
} = UserView.render("user.json", %{user: user})
end
test "Renders with emoji tags" do
user = insert(:user, emoji: [%{"bib" => "/test"}])
assert %{
"tag" => [
%{
"icon" => %{"type" => "Image", "url" => "/test"},
"id" => "/test",
"name" => ":bib:",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z"
}
]
} = UserView.render("user.json", %{user: user})
end
test "Does not add an avatar image if the user hasn't set one" do
user = insert(:user)
{:ok, user} = User.ensure_keys_present(user)
result = UserView.render("user.json", %{user: user})
refute result["icon"]
refute result["image"]
user =
insert(:user,
avatar: %{"url" => [%{"href" => "https://someurl"}]},
banner: %{"url" => [%{"href" => "https://somebanner"}]}
)
{:ok, user} = User.ensure_keys_present(user)
result = UserView.render("user.json", %{user: user})
assert result["icon"]["url"] == "https://someurl"
assert result["image"]["url"] == "https://somebanner"
end
test "renders an invisible user with the invisible property set to true" do
user = insert(:user, invisible: true)
assert %{"invisible" => true} = UserView.render("service.json", %{user: user})
end
describe "endpoints" do
test "local users have a usable endpoints structure" do
user = insert(:user)
{:ok, user} = User.ensure_keys_present(user)
result = UserView.render("user.json", %{user: user})
assert result["id"] == user.ap_id
%{
"sharedInbox" => _,
"oauthAuthorizationEndpoint" => _,
"oauthRegistrationEndpoint" => _,
"oauthTokenEndpoint" => _
} = result["endpoints"]
end
test "remote users have an empty endpoints structure" do
user = insert(:user, local: false)
{:ok, user} = User.ensure_keys_present(user)
result = UserView.render("user.json", %{user: user})
assert result["id"] == user.ap_id
assert result["endpoints"] == %{}
end
test "instance users do not expose oAuth endpoints" do
user = insert(:user, nickname: nil, local: true)
{:ok, user} = User.ensure_keys_present(user)
result = UserView.render("user.json", %{user: user})
refute result["endpoints"]["oauthAuthorizationEndpoint"]
refute result["endpoints"]["oauthRegistrationEndpoint"]
refute result["endpoints"]["oauthTokenEndpoint"]
end
end
describe "followers" do
test "sets totalItems to zero when followers are hidden" do
user = insert(:user)
other_user = insert(:user)
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user})
user = Map.merge(user, %{hide_followers_count: true, hide_followers: true})
refute UserView.render("followers.json", %{user: user}) |> Map.has_key?("totalItems")
end
test "sets correct totalItems when followers are hidden but the follower counter is not" do
user = insert(:user)
other_user = insert(:user)
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user})
user = Map.merge(user, %{hide_followers_count: false, hide_followers: true})
assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user})
end
end
describe "following" do
test "sets totalItems to zero when follows are hidden" do
user = insert(:user)
other_user = insert(:user)
{:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user})
user = Map.merge(user, %{hide_follows_count: true, hide_follows: true})
assert %{"totalItems" => 0} = UserView.render("following.json", %{user: user})
end
test "sets correct totalItems when follows are hidden but the follow counter is not" do
user = insert(:user)
other_user = insert(:user)
{:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user})
user = Map.merge(user, %{hide_follows_count: false, hide_follows: true})
assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user})
end
end
test "activity collection page aginates correctly" do
user = insert(:user)
posts =
for i <- 0..25 do
{:ok, activity} = CommonAPI.post(user, %{"status" => "post #{i}"})
activity
end
# outbox sorts chronologically, newest first, with ten per page
posts = Enum.reverse(posts)
%{"next" => next_url} =
UserView.render("activity_collection_page.json", %{
iri: "#{user.ap_id}/outbox",
activities: Enum.take(posts, 10)
})
next_id = Enum.at(posts, 9).id
assert next_url =~ next_id
%{"next" => next_url} =
UserView.render("activity_collection_page.json", %{
iri: "#{user.ap_id}/outbox",
activities: Enum.take(Enum.drop(posts, 10), 10)
})
next_id = Enum.at(posts, 19).id
assert next_url =~ next_id
end
end
diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs
index 3cd1c1c69..01ea5a6a9 100644
--- a/test/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/web/activity_pub/activity_pub_controller_test.exs
@@ -1,1050 +1,1050 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
+defmodule Pleroma.Federation.ActivityPub.ActivityPubControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Storage.Repo
import Pleroma.Factory
alias Pleroma.Activity
alias Pleroma.Delivery
alias Pleroma.Federation.ActivityPub.Relay
alias Pleroma.Federation.ActivityPub.Utils
alias Pleroma.Instances
alias Pleroma.Object
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ObjectView
- alias Pleroma.Web.ActivityPub.UserView
+ alias Pleroma.Federation.ActivityPub.ObjectView
+ alias Pleroma.Federation.ActivityPub.UserView
alias Pleroma.Web.CommonAPI
alias Pleroma.Workers.ReceiverWorker
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
clear_config_all([:instance, :federating],
do: Pleroma.Config.put([:instance, :federating], true)
)
describe "/relay" 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
Pleroma.Config.put([:instance, :allow_relay], false)
conn
|> get(activity_pub_path(conn, :relay))
|> json_response(404)
|> assert
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
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
end
describe "/object/:uuid" do
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 "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 "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 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 "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") |> Poison.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
test "it clears `unreachable` federation status of the sender", %{conn: conn} do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.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
end
describe "/users/:nickname/inbox" do
setup do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
[data: data]
end
test "it inserts an incoming activity into the database", %{conn: conn, data: data} do
user = insert(:user)
data = Map.put(data, "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 =
Map.put(data, "to", user.ap_id)
|> Map.delete("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 =
Map.put(data, "cc", user.ap_id)
|> Map.delete("to")
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 =
Map.put(data, "bcc", user.ap_id)
|> Map.delete("to")
|> Map.delete("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 announces with to as string instead of array", %{conn: conn} do
user = insert(:user)
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"actor" => "http://mastodon.example.org/users/admin",
"id" => "http://mastodon.example.org/users/admin/statuses/19512778738411822/activity",
"object" => "https://mastodon.social/users/emelie/statuses/101849165031453009",
"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} = 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)
otheruser = insert(:user)
conn =
conn
|> assign(:user, otheruser)
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/inbox")
assert json_response(conn, 403)
end
test "it doesn't crash without an authenticated user", %{conn: conn} do
user = insert(:user)
conn =
conn
|> 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)
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
test "it removes all follower collections but actor's", %{conn: conn} do
[actor, recipient] = insert_pair(:user)
data =
File.read!("test/fixtures/activitypub-client-post-activity.json")
|> Poison.decode!()
object = Map.put(data["object"], "attributedTo", actor.ap_id)
data =
data
|> Map.put("id", Utils.generate_object_id())
|> Map.put("actor", actor.ap_id)
|> Map.put("object", object)
|> Map.put("cc", [
recipient.follower_address,
actor.follower_address
])
|> Map.put("to", [
recipient.ap_id,
recipient.follower_address,
"https://www.w3.org/ns/activitystreams#Public"
])
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))
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
end
describe "/users/:nickname/outbox" do
test "it will not bomb when there is no activity", %{conn: conn} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/outbox")
result = json_response(conn, 200)
assert user.ap_id <> "/outbox" == 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)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
conn =
conn
|> 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
|> 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 rejects posts from other users", %{conn: conn} do
data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
user = insert(:user)
otheruser = insert(:user)
conn =
conn
|> assign(:user, otheruser)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/outbox", data)
assert json_response(conn, 403)
end
test "it inserts an incoming create activity into the database", %{conn: conn} do
data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
user = insert(:user)
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"])
end
test "it rejects an incoming activity with bogus type", %{conn: conn} do
data = File.read!("test/fixtures/activitypub-client-post-activity.json") |> Poison.decode!()
user = insert(:user)
data =
data
|> Map.put("type", "BadType")
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 erects a tombstone when receiving a delete activity", %{conn: conn} do
note_activity = insert(:note_activity)
note_object = Object.normalize(note_activity)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
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)
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["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)
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, 400)
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)
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
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
|> assign(:relay, true)
|> get("/relay/followers")
|> json_response(200)
assert result["first"]["orderedItems"] == [user.ap_id]
end
end
describe "/relay/following" do
test "it returns relay following", %{conn: conn} do
result =
conn
|> assign(:relay, true)
|> get("/relay/following")
|> json_response(200)
assert result["first"]["orderedItems"] == []
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
|> get("/users/#{user_two.nickname}/followers")
|> json_response(200)
assert result["first"]["orderedItems"] == [user.ap_id]
end
test "it returns 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
|> 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 not authenticated",
%{conn: conn} do
user = insert(:user, hide_followers: true)
result =
conn
|> get("/users/#{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
|> 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
|> get("/users/#{user.nickname}/followers?page=2")
|> json_response(200)
assert length(result["orderedItems"]) == 5
assert result["totalItems"] == 15
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
|> 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, hide_follows: true)
user_two = insert(:user)
User.follow(user, user_two)
result =
conn
|> get("/users/#{user.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 not authenticated",
%{conn: conn} do
user = insert(:user, hide_follows: true)
result =
conn
|> get("/users/#{user.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
|> 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
|> get("/users/#{user.nickname}/following?page=2")
|> json_response(200)
assert length(result["orderedItems"]) == 5
assert result["totalItems"] == 15
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)
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)
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)
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)
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 "Additionnal ActivityPub C2S endpoints" do
test "/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)
end
clear_config([:media_proxy])
clear_config([Pleroma.Upload])
test "uploadMedia", %{conn: conn} do
user = insert(:user)
desc = "Description of the image"
image = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
conn =
conn
|> assign(:user, user)
|> post("/api/ap/upload_media", %{"file" => image, "description" => desc})
assert object = json_response(conn, :created)
assert object["name"] == desc
assert object["type"] == "Document"
assert object["actor"] == user.ap_id
end
end
end

File Metadata

Mime Type
text/x-diff
Expires
Fri, Oct 30, 9:34 PM (17 h, 2 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1800570
Default Alt Text
(178 KB)

Event Timeline