2ab09495d0609b0ae091d38174a1eeca3eefdcb2
[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 destroy_multiple_from_types(%{id: user_id}, types) do
345 from(n in Notification,
346 where: n.user_id == ^user_id,
347 where: n.type in ^types
348 )
349 |> Repo.delete_all()
350 end
351
352 def dismiss(%Pleroma.Activity{} = activity) do
353 Notification
354 |> where([n], n.activity_id == ^activity.id)
355 |> Repo.delete_all()
356 |> case do
357 {_, notifications} -> {:ok, notifications}
358 _ -> {:error, "Cannot dismiss notification"}
359 end
360 end
361
362 def dismiss(%{id: user_id} = _user, id) do
363 notification = Repo.get(Notification, id)
364
365 case notification do
366 %{user_id: ^user_id} ->
367 Repo.delete(notification)
368
369 _ ->
370 {:error, "Cannot dismiss notification"}
371 end
372 end
373
374 @spec create_notifications(Activity.t(), keyword()) :: {:ok, [Notification.t()] | []}
375 def create_notifications(activity, options \\ [])
376
377 def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do
378 object = Object.normalize(activity, fetch: false)
379
380 if object && object.data["type"] == "Answer" do
381 {:ok, []}
382 else
383 do_create_notifications(activity, options)
384 end
385 end
386
387 def create_notifications(%Activity{data: %{"type" => type}} = activity, options)
388 when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag"] do
389 do_create_notifications(activity, options)
390 end
391
392 def create_notifications(_, _), do: {:ok, []}
393
394 defp do_create_notifications(%Activity{} = activity, options) do
395 do_send = Keyword.get(options, :do_send, true)
396
397 {enabled_receivers, disabled_receivers} = get_notified_from_activity(activity)
398 potential_receivers = enabled_receivers ++ disabled_receivers
399
400 notifications =
401 Enum.map(potential_receivers, fn user ->
402 do_send = do_send && user in enabled_receivers
403 create_notification(activity, user, do_send: do_send)
404 end)
405 |> Enum.reject(&is_nil/1)
406
407 {:ok, notifications}
408 end
409
410 defp type_from_activity(%{data: %{"type" => type}} = activity) do
411 case type do
412 "Follow" ->
413 if Activity.follow_accepted?(activity) do
414 "follow"
415 else
416 "follow_request"
417 end
418
419 "Announce" ->
420 "reblog"
421
422 "Like" ->
423 "favourite"
424
425 "Move" ->
426 "move"
427
428 "EmojiReact" ->
429 "pleroma:emoji_reaction"
430
431 "Flag" ->
432 "pleroma:report"
433
434 # Compatibility with old reactions
435 "EmojiReaction" ->
436 "pleroma:emoji_reaction"
437
438 "Create" ->
439 activity
440 |> type_from_activity_object()
441
442 t ->
443 raise "No notification type for activity type #{t}"
444 end
445 end
446
447 defp type_from_activity_object(%{data: %{"type" => "Create", "object" => %{}}}), do: "mention"
448
449 defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do
450 object = Object.get_by_ap_id(activity.data["object"])
451
452 case object && object.data["type"] do
453 "ChatMessage" -> "pleroma:chat_mention"
454 _ -> "mention"
455 end
456 end
457
458 # TODO move to sql, too.
459 def create_notification(%Activity{} = activity, %User{} = user, opts \\ []) do
460 do_send = Keyword.get(opts, :do_send, true)
461 type = Keyword.get(opts, :type, type_from_activity(activity))
462
463 unless skip?(activity, user, opts) do
464 {:ok, %{notification: notification}} =
465 Multi.new()
466 |> Multi.insert(:notification, %Notification{
467 user_id: user.id,
468 activity: activity,
469 seen: mark_as_read?(activity, user),
470 type: type
471 })
472 |> Marker.multi_set_last_read_id(user, "notifications")
473 |> Repo.transaction()
474
475 if do_send do
476 Streamer.stream(["user", "user:notification"], notification)
477 Push.send(notification)
478 end
479
480 notification
481 end
482 end
483
484 def create_poll_notifications(%Activity{} = activity) do
485 with %Object{data: %{"type" => "Question", "actor" => actor} = data} <-
486 Object.normalize(activity) do
487 voters =
488 case data do
489 %{"voters" => voters} when is_list(voters) -> voters
490 _ -> []
491 end
492
493 notifications =
494 Enum.reduce([actor | voters], [], fn ap_id, acc ->
495 with %User{local: true} = user <- User.get_by_ap_id(ap_id) do
496 [create_notification(activity, user, type: "poll") | acc]
497 else
498 _ -> acc
499 end
500 end)
501
502 {:ok, notifications}
503 end
504 end
505
506 @doc """
507 Returns a tuple with 2 elements:
508 {notification-enabled receivers, currently disabled receivers (blocking / [thread] muting)}
509
510 NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1
511 """
512 @spec get_notified_from_activity(Activity.t(), boolean()) :: {list(User.t()), list(User.t())}
513 def get_notified_from_activity(activity, local_only \\ true)
514
515 def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
516 when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact", "Flag"] do
517 potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
518
519 potential_receivers =
520 User.get_users_from_set(potential_receiver_ap_ids, local_only: local_only)
521
522 notification_enabled_ap_ids =
523 potential_receiver_ap_ids
524 |> exclude_domain_blocker_ap_ids(activity, potential_receivers)
525 |> exclude_relationship_restricted_ap_ids(activity)
526 |> exclude_thread_muter_ap_ids(activity)
527
528 notification_enabled_users =
529 Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
530
531 {notification_enabled_users, potential_receivers -- notification_enabled_users}
532 end
533
534 def get_notified_from_activity(_, _local_only), do: {[], []}
535
536 # For some activities, only notify the author of the object
537 def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_id}})
538 when type in ~w{Like Announce EmojiReact} do
539 case Object.get_cached_by_ap_id(object_id) do
540 %Object{data: %{"actor" => actor}} ->
541 [actor]
542
543 _ ->
544 []
545 end
546 end
547
548 def get_potential_receiver_ap_ids(%{data: %{"type" => "Follow", "object" => object_id}}) do
549 [object_id]
550 end
551
552 def get_potential_receiver_ap_ids(%{data: %{"type" => "Flag", "actor" => actor}}) do
553 (User.all_superusers() |> Enum.map(fn user -> user.ap_id end)) -- [actor]
554 end
555
556 def get_potential_receiver_ap_ids(activity) do
557 []
558 |> Utils.maybe_notify_to_recipients(activity)
559 |> Utils.maybe_notify_mentioned_recipients(activity)
560 |> Utils.maybe_notify_subscribers(activity)
561 |> Utils.maybe_notify_followers(activity)
562 |> Enum.uniq()
563 end
564
565 @doc "Filters out AP IDs domain-blocking and not following the activity's actor"
566 def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])
567
568 def exclude_domain_blocker_ap_ids([], _activity, _preloaded_users), do: []
569
570 def exclude_domain_blocker_ap_ids(ap_ids, %Activity{} = activity, preloaded_users) do
571 activity_actor_domain = activity.actor && URI.parse(activity.actor).host
572
573 users =
574 ap_ids
575 |> Enum.map(fn ap_id ->
576 Enum.find(preloaded_users, &(&1.ap_id == ap_id)) ||
577 User.get_cached_by_ap_id(ap_id)
578 end)
579 |> Enum.filter(& &1)
580
581 domain_blocker_ap_ids = for u <- users, activity_actor_domain in u.domain_blocks, do: u.ap_id
582
583 domain_blocker_follower_ap_ids =
584 if Enum.any?(domain_blocker_ap_ids) do
585 activity
586 |> Activity.user_actor()
587 |> FollowingRelationship.followers_ap_ids(domain_blocker_ap_ids)
588 else
589 []
590 end
591
592 ap_ids
593 |> Kernel.--(domain_blocker_ap_ids)
594 |> Kernel.++(domain_blocker_follower_ap_ids)
595 end
596
597 @doc "Filters out AP IDs of users basing on their relationships with activity actor user"
598 def exclude_relationship_restricted_ap_ids([], _activity), do: []
599
600 def exclude_relationship_restricted_ap_ids(ap_ids, %Activity{} = activity) do
601 relationship_restricted_ap_ids =
602 activity
603 |> Activity.user_actor()
604 |> User.incoming_relationships_ungrouped_ap_ids([
605 :block,
606 :notification_mute
607 ])
608
609 Enum.uniq(ap_ids) -- relationship_restricted_ap_ids
610 end
611
612 @doc "Filters out AP IDs of users who mute activity thread"
613 def exclude_thread_muter_ap_ids([], _activity), do: []
614
615 def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do
616 thread_muter_ap_ids = ThreadMute.muter_ap_ids(activity.data["context"])
617
618 Enum.uniq(ap_ids) -- thread_muter_ap_ids
619 end
620
621 def skip?(activity, user, opts \\ [])
622
623 @spec skip?(Activity.t(), User.t(), Keyword.t()) :: boolean()
624 def skip?(%Activity{} = activity, %User{} = user, opts) do
625 [
626 :self,
627 :invisible,
628 :block_from_strangers,
629 :recently_followed,
630 :filtered
631 ]
632 |> Enum.find(&skip?(&1, activity, user, opts))
633 end
634
635 def skip?(_activity, _user, _opts), do: false
636
637 @spec skip?(atom(), Activity.t(), User.t(), Keyword.t()) :: boolean()
638 def skip?(:self, %Activity{} = activity, %User{} = user, opts) do
639 cond do
640 opts[:type] == "poll" -> false
641 activity.data["actor"] == user.ap_id -> true
642 true -> false
643 end
644 end
645
646 def skip?(:invisible, %Activity{} = activity, _user, _opts) do
647 actor = activity.data["actor"]
648 user = User.get_cached_by_ap_id(actor)
649 User.invisible?(user)
650 end
651
652 def skip?(
653 :block_from_strangers,
654 %Activity{} = activity,
655 %User{notification_settings: %{block_from_strangers: true}} = user,
656 opts
657 ) do
658 actor = activity.data["actor"]
659 follower = User.get_cached_by_ap_id(actor)
660
661 cond do
662 opts[:type] == "poll" -> false
663 user.ap_id == actor -> false
664 !User.following?(follower, user) -> true
665 true -> false
666 end
667 end
668
669 # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
670 def skip?(
671 :recently_followed,
672 %Activity{data: %{"type" => "Follow"}} = activity,
673 %User{} = user,
674 _opts
675 ) do
676 actor = activity.data["actor"]
677
678 Notification.for_user(user)
679 |> Enum.any?(fn
680 %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
681 _ -> false
682 end)
683 end
684
685 def skip?(:filtered, %{data: %{"type" => type}}, _user, _opts) when type in ["Follow", "Move"],
686 do: false
687
688 def skip?(:filtered, activity, user, _opts) do
689 object = Object.normalize(activity, fetch: false)
690
691 cond do
692 is_nil(object) ->
693 false
694
695 object.data["actor"] == user.ap_id ->
696 false
697
698 not is_nil(regex = Pleroma.Filter.compose_regex(user, :re)) ->
699 Regex.match?(regex, object.data["content"])
700
701 true ->
702 false
703 end
704 end
705
706 def skip?(_type, _activity, _user, _opts), do: false
707
708 def mark_as_read?(activity, target_user) do
709 user = Activity.user_actor(activity)
710 User.mutes_user?(target_user, user) || CommonAPI.thread_muted?(target_user, activity)
711 end
712
713 def for_user_and_activity(user, activity) do
714 from(n in __MODULE__,
715 where: n.user_id == ^user.id,
716 where: n.activity_id == ^activity.id
717 )
718 |> Repo.one()
719 end
720
721 @spec mark_context_as_read(User.t(), String.t()) :: {integer(), nil | [term()]}
722 def mark_context_as_read(%User{id: id}, context) do
723 from(
724 n in Notification,
725 join: a in assoc(n, :activity),
726 where: n.user_id == ^id,
727 where: n.seen == false,
728 where: fragment("?->>'context'", a.data) == ^context
729 )
730 |> Repo.update_all(set: [seen: true])
731 end
732 end