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