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