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