7f3d8fd4b842c0ce3805f049fb8204fd7a4d38d3
[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 # if as:Public is addressed, then make sure the followers collection is also addressed
132 # so that the activities will be delivered to local users.
133 def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
134 recipients = to ++ cc
135
136 if followers_collection not in recipients do
137 cond do
138 "https://www.w3.org/ns/activitystreams#Public" in cc ->
139 to = to ++ [followers_collection]
140 Map.put(object, "to", to)
141
142 "https://www.w3.org/ns/activitystreams#Public" in to ->
143 cc = cc ++ [followers_collection]
144 Map.put(object, "cc", cc)
145
146 true ->
147 object
148 end
149 else
150 object
151 end
152 end
153
154 def fix_implicit_addressing(object, _), do: object
155
156 def fix_addressing(object) do
157 %User{} = user = User.get_cached_by_ap_id(object["actor"])
158 followers_collection = User.ap_followers(user)
159
160 object
161 |> fix_addressing_list("to")
162 |> fix_addressing_list("cc")
163 |> fix_addressing_list("bto")
164 |> fix_addressing_list("bcc")
165 |> fix_explicit_addressing
166 |> fix_implicit_addressing(followers_collection)
167 end
168
169 def fix_actor(%{"attributedTo" => actor} = object) do
170 object
171 |> Map.put("actor", get_actor(%{"actor" => actor}))
172 end
173
174 # Check for standardisation
175 # This is what Peertube does
176 # curl -H 'Accept: application/activity+json' $likes | jq .totalItems
177 # Prismo returns only an integer (count) as "likes"
178 def fix_likes(%{"likes" => likes} = object) when not is_map(likes) do
179 object
180 |> Map.put("likes", [])
181 |> Map.put("like_count", 0)
182 end
183
184 def fix_likes(object) do
185 object
186 end
187
188 def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object)
189 when not is_nil(in_reply_to) do
190 in_reply_to_id =
191 cond do
192 is_bitstring(in_reply_to) ->
193 in_reply_to
194
195 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
196 in_reply_to["id"]
197
198 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
199 Enum.at(in_reply_to, 0)
200
201 # Maybe I should output an error too?
202 true ->
203 ""
204 end
205
206 case fetch_obj_helper(in_reply_to_id) do
207 {:ok, replied_object} ->
208 with %Activity{} = activity <-
209 Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
210 object
211 |> Map.put("inReplyTo", replied_object.data["id"])
212 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
213 |> Map.put("inReplyToStatusId", activity.id)
214 |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
215 |> Map.put("context", replied_object.data["context"] || object["conversation"])
216 else
217 e ->
218 Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
219 object
220 end
221
222 e ->
223 Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
224 object
225 end
226 end
227
228 def fix_in_reply_to(object), do: object
229
230 def fix_context(object) do
231 context = object["context"] || object["conversation"] || Utils.generate_context_id()
232
233 object
234 |> Map.put("context", context)
235 |> Map.put("conversation", context)
236 end
237
238 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
239 attachments =
240 attachment
241 |> Enum.map(fn data ->
242 media_type = data["mediaType"] || data["mimeType"]
243 href = data["url"] || data["href"]
244
245 url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]
246
247 data
248 |> Map.put("mediaType", media_type)
249 |> Map.put("url", url)
250 end)
251
252 object
253 |> Map.put("attachment", attachments)
254 end
255
256 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
257 Map.put(object, "attachment", [attachment])
258 |> fix_attachments()
259 end
260
261 def fix_attachments(object), do: object
262
263 def fix_url(%{"url" => url} = object) when is_map(url) do
264 object
265 |> Map.put("url", url["href"])
266 end
267
268 def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
269 first_element = Enum.at(url, 0)
270
271 link_element =
272 url
273 |> Enum.filter(fn x -> is_map(x) end)
274 |> Enum.filter(fn x -> x["mimeType"] == "text/html" end)
275 |> Enum.at(0)
276
277 object
278 |> Map.put("attachment", [first_element])
279 |> Map.put("url", link_element["href"])
280 end
281
282 def fix_url(%{"type" => object_type, "url" => url} = object)
283 when object_type != "Video" and is_list(url) do
284 first_element = Enum.at(url, 0)
285
286 url_string =
287 cond do
288 is_bitstring(first_element) -> first_element
289 is_map(first_element) -> first_element["href"] || ""
290 true -> ""
291 end
292
293 object
294 |> Map.put("url", url_string)
295 end
296
297 def fix_url(object), do: object
298
299 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
300 emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
301
302 emoji =
303 emoji
304 |> Enum.reduce(%{}, fn data, mapping ->
305 name = String.trim(data["name"], ":")
306
307 mapping |> Map.put(name, data["icon"]["url"])
308 end)
309
310 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
311 emoji = Map.merge(object["emoji"] || %{}, emoji)
312
313 object
314 |> Map.put("emoji", emoji)
315 end
316
317 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
318 name = String.trim(tag["name"], ":")
319 emoji = %{name => tag["icon"]["url"]}
320
321 object
322 |> Map.put("emoji", emoji)
323 end
324
325 def fix_emoji(object), do: object
326
327 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
328 tags =
329 tag
330 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
331 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
332
333 combined = tag ++ tags
334
335 object
336 |> Map.put("tag", combined)
337 end
338
339 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
340 combined = [tag, String.slice(hashtag, 1..-1)]
341
342 object
343 |> Map.put("tag", combined)
344 end
345
346 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
347
348 def fix_tag(object), do: object
349
350 # content map usually only has one language so this will do for now.
351 def fix_content_map(%{"contentMap" => content_map} = object) do
352 content_groups = Map.to_list(content_map)
353 {_, content} = Enum.at(content_groups, 0)
354
355 object
356 |> Map.put("content", content)
357 end
358
359 def fix_content_map(object), do: object
360
361 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
362 with true <- id =~ "follows",
363 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
364 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
365 {:ok, activity}
366 else
367 _ -> {:error, nil}
368 end
369 end
370
371 defp mastodon_follow_hack(_, _), do: {:error, nil}
372
373 defp get_follow_activity(follow_object, followed) do
374 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
375 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
376 {:ok, activity}
377 else
378 # Can't find the activity. This might a Mastodon 2.3 "Accept"
379 {:activity, nil} ->
380 mastodon_follow_hack(follow_object, followed)
381
382 _ ->
383 {:error, nil}
384 end
385 end
386
387 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
388 # with nil ID.
389 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data) do
390 with context <- data["context"] || Utils.generate_context_id(),
391 content <- data["content"] || "",
392 %User{} = actor <- User.get_cached_by_ap_id(actor),
393
394 # Reduce the object list to find the reported user.
395 %User{} = account <-
396 Enum.reduce_while(objects, nil, fn ap_id, _ ->
397 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
398 {:halt, user}
399 else
400 _ -> {:cont, nil}
401 end
402 end),
403
404 # Remove the reported user from the object list.
405 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
406 params = %{
407 actor: actor,
408 context: context,
409 account: account,
410 statuses: statuses,
411 content: content,
412 additional: %{
413 "cc" => [account.ap_id]
414 }
415 }
416
417 ActivityPub.flag(params)
418 end
419 end
420
421 # disallow objects with bogus IDs
422 def handle_incoming(%{"id" => nil}), do: :error
423 def handle_incoming(%{"id" => ""}), do: :error
424 # length of https:// = 8, should validate better, but good enough for now.
425 def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8), do: :error
426
427 # TODO: validate those with a Ecto scheme
428 # - tags
429 # - emoji
430 def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
431 when objtype in ["Article", "Note", "Video", "Page"] do
432 actor = get_actor(data)
433
434 data =
435 Map.put(data, "actor", actor)
436 |> fix_addressing
437
438 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
439 %User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do
440 object = fix_object(data["object"])
441
442 params = %{
443 to: data["to"],
444 object: object,
445 actor: user,
446 context: object["conversation"],
447 local: false,
448 published: data["published"],
449 additional:
450 Map.take(data, [
451 "cc",
452 "directMessage",
453 "id"
454 ])
455 }
456
457 ActivityPub.create(params)
458 else
459 %Activity{} = activity -> {:ok, activity}
460 _e -> :error
461 end
462 end
463
464 def handle_incoming(
465 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
466 ) do
467 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
468 %User{} = follower <- User.get_or_fetch_by_ap_id(follower),
469 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
470 if not User.locked?(followed) do
471 ActivityPub.accept(%{
472 to: [follower.ap_id],
473 actor: followed,
474 object: data,
475 local: true
476 })
477
478 User.follow(follower, followed)
479 end
480
481 {:ok, activity}
482 else
483 _e -> :error
484 end
485 end
486
487 def handle_incoming(
488 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data
489 ) do
490 with actor <- get_actor(data),
491 %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
492 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
493 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
494 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
495 {:ok, activity} <-
496 ActivityPub.accept(%{
497 to: follow_activity.data["to"],
498 type: "Accept",
499 actor: followed,
500 object: follow_activity.data["id"],
501 local: false
502 }) do
503 if not User.following?(follower, followed) do
504 {:ok, _follower} = User.follow(follower, followed)
505 end
506
507 {:ok, activity}
508 else
509 _e -> :error
510 end
511 end
512
513 def handle_incoming(
514 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
515 ) do
516 with actor <- get_actor(data),
517 %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
518 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
519 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
520 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
521 {:ok, activity} <-
522 ActivityPub.reject(%{
523 to: follow_activity.data["to"],
524 type: "Reject",
525 actor: followed,
526 object: follow_activity.data["id"],
527 local: false
528 }) do
529 User.unfollow(follower, followed)
530
531 {:ok, activity}
532 else
533 _e -> :error
534 end
535 end
536
537 def handle_incoming(
538 %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data
539 ) do
540 with actor <- get_actor(data),
541 %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
542 {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
543 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
544 {:ok, activity}
545 else
546 _e -> :error
547 end
548 end
549
550 def handle_incoming(
551 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
552 ) do
553 with actor <- get_actor(data),
554 %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
555 {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
556 public <- Visibility.is_public?(data),
557 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
558 {:ok, activity}
559 else
560 _e -> :error
561 end
562 end
563
564 def handle_incoming(
565 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
566 data
567 )
568 when object_type in ["Person", "Application", "Service", "Organization"] do
569 with %User{ap_id: ^actor_id} = actor <- User.get_by_ap_id(object["id"]) do
570 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
571
572 banner = new_user_data[:info]["banner"]
573 locked = new_user_data[:info]["locked"] || false
574
575 update_data =
576 new_user_data
577 |> Map.take([:name, :bio, :avatar])
578 |> Map.put(:info, %{"banner" => banner, "locked" => locked})
579
580 actor
581 |> User.upgrade_changeset(update_data)
582 |> User.update_and_set_cache()
583
584 ActivityPub.update(%{
585 local: false,
586 to: data["to"] || [],
587 cc: data["cc"] || [],
588 object: object,
589 actor: actor_id
590 })
591 else
592 e ->
593 Logger.error(e)
594 :error
595 end
596 end
597
598 # TODO: We presently assume that any actor on the same origin domain as the object being
599 # deleted has the rights to delete that object. A better way to validate whether or not
600 # the object should be deleted is to refetch the object URI, which should return either
601 # an error or a tombstone. This would allow us to verify that a deletion actually took
602 # place.
603 def handle_incoming(
604 %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data
605 ) do
606 object_id = Utils.get_ap_id(object_id)
607
608 with actor <- get_actor(data),
609 %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
610 {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
611 :ok <- contain_origin(actor.ap_id, object.data),
612 {:ok, activity} <- ActivityPub.delete(object, false) do
613 {:ok, activity}
614 else
615 _e -> :error
616 end
617 end
618
619 def handle_incoming(
620 %{
621 "type" => "Undo",
622 "object" => %{"type" => "Announce", "object" => object_id},
623 "actor" => _actor,
624 "id" => id
625 } = data
626 ) do
627 with actor <- get_actor(data),
628 %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
629 {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
630 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
631 {:ok, activity}
632 else
633 _e -> :error
634 end
635 end
636
637 def handle_incoming(
638 %{
639 "type" => "Undo",
640 "object" => %{"type" => "Follow", "object" => followed},
641 "actor" => follower,
642 "id" => id
643 } = _data
644 ) do
645 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
646 %User{} = follower <- User.get_or_fetch_by_ap_id(follower),
647 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
648 User.unfollow(follower, followed)
649 {:ok, activity}
650 else
651 _e -> :error
652 end
653 end
654
655 def handle_incoming(
656 %{
657 "type" => "Undo",
658 "object" => %{"type" => "Block", "object" => blocked},
659 "actor" => blocker,
660 "id" => id
661 } = _data
662 ) do
663 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
664 %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
665 %User{} = blocker <- User.get_or_fetch_by_ap_id(blocker),
666 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
667 User.unblock(blocker, blocked)
668 {:ok, activity}
669 else
670 _e -> :error
671 end
672 end
673
674 def handle_incoming(
675 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data
676 ) do
677 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
678 %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
679 %User{} = blocker = User.get_or_fetch_by_ap_id(blocker),
680 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
681 User.unfollow(blocker, blocked)
682 User.block(blocker, blocked)
683 {:ok, activity}
684 else
685 _e -> :error
686 end
687 end
688
689 def handle_incoming(
690 %{
691 "type" => "Undo",
692 "object" => %{"type" => "Like", "object" => object_id},
693 "actor" => _actor,
694 "id" => id
695 } = data
696 ) do
697 with actor <- get_actor(data),
698 %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
699 {:ok, object} <- get_obj_helper(object_id) || fetch_obj_helper(object_id),
700 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
701 {:ok, activity}
702 else
703 _e -> :error
704 end
705 end
706
707 def handle_incoming(_), do: :error
708
709 def fetch_obj_helper(id) when is_bitstring(id), do: ActivityPub.fetch_object_from_id(id)
710 def fetch_obj_helper(obj) when is_map(obj), do: ActivityPub.fetch_object_from_id(obj["id"])
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} = data) do
749 object =
750 object
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 fetch_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 # TODO: we should probably send mtime instead of unix epoch time for updated
866 def add_emoji_tags(object) do
867 tags = object["tag"] || []
868 emoji = object["emoji"] || []
869
870 out =
871 emoji
872 |> Enum.map(fn {name, url} ->
873 %{
874 "icon" => %{"url" => url, "type" => "Image"},
875 "name" => ":" <> name <> ":",
876 "type" => "Emoji",
877 "updated" => "1970-01-01T00:00:00Z",
878 "id" => url
879 }
880 end)
881
882 object
883 |> Map.put("tag", tags ++ out)
884 end
885
886 def set_conversation(object) do
887 Map.put(object, "conversation", object["context"])
888 end
889
890 def set_sensitive(object) do
891 tags = object["tag"] || []
892 Map.put(object, "sensitive", "nsfw" in tags)
893 end
894
895 def add_attributed_to(object) do
896 attributed_to = object["attributedTo"] || object["actor"]
897
898 object
899 |> Map.put("attributedTo", attributed_to)
900 end
901
902 def add_likes(%{"id" => id, "like_count" => likes} = object) do
903 likes = %{
904 "id" => "#{id}/likes",
905 "first" => "#{id}/likes?page=1",
906 "type" => "OrderedCollection",
907 "totalItems" => likes
908 }
909
910 object
911 |> Map.put("likes", likes)
912 end
913
914 def add_likes(object) do
915 object
916 end
917
918 def prepare_attachments(object) do
919 attachments =
920 (object["attachment"] || [])
921 |> Enum.map(fn data ->
922 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
923 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
924 end)
925
926 object
927 |> Map.put("attachment", attachments)
928 end
929
930 defp strip_internal_fields(object) do
931 object
932 |> Map.drop([
933 "like_count",
934 "announcements",
935 "announcement_count",
936 "emoji",
937 "context_id",
938 "deleted_activity_id"
939 ])
940 end
941
942 defp strip_internal_tags(%{"tag" => tags} = object) do
943 tags =
944 tags
945 |> Enum.filter(fn x -> is_map(x) end)
946
947 object
948 |> Map.put("tag", tags)
949 end
950
951 defp strip_internal_tags(object), do: object
952
953 defp user_upgrade_task(user) do
954 old_follower_address = User.ap_followers(user)
955
956 q =
957 from(
958 u in User,
959 where: ^old_follower_address in u.following,
960 update: [
961 set: [
962 following:
963 fragment(
964 "array_replace(?,?,?)",
965 u.following,
966 ^old_follower_address,
967 ^user.follower_address
968 )
969 ]
970 ]
971 )
972
973 Repo.update_all(q, [])
974
975 maybe_retire_websub(user.ap_id)
976
977 q =
978 from(
979 a in Activity,
980 where: ^old_follower_address in a.recipients,
981 update: [
982 set: [
983 recipients:
984 fragment(
985 "array_replace(?,?,?)",
986 a.recipients,
987 ^old_follower_address,
988 ^user.follower_address
989 )
990 ]
991 ]
992 )
993
994 Repo.update_all(q, [])
995 end
996
997 def upgrade_user_from_ap_id(ap_id, async \\ true) do
998 with %User{local: false} = user <- User.get_by_ap_id(ap_id),
999 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do
1000 already_ap = User.ap_enabled?(user)
1001
1002 {:ok, user} =
1003 User.upgrade_changeset(user, data)
1004 |> Repo.update()
1005
1006 if !already_ap do
1007 # This could potentially take a long time, do it in the background
1008 if async do
1009 Task.start(fn ->
1010 user_upgrade_task(user)
1011 end)
1012 else
1013 user_upgrade_task(user)
1014 end
1015 end
1016
1017 {:ok, user}
1018 else
1019 e -> e
1020 end
1021 end
1022
1023 def maybe_retire_websub(ap_id) do
1024 # some sanity checks
1025 if is_binary(ap_id) && String.length(ap_id) > 8 do
1026 q =
1027 from(
1028 ws in Pleroma.Web.Websub.WebsubClientSubscription,
1029 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
1030 )
1031
1032 Repo.delete_all(q)
1033 end
1034 end
1035
1036 def maybe_fix_user_url(data) do
1037 if is_map(data["url"]) do
1038 Map.put(data, "url", data["url"]["href"])
1039 else
1040 data
1041 end
1042 end
1043
1044 def maybe_fix_user_object(data) do
1045 data
1046 |> maybe_fix_user_url
1047 end
1048 end