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