Skip to main content

hvz/
player.rs

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, OptionalExtension, 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/// Represents the state of a player.
31#[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/// Represents the role of a player.
43#[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    /// Converts a PlayerState to a SQL text value.
53    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    /// Converts a SQL text value to a PlayerState.
68    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    /// Converts a PlayerRole to a SQL text value.
83    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    /// Converts a SQL text value to a PlayerRole.
95    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/// Player struct
106///
107/// This is the main struct of the application.
108///
109/// It contains all the information about a player.
110#[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
142/// Implement Player struct
143impl Player {
144    /// Create a new player
145    ///
146    /// # Arguments
147    /// * `name` - The player's name
148    /// * `surname` - The player's surname
149    /// * `pronouns` - The player's pronouns
150    /// * `alias` - The player's alias
151    /// * `niu` - The player's NIU
152    /// * `email` - The player's email
153    /// * `social_media` - The player's social media
154    /// * `phone` - The player's phone number
155    /// * `faculty` - The player's faculty
156    /// * `isuab` - Whether the player is from UAB or not
157    /// * `how_did_you_hear_about_us` - How the player heard about us
158    /// * `password` - The player's password
159    /// * `birth_date` - The player's birth date
160    ///
161    /// # Returns
162    /// A new player instance
163    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    /// Get the next player number
214    ///
215    /// This is used to assign a unique number to each player.
216    ///
217    /// # Returns
218    /// The next player number
219    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    /// Sign up a new player
233    ///
234    /// Adds a new player to the database.
235    ///
236    /// # Returns
237    /// The newly created player
238    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    /// Login a player
254    ///
255    /// # Arguments
256    /// * `username` - The username of the player
257    /// * `password` - The password of the player
258    ///
259    /// # Returns
260    /// The logged in player
261    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        // Try to find the player by username (alias or email)
267        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        // Verify password
285        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    /// Logs out a player
305    ///
306    /// # Arguments
307    /// * `self` - The player to log out
308    ///
309    /// # Returns
310    /// The result of the logout operation
311    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    /// Change the password of a player
322    ///
323    /// # Arguments
324    /// * `old_password` - The old password of the player
325    /// * `new_password` - The new password of the player
326    ///
327    /// # Returns
328    /// The result of the password change
329    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    /// Edit player information.
353    ///
354    /// # Arguments
355    /// * `pronouns` - The player's pronouns.
356    /// * `alias` - The player's alias.
357    /// * `email` - The player's email.
358    /// * `phone` - The player's phone number.
359    /// * `social_media` - The player's social media links.
360    /// * `accept_info_other_editions` - Whether the player accepts information about other editions.
361    ///
362    /// # Returns
363    /// * `Ok(())` if the player information was updated successfully.
364    /// * `Err(String)` if there was an error updating the player information.
365    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    /// Set the profile picture of a player
422    ///
423    /// # Arguments
424    /// * `field` - The new profile picture of the player
425    ///
426    /// # Returns
427    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                    // Extract extension
434                    let ext = Path::new(&filename)
435                        .extension()
436                        .and_then(|e| e.to_str())
437                        .ok_or("No file extension found")?;
438
439                    // Remove any existing file for this user
440                    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                    // Save new file
446                    let filepath = format!("{}/{}.{}", UPLOAD_PATH, self.id, ext);
447                    // Save to a file in the current directory
448                    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    /// Remove the profile picture for a player
484    ///
485    /// # Arguments
486    /// * `self` - The player
487    ///
488    /// # Returns
489    /// An error if the profile picture could not be removed
490    pub async fn remove_pfp(&self) -> Result<(), String> {
491        let pattern = format!("{}/{}.*", UPLOAD_PATH, self.id);
492        for entry in glob::glob(&pattern).unwrap().flatten() {
493            if fs::exists(&entry).is_err() {
494                return Err("Profile picture does not exist.".to_string());
495            }
496            let _ = fs::remove_file(entry);
497        }
498        Ok(())
499    }
500
501    /// Get the profile picture for a player
502    ///
503    /// # Arguments
504    /// * `self` - The player
505    ///
506    /// # Returns
507    /// The profile picture for the player, or an error if it could not be opened
508    pub async fn get_pfp(&self) -> Result<NamedFile, String> {
509        debug!("Getting profile picture for player: {}", self.id);
510        if let Ok(entries) = fs::read_dir(UPLOAD_PATH) {
511            for entry in entries {
512                if let Ok(entry) = entry {
513                    let path = entry.path();
514                    if let Some(stem) = path.file_stem() {
515                        if stem == self.id.as_str() {
516                            match NamedFile::open(path) {
517                                Ok(file) => return Ok(file),
518                                Err(err) => {
519                                    return Err(format!("Failed to open profile picture: {}", err));
520                                }
521                            }
522                        }
523                    }
524                }
525            }
526        }
527        Err("No profile picture found".to_string())
528    }
529
530    /// Get a player by their ID
531    ///
532    /// # Arguments
533    /// * `id` - The ID of the player
534    ///
535    /// # Returns
536    /// The player with the given ID, or None if no player was found
537    pub async fn get_player(id: String) -> Result<Option<Player>, String> {
538        let connection = &mut establish_connection();
539
540        debug!("Getting player with ID: {}", id);
541        let player = players::table
542            .filter(players::id.eq(id))
543            .first::<Player>(connection)
544            .optional();
545
546        match player {
547            Ok(Some(player)) => {
548                info!("Player found, username: {}", player.alias);
549                Ok(Some(player))
550            }
551            Ok(None) => {
552                info!("Player not found");
553                Ok(None)
554            }
555            Err(err) => Err(format!("Failed to get player: {}", err)),
556        }
557    }
558
559    /// Get a player by their alias
560    ///
561    /// # Arguments
562    /// * `alias` - The alias of the player
563    ///
564    /// # Returns
565    /// The player with the given alias, or None if no player was found
566    pub async fn get_player_by_alias(alias: String) -> Result<Option<Player>, String> {
567        let connection = &mut establish_connection();
568
569        debug!("Getting player with alias: {}", alias);
570        let player = players::table
571            .filter(players::alias.eq(alias))
572            .first::<Player>(connection);
573
574        match player {
575            Ok(player) => {
576                info!("Player found, username: {}", player.alias);
577                Ok(Some(player))
578            }
579            Err(err) => Err(format!("Failed to get player: {}", err)),
580        }
581    }
582
583    /// Get the alias of a player
584    ///
585    /// # Arguments
586    /// * `id` - The ID of the player
587    ///
588    /// # Returns
589    /// The alias of the player, or an error if the player was not found
590    pub fn get_player_alias(id: String) -> Result<String, String> {
591        let connection = &mut establish_connection();
592
593        debug!("Getting the player's alias from the id: {}", id);
594        let player = players::table
595            .filter(players::id.eq(id))
596            .first::<Player>(connection);
597
598        match player {
599            Ok(player) => {
600                info!("Player found, username: {}", player.alias);
601                Ok(player.alias)
602            }
603            Err(err) => Err(err.to_string()),
604        }
605    }
606
607    /// Get all players
608    ///
609    /// # Returns
610    /// A vector of all players, or an error if the query failed
611    pub async fn get_all_players() -> Result<Vec<Player>, String> {
612        let connection = &mut establish_connection();
613
614        info!("All players requested.");
615        let players = players::table.load::<Player>(connection);
616
617        match players {
618            Ok(players) => Ok(players),
619            Err(err) => Err(format!("Failed to load players: {}", err)),
620        }
621    }
622
623    /// Check if a player is a zombie
624    ///
625    /// This is checked from the PlayerState property.
626    ///
627    /// # Returns
628    /// True if the player is a zombie, false otherwise
629    pub async fn is_zombie(&self) -> bool {
630        self.state == PlayerState::Zombie
631            || self.state == PlayerState::Zombie0
632            || self.state == PlayerState::Corpse
633            || self.state == PlayerState::Other
634    }
635
636    /// Ensure that a player is a zombie
637    ///
638    /// Acts as a guard for the zombie.rs module.
639    ///
640    /// # Returns
641    /// Ok if the player is a zombie, Err otherwise
642    ///
643    /// # Usage
644    /// This function is used to ensure that a player is a zombie before performing certain actions:
645    ///
646    /// ```
647    /// pub async fn zombie_func() {
648    ///     self.ensure_zombie().await?;
649    ///     // The rest of the function
650    /// }
651    /// ```
652    pub async fn ensure_zombie(&self) -> Result<(), String> {
653        if self.is_admin().await {
654            Ok(())
655        } else if self.is_zombie().await {
656            Ok(())
657        } else {
658            Err("Access Denied: This action can only be performed by a zombie.".to_string())
659        }
660    }
661
662    /// Ensure that a player is a human
663    ///
664    /// Acts as a guard for the human-only functions.
665    ///
666    /// # Returns
667    /// Ok if the player is a human, Err otherwise.
668    ///
669    /// # Usage
670    /// This function is used to ensure that a player is a human before performing certain actions:
671    ///
672    /// ```
673    /// pub async fn human_func() {
674    ///     self.ensure_human().await?;
675    ///     // The rest of the function
676    /// }
677    /// ```
678    pub async fn ensure_human(&self) -> Result<(), String> {
679        if self.is_admin().await {
680            Ok(())
681        } else if !self.is_zombie().await {
682            Ok(())
683        } else {
684            Err("Access Denied: This action can only be performed by a human".to_string())
685        }
686    }
687
688    /// Check if the player is an admin
689    ///
690    /// This is checked from the PlayerRole property.
691    ///
692    /// # Returns
693    /// True if the player is an admin, false otherwise
694    pub async fn is_admin(&self) -> bool {
695        self.role == PlayerRole::Mod || self.role == PlayerRole::Other
696    }
697
698    /// Ensure that a player is an admin
699    ///
700    /// Acts as a guard for the admin.rs module.
701    ///
702    /// # Returns
703    /// Ok if the player is an admin, Err otherwise
704    ///
705    /// # Usage
706    /// This function is used to ensure that a player is an admin before performing certain actions:
707    ///
708    /// ```
709    /// pub fn admin_func() {
710    ///     self.ensure_admin().await?;
711    ///     // The rest of the function
712    /// }
713    /// ```
714    pub async fn ensure_admin(&self) -> Result<(), String> {
715        if self.is_admin().await {
716            Ok(())
717        } else {
718            Err("Access Denied: This action can only be performed by an admin.".to_string())
719        }
720    }
721
722    /// Delete a player by ID
723    ///
724    /// # Arguments
725    /// * `id` - The ID of the player to delete
726    ///
727    /// # Returns
728    /// Ok if the player was deleted, Err otherwise
729    pub async fn delete_player(id: String) -> Result<usize, String> {
730        let connection = &mut establish_connection();
731
732        diesel::delete(players::table.filter(players::id.eq(id)))
733            .execute(connection)
734            .map_err(|e| format!("Failed to delete player: {}", e))
735    }
736}