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