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