Merge branch 'develop' into fix/csp-mediaproxy-base-url
[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.FollowingRelationship
11 alias Pleroma.Marker
12 alias Pleroma.Notification
13 alias Pleroma.Object
14 alias Pleroma.Pagination
15 alias Pleroma.Repo
16 alias Pleroma.ThreadMute
17 alias Pleroma.User
18 alias Pleroma.Web.CommonAPI.Utils
19 alias Pleroma.Web.Push
20 alias Pleroma.Web.Streamer
21
22 import Ecto.Query
23 import Ecto.Changeset
24
25 require Logger
26
27 @type t :: %__MODULE__{}
28
29 @include_muted_option :with_muted
30
31 schema "notifications" do
32 field(:seen, :boolean, default: false)
33 # This is an enum type in the database. If you add a new notification type,
34 # remember to add a migration to add it to the `notifications_type` enum
35 # as well.
36 field(:type, :string)
37 belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
38 belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
39
40 timestamps()
41 end
42
43 def update_notification_type(user, activity) do
44 with %__MODULE__{} = notification <-
45 Repo.get_by(__MODULE__, user_id: user.id, activity_id: activity.id) do
46 type =
47 activity
48 |> type_from_activity()
49
50 notification
51 |> changeset(%{type: type})
52 |> Repo.update()
53 end
54 end
55
56 @spec unread_notifications_count(User.t()) :: integer()
57 def unread_notifications_count(%User{id: user_id}) do
58 from(q in __MODULE__,
59 where: q.user_id == ^user_id and q.seen == false
60 )
61 |> Repo.aggregate(:count, :id)
62 end
63
64 @notification_types ~w{
65 favourite
66 follow
67 follow_request
68 mention
69 move
70 pleroma:chat_mention
71 pleroma:emoji_reaction
72 reblog
73 }
74
75 def changeset(%Notification{} = notification, attrs) do
76 notification
77 |> cast(attrs, [:seen, :type])
78 |> validate_inclusion(:type, @notification_types)
79 end
80
81 @spec last_read_query(User.t()) :: Ecto.Queryable.t()
82 def last_read_query(user) do
83 from(q in Pleroma.Notification,
84 where: q.user_id == ^user.id,
85 where: q.seen == true,
86 select: type(q.id, :string),
87 limit: 1,
88 order_by: [desc: :id]
89 )
90 end
91
92 defp for_user_query_ap_id_opts(user, opts) do
93 ap_id_relationships =
94 [:block] ++
95 if opts[@include_muted_option], do: [], else: [:notification_mute]
96
97 preloaded_ap_ids = User.outgoing_relationships_ap_ids(user, ap_id_relationships)
98
99 exclude_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts)
100
101 exclude_notification_muted_opts =
102 Map.merge(%{notification_muted_users_ap_ids: preloaded_ap_ids[:notification_mute]}, opts)
103
104 {exclude_blocked_opts, exclude_notification_muted_opts}
105 end
106
107 def for_user_query(user, opts \\ %{}) do
108 {exclude_blocked_opts, exclude_notification_muted_opts} =
109 for_user_query_ap_id_opts(user, opts)
110
111 Notification
112 |> where(user_id: ^user.id)
113 |> where(
114 [n, a],
115 fragment(
116 "? not in (SELECT ap_id FROM users WHERE deactivated = 'true')",
117 a.actor
118 )
119 )
120 |> join(:inner, [n], activity in assoc(n, :activity))
121 |> join(:left, [n, a], object in Object,
122 on:
123 fragment(
124 "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
125 object.data,
126 a.data,
127 a.data
128 )
129 )
130 |> preload([n, a, o], activity: {a, object: o})
131 |> exclude_notification_muted(user, exclude_notification_muted_opts)
132 |> exclude_blocked(user, exclude_blocked_opts)
133 |> exclude_visibility(opts)
134 end
135
136 # Excludes blocked users and non-followed domain-blocked users
137 defp exclude_blocked(query, user, opts) do
138 blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
139
140 query
141 |> where([n, a], a.actor not in ^blocked_ap_ids)
142 |> FollowingRelationship.keep_following_or_not_domain_blocked(user)
143 end
144
145 defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do
146 query
147 end
148
149 defp exclude_notification_muted(query, user, opts) do
150 notification_muted_ap_ids =
151 opts[:notification_muted_users_ap_ids] || User.notification_muted_users_ap_ids(user)
152
153 query
154 |> where([n, a], a.actor not in ^notification_muted_ap_ids)
155 |> join(:left, [n, a], tm in ThreadMute,
156 on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
157 )
158 |> where([n, a, o, tm], is_nil(tm.user_id))
159 end
160
161 @valid_visibilities ~w[direct unlisted public private]
162
163 defp exclude_visibility(query, %{exclude_visibilities: visibility})
164 when is_list(visibility) do
165 if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
166 query
167 |> join(:left, [n, a], mutated_activity in Pleroma.Activity,
168 on:
169 fragment("?->>'context'", a.data) ==
170 fragment("?->>'context'", mutated_activity.data) and
171 fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
172 fragment("?->>'type'", mutated_activity.data) == "Create",
173 as: :mutated_activity
174 )
175 |> where(
176 [n, a, mutated_activity: mutated_activity],
177 not fragment(
178 """
179 CASE WHEN (?->>'type') = 'Like' or (?->>'type') = 'Announce'
180 THEN (activity_visibility(?, ?, ?) = ANY (?))
181 ELSE (activity_visibility(?, ?, ?) = ANY (?)) END
182 """,
183 a.data,
184 a.data,
185 mutated_activity.actor,
186 mutated_activity.recipients,
187 mutated_activity.data,
188 ^visibility,
189 a.actor,
190 a.recipients,
191 a.data,
192 ^visibility
193 )
194 )
195 else
196 Logger.error("Could not exclude visibility to #{visibility}")
197 query
198 end
199 end
200
201 defp exclude_visibility(query, %{exclude_visibilities: visibility})
202 when visibility in @valid_visibilities do
203 exclude_visibility(query, [visibility])
204 end
205
206 defp exclude_visibility(query, %{exclude_visibilities: visibility})
207 when visibility not in @valid_visibilities do
208 Logger.error("Could not exclude visibility to #{visibility}")
209 query
210 end
211
212 defp exclude_visibility(query, _visibility), do: query
213
214 def for_user(user, opts \\ %{}) do
215 user
216 |> for_user_query(opts)
217 |> Pagination.fetch_paginated(opts)
218 end
219
220 @doc """
221 Returns notifications for user received since given date.
222
223 ## Examples
224
225 iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-13 11:22:33])
226 [%Pleroma.Notification{}, %Pleroma.Notification{}]
227
228 iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-15 11:22:33])
229 []
230 """
231 @spec for_user_since(Pleroma.User.t(), NaiveDateTime.t()) :: [t()]
232 def for_user_since(user, date) do
233 from(n in for_user_query(user),
234 where: n.updated_at > ^date
235 )
236 |> Repo.all()
237 end
238
239 def set_read_up_to(%{id: user_id} = user, id) do
240 query =
241 from(
242 n in Notification,
243 where: n.user_id == ^user_id,
244 where: n.id <= ^id,
245 where: n.seen == false,
246 # Ideally we would preload object and activities here
247 # but Ecto does not support preloads in update_all
248 select: n.id
249 )
250
251 {:ok, %{ids: {_, notification_ids}}} =
252 Multi.new()
253 |> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
254 |> Marker.multi_set_last_read_id(user, "notifications")
255 |> Repo.transaction()
256
257 for_user_query(user)
258 |> where([n], n.id in ^notification_ids)
259 |> Repo.all()
260 end
261
262 @spec read_one(User.t(), String.t()) ::
263 {:ok, Notification.t()} | {:error, Ecto.Changeset.t()} | nil
264 def read_one(%User{} = user, notification_id) do
265 with {:ok, %Notification{} = notification} <- get(user, notification_id) do
266 Multi.new()
267 |> Multi.update(:update, changeset(notification, %{seen: true}))
268 |> Marker.multi_set_last_read_id(user, "notifications")
269 |> Repo.transaction()
270 |> case do
271 {:ok, %{update: notification}} -> {:ok, notification}
272 {:error, :update, changeset, _} -> {:error, changeset}
273 end
274 end
275 end
276
277 def get(%{id: user_id} = _user, id) do
278 query =
279 from(
280 n in Notification,
281 where: n.id == ^id,
282 join: activity in assoc(n, :activity),
283 preload: [activity: activity]
284 )
285
286 notification = Repo.one(query)
287
288 case notification do
289 %{user_id: ^user_id} ->
290 {:ok, notification}
291
292 _ ->
293 {:error, "Cannot get notification"}
294 end
295 end
296
297 def clear(user) do
298 from(n in Notification, where: n.user_id == ^user.id)
299 |> Repo.delete_all()
300 end
301
302 def destroy_multiple(%{id: user_id} = _user, ids) do
303 from(n in Notification,
304 where: n.id in ^ids,
305 where: n.user_id == ^user_id
306 )
307 |> Repo.delete_all()
308 end
309
310 def dismiss(%Pleroma.Activity{} = activity) do
311 Notification
312 |> where([n], n.activity_id == ^activity.id)
313 |> Repo.delete_all()
314 |> case do
315 {_, notifications} -> {:ok, notifications}
316 _ -> {:error, "Cannot dismiss notification"}
317 end
318 end
319
320 def dismiss(%{id: user_id} = _user, id) do
321 notification = Repo.get(Notification, id)
322
323 case notification do
324 %{user_id: ^user_id} ->
325 Repo.delete(notification)
326
327 _ ->
328 {:error, "Cannot dismiss notification"}
329 end
330 end
331
332 def create_notifications(activity, options \\ [])
333
334 def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do
335 object = Object.normalize(activity, false)
336
337 if object && object.data["type"] == "Answer" do
338 {:ok, []}
339 else
340 do_create_notifications(activity, options)
341 end
342 end
343
344 def create_notifications(%Activity{data: %{"type" => type}} = activity, options)
345 when type in ["Follow", "Like", "Announce", "Move", "EmojiReact"] do
346 do_create_notifications(activity, options)
347 end
348
349 def create_notifications(_, _), do: {:ok, []}
350
351 defp do_create_notifications(%Activity{} = activity, options) do
352 do_send = Keyword.get(options, :do_send, true)
353
354 {enabled_receivers, disabled_receivers} = get_notified_from_activity(activity)
355 potential_receivers = enabled_receivers ++ disabled_receivers
356
357 notifications =
358 Enum.map(potential_receivers, fn user ->
359 do_send = do_send && user in enabled_receivers
360 create_notification(activity, user, do_send)
361 end)
362
363 {:ok, notifications}
364 end
365
366 defp type_from_activity(%{data: %{"type" => type}} = activity) do
367 case type do
368 "Follow" ->
369 if Activity.follow_accepted?(activity) do
370 "follow"
371 else
372 "follow_request"
373 end
374
375 "Announce" ->
376 "reblog"
377
378 "Like" ->
379 "favourite"
380
381 "Move" ->
382 "move"
383
384 "EmojiReact" ->
385 "pleroma:emoji_reaction"
386
387 # Compatibility with old reactions
388 "EmojiReaction" ->
389 "pleroma:emoji_reaction"
390
391 "Create" ->
392 activity
393 |> type_from_activity_object()
394
395 t ->
396 raise "No notification type for activity type #{t}"
397 end
398 end
399
400 defp type_from_activity_object(%{data: %{"type" => "Create", "object" => %{}}}), do: "mention"
401
402 defp type_from_activity_object(%{data: %{"type" => "Create"}} = activity) do
403 object = Object.get_by_ap_id(activity.data["object"])
404
405 case object && object.data["type"] do
406 "ChatMessage" -> "pleroma:chat_mention"
407 _ -> "mention"
408 end
409 end
410
411 # TODO move to sql, too.
412 def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do
413 unless skip?(activity, user) do
414 {:ok, %{notification: notification}} =
415 Multi.new()
416 |> Multi.insert(:notification, %Notification{
417 user_id: user.id,
418 activity: activity,
419 type: type_from_activity(activity)
420 })
421 |> Marker.multi_set_last_read_id(user, "notifications")
422 |> Repo.transaction()
423
424 if do_send do
425 Streamer.stream(["user", "user:notification"], notification)
426 Push.send(notification)
427 end
428
429 notification
430 end
431 end
432
433 @doc """
434 Returns a tuple with 2 elements:
435 {notification-enabled receivers, currently disabled receivers (blocking / [thread] muting)}
436
437 NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1
438 """
439 @spec get_notified_from_activity(Activity.t(), boolean()) :: {list(User.t()), list(User.t())}
440 def get_notified_from_activity(activity, local_only \\ true)
441
442 def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
443 when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do
444 potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
445
446 potential_receivers =
447 User.get_users_from_set(potential_receiver_ap_ids, local_only: local_only)
448
449 notification_enabled_ap_ids =
450 potential_receiver_ap_ids
451 |> exclude_domain_blocker_ap_ids(activity, potential_receivers)
452 |> exclude_relationship_restricted_ap_ids(activity)
453 |> exclude_thread_muter_ap_ids(activity)
454
455 notification_enabled_users =
456 Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
457
458 {notification_enabled_users, potential_receivers -- notification_enabled_users}
459 end
460
461 def get_notified_from_activity(_, _local_only), do: {[], []}
462
463 # For some activities, only notify the author of the object
464 def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_id}})
465 when type in ~w{Like Announce EmojiReact} do
466 case Object.get_cached_by_ap_id(object_id) do
467 %Object{data: %{"actor" => actor}} ->
468 [actor]
469
470 _ ->
471 []
472 end
473 end
474
475 def get_potential_receiver_ap_ids(activity) do
476 []
477 |> Utils.maybe_notify_to_recipients(activity)
478 |> Utils.maybe_notify_mentioned_recipients(activity)
479 |> Utils.maybe_notify_subscribers(activity)
480 |> Utils.maybe_notify_followers(activity)
481 |> Enum.uniq()
482 end
483
484 @doc "Filters out AP IDs domain-blocking and not following the activity's actor"
485 def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])
486
487 def exclude_domain_blocker_ap_ids([], _activity, _preloaded_users), do: []
488
489 def exclude_domain_blocker_ap_ids(ap_ids, %Activity{} = activity, preloaded_users) do
490 activity_actor_domain = activity.actor && URI.parse(activity.actor).host
491
492 users =
493 ap_ids
494 |> Enum.map(fn ap_id ->
495 Enum.find(preloaded_users, &(&1.ap_id == ap_id)) ||
496 User.get_cached_by_ap_id(ap_id)
497 end)
498 |> Enum.filter(& &1)
499
500 domain_blocker_ap_ids = for u <- users, activity_actor_domain in u.domain_blocks, do: u.ap_id
501
502 domain_blocker_follower_ap_ids =
503 if Enum.any?(domain_blocker_ap_ids) do
504 activity
505 |> Activity.user_actor()
506 |> FollowingRelationship.followers_ap_ids(domain_blocker_ap_ids)
507 else
508 []
509 end
510
511 ap_ids
512 |> Kernel.--(domain_blocker_ap_ids)
513 |> Kernel.++(domain_blocker_follower_ap_ids)
514 end
515
516 @doc "Filters out AP IDs of users basing on their relationships with activity actor user"
517 def exclude_relationship_restricted_ap_ids([], _activity), do: []
518
519 def exclude_relationship_restricted_ap_ids(ap_ids, %Activity{} = activity) do
520 relationship_restricted_ap_ids =
521 activity
522 |> Activity.user_actor()
523 |> User.incoming_relationships_ungrouped_ap_ids([
524 :block,
525 :notification_mute
526 ])
527
528 Enum.uniq(ap_ids) -- relationship_restricted_ap_ids
529 end
530
531 @doc "Filters out AP IDs of users who mute activity thread"
532 def exclude_thread_muter_ap_ids([], _activity), do: []
533
534 def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do
535 thread_muter_ap_ids = ThreadMute.muter_ap_ids(activity.data["context"])
536
537 Enum.uniq(ap_ids) -- thread_muter_ap_ids
538 end
539
540 @spec skip?(Activity.t(), User.t()) :: boolean()
541 def skip?(%Activity{} = activity, %User{} = user) do
542 [
543 :self,
544 :followers,
545 :follows,
546 :non_followers,
547 :non_follows,
548 :recently_followed
549 ]
550 |> Enum.find(&skip?(&1, activity, user))
551 end
552
553 def skip?(_, _), do: false
554
555 @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
556 def skip?(:self, %Activity{} = activity, %User{} = user) do
557 activity.data["actor"] == user.ap_id
558 end
559
560 def skip?(
561 :followers,
562 %Activity{} = activity,
563 %User{notification_settings: %{followers: false}} = user
564 ) do
565 actor = activity.data["actor"]
566 follower = User.get_cached_by_ap_id(actor)
567 User.following?(follower, user)
568 end
569
570 def skip?(
571 :non_followers,
572 %Activity{} = activity,
573 %User{notification_settings: %{non_followers: false}} = user
574 ) do
575 actor = activity.data["actor"]
576 follower = User.get_cached_by_ap_id(actor)
577 !User.following?(follower, user)
578 end
579
580 def skip?(
581 :follows,
582 %Activity{} = activity,
583 %User{notification_settings: %{follows: false}} = user
584 ) do
585 actor = activity.data["actor"]
586 followed = User.get_cached_by_ap_id(actor)
587 User.following?(user, followed)
588 end
589
590 def skip?(
591 :non_follows,
592 %Activity{} = activity,
593 %User{notification_settings: %{non_follows: false}} = user
594 ) do
595 actor = activity.data["actor"]
596 followed = User.get_cached_by_ap_id(actor)
597 !User.following?(user, followed)
598 end
599
600 # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
601 def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do
602 actor = activity.data["actor"]
603
604 Notification.for_user(user)
605 |> Enum.any?(fn
606 %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
607 _ -> false
608 end)
609 end
610
611 def skip?(_, _, _), do: false
612
613 def for_user_and_activity(user, activity) do
614 from(n in __MODULE__,
615 where: n.user_id == ^user.id,
616 where: n.activity_id == ^activity.id
617 )
618 |> Repo.one()
619 end
620 end