e8df112b9c4d78fab98726f1b73d23ef7c7372f8
[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 {_, 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, _}} <- {:follow_state_update, Utils.update_follow_state(activity, "accept")} do
466 ActivityPub.accept(%{
467 to: [follower.ap_id],
468 actor: followed,
469 object: data,
470 local: true
471 })
472 else
473 {:user_blocked, true} ->
474 {:ok, _} = Utils.update_follow_state(activity, "reject")
475
476 ActivityPub.reject(%{
477 to: [follower.ap_id],
478 actor: followed,
479 object: data,
480 local: true
481 })
482
483 {:follow, {:error, _}} ->
484 {:ok, _} = Utils.update_follow_state(activity, "reject")
485
486 ActivityPub.reject(%{
487 to: [follower.ap_id],
488 actor: followed,
489 object: data,
490 local: true
491 })
492
493 {:user_locked, true} ->
494 :noop
495 end
496
497 {:ok, activity}
498 else
499 _e ->
500 :error
501 end
502 end
503
504 def handle_incoming(
505 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data
506 ) do
507 with actor <- Containment.get_actor(data),
508 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
509 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
510 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
511 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
512 {:ok, _follower} = User.follow(follower, followed) do
513 ActivityPub.accept(%{
514 to: follow_activity.data["to"],
515 type: "Accept",
516 actor: followed,
517 object: follow_activity.data["id"],
518 local: false
519 })
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