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