aboutsummaryrefslogtreecommitdiff
path: root/lib/pleroma/web/activity_pub/activity_pub_controller.ex
diff options
context:
space:
mode:
authordcc <dcc@logografos.com>2023-09-02 00:52:52 -0700
committerdcc <dcc@logografos.com>2023-09-02 00:52:52 -0700
commit3a4773c3c2bd0bbef244eb519b07208da9108e49 (patch)
tree973567a6f3abb37bfb0f785b1cad14ed55840ef5 /lib/pleroma/web/activity_pub/activity_pub_controller.ex
downloadanni-3a4773c3c2bd0bbef244eb519b07208da9108e49.tar.gz
anni-3a4773c3c2bd0bbef244eb519b07208da9108e49.tar.bz2
anni-3a4773c3c2bd0bbef244eb519b07208da9108e49.zip
First
Diffstat (limited to 'lib/pleroma/web/activity_pub/activity_pub_controller.ex')
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub_controller.ex542
1 files changed, 542 insertions, 0 deletions
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
new file mode 100644
index 0000000..1357c37
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -0,0 +1,542 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ActivityPubController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Activity
+ alias Pleroma.Delivery
+ alias Pleroma.Object
+ alias Pleroma.Object.Fetcher
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.InternalFetchActor
+ alias Pleroma.Web.ActivityPub.ObjectView
+ alias Pleroma.Web.ActivityPub.Pipeline
+ alias Pleroma.Web.ActivityPub.Relay
+ alias Pleroma.Web.ActivityPub.Transmogrifier
+ alias Pleroma.Web.ActivityPub.UserView
+ alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.Web.ControllerHelper
+ alias Pleroma.Web.Endpoint
+ alias Pleroma.Web.Federator
+ alias Pleroma.Web.Plugs.EnsureAuthenticatedPlug
+ alias Pleroma.Web.Plugs.FederatingPlug
+
+ require Logger
+
+ action_fallback(:errors)
+
+ @federating_only_actions [:internal_fetch, :relay, :relay_following, :relay_followers]
+
+ plug(FederatingPlug when action in @federating_only_actions)
+
+ plug(
+ EnsureAuthenticatedPlug,
+ [unless_func: &FederatingPlug.federating?/1] when action not in @federating_only_actions
+ )
+
+ # Note: :following and :followers must be served even without authentication (as via :api)
+ plug(
+ EnsureAuthenticatedPlug
+ when action in [:read_inbox, :update_outbox, :whoami, :upload_media]
+ )
+
+ plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:upload_media])
+
+ plug(
+ Pleroma.Web.Plugs.Cache,
+ [query_params: false, tracking_fun: &__MODULE__.track_object_fetch/2]
+ when action in [:activity, :object]
+ )
+
+ plug(:set_requester_reachable when action in [:inbox])
+ plug(:relay_active? when action in [:relay])
+
+ defp relay_active?(conn, _) do
+ if Pleroma.Config.get([:instance, :allow_relay]) do
+ conn
+ else
+ conn
+ |> render_error(:not_found, "not found")
+ |> halt()
+ end
+ end
+
+ def user(conn, %{"nickname" => nickname}) do
+ with %User{local: true} = user <- User.get_cached_by_nickname(nickname) 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
+
+ # POST /relay/inbox -or- POST /internal/fetch/inbox
+ def inbox(conn, %{"type" => "Create"} = params) do
+ if FederatingPlug.federating?() do
+ post_inbox_relayed_create(conn, params)
+ else
+ conn
+ |> put_status(:bad_request)
+ |> json("Not federating")
+ end
+ end
+
+ def inbox(conn, _params) do
+ conn
+ |> put_status(:bad_request)
+ |> json("error, missing HTTP Signature")
+ end
+
+ defp post_inbox_relayed_create(conn, params) do
+ Logger.debug(
+ "Signature missing or not from author, relayed Create message, fetching object from source"
+ )
+
+ Fetcher.fetch_object_from_id(params["object"]["id"])
+
+ json(conn, "ok")
+ end
+
+ defp 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.warn(fn -> "AP C2S: #{inspect(e)}" end)
+
+ conn
+ |> put_status(:bad_request)
+ |> json("Bad Request")
+ end
+ end
+
+ def update_outbox(%{assigns: %{user: %User{} = user}} = conn, %{"nickname" => nickname}) do
+ err =
+ dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}",
+ nickname: nickname,
+ as_nickname: user.nickname
+ )
+
+ conn
+ |> put_status(:forbidden)
+ |> json(err)
+ end
+
+ defp errors(conn, {:error, :not_found}) do
+ conn
+ |> put_status(:not_found)
+ |> json(dgettext("errors", "Not found"))
+ end
+
+ defp errors(conn, _e) do
+ conn
+ |> put_status(:internal_server_error)
+ |> json(dgettext("errors", "error"))
+ end
+
+ defp set_requester_reachable(%Plug.Conn{} = conn, _) do
+ with actor <- conn.params["actor"],
+ true <- is_binary(actor) do
+ Pleroma.Instances.set_reachable(actor)
+ end
+
+ conn
+ end
+
+ 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