nil_server_database/model/
game.rs1use crate::Database;
5use crate::error::{Error, Result};
6use crate::sql_types::duration::Duration;
7use crate::sql_types::game_id::GameId;
8use crate::sql_types::hashed_password::HashedPassword;
9use crate::sql_types::id::UserId;
10use crate::sql_types::version::Version;
11use crate::sql_types::zoned::Zoned;
12use diesel::prelude::*;
13use nil_core::world::World;
14use nil_crypto::password::Password;
15use nil_server_types::round::RoundDuration;
16use std::fmt;
17use tokio::task::spawn_blocking;
18
19#[derive(Identifiable, Queryable, Selectable, Clone)]
20#[diesel(table_name = crate::schema::game)]
21#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
22#[diesel(belongs_to(UserData, foreign_key = created_by))]
23pub struct Game {
24 pub id: GameId,
25 pub password: Option<HashedPassword>,
26 pub description: Option<String>,
27 pub round_duration: Option<Duration>,
28 pub created_by: UserId,
29 pub created_at: Zoned,
30 pub updated_at: Zoned,
31}
32
33impl From<GameWithBlob> for Game {
34 fn from(game: GameWithBlob) -> Self {
35 Self {
36 id: game.id,
37 password: game.password,
38 description: game.description,
39 round_duration: game.round_duration,
40 created_by: game.created_by,
41 created_at: game.created_at,
42 updated_at: game.updated_at,
43 }
44 }
45}
46
47impl fmt::Debug for Game {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 f.debug_struct("Game")
50 .field("id", &self.id.to_string())
51 .field("round_duration", &self.round_duration)
52 .field("created_by", &self.created_by)
53 .field("created_at", &self.created_at.to_string())
54 .field("updated_at", &self.updated_at.to_string())
55 .finish_non_exhaustive()
56 }
57}
58
59#[derive(Identifiable, Queryable, Selectable, Clone)]
60#[diesel(table_name = crate::schema::game)]
61#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
62#[diesel(belongs_to(UserData, foreign_key = created_by))]
63pub struct GameWithBlob {
64 pub id: GameId,
65 pub password: Option<HashedPassword>,
66 pub description: Option<String>,
67 pub round_duration: Option<Duration>,
68 pub server_version: Version,
69 pub created_by: UserId,
70 pub created_at: Zoned,
71 pub updated_at: Zoned,
72 pub world_blob: Vec<u8>,
73}
74
75impl GameWithBlob {
76 #[inline]
77 pub fn to_world(&self) -> Result<World> {
78 Ok(World::load(&self.world_blob)?)
79 }
80}
81
82impl fmt::Debug for GameWithBlob {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 f.debug_struct("GameWithBlob")
85 .field("id", &self.id.to_string())
86 .field("round_duration", &self.round_duration)
87 .field("created_by", &self.created_by)
88 .field("created_at", &self.created_at.to_string())
89 .field("updated_at", &self.updated_at.to_string())
90 .finish_non_exhaustive()
91 }
92}
93
94#[derive(Insertable, Clone)]
95#[diesel(table_name = crate::schema::game)]
96pub struct NewGame {
97 id: GameId,
98 password: Option<HashedPassword>,
99 description: Option<String>,
100 round_duration: Option<Duration>,
101 server_version: Version,
102 created_by: UserId,
103 created_at: Zoned,
104 updated_at: Zoned,
105 world_blob: Vec<u8>,
106}
107
108#[bon::bon]
109impl NewGame {
110 #[builder]
111 pub async fn new(
112 #[builder(start_fn, into)] id: GameId,
113 #[builder(start_fn)] blob: Vec<u8>,
114 password: Option<Password>,
115 mut description: Option<String>,
116 round_duration: Option<RoundDuration>,
117 #[builder(into)] server_version: Version,
118 created_by: UserId,
119 ) -> Result<Self> {
120 if let Some(description) = &mut description {
121 while description.len() > 1000 {
122 description.pop();
123 }
124 }
125
126 Ok(Self {
127 id,
128 password: hash_password(password).await?,
129 description,
130 round_duration: round_duration.map(Into::into),
131 server_version,
132 created_by,
133 created_at: Zoned::now(),
134 updated_at: Zoned::now(),
135 world_blob: blob,
136 })
137 }
138
139 #[inline]
140 pub fn blob(&self) -> &[u8] {
141 &self.world_blob
142 }
143
144 #[inline]
145 pub async fn create(self, database: &Database) -> Result<usize> {
146 database.create_game(self).await
147 }
148}
149
150impl fmt::Debug for NewGame {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 f.debug_struct("NewGame")
153 .field("id", &self.id.to_string())
154 .field("round_duration", &self.round_duration)
155 .field("server_version", &self.server_version)
156 .field("created_by", &self.created_by)
157 .field("created_at", &self.created_at.to_string())
158 .field("updated_at", &self.updated_at.to_string())
159 .finish_non_exhaustive()
160 }
161}
162
163async fn hash_password(password: Option<Password>) -> Result<Option<HashedPassword>> {
164 let Some(password) = password else { return Ok(None) };
165 let pass_len = password.trim().chars().count();
166
167 if pass_len == 0 {
168 return Ok(None);
169 } else if !(3..=50).contains(&pass_len) {
170 return Err(Error::InvalidPassword);
171 }
172
173 spawn_blocking(move || HashedPassword::new(&password))
174 .await?
175 .map(Some)
176 .map_err(Into::into)
177}