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