make bulk user creation from admin works as a transaction
[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 {:ok, %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 {:ok, %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 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
440 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
441 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
442 {:user_blocked, false} <-
443 {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
444 {:user_locked, false} <- {:user_locked, User.locked?(followed)},
445 {:follow, {:ok, follower}} <- {:follow, User.follow(follower, followed)} do
446 ActivityPub.accept(%{
447 to: [follower.ap_id],
448 actor: followed,
449 object: data,
450 local: true
451 })
452 else
453 {:user_blocked, true} ->
454 {:ok, _} = Utils.update_follow_state(activity, "reject")
455
456 ActivityPub.reject(%{
457 to: [follower.ap_id],
458 actor: followed,
459 object: data,
460 local: true
461 })
462
463 {:follow, {:error, _}} ->
464 {:ok, _} = Utils.update_follow_state(activity, "reject")
465
466 ActivityPub.reject(%{
467 to: [follower.ap_id],
468 actor: followed,
469 object: data,
470 local: true
471 })
472
473 {:user_locked, true} ->
474 :noop
475 end
476
477 {:ok, activity}
478 else
479 _e ->
480 :error
481 end
482 end
483
484 def handle_incoming(
485 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data
486 ) do
487 with actor <- Containment.get_actor(data),
488 {:ok, %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, "accept"),
491 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
492 {:ok, activity} <-
493 ActivityPub.accept(%{
494 to: follow_activity.data["to"],
495 type: "Accept",
496 actor: followed,
497 object: follow_activity.data["id"],
498 local: false
499 }) do
500 if not User.following?(follower, followed) do
501 {:ok, _follower} = User.follow(follower, followed)
502 end
503
504 {:ok, activity}
505 else
506 _e -> :error
507 end
508 end
509
510 def handle_incoming(
511 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
512 ) do
513 with actor <- Containment.get_actor(data),
514 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
515 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
516 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
517 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
518 {:ok, activity} <-
519 ActivityPub.reject(%{
520 to: follow_activity.data["to"],
521 type: "Reject",
522 actor: followed,
523 object: follow_activity.data["id"],
524 local: false
525 }) do
526 User.unfollow(follower, followed)
527
528 {:ok, activity}
529 else
530 _e -> :error
531 end
532 end
533
534 def handle_incoming(
535 %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data
536 ) do
537 with actor <- Containment.get_actor(data),
538 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
539 {:ok, object} <- get_obj_helper(object_id),
540 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
541 {:ok, activity}
542 else
543 _e -> :error
544 end
545 end
546
547 def handle_incoming(
548 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
549 ) do
550 with actor <- Containment.get_actor(data),
551 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
552 {:ok, object} <- get_obj_helper(object_id),
553 public <- Visibility.is_public?(data),
554 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
555 {:ok, activity}
556 else
557 _e -> :error
558 end
559 end
560
561 def handle_incoming(
562 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
563 data
564 )
565 when object_type in ["Person", "Application", "Service", "Organization"] do
566 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
567 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
568
569 banner = new_user_data[:info]["banner"]
570 locked = new_user_data[:info]["locked"] || false
571
572 update_data =
573 new_user_data
574 |> Map.take([:name, :bio, :avatar])
575 |> Map.put(:info, %{"banner" => banner, "locked" => locked})
576
577 actor
578 |> User.upgrade_changeset(update_data)
579 |> User.update_and_set_cache()
580
581 ActivityPub.update(%{
582 local: false,
583 to: data["to"] || [],
584 cc: data["cc"] || [],
585 object: object,
586 actor: actor_id
587 })
588 else
589 e ->
590 Logger.error(e)
591 :error
592 end
593 end
594
595 # TODO: We presently assume that any actor on the same origin domain as the object being
596 # deleted has the rights to delete that object. A better way to validate whether or not
597 # the object should be deleted is to refetch the object URI, which should return either
598 # an error or a tombstone. This would allow us to verify that a deletion actually took
599 # place.
600 def handle_incoming(
601 %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data
602 ) do
603 object_id = Utils.get_ap_id(object_id)
604
605 with actor <- Containment.get_actor(data),
606 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
607 {:ok, object} <- get_obj_helper(object_id),
608 :ok <- Containment.contain_origin(actor.ap_id, object.data),
609 {:ok, activity} <- ActivityPub.delete(object, false) do
610 {:ok, activity}
611 else
612 _e -> :error
613 end
614 end
615
616 def handle_incoming(
617 %{
618 "type" => "Undo",
619 "object" => %{"type" => "Announce", "object" => object_id},
620 "actor" => _actor,
621 "id" => id
622 } = data
623 ) do
624 with actor <- Containment.get_actor(data),
625 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
626 {:ok, object} <- get_obj_helper(object_id),
627 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
628 {:ok, activity}
629 else
630 _e -> :error
631 end
632 end
633
634 def handle_incoming(
635 %{
636 "type" => "Undo",
637 "object" => %{"type" => "Follow", "object" => followed},
638 "actor" => follower,
639 "id" => id
640 } = _data
641 ) do
642 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
643 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
644 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
645 User.unfollow(follower, followed)
646 {:ok, activity}
647 else
648 _e -> :error
649 end
650 end
651
652 def handle_incoming(
653 %{
654 "type" => "Undo",
655 "object" => %{"type" => "Block", "object" => blocked},
656 "actor" => blocker,
657 "id" => id
658 } = _data
659 ) do
660 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
661 %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
662 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
663 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
664 User.unblock(blocker, blocked)
665 {:ok, activity}
666 else
667 _e -> :error
668 end
669 end
670
671 def handle_incoming(
672 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data
673 ) do
674 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
675 %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
676 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
677 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
678 User.unfollow(blocker, blocked)
679 User.block(blocker, blocked)
680 {:ok, activity}
681 else
682 _e -> :error
683 end
684 end
685
686 def handle_incoming(
687 %{
688 "type" => "Undo",
689 "object" => %{"type" => "Like", "object" => object_id},
690 "actor" => _actor,
691 "id" => id
692 } = data
693 ) do
694 with actor <- Containment.get_actor(data),
695 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
696 {:ok, object} <- get_obj_helper(object_id),
697 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
698 {:ok, activity}
699 else
700 _e -> :error
701 end
702 end
703
704 def handle_incoming(_), do: :error
705
706 def get_obj_helper(id) do
707 if object = Object.normalize(id), do: {:ok, object}, else: nil
708 end
709
710 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
711 with false <- String.starts_with?(in_reply_to, "http"),
712 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
713 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
714 else
715 _e -> object
716 end
717 end
718
719 def set_reply_to_uri(obj), do: obj
720
721 # Prepares the object of an outgoing create activity.
722 def prepare_object(object) do
723 object
724 |> set_sensitive
725 |> add_hashtags
726 |> add_mention_tags
727 |> add_emoji_tags
728 |> add_attributed_to
729 |> add_likes
730 |> prepare_attachments
731 |> set_conversation
732 |> set_reply_to_uri
733 |> strip_internal_fields
734 |> strip_internal_tags
735 end
736
737 # @doc
738 # """
739 # internal -> Mastodon
740 # """
741
742 def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
743 object =
744 Object.normalize(object_id).data
745 |> prepare_object
746
747 data =
748 data
749 |> Map.put("object", object)
750 |> Map.merge(Utils.make_json_ld_header())
751
752 {:ok, data}
753 end
754
755 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
756 # because of course it does.
757 def prepare_outgoing(%{"type" => "Accept"} = data) do
758 with follow_activity <- Activity.normalize(data["object"]) do
759 object = %{
760 "actor" => follow_activity.actor,
761 "object" => follow_activity.data["object"],
762 "id" => follow_activity.data["id"],
763 "type" => "Follow"
764 }
765
766 data =
767 data
768 |> Map.put("object", object)
769 |> Map.merge(Utils.make_json_ld_header())
770
771 {:ok, data}
772 end
773 end
774
775 def prepare_outgoing(%{"type" => "Reject"} = data) do
776 with follow_activity <- Activity.normalize(data["object"]) do
777 object = %{
778 "actor" => follow_activity.actor,
779 "object" => follow_activity.data["object"],
780 "id" => follow_activity.data["id"],
781 "type" => "Follow"
782 }
783
784 data =
785 data
786 |> Map.put("object", object)
787 |> Map.merge(Utils.make_json_ld_header())
788
789 {:ok, data}
790 end
791 end
792
793 def prepare_outgoing(%{"type" => _type} = data) do
794 data =
795 data
796 |> strip_internal_fields
797 |> maybe_fix_object_url
798 |> Map.merge(Utils.make_json_ld_header())
799
800 {:ok, data}
801 end
802
803 def maybe_fix_object_url(data) do
804 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
805 case get_obj_helper(data["object"]) do
806 {:ok, relative_object} ->
807 if relative_object.data["external_url"] do
808 _data =
809 data
810 |> Map.put("object", relative_object.data["external_url"])
811 else
812 data
813 end
814
815 e ->
816 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
817 data
818 end
819 else
820 data
821 end
822 end
823
824 def add_hashtags(object) do
825 tags =
826 (object["tag"] || [])
827 |> Enum.map(fn
828 # Expand internal representation tags into AS2 tags.
829 tag when is_binary(tag) ->
830 %{
831 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
832 "name" => "##{tag}",
833 "type" => "Hashtag"
834 }
835
836 # Do not process tags which are already AS2 tag objects.
837 tag when is_map(tag) ->
838 tag
839 end)
840
841 object
842 |> Map.put("tag", tags)
843 end
844
845 def add_mention_tags(object) do
846 mentions =
847 object
848 |> Utils.get_notified_from_object()
849 |> Enum.map(fn user ->
850 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
851 end)
852
853 tags = object["tag"] || []
854
855 object
856 |> Map.put("tag", tags ++ mentions)
857 end
858
859 def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
860 user_info = add_emoji_tags(user_info)
861
862 object
863 |> Map.put(:info, user_info)
864 end
865
866 # TODO: we should probably send mtime instead of unix epoch time for updated
867 def add_emoji_tags(%{"emoji" => emoji} = object) do
868 tags = object["tag"] || []
869
870 out =
871 emoji
872 |> Enum.map(fn {name, url} ->
873 %{
874 "icon" => %{"url" => url, "type" => "Image"},
875 "name" => ":" <> name <> ":",
876 "type" => "Emoji",
877 "updated" => "1970-01-01T00:00:00Z",
878 "id" => url
879 }
880 end)
881
882 object
883 |> Map.put("tag", tags ++ out)
884 end
885
886 def add_emoji_tags(object) do
887 object
888 end
889
890 def set_conversation(object) do
891 Map.put(object, "conversation", object["context"])
892 end
893
894 def set_sensitive(object) do
895 tags = object["tag"] || []
896 Map.put(object, "sensitive", "nsfw" in tags)
897 end
898
899 def add_attributed_to(object) do
900 attributed_to = object["attributedTo"] || object["actor"]
901
902 object
903 |> Map.put("attributedTo", attributed_to)
904 end
905
906 def add_likes(%{"id" => id, "like_count" => likes} = object) do
907 likes = %{
908 "id" => "#{id}/likes",
909 "first" => "#{id}/likes?page=1",
910 "type" => "OrderedCollection",
911 "totalItems" => likes
912 }
913
914 object
915 |> Map.put("likes", likes)
916 end
917
918 def add_likes(object) do
919 object
920 end
921
922 def prepare_attachments(object) do
923 attachments =
924 (object["attachment"] || [])
925 |> Enum.map(fn data ->
926 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
927 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
928 end)
929
930 object
931 |> Map.put("attachment", attachments)
932 end
933
934 defp strip_internal_fields(object) do
935 object
936 |> Map.drop([
937 "like_count",
938 "announcements",
939 "announcement_count",
940 "emoji",
941 "context_id",
942 "deleted_activity_id"
943 ])
944 end
945
946 defp strip_internal_tags(%{"tag" => tags} = object) do
947 tags =
948 tags
949 |> Enum.filter(fn x -> is_map(x) end)
950
951 object
952 |> Map.put("tag", tags)
953 end
954
955 defp strip_internal_tags(object), do: object
956
957 def perform(:user_upgrade, user) do
958 # we pass a fake user so that the followers collection is stripped away
959 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
960
961 q =
962 from(
963 u in User,
964 where: ^old_follower_address in u.following,
965 update: [
966 set: [
967 following:
968 fragment(
969 "array_replace(?,?,?)",
970 u.following,
971 ^old_follower_address,
972 ^user.follower_address
973 )
974 ]
975 ]
976 )
977
978 Repo.update_all(q, [])
979
980 maybe_retire_websub(user.ap_id)
981
982 q =
983 from(
984 a in Activity,
985 where: ^old_follower_address in a.recipients,
986 update: [
987 set: [
988 recipients:
989 fragment(
990 "array_replace(?,?,?)",
991 a.recipients,
992 ^old_follower_address,
993 ^user.follower_address
994 )
995 ]
996 ]
997 )
998
999 Repo.update_all(q, [])
1000 end
1001
1002 def upgrade_user_from_ap_id(ap_id) do
1003 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1004 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1005 already_ap <- User.ap_enabled?(user),
1006 {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
1007 unless already_ap do
1008 PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
1009 end
1010
1011 {:ok, user}
1012 else
1013 %User{} = user -> {:ok, user}
1014 e -> e
1015 end
1016 end
1017
1018 def maybe_retire_websub(ap_id) do
1019 # some sanity checks
1020 if is_binary(ap_id) && String.length(ap_id) > 8 do
1021 q =
1022 from(
1023 ws in Pleroma.Web.Websub.WebsubClientSubscription,
1024 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
1025 )
1026
1027 Repo.delete_all(q)
1028 end
1029 end
1030
1031 def maybe_fix_user_url(data) do
1032 if is_map(data["url"]) do
1033 Map.put(data, "url", data["url"]["href"])
1034 else
1035 data
1036 end
1037 end
1038
1039 def maybe_fix_user_object(data) do
1040 data
1041 |> maybe_fix_user_url
1042 end
1043 end