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