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