transmogrifier: reject activities lacking a valid ID
[akkoma] / lib / pleroma / web / activity_pub / transmogrifier.ex
1 defmodule Pleroma.Web.ActivityPub.Transmogrifier do
2 @moduledoc """
3 A module to handle coding from internal to wire ActivityPub and back.
4 """
5 alias Pleroma.User
6 alias Pleroma.Object
7 alias Pleroma.Activity
8 alias Pleroma.Repo
9 alias Pleroma.Web.ActivityPub.ActivityPub
10 alias Pleroma.Web.ActivityPub.Utils
11
12 import Ecto.Query
13
14 require Logger
15
16 def get_actor(%{"actor" => actor}) when is_binary(actor) do
17 actor
18 end
19
20 def get_actor(%{"actor" => actor}) when is_list(actor) do
21 if is_binary(Enum.at(actor, 0)) do
22 Enum.at(actor, 0)
23 else
24 Enum.find(actor, fn %{"type" => type} -> type == "Person" end)
25 |> Map.get("id")
26 end
27 end
28
29 def get_actor(%{"actor" => actor}) when is_map(actor) do
30 actor["id"]
31 end
32
33 @doc """
34 Modifies an incoming AP object (mastodon format) to our internal format.
35 """
36 def fix_object(object) do
37 object
38 |> fix_actor
39 |> fix_attachments
40 |> fix_context
41 |> fix_in_reply_to
42 |> fix_emoji
43 |> fix_tag
44 |> fix_content_map
45 |> fix_likes
46 |> fix_addressing
47 end
48
49 def fix_addressing_list(map, field) do
50 if is_binary(map[field]) do
51 map
52 |> Map.put(field, [map[field]])
53 else
54 map
55 end
56 end
57
58 def fix_addressing(map) do
59 map
60 |> fix_addressing_list("to")
61 |> fix_addressing_list("cc")
62 |> fix_addressing_list("bto")
63 |> fix_addressing_list("bcc")
64 end
65
66 def fix_actor(%{"attributedTo" => actor} = object) do
67 object
68 |> Map.put("actor", get_actor(%{"actor" => actor}))
69 end
70
71 def fix_likes(%{"likes" => likes} = object)
72 when is_bitstring(likes) do
73 # Check for standardisation
74 # This is what Peertube does
75 # curl -H 'Accept: application/activity+json' $likes | jq .totalItems
76 object
77 |> Map.put("likes", [])
78 |> Map.put("like_count", 0)
79 end
80
81 def fix_likes(object) do
82 object
83 end
84
85 def fix_in_reply_to(%{"inReplyTo" => in_reply_to_id} = object)
86 when not is_nil(in_reply_to_id) do
87 case ActivityPub.fetch_object_from_id(in_reply_to_id) do
88 {:ok, replied_object} ->
89 with %Activity{} = activity <-
90 Activity.get_create_activity_by_object_ap_id(replied_object.data["id"]) do
91 object
92 |> Map.put("inReplyTo", replied_object.data["id"])
93 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
94 |> Map.put("inReplyToStatusId", activity.id)
95 |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
96 |> Map.put("context", replied_object.data["context"] || object["conversation"])
97 else
98 e ->
99 Logger.error("Couldn't fetch #{object["inReplyTo"]} #{inspect(e)}")
100 object
101 end
102
103 e ->
104 Logger.error("Couldn't fetch #{object["inReplyTo"]} #{inspect(e)}")
105 object
106 end
107 end
108
109 def fix_in_reply_to(object), do: object
110
111 def fix_context(object) do
112 context = object["context"] || object["conversation"] || Utils.generate_context_id()
113
114 object
115 |> Map.put("context", context)
116 |> Map.put("conversation", context)
117 end
118
119 def fix_attachments(object) do
120 attachments =
121 (object["attachment"] || [])
122 |> Enum.map(fn data ->
123 url = [%{"type" => "Link", "mediaType" => data["mediaType"], "href" => data["url"]}]
124 Map.put(data, "url", url)
125 end)
126
127 object
128 |> Map.put("attachment", attachments)
129 end
130
131 def fix_emoji(object) do
132 tags = object["tag"] || []
133 emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
134
135 emoji =
136 emoji
137 |> Enum.reduce(%{}, fn data, mapping ->
138 name = data["name"]
139
140 name =
141 if String.starts_with?(name, ":") do
142 name |> String.slice(1..-2)
143 else
144 name
145 end
146
147 mapping |> Map.put(name, data["icon"]["url"])
148 end)
149
150 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
151 emoji = Map.merge(object["emoji"] || %{}, emoji)
152
153 object
154 |> Map.put("emoji", emoji)
155 end
156
157 def fix_tag(object) do
158 tags =
159 (object["tag"] || [])
160 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
161 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
162
163 combined = (object["tag"] || []) ++ tags
164
165 object
166 |> Map.put("tag", combined)
167 end
168
169 # content map usually only has one language so this will do for now.
170 def fix_content_map(%{"contentMap" => content_map} = object) do
171 content_groups = Map.to_list(content_map)
172 {_, content} = Enum.at(content_groups, 0)
173
174 object
175 |> Map.put("content", content)
176 end
177
178 def fix_content_map(object), do: object
179
180 # disallow objects with bogus IDs
181 def handle_incoming(%{"id" => nil}), do: :error
182 def handle_incoming(%{"id" => ""}), do: :error
183 # length of https:// = 8, should validate better, but good enough for now.
184 def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8), do: :error
185
186 # TODO: validate those with a Ecto scheme
187 # - tags
188 # - emoji
189 def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
190 when objtype in ["Article", "Note", "Video"] do
191 actor = get_actor(data)
192
193 data =
194 Map.put(data, "actor", actor)
195 |> fix_addressing
196
197 with nil <- Activity.get_create_activity_by_object_ap_id(object["id"]),
198 %User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do
199 object = fix_object(data["object"])
200
201 params = %{
202 to: data["to"],
203 object: object,
204 actor: user,
205 context: object["conversation"],
206 local: false,
207 published: data["published"],
208 additional:
209 Map.take(data, [
210 "cc",
211 "id"
212 ])
213 }
214
215 ActivityPub.create(params)
216 else
217 %Activity{} = activity -> {:ok, activity}
218 _e -> :error
219 end
220 end
221
222 def handle_incoming(
223 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
224 ) do
225 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
226 %User{} = follower <- User.get_or_fetch_by_ap_id(follower),
227 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
228 if not User.locked?(followed) do
229 ActivityPub.accept(%{
230 to: [follower.ap_id],
231 actor: followed.ap_id,
232 object: data,
233 local: true
234 })
235
236 User.follow(follower, followed)
237 end
238
239 {:ok, activity}
240 else
241 _e -> :error
242 end
243 end
244
245 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
246 with true <- id =~ "follows",
247 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
248 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
249 {:ok, activity}
250 else
251 _ -> {:error, nil}
252 end
253 end
254
255 defp mastodon_follow_hack(_), do: {:error, nil}
256
257 defp get_follow_activity(follow_object, followed) do
258 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
259 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
260 {:ok, activity}
261 else
262 # Can't find the activity. This might a Mastodon 2.3 "Accept"
263 {:activity, nil} ->
264 mastodon_follow_hack(follow_object, followed)
265
266 _ ->
267 {:error, nil}
268 end
269 end
270
271 def handle_incoming(
272 %{"type" => "Accept", "object" => follow_object, "actor" => actor, "id" => id} = data
273 ) do
274 with %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
275 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
276 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
277 {:ok, activity} <-
278 ActivityPub.accept(%{
279 to: follow_activity.data["to"],
280 type: "Accept",
281 actor: followed.ap_id,
282 object: follow_activity.data["id"],
283 local: false
284 }) do
285 if not User.following?(follower, followed) do
286 {:ok, follower} = User.follow(follower, followed)
287 end
288
289 {:ok, activity}
290 else
291 _e -> :error
292 end
293 end
294
295 def handle_incoming(
296 %{"type" => "Reject", "object" => follow_object, "actor" => actor, "id" => id} = data
297 ) do
298 with %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
299 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
300 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
301 {:ok, activity} <-
302 ActivityPub.accept(%{
303 to: follow_activity.data["to"],
304 type: "Accept",
305 actor: followed.ap_id,
306 object: follow_activity.data["id"],
307 local: false
308 }) do
309 User.unfollow(follower, followed)
310
311 {:ok, activity}
312 else
313 _e -> :error
314 end
315 end
316
317 def handle_incoming(
318 %{"type" => "Like", "object" => object_id, "actor" => actor, "id" => id} = _data
319 ) do
320 with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
321 {:ok, object} <-
322 get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
323 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
324 {:ok, activity}
325 else
326 _e -> :error
327 end
328 end
329
330 def handle_incoming(
331 %{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = _data
332 ) do
333 with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
334 {:ok, object} <-
335 get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
336 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false) do
337 {:ok, activity}
338 else
339 _e -> :error
340 end
341 end
342
343 def handle_incoming(
344 %{"type" => "Update", "object" => %{"type" => "Person"} = object, "actor" => actor_id} =
345 data
346 ) do
347 with %User{ap_id: ^actor_id} = actor <- User.get_by_ap_id(object["id"]) do
348 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
349
350 banner = new_user_data[:info]["banner"]
351 locked = new_user_data[:info]["locked"] || false
352
353 update_data =
354 new_user_data
355 |> Map.take([:name, :bio, :avatar])
356 |> Map.put(:info, Map.merge(actor.info, %{"banner" => banner, "locked" => locked}))
357
358 actor
359 |> User.upgrade_changeset(update_data)
360 |> User.update_and_set_cache()
361
362 ActivityPub.update(%{
363 local: false,
364 to: data["to"] || [],
365 cc: data["cc"] || [],
366 object: object,
367 actor: actor_id
368 })
369 else
370 e ->
371 Logger.error(e)
372 :error
373 end
374 end
375
376 # TODO: Make secure.
377 def handle_incoming(
378 %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = _data
379 ) do
380 object_id = Utils.get_ap_id(object_id)
381
382 with %User{} = _actor <- User.get_or_fetch_by_ap_id(actor),
383 {:ok, object} <-
384 get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
385 {:ok, activity} <- ActivityPub.delete(object, false) do
386 {:ok, activity}
387 else
388 _e -> :error
389 end
390 end
391
392 def handle_incoming(
393 %{
394 "type" => "Undo",
395 "object" => %{"type" => "Announce", "object" => object_id},
396 "actor" => actor,
397 "id" => id
398 } = _data
399 ) do
400 with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
401 {:ok, object} <-
402 get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
403 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
404 {:ok, activity}
405 else
406 _e -> :error
407 end
408 end
409
410 def handle_incoming(
411 %{
412 "type" => "Undo",
413 "object" => %{"type" => "Follow", "object" => followed},
414 "actor" => follower,
415 "id" => id
416 } = _data
417 ) do
418 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
419 %User{} = follower <- User.get_or_fetch_by_ap_id(follower),
420 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
421 User.unfollow(follower, followed)
422 {:ok, activity}
423 else
424 e -> :error
425 end
426 end
427
428 @ap_config Application.get_env(:pleroma, :activitypub)
429 @accept_blocks Keyword.get(@ap_config, :accept_blocks)
430
431 def handle_incoming(
432 %{
433 "type" => "Undo",
434 "object" => %{"type" => "Block", "object" => blocked},
435 "actor" => blocker,
436 "id" => id
437 } = _data
438 ) do
439 with true <- @accept_blocks,
440 %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
441 %User{} = blocker <- User.get_or_fetch_by_ap_id(blocker),
442 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
443 User.unblock(blocker, blocked)
444 {:ok, activity}
445 else
446 e -> :error
447 end
448 end
449
450 def handle_incoming(
451 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = data
452 ) do
453 with true <- @accept_blocks,
454 %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
455 %User{} = blocker = User.get_or_fetch_by_ap_id(blocker),
456 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
457 User.unfollow(blocker, blocked)
458 User.block(blocker, blocked)
459 {:ok, activity}
460 else
461 e -> :error
462 end
463 end
464
465 def handle_incoming(
466 %{
467 "type" => "Undo",
468 "object" => %{"type" => "Like", "object" => object_id},
469 "actor" => actor,
470 "id" => id
471 } = _data
472 ) do
473 with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
474 {:ok, object} <-
475 get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
476 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
477 {:ok, activity}
478 else
479 _e -> :error
480 end
481 end
482
483 def handle_incoming(_), do: :error
484
485 def get_obj_helper(id) do
486 if object = Object.normalize(id), do: {:ok, object}, else: nil
487 end
488
489 def set_reply_to_uri(%{"inReplyTo" => inReplyTo} = object) do
490 with false <- String.starts_with?(inReplyTo, "http"),
491 {:ok, %{data: replied_to_object}} <- get_obj_helper(inReplyTo) do
492 Map.put(object, "inReplyTo", replied_to_object["external_url"] || inReplyTo)
493 else
494 _e -> object
495 end
496 end
497
498 def set_reply_to_uri(obj), do: obj
499
500 # Prepares the object of an outgoing create activity.
501 def prepare_object(object) do
502 object
503 |> set_sensitive
504 |> add_hashtags
505 |> add_mention_tags
506 |> add_emoji_tags
507 |> add_attributed_to
508 |> prepare_attachments
509 |> set_conversation
510 |> set_reply_to_uri
511 end
512
513 # @doc
514 # """
515 # internal -> Mastodon
516 # """
517
518 def prepare_outgoing(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
519 object =
520 object
521 |> prepare_object
522
523 data =
524 data
525 |> Map.put("object", object)
526 |> Map.put("@context", "https://www.w3.org/ns/activitystreams")
527
528 {:ok, data}
529 end
530
531 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
532 # because of course it does.
533 def prepare_outgoing(%{"type" => "Accept"} = data) do
534 with follow_activity <- Activity.normalize(data["object"]) do
535 object = %{
536 "actor" => follow_activity.actor,
537 "object" => follow_activity.data["object"],
538 "id" => follow_activity.data["id"],
539 "type" => "Follow"
540 }
541
542 data =
543 data
544 |> Map.put("object", object)
545 |> Map.put("@context", "https://www.w3.org/ns/activitystreams")
546
547 {:ok, data}
548 end
549 end
550
551 def prepare_outgoing(%{"type" => "Reject"} = data) do
552 with follow_activity <- Activity.normalize(data["object"]) do
553 object = %{
554 "actor" => follow_activity.actor,
555 "object" => follow_activity.data["object"],
556 "id" => follow_activity.data["id"],
557 "type" => "Follow"
558 }
559
560 data =
561 data
562 |> Map.put("object", object)
563 |> Map.put("@context", "https://www.w3.org/ns/activitystreams")
564
565 {:ok, data}
566 end
567 end
568
569 def prepare_outgoing(%{"type" => _type} = data) do
570 data =
571 data
572 |> maybe_fix_object_url
573 |> Map.put("@context", "https://www.w3.org/ns/activitystreams")
574
575 {:ok, data}
576 end
577
578 def maybe_fix_object_url(data) do
579 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
580 case ActivityPub.fetch_object_from_id(data["object"]) do
581 {:ok, relative_object} ->
582 if relative_object.data["external_url"] do
583 _data =
584 data
585 |> Map.put("object", relative_object.data["external_url"])
586 else
587 data
588 end
589
590 e ->
591 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
592 data
593 end
594 else
595 data
596 end
597 end
598
599 def add_hashtags(object) do
600 tags =
601 (object["tag"] || [])
602 |> Enum.map(fn tag ->
603 %{
604 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
605 "name" => "##{tag}",
606 "type" => "Hashtag"
607 }
608 end)
609
610 object
611 |> Map.put("tag", tags)
612 end
613
614 def add_mention_tags(object) do
615 recipients = object["to"] ++ (object["cc"] || [])
616
617 mentions =
618 recipients
619 |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
620 |> Enum.filter(& &1)
621 |> Enum.map(fn user ->
622 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
623 end)
624
625 tags = object["tag"] || []
626
627 object
628 |> Map.put("tag", tags ++ mentions)
629 end
630
631 # TODO: we should probably send mtime instead of unix epoch time for updated
632 def add_emoji_tags(object) do
633 tags = object["tag"] || []
634 emoji = object["emoji"] || []
635
636 out =
637 emoji
638 |> Enum.map(fn {name, url} ->
639 %{
640 "icon" => %{"url" => url, "type" => "Image"},
641 "name" => ":" <> name <> ":",
642 "type" => "Emoji",
643 "updated" => "1970-01-01T00:00:00Z",
644 "id" => url
645 }
646 end)
647
648 object
649 |> Map.put("tag", tags ++ out)
650 end
651
652 def set_conversation(object) do
653 Map.put(object, "conversation", object["context"])
654 end
655
656 def set_sensitive(object) do
657 tags = object["tag"] || []
658 Map.put(object, "sensitive", "nsfw" in tags)
659 end
660
661 def add_attributed_to(object) do
662 attributedTo = object["attributedTo"] || object["actor"]
663
664 object
665 |> Map.put("attributedTo", attributedTo)
666 end
667
668 def prepare_attachments(object) do
669 attachments =
670 (object["attachment"] || [])
671 |> Enum.map(fn data ->
672 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
673 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
674 end)
675
676 object
677 |> Map.put("attachment", attachments)
678 end
679
680 defp user_upgrade_task(user) do
681 old_follower_address = User.ap_followers(user)
682
683 q =
684 from(
685 u in User,
686 where: ^old_follower_address in u.following,
687 update: [
688 set: [
689 following:
690 fragment(
691 "array_replace(?,?,?)",
692 u.following,
693 ^old_follower_address,
694 ^user.follower_address
695 )
696 ]
697 ]
698 )
699
700 Repo.update_all(q, [])
701
702 maybe_retire_websub(user.ap_id)
703
704 # Only do this for recent activties, don't go through the whole db.
705 # Only look at the last 1000 activities.
706 since = (Repo.aggregate(Activity, :max, :id) || 0) - 1_000
707
708 q =
709 from(
710 a in Activity,
711 where: ^old_follower_address in a.recipients,
712 where: a.id > ^since,
713 update: [
714 set: [
715 recipients:
716 fragment(
717 "array_replace(?,?,?)",
718 a.recipients,
719 ^old_follower_address,
720 ^user.follower_address
721 )
722 ]
723 ]
724 )
725
726 Repo.update_all(q, [])
727 end
728
729 def upgrade_user_from_ap_id(ap_id, async \\ true) do
730 with %User{local: false} = user <- User.get_by_ap_id(ap_id),
731 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do
732 data =
733 data
734 |> Map.put(:info, Map.merge(user.info, data[:info]))
735
736 already_ap = User.ap_enabled?(user)
737
738 {:ok, user} =
739 User.upgrade_changeset(user, data)
740 |> Repo.update()
741
742 if !already_ap do
743 # This could potentially take a long time, do it in the background
744 if async do
745 Task.start(fn ->
746 user_upgrade_task(user)
747 end)
748 else
749 user_upgrade_task(user)
750 end
751 end
752
753 {:ok, user}
754 else
755 e -> e
756 end
757 end
758
759 def maybe_retire_websub(ap_id) do
760 # some sanity checks
761 if is_binary(ap_id) && String.length(ap_id) > 8 do
762 q =
763 from(
764 ws in Pleroma.Web.Websub.WebsubClientSubscription,
765 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
766 )
767
768 Repo.delete_all(q)
769 end
770 end
771
772 def maybe_fix_user_url(data) do
773 if is_map(data["url"]) do
774 Map.put(data, "url", data["url"]["href"])
775 else
776 data
777 end
778 end
779
780 def maybe_fix_user_object(data) do
781 data
782 |> maybe_fix_user_url
783 end
784 end