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