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