Update CHANGELOG
[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 require Pleroma.Constants
23
24 @doc """
25 Modifies an incoming AP object (mastodon format) to our internal format.
26 """
27 def fix_object(object, options \\ []) do
28 object
29 |> fix_actor
30 |> fix_url
31 |> fix_attachments
32 |> fix_context
33 |> fix_in_reply_to(options)
34 |> fix_emoji
35 |> fix_tag
36 |> fix_content_map
37 |> fix_likes
38 |> fix_addressing
39 |> fix_summary
40 |> fix_type(options)
41 end
42
43 def fix_summary(%{"summary" => nil} = object) do
44 object
45 |> Map.put("summary", "")
46 end
47
48 def fix_summary(%{"summary" => _} = object) do
49 # summary is present, nothing to do
50 object
51 end
52
53 def fix_summary(object) do
54 object
55 |> Map.put("summary", "")
56 end
57
58 def fix_addressing_list(map, field) do
59 cond do
60 is_binary(map[field]) ->
61 Map.put(map, field, [map[field]])
62
63 is_nil(map[field]) ->
64 Map.put(map, field, [])
65
66 true ->
67 map
68 end
69 end
70
71 def fix_explicit_addressing(
72 %{"to" => to, "cc" => cc} = object,
73 explicit_mentions,
74 follower_collection
75 ) do
76 explicit_to =
77 to
78 |> Enum.filter(fn x -> x in explicit_mentions end)
79
80 explicit_cc =
81 to
82 |> Enum.filter(fn x -> x not in explicit_mentions end)
83
84 final_cc =
85 (cc ++ explicit_cc)
86 |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
87 |> Enum.uniq()
88
89 object
90 |> Map.put("to", explicit_to)
91 |> Map.put("cc", final_cc)
92 end
93
94 def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object
95
96 # if directMessage flag is set to true, leave the addressing alone
97 def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
98
99 def fix_explicit_addressing(object) do
100 explicit_mentions =
101 object
102 |> Utils.determine_explicit_mentions()
103
104 follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address
105
106 explicit_mentions = explicit_mentions ++ [Pleroma.Constants.as_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 Pleroma.Constants.as_public() in cc ->
119 to = to ++ [followers_collection]
120 Map.put(object, "to", to)
121
122 Pleroma.Constants.as_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 Federator.allowed_incoming_reply_depth?(options[: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 Federator.allowed_incoming_reply_depth?(options[:depth]) do
353 Object.normalize(reply_id, true)
354 end
355
356 if reply && (reply.data["type"] == "Question" and object["name"]) do
357 Map.put(object, "type", "Answer")
358 else
359 object
360 end
361 end
362
363 def fix_type(object, _), do: object
364
365 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
366 with true <- id =~ "follows",
367 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
368 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
369 {:ok, activity}
370 else
371 _ -> {:error, nil}
372 end
373 end
374
375 defp mastodon_follow_hack(_, _), do: {:error, nil}
376
377 defp get_follow_activity(follow_object, followed) do
378 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
379 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
380 {:ok, activity}
381 else
382 # Can't find the activity. This might a Mastodon 2.3 "Accept"
383 {:activity, nil} ->
384 mastodon_follow_hack(follow_object, followed)
385
386 _ ->
387 {:error, nil}
388 end
389 end
390
391 def handle_incoming(data, options \\ [])
392
393 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
394 # with nil ID.
395 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
396 with context <- data["context"] || Utils.generate_context_id(),
397 content <- data["content"] || "",
398 %User{} = actor <- User.get_cached_by_ap_id(actor),
399
400 # Reduce the object list to find the reported user.
401 %User{} = account <-
402 Enum.reduce_while(objects, nil, fn ap_id, _ ->
403 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
404 {:halt, user}
405 else
406 _ -> {:cont, nil}
407 end
408 end),
409
410 # Remove the reported user from the object list.
411 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
412 params = %{
413 actor: actor,
414 context: context,
415 account: account,
416 statuses: statuses,
417 content: content,
418 additional: %{
419 "cc" => [account.ap_id]
420 }
421 }
422
423 ActivityPub.flag(params)
424 end
425 end
426
427 # disallow objects with bogus IDs
428 def handle_incoming(%{"id" => nil}, _options), do: :error
429 def handle_incoming(%{"id" => ""}, _options), do: :error
430 # length of https:// = 8, should validate better, but good enough for now.
431 def handle_incoming(%{"id" => id}, _options) when not (is_binary(id) and length(id) > 8),
432 do: :error
433
434 # TODO: validate those with a Ecto scheme
435 # - tags
436 # - emoji
437 def handle_incoming(
438 %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
439 options
440 )
441 when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do
442 actor = Containment.get_actor(data)
443
444 data =
445 Map.put(data, "actor", actor)
446 |> fix_addressing
447
448 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
449 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
450 options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
451 object = fix_object(data["object"], options)
452
453 params = %{
454 to: data["to"],
455 object: object,
456 actor: user,
457 context: object["conversation"],
458 local: false,
459 published: data["published"],
460 additional:
461 Map.take(data, [
462 "cc",
463 "directMessage",
464 "id"
465 ])
466 }
467
468 ActivityPub.create(params)
469 else
470 %Activity{} = activity -> {:ok, activity}
471 _e -> :error
472 end
473 end
474
475 def handle_incoming(
476 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
477 _options
478 ) do
479 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
480 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
481 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
482 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
483 {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
484 {_, false} <- {:user_locked, User.locked?(followed)},
485 {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
486 {_, {:ok, _}} <-
487 {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do
488 ActivityPub.accept(%{
489 to: [follower.ap_id],
490 actor: followed,
491 object: data,
492 local: true
493 })
494 else
495 {:user_blocked, true} ->
496 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
497
498 ActivityPub.reject(%{
499 to: [follower.ap_id],
500 actor: followed,
501 object: data,
502 local: true
503 })
504
505 {:follow, {:error, _}} ->
506 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
507
508 ActivityPub.reject(%{
509 to: [follower.ap_id],
510 actor: followed,
511 object: data,
512 local: true
513 })
514
515 {:user_locked, true} ->
516 :noop
517 end
518
519 {:ok, activity}
520 else
521 _e ->
522 :error
523 end
524 end
525
526 def handle_incoming(
527 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
528 _options
529 ) do
530 with actor <- Containment.get_actor(data),
531 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
532 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
533 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
534 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
535 {:ok, _follower} = User.follow(follower, followed) do
536 ActivityPub.accept(%{
537 to: follow_activity.data["to"],
538 type: "Accept",
539 actor: followed,
540 object: follow_activity.data["id"],
541 local: false
542 })
543 else
544 _e -> :error
545 end
546 end
547
548 def handle_incoming(
549 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
550 _options
551 ) do
552 with actor <- Containment.get_actor(data),
553 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
554 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
555 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
556 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
557 {:ok, activity} <-
558 ActivityPub.reject(%{
559 to: follow_activity.data["to"],
560 type: "Reject",
561 actor: followed,
562 object: follow_activity.data["id"],
563 local: false
564 }) do
565 User.unfollow(follower, followed)
566
567 {:ok, activity}
568 else
569 _e -> :error
570 end
571 end
572
573 def handle_incoming(
574 %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
575 _options
576 ) do
577 with actor <- Containment.get_actor(data),
578 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
579 {:ok, object} <- get_obj_helper(object_id),
580 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
581 {:ok, activity}
582 else
583 _e -> :error
584 end
585 end
586
587 def handle_incoming(
588 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
589 _options
590 ) do
591 with actor <- Containment.get_actor(data),
592 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
593 {:ok, object} <- get_obj_helper(object_id),
594 public <- Visibility.is_public?(data),
595 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
596 {:ok, activity}
597 else
598 _e -> :error
599 end
600 end
601
602 def handle_incoming(
603 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
604 data,
605 _options
606 )
607 when object_type in ["Person", "Application", "Service", "Organization"] do
608 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
609 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
610
611 banner = new_user_data[:info]["banner"]
612 locked = new_user_data[:info]["locked"] || false
613
614 update_data =
615 new_user_data
616 |> Map.take([:name, :bio, :avatar])
617 |> Map.put(:info, %{"banner" => banner, "locked" => locked})
618
619 actor
620 |> User.upgrade_changeset(update_data)
621 |> User.update_and_set_cache()
622
623 ActivityPub.update(%{
624 local: false,
625 to: data["to"] || [],
626 cc: data["cc"] || [],
627 object: object,
628 actor: actor_id
629 })
630 else
631 e ->
632 Logger.error(e)
633 :error
634 end
635 end
636
637 # TODO: We presently assume that any actor on the same origin domain as the object being
638 # deleted has the rights to delete that object. A better way to validate whether or not
639 # the object should be deleted is to refetch the object URI, which should return either
640 # an error or a tombstone. This would allow us to verify that a deletion actually took
641 # place.
642 def handle_incoming(
643 %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = data,
644 _options
645 ) do
646 object_id = Utils.get_ap_id(object_id)
647
648 with actor <- Containment.get_actor(data),
649 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
650 {:ok, object} <- get_obj_helper(object_id),
651 :ok <- Containment.contain_origin(actor.ap_id, object.data),
652 {:ok, activity} <- ActivityPub.delete(object, false) do
653 {:ok, activity}
654 else
655 nil ->
656 case User.get_cached_by_ap_id(object_id) do
657 %User{ap_id: ^actor} = user ->
658 User.delete(user)
659
660 nil ->
661 :error
662 end
663
664 _e ->
665 :error
666 end
667 end
668
669 def handle_incoming(
670 %{
671 "type" => "Undo",
672 "object" => %{"type" => "Announce", "object" => object_id},
673 "actor" => _actor,
674 "id" => id
675 } = data,
676 _options
677 ) do
678 with actor <- Containment.get_actor(data),
679 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
680 {:ok, object} <- get_obj_helper(object_id),
681 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
682 {:ok, activity}
683 else
684 _e -> :error
685 end
686 end
687
688 def handle_incoming(
689 %{
690 "type" => "Undo",
691 "object" => %{"type" => "Follow", "object" => followed},
692 "actor" => follower,
693 "id" => id
694 } = _data,
695 _options
696 ) do
697 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
698 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
699 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
700 User.unfollow(follower, followed)
701 {:ok, activity}
702 else
703 _e -> :error
704 end
705 end
706
707 def handle_incoming(
708 %{
709 "type" => "Undo",
710 "object" => %{"type" => "Block", "object" => blocked},
711 "actor" => blocker,
712 "id" => id
713 } = _data,
714 _options
715 ) do
716 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
717 %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
718 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
719 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
720 User.unblock(blocker, blocked)
721 {:ok, activity}
722 else
723 _e -> :error
724 end
725 end
726
727 def handle_incoming(
728 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
729 _options
730 ) do
731 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
732 %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
733 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
734 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
735 User.unfollow(blocker, blocked)
736 User.block(blocker, blocked)
737 {:ok, activity}
738 else
739 _e -> :error
740 end
741 end
742
743 def handle_incoming(
744 %{
745 "type" => "Undo",
746 "object" => %{"type" => "Like", "object" => object_id},
747 "actor" => _actor,
748 "id" => id
749 } = data,
750 _options
751 ) do
752 with actor <- Containment.get_actor(data),
753 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
754 {:ok, object} <- get_obj_helper(object_id),
755 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
756 {:ok, activity}
757 else
758 _e -> :error
759 end
760 end
761
762 def handle_incoming(_, _), do: :error
763
764 def get_obj_helper(id, options \\ []) do
765 if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil
766 end
767
768 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
769 with false <- String.starts_with?(in_reply_to, "http"),
770 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
771 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
772 else
773 _e -> object
774 end
775 end
776
777 def set_reply_to_uri(obj), do: obj
778
779 # Prepares the object of an outgoing create activity.
780 def prepare_object(object) do
781 object
782 |> set_sensitive
783 |> add_hashtags
784 |> add_mention_tags
785 |> add_emoji_tags
786 |> add_attributed_to
787 |> add_likes
788 |> prepare_attachments
789 |> set_conversation
790 |> set_reply_to_uri
791 |> strip_internal_fields
792 |> strip_internal_tags
793 |> set_type
794 end
795
796 # @doc
797 # """
798 # internal -> Mastodon
799 # """
800
801 def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
802 object =
803 object_id
804 |> Object.normalize()
805 |> Map.get(:data)
806 |> prepare_object
807
808 data =
809 data
810 |> Map.put("object", object)
811 |> Map.merge(Utils.make_json_ld_header())
812 |> Map.delete("bcc")
813
814 {:ok, data}
815 end
816
817 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
818 # because of course it does.
819 def prepare_outgoing(%{"type" => "Accept"} = data) do
820 with follow_activity <- Activity.normalize(data["object"]) do
821 object = %{
822 "actor" => follow_activity.actor,
823 "object" => follow_activity.data["object"],
824 "id" => follow_activity.data["id"],
825 "type" => "Follow"
826 }
827
828 data =
829 data
830 |> Map.put("object", object)
831 |> Map.merge(Utils.make_json_ld_header())
832
833 {:ok, data}
834 end
835 end
836
837 def prepare_outgoing(%{"type" => "Reject"} = data) do
838 with follow_activity <- Activity.normalize(data["object"]) do
839 object = %{
840 "actor" => follow_activity.actor,
841 "object" => follow_activity.data["object"],
842 "id" => follow_activity.data["id"],
843 "type" => "Follow"
844 }
845
846 data =
847 data
848 |> Map.put("object", object)
849 |> Map.merge(Utils.make_json_ld_header())
850
851 {:ok, data}
852 end
853 end
854
855 def prepare_outgoing(%{"type" => _type} = data) do
856 data =
857 data
858 |> strip_internal_fields
859 |> maybe_fix_object_url
860 |> Map.merge(Utils.make_json_ld_header())
861
862 {:ok, data}
863 end
864
865 def maybe_fix_object_url(data) do
866 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
867 case get_obj_helper(data["object"]) do
868 {:ok, relative_object} ->
869 if relative_object.data["external_url"] do
870 _data =
871 data
872 |> Map.put("object", relative_object.data["external_url"])
873 else
874 data
875 end
876
877 e ->
878 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
879 data
880 end
881 else
882 data
883 end
884 end
885
886 def add_hashtags(object) do
887 tags =
888 (object["tag"] || [])
889 |> Enum.map(fn
890 # Expand internal representation tags into AS2 tags.
891 tag when is_binary(tag) ->
892 %{
893 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
894 "name" => "##{tag}",
895 "type" => "Hashtag"
896 }
897
898 # Do not process tags which are already AS2 tag objects.
899 tag when is_map(tag) ->
900 tag
901 end)
902
903 object
904 |> Map.put("tag", tags)
905 end
906
907 def add_mention_tags(object) do
908 mentions =
909 object
910 |> Utils.get_notified_from_object()
911 |> Enum.map(fn user ->
912 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
913 end)
914
915 tags = object["tag"] || []
916
917 object
918 |> Map.put("tag", tags ++ mentions)
919 end
920
921 def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
922 user_info = add_emoji_tags(user_info)
923
924 object
925 |> Map.put(:info, user_info)
926 end
927
928 # TODO: we should probably send mtime instead of unix epoch time for updated
929 def add_emoji_tags(%{"emoji" => emoji} = object) do
930 tags = object["tag"] || []
931
932 out =
933 emoji
934 |> Enum.map(fn {name, url} ->
935 %{
936 "icon" => %{"url" => url, "type" => "Image"},
937 "name" => ":" <> name <> ":",
938 "type" => "Emoji",
939 "updated" => "1970-01-01T00:00:00Z",
940 "id" => url
941 }
942 end)
943
944 object
945 |> Map.put("tag", tags ++ out)
946 end
947
948 def add_emoji_tags(object) do
949 object
950 end
951
952 def set_conversation(object) do
953 Map.put(object, "conversation", object["context"])
954 end
955
956 def set_sensitive(object) do
957 tags = object["tag"] || []
958 Map.put(object, "sensitive", "nsfw" in tags)
959 end
960
961 def set_type(%{"type" => "Answer"} = object) do
962 Map.put(object, "type", "Note")
963 end
964
965 def set_type(object), do: object
966
967 def add_attributed_to(object) do
968 attributed_to = object["attributedTo"] || object["actor"]
969
970 object
971 |> Map.put("attributedTo", attributed_to)
972 end
973
974 def add_likes(%{"id" => id, "like_count" => likes} = object) do
975 likes = %{
976 "id" => "#{id}/likes",
977 "first" => "#{id}/likes?page=1",
978 "type" => "OrderedCollection",
979 "totalItems" => likes
980 }
981
982 object
983 |> Map.put("likes", likes)
984 end
985
986 def add_likes(object) do
987 object
988 end
989
990 def prepare_attachments(object) do
991 attachments =
992 (object["attachment"] || [])
993 |> Enum.map(fn data ->
994 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
995 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
996 end)
997
998 object
999 |> Map.put("attachment", attachments)
1000 end
1001
1002 defp strip_internal_fields(object) do
1003 object
1004 |> Map.drop([
1005 "like_count",
1006 "announcements",
1007 "announcement_count",
1008 "emoji",
1009 "context_id",
1010 "deleted_activity_id"
1011 ])
1012 end
1013
1014 defp strip_internal_tags(%{"tag" => tags} = object) do
1015 tags =
1016 tags
1017 |> Enum.filter(fn x -> is_map(x) end)
1018
1019 object
1020 |> Map.put("tag", tags)
1021 end
1022
1023 defp strip_internal_tags(object), do: object
1024
1025 def perform(:user_upgrade, user) do
1026 # we pass a fake user so that the followers collection is stripped away
1027 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1028
1029 q =
1030 from(
1031 u in User,
1032 where: ^old_follower_address in u.following,
1033 update: [
1034 set: [
1035 following:
1036 fragment(
1037 "array_replace(?,?,?)",
1038 u.following,
1039 ^old_follower_address,
1040 ^user.follower_address
1041 )
1042 ]
1043 ]
1044 )
1045
1046 Repo.update_all(q, [])
1047
1048 maybe_retire_websub(user.ap_id)
1049
1050 q =
1051 from(
1052 a in Activity,
1053 where: ^old_follower_address in a.recipients,
1054 update: [
1055 set: [
1056 recipients:
1057 fragment(
1058 "array_replace(?,?,?)",
1059 a.recipients,
1060 ^old_follower_address,
1061 ^user.follower_address
1062 )
1063 ]
1064 ]
1065 )
1066
1067 Repo.update_all(q, [])
1068 end
1069
1070 def upgrade_user_from_ap_id(ap_id) do
1071 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1072 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1073 already_ap <- User.ap_enabled?(user),
1074 {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
1075 unless already_ap do
1076 PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
1077 end
1078
1079 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1080 update_following_followers_counters(user)
1081 end
1082
1083 {:ok, user}
1084 else
1085 %User{} = user -> {:ok, user}
1086 e -> e
1087 end
1088 end
1089
1090 def maybe_retire_websub(ap_id) do
1091 # some sanity checks
1092 if is_binary(ap_id) && String.length(ap_id) > 8 do
1093 q =
1094 from(
1095 ws in Pleroma.Web.Websub.WebsubClientSubscription,
1096 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
1097 )
1098
1099 Repo.delete_all(q)
1100 end
1101 end
1102
1103 def maybe_fix_user_url(data) do
1104 if is_map(data["url"]) do
1105 Map.put(data, "url", data["url"]["href"])
1106 else
1107 data
1108 end
1109 end
1110
1111 def maybe_fix_user_object(data) do
1112 data
1113 |> maybe_fix_user_url
1114 end
1115
1116 def update_following_followers_counters(user) do
1117 info = %{}
1118
1119 following = fetch_counter(user.following_address)
1120 info = if following, do: Map.put(info, :following_count, following), else: info
1121
1122 followers = fetch_counter(user.follower_address)
1123 info = if followers, do: Map.put(info, :follower_count, followers), else: info
1124
1125 User.set_info_cache(user, info)
1126 end
1127
1128 defp fetch_counter(url) do
1129 with {:ok, %{body: body, status: code}} when code in 200..299 <-
1130 Pleroma.HTTP.get(
1131 url,
1132 [{:Accept, "application/activity+json"}]
1133 ),
1134 {:ok, data} <- Jason.decode(body) do
1135 data["totalItems"]
1136 end
1137 end
1138 end