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