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