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