logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://hacktivis.me/git/pleroma.git

transmogrifier.ex (27664B)


  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
  3. # SPDX-License-Identifier: AGPL-3.0-only
  4. defmodule Pleroma.Web.ActivityPub.Transmogrifier do
  5. @moduledoc """
  6. A module to handle coding from internal to wire ActivityPub and back.
  7. """
  8. alias Pleroma.Activity
  9. alias Pleroma.EctoType.ActivityPub.ObjectValidators
  10. alias Pleroma.Maps
  11. alias Pleroma.Object
  12. alias Pleroma.Object.Containment
  13. alias Pleroma.Repo
  14. alias Pleroma.User
  15. alias Pleroma.Web.ActivityPub.ActivityPub
  16. alias Pleroma.Web.ActivityPub.Builder
  17. alias Pleroma.Web.ActivityPub.ObjectValidator
  18. alias Pleroma.Web.ActivityPub.Pipeline
  19. alias Pleroma.Web.ActivityPub.Utils
  20. alias Pleroma.Web.ActivityPub.Visibility
  21. alias Pleroma.Web.Federator
  22. import Ecto.Query
  23. require Pleroma.Constants
  24. @doc """
  25. Modifies an incoming AP object (mastodon format) to our internal format.
  26. """
  27. def fix_object(object, options \\ []) do
  28. object
  29. |> strip_internal_fields()
  30. |> fix_actor()
  31. |> fix_url()
  32. |> fix_attachments()
  33. |> fix_context()
  34. |> fix_in_reply_to(options)
  35. |> fix_emoji()
  36. |> fix_tag()
  37. |> fix_content_map()
  38. |> fix_addressing()
  39. |> fix_summary()
  40. end
  41. def fix_summary(%{"summary" => nil} = object) do
  42. Map.put(object, "summary", "")
  43. end
  44. def fix_summary(%{"summary" => _} = object) do
  45. # summary is present, nothing to do
  46. object
  47. end
  48. def fix_summary(object), do: Map.put(object, "summary", "")
  49. def fix_addressing_list(map, field) do
  50. addrs = map[field]
  51. cond do
  52. is_list(addrs) ->
  53. Map.put(map, field, Enum.filter(addrs, &is_binary/1))
  54. is_binary(addrs) ->
  55. Map.put(map, field, [addrs])
  56. true ->
  57. Map.put(map, field, [])
  58. end
  59. end
  60. # if directMessage flag is set to true, leave the addressing alone
  61. def fix_explicit_addressing(%{"directMessage" => true} = object, _follower_collection),
  62. do: object
  63. def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, follower_collection) do
  64. explicit_mentions =
  65. Utils.determine_explicit_mentions(object) ++
  66. [Pleroma.Constants.as_public(), follower_collection]
  67. explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
  68. explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)
  69. final_cc =
  70. (cc ++ explicit_cc)
  71. |> Enum.filter(& &1)
  72. |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
  73. |> Enum.uniq()
  74. object
  75. |> Map.put("to", explicit_to)
  76. |> Map.put("cc", final_cc)
  77. end
  78. # if as:Public is addressed, then make sure the followers collection is also addressed
  79. # so that the activities will be delivered to local users.
  80. def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
  81. recipients = to ++ cc
  82. if followers_collection not in recipients do
  83. cond do
  84. Pleroma.Constants.as_public() in cc ->
  85. to = to ++ [followers_collection]
  86. Map.put(object, "to", to)
  87. Pleroma.Constants.as_public() in to ->
  88. cc = cc ++ [followers_collection]
  89. Map.put(object, "cc", cc)
  90. true ->
  91. object
  92. end
  93. else
  94. object
  95. end
  96. end
  97. def fix_addressing(object) do
  98. {:ok, %User{follower_address: follower_collection}} =
  99. object
  100. |> Containment.get_actor()
  101. |> User.get_or_fetch_by_ap_id()
  102. object
  103. |> fix_addressing_list("to")
  104. |> fix_addressing_list("cc")
  105. |> fix_addressing_list("bto")
  106. |> fix_addressing_list("bcc")
  107. |> fix_explicit_addressing(follower_collection)
  108. |> fix_implicit_addressing(follower_collection)
  109. end
  110. def fix_actor(%{"attributedTo" => actor} = object) do
  111. actor = Containment.get_actor(%{"actor" => actor})
  112. # TODO: Remove actor field for Objects
  113. object
  114. |> Map.put("actor", actor)
  115. |> Map.put("attributedTo", actor)
  116. end
  117. def fix_in_reply_to(object, options \\ [])
  118. def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
  119. when not is_nil(in_reply_to) do
  120. in_reply_to_id = prepare_in_reply_to(in_reply_to)
  121. depth = (options[:depth] || 0) + 1
  122. if Federator.allowed_thread_distance?(depth) do
  123. with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
  124. %Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
  125. object
  126. |> Map.put("inReplyTo", replied_object.data["id"])
  127. |> Map.put("context", replied_object.data["context"] || object["conversation"])
  128. |> Map.drop(["conversation", "inReplyToAtomUri"])
  129. else
  130. _ ->
  131. object
  132. end
  133. else
  134. object
  135. end
  136. end
  137. def fix_in_reply_to(object, _options), do: object
  138. def fix_quote_url_and_maybe_fetch(object, options \\ []) do
  139. quote_url =
  140. case Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes.fix_quote_url(object) do
  141. %{"quoteUrl" => quote_url} -> quote_url
  142. _ -> nil
  143. end
  144. with {:quoting?, true} <- {:quoting?, not is_nil(quote_url)},
  145. {:ok, quoted_object} <- get_obj_helper(quote_url, options),
  146. %Activity{} <- Activity.get_create_by_object_ap_id(quoted_object.data["id"]) do
  147. Map.put(object, "quoteUrl", quoted_object.data["id"])
  148. else
  149. {:quoting?, _} ->
  150. object
  151. _ ->
  152. object
  153. end
  154. end
  155. defp prepare_in_reply_to(in_reply_to) do
  156. cond do
  157. is_bitstring(in_reply_to) ->
  158. in_reply_to
  159. is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
  160. in_reply_to["id"]
  161. is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
  162. Enum.at(in_reply_to, 0)
  163. true ->
  164. ""
  165. end
  166. end
  167. def fix_context(object) do
  168. context = object["context"] || object["conversation"] || Utils.generate_context_id()
  169. object
  170. |> Map.put("context", context)
  171. |> Map.drop(["conversation"])
  172. end
  173. def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
  174. attachments =
  175. Enum.map(attachment, fn data ->
  176. url =
  177. cond do
  178. is_list(data["url"]) -> List.first(data["url"])
  179. is_map(data["url"]) -> data["url"]
  180. true -> nil
  181. end
  182. media_type =
  183. cond do
  184. is_map(url) && url =~ Pleroma.Constants.mime_regex() ->
  185. url["mediaType"]
  186. is_bitstring(data["mediaType"]) && data["mediaType"] =~ Pleroma.Constants.mime_regex() ->
  187. data["mediaType"]
  188. is_bitstring(data["mimeType"]) && data["mimeType"] =~ Pleroma.Constants.mime_regex() ->
  189. data["mimeType"]
  190. true ->
  191. nil
  192. end
  193. href =
  194. cond do
  195. is_map(url) && is_binary(url["href"]) -> url["href"]
  196. is_binary(data["url"]) -> data["url"]
  197. is_binary(data["href"]) -> data["href"]
  198. true -> nil
  199. end
  200. if href do
  201. attachment_url =
  202. %{
  203. "href" => href,
  204. "type" => Map.get(url || %{}, "type", "Link")
  205. }
  206. |> Maps.put_if_present("mediaType", media_type)
  207. |> Maps.put_if_present("width", (url || %{})["width"] || data["width"])
  208. |> Maps.put_if_present("height", (url || %{})["height"] || data["height"])
  209. %{
  210. "url" => [attachment_url],
  211. "type" => data["type"] || "Document"
  212. }
  213. |> Maps.put_if_present("mediaType", media_type)
  214. |> Maps.put_if_present("name", data["name"])
  215. |> Maps.put_if_present("blurhash", data["blurhash"])
  216. else
  217. nil
  218. end
  219. end)
  220. |> Enum.filter(& &1)
  221. Map.put(object, "attachment", attachments)
  222. end
  223. def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
  224. object
  225. |> Map.put("attachment", [attachment])
  226. |> fix_attachments()
  227. end
  228. def fix_attachments(object), do: object
  229. def fix_url(%{"url" => url} = object) when is_map(url) do
  230. Map.put(object, "url", url["href"])
  231. end
  232. def fix_url(%{"url" => url} = object) when is_list(url) do
  233. first_element = Enum.at(url, 0)
  234. url_string =
  235. cond do
  236. is_bitstring(first_element) -> first_element
  237. is_map(first_element) -> first_element["href"] || ""
  238. true -> ""
  239. end
  240. Map.put(object, "url", url_string)
  241. end
  242. def fix_url(object), do: object
  243. def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
  244. emoji =
  245. tags
  246. |> Enum.filter(fn data -> is_map(data) and data["type"] == "Emoji" and data["icon"] end)
  247. |> Enum.reduce(%{}, fn data, mapping ->
  248. name = String.trim(data["name"], ":")
  249. Map.put(mapping, name, data["icon"]["url"])
  250. end)
  251. Map.put(object, "emoji", emoji)
  252. end
  253. def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
  254. name = String.trim(tag["name"], ":")
  255. emoji = %{name => tag["icon"]["url"]}
  256. Map.put(object, "emoji", emoji)
  257. end
  258. def fix_emoji(object), do: object
  259. def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
  260. tags =
  261. tag
  262. |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
  263. |> Enum.map(fn
  264. %{"name" => "#" <> hashtag} -> String.downcase(hashtag)
  265. %{"name" => hashtag} -> String.downcase(hashtag)
  266. end)
  267. Map.put(object, "tag", tag ++ tags)
  268. end
  269. def fix_tag(%{"tag" => %{} = tag} = object) do
  270. object
  271. |> Map.put("tag", [tag])
  272. |> fix_tag
  273. end
  274. def fix_tag(object), do: object
  275. # content map usually only has one language so this will do for now.
  276. def fix_content_map(%{"contentMap" => content_map} = object) do
  277. content_groups = Map.to_list(content_map)
  278. {_, content} = Enum.at(content_groups, 0)
  279. Map.put(object, "content", content)
  280. end
  281. def fix_content_map(object), do: object
  282. defp fix_type(%{"type" => "Note", "inReplyTo" => reply_id, "name" => _} = object, options)
  283. when is_binary(reply_id) do
  284. options = Keyword.put(options, :fetch, true)
  285. with %Object{data: %{"type" => "Question"}} <- Object.normalize(reply_id, options) do
  286. Map.put(object, "type", "Answer")
  287. else
  288. _ -> object
  289. end
  290. end
  291. defp fix_type(object, _options), do: object
  292. # Reduce the object list to find the reported user.
  293. defp get_reported(objects) do
  294. Enum.reduce_while(objects, nil, fn ap_id, _ ->
  295. with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
  296. {:halt, user}
  297. else
  298. _ -> {:cont, nil}
  299. end
  300. end)
  301. end
  302. def handle_incoming(data, options \\ [])
  303. # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
  304. # with nil ID.
  305. def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
  306. with context <- data["context"] || Utils.generate_context_id(),
  307. content <- data["content"] || "",
  308. %User{} = actor <- User.get_cached_by_ap_id(actor),
  309. # Reduce the object list to find the reported user.
  310. %User{} = account <- get_reported(objects),
  311. # Remove the reported user from the object list.
  312. statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
  313. %{
  314. actor: actor,
  315. context: context,
  316. account: account,
  317. statuses: statuses,
  318. content: content,
  319. additional: %{"cc" => [account.ap_id]}
  320. }
  321. |> ActivityPub.flag()
  322. end
  323. end
  324. # disallow objects with bogus IDs
  325. def handle_incoming(%{"id" => nil}, _options), do: :error
  326. def handle_incoming(%{"id" => ""}, _options), do: :error
  327. # length of https:// = 8, should validate better, but good enough for now.
  328. def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8,
  329. do: :error
  330. def handle_incoming(
  331. %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
  332. options
  333. ) do
  334. actor = Containment.get_actor(data)
  335. data =
  336. Map.put(data, "actor", actor)
  337. |> fix_addressing
  338. with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
  339. reply_depth = (options[:depth] || 0) + 1
  340. options = Keyword.put(options, :depth, reply_depth)
  341. object = fix_object(object, options)
  342. params = %{
  343. to: data["to"],
  344. object: object,
  345. actor: user,
  346. context: nil,
  347. local: false,
  348. published: data["published"],
  349. additional: Map.take(data, ["cc", "id"])
  350. }
  351. ActivityPub.listen(params)
  352. else
  353. _e -> :error
  354. end
  355. end
  356. @misskey_reactions %{
  357. "like" => "👍",
  358. "love" => "❤️",
  359. "laugh" => "😆",
  360. "hmm" => "🤔",
  361. "surprise" => "😮",
  362. "congrats" => "🎉",
  363. "angry" => "💢",
  364. "confused" => "😥",
  365. "rip" => "😇",
  366. "pudding" => "🍮",
  367. "star" => "⭐"
  368. }
  369. @doc "Rewrite misskey likes into EmojiReacts"
  370. def handle_incoming(
  371. %{
  372. "type" => "Like",
  373. "_misskey_reaction" => reaction
  374. } = data,
  375. options
  376. ) do
  377. data
  378. |> Map.put("type", "EmojiReact")
  379. |> Map.put("content", @misskey_reactions[reaction] || reaction)
  380. |> handle_incoming(options)
  381. end
  382. def handle_incoming(
  383. %{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,
  384. options
  385. )
  386. when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note Page Image} do
  387. fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
  388. object =
  389. data["object"]
  390. |> strip_internal_fields()
  391. |> fix_type(fetch_options)
  392. |> fix_in_reply_to(fetch_options)
  393. |> fix_quote_url_and_maybe_fetch(fetch_options)
  394. data = Map.put(data, "object", object)
  395. options = Keyword.put(options, :local, false)
  396. with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
  397. nil <- Activity.get_create_by_object_ap_id(obj_id),
  398. {:ok, activity, _} <- Pipeline.common_pipeline(data, options) do
  399. {:ok, activity}
  400. else
  401. %Activity{} = activity -> {:ok, activity}
  402. e -> e
  403. end
  404. end
  405. def handle_incoming(%{"type" => type} = data, _options)
  406. when type in ~w{Like EmojiReact Announce Add Remove} do
  407. with :ok <- ObjectValidator.fetch_actor_and_object(data),
  408. {:ok, activity, _meta} <-
  409. Pipeline.common_pipeline(data, local: false) do
  410. {:ok, activity}
  411. else
  412. e -> {:error, e}
  413. end
  414. end
  415. def handle_incoming(
  416. %{"type" => type} = data,
  417. _options
  418. )
  419. when type in ~w{Update Block Follow Accept Reject} do
  420. with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
  421. {:ok, activity, _} <-
  422. Pipeline.common_pipeline(data, local: false) do
  423. {:ok, activity}
  424. end
  425. end
  426. def handle_incoming(
  427. %{"type" => "Delete"} = data,
  428. _options
  429. ) do
  430. with {:ok, activity, _} <-
  431. Pipeline.common_pipeline(data, local: false) do
  432. {:ok, activity}
  433. else
  434. {:error, {:validate, _}} = e ->
  435. # Check if we have a create activity for this
  436. with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]),
  437. %Activity{data: %{"actor" => actor}} <-
  438. Activity.create_by_object_ap_id(object_id) |> Repo.one(),
  439. # We have one, insert a tombstone and retry
  440. {:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id),
  441. {:ok, _tombstone} <- Object.create(tombstone_data) do
  442. handle_incoming(data)
  443. else
  444. _ -> e
  445. end
  446. end
  447. end
  448. def handle_incoming(
  449. %{
  450. "type" => "Undo",
  451. "object" => %{"type" => "Follow", "object" => followed},
  452. "actor" => follower,
  453. "id" => id
  454. } = _data,
  455. _options
  456. ) do
  457. with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
  458. {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
  459. {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
  460. User.unfollow(follower, followed)
  461. {:ok, activity}
  462. else
  463. _e -> :error
  464. end
  465. end
  466. def handle_incoming(
  467. %{
  468. "type" => "Undo",
  469. "object" => %{"type" => type}
  470. } = data,
  471. _options
  472. )
  473. when type in ["Like", "EmojiReact", "Announce", "Block"] do
  474. with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
  475. {:ok, activity}
  476. end
  477. end
  478. # For Undos that don't have the complete object attached, try to find it in our database.
  479. def handle_incoming(
  480. %{
  481. "type" => "Undo",
  482. "object" => object
  483. } = activity,
  484. options
  485. )
  486. when is_binary(object) do
  487. with %Activity{data: data} <- Activity.get_by_ap_id(object) do
  488. activity
  489. |> Map.put("object", data)
  490. |> handle_incoming(options)
  491. else
  492. _e -> :error
  493. end
  494. end
  495. def handle_incoming(
  496. %{
  497. "type" => "Move",
  498. "actor" => origin_actor,
  499. "object" => origin_actor,
  500. "target" => target_actor
  501. },
  502. _options
  503. ) do
  504. with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
  505. {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
  506. true <- origin_actor in target_user.also_known_as do
  507. ActivityPub.move(origin_user, target_user, false)
  508. else
  509. _e -> :error
  510. end
  511. end
  512. def handle_incoming(_, _), do: :error
  513. @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
  514. def get_obj_helper(id, options \\ []) do
  515. options = Keyword.put(options, :fetch, true)
  516. case Object.normalize(id, options) do
  517. %Object{} = object -> {:ok, object}
  518. _ -> nil
  519. end
  520. end
  521. @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
  522. def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
  523. ap_id: ap_id
  524. })
  525. when attributed_to == ap_id do
  526. with {:ok, activity} <-
  527. handle_incoming(%{
  528. "type" => "Create",
  529. "to" => data["to"],
  530. "cc" => data["cc"],
  531. "actor" => attributed_to,
  532. "object" => data
  533. }) do
  534. {:ok, Object.normalize(activity, fetch: false)}
  535. else
  536. _ -> get_obj_helper(object_id)
  537. end
  538. end
  539. def get_embedded_obj_helper(object_id, _) do
  540. get_obj_helper(object_id)
  541. end
  542. def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
  543. with false <- String.starts_with?(in_reply_to, "http"),
  544. {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
  545. Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
  546. else
  547. _e -> object
  548. end
  549. end
  550. def set_reply_to_uri(obj), do: obj
  551. @doc """
  552. Fedibird compatibility
  553. https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac
  554. """
  555. def set_quote_url(%{"quoteUrl" => quote_url} = object) when is_binary(quote_url) do
  556. Map.put(object, "quoteUri", quote_url)
  557. end
  558. def set_quote_url(obj), do: obj
  559. @doc """
  560. Serialized Mastodon-compatible `replies` collection containing _self-replies_.
  561. Based on Mastodon's ActivityPub::NoteSerializer#replies.
  562. """
  563. def set_replies(obj_data) do
  564. replies_uris =
  565. with limit when limit > 0 <-
  566. Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
  567. %Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
  568. object
  569. |> Object.self_replies()
  570. |> select([o], fragment("?->>'id'", o.data))
  571. |> limit(^limit)
  572. |> Repo.all()
  573. else
  574. _ -> []
  575. end
  576. set_replies(obj_data, replies_uris)
  577. end
  578. defp set_replies(obj, []) do
  579. obj
  580. end
  581. defp set_replies(obj, replies_uris) do
  582. replies_collection = %{
  583. "type" => "Collection",
  584. "items" => replies_uris
  585. }
  586. Map.merge(obj, %{"replies" => replies_collection})
  587. end
  588. def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
  589. items
  590. end
  591. def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
  592. items
  593. end
  594. def replies(_), do: []
  595. # Prepares the object of an outgoing create activity.
  596. def prepare_object(object) do
  597. object
  598. |> add_hashtags
  599. |> add_mention_tags
  600. |> add_emoji_tags
  601. |> add_attributed_to
  602. |> prepare_attachments
  603. |> set_conversation
  604. |> set_reply_to_uri
  605. |> set_quote_url
  606. |> set_replies
  607. |> strip_internal_fields
  608. |> strip_internal_tags
  609. |> set_type
  610. |> maybe_process_history
  611. end
  612. defp maybe_process_history(%{"formerRepresentations" => %{"orderedItems" => history}} = object) do
  613. processed_history =
  614. Enum.map(
  615. history,
  616. fn
  617. item when is_map(item) -> prepare_object(item)
  618. item -> item
  619. end
  620. )
  621. put_in(object, ["formerRepresentations", "orderedItems"], processed_history)
  622. end
  623. defp maybe_process_history(object) do
  624. object
  625. end
  626. # @doc
  627. # """
  628. # internal -> Mastodon
  629. # """
  630. def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
  631. when activity_type in ["Create", "Listen"] do
  632. object =
  633. object_id
  634. |> Object.normalize(fetch: false)
  635. |> Map.get(:data)
  636. |> prepare_object
  637. data =
  638. data
  639. |> Map.put("object", object)
  640. |> Map.merge(Utils.make_json_ld_header())
  641. |> Map.delete("bcc")
  642. {:ok, data}
  643. end
  644. def prepare_outgoing(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data)
  645. when objtype in Pleroma.Constants.updatable_object_types() do
  646. object =
  647. object
  648. |> prepare_object
  649. data =
  650. data
  651. |> Map.put("object", object)
  652. |> Map.merge(Utils.make_json_ld_header())
  653. |> Map.delete("bcc")
  654. {:ok, data}
  655. end
  656. def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
  657. object =
  658. object_id
  659. |> Object.normalize(fetch: false)
  660. data =
  661. if Visibility.private?(object) && object.data["actor"] == ap_id do
  662. data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
  663. else
  664. data |> maybe_fix_object_url
  665. end
  666. data =
  667. data
  668. |> strip_internal_fields
  669. |> Map.merge(Utils.make_json_ld_header())
  670. |> Map.delete("bcc")
  671. {:ok, data}
  672. end
  673. # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
  674. # because of course it does.
  675. def prepare_outgoing(%{"type" => "Accept"} = data) do
  676. with follow_activity <- Activity.normalize(data["object"]) do
  677. object = %{
  678. "actor" => follow_activity.actor,
  679. "object" => follow_activity.data["object"],
  680. "id" => follow_activity.data["id"],
  681. "type" => "Follow"
  682. }
  683. data =
  684. data
  685. |> Map.put("object", object)
  686. |> Map.merge(Utils.make_json_ld_header())
  687. {:ok, data}
  688. end
  689. end
  690. def prepare_outgoing(%{"type" => "Reject"} = data) do
  691. with follow_activity <- Activity.normalize(data["object"]) do
  692. object = %{
  693. "actor" => follow_activity.actor,
  694. "object" => follow_activity.data["object"],
  695. "id" => follow_activity.data["id"],
  696. "type" => "Follow"
  697. }
  698. data =
  699. data
  700. |> Map.put("object", object)
  701. |> Map.merge(Utils.make_json_ld_header())
  702. {:ok, data}
  703. end
  704. end
  705. def prepare_outgoing(%{"type" => _type} = data) do
  706. data =
  707. data
  708. |> strip_internal_fields
  709. |> maybe_fix_object_url
  710. |> Map.merge(Utils.make_json_ld_header())
  711. {:ok, data}
  712. end
  713. def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
  714. with false <- String.starts_with?(object, "http"),
  715. {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
  716. %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
  717. relative_object do
  718. Map.put(data, "object", external_url)
  719. else
  720. {:fetch, _} ->
  721. data
  722. _ ->
  723. data
  724. end
  725. end
  726. def maybe_fix_object_url(data), do: data
  727. def add_hashtags(object) do
  728. tags =
  729. (object["tag"] || [])
  730. |> Enum.map(fn
  731. # Expand internal representation tags into AS2 tags.
  732. tag when is_binary(tag) ->
  733. %{
  734. "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
  735. "name" => "##{tag}",
  736. "type" => "Hashtag"
  737. }
  738. # Do not process tags which are already AS2 tag objects.
  739. tag when is_map(tag) ->
  740. tag
  741. end)
  742. Map.put(object, "tag", tags)
  743. end
  744. # TODO These should be added on our side on insertion, it doesn't make much
  745. # sense to regenerate these all the time
  746. def add_mention_tags(object) do
  747. to = object["to"] || []
  748. cc = object["cc"] || []
  749. mentioned = User.get_users_from_set(to ++ cc, local_only: false)
  750. mentions = Enum.map(mentioned, &build_mention_tag/1)
  751. tags = object["tag"] || []
  752. Map.put(object, "tag", tags ++ mentions)
  753. end
  754. defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
  755. %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
  756. end
  757. def take_emoji_tags(%User{emoji: emoji}) do
  758. emoji
  759. |> Map.to_list()
  760. |> Enum.map(&build_emoji_tag/1)
  761. end
  762. # TODO: we should probably send mtime instead of unix epoch time for updated
  763. def add_emoji_tags(%{"emoji" => emoji} = object) do
  764. tags = object["tag"] || []
  765. out = Enum.map(emoji, &build_emoji_tag/1)
  766. Map.put(object, "tag", tags ++ out)
  767. end
  768. def add_emoji_tags(object), do: object
  769. defp build_emoji_tag({name, url}) do
  770. %{
  771. "icon" => %{"url" => "#{URI.encode(url)}", "type" => "Image"},
  772. "name" => ":" <> name <> ":",
  773. "type" => "Emoji",
  774. "updated" => "1970-01-01T00:00:00Z",
  775. "id" => url
  776. }
  777. end
  778. def set_conversation(object) do
  779. Map.put(object, "conversation", object["context"])
  780. end
  781. def set_type(%{"type" => "Answer"} = object) do
  782. Map.put(object, "type", "Note")
  783. end
  784. def set_type(object), do: object
  785. def add_attributed_to(object) do
  786. attributed_to = object["attributedTo"] || object["actor"]
  787. Map.put(object, "attributedTo", attributed_to)
  788. end
  789. # TODO: Revisit this
  790. def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object
  791. def prepare_attachments(object) do
  792. attachments =
  793. object
  794. |> Map.get("attachment", [])
  795. |> Enum.map(fn data ->
  796. [%{"mediaType" => media_type, "href" => href} = url | _] = data["url"]
  797. %{
  798. "url" => href,
  799. "mediaType" => media_type,
  800. "name" => data["name"],
  801. "type" => "Document"
  802. }
  803. |> Maps.put_if_present("width", url["width"])
  804. |> Maps.put_if_present("height", url["height"])
  805. |> Maps.put_if_present("blurhash", data["blurhash"])
  806. end)
  807. Map.put(object, "attachment", attachments)
  808. end
  809. def strip_internal_fields(object) do
  810. Map.drop(object, Pleroma.Constants.object_internal_fields())
  811. end
  812. defp strip_internal_tags(%{"tag" => tags} = object) do
  813. tags = Enum.filter(tags, fn x -> is_map(x) end)
  814. Map.put(object, "tag", tags)
  815. end
  816. defp strip_internal_tags(object), do: object
  817. def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
  818. Map.put(data, "url", url["href"])
  819. end
  820. def maybe_fix_user_url(data), do: data
  821. def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
  822. end