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