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