1use std::{fs, io::Write, path::Path};
2
3use actix_files::NamedFile;
4use actix_multipart::Multipart;
5use actix_web::web;
6use chrono::{NaiveDate, NaiveDateTime, Utc};
7use diesel::{
8 ExpressionMethods, QueryDsl, RunQueryDsl, Selectable,
9 backend::Backend,
10 deserialize::{self, FromSql, FromSqlRow},
11 expression::AsExpression,
12 mysql::Mysql,
13 prelude::{Insertable, Queryable, QueryableByName},
14 serialize::{self, IsNull, Output, ToSql},
15 sql_types::Text,
16};
17use futures::{StreamExt, TryStreamExt};
18use log::{debug, info, warn};
19use password_auth::{generate_hash, verify_password};
20use serde::{Deserialize, Serialize};
21
22use crate::{
23 establish_connection,
24 schema::players,
25 settings::{CHECKPOINT, Settings, UPLOAD_PATH},
26 time::CustomDateTime,
27 token::Token,
28};
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, AsExpression, FromSqlRow)]
32#[diesel(sql_type = Text)]
33pub enum PlayerState {
34 Human,
35 Zombie,
36 Zombie0,
37 Revived,
38 Corpse,
39 Other,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, AsExpression, FromSqlRow)]
44#[diesel(sql_type = Text)]
45pub enum PlayerRole {
46 Player,
47 Mod,
48 Other,
49}
50
51impl ToSql<Text, Mysql> for PlayerState {
52 fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Mysql>) -> serialize::Result {
54 match self {
55 PlayerState::Human => <str as ToSql<Text, Mysql>>::to_sql("Human", out)?,
56 PlayerState::Zombie => <str as ToSql<Text, Mysql>>::to_sql("Zombie", out)?,
57 PlayerState::Zombie0 => <str as ToSql<Text, Mysql>>::to_sql("Zombie0", out)?,
58 PlayerState::Revived => <str as ToSql<Text, Mysql>>::to_sql("Revived", out)?,
59 PlayerState::Corpse => <str as ToSql<Text, Mysql>>::to_sql("Corpse", out)?,
60 PlayerState::Other => <str as ToSql<Text, Mysql>>::to_sql("Other", out)?,
61 };
62 Ok(IsNull::No)
63 }
64}
65
66impl FromSql<Text, Mysql> for PlayerState {
67 fn from_sql<'a>(string: <Mysql as Backend>::RawValue<'a>) -> deserialize::Result<Self> {
69 match str::from_utf8(string.as_bytes())? {
70 "Human" => Ok(PlayerState::Human),
71 "Zombie" => Ok(PlayerState::Zombie),
72 "Zombie0" => Ok(PlayerState::Zombie0),
73 "Revived" => Ok(PlayerState::Revived),
74 "Corpse" => Ok(PlayerState::Corpse),
75 "Other" => Ok(PlayerState::Other),
76 _ => Err(format!("Invalid PlayerState string value").into()),
77 }
78 }
79}
80
81impl ToSql<Text, Mysql> for PlayerRole {
82 fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Mysql>) -> serialize::Result {
84 match self {
85 PlayerRole::Player => <str as ToSql<Text, Mysql>>::to_sql("Player", out)?,
86 PlayerRole::Mod => <str as ToSql<Text, Mysql>>::to_sql("Mod", out)?,
87 PlayerRole::Other => <str as ToSql<Text, Mysql>>::to_sql("Other", out)?,
88 };
89 Ok(IsNull::No)
90 }
91}
92
93impl FromSql<Text, Mysql> for PlayerRole {
94 fn from_sql<'a>(string: <Mysql as Backend>::RawValue<'a>) -> deserialize::Result<Self> {
96 match str::from_utf8(string.as_bytes())? {
97 "Player" => Ok(PlayerRole::Player),
98 "Mod" => Ok(PlayerRole::Mod),
99 "Other" => Ok(PlayerRole::Other),
100 _ => Err(format!("Invalid PlayerRole string value").into()),
101 }
102 }
103}
104
105#[derive(
111 Clone, Debug, Serialize, Deserialize, Selectable, Queryable, Insertable, QueryableByName,
112)]
113#[diesel(table_name = crate::schema::players)]
114#[diesel(check_for_backend(diesel::mysql::Mysql))]
115pub struct Player {
116 pub id: String,
117 pub name: String,
118 pub surname: String,
119 pub bio: String,
120 pub pronouns: String,
121 pub alias: String,
122 pub niu: String,
123 pub email: String,
124 pub phone: String,
125 pub faculty: String,
126 pub isuab: bool,
127 pub how_did_you_hear_about_us: String,
128 pub social_media: Option<String>,
129 pub password: String,
130 pub player_number: i32,
131 pub state: PlayerState,
132 pub role: PlayerRole,
133 pub birth_date: NaiveDate,
134 pub accept_info_other_editions: bool,
135 pub next_feeding: Option<CustomDateTime>,
136 pub last_login: Option<NaiveDateTime>,
137 pub checkpoint: Option<NaiveDateTime>,
138 pub wants_to_be_zombie: i32,
139 pub register_date: Option<NaiveDateTime>,
140}
141
142impl Player {
144 pub async fn new(
164 name: String,
165 surname: String,
166 pronouns: String,
167 alias: String,
168 niu: String,
169 email: String,
170 phone: String,
171 faculty: String,
172 isuab: bool,
173 how_did_you_hear_about_us: String,
174 social_media: Option<String>,
175 password: String,
176 birth_date: NaiveDate,
177 accept_info_other_editions: bool,
178 wants_to_be_zombie: i32,
179 ) -> Self {
180 info!(
181 "Creating new player with name \"{}\" and alias \"{}\"",
182 name, alias
183 );
184
185 Player {
186 id: uuid::Uuid::new_v4().to_string(),
187 name,
188 surname,
189 bio: String::new(),
190 pronouns,
191 alias,
192 niu,
193 email,
194 phone,
195 faculty,
196 isuab,
197 how_did_you_hear_about_us,
198 social_media,
199 password: generate_hash(password),
200 player_number: Player::get_next_player_number(),
201 state: PlayerState::Human,
202 role: PlayerRole::Player,
203 birth_date,
204 accept_info_other_editions,
205 next_feeding: None,
206 last_login: Some(Utc::now().naive_utc()),
207 checkpoint: None,
208 wants_to_be_zombie,
209 register_date: Some(Utc::now().naive_utc()),
210 }
211 }
212
213 fn get_next_player_number() -> i32 {
220 let connection = &mut establish_connection();
221
222 let max_number = players::table
223 .count()
224 .get_result::<i64>(connection)
225 .expect("Error loading max player number");
226
227 debug!("Next player number: {}", max_number);
228
229 max_number as i32
230 }
231
232 pub async fn signup(&self) -> Result<Player, String> {
239 let connection = &mut establish_connection();
240
241 debug!("Adding player {:?} to the database", self.name);
242
243 diesel::insert_into(players::table)
244 .values(self.clone())
245 .execute(connection)
246 .expect("Error inserting player");
247
248 info!("Player {:?} added successfully", self.name);
249
250 Ok(self.clone())
251 }
252
253 pub async fn login(username: &str, password: String) -> Result<Player, String> {
262 let connection = &mut establish_connection();
263
264 debug!("Player with username \"{}\" is logging in", username);
265
266 let player_result = players::table
268 .filter(players::alias.eq(&username))
269 .or_filter(players::email.eq(&username))
270 .or_filter(players::niu.eq(&username))
271 .first::<Player>(connection);
272
273 let player = match player_result {
274 Ok(p) => p,
275 Err(_) => {
276 return Err("Login failed: user not found".to_string());
277 }
278 };
279
280 debug!(
281 "Verifying password for player with username \"{}\"",
282 username
283 );
284 match verify_password(&password, &player.password) {
286 Ok(()) => {
287 info!("Player \"{}\" logged in successfully", username);
288 let login_date = Utc::now().naive_utc();
289
290 diesel::update(players::table.filter(players::id.eq(&player.id)))
291 .set((players::last_login.eq(login_date),))
292 .execute(connection)
293 .expect("Failed to update last_login and password hash");
294
295 return Ok(player);
296 }
297 Err(_) => {
298 warn!("Incorrect password attempted for player: {}", username);
299 return Err("Login failed: incorrect password".to_string());
300 }
301 };
302 }
303
304 pub async fn logout(&self) -> Result<(), String> {
312 debug!("Logging out player: {}", self.alias);
313
314 Token::clear_player_tokens(self)?;
315
316 info!("Player \"{}\" logged out successfully", self.alias);
317
318 Ok(())
319 }
320
321 pub async fn change_password(
330 &self,
331 old_password: String,
332 new_password: String,
333 ) -> Result<(), String> {
334 let connection = &mut establish_connection();
335
336 debug!("Changing password for player: {}", self.alias);
337 match verify_password(&old_password, &self.password) {
338 Ok(_) => {
339 diesel::update(players::table.filter(players::id.eq(&self.id)))
340 .set(players::password.eq(generate_hash(new_password)))
341 .execute(connection)
342 .expect("Failed to update password hash");
343
344 info!("Password changed for player: {}", self.alias);
345
346 return Ok(());
347 }
348 Err(_) => return Err("Incorrect old password".to_string()),
349 }
350 }
351
352 pub async fn edit(
366 &self,
367 bio: String,
368 pronouns: String,
369 alias: String,
370 email: String,
371 phone: String,
372 social_media: Option<String>,
373 accept_info_other_editions: bool,
374 ) -> Result<(), String> {
375 let connection = &mut establish_connection();
376
377 debug!("Updating player: {}", self.alias);
378
379 diesel::update(players::table.filter(players::id.eq(&self.id)))
380 .set((
381 players::bio.eq(if bio.is_empty() {
382 self.bio.clone()
383 } else {
384 bio
385 }),
386 players::pronouns.eq(if pronouns.is_empty() {
387 self.pronouns.clone()
388 } else {
389 pronouns
390 }),
391 players::alias.eq(if alias.is_empty() {
392 self.alias.clone()
393 } else {
394 alias
395 }),
396 players::email.eq(if email.is_empty() {
397 self.email.clone()
398 } else {
399 email
400 }),
401 players::phone.eq(if phone.is_empty() {
402 self.phone.clone()
403 } else {
404 phone
405 }),
406 players::social_media.eq(if social_media.is_none() {
407 self.social_media.clone()
408 } else {
409 social_media
410 }),
411 players::accept_info_other_editions.eq(accept_info_other_editions),
412 ))
413 .execute(connection)
414 .expect("Failed to update player");
415
416 info!("Player edited: {}", self.alias);
417
418 Ok(())
419 }
420
421 pub async fn set_pfp(&self, payload: &mut Multipart) -> Result<String, String> {
428 while let Ok(Some(mut field)) = payload.try_next().await {
429 if let Some(content_disposition) = field.content_disposition() {
430 let filename = content_disposition.get_filename().map(ToOwned::to_owned);
431
432 if let Some(filename) = filename {
433 let ext = Path::new(&filename)
435 .extension()
436 .and_then(|e| e.to_str())
437 .ok_or("No file extension found")?;
438
439 let pattern = format!("{}/{}.*", UPLOAD_PATH, self.id);
441 for entry in glob::glob(&pattern).unwrap().flatten() {
442 let _ = fs::remove_file(entry);
443 }
444
445 let filepath = format!("{}/{}.{}", UPLOAD_PATH, self.id, ext);
447 if fs::metadata(&filepath).is_ok() {
449 fs::remove_file(&filepath)
450 .map_err(|err| format!("Failed to remove file: {}", err))?;
451 }
452 let mut file = web::block(|| {
453 fs::File::create(filepath)
454 .map_err(|err| format!("Failed to create file: {}", err))
455 })
456 .await
457 .map_err(|err| format!("Failed to create file: {}", err))??;
458
459 while let Some(chunk) = field.next().await {
460 match chunk {
461 Ok(chunk) => {
462 file = web::block(move || {
463 file.write_all(&chunk)
464 .map_err(|err| format!("Failed to write file: {}", err))
465 .unwrap();
466 file
467 })
468 .await
469 .map_err(|err| format!("Failed to write file: {}", err))?;
470 }
471 Err(err) => {
472 return Err(format!("Failed to read chunk: {}", err));
473 }
474 }
475 }
476 return Ok(filename);
477 }
478 }
479 }
480 Err("No file was uploaded.".to_string())
481 }
482
483 pub async fn get_pfp(&self) -> Result<NamedFile, String> {
491 debug!("Getting profile picture for player: {}", self.id);
492 if let Ok(entries) = fs::read_dir(UPLOAD_PATH) {
493 for entry in entries {
494 if let Ok(entry) = entry {
495 let path = entry.path();
496 if let Some(stem) = path.file_stem() {
497 if stem == self.id.as_str() {
498 match NamedFile::open(path) {
499 Ok(file) => return Ok(file),
500 Err(err) => {
501 return Err(format!("Failed to open profile picture: {}", err));
502 }
503 }
504 }
505 }
506 }
507 }
508 }
509 Err("No profile picture found".to_string())
510 }
511
512 pub async fn get_player(id: String) -> Result<Option<Player>, String> {
520 let connection = &mut establish_connection();
521
522 debug!("Getting player with ID: {}", id);
523 let player = players::table
524 .filter(players::id.eq(id))
525 .first::<Player>(connection);
526
527 match player {
528 Ok(player) => {
529 info!("Player found, username: {}", player.alias);
530 Ok(Some(player))
531 }
532 Err(err) => Err(format!("Failed to get player: {}", err)),
533 }
534 }
535
536 pub async fn get_player_by_alias(alias: String) -> Result<Option<Player>, String> {
544 let connection = &mut establish_connection();
545
546 debug!("Getting player with alias: {}", alias);
547 let player = players::table
548 .filter(players::alias.eq(alias))
549 .first::<Player>(connection);
550
551 match player {
552 Ok(player) => {
553 info!("Player found, username: {}", player.alias);
554 Ok(Some(player))
555 }
556 Err(err) => Err(format!("Failed to get player: {}", err)),
557 }
558 }
559
560 pub fn get_player_alias(id: String) -> Result<String, String> {
568 let connection = &mut establish_connection();
569
570 debug!("Getting the player's alias from the id: {}", id);
571 let player = players::table
572 .filter(players::id.eq(id))
573 .first::<Player>(connection);
574
575 match player {
576 Ok(player) => {
577 info!("Player found, username: {}", player.alias);
578 Ok(player.alias)
579 }
580 Err(err) => Err(err.to_string()),
581 }
582 }
583
584 pub async fn get_all_players() -> Result<Vec<Player>, String> {
589 let connection = &mut establish_connection();
590
591 info!("All players requested.");
592 let players = players::table.load::<Player>(connection);
593
594 match players {
595 Ok(players) => Ok(players),
596 Err(err) => Err(format!("Failed to load players: {}", err)),
597 }
598 }
599
600 pub async fn get_checkpoint(&self) -> Result<(i32, i32), String> {
607 self.ensure_human().await?;
608
609 debug!("Checkpoint location requested by {}", self.alias);
610 Settings::get_settings::<String>(CHECKPOINT)
611 .await
612 .map(|value: String| {
613 let parts: Vec<&str> = value.split_whitespace().collect();
614 (
615 parts[0].parse::<i32>().unwrap(),
616 parts[1].parse::<i32>().unwrap(),
617 )
618 })
619 .map_err(|err| err.to_string())
620 }
621
622 pub async fn is_zombie(&self) -> bool {
629 self.state == PlayerState::Zombie
630 || self.state == PlayerState::Zombie0
631 || self.state == PlayerState::Corpse
632 || self.state == PlayerState::Other
633 }
634
635 pub async fn ensure_zombie(&self) -> Result<(), String> {
652 if self.is_admin().await {
653 Ok(())
654 } else if self.is_zombie().await {
655 Ok(())
656 } else {
657 Err("Access Denied: This action can only be performed by a zombie.".to_string())
658 }
659 }
660
661 pub async fn ensure_human(&self) -> Result<(), String> {
678 if self.is_admin().await {
679 Ok(())
680 } else if !self.is_zombie().await {
681 Ok(())
682 } else {
683 Err("Access Denied: This action can only be performed by a human".to_string())
684 }
685 }
686
687 pub async fn is_admin(&self) -> bool {
694 self.role == PlayerRole::Mod || self.role == PlayerRole::Other
695 }
696
697 pub async fn ensure_admin(&self) -> Result<(), String> {
714 if self.is_admin().await {
715 Ok(())
716 } else {
717 Err("Access Denied: This action can only be performed by an admin.".to_string())
718 }
719 }
720
721 pub async fn delete_player(id: String) -> Result<usize, String> {
729 let connection = &mut establish_connection();
730
731 diesel::delete(players::table.filter(players::id.eq(id)))
732 .execute(connection)
733 .map_err(|e| format!("Failed to delete player: {}", e))
734 }
735}