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