Fix merge conflict
[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_id
765 |> Object.normalize()
766 |> Map.get(:data)
767 |> prepare_object
768
769 data =
770 data
771 |> Map.put("object", object)
772 |> Map.merge(Utils.make_json_ld_header())
773 |> Map.delete("bcc")
774
775 {:ok, data}
776 end
777
778 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
779 # because of course it does.
780 def prepare_outgoing(%{"type" => "Accept"} = data) do
781 with follow_activity <- Activity.normalize(data["object"]) do
782 object = %{
783 "actor" => follow_activity.actor,
784 "object" => follow_activity.data["object"],
785 "id" => follow_activity.data["id"],
786 "type" => "Follow"
787 }
788
789 data =
790 data
791 |> Map.put("object", object)
792 |> Map.merge(Utils.make_json_ld_header())
793
794 {:ok, data}
795 end
796 end
797
798 def prepare_outgoing(%{"type" => "Reject"} = data) do
799 with follow_activity <- Activity.normalize(data["object"]) do
800 object = %{
801 "actor" => follow_activity.actor,
802 "object" => follow_activity.data["object"],
803 "id" => follow_activity.data["id"],
804 "type" => "Follow"
805 }
806
807 data =
808 data
809 |> Map.put("object", object)
810 |> Map.merge(Utils.make_json_ld_header())
811
812 {:ok, data}
813 end
814 end
815
816 def prepare_outgoing(%{"type" => _type} = data) do
817 data =
818 data
819 |> strip_internal_fields
820 |> maybe_fix_object_url
821 |> Map.merge(Utils.make_json_ld_header())
822
823 {:ok, data}
824 end
825
826 def maybe_fix_object_url(data) do
827 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
828 case get_obj_helper(data["object"]) do
829 {:ok, relative_object} ->
830 if relative_object.data["external_url"] do
831 _data =
832 data
833 |> Map.put("object", relative_object.data["external_url"])
834 else
835 data
836 end
837
838 e ->
839 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
840 data
841 end
842 else
843 data
844 end
845 end
846
847 def add_hashtags(object) do
848 tags =
849 (object["tag"] || [])
850 |> Enum.map(fn
851 # Expand internal representation tags into AS2 tags.
852 tag when is_binary(tag) ->
853 %{
854 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
855 "name" => "##{tag}",
856 "type" => "Hashtag"
857 }
858
859 # Do not process tags which are already AS2 tag objects.
860 tag when is_map(tag) ->
861 tag
862 end)
863
864 object
865 |> Map.put("tag", tags)
866 end
867
868 def add_mention_tags(object) do
869 mentions =
870 object
871 |> Utils.get_notified_from_object()
872 |> Enum.map(fn user ->
873 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
874 end)
875
876 tags = object["tag"] || []
877
878 object
879 |> Map.put("tag", tags ++ mentions)
880 end
881
882 def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
883 user_info = add_emoji_tags(user_info)
884
885 object
886 |> Map.put(:info, user_info)
887 end
888
889 # TODO: we should probably send mtime instead of unix epoch time for updated
890 def add_emoji_tags(%{"emoji" => emoji} = object) do
891 tags = object["tag"] || []
892
893 out =
894 emoji
895 |> Enum.map(fn {name, url} ->
896 %{
897 "icon" => %{"url" => url, "type" => "Image"},
898 "name" => ":" <> name <> ":",
899 "type" => "Emoji",
900 "updated" => "1970-01-01T00:00:00Z",
901 "id" => url
902 }
903 end)
904
905 object
906 |> Map.put("tag", tags ++ out)
907 end
908
909 def add_emoji_tags(object) do
910 object
911 end
912
913 def set_conversation(object) do
914 Map.put(object, "conversation", object["context"])
915 end
916
917 def set_sensitive(object) do
918 tags = object["tag"] || []
919 Map.put(object, "sensitive", "nsfw" in tags)
920 end
921
922 def set_type(%{"type" => "Answer"} = object) do
923 Map.put(object, "type", "Note")
924 end
925
926 def set_type(object), do: object
927
928 def add_attributed_to(object) do
929 attributed_to = object["attributedTo"] || object["actor"]
930
931 object
932 |> Map.put("attributedTo", attributed_to)
933 end
934
935 def add_likes(%{"id" => id, "like_count" => likes} = object) do
936 likes = %{
937 "id" => "#{id}/likes",
938 "first" => "#{id}/likes?page=1",
939 "type" => "OrderedCollection",
940 "totalItems" => likes
941 }
942
943 object
944 |> Map.put("likes", likes)
945 end
946
947 def add_likes(object) do
948 object
949 end
950
951 def prepare_attachments(object) do
952 attachments =
953 (object["attachment"] || [])
954 |> Enum.map(fn data ->
955 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
956 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
957 end)
958
959 object
960 |> Map.put("attachment", attachments)
961 end
962
963 defp strip_internal_fields(object) do
964 object
965 |> Map.drop([
966 "like_count",
967 "announcements",
968 "announcement_count",
969 "emoji",
970 "context_id",
971 "deleted_activity_id"
972 ])
973 end
974
975 defp strip_internal_tags(%{"tag" => tags} = object) do
976 tags =
977 tags
978 |> Enum.filter(fn x -> is_map(x) end)
979
980 object
981 |> Map.put("tag", tags)
982 end
983
984 defp strip_internal_tags(object), do: object
985
986 def perform(:user_upgrade, user) do
987 # we pass a fake user so that the followers collection is stripped away
988 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
989
990 q =
991 from(
992 u in User,
993 where: ^old_follower_address in u.following,
994 update: [
995 set: [
996 following:
997 fragment(
998 "array_replace(?,?,?)",
999 u.following,
1000 ^old_follower_address,
1001 ^user.follower_address
1002 )
1003 ]
1004 ]
1005 )
1006
1007 Repo.update_all(q, [])
1008
1009 maybe_retire_websub(user.ap_id)
1010
1011 q =
1012 from(
1013 a in Activity,
1014 where: ^old_follower_address in a.recipients,
1015 update: [
1016 set: [
1017 recipients:
1018 fragment(
1019 "array_replace(?,?,?)",
1020 a.recipients,
1021 ^old_follower_address,
1022 ^user.follower_address
1023 )
1024 ]
1025 ]
1026 )
1027
1028 Repo.update_all(q, [])
1029 end
1030
1031 def upgrade_user_from_ap_id(ap_id) do
1032 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1033 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1034 already_ap <- User.ap_enabled?(user),
1035 {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
1036 unless already_ap do
1037 PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
1038 end
1039
1040 {:ok, user}
1041 else
1042 %User{} = user -> {:ok, user}
1043 e -> e
1044 end
1045 end
1046
1047 def maybe_retire_websub(ap_id) do
1048 # some sanity checks
1049 if is_binary(ap_id) && String.length(ap_id) > 8 do
1050 q =
1051 from(
1052 ws in Pleroma.Web.Websub.WebsubClientSubscription,
1053 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
1054 )
1055
1056 Repo.delete_all(q)
1057 end
1058 end
1059
1060 def maybe_fix_user_url(data) do
1061 if is_map(data["url"]) do
1062 Map.put(data, "url", data["url"]["href"])
1063 else
1064 data
1065 end
1066 end
1067
1068 def maybe_fix_user_object(data) do
1069 data
1070 |> maybe_fix_user_url
1071 end
1072 end