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