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