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