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