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