Test removed HTTP adapter
[akkoma] / lib / pleroma / notification.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Notification do
6 use Ecto.Schema
7
8 alias Ecto.Multi
9 alias Pleroma.Activity
10 alias Pleroma.FollowingRelationship
11 alias Pleroma.Marker
12 alias Pleroma.Notification
13 alias Pleroma.Object
14 alias Pleroma.Pagination
15 alias Pleroma.Repo
16 alias Pleroma.ThreadMute
17 alias Pleroma.User
18 alias Pleroma.Web.CommonAPI
19 alias Pleroma.Web.CommonAPI.Utils
20 alias Pleroma.Web.Push
21 alias Pleroma.Web.Streamer
22
23 import Ecto.Query
24 import Ecto.Changeset
25
26 require Logger
27
28 @type t :: %__MODULE__{}
29
30 @include_muted_option :with_muted
31
32 schema "notifications" do
33 field(:seen, :boolean, default: false)
34 # This is an enum type in the database. If you add a new notification type,
35 # remember to add a migration to add it to the `notifications_type` enum
36 # as well.
37 field(:type, :string)
38 belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
39 belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
40
41 timestamps()
42 end
43
44 def update_notification_type(user, activity) do
45 with %__MODULE__{} = notification <-
46 Repo.get_by(__MODULE__, user_id: user.id, activity_id: activity.id) do
47 type =
48 activity
49 |> type_from_activity()
50
51 notification
52 |> changeset(%{type: type})
53 |> Repo.update()
54 end
55 end
56
57 @spec unread_notifications_count(User.t()) :: integer()
58 def unread_notifications_count(%User{id: user_id}) do
59 from(q in __MODULE__,
60 where: q.user_id == ^user_id and q.seen == false
61 )
62 |> Repo.aggregate(:count, :id)
63 end
64
65 @notification_types ~w{
66 favourite
67 follow
68 follow_request
69 mention
70 move
71 pleroma:emoji_reaction
72 pleroma:report
73 reblog
74 poll
75 }
76
77 def changeset(%Notification{} = notification, attrs) do
78 notification
79 |> cast(attrs, [:seen, :type])
80 |> validate_inclusion(:type, @notification_types)
81 end
82
83 @spec last_read_query(User.t()) :: Ecto.Queryable.t()
84 def last_read_query(user) do
85 from(q in Pleroma.Notification,
86 where: q.user_id == ^user.id,
87 where: q.seen == true,
88 select: type(q.id, :string),
89 limit: 1,
90 order_by: [desc: :id]
91 )
92 end
93
94 defp for_user_query_ap_id_opts(user, opts) do
95 ap_id_relationships =
96 [:block] ++
97 if opts[@include_muted_option], do: [], else: [:notification_mute]
98
99 preloaded_ap_ids = User.outgoing_relationships_ap_ids(user, ap_id_relationships)
100
101 exclude_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts)
102
103 exclude_notification_muted_opts =
104 Map.merge(%{notification_muted_users_ap_ids: preloaded_ap_ids[:notification_mute]}, opts)
105
106 {exclude_blocked_opts, exclude_notification_muted_opts}
107 end
108
109 def for_user_query(user, opts \\ %{}) do
110 {exclude_blocked_opts, exclude_notification_muted_opts} =
111 for_user_query_ap_id_opts(user, opts)
112
113 Notification
114 |> where(user_id: ^user.id)
115 |> join(:inner, [n], activity in assoc(n, :activity))
116 |> join(:left, [n, a], object in Object,
117 on:
118 fragment(
119 "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
120 object.data,
121 a.data,
122 a.data
123 )
124 )
125 |> join(:inner, [_n, a], u in User, on: u.ap_id == a.actor, as: :user_actor)
126 |> preload([n, a, o], activity: {a, object: o})
127 |> where([user_actor: user_actor], user_actor.is_active)
128 |> exclude_notification_muted(user, exclude_notification_muted_opts)
129 |> exclude_blocked(user, exclude_blocked_opts)
130 |> exclude_blockers(user)
131 |> exclude_filtered(user)
132 |> exclude_visibility(opts)
133 end
134
135 # Excludes blocked users and non-followed domain-blocked users
136 defp exclude_blocked(query, user, opts) do
137 blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
138
139 query
140 |> where([n, a], a.actor not in ^blocked_ap_ids)
141 |> restrict_domain_blocked(user)
142 end
143
144 defp restrict_domain_blocked(query, user) do
145 where(
146 query,
147 [_, activity],
148 fragment(
149 # "(actor's domain NOT in domain_blocks)"
150 """
151 NOT (
152 substring(? from '.*://([^/]*)') = ANY(?)
153 )
154 """,
155 activity.actor,
156 ^user.domain_blocks
157 )
158 )
159 end
160
161 defp exclude_blockers(query, user) do
162 if Pleroma.Config.get([:activitypub, :blockers_visible]) == true do
163 query
164 else
165 blocker_ap_ids = User.incoming_relationships_ungrouped_ap_ids(user, [:block])
166
167 query
168 |> where([n, a], a.actor not in ^blocker_ap_ids)
169 end
170 end
171
172 defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do
173 query
174 end
175
176 defp exclude_notification_muted(query, user, opts) do
177 notification_muted_ap_ids =
178 opts[:notification_muted_users_ap_ids] || User.notification_muted_users_ap_ids(user)
179
180 query
181 |> where([n, a], a.actor not in ^notification_muted_ap_ids)
182 |> join(:left, [n, a], tm in ThreadMute,
183 on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data),
184 as: :thread_mute
185 )
186 |> where([thread_mute: thread_mute], is_nil(thread_mute.user_id))
187 end
188
189 defp exclude_filtered(query, user) do
190 case Pleroma.Filter.compose_regex(user) do
191 nil ->
192 query
193
194 regex ->
195 from([_n, a, o] in query,
196 where:
197 fragment("not(?->>'content' ~* ?)", o.data, ^regex) or
198 fragment("?->>'actor' = ?", o.data, ^user.ap_id)
199 )
200 end
201 end
202
203 @valid_visibilities ~w[direct unlisted public private]
204
205 defp exclude_visibility(query, %{exclude_visibilities: visibility})
206 when is_list(visibility) do
207 if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
208 query
209 |> join(:left, [n, a], mutated_activity in Pleroma.Activity,
210 on:
211 fragment(
212 "COALESCE((?->'object')->>'id', ?->>'object')",
213 a.data,
214 a.data
215 ) ==
216 fragment(
217 "COALESCE((?->'object')->>'id', ?->>'object')",
218 mutated_activity.data,
219 mutated_activity.data
220 ) and
221 fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
222 fragment("?->>'type'", mutated_activity.data) == "Create",
223 as: :mutated_activity
224 )
225 |> where(
226 [n, a, mutated_activity: mutated_activity],
227 not fragment(
228 """
229 CASE WHEN (?->>'type') = 'Like' or (?->>'type') = 'Announce'
230 THEN (activity_visibility(?, ?, ?) = ANY (?))
231 ELSE (activity_visibility(?, ?, ?) = ANY (?)) END
232 """,
233 a.data,
234 a.data,
235 mutated_activity.actor,
236 mutated_activity.recipients,
237 mutated_activity.data,
238 ^visibility,
239 a.actor,
240 a.recipients,
241 a.data,
242 ^visibility
243 )
244 )
245 else
246 Logger.error("Could not exclude visibility to #{visibility}")
247 query
248 end
249 end
250
251 defp exclude_visibility(query, %{exclude_visibilities: visibility})
252 when visibility in @valid_visibilities do
253 exclude_visibility(query, [visibility])
254 end
255
256 defp exclude_visibility(query, %{exclude_visibilities: visibility})
257 when visibility not in @valid_visibilities do
258 Logger.error("Could not exclude visibility to #{visibility}")
259 query
260 end
261
262 defp exclude_visibility(query, _visibility), do: query
263
264 def for_user(user, opts \\ %{}) do
265 user
266 |> for_user_query(opts)
267 |> Pagination.fetch_paginated(opts)
268 end
269
270 @doc """
271 Returns notifications for user received since given date.
272
273 ## Examples
274
275 iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-13 11:22:33])
276 [%Pleroma.Notification{}, %Pleroma.Notification{}]
277
278 iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-15 11:22:33])
279 []
280 """
281 @spec for_user_since(Pleroma.User.t(), NaiveDateTime.t()) :: [t()]
282 def for_user_since(user, date) do
283 from(n in for_user_query(user),
284 where: n.updated_at > ^date
285 )
286 |> Repo.all()
287 end
288
289 def set_read_up_to(%{id: user_id} = user, id) do
290 query =
291 from(
292 n in Notification,
293 where: n.user_id == ^user_id,
294 where: n.id <= ^id,
295 where: n.seen == false,
296 # Ideally we would preload object and activities here
297 # but Ecto does not support preloads in update_all
298 select: n.id
299 )
300
301 {:ok, %{ids: {_, notification_ids}}} =
302 Multi.new()
303 |> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
304 |> Marker.multi_set_last_read_id(user, "notifications")
305 |> Repo.transaction()
306
307 for_user_query(user)
308 |> where([n], n.id in ^notification_ids)
309 |> Repo.all()
310 end
311
312 @spec read_one(User.t(), String.t()) ::
313 {:ok, Notification.t()} | {:error, Ecto.Changeset.t()} | nil
314 def read_one(%User{} = user, notification_id) do
315 with {:ok, %Notification{} = notification} <- get(user, notification_id) do
316 Multi.new()
317 |> Multi.update(:update, changeset(notification, %{seen: true}))
318 |> Marker.multi_set_last_read_id(user, "notifications")
319 |> Repo.transaction()
320 |> case do
321 {:ok, %{update: notification}} -> {:ok, notification}
322 {:error, :update, changeset, _} -> {:error, changeset}
323 end
324 end
325 end
326
327 def get(%{id: user_id} = _user, id) do
328 query =
329 from(
330 n in Notification,
331 where: n.id == ^id,
332 join: activity in assoc(n, :activity),
333 preload: [activity: activity]
334 )
335
336 notification = Repo.one(query)
337
338 case notification do
339 %{user_id: ^user_id} ->
340 {:ok, notification}
341
342 _ ->
343 {:error, "Cannot get notification"}
344 end
345 end
346
347 def clear(user) do
348 from(n in Notification, where: n.user_id == ^user.id)
349 |> Repo.delete_all()
350 end
351
352 def destroy_multiple(%{id: user_id} = _user, ids) do
353 from(n in Notification,
354 where: n.id in ^ids,
355 where: n.user_id == ^user_id
356 )
357 |> Repo.delete_all()
358 end
359
360 def destroy_multiple_from_types(%{id: user_id}, types) do
361 from(n in Notification,
362 where: n.user_id == ^user_id,
363 where: n.type in ^types
364 )
365 |> Repo.delete_all()
366 end
367
368 def dismiss(%Pleroma.Activity{} = activity) do
369 Notification
370 |> where([n], n.activity_id == ^activity.id)
371 |> Repo.delete_all()
372 |> case do
373 {_, notifications} -> {:ok, notifications}
374 _ -> {:error, "Cannot dismiss notification"}
375 end
376 end
377
378 def dismiss(%{id: user_id} = _user, id) do
379 notification = Repo.get(Notification, id)
380
381 case notification do
382 %{user_id: ^user_id} ->
383 Repo.delete(notification)
384
385 _ ->
386 {:error, "Cannot dismiss notification"}
387 end
388 end
389
390 @spec create_notifications(Activity.t(), keyword()) :: {:ok, [Notification.t()] | []}
391 def create_notifications(activity, options \\ [])
392
393 def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do
394 object = Object.normalize(activity, fetch: false)
395
396 if object && object.data["type"] == "Answer" do
397 {:ok, []}
398 else
399 do_create_notifications(activity, options)
400 end
401 end
402
403 def create_notifications(%Activity{data: %{"type" => type}} = activity, options)
404 when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag", "Update"] do
405 do_create_notifications(activity, options)
406 end
407
408 def create_notifications(_, _), do: {:ok, []}
409
410 defp do_create_notifications(%Activity{} = activity, options) do
411 do_send = Keyword.get(options, :do_send, true)
412
413 {enabled_receivers, disabled_receivers} = get_notified_from_activity(activity)
414 potential_receivers = enabled_receivers ++ disabled_receivers
415
416 notifications =
417 Enum.map(potential_receivers, fn user ->
418 do_send = do_send && user in enabled_receivers
419 create_notification(activity, user, do_send: do_send)
420 end)
421 |> Enum.reject(&is_nil/1)
422
423 {:ok, notifications}
424 end
425
426 defp type_from_activity(%{data: %{"type" => type}} = activity) do
427 case type do
428 "Follow" ->
429 if Activity.follow_accepted?(activity) do
430 "follow"
431 else
432 "follow_request"
433 end
434
435 "Announce" ->
436 "reblog"
437
438 "Like" ->
439 "favourite"
440
441 "Move" ->
442 "move"
443
444 "EmojiReact" ->
445 "pleroma:emoji_reaction"
446
447 "Flag" ->
448 "pleroma:report"
449
450 # Compatibility with old reactions
451 "EmojiReaction" ->
452 "pleroma:emoji_reaction"
453
454 "Create" ->
455 activity
456 |> type_from_activity_object()
457
458 "Update" ->
459 "update"
460
461 t ->
462 raise "No notification type for activity type #{t}"
463 end
464 end
465
466 defp type_from_activity_object(%{data: %{"type" => "Create"}}), do: "mention"
467
468 # TODO move to sql, too.
469 def create_notification(%Activity{} = activity, %User{} = user, opts \\ []) do
470 do_send = Keyword.get(opts, :do_send, true)
471 type = Keyword.get(opts, :type, type_from_activity(activity))
472
473 unless skip?(activity, user, opts) do
474 {:ok, %{notification: notification}} =
475 Multi.new()
476 |> Multi.insert(:notification, %Notification{
477 user_id: user.id,
478 activity: activity,
479 seen: mark_as_read?(activity, user),
480 type: type
481 })
482 |> Marker.multi_set_last_read_id(user, "notifications")
483 |> Repo.transaction()
484
485 if do_send do
486 Streamer.stream(["user", "user:notification"], notification)
487 Push.send(notification)
488 end
489
490 notification
491 end
492 end
493
494 def create_poll_notifications(%Activity{} = activity) do
495 with %Object{data: %{"type" => "Question", "actor" => actor} = data} <-
496 Object.normalize(activity) do
497 voters =
498 case data do
499 %{"voters" => voters} when is_list(voters) -> voters
500 _ -> []
501 end
502
503 notifications =
504 Enum.reduce([actor | voters], [], fn ap_id, acc ->
505 with %User{local: true} = user <- User.get_by_ap_id(ap_id) do
506 [create_notification(activity, user, type: "poll") | acc]
507 else
508 _ -> acc
509 end
510 end)
511
512 {:ok, notifications}
513 end
514 end
515
516 @doc """
517 Returns a tuple with 2 elements:
518 {notification-enabled receivers, currently disabled receivers (blocking / [thread] muting)}
519
520 NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1
521 """
522 @spec get_notified_from_activity(Activity.t(), boolean()) :: {list(User.t()), list(User.t())}
523 def get_notified_from_activity(activity, local_only \\ true)
524
525 def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
526 when type in [
527 "Create",
528 "Like",
529 "Announce",
530 "Follow",
531 "Move",
532 "EmojiReact",
533 "Flag",
534 "Update"
535 ] do
536 potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
537
538 potential_receivers =
539 User.get_users_from_set(potential_receiver_ap_ids, local_only: local_only)
540
541 notification_enabled_ap_ids =
542 potential_receiver_ap_ids
543 |> exclude_domain_blocker_ap_ids(activity, potential_receivers)
544 |> exclude_relationship_restricted_ap_ids(activity)
545 |> exclude_thread_muter_ap_ids(activity)
546
547 notification_enabled_users =
548 Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
549
550 {notification_enabled_users, potential_receivers -- notification_enabled_users}
551 end
552
553 def get_notified_from_activity(_, _local_only), do: {[], []}
554
555 # For some activities, only notify the author of the object
556 def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_id}})
557 when type in ~w{Like Announce EmojiReact} do
558 case Object.get_cached_by_ap_id(object_id) do
559 %Object{data: %{"actor" => actor}} ->
560 [actor]
561
562 _ ->
563 []
564 end
565 end
566
567 def get_potential_receiver_ap_ids(%{data: %{"type" => "Follow", "object" => object_id}}) do
568 [object_id]
569 end
570
571 def get_potential_receiver_ap_ids(%{data: %{"type" => "Flag", "actor" => actor}}) do
572 (User.all_superusers() |> Enum.map(fn user -> user.ap_id end)) -- [actor]
573 end
574
575 # Update activity: notify all who repeated this
576 def get_potential_receiver_ap_ids(%{data: %{"type" => "Update", "actor" => actor}} = activity) do
577 with %Object{data: %{"id" => object_id}} <- Object.normalize(activity, fetch: false) do
578 repeaters =
579 Activity.Queries.by_type("Announce")
580 |> Activity.Queries.by_object_id(object_id)
581 |> Activity.with_joined_user_actor()
582 |> where([a, u], u.local)
583 |> select([a, u], u.ap_id)
584 |> Repo.all()
585
586 repeaters -- [actor]
587 end
588 end
589
590 def get_potential_receiver_ap_ids(activity) do
591 []
592 |> Utils.maybe_notify_to_recipients(activity)
593 |> Utils.maybe_notify_mentioned_recipients(activity)
594 |> Utils.maybe_notify_subscribers(activity)
595 |> Utils.maybe_notify_followers(activity)
596 |> Enum.uniq()
597 end
598
599 @doc "Filters out AP IDs domain-blocking and not following the activity's actor"
600 def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])
601
602 def exclude_domain_blocker_ap_ids([], _activity, _preloaded_users), do: []
603
604 def exclude_domain_blocker_ap_ids(ap_ids, %Activity{} = activity, preloaded_users) do
605 activity_actor_domain = activity.actor && URI.parse(activity.actor).host
606
607 users =
608 ap_ids
609 |> Enum.map(fn ap_id ->
610 Enum.find(preloaded_users, &(&1.ap_id == ap_id)) ||
611 User.get_cached_by_ap_id(ap_id)
612 end)
613 |> Enum.filter(& &1)
614
615 domain_blocker_ap_ids = for u <- users, activity_actor_domain in u.domain_blocks, do: u.ap_id
616
617 domain_blocker_follower_ap_ids =
618 if Enum.any?(domain_blocker_ap_ids) do
619 activity
620 |> Activity.user_actor()
621 |> FollowingRelationship.followers_ap_ids(domain_blocker_ap_ids)
622 else
623 []
624 end
625
626 ap_ids
627 |> Kernel.--(domain_blocker_ap_ids)
628 |> Kernel.++(domain_blocker_follower_ap_ids)
629 end
630
631 @doc "Filters out AP IDs of users basing on their relationships with activity actor user"
632 def exclude_relationship_restricted_ap_ids([], _activity), do: []
633
634 def exclude_relationship_restricted_ap_ids(ap_ids, %Activity{} = activity) do
635 relationship_restricted_ap_ids =
636 activity
637 |> Activity.user_actor()
638 |> User.incoming_relationships_ungrouped_ap_ids([
639 :block,
640 :notification_mute
641 ])
642
643 Enum.uniq(ap_ids) -- relationship_restricted_ap_ids
644 end
645
646 @doc "Filters out AP IDs of users who mute activity thread"
647 def exclude_thread_muter_ap_ids([], _activity), do: []
648
649 def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do
650 thread_muter_ap_ids = ThreadMute.muter_ap_ids(activity.data["context"])
651
652 Enum.uniq(ap_ids) -- thread_muter_ap_ids
653 end
654
655 def skip?(activity, user, opts \\ [])
656
657 @spec skip?(Activity.t(), User.t(), Keyword.t()) :: boolean()
658 def skip?(%Activity{} = activity, %User{} = user, opts) do
659 [
660 :self,
661 :invisible,
662 :block_from_strangers,
663 :recently_followed,
664 :filtered
665 ]
666 |> Enum.find(&skip?(&1, activity, user, opts))
667 end
668
669 def skip?(_activity, _user, _opts), do: false
670
671 @spec skip?(atom(), Activity.t(), User.t(), Keyword.t()) :: boolean()
672 def skip?(:self, %Activity{} = activity, %User{} = user, opts) do
673 cond do
674 opts[:type] == "poll" -> false
675 activity.data["actor"] == user.ap_id -> true
676 true -> false
677 end
678 end
679
680 def skip?(:invisible, %Activity{} = activity, _user, _opts) do
681 actor = activity.data["actor"]
682 user = User.get_cached_by_ap_id(actor)
683 User.invisible?(user)
684 end
685
686 def skip?(
687 :block_from_strangers,
688 %Activity{} = activity,
689 %User{notification_settings: %{block_from_strangers: true}} = user,
690 opts
691 ) do
692 actor = activity.data["actor"]
693 follower = User.get_cached_by_ap_id(actor)
694
695 cond do
696 opts[:type] == "poll" -> false
697 user.ap_id == actor -> false
698 !User.following?(follower, user) -> true
699 true -> false
700 end
701 end
702
703 # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
704 def skip?(
705 :recently_followed,
706 %Activity{data: %{"type" => "Follow"}} = activity,
707 %User{} = user,
708 _opts
709 ) do
710 actor = activity.data["actor"]
711
712 Notification.for_user(user)
713 |> Enum.any?(fn
714 %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
715 _ -> false
716 end)
717 end
718
719 def skip?(:filtered, %{data: %{"type" => type}}, _user, _opts) when type in ["Follow", "Move"],
720 do: false
721
722 def skip?(:filtered, activity, user, _opts) do
723 object = Object.normalize(activity, fetch: false)
724
725 cond do
726 is_nil(object) ->
727 false
728
729 object.data["actor"] == user.ap_id ->
730 false
731
732 not is_nil(regex = Pleroma.Filter.compose_regex(user, :re)) ->
733 Regex.match?(regex, object.data["content"])
734
735 true ->
736 false
737 end
738 end
739
740 def skip?(_type, _activity, _user, _opts), do: false
741
742 def mark_as_read?(activity, target_user) do
743 user = Activity.user_actor(activity)
744 User.mutes_user?(target_user, user) || CommonAPI.thread_muted?(target_user, activity)
745 end
746
747 def for_user_and_activity(user, activity) do
748 from(n in __MODULE__,
749 where: n.user_id == ^user.id,
750 where: n.activity_id == ^activity.id
751 )
752 |> Repo.one()
753 end
754
755 @spec mark_context_as_read(User.t(), String.t()) :: {integer(), nil | [term()]}
756 def mark_context_as_read(%User{id: id}, context) do
757 from(
758 n in Notification,
759 join: a in assoc(n, :activity),
760 where: n.user_id == ^id,
761 where: n.seen == false,
762 where: fragment("?->>'context'", a.data) == ^context
763 )
764 |> Repo.update_all(set: [seen: true])
765 end
766 end