Merge remote-tracking branch 'pleroma/develop' into feature/addressable-lists
[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_id
744 |> Object.normalize()
745 |> Map.get(:data)
746 |> prepare_object
747
748 data =
749 data
750 |> Map.put("object", object)
751 |> Map.merge(Utils.make_json_ld_header())
752 |> Map.delete("bcc")
753
754 {:ok, data}
755 end
756
757 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
758 # because of course it does.
759 def prepare_outgoing(%{"type" => "Accept"} = data) do
760 with follow_activity <- Activity.normalize(data["object"]) do
761 object = %{
762 "actor" => follow_activity.actor,
763 "object" => follow_activity.data["object"],
764 "id" => follow_activity.data["id"],
765 "type" => "Follow"
766 }
767
768 data =
769 data
770 |> Map.put("object", object)
771 |> Map.merge(Utils.make_json_ld_header())
772
773 {:ok, data}
774 end
775 end
776
777 def prepare_outgoing(%{"type" => "Reject"} = data) do
778 with follow_activity <- Activity.normalize(data["object"]) do
779 object = %{
780 "actor" => follow_activity.actor,
781 "object" => follow_activity.data["object"],
782 "id" => follow_activity.data["id"],
783 "type" => "Follow"
784 }
785
786 data =
787 data
788 |> Map.put("object", object)
789 |> Map.merge(Utils.make_json_ld_header())
790
791 {:ok, data}
792 end
793 end
794
795 def prepare_outgoing(%{"type" => _type} = data) do
796 data =
797 data
798 |> strip_internal_fields
799 |> maybe_fix_object_url
800 |> Map.merge(Utils.make_json_ld_header())
801
802 {:ok, data}
803 end
804
805 def maybe_fix_object_url(data) do
806 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
807 case get_obj_helper(data["object"]) do
808 {:ok, relative_object} ->
809 if relative_object.data["external_url"] do
810 _data =
811 data
812 |> Map.put("object", relative_object.data["external_url"])
813 else
814 data
815 end
816
817 e ->
818 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
819 data
820 end
821 else
822 data
823 end
824 end
825
826 def add_hashtags(object) do
827 tags =
828 (object["tag"] || [])
829 |> Enum.map(fn
830 # Expand internal representation tags into AS2 tags.
831 tag when is_binary(tag) ->
832 %{
833 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
834 "name" => "##{tag}",
835 "type" => "Hashtag"
836 }
837
838 # Do not process tags which are already AS2 tag objects.
839 tag when is_map(tag) ->
840 tag
841 end)
842
843 object
844 |> Map.put("tag", tags)
845 end
846
847 def add_mention_tags(object) do
848 mentions =
849 object
850 |> Utils.get_notified_from_object()
851 |> Enum.map(fn user ->
852 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
853 end)
854
855 tags = object["tag"] || []
856
857 object
858 |> Map.put("tag", tags ++ mentions)
859 end
860
861 def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
862 user_info = add_emoji_tags(user_info)
863
864 object
865 |> Map.put(:info, user_info)
866 end
867
868 # TODO: we should probably send mtime instead of unix epoch time for updated
869 def add_emoji_tags(%{"emoji" => emoji} = object) do
870 tags = object["tag"] || []
871
872 out =
873 emoji
874 |> Enum.map(fn {name, url} ->
875 %{
876 "icon" => %{"url" => url, "type" => "Image"},
877 "name" => ":" <> name <> ":",
878 "type" => "Emoji",
879 "updated" => "1970-01-01T00:00:00Z",
880 "id" => url
881 }
882 end)
883
884 object
885 |> Map.put("tag", tags ++ out)
886 end
887
888 def add_emoji_tags(object) do
889 object
890 end
891
892 def set_conversation(object) do
893 Map.put(object, "conversation", object["context"])
894 end
895
896 def set_sensitive(object) do
897 tags = object["tag"] || []
898 Map.put(object, "sensitive", "nsfw" in tags)
899 end
900
901 def add_attributed_to(object) do
902 attributed_to = object["attributedTo"] || object["actor"]
903
904 object
905 |> Map.put("attributedTo", attributed_to)
906 end
907
908 def add_likes(%{"id" => id, "like_count" => likes} = object) do
909 likes = %{
910 "id" => "#{id}/likes",
911 "first" => "#{id}/likes?page=1",
912 "type" => "OrderedCollection",
913 "totalItems" => likes
914 }
915
916 object
917 |> Map.put("likes", likes)
918 end
919
920 def add_likes(object) do
921 object
922 end
923
924 def prepare_attachments(object) do
925 attachments =
926 (object["attachment"] || [])
927 |> Enum.map(fn data ->
928 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
929 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
930 end)
931
932 object
933 |> Map.put("attachment", attachments)
934 end
935
936 defp strip_internal_fields(object) do
937 object
938 |> Map.drop([
939 "like_count",
940 "announcements",
941 "announcement_count",
942 "emoji",
943 "context_id",
944 "deleted_activity_id"
945 ])
946 end
947
948 defp strip_internal_tags(%{"tag" => tags} = object) do
949 tags =
950 tags
951 |> Enum.filter(fn x -> is_map(x) end)
952
953 object
954 |> Map.put("tag", tags)
955 end
956
957 defp strip_internal_tags(object), do: object
958
959 def perform(:user_upgrade, user) do
960 # we pass a fake user so that the followers collection is stripped away
961 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
962
963 q =
964 from(
965 u in User,
966 where: ^old_follower_address in u.following,
967 update: [
968 set: [
969 following:
970 fragment(
971 "array_replace(?,?,?)",
972 u.following,
973 ^old_follower_address,
974 ^user.follower_address
975 )
976 ]
977 ]
978 )
979
980 Repo.update_all(q, [])
981
982 maybe_retire_websub(user.ap_id)
983
984 q =
985 from(
986 a in Activity,
987 where: ^old_follower_address in a.recipients,
988 update: [
989 set: [
990 recipients:
991 fragment(
992 "array_replace(?,?,?)",
993 a.recipients,
994 ^old_follower_address,
995 ^user.follower_address
996 )
997 ]
998 ]
999 )
1000
1001 Repo.update_all(q, [])
1002 end
1003
1004 def upgrade_user_from_ap_id(ap_id) do
1005 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1006 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1007 already_ap <- User.ap_enabled?(user),
1008 {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
1009 unless already_ap do
1010 PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
1011 end
1012
1013 {:ok, user}
1014 else
1015 %User{} = user -> {:ok, user}
1016 e -> e
1017 end
1018 end
1019
1020 def maybe_retire_websub(ap_id) do
1021 # some sanity checks
1022 if is_binary(ap_id) && String.length(ap_id) > 8 do
1023 q =
1024 from(
1025 ws in Pleroma.Web.Websub.WebsubClientSubscription,
1026 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
1027 )
1028
1029 Repo.delete_all(q)
1030 end
1031 end
1032
1033 def maybe_fix_user_url(data) do
1034 if is_map(data["url"]) do
1035 Map.put(data, "url", data["url"]["href"])
1036 else
1037 data
1038 end
1039 end
1040
1041 def maybe_fix_user_object(data) do
1042 data
1043 |> maybe_fix_user_url
1044 end
1045 end