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