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 |> join(:left, [n, a], mutated_activity in Pleroma.Activity,
146 on:
147 fragment("?->>'context'", a.data) ==
148 fragment("?->>'context'", mutated_activity.data) and
149 fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
150 fragment("?->>'type'", mutated_activity.data) == "Create",
151 as: :mutated_activity
152 )
153 |> where(
154 [n, a, mutated_activity: mutated_activity],
155 not fragment(
156 """
157 CASE WHEN (?->>'type') = 'Like' or (?->>'type') = 'Announce'
158 THEN (activity_visibility(?, ?, ?) = ANY (?))
159 ELSE (activity_visibility(?, ?, ?) = ANY (?)) END
160 """,
161 a.data,
162 a.data,
163 mutated_activity.actor,
164 mutated_activity.recipients,
165 mutated_activity.data,
166 ^visibility,
167 a.actor,
168 a.recipients,
169 a.data,
170 ^visibility
171 )
172 )
173 else
174 Logger.error("Could not exclude visibility to #{visibility}")
175 query
176 end
177 end
178
179 defp exclude_visibility(query, %{exclude_visibilities: visibility})
180 when visibility in @valid_visibilities do
181 exclude_visibility(query, [visibility])
182 end
183
184 defp exclude_visibility(query, %{exclude_visibilities: visibility})
185 when visibility not in @valid_visibilities do
186 Logger.error("Could not exclude visibility to #{visibility}")
187 query
188 end
189
190 defp exclude_visibility(query, _visibility), do: query
191
192 def for_user(user, opts \\ %{}) do
193 user
194 |> for_user_query(opts)
195 |> Pagination.fetch_paginated(opts)
196 end
197
198 @doc """
199 Returns notifications for user received since given date.
200
201 ## Examples
202
203 iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-13 11:22:33])
204 [%Pleroma.Notification{}, %Pleroma.Notification{}]
205
206 iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-15 11:22:33])
207 []
208 """
209 @spec for_user_since(Pleroma.User.t(), NaiveDateTime.t()) :: [t()]
210 def for_user_since(user, date) do
211 from(n in for_user_query(user),
212 where: n.updated_at > ^date
213 )
214 |> Repo.all()
215 end
216
217 def set_read_up_to(%{id: user_id} = user, id) do
218 query =
219 from(
220 n in Notification,
221 where: n.user_id == ^user_id,
222 where: n.id <= ^id,
223 where: n.seen == false,
224 # Ideally we would preload object and activities here
225 # but Ecto does not support preloads in update_all
226 select: n.id
227 )
228
229 {:ok, %{ids: {_, notification_ids}}} =
230 Multi.new()
231 |> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
232 |> Marker.multi_set_unread_count(user, "notifications")
233 |> Repo.transaction()
234
235 Notification
236 |> where([n], n.id in ^notification_ids)
237 |> join(:inner, [n], activity in assoc(n, :activity))
238 |> join(:left, [n, a], object in Object,
239 on:
240 fragment(
241 "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
242 object.data,
243 a.data
244 )
245 )
246 |> preload([n, a, o], activity: {a, object: o})
247 |> Repo.all()
248 end
249
250 @spec read_one(User.t(), String.t()) ::
251 {:ok, Notification.t()} | {:error, Ecto.Changeset.t()} | nil
252 def read_one(%User{} = user, notification_id) do
253 with {:ok, %Notification{} = notification} <- get(user, notification_id) do
254 Multi.new()
255 |> Multi.update(:update, changeset(notification, %{seen: true}))
256 |> Marker.multi_set_unread_count(user, "notifications")
257 |> Repo.transaction()
258 |> case do
259 {:ok, %{update: notification}} -> {:ok, notification}
260 {:error, :update, changeset, _} -> {:error, changeset}
261 end
262 end
263 end
264
265 def get(%{id: user_id} = _user, id) do
266 query =
267 from(
268 n in Notification,
269 where: n.id == ^id,
270 join: activity in assoc(n, :activity),
271 preload: [activity: activity]
272 )
273
274 notification = Repo.one(query)
275
276 case notification do
277 %{user_id: ^user_id} ->
278 {:ok, notification}
279
280 _ ->
281 {:error, "Cannot get notification"}
282 end
283 end
284
285 def clear(user) do
286 from(n in Notification, where: n.user_id == ^user.id)
287 |> Repo.delete_all()
288 end
289
290 def destroy_multiple(%{id: user_id} = _user, ids) do
291 from(n in Notification,
292 where: n.id in ^ids,
293 where: n.user_id == ^user_id
294 )
295 |> Repo.delete_all()
296 end
297
298 def dismiss(%{id: user_id} = _user, id) do
299 notification = Repo.get(Notification, id)
300
301 case notification do
302 %{user_id: ^user_id} ->
303 Repo.delete(notification)
304
305 _ ->
306 {:error, "Cannot dismiss notification"}
307 end
308 end
309
310 def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
311 object = Object.normalize(activity)
312
313 unless object && object.data["type"] == "Answer" do
314 notifications =
315 activity
316 |> get_notified_from_activity()
317 |> Enum.map(&create_notification(activity, &1))
318
319 {:ok, notifications}
320 else
321 {:ok, []}
322 end
323 end
324
325 def create_notifications(%Activity{data: %{"type" => type}} = activity)
326 when type in ["Like", "Announce", "Follow", "Move", "EmojiReaction"] do
327 notifications =
328 activity
329 |> get_notified_from_activity()
330 |> Enum.map(&create_notification(activity, &1))
331
332 {:ok, notifications}
333 end
334
335 def create_notifications(_), do: {:ok, []}
336
337 # TODO move to sql, too.
338 def create_notification(%Activity{} = activity, %User{} = user) do
339 unless skip?(activity, user) do
340 {:ok, %{notification: notification}} =
341 Multi.new()
342 |> Multi.insert(:notification, %Notification{user_id: user.id, activity: activity})
343 |> Marker.multi_set_unread_count(user, "notifications")
344 |> Repo.transaction()
345
346 ["user", "user:notification"]
347 |> Streamer.stream(notification)
348
349 Push.send(notification)
350 notification
351 end
352 end
353
354 def get_notified_from_activity(activity, local_only \\ true)
355
356 def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
357 when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReaction"] do
358 []
359 |> Utils.maybe_notify_to_recipients(activity)
360 |> Utils.maybe_notify_mentioned_recipients(activity)
361 |> Utils.maybe_notify_subscribers(activity)
362 |> Utils.maybe_notify_followers(activity)
363 |> Enum.uniq()
364 |> User.get_users_from_set(local_only)
365 end
366
367 def get_notified_from_activity(_, _local_only), do: []
368
369 @spec skip?(Activity.t(), User.t()) :: boolean()
370 def skip?(activity, user) do
371 [
372 :self,
373 :followers,
374 :follows,
375 :non_followers,
376 :non_follows,
377 :recently_followed
378 ]
379 |> Enum.any?(&skip?(&1, activity, user))
380 end
381
382 @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
383 def skip?(:self, activity, user) do
384 activity.data["actor"] == user.ap_id
385 end
386
387 def skip?(
388 :followers,
389 activity,
390 %{notification_settings: %{followers: false}} = user
391 ) do
392 actor = activity.data["actor"]
393 follower = User.get_cached_by_ap_id(actor)
394 User.following?(follower, user)
395 end
396
397 def skip?(
398 :non_followers,
399 activity,
400 %{notification_settings: %{non_followers: false}} = user
401 ) do
402 actor = activity.data["actor"]
403 follower = User.get_cached_by_ap_id(actor)
404 !User.following?(follower, user)
405 end
406
407 def skip?(:follows, activity, %{notification_settings: %{follows: false}} = user) do
408 actor = activity.data["actor"]
409 followed = User.get_cached_by_ap_id(actor)
410 User.following?(user, followed)
411 end
412
413 def skip?(
414 :non_follows,
415 activity,
416 %{notification_settings: %{non_follows: false}} = user
417 ) do
418 actor = activity.data["actor"]
419 followed = User.get_cached_by_ap_id(actor)
420 !User.following?(user, followed)
421 end
422
423 def skip?(:recently_followed, %{data: %{"type" => "Follow"}} = activity, user) do
424 actor = activity.data["actor"]
425
426 Notification.for_user(user)
427 |> Enum.any?(fn
428 %{activity: %{data: %{"type" => "Follow", "actor" => ^actor}}} -> true
429 _ -> false
430 end)
431 end
432
433 def skip?(_, _, _), do: false
434 end