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