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