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