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