Fork of Pleroma with site-specific changes and feature branches https://git.pleroma.social/pleroma/pleroma
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

1035 Zeilen
28KB

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