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