Merge branch 'develop' into feature/reports-groups-and-multiple-state-update
[akkoma] / lib / pleroma / web / activity_pub / utils.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.Utils do
6 alias Ecto.Changeset
7 alias Ecto.UUID
8 alias Pleroma.Activity
9 alias Pleroma.Activity.Queries
10 alias Pleroma.Notification
11 alias Pleroma.Object
12 alias Pleroma.Repo
13 alias Pleroma.User
14 alias Pleroma.Web
15 alias Pleroma.Web.ActivityPub.ActivityPub
16 alias Pleroma.Web.ActivityPub.Visibility
17 alias Pleroma.Web.AdminAPI.AccountView
18 alias Pleroma.Web.Endpoint
19 alias Pleroma.Web.Router.Helpers
20
21 import Ecto.Query
22
23 require Logger
24 require Pleroma.Constants
25
26 @supported_object_types ["Article", "Note", "Video", "Page", "Question", "Answer", "Audio"]
27 @strip_status_report_states ~w(closed resolved)
28 @supported_report_states ~w(open closed resolved)
29 @valid_visibilities ~w(public unlisted private direct)
30
31 # Some implementations send the actor URI as the actor field, others send the entire actor object,
32 # so figure out what the actor's URI is based on what we have.
33 def get_ap_id(%{"id" => id} = _), do: id
34 def get_ap_id(id), do: id
35
36 def normalize_params(params) do
37 Map.put(params, "actor", get_ap_id(params["actor"]))
38 end
39
40 @spec determine_explicit_mentions(map()) :: map()
41 def determine_explicit_mentions(%{"tag" => tag} = _) when is_list(tag) do
42 Enum.flat_map(tag, fn
43 %{"type" => "Mention", "href" => href} -> [href]
44 _ -> []
45 end)
46 end
47
48 def determine_explicit_mentions(%{"tag" => tag} = object) when is_map(tag) do
49 object
50 |> Map.put("tag", [tag])
51 |> determine_explicit_mentions()
52 end
53
54 def determine_explicit_mentions(_), do: []
55
56 @spec label_in_collection?(any(), any()) :: boolean()
57 defp label_in_collection?(ap_id, coll) when is_binary(coll), do: ap_id == coll
58 defp label_in_collection?(ap_id, coll) when is_list(coll), do: ap_id in coll
59 defp label_in_collection?(_, _), do: false
60
61 @spec label_in_message?(String.t(), map()) :: boolean()
62 def label_in_message?(label, params),
63 do:
64 [params["to"], params["cc"], params["bto"], params["bcc"]]
65 |> Enum.any?(&label_in_collection?(label, &1))
66
67 @spec unaddressed_message?(map()) :: boolean()
68 def unaddressed_message?(params),
69 do:
70 [params["to"], params["cc"], params["bto"], params["bcc"]]
71 |> Enum.all?(&is_nil(&1))
72
73 @spec recipient_in_message(User.t(), User.t(), map()) :: boolean()
74 def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params),
75 do:
76 label_in_message?(ap_id, params) || unaddressed_message?(params) ||
77 User.following?(recipient, actor)
78
79 defp extract_list(target) when is_binary(target), do: [target]
80 defp extract_list(lst) when is_list(lst), do: lst
81 defp extract_list(_), do: []
82
83 def maybe_splice_recipient(ap_id, params) do
84 need_splice? =
85 !label_in_collection?(ap_id, params["to"]) &&
86 !label_in_collection?(ap_id, params["cc"])
87
88 if need_splice? do
89 cc_list = extract_list(params["cc"])
90 Map.put(params, "cc", [ap_id | cc_list])
91 else
92 params
93 end
94 end
95
96 def make_json_ld_header do
97 %{
98 "@context" => [
99 "https://www.w3.org/ns/activitystreams",
100 "#{Web.base_url()}/schemas/litepub-0.1.jsonld",
101 %{
102 "@language" => "und"
103 }
104 ]
105 }
106 end
107
108 def make_date do
109 DateTime.utc_now() |> DateTime.to_iso8601()
110 end
111
112 def generate_activity_id do
113 generate_id("activities")
114 end
115
116 def generate_context_id do
117 generate_id("contexts")
118 end
119
120 def generate_object_id do
121 Helpers.o_status_url(Endpoint, :object, UUID.generate())
122 end
123
124 def generate_id(type) do
125 "#{Web.base_url()}/#{type}/#{UUID.generate()}"
126 end
127
128 def get_notified_from_object(%{"type" => type} = object) when type in @supported_object_types do
129 fake_create_activity = %{
130 "to" => object["to"],
131 "cc" => object["cc"],
132 "type" => "Create",
133 "object" => object
134 }
135
136 get_notified_from_object(fake_create_activity)
137 end
138
139 def get_notified_from_object(object) do
140 Notification.get_notified_from_activity(%Activity{data: object}, false)
141 end
142
143 def create_context(context) do
144 context = context || generate_id("contexts")
145
146 # Ecto has problems accessing the constraint inside the jsonb,
147 # so we explicitly check for the existed object before insert
148 object = Object.get_cached_by_ap_id(context)
149
150 with true <- is_nil(object),
151 changeset <- Object.context_mapping(context),
152 {:ok, inserted_object} <- Repo.insert(changeset) do
153 inserted_object
154 else
155 _ ->
156 object
157 end
158 end
159
160 @doc """
161 Enqueues an activity for federation if it's local
162 """
163 @spec maybe_federate(any()) :: :ok
164 def maybe_federate(%Activity{local: true} = activity) do
165 if Pleroma.Config.get!([:instance, :federating]) do
166 Pleroma.Web.Federator.publish(activity)
167 end
168
169 :ok
170 end
171
172 def maybe_federate(_), do: :ok
173
174 @doc """
175 Adds an id and a published data if they aren't there,
176 also adds it to an included object
177 """
178 @spec lazy_put_activity_defaults(map(), boolean) :: map()
179 def lazy_put_activity_defaults(map, fake? \\ false)
180
181 def lazy_put_activity_defaults(map, true) do
182 map
183 |> Map.put_new("id", "pleroma:fakeid")
184 |> Map.put_new_lazy("published", &make_date/0)
185 |> Map.put_new("context", "pleroma:fakecontext")
186 |> Map.put_new("context_id", -1)
187 |> lazy_put_object_defaults(true)
188 end
189
190 def lazy_put_activity_defaults(map, _fake?) do
191 %{data: %{"id" => context}, id: context_id} = create_context(map["context"])
192
193 map
194 |> Map.put_new_lazy("id", &generate_activity_id/0)
195 |> Map.put_new_lazy("published", &make_date/0)
196 |> Map.put_new("context", context)
197 |> Map.put_new("context_id", context_id)
198 |> lazy_put_object_defaults(false)
199 end
200
201 # Adds an id and published date if they aren't there.
202 #
203 @spec lazy_put_object_defaults(map(), boolean()) :: map()
204 defp lazy_put_object_defaults(%{"object" => map} = activity, true)
205 when is_map(map) do
206 object =
207 map
208 |> Map.put_new("id", "pleroma:fake_object_id")
209 |> Map.put_new_lazy("published", &make_date/0)
210 |> Map.put_new("context", activity["context"])
211 |> Map.put_new("context_id", activity["context_id"])
212 |> Map.put_new("fake", true)
213
214 %{activity | "object" => object}
215 end
216
217 defp lazy_put_object_defaults(%{"object" => map} = activity, _)
218 when is_map(map) do
219 object =
220 map
221 |> Map.put_new_lazy("id", &generate_object_id/0)
222 |> Map.put_new_lazy("published", &make_date/0)
223 |> Map.put_new("context", activity["context"])
224 |> Map.put_new("context_id", activity["context_id"])
225
226 %{activity | "object" => object}
227 end
228
229 defp lazy_put_object_defaults(activity, _), do: activity
230
231 @doc """
232 Inserts a full object if it is contained in an activity.
233 """
234 def insert_full_object(%{"object" => %{"type" => type} = object_data} = map)
235 when is_map(object_data) and type in @supported_object_types do
236 with {:ok, object} <- Object.create(object_data) do
237 map = Map.put(map, "object", object.data["id"])
238
239 {:ok, map, object}
240 end
241 end
242
243 def insert_full_object(map), do: {:ok, map, nil}
244
245 #### Like-related helpers
246
247 @doc """
248 Returns an existing like if a user already liked an object
249 """
250 @spec get_existing_like(String.t(), map()) :: Activity.t() | nil
251 def get_existing_like(actor, %{data: %{"id" => id}}) do
252 actor
253 |> Activity.Queries.by_actor()
254 |> Activity.Queries.by_object_id(id)
255 |> Activity.Queries.by_type("Like")
256 |> limit(1)
257 |> Repo.one()
258 end
259
260 @spec make_like_data(User.t(), map(), String.t()) :: map()
261 def make_like_data(
262 %User{ap_id: ap_id} = actor,
263 %{data: %{"actor" => object_actor_id, "id" => id}} = object,
264 activity_id
265 ) do
266 object_actor = User.get_cached_by_ap_id(object_actor_id)
267
268 to =
269 if Visibility.is_public?(object) do
270 [actor.follower_address, object.data["actor"]]
271 else
272 [object.data["actor"]]
273 end
274
275 cc =
276 (object.data["to"] ++ (object.data["cc"] || []))
277 |> List.delete(actor.ap_id)
278 |> List.delete(object_actor.follower_address)
279
280 %{
281 "type" => "Like",
282 "actor" => ap_id,
283 "object" => id,
284 "to" => to,
285 "cc" => cc,
286 "context" => object.data["context"]
287 }
288 |> maybe_put("id", activity_id)
289 end
290
291 @spec update_element_in_object(String.t(), list(any), Object.t()) ::
292 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
293 def update_element_in_object(property, element, object) do
294 data =
295 Map.merge(
296 object.data,
297 %{"#{property}_count" => length(element), "#{property}s" => element}
298 )
299
300 object
301 |> Changeset.change(data: data)
302 |> Object.update_and_set_cache()
303 end
304
305 @spec add_like_to_object(Activity.t(), Object.t()) ::
306 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
307 def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
308 [actor | fetch_likes(object)]
309 |> Enum.uniq()
310 |> update_likes_in_object(object)
311 end
312
313 @spec remove_like_from_object(Activity.t(), Object.t()) ::
314 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
315 def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
316 object
317 |> fetch_likes()
318 |> List.delete(actor)
319 |> update_likes_in_object(object)
320 end
321
322 defp update_likes_in_object(likes, object) do
323 update_element_in_object("like", likes, object)
324 end
325
326 defp fetch_likes(object) do
327 if is_list(object.data["likes"]) do
328 object.data["likes"]
329 else
330 []
331 end
332 end
333
334 #### Follow-related helpers
335
336 @doc """
337 Updates a follow activity's state (for locked accounts).
338 """
339 @spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity} | {:error, any()}
340 def update_follow_state_for_all(
341 %Activity{data: %{"actor" => actor, "object" => object}} = activity,
342 state
343 ) do
344 "Follow"
345 |> Activity.Queries.by_type()
346 |> Activity.Queries.by_actor(actor)
347 |> Activity.Queries.by_object_id(object)
348 |> where(fragment("data->>'state' = 'pending'"))
349 |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
350 |> Repo.update_all([])
351
352 User.set_follow_state_cache(actor, object, state)
353
354 activity = Activity.get_by_id(activity.id)
355
356 {:ok, activity}
357 end
358
359 def update_follow_state(
360 %Activity{data: %{"actor" => actor, "object" => object}} = activity,
361 state
362 ) do
363 new_data = Map.put(activity.data, "state", state)
364 changeset = Changeset.change(activity, data: new_data)
365
366 with {:ok, activity} <- Repo.update(changeset) do
367 User.set_follow_state_cache(actor, object, state)
368 {:ok, activity}
369 end
370 end
371
372 @doc """
373 Makes a follow activity data for the given follower and followed
374 """
375 def make_follow_data(
376 %User{ap_id: follower_id},
377 %User{ap_id: followed_id} = _followed,
378 activity_id
379 ) do
380 %{
381 "type" => "Follow",
382 "actor" => follower_id,
383 "to" => [followed_id],
384 "cc" => [Pleroma.Constants.as_public()],
385 "object" => followed_id,
386 "state" => "pending"
387 }
388 |> maybe_put("id", activity_id)
389 end
390
391 def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
392 "Follow"
393 |> Activity.Queries.by_type()
394 |> where(actor: ^follower_id)
395 # this is to use the index
396 |> Activity.Queries.by_object_id(followed_id)
397 |> order_by([activity], fragment("? desc nulls last", activity.id))
398 |> limit(1)
399 |> Repo.one()
400 end
401
402 #### Announce-related helpers
403
404 @doc """
405 Retruns an existing announce activity if the notice has already been announced
406 """
407 @spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
408 def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do
409 "Announce"
410 |> Activity.Queries.by_type()
411 |> where(actor: ^actor)
412 # this is to use the index
413 |> Activity.Queries.by_object_id(ap_id)
414 |> Repo.one()
415 end
416
417 @doc """
418 Make announce activity data for the given actor and object
419 """
420 # for relayed messages, we only want to send to subscribers
421 def make_announce_data(
422 %User{ap_id: ap_id} = user,
423 %Object{data: %{"id" => id}} = object,
424 activity_id,
425 false
426 ) do
427 %{
428 "type" => "Announce",
429 "actor" => ap_id,
430 "object" => id,
431 "to" => [user.follower_address],
432 "cc" => [],
433 "context" => object.data["context"]
434 }
435 |> maybe_put("id", activity_id)
436 end
437
438 def make_announce_data(
439 %User{ap_id: ap_id} = user,
440 %Object{data: %{"id" => id}} = object,
441 activity_id,
442 true
443 ) do
444 %{
445 "type" => "Announce",
446 "actor" => ap_id,
447 "object" => id,
448 "to" => [user.follower_address, object.data["actor"]],
449 "cc" => [Pleroma.Constants.as_public()],
450 "context" => object.data["context"]
451 }
452 |> maybe_put("id", activity_id)
453 end
454
455 @doc """
456 Make unannounce activity data for the given actor and object
457 """
458 def make_unannounce_data(
459 %User{ap_id: ap_id} = user,
460 %Activity{data: %{"context" => context, "object" => object}} = activity,
461 activity_id
462 ) do
463 object = Object.normalize(object)
464
465 %{
466 "type" => "Undo",
467 "actor" => ap_id,
468 "object" => activity.data,
469 "to" => [user.follower_address, object.data["actor"]],
470 "cc" => [Pleroma.Constants.as_public()],
471 "context" => context
472 }
473 |> maybe_put("id", activity_id)
474 end
475
476 def make_unlike_data(
477 %User{ap_id: ap_id} = user,
478 %Activity{data: %{"context" => context, "object" => object}} = activity,
479 activity_id
480 ) do
481 object = Object.normalize(object)
482
483 %{
484 "type" => "Undo",
485 "actor" => ap_id,
486 "object" => activity.data,
487 "to" => [user.follower_address, object.data["actor"]],
488 "cc" => [Pleroma.Constants.as_public()],
489 "context" => context
490 }
491 |> maybe_put("id", activity_id)
492 end
493
494 @spec add_announce_to_object(Activity.t(), Object.t()) ::
495 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
496 def add_announce_to_object(
497 %Activity{data: %{"actor" => actor}},
498 object
499 ) do
500 unless actor |> User.get_cached_by_ap_id() |> User.invisible?() do
501 announcements = take_announcements(object)
502
503 with announcements <- Enum.uniq([actor | announcements]) do
504 update_element_in_object("announcement", announcements, object)
505 end
506 else
507 {:ok, object}
508 end
509 end
510
511 def add_announce_to_object(_, object), do: {:ok, object}
512
513 @spec remove_announce_from_object(Activity.t(), Object.t()) ::
514 {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
515 def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
516 with announcements <- List.delete(take_announcements(object), actor) do
517 update_element_in_object("announcement", announcements, object)
518 end
519 end
520
521 defp take_announcements(%{data: %{"announcements" => announcements}} = _)
522 when is_list(announcements),
523 do: announcements
524
525 defp take_announcements(_), do: []
526
527 #### Unfollow-related helpers
528
529 def make_unfollow_data(follower, followed, follow_activity, activity_id) do
530 %{
531 "type" => "Undo",
532 "actor" => follower.ap_id,
533 "to" => [followed.ap_id],
534 "object" => follow_activity.data
535 }
536 |> maybe_put("id", activity_id)
537 end
538
539 #### Block-related helpers
540 @spec fetch_latest_block(User.t(), User.t()) :: Activity.t() | nil
541 def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
542 "Block"
543 |> Activity.Queries.by_type()
544 |> where(actor: ^blocker_id)
545 # this is to use the index
546 |> Activity.Queries.by_object_id(blocked_id)
547 |> order_by([activity], fragment("? desc nulls last", activity.id))
548 |> limit(1)
549 |> Repo.one()
550 end
551
552 def make_block_data(blocker, blocked, activity_id) do
553 %{
554 "type" => "Block",
555 "actor" => blocker.ap_id,
556 "to" => [blocked.ap_id],
557 "object" => blocked.ap_id
558 }
559 |> maybe_put("id", activity_id)
560 end
561
562 def make_unblock_data(blocker, blocked, block_activity, activity_id) do
563 %{
564 "type" => "Undo",
565 "actor" => blocker.ap_id,
566 "to" => [blocked.ap_id],
567 "object" => block_activity.data
568 }
569 |> maybe_put("id", activity_id)
570 end
571
572 #### Create-related helpers
573
574 def make_create_data(params, additional) do
575 published = params.published || make_date()
576
577 %{
578 "type" => "Create",
579 "to" => params.to |> Enum.uniq(),
580 "actor" => params.actor.ap_id,
581 "object" => params.object,
582 "published" => published,
583 "context" => params.context
584 }
585 |> Map.merge(additional)
586 end
587
588 #### Listen-related helpers
589 def make_listen_data(params, additional) do
590 published = params.published || make_date()
591
592 %{
593 "type" => "Listen",
594 "to" => params.to |> Enum.uniq(),
595 "actor" => params.actor.ap_id,
596 "object" => params.object,
597 "published" => published,
598 "context" => params.context
599 }
600 |> Map.merge(additional)
601 end
602
603 #### Flag-related helpers
604 @spec make_flag_data(map(), map()) :: map()
605 def make_flag_data(%{actor: actor, context: context, content: content} = params, additional) do
606 %{
607 "type" => "Flag",
608 "actor" => actor.ap_id,
609 "content" => content,
610 "object" => build_flag_object(params),
611 "context" => context,
612 "state" => "open"
613 }
614 |> Map.merge(additional)
615 end
616
617 def make_flag_data(_, _), do: %{}
618
619 defp build_flag_object(%{account: account, statuses: statuses} = _) do
620 [account.ap_id] ++
621 Enum.map(statuses || [], fn act ->
622 id =
623 case act do
624 %Activity{} = act -> act.data["id"]
625 act when is_map(act) -> act["id"]
626 act when is_binary(act) -> act
627 end
628
629 activity = Activity.get_by_ap_id_with_object(id)
630 actor = User.get_by_ap_id(activity.object.data["actor"])
631
632 %{
633 "type" => "Note",
634 "id" => activity.data["id"],
635 "content" => activity.object.data["content"],
636 "published" => activity.object.data["published"],
637 "actor" => AccountView.render("show.json", %{user: actor})
638 }
639 end)
640 end
641
642 defp build_flag_object(_), do: []
643
644 @doc """
645 Fetches the OrderedCollection/OrderedCollectionPage from `from`, limiting the amount of pages fetched after
646 the first one to `pages_left` pages.
647 If the amount of pages is higher than the collection has, it returns whatever was there.
648 """
649 def fetch_ordered_collection(from, pages_left, acc \\ []) do
650 with {:ok, response} <- Tesla.get(from),
651 {:ok, collection} <- Jason.decode(response.body) do
652 case collection["type"] do
653 "OrderedCollection" ->
654 # If we've encountered the OrderedCollection and not the page,
655 # just call the same function on the page address
656 fetch_ordered_collection(collection["first"], pages_left)
657
658 "OrderedCollectionPage" ->
659 if pages_left > 0 do
660 # There are still more pages
661 if Map.has_key?(collection, "next") do
662 # There are still more pages, go deeper saving what we have into the accumulator
663 fetch_ordered_collection(
664 collection["next"],
665 pages_left - 1,
666 acc ++ collection["orderedItems"]
667 )
668 else
669 # No more pages left, just return whatever we already have
670 acc ++ collection["orderedItems"]
671 end
672 else
673 # Got the amount of pages needed, add them all to the accumulator
674 acc ++ collection["orderedItems"]
675 end
676
677 _ ->
678 {:error, "Not an OrderedCollection or OrderedCollectionPage"}
679 end
680 end
681 end
682
683 #### Report-related helpers
684 def get_reports(params, page, page_size) do
685 params =
686 params
687 |> Map.put("type", "Flag")
688 |> Map.put("skip_preload", true)
689 |> Map.put("total", true)
690 |> Map.put("limit", page_size)
691 |> Map.put("offset", (page - 1) * page_size)
692
693 ActivityPub.fetch_activities([], params, :offset)
694 end
695
696 @spec get_reports_grouped_by_status() :: %{
697 required(:groups) => [
698 %{
699 required(:date) => String.t(),
700 required(:account) => %User{},
701 required(:status) => %Activity{},
702 required(:actors) => [%User{}],
703 required(:reports) => [%Activity{}]
704 }
705 ],
706 required(:total) => integer
707 }
708 def get_reports_grouped_by_status do
709 paginated_activities = get_reported_status_ids()
710
711 groups =
712 paginated_activities
713 |> Enum.map(fn entry ->
714 status =
715 Activity
716 |> Queries.by_ap_id(entry[:activity_id])
717 |> Activity.with_preloaded_object(:left)
718 |> Activity.with_preloaded_user_actor()
719 |> Repo.one()
720
721 reports = get_reports_by_status_id(status.data["id"])
722
723 max_date =
724 Enum.max_by(reports, &Pleroma.Web.CommonAPI.Utils.to_masto_date(&1.data["published"])).data[
725 "published"
726 ]
727
728 actors = Enum.map(reports, & &1.user_actor)
729
730 %{
731 date: max_date,
732 account: status.user_actor,
733 status: status,
734 actors: actors,
735 reports: reports
736 }
737 end)
738
739 %{
740 groups: groups
741 }
742 end
743
744 def get_reports_by_status_id(status_id) do
745 from(a in Activity,
746 where: fragment("(?)->>'type' = 'Flag'", a.data),
747 where: fragment("(?)->'object' \\? (?)", a.data, ^status_id)
748 )
749 |> Activity.with_preloaded_user_actor()
750 |> Repo.all()
751 end
752
753 @spec get_reported_status_ids() :: %{
754 required(:items) => [%Activity{}],
755 required(:total) => integer
756 }
757 def get_reported_status_ids do
758 from(a in Activity,
759 where: fragment("(?)->>'type' = 'Flag'", a.data),
760 select: %{
761 date: fragment("max(?->>'published') date", a.data),
762 activity_id:
763 fragment("jsonb_array_elements_text((? #- '{object,0}')->'object') activity_id", a.data)
764 },
765 group_by: fragment("activity_id"),
766 order_by: fragment("date DESC")
767 )
768 |> Repo.all()
769 end
770
771 def update_report_state(%Activity{} = activity, state)
772 when state in @strip_status_report_states do
773 {:ok, stripped_activity} = strip_report_status_data(activity)
774
775 new_data =
776 activity.data
777 |> Map.put("state", state)
778 |> Map.put("object", stripped_activity.data["object"])
779
780 activity
781 |> Changeset.change(data: new_data)
782 |> Repo.update()
783 end
784
785 def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
786 new_data = Map.put(activity.data, "state", state)
787
788 activity
789 |> Changeset.change(data: new_data)
790 |> Repo.update()
791 end
792
793 def update_report_state(activity_ids, state) when state in @supported_report_states do
794 activities_num = length(activity_ids)
795
796 from(a in Activity, where: a.id in ^activity_ids)
797 |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
798 |> Repo.update_all([])
799 |> case do
800 {^activities_num, _} -> :ok
801 _ -> {:error, activity_ids}
802 end
803 end
804
805 def update_report_state(_, _), do: {:error, "Unsupported state"}
806
807 def strip_report_status_data(activity) do
808 [actor | reported_activities] = activity.data["object"]
809 stripped_activities = Enum.map(reported_activities, & &1["id"])
810 new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
811
812 {:ok, %{activity | data: new_data}}
813 end
814
815 def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
816 [to, cc, recipients] =
817 activity
818 |> get_updated_targets(visibility)
819 |> Enum.map(&Enum.uniq/1)
820
821 object_data =
822 activity.object.data
823 |> Map.put("to", to)
824 |> Map.put("cc", cc)
825
826 {:ok, object} =
827 activity.object
828 |> Object.change(%{data: object_data})
829 |> Object.update_and_set_cache()
830
831 activity_data =
832 activity.data
833 |> Map.put("to", to)
834 |> Map.put("cc", cc)
835
836 activity
837 |> Map.put(:object, object)
838 |> Activity.change(%{data: activity_data, recipients: recipients})
839 |> Repo.update()
840 end
841
842 def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}
843
844 defp get_updated_targets(
845 %Activity{data: %{"to" => to} = data, recipients: recipients},
846 visibility
847 ) do
848 cc = Map.get(data, "cc", [])
849 follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
850 public = Pleroma.Constants.as_public()
851
852 case visibility do
853 "public" ->
854 to = [public | List.delete(to, follower_address)]
855 cc = [follower_address | List.delete(cc, public)]
856 recipients = [public | recipients]
857 [to, cc, recipients]
858
859 "private" ->
860 to = [follower_address | List.delete(to, public)]
861 cc = List.delete(cc, public)
862 recipients = List.delete(recipients, public)
863 [to, cc, recipients]
864
865 "unlisted" ->
866 to = [follower_address | List.delete(to, public)]
867 cc = [public | List.delete(cc, follower_address)]
868 recipients = recipients ++ [follower_address, public]
869 [to, cc, recipients]
870
871 _ ->
872 [to, cc, recipients]
873 end
874 end
875
876 def get_existing_votes(actor, %{data: %{"id" => id}}) do
877 actor
878 |> Activity.Queries.by_actor()
879 |> Activity.Queries.by_type("Create")
880 |> Activity.with_preloaded_object()
881 |> where([a, object: o], fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(id)))
882 |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
883 |> Repo.all()
884 end
885
886 def maybe_put(map, _key, nil), do: map
887 def maybe_put(map, key, value), do: Map.put(map, key, value)
888 end