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