Merge branch 'dokku' into 'develop'
[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 follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address
97
98 explicit_mentions =
99 explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public", follower_collection]
100
101 object
102 |> fix_explicit_addressing(explicit_mentions)
103 end
104
105 # if as:Public is addressed, then make sure the followers collection is also addressed
106 # so that the activities will be delivered to local users.
107 def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
108 recipients = to ++ cc
109
110 if followers_collection not in recipients do
111 cond do
112 "https://www.w3.org/ns/activitystreams#Public" in cc ->
113 to = to ++ [followers_collection]
114 Map.put(object, "to", to)
115
116 "https://www.w3.org/ns/activitystreams#Public" in to ->
117 cc = cc ++ [followers_collection]
118 Map.put(object, "cc", cc)
119
120 true ->
121 object
122 end
123 else
124 object
125 end
126 end
127
128 def fix_implicit_addressing(object, _), do: object
129
130 def fix_addressing(object) do
131 {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
132 followers_collection = User.ap_followers(user)
133
134 object
135 |> fix_addressing_list("to")
136 |> fix_addressing_list("cc")
137 |> fix_addressing_list("bto")
138 |> fix_addressing_list("bcc")
139 |> fix_explicit_addressing
140 |> fix_implicit_addressing(followers_collection)
141 end
142
143 def fix_actor(%{"attributedTo" => actor} = object) do
144 object
145 |> Map.put("actor", Containment.get_actor(%{"actor" => actor}))
146 end
147
148 # Check for standardisation
149 # This is what Peertube does
150 # curl -H 'Accept: application/activity+json' $likes | jq .totalItems
151 # Prismo returns only an integer (count) as "likes"
152 def fix_likes(%{"likes" => likes} = object) when not is_map(likes) do
153 object
154 |> Map.put("likes", [])
155 |> Map.put("like_count", 0)
156 end
157
158 def fix_likes(object) do
159 object
160 end
161
162 def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object)
163 when not is_nil(in_reply_to) do
164 in_reply_to_id =
165 cond do
166 is_bitstring(in_reply_to) ->
167 in_reply_to
168
169 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
170 in_reply_to["id"]
171
172 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
173 Enum.at(in_reply_to, 0)
174
175 # Maybe I should output an error too?
176 true ->
177 ""
178 end
179
180 case get_obj_helper(in_reply_to_id) do
181 {:ok, replied_object} ->
182 with %Activity{} = _activity <-
183 Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
184 object
185 |> Map.put("inReplyTo", replied_object.data["id"])
186 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
187 |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
188 |> Map.put("context", replied_object.data["context"] || object["conversation"])
189 else
190 e ->
191 Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
192 object
193 end
194
195 e ->
196 Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
197 object
198 end
199 end
200
201 def fix_in_reply_to(object), do: object
202
203 def fix_context(object) do
204 context = object["context"] || object["conversation"] || Utils.generate_context_id()
205
206 object
207 |> Map.put("context", context)
208 |> Map.put("conversation", context)
209 end
210
211 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
212 attachments =
213 attachment
214 |> Enum.map(fn data ->
215 media_type = data["mediaType"] || data["mimeType"]
216 href = data["url"] || data["href"]
217
218 url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]
219
220 data
221 |> Map.put("mediaType", media_type)
222 |> Map.put("url", url)
223 end)
224
225 object
226 |> Map.put("attachment", attachments)
227 end
228
229 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
230 Map.put(object, "attachment", [attachment])
231 |> fix_attachments()
232 end
233
234 def fix_attachments(object), do: object
235
236 def fix_url(%{"url" => url} = object) when is_map(url) do
237 object
238 |> Map.put("url", url["href"])
239 end
240
241 def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
242 first_element = Enum.at(url, 0)
243
244 link_element =
245 url
246 |> Enum.filter(fn x -> is_map(x) end)
247 |> Enum.filter(fn x -> x["mimeType"] == "text/html" end)
248 |> Enum.at(0)
249
250 object
251 |> Map.put("attachment", [first_element])
252 |> Map.put("url", link_element["href"])
253 end
254
255 def fix_url(%{"type" => object_type, "url" => url} = object)
256 when object_type != "Video" and is_list(url) do
257 first_element = Enum.at(url, 0)
258
259 url_string =
260 cond do
261 is_bitstring(first_element) -> first_element
262 is_map(first_element) -> first_element["href"] || ""
263 true -> ""
264 end
265
266 object
267 |> Map.put("url", url_string)
268 end
269
270 def fix_url(object), do: object
271
272 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
273 emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
274
275 emoji =
276 emoji
277 |> Enum.reduce(%{}, fn data, mapping ->
278 name = String.trim(data["name"], ":")
279
280 mapping |> Map.put(name, data["icon"]["url"])
281 end)
282
283 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
284 emoji = Map.merge(object["emoji"] || %{}, emoji)
285
286 object
287 |> Map.put("emoji", emoji)
288 end
289
290 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
291 name = String.trim(tag["name"], ":")
292 emoji = %{name => tag["icon"]["url"]}
293
294 object
295 |> Map.put("emoji", emoji)
296 end
297
298 def fix_emoji(object), do: object
299
300 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
301 tags =
302 tag
303 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
304 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
305
306 combined = tag ++ tags
307
308 object
309 |> Map.put("tag", combined)
310 end
311
312 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
313 combined = [tag, String.slice(hashtag, 1..-1)]
314
315 object
316 |> Map.put("tag", combined)
317 end
318
319 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
320
321 def fix_tag(object), do: object
322
323 # content map usually only has one language so this will do for now.
324 def fix_content_map(%{"contentMap" => content_map} = object) do
325 content_groups = Map.to_list(content_map)
326 {_, content} = Enum.at(content_groups, 0)
327
328 object
329 |> Map.put("content", content)
330 end
331
332 def fix_content_map(object), do: object
333
334 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
335 with true <- id =~ "follows",
336 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
337 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
338 {:ok, activity}
339 else
340 _ -> {:error, nil}
341 end
342 end
343
344 defp mastodon_follow_hack(_, _), do: {:error, nil}
345
346 defp get_follow_activity(follow_object, followed) do
347 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
348 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
349 {:ok, activity}
350 else
351 # Can't find the activity. This might a Mastodon 2.3 "Accept"
352 {:activity, nil} ->
353 mastodon_follow_hack(follow_object, followed)
354
355 _ ->
356 {:error, nil}
357 end
358 end
359
360 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
361 # with nil ID.
362 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data) do
363 with context <- data["context"] || Utils.generate_context_id(),
364 content <- data["content"] || "",
365 %User{} = actor <- User.get_cached_by_ap_id(actor),
366
367 # Reduce the object list to find the reported user.
368 %User{} = account <-
369 Enum.reduce_while(objects, nil, fn ap_id, _ ->
370 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
371 {:halt, user}
372 else
373 _ -> {:cont, nil}
374 end
375 end),
376
377 # Remove the reported user from the object list.
378 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
379 params = %{
380 actor: actor,
381 context: context,
382 account: account,
383 statuses: statuses,
384 content: content,
385 additional: %{
386 "cc" => [account.ap_id]
387 }
388 }
389
390 ActivityPub.flag(params)
391 end
392 end
393
394 # disallow objects with bogus IDs
395 def handle_incoming(%{"id" => nil}), do: :error
396 def handle_incoming(%{"id" => ""}), do: :error
397 # length of https:// = 8, should validate better, but good enough for now.
398 def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8), do: :error
399
400 # TODO: validate those with a Ecto scheme
401 # - tags
402 # - emoji
403 def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
404 when objtype in ["Article", "Note", "Video", "Page"] do
405 actor = Containment.get_actor(data)
406
407 data =
408 Map.put(data, "actor", actor)
409 |> fix_addressing
410
411 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
412 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
413 object = fix_object(data["object"])
414
415 params = %{
416 to: data["to"],
417 object: object,
418 actor: user,
419 context: object["conversation"],
420 local: false,
421 published: data["published"],
422 additional:
423 Map.take(data, [
424 "cc",
425 "directMessage",
426 "id"
427 ])
428 }
429
430 ActivityPub.create(params)
431 else
432 %Activity{} = activity -> {:ok, activity}
433 _e -> :error
434 end
435 end
436
437 def handle_incoming(
438 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
439 ) do
440 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
441 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
442 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
443 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
444 {:user_blocked, false} <-
445 {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
446 {:user_locked, false} <- {:user_locked, User.locked?(followed)},
447 {:follow, {:ok, follower}} <- {:follow, User.follow(follower, followed)} do
448 ActivityPub.accept(%{
449 to: [follower.ap_id],
450 actor: followed,
451 object: data,
452 local: true
453 })
454 else
455 {:user_blocked, true} ->
456 {:ok, _} = Utils.update_follow_state(activity, "reject")
457
458 ActivityPub.reject(%{
459 to: [follower.ap_id],
460 actor: followed,
461 object: data,
462 local: true
463 })
464
465 {:follow, {:error, _}} ->
466 {:ok, _} = Utils.update_follow_state(activity, "reject")
467
468 ActivityPub.reject(%{
469 to: [follower.ap_id],
470 actor: followed,
471 object: data,
472 local: true
473 })
474
475 {:user_locked, true} ->
476 :noop
477 end
478
479 {:ok, activity}
480 else
481 _e ->
482 :error
483 end
484 end
485
486 def handle_incoming(
487 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data
488 ) do
489 with actor <- Containment.get_actor(data),
490 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
491 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
492 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
493 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
494 {:ok, activity} <-
495 ActivityPub.accept(%{
496 to: follow_activity.data["to"],
497 type: "Accept",
498 actor: followed,
499 object: follow_activity.data["id"],
500 local: false
501 }) do
502 if not User.following?(follower, followed) do
503 {:ok, _follower} = User.follow(follower, followed)
504 end
505
506 {:ok, activity}
507 else
508 _e -> :error
509 end
510 end
511
512 def handle_incoming(
513 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
514 ) do
515 with actor <- Containment.get_actor(data),
516 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
517 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
518 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
519 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
520 {:ok, activity} <-
521 ActivityPub.reject(%{
522 to: follow_activity.data["to"],
523 type: "Reject",
524 actor: followed,
525 object: follow_activity.data["id"],
526 local: false
527 }) do
528 User.unfollow(follower, followed)
529
530 {:ok, activity}
531 else
532 _e -> :error
533 end
534 end
535
536 def handle_incoming(
537 %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data
538 ) do
539 with actor <- Containment.get_actor(data),
540 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
541 {:ok, object} <- get_obj_helper(object_id),
542 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
543 {:ok, activity}
544 else
545 _e -> :error
546 end
547 end
548
549 def handle_incoming(
550 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
551 ) do
552 with actor <- Containment.get_actor(data),
553 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
554 {:ok, object} <- get_obj_helper(object_id),
555 public <- Visibility.is_public?(data),
556 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
557 {:ok, activity}
558 else
559 _e -> :error
560 end
561 end
562
563 def handle_incoming(
564 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
565 data
566 )
567 when object_type in ["Person", "Application", "Service", "Organization"] do
568 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
569 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
570
571 banner = new_user_data[:info]["banner"]
572 locked = new_user_data[:info]["locked"] || false
573
574 update_data =
575 new_user_data
576 |> Map.take([:name, :bio, :avatar])
577 |> Map.put(:info, %{"banner" => banner, "locked" => locked})
578
579 actor
580 |> User.upgrade_changeset(update_data)
581 |> User.update_and_set_cache()
582
583 ActivityPub.update(%{
584 local: false,
585 to: data["to"] || [],
586 cc: data["cc"] || [],
587 object: object,
588 actor: actor_id
589 })
590 else
591 e ->
592 Logger.error(e)
593 :error
594 end
595 end
596
597 # TODO: We presently assume that any actor on the same origin domain as the object being
598 # deleted has the rights to delete that object. A better way to validate whether or not
599 # the object should be deleted is to refetch the object URI, which should return either
600 # an error or a tombstone. This would allow us to verify that a deletion actually took
601 # place.
602 def handle_incoming(
603 %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data
604 ) do
605 object_id = Utils.get_ap_id(object_id)
606
607 with actor <- Containment.get_actor(data),
608 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
609 {:ok, object} <- get_obj_helper(object_id),
610 :ok <- Containment.contain_origin(actor.ap_id, object.data),
611 {:ok, activity} <- ActivityPub.delete(object, false) do
612 {:ok, activity}
613 else
614 _e -> :error
615 end
616 end
617
618 def handle_incoming(
619 %{
620 "type" => "Undo",
621 "object" => %{"type" => "Announce", "object" => object_id},
622 "actor" => _actor,
623 "id" => id
624 } = data
625 ) do
626 with actor <- Containment.get_actor(data),
627 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
628 {:ok, object} <- get_obj_helper(object_id),
629 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
630 {:ok, activity}
631 else
632 _e -> :error
633 end
634 end
635
636 def handle_incoming(
637 %{
638 "type" => "Undo",
639 "object" => %{"type" => "Follow", "object" => followed},
640 "actor" => follower,
641 "id" => id
642 } = _data
643 ) do
644 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
645 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
646 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
647 User.unfollow(follower, followed)
648 {:ok, activity}
649 else
650 _e -> :error
651 end
652 end
653
654 def handle_incoming(
655 %{
656 "type" => "Undo",
657 "object" => %{"type" => "Block", "object" => blocked},
658 "actor" => blocker,
659 "id" => id
660 } = _data
661 ) do
662 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
663 %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
664 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
665 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
666 User.unblock(blocker, blocked)
667 {:ok, activity}
668 else
669 _e -> :error
670 end
671 end
672
673 def handle_incoming(
674 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data
675 ) do
676 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
677 %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
678 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
679 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
680 User.unfollow(blocker, blocked)
681 User.block(blocker, blocked)
682 {:ok, activity}
683 else
684 _e -> :error
685 end
686 end
687
688 def handle_incoming(
689 %{
690 "type" => "Undo",
691 "object" => %{"type" => "Like", "object" => object_id},
692 "actor" => _actor,
693 "id" => id
694 } = data
695 ) do
696 with actor <- Containment.get_actor(data),
697 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
698 {:ok, object} <- get_obj_helper(object_id),
699 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
700 {:ok, activity}
701 else
702 _e -> :error
703 end
704 end
705
706 def handle_incoming(_), do: :error
707
708 def get_obj_helper(id) do
709 if object = Object.normalize(id), do: {:ok, object}, else: nil
710 end
711
712 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
713 with false <- String.starts_with?(in_reply_to, "http"),
714 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
715 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
716 else
717 _e -> object
718 end
719 end
720
721 def set_reply_to_uri(obj), do: obj
722
723 # Prepares the object of an outgoing create activity.
724 def prepare_object(object) do
725 object
726 |> set_sensitive
727 |> add_hashtags
728 |> add_mention_tags
729 |> add_emoji_tags
730 |> add_attributed_to
731 |> add_likes
732 |> prepare_attachments
733 |> set_conversation
734 |> set_reply_to_uri
735 |> strip_internal_fields
736 |> strip_internal_tags
737 end
738
739 # @doc
740 # """
741 # internal -> Mastodon
742 # """
743
744 def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
745 object =
746 Object.normalize(object_id).data
747 |> prepare_object
748
749 data =
750 data
751 |> Map.put("object", object)
752 |> Map.merge(Utils.make_json_ld_header())
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