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