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