forked from AkkomaGang/akkoma
FloatingGhost
98cb255d12
OTP builds to 1.15 Changelog entry Ensure policies are fully loaded Fix :warn use main branch for linkify Fix warn in tests Migrations for phoenix 1.17 Revert "Migrations for phoenix 1.17" This reverts commit 6a3b2f15b74ea5e33150529385215b7a531f3999. Oban upgrade Add default empty whitelist mix format limit test to amd64 OTP 26 tests for 1.15 use OTP_VERSION tag baka just 1.15 Massive deps update Update locale, deps Mix format shell???? multiline??? ? max cases 1 use assert_recieve don't put_env in async tests don't async conn/fs tests mix format FIx some uploader issues Fix tests
520 lines
16 KiB
Elixir
520 lines
16 KiB
Elixir
# Pleroma: A lightweight social networking server
|
|
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|
|
use Pleroma.Web, :controller
|
|
|
|
alias Pleroma.Activity
|
|
alias Pleroma.Delivery
|
|
alias Pleroma.Object
|
|
alias Pleroma.User
|
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
|
alias Pleroma.Web.ActivityPub.InternalFetchActor
|
|
alias Pleroma.Web.ActivityPub.ObjectView
|
|
alias Pleroma.Web.ActivityPub.Pipeline
|
|
alias Pleroma.Web.ActivityPub.Relay
|
|
alias Pleroma.Web.ActivityPub.Transmogrifier
|
|
alias Pleroma.Web.ActivityPub.UserView
|
|
alias Pleroma.Web.ActivityPub.Utils
|
|
alias Pleroma.Web.ActivityPub.Visibility
|
|
alias Pleroma.Web.ControllerHelper
|
|
alias Pleroma.Web.Endpoint
|
|
alias Pleroma.Web.Federator
|
|
alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
|
|
alias Pleroma.Web.Plugs.FederatingPlug
|
|
|
|
require Logger
|
|
|
|
action_fallback(:errors)
|
|
|
|
@federating_only_actions [:internal_fetch, :relay, :relay_following, :relay_followers]
|
|
|
|
plug(FederatingPlug when action in @federating_only_actions)
|
|
|
|
plug(
|
|
EnsureAuthenticatedPlug,
|
|
[unless_func: &FederatingPlug.federating?/1] when action not in @federating_only_actions
|
|
)
|
|
|
|
# Note: :following and :followers must be served even without authentication (as via :api)
|
|
plug(
|
|
EnsureAuthenticatedPlug
|
|
when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
|
|
)
|
|
|
|
plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media])
|
|
|
|
plug(
|
|
Pleroma.Web.Plugs.Cache,
|
|
[query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
|
|
when action in [:activity, :object]
|
|
)
|
|
|
|
plug(:set_requester_reachable when action in [:inbox])
|
|
plug(:relay_active? when action in [:relay])
|
|
|
|
defp relay_active?(conn, _) do
|
|
if Pleroma.Config.get([:instance, :allow_relay]) do
|
|
conn
|
|
else
|
|
conn
|
|
|> render_error(:not_found, "not found")
|
|
|> halt()
|
|
end
|
|
end
|
|
|
|
def user(conn, %{"nickname" => nickname}) do
|
|
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
|
|
conn
|
|
|> put_resp_content_type("application/activity+json")
|
|
|> put_view(UserView)
|
|
|> render("user.json", %{user: user})
|
|
else
|
|
nil -> {:error, :not_found}
|
|
%{local: false} -> {:error, :not_found}
|
|
end
|
|
end
|
|
|
|
def object(%{assigns: assigns} = conn, _) do
|
|
with ap_id <- Endpoint.url() <> conn.request_path,
|
|
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
|
|
user <- Map.get(assigns, :user, nil),
|
|
{_, true} <- {:visible?, Visibility.visible_for_user?(object, user)} do
|
|
conn
|
|
|> maybe_skip_cache(user)
|
|
|> assign(:tracking_fun_data, object.id)
|
|
|> set_cache_ttl_for(object)
|
|
|> put_resp_content_type("application/activity+json")
|
|
|> put_view(ObjectView)
|
|
|> render("object.json", object: object)
|
|
else
|
|
{:visible?, false} -> {:error, :not_found}
|
|
nil -> {:error, :not_found}
|
|
end
|
|
end
|
|
|
|
def track_object_fetch(conn, nil), do: conn
|
|
|
|
def track_object_fetch(conn, object_id) do
|
|
with %{assigns: %{user: %User{id: user_id}}} <- conn do
|
|
Delivery.create(object_id, user_id)
|
|
end
|
|
|
|
conn
|
|
end
|
|
|
|
def activity(%{assigns: assigns} = conn, _) do
|
|
with ap_id <- Endpoint.url() <> conn.request_path,
|
|
%Activity{} = activity <- Activity.normalize(ap_id),
|
|
{_, true} <- {:local?, activity.local},
|
|
user <- Map.get(assigns, :user, nil),
|
|
{_, true} <- {:visible?, Visibility.visible_for_user?(activity, user)} do
|
|
conn
|
|
|> maybe_skip_cache(user)
|
|
|> maybe_set_tracking_data(activity)
|
|
|> set_cache_ttl_for(activity)
|
|
|> put_resp_content_type("application/activity+json")
|
|
|> put_view(ObjectView)
|
|
|> render("object.json", object: activity)
|
|
else
|
|
{:visible?, false} -> {:error, :not_found}
|
|
{:local?, false} -> {:error, :not_found}
|
|
nil -> {:error, :not_found}
|
|
end
|
|
end
|
|
|
|
defp maybe_set_tracking_data(conn, %Activity{data: %{"type" => "Create"}} = activity) do
|
|
object_id = Object.normalize(activity, fetch: false).id
|
|
assign(conn, :tracking_fun_data, object_id)
|
|
end
|
|
|
|
defp maybe_set_tracking_data(conn, _activity), do: conn
|
|
|
|
defp set_cache_ttl_for(conn, %Activity{object: object}) do
|
|
set_cache_ttl_for(conn, object)
|
|
end
|
|
|
|
defp set_cache_ttl_for(conn, entity) do
|
|
ttl =
|
|
case entity do
|
|
%Object{data: %{"type" => "Question"}} ->
|
|
Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
|
|
|
|
%Object{} ->
|
|
Pleroma.Config.get([:web_cache_ttl, :activity_pub])
|
|
|
|
_ ->
|
|
nil
|
|
end
|
|
|
|
assign(conn, :cache_ttl, ttl)
|
|
end
|
|
|
|
def maybe_skip_cache(conn, user) do
|
|
if user do
|
|
conn
|
|
|> assign(:skip_cache, true)
|
|
else
|
|
conn
|
|
end
|
|
end
|
|
|
|
# GET /relay/following
|
|
def relay_following(conn, _params) do
|
|
with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
|
|
conn
|
|
|> put_resp_content_type("application/activity+json")
|
|
|> put_view(UserView)
|
|
|> render("following.json", %{user: Relay.get_actor()})
|
|
end
|
|
end
|
|
|
|
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
|
|
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
|
{: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) do
|
|
conn
|
|
|> put_resp_content_type("application/activity+json")
|
|
|> put_view(UserView)
|
|
|> render("following.json", %{user: user, for: for_user})
|
|
end
|
|
end
|
|
|
|
# GET /relay/followers
|
|
def relay_followers(conn, _params) do
|
|
with %{halted: false} = conn <- FederatingPlug.call(conn, []) do
|
|
conn
|
|
|> put_resp_content_type("application/activity+json")
|
|
|> put_view(UserView)
|
|
|> render("followers.json", %{user: Relay.get_actor()})
|
|
end
|
|
end
|
|
|
|
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
|
|
with %User{} = user <- User.get_cached_by_nickname(nickname),
|
|
{: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) do
|
|
conn
|
|
|> put_resp_content_type("application/activity+json")
|
|
|> put_view(UserView)
|
|
|> render("followers.json", %{user: user, for: for_user})
|
|
end
|
|
end
|
|
|
|
def outbox(
|
|
%{assigns: %{user: for_user}} = conn,
|
|
%{"nickname" => nickname, "page" => page?} = params
|
|
)
|
|
when page? in [true, "true"] do
|
|
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
|
# "include_poll_votes" is a hack because postgres generates inefficient
|
|
# queries when filtering by 'Answer', poll votes will be hidden by the
|
|
# visibility filter in this case anyway
|
|
params =
|
|
params
|
|
|> Map.drop(["nickname", "page"])
|
|
|> Map.put("include_poll_votes", true)
|
|
|> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
|
|
|
|
activities = ActivityPub.fetch_user_activities(user, for_user, params)
|
|
|
|
conn
|
|
|> put_resp_content_type("application/activity+json")
|
|
|> put_view(UserView)
|
|
|> render("activity_collection_page.json", %{
|
|
activities: activities,
|
|
pagination: ControllerHelper.get_pagination_fields(conn, activities),
|
|
iri: "#{user.ap_id}/outbox"
|
|
})
|
|
end
|
|
end
|
|
|
|
def outbox(conn, %{"nickname" => nickname}) do
|
|
with %User{} = user <- User.get_cached_by_nickname(nickname) 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
|
|
|
|
def inbox(%{assigns: %{valid_signature: false}} = conn, _params) do
|
|
conn
|
|
|> put_status(:bad_request)
|
|
|> json("Invalid HTTP Signature")
|
|
end
|
|
|
|
def inbox(conn, _params) do
|
|
conn
|
|
|> put_status(:bad_request)
|
|
|> json("error, missing HTTP Signature")
|
|
end
|
|
|
|
defp represent_service_actor(%User{} = user, conn) do
|
|
conn
|
|
|> put_resp_content_type("application/activity+json")
|
|
|> put_view(UserView)
|
|
|> render("user.json", %{user: user})
|
|
end
|
|
|
|
defp represent_service_actor(nil, _), do: {:error, :not_found}
|
|
|
|
def relay(conn, _params) do
|
|
Relay.get_actor()
|
|
|> represent_service_actor(conn)
|
|
end
|
|
|
|
def internal_fetch(conn, _params) do
|
|
InternalFetchActor.get_actor()
|
|
|> represent_service_actor(conn)
|
|
end
|
|
|
|
@doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
|
|
def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
|
|
conn
|
|
|> put_resp_content_type("application/activity+json")
|
|
|> put_view(UserView)
|
|
|> render("user.json", %{user: user})
|
|
end
|
|
|
|
def read_inbox(
|
|
%{assigns: %{user: %User{nickname: nickname} = user}} = conn,
|
|
%{"nickname" => nickname, "page" => page?} = params
|
|
)
|
|
when page? in [true, "true"] do
|
|
params =
|
|
params
|
|
|> Map.drop(["nickname", "page"])
|
|
|> Map.put("blocking_user", user)
|
|
|> Map.put("user", user)
|
|
|> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
|
|
|
|
activities =
|
|
[user.ap_id | User.following(user)]
|
|
|> ActivityPub.fetch_activities(params)
|
|
|> Enum.reverse()
|
|
|
|
conn
|
|
|> put_resp_content_type("application/activity+json")
|
|
|> put_view(UserView)
|
|
|> render("activity_collection_page.json", %{
|
|
activities: activities,
|
|
pagination: ControllerHelper.get_pagination_fields(conn, activities),
|
|
iri: "#{user.ap_id}/inbox"
|
|
})
|
|
end
|
|
|
|
def read_inbox(%{assigns: %{user: %User{nickname: nickname} = user}} = conn, %{
|
|
"nickname" => nickname
|
|
}) do
|
|
conn
|
|
|> put_resp_content_type("application/activity+json")
|
|
|> put_view(UserView)
|
|
|> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
|
|
end
|
|
|
|
def read_inbox(%{assigns: %{user: %User{nickname: as_nickname}}} = conn, %{
|
|
"nickname" => nickname
|
|
}) do
|
|
err =
|
|
dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
|
|
nickname: nickname,
|
|
as_nickname: as_nickname
|
|
)
|
|
|
|
conn
|
|
|> put_status(:forbidden)
|
|
|> json(err)
|
|
end
|
|
|
|
defp fix_user_message(%User{ap_id: actor}, %{"type" => "Create", "object" => object} = activity)
|
|
when is_map(object) do
|
|
length =
|
|
[object["content"], object["summary"], object["name"]]
|
|
|> Enum.filter(&is_binary(&1))
|
|
|> Enum.join("")
|
|
|> String.length()
|
|
|
|
limit = Pleroma.Config.get([:instance, :limit])
|
|
|
|
if length < limit do
|
|
object =
|
|
object
|
|
|> Transmogrifier.strip_internal_fields()
|
|
|> Map.put("attributedTo", actor)
|
|
|> Map.put("actor", actor)
|
|
|> Map.put("id", Utils.generate_object_id())
|
|
|
|
{:ok, Map.put(activity, "object", object)}
|
|
else
|
|
{:error,
|
|
dgettext(
|
|
"errors",
|
|
"Character limit (%{limit} characters) exceeded, contains %{length} characters",
|
|
limit: limit,
|
|
length: length
|
|
)}
|
|
end
|
|
end
|
|
|
|
defp fix_user_message(
|
|
%User{ap_id: actor} = user,
|
|
%{"type" => "Delete", "object" => object} = activity
|
|
) do
|
|
with {_, %Object{data: object_data}} <- {:normalize, Object.normalize(object, fetch: false)},
|
|
{_, true} <- {:permission, user.is_moderator || actor == object_data["actor"]} do
|
|
{:ok, activity}
|
|
else
|
|
{:normalize, _} ->
|
|
{:error, "No such object found"}
|
|
|
|
{:permission, _} ->
|
|
{:forbidden, "You can't delete this object"}
|
|
end
|
|
end
|
|
|
|
defp fix_user_message(%User{}, activity) do
|
|
{:ok, activity}
|
|
end
|
|
|
|
def update_outbox(
|
|
%{assigns: %{user: %User{nickname: nickname, ap_id: actor} = user}} = conn,
|
|
%{"nickname" => nickname} = params
|
|
) do
|
|
params =
|
|
params
|
|
|> Map.drop(["nickname"])
|
|
|> Map.put("id", Utils.generate_activity_id())
|
|
|> Map.put("actor", actor)
|
|
|
|
with {:ok, params} <- fix_user_message(user, params),
|
|
{:ok, activity, _} <- Pipeline.common_pipeline(params, local: true),
|
|
%Activity{data: activity_data} <- Activity.normalize(activity) do
|
|
conn
|
|
|> put_status(:created)
|
|
|> put_resp_header("location", activity_data["id"])
|
|
|> json(activity_data)
|
|
else
|
|
{:forbidden, message} ->
|
|
conn
|
|
|> put_status(:forbidden)
|
|
|> json(message)
|
|
|
|
{:error, message} ->
|
|
conn
|
|
|> put_status(:bad_request)
|
|
|> json(message)
|
|
|
|
e ->
|
|
Logger.warning(fn -> "AP C2S: #{inspect(e)}" end)
|
|
|
|
conn
|
|
|> put_status(:bad_request)
|
|
|> json("Bad Request")
|
|
end
|
|
end
|
|
|
|
def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
|
|
err =
|
|
dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
|
|
nickname: nickname,
|
|
as_nickname: user.nickname
|
|
)
|
|
|
|
conn
|
|
|> put_status(:forbidden)
|
|
|> json(err)
|
|
end
|
|
|
|
defp errors(conn, {:error, :not_found}) do
|
|
conn
|
|
|> put_status(:not_found)
|
|
|> json(dgettext("errors", "Not found"))
|
|
end
|
|
|
|
defp errors(conn, _e) do
|
|
conn
|
|
|> put_status(:internal_server_error)
|
|
|> json(dgettext("errors", "error"))
|
|
end
|
|
|
|
defp set_requester_reachable(%Plug.Conn{} = conn, _) do
|
|
with actor <- conn.params["actor"],
|
|
true <- is_binary(actor) do
|
|
Pleroma.Instances.set_reachable(actor)
|
|
end
|
|
|
|
conn
|
|
end
|
|
|
|
def upload_media(%{assigns: %{user: %User{} = user}} = conn, %{"file" => file} = data) do
|
|
with {:ok, object} <-
|
|
ActivityPub.upload(
|
|
file,
|
|
actor: User.ap_id(user),
|
|
description: Map.get(data, "description")
|
|
) do
|
|
Logger.debug(inspect(object))
|
|
|
|
conn
|
|
|> put_status(:created)
|
|
|> json(object.data)
|
|
end
|
|
end
|
|
|
|
def pinned(conn, %{"nickname" => nickname}) do
|
|
with %User{} = user <- User.get_cached_by_nickname(nickname) do
|
|
conn
|
|
|> put_resp_header("content-type", "application/activity+json")
|
|
|> json(UserView.render("featured.json", %{user: user}))
|
|
end
|
|
end
|
|
end
|