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