[Credo] fix Credo.Check.Readability.AliasOrder
[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.Repo
12 alias Pleroma.User
13 alias Pleroma.Web.ActivityPub.ActivityPub
14 alias Pleroma.Web.ActivityPub.Utils
15 alias Pleroma.Web.ActivityPub.Visibility
16
17 import Ecto.Query
18
19 require Logger
20
21 def get_actor(%{"actor" => actor}) when is_binary(actor) do
22 actor
23 end
24
25 def get_actor(%{"actor" => actor}) when is_list(actor) do
26 if is_binary(Enum.at(actor, 0)) do
27 Enum.at(actor, 0)
28 else
29 Enum.find(actor, fn %{"type" => type} -> type in ["Person", "Service", "Application"] end)
30 |> Map.get("id")
31 end
32 end
33
34 def get_actor(%{"actor" => %{"id" => id}}) when is_bitstring(id) do
35 id
36 end
37
38 def get_actor(%{"actor" => nil, "attributedTo" => actor}) when not is_nil(actor) do
39 get_actor(%{"actor" => actor})
40 end
41
42 @doc """
43 Checks that an imported AP object's actor matches the domain it came from.
44 """
45 def contain_origin(_id, %{"actor" => nil}), do: :error
46
47 def contain_origin(id, %{"actor" => _actor} = params) do
48 id_uri = URI.parse(id)
49 actor_uri = URI.parse(get_actor(params))
50
51 if id_uri.host == actor_uri.host do
52 :ok
53 else
54 :error
55 end
56 end
57
58 def contain_origin_from_id(_id, %{"id" => nil}), do: :error
59
60 def contain_origin_from_id(id, %{"id" => other_id} = _params) do
61 id_uri = URI.parse(id)
62 other_uri = URI.parse(other_id)
63
64 if id_uri.host == other_uri.host do
65 :ok
66 else
67 :error
68 end
69 end
70
71 @doc """
72 Modifies an incoming AP object (mastodon format) to our internal format.
73 """
74 def fix_object(object) do
75 object
76 |> fix_actor
77 |> fix_url
78 |> fix_attachments
79 |> fix_context
80 |> fix_in_reply_to
81 |> fix_emoji
82 |> fix_tag
83 |> fix_content_map
84 |> fix_likes
85 |> fix_addressing
86 end
87
88 def fix_addressing_list(map, field) do
89 if is_binary(map[field]) do
90 map
91 |> Map.put(field, [map[field]])
92 else
93 map
94 end
95 end
96
97 def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mentions) do
98 explicit_to =
99 to
100 |> Enum.filter(fn x -> x in explicit_mentions end)
101
102 explicit_cc =
103 to
104 |> Enum.filter(fn x -> x not in explicit_mentions end)
105
106 final_cc =
107 (cc ++ explicit_cc)
108 |> Enum.uniq()
109
110 object
111 |> Map.put("to", explicit_to)
112 |> Map.put("cc", final_cc)
113 end
114
115 def fix_explicit_addressing(object, _explicit_mentions), do: object
116
117 # if directMessage flag is set to true, leave the addressing alone
118 def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
119
120 def fix_explicit_addressing(object) do
121 explicit_mentions =
122 object
123 |> Utils.determine_explicit_mentions()
124
125 explicit_mentions = explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public"]
126
127 object
128 |> fix_explicit_addressing(explicit_mentions)
129 end
130
131 def fix_addressing(object) do
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 end
139
140 def fix_actor(%{"attributedTo" => actor} = object) do
141 object
142 |> Map.put("actor", 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 fetch_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("inReplyToStatusId", activity.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 # disallow objects with bogus IDs
359 def handle_incoming(%{"id" => nil}), do: :error
360 def handle_incoming(%{"id" => ""}), do: :error
361 # length of https:// = 8, should validate better, but good enough for now.
362 def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8), do: :error
363
364 # TODO: validate those with a Ecto scheme
365 # - tags
366 # - emoji
367 def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
368 when objtype in ["Article", "Note", "Video", "Page"] do
369 actor = get_actor(data)
370
371 data =
372 Map.put(data, "actor", actor)
373 |> fix_addressing
374
375 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
376 %User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do
377 object = fix_object(data["object"])
378
379 params = %{
380 to: data["to"],
381 object: object,
382 actor: user,
383 context: object["conversation"],
384 local: false,
385 published: data["published"],
386 additional:
387 Map.take(data, [
388 "cc",
389 "directMessage",
390 "id"
391 ])
392 }
393
394 ActivityPub.create(params)
395 else
396 %Activity{} = activity -> {:ok, activity}
397 _e -> :error
398 end
399 end
400
401 def handle_incoming(
402 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
403 ) do
404 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
405 %User{} = follower <- User.get_or_fetch_by_ap_id(follower),
406 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
407 if not User.locked?(followed) do
408 ActivityPub.accept(%{
409 to: [follower.ap_id],
410 actor: followed,
411 object: data,
412 local: true
413 })
414
415 User.follow(follower, followed)
416 end
417
418 {:ok, activity}
419 else
420 _e -> :error
421 end
422 end
423
424 def handle_incoming(
425 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data
426 ) do
427 with actor <- get_actor(data),
428 %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
429 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
430 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
431 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
432 {:ok, activity} <-
433 ActivityPub.accept(%{
434 to: follow_activity.data["to"],
435 type: "Accept",
436 actor: followed,
437 object: follow_activity.data["id"],
438 local: false
439 }) do
440 if not User.following?(follower, followed) do
441 {:ok, _follower} = User.follow(follower, followed)
442 end
443
444 {:ok, activity}
445 else
446 _e -> :error
447 end
448 end
449
450 def handle_incoming(
451 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
452 ) do
453 with actor <- get_actor(data),
454 %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
455 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
456 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
457 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
458 {:ok, activity} <-
459 ActivityPub.reject(%{
460 to: follow_activity.data["to"],
461 type: "Reject",
462 actor: followed,
463 object: follow_activity.data["id"],
464 local: false
465 }) do
466 User.unfollow(follower, followed)
467
468 {:ok, activity}
469 else
470 _e -> :error
471 end
472 end
473
474 def handle_incoming(
475 %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data
476 ) do
477 with actor <- get_actor(data),
478 %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
479 {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
480 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
481 {:ok, activity}
482 else
483 _e -> :error
484 end
485 end
486
487 def handle_incoming(
488 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
489 ) do
490 with actor <- get_actor(data),
491 %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
492 {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
493 public <- Visibility.is_public?(data),
494 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
495 {:ok, activity}
496 else
497 _e -> :error
498 end
499 end
500
501 def handle_incoming(
502 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
503 data
504 )
505 when object_type in ["Person", "Application", "Service", "Organization"] do
506 with %User{ap_id: ^actor_id} = actor <- User.get_by_ap_id(object["id"]) do
507 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
508
509 banner = new_user_data[:info]["banner"]
510 locked = new_user_data[:info]["locked"] || false
511
512 update_data =
513 new_user_data
514 |> Map.take([:name, :bio, :avatar])
515 |> Map.put(:info, %{"banner" => banner, "locked" => locked})
516
517 actor
518 |> User.upgrade_changeset(update_data)
519 |> User.update_and_set_cache()
520
521 ActivityPub.update(%{
522 local: false,
523 to: data["to"] || [],
524 cc: data["cc"] || [],
525 object: object,
526 actor: actor_id
527 })
528 else
529 e ->
530 Logger.error(e)
531 :error
532 end
533 end
534
535 # TODO: We presently assume that any actor on the same origin domain as the object being
536 # deleted has the rights to delete that object. A better way to validate whether or not
537 # the object should be deleted is to refetch the object URI, which should return either
538 # an error or a tombstone. This would allow us to verify that a deletion actually took
539 # place.
540 def handle_incoming(
541 %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data
542 ) do
543 object_id = Utils.get_ap_id(object_id)
544
545 with actor <- get_actor(data),
546 %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
547 {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
548 :ok <- contain_origin(actor.ap_id, object.data),
549 {:ok, activity} <- ActivityPub.delete(object, false) do
550 {:ok, activity}
551 else
552 _e -> :error
553 end
554 end
555
556 def handle_incoming(
557 %{
558 "type" => "Undo",
559 "object" => %{"type" => "Announce", "object" => object_id},
560 "actor" => _actor,
561 "id" => id
562 } = data
563 ) do
564 with actor <- get_actor(data),
565 %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
566 {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
567 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
568 {:ok, activity}
569 else
570 _e -> :error
571 end
572 end
573
574 def handle_incoming(
575 %{
576 "type" => "Undo",
577 "object" => %{"type" => "Follow", "object" => followed},
578 "actor" => follower,
579 "id" => id
580 } = _data
581 ) do
582 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
583 %User{} = follower <- User.get_or_fetch_by_ap_id(follower),
584 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
585 User.unfollow(follower, followed)
586 {:ok, activity}
587 else
588 _e -> :error
589 end
590 end
591
592 def handle_incoming(
593 %{
594 "type" => "Undo",
595 "object" => %{"type" => "Block", "object" => blocked},
596 "actor" => blocker,
597 "id" => id
598 } = _data
599 ) do
600 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
601 %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
602 %User{} = blocker <- User.get_or_fetch_by_ap_id(blocker),
603 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
604 User.unblock(blocker, blocked)
605 {:ok, activity}
606 else
607 _e -> :error
608 end
609 end
610
611 def handle_incoming(
612 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data
613 ) do
614 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
615 %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
616 %User{} = blocker = User.get_or_fetch_by_ap_id(blocker),
617 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
618 User.unfollow(blocker, blocked)
619 User.block(blocker, blocked)
620 {:ok, activity}
621 else
622 _e -> :error
623 end
624 end
625
626 def handle_incoming(
627 %{
628 "type" => "Undo",
629 "object" => %{"type" => "Like", "object" => object_id},
630 "actor" => _actor,
631 "id" => id
632 } = data
633 ) do
634 with actor <- get_actor(data),
635 %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
636 {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
637 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
638 {:ok, activity}
639 else
640 _e -> :error
641 end
642 end
643
644 def handle_incoming(_), do: :error
645
646 def fetch_obj_helper(id) when is_bitstring(id), do: ActivityPub.fetch_object_from_id(id)
647 def fetch_obj_helper(obj) when is_map(obj), do: ActivityPub.fetch_object_from_id(obj["id"])
648
649 def get_obj_helper(id) do
650 if object = Object.normalize(id), do: {:ok, object}, else: nil
651 end
652
653 def set_reply_to_uri(%{"inReplyTo" => inReplyTo} = object) when is_binary(inReplyTo) do
654 with false <- String.starts_with?(inReplyTo, "http"),
655 {:ok, %{data: replied_to_object}} <- get_obj_helper(inReplyTo) do
656 Map.put(object, "inReplyTo", replied_to_object["external_url"] || inReplyTo)
657 else
658 _e -> object
659 end
660 end
661
662 def set_reply_to_uri(obj), do: obj
663
664 # Prepares the object of an outgoing create activity.
665 def prepare_object(object) do
666 object
667 |> set_sensitive
668 |> add_hashtags
669 |> add_mention_tags
670 |> add_emoji_tags
671 |> add_attributed_to
672 |> add_likes
673 |> prepare_attachments
674 |> set_conversation
675 |> set_reply_to_uri
676 |> strip_internal_fields
677 |> strip_internal_tags
678 end
679
680 # @doc
681 # """
682 # internal -> Mastodon
683 # """
684
685 def prepare_outgoing(%{"type" => "Create", "object" => object} = data) do
686 object =
687 object
688 |> prepare_object
689
690 data =
691 data
692 |> Map.put("object", object)
693 |> Map.merge(Utils.make_json_ld_header())
694
695 {:ok, data}
696 end
697
698 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
699 # because of course it does.
700 def prepare_outgoing(%{"type" => "Accept"} = data) do
701 with follow_activity <- Activity.normalize(data["object"]) do
702 object = %{
703 "actor" => follow_activity.actor,
704 "object" => follow_activity.data["object"],
705 "id" => follow_activity.data["id"],
706 "type" => "Follow"
707 }
708
709 data =
710 data
711 |> Map.put("object", object)
712 |> Map.merge(Utils.make_json_ld_header())
713
714 {:ok, data}
715 end
716 end
717
718 def prepare_outgoing(%{"type" => "Reject"} = data) do
719 with follow_activity <- Activity.normalize(data["object"]) do
720 object = %{
721 "actor" => follow_activity.actor,
722 "object" => follow_activity.data["object"],
723 "id" => follow_activity.data["id"],
724 "type" => "Follow"
725 }
726
727 data =
728 data
729 |> Map.put("object", object)
730 |> Map.merge(Utils.make_json_ld_header())
731
732 {:ok, data}
733 end
734 end
735
736 def prepare_outgoing(%{"type" => _type} = data) do
737 data =
738 data
739 |> strip_internal_fields
740 |> maybe_fix_object_url
741 |> Map.merge(Utils.make_json_ld_header())
742
743 {:ok, data}
744 end
745
746 def maybe_fix_object_url(data) do
747 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
748 case fetch_obj_helper(data["object"]) do
749 {:ok, relative_object} ->
750 if relative_object.data["external_url"] do
751 _data =
752 data
753 |> Map.put("object", relative_object.data["external_url"])
754 else
755 data
756 end
757
758 e ->
759 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
760 data
761 end
762 else
763 data
764 end
765 end
766
767 def add_hashtags(object) do
768 tags =
769 (object["tag"] || [])
770 |> Enum.map(fn
771 # Expand internal representation tags into AS2 tags.
772 tag when is_binary(tag) ->
773 %{
774 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
775 "name" => "##{tag}",
776 "type" => "Hashtag"
777 }
778
779 # Do not process tags which are already AS2 tag objects.
780 tag when is_map(tag) ->
781 tag
782 end)
783
784 object
785 |> Map.put("tag", tags)
786 end
787
788 def add_mention_tags(object) do
789 mentions =
790 object
791 |> Utils.get_notified_from_object()
792 |> Enum.map(fn user ->
793 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
794 end)
795
796 tags = object["tag"] || []
797
798 object
799 |> Map.put("tag", tags ++ mentions)
800 end
801
802 # TODO: we should probably send mtime instead of unix epoch time for updated
803 def add_emoji_tags(object) do
804 tags = object["tag"] || []
805 emoji = object["emoji"] || []
806
807 out =
808 emoji
809 |> Enum.map(fn {name, url} ->
810 %{
811 "icon" => %{"url" => url, "type" => "Image"},
812 "name" => ":" <> name <> ":",
813 "type" => "Emoji",
814 "updated" => "1970-01-01T00:00:00Z",
815 "id" => url
816 }
817 end)
818
819 object
820 |> Map.put("tag", tags ++ out)
821 end
822
823 def set_conversation(object) do
824 Map.put(object, "conversation", object["context"])
825 end
826
827 def set_sensitive(object) do
828 tags = object["tag"] || []
829 Map.put(object, "sensitive", "nsfw" in tags)
830 end
831
832 def add_attributed_to(object) do
833 attributedTo = object["attributedTo"] || object["actor"]
834
835 object
836 |> Map.put("attributedTo", attributedTo)
837 end
838
839 def add_likes(%{"id" => id, "like_count" => likes} = object) do
840 likes = %{
841 "id" => "#{id}/likes",
842 "first" => "#{id}/likes?page=1",
843 "type" => "OrderedCollection",
844 "totalItems" => likes
845 }
846
847 object
848 |> Map.put("likes", likes)
849 end
850
851 def add_likes(object) do
852 object
853 end
854
855 def prepare_attachments(object) do
856 attachments =
857 (object["attachment"] || [])
858 |> Enum.map(fn data ->
859 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
860 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
861 end)
862
863 object
864 |> Map.put("attachment", attachments)
865 end
866
867 defp strip_internal_fields(object) do
868 object
869 |> Map.drop([
870 "like_count",
871 "announcements",
872 "announcement_count",
873 "emoji",
874 "context_id",
875 "deleted_activity_id"
876 ])
877 end
878
879 defp strip_internal_tags(%{"tag" => tags} = object) do
880 tags =
881 tags
882 |> Enum.filter(fn x -> is_map(x) end)
883
884 object
885 |> Map.put("tag", tags)
886 end
887
888 defp strip_internal_tags(object), do: object
889
890 defp user_upgrade_task(user) do
891 old_follower_address = User.ap_followers(user)
892
893 q =
894 from(
895 u in User,
896 where: ^old_follower_address in u.following,
897 update: [
898 set: [
899 following:
900 fragment(
901 "array_replace(?,?,?)",
902 u.following,
903 ^old_follower_address,
904 ^user.follower_address
905 )
906 ]
907 ]
908 )
909
910 Repo.update_all(q, [])
911
912 maybe_retire_websub(user.ap_id)
913
914 q =
915 from(
916 a in Activity,
917 where: ^old_follower_address in a.recipients,
918 update: [
919 set: [
920 recipients:
921 fragment(
922 "array_replace(?,?,?)",
923 a.recipients,
924 ^old_follower_address,
925 ^user.follower_address
926 )
927 ]
928 ]
929 )
930
931 Repo.update_all(q, [])
932 end
933
934 def upgrade_user_from_ap_id(ap_id, async \\ true) do
935 with %User{local: false} = user <- User.get_by_ap_id(ap_id),
936 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do
937 already_ap = User.ap_enabled?(user)
938
939 {:ok, user} =
940 User.upgrade_changeset(user, data)
941 |> Repo.update()
942
943 if !already_ap do
944 # This could potentially take a long time, do it in the background
945 if async do
946 Task.start(fn ->
947 user_upgrade_task(user)
948 end)
949 else
950 user_upgrade_task(user)
951 end
952 end
953
954 {:ok, user}
955 else
956 e -> e
957 end
958 end
959
960 def maybe_retire_websub(ap_id) do
961 # some sanity checks
962 if is_binary(ap_id) && String.length(ap_id) > 8 do
963 q =
964 from(
965 ws in Pleroma.Web.Websub.WebsubClientSubscription,
966 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
967 )
968
969 Repo.delete_all(q)
970 end
971 end
972
973 def maybe_fix_user_url(data) do
974 if is_map(data["url"]) do
975 Map.put(data, "url", data["url"]["href"])
976 else
977 data
978 end
979 end
980
981 def maybe_fix_user_object(data) do
982 data
983 |> maybe_fix_user_url
984 end
985 end