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