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