Merge branch 'bugfix/delete-activity-audience' of git.pleroma.social:pleroma/pleroma...
[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.User
11 alias Pleroma.Object
12 alias Pleroma.Repo
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 |> maybe_fix_object_url
740 |> Map.merge(Utils.make_json_ld_header())
741
742 {:ok, data}
743 end
744
745 def maybe_fix_object_url(data) do
746 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
747 case fetch_obj_helper(data["object"]) do
748 {:ok, relative_object} ->
749 if relative_object.data["external_url"] do
750 _data =
751 data
752 |> Map.put("object", relative_object.data["external_url"])
753 else
754 data
755 end
756
757 e ->
758 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
759 data
760 end
761 else
762 data
763 end
764 end
765
766 def add_hashtags(object) do
767 tags =
768 (object["tag"] || [])
769 |> Enum.map(fn
770 # Expand internal representation tags into AS2 tags.
771 tag when is_binary(tag) ->
772 %{
773 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
774 "name" => "##{tag}",
775 "type" => "Hashtag"
776 }
777
778 # Do not process tags which are already AS2 tag objects.
779 tag when is_map(tag) ->
780 tag
781 end)
782
783 object
784 |> Map.put("tag", tags)
785 end
786
787 def add_mention_tags(object) do
788 mentions =
789 object
790 |> Utils.get_notified_from_object()
791 |> Enum.map(fn user ->
792 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
793 end)
794
795 tags = object["tag"] || []
796
797 object
798 |> Map.put("tag", tags ++ mentions)
799 end
800
801 # TODO: we should probably send mtime instead of unix epoch time for updated
802 def add_emoji_tags(object) do
803 tags = object["tag"] || []
804 emoji = object["emoji"] || []
805
806 out =
807 emoji
808 |> Enum.map(fn {name, url} ->
809 %{
810 "icon" => %{"url" => url, "type" => "Image"},
811 "name" => ":" <> name <> ":",
812 "type" => "Emoji",
813 "updated" => "1970-01-01T00:00:00Z",
814 "id" => url
815 }
816 end)
817
818 object
819 |> Map.put("tag", tags ++ out)
820 end
821
822 def set_conversation(object) do
823 Map.put(object, "conversation", object["context"])
824 end
825
826 def set_sensitive(object) do
827 tags = object["tag"] || []
828 Map.put(object, "sensitive", "nsfw" in tags)
829 end
830
831 def add_attributed_to(object) do
832 attributedTo = object["attributedTo"] || object["actor"]
833
834 object
835 |> Map.put("attributedTo", attributedTo)
836 end
837
838 def add_likes(%{"id" => id, "like_count" => likes} = object) do
839 likes = %{
840 "id" => "#{id}/likes",
841 "first" => "#{id}/likes?page=1",
842 "type" => "OrderedCollection",
843 "totalItems" => likes
844 }
845
846 object
847 |> Map.put("likes", likes)
848 end
849
850 def add_likes(object) do
851 object
852 end
853
854 def prepare_attachments(object) do
855 attachments =
856 (object["attachment"] || [])
857 |> Enum.map(fn data ->
858 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
859 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
860 end)
861
862 object
863 |> Map.put("attachment", attachments)
864 end
865
866 defp strip_internal_fields(object) do
867 object
868 |> Map.drop([
869 "like_count",
870 "announcements",
871 "announcement_count",
872 "emoji",
873 "context_id"
874 ])
875 end
876
877 defp strip_internal_tags(%{"tag" => tags} = object) do
878 tags =
879 tags
880 |> Enum.filter(fn x -> is_map(x) end)
881
882 object
883 |> Map.put("tag", tags)
884 end
885
886 defp strip_internal_tags(object), do: object
887
888 defp user_upgrade_task(user) do
889 old_follower_address = User.ap_followers(user)
890
891 q =
892 from(
893 u in User,
894 where: ^old_follower_address in u.following,
895 update: [
896 set: [
897 following:
898 fragment(
899 "array_replace(?,?,?)",
900 u.following,
901 ^old_follower_address,
902 ^user.follower_address
903 )
904 ]
905 ]
906 )
907
908 Repo.update_all(q, [])
909
910 maybe_retire_websub(user.ap_id)
911
912 q =
913 from(
914 a in Activity,
915 where: ^old_follower_address in a.recipients,
916 update: [
917 set: [
918 recipients:
919 fragment(
920 "array_replace(?,?,?)",
921 a.recipients,
922 ^old_follower_address,
923 ^user.follower_address
924 )
925 ]
926 ]
927 )
928
929 Repo.update_all(q, [])
930 end
931
932 def upgrade_user_from_ap_id(ap_id, async \\ true) do
933 with %User{local: false} = user <- User.get_by_ap_id(ap_id),
934 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do
935 already_ap = User.ap_enabled?(user)
936
937 {:ok, user} =
938 User.upgrade_changeset(user, data)
939 |> Repo.update()
940
941 if !already_ap do
942 # This could potentially take a long time, do it in the background
943 if async do
944 Task.start(fn ->
945 user_upgrade_task(user)
946 end)
947 else
948 user_upgrade_task(user)
949 end
950 end
951
952 {:ok, user}
953 else
954 e -> e
955 end
956 end
957
958 def maybe_retire_websub(ap_id) do
959 # some sanity checks
960 if is_binary(ap_id) && String.length(ap_id) > 8 do
961 q =
962 from(
963 ws in Pleroma.Web.Websub.WebsubClientSubscription,
964 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
965 )
966
967 Repo.delete_all(q)
968 end
969 end
970
971 def maybe_fix_user_url(data) do
972 if is_map(data["url"]) do
973 Map.put(data, "url", data["url"]["href"])
974 else
975 data
976 end
977 end
978
979 def maybe_fix_user_object(data) do
980 data
981 |> maybe_fix_user_url
982 end
983 end