mix: add ex_const dependency
[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 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} <-
484 {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
485 {_, false} <- {:user_locked, User.locked?(followed)},
486 {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
487 {_, {:ok, _}} <-
488 {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do
489 ActivityPub.accept(%{
490 to: [follower.ap_id],
491 actor: followed,
492 object: data,
493 local: true
494 })
495 else
496 {:user_blocked, true} ->
497 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
498
499 ActivityPub.reject(%{
500 to: [follower.ap_id],
501 actor: followed,
502 object: data,
503 local: true
504 })
505
506 {:follow, {:error, _}} ->
507 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
508
509 ActivityPub.reject(%{
510 to: [follower.ap_id],
511 actor: followed,
512 object: data,
513 local: true
514 })
515
516 {:user_locked, true} ->
517 :noop
518 end
519
520 {:ok, activity}
521 else
522 _e ->
523 :error
524 end
525 end
526
527 def handle_incoming(
528 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
529 _options
530 ) do
531 with actor <- Containment.get_actor(data),
532 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
533 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
534 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
535 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
536 {:ok, _follower} = User.follow(follower, followed) do
537 ActivityPub.accept(%{
538 to: follow_activity.data["to"],
539 type: "Accept",
540 actor: followed,
541 object: follow_activity.data["id"],
542 local: false
543 })
544 else
545 _e -> :error
546 end
547 end
548
549 def handle_incoming(
550 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
551 _options
552 ) do
553 with actor <- Containment.get_actor(data),
554 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
555 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
556 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
557 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
558 {:ok, activity} <-
559 ActivityPub.reject(%{
560 to: follow_activity.data["to"],
561 type: "Reject",
562 actor: followed,
563 object: follow_activity.data["id"],
564 local: false
565 }) do
566 User.unfollow(follower, followed)
567
568 {:ok, activity}
569 else
570 _e -> :error
571 end
572 end
573
574 def handle_incoming(
575 %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
576 _options
577 ) do
578 with actor <- Containment.get_actor(data),
579 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
580 {:ok, object} <- get_obj_helper(object_id),
581 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
582 {:ok, activity}
583 else
584 _e -> :error
585 end
586 end
587
588 def handle_incoming(
589 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
590 _options
591 ) do
592 with actor <- Containment.get_actor(data),
593 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
594 {:ok, object} <- get_obj_helper(object_id),
595 public <- Visibility.is_public?(data),
596 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
597 {:ok, activity}
598 else
599 _e -> :error
600 end
601 end
602
603 def handle_incoming(
604 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
605 data,
606 _options
607 )
608 when object_type in ["Person", "Application", "Service", "Organization"] do
609 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
610 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
611
612 banner = new_user_data[:info]["banner"]
613 locked = new_user_data[:info]["locked"] || false
614
615 update_data =
616 new_user_data
617 |> Map.take([:name, :bio, :avatar])
618 |> Map.put(:info, %{"banner" => banner, "locked" => locked})
619
620 actor
621 |> User.upgrade_changeset(update_data)
622 |> User.update_and_set_cache()
623
624 ActivityPub.update(%{
625 local: false,
626 to: data["to"] || [],
627 cc: data["cc"] || [],
628 object: object,
629 actor: actor_id
630 })
631 else
632 e ->
633 Logger.error(e)
634 :error
635 end
636 end
637
638 # TODO: We presently assume that any actor on the same origin domain as the object being
639 # deleted has the rights to delete that object. A better way to validate whether or not
640 # the object should be deleted is to refetch the object URI, which should return either
641 # an error or a tombstone. This would allow us to verify that a deletion actually took
642 # place.
643 def handle_incoming(
644 %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = data,
645 _options
646 ) do
647 object_id = Utils.get_ap_id(object_id)
648
649 with actor <- Containment.get_actor(data),
650 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
651 {:ok, object} <- get_obj_helper(object_id),
652 :ok <- Containment.contain_origin(actor.ap_id, object.data),
653 {:ok, activity} <- ActivityPub.delete(object, false) do
654 {:ok, activity}
655 else
656 nil ->
657 case User.get_cached_by_ap_id(object_id) do
658 %User{ap_id: ^actor} = user ->
659 User.delete(user)
660
661 nil ->
662 :error
663 end
664
665 _e ->
666 :error
667 end
668 end
669
670 def handle_incoming(
671 %{
672 "type" => "Undo",
673 "object" => %{"type" => "Announce", "object" => object_id},
674 "actor" => _actor,
675 "id" => id
676 } = data,
677 _options
678 ) do
679 with actor <- Containment.get_actor(data),
680 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
681 {:ok, object} <- get_obj_helper(object_id),
682 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
683 {:ok, activity}
684 else
685 _e -> :error
686 end
687 end
688
689 def handle_incoming(
690 %{
691 "type" => "Undo",
692 "object" => %{"type" => "Follow", "object" => followed},
693 "actor" => follower,
694 "id" => id
695 } = _data,
696 _options
697 ) do
698 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
699 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
700 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
701 User.unfollow(follower, followed)
702 {:ok, activity}
703 else
704 _e -> :error
705 end
706 end
707
708 def handle_incoming(
709 %{
710 "type" => "Undo",
711 "object" => %{"type" => "Block", "object" => blocked},
712 "actor" => blocker,
713 "id" => id
714 } = _data,
715 _options
716 ) do
717 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
718 %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
719 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
720 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
721 User.unblock(blocker, blocked)
722 {:ok, activity}
723 else
724 _e -> :error
725 end
726 end
727
728 def handle_incoming(
729 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
730 _options
731 ) do
732 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
733 %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
734 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
735 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
736 User.unfollow(blocker, blocked)
737 User.block(blocker, blocked)
738 {:ok, activity}
739 else
740 _e -> :error
741 end
742 end
743
744 def handle_incoming(
745 %{
746 "type" => "Undo",
747 "object" => %{"type" => "Like", "object" => object_id},
748 "actor" => _actor,
749 "id" => id
750 } = data,
751 _options
752 ) do
753 with actor <- Containment.get_actor(data),
754 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
755 {:ok, object} <- get_obj_helper(object_id),
756 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
757 {:ok, activity}
758 else
759 _e -> :error
760 end
761 end
762
763 def handle_incoming(_, _), do: :error
764
765 def get_obj_helper(id, options \\ []) do
766 if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil
767 end
768
769 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
770 with false <- String.starts_with?(in_reply_to, "http"),
771 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
772 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
773 else
774 _e -> object
775 end
776 end
777
778 def set_reply_to_uri(obj), do: obj
779
780 # Prepares the object of an outgoing create activity.
781 def prepare_object(object) do
782 object
783 |> set_sensitive
784 |> add_hashtags
785 |> add_mention_tags
786 |> add_emoji_tags
787 |> add_attributed_to
788 |> add_likes
789 |> prepare_attachments
790 |> set_conversation
791 |> set_reply_to_uri
792 |> strip_internal_fields
793 |> strip_internal_tags
794 |> set_type
795 end
796
797 # @doc
798 # """
799 # internal -> Mastodon
800 # """
801
802 def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
803 object =
804 object_id
805 |> Object.normalize()
806 |> Map.get(:data)
807 |> prepare_object
808
809 data =
810 data
811 |> Map.put("object", object)
812 |> Map.merge(Utils.make_json_ld_header())
813 |> Map.delete("bcc")
814
815 {:ok, data}
816 end
817
818 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
819 # because of course it does.
820 def prepare_outgoing(%{"type" => "Accept"} = data) do
821 with follow_activity <- Activity.normalize(data["object"]) do
822 object = %{
823 "actor" => follow_activity.actor,
824 "object" => follow_activity.data["object"],
825 "id" => follow_activity.data["id"],
826 "type" => "Follow"
827 }
828
829 data =
830 data
831 |> Map.put("object", object)
832 |> Map.merge(Utils.make_json_ld_header())
833
834 {:ok, data}
835 end
836 end
837
838 def prepare_outgoing(%{"type" => "Reject"} = data) do
839 with follow_activity <- Activity.normalize(data["object"]) do
840 object = %{
841 "actor" => follow_activity.actor,
842 "object" => follow_activity.data["object"],
843 "id" => follow_activity.data["id"],
844 "type" => "Follow"
845 }
846
847 data =
848 data
849 |> Map.put("object", object)
850 |> Map.merge(Utils.make_json_ld_header())
851
852 {:ok, data}
853 end
854 end
855
856 def prepare_outgoing(%{"type" => _type} = data) do
857 data =
858 data
859 |> strip_internal_fields
860 |> maybe_fix_object_url
861 |> Map.merge(Utils.make_json_ld_header())
862
863 {:ok, data}
864 end
865
866 def maybe_fix_object_url(data) do
867 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
868 case get_obj_helper(data["object"]) do
869 {:ok, relative_object} ->
870 if relative_object.data["external_url"] do
871 _data =
872 data
873 |> Map.put("object", relative_object.data["external_url"])
874 else
875 data
876 end
877
878 e ->
879 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
880 data
881 end
882 else
883 data
884 end
885 end
886
887 def add_hashtags(object) do
888 tags =
889 (object["tag"] || [])
890 |> Enum.map(fn
891 # Expand internal representation tags into AS2 tags.
892 tag when is_binary(tag) ->
893 %{
894 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
895 "name" => "##{tag}",
896 "type" => "Hashtag"
897 }
898
899 # Do not process tags which are already AS2 tag objects.
900 tag when is_map(tag) ->
901 tag
902 end)
903
904 object
905 |> Map.put("tag", tags)
906 end
907
908 def add_mention_tags(object) do
909 mentions =
910 object
911 |> Utils.get_notified_from_object()
912 |> Enum.map(fn user ->
913 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
914 end)
915
916 tags = object["tag"] || []
917
918 object
919 |> Map.put("tag", tags ++ mentions)
920 end
921
922 def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
923 user_info = add_emoji_tags(user_info)
924
925 object
926 |> Map.put(:info, user_info)
927 end
928
929 # TODO: we should probably send mtime instead of unix epoch time for updated
930 def add_emoji_tags(%{"emoji" => emoji} = object) do
931 tags = object["tag"] || []
932
933 out =
934 emoji
935 |> Enum.map(fn {name, url} ->
936 %{
937 "icon" => %{"url" => url, "type" => "Image"},
938 "name" => ":" <> name <> ":",
939 "type" => "Emoji",
940 "updated" => "1970-01-01T00:00:00Z",
941 "id" => url
942 }
943 end)
944
945 object
946 |> Map.put("tag", tags ++ out)
947 end
948
949 def add_emoji_tags(object) do
950 object
951 end
952
953 def set_conversation(object) do
954 Map.put(object, "conversation", object["context"])
955 end
956
957 def set_sensitive(object) do
958 tags = object["tag"] || []
959 Map.put(object, "sensitive", "nsfw" in tags)
960 end
961
962 def set_type(%{"type" => "Answer"} = object) do
963 Map.put(object, "type", "Note")
964 end
965
966 def set_type(object), do: object
967
968 def add_attributed_to(object) do
969 attributed_to = object["attributedTo"] || object["actor"]
970
971 object
972 |> Map.put("attributedTo", attributed_to)
973 end
974
975 def add_likes(%{"id" => id, "like_count" => likes} = object) do
976 likes = %{
977 "id" => "#{id}/likes",
978 "first" => "#{id}/likes?page=1",
979 "type" => "OrderedCollection",
980 "totalItems" => likes
981 }
982
983 object
984 |> Map.put("likes", likes)
985 end
986
987 def add_likes(object) do
988 object
989 end
990
991 def prepare_attachments(object) do
992 attachments =
993 (object["attachment"] || [])
994 |> Enum.map(fn data ->
995 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
996 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
997 end)
998
999 object
1000 |> Map.put("attachment", attachments)
1001 end
1002
1003 defp strip_internal_fields(object) do
1004 object
1005 |> Map.drop([
1006 "like_count",
1007 "announcements",
1008 "announcement_count",
1009 "emoji",
1010 "context_id",
1011 "deleted_activity_id"
1012 ])
1013 end
1014
1015 defp strip_internal_tags(%{"tag" => tags} = object) do
1016 tags =
1017 tags
1018 |> Enum.filter(fn x -> is_map(x) end)
1019
1020 object
1021 |> Map.put("tag", tags)
1022 end
1023
1024 defp strip_internal_tags(object), do: object
1025
1026 def perform(:user_upgrade, user) do
1027 # we pass a fake user so that the followers collection is stripped away
1028 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1029
1030 q =
1031 from(
1032 u in User,
1033 where: ^old_follower_address in u.following,
1034 update: [
1035 set: [
1036 following:
1037 fragment(
1038 "array_replace(?,?,?)",
1039 u.following,
1040 ^old_follower_address,
1041 ^user.follower_address
1042 )
1043 ]
1044 ]
1045 )
1046
1047 Repo.update_all(q, [])
1048
1049 maybe_retire_websub(user.ap_id)
1050
1051 q =
1052 from(
1053 a in Activity,
1054 where: ^old_follower_address in a.recipients,
1055 update: [
1056 set: [
1057 recipients:
1058 fragment(
1059 "array_replace(?,?,?)",
1060 a.recipients,
1061 ^old_follower_address,
1062 ^user.follower_address
1063 )
1064 ]
1065 ]
1066 )
1067
1068 Repo.update_all(q, [])
1069 end
1070
1071 def upgrade_user_from_ap_id(ap_id) do
1072 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1073 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1074 already_ap <- User.ap_enabled?(user),
1075 {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
1076 unless already_ap do
1077 PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
1078 end
1079
1080 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1081 update_following_followers_counters(user)
1082 end
1083
1084 {:ok, user}
1085 else
1086 %User{} = user -> {:ok, user}
1087 e -> e
1088 end
1089 end
1090
1091 def maybe_retire_websub(ap_id) do
1092 # some sanity checks
1093 if is_binary(ap_id) && String.length(ap_id) > 8 do
1094 q =
1095 from(
1096 ws in Pleroma.Web.Websub.WebsubClientSubscription,
1097 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
1098 )
1099
1100 Repo.delete_all(q)
1101 end
1102 end
1103
1104 def maybe_fix_user_url(data) do
1105 if is_map(data["url"]) do
1106 Map.put(data, "url", data["url"]["href"])
1107 else
1108 data
1109 end
1110 end
1111
1112 def maybe_fix_user_object(data) do
1113 data
1114 |> maybe_fix_user_url
1115 end
1116
1117 def update_following_followers_counters(user) do
1118 info = %{}
1119
1120 following = fetch_counter(user.following_address)
1121 info = if following, do: Map.put(info, :following_count, following), else: info
1122
1123 followers = fetch_counter(user.follower_address)
1124 info = if followers, do: Map.put(info, :follower_count, followers), else: info
1125
1126 User.set_info_cache(user, info)
1127 end
1128
1129 defp fetch_counter(url) do
1130 with {:ok, %{body: body, status: code}} when code in 200..299 <-
1131 Pleroma.HTTP.get(
1132 url,
1133 [{:Accept, "application/activity+json"}]
1134 ),
1135 {:ok, data} <- Jason.decode(body) do
1136 data["totalItems"]
1137 end
1138 end
1139 end