d89ee464568fada6f9614abb56c19e7a90782455
[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(no_cachex: true)
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, opts \\ []) do
368 case type do
369 "Follow" ->
370 accepted_function =
371 if Keyword.get(opts, :no_cachex, false) do
372 # A special function to make this usable in a migration.
373 fn activity ->
374 with %User{} = follower <- User.get_by_ap_id(activity.data["actor"]),
375 %User{} = followed <- User.get_by_ap_id(activity.data["object"]) do
376 Pleroma.FollowingRelationship.following?(follower, followed)
377 end
378 end
379 else
380 &Activity.follow_accepted?/1
381 end
382
383 if accepted_function.(activity) do
384 "follow"
385 else
386 "follow_request"
387 end
388
389 "Announce" ->
390 "reblog"
391
392 "Like" ->
393 "favourite"
394
395 "Move" ->
396 "move"
397
398 "EmojiReact" ->
399 "pleroma:emoji_reaction"
400
401 "Create" ->
402 activity
403 |> type_from_activity_object()
404
405 t ->
406 raise "No notification type for activity type #{t}"
407 end
408 end
409
410 defp type_from_activity_object(%{data: %{"type" => "Create", "object" => %{}}}), do: "mention"
411
412 defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do
413 object = Object.get_by_ap_id(activity.data["object"])
414
415 case object.data["type"] do
416 "ChatMessage" -> "pleroma:chat_mention"
417 _ -> "mention"
418 end
419 end
420
421 # TODO move to sql, too.
422 def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do
423 unless skip?(activity, user) do
424 {:ok, %{notification: notification}} =
425 Multi.new()
426 |> Multi.insert(:notification, %Notification{
427 user_id: user.id,
428 activity: activity,
429 type: type_from_activity(activity)
430 })
431 |> Marker.multi_set_last_read_id(user, "notifications")
432 |> Repo.transaction()
433
434 if do_send do
435 Streamer.stream(["user", "user:notification"], notification)
436 Push.send(notification)
437 end
438
439 notification
440 end
441 end
442
443 @doc """
444 Returns a tuple with 2 elements:
445 {notification-enabled receivers, currently disabled receivers (blocking / [thread] muting)}
446
447 NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1
448 """
449 @spec get_notified_from_activity(Activity.t(), boolean()) :: {list(User.t()), list(User.t())}
450 def get_notified_from_activity(activity, local_only \\ true)
451
452 def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
453 when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do
454 potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
455
456 potential_receivers =
457 User.get_users_from_set(potential_receiver_ap_ids, local_only: local_only)
458
459 notification_enabled_ap_ids =
460 potential_receiver_ap_ids
461 |> exclude_domain_blocker_ap_ids(activity, potential_receivers)
462 |> exclude_relationship_restricted_ap_ids(activity)
463 |> exclude_thread_muter_ap_ids(activity)
464
465 notification_enabled_users =
466 Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
467
468 {notification_enabled_users, potential_receivers -- notification_enabled_users}
469 end
470
471 def get_notified_from_activity(_, _local_only), do: {[], []}
472
473 # For some activities, only notify the author of the object
474 def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_id}})
475 when type in ~w{Like Announce EmojiReact} do
476 case Object.get_cached_by_ap_id(object_id) do
477 %Object{data: %{"actor" => actor}} ->
478 [actor]
479
480 _ ->
481 []
482 end
483 end
484
485 def get_potential_receiver_ap_ids(activity) do
486 []
487 |> Utils.maybe_notify_to_recipients(activity)
488 |> Utils.maybe_notify_mentioned_recipients(activity)
489 |> Utils.maybe_notify_subscribers(activity)
490 |> Utils.maybe_notify_followers(activity)
491 |> Enum.uniq()
492 end
493
494 @doc "Filters out AP IDs domain-blocking and not following the activity's actor"
495 def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])
496
497 def exclude_domain_blocker_ap_ids([], _activity, _preloaded_users), do: []
498
499 def exclude_domain_blocker_ap_ids(ap_ids, %Activity{} = activity, preloaded_users) do
500 activity_actor_domain = activity.actor && URI.parse(activity.actor).host
501
502 users =
503 ap_ids
504 |> Enum.map(fn ap_id ->
505 Enum.find(preloaded_users, &(&1.ap_id == ap_id)) ||
506 User.get_cached_by_ap_id(ap_id)
507 end)
508 |> Enum.filter(& &1)
509
510 domain_blocker_ap_ids = for u <- users, activity_actor_domain in u.domain_blocks, do: u.ap_id
511
512 domain_blocker_follower_ap_ids =
513 if Enum.any?(domain_blocker_ap_ids) do
514 activity
515 |> Activity.user_actor()
516 |> FollowingRelationship.followers_ap_ids(domain_blocker_ap_ids)
517 else
518 []
519 end
520
521 ap_ids
522 |> Kernel.--(domain_blocker_ap_ids)
523 |> Kernel.++(domain_blocker_follower_ap_ids)
524 end
525
526 @doc "Filters out AP IDs of users basing on their relationships with activity actor user"
527 def exclude_relationship_restricted_ap_ids([], _activity), do: []
528
529 def exclude_relationship_restricted_ap_ids(ap_ids, %Activity{} = activity) do
530 relationship_restricted_ap_ids =
531 activity
532 |> Activity.user_actor()
533 |> User.incoming_relationships_ungrouped_ap_ids([
534 :block,
535 :notification_mute
536 ])
537
538 Enum.uniq(ap_ids) -- relationship_restricted_ap_ids
539 end
540
541 @doc "Filters out AP IDs of users who mute activity thread"
542 def exclude_thread_muter_ap_ids([], _activity), do: []
543
544 def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do
545 thread_muter_ap_ids = ThreadMute.muter_ap_ids(activity.data["context"])
546
547 Enum.uniq(ap_ids) -- thread_muter_ap_ids
548 end
549
550 @spec skip?(Activity.t(), User.t()) :: boolean()
551 def skip?(%Activity{} = activity, %User{} = user) do
552 [
553 :self,
554 :followers,
555 :follows,
556 :non_followers,
557 :non_follows,
558 :recently_followed
559 ]
560 |> Enum.find(&skip?(&1, activity, user))
561 end
562
563 def skip?(_, _), do: false
564
565 @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
566 def skip?(:self, %Activity{} = activity, %User{} = user) do
567 activity.data["actor"] == user.ap_id
568 end
569
570 def skip?(
571 :followers,
572 %Activity{} = activity,
573 %User{notification_settings: %{followers: false}} = user
574 ) do
575 actor = activity.data["actor"]
576 follower = User.get_cached_by_ap_id(actor)
577 User.following?(follower, user)
578 end
579
580 def skip?(
581 :non_followers,
582 %Activity{} = activity,
583 %User{notification_settings: %{non_followers: false}} = user
584 ) do
585 actor = activity.data["actor"]
586 follower = User.get_cached_by_ap_id(actor)
587 !User.following?(follower, user)
588 end
589
590 def skip?(
591 :follows,
592 %Activity{} = activity,
593 %User{notification_settings: %{follows: false}} = user
594 ) do
595 actor = activity.data["actor"]
596 followed = User.get_cached_by_ap_id(actor)
597 User.following?(user, followed)
598 end
599
600 def skip?(
601 :non_follows,
602 %Activity{} = activity,
603 %User{notification_settings: %{non_follows: false}} = user
604 ) do
605 actor = activity.data["actor"]
606 followed = User.get_cached_by_ap_id(actor)
607 !User.following?(user, followed)
608 end
609
610 # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
611 def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do
612 actor = activity.data["actor"]
613
614 Notification.for_user(user)
615 |> Enum.any?(fn
616 %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
617 _ -> false
618 end)
619 end
620
621 def skip?(_, _, _), do: false
622 end