1use crate::{
2 auth::{AuthInfo, AuthInfoData, GetAuthInfo},
3 common::Storage,
4 db_storage::models::{
5 DieselAuthInfo, DieselAuthInfoEmail, DieselAuthInfoGithub, DieselGame, DieselGamePlayer,
6 DieselGameScore, DieselPlayer,
7 },
8 env::{ENV_PG_URL, ENV_REDIS_URL},
9};
10use async_trait::async_trait;
11use diesel::pg::PgConnection;
12use diesel::prelude::*;
13use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
14use mahjong_core::{GameId, PlayerId, Players};
15use redis::Commands;
16use service_contracts::{ServiceGame, ServicePlayer, ServicePlayerGame};
17use tracing::debug;
18
19pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
20
21use self::{
22 models::{
23 DieselAuthInfoAnonymous, DieselGameBoard, DieselGameDrawWall, DieselGameHand,
24 DieselGameSettings,
25 },
26 models_translation::DieselGameExtra,
27};
28
29mod models;
30mod models_translation;
31mod schema;
32
33pub struct DBStorage {
34 db_path: String,
35 redis_path: String,
36}
37
38#[async_trait]
39impl Storage for DBStorage {
40 async fn get_auth_info(&self, get_auth_info: GetAuthInfo) -> Result<Option<AuthInfo>, String> {
41 let mut connection = PgConnection::establish(&self.db_path).unwrap();
42
43 match get_auth_info {
44 GetAuthInfo::EmailUsername(username) => {
45 DieselAuthInfoEmail::get_info_by_username(&mut connection, &username)
46 }
47 GetAuthInfo::GithubUsername(username) => {
48 DieselAuthInfoGithub::get_info_by_username(&mut connection, &username)
49 }
50 GetAuthInfo::PlayerId(player_id) => {
51 DieselAuthInfo::get_info_by_id(&mut connection, &player_id)
52 }
53 GetAuthInfo::AnonymousToken(id_token) => {
54 DieselAuthInfoAnonymous::get_info_by_hashed_token(&mut connection, &id_token)
55 }
56 }
57 }
58
59 async fn get_player_total_score(&self, player_id: &PlayerId) -> Result<i32, String> {
60 let mut connection = PgConnection::establish(&self.db_path).unwrap();
61
62 let total_score = DieselGameScore::read_total_from_player(&mut connection, player_id);
63
64 Ok(total_score)
65 }
66
67 async fn save_auth_info(&self, auth_info: &AuthInfo) -> Result<(), String> {
68 use schema::auth_info::table;
69 use schema::auth_info_anonymous::table as anonymous_table;
70 use schema::auth_info_email::table as email_table;
71 use schema::auth_info_github::table as github_table;
72
73 let mut connection = PgConnection::establish(&self.db_path).unwrap();
74 let diesel_auth_info = DieselAuthInfo::from_raw(auth_info);
75
76 diesel::insert_into(table)
77 .values(&diesel_auth_info)
78 .execute(&mut connection)
79 .unwrap();
80
81 match auth_info.data {
82 AuthInfoData::Email(ref email) => {
83 let diesel_auth_info_email = DieselAuthInfoEmail::from_raw(email);
84
85 diesel::insert_into(email_table)
86 .values(&diesel_auth_info_email)
87 .execute(&mut connection)
88 .unwrap();
89 }
90 AuthInfoData::Github(ref github) => {
91 let diesel_auth_info_github = DieselAuthInfoGithub::from_raw(github);
92
93 diesel::insert_into(github_table)
94 .values(&diesel_auth_info_github)
95 .execute(&mut connection)
96 .unwrap();
97 }
98 AuthInfoData::Anonymous(ref anonymous) => {
99 let diesel_auth_info_anonymous = DieselAuthInfoAnonymous::from_raw(anonymous);
100
101 diesel::insert_into(anonymous_table)
102 .values(&diesel_auth_info_anonymous)
103 .execute(&mut connection)
104 .unwrap();
105 }
106 }
107
108 Ok(())
109 }
110
111 async fn save_game(&self, service_game: &ServiceGame) -> Result<(), String> {
112 let mut connection = PgConnection::establish(&self.db_path).unwrap();
113 let redis_client = redis::Client::open(self.redis_path.clone()).unwrap();
114 let mut redis_connection = redis_client.get_connection().unwrap();
115
116 let game_str = serde_json::to_string(&service_game).unwrap();
117 let redis_key = format!("game:{}", service_game.game.id);
118
119 let _: () = redis_connection.set(redis_key.clone(), game_str).unwrap();
120 let _: () = redis_connection.expire(redis_key, 60 * 60).unwrap();
121
122 DieselPlayer::update_from_game(&mut connection, service_game);
123
124 let diesel_game_extra = DieselGameExtra {
125 created_at: chrono::DateTime::from_timestamp_millis(service_game.created_at)
126 .unwrap()
127 .naive_utc(),
128 game: service_game.game.clone(),
129 updated_at: chrono::DateTime::from_timestamp_millis(service_game.updated_at)
130 .unwrap()
131 .naive_utc(),
132 };
133
134 let diesel_game = DieselGame::from_raw(&diesel_game_extra);
135
136 diesel_game.update(&mut connection);
137
138 let diesel_game_players = DieselGamePlayer::from_game(&service_game.game);
139 DieselGamePlayer::update(&mut connection, &diesel_game_players, &service_game.game);
140
141 DieselGameScore::update_from_game(&mut connection, service_game);
142 DieselGameBoard::update_from_game(&mut connection, service_game);
143 DieselGameDrawWall::update_from_game(&mut connection, service_game);
144 DieselGameHand::update_from_game(&mut connection, service_game);
145 DieselGameSettings::update_from_game(&mut connection, service_game);
146
147 Ok(())
148 }
149
150 async fn get_game(&self, id: &GameId, use_cache: bool) -> Result<Option<ServiceGame>, String> {
151 let redis_client = redis::Client::open(self.redis_path.clone()).unwrap();
152 let mut redis_connection = redis_client.get_connection().unwrap();
153
154 let redis_key = format!("game:{}", id);
155
156 if use_cache {
157 let game_str: Option<String> = redis_connection.get(redis_key).unwrap();
158
159 if let Some(game_str) = game_str {
160 let game: ServiceGame = serde_json::from_str(&game_str).unwrap();
161
162 return Ok(Some(game));
163 }
164 } else {
165 redis_connection.del::<String, bool>(redis_key).unwrap();
166 }
167
168 let mut connection = PgConnection::establish(&self.db_path).unwrap();
169
170 let result = DieselGame::read_from_id(&mut connection, id);
171
172 if result.is_none() {
173 return Ok(None);
174 }
175
176 let game_players = DieselGamePlayer::read_from_game(&mut connection, id);
177 let players = DieselPlayer::read_from_ids(&mut connection, &game_players);
178
179 let score = DieselGameScore::read_from_game(&mut connection, id);
180 let board = DieselGameBoard::read_from_game(&mut connection, id);
181 let draw_wall = DieselGameDrawWall::read_from_game(&mut connection, id);
182 let (hands, bonus_tiles) = DieselGameHand::read_from_game(&mut connection, id);
183 let settings = DieselGameSettings::read_from_game(&mut connection, id);
184
185 if settings.is_none() {
186 return Ok(None);
187 }
188
189 let game_extra = result.unwrap();
190 let mut game = game_extra.game;
191 game.players = Players(game_players);
192 game.score = score;
193 game.table.hands = hands;
194 game.table.bonus_tiles = bonus_tiles;
195 game.table.board = board;
196 game.table.draw_wall = draw_wall;
197
198 let service_game = ServiceGame {
199 created_at: game_extra.created_at.and_utc().timestamp_millis(),
200 game,
201 players,
202 settings: settings.unwrap(),
203 updated_at: game_extra.updated_at.and_utc().timestamp_millis(),
204 };
205
206 Ok(Some(service_game))
207 }
208
209 async fn get_player_games(
210 &self,
211 player_id: &Option<PlayerId>,
212 ) -> Result<Vec<ServicePlayerGame>, String> {
213 let mut connection = PgConnection::establish(&self.db_path).unwrap();
214
215 if player_id.is_some() {
216 let result =
217 DieselGamePlayer::read_from_player(&mut connection, &player_id.clone().unwrap());
218
219 return Ok(result);
220 }
221
222 let all = DieselGame::read_player_games(&mut connection);
223
224 Ok(all)
225 }
226
227 async fn get_player(&self, player_id: &PlayerId) -> Result<Option<ServicePlayer>, String> {
228 let mut connection = PgConnection::establish(&self.db_path).unwrap();
229
230 let player = DieselPlayer::read_from_id(&mut connection, player_id);
231
232 Ok(player)
233 }
234
235 async fn save_player(&self, player: &ServicePlayer) -> Result<(), String> {
236 let mut connection = PgConnection::establish(&self.db_path).unwrap();
237
238 DieselPlayer::save(&mut connection, player);
239
240 Ok(())
241 }
242
243 async fn delete_games(&self, ids: &[GameId]) -> Result<(), String> {
244 let mut connection = PgConnection::establish(&self.db_path).unwrap();
245
246 DieselGamePlayer::delete_games(&mut connection, ids);
247 DieselGameScore::delete_games(&mut connection, ids);
248 DieselGameBoard::delete_games(&mut connection, ids);
249 DieselGameDrawWall::delete_games(&mut connection, ids);
250 DieselGameHand::delete_games(&mut connection, ids);
251 DieselGameSettings::delete_games(&mut connection, ids);
252 DieselGame::delete_games(&mut connection, ids);
253
254 Ok(())
255 }
256}
257
258impl DBStorage {
259 #[allow(dead_code)]
260 pub fn new_dyn() -> Box<dyn Storage> {
261 let db_path = std::env::var(ENV_PG_URL)
262 .unwrap_or("postgres://postgres:postgres@localhost/mahjong".to_string());
263 let redis_path = std::env::var(ENV_REDIS_URL).unwrap_or("redis://localhost".to_string());
264
265 debug!("DBStorage: {} {}", db_path, redis_path);
266
267 let file_storage = Self {
268 db_path: db_path.clone(),
269 redis_path,
270 };
271
272 let mut connection = PgConnection::establish(&db_path).unwrap();
273
274 connection.run_pending_migrations(MIGRATIONS).unwrap();
275
276 Box::new(file_storage)
277 }
278}