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