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