Merge branch 'develop' into issue/1276
[akkoma] / lib / pleroma / notification.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 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 last_read_query(User.t()) :: Ecto.Queryable.t()
42 def last_read_query(user) do
43 from(q in Pleroma.Notification,
44 where: q.user_id == ^user.id,
45 where: q.seen == true,
46 select: type(q.id, :string),
47 limit: 1,
48 order_by: [desc: :id]
49 )
50 end
51
52 defp for_user_query_ap_id_opts(user, opts) do
53 ap_id_relations =
54 [:block] ++
55 if opts[@include_muted_option], do: [], else: [:notification_mute]
56
57 preloaded_ap_ids = User.outgoing_relations_ap_ids(user, ap_id_relations)
58
59 exclude_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts)
60
61 exclude_notification_muted_opts =
62 Map.merge(%{notification_muted_users_ap_ids: preloaded_ap_ids[:notification_mute]}, opts)
63
64 {exclude_blocked_opts, exclude_notification_muted_opts}
65 end
66
67 def for_user_query(user, opts \\ %{}) do
68 {exclude_blocked_opts, exclude_notification_muted_opts} =
69 for_user_query_ap_id_opts(user, opts)
70
71 Notification
72 |> where(user_id: ^user.id)
73 |> where(
74 [n, a],
75 fragment(
76 "? not in (SELECT ap_id FROM users WHERE deactivated = 'true')",
77 a.actor
78 )
79 )
80 |> join(:inner, [n], activity in assoc(n, :activity))
81 |> join(:left, [n, a], object in Object,
82 on:
83 fragment(
84 "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
85 object.data,
86 a.data
87 )
88 )
89 |> preload([n, a, o], activity: {a, object: o})
90 |> exclude_notification_muted(user, exclude_notification_muted_opts)
91 |> exclude_blocked(user, exclude_blocked_opts)
92 |> exclude_visibility(opts)
93 |> exclude_move(opts)
94 end
95
96 defp exclude_blocked(query, user, opts) do
97 blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
98
99 query
100 |> where([n, a], a.actor not in ^blocked_ap_ids)
101 |> where(
102 [n, a],
103 fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.domain_blocks
104 )
105 end
106
107 defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do
108 query
109 end
110
111 defp exclude_notification_muted(query, user, opts) do
112 notification_muted_ap_ids =
113 opts[:notification_muted_users_ap_ids] || User.notification_muted_users_ap_ids(user)
114
115 query
116 |> where([n, a], a.actor not in ^notification_muted_ap_ids)
117 |> join(:left, [n, a], tm in Pleroma.ThreadMute,
118 on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
119 )
120 |> where([n, a, o, tm], is_nil(tm.user_id))
121 end
122
123 defp exclude_move(query, %{with_move: true}) do
124 query
125 end
126
127 defp exclude_move(query, _opts) do
128 where(query, [n, a], fragment("?->>'type' != 'Move'", a.data))
129 end
130
131 @valid_visibilities ~w[direct unlisted public private]
132
133 defp exclude_visibility(query, %{exclude_visibilities: visibility})
134 when is_list(visibility) do
135 if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
136 query
137 |> join(:left, [n, a], mutated_activity in Pleroma.Activity,
138 on:
139 fragment("?->>'context'", a.data) ==
140 fragment("?->>'context'", mutated_activity.data) and
141 fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
142 fragment("?->>'type'", mutated_activity.data) == "Create",
143 as: :mutated_activity
144 )
145 |> where(
146 [n, a, mutated_activity: mutated_activity],
147 not fragment(
148 """
149 CASE WHEN (?->>'type') = 'Like' or (?->>'type') = 'Announce'
150 THEN (activity_visibility(?, ?, ?) = ANY (?))
151 ELSE (activity_visibility(?, ?, ?) = ANY (?)) END
152 """,
153 a.data,
154 a.data,
155 mutated_activity.actor,
156 mutated_activity.recipients,
157 mutated_activity.data,
158 ^visibility,
159 a.actor,
160 a.recipients,
161 a.data,
162 ^visibility
163 )
164 )
165 else
166 Logger.error("Could not exclude visibility to #{visibility}")
167 query
168 end
169 end
170
171 defp exclude_visibility(query, %{exclude_visibilities: visibility})
172 when visibility in @valid_visibilities do
173 exclude_visibility(query, [visibility])
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_last_read_id(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_last_read_id(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", "EmojiReact"] 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_last_read_id(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", "EmojiReact"] 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