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