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