forked from projectmoon/tenebrous-dicebot
Add database and storage of user variables.
This commit introduces the Sled embedded key-value store for keeping track of user variables on a per-room basis. Extensive changes were made to the command module to separate concerns and also pass the database "connection" down the line. - A new "Context" object was created to hold information and state needed for command execution (namely the database). - Database is very simple for now, storing only user variables. Refactoring later for storing more complicated types. - State actor moved into Actors struct, in preparation for either more actors, or ripping the whole thing out entirely. - Other modules are also more properly separated, notably the config module is entirely self-contained.
This commit is contained in:
parent
35485cdfc8
commit
6cdc465a2e
|
@ -5,3 +5,4 @@ todo.org
|
|||
cache
|
||||
*.tar
|
||||
*.tar.gz
|
||||
test-db/
|
||||
|
|
|
@ -283,9 +283,11 @@ dependencies = [
|
|||
"actix",
|
||||
"actix-rt",
|
||||
"async-trait",
|
||||
"byteorder",
|
||||
"combine",
|
||||
"dirs",
|
||||
"env_logger",
|
||||
"futures 0.3.6",
|
||||
"indoc",
|
||||
"itertools",
|
||||
"log",
|
||||
|
@ -296,10 +298,12 @@ dependencies = [
|
|||
"nom",
|
||||
"rand",
|
||||
"serde",
|
||||
"sled",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"toml",
|
||||
"url",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -391,6 +395,15 @@ version = "0.7.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.4.4"
|
||||
|
@ -401,6 +414,21 @@ dependencies = [
|
|||
"maybe-uninit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if 0.1.10",
|
||||
"crossbeam-utils",
|
||||
"lazy_static",
|
||||
"maybe-uninit",
|
||||
"memoffset",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.2.3"
|
||||
|
@ -548,6 +576,16 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-zircon"
|
||||
version = "0.3.3"
|
||||
|
@ -692,6 +730,15 @@ dependencies = [
|
|||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fxhash"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.15"
|
||||
|
@ -1197,6 +1244,15 @@ version = "2.3.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.16"
|
||||
|
@ -2007,6 +2063,22 @@ version = "0.4.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
|
||||
|
||||
[[package]]
|
||||
name = "sled"
|
||||
version = "0.34.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f72c064e63fbca3138ad07f3588c58093f1684f3a99f60dcfa6d46b87e60fde7"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
"fs2",
|
||||
"fxhash",
|
||||
"libc",
|
||||
"log",
|
||||
"parking_lot",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.4.2"
|
||||
|
@ -2656,6 +2728,27 @@ dependencies = [
|
|||
"winapi-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6580539ad917b7c026220c4b3f2c08d52ce54d6ce0dc491e66002e35388fab46"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d498dbd1fd7beb83c86709ae1c33ca50942889473473d287d56ce4770a18edfb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.1.1"
|
||||
|
|
|
@ -25,6 +25,11 @@ indoc = "1.0"
|
|||
actix = "0.10"
|
||||
actix-rt = "1.1"
|
||||
combine = "4.3"
|
||||
sled = "0.34"
|
||||
zerocopy = "0.3"
|
||||
byteorder = "1.3"
|
||||
futures = "0.3"
|
||||
|
||||
|
||||
# The versioning of the matrix SDK follows its Cargo.toml. The SDK and
|
||||
# macros are on master, but it imports the common and base from 0.1.0.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# Builder image with development dependencies.
|
||||
FROM bougyman/voidlinux:glibc as builder
|
||||
RUN xbps-install -Syu
|
||||
RUN xbps-install -Sy base-devel rust cargo cmake wget gnupg
|
||||
RUN xbps-install -Sy libressl-devel olm-devel libstdc++-devel
|
||||
|
||||
|
@ -36,4 +37,5 @@ COPY --from=builder \
|
|||
/usr/local/bin/
|
||||
|
||||
ENV XDG_CACHE_HOME "/cache"
|
||||
ENV DATABASE_PATH "/cache/bot-db"
|
||||
ENTRYPOINT [ "/usr/local/bin/tini", "-v", "--", "/usr/local/bin/dicebot", "/config/dicebot-config.toml" ]
|
||||
|
|
52
README.md
52
README.md
|
@ -75,6 +75,7 @@ either the Storytelling System or more traditional RPG dice rolls.
|
|||
The bot supports a `!help` command for basic help information about
|
||||
its capabilities.
|
||||
|
||||
### Basic Dice Rolling
|
||||
The commands `!roll` and `!r` can handle arbitrary dice roll
|
||||
expressions.
|
||||
|
||||
|
@ -84,26 +85,49 @@ expressions.
|
|||
!r 3d12 - 5d2 + 3 - 7d3 + 20d20
|
||||
```
|
||||
|
||||
This system does not yet have the capability to handle things like D&D
|
||||
5e advantage or disadvantage.
|
||||
|
||||
### Storytelling System
|
||||
|
||||
The commands `!pool` (or `!rp`) and `!chance` are for the Storytelling
|
||||
System, and they use a specific syntax to support the dice system. The
|
||||
simplest version of the command is `!pool <num>` to roll a pool of the
|
||||
given size using the most common type of roll.
|
||||
|
||||
The type of roll can be controlled by adding `n`, `e`, or `r` after
|
||||
The type of roll can be controlled by adding `n`, `e`, or `r` before
|
||||
the number, for 9-again, 8-again, and rote quality rolls. The number
|
||||
of successes required for an exceptional success can be controlled by
|
||||
`s<num>`, e.g. `s3` to only need 3 successes for an exceptional
|
||||
success.
|
||||
success. All modifiers should come before the number, with a `:`
|
||||
colon.
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
!pool 8 //regular pool of 8 dice
|
||||
!pool 8n //roll 8 dice, 9-again
|
||||
!pool 8ns3 //roll 8 dice, 9-again with only 3 successes for exceptional
|
||||
!pool 5rs2 //5 dice, rote quality, 2 successes for exceptional
|
||||
!pool n:8 //roll 8 dice, 9-again
|
||||
!pool ns3:8 //roll 8 dice, 9-again with only 3 successes for exceptional
|
||||
!pool rs2:5 //5 dice, rote quality, 2 successes for exceptional
|
||||
```
|
||||
|
||||
### User Variables
|
||||
|
||||
Users can store variables for use with the Storytelling dice pool
|
||||
system. Variables are stored on a per-room, per-user basis in the
|
||||
database (currently located in the cache directory if using the Docker
|
||||
image).
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
!set myvar 5 //stores 5 for this room under the name "myvar"
|
||||
!get myvar //will print 5
|
||||
```
|
||||
|
||||
Variables can be referenced in dice pool rolling expressions, for
|
||||
example `!pool myvar` or `!pool myvar+3`.
|
||||
|
||||
## Running the Bot
|
||||
|
||||
The easiest way to run the bot is to use the [official Docker
|
||||
|
@ -123,13 +147,14 @@ ghcr.io/projectmoon/chronicle-dicebot:$VERSION
|
|||
```
|
||||
|
||||
The Docker image requires two volume mounts: the location of the
|
||||
[config file][config-file], which should be mounted at `/config/dicebot-config.toml`,
|
||||
and a cache directory to store client state after initial sync. That
|
||||
should be mounted at `/cache/`in the container.
|
||||
[config file][config-file], which should be mounted at
|
||||
`/config/dicebot-config.toml`, and a cache directory to store the
|
||||
database and client state after initial sync. That should be mounted
|
||||
at `/cache/`in the container.
|
||||
|
||||
### Configuration File
|
||||
|
||||
The configuration file is a TOML file with two sections.
|
||||
The configuration file is a TOML file with three sections.
|
||||
|
||||
```toml
|
||||
[matrix]
|
||||
|
@ -137,6 +162,9 @@ home_server = 'https://example.com'
|
|||
username = 'thisismyusername'
|
||||
password = 'thisismypassword'
|
||||
|
||||
[datbase]
|
||||
path = '/path/to/database/directory/'
|
||||
|
||||
[bot]
|
||||
oldest_message_age = 300
|
||||
```
|
||||
|
@ -151,6 +179,12 @@ bot's matrix account.
|
|||
- `username`: Bot account username.
|
||||
- `password`: Bot account password.
|
||||
|
||||
The `[database]` section contains information for connecting to the
|
||||
embedded database. Note: **you do not need this** if you are using the
|
||||
Docker image.
|
||||
- `path`: Path on the filesystem to use as the database storage
|
||||
directory.
|
||||
|
||||
The `[bot]` section has settings for controlling how the bot operates.
|
||||
This section is optional and the settings will fall back to their
|
||||
default values if the section or setting is not present.
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
use crate::config::Config;
|
||||
use crate::db::Database;
|
||||
use actix::prelude::*;
|
||||
use state::DiceBotState;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub mod state;
|
||||
|
||||
pub struct Actors {
|
||||
global_state: Addr<DiceBotState>,
|
||||
}
|
||||
|
||||
impl Actors {
|
||||
pub fn new(config: &Arc<Config>, _db: &Database) -> Actors {
|
||||
let global_state = DiceBotState::new(&config);
|
||||
|
||||
Actors {
|
||||
global_state: global_state.start(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn global_state(&self) -> Addr<DiceBotState> {
|
||||
self.global_state.clone()
|
||||
}
|
||||
}
|
|
@ -1,12 +1,17 @@
|
|||
use chronicle_dicebot::commands::parse_command;
|
||||
use chronicle_dicebot::commands::Command;
|
||||
use chronicle_dicebot::context::Context;
|
||||
use chronicle_dicebot::db::Database;
|
||||
use chronicle_dicebot::error::BotError;
|
||||
|
||||
fn main() -> Result<(), String> {
|
||||
let command = std::env::args().skip(1).collect::<Vec<String>>().join(" ");
|
||||
let command = match parse_command(&command) {
|
||||
Ok(Some(command)) => command,
|
||||
Ok(None) => return Err("Command not recognized".into()),
|
||||
Err(e) => return Err(format!("Error parsing command: {}", e)),
|
||||
fn main() -> Result<(), BotError> {
|
||||
let db = Database::new(&sled::open("test-db")?);
|
||||
let input = std::env::args().skip(1).collect::<Vec<String>>().join(" ");
|
||||
let command = match Command::parse(&input) {
|
||||
Ok(command) => command,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
println!("{}", command.execute().plain());
|
||||
|
||||
let context = Context::new(&db, "roomid", "localuser", &input);
|
||||
println!("{}", command.execute(&context).plain());
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,51 +1,36 @@
|
|||
//Needed for nested Result handling from tokio. Probably can go away after 1.47.0.
|
||||
#![type_length_limit = "7605144"]
|
||||
use actix::prelude::*;
|
||||
use chronicle_dicebot::actors::Actors;
|
||||
use chronicle_dicebot::bot::DiceBot;
|
||||
use chronicle_dicebot::config::*;
|
||||
use chronicle_dicebot::db::Database;
|
||||
use chronicle_dicebot::error::BotError;
|
||||
use chronicle_dicebot::state::DiceBotState;
|
||||
use env_logger::Env;
|
||||
use log::error;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn read_config<P: Into<PathBuf>>(config_path: P) -> Result<Config, BotError> {
|
||||
let config_path = config_path.into();
|
||||
let config = {
|
||||
let contents = fs::read_to_string(&config_path)?;
|
||||
deserialize_config(&contents)?
|
||||
};
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn deserialize_config(contents: &str) -> Result<Config, BotError> {
|
||||
let config = toml::from_str(&contents)?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() {
|
||||
env_logger::from_env(Env::default().default_filter_or("chronicle_dicebot=info,dicebot=info"))
|
||||
.init();
|
||||
match run().await {
|
||||
Ok(_) => (),
|
||||
Err(e) => error!("Error: {:?}", e),
|
||||
Err(e) => error!("Error: {}", e),
|
||||
};
|
||||
}
|
||||
|
||||
async fn run() -> Result<(), BotError> {
|
||||
env_logger::from_env(Env::default().default_filter_or("chronicle_dicebot=info")).init();
|
||||
|
||||
let config_path = std::env::args()
|
||||
.skip(1)
|
||||
.next()
|
||||
.expect("Need a config as an argument");
|
||||
|
||||
let cfg = Arc::new(read_config(config_path)?);
|
||||
let bot_state = DiceBotState::new(&cfg).start();
|
||||
let db = Database::new(&sled::open(cfg.database_path())?);
|
||||
let actors = Actors::new(&cfg, &db);
|
||||
|
||||
match DiceBot::new(&cfg, bot_state) {
|
||||
match DiceBot::new(&cfg, actors, &db) {
|
||||
Ok(bot) => bot.run().await?,
|
||||
Err(e) => println!("Error connecting: {:?}", e),
|
||||
};
|
||||
|
@ -53,38 +38,3 @@ async fn run() -> Result<(), BotError> {
|
|||
System::current().stop();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use indoc::indoc;
|
||||
|
||||
#[test]
|
||||
fn deserialize_config_without_bot_section_test() {
|
||||
let contents = indoc! {"
|
||||
[matrix]
|
||||
home_server = 'https://matrix.example.com'
|
||||
username = 'username'
|
||||
password = 'password'
|
||||
"};
|
||||
|
||||
let cfg: Result<_, _> = deserialize_config(contents);
|
||||
assert_eq!(true, cfg.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_config_without_oldest_message_setting_test() {
|
||||
let contents = indoc! {"
|
||||
[matrix]
|
||||
home_server = 'https://matrix.example.com'
|
||||
username = 'username'
|
||||
password = 'password'
|
||||
|
||||
[bot]
|
||||
not_a_real_setting = 2
|
||||
"};
|
||||
|
||||
let cfg: Result<_, _> = deserialize_config(contents);
|
||||
assert_eq!(true, cfg.is_ok());
|
||||
}
|
||||
}
|
||||
|
|
59
src/bot.rs
59
src/bot.rs
|
@ -1,8 +1,10 @@
|
|||
use crate::commands::parse_command;
|
||||
use crate::actors::state::LogSkippedOldMessages;
|
||||
use crate::actors::Actors;
|
||||
use crate::commands::execute_command;
|
||||
use crate::config::*;
|
||||
use crate::context::Context;
|
||||
use crate::db::Database;
|
||||
use crate::error::BotError;
|
||||
use crate::state::{DiceBotState, LogSkippedOldMessages};
|
||||
use actix::Addr;
|
||||
use dirs;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use matrix_sdk::{
|
||||
|
@ -31,9 +33,11 @@ pub struct DiceBot {
|
|||
/// The matrix client.
|
||||
client: Client,
|
||||
|
||||
/// Current state of the dice bot. Actor ref to the core state
|
||||
/// actor.
|
||||
state: Addr<DiceBotState>,
|
||||
/// Actors used by the dice bot to manage internal state.
|
||||
actors: Actors,
|
||||
|
||||
/// Active database layer
|
||||
db: Database,
|
||||
}
|
||||
|
||||
fn cache_dir() -> Result<PathBuf, BotError> {
|
||||
|
@ -57,11 +61,12 @@ impl DiceBot {
|
|||
/// actor. This function returns a Result because it is possible
|
||||
/// for client creation to fail for some reason (e.g. invalid
|
||||
/// homeserver URL).
|
||||
pub fn new(config: &Arc<Config>, state_actor: Addr<DiceBotState>) -> Result<Self, BotError> {
|
||||
pub fn new(config: &Arc<Config>, actors: Actors, db: &Database) -> Result<Self, BotError> {
|
||||
Ok(DiceBot {
|
||||
client: create_client(&config)?,
|
||||
config: config.clone(),
|
||||
state: state_actor,
|
||||
actors: actors,
|
||||
db: db.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -135,13 +140,13 @@ fn check_message_age(
|
|||
}
|
||||
}
|
||||
|
||||
async fn should_process(
|
||||
async fn should_process<'a>(
|
||||
bot: &DiceBot,
|
||||
event: &SyncMessageEvent<MessageEventContent>,
|
||||
) -> Result<(String, String), BotError> {
|
||||
//Ignore messages that are older than configured duration.
|
||||
if !check_message_age(event, bot.config.oldest_message_age()) {
|
||||
let res = bot.state.send(LogSkippedOldMessages).await;
|
||||
let res = bot.actors.global_state().send(LogSkippedOldMessages).await;
|
||||
|
||||
if let Err(e) = res {
|
||||
error!("Actix error: {:?}", e);
|
||||
|
@ -209,37 +214,29 @@ impl EventEmitter for DiceBot {
|
|||
return;
|
||||
};
|
||||
|
||||
let (plain, html) = match parse_command(&msg_body) {
|
||||
Ok(Some(command)) => {
|
||||
let command = command.execute();
|
||||
(command.plain().into(), command.html().into())
|
||||
}
|
||||
Ok(None) => return, //Ignore non-commands.
|
||||
Err(e) => {
|
||||
let message = format!("Error parsing command: {}", e);
|
||||
let html_message = format!("<p><strong>{}</strong></p>", message);
|
||||
(message, html_message)
|
||||
}
|
||||
};
|
||||
|
||||
let plain = format!("{}\n{}", sender_username, plain);
|
||||
let html = format!("<p>{}</p>\n{}", sender_username, html);
|
||||
let content = AnyMessageEventContent::RoomMessage(MessageEventContent::Notice(
|
||||
NoticeMessageEventContent::html(plain, html),
|
||||
));
|
||||
|
||||
//we clone here to hold the lock for as little time as possible.
|
||||
let (room_name, room_id) = {
|
||||
let real_room = room.read().await;
|
||||
(real_room.display_name().clone(), real_room.room_id.clone())
|
||||
};
|
||||
|
||||
let result = self.client.room_send(&room_id, content, None).await;
|
||||
let ctx = Context::new(&self.db, &room_id.as_str(), &sender_username, &msg_body);
|
||||
|
||||
if let Some(cmd_result) = execute_command(&ctx) {
|
||||
let response = AnyMessageEventContent::RoomMessage(MessageEventContent::Notice(
|
||||
NoticeMessageEventContent::html(cmd_result.plain, cmd_result.html),
|
||||
));
|
||||
|
||||
let result = self.client.room_send(&room_id, response, None).await;
|
||||
if let Err(e) = result {
|
||||
error!("Error sending message: {}", e.to_string());
|
||||
};
|
||||
|
||||
info!("[{}] {} executed: {}", room_name, sender_username, msg_body);
|
||||
info!(
|
||||
"[{}] {} executed: {}",
|
||||
room_name, ctx.username, ctx.message_body
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
183
src/commands.rs
183
src/commands.rs
|
@ -1,6 +1,7 @@
|
|||
use crate::cofd::dice::DicePool;
|
||||
use crate::context::Context;
|
||||
use crate::dice::ElementExpression;
|
||||
use crate::error::BotError;
|
||||
use crate::error::{BotError, CommandError};
|
||||
use crate::help::HelpTopic;
|
||||
use crate::roll::Roll;
|
||||
|
||||
|
@ -22,7 +23,7 @@ impl Execution {
|
|||
}
|
||||
|
||||
pub trait Command {
|
||||
fn execute(&self) -> Execution;
|
||||
fn execute(&self, ctx: &Context) -> Execution;
|
||||
fn name(&self) -> &'static str;
|
||||
}
|
||||
|
||||
|
@ -33,7 +34,7 @@ impl Command for RollCommand {
|
|||
"roll regular dice"
|
||||
}
|
||||
|
||||
fn execute(&self) -> Execution {
|
||||
fn execute(&self, _ctx: &Context) -> Execution {
|
||||
let roll = self.0.roll();
|
||||
let plain = format!("Dice: {}\nResult: {}", self.0, roll);
|
||||
let html = format!(
|
||||
|
@ -51,7 +52,7 @@ impl Command for PoolRollCommand {
|
|||
"roll dice pool"
|
||||
}
|
||||
|
||||
fn execute(&self) -> Execution {
|
||||
fn execute(&self, _ctx: &Context) -> Execution {
|
||||
let roll_result = self.0.roll();
|
||||
|
||||
let (plain, html) = match roll_result {
|
||||
|
@ -81,7 +82,7 @@ impl Command for HelpCommand {
|
|||
"help information"
|
||||
}
|
||||
|
||||
fn execute(&self) -> Execution {
|
||||
fn execute(&self, _ctx: &Context) -> Execution {
|
||||
let help = match &self.0 {
|
||||
Some(topic) => topic.message(),
|
||||
_ => "There is no help for this topic",
|
||||
|
@ -93,24 +94,116 @@ impl Command for HelpCommand {
|
|||
}
|
||||
}
|
||||
|
||||
/// Parse a command string into a dynamic command execution trait
|
||||
/// object. Returns an error if a command was recognized but not
|
||||
/// parsed correctly. Returns Ok(None) if no command was recognized.
|
||||
pub fn parse_command(s: &str) -> Result<Option<Box<dyn Command>>, BotError> {
|
||||
// match parser::parse_command(s) {
|
||||
// Ok(Some(command)) => match &command {
|
||||
// //Any command, or text transformed into non-command is
|
||||
// //sent upwards.
|
||||
// ("", Some(_)) | (_, None) => Ok(command),
|
||||
pub struct GetVariableCommand(String);
|
||||
|
||||
// //TODO replcae with nom all_consuming?
|
||||
// //Any unconsumed input (whitespace should already be
|
||||
// // stripped) is considered a parsing error.
|
||||
// _ => Err(format!("{}: malformed expression", s)),
|
||||
// },
|
||||
// Err(err) => Err(err),
|
||||
// }
|
||||
parser::parse_command(s)
|
||||
impl Command for GetVariableCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"retrieve variable value"
|
||||
}
|
||||
|
||||
fn execute(&self, ctx: &Context) -> Execution {
|
||||
let name = &self.0;
|
||||
let value = match ctx.db.get_user_variable(ctx.room_id, ctx.username, name) {
|
||||
Ok(Some(num)) => format!("{} = {}", name, num),
|
||||
Ok(None) => format!("{} is not set", name),
|
||||
Err(e) => format!("error getting {}: {}", name, e),
|
||||
};
|
||||
|
||||
let plain = format!("Variable: {}", value);
|
||||
let html = format!("<p><strong>Variable:</strong> {}", value);
|
||||
Execution { plain, html }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SetVariableCommand(String, i32);
|
||||
|
||||
impl Command for SetVariableCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"set variable value"
|
||||
}
|
||||
|
||||
fn execute(&self, ctx: &Context) -> Execution {
|
||||
let name = &self.0;
|
||||
let value = self.1;
|
||||
let value = match ctx
|
||||
.db
|
||||
.set_user_variable(ctx.room_id, ctx.username, name, value)
|
||||
{
|
||||
Ok(_) => format!("{} = {}", name, value),
|
||||
Err(e) => format!("error setting {}: {}", name, e),
|
||||
};
|
||||
|
||||
let plain = format!("Set Variable: {}", value);
|
||||
let html = format!("<p><strong>Set Variable:</strong> {}", value);
|
||||
Execution { plain, html }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DeleteVariableCommand(String);
|
||||
|
||||
impl Command for DeleteVariableCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"delete variable"
|
||||
}
|
||||
|
||||
fn execute(&self, ctx: &Context) -> Execution {
|
||||
let name = &self.0;
|
||||
let value = match ctx.db.delete_user_variable(ctx.room_id, ctx.username, name) {
|
||||
Ok(()) => format!("{} now unset", name),
|
||||
Err(e) => format!("error deleting {}: {}", name, e),
|
||||
};
|
||||
|
||||
let plain = format!("Remove Variable: {}", value);
|
||||
let html = format!("<p><strong>Remove Variable:</strong> {}", value);
|
||||
Execution { plain, html }
|
||||
}
|
||||
}
|
||||
|
||||
impl dyn Command {
|
||||
/// Parse a command string into a dynamic command execution trait
|
||||
/// object. Returns an error if a command was recognized but not
|
||||
/// parsed correctly. Returns Ok(None) if no command was recognized.
|
||||
pub fn parse(s: &str) -> Result<Box<dyn Command>, BotError> {
|
||||
match parser::parse_command(s) {
|
||||
Ok(Some(command)) => Ok(command),
|
||||
Ok(None) => Err(BotError::CommandError(CommandError::IgnoredCommand)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CommandResult {
|
||||
pub plain: String,
|
||||
pub html: String,
|
||||
}
|
||||
|
||||
/// Attempt to execute a command, and return the content that should
|
||||
/// go back to Matrix, if the command was executed (successfully or
|
||||
/// not). If a command is determined to be ignored, this function will
|
||||
/// return None, signifying that we should not send a response.
|
||||
pub fn execute_command<'a>(ctx: &'a Context) -> Option<CommandResult> {
|
||||
let res = Command::parse(ctx.message_body).map(|cmd| {
|
||||
let execution = cmd.execute(ctx);
|
||||
(execution.plain().into(), execution.html().into())
|
||||
});
|
||||
|
||||
let (plain, html) = match res {
|
||||
Ok(plain_and_html) => plain_and_html,
|
||||
Err(BotError::CommandError(CommandError::IgnoredCommand)) => return None,
|
||||
Err(e) => {
|
||||
let message = format!("Error parsing command: {}", e);
|
||||
let html_message = format!("<p><strong>{}</strong></p>", message);
|
||||
(message, html_message)
|
||||
}
|
||||
};
|
||||
|
||||
let plain = format!("{}\n{}", ctx.username, plain);
|
||||
let html = format!("<p>{}</p>\n{}", ctx.username, html);
|
||||
|
||||
Some(CommandResult {
|
||||
plain: plain,
|
||||
html: html,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -119,57 +212,39 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn chance_die_is_not_malformed() {
|
||||
assert!(parse_command("!chance").is_ok());
|
||||
assert!(Command::parse("!chance").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roll_malformed_expression_test() {
|
||||
assert!(parse_command("!roll 1d20asdlfkj").is_err());
|
||||
assert!(parse_command("!roll 1d20asdlfkj ").is_err());
|
||||
assert!(Command::parse("!roll 1d20asdlfkj").is_err());
|
||||
assert!(Command::parse("!roll 1d20asdlfkj ").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roll_dice_pool_malformed_expression_test() {
|
||||
assert!(parse_command("!pool 8abc").is_err());
|
||||
assert!(parse_command("!pool 8abc ").is_err());
|
||||
assert!(Command::parse("!pool 8abc").is_err());
|
||||
assert!(Command::parse("!pool 8abc ").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_whitespace_test() {
|
||||
assert!(parse_command("!pool ns3:8 ")
|
||||
.map(|p| p.is_some())
|
||||
.expect("was error"));
|
||||
assert!(parse_command(" !pool ns3:8")
|
||||
.map(|p| p.is_some())
|
||||
.expect("was error"));
|
||||
assert!(parse_command(" !pool ns3:8 ")
|
||||
.map(|p| p.is_some())
|
||||
.expect("was error"));
|
||||
Command::parse("!pool ns3:8 ").expect("was error");
|
||||
Command::parse(" !pool ns3:8").expect("was error");
|
||||
Command::parse(" !pool ns3:8 ").expect("was error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_whitespace_test() {
|
||||
assert!(parse_command("!help stuff ")
|
||||
.map(|p| p.is_some())
|
||||
.expect("was error"));
|
||||
assert!(parse_command(" !help stuff")
|
||||
.map(|p| p.is_some())
|
||||
.expect("was error"));
|
||||
assert!(parse_command(" !help stuff ")
|
||||
.map(|p| p.is_some())
|
||||
.expect("was error"));
|
||||
Command::parse("!help stuff ").expect("was error");
|
||||
Command::parse(" !help stuff").expect("was error");
|
||||
Command::parse(" !help stuff ").expect("was error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roll_whitespace_test() {
|
||||
assert!(parse_command("!roll 1d4 + 5d6 -3 ")
|
||||
.map(|p| p.is_some())
|
||||
.expect("was error"));
|
||||
assert!(parse_command("!roll 1d4 + 5d6 -3 ")
|
||||
.map(|p| p.is_some())
|
||||
.expect("was error"));
|
||||
assert!(parse_command(" !roll 1d4 + 5d6 -3 ")
|
||||
.map(|p| p.is_some())
|
||||
.expect("was error"));
|
||||
Command::parse("!roll 1d4 + 5d6 -3 ").expect("was error");
|
||||
Command::parse("!roll 1d4 + 5d6 -3 ").expect("was error");
|
||||
Command::parse(" !roll 1d4 + 5d6 -3 ").expect("was error");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
use crate::cofd::parser::{create_chance_die, parse_dice_pool};
|
||||
use crate::commands::{Command, HelpCommand, PoolRollCommand, RollCommand};
|
||||
use crate::commands::{
|
||||
Command, DeleteVariableCommand, GetVariableCommand, HelpCommand, PoolRollCommand, RollCommand,
|
||||
SetVariableCommand,
|
||||
};
|
||||
use crate::dice::parser::parse_element_expression;
|
||||
use crate::error::BotError;
|
||||
use crate::help::parse_help_topic;
|
||||
use crate::variables::parse_set_variable;
|
||||
use combine::parser::char::{char, letter, space};
|
||||
use combine::{any, many1, optional, Parser};
|
||||
use nom::Err as NomErr;
|
||||
|
@ -20,6 +24,19 @@ fn parse_roll(input: &str) -> Result<Box<dyn Command>, BotError> {
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_get_variable_command(input: &str) -> Result<Box<dyn Command>, BotError> {
|
||||
Ok(Box::new(GetVariableCommand(input.to_owned())))
|
||||
}
|
||||
|
||||
fn parse_set_variable_command(input: &str) -> Result<Box<dyn Command>, BotError> {
|
||||
let (variable_name, value) = parse_set_variable(input)?;
|
||||
Ok(Box::new(SetVariableCommand(variable_name, value)))
|
||||
}
|
||||
|
||||
fn parse_delete_variable_command(input: &str) -> Result<Box<dyn Command>, BotError> {
|
||||
Ok(Box::new(DeleteVariableCommand(input.to_owned())))
|
||||
}
|
||||
|
||||
fn parse_pool_roll(input: &str) -> Result<Box<dyn Command>, BotError> {
|
||||
let pool = parse_dice_pool(input)?;
|
||||
Ok(Box::new(PoolRollCommand(pool)))
|
||||
|
@ -72,6 +89,9 @@ fn split_command(input: &str) -> Result<(String, String), BotError> {
|
|||
pub fn parse_command(input: &str) -> Result<Option<Box<dyn Command>>, BotError> {
|
||||
match split_command(input) {
|
||||
Ok((cmd, cmd_input)) => match cmd.as_ref() {
|
||||
"get" => parse_get_variable_command(&cmd_input).map(|command| Some(command)),
|
||||
"set" => parse_set_variable_command(&cmd_input).map(|command| Some(command)),
|
||||
"del" => parse_delete_variable_command(&cmd_input).map(|command| Some(command)),
|
||||
"r" | "roll" => parse_roll(&cmd_input).map(|command| Some(command)),
|
||||
"rp" | "pool" => parse_pool_roll(&cmd_input).map(|command| Some(command)),
|
||||
"chance" => chance_die().map(|command| Some(command)),
|
||||
|
|
183
src/config.rs
183
src/config.rs
|
@ -1,4 +1,23 @@
|
|||
use crate::error::ConfigError;
|
||||
use serde::{self, Deserialize, Serialize};
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn read_config<P: Into<PathBuf>>(config_path: P) -> Result<Config, ConfigError> {
|
||||
let config_path = config_path.into();
|
||||
let config = {
|
||||
let contents = fs::read_to_string(&config_path)?;
|
||||
deserialize_config(&contents)?
|
||||
};
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn deserialize_config(contents: &str) -> Result<Config, ConfigError> {
|
||||
let config = toml::from_str(&contents)?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// The "matrix" section of the config, which gives home server, login information, and etc.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
|
@ -15,6 +34,11 @@ struct MatrixConfig {
|
|||
|
||||
const DEFAULT_OLDEST_MESSAGE_AGE: u64 = 15 * 60;
|
||||
|
||||
fn db_path_from_env() -> String {
|
||||
env::var("DATABASE_PATH")
|
||||
.expect("could not find database path in config or environment variable")
|
||||
}
|
||||
|
||||
/// The "bot" section of the config file, for bot settings.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
struct BotConfig {
|
||||
|
@ -22,6 +46,21 @@ struct BotConfig {
|
|||
oldest_message_age: Option<u64>,
|
||||
}
|
||||
|
||||
/// The "database" section of the config file.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
struct DatabaseConfig {
|
||||
/// Path to the database storage directory. Required.
|
||||
path: Option<String>,
|
||||
}
|
||||
|
||||
impl DatabaseConfig {
|
||||
#[inline]
|
||||
#[must_use]
|
||||
fn path(&self) -> String {
|
||||
self.path.clone().unwrap_or_else(|| db_path_from_env())
|
||||
}
|
||||
}
|
||||
|
||||
impl BotConfig {
|
||||
/// Determine the oldest allowable message age, in seconds. If the
|
||||
/// setting is defined, use that value. If it is not defined, fall
|
||||
|
@ -40,6 +79,7 @@ impl BotConfig {
|
|||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Config {
|
||||
matrix: MatrixConfig,
|
||||
database: Option<DatabaseConfig>,
|
||||
bot: Option<BotConfig>,
|
||||
}
|
||||
|
||||
|
@ -65,6 +105,16 @@ impl Config {
|
|||
&self.matrix.password
|
||||
}
|
||||
|
||||
/// The path to the database storage directory.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn database_path(&self) -> String {
|
||||
self.database
|
||||
.as_ref()
|
||||
.map(|db| db.path())
|
||||
.unwrap_or_else(|| db_path_from_env())
|
||||
}
|
||||
|
||||
/// Figure out the allowed oldest message age, in seconds. This will
|
||||
/// be the defined oldest message age in the bot config, if the bot
|
||||
/// configuration and associated "oldest_message_age" setting are
|
||||
|
@ -92,6 +142,9 @@ mod tests {
|
|||
username: "".to_owned(),
|
||||
password: "".to_owned(),
|
||||
},
|
||||
database: Some(DatabaseConfig {
|
||||
path: Some("".to_owned()),
|
||||
}),
|
||||
bot: Some(BotConfig {
|
||||
oldest_message_age: None,
|
||||
}),
|
||||
|
@ -108,9 +161,139 @@ mod tests {
|
|||
username: "".to_owned(),
|
||||
password: "".to_owned(),
|
||||
},
|
||||
database: Some(DatabaseConfig {
|
||||
path: Some("".to_owned()),
|
||||
}),
|
||||
bot: None,
|
||||
};
|
||||
|
||||
assert_eq!(15 * 60, cfg.oldest_message_age());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn db_path_uses_setting_first_test() {
|
||||
let cfg = Config {
|
||||
matrix: MatrixConfig {
|
||||
home_server: "".to_owned(),
|
||||
username: "".to_owned(),
|
||||
password: "".to_owned(),
|
||||
},
|
||||
database: Some(DatabaseConfig {
|
||||
path: Some("the-db-path".to_owned()),
|
||||
}),
|
||||
bot: None,
|
||||
};
|
||||
|
||||
assert_eq!("the-db-path".to_owned(), cfg.database_path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn db_path_uses_env_if_setting_not_defined_test() {
|
||||
env::set_var("DATABASE_PATH", "the-db-path");
|
||||
|
||||
let cfg = Config {
|
||||
matrix: MatrixConfig {
|
||||
home_server: "".to_owned(),
|
||||
username: "".to_owned(),
|
||||
password: "".to_owned(),
|
||||
},
|
||||
database: Some(DatabaseConfig { path: None }),
|
||||
bot: None,
|
||||
};
|
||||
|
||||
assert_eq!("the-db-path".to_owned(), cfg.database_path());
|
||||
|
||||
env::remove_var("DATABASE_PATH");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn db_path_uses_env_if_section_not_defined_test() {
|
||||
env::set_var("DATABASE_PATH", "the-db-path");
|
||||
|
||||
let cfg = Config {
|
||||
matrix: MatrixConfig {
|
||||
home_server: "".to_owned(),
|
||||
username: "".to_owned(),
|
||||
password: "".to_owned(),
|
||||
},
|
||||
database: None,
|
||||
bot: None,
|
||||
};
|
||||
|
||||
assert_eq!("the-db-path".to_owned(), cfg.database_path());
|
||||
|
||||
env::remove_var("DATABASE_PATH");
|
||||
}
|
||||
|
||||
use indoc::indoc;
|
||||
|
||||
#[test]
|
||||
fn deserialize_config_without_bot_section_test() {
|
||||
let contents = indoc! {"
|
||||
[matrix]
|
||||
home_server = 'https://matrix.example.com'
|
||||
username = 'username'
|
||||
password = 'password'
|
||||
|
||||
[database]
|
||||
path = ''
|
||||
"};
|
||||
|
||||
let cfg: Result<_, _> = deserialize_config(contents);
|
||||
assert_eq!(true, cfg.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_config_without_oldest_message_setting_test() {
|
||||
let contents = indoc! {"
|
||||
[matrix]
|
||||
home_server = 'https://matrix.example.com'
|
||||
username = 'username'
|
||||
password = 'password'
|
||||
|
||||
[database]
|
||||
path = ''
|
||||
|
||||
[bot]
|
||||
not_a_real_setting = 2
|
||||
"};
|
||||
|
||||
let cfg: Result<_, _> = deserialize_config(contents);
|
||||
assert_eq!(true, cfg.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_config_without_db_path_setting_test() {
|
||||
let contents = indoc! {"
|
||||
[matrix]
|
||||
home_server = 'https://matrix.example.com'
|
||||
username = 'username'
|
||||
password = 'password'
|
||||
|
||||
[database]
|
||||
not_a_real_setting = 1
|
||||
|
||||
[bot]
|
||||
not_a_real_setting = 2
|
||||
"};
|
||||
|
||||
let cfg: Result<_, _> = deserialize_config(contents);
|
||||
assert_eq!(true, cfg.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_config_without_db_section_test() {
|
||||
let contents = indoc! {"
|
||||
[matrix]
|
||||
home_server = 'https://matrix.example.com'
|
||||
username = 'username'
|
||||
password = 'password'
|
||||
|
||||
[bot]
|
||||
not_a_real_setting = 2
|
||||
"};
|
||||
|
||||
let cfg: Result<_, _> = deserialize_config(contents);
|
||||
assert_eq!(true, cfg.is_ok());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
use crate::db::Database;
|
||||
|
||||
/// A context carried through the system providing access to things
|
||||
/// like the database.
|
||||
#[derive(Clone)]
|
||||
pub struct Context<'a> {
|
||||
pub db: &'a Database,
|
||||
pub room_id: &'a str,
|
||||
pub username: &'a str,
|
||||
pub message_body: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Context<'a> {
|
||||
pub fn new(
|
||||
db: &'a Database,
|
||||
room_id: &'a str,
|
||||
username: &'a str,
|
||||
message_body: &'a str,
|
||||
) -> Context<'a> {
|
||||
Context {
|
||||
db: db,
|
||||
room_id: room_id,
|
||||
username: username,
|
||||
message_body: message_body,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
use byteorder::LittleEndian;
|
||||
use sled::{Db, IVec};
|
||||
use thiserror::Error;
|
||||
use zerocopy::byteorder::I32;
|
||||
use zerocopy::{AsBytes, LayoutVerified};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Database {
|
||||
db: Db,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DataError {
|
||||
#[error("value does not exist for key: {0}")]
|
||||
KeyDoesNotExist(String),
|
||||
|
||||
#[error("internal database error: {0}")]
|
||||
InternalError(#[from] sled::Error),
|
||||
}
|
||||
|
||||
fn to_key(room_id: &str, username: &str, variable_name: &str) -> Vec<u8> {
|
||||
let mut key = vec![];
|
||||
key.extend_from_slice(room_id.as_bytes());
|
||||
key.extend_from_slice(username.as_bytes());
|
||||
key.extend_from_slice(variable_name.as_bytes());
|
||||
key
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub fn new(db: &Db) -> Database {
|
||||
Database { db: db.clone() }
|
||||
}
|
||||
|
||||
pub fn get_user_variable(
|
||||
&self,
|
||||
room_id: &str,
|
||||
username: &str,
|
||||
variable_name: &str,
|
||||
) -> Result<Option<i32>, DataError> {
|
||||
let key = to_key(room_id, username, variable_name);
|
||||
|
||||
if let Some(raw_value) = self.db.get(key)? {
|
||||
let layout: LayoutVerified<&[u8], I32<LittleEndian>> =
|
||||
LayoutVerified::new_unaligned(&*raw_value).expect("bytes do not fit schema");
|
||||
|
||||
let value: I32<LittleEndian> = *layout;
|
||||
Ok(Some(value.get()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_user_variable(
|
||||
&self,
|
||||
room_id: &str,
|
||||
username: &str,
|
||||
variable_name: &str,
|
||||
value: i32,
|
||||
) -> Result<(), DataError> {
|
||||
let key = to_key(room_id, username, variable_name);
|
||||
let db_value: I32<LittleEndian> = I32::new(value);
|
||||
self.db.insert(key, IVec::from(db_value.as_bytes()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_user_variable(
|
||||
&self,
|
||||
room_id: &str,
|
||||
username: &str,
|
||||
variable_name: &str,
|
||||
) -> Result<(), DataError> {
|
||||
let key = to_key(room_id, username, variable_name);
|
||||
if let Some(_) = self.db.remove(key)? {
|
||||
Ok(())
|
||||
} else {
|
||||
let mut key = room_id.to_owned();
|
||||
key.push_str(username);
|
||||
key.push_str(variable_name);
|
||||
Err(DataError::KeyDoesNotExist(key))
|
||||
}
|
||||
}
|
||||
}
|
41
src/error.rs
41
src/error.rs
|
@ -1,11 +1,39 @@
|
|||
use crate::db::DataError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ConfigError {
|
||||
#[error("i/o error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("toml parsing error: {0}")]
|
||||
TomlParsingError(#[from] toml::de::Error),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CommandError {
|
||||
#[error("invalid command: {0}")]
|
||||
InvalidCommand(String),
|
||||
|
||||
#[error("ignored command")]
|
||||
IgnoredCommand,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum BotError {
|
||||
#[error("configuration error: {0}")]
|
||||
ConfigurationError(#[from] ConfigError),
|
||||
|
||||
/// Sync token couldn't be found.
|
||||
#[error("the sync token could not be retrieved")]
|
||||
SyncTokenRequired,
|
||||
|
||||
#[error("command error: {0}")]
|
||||
CommandError(#[from] CommandError),
|
||||
|
||||
#[error("database error: {0}")]
|
||||
DataError(#[from] DataError),
|
||||
|
||||
#[error("the message should not be processed because it failed validation")]
|
||||
ShouldNotProcessError,
|
||||
|
||||
|
@ -31,15 +59,21 @@ pub enum BotError {
|
|||
#[error("toml parsing error")]
|
||||
TomlParsingError(#[from] toml::de::Error),
|
||||
|
||||
#[error("i/o error")]
|
||||
#[error("i/o error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("actor mailbox error: {0}")]
|
||||
ActixMailboxError(#[from] actix::MailboxError),
|
||||
|
||||
#[error("parsing error")]
|
||||
ParserError(#[from] combine::error::StringStreamError),
|
||||
|
||||
#[error("dice parsing error")]
|
||||
#[error("dice parsing error: {0}")]
|
||||
DiceParsingError(#[from] crate::cofd::parser::DiceParsingError),
|
||||
|
||||
#[error("variable parsing error: {0}")]
|
||||
VariableParsingError(#[from] crate::variables::VariableParsingError),
|
||||
|
||||
#[error("legacy parsing error")]
|
||||
NomParserError(nom::error::ErrorKind),
|
||||
|
||||
|
@ -48,4 +82,7 @@ pub enum BotError {
|
|||
|
||||
#[error("variables not yet supported")]
|
||||
VariablesNotSupported,
|
||||
|
||||
#[error("database error")]
|
||||
DatabaseErrror(#[from] sled::Error),
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
pub mod actors;
|
||||
pub mod bot;
|
||||
pub mod cofd;
|
||||
pub mod commands;
|
||||
pub mod config;
|
||||
pub mod context;
|
||||
pub mod db;
|
||||
pub mod dice;
|
||||
pub mod error;
|
||||
mod help;
|
||||
mod parser;
|
||||
pub mod roll;
|
||||
pub mod state;
|
||||
pub mod variables;
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
use crate::error::BotError;
|
||||
use combine::parser::char::{digit, letter, spaces};
|
||||
use combine::{many1, Parser};
|
||||
use thiserror::Error;
|
||||
|
||||
enum ParsedValue {
|
||||
Valid(i32),
|
||||
Invalid,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum VariableParsingError {
|
||||
#[error("invalid variable value, must be a number")]
|
||||
InvalidValue,
|
||||
|
||||
#[error("unconsumed input")]
|
||||
UnconsumedInput,
|
||||
}
|
||||
|
||||
pub fn parse_set_variable(input: &str) -> Result<(String, i32), BotError> {
|
||||
let name = many1(letter()).map(|value: String| value);
|
||||
|
||||
let value = many1(digit()).map(|value: String| match value.parse::<i32>() {
|
||||
Ok(num) => ParsedValue::Valid(num),
|
||||
_ => ParsedValue::Invalid,
|
||||
});
|
||||
|
||||
let mut parser = name.skip(spaces().silent()).and(value);
|
||||
let (result, rest) = parser.parse(input)?;
|
||||
|
||||
if rest.len() == 0 {
|
||||
match result {
|
||||
(variable_name, ParsedValue::Valid(value)) => Ok((variable_name, value)),
|
||||
_ => Err(BotError::VariableParsingError(
|
||||
VariableParsingError::InvalidValue,
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
Err(BotError::VariableParsingError(
|
||||
VariableParsingError::UnconsumedInput,
|
||||
))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue