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