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