Extend `/api/pleroma/notifications/read` to mark multiple notifications
[akkoma] / lib / pleroma / notification.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 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 Pleroma.Activity
9 alias Pleroma.Notification
10 alias Pleroma.Object
11 alias Pleroma.Pagination
12 alias Pleroma.Repo
13 alias Pleroma.User
14 alias Pleroma.Web.CommonAPI.Utils
15 alias Pleroma.Web.Push
16 alias Pleroma.Web.Streamer
17
18 import Ecto.Query
19 import Ecto.Changeset
20
21 @type t :: %__MODULE__{}
22
23 schema "notifications" do
24 field(:seen, :boolean, default: false)
25 belongs_to(:user, User, type: Pleroma.FlakeId)
26 belongs_to(:activity, Activity, type: Pleroma.FlakeId)
27
28 timestamps()
29 end
30
31 def changeset(%Notification{} = notification, attrs) do
32 notification
33 |> cast(attrs, [:seen])
34 end
35
36 def for_user_query(user, opts \\ []) do
37 query =
38 Notification
39 |> where(user_id: ^user.id)
40 |> where(
41 [n, a],
42 fragment(
43 "? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
44 a.actor
45 )
46 )
47 |> join(:inner, [n], activity in assoc(n, :activity))
48 |> join(:left, [n, a], object in Object,
49 on:
50 fragment(
51 "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
52 object.data,
53 a.data
54 )
55 )
56 |> preload([n, a, o], activity: {a, object: o})
57
58 if opts[:with_muted] do
59 query
60 else
61 where(query, [n, a], a.actor not in ^user.info.muted_notifications)
62 |> where([n, a], a.actor not in ^user.info.blocks)
63 |> where(
64 [n, a],
65 fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.info.domain_blocks
66 )
67 |> join(:left, [n, a], tm in Pleroma.ThreadMute,
68 on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
69 )
70 |> where([n, a, o, tm], is_nil(tm.user_id))
71 end
72 end
73
74 def for_user(user, opts \\ %{}) do
75 user
76 |> for_user_query(opts)
77 |> Pagination.fetch_paginated(opts)
78 end
79
80 @doc """
81 Returns notifications for user received since given date.
82
83 ## Examples
84
85 iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-13 11:22:33])
86 [%Pleroma.Notification{}, %Pleroma.Notification{}]
87
88 iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-15 11:22:33])
89 []
90 """
91 @spec for_user_since(Pleroma.User.t(), NaiveDateTime.t()) :: [t()]
92 def for_user_since(user, date) do
93 from(n in for_user_query(user),
94 where: n.updated_at > ^date
95 )
96 |> Repo.all()
97 end
98
99 def set_read_up_to(%{id: user_id} = _user, id) do
100 query =
101 from(
102 n in Notification,
103 where: n.user_id == ^user_id,
104 where: n.id <= ^id,
105 where: n.seen == false,
106 update: [
107 set: [
108 seen: true,
109 updated_at: ^NaiveDateTime.utc_now()
110 ]
111 ],
112 # Ideally we would preload object and activities here
113 # but Ecto does not support preloads in update_all
114 select: n.id
115 )
116
117 {_, notification_ids} = Repo.update_all(query, [])
118
119 from(n in Notification, where: n.id in ^notification_ids)
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'::text) ->> 'id'::text)",
125 object.data,
126 a.data
127 )
128 )
129 |> preload([n, a, o], activity: {a, object: o})
130 |> Repo.all()
131 end
132
133 def read_one(%User{} = user, notification_id) do
134 with {:ok, %Notification{} = notification} <- get(user, notification_id) do
135 notification
136 |> changeset(%{seen: true})
137 |> Repo.update()
138 end
139 end
140
141 def get(%{id: user_id} = _user, id) do
142 query =
143 from(
144 n in Notification,
145 where: n.id == ^id,
146 join: activity in assoc(n, :activity),
147 preload: [activity: activity]
148 )
149
150 notification = Repo.one(query)
151
152 case notification do
153 %{user_id: ^user_id} ->
154 {:ok, notification}
155
156 _ ->
157 {:error, "Cannot get notification"}
158 end
159 end
160
161 def clear(user) do
162 from(n in Notification, where: n.user_id == ^user.id)
163 |> Repo.delete_all()
164 end
165
166 def destroy_multiple(%{id: user_id} = _user, ids) do
167 from(n in Notification,
168 where: n.id in ^ids,
169 where: n.user_id == ^user_id
170 )
171 |> Repo.delete_all()
172 end
173
174 def dismiss(%{id: user_id} = _user, id) do
175 notification = Repo.get(Notification, id)
176
177 case notification do
178 %{user_id: ^user_id} ->
179 Repo.delete(notification)
180
181 _ ->
182 {:error, "Cannot dismiss notification"}
183 end
184 end
185
186 def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
187 object = Object.normalize(activity)
188
189 unless object && object.data["type"] == "Answer" do
190 users = get_notified_from_activity(activity)
191 notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
192 {:ok, notifications}
193 else
194 {:ok, []}
195 end
196 end
197
198 def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)
199 when type in ["Like", "Announce", "Follow"] do
200 users = get_notified_from_activity(activity)
201 notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
202 {:ok, notifications}
203 end
204
205 def create_notifications(_), do: {:ok, []}
206
207 # TODO move to sql, too.
208 def create_notification(%Activity{} = activity, %User{} = user) do
209 unless skip?(activity, user) do
210 notification = %Notification{user_id: user.id, activity: activity}
211 {:ok, notification} = Repo.insert(notification)
212 Streamer.stream("user", notification)
213 Streamer.stream("user:notification", notification)
214 Push.send(notification)
215 notification
216 end
217 end
218
219 def get_notified_from_activity(activity, local_only \\ true)
220
221 def get_notified_from_activity(
222 %Activity{data: %{"to" => _, "type" => type} = _data} = activity,
223 local_only
224 )
225 when type in ["Create", "Like", "Announce", "Follow"] do
226 recipients =
227 []
228 |> Utils.maybe_notify_to_recipients(activity)
229 |> Utils.maybe_notify_mentioned_recipients(activity)
230 |> Utils.maybe_notify_subscribers(activity)
231 |> Enum.uniq()
232
233 User.get_users_from_set(recipients, local_only)
234 end
235
236 def get_notified_from_activity(_, _local_only), do: []
237
238 @spec skip?(Activity.t(), User.t()) :: boolean()
239 def skip?(activity, user) do
240 [
241 :self,
242 :followers,
243 :follows,
244 :non_followers,
245 :non_follows,
246 :recently_followed
247 ]
248 |> Enum.any?(&skip?(&1, activity, user))
249 end
250
251 @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
252 def skip?(:self, activity, user) do
253 activity.data["actor"] == user.ap_id
254 end
255
256 def skip?(
257 :followers,
258 activity,
259 %{info: %{notification_settings: %{"followers" => false}}} = user
260 ) do
261 actor = activity.data["actor"]
262 follower = User.get_cached_by_ap_id(actor)
263 User.following?(follower, user)
264 end
265
266 def skip?(
267 :non_followers,
268 activity,
269 %{info: %{notification_settings: %{"non_followers" => false}}} = user
270 ) do
271 actor = activity.data["actor"]
272 follower = User.get_cached_by_ap_id(actor)
273 !User.following?(follower, user)
274 end
275
276 def skip?(:follows, activity, %{info: %{notification_settings: %{"follows" => false}}} = user) do
277 actor = activity.data["actor"]
278 followed = User.get_cached_by_ap_id(actor)
279 User.following?(user, followed)
280 end
281
282 def skip?(
283 :non_follows,
284 activity,
285 %{info: %{notification_settings: %{"non_follows" => false}}} = user
286 ) do
287 actor = activity.data["actor"]
288 followed = User.get_cached_by_ap_id(actor)
289 !User.following?(user, followed)
290 end
291
292 def skip?(:recently_followed, %{data: %{"type" => "Follow"}} = activity, user) do
293 actor = activity.data["actor"]
294
295 Notification.for_user(user)
296 |> Enum.any?(fn
297 %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
298 _ -> false
299 end)
300 end
301
302 def skip?(_, _, _), do: false
303 end