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