1use crate::{
2 establish_connection,
3 player::{PlayerRole, PlayerState},
4 schema::{notifications, players, subscriptions},
5 vapid_keys,
6};
7use chrono::NaiveDateTime;
8use diesel::{
9 ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl,
10 backend::Backend,
11 deserialize::{self, FromSql, FromSqlRow},
12 expression::AsExpression,
13 mysql::Mysql,
14 prelude::{Insertable, Queryable},
15 serialize::{self, IsNull, Output, ToSql},
16 sql_types::Text,
17};
18use log::info;
19use serde::{Deserialize, Serialize};
20use uuid::Uuid;
21use web_push::{
22 IsahcWebPushClient, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder, WebPushClient,
23 WebPushMessageBuilder,
24};
25
26#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, FromSqlRow, AsExpression)]
28#[diesel(sql_type = Text)]
29pub enum PlayerGroups {
30 All,
31 Humans,
32 Zombies,
33 Mods,
34 Individuals(String),
35}
36
37impl FromSql<Text, Mysql> for PlayerGroups {
38 fn from_sql<'a>(string: <Mysql as Backend>::RawValue<'a>) -> deserialize::Result<Self> {
40 match str::from_utf8(string.as_bytes())? {
41 "All" => Ok(PlayerGroups::All),
42 "Humans" => Ok(PlayerGroups::Humans),
43 "Zombies" => Ok(PlayerGroups::Zombies),
44 "Mods" => Ok(PlayerGroups::Mods),
45 str => Ok(PlayerGroups::Individuals(str.to_string())),
46 }
47 }
48}
49
50impl ToSql<Text, Mysql> for PlayerGroups {
51 fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Mysql>) -> serialize::Result {
53 match self {
54 PlayerGroups::All => <str as ToSql<Text, Mysql>>::to_sql("All", out)?,
55 PlayerGroups::Humans => <str as ToSql<Text, Mysql>>::to_sql("Humans", out)?,
56 PlayerGroups::Zombies => <str as ToSql<Text, Mysql>>::to_sql("Zombies", out)?,
57 PlayerGroups::Mods => <str as ToSql<Text, Mysql>>::to_sql("Mods", out)?,
58 PlayerGroups::Individuals(id) => <String as ToSql<Text, Mysql>>::to_sql(id, out)?,
59 };
60 Ok(IsNull::No)
61 }
62}
63
64#[derive(Debug, Serialize, Deserialize, Insertable, Clone, Queryable)]
65#[diesel(table_name = crate::schema::subscriptions)]
66#[diesel(check_for_backend(diesel::mysql::Mysql))]
67pub struct PushSubscription {
68 pub id: String,
69 pub player_id: String,
70 pub endpoint: String,
71 pub p256dh: String,
72 pub auth: String,
73 pub date: NaiveDateTime,
74}
75
76impl PushSubscription {
77 pub async fn new(player_id: String, endpoint: String, p256dh: String, auth: String) -> Self {
78 PushSubscription {
79 id: Uuid::new_v4().to_string(),
80 player_id,
81 endpoint,
82 p256dh,
83 auth,
84 date: chrono::Utc::now().naive_utc(),
85 }
86 }
87
88 pub async fn save(&self) -> Result<Self, String> {
89 let connection = &mut establish_connection();
90
91 diesel::insert_into(subscriptions::table)
92 .values(self.clone())
93 .execute(connection)
94 .expect("Failed to save subscription");
95
96 Ok(self.clone())
97 }
98
99 pub async fn get_subscription(player_id: String) -> Result<Option<Self>, String> {
100 let connection = &mut establish_connection();
101
102 subscriptions::table
103 .filter(subscriptions::player_id.eq(player_id))
104 .first::<PushSubscription>(connection)
105 .optional()
106 .map_err(|e| format!("Failed to get push subscription: {}", e))
107 }
108
109 pub async fn get_all_subscriptions(
110 player_group: PlayerGroups,
111 ) -> Result<Option<Vec<Self>>, String> {
112 let connection = &mut establish_connection();
113
114 let player_ids = match player_group {
115 PlayerGroups::All => players::table
116 .select(players::id)
117 .load::<String>(connection)
118 .map_err(|e| format!("Failed to load players: {}", e)),
119 PlayerGroups::Humans => players::table
120 .filter(players::state.eq(PlayerState::Human))
121 .select(players::id)
122 .load::<String>(connection)
123 .map_err(|e| format!("Failed to load players: {}", e)),
124 PlayerGroups::Zombies => players::table
125 .filter(players::state.eq(PlayerState::Zombie))
126 .select(players::id)
127 .load::<String>(connection)
128 .map_err(|e| format!("Failed to load players: {}", e)),
129 PlayerGroups::Mods => players::table
130 .filter(players::role.eq(PlayerRole::Mod))
131 .select(players::id)
132 .load::<String>(connection)
133 .map_err(|e| format!("Failed to load players: {}", e)),
134 PlayerGroups::Individuals(id) => players::table
135 .filter(players::id.eq(id))
136 .select(players::id)
137 .load::<String>(connection)
138 .map_err(|e| format!("Failed to load players: {}", e)),
139 }?;
140
141 subscriptions::table
142 .filter(subscriptions::player_id.eq_any(player_ids))
143 .load::<PushSubscription>(connection)
144 .optional()
145 .map_err(|e| format!("Failed to get push subscription: {}", e))
146 }
147
148 pub async fn unsubscribe(player_id: String) -> Result<usize, String> {
149 let connection = &mut establish_connection();
150
151 diesel::delete(subscriptions::table.filter(subscriptions::player_id.eq(player_id)))
152 .execute(connection)
153 .map_err(|e| format!("Failed to unsubscribe player: {}", e))
154 }
155}
156
157#[derive(Debug, Serialize, Deserialize, Insertable, Clone)]
159#[diesel(table_name = crate::schema::notifications)]
160#[diesel(check_for_backend(diesel::mysql::Mysql))]
161pub struct Notification {
162 pub id: String,
163 pub player: String,
164 pub title: String,
165 pub content: String,
166 pub player_group: PlayerGroups,
167 pub date: NaiveDateTime,
168}
169
170impl Notification {
171 pub async fn new(
172 player: String,
173 title: String,
174 content: String,
175 player_groups: PlayerGroups,
176 ) -> Self {
177 Notification {
178 id: Uuid::new_v4().to_string(),
179 player,
180 title,
181 content,
182 player_group: player_groups,
183 date: chrono::Utc::now().naive_utc(),
184 }
185 }
186
187 pub async fn save(&self) -> Result<Notification, String> {
188 let connection = &mut establish_connection();
189
190 diesel::insert_into(notifications::table)
191 .values(self.clone())
192 .execute(connection)
193 .expect("Error inserting notification");
194
195 info!("Notification {:?} created successfully", self.id);
196
197 Ok(self.clone())
198 }
199
200 pub async fn send_push_notification(&self) -> Result<String, String> {
201 let (vapid_private_key, _, vapid_email) = vapid_keys()?;
202
203 let subscriptions =
204 match PushSubscription::get_all_subscriptions(self.player_group.clone()).await? {
205 Some(subscriptions) => subscriptions,
206 None => {
207 return Err("No push subscriptions".to_string());
208 }
209 };
210
211 for sub in subscriptions {
212 let subscription_info = SubscriptionInfo {
213 endpoint: sub.endpoint,
214 keys: SubscriptionKeys {
215 p256dh: sub.p256dh,
216 auth: sub.auth,
217 },
218 };
219 let mut builder = WebPushMessageBuilder::new(&subscription_info);
220 let payload = serde_json::json!({
221 "title": self.title,
222 "content": self.content
223 })
224 .to_string();
225 builder.set_payload(web_push::ContentEncoding::AesGcm, payload.as_bytes());
226
227 let mut signature_builder =
228 VapidSignatureBuilder::from_base64(&vapid_private_key, &subscription_info)
229 .map_err(|e| e.to_string())?;
230 signature_builder.add_claim("sub", vapid_email.clone());
231
232 builder.set_vapid_signature(signature_builder.build().map_err(|e| e.to_string())?);
233
234 let client = IsahcWebPushClient::new().map_err(|e| e.to_string())?;
235
236 client
237 .send(builder.build().map_err(|e| e.to_string())?)
238 .await
239 .map_err(|e| e.to_string())?;
240 }
241 Ok(String::from("Notifications sent successfully"))
242 }
243}