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.

1058 lines
29KB

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