f517282f7c030928513e3ce87c1bd5af59b23938
[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.FollowingRelationship
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 defp for_user_query_ap_id_opts(user, opts) do
44 ap_id_relationships =
45 [:block] ++
46 if opts[@include_muted_option], do: [], else: [:notification_mute]
47
48 preloaded_ap_ids = User.outgoing_relationships_ap_ids(user, ap_id_relationships)
49
50 exclude_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts)
51
52 exclude_notification_muted_opts =
53 Map.merge(%{notification_muted_users_ap_ids: preloaded_ap_ids[:notification_mute]}, opts)
54
55 {exclude_blocked_opts, exclude_notification_muted_opts}
56 end
57
58 def for_user_query(user, opts \\ %{}) do
59 {exclude_blocked_opts, exclude_notification_muted_opts} =
60 for_user_query_ap_id_opts(user, opts)
61
62 Notification
63 |> where(user_id: ^user.id)
64 |> where(
65 [n, a],
66 fragment(
67 "? not in (SELECT ap_id FROM users WHERE deactivated = 'true')",
68 a.actor
69 )
70 )
71 |> join(:inner, [n], activity in assoc(n, :activity))
72 |> join(:left, [n, a], object in Object,
73 on:
74 fragment(
75 "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
76 object.data,
77 a.data
78 )
79 )
80 |> preload([n, a, o], activity: {a, object: o})
81 |> exclude_notification_muted(user, exclude_notification_muted_opts)
82 |> exclude_blocked(user, exclude_blocked_opts)
83 |> exclude_visibility(opts)
84 end
85
86 # Excludes blocked users and non-followed domain-blocked users
87 defp exclude_blocked(query, user, opts) do
88 blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
89
90 query
91 |> where([n, a], a.actor not in ^blocked_ap_ids)
92 |> FollowingRelationship.keep_following_or_not_domain_blocked(user)
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 {notification-enabled 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 @spec get_notified_from_activity(Activity.t(), boolean()) :: {list(User.t()), list(User.t())}
329 def get_notified_from_activity(activity, local_only \\ true)
330
331 def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
332 when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do
333 potential_receiver_ap_ids =
334 []
335 |> Utils.maybe_notify_to_recipients(activity)
336 |> Utils.maybe_notify_mentioned_recipients(activity)
337 |> Utils.maybe_notify_subscribers(activity)
338 |> Utils.maybe_notify_followers(activity)
339 |> Enum.uniq()
340
341 potential_receivers = User.get_users_from_set(potential_receiver_ap_ids, local_only)
342
343 notification_enabled_ap_ids =
344 potential_receiver_ap_ids
345 |> exclude_domain_blocker_ap_ids(activity, potential_receivers)
346 |> exclude_relationship_restricted_ap_ids(activity)
347 |> exclude_thread_muter_ap_ids(activity)
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 who domain-block and not follow activity actor"
358 def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])
359
360 def exclude_domain_blocker_ap_ids([], _activity, _preloaded_users), do: []
361
362 def exclude_domain_blocker_ap_ids(ap_ids, %Activity{} = activity, preloaded_users) do
363 activity_actor_domain = activity.actor && URI.parse(activity.actor).host
364
365 users =
366 ap_ids
367 |> Enum.map(fn ap_id ->
368 Enum.find(preloaded_users, &(&1.ap_id == ap_id)) ||
369 User.get_cached_by_ap_id(ap_id)
370 end)
371 |> Enum.filter(& &1)
372
373 domain_blocker_ap_ids = for u <- users, activity_actor_domain in u.domain_blocks, do: u.ap_id
374
375 domain_blocker_follower_ap_ids =
376 if Enum.any?(domain_blocker_ap_ids) do
377 activity
378 |> Activity.user_actor()
379 |> FollowingRelationship.followers_ap_ids(domain_blocker_ap_ids)
380 else
381 []
382 end
383
384 ap_ids
385 |> Kernel.--(domain_blocker_ap_ids)
386 |> Kernel.++(domain_blocker_follower_ap_ids)
387 end
388
389 @doc "Filters out AP IDs of users basing on their relationships with activity actor user"
390 def exclude_relationship_restricted_ap_ids([], _activity), do: []
391
392 def exclude_relationship_restricted_ap_ids(ap_ids, %Activity{} = activity) do
393 relationship_restricted_ap_ids =
394 activity
395 |> Activity.user_actor()
396 |> User.incoming_relationships_ungrouped_ap_ids([
397 :block,
398 :notification_mute
399 ])
400
401 Enum.uniq(ap_ids) -- relationship_restricted_ap_ids
402 end
403
404 @doc "Filters out AP IDs of users who mute activity thread"
405 def exclude_thread_muter_ap_ids([], _activity), do: []
406
407 def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do
408 thread_muter_ap_ids = ThreadMute.muter_ap_ids(activity.data["context"])
409
410 Enum.uniq(ap_ids) -- thread_muter_ap_ids
411 end
412
413 @spec skip?(Activity.t(), User.t()) :: boolean()
414 def skip?(%Activity{} = activity, %User{} = user) do
415 [
416 :self,
417 :followers,
418 :follows,
419 :non_followers,
420 :non_follows,
421 :recently_followed
422 ]
423 |> Enum.find(&skip?(&1, activity, user))
424 end
425
426 def skip?(_, _), do: false
427
428 @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
429 def skip?(:self, %Activity{} = activity, %User{} = user) do
430 activity.data["actor"] == user.ap_id
431 end
432
433 def skip?(
434 :followers,
435 %Activity{} = activity,
436 %User{notification_settings: %{followers: false}} = user
437 ) do
438 actor = activity.data["actor"]
439 follower = User.get_cached_by_ap_id(actor)
440 User.following?(follower, user)
441 end
442
443 def skip?(
444 :non_followers,
445 %Activity{} = activity,
446 %User{notification_settings: %{non_followers: false}} = user
447 ) do
448 actor = activity.data["actor"]
449 follower = User.get_cached_by_ap_id(actor)
450 !User.following?(follower, user)
451 end
452
453 def skip?(
454 :follows,
455 %Activity{} = activity,
456 %User{notification_settings: %{follows: false}} = user
457 ) do
458 actor = activity.data["actor"]
459 followed = User.get_cached_by_ap_id(actor)
460 User.following?(user, followed)
461 end
462
463 def skip?(
464 :non_follows,
465 %Activity{} = activity,
466 %User{notification_settings: %{non_follows: false}} = user
467 ) do
468 actor = activity.data["actor"]
469 followed = User.get_cached_by_ap_id(actor)
470 !User.following?(user, followed)
471 end
472
473 # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
474 def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do
475 actor = activity.data["actor"]
476
477 Notification.for_user(user)
478 |> Enum.any?(fn
479 %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
480 _ -> false
481 end)
482 end
483
484 def skip?(_, _, _), do: false
485 end