0d7a6610a16464f9476949e1a27dfb1abacff636
[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_relations =
43 [:block] ++
44 if opts[@include_muted_option], do: [], else: [:notification_mute]
45
46 preloaded_ap_ids = User.outgoing_relations_ap_ids(user, ap_id_relations)
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'::text) ->> 'id'::text)",
74 object.data,
75 a.data
76 )
77 )
78 |> preload([n, a, o], activity: {a, object: o})
79 |> exclude_notification_muted(user, exclude_notification_muted_opts)
80 |> exclude_blocked(user, exclude_blocked_opts)
81 |> exclude_visibility(opts)
82 |> exclude_move(opts)
83 end
84
85 defp exclude_blocked(query, user, opts) do
86 blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
87
88 query
89 |> where([n, a], a.actor not in ^blocked_ap_ids)
90 |> where(
91 [n, a],
92 fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.domain_blocks
93 )
94 end
95
96 defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do
97 query
98 end
99
100 defp exclude_notification_muted(query, user, opts) do
101 notification_muted_ap_ids =
102 opts[:notification_muted_users_ap_ids] || User.notification_muted_users_ap_ids(user)
103
104 query
105 |> where([n, a], a.actor not in ^notification_muted_ap_ids)
106 |> join(:left, [n, a], tm in ThreadMute,
107 on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
108 )
109 |> where([n, a, o, tm], is_nil(tm.user_id))
110 end
111
112 defp exclude_move(query, %{with_move: true}) do
113 query
114 end
115
116 defp exclude_move(query, _opts) do
117 where(query, [n, a], fragment("?->>'type' != 'Move'", a.data))
118 end
119
120 @valid_visibilities ~w[direct unlisted public private]
121
122 defp exclude_visibility(query, %{exclude_visibilities: visibility})
123 when is_list(visibility) do
124 if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
125 query
126 |> join(:left, [n, a], mutated_activity in Pleroma.Activity,
127 on:
128 fragment("?->>'context'", a.data) ==
129 fragment("?->>'context'", mutated_activity.data) and
130 fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
131 fragment("?->>'type'", mutated_activity.data) == "Create",
132 as: :mutated_activity
133 )
134 |> where(
135 [n, a, mutated_activity: mutated_activity],
136 not fragment(
137 """
138 CASE WHEN (?->>'type') = 'Like' or (?->>'type') = 'Announce'
139 THEN (activity_visibility(?, ?, ?) = ANY (?))
140 ELSE (activity_visibility(?, ?, ?) = ANY (?)) END
141 """,
142 a.data,
143 a.data,
144 mutated_activity.actor,
145 mutated_activity.recipients,
146 mutated_activity.data,
147 ^visibility,
148 a.actor,
149 a.recipients,
150 a.data,
151 ^visibility
152 )
153 )
154 else
155 Logger.error("Could not exclude visibility to #{visibility}")
156 query
157 end
158 end
159
160 defp exclude_visibility(query, %{exclude_visibilities: visibility})
161 when visibility in @valid_visibilities do
162 exclude_visibility(query, [visibility])
163 end
164
165 defp exclude_visibility(query, %{exclude_visibilities: visibility})
166 when visibility not in @valid_visibilities do
167 Logger.error("Could not exclude visibility to #{visibility}")
168 query
169 end
170
171 defp exclude_visibility(query, _visibility), do: query
172
173 def for_user(user, opts \\ %{}) do
174 user
175 |> for_user_query(opts)
176 |> Pagination.fetch_paginated(opts)
177 end
178
179 @doc """
180 Returns notifications for user received since given date.
181
182 ## Examples
183
184 iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-13 11:22:33])
185 [%Pleroma.Notification{}, %Pleroma.Notification{}]
186
187 iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-15 11:22:33])
188 []
189 """
190 @spec for_user_since(Pleroma.User.t(), NaiveDateTime.t()) :: [t()]
191 def for_user_since(user, date) do
192 from(n in for_user_query(user),
193 where: n.updated_at > ^date
194 )
195 |> Repo.all()
196 end
197
198 def set_read_up_to(%{id: user_id} = _user, id) do
199 query =
200 from(
201 n in Notification,
202 where: n.user_id == ^user_id,
203 where: n.id <= ^id,
204 where: n.seen == false,
205 update: [
206 set: [
207 seen: true,
208 updated_at: ^NaiveDateTime.utc_now()
209 ]
210 ],
211 # Ideally we would preload object and activities here
212 # but Ecto does not support preloads in update_all
213 select: n.id
214 )
215
216 {_, notification_ids} = Repo.update_all(query, [])
217
218 Notification
219 |> where([n], n.id in ^notification_ids)
220 |> join(:inner, [n], activity in assoc(n, :activity))
221 |> join(:left, [n, a], object in Object,
222 on:
223 fragment(
224 "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
225 object.data,
226 a.data
227 )
228 )
229 |> preload([n, a, o], activity: {a, object: o})
230 |> Repo.all()
231 end
232
233 def read_one(%User{} = user, notification_id) do
234 with {:ok, %Notification{} = notification} <- get(user, notification_id) do
235 notification
236 |> changeset(%{seen: true})
237 |> Repo.update()
238 end
239 end
240
241 def get(%{id: user_id} = _user, id) do
242 query =
243 from(
244 n in Notification,
245 where: n.id == ^id,
246 join: activity in assoc(n, :activity),
247 preload: [activity: activity]
248 )
249
250 notification = Repo.one(query)
251
252 case notification do
253 %{user_id: ^user_id} ->
254 {:ok, notification}
255
256 _ ->
257 {:error, "Cannot get notification"}
258 end
259 end
260
261 def clear(user) do
262 from(n in Notification, where: n.user_id == ^user.id)
263 |> Repo.delete_all()
264 end
265
266 def destroy_multiple(%{id: user_id} = _user, ids) do
267 from(n in Notification,
268 where: n.id in ^ids,
269 where: n.user_id == ^user_id
270 )
271 |> Repo.delete_all()
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 ["Like", "Announce", "Follow", "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 {enabled notification receivers, currently disabled receivers (blocking / [thread] muting)}
334 """
335 def get_notified_from_activity(activity, local_only \\ true)
336
337 def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
338 when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do
339 potential_receiver_ap_ids =
340 []
341 |> Utils.maybe_notify_to_recipients(activity)
342 |> Utils.maybe_notify_mentioned_recipients(activity)
343 |> Utils.maybe_notify_subscribers(activity)
344 |> Utils.maybe_notify_followers(activity)
345 |> Enum.uniq()
346
347 notification_enabled_ap_ids =
348 potential_receiver_ap_ids
349 |> exclude_relation_restricting_ap_ids(activity)
350 |> exclude_thread_muter_ap_ids(activity)
351
352 potential_receivers =
353 potential_receiver_ap_ids
354 |> Enum.uniq()
355 |> User.get_users_from_set(local_only)
356
357 notification_enabled_users =
358 Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
359
360 {notification_enabled_users, potential_receivers -- notification_enabled_users}
361 end
362
363 def get_notified_from_activity(_, _local_only), do: {[], []}
364
365 @doc "Filters out AP IDs of users basing on their relationships with activity actor user"
366 def exclude_relation_restricting_ap_ids([], _activity), do: []
367
368 def exclude_relation_restricting_ap_ids(ap_ids, %Activity{} = activity) do
369 relation_restricted_ap_ids =
370 activity
371 |> Activity.user_actor()
372 |> User.incoming_relations_ungrouped_ap_ids([
373 :block,
374 :notification_mute
375 ])
376
377 Enum.uniq(ap_ids) -- relation_restricted_ap_ids
378 end
379
380 @doc "Filters out AP IDs of users who mute activity thread"
381 def exclude_thread_muter_ap_ids([], _activity), do: []
382
383 def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do
384 thread_muter_ap_ids = ThreadMute.muter_ap_ids(activity.data["context"])
385
386 Enum.uniq(ap_ids) -- thread_muter_ap_ids
387 end
388
389 @spec skip?(Activity.t(), User.t()) :: boolean()
390 def skip?(%Activity{} = activity, %User{} = user) do
391 [
392 :self,
393 :followers,
394 :follows,
395 :non_followers,
396 :non_follows,
397 :recently_followed
398 ]
399 |> Enum.find(&skip?(&1, activity, user))
400 end
401
402 def skip?(_, _), do: false
403
404 @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
405 def skip?(:self, %Activity{} = activity, %User{} = user) do
406 activity.data["actor"] == user.ap_id
407 end
408
409 def skip?(
410 :followers,
411 %Activity{} = activity,
412 %User{notification_settings: %{followers: false}} = user
413 ) do
414 actor = activity.data["actor"]
415 follower = User.get_cached_by_ap_id(actor)
416 User.following?(follower, user)
417 end
418
419 def skip?(
420 :non_followers,
421 %Activity{} = activity,
422 %User{notification_settings: %{non_followers: false}} = user
423 ) do
424 actor = activity.data["actor"]
425 follower = User.get_cached_by_ap_id(actor)
426 !User.following?(follower, user)
427 end
428
429 def skip?(
430 :follows,
431 %Activity{} = activity,
432 %User{notification_settings: %{follows: false}} = user
433 ) do
434 actor = activity.data["actor"]
435 followed = User.get_cached_by_ap_id(actor)
436 User.following?(user, followed)
437 end
438
439 def skip?(
440 :non_follows,
441 %Activity{} = activity,
442 %User{notification_settings: %{non_follows: false}} = user
443 ) do
444 actor = activity.data["actor"]
445 followed = User.get_cached_by_ap_id(actor)
446 !User.following?(user, followed)
447 end
448
449 # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
450 def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do
451 actor = activity.data["actor"]
452
453 Notification.for_user(user)
454 |> Enum.any?(fn
455 %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
456 _ -> false
457 end)
458 end
459
460 def skip?(_, _, _), do: false
461 end