Merge branch 'instance-deletion' into 'develop'
[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.reduce([actor | voters], [], fn ap_id, acc ->
475 with %User{local: true} = user <- User.get_by_ap_id(ap_id) do
476 [create_notification(activity, user, type: "poll") | acc]
477 else
478 _ -> acc
479 end
480 end)
481
482 {:ok, notifications}
483 end
484 end
485
486 @doc """
487 Returns a tuple with 2 elements:
488 {notification-enabled receivers, currently disabled receivers (blocking / [thread] muting)}
489
490 NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1
491 """
492 @spec get_notified_from_activity(Activity.t(), boolean()) :: {list(User.t()), list(User.t())}
493 def get_notified_from_activity(activity, local_only \\ true)
494
495 def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
496 when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact", "Flag"] do
497 potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
498
499 potential_receivers =
500 User.get_users_from_set(potential_receiver_ap_ids, local_only: local_only)
501
502 notification_enabled_ap_ids =
503 potential_receiver_ap_ids
504 |> exclude_domain_blocker_ap_ids(activity, potential_receivers)
505 |> exclude_relationship_restricted_ap_ids(activity)
506 |> exclude_thread_muter_ap_ids(activity)
507
508 notification_enabled_users =
509 Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
510
511 {notification_enabled_users, potential_receivers -- notification_enabled_users}
512 end
513
514 def get_notified_from_activity(_, _local_only), do: {[], []}
515
516 # For some activities, only notify the author of the object
517 def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_id}})
518 when type in ~w{Like Announce EmojiReact} do
519 case Object.get_cached_by_ap_id(object_id) do
520 %Object{data: %{"actor" => actor}} ->
521 [actor]
522
523 _ ->
524 []
525 end
526 end
527
528 def get_potential_receiver_ap_ids(%{data: %{"type" => "Follow", "object" => object_id}}) do
529 [object_id]
530 end
531
532 def get_potential_receiver_ap_ids(%{data: %{"type" => "Flag", "actor" => actor}}) do
533 (User.all_superusers() |> Enum.map(fn user -> user.ap_id end)) -- [actor]
534 end
535
536 def get_potential_receiver_ap_ids(activity) do
537 []
538 |> Utils.maybe_notify_to_recipients(activity)
539 |> Utils.maybe_notify_mentioned_recipients(activity)
540 |> Utils.maybe_notify_subscribers(activity)
541 |> Utils.maybe_notify_followers(activity)
542 |> Enum.uniq()
543 end
544
545 @doc "Filters out AP IDs domain-blocking and not following the activity's actor"
546 def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])
547
548 def exclude_domain_blocker_ap_ids([], _activity, _preloaded_users), do: []
549
550 def exclude_domain_blocker_ap_ids(ap_ids, %Activity{} = activity, preloaded_users) do
551 activity_actor_domain = activity.actor && URI.parse(activity.actor).host
552
553 users =
554 ap_ids
555 |> Enum.map(fn ap_id ->
556 Enum.find(preloaded_users, &(&1.ap_id == ap_id)) ||
557 User.get_cached_by_ap_id(ap_id)
558 end)
559 |> Enum.filter(& &1)
560
561 domain_blocker_ap_ids = for u <- users, activity_actor_domain in u.domain_blocks, do: u.ap_id
562
563 domain_blocker_follower_ap_ids =
564 if Enum.any?(domain_blocker_ap_ids) do
565 activity
566 |> Activity.user_actor()
567 |> FollowingRelationship.followers_ap_ids(domain_blocker_ap_ids)
568 else
569 []
570 end
571
572 ap_ids
573 |> Kernel.--(domain_blocker_ap_ids)
574 |> Kernel.++(domain_blocker_follower_ap_ids)
575 end
576
577 @doc "Filters out AP IDs of users basing on their relationships with activity actor user"
578 def exclude_relationship_restricted_ap_ids([], _activity), do: []
579
580 def exclude_relationship_restricted_ap_ids(ap_ids, %Activity{} = activity) do
581 relationship_restricted_ap_ids =
582 activity
583 |> Activity.user_actor()
584 |> User.incoming_relationships_ungrouped_ap_ids([
585 :block,
586 :notification_mute
587 ])
588
589 Enum.uniq(ap_ids) -- relationship_restricted_ap_ids
590 end
591
592 @doc "Filters out AP IDs of users who mute activity thread"
593 def exclude_thread_muter_ap_ids([], _activity), do: []
594
595 def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do
596 thread_muter_ap_ids = ThreadMute.muter_ap_ids(activity.data["context"])
597
598 Enum.uniq(ap_ids) -- thread_muter_ap_ids
599 end
600
601 def skip?(activity, user, opts \\ [])
602
603 @spec skip?(Activity.t(), User.t(), Keyword.t()) :: boolean()
604 def skip?(%Activity{} = activity, %User{} = user, opts) do
605 [
606 :self,
607 :invisible,
608 :block_from_strangers,
609 :recently_followed,
610 :filtered
611 ]
612 |> Enum.find(&skip?(&1, activity, user, opts))
613 end
614
615 def skip?(_activity, _user, _opts), do: false
616
617 @spec skip?(atom(), Activity.t(), User.t(), Keyword.t()) :: boolean()
618 def skip?(:self, %Activity{} = activity, %User{} = user, opts) do
619 cond do
620 opts[:type] == "poll" -> false
621 activity.data["actor"] == user.ap_id -> true
622 true -> false
623 end
624 end
625
626 def skip?(:invisible, %Activity{} = activity, _user, _opts) do
627 actor = activity.data["actor"]
628 user = User.get_cached_by_ap_id(actor)
629 User.invisible?(user)
630 end
631
632 def skip?(
633 :block_from_strangers,
634 %Activity{} = activity,
635 %User{notification_settings: %{block_from_strangers: true}} = user,
636 opts
637 ) do
638 actor = activity.data["actor"]
639 follower = User.get_cached_by_ap_id(actor)
640
641 cond do
642 opts[:type] == "poll" -> false
643 user.ap_id == actor -> false
644 !User.following?(follower, user) -> true
645 true -> false
646 end
647 end
648
649 # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
650 def skip?(
651 :recently_followed,
652 %Activity{data: %{"type" => "Follow"}} = activity,
653 %User{} = user,
654 _opts
655 ) do
656 actor = activity.data["actor"]
657
658 Notification.for_user(user)
659 |> Enum.any?(fn
660 %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
661 _ -> false
662 end)
663 end
664
665 def skip?(:filtered, %{data: %{"type" => type}}, _user, _opts) when type in ["Follow", "Move"],
666 do: false
667
668 def skip?(:filtered, activity, user, _opts) do
669 object = Object.normalize(activity, fetch: false)
670
671 cond do
672 is_nil(object) ->
673 false
674
675 object.data["actor"] == user.ap_id ->
676 false
677
678 not is_nil(regex = Pleroma.Filter.compose_regex(user, :re)) ->
679 Regex.match?(regex, object.data["content"])
680
681 true ->
682 false
683 end
684 end
685
686 def skip?(_type, _activity, _user, _opts), do: false
687
688 def mark_as_read?(activity, target_user) do
689 user = Activity.user_actor(activity)
690 User.mutes_user?(target_user, user) || CommonAPI.thread_muted?(target_user, activity)
691 end
692
693 def for_user_and_activity(user, activity) do
694 from(n in __MODULE__,
695 where: n.user_id == ^user.id,
696 where: n.activity_id == ^activity.id
697 )
698 |> Repo.one()
699 end
700
701 @spec mark_context_as_read(User.t(), String.t()) :: {integer(), nil | [term()]}
702 def mark_context_as_read(%User{id: id}, context) do
703 from(
704 n in Notification,
705 join: a in assoc(n, :activity),
706 where: n.user_id == ^id,
707 where: n.seen == false,
708 where: fragment("?->>'context'", a.data) == ^context
709 )
710 |> Repo.update_all(set: [seen: true])
711 end
712 end