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