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