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.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] ++ build_flag_object(%{statuses: statuses})
620 end
621
622 defp build_flag_object(%{statuses: statuses}) do
623 Enum.map(statuses || [], &build_flag_object/1)
624 end
625
626 defp build_flag_object(act) when is_map(act) or is_binary(act) do
627 id =
628 case act do
629 %Activity{} = act -> act.data["id"]
630 act when is_map(act) -> act["id"]
631 act when is_binary(act) -> act
632 end
633
634 activity = Activity.get_by_ap_id_with_object(id)
635 actor = User.get_by_ap_id(activity.object.data["actor"])
636
637 %{
638 "type" => "Note",
639 "id" => activity.data["id"],
640 "content" => activity.object.data["content"],
641 "published" => activity.object.data["published"],
642 "actor" => AccountView.render("show.json", %{user: actor})
643 }
644 end
645
646 defp build_flag_object(_), do: []
647
648 @doc """
649 Fetches the OrderedCollection/OrderedCollectionPage from `from`, limiting the amount of pages fetched after
650 the first one to `pages_left` pages.
651 If the amount of pages is higher than the collection has, it returns whatever was there.
652 """
653 def fetch_ordered_collection(from, pages_left, acc \\ []) do
654 with {:ok, response} <- Tesla.get(from),
655 {:ok, collection} <- Jason.decode(response.body) do
656 case collection["type"] do
657 "OrderedCollection" ->
658 # If we've encountered the OrderedCollection and not the page,
659 # just call the same function on the page address
660 fetch_ordered_collection(collection["first"], pages_left)
661
662 "OrderedCollectionPage" ->
663 if pages_left > 0 do
664 # There are still more pages
665 if Map.has_key?(collection, "next") do
666 # There are still more pages, go deeper saving what we have into the accumulator
667 fetch_ordered_collection(
668 collection["next"],
669 pages_left - 1,
670 acc ++ collection["orderedItems"]
671 )
672 else
673 # No more pages left, just return whatever we already have
674 acc ++ collection["orderedItems"]
675 end
676 else
677 # Got the amount of pages needed, add them all to the accumulator
678 acc ++ collection["orderedItems"]
679 end
680
681 _ ->
682 {:error, "Not an OrderedCollection or OrderedCollectionPage"}
683 end
684 end
685 end
686
687 #### Report-related helpers
688 def get_reports(params, page, page_size) do
689 params =
690 params
691 |> Map.put("type", "Flag")
692 |> Map.put("skip_preload", true)
693 |> Map.put("total", true)
694 |> Map.put("limit", page_size)
695 |> Map.put("offset", (page - 1) * page_size)
696
697 ActivityPub.fetch_activities([], params, :offset)
698 end
699
700 @spec get_reports_grouped_by_status(%{required(:activity) => String.t()}) :: %{
701 required(:groups) => [
702 %{
703 required(:date) => String.t(),
704 required(:account) => %{},
705 required(:status) => %{},
706 required(:actors) => [%User{}],
707 required(:reports) => [%Activity{}]
708 }
709 ],
710 required(:total) => integer
711 }
712 def get_reports_grouped_by_status(groups) do
713 parsed_groups =
714 groups
715 |> Enum.map(fn entry ->
716 activity =
717 case Jason.decode(entry.activity) do
718 {:ok, activity} -> activity
719 _ -> build_flag_object(entry.activity)
720 end
721
722 parse_report_group(activity)
723 end)
724
725 %{
726 groups: parsed_groups
727 }
728 end
729
730 def parse_report_group(activity) do
731 reports = get_reports_by_status_id(activity["id"])
732 max_date = Enum.max_by(reports, &NaiveDateTime.from_iso8601!(&1.data["published"]))
733 actors = Enum.map(reports, & &1.user_actor)
734
735 %{
736 date: max_date.data["published"],
737 account: activity["actor"],
738 status: %{
739 id: activity["id"],
740 content: activity["content"],
741 published: activity["published"]
742 },
743 actors: Enum.uniq(actors),
744 reports: reports
745 }
746 end
747
748 def get_reports_by_status_id(ap_id) do
749 from(a in Activity,
750 where: fragment("(?)->>'type' = 'Flag'", a.data),
751 where: fragment("(?)->'object' @> ?", a.data, ^[%{id: ap_id}])
752 )
753 |> Activity.with_preloaded_user_actor()
754 |> Repo.all()
755 end
756
757 @spec get_reported_activities() :: [
758 %{
759 required(:activity) => String.t(),
760 required(:date) => String.t()
761 }
762 ]
763 def get_reported_activities do
764 from(a in Activity,
765 where: fragment("(?)->>'type' = 'Flag'", a.data),
766 select: %{
767 date: fragment("max(?->>'published') date", a.data),
768 activity:
769 fragment("jsonb_array_elements_text((? #- '{object,0}')->'object') activity", a.data)
770 },
771 group_by: fragment("activity"),
772 order_by: fragment("date DESC")
773 )
774 |> Repo.all()
775 end
776
777 def update_report_state(%Activity{} = activity, state)
778 when state in @strip_status_report_states do
779 {:ok, stripped_activity} = strip_report_status_data(activity)
780
781 new_data =
782 activity.data
783 |> Map.put("state", state)
784 |> Map.put("object", stripped_activity.data["object"])
785
786 activity
787 |> Changeset.change(data: new_data)
788 |> Repo.update()
789 end
790
791 def update_report_state(%Activity{} = activity, state) when state in @supported_report_states do
792 new_data = Map.put(activity.data, "state", state)
793
794 activity
795 |> Changeset.change(data: new_data)
796 |> Repo.update()
797 end
798
799 def update_report_state(activity_ids, state) when state in @supported_report_states do
800 activities_num = length(activity_ids)
801
802 from(a in Activity, where: a.id in ^activity_ids)
803 |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
804 |> Repo.update_all([])
805 |> case do
806 {^activities_num, _} -> :ok
807 _ -> {:error, activity_ids}
808 end
809 end
810
811 def update_report_state(_, _), do: {:error, "Unsupported state"}
812
813 def strip_report_status_data(activity) do
814 [actor | reported_activities] = activity.data["object"]
815 stripped_activities = Enum.map(reported_activities, & &1["id"])
816 new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
817
818 {:ok, %{activity | data: new_data}}
819 end
820
821 def update_activity_visibility(activity, visibility) when visibility in @valid_visibilities do
822 [to, cc, recipients] =
823 activity
824 |> get_updated_targets(visibility)
825 |> Enum.map(&Enum.uniq/1)
826
827 object_data =
828 activity.object.data
829 |> Map.put("to", to)
830 |> Map.put("cc", cc)
831
832 {:ok, object} =
833 activity.object
834 |> Object.change(%{data: object_data})
835 |> Object.update_and_set_cache()
836
837 activity_data =
838 activity.data
839 |> Map.put("to", to)
840 |> Map.put("cc", cc)
841
842 activity
843 |> Map.put(:object, object)
844 |> Activity.change(%{data: activity_data, recipients: recipients})
845 |> Repo.update()
846 end
847
848 def update_activity_visibility(_, _), do: {:error, "Unsupported visibility"}
849
850 defp get_updated_targets(
851 %Activity{data: %{"to" => to} = data, recipients: recipients},
852 visibility
853 ) do
854 cc = Map.get(data, "cc", [])
855 follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address
856 public = Pleroma.Constants.as_public()
857
858 case visibility do
859 "public" ->
860 to = [public | List.delete(to, follower_address)]
861 cc = [follower_address | List.delete(cc, public)]
862 recipients = [public | recipients]
863 [to, cc, recipients]
864
865 "private" ->
866 to = [follower_address | List.delete(to, public)]
867 cc = List.delete(cc, public)
868 recipients = List.delete(recipients, public)
869 [to, cc, recipients]
870
871 "unlisted" ->
872 to = [follower_address | List.delete(to, public)]
873 cc = [public | List.delete(cc, follower_address)]
874 recipients = recipients ++ [follower_address, public]
875 [to, cc, recipients]
876
877 _ ->
878 [to, cc, recipients]
879 end
880 end
881
882 def get_existing_votes(actor, %{data: %{"id" => id}}) do
883 actor
884 |> Activity.Queries.by_actor()
885 |> Activity.Queries.by_type("Create")
886 |> Activity.with_preloaded_object()
887 |> where([a, object: o], fragment("(?)->>'inReplyTo' = ?", o.data, ^to_string(id)))
888 |> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
889 |> Repo.all()
890 end
891
892 def maybe_put(map, _key, nil), do: map
893 def maybe_put(map, key, value), do: Map.put(map, key, value)
894 end