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