First
[anni] / lib / pleroma / notification.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 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:chat_mention
72     pleroma:emoji_reaction
73     pleroma:report
74     reblog
75     poll
76   }
77
78   def changeset(%Notification{} = notification, attrs) do
79     notification
80     |> cast(attrs, [:seen, :type])
81     |> validate_inclusion(:type, @notification_types)
82   end
83
84   @spec last_read_query(User.t()) :: Ecto.Queryable.t()
85   def last_read_query(user) do
86     from(q in Pleroma.Notification,
87       where: q.user_id == ^user.id,
88       where: q.seen == true,
89       select: type(q.id, :string),
90       limit: 1,
91       order_by: [desc: :id]
92     )
93   end
94
95   defp for_user_query_ap_id_opts(user, opts) do
96     ap_id_relationships =
97       [:block] ++
98         if opts[@include_muted_option], do: [], else: [:notification_mute]
99
100     preloaded_ap_ids = User.outgoing_relationships_ap_ids(user, ap_id_relationships)
101
102     exclude_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts)
103
104     exclude_notification_muted_opts =
105       Map.merge(%{notification_muted_users_ap_ids: preloaded_ap_ids[:notification_mute]}, opts)
106
107     {exclude_blocked_opts, exclude_notification_muted_opts}
108   end
109
110   def for_user_query(user, opts \\ %{}) do
111     {exclude_blocked_opts, exclude_notification_muted_opts} =
112       for_user_query_ap_id_opts(user, opts)
113
114     Notification
115     |> where(user_id: ^user.id)
116     |> join(:inner, [n], activity in assoc(n, :activity))
117     |> join(:left, [n, a], object in Object,
118       on:
119         fragment(
120           "(?->>'id') = associated_object_id(?)",
121           object.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("?->>'content' is null", o.data) or
182               fragment("?->>'actor' = ?", o.data, ^user.ap_id)
183         )
184     end
185   end
186
187   @valid_visibilities ~w[direct unlisted public private]
188
189   defp exclude_visibility(query, %{exclude_visibilities: visibility})
190        when is_list(visibility) do
191     if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
192       query
193       |> join(:left, [n, a], mutated_activity in Pleroma.Activity,
194         on:
195           fragment(
196             "associated_object_id(?)",
197             a.data
198           ) ==
199             fragment(
200               "associated_object_id(?)",
201               mutated_activity.data
202             ) and
203             fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
204             fragment("?->>'type'", mutated_activity.data) == "Create",
205         as: :mutated_activity
206       )
207       |> where(
208         [n, a, mutated_activity: mutated_activity],
209         not fragment(
210           """
211           CASE WHEN (?->>'type') = 'Like' or (?->>'type') = 'Announce'
212             THEN (activity_visibility(?, ?, ?) = ANY (?))
213             ELSE (activity_visibility(?, ?, ?) = ANY (?)) END
214           """,
215           a.data,
216           a.data,
217           mutated_activity.actor,
218           mutated_activity.recipients,
219           mutated_activity.data,
220           ^visibility,
221           a.actor,
222           a.recipients,
223           a.data,
224           ^visibility
225         )
226       )
227     else
228       Logger.error("Could not exclude visibility to #{visibility}")
229       query
230     end
231   end
232
233   defp exclude_visibility(query, %{exclude_visibilities: visibility})
234        when visibility in @valid_visibilities do
235     exclude_visibility(query, [visibility])
236   end
237
238   defp exclude_visibility(query, %{exclude_visibilities: visibility})
239        when visibility not in @valid_visibilities do
240     Logger.error("Could not exclude visibility to #{visibility}")
241     query
242   end
243
244   defp exclude_visibility(query, _visibility), do: query
245
246   def for_user(user, opts \\ %{}) do
247     user
248     |> for_user_query(opts)
249     |> Pagination.fetch_paginated(opts)
250   end
251
252   @doc """
253   Returns notifications for user received since given date.
254
255   ## Examples
256
257       iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-13 11:22:33])
258       [%Pleroma.Notification{}, %Pleroma.Notification{}]
259
260       iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-15 11:22:33])
261       []
262   """
263   @spec for_user_since(Pleroma.User.t(), NaiveDateTime.t()) :: [t()]
264   def for_user_since(user, date) do
265     from(n in for_user_query(user),
266       where: n.updated_at > ^date
267     )
268     |> Repo.all()
269   end
270
271   def set_read_up_to(%{id: user_id} = user, id) do
272     query =
273       from(
274         n in Notification,
275         where: n.user_id == ^user_id,
276         where: n.id <= ^id,
277         where: n.seen == false,
278         # Ideally we would preload object and activities here
279         # but Ecto does not support preloads in update_all
280         select: n.id
281       )
282
283     {:ok, %{ids: {_, notification_ids}}} =
284       Multi.new()
285       |> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
286       |> Marker.multi_set_last_read_id(user, "notifications")
287       |> Repo.transaction()
288
289     for_user_query(user)
290     |> where([n], n.id in ^notification_ids)
291     |> Repo.all()
292   end
293
294   @spec read_one(User.t(), String.t()) ::
295           {:ok, Notification.t()} | {:error, Ecto.Changeset.t()} | nil
296   def read_one(%User{} = user, notification_id) do
297     with {:ok, %Notification{} = notification} <- get(user, notification_id) do
298       Multi.new()
299       |> Multi.update(:update, changeset(notification, %{seen: true}))
300       |> Marker.multi_set_last_read_id(user, "notifications")
301       |> Repo.transaction()
302       |> case do
303         {:ok, %{update: notification}} -> {:ok, notification}
304         {:error, :update, changeset, _} -> {:error, changeset}
305       end
306     end
307   end
308
309   def get(%{id: user_id} = _user, id) do
310     query =
311       from(
312         n in Notification,
313         where: n.id == ^id,
314         join: activity in assoc(n, :activity),
315         preload: [activity: activity]
316       )
317
318     notification = Repo.one(query)
319
320     case notification do
321       %{user_id: ^user_id} ->
322         {:ok, notification}
323
324       _ ->
325         {:error, "Cannot get notification"}
326     end
327   end
328
329   def clear(user) do
330     from(n in Notification, where: n.user_id == ^user.id)
331     |> Repo.delete_all()
332   end
333
334   def destroy_multiple(%{id: user_id} = _user, ids) do
335     from(n in Notification,
336       where: n.id in ^ids,
337       where: n.user_id == ^user_id
338     )
339     |> Repo.delete_all()
340   end
341
342   def dismiss(%Pleroma.Activity{} = activity) do
343     Notification
344     |> where([n], n.activity_id == ^activity.id)
345     |> Repo.delete_all()
346     |> case do
347       {_, notifications} -> {:ok, notifications}
348       _ -> {:error, "Cannot dismiss notification"}
349     end
350   end
351
352   def dismiss(%{id: user_id} = _user, id) do
353     notification = Repo.get(Notification, id)
354
355     case notification do
356       %{user_id: ^user_id} ->
357         Repo.delete(notification)
358
359       _ ->
360         {:error, "Cannot dismiss notification"}
361     end
362   end
363
364   @spec create_notifications(Activity.t(), keyword()) :: {:ok, [Notification.t()] | []}
365   def create_notifications(activity, options \\ [])
366
367   def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do
368     object = Object.normalize(activity, fetch: false)
369
370     if object && object.data["type"] == "Answer" do
371       {:ok, []}
372     else
373       do_create_notifications(activity, options)
374     end
375   end
376
377   def create_notifications(%Activity{data: %{"type" => type}} = activity, options)
378       when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag", "Update"] do
379     do_create_notifications(activity, options)
380   end
381
382   def create_notifications(_, _), do: {:ok, []}
383
384   defp do_create_notifications(%Activity{} = activity, options) do
385     do_send = Keyword.get(options, :do_send, true)
386
387     {enabled_receivers, disabled_receivers} = get_notified_from_activity(activity)
388     potential_receivers = enabled_receivers ++ disabled_receivers
389
390     notifications =
391       Enum.map(potential_receivers, fn user ->
392         do_send = do_send && user in enabled_receivers
393         create_notification(activity, user, do_send: do_send)
394       end)
395       |> Enum.reject(&is_nil/1)
396
397     {:ok, notifications}
398   end
399
400   defp type_from_activity(%{data: %{"type" => type}} = activity) do
401     case type do
402       "Follow" ->
403         if Activity.follow_accepted?(activity) do
404           "follow"
405         else
406           "follow_request"
407         end
408
409       "Announce" ->
410         "reblog"
411
412       "Like" ->
413         "favourite"
414
415       "Move" ->
416         "move"
417
418       "EmojiReact" ->
419         "pleroma:emoji_reaction"
420
421       "Flag" ->
422         "pleroma:report"
423
424       # Compatibility with old reactions
425       "EmojiReaction" ->
426         "pleroma:emoji_reaction"
427
428       "Create" ->
429         activity
430         |> type_from_activity_object()
431
432       "Update" ->
433         "update"
434
435       t ->
436         raise "No notification type for activity type #{t}"
437     end
438   end
439
440   defp type_from_activity_object(%{data: %{"type" => "Create", "object" => %{}}}), do: "mention"
441
442   defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do
443     object = Object.get_by_ap_id(activity.data["object"])
444
445     case object && object.data["type"] do
446       "ChatMessage" -> "pleroma:chat_mention"
447       _ -> "mention"
448     end
449   end
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_users_with_privilege(:reports_manage_reports)
556      |> Enum.map(fn user -> user.ap_id end)) --
557       [actor]
558   end
559
560   # Update activity: notify all who repeated this
561   def get_potential_receiver_ap_ids(%{data: %{"type" => "Update", "actor" => actor}} = activity) do
562     with %Object{data: %{"id" => object_id}} <- Object.normalize(activity, fetch: false) do
563       repeaters =
564         Activity.Queries.by_type("Announce")
565         |> Activity.Queries.by_object_id(object_id)
566         |> Activity.with_joined_user_actor()
567         |> where([a, u], u.local)
568         |> select([a, u], u.ap_id)
569         |> Repo.all()
570
571       repeaters -- [actor]
572     end
573   end
574
575   def get_potential_receiver_ap_ids(activity) do
576     []
577     |> Utils.maybe_notify_to_recipients(activity)
578     |> Utils.maybe_notify_mentioned_recipients(activity)
579     |> Utils.maybe_notify_subscribers(activity)
580     |> Utils.maybe_notify_followers(activity)
581     |> Enum.uniq()
582   end
583
584   @doc "Filters out AP IDs domain-blocking and not following the activity's actor"
585   def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])
586
587   def exclude_domain_blocker_ap_ids([], _activity, _preloaded_users), do: []
588
589   def exclude_domain_blocker_ap_ids(ap_ids, %Activity{} = activity, preloaded_users) do
590     activity_actor_domain = activity.actor && URI.parse(activity.actor).host
591
592     users =
593       ap_ids
594       |> Enum.map(fn ap_id ->
595         Enum.find(preloaded_users, &(&1.ap_id == ap_id)) ||
596           User.get_cached_by_ap_id(ap_id)
597       end)
598       |> Enum.filter(& &1)
599
600     domain_blocker_ap_ids = for u <- users, activity_actor_domain in u.domain_blocks, do: u.ap_id
601
602     domain_blocker_follower_ap_ids =
603       if Enum.any?(domain_blocker_ap_ids) do
604         activity
605         |> Activity.user_actor()
606         |> FollowingRelationship.followers_ap_ids(domain_blocker_ap_ids)
607       else
608         []
609       end
610
611     ap_ids
612     |> Kernel.--(domain_blocker_ap_ids)
613     |> Kernel.++(domain_blocker_follower_ap_ids)
614   end
615
616   @doc "Filters out AP IDs of users basing on their relationships with activity actor user"
617   def exclude_relationship_restricted_ap_ids([], _activity), do: []
618
619   def exclude_relationship_restricted_ap_ids(ap_ids, %Activity{} = activity) do
620     relationship_restricted_ap_ids =
621       activity
622       |> Activity.user_actor()
623       |> User.incoming_relationships_ungrouped_ap_ids([
624         :block,
625         :notification_mute
626       ])
627
628     Enum.uniq(ap_ids) -- relationship_restricted_ap_ids
629   end
630
631   @doc "Filters out AP IDs of users who mute activity thread"
632   def exclude_thread_muter_ap_ids([], _activity), do: []
633
634   def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do
635     thread_muter_ap_ids = ThreadMute.muter_ap_ids(activity.data["context"])
636
637     Enum.uniq(ap_ids) -- thread_muter_ap_ids
638   end
639
640   def skip?(activity, user, opts \\ [])
641
642   @spec skip?(Activity.t(), User.t(), Keyword.t()) :: boolean()
643   def skip?(%Activity{} = activity, %User{} = user, opts) do
644     [
645       :self,
646       :invisible,
647       :block_from_strangers,
648       :recently_followed,
649       :filtered
650     ]
651     |> Enum.find(&skip?(&1, activity, user, opts))
652   end
653
654   def skip?(_activity, _user, _opts), do: false
655
656   @spec skip?(atom(), Activity.t(), User.t(), Keyword.t()) :: boolean()
657   def skip?(:self, %Activity{} = activity, %User{} = user, opts) do
658     cond do
659       opts[:type] == "poll" -> false
660       activity.data["actor"] == user.ap_id -> true
661       true -> false
662     end
663   end
664
665   def skip?(:invisible, %Activity{} = activity, _user, _opts) do
666     actor = activity.data["actor"]
667     user = User.get_cached_by_ap_id(actor)
668     User.invisible?(user)
669   end
670
671   def skip?(
672         :block_from_strangers,
673         %Activity{} = activity,
674         %User{notification_settings: %{block_from_strangers: true}} = user,
675         opts
676       ) do
677     actor = activity.data["actor"]
678     follower = User.get_cached_by_ap_id(actor)
679
680     cond do
681       opts[:type] == "poll" -> false
682       user.ap_id == actor -> false
683       !User.following?(user, follower) -> true
684       true -> false
685     end
686   end
687
688   # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
689   def skip?(
690         :recently_followed,
691         %Activity{data: %{"type" => "Follow"}} = activity,
692         %User{} = user,
693         _opts
694       ) do
695     actor = activity.data["actor"]
696
697     Notification.for_user(user)
698     |> Enum.any?(fn
699       %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
700       _ -> false
701     end)
702   end
703
704   def skip?(:filtered, %{data: %{"type" => type}}, _user, _opts) when type in ["Follow", "Move"],
705     do: false
706
707   def skip?(:filtered, activity, user, _opts) do
708     object = Object.normalize(activity, fetch: false)
709
710     cond do
711       is_nil(object) ->
712         false
713
714       object.data["actor"] == user.ap_id ->
715         false
716
717       not is_nil(regex = Pleroma.Filter.compose_regex(user, :re)) ->
718         Regex.match?(regex, object.data["content"])
719
720       true ->
721         false
722     end
723   end
724
725   def skip?(_type, _activity, _user, _opts), do: false
726
727   def mark_as_read?(activity, target_user) do
728     user = Activity.user_actor(activity)
729     User.mutes_user?(target_user, user) || CommonAPI.thread_muted?(target_user, activity)
730   end
731
732   def for_user_and_activity(user, activity) do
733     from(n in __MODULE__,
734       where: n.user_id == ^user.id,
735       where: n.activity_id == ^activity.id
736     )
737     |> Repo.one()
738   end
739
740   @spec mark_context_as_read(User.t(), String.t()) :: {integer(), nil | [term()]}
741   def mark_context_as_read(%User{id: id}, context) do
742     from(
743       n in Notification,
744       join: a in assoc(n, :activity),
745       where: n.user_id == ^id,
746       where: n.seen == false,
747       where: fragment("?->>'context'", a.data) == ^context
748     )
749     |> Repo.update_all(set: [seen: true])
750   end
751 end