Merge branch 'issue/1276' 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 Ecto.Multi
9 alias Pleroma.Activity
10 alias Pleroma.Marker
11 alias Pleroma.Notification
12 alias Pleroma.Object
13 alias Pleroma.Pagination
14 alias Pleroma.Repo
15 alias Pleroma.ThreadMute
16 alias Pleroma.User
17 alias Pleroma.Web.CommonAPI.Utils
18 alias Pleroma.Web.Push
19 alias Pleroma.Web.Streamer
20
21 import Ecto.Query
22 import Ecto.Changeset
23
24 require Logger
25
26 @type t :: %__MODULE__{}
27
28 @include_muted_option :with_muted
29
30 schema "notifications" do
31 field(:seen, :boolean, default: false)
32 belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
33 belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
34
35 timestamps()
36 end
37
38 def changeset(%Notification{} = notification, attrs) do
39 notification
40 |> cast(attrs, [:seen])
41 end
42
43 @spec last_read_query(User.t()) :: Ecto.Queryable.t()
44 def last_read_query(user) do
45 from(q in Pleroma.Notification,
46 where: q.user_id == ^user.id,
47 where: q.seen == true,
48 select: type(q.id, :string),
49 limit: 1,
50 order_by: [desc: :id]
51 )
52 end
53
54 defp for_user_query_ap_id_opts(user, opts) do
55 ap_id_relationships =
56 [:block] ++
57 if opts[@include_muted_option], do: [], else: [:notification_mute]
58
59 preloaded_ap_ids = User.outgoing_relationships_ap_ids(user, ap_id_relationships)
60
61 exclude_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts)
62
63 exclude_notification_muted_opts =
64 Map.merge(%{notification_muted_users_ap_ids: preloaded_ap_ids[:notification_mute]}, opts)
65
66 {exclude_blocked_opts, exclude_notification_muted_opts}
67 end
68
69 def for_user_query(user, opts \\ %{}) do
70 {exclude_blocked_opts, exclude_notification_muted_opts} =
71 for_user_query_ap_id_opts(user, opts)
72
73 Notification
74 |> where(user_id: ^user.id)
75 |> where(
76 [n, a],
77 fragment(
78 "? not in (SELECT ap_id FROM users WHERE deactivated = 'true')",
79 a.actor
80 )
81 )
82 |> join(:inner, [n], activity in assoc(n, :activity))
83 |> join(:left, [n, a], object in Object,
84 on:
85 fragment(
86 "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
87 object.data,
88 a.data
89 )
90 )
91 |> preload([n, a, o], activity: {a, object: o})
92 |> exclude_notification_muted(user, exclude_notification_muted_opts)
93 |> exclude_blocked(user, exclude_blocked_opts)
94 |> exclude_visibility(opts)
95 end
96
97 defp exclude_blocked(query, user, opts) do
98 blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
99
100 query
101 |> where([n, a], a.actor not in ^blocked_ap_ids)
102 |> where(
103 [n, a],
104 fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.domain_blocks
105 )
106 end
107
108 defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do
109 query
110 end
111
112 defp exclude_notification_muted(query, user, opts) do
113 notification_muted_ap_ids =
114 opts[:notification_muted_users_ap_ids] || User.notification_muted_users_ap_ids(user)
115
116 query
117 |> where([n, a], a.actor not in ^notification_muted_ap_ids)
118 |> join(:left, [n, a], tm in ThreadMute,
119 on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
120 )
121 |> where([n, a, o, tm], is_nil(tm.user_id))
122 end
123
124 @valid_visibilities ~w[direct unlisted public private]
125
126 defp exclude_visibility(query, %{exclude_visibilities: visibility})
127 when is_list(visibility) do
128 if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
129 query
130 |> join(:left, [n, a], mutated_activity in Pleroma.Activity,
131 on:
132 fragment("?->>'context'", a.data) ==
133 fragment("?->>'context'", mutated_activity.data) and
134 fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
135 fragment("?->>'type'", mutated_activity.data) == "Create",
136 as: :mutated_activity
137 )
138 |> where(
139 [n, a, mutated_activity: mutated_activity],
140 not fragment(
141 """
142 CASE WHEN (?->>'type') = 'Like' or (?->>'type') = 'Announce'
143 THEN (activity_visibility(?, ?, ?) = ANY (?))
144 ELSE (activity_visibility(?, ?, ?) = ANY (?)) END
145 """,
146 a.data,
147 a.data,
148 mutated_activity.actor,
149 mutated_activity.recipients,
150 mutated_activity.data,
151 ^visibility,
152 a.actor,
153 a.recipients,
154 a.data,
155 ^visibility
156 )
157 )
158 else
159 Logger.error("Could not exclude visibility to #{visibility}")
160 query
161 end
162 end
163
164 defp exclude_visibility(query, %{exclude_visibilities: visibility})
165 when visibility in @valid_visibilities do
166 exclude_visibility(query, [visibility])
167 end
168
169 defp exclude_visibility(query, %{exclude_visibilities: visibility})
170 when visibility not in @valid_visibilities do
171 Logger.error("Could not exclude visibility to #{visibility}")
172 query
173 end
174
175 defp exclude_visibility(query, _visibility), do: query
176
177 def for_user(user, opts \\ %{}) do
178 user
179 |> for_user_query(opts)
180 |> Pagination.fetch_paginated(opts)
181 end
182
183 @doc """
184 Returns notifications for user received since given date.
185
186 ## Examples
187
188 iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-13 11:22:33])
189 [%Pleroma.Notification{}, %Pleroma.Notification{}]
190
191 iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-15 11:22:33])
192 []
193 """
194 @spec for_user_since(Pleroma.User.t(), NaiveDateTime.t()) :: [t()]
195 def for_user_since(user, date) do
196 from(n in for_user_query(user),
197 where: n.updated_at > ^date
198 )
199 |> Repo.all()
200 end
201
202 def set_read_up_to(%{id: user_id} = user, id) do
203 query =
204 from(
205 n in Notification,
206 where: n.user_id == ^user_id,
207 where: n.id <= ^id,
208 where: n.seen == false,
209 # Ideally we would preload object and activities here
210 # but Ecto does not support preloads in update_all
211 select: n.id
212 )
213
214 {:ok, %{ids: {_, notification_ids}}} =
215 Multi.new()
216 |> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
217 |> Marker.multi_set_last_read_id(user, "notifications")
218 |> Repo.transaction()
219
220 Notification
221 |> where([n], n.id in ^notification_ids)
222 |> join(:inner, [n], activity in assoc(n, :activity))
223 |> join(:left, [n, a], object in Object,
224 on:
225 fragment(
226 "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
227 object.data,
228 a.data
229 )
230 )
231 |> preload([n, a, o], activity: {a, object: o})
232 |> Repo.all()
233 end
234
235 @spec read_one(User.t(), String.t()) ::
236 {:ok, Notification.t()} | {:error, Ecto.Changeset.t()} | nil
237 def read_one(%User{} = user, notification_id) do
238 with {:ok, %Notification{} = notification} <- get(user, notification_id) do
239 Multi.new()
240 |> Multi.update(:update, changeset(notification, %{seen: true}))
241 |> Marker.multi_set_last_read_id(user, "notifications")
242 |> Repo.transaction()
243 |> case do
244 {:ok, %{update: notification}} -> {:ok, notification}
245 {:error, :update, changeset, _} -> {:error, changeset}
246 end
247 end
248 end
249
250 def get(%{id: user_id} = _user, id) do
251 query =
252 from(
253 n in Notification,
254 where: n.id == ^id,
255 join: activity in assoc(n, :activity),
256 preload: [activity: activity]
257 )
258
259 notification = Repo.one(query)
260
261 case notification do
262 %{user_id: ^user_id} ->
263 {:ok, notification}
264
265 _ ->
266 {:error, "Cannot get notification"}
267 end
268 end
269
270 def clear(user) do
271 from(n in Notification, where: n.user_id == ^user.id)
272 |> Repo.delete_all()
273 end
274
275 def destroy_multiple(%{id: user_id} = _user, ids) do
276 from(n in Notification,
277 where: n.id in ^ids,
278 where: n.user_id == ^user_id
279 )
280 |> Repo.delete_all()
281 end
282
283 def dismiss(%{id: user_id} = _user, id) do
284 notification = Repo.get(Notification, id)
285
286 case notification do
287 %{user_id: ^user_id} ->
288 Repo.delete(notification)
289
290 _ ->
291 {:error, "Cannot dismiss notification"}
292 end
293 end
294
295 def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
296 object = Object.normalize(activity)
297
298 if object && object.data["type"] == "Answer" do
299 {:ok, []}
300 else
301 do_create_notifications(activity)
302 end
303 end
304
305 def create_notifications(%Activity{data: %{"type" => type}} = activity)
306 when type in ["Like", "Announce", "Follow", "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 {:ok, %{notification: notification}} =
329 Multi.new()
330 |> Multi.insert(:notification, %Notification{user_id: user.id, activity: activity})
331 |> Marker.multi_set_last_read_id(user, "notifications")
332 |> Repo.transaction()
333
334 if do_send do
335 Streamer.stream(["user", "user:notification"], notification)
336 Push.send(notification)
337 end
338
339 notification
340 end
341 end
342
343 @doc """
344 Returns a tuple with 2 elements:
345 {enabled notification receivers, currently disabled receivers (blocking / [thread] muting)}
346
347 NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1
348 """
349 def get_notified_from_activity(activity, local_only \\ true)
350
351 def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
352 when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do
353 potential_receiver_ap_ids =
354 []
355 |> Utils.maybe_notify_to_recipients(activity)
356 |> Utils.maybe_notify_mentioned_recipients(activity)
357 |> Utils.maybe_notify_subscribers(activity)
358 |> Utils.maybe_notify_followers(activity)
359 |> Enum.uniq()
360
361 # Since even subscribers and followers can mute / thread-mute, filtering all above AP IDs
362 notification_enabled_ap_ids =
363 potential_receiver_ap_ids
364 |> exclude_relationship_restricted_ap_ids(activity)
365 |> exclude_thread_muter_ap_ids(activity)
366
367 potential_receivers =
368 potential_receiver_ap_ids
369 |> Enum.uniq()
370 |> User.get_users_from_set(local_only)
371
372 notification_enabled_users =
373 Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
374
375 {notification_enabled_users, potential_receivers -- notification_enabled_users}
376 end
377
378 def get_notified_from_activity(_, _local_only), do: {[], []}
379
380 @doc "Filters out AP IDs of users basing on their relationships with activity actor user"
381 def exclude_relationship_restricted_ap_ids([], _activity), do: []
382
383 def exclude_relationship_restricted_ap_ids(ap_ids, %Activity{} = activity) do
384 relationship_restricted_ap_ids =
385 activity
386 |> Activity.user_actor()
387 |> User.incoming_relationships_ungrouped_ap_ids([
388 :block,
389 :notification_mute
390 ])
391
392 Enum.uniq(ap_ids) -- relationship_restricted_ap_ids
393 end
394
395 @doc "Filters out AP IDs of users who mute activity thread"
396 def exclude_thread_muter_ap_ids([], _activity), do: []
397
398 def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do
399 thread_muter_ap_ids = ThreadMute.muter_ap_ids(activity.data["context"])
400
401 Enum.uniq(ap_ids) -- thread_muter_ap_ids
402 end
403
404 @spec skip?(Activity.t(), User.t()) :: boolean()
405 def skip?(%Activity{} = activity, %User{} = user) do
406 [
407 :self,
408 :followers,
409 :follows,
410 :non_followers,
411 :non_follows,
412 :recently_followed
413 ]
414 |> Enum.find(&skip?(&1, activity, user))
415 end
416
417 def skip?(_, _), do: false
418
419 @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
420 def skip?(:self, %Activity{} = activity, %User{} = user) do
421 activity.data["actor"] == user.ap_id
422 end
423
424 def skip?(
425 :followers,
426 %Activity{} = activity,
427 %User{notification_settings: %{followers: false}} = user
428 ) do
429 actor = activity.data["actor"]
430 follower = User.get_cached_by_ap_id(actor)
431 User.following?(follower, user)
432 end
433
434 def skip?(
435 :non_followers,
436 %Activity{} = activity,
437 %User{notification_settings: %{non_followers: false}} = user
438 ) do
439 actor = activity.data["actor"]
440 follower = User.get_cached_by_ap_id(actor)
441 !User.following?(follower, user)
442 end
443
444 def skip?(
445 :follows,
446 %Activity{} = activity,
447 %User{notification_settings: %{follows: false}} = user
448 ) do
449 actor = activity.data["actor"]
450 followed = User.get_cached_by_ap_id(actor)
451 User.following?(user, followed)
452 end
453
454 def skip?(
455 :non_follows,
456 %Activity{} = activity,
457 %User{notification_settings: %{non_follows: false}} = user
458 ) do
459 actor = activity.data["actor"]
460 followed = User.get_cached_by_ap_id(actor)
461 !User.following?(user, followed)
462 end
463
464 # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
465 def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do
466 actor = activity.data["actor"]
467
468 Notification.for_user(user)
469 |> Enum.any?(fn
470 %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
471 _ -> false
472 end)
473 end
474
475 def skip?(_, _, _), do: false
476 end