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