d6149cd0da2bf8500bab549e7b3fd0efc063fa6e
[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: %{"type" => type}} = activity)
284 when type in ["Like", "Announce", "Follow", "Move"] 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(%Activity{data: %{"type" => type}} = activity, local_only)
315 when type in ["Create", "Like", "Announce", "Follow", "Move"] do
316 []
317 |> Utils.maybe_notify_to_recipients(activity)
318 |> Utils.maybe_notify_mentioned_recipients(activity)
319 |> Utils.maybe_notify_subscribers(activity)
320 |> Utils.maybe_notify_followers(activity)
321 |> Enum.uniq()
322 |> User.get_users_from_set(local_only)
323 end
324
325 def get_notified_from_activity(_, _local_only), do: []
326
327 @spec skip?(Activity.t(), User.t()) :: boolean()
328 def skip?(activity, user) do
329 [
330 :self,
331 :followers,
332 :follows,
333 :non_followers,
334 :non_follows,
335 :recently_followed
336 ]
337 |> Enum.any?(&skip?(&1, activity, user))
338 end
339
340 @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
341 def skip?(:self, activity, user) do
342 activity.data["actor"] == user.ap_id
343 end
344
345 def skip?(
346 :followers,
347 activity,
348 %{notification_settings: %{"followers" => false}} = user
349 ) do
350 actor = activity.data["actor"]
351 follower = User.get_cached_by_ap_id(actor)
352 User.following?(follower, user)
353 end
354
355 def skip?(
356 :non_followers,
357 activity,
358 %{notification_settings: %{"non_followers" => false}} = user
359 ) do
360 actor = activity.data["actor"]
361 follower = User.get_cached_by_ap_id(actor)
362 !User.following?(follower, user)
363 end
364
365 def skip?(:follows, activity, %{notification_settings: %{"follows" => false}} = user) do
366 actor = activity.data["actor"]
367 followed = User.get_cached_by_ap_id(actor)
368 User.following?(user, followed)
369 end
370
371 def skip?(
372 :non_follows,
373 activity,
374 %{notification_settings: %{"non_follows" => false}} = user
375 ) do
376 actor = activity.data["actor"]
377 followed = User.get_cached_by_ap_id(actor)
378 !User.following?(user, followed)
379 end
380
381 def skip?(:recently_followed, %{data: %{"type" => "Follow"}} = activity, user) do
382 actor = activity.data["actor"]
383
384 Notification.for_user(user)
385 |> Enum.any?(fn
386 %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
387 _ -> false
388 end)
389 end
390
391 def skip?(_, _, _), do: false
392 end