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