94dc0c2b0201e79a3d71c26c4f049159739963c4
[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" => "Follow"}} = activity) do
297 if Pleroma.Config.get([:notifications, :enable_follow_request_notifications]) ||
298 Activity.follow_accepted?(activity) do
299 do_create_notifications(activity)
300 else
301 {:ok, []}
302 end
303 end
304
305 def create_notifications(%Activity{data: %{"type" => type}} = activity)
306 when type in ["Like", "Announce", "Move", "EmojiReact"] do
307 do_create_notifications(activity)
308 end
309
310 def create_notifications(_), do: {:ok, []}
311
312 defp do_create_notifications(%Activity{} = activity) do
313 {enabled_receivers, disabled_receivers} = get_notified_from_activity(activity)
314 potential_receivers = enabled_receivers ++ disabled_receivers
315
316 notifications =
317 Enum.map(potential_receivers, fn user ->
318 do_send = user in enabled_receivers
319 create_notification(activity, user, do_send)
320 end)
321
322 {:ok, notifications}
323 end
324
325 # TODO move to sql, too.
326 def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do
327 unless skip?(activity, user) do
328 notification = %Notification{user_id: user.id, activity: activity}
329 {:ok, notification} = Repo.insert(notification)
330
331 if do_send do
332 Streamer.stream(["user", "user:notification"], notification)
333 Push.send(notification)
334 end
335
336 notification
337 end
338 end
339
340 @doc """
341 Returns a tuple with 2 elements:
342 {enabled notification receivers, currently disabled receivers (blocking / [thread] muting)}
343
344 NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1
345 """
346 def get_notified_from_activity(activity, local_only \\ true)
347
348 def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
349 when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do
350 potential_receiver_ap_ids =
351 []
352 |> Utils.maybe_notify_to_recipients(activity)
353 |> Utils.maybe_notify_mentioned_recipients(activity)
354 |> Utils.maybe_notify_subscribers(activity)
355 |> Utils.maybe_notify_followers(activity)
356 |> Enum.uniq()
357
358 # Since even subscribers and followers can mute / thread-mute, filtering all above AP IDs
359 notification_enabled_ap_ids =
360 potential_receiver_ap_ids
361 |> exclude_relationship_restricted_ap_ids(activity)
362 |> exclude_thread_muter_ap_ids(activity)
363
364 potential_receivers =
365 potential_receiver_ap_ids
366 |> Enum.uniq()
367 |> User.get_users_from_set(local_only)
368
369 notification_enabled_users =
370 Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
371
372 {notification_enabled_users, potential_receivers -- notification_enabled_users}
373 end
374
375 def get_notified_from_activity(_, _local_only), do: {[], []}
376
377 @doc "Filters out AP IDs of users basing on their relationships with activity actor user"
378 def exclude_relationship_restricted_ap_ids([], _activity), do: []
379
380 def exclude_relationship_restricted_ap_ids(ap_ids, %Activity{} = activity) do
381 relationship_restricted_ap_ids =
382 activity
383 |> Activity.user_actor()
384 |> User.incoming_relationships_ungrouped_ap_ids([
385 :block,
386 :notification_mute
387 ])
388
389 Enum.uniq(ap_ids) -- relationship_restricted_ap_ids
390 end
391
392 @doc "Filters out AP IDs of users who mute activity thread"
393 def exclude_thread_muter_ap_ids([], _activity), do: []
394
395 def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do
396 thread_muter_ap_ids = ThreadMute.muter_ap_ids(activity.data["context"])
397
398 Enum.uniq(ap_ids) -- thread_muter_ap_ids
399 end
400
401 @spec skip?(Activity.t(), User.t()) :: boolean()
402 def skip?(%Activity{} = activity, %User{} = user) do
403 [
404 :self,
405 :followers,
406 :follows,
407 :non_followers,
408 :non_follows,
409 :recently_followed
410 ]
411 |> Enum.find(&skip?(&1, activity, user))
412 end
413
414 def skip?(_, _), do: false
415
416 @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
417 def skip?(:self, %Activity{} = activity, %User{} = user) do
418 activity.data["actor"] == user.ap_id
419 end
420
421 def skip?(
422 :followers,
423 %Activity{} = activity,
424 %User{notification_settings: %{followers: false}} = user
425 ) do
426 actor = activity.data["actor"]
427 follower = User.get_cached_by_ap_id(actor)
428 User.following?(follower, user)
429 end
430
431 def skip?(
432 :non_followers,
433 %Activity{} = activity,
434 %User{notification_settings: %{non_followers: false}} = user
435 ) do
436 actor = activity.data["actor"]
437 follower = User.get_cached_by_ap_id(actor)
438 !User.following?(follower, user)
439 end
440
441 def skip?(
442 :follows,
443 %Activity{} = activity,
444 %User{notification_settings: %{follows: false}} = user
445 ) do
446 actor = activity.data["actor"]
447 followed = User.get_cached_by_ap_id(actor)
448 User.following?(user, followed)
449 end
450
451 def skip?(
452 :non_follows,
453 %Activity{} = activity,
454 %User{notification_settings: %{non_follows: false}} = user
455 ) do
456 actor = activity.data["actor"]
457 followed = User.get_cached_by_ap_id(actor)
458 !User.following?(user, followed)
459 end
460
461 # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
462 def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do
463 actor = activity.data["actor"]
464
465 Notification.for_user(user)
466 |> Enum.any?(fn
467 %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
468 _ -> false
469 end)
470 end
471
472 def skip?(_, _, _), do: false
473 end