Merge branch 'docs/debian-packages' 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 end
83
84 defp exclude_blocked(query, user, opts) do
85 blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
86
87 query
88 |> where([n, a], a.actor not in ^blocked_ap_ids)
89 |> where(
90 [n, a],
91 fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.domain_blocks
92 )
93 end
94
95 defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do
96 query
97 end
98
99 defp exclude_notification_muted(query, user, opts) do
100 notification_muted_ap_ids =
101 opts[:notification_muted_users_ap_ids] || User.notification_muted_users_ap_ids(user)
102
103 query
104 |> where([n, a], a.actor not in ^notification_muted_ap_ids)
105 |> join(:left, [n, a], tm in ThreadMute,
106 on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
107 )
108 |> where([n, a, o, tm], is_nil(tm.user_id))
109 end
110
111 @valid_visibilities ~w[direct unlisted public private]
112
113 defp exclude_visibility(query, %{exclude_visibilities: visibility})
114 when is_list(visibility) do
115 if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
116 query
117 |> join(:left, [n, a], mutated_activity in Pleroma.Activity,
118 on:
119 fragment("?->>'context'", a.data) ==
120 fragment("?->>'context'", mutated_activity.data) and
121 fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
122 fragment("?->>'type'", mutated_activity.data) == "Create",
123 as: :mutated_activity
124 )
125 |> where(
126 [n, a, mutated_activity: mutated_activity],
127 not fragment(
128 """
129 CASE WHEN (?->>'type') = 'Like' or (?->>'type') = 'Announce'
130 THEN (activity_visibility(?, ?, ?) = ANY (?))
131 ELSE (activity_visibility(?, ?, ?) = ANY (?)) END
132 """,
133 a.data,
134 a.data,
135 mutated_activity.actor,
136 mutated_activity.recipients,
137 mutated_activity.data,
138 ^visibility,
139 a.actor,
140 a.recipients,
141 a.data,
142 ^visibility
143 )
144 )
145 else
146 Logger.error("Could not exclude visibility to #{visibility}")
147 query
148 end
149 end
150
151 defp exclude_visibility(query, %{exclude_visibilities: visibility})
152 when visibility in @valid_visibilities do
153 exclude_visibility(query, [visibility])
154 end
155
156 defp exclude_visibility(query, %{exclude_visibilities: visibility})
157 when visibility not in @valid_visibilities do
158 Logger.error("Could not exclude visibility to #{visibility}")
159 query
160 end
161
162 defp exclude_visibility(query, _visibility), do: query
163
164 def for_user(user, opts \\ %{}) do
165 user
166 |> for_user_query(opts)
167 |> Pagination.fetch_paginated(opts)
168 end
169
170 @doc """
171 Returns notifications for user received since given date.
172
173 ## Examples
174
175 iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-13 11:22:33])
176 [%Pleroma.Notification{}, %Pleroma.Notification{}]
177
178 iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-15 11:22:33])
179 []
180 """
181 @spec for_user_since(Pleroma.User.t(), NaiveDateTime.t()) :: [t()]
182 def for_user_since(user, date) do
183 from(n in for_user_query(user),
184 where: n.updated_at > ^date
185 )
186 |> Repo.all()
187 end
188
189 def set_read_up_to(%{id: user_id} = _user, id) do
190 query =
191 from(
192 n in Notification,
193 where: n.user_id == ^user_id,
194 where: n.id <= ^id,
195 where: n.seen == false,
196 update: [
197 set: [
198 seen: true,
199 updated_at: ^NaiveDateTime.utc_now()
200 ]
201 ],
202 # Ideally we would preload object and activities here
203 # but Ecto does not support preloads in update_all
204 select: n.id
205 )
206
207 {_, notification_ids} = Repo.update_all(query, [])
208
209 Notification
210 |> where([n], n.id in ^notification_ids)
211 |> join(:inner, [n], activity in assoc(n, :activity))
212 |> join(:left, [n, a], object in Object,
213 on:
214 fragment(
215 "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
216 object.data,
217 a.data
218 )
219 )
220 |> preload([n, a, o], activity: {a, object: o})
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(%{id: user_id} = _user, id) do
266 notification = Repo.get(Notification, id)
267
268 case notification do
269 %{user_id: ^user_id} ->
270 Repo.delete(notification)
271
272 _ ->
273 {:error, "Cannot dismiss notification"}
274 end
275 end
276
277 def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
278 object = Object.normalize(activity)
279
280 if object && object.data["type"] == "Answer" do
281 {:ok, []}
282 else
283 do_create_notifications(activity)
284 end
285 end
286
287 def create_notifications(%Activity{data: %{"type" => "Follow"}} = activity) do
288 if Pleroma.Config.get([:notifications, :enable_follow_request_notifications]) ||
289 Activity.follow_accepted?(activity) do
290 do_create_notifications(activity)
291 else
292 {:ok, []}
293 end
294 end
295
296 def create_notifications(%Activity{data: %{"type" => type}} = activity)
297 when type in ["Like", "Announce", "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