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, 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    /// Get the profile picture for a player
484    ///
485    /// # Arguments
486    /// * `self` - The player
487    ///
488    /// # Returns
489    /// The profile picture for the player, or an error if it could not be opened
490    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    /// Get a player by their ID
513    ///
514    /// # Arguments
515    /// * `id` - The ID of the player
516    ///
517    /// # Returns
518    /// The player with the given ID, or None if no player was found
519    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    /// Get a player by their alias
537    ///
538    /// # Arguments
539    /// * `alias` - The alias of the player
540    ///
541    /// # Returns
542    /// The player with the given alias, or None if no player was found
543    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    /// Get the alias of a player
561    ///
562    /// # Arguments
563    /// * `id` - The ID of the player
564    ///
565    /// # Returns
566    /// The alias of the player, or an error if the player was not found
567    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    /// Get all players
585    ///
586    /// # Returns
587    /// A vector of all players, or an error if the query failed
588    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    /// Gets the checkpoints location
601    ///
602    /// Method only available to human players.
603    ///
604    /// # Returns
605    /// The location of the checkpoint
606    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    /// Check if a player is a zombie
623    ///
624    /// This is checked from the PlayerState property.
625    ///
626    /// # Returns
627    /// True if the player is a zombie, false otherwise
628    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    /// Ensure that a player is a zombie
636    ///
637    /// Acts as a guard for the zombie.rs module.
638    ///
639    /// # Returns
640    /// Ok if the player is a zombie, Err otherwise
641    ///
642    /// # Usage
643    /// This function is used to ensure that a player is a zombie before performing certain actions:
644    ///
645    /// ```
646    /// pub async fn zombie_func() {
647    ///     self.ensure_zombie().await?;
648    ///     // The rest of the function
649    /// }
650    /// ```
651    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    /// Ensure that a player is a human
662    ///
663    /// Acts as a guard for the human-only functions.
664    ///
665    /// # Returns
666    /// Ok if the player is a human, Err otherwise.
667    ///
668    /// # Usage
669    /// This function is used to ensure that a player is a human before performing certain actions:
670    ///
671    /// ```
672    /// pub async fn human_func() {
673    ///     self.ensure_human().await?;
674    ///     // The rest of the function
675    /// }
676    /// ```
677    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    /// Check if the player is an admin
688    ///
689    /// This is checked from the PlayerRole property.
690    ///
691    /// # Returns
692    /// True if the player is an admin, false otherwise
693    pub async fn is_admin(&self) -> bool {
694        self.role == PlayerRole::Mod || self.role == PlayerRole::Other
695    }
696
697    /// Ensure that a player is an admin
698    ///
699    /// Acts as a guard for the admin.rs module.
700    ///
701    /// # Returns
702    /// Ok if the player is an admin, Err otherwise
703    ///
704    /// # Usage
705    /// This function is used to ensure that a player is an admin before performing certain actions:
706    ///
707    /// ```
708    /// pub fn admin_func() {
709    ///     self.ensure_admin().await?;
710    ///     // The rest of the function
711    /// }
712    /// ```
713    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    /// Delete a player by ID
722    ///
723    /// # Arguments
724    /// * `id` - The ID of the player to delete
725    ///
726    /// # Returns
727    /// Ok if the player was deleted, Err otherwise
728    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}