nil_server_database/impl/
game.rs1use crate::error::{Error, Result};
5use crate::model::game::{Game, GameWithBlob, NewGame};
6use crate::sql_types::duration::db_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::player_id::db_PlayerId;
11use crate::sql_types::zoned::db_Zoned;
12use crate::{Database, conn};
13use diesel::prelude::*;
14use diesel::result::Error as DieselError;
15use diesel_async::RunQueryDsl;
16use nil_crypto::password::Password;
17use tap::Pipe;
18
19macro_rules! decl_get {
20 ($fn_name:ident, $model:ident) => {
21 pub async fn $fn_name(&self, id: impl Into<GameId>) -> Result<$model> {
22 use $crate::schema::game;
23
24 let id: GameId = id.into();
25 let result = game::table
26 .find(&id)
27 .select($model::as_select())
28 .first(conn!(self))
29 .await;
30
31 if let Err(DieselError::NotFound) = &result {
32 Err(Error::GameNotFound(id))
33 } else {
34 Ok(result?)
35 }
36 }
37 };
38}
39
40macro_rules! decl_get_all {
41 ($fn_name:ident, $model:ident) => {
42 pub async fn $fn_name(&self) -> Result<Vec<$model>> {
43 use $crate::schema::game;
44
45 game::table
46 .select($model::as_select())
47 .load(conn!(self))
48 .await
49 .map_err(Into::into)
50 }
51 };
52}
53
54impl Database {
55 decl_get!(get_game, Game);
56 decl_get!(get_game_with_blob, GameWithBlob);
57
58 decl_get_all!(get_games, Game);
59 decl_get_all!(get_games_with_blob, GameWithBlob);
60
61 pub async fn count_games(&self) -> Result<i64> {
62 use crate::schema::game;
63
64 game::table
65 .count()
66 .get_result(conn!(self))
67 .await
68 .map_err(Into::into)
69 }
70
71 pub async fn create_game(&self, new: &NewGame) -> Result<usize> {
72 use crate::schema::game;
73
74 diesel::insert_into(game::table)
75 .values(new)
76 .on_conflict(game::id)
77 .do_update()
78 .set((
79 game::world_blob.eq(new.blob()),
80 game::updated_at.eq(db_Zoned::now()),
81 ))
82 .execute(conn!(self))
83 .await
84 .map_err(Into::into)
85 }
86
87 pub async fn delete_game(&self, id: impl Into<GameId>) -> Result<usize> {
88 use crate::schema::game;
89
90 let id: GameId = id.into();
91 diesel::delete(game::table.find(id))
92 .execute(conn!(self))
93 .await
94 .map_err(Into::into)
95 }
96
97 pub async fn delete_games(&self, ids: &[GameId]) -> Result<usize> {
98 use crate::schema::game;
99
100 if ids.is_empty() {
101 Ok(0)
102 } else {
103 diesel::delete(game::table.filter(game::id.eq_any(ids)))
104 .execute(conn!(self))
105 .await
106 .map_err(Into::into)
107 }
108 }
109
110 pub async fn game_exists(&self, id: GameId) -> Result<bool> {
111 use crate::schema::game;
112 use diesel::dsl::{exists, select};
113
114 select(exists(game::table.find(id)))
115 .get_result(conn!(self))
116 .await
117 .map_err(Into::into)
118 }
119
120 pub async fn get_game_owner(&self, id: GameId) -> Result<db_PlayerId> {
121 use crate::schema::game;
122
123 let user_id = game::table
124 .find(&id)
125 .select(game::created_by)
126 .first::<UserId>(conn!(self))
127 .await?;
128
129 self.get_user_player_id(user_id).await
130 }
131
132 pub async fn get_game_ids(&self) -> Result<Vec<GameId>> {
133 use crate::schema::game;
134
135 game::table
136 .select(game::id)
137 .load(conn!(self))
138 .await
139 .map_err(Into::into)
140 }
141
142 pub async fn get_game_password(&self, id: GameId) -> Result<Option<HashedPassword>> {
143 use crate::schema::game;
144
145 let result = game::table
146 .find(&id)
147 .select(game::password)
148 .first(conn!(self))
149 .await;
150
151 if let Err(DieselError::NotFound) = &result {
152 Err(Error::GameNotFound(id))
153 } else {
154 Ok(result?)
155 }
156 }
157
158 pub async fn get_game_round_duration(&self, id: GameId) -> Result<Option<db_Duration>> {
159 use crate::schema::game;
160
161 let result = game::table
162 .find(&id)
163 .select(game::round_duration)
164 .first(conn!(self))
165 .await;
166
167 if let Err(DieselError::NotFound) = &result {
168 Err(Error::GameNotFound(id))
169 } else {
170 Ok(result?)
171 }
172 }
173
174 pub async fn update_game_blob(&self, id: GameId, blob: &[u8]) -> Result<usize> {
175 use crate::schema::game;
176
177 let n = diesel::update(game::table.find(&id))
178 .set((
179 game::world_blob.eq(blob),
180 game::updated_at.eq(db_Zoned::now()),
181 ))
182 .execute(conn!(self))
183 .await?;
184
185 if n == 0 { Err(Error::GameNotFound(id)) } else { Ok(n) }
186 }
187
188 pub async fn verify_game_password(
189 &self,
190 id: impl Into<GameId>,
191 password: Option<&Password>,
192 ) -> Result<bool> {
193 let id: GameId = id.into();
194 if let Some(hash) = self.get_game_password(id).await? {
195 password
196 .filter(|it| !it.trim().is_empty())
197 .is_some_and(|it| matches!(hash.verify(it), Ok(true)))
198 .pipe(Ok)
199 } else {
200 Ok(true)
201 }
202 }
203
204 pub async fn is_game_owned_by(
205 &self,
206 game_id: impl Into<GameId>,
207 player_id: impl Into<db_PlayerId>,
208 ) -> Result<bool> {
209 let game_id: GameId = game_id.into();
210 let player_id: db_PlayerId = player_id.into();
211 let owner = self.get_game_owner(game_id).await?;
212 Ok(owner == player_id)
213 }
214}