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