Merge branch '1364-notifications-sending-control' 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 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'::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 NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1
336 """
337 def get_notified_from_activity(activity, local_only \\ true)
338
339 def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
340 when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do
341 potential_receiver_ap_ids =
342 []
343 |> Utils.maybe_notify_to_recipients(activity)
344 |> Utils.maybe_notify_mentioned_recipients(activity)
345 |> Utils.maybe_notify_subscribers(activity)
346 |> Utils.maybe_notify_followers(activity)
347 |> Enum.uniq()
348
349 # Since even subscribers and followers can mute / thread-mute, filtering all above AP IDs
350 notification_enabled_ap_ids =
351 potential_receiver_ap_ids
352 |> exclude_relationship_restricted_ap_ids(activity)
353 |> exclude_thread_muter_ap_ids(activity)
354
355 potential_receivers =
356 potential_receiver_ap_ids
357 |> Enum.uniq()
358 |> User.get_users_from_set(local_only)
359
360 notification_enabled_users =
361 Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
362
363 {notification_enabled_users, potential_receivers -- notification_enabled_users}
364 end
365
366 def get_notified_from_activity(_, _local_only), do: {[], []}
367
368 @doc "Filters out AP IDs of users basing on their relationships with activity actor user"
369 def exclude_relationship_restricted_ap_ids([], _activity), do: []
370
371 def exclude_relationship_restricted_ap_ids(ap_ids, %Activity{} = activity) do
372 relationship_restricted_ap_ids =
373 activity
374 |> Activity.user_actor()
375 |> User.incoming_relationships_ungrouped_ap_ids([
376 :block,
377 :notification_mute
378 ])
379
380 Enum.uniq(ap_ids) -- relationship_restricted_ap_ids
381 end
382
383 @doc "Filters out AP IDs of users who mute activity thread"
384 def exclude_thread_muter_ap_ids([], _activity), do: []
385
386 def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do
387 thread_muter_ap_ids = ThreadMute.muter_ap_ids(activity.data["context"])
388
389 Enum.uniq(ap_ids) -- thread_muter_ap_ids
390 end
391
392 @spec skip?(Activity.t(), User.t()) :: boolean()
393 def skip?(%Activity{} = activity, %User{} = user) do
394 [
395 :self,
396 :followers,
397 :follows,
398 :non_followers,
399 :non_follows,
400 :recently_followed
401 ]
402 |> Enum.find(&skip?(&1, activity, user))
403 end
404
405 def skip?(_, _), do: false
406
407 @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
408 def skip?(:self, %Activity{} = activity, %User{} = user) do
409 activity.data["actor"] == user.ap_id
410 end
411
412 def skip?(
413 :followers,
414 %Activity{} = activity,
415 %User{notification_settings: %{followers: false}} = user
416 ) do
417 actor = activity.data["actor"]
418 follower = User.get_cached_by_ap_id(actor)
419 User.following?(follower, user)
420 end
421
422 def skip?(
423 :non_followers,
424 %Activity{} = activity,
425 %User{notification_settings: %{non_followers: false}} = user
426 ) do
427 actor = activity.data["actor"]
428 follower = User.get_cached_by_ap_id(actor)
429 !User.following?(follower, user)
430 end
431
432 def skip?(
433 :follows,
434 %Activity{} = activity,
435 %User{notification_settings: %{follows: false}} = user
436 ) do
437 actor = activity.data["actor"]
438 followed = User.get_cached_by_ap_id(actor)
439 User.following?(user, followed)
440 end
441
442 def skip?(
443 :non_follows,
444 %Activity{} = activity,
445 %User{notification_settings: %{non_follows: false}} = user
446 ) do
447 actor = activity.data["actor"]
448 followed = User.get_cached_by_ap_id(actor)
449 !User.following?(user, followed)
450 end
451
452 # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
453 def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do
454 actor = activity.data["actor"]
455
456 Notification.for_user(user)
457 |> Enum.any?(fn
458 %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
459 _ -> false
460 end)
461 end
462
463 def skip?(_, _, _), do: false
464 end