Merge branch 'develop' into test/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 url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]
208
209 data
210 |> Map.put("mediaType", media_type)
211 |> Map.put("url", url)
212 end)
213
214 Map.put(object, "attachment", attachments)
215 end
216
217 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
218 object
219 |> Map.put("attachment", [attachment])
220 |> fix_attachments()
221 end
222
223 def fix_attachments(object), do: object
224
225 def fix_url(%{"url" => url} = object) when is_map(url) do
226 Map.put(object, "url", url["href"])
227 end
228
229 def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
230 first_element = Enum.at(url, 0)
231
232 link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
233
234 object
235 |> Map.put("attachment", [first_element])
236 |> Map.put("url", link_element["href"])
237 end
238
239 def fix_url(%{"type" => object_type, "url" => url} = object)
240 when object_type != "Video" and is_list(url) do
241 first_element = Enum.at(url, 0)
242
243 url_string =
244 cond do
245 is_bitstring(first_element) -> first_element
246 is_map(first_element) -> first_element["href"] || ""
247 true -> ""
248 end
249
250 Map.put(object, "url", url_string)
251 end
252
253 def fix_url(object), do: object
254
255 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
256 emoji =
257 tags
258 |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
259 |> Enum.reduce(%{}, fn data, mapping ->
260 name = String.trim(data["name"], ":")
261
262 Map.put(mapping, name, data["icon"]["url"])
263 end)
264
265 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
266 emoji = Map.merge(object["emoji"] || %{}, emoji)
267
268 Map.put(object, "emoji", emoji)
269 end
270
271 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
272 name = String.trim(tag["name"], ":")
273 emoji = %{name => tag["icon"]["url"]}
274
275 Map.put(object, "emoji", emoji)
276 end
277
278 def fix_emoji(object), do: object
279
280 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
281 tags =
282 tag
283 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
284 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
285
286 Map.put(object, "tag", tag ++ tags)
287 end
288
289 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
290 combined = [tag, String.slice(hashtag, 1..-1)]
291
292 Map.put(object, "tag", combined)
293 end
294
295 def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
296
297 def fix_tag(object), do: object
298
299 # content map usually only has one language so this will do for now.
300 def fix_content_map(%{"contentMap" => content_map} = object) do
301 content_groups = Map.to_list(content_map)
302 {_, content} = Enum.at(content_groups, 0)
303
304 Map.put(object, "content", content)
305 end
306
307 def fix_content_map(object), do: object
308
309 def fix_type(object, options \\ [])
310
311 def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
312 when is_binary(reply_id) do
313 with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
314 {:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
315 Map.put(object, "type", "Answer")
316 else
317 _ -> object
318 end
319 end
320
321 def fix_type(object, _), do: object
322
323 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
324 with true <- id =~ "follows",
325 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
326 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
327 {:ok, activity}
328 else
329 _ -> {:error, nil}
330 end
331 end
332
333 defp mastodon_follow_hack(_, _), do: {:error, nil}
334
335 defp get_follow_activity(follow_object, followed) do
336 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
337 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
338 {:ok, activity}
339 else
340 # Can't find the activity. This might a Mastodon 2.3 "Accept"
341 {:activity, nil} ->
342 mastodon_follow_hack(follow_object, followed)
343
344 _ ->
345 {:error, nil}
346 end
347 end
348
349 # Reduce the object list to find the reported user.
350 defp get_reported(objects) do
351 Enum.reduce_while(objects, nil, fn ap_id, _ ->
352 with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
353 {:halt, user}
354 else
355 _ -> {:cont, nil}
356 end
357 end)
358 end
359
360 def handle_incoming(data, options \\ [])
361
362 # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
363 # with nil ID.
364 def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do
365 with context <- data["context"] || Utils.generate_context_id(),
366 content <- data["content"] || "",
367 %User{} = actor <- User.get_cached_by_ap_id(actor),
368 # Reduce the object list to find the reported user.
369 %User{} = account <- get_reported(objects),
370 # Remove the reported user from the object list.
371 statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
372 %{
373 actor: actor,
374 context: context,
375 account: account,
376 statuses: statuses,
377 content: content,
378 additional: %{"cc" => [account.ap_id]}
379 }
380 |> ActivityPub.flag()
381 end
382 end
383
384 # disallow objects with bogus IDs
385 def handle_incoming(%{"id" => nil}, _options), do: :error
386 def handle_incoming(%{"id" => ""}, _options), do: :error
387 # length of https:// = 8, should validate better, but good enough for now.
388 def handle_incoming(%{"id" => id}, _options) when not (is_binary(id) and length(id) > 8),
389 do: :error
390
391 # TODO: validate those with a Ecto scheme
392 # - tags
393 # - emoji
394 def handle_incoming(
395 %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
396 options
397 )
398 when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do
399 actor = Containment.get_actor(data)
400
401 data =
402 Map.put(data, "actor", actor)
403 |> fix_addressing
404
405 with nil <- Activity.get_create_by_object_ap_id(object["id"]),
406 {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
407 options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
408 object = fix_object(data["object"], options)
409
410 params = %{
411 to: data["to"],
412 object: object,
413 actor: user,
414 context: object["conversation"],
415 local: false,
416 published: data["published"],
417 additional:
418 Map.take(data, [
419 "cc",
420 "directMessage",
421 "id"
422 ])
423 }
424
425 ActivityPub.create(params)
426 else
427 %Activity{} = activity -> {:ok, activity}
428 _e -> :error
429 end
430 end
431
432 def handle_incoming(
433 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
434 _options
435 ) do
436 with %User{local: true} = followed <-
437 User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
438 {:ok, %User{} = follower} <-
439 User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
440 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
441 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
442 {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
443 {_, false} <- {:user_locked, User.locked?(followed)},
444 {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
445 {_, {:ok, _}} <-
446 {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do
447 ActivityPub.accept(%{
448 to: [follower.ap_id],
449 actor: followed,
450 object: data,
451 local: true
452 })
453 else
454 {:user_blocked, true} ->
455 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
456
457 ActivityPub.reject(%{
458 to: [follower.ap_id],
459 actor: followed,
460 object: data,
461 local: true
462 })
463
464 {:follow, {:error, _}} ->
465 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
466
467 ActivityPub.reject(%{
468 to: [follower.ap_id],
469 actor: followed,
470 object: data,
471 local: true
472 })
473
474 {:user_locked, true} ->
475 :noop
476 end
477
478 {:ok, activity}
479 else
480 _e ->
481 :error
482 end
483 end
484
485 def handle_incoming(
486 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
487 _options
488 ) do
489 with actor <- Containment.get_actor(data),
490 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
491 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
492 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
493 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
494 {:ok, _follower} = User.follow(follower, followed) do
495 ActivityPub.accept(%{
496 to: follow_activity.data["to"],
497 type: "Accept",
498 actor: followed,
499 object: follow_activity.data["id"],
500 local: false
501 })
502 else
503 _e -> :error
504 end
505 end
506
507 def handle_incoming(
508 %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
509 _options
510 ) do
511 with actor <- Containment.get_actor(data),
512 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
513 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
514 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
515 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
516 {:ok, activity} <-
517 ActivityPub.reject(%{
518 to: follow_activity.data["to"],
519 type: "Reject",
520 actor: followed,
521 object: follow_activity.data["id"],
522 local: false
523 }) do
524 User.unfollow(follower, followed)
525
526 {:ok, activity}
527 else
528 _e -> :error
529 end
530 end
531
532 def handle_incoming(
533 %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
534 _options
535 ) do
536 with actor <- Containment.get_actor(data),
537 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
538 {:ok, object} <- get_obj_helper(object_id),
539 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
540 {:ok, activity}
541 else
542 _e -> :error
543 end
544 end
545
546 def handle_incoming(
547 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
548 _options
549 ) do
550 with actor <- Containment.get_actor(data),
551 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
552 {:ok, object} <- get_obj_helper(object_id),
553 public <- Visibility.is_public?(data),
554 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
555 {:ok, activity}
556 else
557 _e -> :error
558 end
559 end
560
561 def handle_incoming(
562 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
563 data,
564 _options
565 )
566 when object_type in ["Person", "Application", "Service", "Organization"] do
567 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
568 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
569
570 banner = new_user_data[:info][:banner]
571 locked = new_user_data[:info][:locked] || false
572 attachment = get_in(new_user_data, [:info, :source_data, "attachment"]) || []
573
574 fields =
575 attachment
576 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
577 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
578
579 update_data =
580 new_user_data
581 |> Map.take([:name, :bio, :avatar])
582 |> Map.put(:info, %{banner: banner, locked: locked, fields: fields})
583
584 actor
585 |> User.upgrade_changeset(update_data, true)
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 _options
610 ) do
611 object_id = Utils.get_ap_id(object_id)
612
613 with actor <- Containment.get_actor(data),
614 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
615 {:ok, object} <- get_obj_helper(object_id),
616 :ok <- Containment.contain_origin(actor.ap_id, object.data),
617 {:ok, activity} <- ActivityPub.delete(object, false) do
618 {:ok, activity}
619 else
620 nil ->
621 case User.get_cached_by_ap_id(object_id) do
622 %User{ap_id: ^actor} = user ->
623 User.delete(user)
624
625 nil ->
626 :error
627 end
628
629 _e ->
630 :error
631 end
632 end
633
634 def handle_incoming(
635 %{
636 "type" => "Undo",
637 "object" => %{"type" => "Announce", "object" => object_id},
638 "actor" => _actor,
639 "id" => id
640 } = data,
641 _options
642 ) do
643 with actor <- Containment.get_actor(data),
644 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
645 {:ok, object} <- get_obj_helper(object_id),
646 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
647 {:ok, activity}
648 else
649 _e -> :error
650 end
651 end
652
653 def handle_incoming(
654 %{
655 "type" => "Undo",
656 "object" => %{"type" => "Follow", "object" => followed},
657 "actor" => follower,
658 "id" => id
659 } = _data,
660 _options
661 ) do
662 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
663 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
664 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
665 User.unfollow(follower, followed)
666 {:ok, activity}
667 else
668 _e -> :error
669 end
670 end
671
672 def handle_incoming(
673 %{
674 "type" => "Undo",
675 "object" => %{"type" => "Block", "object" => blocked},
676 "actor" => blocker,
677 "id" => id
678 } = _data,
679 _options
680 ) do
681 with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
682 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
683 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
684 User.unblock(blocker, blocked)
685 {:ok, activity}
686 else
687 _e -> :error
688 end
689 end
690
691 def handle_incoming(
692 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
693 _options
694 ) do
695 with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
696 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
697 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
698 User.unfollow(blocker, blocked)
699 User.block(blocker, blocked)
700 {:ok, activity}
701 else
702 _e -> :error
703 end
704 end
705
706 def handle_incoming(
707 %{
708 "type" => "Undo",
709 "object" => %{"type" => "Like", "object" => object_id},
710 "actor" => _actor,
711 "id" => id
712 } = data,
713 _options
714 ) do
715 with actor <- Containment.get_actor(data),
716 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
717 {:ok, object} <- get_obj_helper(object_id),
718 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
719 {:ok, activity}
720 else
721 _e -> :error
722 end
723 end
724
725 def handle_incoming(_, _), do: :error
726
727 @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
728 def get_obj_helper(id, options \\ []) do
729 case Object.normalize(id, true, options) do
730 %Object{} = object -> {:ok, object}
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(&build_mention_tag/1)
874
875 tags = object["tag"] || []
876
877 Map.put(object, "tag", tags ++ mentions)
878 end
879
880 defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
881 %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
882 end
883
884 def take_emoji_tags(%User{info: %{emoji: emoji} = _user_info} = _user) do
885 emoji
886 |> Enum.flat_map(&Map.to_list/1)
887 |> Enum.map(&build_emoji_tag/1)
888 end
889
890 # TODO: we should probably send mtime instead of unix epoch time for updated
891 def add_emoji_tags(%{"emoji" => emoji} = object) do
892 tags = object["tag"] || []
893
894 out = Enum.map(emoji, &build_emoji_tag/1)
895
896 Map.put(object, "tag", tags ++ out)
897 end
898
899 def add_emoji_tags(object), do: object
900
901 defp build_emoji_tag({name, url}) do
902 %{
903 "icon" => %{"url" => url, "type" => "Image"},
904 "name" => ":" <> name <> ":",
905 "type" => "Emoji",
906 "updated" => "1970-01-01T00:00:00Z",
907 "id" => url
908 }
909 end
910
911 def set_conversation(object) do
912 Map.put(object, "conversation", object["context"])
913 end
914
915 def set_sensitive(object) do
916 tags = object["tag"] || []
917 Map.put(object, "sensitive", "nsfw" in tags)
918 end
919
920 def set_type(%{"type" => "Answer"} = object) do
921 Map.put(object, "type", "Note")
922 end
923
924 def set_type(object), do: object
925
926 def add_attributed_to(object) do
927 attributed_to = object["attributedTo"] || object["actor"]
928 Map.put(object, "attributedTo", attributed_to)
929 end
930
931 def prepare_attachments(object) do
932 attachments =
933 (object["attachment"] || [])
934 |> Enum.map(fn data ->
935 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
936 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
937 end)
938
939 Map.put(object, "attachment", attachments)
940 end
941
942 defp strip_internal_fields(object) do
943 object
944 |> Map.drop([
945 "likes",
946 "like_count",
947 "announcements",
948 "announcement_count",
949 "emoji",
950 "context_id",
951 "deleted_activity_id"
952 ])
953 end
954
955 defp strip_internal_tags(%{"tag" => tags} = object) do
956 tags = Enum.filter(tags, fn x -> is_map(x) end)
957
958 Map.put(object, "tag", tags)
959 end
960
961 defp strip_internal_tags(object), do: object
962
963 def perform(:user_upgrade, user) do
964 # we pass a fake user so that the followers collection is stripped away
965 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
966
967 q =
968 from(
969 u in User,
970 where: ^old_follower_address in u.following,
971 update: [
972 set: [
973 following:
974 fragment(
975 "array_replace(?,?,?)",
976 u.following,
977 ^old_follower_address,
978 ^user.follower_address
979 )
980 ]
981 ]
982 )
983
984 Repo.update_all(q, [])
985
986 maybe_retire_websub(user.ap_id)
987
988 q =
989 from(
990 a in Activity,
991 where: ^old_follower_address in a.recipients,
992 update: [
993 set: [
994 recipients:
995 fragment(
996 "array_replace(?,?,?)",
997 a.recipients,
998 ^old_follower_address,
999 ^user.follower_address
1000 )
1001 ]
1002 ]
1003 )
1004
1005 Repo.update_all(q, [])
1006 end
1007
1008 def upgrade_user_from_ap_id(ap_id) do
1009 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1010 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1011 already_ap <- User.ap_enabled?(user),
1012 {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
1013 unless already_ap do
1014 PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
1015 end
1016
1017 {:ok, user}
1018 else
1019 %User{} = user -> {:ok, user}
1020 e -> e
1021 end
1022 end
1023
1024 def maybe_retire_websub(ap_id) do
1025 # some sanity checks
1026 if is_binary(ap_id) && String.length(ap_id) > 8 do
1027 q =
1028 from(
1029 ws in Pleroma.Web.Websub.WebsubClientSubscription,
1030 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
1031 )
1032
1033 Repo.delete_all(q)
1034 end
1035 end
1036
1037 def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
1038 Map.put(data, "url", url["href"])
1039 end
1040
1041 def maybe_fix_user_url(data), do: data
1042
1043 def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
1044 end