1cc6a4735c094f3ed5df2306934680bf4cb68ac1
[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 Ecto.Multi
9 alias Pleroma.Activity
10 alias Pleroma.Marker
11 alias Pleroma.Notification
12 alias Pleroma.Object
13 alias Pleroma.Pagination
14 alias Pleroma.Repo
15 alias Pleroma.User
16 alias Pleroma.Web.CommonAPI.Utils
17 alias Pleroma.Web.Push
18 alias Pleroma.Web.Streamer
19
20 import Ecto.Query
21 import Ecto.Changeset
22 require Logger
23
24 @type t :: %__MODULE__{}
25
26 schema "notifications" do
27 field(:seen, :boolean, default: false)
28 belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
29 belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
30
31 timestamps()
32 end
33
34 def changeset(%Notification{} = notification, attrs) do
35 notification
36 |> cast(attrs, [:seen])
37 end
38
39 @spec unread_count_query(User.t()) :: Ecto.Queryable.t()
40 def unread_count_query(user) do
41 from(q in Pleroma.Notification,
42 where: q.user_id == ^user.id,
43 where: q.seen == false
44 )
45 end
46
47 @spec last_read_query(User.t()) :: Ecto.Queryable.t()
48 def last_read_query(user) do
49 from(q in Pleroma.Notification,
50 where: q.user_id == ^user.id,
51 where: q.seen == true,
52 select: type(q.id, :string),
53 limit: 1,
54 order_by: [desc: :id]
55 )
56 end
57
58 def for_user_query(user, opts \\ []) do
59 Notification
60 |> where(user_id: ^user.id)
61 |> where(
62 [n, a],
63 fragment(
64 "? not in (SELECT ap_id FROM users WHERE deactivated = 'true')",
65 a.actor
66 )
67 )
68 |> join(:inner, [n], activity in assoc(n, :activity))
69 |> join(:left, [n, a], object in Object,
70 on:
71 fragment(
72 "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
73 object.data,
74 a.data
75 )
76 )
77 |> preload([n, a, o], activity: {a, object: o})
78 |> exclude_muted(user, opts)
79 |> exclude_blocked(user)
80 |> exclude_visibility(opts)
81 end
82
83 defp exclude_blocked(query, user) do
84 query
85 |> where([n, a], a.actor not in ^user.blocks)
86 |> where(
87 [n, a],
88 fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.domain_blocks
89 )
90 end
91
92 defp exclude_muted(query, _, %{with_muted: true}) do
93 query
94 end
95
96 defp exclude_muted(query, user, _opts) do
97 query
98 |> where([n, a], a.actor not in ^user.muted_notifications)
99 |> join(:left, [n, a], tm in Pleroma.ThreadMute,
100 on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
101 )
102 |> where([n, a, o, tm], is_nil(tm.user_id))
103 end
104
105 @valid_visibilities ~w[direct unlisted public private]
106
107 defp exclude_visibility(query, %{exclude_visibilities: visibility})
108 when is_list(visibility) do
109 if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
110 query
111 |> where(
112 [n, a],
113 not fragment(
114 "activity_visibility(?, ?, ?) = ANY (?)",
115 a.actor,
116 a.recipients,
117 a.data,
118 ^visibility
119 )
120 )
121 else
122 Logger.error("Could not exclude visibility to #{visibility}")
123 query
124 end
125 end
126
127 defp exclude_visibility(query, %{exclude_visibilities: visibility})
128 when visibility in @valid_visibilities do
129 query
130 |> where(
131 [n, a],
132 not fragment(
133 "activity_visibility(?, ?, ?) = (?)",
134 a.actor,
135 a.recipients,
136 a.data,
137 ^visibility
138 )
139 )
140 end
141
142 defp exclude_visibility(query, %{exclude_visibilities: visibility})
143 when visibility not in @valid_visibilities do
144 Logger.error("Could not exclude visibility to #{visibility}")
145 query
146 end
147
148 defp exclude_visibility(query, _visibility), do: query
149
150 def for_user(user, opts \\ %{}) do
151 user
152 |> for_user_query(opts)
153 |> Pagination.fetch_paginated(opts)
154 end
155
156 @doc """
157 Returns notifications for user received since given date.
158
159 ## Examples
160
161 iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-13 11:22:33])
162 [%Pleroma.Notification{}, %Pleroma.Notification{}]
163
164 iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-15 11:22:33])
165 []
166 """
167 @spec for_user_since(Pleroma.User.t(), NaiveDateTime.t()) :: [t()]
168 def for_user_since(user, date) do
169 from(n in for_user_query(user),
170 where: n.updated_at > ^date
171 )
172 |> Repo.all()
173 end
174
175 def set_read_up_to(%{id: user_id} = user, id) do
176 query =
177 from(
178 n in Notification,
179 where: n.user_id == ^user_id,
180 where: n.id <= ^id,
181 where: n.seen == false,
182 # Ideally we would preload object and activities here
183 # but Ecto does not support preloads in update_all
184 select: n.id
185 )
186
187 {:ok, %{ids: {_, notification_ids}}} =
188 Multi.new()
189 |> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
190 |> Marker.multi_set_unread_count(user, "notifications")
191 |> Repo.transaction()
192
193 Notification
194 |> where([n], n.id in ^notification_ids)
195 |> join(:inner, [n], activity in assoc(n, :activity))
196 |> join(:left, [n, a], object in Object,
197 on:
198 fragment(
199 "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
200 object.data,
201 a.data
202 )
203 )
204 |> preload([n, a, o], activity: {a, object: o})
205 |> Repo.all()
206 end
207
208 @spec read_one(User.t(), String.t()) ::
209 {:ok, Notification.t()} | {:error, Ecto.Changeset.t()} | nil
210 def read_one(%User{} = user, notification_id) do
211 with {:ok, %Notification{} = notification} <- get(user, notification_id) do
212 Multi.new()
213 |> Multi.update(:update, changeset(notification, %{seen: true}))
214 |> Marker.multi_set_unread_count(user, "notifications")
215 |> Repo.transaction()
216 |> case do
217 {:ok, %{update: notification}} -> {:ok, notification}
218 {:error, :update, changeset, _} -> {:error, changeset}
219 end
220 end
221 end
222
223 def get(%{id: user_id} = _user, id) do
224 query =
225 from(
226 n in Notification,
227 where: n.id == ^id,
228 join: activity in assoc(n, :activity),
229 preload: [activity: activity]
230 )
231
232 notification = Repo.one(query)
233
234 case notification do
235 %{user_id: ^user_id} ->
236 {:ok, notification}
237
238 _ ->
239 {:error, "Cannot get notification"}
240 end
241 end
242
243 def clear(user) do
244 from(n in Notification, where: n.user_id == ^user.id)
245 |> Repo.delete_all()
246 end
247
248 def destroy_multiple(%{id: user_id} = _user, ids) do
249 from(n in Notification,
250 where: n.id in ^ids,
251 where: n.user_id == ^user_id
252 )
253 |> Repo.delete_all()
254 end
255
256 def dismiss(%{id: user_id} = _user, id) do
257 notification = Repo.get(Notification, id)
258
259 case notification do
260 %{user_id: ^user_id} ->
261 Repo.delete(notification)
262
263 _ ->
264 {:error, "Cannot dismiss notification"}
265 end
266 end
267
268 def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
269 object = Object.normalize(activity)
270
271 unless object && object.data["type"] == "Answer" do
272 notifications =
273 activity
274 |> get_notified_from_activity()
275 |> Enum.map(&create_notification(activity, &1))
276
277 {:ok, notifications}
278 else
279 {:ok, []}
280 end
281 end
282
283 def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)
284 when type in ["Like", "Announce", "Follow"] do
285 notifications =
286 activity
287 |> get_notified_from_activity
288 |> Enum.map(&create_notification(activity, &1))
289
290 {:ok, notifications}
291 end
292
293 def create_notifications(_), do: {:ok, []}
294
295 # TODO move to sql, too.
296 def create_notification(%Activity{} = activity, %User{} = user) do
297 unless skip?(activity, user) do
298 {:ok, %{notification: notification}} =
299 Multi.new()
300 |> Multi.insert(:notification, %Notification{user_id: user.id, activity: activity})
301 |> Marker.multi_set_unread_count(user, "notifications")
302 |> Repo.transaction()
303
304 ["user", "user:notification"]
305 |> Streamer.stream(notification)
306
307 Push.send(notification)
308 notification
309 end
310 end
311
312 def get_notified_from_activity(activity, local_only \\ true)
313
314 def get_notified_from_activity(
315 %Activity{data: %{"to" => _, "type" => type} = _data} = activity,
316 local_only
317 )
318 when type in ["Create", "Like", "Announce", "Follow"] do
319 recipients =
320 []
321 |> Utils.maybe_notify_to_recipients(activity)
322 |> Utils.maybe_notify_mentioned_recipients(activity)
323 |> Utils.maybe_notify_subscribers(activity)
324 |> Enum.uniq()
325
326 User.get_users_from_set(recipients, local_only)
327 end
328
329 def get_notified_from_activity(_, _local_only), do: []
330
331 @spec skip?(Activity.t(), User.t()) :: boolean()
332 def skip?(activity, user) do
333 [
334 :self,
335 :followers,
336 :follows,
337 :non_followers,
338 :non_follows,
339 :recently_followed
340 ]
341 |> Enum.any?(&skip?(&1, activity, user))
342 end
343
344 @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
345 def skip?(:self, activity, user) do
346 activity.data["actor"] == user.ap_id
347 end
348
349 def skip?(
350 :followers,
351 activity,
352 %{notification_settings: %{"followers" => false}} = user
353 ) do
354 actor = activity.data["actor"]
355 follower = User.get_cached_by_ap_id(actor)
356 User.following?(follower, user)
357 end
358
359 def skip?(
360 :non_followers,
361 activity,
362 %{notification_settings: %{"non_followers" => false}} = user
363 ) do
364 actor = activity.data["actor"]
365 follower = User.get_cached_by_ap_id(actor)
366 !User.following?(follower, user)
367 end
368
369 def skip?(:follows, activity, %{notification_settings: %{"follows" => false}} = user) do
370 actor = activity.data["actor"]
371 followed = User.get_cached_by_ap_id(actor)
372 User.following?(user, followed)
373 end
374
375 def skip?(
376 :non_follows,
377 activity,
378 %{notification_settings: %{"non_follows" => false}} = user
379 ) do
380 actor = activity.data["actor"]
381 followed = User.get_cached_by_ap_id(actor)
382 !User.following?(user, followed)
383 end
384
385 def skip?(:recently_followed, %{data: %{"type" => "Follow"}} = activity, user) do
386 actor = activity.data["actor"]
387
388 Notification.for_user(user)
389 |> Enum.any?(fn
390 %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
391 _ -> false
392 end)
393 end
394
395 def skip?(_, _, _), do: false
396 end