Merge branch 'develop' into issue/2099
[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 reblog
74 }
75
76 def changeset(%Notification{} = notification, attrs) do
77 notification
78 |> cast(attrs, [:seen, :type])
79 |> validate_inclusion(:type, @notification_types)
80 end
81
82 @spec last_read_query(User.t()) :: Ecto.Queryable.t()
83 def last_read_query(user) do
84 from(q in Pleroma.Notification,
85 where: q.user_id == ^user.id,
86 where: q.seen == true,
87 select: type(q.id, :string),
88 limit: 1,
89 order_by: [desc: :id]
90 )
91 end
92
93 defp for_user_query_ap_id_opts(user, opts) do
94 ap_id_relationships =
95 [:block] ++
96 if opts[@include_muted_option], do: [], else: [:notification_mute]
97
98 preloaded_ap_ids = User.outgoing_relationships_ap_ids(user, ap_id_relationships)
99
100 exclude_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts)
101
102 exclude_notification_muted_opts =
103 Map.merge(%{notification_muted_users_ap_ids: preloaded_ap_ids[:notification_mute]}, opts)
104
105 {exclude_blocked_opts, exclude_notification_muted_opts}
106 end
107
108 def for_user_query(user, opts \\ %{}) do
109 {exclude_blocked_opts, exclude_notification_muted_opts} =
110 for_user_query_ap_id_opts(user, opts)
111
112 Notification
113 |> where(user_id: ^user.id)
114 |> where(
115 [n, a],
116 fragment(
117 "? not in (SELECT ap_id FROM users WHERE deactivated = 'true')",
118 a.actor
119 )
120 )
121 |> join(:inner, [n], activity in assoc(n, :activity))
122 |> join(:left, [n, a], object in Object,
123 on:
124 fragment(
125 "(?->>'id') = COALESCE(?->'object'->>'id', ?->>'object')",
126 object.data,
127 a.data,
128 a.data
129 )
130 )
131 |> preload([n, a, o], activity: {a, object: o})
132 |> exclude_notification_muted(user, exclude_notification_muted_opts)
133 |> exclude_blocked(user, exclude_blocked_opts)
134 |> exclude_filtered(user)
135 |> exclude_visibility(opts)
136 end
137
138 # Excludes blocked users and non-followed domain-blocked users
139 defp exclude_blocked(query, user, opts) do
140 blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
141
142 query
143 |> where([n, a], a.actor not in ^blocked_ap_ids)
144 |> FollowingRelationship.keep_following_or_not_domain_blocked(user)
145 end
146
147 defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do
148 query
149 end
150
151 defp exclude_notification_muted(query, user, opts) do
152 notification_muted_ap_ids =
153 opts[:notification_muted_users_ap_ids] || User.notification_muted_users_ap_ids(user)
154
155 query
156 |> where([n, a], a.actor not in ^notification_muted_ap_ids)
157 |> join(:left, [n, a], tm in ThreadMute,
158 on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
159 )
160 |> where([n, a, o, tm], is_nil(tm.user_id))
161 end
162
163 defp exclude_filtered(query, user) do
164 case Pleroma.Filter.compose_regex(user) do
165 nil ->
166 query
167
168 regex ->
169 from([_n, a, o] in query,
170 where:
171 fragment("not(?->>'content' ~* ?)", o.data, ^regex) or
172 fragment("?->>'actor' = ?", o.data, ^user.ap_id)
173 )
174 end
175 end
176
177 @valid_visibilities ~w[direct unlisted public private]
178
179 defp exclude_visibility(query, %{exclude_visibilities: visibility})
180 when is_list(visibility) do
181 if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
182 query
183 |> join(:left, [n, a], mutated_activity in Pleroma.Activity,
184 on:
185 fragment(
186 "COALESCE((?->'object')->>'id', ?->>'object')",
187 a.data,
188 a.data
189 ) ==
190 fragment(
191 "COALESCE((?->'object')->>'id', ?->>'object')",
192 mutated_activity.data,
193 mutated_activity.data
194 ) and
195 fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
196 fragment("?->>'type'", mutated_activity.data) == "Create",
197 as: :mutated_activity
198 )
199 |> where(
200 [n, a, mutated_activity: mutated_activity],
201 not fragment(
202 """
203 CASE WHEN (?->>'type') = 'Like' or (?->>'type') = 'Announce'
204 THEN (activity_visibility(?, ?, ?) = ANY (?))
205 ELSE (activity_visibility(?, ?, ?) = ANY (?)) END
206 """,
207 a.data,
208 a.data,
209 mutated_activity.actor,
210 mutated_activity.recipients,
211 mutated_activity.data,
212 ^visibility,
213 a.actor,
214 a.recipients,
215 a.data,
216 ^visibility
217 )
218 )
219 else
220 Logger.error("Could not exclude visibility to #{visibility}")
221 query
222 end
223 end
224
225 defp exclude_visibility(query, %{exclude_visibilities: visibility})
226 when visibility in @valid_visibilities do
227 exclude_visibility(query, [visibility])
228 end
229
230 defp exclude_visibility(query, %{exclude_visibilities: visibility})
231 when visibility not in @valid_visibilities do
232 Logger.error("Could not exclude visibility to #{visibility}")
233 query
234 end
235
236 defp exclude_visibility(query, _visibility), do: query
237
238 def for_user(user, opts \\ %{}) do
239 user
240 |> for_user_query(opts)
241 |> Pagination.fetch_paginated(opts)
242 end
243
244 @doc """
245 Returns notifications for user received since given date.
246
247 ## Examples
248
249 iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-13 11:22:33])
250 [%Pleroma.Notification{}, %Pleroma.Notification{}]
251
252 iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-15 11:22:33])
253 []
254 """
255 @spec for_user_since(Pleroma.User.t(), NaiveDateTime.t()) :: [t()]
256 def for_user_since(user, date) do
257 from(n in for_user_query(user),
258 where: n.updated_at > ^date
259 )
260 |> Repo.all()
261 end
262
263 def set_read_up_to(%{id: user_id} = user, id) do
264 query =
265 from(
266 n in Notification,
267 where: n.user_id == ^user_id,
268 where: n.id <= ^id,
269 where: n.seen == false,
270 # Ideally we would preload object and activities here
271 # but Ecto does not support preloads in update_all
272 select: n.id
273 )
274
275 {:ok, %{ids: {_, notification_ids}}} =
276 Multi.new()
277 |> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
278 |> Marker.multi_set_last_read_id(user, "notifications")
279 |> Repo.transaction()
280
281 for_user_query(user)
282 |> where([n], n.id in ^notification_ids)
283 |> Repo.all()
284 end
285
286 @spec read_one(User.t(), String.t()) ::
287 {:ok, Notification.t()} | {:error, Ecto.Changeset.t()} | nil
288 def read_one(%User{} = user, notification_id) do
289 with {:ok, %Notification{} = notification} <- get(user, notification_id) do
290 Multi.new()
291 |> Multi.update(:update, changeset(notification, %{seen: true}))
292 |> Marker.multi_set_last_read_id(user, "notifications")
293 |> Repo.transaction()
294 |> case do
295 {:ok, %{update: notification}} -> {:ok, notification}
296 {:error, :update, changeset, _} -> {:error, changeset}
297 end
298 end
299 end
300
301 def get(%{id: user_id} = _user, id) do
302 query =
303 from(
304 n in Notification,
305 where: n.id == ^id,
306 join: activity in assoc(n, :activity),
307 preload: [activity: activity]
308 )
309
310 notification = Repo.one(query)
311
312 case notification do
313 %{user_id: ^user_id} ->
314 {:ok, notification}
315
316 _ ->
317 {:error, "Cannot get notification"}
318 end
319 end
320
321 def clear(user) do
322 from(n in Notification, where: n.user_id == ^user.id)
323 |> Repo.delete_all()
324 end
325
326 def destroy_multiple(%{id: user_id} = _user, ids) do
327 from(n in Notification,
328 where: n.id in ^ids,
329 where: n.user_id == ^user_id
330 )
331 |> Repo.delete_all()
332 end
333
334 def dismiss(%Pleroma.Activity{} = activity) do
335 Notification
336 |> where([n], n.activity_id == ^activity.id)
337 |> Repo.delete_all()
338 |> case do
339 {_, notifications} -> {:ok, notifications}
340 _ -> {:error, "Cannot dismiss notification"}
341 end
342 end
343
344 def dismiss(%{id: user_id} = _user, id) do
345 notification = Repo.get(Notification, id)
346
347 case notification do
348 %{user_id: ^user_id} ->
349 Repo.delete(notification)
350
351 _ ->
352 {:error, "Cannot dismiss notification"}
353 end
354 end
355
356 @spec create_notifications(Activity.t(), keyword()) :: {:ok, [Notification.t()] | []}
357 def create_notifications(activity, options \\ [])
358
359 def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do
360 object = Object.normalize(activity, false)
361
362 if object && object.data["type"] == "Answer" do
363 {:ok, []}
364 else
365 do_create_notifications(activity, options)
366 end
367 end
368
369 def create_notifications(%Activity{data: %{"type" => type}} = activity, options)
370 when type in ["Follow", "Like", "Announce", "Move", "EmojiReact"] do
371 do_create_notifications(activity, options)
372 end
373
374 def create_notifications(_, _), do: {:ok, []}
375
376 defp do_create_notifications(%Activity{} = activity, options) do
377 do_send = Keyword.get(options, :do_send, true)
378
379 {enabled_receivers, disabled_receivers} = get_notified_from_activity(activity)
380 potential_receivers = enabled_receivers ++ disabled_receivers
381
382 notifications =
383 Enum.map(potential_receivers, fn user ->
384 do_send = do_send && user in enabled_receivers
385 create_notification(activity, user, do_send)
386 end)
387 |> Enum.reject(&is_nil/1)
388
389 {:ok, notifications}
390 end
391
392 defp type_from_activity(%{data: %{"type" => type}} = activity) do
393 case type do
394 "Follow" ->
395 if Activity.follow_accepted?(activity) do
396 "follow"
397 else
398 "follow_request"
399 end
400
401 "Announce" ->
402 "reblog"
403
404 "Like" ->
405 "favourite"
406
407 "Move" ->
408 "move"
409
410 "EmojiReact" ->
411 "pleroma:emoji_reaction"
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"] 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(activity) do
507 []
508 |> Utils.maybe_notify_to_recipients(activity)
509 |> Utils.maybe_notify_mentioned_recipients(activity)
510 |> Utils.maybe_notify_subscribers(activity)
511 |> Utils.maybe_notify_followers(activity)
512 |> Enum.uniq()
513 end
514
515 @doc "Filters out AP IDs domain-blocking and not following the activity's actor"
516 def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])
517
518 def exclude_domain_blocker_ap_ids([], _activity, _preloaded_users), do: []
519
520 def exclude_domain_blocker_ap_ids(ap_ids, %Activity{} = activity, preloaded_users) do
521 activity_actor_domain = activity.actor && URI.parse(activity.actor).host
522
523 users =
524 ap_ids
525 |> Enum.map(fn ap_id ->
526 Enum.find(preloaded_users, &(&1.ap_id == ap_id)) ||
527 User.get_cached_by_ap_id(ap_id)
528 end)
529 |> Enum.filter(& &1)
530
531 domain_blocker_ap_ids = for u <- users, activity_actor_domain in u.domain_blocks, do: u.ap_id
532
533 domain_blocker_follower_ap_ids =
534 if Enum.any?(domain_blocker_ap_ids) do
535 activity
536 |> Activity.user_actor()
537 |> FollowingRelationship.followers_ap_ids(domain_blocker_ap_ids)
538 else
539 []
540 end
541
542 ap_ids
543 |> Kernel.--(domain_blocker_ap_ids)
544 |> Kernel.++(domain_blocker_follower_ap_ids)
545 end
546
547 @doc "Filters out AP IDs of users basing on their relationships with activity actor user"
548 def exclude_relationship_restricted_ap_ids([], _activity), do: []
549
550 def exclude_relationship_restricted_ap_ids(ap_ids, %Activity{} = activity) do
551 relationship_restricted_ap_ids =
552 activity
553 |> Activity.user_actor()
554 |> User.incoming_relationships_ungrouped_ap_ids([
555 :block,
556 :notification_mute
557 ])
558
559 Enum.uniq(ap_ids) -- relationship_restricted_ap_ids
560 end
561
562 @doc "Filters out AP IDs of users who mute activity thread"
563 def exclude_thread_muter_ap_ids([], _activity), do: []
564
565 def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do
566 thread_muter_ap_ids = ThreadMute.muter_ap_ids(activity.data["context"])
567
568 Enum.uniq(ap_ids) -- thread_muter_ap_ids
569 end
570
571 @spec skip?(Activity.t(), User.t()) :: boolean()
572 def skip?(%Activity{} = activity, %User{} = user) do
573 [
574 :self,
575 :invisible,
576 :block_from_strangers,
577 :recently_followed,
578 :filtered
579 ]
580 |> Enum.find(&skip?(&1, activity, user))
581 end
582
583 def skip?(_, _), do: false
584
585 @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
586 def skip?(:self, %Activity{} = activity, %User{} = user) do
587 activity.data["actor"] == user.ap_id
588 end
589
590 def skip?(:invisible, %Activity{} = activity, _) do
591 actor = activity.data["actor"]
592 user = User.get_cached_by_ap_id(actor)
593 User.invisible?(user)
594 end
595
596 def skip?(
597 :block_from_strangers,
598 %Activity{} = activity,
599 %User{notification_settings: %{block_from_strangers: true}} = user
600 ) do
601 actor = activity.data["actor"]
602 follower = User.get_cached_by_ap_id(actor)
603 !User.following?(follower, user)
604 end
605
606 # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
607 def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do
608 actor = activity.data["actor"]
609
610 Notification.for_user(user)
611 |> Enum.any?(fn
612 %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
613 _ -> false
614 end)
615 end
616
617 def skip?(:filtered, %{data: %{"type" => type}}, _) when type in ["Follow", "Move"], do: false
618
619 def skip?(:filtered, activity, user) do
620 object = Object.normalize(activity)
621
622 cond do
623 is_nil(object) ->
624 false
625
626 object.data["actor"] == user.ap_id ->
627 false
628
629 not is_nil(regex = Pleroma.Filter.compose_regex(user, :re)) ->
630 Regex.match?(regex, object.data["content"])
631
632 true ->
633 false
634 end
635 end
636
637 def skip?(_, _, _), do: false
638
639 def mark_as_read?(activity, target_user) do
640 user = Activity.user_actor(activity)
641 User.mutes_user?(target_user, user) || CommonAPI.thread_muted?(target_user, activity)
642 end
643
644 def for_user_and_activity(user, activity) do
645 from(n in __MODULE__,
646 where: n.user_id == ^user.id,
647 where: n.activity_id == ^activity.id
648 )
649 |> Repo.one()
650 end
651
652 @spec mark_context_as_read(User.t(), String.t()) :: {integer(), nil | [term()]}
653 def mark_context_as_read(%User{id: id}, context) do
654 from(
655 n in Notification,
656 join: a in assoc(n, :activity),
657 where: n.user_id == ^id,
658 where: n.seen == false,
659 where: fragment("?->>'context'", a.data) == ^context
660 )
661 |> Repo.update_all(set: [seen: true])
662 end
663 end