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