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