Add dicebot-roll dice expression parser

This commit is contained in:
Taylor C. Richberger 2020-04-19 16:07:33 -06:00
parent b4ccae581a
commit 59cb407eae
8 changed files with 199 additions and 17 deletions

37
Cargo.lock generated
View File

@ -1,5 +1,14 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
[[package]]
name = "aho-corasick"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "arc-swap" name = "arc-swap"
version = "0.4.5" version = "0.4.5"
@ -16,6 +25,7 @@ checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
name = "axfive-matrix-dicebot" name = "axfive-matrix-dicebot"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"regex",
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
@ -581,6 +591,24 @@ version = "0.1.56"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
[[package]]
name = "regex"
version = "1.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
"thread_local",
]
[[package]]
name = "regex-syntax"
version = "0.6.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae"
[[package]] [[package]]
name = "remove_dir_all" name = "remove_dir_all"
version = "0.5.2" version = "0.5.2"
@ -754,6 +782,15 @@ dependencies = [
"winapi 0.3.8", "winapi 0.3.8",
] ]
[[package]]
name = "thread_local"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
dependencies = [
"lazy_static",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.1.42" version = "0.1.42"

View File

@ -9,7 +9,8 @@ edition = "2018"
[dependencies] [dependencies]
reqwest = "^0.10" reqwest = "^0.10"
serde_json = "^1" serde_json = "^1"
toml = "0.5" toml = "^0.5"
regex = "^1.3"
[dependencies.serde] [dependencies.serde]
version = "^1" version = "^1"

View File

@ -0,0 +1,7 @@
{
"folders": [
{
"path": "."
}
]
}

128
src/bin/dicebot-roll.rs Normal file
View File

@ -0,0 +1,128 @@
use regex::Regex;
use std::error::Error;
use std::fmt;
use std::str::FromStr;
#[derive(Debug)]
struct ParseError {
reason: String,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ParseError: {}", self.reason)
}
}
impl Error for ParseError {}
#[derive(Debug)]
struct Dice {
count: u32,
sides: u32,
}
#[derive(Debug)]
enum Roll {
Dice(Dice),
Bonus(u32),
}
impl From<u32> for Roll {
fn from(value: u32) -> Roll {
Roll::Bonus(value)
}
}
impl From<Dice> for Roll {
fn from(value: Dice) -> Roll {
Roll::Dice(value)
}
}
impl FromStr for Roll {
type Err = Box<dyn Error>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
let regex = Regex::new(r"(?i)^(\d+)\s*(d\s*(\d+))?$")?;
let captures = match regex.captures(s) {
Some(captures) => captures,
None => {
return Err(ParseError {
reason: format!("{:?} is not a legal Roll Part expression", s),
}
.into())
}
};
match captures.get(2) {
Some(_) => Ok(Dice {
count: captures.get(1).unwrap().as_str().parse()?,
sides: captures.get(3).unwrap().as_str().parse()?,
}
.into()),
None => Ok(Roll::Bonus(captures.get(1).unwrap().as_str().parse()?)),
}
}
}
#[derive(Debug)]
enum Part {
Plus(Roll),
Minus(Roll),
}
impl FromStr for Part {
type Err = Box<dyn Error>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
match s.chars().next() {
Some('+') => Ok(Part::Plus(s[1..].parse()?)),
Some('-') => Ok(Part::Minus(s[1..].parse()?)),
Some(_) => Ok(Part::Plus(s.parse()?)),
None => Err(ParseError {
reason: format!("{:?} is not a legal Roll Part expression", s),
}
.into()),
}
}
}
#[derive(Debug)]
struct Expression(Vec<Part>);
impl FromStr for Expression {
type Err = Box<dyn Error>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
let validation_regex = Regex::new(
r"(?xi)^
([-\+])?\s*(\d+)\s*(d\s*(\d+))?
(\s*([-\+])\s*(\d+)\s*(d\s*(\d+))?)*
$",
)?;
if !validation_regex.is_match(s) {
return Err(ParseError {
reason: format!("{:?} is not a legal dice expression", s),
}
.into());
}
let part = Regex::new(r"(?i)[-\+]?\s*\d+\s*(d\s*(\d+))?")?;
let results: Result<_, _> = part.find_iter(s).map(|p| p.as_str().parse()).collect();
Ok(Expression(results?))
}
}
fn main() -> Result<(), Box<dyn Error>> {
let roll_string = std::env::args().skip(1).collect::<Vec<String>>().join(" ");
// first regex needs to be different because the sign is mandatory for the rest
let expression: Expression = roll_string.parse()?;
println!("{:?}", expression);
Ok(())
}

View File

@ -1,6 +1,6 @@
use axfive_matrix_dicebot::bot::DiceBot;
use tokio::select; use tokio::select;
use tokio::signal::unix::{signal, SignalKind}; use tokio::signal::unix::{signal, SignalKind};
use axfive_matrix_dicebot::bot::DiceBot;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {

View File

@ -1,10 +1,11 @@
use serde::{self, Deserialize, Serialize};
use reqwest::{Client, Url};
use crate::matrix::SyncCommand; use crate::matrix::SyncCommand;
use std::path::PathBuf; use reqwest::{Client, Url};
use serde::{self, Deserialize, Serialize};
use std::fs; use std::fs;
use std::path::PathBuf;
const USER_AGENT: &str = "AxFive Matrix DiceBot/0.1.0 (+https://gitlab.com/Taywee/axfive-matrix-dicebot)"; const USER_AGENT: &str =
"AxFive Matrix DiceBot/0.1.0 (+https://gitlab.com/Taywee/axfive-matrix-dicebot)";
/// The "matrix" section of the config, which gives home server, login information, and etc. /// The "matrix" section of the config, which gives home server, login information, and etc.
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -24,7 +25,7 @@ pub struct Config {
} }
/// The actual dicebot structure, which drives the entire operation. /// The actual dicebot structure, which drives the entire operation.
/// ///
/// This is the core of the dicebot program. /// This is the core of the dicebot program.
pub struct DiceBot { pub struct DiceBot {
config_path: Option<PathBuf>, config_path: Option<PathBuf>,
@ -42,20 +43,24 @@ struct LoginResponse {
impl DiceBot { impl DiceBot {
/// Create a new dicebot from the given config path and config /// Create a new dicebot from the given config path and config
pub async fn new(config_path: Option<PathBuf>, config: Config) -> Result<Self, Box<dyn std::error::Error>> { pub async fn new(
config_path: Option<PathBuf>,
config: Config,
) -> Result<Self, Box<dyn std::error::Error>> {
let home_server: Url = format!("https://{}", config.matrix.home_server).parse()?; let home_server: Url = format!("https://{}", config.matrix.home_server).parse()?;
let client = Client::new(); let client = Client::new();
let request = serde_json::to_string(&config.matrix.login)?; let request = serde_json::to_string(&config.matrix.login)?;
let mut login_url = home_server.clone(); let mut login_url = home_server.clone();
login_url.set_path("/_matrix/client/r0/login"); login_url.set_path("/_matrix/client/r0/login");
let response = client.post(login_url) let response = client
.post(login_url)
.header("user-agent", USER_AGENT) .header("user-agent", USER_AGENT)
.body(request) .body(request)
.send() .send()
.await?; .await?;
let body: LoginResponse = serde_json::from_str(&response.text().await?)?; let body: LoginResponse = serde_json::from_str(&response.text().await?)?;
let next_batch = config.matrix.next_batch.clone(); let next_batch = config.matrix.next_batch.clone();
Ok(DiceBot{ Ok(DiceBot {
home_server, home_server,
config_path, config_path,
client, client,
@ -66,7 +71,9 @@ impl DiceBot {
} }
/// Create a new dicebot, storing the config path to write it out /// Create a new dicebot, storing the config path to write it out
pub async fn from_path<P: Into<PathBuf>>(config_path: P) -> Result<Self, Box<dyn std::error::Error>> { pub async fn from_path<P: Into<PathBuf>>(
config_path: P,
) -> Result<Self, Box<dyn std::error::Error>> {
let config_path = config_path.into(); let config_path = config_path.into();
let config = { let config = {
let contents = fs::read_to_string(&config_path)?; let contents = fs::read_to_string(&config_path)?;
@ -98,10 +105,11 @@ impl DiceBot {
// TODO: handle http 429 // TODO: handle http 429
if let Some(since) = &self.next_batch { if let Some(since) = &self.next_batch {
sync_url.query_pairs_mut() sync_url.query_pairs_mut().append_pair("since", since);
.append_pair("since", since);
} }
let body = self.client.get(sync_url) let body = self
.client
.get(sync_url)
.header("user-agent", USER_AGENT) .header("user-agent", USER_AGENT)
.send() .send()
.await? .await?
@ -117,7 +125,8 @@ impl DiceBot {
/// construction /// construction
pub async fn logout(mut self) -> Result<(), Box<dyn std::error::Error>> { pub async fn logout(mut self) -> Result<(), Box<dyn std::error::Error>> {
let logout_url = self.url("/_matrix/client/r0/logout", &[]); let logout_url = self.url("/_matrix/client/r0/logout", &[]);
self.client.post(logout_url) self.client
.post(logout_url)
.header("user-agent", USER_AGENT) .header("user-agent", USER_AGENT)
.body("{}") .body("{}")
.send() .send()

View File

@ -1,2 +1,2 @@
pub mod matrix;
pub mod bot; pub mod bot;
pub mod matrix;

View File

@ -5,7 +5,7 @@ use std::collections::HashMap;
#[serde(tag = "msgtype")] #[serde(tag = "msgtype")]
#[serde(rename = "m.text")] #[serde(rename = "m.text")]
pub struct TextMessage { pub struct TextMessage {
body: String body: String,
} }
// Need untagged because redactions are blank // Need untagged because redactions are blank