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