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