]> git.seodisparate.com - EN605.607.81.SP22_ASDM_Project/commitdiff
Update specs, impl back-end support for send emote
authorStephen Seo <seo.disparate@gmail.com>
Fri, 29 Apr 2022 08:16:32 +0000 (17:16 +0900)
committerStephen Seo <seo.disparate@gmail.com>
Fri, 29 Apr 2022 08:16:32 +0000 (17:16 +0900)
back_end/src/db_handler.rs
back_end/src/json_handlers.rs
specifications/backend_database_specification.md
specifications/backend_protocol_specification.md

index 16cc79d2f22b5358fa5f9c2badb1e3907096539b..0c73f835d1c63327a4917dd1b11d151f66ea3d1b 100644 (file)
@@ -14,6 +14,7 @@ use crate::constants::{
 use crate::state::{board_from_string, new_string_board, string_from_board, BoardState, Turn};
 
 use std::collections::HashMap;
+use std::fmt::Display;
 use std::sync::mpsc::{Receiver, RecvTimeoutError, SyncSender};
 use std::time::{Duration, Instant};
 use std::{fmt, thread};
@@ -29,7 +30,8 @@ pub type GetIDSenderType = (Option<u32>, Option<bool>);
 /// third bool is if cyan player
 pub type CheckPairingType = (bool, bool, bool);
 
-pub type BoardStateType = (DBGameState, Option<String>);
+/// second String is board string, third String is received emote type
+pub type BoardStateType = (DBGameState, Option<String>, Option<EmoteEnum>);
 
 pub type PlaceResultType = Result<(DBPlaceStatus, Option<String>), DBPlaceError>;
 
@@ -117,6 +119,50 @@ impl fmt::Display for DBPlaceError {
     }
 }
 
+#[derive(Copy, Clone, PartialEq, Eq, Debug)]
+pub enum EmoteEnum {
+    Smile,
+    Neutral,
+    Frown,
+    Think,
+}
+
+impl Display for EmoteEnum {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match *self {
+            EmoteEnum::Smile => f.write_str("smile"),
+            EmoteEnum::Neutral => f.write_str("neutral"),
+            EmoteEnum::Frown => f.write_str("frown"),
+            EmoteEnum::Think => f.write_str("think"),
+        }
+    }
+}
+
+impl TryFrom<&str> for EmoteEnum {
+    type Error = ();
+
+    fn try_from(value: &str) -> Result<Self, Self::Error> {
+        match value.to_lowercase().as_str() {
+            "smile" => Ok(Self::Smile),
+            "neutral" => Ok(Self::Neutral),
+            "frown" => Ok(Self::Frown),
+            "think" => Ok(Self::Think),
+            _ => Err(()),
+        }
+    }
+}
+
+impl From<EmoteEnum> for String {
+    fn from(e: EmoteEnum) -> Self {
+        match e {
+            EmoteEnum::Smile => "smile".into(),
+            EmoteEnum::Neutral => "neutral".into(),
+            EmoteEnum::Frown => "frown".into(),
+            EmoteEnum::Think => "think".into(),
+        }
+    }
+}
+
 #[derive(Clone, Debug)]
 pub enum DBHandlerRequest {
     GetID {
@@ -140,6 +186,11 @@ pub enum DBHandlerRequest {
         pos: usize,
         response_sender: SyncSender<PlaceResultType>,
     },
+    SendEmote {
+        id: u32,
+        emote_type: EmoteEnum,
+        response_sender: SyncSender<Result<(), ()>>,
+    },
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -241,7 +292,9 @@ impl DBHandler {
                     println!("{}", e);
                     // don't stop server on send fail, may have timed out and
                     // dropped the receiver
-                    response_sender.send((DBGameState::UnknownID, None)).ok();
+                    response_sender
+                        .send((DBGameState::UnknownID, None, None))
+                        .ok();
                     return false;
                 }
                 // don't stop server on send fail, may have timed out and
@@ -268,6 +321,23 @@ impl DBHandler {
                 // dropped the receiver
                 response_sender.send(place_result).ok();
             }
+            DBHandlerRequest::SendEmote {
+                id,
+                emote_type,
+                response_sender,
+            } => {
+                let result = self.create_new_sent_emote(None, id, emote_type);
+                if let Err(error_string) = result {
+                    println!("{}", error_string);
+                    // don't stop server on send fail, may have timed
+                    // out and dropped the receiver
+                    response_sender.send(Err(())).ok();
+                } else {
+                    // don't stop server on send fail, may have timed
+                    // out and dropped the receiver
+                    response_sender.send(Ok(())).ok();
+                }
+            }
         } // match db_request
 
         false
@@ -319,6 +389,25 @@ impl DBHandler {
             } else if first_run == DBFirstRun::FirstRun {
                 println!("\"games\" table exists");
             }
+
+            let result = conn.execute(
+                "
+                CREATE TABLE emotes (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+                    type TEXT NOT NULL,
+                    date_added TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                    receiver_id INTEGER NOT NULL,
+                    FOREIGN KEY(receiver_id) REFERENCES players (id) ON DELETE CASCADE);
+            ",
+                [],
+            );
+            if result.is_ok() {
+                if first_run == DBFirstRun::FirstRun {
+                    println!("Created \"emotes\" table");
+                }
+            } else if first_run == DBFirstRun::FirstRun {
+                println!("\"emotes\" table exists");
+            }
+
             Ok(conn)
         } else {
             Err(String::from("Failed to open connection"))
@@ -603,6 +692,33 @@ impl DBHandler {
             _conn_result.as_ref().unwrap()
         };
 
+        let mut received_emote: Option<EmoteEnum> = None;
+        {
+            let row_result: Result<(u64, String), RusqliteError> = conn.query_row(
+                "SELECT id, type FROM emotes WHERE receiver_id = ? ORDER BY date_added ASC;",
+                [player_id],
+                |row| {
+                    Ok((
+                        row.get(0).expect("emotes.id should exist"),
+                        row.get(1).expect("emotes.type should exist"),
+                    ))
+                },
+            );
+            if let Err(RusqliteError::QueryReturnedNoRows) = row_result {
+                // no-op
+            } else if let Err(e) = row_result {
+                println!("Error while fetching received emotes: {:?}", e);
+            } else {
+                let (emote_id, emote_type) = row_result.unwrap();
+                received_emote = emote_type.as_str().try_into().ok();
+                if received_emote.is_none() {
+                    println!("WARNING: Invalid emote type \"{}\" in db", emote_type);
+                }
+                conn.execute("DELETE FROM emotes WHERE id = ?;", [emote_id])
+                    .ok();
+            }
+        }
+
         // TODO maybe handle "opponent_disconnected" case
         let row_result: Result<(String, i64, Option<u32>, Option<u32>), RusqliteError> = conn.query_row(
             "SELECT games.board, games.status, games.cyan_player, games.magenta_player FROM games JOIN players WHERE players.id = ? AND games.id = players.game_id;",
@@ -636,30 +752,34 @@ impl DBHandler {
         if let Ok((board, status, cyan_opt, magenta_opt)) = row_result {
             if board.len() != (ROWS * COLS) as usize {
                 // board is invalid size
-                Ok((DBGameState::InternalError, None))
+                Ok((DBGameState::InternalError, None, received_emote))
             } else if cyan_opt.is_none() || magenta_opt.is_none() {
                 // One player disconnected
                 self.disconnect_player(Some(conn), player_id).ok();
                 // Remove the game(s) with disconnected players
                 if self.clear_empty_games(Some(conn)).is_err() {
-                    Ok((DBGameState::InternalError, None))
+                    Ok((DBGameState::InternalError, None, received_emote))
                 } else if status == 2 || status == 3 {
-                    Ok((DBGameState::from(status), Some(board)))
+                    Ok((DBGameState::from(status), Some(board), received_emote))
                 } else {
-                    Ok((DBGameState::OpponentDisconnected, Some(board)))
+                    Ok((
+                        DBGameState::OpponentDisconnected,
+                        Some(board),
+                        received_emote,
+                    ))
                 }
             } else {
                 // Game in progress, or other state depending on "status"
-                Ok((DBGameState::from(status), Some(board)))
+                Ok((DBGameState::from(status), Some(board), received_emote))
             }
         } else if let Err(RusqliteError::QueryReturnedNoRows) = row_result {
             // No rows is either player doesn't exist or not paired
             let (exists, is_paired, _is_cyan) =
                 self.check_if_player_is_paired(Some(conn), player_id)?;
             if !exists {
-                Ok((DBGameState::UnknownID, None))
+                Ok((DBGameState::UnknownID, None, received_emote))
             } else if !is_paired {
-                Ok((DBGameState::NotPaired, None))
+                Ok((DBGameState::NotPaired, None, received_emote))
             } else {
                 unreachable!("either exists or is_paired must be false");
             }
@@ -805,26 +925,14 @@ impl DBHandler {
             }
             2 => {
                 // game over, cyan won
-                self.disconnect_player(Some(conn), player_id).ok();
-                if self.clear_empty_games(Some(conn)).is_err() {
-                    return Err(DBPlaceError::InternalError);
-                }
                 return Ok((DBPlaceStatus::GameEndedCyanWon, Some(board_string)));
             }
             3 => {
                 // game over, magenta won
-                self.disconnect_player(Some(conn), player_id).ok();
-                if self.clear_empty_games(Some(conn)).is_err() {
-                    return Err(DBPlaceError::InternalError);
-                }
                 return Ok((DBPlaceStatus::GameEndedMagentaWon, Some(board_string)));
             }
             4 => {
                 // game over, draw
-                self.disconnect_player(Some(conn), player_id).ok();
-                if self.clear_empty_games(Some(conn)).is_err() {
-                    return Err(DBPlaceError::InternalError);
-                }
                 return Ok((DBPlaceStatus::GameEndedDraw, Some(board_string)));
             }
             _ => (),
@@ -888,7 +996,6 @@ impl DBHandler {
         }
 
         if let Some(ended_state) = ended_state_opt {
-            self.disconnect_player(Some(conn), player_id).ok();
             Ok((
                 match ended_state {
                     BoardState::Empty => DBPlaceStatus::GameEndedDraw,
@@ -1065,6 +1172,79 @@ impl DBHandler {
 
         Ok(())
     }
+
+    fn cleanup_stale_emotes(&self, conn: Option<&Connection>) -> Result<(), String> {
+        let mut _conn_result = Err(String::new());
+        let conn = if let Some(c) = conn {
+            c
+        } else {
+            _conn_result = self.get_conn(DBFirstRun::NotFirstRun);
+            _conn_result.as_ref().unwrap()
+        };
+
+        conn.execute(
+            "DELETE FROM emotes WHERE unixepoch() - unixepoch(date_added) > ?;",
+            [GAME_CLEANUP_TIMEOUT],
+        )
+        .ok();
+        Ok(())
+    }
+
+    fn create_new_sent_emote(
+        &self,
+        conn: Option<&Connection>,
+        sender_id: u32,
+        emote: EmoteEnum,
+    ) -> Result<(), String> {
+        let mut _conn_result = Err(String::new());
+        let conn = if let Some(c) = conn {
+            c
+        } else {
+            _conn_result = self.get_conn(DBFirstRun::NotFirstRun);
+            _conn_result.as_ref().unwrap()
+        };
+
+        let mut prepared_stmt = conn.prepare("SELECT games.cyan_player, games.magenta_player FROM games JOIN players WHERE players.id = ?, games.id = players.game_id;")
+            .map_err(|_| String::from("Failed to prepare db query for getting opponent id for sending emote"))?;
+        let row_result: Result<(Option<u32>, Option<u32>), RusqliteError> =
+            prepared_stmt.query_row([sender_id], |row| Ok((row.get(0).ok(), row.get(1).ok())));
+        if let Err(RusqliteError::QueryReturnedNoRows) = row_result {
+            return Err(String::from("Failed to send emote, game doesn't exist"));
+        } else if let Err(e) = row_result {
+            return Err(format!("Failed to send emote: {:?}", e));
+        }
+        let (cyan_player_opt, magenta_player_opt) = row_result.unwrap();
+        if cyan_player_opt.is_none() {
+            return Err(String::from(
+                "Failed to send emote, cyan player disconnected",
+            ));
+        } else if magenta_player_opt.is_none() {
+            return Err(String::from(
+                "Failed to send emote, magenta player disconnected",
+            ));
+        }
+        let cyan_player_id = cyan_player_opt.unwrap();
+        let magenta_player_id = magenta_player_opt.unwrap();
+
+        let receiver_id = if cyan_player_id == sender_id {
+            magenta_player_id
+        } else {
+            cyan_player_id
+        };
+
+        conn.execute(
+            "INSERT INTO emotes (type, receiver_id) VALUES (?, ?);",
+            params![String::from(emote), receiver_id],
+        )
+        .map_err(|_| {
+            format!(
+                "Failed to store emote from player {} to player {}",
+                sender_id, receiver_id
+            )
+        })?;
+
+        Ok(())
+    }
 }
 
 pub fn start_db_handler_thread(
@@ -1106,6 +1286,12 @@ pub fn start_db_handler_thread(
                 if let Err(e) = handler.cleanup_stale_players(None) {
                     println!("{}", e);
                 }
+                if let Err(e) = handler.cleanup_stale_emotes(None) {
+                    println!("{}", e);
+                }
+                if let Err(e) = handler.clear_empty_games(None) {
+                    println!("{}", e);
+                }
             }
         }
     });
index e10a62f241ec6b1fcb68bdad19a14213603ebddc..95ff212425a87a21fbb8bec7d6863cf6f1233007 100644 (file)
@@ -8,7 +8,7 @@
 //You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
 use crate::{
     constants::BACKEND_PHRASE_MAX_LENGTH,
-    db_handler::{CheckPairingType, DBHandlerRequest, GetIDSenderType},
+    db_handler::{CheckPairingType, DBHandlerRequest, EmoteEnum, GetIDSenderType},
 };
 
 use std::{
@@ -32,6 +32,7 @@ pub fn handle_json(
             "place_token" => handle_place_token(root, tx),
             "disconnect" => handle_disconnect(root, tx),
             "game_state" => handle_game_state(root, tx),
+            "send_emote" => handle_send_emote(root, tx),
             _ => Err("{\"type\":\"invalid_type\"}".into()),
         }
     } else {
@@ -264,12 +265,21 @@ fn handle_game_state(root: Value, tx: SyncSender<DBHandlerRequest>) -> Result<St
         return Err("{\"type\":\"game_state\", \"status\":\"internal_error\"}".into());
     }
 
-    if let Ok((db_game_state, board_string_opt)) = resp_rx.recv_timeout(DB_REQUEST_TIMEOUT) {
+    if let Ok((db_game_state, board_string_opt, received_emote_opt)) =
+        resp_rx.recv_timeout(DB_REQUEST_TIMEOUT)
+    {
         if let Some(board_string) = board_string_opt {
-            Ok(format!(
-                "{{\"type\":\"game_state\", \"status\":\"{}\", \"board\":\"{}\"}}",
-                db_game_state, board_string
-            ))
+            if let Some(emote) = received_emote_opt {
+                Ok(format!(
+                    "{{\"type\":\"game_state\", \"status\":\"{}\", \"board\":\"{}\", \"peer_emote\": \"{}\"}}",
+                    db_game_state, board_string, emote
+                ))
+            } else {
+                Ok(format!(
+                    "{{\"type\":\"game_state\", \"status\":\"{}\", \"board\":\"{}\"}}",
+                    db_game_state, board_string
+                ))
+            }
         } else {
             Ok(format!(
                 "{{\"type\":\"game_state\", \"status\":\"{}\"}}",
@@ -280,3 +290,56 @@ fn handle_game_state(root: Value, tx: SyncSender<DBHandlerRequest>) -> Result<St
         Err("{\"type\":\"game_state\", \"status\":\"internal_error_timeout\"}".into())
     }
 }
+
+fn handle_send_emote(root: Value, tx: SyncSender<DBHandlerRequest>) -> Result<String, String> {
+    let id_option = root.get("id");
+    if id_option.is_none() {
+        return Err("{\"type\":\"invalid_syntax\"}".into());
+    }
+    let player_id = id_option
+        .unwrap()
+        .as_u64()
+        .ok_or_else(|| String::from("{\"type\":\"invalid_syntax\"}"))?;
+    let player_id: u32 = player_id
+        .try_into()
+        .map_err(|_| String::from("{\"type\":\"invalid_syntax\"}"))?;
+
+    let emote_type_option = root.get("emote");
+    if emote_type_option.is_none() {
+        return Err("{\"type\":\"invalid_syntax\"}".into());
+    }
+    let emote_type_option = emote_type_option.unwrap().as_str();
+    if emote_type_option.is_none() {
+        return Err("{\"type\":\"invalid_syntax\"}".into());
+    }
+    let emote_type = emote_type_option.unwrap();
+
+    let emote_enum: Result<EmoteEnum, ()> = emote_type.try_into();
+    if emote_enum.is_err() {
+        return Err("{\"type\":\"invalid_syntax\"}".into());
+    }
+    let emote_enum = emote_enum.unwrap();
+
+    let (resp_tx, resp_rx) = sync_channel(1);
+
+    if tx
+        .send(DBHandlerRequest::SendEmote {
+            id: player_id,
+            emote_type: emote_enum,
+            response_sender: resp_tx,
+        })
+        .is_err()
+    {
+        return Err("{\"type\":\"send_emote\", \"status\":\"internal_error\"}".into());
+    }
+
+    if let Ok(db_response) = resp_rx.recv_timeout(DB_REQUEST_TIMEOUT) {
+        if db_response.is_ok() {
+            Ok("{\"type\":\"send_emote\", \"status\":\"ok\"}".into())
+        } else {
+            Err("{\"type\":\"send_emote\", \"status\":\"internal_error\"}".into())
+        }
+    } else {
+        Err("{\"type\":\"send_emote\", \"status\":\"internal_error\"}".into())
+    }
+}
index 6c8e427ce5259d321aa9be2f0f33b04c03a198cf..b70bcc346dd248555d9d99537d7a81118b1450b8 100644 (file)
@@ -37,6 +37,14 @@ CREATE TABLE games (id INTEGER PRIMARY KEY NOT NULL,
                     turn_time_start TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
                     FOREIGN KEY(cyan_player) REFERENCES players (id) ON DELETE SET NULL,
                     FOREIGN KEY(magenta_player) REFERENCES players (id) ON DELETE SET NULL);
+
+// "type" is one of the four possible emotes
+
+CREATE TABLE emotes (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+                     type TEXT NOT NULL,
+                     date_added TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                     receiver_id INTEGER NOT NULL,
+                     FOREIGN KEY(receiver_id) REFERENCES players (id) ON DELETE CASCADE);
 ```
 
 "date" entries are used for garbage collection of the database. A predefined
index 68049b6073e3ac1d8b587e1e78a68c76eb71fac8..119cffee5f9cb274368c05d9369a1c93ead9bc8b 100644 (file)
@@ -164,6 +164,16 @@ then the back-end will respond with "too\_many\_players".
     }
 ```
 
+6. Send Emote Request Response
+
+```
+    {
+        "type": "send_emote",
+        "status": "ok", // or "invalid_emote", "peer_disconnected",
+                        // "internal_error"
+    }
+```
+
 Note that the backend will stop keeping track of the game once both players have
 successfully requested the Game State once after the game has ended. Thus,
 future requests may return "unknown\_id" as the "status".