Implement room and dbstate for sqlite.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details

This commit is contained in:
projectmoon 2021-05-16 14:17:03 +00:00
parent cf9ce63892
commit 9798821b7b
6 changed files with 235 additions and 13 deletions

View File

@ -1,5 +1,47 @@
{ {
"db": "SQLite", "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": { "636b1b868eaf04cd234fbf17747d94a66e81f7bc1b060ba14151dbfaf40eeefc": {
"query": "SELECT value as \"value: i32\" FROM user_variables\n WHERE user_id = ? AND room_id = ? AND key = ?", "query": "SELECT value as \"value: i32\" FROM user_variables\n WHERE user_id = ? AND room_id = ? AND key = ?",
"describe": { "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": { "d6558668b7395b95ded8da71c80963ddde957abdcc3c68b03431f8e904e0d21f": {
"query": "SELECT key, value as \"value: i32\" FROM user_variables\n WHERE room_id = ?", "query": "SELECT key, value as \"value: i32\" FROM user_variables\n WHERE room_id = ?",
"describe": { "describe": {

View File

@ -161,12 +161,6 @@ impl DiceBot {
let client = self.client.clone(); let client = self.client.clone();
self.login(&client).await?; 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; client.set_event_handler(Box::new(self)).await;
info!("Listening for commands"); info!("Listening for commands");

View File

@ -1,3 +1,5 @@
use std::num::TryFromIntError;
use sled::transaction::{TransactionError, UnabortableTransactionError}; use sled::transaction::{TransactionError, UnabortableTransactionError};
use thiserror::Error; use thiserror::Error;
@ -52,6 +54,9 @@ pub enum DataError {
#[error("sqlx error: {0}")] #[error("sqlx error: {0}")]
SqlxError(#[from] sqlx::Error), SqlxError(#[from] sqlx::Error),
#[error("numeric conversion error")]
NumericConversionError(#[from] TryFromIntError),
} }
/// This From implementation is necessary to deal with the recursive /// This From implementation is necessary to deal with the recursive

View File

@ -2,42 +2,141 @@ use super::errors::DataError;
use super::{Database, Rooms}; use super::{Database, Rooms};
use crate::models::RoomInfo; use crate::models::RoomInfo;
use async_trait::async_trait; use async_trait::async_trait;
use sqlx::SqlitePool;
use std::collections::{HashMap, HashSet}; 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] #[async_trait]
impl Rooms for Database { impl Rooms for Database {
async fn should_process(&self, room_id: &str, event_id: &str) -> Result<bool, DataError> { async fn should_process(&self, room_id: &str, event_id: &str) -> Result<bool, DataError> {
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> { 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(()) Ok(())
} }
async fn get_room_info(&self, room_id: &str) -> Result<Option<RoomInfo>, DataError> { async fn get_room_info(&self, room_id: &str) -> Result<Option<RoomInfo>, DataError> {
Ok(Some(RoomInfo { let info = sqlx::query!(
room_id: "".to_string(), r#"SELECT room_id, room_name FROM room_info
room_name: "".to_string(), 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<HashSet<String>, DataError> { async fn get_rooms_for_user(&self, user_id: &str) -> Result<HashSet<String>, 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<HashSet<String>, DataError> { async fn get_users_in_room(&self, room_id: &str) -> Result<HashSet<String>, 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> { 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(()) Ok(())
} }
async fn remove_user_from_room(&self, username: &str, room_id: &str) -> Result<(), DataError> { 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(()) Ok(())
} }
async fn clear_info(&self, room_id: &str) -> Result<(), DataError> { 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(()) Ok(())
} }
} }

View File

@ -5,10 +5,23 @@ use async_trait::async_trait;
#[async_trait] #[async_trait]
impl DbState for Database { impl DbState for Database {
async fn get_device_id(&self) -> Result<Option<String>, DataError> { async fn get_device_id(&self) -> Result<Option<String>, 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> { 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(()) Ok(())
} }
} }

View File

@ -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::<Sqlite>()
}