Merge branch 'tests/activity_pub' into 'develop'
[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, "name" => _} = object, options)
337 when is_binary(reply_id) do
338 reply =
339 with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
340 {:ok, object} <- get_obj_helper(reply_id, options) do
341 object
342 end
343
344 if reply && reply.data["type"] == "Question" do
345 Map.put(object, "type", "Answer")
346 else
347 object
348 end
349 end
350
351 def fix_type(object, _), do: object
352
353 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
354 with true <- id =~ "follows",
355 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
356 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
357 {:ok, activity}
358 else
359 _ -> {:error, nil}
360 end
361 end
362
363 defp mastodon_follow_hack(_, _), do: {:error, nil}
364
365 defp get_follow_activity(follow_object, followed) do
366 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
367 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
368 {:ok, activity}
369 else
370 # Can't find the activity. This might a Mastodon 2.3 "Accept"
371 {:activity, nil} ->
372 mastodon_follow_hack(follow_object, followed)
373
374 _ ->
375 {:error, nil}
376 end
377 end
378
379 def handle_incoming(data, options \\ [])
380
381 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
382 # with nil ID.
383 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
384 with context <- data["context"] || Utils.generate_context_id(),
385 content <- data["content"] || "",
386 %User{} = actor <- User.get_cached_by_ap_id(actor),
387
388 # Reduce the object list to find the reported user.
389 %User{} = account <-
390 Enum.reduce_while(objects, nil, fn ap_id, _ ->
391 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
392 {:halt, user}
393 else
394 _ -> {:cont, nil}
395 end
396 end),
397
398 # Remove the reported user from the object list.
399 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
400 params = %{
401 actor: actor,
402 context: context,
403 account: account,
404 statuses: statuses,
405 content: content,
406 additional: %{
407 "cc" => [account.ap_id]
408 }
409 }
410
411 ActivityPub.flag(params)
412 end
413 end
414
415 # disallow objects with bogus IDs
416 def handle_incoming(%{"id" => nil}, _options), do: :error
417 def handle_incoming(%{"id" => ""}, _options), do: :error
418 # length of https:// = 8, should validate better, but good enough for now.
419 def handle_incoming(%{"id" => id}, _options) when not (is_binary(id) and length(id) > 8),
420 do: :error
421
422 # TODO: validate those with a Ecto scheme
423 # - tags
424 # - emoji
425 def handle_incoming(
426 %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
427 options
428 )
429 when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do
430 actor = Containment.get_actor(data)
431
432 data =
433 Map.put(data, "actor", actor)
434 |> fix_addressing
435
436 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
437 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
438 options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
439 object = fix_object(data["object"], options)
440
441 params = %{
442 to: data["to"],
443 object: object,
444 actor: user,
445 context: object["conversation"],
446 local: false,
447 published: data["published"],
448 additional:
449 Map.take(data, [
450 "cc",
451 "directMessage",
452 "id"
453 ])
454 }
455
456 ActivityPub.create(params)
457 else
458 %Activity{} = activity -> {:ok, activity}
459 _e -> :error
460 end
461 end
462
463 def handle_incoming(
464 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
465 _options
466 ) do
467 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
468 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
469 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
470 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
471 {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
472 {_, false} <- {:user_locked, User.locked?(followed)},
473 {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
474 {_, {:ok, _}} <-
475 {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do
476 ActivityPub.accept(%{
477 to: [follower.ap_id],
478 actor: followed,
479 object: data,
480 local: true
481 })
482 else
483 {:user_blocked, true} ->
484 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
485
486 ActivityPub.reject(%{
487 to: [follower.ap_id],
488 actor: followed,
489 object: data,
490 local: true
491 })
492
493 {:follow, {:error, _}} ->
494 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
495
496 ActivityPub.reject(%{
497 to: [follower.ap_id],
498 actor: followed,
499 object: data,
500 local: true
501 })
502
503 {:user_locked, true} ->
504 :noop
505 end
506
507 {:ok, activity}
508 else
509 _e ->
510 :error
511 end
512 end
513
514 def handle_incoming(
515 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
516 _options
517 ) do
518 with actor <- Containment.get_actor(data),
519 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
520 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
521 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
522 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
523 {:ok, _follower} = User.follow(follower, followed) do
524 ActivityPub.accept(%{
525 to: follow_activity.data["to"],
526 type: "Accept",
527 actor: followed,
528 object: follow_activity.data["id"],
529 local: false
530 })
531 else
532 _e -> :error
533 end
534 end
535
536 def handle_incoming(
537 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
538 _options
539 ) do
540 with actor <- Containment.get_actor(data),
541 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
542 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
543 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
544 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
545 {:ok, activity} <-
546 ActivityPub.reject(%{
547 to: follow_activity.data["to"],
548 type: "Reject",
549 actor: followed,
550 object: follow_activity.data["id"],
551 local: false
552 }) do
553 User.unfollow(follower, followed)
554
555 {:ok, activity}
556 else
557 _e -> :error
558 end
559 end
560
561 def handle_incoming(
562 %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
563 _options
564 ) do
565 with actor <- Containment.get_actor(data),
566 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
567 {:ok, object} <- get_obj_helper(object_id),
568 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
569 {:ok, activity}
570 else
571 _e -> :error
572 end
573 end
574
575 def handle_incoming(
576 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
577 _options
578 ) do
579 with actor <- Containment.get_actor(data),
580 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
581 {:ok, object} <- get_obj_helper(object_id),
582 public <- Visibility.is_public?(data),
583 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
584 {:ok, activity}
585 else
586 _e -> :error
587 end
588 end
589
590 def handle_incoming(
591 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
592 data,
593 _options
594 )
595 when object_type in ["Person", "Application", "Service", "Organization"] do
596 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
597 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
598
599 banner = new_user_data[:info][:banner]
600 locked = new_user_data[:info][:locked] || false
601 attachment = get_in(new_user_data, [:info, :source_data, "attachment"]) || []
602
603 fields =
604 attachment
605 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
606 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
607
608 update_data =
609 new_user_data
610 |> Map.take([:name, :bio, :avatar])
611 |> Map.put(:info, %{banner: banner, locked: locked, fields: fields})
612
613 actor
614 |> User.upgrade_changeset(update_data, true)
615 |> User.update_and_set_cache()
616
617 ActivityPub.update(%{
618 local: false,
619 to: data["to"] || [],
620 cc: data["cc"] || [],
621 object: object,
622 actor: actor_id
623 })
624 else
625 e ->
626 Logger.error(e)
627 :error
628 end
629 end
630
631 # TODO: We presently assume that any actor on the same origin domain as the object being
632 # deleted has the rights to delete that object. A better way to validate whether or not
633 # the object should be deleted is to refetch the object URI, which should return either
634 # an error or a tombstone. This would allow us to verify that a deletion actually took
635 # place.
636 def handle_incoming(
637 %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = data,
638 _options
639 ) do
640 object_id = Utils.get_ap_id(object_id)
641
642 with actor <- Containment.get_actor(data),
643 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
644 {:ok, object} <- get_obj_helper(object_id),
645 :ok <- Containment.contain_origin(actor.ap_id, object.data),
646 {:ok, activity} <- ActivityPub.delete(object, false) do
647 {:ok, activity}
648 else
649 nil ->
650 case User.get_cached_by_ap_id(object_id) do
651 %User{ap_id: ^actor} = user ->
652 User.delete(user)
653
654 nil ->
655 :error
656 end
657
658 _e ->
659 :error
660 end
661 end
662
663 def handle_incoming(
664 %{
665 "type" => "Undo",
666 "object" => %{"type" => "Announce", "object" => object_id},
667 "actor" => _actor,
668 "id" => id
669 } = data,
670 _options
671 ) do
672 with actor <- Containment.get_actor(data),
673 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
674 {:ok, object} <- get_obj_helper(object_id),
675 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
676 {:ok, activity}
677 else
678 _e -> :error
679 end
680 end
681
682 def handle_incoming(
683 %{
684 "type" => "Undo",
685 "object" => %{"type" => "Follow", "object" => followed},
686 "actor" => follower,
687 "id" => id
688 } = _data,
689 _options
690 ) do
691 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
692 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
693 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
694 User.unfollow(follower, followed)
695 {:ok, activity}
696 else
697 _e -> :error
698 end
699 end
700
701 def handle_incoming(
702 %{
703 "type" => "Undo",
704 "object" => %{"type" => "Block", "object" => blocked},
705 "actor" => blocker,
706 "id" => id
707 } = _data,
708 _options
709 ) do
710 with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
711 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
712 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
713 User.unblock(blocker, blocked)
714 {:ok, activity}
715 else
716 _e -> :error
717 end
718 end
719
720 def handle_incoming(
721 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
722 _options
723 ) do
724 with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
725 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
726 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
727 User.unfollow(blocker, blocked)
728 User.block(blocker, blocked)
729 {:ok, activity}
730 else
731 _e -> :error
732 end
733 end
734
735 def handle_incoming(
736 %{
737 "type" => "Undo",
738 "object" => %{"type" => "Like", "object" => object_id},
739 "actor" => _actor,
740 "id" => id
741 } = data,
742 _options
743 ) do
744 with actor <- Containment.get_actor(data),
745 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
746 {:ok, object} <- get_obj_helper(object_id),
747 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
748 {:ok, activity}
749 else
750 _e -> :error
751 end
752 end
753
754 def handle_incoming(_, _), do: :error
755
756 def get_obj_helper(id, options \\ []) do
757 if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil
758 end
759
760 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
761 with false <- String.starts_with?(in_reply_to, "http"),
762 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
763 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
764 else
765 _e -> object
766 end
767 end
768
769 def set_reply_to_uri(obj), do: obj
770
771 # Prepares the object of an outgoing create activity.
772 def prepare_object(object) do
773 object
774 |> set_sensitive
775 |> add_hashtags
776 |> add_mention_tags
777 |> add_emoji_tags
778 |> add_attributed_to
779 |> prepare_attachments
780 |> set_conversation
781 |> set_reply_to_uri
782 |> strip_internal_fields
783 |> strip_internal_tags
784 |> set_type
785 end
786
787 # @doc
788 # """
789 # internal -> Mastodon
790 # """
791
792 def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
793 object =
794 object_id
795 |> Object.normalize()
796 |> Map.get(:data)
797 |> prepare_object
798
799 data =
800 data
801 |> Map.put("object", object)
802 |> Map.merge(Utils.make_json_ld_header())
803 |> Map.delete("bcc")
804
805 {:ok, data}
806 end
807
808 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
809 # because of course it does.
810 def prepare_outgoing(%{"type" => "Accept"} = data) do
811 with follow_activity <- Activity.normalize(data["object"]) do
812 object = %{
813 "actor" => follow_activity.actor,
814 "object" => follow_activity.data["object"],
815 "id" => follow_activity.data["id"],
816 "type" => "Follow"
817 }
818
819 data =
820 data
821 |> Map.put("object", object)
822 |> Map.merge(Utils.make_json_ld_header())
823
824 {:ok, data}
825 end
826 end
827
828 def prepare_outgoing(%{"type" => "Reject"} = data) do
829 with follow_activity <- Activity.normalize(data["object"]) do
830 object = %{
831 "actor" => follow_activity.actor,
832 "object" => follow_activity.data["object"],
833 "id" => follow_activity.data["id"],
834 "type" => "Follow"
835 }
836
837 data =
838 data
839 |> Map.put("object", object)
840 |> Map.merge(Utils.make_json_ld_header())
841
842 {:ok, data}
843 end
844 end
845
846 def prepare_outgoing(%{"type" => _type} = data) do
847 data =
848 data
849 |> strip_internal_fields
850 |> maybe_fix_object_url
851 |> Map.merge(Utils.make_json_ld_header())
852
853 {:ok, data}
854 end
855
856 def maybe_fix_object_url(data) do
857 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
858 case get_obj_helper(data["object"]) do
859 {:ok, relative_object} ->
860 if relative_object.data["external_url"] do
861 _data =
862 data
863 |> Map.put("object", relative_object.data["external_url"])
864 else
865 data
866 end
867
868 e ->
869 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
870 data
871 end
872 else
873 data
874 end
875 end
876
877 def add_hashtags(object) do
878 tags =
879 (object["tag"] || [])
880 |> Enum.map(fn
881 # Expand internal representation tags into AS2 tags.
882 tag when is_binary(tag) ->
883 %{
884 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
885 "name" => "##{tag}",
886 "type" => "Hashtag"
887 }
888
889 # Do not process tags which are already AS2 tag objects.
890 tag when is_map(tag) ->
891 tag
892 end)
893
894 object
895 |> Map.put("tag", tags)
896 end
897
898 def add_mention_tags(object) do
899 mentions =
900 object
901 |> Utils.get_notified_from_object()
902 |> Enum.map(fn user ->
903 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
904 end)
905
906 tags = object["tag"] || []
907
908 object
909 |> Map.put("tag", tags ++ mentions)
910 end
911
912 def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
913 user_info = add_emoji_tags(user_info)
914
915 object
916 |> Map.put(:info, user_info)
917 end
918
919 # TODO: we should probably send mtime instead of unix epoch time for updated
920 def add_emoji_tags(%{"emoji" => emoji} = object) do
921 tags = object["tag"] || []
922
923 out =
924 emoji
925 |> Enum.map(fn {name, url} ->
926 %{
927 "icon" => %{"url" => url, "type" => "Image"},
928 "name" => ":" <> name <> ":",
929 "type" => "Emoji",
930 "updated" => "1970-01-01T00:00:00Z",
931 "id" => url
932 }
933 end)
934
935 object
936 |> Map.put("tag", tags ++ out)
937 end
938
939 def add_emoji_tags(object) do
940 object
941 end
942
943 def set_conversation(object) do
944 Map.put(object, "conversation", object["context"])
945 end
946
947 def set_sensitive(object) do
948 tags = object["tag"] || []
949 Map.put(object, "sensitive", "nsfw" in tags)
950 end
951
952 def set_type(%{"type" => "Answer"} = object) do
953 Map.put(object, "type", "Note")
954 end
955
956 def set_type(object), do: object
957
958 def add_attributed_to(object) do
959 attributed_to = object["attributedTo"] || object["actor"]
960
961 object
962 |> Map.put("attributedTo", attributed_to)
963 end
964
965 def prepare_attachments(object) do
966 attachments =
967 (object["attachment"] || [])
968 |> Enum.map(fn data ->
969 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
970 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
971 end)
972
973 object
974 |> Map.put("attachment", attachments)
975 end
976
977 defp strip_internal_fields(object) do
978 object
979 |> Map.drop([
980 "likes",
981 "like_count",
982 "announcements",
983 "announcement_count",
984 "emoji",
985 "context_id",
986 "deleted_activity_id"
987 ])
988 end
989
990 defp strip_internal_tags(%{"tag" => tags} = object) do
991 tags =
992 tags
993 |> Enum.filter(fn x -> is_map(x) end)
994
995 object
996 |> Map.put("tag", tags)
997 end
998
999 defp strip_internal_tags(object), do: object
1000
1001 def perform(:user_upgrade, user) do
1002 # we pass a fake user so that the followers collection is stripped away
1003 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1004
1005 q =
1006 from(
1007 u in User,
1008 where: ^old_follower_address in u.following,
1009 update: [
1010 set: [
1011 following:
1012 fragment(
1013 "array_replace(?,?,?)",
1014 u.following,
1015 ^old_follower_address,
1016 ^user.follower_address
1017 )
1018 ]
1019 ]
1020 )
1021
1022 Repo.update_all(q, [])
1023
1024 maybe_retire_websub(user.ap_id)
1025
1026 q =
1027 from(
1028 a in Activity,
1029 where: ^old_follower_address in a.recipients,
1030 update: [
1031 set: [
1032 recipients:
1033 fragment(
1034 "array_replace(?,?,?)",
1035 a.recipients,
1036 ^old_follower_address,
1037 ^user.follower_address
1038 )
1039 ]
1040 ]
1041 )
1042
1043 Repo.update_all(q, [])
1044 end
1045
1046 def upgrade_user_from_ap_id(ap_id) do
1047 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1048 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1049 already_ap <- User.ap_enabled?(user),
1050 {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
1051 unless already_ap do
1052 PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
1053 end
1054
1055 {:ok, user}
1056 else
1057 %User{} = user -> {:ok, user}
1058 e -> e
1059 end
1060 end
1061
1062 def maybe_retire_websub(ap_id) do
1063 # some sanity checks
1064 if is_binary(ap_id) && String.length(ap_id) > 8 do
1065 q =
1066 from(
1067 ws in Pleroma.Web.Websub.WebsubClientSubscription,
1068 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
1069 )
1070
1071 Repo.delete_all(q)
1072 end
1073 end
1074
1075 def maybe_fix_user_url(data) do
1076 if is_map(data["url"]) do
1077 Map.put(data, "url", data["url"]["href"])
1078 else
1079 data
1080 end
1081 end
1082
1083 def maybe_fix_user_object(data) do
1084 data
1085 |> maybe_fix_user_url
1086 end
1087 end