Merge branch 'develop' into 'fix/hide-follows-counters'
[akkoma] / lib / pleroma / web / activity_pub / transmogrifier.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ActivityPub.Transmogrifier do
6 @moduledoc """
7 A module to handle coding from internal to wire ActivityPub and back.
8 """
9 alias Pleroma.Activity
10 alias Pleroma.Object
11 alias Pleroma.Object.Containment
12 alias Pleroma.Repo
13 alias Pleroma.User
14 alias Pleroma.Web.ActivityPub.ActivityPub
15 alias Pleroma.Web.ActivityPub.Utils
16 alias Pleroma.Web.ActivityPub.Visibility
17 alias Pleroma.Web.Federator
18
19 import Ecto.Query
20
21 require Logger
22 require Pleroma.Constants
23
24 @doc """
25 Modifies an incoming AP object (mastodon format) to our internal format.
26 """
27 def fix_object(object, options \\ []) do
28 object
29 |> strip_internal_fields
30 |> fix_actor
31 |> fix_url
32 |> fix_attachments
33 |> fix_context
34 |> fix_in_reply_to(options)
35 |> fix_emoji
36 |> fix_tag
37 |> fix_content_map
38 |> fix_addressing
39 |> fix_summary
40 |> fix_type(options)
41 end
42
43 def fix_summary(%{"summary" => nil} = object) do
44 object
45 |> Map.put("summary", "")
46 end
47
48 def fix_summary(%{"summary" => _} = object) do
49 # summary is present, nothing to do
50 object
51 end
52
53 def fix_summary(object) do
54 object
55 |> Map.put("summary", "")
56 end
57
58 def fix_addressing_list(map, field) do
59 cond do
60 is_binary(map[field]) ->
61 Map.put(map, field, [map[field]])
62
63 is_nil(map[field]) ->
64 Map.put(map, field, [])
65
66 true ->
67 map
68 end
69 end
70
71 def fix_explicit_addressing(
72 %{"to" => to, "cc" => cc} = object,
73 explicit_mentions,
74 follower_collection
75 ) do
76 explicit_to =
77 to
78 |> Enum.filter(fn x -> x in explicit_mentions end)
79
80 explicit_cc =
81 to
82 |> Enum.filter(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 =
101 object
102 |> Utils.determine_explicit_mentions()
103
104 follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address
105
106 explicit_mentions = explicit_mentions ++ [Pleroma.Constants.as_public(), follower_collection]
107
108 fix_explicit_addressing(object, explicit_mentions, follower_collection)
109 end
110
111 # if as:Public is addressed, then make sure the followers collection is also addressed
112 # so that the activities will be delivered to local users.
113 def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
114 recipients = to ++ cc
115
116 if followers_collection not in recipients do
117 cond do
118 Pleroma.Constants.as_public() in cc ->
119 to = to ++ [followers_collection]
120 Map.put(object, "to", to)
121
122 Pleroma.Constants.as_public() in to ->
123 cc = cc ++ [followers_collection]
124 Map.put(object, "cc", cc)
125
126 true ->
127 object
128 end
129 else
130 object
131 end
132 end
133
134 def fix_implicit_addressing(object, _), do: object
135
136 def fix_addressing(object) do
137 {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"])
138 followers_collection = User.ap_followers(user)
139
140 object
141 |> fix_addressing_list("to")
142 |> fix_addressing_list("cc")
143 |> fix_addressing_list("bto")
144 |> fix_addressing_list("bcc")
145 |> fix_explicit_addressing()
146 |> fix_implicit_addressing(followers_collection)
147 end
148
149 def fix_actor(%{"attributedTo" => actor} = object) do
150 object
151 |> Map.put("actor", Containment.get_actor(%{"actor" => actor}))
152 end
153
154 def fix_in_reply_to(object, options \\ [])
155
156 def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
157 when not is_nil(in_reply_to) do
158 in_reply_to_id =
159 cond do
160 is_bitstring(in_reply_to) ->
161 in_reply_to
162
163 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
164 in_reply_to["id"]
165
166 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
167 Enum.at(in_reply_to, 0)
168
169 # Maybe I should output an error too?
170 true ->
171 ""
172 end
173
174 object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
175
176 if Federator.allowed_incoming_reply_depth?(options[:depth]) do
177 case get_obj_helper(in_reply_to_id, options) do
178 {:ok, replied_object} ->
179 with %Activity{} = _activity <-
180 Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
181 object
182 |> Map.put("inReplyTo", replied_object.data["id"])
183 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
184 |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
185 |> Map.put("context", replied_object.data["context"] || object["conversation"])
186 else
187 e ->
188 Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
189 object
190 end
191
192 e ->
193 Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
194 object
195 end
196 else
197 object
198 end
199 end
200
201 def fix_in_reply_to(object, _options), do: object
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 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
212 attachments =
213 attachment
214 |> Enum.map(fn data ->
215 media_type = data["mediaType"] || data["mimeType"]
216 href = data["url"] || data["href"]
217
218 url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]
219
220 data
221 |> Map.put("mediaType", media_type)
222 |> Map.put("url", url)
223 end)
224
225 object
226 |> Map.put("attachment", attachments)
227 end
228
229 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
230 Map.put(object, "attachment", [attachment])
231 |> fix_attachments()
232 end
233
234 def fix_attachments(object), do: object
235
236 def fix_url(%{"url" => url} = object) when is_map(url) do
237 object
238 |> Map.put("url", url["href"])
239 end
240
241 def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
242 first_element = Enum.at(url, 0)
243
244 link_element =
245 url
246 |> Enum.filter(fn x -> is_map(x) end)
247 |> Enum.filter(fn x -> x["mimeType"] == "text/html" end)
248 |> Enum.at(0)
249
250 object
251 |> Map.put("attachment", [first_element])
252 |> Map.put("url", link_element["href"])
253 end
254
255 def fix_url(%{"type" => object_type, "url" => url} = object)
256 when object_type != "Video" and is_list(url) do
257 first_element = Enum.at(url, 0)
258
259 url_string =
260 cond do
261 is_bitstring(first_element) -> first_element
262 is_map(first_element) -> first_element["href"] || ""
263 true -> ""
264 end
265
266 object
267 |> Map.put("url", url_string)
268 end
269
270 def fix_url(object), do: object
271
272 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
273 emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
274
275 emoji =
276 emoji
277 |> Enum.reduce(%{}, fn data, mapping ->
278 name = String.trim(data["name"], ":")
279
280 mapping |> Map.put(name, data["icon"]["url"])
281 end)
282
283 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
284 emoji = Map.merge(object["emoji"] || %{}, emoji)
285
286 object
287 |> Map.put("emoji", emoji)
288 end
289
290 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
291 name = String.trim(tag["name"], ":")
292 emoji = %{name => tag["icon"]["url"]}
293
294 object
295 |> Map.put("emoji", emoji)
296 end
297
298 def fix_emoji(object), do: object
299
300 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
301 tags =
302 tag
303 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
304 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
305
306 combined = tag ++ tags
307
308 object
309 |> Map.put("tag", combined)
310 end
311
312 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
313 combined = [tag, String.slice(hashtag, 1..-1)]
314
315 object
316 |> Map.put("tag", combined)
317 end
318
319 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
320
321 def fix_tag(object), do: object
322
323 # content map usually only has one language so this will do for now.
324 def fix_content_map(%{"contentMap" => content_map} = object) do
325 content_groups = Map.to_list(content_map)
326 {_, content} = Enum.at(content_groups, 0)
327
328 object
329 |> Map.put("content", content)
330 end
331
332 def fix_content_map(object), do: object
333
334 def fix_type(object, options \\ [])
335
336 def fix_type(%{"inReplyTo" => reply_id} = object, options) when is_binary(reply_id) do
337 reply =
338 if Federator.allowed_incoming_reply_depth?(options[:depth]) do
339 Object.normalize(reply_id, true)
340 end
341
342 if reply && (reply.data["type"] == "Question" and object["name"]) do
343 Map.put(object, "type", "Answer")
344 else
345 object
346 end
347 end
348
349 def fix_type(object, _), do: object
350
351 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
352 with true <- id =~ "follows",
353 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
354 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
355 {:ok, activity}
356 else
357 _ -> {:error, nil}
358 end
359 end
360
361 defp mastodon_follow_hack(_, _), do: {:error, nil}
362
363 defp get_follow_activity(follow_object, followed) do
364 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
365 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
366 {:ok, activity}
367 else
368 # Can't find the activity. This might a Mastodon 2.3 "Accept"
369 {:activity, nil} ->
370 mastodon_follow_hack(follow_object, followed)
371
372 _ ->
373 {:error, nil}
374 end
375 end
376
377 def handle_incoming(data, options \\ [])
378
379 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
380 # with nil ID.
381 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
382 with context <- data["context"] || Utils.generate_context_id(),
383 content <- data["content"] || "",
384 %User{} = actor <- User.get_cached_by_ap_id(actor),
385
386 # Reduce the object list to find the reported user.
387 %User{} = account <-
388 Enum.reduce_while(objects, nil, fn ap_id, _ ->
389 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
390 {:halt, user}
391 else
392 _ -> {:cont, nil}
393 end
394 end),
395
396 # Remove the reported user from the object list.
397 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
398 params = %{
399 actor: actor,
400 context: context,
401 account: account,
402 statuses: statuses,
403 content: content,
404 additional: %{
405 "cc" => [account.ap_id]
406 }
407 }
408
409 ActivityPub.flag(params)
410 end
411 end
412
413 # disallow objects with bogus IDs
414 def handle_incoming(%{"id" => nil}, _options), do: :error
415 def handle_incoming(%{"id" => ""}, _options), do: :error
416 # length of https:// = 8, should validate better, but good enough for now.
417 def handle_incoming(%{"id" => id}, _options) when not (is_binary(id) and length(id) > 8),
418 do: :error
419
420 # TODO: validate those with a Ecto scheme
421 # - tags
422 # - emoji
423 def handle_incoming(
424 %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
425 options
426 )
427 when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do
428 actor = Containment.get_actor(data)
429
430 data =
431 Map.put(data, "actor", actor)
432 |> fix_addressing
433
434 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
435 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
436 options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
437 object = fix_object(data["object"], options)
438
439 params = %{
440 to: data["to"],
441 object: object,
442 actor: user,
443 context: object["conversation"],
444 local: false,
445 published: data["published"],
446 additional:
447 Map.take(data, [
448 "cc",
449 "directMessage",
450 "id"
451 ])
452 }
453
454 ActivityPub.create(params)
455 else
456 %Activity{} = activity -> {:ok, activity}
457 _e -> :error
458 end
459 end
460
461 def handle_incoming(
462 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
463 _options
464 ) do
465 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
466 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
467 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
468 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
469 {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
470 {_, false} <- {:user_locked, User.locked?(followed)},
471 {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
472 {_, {:ok, _}} <-
473 {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do
474 ActivityPub.accept(%{
475 to: [follower.ap_id],
476 actor: followed,
477 object: data,
478 local: true
479 })
480 else
481 {:user_blocked, true} ->
482 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
483
484 ActivityPub.reject(%{
485 to: [follower.ap_id],
486 actor: followed,
487 object: data,
488 local: true
489 })
490
491 {:follow, {:error, _}} ->
492 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
493
494 ActivityPub.reject(%{
495 to: [follower.ap_id],
496 actor: followed,
497 object: data,
498 local: true
499 })
500
501 {:user_locked, true} ->
502 :noop
503 end
504
505 {:ok, activity}
506 else
507 _e ->
508 :error
509 end
510 end
511
512 def handle_incoming(
513 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
514 _options
515 ) do
516 with actor <- Containment.get_actor(data),
517 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
518 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
519 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
520 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
521 {:ok, _follower} = User.follow(follower, followed) do
522 ActivityPub.accept(%{
523 to: follow_activity.data["to"],
524 type: "Accept",
525 actor: followed,
526 object: follow_activity.data["id"],
527 local: false
528 })
529 else
530 _e -> :error
531 end
532 end
533
534 def handle_incoming(
535 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
536 _options
537 ) do
538 with actor <- Containment.get_actor(data),
539 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
540 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
541 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
542 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
543 {:ok, activity} <-
544 ActivityPub.reject(%{
545 to: follow_activity.data["to"],
546 type: "Reject",
547 actor: followed,
548 object: follow_activity.data["id"],
549 local: false
550 }) do
551 User.unfollow(follower, followed)
552
553 {:ok, activity}
554 else
555 _e -> :error
556 end
557 end
558
559 def handle_incoming(
560 %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
561 _options
562 ) do
563 with actor <- Containment.get_actor(data),
564 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
565 {:ok, object} <- get_obj_helper(object_id),
566 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
567 {:ok, activity}
568 else
569 _e -> :error
570 end
571 end
572
573 def handle_incoming(
574 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
575 _options
576 ) do
577 with actor <- Containment.get_actor(data),
578 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
579 {:ok, object} <- get_obj_helper(object_id),
580 public <- Visibility.is_public?(data),
581 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
582 {:ok, activity}
583 else
584 _e -> :error
585 end
586 end
587
588 def handle_incoming(
589 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
590 data,
591 _options
592 )
593 when object_type in ["Person", "Application", "Service", "Organization"] do
594 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
595 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
596
597 banner = new_user_data[:info][:banner]
598 locked = new_user_data[:info][:locked] || false
599
600 update_data =
601 new_user_data
602 |> Map.take([:name, :bio, :avatar])
603 |> Map.put(:info, %{banner: banner, locked: locked})
604
605 actor
606 |> User.upgrade_changeset(update_data)
607 |> User.update_and_set_cache()
608
609 ActivityPub.update(%{
610 local: false,
611 to: data["to"] || [],
612 cc: data["cc"] || [],
613 object: object,
614 actor: actor_id
615 })
616 else
617 e ->
618 Logger.error(e)
619 :error
620 end
621 end
622
623 # TODO: We presently assume that any actor on the same origin domain as the object being
624 # deleted has the rights to delete that object. A better way to validate whether or not
625 # the object should be deleted is to refetch the object URI, which should return either
626 # an error or a tombstone. This would allow us to verify that a deletion actually took
627 # place.
628 def handle_incoming(
629 %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = data,
630 _options
631 ) do
632 object_id = Utils.get_ap_id(object_id)
633
634 with actor <- Containment.get_actor(data),
635 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
636 {:ok, object} <- get_obj_helper(object_id),
637 :ok <- Containment.contain_origin(actor.ap_id, object.data),
638 {:ok, activity} <- ActivityPub.delete(object, false) do
639 {:ok, activity}
640 else
641 nil ->
642 case User.get_cached_by_ap_id(object_id) do
643 %User{ap_id: ^actor} = user ->
644 User.delete(user)
645
646 nil ->
647 :error
648 end
649
650 _e ->
651 :error
652 end
653 end
654
655 def handle_incoming(
656 %{
657 "type" => "Undo",
658 "object" => %{"type" => "Announce", "object" => object_id},
659 "actor" => _actor,
660 "id" => id
661 } = data,
662 _options
663 ) do
664 with actor <- Containment.get_actor(data),
665 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
666 {:ok, object} <- get_obj_helper(object_id),
667 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
668 {:ok, activity}
669 else
670 _e -> :error
671 end
672 end
673
674 def handle_incoming(
675 %{
676 "type" => "Undo",
677 "object" => %{"type" => "Follow", "object" => followed},
678 "actor" => follower,
679 "id" => id
680 } = _data,
681 _options
682 ) do
683 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
684 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
685 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
686 User.unfollow(follower, followed)
687 {:ok, activity}
688 else
689 _e -> :error
690 end
691 end
692
693 def handle_incoming(
694 %{
695 "type" => "Undo",
696 "object" => %{"type" => "Block", "object" => blocked},
697 "actor" => blocker,
698 "id" => id
699 } = _data,
700 _options
701 ) do
702 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
703 %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
704 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
705 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
706 User.unblock(blocker, blocked)
707 {:ok, activity}
708 else
709 _e -> :error
710 end
711 end
712
713 def handle_incoming(
714 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
715 _options
716 ) do
717 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
718 %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
719 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
720 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
721 User.unfollow(blocker, blocked)
722 User.block(blocker, blocked)
723 {:ok, activity}
724 else
725 _e -> :error
726 end
727 end
728
729 def handle_incoming(
730 %{
731 "type" => "Undo",
732 "object" => %{"type" => "Like", "object" => object_id},
733 "actor" => _actor,
734 "id" => id
735 } = data,
736 _options
737 ) do
738 with actor <- Containment.get_actor(data),
739 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
740 {:ok, object} <- get_obj_helper(object_id),
741 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
742 {:ok, activity}
743 else
744 _e -> :error
745 end
746 end
747
748 def handle_incoming(_, _), do: :error
749
750 def get_obj_helper(id, options \\ []) do
751 if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil
752 end
753
754 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
755 with false <- String.starts_with?(in_reply_to, "http"),
756 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
757 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
758 else
759 _e -> object
760 end
761 end
762
763 def set_reply_to_uri(obj), do: obj
764
765 # Prepares the object of an outgoing create activity.
766 def prepare_object(object) do
767 object
768 |> set_sensitive
769 |> add_hashtags
770 |> add_mention_tags
771 |> add_emoji_tags
772 |> add_attributed_to
773 |> prepare_attachments
774 |> set_conversation
775 |> set_reply_to_uri
776 |> strip_internal_fields
777 |> strip_internal_tags
778 |> set_type
779 end
780
781 # @doc
782 # """
783 # internal -> Mastodon
784 # """
785
786 def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
787 object =
788 object_id
789 |> Object.normalize()
790 |> Map.get(:data)
791 |> prepare_object
792
793 data =
794 data
795 |> Map.put("object", object)
796 |> Map.merge(Utils.make_json_ld_header())
797 |> Map.delete("bcc")
798
799 {:ok, data}
800 end
801
802 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
803 # because of course it does.
804 def prepare_outgoing(%{"type" => "Accept"} = data) do
805 with follow_activity <- Activity.normalize(data["object"]) do
806 object = %{
807 "actor" => follow_activity.actor,
808 "object" => follow_activity.data["object"],
809 "id" => follow_activity.data["id"],
810 "type" => "Follow"
811 }
812
813 data =
814 data
815 |> Map.put("object", object)
816 |> Map.merge(Utils.make_json_ld_header())
817
818 {:ok, data}
819 end
820 end
821
822 def prepare_outgoing(%{"type" => "Reject"} = data) do
823 with follow_activity <- Activity.normalize(data["object"]) do
824 object = %{
825 "actor" => follow_activity.actor,
826 "object" => follow_activity.data["object"],
827 "id" => follow_activity.data["id"],
828 "type" => "Follow"
829 }
830
831 data =
832 data
833 |> Map.put("object", object)
834 |> Map.merge(Utils.make_json_ld_header())
835
836 {:ok, data}
837 end
838 end
839
840 def prepare_outgoing(%{"type" => _type} = data) do
841 data =
842 data
843 |> strip_internal_fields
844 |> maybe_fix_object_url
845 |> Map.merge(Utils.make_json_ld_header())
846
847 {:ok, data}
848 end
849
850 def maybe_fix_object_url(data) do
851 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
852 case get_obj_helper(data["object"]) do
853 {:ok, relative_object} ->
854 if relative_object.data["external_url"] do
855 _data =
856 data
857 |> Map.put("object", relative_object.data["external_url"])
858 else
859 data
860 end
861
862 e ->
863 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
864 data
865 end
866 else
867 data
868 end
869 end
870
871 def add_hashtags(object) do
872 tags =
873 (object["tag"] || [])
874 |> Enum.map(fn
875 # Expand internal representation tags into AS2 tags.
876 tag when is_binary(tag) ->
877 %{
878 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
879 "name" => "##{tag}",
880 "type" => "Hashtag"
881 }
882
883 # Do not process tags which are already AS2 tag objects.
884 tag when is_map(tag) ->
885 tag
886 end)
887
888 object
889 |> Map.put("tag", tags)
890 end
891
892 def add_mention_tags(object) do
893 mentions =
894 object
895 |> Utils.get_notified_from_object()
896 |> Enum.map(fn user ->
897 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
898 end)
899
900 tags = object["tag"] || []
901
902 object
903 |> Map.put("tag", tags ++ mentions)
904 end
905
906 def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
907 user_info = add_emoji_tags(user_info)
908
909 object
910 |> Map.put(:info, user_info)
911 end
912
913 # TODO: we should probably send mtime instead of unix epoch time for updated
914 def add_emoji_tags(%{"emoji" => emoji} = object) do
915 tags = object["tag"] || []
916
917 out =
918 emoji
919 |> Enum.map(fn {name, url} ->
920 %{
921 "icon" => %{"url" => url, "type" => "Image"},
922 "name" => ":" <> name <> ":",
923 "type" => "Emoji",
924 "updated" => "1970-01-01T00:00:00Z",
925 "id" => url
926 }
927 end)
928
929 object
930 |> Map.put("tag", tags ++ out)
931 end
932
933 def add_emoji_tags(object) do
934 object
935 end
936
937 def set_conversation(object) do
938 Map.put(object, "conversation", object["context"])
939 end
940
941 def set_sensitive(object) do
942 tags = object["tag"] || []
943 Map.put(object, "sensitive", "nsfw" in tags)
944 end
945
946 def set_type(%{"type" => "Answer"} = object) do
947 Map.put(object, "type", "Note")
948 end
949
950 def set_type(object), do: object
951
952 def add_attributed_to(object) do
953 attributed_to = object["attributedTo"] || object["actor"]
954
955 object
956 |> Map.put("attributedTo", attributed_to)
957 end
958
959 def prepare_attachments(object) do
960 attachments =
961 (object["attachment"] || [])
962 |> Enum.map(fn data ->
963 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
964 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
965 end)
966
967 object
968 |> Map.put("attachment", attachments)
969 end
970
971 defp strip_internal_fields(object) do
972 object
973 |> Map.drop([
974 "likes",
975 "like_count",
976 "announcements",
977 "announcement_count",
978 "emoji",
979 "context_id",
980 "deleted_activity_id"
981 ])
982 end
983
984 defp strip_internal_tags(%{"tag" => tags} = object) do
985 tags =
986 tags
987 |> Enum.filter(fn x -> is_map(x) end)
988
989 object
990 |> Map.put("tag", tags)
991 end
992
993 defp strip_internal_tags(object), do: object
994
995 def perform(:user_upgrade, user) do
996 # we pass a fake user so that the followers collection is stripped away
997 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
998
999 q =
1000 from(
1001 u in User,
1002 where: ^old_follower_address in u.following,
1003 update: [
1004 set: [
1005 following:
1006 fragment(
1007 "array_replace(?,?,?)",
1008 u.following,
1009 ^old_follower_address,
1010 ^user.follower_address
1011 )
1012 ]
1013 ]
1014 )
1015
1016 Repo.update_all(q, [])
1017
1018 maybe_retire_websub(user.ap_id)
1019
1020 q =
1021 from(
1022 a in Activity,
1023 where: ^old_follower_address in a.recipients,
1024 update: [
1025 set: [
1026 recipients:
1027 fragment(
1028 "array_replace(?,?,?)",
1029 a.recipients,
1030 ^old_follower_address,
1031 ^user.follower_address
1032 )
1033 ]
1034 ]
1035 )
1036
1037 Repo.update_all(q, [])
1038 end
1039
1040 def upgrade_user_from_ap_id(ap_id) do
1041 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1042 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1043 already_ap <- User.ap_enabled?(user),
1044 {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
1045 unless already_ap do
1046 PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
1047 end
1048
1049 {:ok, user}
1050 else
1051 %User{} = user -> {:ok, user}
1052 e -> e
1053 end
1054 end
1055
1056 def maybe_retire_websub(ap_id) do
1057 # some sanity checks
1058 if is_binary(ap_id) && String.length(ap_id) > 8 do
1059 q =
1060 from(
1061 ws in Pleroma.Web.Websub.WebsubClientSubscription,
1062 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
1063 )
1064
1065 Repo.delete_all(q)
1066 end
1067 end
1068
1069 def maybe_fix_user_url(data) do
1070 if is_map(data["url"]) do
1071 Map.put(data, "url", data["url"]["href"])
1072 else
1073 data
1074 end
1075 end
1076
1077 def maybe_fix_user_object(data) do
1078 data
1079 |> maybe_fix_user_url
1080 end
1081 end