From 9798821b7b10e88f4c68c2f933fd289970c086b8 Mon Sep 17 00:00:00 2001 From: projectmoon Date: Sun, 16 May 2021 14:17:03 +0000 Subject: [PATCH] Implement room and dbstate for sqlite. --- sqlx-data.json | 96 +++++++++++++++++++++ src/bot.rs | 6 -- src/db/sqlite/errors.rs | 5 ++ src/db/sqlite/rooms.rs | 111 +++++++++++++++++++++++-- src/db/sqlite/state.rs | 15 +++- src/migrator/migrations/V3__dbstate.rs | 15 ++++ 6 files changed, 235 insertions(+), 13 deletions(-) create mode 100644 src/migrator/migrations/V3__dbstate.rs diff --git a/sqlx-data.json b/sqlx-data.json index 9d050f1..835daf1 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -1,5 +1,47 @@ { "db": "SQLite", + "0ccdb5669e5819628096b8d2f81fef361f73dbcbc27428208a1a4e315a8c7880": { + "query": "SELECT room_id, room_name FROM room_info\n WHERE room_id = ?", + "describe": { + "columns": [ + { + "name": "room_id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "room_name", + "ordinal": 1, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false + ] + } + }, + "19d89370cac05c1bc4de0eb3508712da9ca133b1cf9445b5407d238f89c3ab0c": { + "query": "SELECT device_id FROM bot_state limit 1", + "describe": { + "columns": [ + { + "name": "device_id", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + } + }, "636b1b868eaf04cd234fbf17747d94a66e81f7bc1b060ba14151dbfaf40eeefc": { "query": "SELECT value as \"value: i32\" FROM user_variables\n WHERE user_id = ? AND room_id = ? AND key = ?", "describe": { @@ -18,6 +60,60 @@ ] } }, + "ad52bc29fc1eef2bbff8b5f7316047f81ea31b1f910d0e0226125fea89530aa2": { + "query": "SELECT room_id FROM room_users\n WHERE username = ?", + "describe": { + "columns": [ + { + "name": "room_id", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + } + }, + "b302d586e5ac4c72c2970361ea5a5936c0b8c6dad10033c626a0ce0404cadb25": { + "query": "SELECT username FROM room_users\n WHERE room_id = ?", + "describe": { + "columns": [ + { + "name": "username", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + } + }, + "bba0fc255e7c30d1d2d9468c68ba38db6e8a13be035aa1152933ba9247b14f8c": { + "query": "SELECT event_id FROM room_events\n WHERE room_id = ? AND event_id = ?", + "describe": { + "columns": [ + { + "name": "event_id", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false + ] + } + }, "d6558668b7395b95ded8da71c80963ddde957abdcc3c68b03431f8e904e0d21f": { "query": "SELECT key, value as \"value: i32\" FROM user_variables\n WHERE room_id = ?", "describe": { diff --git a/src/bot.rs b/src/bot.rs index d7e11d1..b941789 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -161,12 +161,6 @@ impl DiceBot { let client = self.client.clone(); self.login(&client).await?; - // Initial sync without event handler prevents responding to - // messages received while bot was offline. TODO: selectively - // respond to old messages? e.g. comands missed while offline. - info!("Performing intial sync (no commands will be responded to)"); - self.client.sync_once(SyncSettings::default()).await?; - client.set_event_handler(Box::new(self)).await; info!("Listening for commands"); diff --git a/src/db/sqlite/errors.rs b/src/db/sqlite/errors.rs index 0078b4e..dd06b77 100644 --- a/src/db/sqlite/errors.rs +++ b/src/db/sqlite/errors.rs @@ -1,3 +1,5 @@ +use std::num::TryFromIntError; + use sled::transaction::{TransactionError, UnabortableTransactionError}; use thiserror::Error; @@ -52,6 +54,9 @@ pub enum DataError { #[error("sqlx error: {0}")] SqlxError(#[from] sqlx::Error), + + #[error("numeric conversion error")] + NumericConversionError(#[from] TryFromIntError), } /// This From implementation is necessary to deal with the recursive diff --git a/src/db/sqlite/rooms.rs b/src/db/sqlite/rooms.rs index 797c0d3..9078a79 100644 --- a/src/db/sqlite/rooms.rs +++ b/src/db/sqlite/rooms.rs @@ -2,42 +2,141 @@ use super::errors::DataError; use super::{Database, Rooms}; use crate::models::RoomInfo; use async_trait::async_trait; +use sqlx::SqlitePool; use std::collections::{HashMap, HashSet}; +use std::time::{SystemTime, UNIX_EPOCH}; + +async fn record_event(conn: &SqlitePool, event_id: &str, room_id: &str) -> Result<(), DataError> { + use std::convert::TryFrom; + let now: i64 = i64::try_from( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Clock has gone backwards") + .as_secs(), + )?; + + sqlx::query( + r#"INSERT INTO room_events + (room_id, event_id, event_timestamp) + VALUES (?, ?, ?)"#, + ) + .bind(room_id) + .bind(event_id) + .bind(now) + .execute(conn) + .await?; + + Ok(()) +} #[async_trait] impl Rooms for Database { async fn should_process(&self, room_id: &str, event_id: &str) -> Result { - Ok(true) + let row = sqlx::query!( + r#"SELECT event_id FROM room_events + WHERE room_id = ? AND event_id = ?"#, + room_id, + event_id + ) + .fetch_optional(&self.conn) + .await?; + + match row { + Some(_) => Ok(false), + None => { + record_event(&self.conn, room_id, event_id).await?; + Ok(true) + } + } } async fn insert_room_info(&self, info: &RoomInfo) -> Result<(), DataError> { + sqlx::query(r#"INSERT INTO room_info (room_id, room_name) VALUES (?, ?)"#) + .bind(&info.room_id) + .bind(&info.room_name) + .execute(&self.conn) + .await?; + Ok(()) } async fn get_room_info(&self, room_id: &str) -> Result, DataError> { - Ok(Some(RoomInfo { - room_id: "".to_string(), - room_name: "".to_string(), + let info = sqlx::query!( + r#"SELECT room_id, room_name FROM room_info + WHERE room_id = ?"#, + room_id + ) + .fetch_optional(&self.conn) + .await?; + + Ok(info.map(|i| RoomInfo { + room_id: i.room_id, + room_name: i.room_name, })) } async fn get_rooms_for_user(&self, user_id: &str) -> Result, DataError> { - Ok(HashSet::new()) + let room_ids = sqlx::query!( + r#"SELECT room_id FROM room_users + WHERE username = ?"#, + user_id + ) + .fetch_all(&self.conn) + .await?; + + Ok(room_ids.into_iter().map(|row| row.room_id).collect()) } async fn get_users_in_room(&self, room_id: &str) -> Result, DataError> { - Ok(HashSet::new()) + let usernames = sqlx::query!( + r#"SELECT username FROM room_users + WHERE room_id = ?"#, + room_id + ) + .fetch_all(&self.conn) + .await?; + + Ok(usernames.into_iter().map(|row| row.username).collect()) } async fn add_user_to_room(&self, username: &str, room_id: &str) -> Result<(), DataError> { + self.remove_user_from_room(username, room_id).await.ok(); + + sqlx::query("INSERT INTO room_users (room_id, username) VALUES (?, ?)") + .bind(room_id) + .bind(username) + .execute(&self.conn) + .await?; + Ok(()) } async fn remove_user_from_room(&self, username: &str, room_id: &str) -> Result<(), DataError> { + sqlx::query("DELETE FROM room_users where username = ? AND room_id = ?") + .bind(username) + .bind(room_id) + .execute(&self.conn) + .await?; + Ok(()) } async fn clear_info(&self, room_id: &str) -> Result<(), DataError> { + sqlx::query("DELETE FROM room_info where room_id = ?") + .bind(room_id) + .execute(&self.conn) + .await?; + + sqlx::query("DELETE FROM room_users where room_id = ?") + .bind(room_id) + .execute(&self.conn) + .await?; + + sqlx::query("DELETE FROM room_events where room_id = ?") + .bind(room_id) + .execute(&self.conn) + .await?; + Ok(()) } } diff --git a/src/db/sqlite/state.rs b/src/db/sqlite/state.rs index 74a2a3f..2830df4 100644 --- a/src/db/sqlite/state.rs +++ b/src/db/sqlite/state.rs @@ -5,10 +5,23 @@ use async_trait::async_trait; #[async_trait] impl DbState for Database { async fn get_device_id(&self) -> Result, DataError> { - Ok(None) + let state = sqlx::query!(r#"SELECT device_id FROM bot_state limit 1"#) + .fetch_optional(&self.conn) + .await?; + + Ok(state.map(|s| s.device_id)) } async fn set_device_id(&self, device_id: &str) -> Result<(), DataError> { + sqlx::query( + r#"INSERT INTO bot_state + (device_id) + VALUES (?)"#, + ) + .bind(device_id) + .execute(&self.conn) + .await?; + Ok(()) } } diff --git a/src/migrator/migrations/V3__dbstate.rs b/src/migrator/migrations/V3__dbstate.rs new file mode 100644 index 0000000..0ae82c2 --- /dev/null +++ b/src/migrator/migrations/V3__dbstate.rs @@ -0,0 +1,15 @@ +use barrel::backend::Sqlite; +use barrel::{types, Migration}; +use log::info; + +pub fn migration() -> String { + let mut m = Migration::new(); + info!("Applying migration: {}", file!()); + + //Basic state table with only device ID for now. Uses only one row. + m.create_table("bot_state", move |t| { + t.add_column("device_id", types::text()); + }); + + m.make::() +}