User: Change signature of get_users_from_set
[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)
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 = get_potential_receiver_ap_ids(activity)
372
373 potential_receivers =
374 User.get_users_from_set(potential_receiver_ap_ids, local_only: local_only)
375
376 notification_enabled_ap_ids =
377 potential_receiver_ap_ids
378 |> exclude_domain_blocker_ap_ids(activity, potential_receivers)
379 |> exclude_relationship_restricted_ap_ids(activity)
380 |> exclude_thread_muter_ap_ids(activity)
381
382 notification_enabled_users =
383 Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
384
385 {notification_enabled_users, potential_receivers -- notification_enabled_users}
386 end
387
388 def get_notified_from_activity(_, _local_only), do: {[], []}
389
390 # For some activities, only notify the author of the object
391 def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_id}})
392 when type in ~w{Like Announce EmojiReact} do
393 case Object.get_cached_by_ap_id(object_id) do
394 %Object{data: %{"actor" => actor}} ->
395 [actor]
396
397 _ ->
398 []
399 end
400 end
401
402 def get_potential_receiver_ap_ids(activity) do
403 []
404 |> Utils.maybe_notify_to_recipients(activity)
405 |> Utils.maybe_notify_mentioned_recipients(activity)
406 |> Utils.maybe_notify_subscribers(activity)
407 |> Utils.maybe_notify_followers(activity)
408 |> Enum.uniq()
409 end
410
411 @doc "Filters out AP IDs domain-blocking and not following the activity's actor"
412 def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])
413
414 def exclude_domain_blocker_ap_ids([], _activity, _preloaded_users), do: []
415
416 def exclude_domain_blocker_ap_ids(ap_ids, %Activity{} = activity, preloaded_users) do
417 activity_actor_domain = activity.actor && URI.parse(activity.actor).host
418
419 users =
420 ap_ids
421 |> Enum.map(fn ap_id ->
422 Enum.find(preloaded_users, &(&1.ap_id == ap_id)) ||
423 User.get_cached_by_ap_id(ap_id)
424 end)
425 |> Enum.filter(& &1)
426
427 domain_blocker_ap_ids = for u <- users, activity_actor_domain in u.domain_blocks, do: u.ap_id
428
429 domain_blocker_follower_ap_ids =
430 if Enum.any?(domain_blocker_ap_ids) do
431 activity
432 |> Activity.user_actor()
433 |> FollowingRelationship.followers_ap_ids(domain_blocker_ap_ids)
434 else
435 []
436 end
437
438 ap_ids
439 |> Kernel.--(domain_blocker_ap_ids)
440 |> Kernel.++(domain_blocker_follower_ap_ids)
441 end
442
443 @doc "Filters out AP IDs of users basing on their relationships with activity actor user"
444 def exclude_relationship_restricted_ap_ids([], _activity), do: []
445
446 def exclude_relationship_restricted_ap_ids(ap_ids, %Activity{} = activity) do
447 relationship_restricted_ap_ids =
448 activity
449 |> Activity.user_actor()
450 |> User.incoming_relationships_ungrouped_ap_ids([
451 :block,
452 :notification_mute
453 ])
454
455 Enum.uniq(ap_ids) -- relationship_restricted_ap_ids
456 end
457
458 @doc "Filters out AP IDs of users who mute activity thread"
459 def exclude_thread_muter_ap_ids([], _activity), do: []
460
461 def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do
462 thread_muter_ap_ids = ThreadMute.muter_ap_ids(activity.data["context"])
463
464 Enum.uniq(ap_ids) -- thread_muter_ap_ids
465 end
466
467 @spec skip?(Activity.t(), User.t()) :: boolean()
468 def skip?(%Activity{} = activity, %User{} = user) do
469 [
470 :self,
471 :followers,
472 :follows,
473 :non_followers,
474 :non_follows,
475 :recently_followed
476 ]
477 |> Enum.find(&skip?(&1, activity, user))
478 end
479
480 def skip?(_, _), do: false
481
482 @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
483 def skip?(:self, %Activity{} = activity, %User{} = user) do
484 activity.data["actor"] == user.ap_id
485 end
486
487 def skip?(
488 :followers,
489 %Activity{} = activity,
490 %User{notification_settings: %{followers: false}} = user
491 ) do
492 actor = activity.data["actor"]
493 follower = User.get_cached_by_ap_id(actor)
494 User.following?(follower, user)
495 end
496
497 def skip?(
498 :non_followers,
499 %Activity{} = activity,
500 %User{notification_settings: %{non_followers: false}} = user
501 ) do
502 actor = activity.data["actor"]
503 follower = User.get_cached_by_ap_id(actor)
504 !User.following?(follower, user)
505 end
506
507 def skip?(
508 :follows,
509 %Activity{} = activity,
510 %User{notification_settings: %{follows: false}} = user
511 ) do
512 actor = activity.data["actor"]
513 followed = User.get_cached_by_ap_id(actor)
514 User.following?(user, followed)
515 end
516
517 def skip?(
518 :non_follows,
519 %Activity{} = activity,
520 %User{notification_settings: %{non_follows: false}} = user
521 ) do
522 actor = activity.data["actor"]
523 followed = User.get_cached_by_ap_id(actor)
524 !User.following?(user, followed)
525 end
526
527 # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
528 def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do
529 actor = activity.data["actor"]
530
531 Notification.for_user(user)
532 |> Enum.any?(fn
533 %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
534 _ -> false
535 end)
536 end
537
538 def skip?(_, _, _), do: false
539 end