cb5755ccc31a85d8c249165f35434f2842f1d7cb
[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 |> strip_internal_fields
30 |> fix_actor
31 |> fix_url
32 |> fix_attachments
33 |> fix_context
34 |> fix_in_reply_to(options)
35 |> fix_emoji
36 |> fix_tag
37 |> fix_content_map
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 def fix_in_reply_to(object, options \\ [])
155
156 def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
157 when not is_nil(in_reply_to) do
158 in_reply_to_id =
159 cond do
160 is_bitstring(in_reply_to) ->
161 in_reply_to
162
163 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
164 in_reply_to["id"]
165
166 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
167 Enum.at(in_reply_to, 0)
168
169 # Maybe I should output an error too?
170 true ->
171 ""
172 end
173
174 object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
175
176 if Federator.allowed_incoming_reply_depth?(options[:depth]) do
177 case get_obj_helper(in_reply_to_id, options) do
178 {:ok, replied_object} ->
179 with %Activity{} = _activity <-
180 Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
181 object
182 |> Map.put("inReplyTo", replied_object.data["id"])
183 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
184 |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
185 |> Map.put("context", replied_object.data["context"] || object["conversation"])
186 else
187 e ->
188 Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
189 object
190 end
191
192 e ->
193 Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
194 object
195 end
196 else
197 object
198 end
199 end
200
201 def fix_in_reply_to(object, _options), do: object
202
203 def fix_context(object) do
204 context = object["context"] || object["conversation"] || Utils.generate_context_id()
205
206 object
207 |> Map.put("context", context)
208 |> Map.put("conversation", context)
209 end
210
211 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
212 attachments =
213 attachment
214 |> Enum.map(fn data ->
215 media_type = data["mediaType"] || data["mimeType"]
216 href = data["url"] || data["href"]
217
218 url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]
219
220 data
221 |> Map.put("mediaType", media_type)
222 |> Map.put("url", url)
223 end)
224
225 object
226 |> Map.put("attachment", attachments)
227 end
228
229 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
230 Map.put(object, "attachment", [attachment])
231 |> fix_attachments()
232 end
233
234 def fix_attachments(object), do: object
235
236 def fix_url(%{"url" => url} = object) when is_map(url) do
237 object
238 |> Map.put("url", url["href"])
239 end
240
241 def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
242 first_element = Enum.at(url, 0)
243
244 link_element =
245 url
246 |> Enum.filter(fn x -> is_map(x) end)
247 |> Enum.filter(fn x -> x["mimeType"] == "text/html" end)
248 |> Enum.at(0)
249
250 object
251 |> Map.put("attachment", [first_element])
252 |> Map.put("url", link_element["href"])
253 end
254
255 def fix_url(%{"type" => object_type, "url" => url} = object)
256 when object_type != "Video" and is_list(url) do
257 first_element = Enum.at(url, 0)
258
259 url_string =
260 cond do
261 is_bitstring(first_element) -> first_element
262 is_map(first_element) -> first_element["href"] || ""
263 true -> ""
264 end
265
266 object
267 |> Map.put("url", url_string)
268 end
269
270 def fix_url(object), do: object
271
272 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
273 emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
274
275 emoji =
276 emoji
277 |> Enum.reduce(%{}, fn data, mapping ->
278 name = String.trim(data["name"], ":")
279
280 mapping |> Map.put(name, data["icon"]["url"])
281 end)
282
283 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
284 emoji = Map.merge(object["emoji"] || %{}, emoji)
285
286 object
287 |> Map.put("emoji", emoji)
288 end
289
290 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
291 name = String.trim(tag["name"], ":")
292 emoji = %{name => tag["icon"]["url"]}
293
294 object
295 |> Map.put("emoji", emoji)
296 end
297
298 def fix_emoji(object), do: object
299
300 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
301 tags =
302 tag
303 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
304 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
305
306 combined = tag ++ tags
307
308 object
309 |> Map.put("tag", combined)
310 end
311
312 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
313 combined = [tag, String.slice(hashtag, 1..-1)]
314
315 object
316 |> Map.put("tag", combined)
317 end
318
319 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
320
321 def fix_tag(object), do: object
322
323 # content map usually only has one language so this will do for now.
324 def fix_content_map(%{"contentMap" => content_map} = object) do
325 content_groups = Map.to_list(content_map)
326 {_, content} = Enum.at(content_groups, 0)
327
328 object
329 |> Map.put("content", content)
330 end
331
332 def fix_content_map(object), do: object
333
334 def fix_type(object, options \\ [])
335
336 def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
337 when is_binary(reply_id) do
338 reply =
339 with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
340 {:ok, object} <- get_obj_helper(reply_id, options) do
341 object
342 end
343
344 if reply && reply.data["type"] == "Question" do
345 Map.put(object, "type", "Answer")
346 else
347 object
348 end
349 end
350
351 def fix_type(object, _), do: object
352
353 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
354 with true <- id =~ "follows",
355 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
356 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
357 {:ok, activity}
358 else
359 _ -> {:error, nil}
360 end
361 end
362
363 defp mastodon_follow_hack(_, _), do: {:error, nil}
364
365 defp get_follow_activity(follow_object, followed) do
366 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
367 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
368 {:ok, activity}
369 else
370 # Can't find the activity. This might a Mastodon 2.3 "Accept"
371 {:activity, nil} ->
372 mastodon_follow_hack(follow_object, followed)
373
374 _ ->
375 {:error, nil}
376 end
377 end
378
379 def handle_incoming(data, options \\ [])
380
381 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
382 # with nil ID.
383 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
384 with context <- data["context"] || Utils.generate_context_id(),
385 content <- data["content"] || "",
386 %User{} = actor <- User.get_cached_by_ap_id(actor),
387
388 # Reduce the object list to find the reported user.
389 %User{} = account <-
390 Enum.reduce_while(objects, nil, fn ap_id, _ ->
391 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
392 {:halt, user}
393 else
394 _ -> {:cont, nil}
395 end
396 end),
397
398 # Remove the reported user from the object list.
399 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
400 params = %{
401 actor: actor,
402 context: context,
403 account: account,
404 statuses: statuses,
405 content: content,
406 additional: %{
407 "cc" => [account.ap_id]
408 }
409 }
410
411 ActivityPub.flag(params)
412 end
413 end
414
415 # disallow objects with bogus IDs
416 def handle_incoming(%{"id" => nil}, _options), do: :error
417 def handle_incoming(%{"id" => ""}, _options), do: :error
418 # length of https:// = 8, should validate better, but good enough for now.
419 def handle_incoming(%{"id" => id}, _options) when not (is_binary(id) and length(id) > 8),
420 do: :error
421
422 # TODO: validate those with a Ecto scheme
423 # - tags
424 # - emoji
425 def handle_incoming(
426 %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
427 options
428 )
429 when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do
430 actor = Containment.get_actor(data)
431
432 data =
433 Map.put(data, "actor", actor)
434 |> fix_addressing
435
436 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
437 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
438 options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
439 object = fix_object(data["object"], options)
440
441 params = %{
442 to: data["to"],
443 object: object,
444 actor: user,
445 context: object["conversation"],
446 local: false,
447 published: data["published"],
448 additional:
449 Map.take(data, [
450 "cc",
451 "directMessage",
452 "id"
453 ])
454 }
455
456 ActivityPub.create(params)
457 else
458 %Activity{} = activity -> {:ok, activity}
459 _e -> :error
460 end
461 end
462
463 def handle_incoming(
464 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
465 _options
466 ) do
467 with %User{local: true} = followed <-
468 User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
469 {:ok, %User{} = follower} <-
470 User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
471 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
472 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
473 {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
474 {_, false} <- {:user_locked, User.locked?(followed)},
475 {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
476 {_, {:ok, _}} <-
477 {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do
478 ActivityPub.accept(%{
479 to: [follower.ap_id],
480 actor: followed,
481 object: data,
482 local: true
483 })
484 else
485 {:user_blocked, true} ->
486 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
487
488 ActivityPub.reject(%{
489 to: [follower.ap_id],
490 actor: followed,
491 object: data,
492 local: true
493 })
494
495 {:follow, {:error, _}} ->
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 {:user_locked, true} ->
506 :noop
507 end
508
509 {:ok, activity}
510 else
511 _e ->
512 :error
513 end
514 end
515
516 def handle_incoming(
517 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
518 _options
519 ) do
520 with actor <- Containment.get_actor(data),
521 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
522 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
523 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
524 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
525 {:ok, _follower} = User.follow(follower, followed) do
526 ActivityPub.accept(%{
527 to: follow_activity.data["to"],
528 type: "Accept",
529 actor: followed,
530 object: follow_activity.data["id"],
531 local: false
532 })
533 else
534 _e -> :error
535 end
536 end
537
538 def handle_incoming(
539 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
540 _options
541 ) do
542 with actor <- Containment.get_actor(data),
543 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
544 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
545 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
546 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
547 {:ok, activity} <-
548 ActivityPub.reject(%{
549 to: follow_activity.data["to"],
550 type: "Reject",
551 actor: followed,
552 object: follow_activity.data["id"],
553 local: false
554 }) do
555 User.unfollow(follower, followed)
556
557 {:ok, activity}
558 else
559 _e -> :error
560 end
561 end
562
563 def handle_incoming(
564 %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
565 _options
566 ) do
567 with actor <- Containment.get_actor(data),
568 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
569 {:ok, object} <- get_obj_helper(object_id),
570 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
571 {:ok, activity}
572 else
573 _e -> :error
574 end
575 end
576
577 def handle_incoming(
578 %{
579 "type" => "EmojiReaction",
580 "object" => object_id,
581 "actor" => _actor,
582 "id" => id,
583 "content" => emoji
584 } = data,
585 _options
586 ) do
587 with actor <- Containment.get_actor(data),
588 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
589 {:ok, object} <- get_obj_helper(object_id),
590 {:ok, activity, _object} <-
591 ActivityPub.react_with_emoji(actor, object, emoji, activity_id: id, local: false) do
592 {:ok, activity}
593 else
594 _e -> :error
595 end
596 end
597
598 def handle_incoming(
599 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
600 _options
601 ) do
602 with actor <- Containment.get_actor(data),
603 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
604 {:ok, object} <- get_obj_helper(object_id),
605 public <- Visibility.is_public?(data),
606 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
607 {:ok, activity}
608 else
609 _e -> :error
610 end
611 end
612
613 def handle_incoming(
614 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
615 data,
616 _options
617 )
618 when object_type in ["Person", "Application", "Service", "Organization"] do
619 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
620 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
621
622 banner = new_user_data[:info][:banner]
623 locked = new_user_data[:info][:locked] || false
624 attachment = get_in(new_user_data, [:info, :source_data, "attachment"]) || []
625
626 fields =
627 attachment
628 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
629 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
630
631 update_data =
632 new_user_data
633 |> Map.take([:name, :bio, :avatar])
634 |> Map.put(:info, %{banner: banner, locked: locked, fields: fields})
635
636 actor
637 |> User.upgrade_changeset(update_data, true)
638 |> User.update_and_set_cache()
639
640 ActivityPub.update(%{
641 local: false,
642 to: data["to"] || [],
643 cc: data["cc"] || [],
644 object: object,
645 actor: actor_id
646 })
647 else
648 e ->
649 Logger.error(e)
650 :error
651 end
652 end
653
654 # TODO: We presently assume that any actor on the same origin domain as the object being
655 # deleted has the rights to delete that object. A better way to validate whether or not
656 # the object should be deleted is to refetch the object URI, which should return either
657 # an error or a tombstone. This would allow us to verify that a deletion actually took
658 # place.
659 def handle_incoming(
660 %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = data,
661 _options
662 ) do
663 object_id = Utils.get_ap_id(object_id)
664
665 with actor <- Containment.get_actor(data),
666 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
667 {:ok, object} <- get_obj_helper(object_id),
668 :ok <- Containment.contain_origin(actor.ap_id, object.data),
669 {:ok, activity} <- ActivityPub.delete(object, false) do
670 {:ok, activity}
671 else
672 nil ->
673 case User.get_cached_by_ap_id(object_id) do
674 %User{ap_id: ^actor} = user ->
675 User.delete(user)
676
677 nil ->
678 :error
679 end
680
681 _e ->
682 :error
683 end
684 end
685
686 def handle_incoming(
687 %{
688 "type" => "Undo",
689 "object" => %{"type" => "Announce", "object" => object_id},
690 "actor" => _actor,
691 "id" => id
692 } = data,
693 _options
694 ) do
695 with actor <- Containment.get_actor(data),
696 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
697 {:ok, object} <- get_obj_helper(object_id),
698 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
699 {:ok, activity}
700 else
701 _e -> :error
702 end
703 end
704
705 def handle_incoming(
706 %{
707 "type" => "Undo",
708 "object" => %{"type" => "Follow", "object" => followed},
709 "actor" => follower,
710 "id" => id
711 } = _data,
712 _options
713 ) do
714 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
715 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
716 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
717 User.unfollow(follower, followed)
718 {:ok, activity}
719 else
720 _e -> :error
721 end
722 end
723
724 def handle_incoming(
725 %{
726 "type" => "Undo",
727 "object" => %{"type" => "Block", "object" => blocked},
728 "actor" => blocker,
729 "id" => id
730 } = _data,
731 _options
732 ) do
733 with %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.unblock(blocker, blocked, id, false) do
736 User.unblock(blocker, blocked)
737 {:ok, activity}
738 else
739 _e -> :error
740 end
741 end
742
743 def handle_incoming(
744 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
745 _options
746 ) do
747 with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
748 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
749 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
750 User.unfollow(blocker, blocked)
751 User.block(blocker, blocked)
752 {:ok, activity}
753 else
754 _e -> :error
755 end
756 end
757
758 def handle_incoming(
759 %{
760 "type" => "Undo",
761 "object" => %{"type" => "Like", "object" => object_id},
762 "actor" => _actor,
763 "id" => id
764 } = data,
765 _options
766 ) do
767 with actor <- Containment.get_actor(data),
768 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
769 {:ok, object} <- get_obj_helper(object_id),
770 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
771 {:ok, activity}
772 else
773 _e -> :error
774 end
775 end
776
777 def handle_incoming(_, _), do: :error
778
779 def get_obj_helper(id, options \\ []) do
780 if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil
781 end
782
783 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
784 with false <- String.starts_with?(in_reply_to, "http"),
785 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
786 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
787 else
788 _e -> object
789 end
790 end
791
792 def set_reply_to_uri(obj), do: obj
793
794 # Prepares the object of an outgoing create activity.
795 def prepare_object(object) do
796 object
797 |> set_sensitive
798 |> add_hashtags
799 |> add_mention_tags
800 |> add_emoji_tags
801 |> add_attributed_to
802 |> prepare_attachments
803 |> set_conversation
804 |> set_reply_to_uri
805 |> strip_internal_fields
806 |> strip_internal_tags
807 |> set_type
808 end
809
810 # @doc
811 # """
812 # internal -> Mastodon
813 # """
814
815 def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
816 object =
817 object_id
818 |> Object.normalize()
819 |> Map.get(:data)
820 |> prepare_object
821
822 data =
823 data
824 |> Map.put("object", object)
825 |> Map.merge(Utils.make_json_ld_header())
826 |> Map.delete("bcc")
827
828 {:ok, data}
829 end
830
831 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
832 # because of course it does.
833 def prepare_outgoing(%{"type" => "Accept"} = data) do
834 with follow_activity <- Activity.normalize(data["object"]) do
835 object = %{
836 "actor" => follow_activity.actor,
837 "object" => follow_activity.data["object"],
838 "id" => follow_activity.data["id"],
839 "type" => "Follow"
840 }
841
842 data =
843 data
844 |> Map.put("object", object)
845 |> Map.merge(Utils.make_json_ld_header())
846
847 {:ok, data}
848 end
849 end
850
851 def prepare_outgoing(%{"type" => "Reject"} = data) do
852 with follow_activity <- Activity.normalize(data["object"]) do
853 object = %{
854 "actor" => follow_activity.actor,
855 "object" => follow_activity.data["object"],
856 "id" => follow_activity.data["id"],
857 "type" => "Follow"
858 }
859
860 data =
861 data
862 |> Map.put("object", object)
863 |> Map.merge(Utils.make_json_ld_header())
864
865 {:ok, data}
866 end
867 end
868
869 def prepare_outgoing(%{"type" => _type} = data) do
870 data =
871 data
872 |> strip_internal_fields
873 |> maybe_fix_object_url
874 |> Map.merge(Utils.make_json_ld_header())
875
876 {:ok, data}
877 end
878
879 def maybe_fix_object_url(data) do
880 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
881 case get_obj_helper(data["object"]) do
882 {:ok, relative_object} ->
883 if relative_object.data["external_url"] do
884 _data =
885 data
886 |> Map.put("object", relative_object.data["external_url"])
887 else
888 data
889 end
890
891 e ->
892 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
893 data
894 end
895 else
896 data
897 end
898 end
899
900 def add_hashtags(object) do
901 tags =
902 (object["tag"] || [])
903 |> Enum.map(fn
904 # Expand internal representation tags into AS2 tags.
905 tag when is_binary(tag) ->
906 %{
907 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
908 "name" => "##{tag}",
909 "type" => "Hashtag"
910 }
911
912 # Do not process tags which are already AS2 tag objects.
913 tag when is_map(tag) ->
914 tag
915 end)
916
917 object
918 |> Map.put("tag", tags)
919 end
920
921 def add_mention_tags(object) do
922 mentions =
923 object
924 |> Utils.get_notified_from_object()
925 |> Enum.map(fn user ->
926 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
927 end)
928
929 tags = object["tag"] || []
930
931 object
932 |> Map.put("tag", tags ++ mentions)
933 end
934
935 def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
936 user_info = add_emoji_tags(user_info)
937
938 object
939 |> Map.put(:info, user_info)
940 end
941
942 # TODO: we should probably send mtime instead of unix epoch time for updated
943 def add_emoji_tags(%{"emoji" => emoji} = object) do
944 tags = object["tag"] || []
945
946 out =
947 emoji
948 |> Enum.map(fn {name, url} ->
949 %{
950 "icon" => %{"url" => url, "type" => "Image"},
951 "name" => ":" <> name <> ":",
952 "type" => "Emoji",
953 "updated" => "1970-01-01T00:00:00Z",
954 "id" => url
955 }
956 end)
957
958 object
959 |> Map.put("tag", tags ++ out)
960 end
961
962 def add_emoji_tags(object) do
963 object
964 end
965
966 def set_conversation(object) do
967 Map.put(object, "conversation", object["context"])
968 end
969
970 def set_sensitive(object) do
971 tags = object["tag"] || []
972 Map.put(object, "sensitive", "nsfw" in tags)
973 end
974
975 def set_type(%{"type" => "Answer"} = object) do
976 Map.put(object, "type", "Note")
977 end
978
979 def set_type(object), do: object
980
981 def add_attributed_to(object) do
982 attributed_to = object["attributedTo"] || object["actor"]
983
984 object
985 |> Map.put("attributedTo", attributed_to)
986 end
987
988 def prepare_attachments(object) do
989 attachments =
990 (object["attachment"] || [])
991 |> Enum.map(fn data ->
992 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
993 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
994 end)
995
996 object
997 |> Map.put("attachment", attachments)
998 end
999
1000 def strip_internal_fields(object) do
1001 object
1002 |> Map.drop([
1003 "reactions",
1004 "reaction_count",
1005 "likes",
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 {:ok, user}
1081 else
1082 %User{} = user -> {:ok, user}
1083 e -> e
1084 end
1085 end
1086
1087 def maybe_retire_websub(ap_id) do
1088 # some sanity checks
1089 if is_binary(ap_id) && String.length(ap_id) > 8 do
1090 q =
1091 from(
1092 ws in Pleroma.Web.Websub.WebsubClientSubscription,
1093 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
1094 )
1095
1096 Repo.delete_all(q)
1097 end
1098 end
1099
1100 def maybe_fix_user_url(data) do
1101 if is_map(data["url"]) do
1102 Map.put(data, "url", data["url"]["href"])
1103 else
1104 data
1105 end
1106 end
1107
1108 def maybe_fix_user_object(data) do
1109 data
1110 |> maybe_fix_user_url
1111 end
1112 end