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