purge chat and shout endpoints
[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:emoji_reaction
72 pleroma:report
73 reblog
74 poll
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 destroy_multiple_from_types(%{id: user_id}, types) do
344 from(n in Notification,
345 where: n.user_id == ^user_id,
346 where: n.type in ^types
347 )
348 |> Repo.delete_all()
349 end
350
351 def dismiss(%Pleroma.Activity{} = activity) do
352 Notification
353 |> where([n], n.activity_id == ^activity.id)
354 |> Repo.delete_all()
355 |> case do
356 {_, notifications} -> {:ok, notifications}
357 _ -> {:error, "Cannot dismiss notification"}
358 end
359 end
360
361 def dismiss(%{id: user_id} = _user, id) do
362 notification = Repo.get(Notification, id)
363
364 case notification do
365 %{user_id: ^user_id} ->
366 Repo.delete(notification)
367
368 _ ->
369 {:error, "Cannot dismiss notification"}
370 end
371 end
372
373 @spec create_notifications(Activity.t(), keyword()) :: {:ok, [Notification.t()] | []}
374 def create_notifications(activity, options \\ [])
375
376 def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do
377 object = Object.normalize(activity, fetch: false)
378
379 if object && object.data["type"] == "Answer" do
380 {:ok, []}
381 else
382 do_create_notifications(activity, options)
383 end
384 end
385
386 def create_notifications(%Activity{data: %{"type" => type}} = activity, options)
387 when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag"] do
388 do_create_notifications(activity, options)
389 end
390
391 def create_notifications(_, _), do: {:ok, []}
392
393 defp do_create_notifications(%Activity{} = activity, options) do
394 do_send = Keyword.get(options, :do_send, true)
395
396 {enabled_receivers, disabled_receivers} = get_notified_from_activity(activity)
397 potential_receivers = enabled_receivers ++ disabled_receivers
398
399 notifications =
400 Enum.map(potential_receivers, fn user ->
401 do_send = do_send && user in enabled_receivers
402 create_notification(activity, user, do_send: do_send)
403 end)
404 |> Enum.reject(&is_nil/1)
405
406 {:ok, notifications}
407 end
408
409 defp type_from_activity(%{data: %{"type" => type}} = activity) do
410 case type do
411 "Follow" ->
412 if Activity.follow_accepted?(activity) do
413 "follow"
414 else
415 "follow_request"
416 end
417
418 "Announce" ->
419 "reblog"
420
421 "Like" ->
422 "favourite"
423
424 "Move" ->
425 "move"
426
427 "EmojiReact" ->
428 "pleroma:emoji_reaction"
429
430 "Flag" ->
431 "pleroma:report"
432
433 # Compatibility with old reactions
434 "EmojiReaction" ->
435 "pleroma:emoji_reaction"
436
437 "Create" ->
438 activity
439 |> type_from_activity_object()
440
441 t ->
442 raise "No notification type for activity type #{t}"
443 end
444 end
445
446 defp type_from_activity_object(%{data: %{"type" => "Create"}}), do: "mention"
447
448 # TODO move to sql, too.
449 def create_notification(%Activity{} = activity, %User{} = user, opts \\ []) do
450 do_send = Keyword.get(opts, :do_send, true)
451 type = Keyword.get(opts, :type, type_from_activity(activity))
452
453 unless skip?(activity, user, opts) do
454 {:ok, %{notification: notification}} =
455 Multi.new()
456 |> Multi.insert(:notification, %Notification{
457 user_id: user.id,
458 activity: activity,
459 seen: mark_as_read?(activity, user),
460 type: type
461 })
462 |> Marker.multi_set_last_read_id(user, "notifications")
463 |> Repo.transaction()
464
465 if do_send do
466 Streamer.stream(["user", "user:notification"], notification)
467 Push.send(notification)
468 end
469
470 notification
471 end
472 end
473
474 def create_poll_notifications(%Activity{} = activity) do
475 with %Object{data: %{"type" => "Question", "actor" => actor} = data} <-
476 Object.normalize(activity) do
477 voters =
478 case data do
479 %{"voters" => voters} when is_list(voters) -> voters
480 _ -> []
481 end
482
483 notifications =
484 Enum.reduce([actor | voters], [], fn ap_id, acc ->
485 with %User{local: true} = user <- User.get_by_ap_id(ap_id) do
486 [create_notification(activity, user, type: "poll") | acc]
487 else
488 _ -> acc
489 end
490 end)
491
492 {:ok, notifications}
493 end
494 end
495
496 @doc """
497 Returns a tuple with 2 elements:
498 {notification-enabled receivers, currently disabled receivers (blocking / [thread] muting)}
499
500 NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1
501 """
502 @spec get_notified_from_activity(Activity.t(), boolean()) :: {list(User.t()), list(User.t())}
503 def get_notified_from_activity(activity, local_only \\ true)
504
505 def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
506 when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact", "Flag"] do
507 potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
508
509 potential_receivers =
510 User.get_users_from_set(potential_receiver_ap_ids, local_only: local_only)
511
512 notification_enabled_ap_ids =
513 potential_receiver_ap_ids
514 |> exclude_domain_blocker_ap_ids(activity, potential_receivers)
515 |> exclude_relationship_restricted_ap_ids(activity)
516 |> exclude_thread_muter_ap_ids(activity)
517
518 notification_enabled_users =
519 Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
520
521 {notification_enabled_users, potential_receivers -- notification_enabled_users}
522 end
523
524 def get_notified_from_activity(_, _local_only), do: {[], []}
525
526 # For some activities, only notify the author of the object
527 def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_id}})
528 when type in ~w{Like Announce EmojiReact} do
529 case Object.get_cached_by_ap_id(object_id) do
530 %Object{data: %{"actor" => actor}} ->
531 [actor]
532
533 _ ->
534 []
535 end
536 end
537
538 def get_potential_receiver_ap_ids(%{data: %{"type" => "Follow", "object" => object_id}}) do
539 [object_id]
540 end
541
542 def get_potential_receiver_ap_ids(%{data: %{"type" => "Flag", "actor" => actor}}) do
543 (User.all_superusers() |> Enum.map(fn user -> user.ap_id end)) -- [actor]
544 end
545
546 def get_potential_receiver_ap_ids(activity) do
547 []
548 |> Utils.maybe_notify_to_recipients(activity)
549 |> Utils.maybe_notify_mentioned_recipients(activity)
550 |> Utils.maybe_notify_subscribers(activity)
551 |> Utils.maybe_notify_followers(activity)
552 |> Enum.uniq()
553 end
554
555 @doc "Filters out AP IDs domain-blocking and not following the activity's actor"
556 def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])
557
558 def exclude_domain_blocker_ap_ids([], _activity, _preloaded_users), do: []
559
560 def exclude_domain_blocker_ap_ids(ap_ids, %Activity{} = activity, preloaded_users) do
561 activity_actor_domain = activity.actor && URI.parse(activity.actor).host
562
563 users =
564 ap_ids
565 |> Enum.map(fn ap_id ->
566 Enum.find(preloaded_users, &(&1.ap_id == ap_id)) ||
567 User.get_cached_by_ap_id(ap_id)
568 end)
569 |> Enum.filter(& &1)
570
571 domain_blocker_ap_ids = for u <- users, activity_actor_domain in u.domain_blocks, do: u.ap_id
572
573 domain_blocker_follower_ap_ids =
574 if Enum.any?(domain_blocker_ap_ids) do
575 activity
576 |> Activity.user_actor()
577 |> FollowingRelationship.followers_ap_ids(domain_blocker_ap_ids)
578 else
579 []
580 end
581
582 ap_ids
583 |> Kernel.--(domain_blocker_ap_ids)
584 |> Kernel.++(domain_blocker_follower_ap_ids)
585 end
586
587 @doc "Filters out AP IDs of users basing on their relationships with activity actor user"
588 def exclude_relationship_restricted_ap_ids([], _activity), do: []
589
590 def exclude_relationship_restricted_ap_ids(ap_ids, %Activity{} = activity) do
591 relationship_restricted_ap_ids =
592 activity
593 |> Activity.user_actor()
594 |> User.incoming_relationships_ungrouped_ap_ids([
595 :block,
596 :notification_mute
597 ])
598
599 Enum.uniq(ap_ids) -- relationship_restricted_ap_ids
600 end
601
602 @doc "Filters out AP IDs of users who mute activity thread"
603 def exclude_thread_muter_ap_ids([], _activity), do: []
604
605 def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do
606 thread_muter_ap_ids = ThreadMute.muter_ap_ids(activity.data["context"])
607
608 Enum.uniq(ap_ids) -- thread_muter_ap_ids
609 end
610
611 def skip?(activity, user, opts \\ [])
612
613 @spec skip?(Activity.t(), User.t(), Keyword.t()) :: boolean()
614 def skip?(%Activity{} = activity, %User{} = user, opts) do
615 [
616 :self,
617 :invisible,
618 :block_from_strangers,
619 :recently_followed,
620 :filtered
621 ]
622 |> Enum.find(&skip?(&1, activity, user, opts))
623 end
624
625 def skip?(_activity, _user, _opts), do: false
626
627 @spec skip?(atom(), Activity.t(), User.t(), Keyword.t()) :: boolean()
628 def skip?(:self, %Activity{} = activity, %User{} = user, opts) do
629 cond do
630 opts[:type] == "poll" -> false
631 activity.data["actor"] == user.ap_id -> true
632 true -> false
633 end
634 end
635
636 def skip?(:invisible, %Activity{} = activity, _user, _opts) do
637 actor = activity.data["actor"]
638 user = User.get_cached_by_ap_id(actor)
639 User.invisible?(user)
640 end
641
642 def skip?(
643 :block_from_strangers,
644 %Activity{} = activity,
645 %User{notification_settings: %{block_from_strangers: true}} = user,
646 opts
647 ) do
648 actor = activity.data["actor"]
649 follower = User.get_cached_by_ap_id(actor)
650
651 cond do
652 opts[:type] == "poll" -> false
653 user.ap_id == actor -> false
654 !User.following?(follower, user) -> true
655 true -> false
656 end
657 end
658
659 # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
660 def skip?(
661 :recently_followed,
662 %Activity{data: %{"type" => "Follow"}} = activity,
663 %User{} = user,
664 _opts
665 ) do
666 actor = activity.data["actor"]
667
668 Notification.for_user(user)
669 |> Enum.any?(fn
670 %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
671 _ -> false
672 end)
673 end
674
675 def skip?(:filtered, %{data: %{"type" => type}}, _user, _opts) when type in ["Follow", "Move"],
676 do: false
677
678 def skip?(:filtered, activity, user, _opts) do
679 object = Object.normalize(activity, fetch: false)
680
681 cond do
682 is_nil(object) ->
683 false
684
685 object.data["actor"] == user.ap_id ->
686 false
687
688 not is_nil(regex = Pleroma.Filter.compose_regex(user, :re)) ->
689 Regex.match?(regex, object.data["content"])
690
691 true ->
692 false
693 end
694 end
695
696 def skip?(_type, _activity, _user, _opts), do: false
697
698 def mark_as_read?(activity, target_user) do
699 user = Activity.user_actor(activity)
700 User.mutes_user?(target_user, user) || CommonAPI.thread_muted?(target_user, activity)
701 end
702
703 def for_user_and_activity(user, activity) do
704 from(n in __MODULE__,
705 where: n.user_id == ^user.id,
706 where: n.activity_id == ^activity.id
707 )
708 |> Repo.one()
709 end
710
711 @spec mark_context_as_read(User.t(), String.t()) :: {integer(), nil | [term()]}
712 def mark_context_as_read(%User{id: id}, context) do
713 from(
714 n in Notification,
715 join: a in assoc(n, :activity),
716 where: n.user_id == ^id,
717 where: n.seen == false,
718 where: fragment("?->>'context'", a.data) == ^context
719 )
720 |> Repo.update_all(set: [seen: true])
721 end
722 end