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