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