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