rework to use properties instead of compound typing, per SocialCG
[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" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
435 options
436 ) do
437 actor = Containment.get_actor(data)
438
439 data =
440 Map.put(data, "actor", actor)
441 |> fix_addressing
442
443 with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
444 options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
445 object = fix_object(object, options)
446
447 params = %{
448 to: data["to"],
449 object: object,
450 actor: user,
451 context: nil,
452 local: false,
453 published: data["published"],
454 additional: Map.take(data, ["cc", "id"])
455 }
456
457 ActivityPub.listen(params)
458 else
459 _e -> :error
460 end
461 end
462
463 def handle_incoming(
464 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
465 _options
466 ) do
467 with %User{local: true} = followed <-
468 User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})),
469 {:ok, %User{} = follower} <-
470 User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})),
471 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
472 with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
473 {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
474 {_, false} <- {:user_locked, User.locked?(followed)},
475 {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
476 {_, {:ok, _}} <-
477 {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do
478 ActivityPub.accept(%{
479 to: [follower.ap_id],
480 actor: followed,
481 object: data,
482 local: true
483 })
484 else
485 {:user_blocked, true} ->
486 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
487
488 ActivityPub.reject(%{
489 to: [follower.ap_id],
490 actor: followed,
491 object: data,
492 local: true
493 })
494
495 {:follow, {:error, _}} ->
496 {:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
497
498 ActivityPub.reject(%{
499 to: [follower.ap_id],
500 actor: followed,
501 object: data,
502 local: true
503 })
504
505 {:user_locked, true} ->
506 :noop
507 end
508
509 {:ok, activity}
510 else
511 _e ->
512 :error
513 end
514 end
515
516 def handle_incoming(
517 %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
518 _options
519 ) do
520 with actor <- Containment.get_actor(data),
521 {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor),
522 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
523 {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
524 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
525 {:ok, _follower} = User.follow(follower, followed) do
526 ActivityPub.accept(%{
527 to: follow_activity.data["to"],
528 type: "Accept",
529 actor: followed,
530 object: follow_activity.data["id"],
531 local: false,
532 activity_id: id
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 activity_id: id
556 }) do
557 User.unfollow(follower, followed)
558
559 {:ok, activity}
560 else
561 _e -> :error
562 end
563 end
564
565 def handle_incoming(
566 %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
567 _options
568 ) do
569 with actor <- Containment.get_actor(data),
570 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
571 {:ok, object} <- get_obj_helper(object_id),
572 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
573 {:ok, activity}
574 else
575 _e -> :error
576 end
577 end
578
579 def handle_incoming(
580 %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
581 _options
582 ) do
583 with actor <- Containment.get_actor(data),
584 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
585 {:ok, object} <- get_embedded_obj_helper(object_id, actor),
586 public <- Visibility.is_public?(data),
587 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
588 {:ok, activity}
589 else
590 _e -> :error
591 end
592 end
593
594 def handle_incoming(
595 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
596 data,
597 _options
598 )
599 when object_type in [
600 "Person",
601 "Application",
602 "Service",
603 "Organization"
604 ] do
605 with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
606 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
607
608 banner = new_user_data[:info][:banner]
609 locked = new_user_data[:info][:locked] || false
610 attachment = get_in(new_user_data, [:info, :source_data, "attachment"]) || []
611 invisible = new_user_data[:info][:invisible] || false
612
613 fields =
614 attachment
615 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
616 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
617
618 update_data =
619 new_user_data
620 |> Map.take([:name, :bio, :avatar])
621 |> Map.put(:info, %{banner: banner, locked: locked, fields: fields, invisible: invisible})
622
623 actor
624 |> User.upgrade_changeset(update_data, true)
625 |> User.update_and_set_cache()
626
627 ActivityPub.update(%{
628 local: false,
629 to: data["to"] || [],
630 cc: data["cc"] || [],
631 object: object,
632 actor: actor_id,
633 activity_id: data["id"]
634 })
635 else
636 e ->
637 Logger.error(e)
638 :error
639 end
640 end
641
642 # TODO: We presently assume that any actor on the same origin domain as the object being
643 # deleted has the rights to delete that object. A better way to validate whether or not
644 # the object should be deleted is to refetch the object URI, which should return either
645 # an error or a tombstone. This would allow us to verify that a deletion actually took
646 # place.
647 def handle_incoming(
648 %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data,
649 _options
650 ) do
651 object_id = Utils.get_ap_id(object_id)
652
653 with actor <- Containment.get_actor(data),
654 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
655 {:ok, object} <- get_obj_helper(object_id),
656 :ok <- Containment.contain_origin(actor.ap_id, object.data),
657 {:ok, activity} <-
658 ActivityPub.delete(object, local: false, activity_id: id, actor: actor.ap_id) do
659 {:ok, activity}
660 else
661 nil ->
662 case User.get_cached_by_ap_id(object_id) do
663 %User{ap_id: ^actor} = user ->
664 User.delete(user)
665
666 nil ->
667 :error
668 end
669
670 _e ->
671 :error
672 end
673 end
674
675 def handle_incoming(
676 %{
677 "type" => "Undo",
678 "object" => %{"type" => "Announce", "object" => object_id},
679 "actor" => _actor,
680 "id" => id
681 } = data,
682 _options
683 ) do
684 with actor <- Containment.get_actor(data),
685 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
686 {:ok, object} <- get_obj_helper(object_id),
687 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
688 {:ok, activity}
689 else
690 _e -> :error
691 end
692 end
693
694 def handle_incoming(
695 %{
696 "type" => "Undo",
697 "object" => %{"type" => "Follow", "object" => followed},
698 "actor" => follower,
699 "id" => id
700 } = _data,
701 _options
702 ) do
703 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
704 {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
705 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
706 User.unfollow(follower, followed)
707 {:ok, activity}
708 else
709 _e -> :error
710 end
711 end
712
713 def handle_incoming(
714 %{
715 "type" => "Undo",
716 "object" => %{"type" => "Block", "object" => blocked},
717 "actor" => blocker,
718 "id" => id
719 } = _data,
720 _options
721 ) do
722 with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
723 {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
724 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
725 User.unblock(blocker, blocked)
726 {:ok, activity}
727 else
728 _e -> :error
729 end
730 end
731
732 def handle_incoming(
733 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
734 _options
735 ) do
736 with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
737 {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
738 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
739 User.unfollow(blocker, blocked)
740 User.block(blocker, blocked)
741 {:ok, activity}
742 else
743 _e -> :error
744 end
745 end
746
747 def handle_incoming(
748 %{
749 "type" => "Undo",
750 "object" => %{"type" => "Like", "object" => object_id},
751 "actor" => _actor,
752 "id" => id
753 } = data,
754 _options
755 ) do
756 with actor <- Containment.get_actor(data),
757 {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
758 {:ok, object} <- get_obj_helper(object_id),
759 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
760 {:ok, activity}
761 else
762 _e -> :error
763 end
764 end
765
766 # For Undos that don't have the complete object attached, try to find it in our database.
767 def handle_incoming(
768 %{
769 "type" => "Undo",
770 "object" => object
771 } = activity,
772 options
773 )
774 when is_binary(object) do
775 with %Activity{data: data} <- Activity.get_by_ap_id(object) do
776 activity
777 |> Map.put("object", data)
778 |> handle_incoming(options)
779 else
780 _e -> :error
781 end
782 end
783
784 def handle_incoming(_, _), do: :error
785
786 @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
787 def get_obj_helper(id, options \\ []) do
788 case Object.normalize(id, true, options) do
789 %Object{} = object -> {:ok, object}
790 _ -> nil
791 end
792 end
793
794 @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
795 def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
796 ap_id: ap_id
797 })
798 when attributed_to == ap_id do
799 with {:ok, activity} <-
800 handle_incoming(%{
801 "type" => "Create",
802 "to" => data["to"],
803 "cc" => data["cc"],
804 "actor" => attributed_to,
805 "object" => data
806 }) do
807 {:ok, Object.normalize(activity)}
808 else
809 _ -> get_obj_helper(object_id)
810 end
811 end
812
813 def get_embedded_obj_helper(object_id, _) do
814 get_obj_helper(object_id)
815 end
816
817 def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
818 with false <- String.starts_with?(in_reply_to, "http"),
819 {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
820 Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
821 else
822 _e -> object
823 end
824 end
825
826 def set_reply_to_uri(obj), do: obj
827
828 # Prepares the object of an outgoing create activity.
829 def prepare_object(object) do
830 object
831 |> set_sensitive
832 |> add_hashtags
833 |> add_mention_tags
834 |> add_emoji_tags
835 |> add_attributed_to
836 |> prepare_attachments
837 |> set_conversation
838 |> set_reply_to_uri
839 |> strip_internal_fields
840 |> strip_internal_tags
841 |> set_type
842 end
843
844 # @doc
845 # """
846 # internal -> Mastodon
847 # """
848
849 def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
850 when activity_type in ["Create", "Listen"] do
851 object =
852 object_id
853 |> Object.normalize()
854 |> Map.get(:data)
855 |> prepare_object
856
857 data =
858 data
859 |> Map.put("object", object)
860 |> Map.merge(Utils.make_json_ld_header())
861 |> Map.delete("bcc")
862
863 {:ok, data}
864 end
865
866 def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
867 object =
868 object_id
869 |> Object.normalize()
870
871 data =
872 if Visibility.is_private?(object) && object.data["actor"] == ap_id do
873 data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
874 else
875 data |> maybe_fix_object_url
876 end
877
878 data =
879 data
880 |> strip_internal_fields
881 |> Map.merge(Utils.make_json_ld_header())
882 |> Map.delete("bcc")
883
884 {:ok, data}
885 end
886
887 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
888 # because of course it does.
889 def prepare_outgoing(%{"type" => "Accept"} = data) do
890 with follow_activity <- Activity.normalize(data["object"]) do
891 object = %{
892 "actor" => follow_activity.actor,
893 "object" => follow_activity.data["object"],
894 "id" => follow_activity.data["id"],
895 "type" => "Follow"
896 }
897
898 data =
899 data
900 |> Map.put("object", object)
901 |> Map.merge(Utils.make_json_ld_header())
902
903 {:ok, data}
904 end
905 end
906
907 def prepare_outgoing(%{"type" => "Reject"} = data) do
908 with follow_activity <- Activity.normalize(data["object"]) do
909 object = %{
910 "actor" => follow_activity.actor,
911 "object" => follow_activity.data["object"],
912 "id" => follow_activity.data["id"],
913 "type" => "Follow"
914 }
915
916 data =
917 data
918 |> Map.put("object", object)
919 |> Map.merge(Utils.make_json_ld_header())
920
921 {:ok, data}
922 end
923 end
924
925 def prepare_outgoing(%{"type" => _type} = data) do
926 data =
927 data
928 |> strip_internal_fields
929 |> maybe_fix_object_url
930 |> Map.merge(Utils.make_json_ld_header())
931
932 {:ok, data}
933 end
934
935 def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
936 with false <- String.starts_with?(object, "http"),
937 {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
938 %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
939 relative_object do
940 Map.put(data, "object", external_url)
941 else
942 {:fetch, e} ->
943 Logger.error("Couldn't fetch #{object} #{inspect(e)}")
944 data
945
946 _ ->
947 data
948 end
949 end
950
951 def maybe_fix_object_url(data), do: data
952
953 def add_hashtags(object) do
954 tags =
955 (object["tag"] || [])
956 |> Enum.map(fn
957 # Expand internal representation tags into AS2 tags.
958 tag when is_binary(tag) ->
959 %{
960 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
961 "name" => "##{tag}",
962 "type" => "Hashtag"
963 }
964
965 # Do not process tags which are already AS2 tag objects.
966 tag when is_map(tag) ->
967 tag
968 end)
969
970 Map.put(object, "tag", tags)
971 end
972
973 def add_mention_tags(object) do
974 mentions =
975 object
976 |> Utils.get_notified_from_object()
977 |> Enum.map(&build_mention_tag/1)
978
979 tags = object["tag"] || []
980
981 Map.put(object, "tag", tags ++ mentions)
982 end
983
984 defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
985 %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
986 end
987
988 def take_emoji_tags(%User{info: %{emoji: emoji} = _user_info} = _user) do
989 emoji
990 |> Enum.flat_map(&Map.to_list/1)
991 |> Enum.map(&build_emoji_tag/1)
992 end
993
994 # TODO: we should probably send mtime instead of unix epoch time for updated
995 def add_emoji_tags(%{"emoji" => emoji} = object) do
996 tags = object["tag"] || []
997
998 out = Enum.map(emoji, &build_emoji_tag/1)
999
1000 Map.put(object, "tag", tags ++ out)
1001 end
1002
1003 def add_emoji_tags(object), do: object
1004
1005 defp build_emoji_tag({name, url}) do
1006 %{
1007 "icon" => %{"url" => url, "type" => "Image"},
1008 "name" => ":" <> name <> ":",
1009 "type" => "Emoji",
1010 "updated" => "1970-01-01T00:00:00Z",
1011 "id" => url
1012 }
1013 end
1014
1015 def set_conversation(object) do
1016 Map.put(object, "conversation", object["context"])
1017 end
1018
1019 def set_sensitive(object) do
1020 tags = object["tag"] || []
1021 Map.put(object, "sensitive", "nsfw" in tags)
1022 end
1023
1024 def set_type(%{"type" => "Answer"} = object) do
1025 Map.put(object, "type", "Note")
1026 end
1027
1028 def set_type(object), do: object
1029
1030 def add_attributed_to(object) do
1031 attributed_to = object["attributedTo"] || object["actor"]
1032 Map.put(object, "attributedTo", attributed_to)
1033 end
1034
1035 def prepare_attachments(object) do
1036 attachments =
1037 (object["attachment"] || [])
1038 |> Enum.map(fn data ->
1039 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
1040 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
1041 end)
1042
1043 Map.put(object, "attachment", attachments)
1044 end
1045
1046 defp strip_internal_fields(object) do
1047 object
1048 |> Map.drop(Pleroma.Constants.object_internal_fields())
1049 end
1050
1051 defp strip_internal_tags(%{"tag" => tags} = object) do
1052 tags = Enum.filter(tags, fn x -> is_map(x) end)
1053
1054 Map.put(object, "tag", tags)
1055 end
1056
1057 defp strip_internal_tags(object), do: object
1058
1059 def perform(:user_upgrade, user) do
1060 # we pass a fake user so that the followers collection is stripped away
1061 old_follower_address = User.ap_followers(%User{nickname: user.nickname})
1062
1063 q =
1064 from(
1065 u in User,
1066 where: ^old_follower_address in u.following,
1067 update: [
1068 set: [
1069 following:
1070 fragment(
1071 "array_replace(?,?,?)",
1072 u.following,
1073 ^old_follower_address,
1074 ^user.follower_address
1075 )
1076 ]
1077 ]
1078 )
1079
1080 Repo.update_all(q, [])
1081
1082 q =
1083 from(
1084 a in Activity,
1085 where: ^old_follower_address in a.recipients,
1086 update: [
1087 set: [
1088 recipients:
1089 fragment(
1090 "array_replace(?,?,?)",
1091 a.recipients,
1092 ^old_follower_address,
1093 ^user.follower_address
1094 )
1095 ]
1096 ]
1097 )
1098
1099 Repo.update_all(q, [])
1100 end
1101
1102 def upgrade_user_from_ap_id(ap_id) do
1103 with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
1104 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
1105 already_ap <- User.ap_enabled?(user),
1106 {:ok, user} <- upgrade_user(user, data) do
1107 if not already_ap do
1108 TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
1109 end
1110
1111 {:ok, user}
1112 else
1113 %User{} = user -> {:ok, user}
1114 e -> e
1115 end
1116 end
1117
1118 defp upgrade_user(user, data) do
1119 user
1120 |> User.upgrade_changeset(data, true)
1121 |> User.update_and_set_cache()
1122 end
1123
1124 def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
1125 Map.put(data, "url", url["href"])
1126 end
1127
1128 def maybe_fix_user_url(data), do: data
1129
1130 def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
1131 end