66fa7c0b3fe2c4528924b9418dd1c2df69311a2a
[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.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 {:user_blocked, false} <-
462 {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
463 {:user_locked, false} <- {:user_locked, User.locked?(followed)},
464 {:follow, {:ok, follower}} <- {:follow, User.follow(follower, followed)} do
465 ActivityPub.accept(%{
466 to: [follower.ap_id],
467 actor: followed,
468 object: data,
469 local: true
470 })
471 else
472 {:user_blocked, true} ->
473 {:ok, _} = Utils.update_follow_state(activity, "reject")
474
475 ActivityPub.reject(%{
476 to: [follower.ap_id],
477 actor: followed,
478 object: data,
479 local: true
480 })
481
482 {:follow, {:error, _}} ->
483 {:ok, _} = Utils.update_follow_state(activity, "reject")
484
485 ActivityPub.reject(%{
486 to: [follower.ap_id],
487 actor: followed,
488 object: data,
489 local: true
490 })
491
492 {:user_locked, true} ->
493 :noop
494 end
495
496 {:ok, activity}
497 else
498 _e ->
499 :error
500 end
501 end
502
503 def handle_incoming(
504 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data
505 ) do
506 with actor <- Containment.get_actor(data),
507 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
508 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
509 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
510 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
511 {:ok, activity} <-
512 ActivityPub.accept(%{
513 to: follow_activity.data["to"],
514 type: "Accept",
515 actor: followed,
516 object: follow_activity.data["id"],
517 local: false
518 }) do
519 if not User.following?(follower, followed) do
520 {:ok, _follower} = User.follow(follower, followed)
521 end
522
523 {:ok, activity}
524 else
525 _e -> :error
526 end
527 end
528
529 def handle_incoming(
530 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data
531 ) do
532 with actor <- Containment.get_actor(data),
533 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
534 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
535 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
536 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
537 {:ok, activity} <-
538 ActivityPub.reject(%{
539 to: follow_activity.data["to"],
540 type: "Reject",
541 actor: followed,
542 object: follow_activity.data["id"],
543 local: false
544 }) do
545 User.unfollow(follower, followed)
546
547 {:ok, activity}
548 else
549 _e -> :error
550 end
551 end
552
553 def handle_incoming(
554 %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data
555 ) do
556 with actor <- Containment.get_actor(data),
557 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
558 {:ok, object} <- get_obj_helper(object_id),
559 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
560 {:ok, activity}
561 else
562 _e -> :error
563 end
564 end
565
566 def handle_incoming(
567 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
568 ) do
569 with actor <- Containment.get_actor(data),
570 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
571 {:ok, object} <- get_obj_helper(object_id),
572 public <- Visibility.is_public?(data),
573 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
574 {:ok, activity}
575 else
576 _e -> :error
577 end
578 end
579
580 def handle_incoming(
581 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
582 data
583 )
584 when object_type in ["Person", "Application", "Service", "Organization"] do
585 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
586 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
587
588 banner = new_user_data[:info]["banner"]
589 locked = new_user_data[:info]["locked"] || false
590
591 update_data =
592 new_user_data
593 |> Map.take([:name, :bio, :avatar])
594 |> Map.put(:info, %{"banner" => banner, "locked" => locked})
595
596 actor
597 |> User.upgrade_changeset(update_data)
598 |> User.update_and_set_cache()
599
600 ActivityPub.update(%{
601 local: false,
602 to: data["to"] || [],
603 cc: data["cc"] || [],
604 object: object,
605 actor: actor_id
606 })
607 else
608 e ->
609 Logger.error(e)
610 :error
611 end
612 end
613
614 # TODO: We presently assume that any actor on the same origin domain as the object being
615 # deleted has the rights to delete that object. A better way to validate whether or not
616 # the object should be deleted is to refetch the object URI, which should return either
617 # an error or a tombstone. This would allow us to verify that a deletion actually took
618 # place.
619 def handle_incoming(
620 %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data
621 ) do
622 object_id = Utils.get_ap_id(object_id)
623
624 with actor <- Containment.get_actor(data),
625 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
626 {:ok, object} <- get_obj_helper(object_id),
627 :ok <- Containment.contain_origin(actor.ap_id, object.data),
628 {:ok, activity} <- ActivityPub.delete(object, false) do
629 {:ok, activity}
630 else
631 _e -> :error
632 end
633 end
634
635 def handle_incoming(
636 %{
637 "type" => "Undo",
638 "object" => %{"type" => "Announce", "object" => object_id},
639 "actor" => _actor,
640 "id" => id
641 } = data
642 ) do
643 with actor <- Containment.get_actor(data),
644 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
645 {:ok, object} <- get_obj_helper(object_id),
646 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
647 {:ok, activity}
648 else
649 _e -> :error
650 end
651 end
652
653 def handle_incoming(
654 %{
655 "type" => "Undo",
656 "object" => %{"type" => "Follow", "object" => followed},
657 "actor" => follower,
658 "id" => id
659 } = _data
660 ) do
661 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
662 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
663 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
664 User.unfollow(follower, followed)
665 {:ok, activity}
666 else
667 _e -> :error
668 end
669 end
670
671 def handle_incoming(
672 %{
673 "type" => "Undo",
674 "object" => %{"type" => "Block", "object" => blocked},
675 "actor" => blocker,
676 "id" => id
677 } = _data
678 ) do
679 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
680 %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
681 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
682 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
683 User.unblock(blocker, blocked)
684 {:ok, activity}
685 else
686 _e -> :error
687 end
688 end
689
690 def handle_incoming(
691 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data
692 ) do
693 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
694 %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
695 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
696 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
697 User.unfollow(blocker, blocked)
698 User.block(blocker, blocked)
699 {:ok, activity}
700 else
701 _e -> :error
702 end
703 end
704
705 def handle_incoming(
706 %{
707 "type" => "Undo",
708 "object" => %{"type" => "Like", "object" => object_id},
709 "actor" => _actor,
710 "id" => id
711 } = data
712 ) do
713 with actor <- Containment.get_actor(data),
714 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
715 {:ok, object} <- get_obj_helper(object_id),
716 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
717 {:ok, activity}
718 else
719 _e -> :error
720 end
721 end
722
723 def handle_incoming(_), do: :error
724
725 def get_obj_helper(id) do
726 if object = Object.normalize(id), do: {:ok, object}, else: nil
727 end
728
729 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
730 with false <- String.starts_with?(in_reply_to, "http"),
731 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
732 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
733 else
734 _e -> object
735 end
736 end
737
738 def set_reply_to_uri(obj), do: obj
739
740 # Prepares the object of an outgoing create activity.
741 def prepare_object(object) do
742 object
743 |> set_sensitive
744 |> add_hashtags
745 |> add_mention_tags
746 |> add_emoji_tags
747 |> add_attributed_to
748 |> add_likes
749 |> prepare_attachments
750 |> set_conversation
751 |> set_reply_to_uri
752 |> strip_internal_fields
753 |> strip_internal_tags
754 |> set_type
755 end
756
757 # @doc
758 # """
759 # internal -> Mastodon
760 # """
761
762 def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
763 object =
764 Object.normalize(object_id).data
765 |> prepare_object
766
767 data =
768 data
769 |> Map.put("object", object)
770 |> Map.merge(Utils.make_json_ld_header())
771
772 {:ok, data}
773 end
774
775 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
776 # because of course it does.
777 def prepare_outgoing(%{"type" => "Accept"} = data) do
778 with follow_activity <- Activity.normalize(data["object"]) do
779 object = %{
780 "actor" => follow_activity.actor,
781 "object" => follow_activity.data["object"],
782 "id" => follow_activity.data["id"],
783 "type" => "Follow"
784 }
785
786 data =
787 data
788 |> Map.put("object", object)
789 |> Map.merge(Utils.make_json_ld_header())
790
791 {:ok, data}
792 end
793 end
794
795 def prepare_outgoing(%{"type" => "Reject"} = data) do
796 with follow_activity <- Activity.normalize(data["object"]) do
797 object = %{
798 "actor" => follow_activity.actor,
799 "object" => follow_activity.data["object"],
800 "id" => follow_activity.data["id"],
801 "type" => "Follow"
802 }
803
804 data =
805 data
806 |> Map.put("object", object)
807 |> Map.merge(Utils.make_json_ld_header())
808
809 {:ok, data}
810 end
811 end
812
813 def prepare_outgoing(%{"type" => _type} = data) do
814 data =
815 data
816 |> strip_internal_fields
817 |> maybe_fix_object_url
818 |> Map.merge(Utils.make_json_ld_header())
819
820 {:ok, data}
821 end
822
823 def maybe_fix_object_url(data) do
824 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
825 case get_obj_helper(data["object"]) do
826 {:ok, relative_object} ->
827 if relative_object.data["external_url"] do
828 _data =
829 data
830 |> Map.put("object", relative_object.data["external_url"])
831 else
832 data
833 end
834
835 e ->
836 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
837 data
838 end
839 else
840 data
841 end
842 end
843
844 def add_hashtags(object) do
845 tags =
846 (object["tag"] || [])
847 |> Enum.map(fn
848 # Expand internal representation tags into AS2 tags.
849 tag when is_binary(tag) ->
850 %{
851 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
852 "name" => "##{tag}",
853 "type" => "Hashtag"
854 }
855
856 # Do not process tags which are already AS2 tag objects.
857 tag when is_map(tag) ->
858 tag
859 end)
860
861 object
862 |> Map.put("tag", tags)
863 end
864
865 def add_mention_tags(object) do
866 mentions =
867 object
868 |> Utils.get_notified_from_object()
869 |> Enum.map(fn user ->
870 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
871 end)
872
873 tags = object["tag"] || []
874
875 object
876 |> Map.put("tag", tags ++ mentions)
877 end
878
879 def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
880 user_info = add_emoji_tags(user_info)
881
882 object
883 |> Map.put(:info, user_info)
884 end
885
886 # TODO: we should probably send mtime instead of unix epoch time for updated
887 def add_emoji_tags(%{"emoji" => emoji} = object) do
888 tags = object["tag"] || []
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 add_emoji_tags(object) do
907 object
908 end
909
910 def set_conversation(object) do
911 Map.put(object, "conversation", object["context"])
912 end
913
914 def set_sensitive(object) do
915 tags = object["tag"] || []
916 Map.put(object, "sensitive", "nsfw" in tags)
917 end
918
919 def set_type(%{"type" => "Answer"} = object) do
920 Map.put(object, "type", "Note")
921 end
922
923 def set_type(object), do: object
924
925 def add_attributed_to(object) do
926 attributed_to = object["attributedTo"] || object["actor"]
927
928 object
929 |> Map.put("attributedTo", attributed_to)
930 end
931
932 def add_likes(%{"id" => id, "like_count" => likes} = object) do
933 likes = %{
934 "id" => "#{id}/likes",
935 "first" => "#{id}/likes?page=1",
936 "type" => "OrderedCollection",
937 "totalItems" => likes
938 }
939
940 object
941 |> Map.put("likes", likes)
942 end
943
944 def add_likes(object) do
945 object
946 end
947
948 def prepare_attachments(object) do
949 attachments =
950 (object["attachment"] || [])
951 |> Enum.map(fn data ->
952 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
953 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
954 end)
955
956 object
957 |> Map.put("attachment", attachments)
958 end
959
960 defp strip_internal_fields(object) do
961 object
962 |> Map.drop([
963 "like_count",
964 "announcements",
965 "announcement_count",
966 "emoji",
967 "context_id",
968 "deleted_activity_id"
969 ])
970 end
971
972 defp strip_internal_tags(%{"tag" => tags} = object) do
973 tags =
974 tags
975 |> Enum.filter(fn x -> is_map(x) end)
976
977 object
978 |> Map.put("tag", tags)
979 end
980
981 defp strip_internal_tags(object), do: object
982
983 def perform(:user_upgrade, user) do
984 # we pass a fake user so that the followers collection is stripped away
985 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
986
987 q =
988 from(
989 u in User,
990 where: ^old_follower_address in u.following,
991 update: [
992 set: [
993 following:
994 fragment(
995 "array_replace(?,?,?)",
996 u.following,
997 ^old_follower_address,
998 ^user.follower_address
999 )
1000 ]
1001 ]
1002 )
1003
1004 Repo.update_all(q, [])
1005
1006 maybe_retire_websub(user.ap_id)
1007
1008 q =
1009 from(
1010 a in Activity,
1011 where: ^old_follower_address in a.recipients,
1012 update: [
1013 set: [
1014 recipients:
1015 fragment(
1016 "array_replace(?,?,?)",
1017 a.recipients,
1018 ^old_follower_address,
1019 ^user.follower_address
1020 )
1021 ]
1022 ]
1023 )
1024
1025 Repo.update_all(q, [])
1026 end
1027
1028 def upgrade_user_from_ap_id(ap_id) do
1029 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1030 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1031 already_ap <- User.ap_enabled?(user),
1032 {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
1033 unless already_ap do
1034 PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
1035 end
1036
1037 {:ok, user}
1038 else
1039 %User{} = user -> {:ok, user}
1040 e -> e
1041 end
1042 end
1043
1044 def maybe_retire_websub(ap_id) do
1045 # some sanity checks
1046 if is_binary(ap_id) && String.length(ap_id) > 8 do
1047 q =
1048 from(
1049 ws in Pleroma.Web.Websub.WebsubClientSubscription,
1050 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
1051 )
1052
1053 Repo.delete_all(q)
1054 end
1055 end
1056
1057 def maybe_fix_user_url(data) do
1058 if is_map(data["url"]) do
1059 Map.put(data, "url", data["url"]["href"])
1060 else
1061 data
1062 end
1063 end
1064
1065 def maybe_fix_user_object(data) do
1066 data
1067 |> maybe_fix_user_url
1068 end
1069 end