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