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