Fork of Pleroma with site-specific changes and feature branches https://git.pleroma.social/pleroma/pleroma
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

282 lines
8.0KB

  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
  3. # SPDX-License-Identifier: AGPL-3.0-only
  4. defmodule Pleroma.Web.ActivityPub.Publisher do
  5. alias Pleroma.Activity
  6. alias Pleroma.Config
  7. alias Pleroma.Delivery
  8. alias Pleroma.HTTP
  9. alias Pleroma.Instances
  10. alias Pleroma.Object
  11. alias Pleroma.Repo
  12. alias Pleroma.User
  13. alias Pleroma.Web.ActivityPub.Relay
  14. alias Pleroma.Web.ActivityPub.Transmogrifier
  15. require Pleroma.Constants
  16. import Pleroma.Web.ActivityPub.Visibility
  17. @behaviour Pleroma.Web.Federator.Publisher
  18. require Logger
  19. @moduledoc """
  20. ActivityPub outgoing federation module.
  21. """
  22. @doc """
  23. Determine if an activity can be represented by running it through Transmogrifier.
  24. """
  25. def is_representable?(%Activity{} = activity) do
  26. with {:ok, _data} <- Transmogrifier.prepare_outgoing(activity.data) do
  27. true
  28. else
  29. _e ->
  30. false
  31. end
  32. end
  33. @doc """
  34. Publish a single message to a peer. Takes a struct with the following
  35. parameters set:
  36. * `inbox`: the inbox to publish to
  37. * `json`: the JSON message body representing the ActivityPub message
  38. * `actor`: the actor which is signing the message
  39. * `id`: the ActivityStreams URI of the message
  40. """
  41. def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
  42. Logger.debug("Federating #{id} to #{inbox}")
  43. uri = %{path: path} = URI.parse(inbox)
  44. digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
  45. date = Pleroma.Signature.signed_date()
  46. signature =
  47. Pleroma.Signature.sign(actor, %{
  48. "(request-target)": "post #{path}",
  49. host: signature_host(uri),
  50. "content-length": byte_size(json),
  51. digest: digest,
  52. date: date
  53. })
  54. with {:ok, %{status: code}} when code in 200..299 <-
  55. result =
  56. HTTP.post(
  57. inbox,
  58. json,
  59. [
  60. {"Content-Type", "application/activity+json"},
  61. {"Date", date},
  62. {"signature", signature},
  63. {"digest", digest}
  64. ]
  65. ) do
  66. if not Map.has_key?(params, :unreachable_since) || params[:unreachable_since] do
  67. Instances.set_reachable(inbox)
  68. end
  69. result
  70. else
  71. {_post_result, response} ->
  72. unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
  73. {:error, response}
  74. end
  75. end
  76. def publish_one(%{actor_id: actor_id} = params) do
  77. actor = User.get_cached_by_id(actor_id)
  78. params
  79. |> Map.delete(:actor_id)
  80. |> Map.put(:actor, actor)
  81. |> publish_one()
  82. end
  83. defp signature_host(%URI{port: port, scheme: scheme, host: host}) do
  84. if port == URI.default_port(scheme) do
  85. host
  86. else
  87. "#{host}:#{port}"
  88. end
  89. end
  90. defp should_federate?(inbox, public) do
  91. if public do
  92. true
  93. else
  94. %{host: host} = URI.parse(inbox)
  95. quarantined_instances =
  96. Config.get([:instance, :quarantined_instances], [])
  97. |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
  98. !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
  99. end
  100. end
  101. @spec recipients(User.t(), Activity.t()) :: list(User.t()) | []
  102. defp recipients(actor, activity) do
  103. followers =
  104. if actor.follower_address in activity.recipients do
  105. User.get_external_followers(actor)
  106. else
  107. []
  108. end
  109. fetchers =
  110. with %Activity{data: %{"type" => "Delete"}} <- activity,
  111. %Object{id: object_id} <- Object.normalize(activity, fetch: false),
  112. fetchers <- User.get_delivered_users_by_object_id(object_id),
  113. _ <- Delivery.delete_all_by_object_id(object_id) do
  114. fetchers
  115. else
  116. _ ->
  117. []
  118. end
  119. Pleroma.Web.Federator.Publisher.remote_users(actor, activity) ++ followers ++ fetchers
  120. end
  121. defp get_cc_ap_ids(ap_id, recipients) do
  122. host = Map.get(URI.parse(ap_id), :host)
  123. recipients
  124. |> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end)
  125. |> Enum.map(& &1.ap_id)
  126. end
  127. defp maybe_use_sharedinbox(%User{shared_inbox: nil, inbox: inbox}), do: inbox
  128. defp maybe_use_sharedinbox(%User{shared_inbox: shared_inbox}), do: shared_inbox
  129. @doc """
  130. Determine a user inbox to use based on heuristics. These heuristics
  131. are based on an approximation of the ``sharedInbox`` rules in the
  132. [ActivityPub specification][ap-sharedinbox].
  133. Please do not edit this function (or its children) without reading
  134. the spec, as editing the code is likely to introduce some breakage
  135. without some familiarity.
  136. [ap-sharedinbox]: https://www.w3.org/TR/activitypub/#shared-inbox-delivery
  137. """
  138. def determine_inbox(
  139. %Activity{data: activity_data},
  140. %User{inbox: inbox} = user
  141. ) do
  142. to = activity_data["to"] || []
  143. cc = activity_data["cc"] || []
  144. type = activity_data["type"]
  145. cond do
  146. type == "Delete" ->
  147. maybe_use_sharedinbox(user)
  148. Pleroma.Constants.as_public() in to || Pleroma.Constants.as_public() in cc ->
  149. maybe_use_sharedinbox(user)
  150. length(to) + length(cc) > 1 ->
  151. maybe_use_sharedinbox(user)
  152. true ->
  153. inbox
  154. end
  155. end
  156. @doc """
  157. Publishes an activity with BCC to all relevant peers.
  158. """
  159. def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity)
  160. when is_list(bcc) and bcc != [] do
  161. public = is_public?(activity)
  162. {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
  163. recipients = recipients(actor, activity)
  164. inboxes =
  165. recipients
  166. |> Enum.filter(&User.ap_enabled?/1)
  167. |> Enum.map(fn actor -> actor.inbox end)
  168. |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
  169. |> Instances.filter_reachable()
  170. Repo.checkout(fn ->
  171. Enum.each(inboxes, fn {inbox, unreachable_since} ->
  172. %User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end)
  173. # Get all the recipients on the same host and add them to cc. Otherwise, a remote
  174. # instance would only accept a first message for the first recipient and ignore the rest.
  175. cc = get_cc_ap_ids(ap_id, recipients)
  176. json =
  177. data
  178. |> Map.put("cc", cc)
  179. |> Jason.encode!()
  180. Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{
  181. inbox: inbox,
  182. json: json,
  183. actor_id: actor.id,
  184. id: activity.data["id"],
  185. unreachable_since: unreachable_since
  186. })
  187. end)
  188. end)
  189. end
  190. # Publishes an activity to all relevant peers.
  191. def publish(%User{} = actor, %Activity{} = activity) do
  192. public = is_public?(activity)
  193. if public && Config.get([:instance, :allow_relay]) do
  194. Logger.debug(fn -> "Relaying #{activity.data["id"]} out" end)
  195. Relay.publish(activity)
  196. end
  197. {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
  198. json = Jason.encode!(data)
  199. recipients(actor, activity)
  200. |> Enum.filter(fn user -> User.ap_enabled?(user) end)
  201. |> Enum.map(fn %User{} = user ->
  202. determine_inbox(activity, user)
  203. end)
  204. |> Enum.uniq()
  205. |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
  206. |> Instances.filter_reachable()
  207. |> Enum.each(fn {inbox, unreachable_since} ->
  208. Pleroma.Web.Federator.Publisher.enqueue_one(
  209. __MODULE__,
  210. %{
  211. inbox: inbox,
  212. json: json,
  213. actor_id: actor.id,
  214. id: activity.data["id"],
  215. unreachable_since: unreachable_since
  216. }
  217. )
  218. end)
  219. end
  220. def gather_webfinger_links(%User{} = user) do
  221. [
  222. %{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id},
  223. %{
  224. "rel" => "self",
  225. "type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
  226. "href" => user.ap_id
  227. },
  228. %{
  229. "rel" => "http://ostatus.org/schema/1.0/subscribe",
  230. "template" => "#{Pleroma.Web.Endpoint.url()}/ostatus_subscribe?acct={uri}"
  231. }
  232. ]
  233. end
  234. def gather_nodeinfo_protocol_names, do: ["activitypub"]
  235. end