Merge remote-tracking branch 'remotes/upstream/develop' into 1335-user-api-id-fields...
[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: %{"to" => _, "type" => type}} = activity)
280 when type in ["Like", "Announce", "Follow"] do
281 users = get_notified_from_activity(activity)
282 notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
283 {:ok, notifications}
284 end
285
286 def create_notifications(_), do: {:ok, []}
287
288 # TODO move to sql, too.
289 def create_notification(%Activity{} = activity, %User{} = user) do
290 unless skip?(activity, user) do
291 notification = %Notification{user_id: user.id, activity: activity}
292 {:ok, notification} = Repo.insert(notification)
293
294 ["user", "user:notification"]
295 |> Streamer.stream(notification)
296
297 Push.send(notification)
298 notification
299 end
300 end
301
302 def get_notified_from_activity(activity, local_only \\ true)
303
304 def get_notified_from_activity(
305 %Activity{data: %{"to" => _, "type" => type} = _data} = activity,
306 local_only
307 )
308 when type in ["Create", "Like", "Announce", "Follow"] do
309 recipients =
310 []
311 |> Utils.maybe_notify_to_recipients(activity)
312 |> Utils.maybe_notify_mentioned_recipients(activity)
313 |> Utils.maybe_notify_subscribers(activity)
314 |> Enum.uniq()
315
316 User.get_users_from_set(recipients, local_only)
317 end
318
319 def get_notified_from_activity(_, _local_only), do: []
320
321 @spec skip?(Activity.t(), User.t()) :: boolean()
322 def skip?(activity, user) do
323 [
324 :self,
325 :followers,
326 :follows,
327 :non_followers,
328 :non_follows,
329 :recently_followed
330 ]
331 |> Enum.any?(&skip?(&1, activity, user))
332 end
333
334 @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
335 def skip?(:self, activity, user) do
336 activity.data["actor"] == user.ap_id
337 end
338
339 def skip?(
340 :followers,
341 activity,
342 %{notification_settings: %{"followers" => false}} = user
343 ) do
344 actor = activity.data["actor"]
345 follower = User.get_cached_by_ap_id(actor)
346 User.following?(follower, user)
347 end
348
349 def skip?(
350 :non_followers,
351 activity,
352 %{notification_settings: %{"non_followers" => false}} = user
353 ) do
354 actor = activity.data["actor"]
355 follower = User.get_cached_by_ap_id(actor)
356 !User.following?(follower, user)
357 end
358
359 def skip?(:follows, activity, %{notification_settings: %{"follows" => false}} = user) do
360 actor = activity.data["actor"]
361 followed = User.get_cached_by_ap_id(actor)
362 User.following?(user, followed)
363 end
364
365 def skip?(
366 :non_follows,
367 activity,
368 %{notification_settings: %{"non_follows" => false}} = user
369 ) do
370 actor = activity.data["actor"]
371 followed = User.get_cached_by_ap_id(actor)
372 !User.following?(user, followed)
373 end
374
375 def skip?(:recently_followed, %{data: %{"type" => "Follow"}} = activity, user) do
376 actor = activity.data["actor"]
377
378 Notification.for_user(user)
379 |> Enum.any?(fn
380 %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
381 _ -> false
382 end)
383 end
384
385 def skip?(_, _, _), do: false
386 end