Fix/902 random compile failing
[akkoma] / lib / pleroma / web / activity_pub / transmogrifier.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 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.Object
11 alias Pleroma.Object.Containment
12 alias Pleroma.Repo
13 alias Pleroma.User
14 alias Pleroma.Web.ActivityPub.ActivityPub
15 alias Pleroma.Web.ActivityPub.Utils
16 alias Pleroma.Web.ActivityPub.Visibility
17
18 import Ecto.Query
19
20 require Logger
21
22 @doc """
23 Modifies an incoming AP object (mastodon format) to our internal format.
24 """
25 def fix_object(object) do
26 object
27 |> fix_actor
28 |> fix_url
29 |> fix_attachments
30 |> fix_context
31 |> fix_in_reply_to
32 |> fix_emoji
33 |> fix_tag
34 |> fix_content_map
35 |> fix_likes
36 |> fix_addressing
37 |> fix_summary
38 end
39
40 def fix_summary(%{"summary" => nil} = object) do
41 object
42 |> Map.put("summary", "")
43 end
44
45 def fix_summary(%{"summary" => _} = object) do
46 # summary is present, nothing to do
47 object
48 end
49
50 def fix_summary(object) do
51 object
52 |> Map.put("summary", "")
53 end
54
55 def fix_addressing_list(map, field) do
56 cond do
57 is_binary(map[field]) ->
58 Map.put(map, field, [map[field]])
59
60 is_nil(map[field]) ->
61 Map.put(map, field, [])
62
63 true ->
64 map
65 end
66 end
67
68 def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mentions) do
69 explicit_to =
70 to
71 |> Enum.filter(fn x -> x in explicit_mentions end)
72
73 explicit_cc =
74 to
75 |> Enum.filter(fn x -> x not in explicit_mentions end)
76
77 final_cc =
78 (cc ++ explicit_cc)
79 |> Enum.uniq()
80
81 object
82 |> Map.put("to", explicit_to)
83 |> Map.put("cc", final_cc)
84 end
85
86 def fix_explicit_addressing(object, _explicit_mentions), do: object
87
88 # if directMessage flag is set to true, leave the addressing alone
89 def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
90
91 def fix_explicit_addressing(object) do
92 explicit_mentions =
93 object
94 |> Utils.determine_explicit_mentions()
95
96 explicit_mentions = explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public"]
97
98 object
99 |> fix_explicit_addressing(explicit_mentions)
100 end
101
102 # if as:Public is addressed, then make sure the followers collection is also addressed
103 # so that the activities will be delivered to local users.
104 def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
105 recipients = to ++ cc
106
107 if followers_collection not in recipients do
108 cond do
109 "https://www.w3.org/ns/activitystreams#Public" in cc ->
110 to = to ++ [followers_collection]
111 Map.put(object, "to", to)
112
113 "https://www.w3.org/ns/activitystreams#Public" in to ->
114 cc = cc ++ [followers_collection]
115 Map.put(object, "cc", cc)
116
117 true ->
118 object
119 end
120 else
121 object
122 end
123 end
124
125 def fix_implicit_addressing(object, _), do: object
126
127 def fix_addressing(object) do
128 {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
129 followers_collection = User.ap_followers(user)
130
131 object
132 |> fix_addressing_list("to")
133 |> fix_addressing_list("cc")
134 |> fix_addressing_list("bto")
135 |> fix_addressing_list("bcc")
136 |> fix_explicit_addressing
137 |> fix_implicit_addressing(followers_collection)
138 end
139
140 def fix_actor(%{"attributedTo" => actor} = object) do
141 object
142 |> Map.put("actor", Containment.get_actor(%{"actor" => actor}))
143 end
144
145 # Check for standardisation
146 # This is what Peertube does
147 # curl -H 'Accept: application/activity+json' $likes | jq .totalItems
148 # Prismo returns only an integer (count) as "likes"
149 def fix_likes(%{"likes" => likes} = object) when not is_map(likes) do
150 object
151 |> Map.put("likes", [])
152 |> Map.put("like_count", 0)
153 end
154
155 def fix_likes(object) do
156 object
157 end
158
159 def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object)
160 when not is_nil(in_reply_to) do
161 in_reply_to_id =
162 cond do
163 is_bitstring(in_reply_to) ->
164 in_reply_to
165
166 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
167 in_reply_to["id"]
168
169 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
170 Enum.at(in_reply_to, 0)
171
172 # Maybe I should output an error too?
173 true ->
174 ""
175 end
176
177 case get_obj_helper(in_reply_to_id) do
178 {:ok, replied_object} ->
179 with %Activity{} = _activity <-
180 Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
181 object
182 |> Map.put("inReplyTo", replied_object.data["id"])
183 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
184 |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
185 |> Map.put("context", replied_object.data["context"] || object["conversation"])
186 else
187 e ->
188 Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
189 object
190 end
191
192 e ->
193 Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
194 object
195 end
196 end
197
198 def fix_in_reply_to(object), do: object
199
200 def fix_context(object) do
201 context = object["context"] || object["conversation"] || Utils.generate_context_id()
202
203 object
204 |> Map.put("context", context)
205 |> Map.put("conversation", context)
206 end
207
208 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
209 attachments =
210 attachment
211 |> Enum.map(fn data ->
212 media_type = data["mediaType"] || data["mimeType"]
213 href = data["url"] || data["href"]
214
215 url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]
216
217 data
218 |> Map.put("mediaType", media_type)
219 |> Map.put("url", url)
220 end)
221
222 object
223 |> Map.put("attachment", attachments)
224 end
225
226 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
227 Map.put(object, "attachment", [attachment])
228 |> fix_attachments()
229 end
230
231 def fix_attachments(object), do: object
232
233 def fix_url(%{"url" => url} = object) when is_map(url) do
234 object
235 |> Map.put("url", url["href"])
236 end
237
238 def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
239 first_element = Enum.at(url, 0)
240
241 link_element =
242 url
243 |> Enum.filter(fn x -> is_map(x) end)
244 |> Enum.filter(fn x -> x["mimeType"] == "text/html" end)
245 |> Enum.at(0)
246
247 object
248 |> Map.put("attachment", [first_element])
249 |> Map.put("url", link_element["href"])
250 end
251
252 def fix_url(%{"type" => object_type, "url" => url} = object)
253 when object_type != "Video" and is_list(url) do
254 first_element = Enum.at(url, 0)
255
256 url_string =
257 cond do
258 is_bitstring(first_element) -> first_element
259 is_map(first_element) -> first_element["href"] || ""
260 true -> ""
261 end
262
263 object
264 |> Map.put("url", url_string)
265 end
266
267 def fix_url(object), do: object
268
269 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
270 emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
271
272 emoji =
273 emoji
274 |> Enum.reduce(%{}, fn data, mapping ->
275 name = String.trim(data["name"], ":")
276
277 mapping |> Map.put(name, data["icon"]["url"])
278 end)
279
280 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
281 emoji = Map.merge(object["emoji"] || %{}, emoji)
282
283 object
284 |> Map.put("emoji", emoji)
285 end
286
287 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
288 name = String.trim(tag["name"], ":")
289 emoji = %{name => tag["icon"]["url"]}
290
291 object
292 |> Map.put("emoji", emoji)
293 end
294
295 def fix_emoji(object), do: object
296
297 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
298 tags =
299 tag
300 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
301 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
302
303 combined = tag ++ tags
304
305 object
306 |> Map.put("tag", combined)
307 end
308
309 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
310 combined = [tag, String.slice(hashtag, 1..-1)]
311
312 object
313 |> Map.put("tag", combined)
314 end
315
316 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
317
318 def fix_tag(object), do: object
319
320 # content map usually only has one language so this will do for now.
321 def fix_content_map(%{"contentMap" => content_map} = object) do
322 content_groups = Map.to_list(content_map)
323 {_, content} = Enum.at(content_groups, 0)
324
325 object
326 |> Map.put("content", content)
327 end
328
329 def fix_content_map(object), do: object
330
331 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
332 with true <- id =~ "follows",
333 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
334 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
335 {:ok, activity}
336 else
337 _ -> {:error, nil}
338 end
339 end
340
341 defp mastodon_follow_hack(_, _), do: {:error, nil}
342
343 defp get_follow_activity(follow_object, followed) do
344 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
345 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
346 {:ok, activity}
347 else
348 # Can't find the activity. This might a Mastodon 2.3 "Accept"
349 {:activity, nil} ->
350 mastodon_follow_hack(follow_object, followed)
351
352 _ ->
353 {:error, nil}
354 end
355 end
356
357 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
358 # with nil ID.
359 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data) do
360 with context <- data["context"] || Utils.generate_context_id(),
361 content <- data["content"] || "",
362 %User{} = actor <- User.get_cached_by_ap_id(actor),
363
364 # Reduce the object list to find the reported user.
365 %User{} = account <-
366 Enum.reduce_while(objects, nil, fn ap_id, _ ->
367 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
368 {:halt, user}
369 else
370 _ -> {:cont, nil}
371 end
372 end),
373
374 # Remove the reported user from the object list.
375 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
376 params = %{
377 actor: actor,
378 context: context,
379 account: account,
380 statuses: statuses,
381 content: content,
382 additional: %{
383 "cc" => [account.ap_id]
384 }
385 }
386
387 ActivityPub.flag(params)
388 end
389 end
390
391 # disallow objects with bogus IDs
392 def handle_incoming(%{"id" => nil}), do: :error
393 def handle_incoming(%{"id" => ""}), do: :error
394 # length of https:// = 8, should validate better, but good enough for now.
395 def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8), do: :error
396
397 # TODO: validate those with a Ecto scheme
398 # - tags
399 # - emoji
400 def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
401 when objtype in ["Article", "Note", "Video", "Page"] do
402 actor = Containment.get_actor(data)
403
404 data =
405 Map.put(data, "actor", actor)
406 |> fix_addressing
407
408 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
409 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
410 object = fix_object(data["object"])
411
412 params = %{
413 to: data["to"],
414 object: object,
415 actor: user,
416 context: object["conversation"],
417 local: false,
418 published: data["published"],
419 additional:
420 Map.take(data, [
421 "cc",
422 "directMessage",
423 "id"
424 ])
425 }
426
427 ActivityPub.create(params)
428 else
429 %Activity{} = activity -> {:ok, activity}
430 _e -> :error
431 end
432 end
433
434 def handle_incoming(
435 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
436 ) do
437 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
438 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
439 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
440 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
441 {:user_blocked, false} <-
442 {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
443 {:user_locked, false} <- {:user_locked, User.locked?(followed)},
444 {:follow, {:ok, follower}} <- {:follow, User.follow(follower, followed)} do
445 ActivityPub.accept(%{
446 to: [follower.ap_id],
447 actor: followed,
448 object: data,
449 local: true
450 })
451 else
452 {:user_blocked, true} ->
453 {:ok, _} = Utils.update_follow_state(activity, "reject")
454
455 ActivityPub.reject(%{
456 to: [follower.ap_id],
457 actor: followed,
458 object: data,
459 local: true
460 })
461
462 {:follow, {:error, _}} ->
463 {:ok, _} = Utils.update_follow_state(activity, "reject")
464
465 ActivityPub.reject(%{
466 to: [follower.ap_id],
467 actor: followed,
468 object: data,
469 local: true
470 })
471
472 {:user_locked, true} ->
473 :noop
474 end
475
476 {:ok, activity}
477 else
478 _e ->
479 :error
480 end
481 end
482
483 def handle_incoming(
484 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data
485 ) do
486 with actor <- Containment.get_actor(data),
487 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
488 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
489 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
490 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
491 {:ok, activity} <-
492 ActivityPub.accept(%{
493 to: follow_activity.data["to"],
494 type: "Accept",
495 actor: followed,
496 object: follow_activity.data["id"],
497 local: false
498 }) do
499 if not User.following?(follower, followed) do
500 {:ok, _follower} = User.follow(follower, followed)
501 end
502
503 {:ok, activity}
504 else
505 _e -> :error
506 end
507 end
508
509 def handle_incoming(
510 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
511 ) do
512 with actor <- Containment.get_actor(data),
513 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
514 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
515 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
516 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
517 {:ok, activity} <-
518 ActivityPub.reject(%{
519 to: follow_activity.data["to"],
520 type: "Reject",
521 actor: followed,
522 object: follow_activity.data["id"],
523 local: false
524 }) do
525 User.unfollow(follower, followed)
526
527 {:ok, activity}
528 else
529 _e -> :error
530 end
531 end
532
533 def handle_incoming(
534 %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data
535 ) do
536 with actor <- Containment.get_actor(data),
537 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
538 {:ok, object} <- get_obj_helper(object_id),
539 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
540 {:ok, activity}
541 else
542 _e -> :error
543 end
544 end
545
546 def handle_incoming(
547 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
548 ) do
549 with actor <- Containment.get_actor(data),
550 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
551 {:ok, object} <- get_obj_helper(object_id),
552 public <- Visibility.is_public?(data),
553 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
554 {:ok, activity}
555 else
556 _e -> :error
557 end
558 end
559
560 def handle_incoming(
561 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
562 data
563 )
564 when object_type in ["Person", "Application", "Service", "Organization"] do
565 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
566 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
567
568 banner = new_user_data[:info]["banner"]
569 locked = new_user_data[:info]["locked"] || false
570
571 update_data =
572 new_user_data
573 |> Map.take([:name, :bio, :avatar])
574 |> Map.put(:info, %{"banner" => banner, "locked" => locked})
575
576 actor
577 |> User.upgrade_changeset(update_data)
578 |> User.update_and_set_cache()
579
580 ActivityPub.update(%{
581 local: false,
582 to: data["to"] || [],
583 cc: data["cc"] || [],
584 object: object,
585 actor: actor_id
586 })
587 else
588 e ->
589 Logger.error(e)
590 :error
591 end
592 end
593
594 # TODO: We presently assume that any actor on the same origin domain as the object being
595 # deleted has the rights to delete that object. A better way to validate whether or not
596 # the object should be deleted is to refetch the object URI, which should return either
597 # an error or a tombstone. This would allow us to verify that a deletion actually took
598 # place.
599 def handle_incoming(
600 %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data
601 ) do
602 object_id = Utils.get_ap_id(object_id)
603
604 with actor <- Containment.get_actor(data),
605 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
606 {:ok, object} <- get_obj_helper(object_id),
607 :ok <- Containment.contain_origin(actor.ap_id, object.data),
608 {:ok, activity} <- ActivityPub.delete(object, false) do
609 {:ok, activity}
610 else
611 _e -> :error
612 end
613 end
614
615 def handle_incoming(
616 %{
617 "type" => "Undo",
618 "object" => %{"type" => "Announce", "object" => object_id},
619 "actor" => _actor,
620 "id" => id
621 } = data
622 ) do
623 with actor <- Containment.get_actor(data),
624 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
625 {:ok, object} <- get_obj_helper(object_id),
626 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
627 {:ok, activity}
628 else
629 _e -> :error
630 end
631 end
632
633 def handle_incoming(
634 %{
635 "type" => "Undo",
636 "object" => %{"type" => "Follow", "object" => followed},
637 "actor" => follower,
638 "id" => id
639 } = _data
640 ) do
641 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
642 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
643 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
644 User.unfollow(follower, followed)
645 {:ok, activity}
646 else
647 _e -> :error
648 end
649 end
650
651 def handle_incoming(
652 %{
653 "type" => "Undo",
654 "object" => %{"type" => "Block", "object" => blocked},
655 "actor" => blocker,
656 "id" => id
657 } = _data
658 ) do
659 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
660 %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
661 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
662 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
663 User.unblock(blocker, blocked)
664 {:ok, activity}
665 else
666 _e -> :error
667 end
668 end
669
670 def handle_incoming(
671 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data
672 ) do
673 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
674 %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
675 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
676 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
677 User.unfollow(blocker, blocked)
678 User.block(blocker, blocked)
679 {:ok, activity}
680 else
681 _e -> :error
682 end
683 end
684
685 def handle_incoming(
686 %{
687 "type" => "Undo",
688 "object" => %{"type" => "Like", "object" => object_id},
689 "actor" => _actor,
690 "id" => id
691 } = data
692 ) do
693 with actor <- Containment.get_actor(data),
694 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
695 {:ok, object} <- get_obj_helper(object_id),
696 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
697 {:ok, activity}
698 else
699 _e -> :error
700 end
701 end
702
703 def handle_incoming(_), do: :error
704
705 def get_obj_helper(id) do
706 if object = Object.normalize(id), do: {:ok, object}, else: nil
707 end
708
709 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
710 with false <- String.starts_with?(in_reply_to, "http"),
711 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
712 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
713 else
714 _e -> object
715 end
716 end
717
718 def set_reply_to_uri(obj), do: obj
719
720 # Prepares the object of an outgoing create activity.
721 def prepare_object(object) do
722 object
723 |> set_sensitive
724 |> add_hashtags
725 |> add_mention_tags
726 |> add_emoji_tags
727 |> add_attributed_to
728 |> add_likes
729 |> prepare_attachments
730 |> set_conversation
731 |> set_reply_to_uri
732 |> strip_internal_fields
733 |> strip_internal_tags
734 end
735
736 # @doc
737 # """
738 # internal -> Mastodon
739 # """
740
741 def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
742 object =
743 Object.normalize(object_id).data
744 |> prepare_object
745
746 data =
747 data
748 |> Map.put("object", object)
749 |> Map.merge(Utils.make_json_ld_header())
750
751 {:ok, data}
752 end
753
754 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
755 # because of course it does.
756 def prepare_outgoing(%{"type" => "Accept"} = data) do
757 with follow_activity <- Activity.normalize(data["object"]) do
758 object = %{
759 "actor" => follow_activity.actor,
760 "object" => follow_activity.data["object"],
761 "id" => follow_activity.data["id"],
762 "type" => "Follow"
763 }
764
765 data =
766 data
767 |> Map.put("object", object)
768 |> Map.merge(Utils.make_json_ld_header())
769
770 {:ok, data}
771 end
772 end
773
774 def prepare_outgoing(%{"type" => "Reject"} = data) do
775 with follow_activity <- Activity.normalize(data["object"]) do
776 object = %{
777 "actor" => follow_activity.actor,
778 "object" => follow_activity.data["object"],
779 "id" => follow_activity.data["id"],
780 "type" => "Follow"
781 }
782
783 data =
784 data
785 |> Map.put("object", object)
786 |> Map.merge(Utils.make_json_ld_header())
787
788 {:ok, data}
789 end
790 end
791
792 def prepare_outgoing(%{"type" => _type} = data) do
793 data =
794 data
795 |> strip_internal_fields
796 |> maybe_fix_object_url
797 |> Map.merge(Utils.make_json_ld_header())
798
799 {:ok, data}
800 end
801
802 def maybe_fix_object_url(data) do
803 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
804 case get_obj_helper(data["object"]) do
805 {:ok, relative_object} ->
806 if relative_object.data["external_url"] do
807 _data =
808 data
809 |> Map.put("object", relative_object.data["external_url"])
810 else
811 data
812 end
813
814 e ->
815 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
816 data
817 end
818 else
819 data
820 end
821 end
822
823 def add_hashtags(object) do
824 tags =
825 (object["tag"] || [])
826 |> Enum.map(fn
827 # Expand internal representation tags into AS2 tags.
828 tag when is_binary(tag) ->
829 %{
830 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
831 "name" => "##{tag}",
832 "type" => "Hashtag"
833 }
834
835 # Do not process tags which are already AS2 tag objects.
836 tag when is_map(tag) ->
837 tag
838 end)
839
840 object
841 |> Map.put("tag", tags)
842 end
843
844 def add_mention_tags(object) do
845 mentions =
846 object
847 |> Utils.get_notified_from_object()
848 |> Enum.map(fn user ->
849 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
850 end)
851
852 tags = object["tag"] || []
853
854 object
855 |> Map.put("tag", tags ++ mentions)
856 end
857
858 def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
859 user_info = add_emoji_tags(user_info)
860
861 object
862 |> Map.put(:info, user_info)
863 end
864
865 # TODO: we should probably send mtime instead of unix epoch time for updated
866 def add_emoji_tags(%{"emoji" => emoji} = object) do
867 tags = object["tag"] || []
868
869 out =
870 emoji
871 |> Enum.map(fn {name, url} ->
872 %{
873 "icon" => %{"url" => url, "type" => "Image"},
874 "name" => ":" <> name <> ":",
875 "type" => "Emoji",
876 "updated" => "1970-01-01T00:00:00Z",
877 "id" => url
878 }
879 end)
880
881 object
882 |> Map.put("tag", tags ++ out)
883 end
884
885 def add_emoji_tags(object) do
886 object
887 end
888
889 def set_conversation(object) do
890 Map.put(object, "conversation", object["context"])
891 end
892
893 def set_sensitive(object) do
894 tags = object["tag"] || []
895 Map.put(object, "sensitive", "nsfw" in tags)
896 end
897
898 def add_attributed_to(object) do
899 attributed_to = object["attributedTo"] || object["actor"]
900
901 object
902 |> Map.put("attributedTo", attributed_to)
903 end
904
905 def add_likes(%{"id" => id, "like_count" => likes} = object) do
906 likes = %{
907 "id" => "#{id}/likes",
908 "first" => "#{id}/likes?page=1",
909 "type" => "OrderedCollection",
910 "totalItems" => likes
911 }
912
913 object
914 |> Map.put("likes", likes)
915 end
916
917 def add_likes(object) do
918 object
919 end
920
921 def prepare_attachments(object) do
922 attachments =
923 (object["attachment"] || [])
924 |> Enum.map(fn data ->
925 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
926 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
927 end)
928
929 object
930 |> Map.put("attachment", attachments)
931 end
932
933 defp strip_internal_fields(object) do
934 object
935 |> Map.drop([
936 "like_count",
937 "announcements",
938 "announcement_count",
939 "emoji",
940 "context_id",
941 "deleted_activity_id"
942 ])
943 end
944
945 defp strip_internal_tags(%{"tag" => tags} = object) do
946 tags =
947 tags
948 |> Enum.filter(fn x -> is_map(x) end)
949
950 object
951 |> Map.put("tag", tags)
952 end
953
954 defp strip_internal_tags(object), do: object
955
956 def perform(:user_upgrade, user) do
957 # we pass a fake user so that the followers collection is stripped away
958 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
959
960 q =
961 from(
962 u in User,
963 where: ^old_follower_address in u.following,
964 update: [
965 set: [
966 following:
967 fragment(
968 "array_replace(?,?,?)",
969 u.following,
970 ^old_follower_address,
971 ^user.follower_address
972 )
973 ]
974 ]
975 )
976
977 Repo.update_all(q, [])
978
979 maybe_retire_websub(user.ap_id)
980
981 q =
982 from(
983 a in Activity,
984 where: ^old_follower_address in a.recipients,
985 update: [
986 set: [
987 recipients:
988 fragment(
989 "array_replace(?,?,?)",
990 a.recipients,
991 ^old_follower_address,
992 ^user.follower_address
993 )
994 ]
995 ]
996 )
997
998 Repo.update_all(q, [])
999 end
1000
1001 def upgrade_user_from_ap_id(ap_id) do
1002 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1003 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1004 already_ap <- User.ap_enabled?(user),
1005 {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
1006 unless already_ap do
1007 PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
1008 end
1009
1010 {:ok, user}
1011 else
1012 %User{} = user -> {:ok, user}
1013 e -> e
1014 end
1015 end
1016
1017 def maybe_retire_websub(ap_id) do
1018 # some sanity checks
1019 if is_binary(ap_id) && String.length(ap_id) > 8 do
1020 q =
1021 from(
1022 ws in Pleroma.Web.Websub.WebsubClientSubscription,
1023 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
1024 )
1025
1026 Repo.delete_all(q)
1027 end
1028 end
1029
1030 def maybe_fix_user_url(data) do
1031 if is_map(data["url"]) do
1032 Map.put(data, "url", data["url"]["href"])
1033 else
1034 data
1035 end
1036 end
1037
1038 def maybe_fix_user_object(data) do
1039 data
1040 |> maybe_fix_user_url
1041 end
1042 end