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