logo

pleroma

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

transmogrifier.ex (29437B)


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