117e930b37a0a73a85bc4c3429760c0f14c1a3a2
[akkoma] / lib / pleroma / web / activity_pub / transmogrifier.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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.EarmarkRenderer
11 alias Pleroma.EctoType.ActivityPub.ObjectValidators
12 alias Pleroma.FollowingRelationship
13 alias Pleroma.Maps
14 alias Pleroma.Notification
15 alias Pleroma.Object
16 alias Pleroma.Object.Containment
17 alias Pleroma.Repo
18 alias Pleroma.User
19 alias Pleroma.Web.ActivityPub.ActivityPub
20 alias Pleroma.Web.ActivityPub.Builder
21 alias Pleroma.Web.ActivityPub.ObjectValidator
22 alias Pleroma.Web.ActivityPub.Pipeline
23 alias Pleroma.Web.ActivityPub.Utils
24 alias Pleroma.Web.ActivityPub.Visibility
25 alias Pleroma.Web.Federator
26 alias Pleroma.Workers.TransmogrifierWorker
27
28 import Ecto.Query
29
30 require Logger
31 require Pleroma.Constants
32
33 @doc """
34 Modifies an incoming AP object (mastodon format) to our internal format.
35 """
36 def fix_object(object, options \\ []) do
37 object
38 |> strip_internal_fields
39 |> fix_actor
40 |> fix_url
41 |> fix_attachments
42 |> fix_context
43 |> fix_in_reply_to(options)
44 |> fix_emoji
45 |> fix_tag
46 |> fix_content_map
47 |> fix_addressing
48 |> fix_summary
49 |> fix_type(options)
50 |> fix_content
51 end
52
53 def fix_summary(%{"summary" => nil} = object) do
54 Map.put(object, "summary", "")
55 end
56
57 def fix_summary(%{"summary" => _} = object) do
58 # summary is present, nothing to do
59 object
60 end
61
62 def fix_summary(object), do: Map.put(object, "summary", "")
63
64 def fix_addressing_list(map, field) do
65 cond do
66 is_binary(map[field]) ->
67 Map.put(map, field, [map[field]])
68
69 is_nil(map[field]) ->
70 Map.put(map, field, [])
71
72 true ->
73 map
74 end
75 end
76
77 def fix_explicit_addressing(
78 %{"to" => to, "cc" => cc} = object,
79 explicit_mentions,
80 follower_collection
81 ) do
82 explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
83
84 explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)
85
86 final_cc =
87 (cc ++ explicit_cc)
88 |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
89 |> Enum.uniq()
90
91 object
92 |> Map.put("to", explicit_to)
93 |> Map.put("cc", final_cc)
94 end
95
96 def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
97
98 # if directMessage flag is set to true, leave the addressing alone
99 def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
100
101 def fix_explicit_addressing(object) do
102 explicit_mentions = Utils.determine_explicit_mentions(object)
103
104 %User{follower_address: follower_collection} =
105 object
106 |> Containment.get_actor()
107 |> User.get_cached_by_ap_id()
108
109 explicit_mentions =
110 explicit_mentions ++
111 [
112 Pleroma.Constants.as_public(),
113 follower_collection
114 ]
115
116 fix_explicit_addressing(object, explicit_mentions, follower_collection)
117 end
118
119 # if as:Public is addressed, then make sure the followers collection is also addressed
120 # so that the activities will be delivered to local users.
121 def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
122 recipients = to ++ cc
123
124 if followers_collection not in recipients do
125 cond do
126 Pleroma.Constants.as_public() in cc ->
127 to = to ++ [followers_collection]
128 Map.put(object, "to", to)
129
130 Pleroma.Constants.as_public() in to ->
131 cc = cc ++ [followers_collection]
132 Map.put(object, "cc", cc)
133
134 true ->
135 object
136 end
137 else
138 object
139 end
140 end
141
142 def fix_implicit_addressing(object, _), do: object
143
144 def fix_addressing(object) do
145 {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
146 followers_collection = User.ap_followers(user)
147
148 object
149 |> fix_addressing_list("to")
150 |> fix_addressing_list("cc")
151 |> fix_addressing_list("bto")
152 |> fix_addressing_list("bcc")
153 |> fix_explicit_addressing()
154 |> fix_implicit_addressing(followers_collection)
155 end
156
157 def fix_actor(%{"attributedTo" => actor} = object) do
158 Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
159 end
160
161 def fix_in_reply_to(object, options \\ [])
162
163 def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
164 when not is_nil(in_reply_to) do
165 in_reply_to_id = prepare_in_reply_to(in_reply_to)
166 object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
167 depth = (options[:depth] || 0) + 1
168
169 if Federator.allowed_thread_distance?(depth) do
170 with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
171 %Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
172 object
173 |> Map.put("inReplyTo", replied_object.data["id"])
174 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
175 |> Map.put("context", replied_object.data["context"] || object["conversation"])
176 |> Map.drop(["conversation"])
177 else
178 e ->
179 Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
180 object
181 end
182 else
183 object
184 end
185 end
186
187 def fix_in_reply_to(object, _options), do: object
188
189 defp prepare_in_reply_to(in_reply_to) do
190 cond do
191 is_bitstring(in_reply_to) ->
192 in_reply_to
193
194 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
195 in_reply_to["id"]
196
197 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
198 Enum.at(in_reply_to, 0)
199
200 true ->
201 ""
202 end
203 end
204
205 def fix_context(object) do
206 context = object["context"] || object["conversation"] || Utils.generate_context_id()
207
208 object
209 |> Map.put("context", context)
210 |> Map.drop(["conversation"])
211 end
212
213 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
214 attachments =
215 Enum.map(attachment, fn data ->
216 url =
217 cond do
218 is_list(data["url"]) -> List.first(data["url"])
219 is_map(data["url"]) -> data["url"]
220 true -> nil
221 end
222
223 media_type =
224 cond do
225 is_map(url) && MIME.valid?(url["mediaType"]) -> url["mediaType"]
226 MIME.valid?(data["mediaType"]) -> data["mediaType"]
227 MIME.valid?(data["mimeType"]) -> data["mimeType"]
228 true -> nil
229 end
230
231 href =
232 cond do
233 is_map(url) && is_binary(url["href"]) -> url["href"]
234 is_binary(data["url"]) -> data["url"]
235 is_binary(data["href"]) -> data["href"]
236 true -> nil
237 end
238
239 if href do
240 attachment_url =
241 %{"href" => href}
242 |> Maps.put_if_present("mediaType", media_type)
243 |> Maps.put_if_present("type", Map.get(url || %{}, "type"))
244
245 %{"url" => [attachment_url]}
246 |> Maps.put_if_present("mediaType", media_type)
247 |> Maps.put_if_present("type", data["type"])
248 |> Maps.put_if_present("name", data["name"])
249 else
250 nil
251 end
252 end)
253 |> Enum.filter(& &1)
254
255 Map.put(object, "attachment", attachments)
256 end
257
258 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
259 object
260 |> Map.put("attachment", [attachment])
261 |> fix_attachments()
262 end
263
264 def fix_attachments(object), do: object
265
266 def fix_url(%{"url" => url} = object) when is_map(url) do
267 Map.put(object, "url", url["href"])
268 end
269
270 def fix_url(%{"type" => object_type, "url" => url} = object)
271 when object_type in ["Video", "Audio"] and is_list(url) do
272 attachment =
273 Enum.find(url, fn x ->
274 media_type = x["mediaType"] || x["mimeType"] || ""
275
276 is_map(x) and String.starts_with?(media_type, ["audio/", "video/"])
277 end)
278
279 link_element =
280 Enum.find(url, fn x -> is_map(x) and (x["mediaType"] || x["mimeType"]) == "text/html" end)
281
282 object
283 |> Map.put("attachment", [attachment])
284 |> Map.put("url", link_element["href"])
285 end
286
287 def fix_url(%{"type" => object_type, "url" => url} = object)
288 when object_type != "Video" and is_list(url) do
289 first_element = Enum.at(url, 0)
290
291 url_string =
292 cond do
293 is_bitstring(first_element) -> first_element
294 is_map(first_element) -> first_element["href"] || ""
295 true -> ""
296 end
297
298 Map.put(object, "url", url_string)
299 end
300
301 def fix_url(object), do: object
302
303 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
304 emoji =
305 tags
306 |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
307 |> Enum.reduce(%{}, fn data, mapping ->
308 name = String.trim(data["name"], ":")
309
310 Map.put(mapping, name, data["icon"]["url"])
311 end)
312
313 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
314 emoji = Map.merge(object["emoji"] || %{}, emoji)
315
316 Map.put(object, "emoji", emoji)
317 end
318
319 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
320 name = String.trim(tag["name"], ":")
321 emoji = %{name => tag["icon"]["url"]}
322
323 Map.put(object, "emoji", emoji)
324 end
325
326 def fix_emoji(object), do: object
327
328 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
329 tags =
330 tag
331 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
332 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
333
334 Map.put(object, "tag", tag ++ tags)
335 end
336
337 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
338 combined = [tag, String.slice(hashtag, 1..-1)]
339
340 Map.put(object, "tag", combined)
341 end
342
343 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
344
345 def fix_tag(object), do: object
346
347 # content map usually only has one language so this will do for now.
348 def fix_content_map(%{"contentMap" => content_map} = object) do
349 content_groups = Map.to_list(content_map)
350 {_, content} = Enum.at(content_groups, 0)
351
352 Map.put(object, "content", content)
353 end
354
355 def fix_content_map(object), do: object
356
357 def fix_type(object, options \\ [])
358
359 def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
360 when is_binary(reply_id) do
361 with true <- Federator.allowed_thread_distance?(options[:depth]),
362 {:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
363 Map.put(object, "type", "Answer")
364 else
365 _ -> object
366 end
367 end
368
369 def fix_type(object, _), do: object
370
371 defp fix_content(%{"mediaType" => "text/markdown", "content" => content} = object)
372 when is_binary(content) do
373 html_content =
374 content
375 |> Earmark.as_html!(%Earmark.Options{renderer: EarmarkRenderer})
376 |> Pleroma.HTML.filter_tags()
377
378 Map.merge(object, %{"content" => html_content, "mediaType" => "text/html"})
379 end
380
381 defp fix_content(object), do: object
382
383 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
384 with true <- id =~ "follows",
385 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
386 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
387 {:ok, activity}
388 else
389 _ -> {:error, nil}
390 end
391 end
392
393 defp mastodon_follow_hack(_, _), do: {:error, nil}
394
395 defp get_follow_activity(follow_object, followed) do
396 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
397 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
398 {:ok, activity}
399 else
400 # Can't find the activity. This might a Mastodon 2.3 "Accept"
401 {:activity, nil} ->
402 mastodon_follow_hack(follow_object, followed)
403
404 _ ->
405 {:error, nil}
406 end
407 end
408
409 # Reduce the object list to find the reported user.
410 defp get_reported(objects) do
411 Enum.reduce_while(objects, nil, fn ap_id, _ ->
412 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
413 {:halt, user}
414 else
415 _ -> {:cont, nil}
416 end
417 end)
418 end
419
420 def handle_incoming(data, options \\ [])
421
422 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
423 # with nil ID.
424 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
425 with context <- data["context"] || Utils.generate_context_id(),
426 content <- data["content"] || "",
427 %User{} = actor <- User.get_cached_by_ap_id(actor),
428 # Reduce the object list to find the reported user.
429 %User{} = account <- get_reported(objects),
430 # Remove the reported user from the object list.
431 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
432 %{
433 actor: actor,
434 context: context,
435 account: account,
436 statuses: statuses,
437 content: content,
438 additional: %{"cc" => [account.ap_id]}
439 }
440 |> ActivityPub.flag()
441 end
442 end
443
444 # disallow objects with bogus IDs
445 def handle_incoming(%{"id" => nil}, _options), do: :error
446 def handle_incoming(%{"id" => ""}, _options), do: :error
447 # length of https:// = 8, should validate better, but good enough for now.
448 def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8,
449 do: :error
450
451 # TODO: validate those with a Ecto scheme
452 # - tags
453 # - emoji
454 def handle_incoming(
455 %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
456 options
457 )
458 when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do
459 actor = Containment.get_actor(data)
460
461 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
462 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(actor),
463 data <- Map.put(data, "actor", actor) |> fix_addressing() do
464 object = fix_object(object, options)
465
466 params = %{
467 to: data["to"],
468 object: object,
469 actor: user,
470 context: object["context"],
471 local: false,
472 published: data["published"],
473 additional:
474 Map.take(data, [
475 "cc",
476 "directMessage",
477 "id"
478 ])
479 }
480
481 with {:ok, created_activity} <- ActivityPub.create(params) do
482 reply_depth = (options[:depth] || 0) + 1
483
484 if Federator.allowed_thread_distance?(reply_depth) do
485 for reply_id <- replies(object) do
486 Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
487 "id" => reply_id,
488 "depth" => reply_depth
489 })
490 end
491 end
492
493 {:ok, created_activity}
494 end
495 else
496 %Activity{} = activity -> {:ok, activity}
497 _e -> :error
498 end
499 end
500
501 def handle_incoming(
502 %{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
503 options
504 ) do
505 actor = Containment.get_actor(data)
506
507 data =
508 Map.put(data, "actor", actor)
509 |> fix_addressing
510
511 with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
512 reply_depth = (options[:depth] || 0) + 1
513 options = Keyword.put(options, :depth, reply_depth)
514 object = fix_object(object, options)
515
516 params = %{
517 to: data["to"],
518 object: object,
519 actor: user,
520 context: nil,
521 local: false,
522 published: data["published"],
523 additional: Map.take(data, ["cc", "id"])
524 }
525
526 ActivityPub.listen(params)
527 else
528 _e -> :error
529 end
530 end
531
532 def handle_incoming(
533 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
534 _options
535 ) do
536 with %User{local: true} = followed <-
537 User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
538 {:ok, %User{} = follower} <-
539 User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
540 {:ok, activity} <-
541 ActivityPub.follow(follower, followed, id, false, skip_notify_and_stream: true) do
542 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
543 {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
544 {_, false} <- {:user_locked, User.locked?(followed)},
545 {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
546 {_, {:ok, _}} <-
547 {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")},
548 {:ok, _relationship} <-
549 FollowingRelationship.update(follower, followed, :follow_accept) do
550 ActivityPub.accept(%{
551 to: [follower.ap_id],
552 actor: followed,
553 object: data,
554 local: true
555 })
556 else
557 {:user_blocked, true} ->
558 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
559 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
560
561 ActivityPub.reject(%{
562 to: [follower.ap_id],
563 actor: followed,
564 object: data,
565 local: true
566 })
567
568 {:follow, {:error, _}} ->
569 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
570 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
571
572 ActivityPub.reject(%{
573 to: [follower.ap_id],
574 actor: followed,
575 object: data,
576 local: true
577 })
578
579 {:user_locked, true} ->
580 {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending)
581 :noop
582 end
583
584 ActivityPub.notify_and_stream(activity)
585 {:ok, activity}
586 else
587 _e ->
588 :error
589 end
590 end
591
592 def handle_incoming(
593 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
594 _options
595 ) do
596 with actor <- Containment.get_actor(data),
597 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
598 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
599 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
600 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
601 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do
602 User.update_follower_count(followed)
603 User.update_following_count(follower)
604
605 Notification.update_notification_type(followed, follow_activity)
606
607 ActivityPub.accept(%{
608 to: follow_activity.data["to"],
609 type: "Accept",
610 actor: followed,
611 object: follow_activity.data["id"],
612 local: false,
613 activity_id: id
614 })
615 else
616 _e ->
617 :error
618 end
619 end
620
621 def handle_incoming(
622 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data,
623 _options
624 ) do
625 with actor <- Containment.get_actor(data),
626 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
627 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
628 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
629 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
630 {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
631 {:ok, activity} <-
632 ActivityPub.reject(%{
633 to: follow_activity.data["to"],
634 type: "Reject",
635 actor: followed,
636 object: follow_activity.data["id"],
637 local: false,
638 activity_id: id
639 }) do
640 {:ok, activity}
641 else
642 _e -> :error
643 end
644 end
645
646 @misskey_reactions %{
647 "like" => "👍",
648 "love" => "❤️",
649 "laugh" => "😆",
650 "hmm" => "🤔",
651 "surprise" => "😮",
652 "congrats" => "🎉",
653 "angry" => "💢",
654 "confused" => "😥",
655 "rip" => "😇",
656 "pudding" => "🍮",
657 "star" => "⭐"
658 }
659
660 @doc "Rewrite misskey likes into EmojiReacts"
661 def handle_incoming(
662 %{
663 "type" => "Like",
664 "_misskey_reaction" => reaction
665 } = data,
666 options
667 ) do
668 data
669 |> Map.put("type", "EmojiReact")
670 |> Map.put("content", @misskey_reactions[reaction] || reaction)
671 |> handle_incoming(options)
672 end
673
674 def handle_incoming(
675 %{"type" => "Create", "object" => %{"type" => "ChatMessage"}} = data,
676 _options
677 ) do
678 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
679 {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
680 {:ok, activity}
681 end
682 end
683
684 def handle_incoming(%{"type" => type} = data, _options)
685 when type in ~w{Like EmojiReact Announce} do
686 with :ok <- ObjectValidator.fetch_actor_and_object(data),
687 {:ok, activity, _meta} <-
688 Pipeline.common_pipeline(data, local: false) do
689 {:ok, activity}
690 else
691 e -> {:error, e}
692 end
693 end
694
695 def handle_incoming(
696 %{"type" => type} = data,
697 _options
698 )
699 when type in ~w{Update Block} do
700 with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
701 {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
702 {:ok, activity}
703 end
704 end
705
706 def handle_incoming(
707 %{"type" => "Delete"} = data,
708 _options
709 ) do
710 with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
711 {:ok, activity}
712 else
713 {:error, {:validate_object, _}} = e ->
714 # Check if we have a create activity for this
715 with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]),
716 %Activity{data: %{"actor" => actor}} <-
717 Activity.create_by_object_ap_id(object_id) |> Repo.one(),
718 # We have one, insert a tombstone and retry
719 {:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id),
720 {:ok, _tombstone} <- Object.create(tombstone_data) do
721 handle_incoming(data)
722 else
723 _ -> e
724 end
725 end
726 end
727
728 def handle_incoming(
729 %{
730 "type" => "Undo",
731 "object" => %{"type" => "Follow", "object" => followed},
732 "actor" => follower,
733 "id" => id
734 } = _data,
735 _options
736 ) do
737 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
738 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
739 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
740 User.unfollow(follower, followed)
741 {:ok, activity}
742 else
743 _e -> :error
744 end
745 end
746
747 def handle_incoming(
748 %{
749 "type" => "Undo",
750 "object" => %{"type" => type}
751 } = data,
752 _options
753 )
754 when type in ["Like", "EmojiReact", "Announce", "Block"] do
755 with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
756 {:ok, activity}
757 end
758 end
759
760 # For Undos that don't have the complete object attached, try to find it in our database.
761 def handle_incoming(
762 %{
763 "type" => "Undo",
764 "object" => object
765 } = activity,
766 options
767 )
768 when is_binary(object) do
769 with %Activity{data: data} <- Activity.get_by_ap_id(object) do
770 activity
771 |> Map.put("object", data)
772 |> handle_incoming(options)
773 else
774 _e -> :error
775 end
776 end
777
778 def handle_incoming(
779 %{
780 "type" => "Move",
781 "actor" => origin_actor,
782 "object" => origin_actor,
783 "target" => target_actor
784 },
785 _options
786 ) do
787 with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
788 {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
789 true <- origin_actor in target_user.also_known_as do
790 ActivityPub.move(origin_user, target_user, false)
791 else
792 _e -> :error
793 end
794 end
795
796 def handle_incoming(_, _), do: :error
797
798 @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
799 def get_obj_helper(id, options \\ []) do
800 case Object.normalize(id, true, options) do
801 %Object{} = object -> {:ok, object}
802 _ -> nil
803 end
804 end
805
806 @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
807 def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
808 ap_id: ap_id
809 })
810 when attributed_to == ap_id do
811 with {:ok, activity} <-
812 handle_incoming(%{
813 "type" => "Create",
814 "to" => data["to"],
815 "cc" => data["cc"],
816 "actor" => attributed_to,
817 "object" => data
818 }) do
819 {:ok, Object.normalize(activity)}
820 else
821 _ -> get_obj_helper(object_id)
822 end
823 end
824
825 def get_embedded_obj_helper(object_id, _) do
826 get_obj_helper(object_id)
827 end
828
829 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
830 with false <- String.starts_with?(in_reply_to, "http"),
831 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
832 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
833 else
834 _e -> object
835 end
836 end
837
838 def set_reply_to_uri(obj), do: obj
839
840 @doc """
841 Serialized Mastodon-compatible `replies` collection containing _self-replies_.
842 Based on Mastodon's ActivityPub::NoteSerializer#replies.
843 """
844 def set_replies(obj_data) do
845 replies_uris =
846 with limit when limit > 0 <-
847 Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
848 %Object{} = object <- Object.get_cached_by_ap_id(obj_data["id"]) do
849 object
850 |> Object.self_replies()
851 |> select([o], fragment("?->>'id'", o.data))
852 |> limit(^limit)
853 |> Repo.all()
854 else
855 _ -> []
856 end
857
858 set_replies(obj_data, replies_uris)
859 end
860
861 defp set_replies(obj, []) do
862 obj
863 end
864
865 defp set_replies(obj, replies_uris) do
866 replies_collection = %{
867 "type" => "Collection",
868 "items" => replies_uris
869 }
870
871 Map.merge(obj, %{"replies" => replies_collection})
872 end
873
874 def replies(%{"replies" => %{"first" => %{"items" => items}}}) when not is_nil(items) do
875 items
876 end
877
878 def replies(%{"replies" => %{"items" => items}}) when not is_nil(items) do
879 items
880 end
881
882 def replies(_), do: []
883
884 # Prepares the object of an outgoing create activity.
885 def prepare_object(object) do
886 object
887 |> set_sensitive
888 |> add_hashtags
889 |> add_mention_tags
890 |> add_emoji_tags
891 |> add_attributed_to
892 |> prepare_attachments
893 |> set_conversation
894 |> set_reply_to_uri
895 |> set_replies
896 |> strip_internal_fields
897 |> strip_internal_tags
898 |> set_type
899 end
900
901 # @doc
902 # """
903 # internal -> Mastodon
904 # """
905
906 def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
907 when activity_type in ["Create", "Listen"] do
908 object =
909 object_id
910 |> Object.normalize()
911 |> Map.get(:data)
912 |> prepare_object
913
914 data =
915 data
916 |> Map.put("object", object)
917 |> Map.merge(Utils.make_json_ld_header())
918 |> Map.delete("bcc")
919
920 {:ok, data}
921 end
922
923 def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
924 object =
925 object_id
926 |> Object.normalize()
927
928 data =
929 if Visibility.is_private?(object) && object.data["actor"] == ap_id do
930 data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
931 else
932 data |> maybe_fix_object_url
933 end
934
935 data =
936 data
937 |> strip_internal_fields
938 |> Map.merge(Utils.make_json_ld_header())
939 |> Map.delete("bcc")
940
941 {:ok, data}
942 end
943
944 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
945 # because of course it does.
946 def prepare_outgoing(%{"type" => "Accept"} = data) do
947 with follow_activity <- Activity.normalize(data["object"]) do
948 object = %{
949 "actor" => follow_activity.actor,
950 "object" => follow_activity.data["object"],
951 "id" => follow_activity.data["id"],
952 "type" => "Follow"
953 }
954
955 data =
956 data
957 |> Map.put("object", object)
958 |> Map.merge(Utils.make_json_ld_header())
959
960 {:ok, data}
961 end
962 end
963
964 def prepare_outgoing(%{"type" => "Reject"} = data) do
965 with follow_activity <- Activity.normalize(data["object"]) do
966 object = %{
967 "actor" => follow_activity.actor,
968 "object" => follow_activity.data["object"],
969 "id" => follow_activity.data["id"],
970 "type" => "Follow"
971 }
972
973 data =
974 data
975 |> Map.put("object", object)
976 |> Map.merge(Utils.make_json_ld_header())
977
978 {:ok, data}
979 end
980 end
981
982 def prepare_outgoing(%{"type" => _type} = data) do
983 data =
984 data
985 |> strip_internal_fields
986 |> maybe_fix_object_url
987 |> Map.merge(Utils.make_json_ld_header())
988
989 {:ok, data}
990 end
991
992 def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
993 with false <- String.starts_with?(object, "http"),
994 {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
995 %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
996 relative_object do
997 Map.put(data, "object", external_url)
998 else
999 {:fetch, e} ->
1000 Logger.error("Couldn't fetch #{object} #{inspect(e)}")
1001 data
1002
1003 _ ->
1004 data
1005 end
1006 end
1007
1008 def maybe_fix_object_url(data), do: data
1009
1010 def add_hashtags(object) do
1011 tags =
1012 (object["tag"] || [])
1013 |> Enum.map(fn
1014 # Expand internal representation tags into AS2 tags.
1015 tag when is_binary(tag) ->
1016 %{
1017 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
1018 "name" => "##{tag}",
1019 "type" => "Hashtag"
1020 }
1021
1022 # Do not process tags which are already AS2 tag objects.
1023 tag when is_map(tag) ->
1024 tag
1025 end)
1026
1027 Map.put(object, "tag", tags)
1028 end
1029
1030 # TODO These should be added on our side on insertion, it doesn't make much
1031 # sense to regenerate these all the time
1032 def add_mention_tags(object) do
1033 to = object["to"] || []
1034 cc = object["cc"] || []
1035 mentioned = User.get_users_from_set(to ++ cc, local_only: false)
1036
1037 mentions = Enum.map(mentioned, &build_mention_tag/1)
1038
1039 tags = object["tag"] || []
1040 Map.put(object, "tag", tags ++ mentions)
1041 end
1042
1043 defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
1044 %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
1045 end
1046
1047 def take_emoji_tags(%User{emoji: emoji}) do
1048 emoji
1049 |> Map.to_list()
1050 |> Enum.map(&build_emoji_tag/1)
1051 end
1052
1053 # TODO: we should probably send mtime instead of unix epoch time for updated
1054 def add_emoji_tags(%{"emoji" => emoji} = object) do
1055 tags = object["tag"] || []
1056
1057 out = Enum.map(emoji, &build_emoji_tag/1)
1058
1059 Map.put(object, "tag", tags ++ out)
1060 end
1061
1062 def add_emoji_tags(object), do: object
1063
1064 defp build_emoji_tag({name, url}) do
1065 %{
1066 "icon" => %{"url" => url, "type" => "Image"},
1067 "name" => ":" <> name <> ":",
1068 "type" => "Emoji",
1069 "updated" => "1970-01-01T00:00:00Z",
1070 "id" => url
1071 }
1072 end
1073
1074 def set_conversation(object) do
1075 Map.put(object, "conversation", object["context"])
1076 end
1077
1078 def set_sensitive(%{"sensitive" => true} = object) do
1079 object
1080 end
1081
1082 def set_sensitive(object) do
1083 tags = object["tag"] || []
1084 Map.put(object, "sensitive", "nsfw" in tags)
1085 end
1086
1087 def set_type(%{"type" => "Answer"} = object) do
1088 Map.put(object, "type", "Note")
1089 end
1090
1091 def set_type(object), do: object
1092
1093 def add_attributed_to(object) do
1094 attributed_to = object["attributedTo"] || object["actor"]
1095 Map.put(object, "attributedTo", attributed_to)
1096 end
1097
1098 # TODO: Revisit this
1099 def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object
1100
1101 def prepare_attachments(object) do
1102 attachments =
1103 object
1104 |> Map.get("attachment", [])
1105 |> Enum.map(fn data ->
1106 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
1107
1108 %{
1109 "url" => href,
1110 "mediaType" => media_type,
1111 "name" => data["name"],
1112 "type" => "Document"
1113 }
1114 end)
1115
1116 Map.put(object, "attachment", attachments)
1117 end
1118
1119 def strip_internal_fields(object) do
1120 Map.drop(object, Pleroma.Constants.object_internal_fields())
1121 end
1122
1123 defp strip_internal_tags(%{"tag" => tags} = object) do
1124 tags = Enum.filter(tags, fn x -> is_map(x) end)
1125
1126 Map.put(object, "tag", tags)
1127 end
1128
1129 defp strip_internal_tags(object), do: object
1130
1131 def perform(:user_upgrade, user) do
1132 # we pass a fake user so that the followers collection is stripped away
1133 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1134
1135 from(
1136 a in Activity,
1137 where: ^old_follower_address in a.recipients,
1138 update: [
1139 set: [
1140 recipients:
1141 fragment(
1142 "array_replace(?,?,?)",
1143 a.recipients,
1144 ^old_follower_address,
1145 ^user.follower_address
1146 )
1147 ]
1148 ]
1149 )
1150 |> Repo.update_all([])
1151 end
1152
1153 def upgrade_user_from_ap_id(ap_id) do
1154 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1155 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1156 {:ok, user} <- update_user(user, data) do
1157 TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
1158 {:ok, user}
1159 else
1160 %User{} = user -> {:ok, user}
1161 e -> e
1162 end
1163 end
1164
1165 defp update_user(user, data) do
1166 user
1167 |> User.remote_user_changeset(data)
1168 |> User.update_and_set_cache()
1169 end
1170
1171 def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
1172 Map.put(data, "url", url["href"])
1173 end
1174
1175 def maybe_fix_user_url(data), do: data
1176
1177 def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
1178 end