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