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