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