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