formatting
[akkoma] / lib / pleroma / web / activity_pub / transmogrifier.ex
1 defmodule Pleroma.Web.ActivityPub.Transmogrifier do
2 @moduledoc """
3 A module to handle coding from internal to wire ActivityPub and back.
4 """
5 alias Pleroma.User
6 alias Pleroma.Object
7 alias Pleroma.Object.{Containment, Fetcher}
8 alias Pleroma.Activity
9 alias Pleroma.Repo
10 alias Pleroma.Web.ActivityPub.ActivityPub
11 alias Pleroma.Web.ActivityPub.Utils
12
13 import Ecto.Query
14
15 require Logger
16
17 @doc """
18 Modifies an incoming AP object (mastodon format) to our internal format.
19 """
20 def fix_object(object) do
21 object
22 |> fix_actor
23 |> fix_attachments
24 |> fix_url
25 |> fix_context
26 |> fix_in_reply_to
27 |> fix_emoji
28 |> fix_tag
29 |> fix_content_map
30 |> fix_likes
31 |> fix_addressing
32 end
33
34 def fix_addressing_list(map, field) do
35 if is_binary(map[field]) do
36 map
37 |> Map.put(field, [map[field]])
38 else
39 map
40 end
41 end
42
43 def fix_addressing(map) do
44 map
45 |> fix_addressing_list("to")
46 |> fix_addressing_list("cc")
47 |> fix_addressing_list("bto")
48 |> fix_addressing_list("bcc")
49 end
50
51 def fix_actor(%{"attributedTo" => actor} = object) do
52 object
53 |> Map.put("actor", Containment.get_actor(%{"actor" => actor}))
54 end
55
56 def fix_likes(%{"likes" => likes} = object)
57 when is_bitstring(likes) do
58 # Check for standardisation
59 # This is what Peertube does
60 # curl -H 'Accept: application/activity+json' $likes | jq .totalItems
61 object
62 |> Map.put("likes", [])
63 |> Map.put("like_count", 0)
64 end
65
66 def fix_likes(object) do
67 object
68 end
69
70 def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object)
71 when not is_nil(in_reply_to) do
72 in_reply_to_id =
73 cond do
74 is_bitstring(in_reply_to) ->
75 in_reply_to
76
77 is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
78 in_reply_to["id"]
79
80 is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
81 Enum.at(in_reply_to, 0)
82
83 # Maybe I should output an error too?
84 true ->
85 ""
86 end
87
88 case get_obj_helper(in_reply_to_id) do
89 {:ok, replied_object} ->
90 with %Activity{} = activity <-
91 Activity.get_create_activity_by_object_ap_id(replied_object.data["id"]) do
92 object
93 |> Map.put("inReplyTo", replied_object.data["id"])
94 |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
95 |> Map.put("inReplyToStatusId", activity.id)
96 |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
97 |> Map.put("context", replied_object.data["context"] || object["conversation"])
98 else
99 e ->
100 Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
101 object
102 end
103
104 e ->
105 Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
106 object
107 end
108 end
109
110 def fix_in_reply_to(object), do: object
111
112 def fix_context(object) do
113 context = object["context"] || object["conversation"] || Utils.generate_context_id()
114
115 object
116 |> Map.put("context", context)
117 |> Map.put("conversation", context)
118 end
119
120 def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
121 attachments =
122 attachment
123 |> Enum.map(fn data ->
124 url = [%{"type" => "Link", "mediaType" => data["mediaType"], "href" => data["url"]}]
125 Map.put(data, "url", url)
126 end)
127
128 object
129 |> Map.put("attachment", attachments)
130 end
131
132 def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
133 Map.put(object, "attachment", [attachment])
134 |> fix_attachments()
135 end
136
137 def fix_attachments(object), do: object
138
139 def fix_url(%{"url" => url} = object) when is_map(url) do
140 object
141 |> Map.put("url", url["href"])
142 end
143
144 def fix_url(%{"url" => url} = object) when is_list(url) do
145 first_element = Enum.at(url, 0)
146
147 url_string =
148 cond do
149 is_bitstring(first_element) -> first_element
150 is_map(first_element) -> first_element["href"] || ""
151 true -> ""
152 end
153
154 object
155 |> Map.put("url", url_string)
156 end
157
158 def fix_url(object), do: object
159
160 def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
161 emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
162
163 emoji =
164 emoji
165 |> Enum.reduce(%{}, fn data, mapping ->
166 name = String.trim(data["name"], ":")
167
168 mapping |> Map.put(name, data["icon"]["url"])
169 end)
170
171 # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
172 emoji = Map.merge(object["emoji"] || %{}, emoji)
173
174 object
175 |> Map.put("emoji", emoji)
176 end
177
178 def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
179 name = String.trim(tag["name"], ":")
180 emoji = %{name => tag["icon"]["url"]}
181
182 object
183 |> Map.put("emoji", emoji)
184 end
185
186 def fix_emoji(object), do: object
187
188 def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
189 tags =
190 tag
191 |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
192 |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
193
194 combined = tag ++ tags
195
196 object
197 |> Map.put("tag", combined)
198 end
199
200 def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
201 combined = [tag, String.slice(hashtag, 1..-1)]
202
203 object
204 |> Map.put("tag", combined)
205 end
206
207 def fix_tag(object), do: object
208
209 # content map usually only has one language so this will do for now.
210 def fix_content_map(%{"contentMap" => content_map} = object) do
211 content_groups = Map.to_list(content_map)
212 {_, content} = Enum.at(content_groups, 0)
213
214 object
215 |> Map.put("content", content)
216 end
217
218 def fix_content_map(object), do: object
219
220 # disallow objects with bogus IDs
221 def handle_incoming(%{"id" => nil}), do: :error
222 def handle_incoming(%{"id" => ""}), do: :error
223 # length of https:// = 8, should validate better, but good enough for now.
224 def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8), do: :error
225
226 # TODO: validate those with a Ecto scheme
227 # - tags
228 # - emoji
229 def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
230 when objtype in ["Article", "Note", "Video", "Page"] do
231 actor = Containment.get_actor(data)
232
233 data =
234 Map.put(data, "actor", actor)
235 |> fix_addressing
236
237 with nil <- Activity.get_create_activity_by_object_ap_id(object["id"]),
238 %User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do
239 object = fix_object(data["object"])
240
241 params = %{
242 to: data["to"],
243 object: object,
244 actor: user,
245 context: object["conversation"],
246 local: false,
247 published: data["published"],
248 additional:
249 Map.take(data, [
250 "cc",
251 "id"
252 ])
253 }
254
255 ActivityPub.create(params)
256 else
257 %Activity{} = activity -> {:ok, activity}
258 _e -> :error
259 end
260 end
261
262 def handle_incoming(
263 %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
264 ) do
265 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
266 %User{} = follower <- User.get_or_fetch_by_ap_id(follower),
267 {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
268 if not User.locked?(followed) do
269 ActivityPub.accept(%{
270 to: [follower.ap_id],
271 actor: followed.ap_id,
272 object: data,
273 local: true
274 })
275
276 User.follow(follower, followed)
277 end
278
279 {:ok, activity}
280 else
281 _e -> :error
282 end
283 end
284
285 defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
286 with true <- id =~ "follows",
287 %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
288 %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
289 {:ok, activity}
290 else
291 _ -> {:error, nil}
292 end
293 end
294
295 defp mastodon_follow_hack(_), do: {:error, nil}
296
297 defp get_follow_activity(follow_object, followed) do
298 with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
299 {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
300 {:ok, activity}
301 else
302 # Can't find the activity. This might a Mastodon 2.3 "Accept"
303 {:activity, nil} ->
304 mastodon_follow_hack(follow_object, followed)
305
306 _ ->
307 {:error, nil}
308 end
309 end
310
311 def handle_incoming(
312 %{"type" => "Accept", "object" => follow_object, "actor" => actor, "id" => id} = data
313 ) do
314 with actor <- Containment.get_actor(data),
315 %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
316 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
317 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
318 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
319 {:ok, activity} <-
320 ActivityPub.accept(%{
321 to: follow_activity.data["to"],
322 type: "Accept",
323 actor: followed.ap_id,
324 object: follow_activity.data["id"],
325 local: false
326 }) do
327 if not User.following?(follower, followed) do
328 {:ok, follower} = User.follow(follower, followed)
329 end
330
331 {:ok, activity}
332 else
333 _e -> :error
334 end
335 end
336
337 def handle_incoming(
338 %{"type" => "Reject", "object" => follow_object, "actor" => actor, "id" => id} = data
339 ) do
340 with actor <- Containment.get_actor(data),
341 %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
342 {:ok, follow_activity} <- get_follow_activity(follow_object, followed),
343 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
344 %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
345 {:ok, activity} <-
346 ActivityPub.accept(%{
347 to: follow_activity.data["to"],
348 type: "Accept",
349 actor: followed.ap_id,
350 object: follow_activity.data["id"],
351 local: false
352 }) do
353 User.unfollow(follower, followed)
354
355 {:ok, activity}
356 else
357 _e -> :error
358 end
359 end
360
361 def handle_incoming(
362 %{"type" => "Like", "object" => object_id, "actor" => actor, "id" => id} = data
363 ) do
364 with actor <- Containment.get_actor(data),
365 %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
366 {:ok, object} <- get_obj_helper(object_id),
367 {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
368 {:ok, activity}
369 else
370 _e -> :error
371 end
372 end
373
374 def handle_incoming(
375 %{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = data
376 ) do
377 with actor <- Containment.get_actor(data),
378 %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
379 {:ok, object} <- get_obj_helper(object_id),
380 {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false) do
381 {:ok, activity}
382 else
383 _e -> :error
384 end
385 end
386
387 def handle_incoming(
388 %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} =
389 data
390 )
391 when object_type in ["Person", "Application", "Service", "Organization"] do
392 with %User{ap_id: ^actor_id} = actor <- User.get_by_ap_id(object["id"]) do
393 {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
394
395 banner = new_user_data[:info]["banner"]
396 locked = new_user_data[:info]["locked"] || false
397
398 update_data =
399 new_user_data
400 |> Map.take([:name, :bio, :avatar])
401 |> Map.put(:info, %{"banner" => banner, "locked" => locked})
402
403 actor
404 |> User.upgrade_changeset(update_data)
405 |> User.update_and_set_cache()
406
407 ActivityPub.update(%{
408 local: false,
409 to: data["to"] || [],
410 cc: data["cc"] || [],
411 object: object,
412 actor: actor_id
413 })
414 else
415 e ->
416 Logger.error(e)
417 :error
418 end
419 end
420
421 # TODO: We presently assume that any actor on the same origin domain as the object being
422 # deleted has the rights to delete that object. A better way to validate whether or not
423 # the object should be deleted is to refetch the object URI, which should return either
424 # an error or a tombstone. This would allow us to verify that a deletion actually took
425 # place.
426 def handle_incoming(
427 %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data
428 ) do
429 object_id = Utils.get_ap_id(object_id)
430
431 with actor <- Containment.get_actor(data),
432 %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
433 {:ok, object} <- get_obj_helper(object_id),
434 :ok <- Containment.contain_origin(actor.ap_id, object.data),
435 {:ok, activity} <- ActivityPub.delete(object, false) do
436 {:ok, activity}
437 else
438 _e -> :error
439 end
440 end
441
442 def handle_incoming(
443 %{
444 "type" => "Undo",
445 "object" => %{"type" => "Announce", "object" => object_id},
446 "actor" => actor,
447 "id" => id
448 } = data
449 ) do
450 with actor <- Containment.get_actor(data),
451 %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
452 {:ok, object} <- get_obj_helper(object_id),
453 {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
454 {:ok, activity}
455 else
456 _e -> :error
457 end
458 end
459
460 def handle_incoming(
461 %{
462 "type" => "Undo",
463 "object" => %{"type" => "Follow", "object" => followed},
464 "actor" => follower,
465 "id" => id
466 } = _data
467 ) do
468 with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
469 %User{} = follower <- User.get_or_fetch_by_ap_id(follower),
470 {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
471 User.unfollow(follower, followed)
472 {:ok, activity}
473 else
474 e -> :error
475 end
476 end
477
478 def handle_incoming(
479 %{
480 "type" => "Undo",
481 "object" => %{"type" => "Block", "object" => blocked},
482 "actor" => blocker,
483 "id" => id
484 } = _data
485 ) do
486 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
487 %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
488 %User{} = blocker <- User.get_or_fetch_by_ap_id(blocker),
489 {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
490 User.unblock(blocker, blocked)
491 {:ok, activity}
492 else
493 e -> :error
494 end
495 end
496
497 def handle_incoming(
498 %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = data
499 ) do
500 with true <- Pleroma.Config.get([:activitypub, :accept_blocks]),
501 %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
502 %User{} = blocker = User.get_or_fetch_by_ap_id(blocker),
503 {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
504 User.unfollow(blocker, blocked)
505 User.block(blocker, blocked)
506 {:ok, activity}
507 else
508 e -> :error
509 end
510 end
511
512 def handle_incoming(
513 %{
514 "type" => "Undo",
515 "object" => %{"type" => "Like", "object" => object_id},
516 "actor" => actor,
517 "id" => id
518 } = data
519 ) do
520 with actor <- Containment.get_actor(data),
521 %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
522 {:ok, object} <- get_obj_helper(object_id),
523 {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
524 {:ok, activity}
525 else
526 _e -> :error
527 end
528 end
529
530 def handle_incoming(_), do: :error
531
532 def get_obj_helper(id) do
533 if object = Object.normalize(id), do: {:ok, object}, else: nil
534 end
535
536 def set_reply_to_uri(%{"inReplyTo" => inReplyTo} = object) do
537 with false <- String.starts_with?(inReplyTo, "http"),
538 {:ok, %{data: replied_to_object}} <- get_obj_helper(inReplyTo) do
539 Map.put(object, "inReplyTo", replied_to_object["external_url"] || inReplyTo)
540 else
541 _e -> object
542 end
543 end
544
545 def set_reply_to_uri(obj), do: obj
546
547 # Prepares the object of an outgoing create activity.
548 def prepare_object(object) do
549 object
550 |> set_sensitive
551 |> add_hashtags
552 |> add_mention_tags
553 |> add_emoji_tags
554 |> add_attributed_to
555 |> prepare_attachments
556 |> set_conversation
557 |> set_reply_to_uri
558 |> strip_internal_fields
559 |> strip_internal_tags
560 end
561
562 # @doc
563 # """
564 # internal -> Mastodon
565 # """
566
567 def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
568 object =
569 Object.normalize(object_id).data
570 |> prepare_object
571
572 data =
573 data
574 |> Map.put("object", object)
575 |> Map.merge(Utils.make_json_ld_header())
576
577 {:ok, data}
578 end
579
580 # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
581 # because of course it does.
582 def prepare_outgoing(%{"type" => "Accept"} = data) do
583 with follow_activity <- Activity.normalize(data["object"]) do
584 object = %{
585 "actor" => follow_activity.actor,
586 "object" => follow_activity.data["object"],
587 "id" => follow_activity.data["id"],
588 "type" => "Follow"
589 }
590
591 data =
592 data
593 |> Map.put("object", object)
594 |> Map.merge(Utils.make_json_ld_header())
595
596 {:ok, data}
597 end
598 end
599
600 def prepare_outgoing(%{"type" => "Reject"} = data) do
601 with follow_activity <- Activity.normalize(data["object"]) do
602 object = %{
603 "actor" => follow_activity.actor,
604 "object" => follow_activity.data["object"],
605 "id" => follow_activity.data["id"],
606 "type" => "Follow"
607 }
608
609 data =
610 data
611 |> Map.put("object", object)
612 |> Map.merge(Utils.make_json_ld_header())
613
614 {:ok, data}
615 end
616 end
617
618 def prepare_outgoing(%{"type" => _type} = data) do
619 data =
620 data
621 |> maybe_fix_object_url
622 |> Map.merge(Utils.make_json_ld_header())
623
624 {:ok, data}
625 end
626
627 def maybe_fix_object_url(data) do
628 if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
629 case get_obj_helper(data["object"]) do
630 {:ok, relative_object} ->
631 if relative_object.data["external_url"] do
632 _data =
633 data
634 |> Map.put("object", relative_object.data["external_url"])
635 else
636 data
637 end
638
639 e ->
640 Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
641 data
642 end
643 else
644 data
645 end
646 end
647
648 def add_hashtags(object) do
649 tags =
650 (object["tag"] || [])
651 |> Enum.map(fn tag ->
652 %{
653 "href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
654 "name" => "##{tag}",
655 "type" => "Hashtag"
656 }
657 end)
658
659 object
660 |> Map.put("tag", tags)
661 end
662
663 def add_mention_tags(object) do
664 mentions =
665 object
666 |> Utils.get_notified_from_object()
667 |> Enum.map(fn user ->
668 %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
669 end)
670
671 tags = object["tag"] || []
672
673 object
674 |> Map.put("tag", tags ++ mentions)
675 end
676
677 # TODO: we should probably send mtime instead of unix epoch time for updated
678 def add_emoji_tags(object) do
679 tags = object["tag"] || []
680 emoji = object["emoji"] || []
681
682 out =
683 emoji
684 |> Enum.map(fn {name, url} ->
685 %{
686 "icon" => %{"url" => url, "type" => "Image"},
687 "name" => ":" <> name <> ":",
688 "type" => "Emoji",
689 "updated" => "1970-01-01T00:00:00Z",
690 "id" => url
691 }
692 end)
693
694 object
695 |> Map.put("tag", tags ++ out)
696 end
697
698 def set_conversation(object) do
699 Map.put(object, "conversation", object["context"])
700 end
701
702 def set_sensitive(object) do
703 tags = object["tag"] || []
704 Map.put(object, "sensitive", "nsfw" in tags)
705 end
706
707 def add_attributed_to(object) do
708 attributedTo = object["attributedTo"] || object["actor"]
709
710 object
711 |> Map.put("attributedTo", attributedTo)
712 end
713
714 def prepare_attachments(object) do
715 attachments =
716 (object["attachment"] || [])
717 |> Enum.map(fn data ->
718 [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
719 %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
720 end)
721
722 object
723 |> Map.put("attachment", attachments)
724 end
725
726 defp strip_internal_fields(object) do
727 object
728 |> Map.drop([
729 "likes",
730 "like_count",
731 "announcements",
732 "announcement_count",
733 "emoji",
734 "context_id"
735 ])
736 end
737
738 defp strip_internal_tags(%{"tag" => tags} = object) do
739 tags =
740 tags
741 |> Enum.filter(fn x -> is_map(x) end)
742
743 object
744 |> Map.put("tag", tags)
745 end
746
747 defp strip_internal_tags(object), do: object
748
749 defp user_upgrade_task(user) do
750 old_follower_address = User.ap_followers(user)
751
752 q =
753 from(
754 u in User,
755 where: ^old_follower_address in u.following,
756 update: [
757 set: [
758 following:
759 fragment(
760 "array_replace(?,?,?)",
761 u.following,
762 ^old_follower_address,
763 ^user.follower_address
764 )
765 ]
766 ]
767 )
768
769 Repo.update_all(q, [])
770
771 maybe_retire_websub(user.ap_id)
772
773 # Only do this for recent activties, don't go through the whole db.
774 # Only look at the last 1000 activities.
775 since = (Repo.aggregate(Activity, :max, :id) || 0) - 1_000
776
777 q =
778 from(
779 a in Activity,
780 where: ^old_follower_address in a.recipients,
781 where: a.id > ^since,
782 update: [
783 set: [
784 recipients:
785 fragment(
786 "array_replace(?,?,?)",
787 a.recipients,
788 ^old_follower_address,
789 ^user.follower_address
790 )
791 ]
792 ]
793 )
794
795 Repo.update_all(q, [])
796 end
797
798 def upgrade_user_from_ap_id(ap_id, async \\ true) do
799 with %User{local: false} = user <- User.get_by_ap_id(ap_id),
800 {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do
801 already_ap = User.ap_enabled?(user)
802
803 {:ok, user} =
804 User.upgrade_changeset(user, data)
805 |> Repo.update()
806
807 if !already_ap do
808 # This could potentially take a long time, do it in the background
809 if async do
810 Task.start(fn ->
811 user_upgrade_task(user)
812 end)
813 else
814 user_upgrade_task(user)
815 end
816 end
817
818 {:ok, user}
819 else
820 e -> e
821 end
822 end
823
824 def maybe_retire_websub(ap_id) do
825 # some sanity checks
826 if is_binary(ap_id) && String.length(ap_id) > 8 do
827 q =
828 from(
829 ws in Pleroma.Web.Websub.WebsubClientSubscription,
830 where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
831 )
832
833 Repo.delete_all(q)
834 end
835 end
836
837 def maybe_fix_user_url(data) do
838 if is_map(data["url"]) do
839 Map.put(data, "url", data["url"]["href"])
840 else
841 data
842 end
843 end
844
845 def maybe_fix_user_object(data) do
846 data
847 |> maybe_fix_user_url
848 end
849 end