diff --git a/.drone.yml b/.drone.yml index f45000c..9710734 100644 --- a/.drone.yml +++ b/.drone.yml @@ -7,6 +7,7 @@ steps: commands: - apt-get update - apt-get install -y cmake + - rustup component add rustfmt - cargo build --verbose --all - cargo test --verbose --all diff --git a/Cargo.lock b/Cargo.lock index 28835d9..536a0a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,6 +101,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "anyhow" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" + [[package]] name = "arrayref" version = "0.3.6" @@ -119,6 +125,27 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f093eed78becd229346bf859eec0aa4dd7ddde0757287b2b4107a1f09c80002" +[[package]] +name = "async-stream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171374e7e3b2504e0e5236e3b59260560f9fe94bfe9ac39ba5e4e929c5590625" +dependencies = [ + "async-stream-impl", + "futures-core", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "648ed8c8d2ce5409ccd57453d9d1b214b342a0d69376a6feda1fd6cae3299308" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.50" @@ -529,6 +556,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fixedbitset" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" + [[package]] name = "fnv" version = "1.0.7" @@ -991,6 +1024,15 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.10.0" @@ -1263,6 +1305,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + [[package]] name = "native-tls" version = "0.2.7" @@ -1462,6 +1510,16 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "petgraph" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "phf" version = "0.8.0" @@ -1618,6 +1676,57 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "prost" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e6984d2f1a23009bd270b8bb56d0926810a3d483f59c987d77969e9d8e840b2" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d3ebd75ac2679c2af3a92246639f9fcc8a442ee420719cc4fe195b98dd5fa3" +dependencies = [ + "bytes", + "heck", + "itertools 0.9.0", + "log", + "multimap", + "petgraph", + "prost", + "prost-types", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "169a15f3008ecb5160cba7d37bcd690a7601b6d30cfb87a117d45e59d52af5d4" +dependencies = [ + "anyhow", + "itertools 0.9.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b518d7cdd93dab1d1122cf07fa9a60771836c668dde9d9e2a139f957f0d9f1bb" +dependencies = [ + "bytes", + "prost", +] + [[package]] name = "quote" version = "1.0.9" @@ -2557,11 +2666,12 @@ dependencies = [ "futures", "html2text", "indoc", - "itertools", + "itertools 0.10.0", "log", "matrix-sdk", "nom 5.1.2", "phf", + "prost", "rand 0.8.3", "refinery", "rust-argon2", @@ -2569,13 +2679,25 @@ dependencies = [ "sqlx", "substring", "tempfile", + "tenebrous-rpc", "thiserror", "tokio", "toml", + "tonic", + "tonic-build", "tracing-subscriber", "url", ] +[[package]] +name = "tenebrous-rpc" +version = "0.1.0" +dependencies = [ + "prost", + "tonic", + "tonic-build", +] + [[package]] name = "thiserror" version = "1.0.24" @@ -2743,6 +2865,73 @@ dependencies = [ "serde", ] +[[package]] +name = "tonic" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac42cd97ac6bd2339af5bcabf105540e21e45636ec6fa6aae5e85d44db31be0" +dependencies = [ + "async-stream", + "async-trait", + "base64", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "percent-encoding", + "pin-project", + "prost", + "prost-derive", + "tokio", + "tokio-stream", + "tokio-util", + "tower", + "tower-service", + "tracing", + "tracing-futures", +] + +[[package]] +name = "tonic-build" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c695de27302f4697191dda1c7178131a8cb805463dda02864acb80fe1322fdcf" +dependencies = [ + "proc-macro2", + "prost-build", + "quote", + "syn", +] + +[[package]] +name = "tower" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60422bc7fefa2f3ec70359b8ff1caff59d785877eb70595904605bcc412470f" +dependencies = [ + "futures-core", + "futures-util", + "indexmap", + "pin-project", + "rand 0.8.3", + "slab", + "tokio", + "tokio-stream", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" + [[package]] name = "tower-service" version = "0.3.1" @@ -2756,6 +2945,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" dependencies = [ "cfg-if", + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -3055,6 +3245,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "which" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55551e42cbdf2ce2bedd2203d0cc08dba002c27510f86dab6d0ce304cba3dfe" +dependencies = [ + "either", + "libc", +] + [[package]] name = "whoami" version = "1.1.2" diff --git a/Cargo.toml b/Cargo.toml index 840c280..3c81cef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,47 +1,6 @@ -[package] -name = "tenebrous-dicebot" -version = "0.10.0" -authors = ["Taylor C. Richberger ", "projectmoon "] -edition = "2018" -license = 'AGPL-3.0-or-later' -description = 'An async Matrix dice bot for role-playing games' -readme = 'README.md' -repository = 'https://git.agnos.is/projectmoon/matrix-dicebot' -keywords = ["games", "dice", "matrix", "bot"] -categories = ["games"] +[workspace] -[dependencies] -log = "0.4" -tracing-subscriber = "0.2" -toml = "0.5" -nom = "5" -rand = "0.8" -rust-argon2 = "0.8" -thiserror = "1.0" -itertools = "0.10" -async-trait = "0.1" -url = "2.1" -dirs = "3.0" -indoc = "1.0" -combine = "4.5" -futures = "0.3" -html2text = "0.2" -phf = { version = "0.8", features = ["macros"] } -matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk", branch = "master" } -refinery = { version = "0.5", features = ["rusqlite"]} -barrel = { version = "0.6", features = ["sqlite3"] } -tempfile = "3" -substring = "1.4" -fuse-rust = "0.2" - -[dependencies.sqlx] -version = "0.5" -features = [ "offline", "sqlite", "runtime-tokio-native-tls" ] - -[dependencies.serde] -version = "1" -features = ['derive'] - -[dependencies.tokio] -version = "1" -features = [ "full" ] +members = [ + "dicebot", + "rpc", +] \ No newline at end of file diff --git a/dicebot/Cargo.toml b/dicebot/Cargo.toml new file mode 100644 index 0000000..994b71c --- /dev/null +++ b/dicebot/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "tenebrous-dicebot" +version = "0.10.0" +authors = ["Taylor C. Richberger ", "projectmoon "] +edition = "2018" +license = 'AGPL-3.0-or-later' +description = 'An async Matrix dice bot for role-playing games' +readme = 'README.md' +repository = 'https://git.agnos.is/projectmoon/matrix-dicebot' +keywords = ["games", "dice", "matrix", "bot"] +categories = ["games"] + +[build-dependencies] +tonic-build = "0.4" + +[dependencies] +log = "0.4" +tracing-subscriber = "0.2" +toml = "0.5" +nom = "5" +rand = "0.8" +rust-argon2 = "0.8" +thiserror = "1.0" +itertools = "0.10" +async-trait = "0.1" +url = "2.1" +dirs = "3.0" +indoc = "1.0" +combine = "4.5" +futures = "0.3" +html2text = "0.2" +phf = { version = "0.8", features = ["macros"] } +matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk", branch = "master" } +refinery = { version = "0.5", features = ["rusqlite"]} +barrel = { version = "0.6", features = ["sqlite3"] } +tempfile = "3" +substring = "1.4" +fuse-rust = "0.2" +tonic = "0.4" +prost = "0.7" +tenebrous-rpc = { path = "../rpc" } + +[dependencies.sqlx] +version = "0.5" +features = [ "offline", "sqlite", "runtime-tokio-native-tls" ] + +[dependencies.serde] +version = "1" +features = ['derive'] + +[dependencies.tokio] +version = "1" +features = [ "full" ] diff --git a/dicebot/Dockerfile b/dicebot/Dockerfile new file mode 100644 index 0000000..6dc7621 --- /dev/null +++ b/dicebot/Dockerfile @@ -0,0 +1,35 @@ +# Builder image with development dependencies. +FROM bougyman/voidlinux:glibc as builder +RUN xbps-install -Syu +RUN xbps-install -Sy base-devel rustup cargo cmake wget gnupg +RUN xbps-install -Sy openssl-devel libstdc++-devel +RUN rustup-init -qy +RUN rustup component add rustfmt # Needed for protobuf building. + +# Install tini for signal processing and zombie killing +ENV TINI_VERSION v0.19.0 +ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /usr/local/bin/tini +ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini.asc /tini.asc +RUN gpg --batch --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 595E85A6B1B4779EA4DAAEC70B588DFF0527A9B7 \ + && gpg --batch --verify /tini.asc /usr/local/bin/tini +RUN chmod +x /usr/local/bin/tini + +# Build dicebot +RUN mkdir -p /root/src +WORKDIR /root/src +ADD . ./ +RUN . /root/.cargo/env && cargo build --release + +# Final image +FROM bougyman/voidlinux:tiny +RUN xbps-install -Sy ca-certificates libstdc++ +COPY --from=builder \ + /root/src/target/release/dicebot \ + /usr/local/bin/ +COPY --from=builder \ + /usr/local/bin/tini \ + /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" ] diff --git a/sqlx-data.json b/dicebot/sqlx-data.json similarity index 100% rename from sqlx-data.json rename to dicebot/sqlx-data.json diff --git a/src/basic/dice.rs b/dicebot/src/basic/dice.rs similarity index 100% rename from src/basic/dice.rs rename to dicebot/src/basic/dice.rs diff --git a/src/basic/mod.rs b/dicebot/src/basic/mod.rs similarity index 100% rename from src/basic/mod.rs rename to dicebot/src/basic/mod.rs diff --git a/src/basic/parser.rs b/dicebot/src/basic/parser.rs similarity index 100% rename from src/basic/parser.rs rename to dicebot/src/basic/parser.rs diff --git a/src/basic/roll.rs b/dicebot/src/basic/roll.rs similarity index 100% rename from src/basic/roll.rs rename to dicebot/src/basic/roll.rs diff --git a/src/bin/dicebot-cmd.rs b/dicebot/src/bin/dicebot-cmd.rs similarity index 100% rename from src/bin/dicebot-cmd.rs rename to dicebot/src/bin/dicebot-cmd.rs diff --git a/src/bin/dicebot.rs b/dicebot/src/bin/dicebot.rs similarity index 52% rename from src/bin/dicebot.rs rename to dicebot/src/bin/dicebot.rs index a5abbb1..a1d488c 100644 --- a/src/bin/dicebot.rs +++ b/dicebot/src/bin/dicebot.rs @@ -1,21 +1,36 @@ //Needed for nested Result handling from tokio. Probably can go away after 1.47.0. #![type_length_limit = "7605144"] +use futures::try_join; use log::error; +use matrix_sdk::Client; use std::env; use std::sync::{Arc, RwLock}; use tenebrous_dicebot::bot::DiceBot; use tenebrous_dicebot::config::*; use tenebrous_dicebot::db::sqlite::Database; use tenebrous_dicebot::error::BotError; +use tenebrous_dicebot::rpc; use tenebrous_dicebot::state::DiceBotState; use tracing_subscriber::filter::EnvFilter; +/// Attempt to create config object and ddatabase connection pool from +/// the given config path. An error is returned if config creation or +/// database pool creation fails for some reason. +async fn init(config_path: &str) -> Result<(Arc, Database, Client), BotError> { + let cfg = read_config(config_path)?; + let cfg = Arc::new(cfg); + let sqlite_path = format!("{}/dicebot.sqlite", cfg.database_path()); + let db = Database::new(&sqlite_path).await?; + let client = tenebrous_dicebot::matrix::create_client(&cfg)?; + Ok((cfg, db, client)) +} + #[tokio::main] -async fn main() { +async fn main() -> Result<(), BotError> { let filter = if env::var("RUST_LOG").is_ok() { EnvFilter::from_default_env() } else { - EnvFilter::new("tenebrous_dicebot=info,dicebot=info,refinery=info") + EnvFilter::new("tonic=info,tenebrous_dicebot=info,dicebot=info,refinery=info") }; tracing_subscriber::fmt().with_env_filter(filter).init(); @@ -23,7 +38,9 @@ async fn main() { match run().await { Ok(_) => (), Err(e) => error!("Error: {}", e), - }; + } + + Ok(()) } async fn run() -> Result<(), BotError> { @@ -32,12 +49,22 @@ async fn run() -> Result<(), BotError> { .next() .expect("Need a config as an argument"); - let cfg = Arc::new(read_config(config_path)?); - let sqlite_path = format!("{}/dicebot.sqlite", cfg.database_path()); - let db = Database::new(&sqlite_path).await?; + let (cfg, db, client) = init(&config_path).await?; + let grpc = rpc::serve_grpc(&cfg, &db, &client); + let bot = run_bot(&cfg, &db, &client); + + match try_join!(bot, grpc) { + Ok(_) => (), + Err(e) => error!("Error: {}", e), + }; + + Ok(()) +} + +async fn run_bot(cfg: &Arc, db: &Database, client: &Client) -> Result<(), BotError> { let state = Arc::new(RwLock::new(DiceBotState::new(&cfg))); - match DiceBot::new(&cfg, &state, &db) { + match DiceBot::new(cfg, &state, db, client) { Ok(bot) => bot.run().await?, Err(e) => println!("Error connecting: {:?}", e), }; diff --git a/src/bin/dicebot_migrate.rs b/dicebot/src/bin/dicebot_migrate.rs similarity index 100% rename from src/bin/dicebot_migrate.rs rename to dicebot/src/bin/dicebot_migrate.rs diff --git a/dicebot/src/bin/tonic_client.rs b/dicebot/src/bin/tonic_client.rs new file mode 100644 index 0000000..4ac8fd8 --- /dev/null +++ b/dicebot/src/bin/tonic_client.rs @@ -0,0 +1,43 @@ +use tenebrous_rpc::protos::dicebot::UserIdRequest; +use tenebrous_rpc::protos::dicebot::{dicebot_client::DicebotClient, GetVariableRequest}; +use tonic::{metadata::MetadataValue, transport::Channel, Request}; + +async fn create_client( + shared_secret: &str, +) -> Result, Box> { + let channel = Channel::from_static("http://0.0.0.0:9090") + .connect() + .await?; + + let bearer = MetadataValue::from_str(&format!("Bearer {}", shared_secret))?; + + let client = DicebotClient::with_interceptor(channel, move |mut req: Request<()>| { + req.metadata_mut().insert("authorization", bearer.clone()); + Ok(req) + }); + + Ok(client) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let mut client = create_client("example-key").await?; + + // let request = tonic::Request::new(GetVariableRequest { + // user_id: "@projectmoon:agnos.is".into(), + // room_id: "!agICWvldGfuCywUVUM:agnos.is".into(), + // variable_name: "stuff".into(), + // }); + + // let response = client.get_variable(request).await?.into_inner(); + + let request = tonic::Request::new(UserIdRequest { + user_id: "@projectmoon:agnos.is".into(), + }); + + let response = client.rooms_for_user(request).await?.into_inner(); + // println!("RESPONSE={:?}", response); + // println!("User friendly response is: {:?}", response.value); + println!("Rooms: {:?}", response.rooms); + Ok(()) +} diff --git a/src/bot/command_execution.rs b/dicebot/src/bot/command_execution.rs similarity index 100% rename from src/bot/command_execution.rs rename to dicebot/src/bot/command_execution.rs diff --git a/src/bot/event_handlers.rs b/dicebot/src/bot/event_handlers.rs similarity index 100% rename from src/bot/event_handlers.rs rename to dicebot/src/bot/event_handlers.rs diff --git a/src/bot/mod.rs b/dicebot/src/bot/mod.rs similarity index 87% rename from src/bot/mod.rs rename to dicebot/src/bot/mod.rs index 965b7f3..955f9ff 100644 --- a/src/bot/mod.rs +++ b/dicebot/src/bot/mod.rs @@ -4,13 +4,10 @@ use crate::db::sqlite::Database; use crate::db::DbState; use crate::error::BotError; use crate::state::DiceBotState; -use dirs; use log::info; -use matrix_sdk::{self, identifiers::EventId, room::Joined, Client, ClientConfig, SyncSettings}; +use matrix_sdk::{self, identifiers::EventId, room::Joined, Client, SyncSettings}; use std::clone::Clone; -use std::path::PathBuf; use std::sync::{Arc, RwLock}; -use url::Url; mod command_execution; pub mod event_handlers; @@ -35,22 +32,6 @@ pub struct DiceBot { db: Database, } -fn cache_dir() -> Result { - let mut dir = dirs::cache_dir().ok_or(BotError::NoCacheDirectoryError)?; - dir.push("matrix-dicebot"); - Ok(dir) -} - -/// Creates the matrix client. -fn create_client(config: &Config) -> Result { - let cache_dir = cache_dir()?; - //let store = JsonStore::open(&cache_dir)?; - let client_config = ClientConfig::new().store_path(cache_dir); - let homeserver_url = Url::parse(&config.matrix_homeserver())?; - - Ok(Client::new_with_config(homeserver_url, client_config)?) -} - impl DiceBot { /// Create a new dicebot with the given configuration and state /// actor. This function returns a Result because it is possible @@ -60,9 +41,10 @@ impl DiceBot { config: &Arc, state: &Arc>, db: &Database, + client: &Client, ) -> Result { Ok(DiceBot { - client: create_client(&config)?, + client: client.clone(), config: config.clone(), state: state.clone(), db: db.clone(), diff --git a/src/cofd/dice.rs b/dicebot/src/cofd/dice.rs similarity index 100% rename from src/cofd/dice.rs rename to dicebot/src/cofd/dice.rs diff --git a/src/cofd/mod.rs b/dicebot/src/cofd/mod.rs similarity index 100% rename from src/cofd/mod.rs rename to dicebot/src/cofd/mod.rs diff --git a/src/cofd/parser.rs b/dicebot/src/cofd/parser.rs similarity index 100% rename from src/cofd/parser.rs rename to dicebot/src/cofd/parser.rs diff --git a/src/commands/basic_rolling.rs b/dicebot/src/commands/basic_rolling.rs similarity index 100% rename from src/commands/basic_rolling.rs rename to dicebot/src/commands/basic_rolling.rs diff --git a/src/commands/cofd.rs b/dicebot/src/commands/cofd.rs similarity index 100% rename from src/commands/cofd.rs rename to dicebot/src/commands/cofd.rs diff --git a/src/commands/cthulhu.rs b/dicebot/src/commands/cthulhu.rs similarity index 100% rename from src/commands/cthulhu.rs rename to dicebot/src/commands/cthulhu.rs diff --git a/src/commands/management.rs b/dicebot/src/commands/management.rs similarity index 100% rename from src/commands/management.rs rename to dicebot/src/commands/management.rs diff --git a/src/commands/misc.rs b/dicebot/src/commands/misc.rs similarity index 100% rename from src/commands/misc.rs rename to dicebot/src/commands/misc.rs diff --git a/src/commands/mod.rs b/dicebot/src/commands/mod.rs similarity index 100% rename from src/commands/mod.rs rename to dicebot/src/commands/mod.rs diff --git a/src/commands/parser.rs b/dicebot/src/commands/parser.rs similarity index 100% rename from src/commands/parser.rs rename to dicebot/src/commands/parser.rs diff --git a/src/commands/rooms.rs b/dicebot/src/commands/rooms.rs similarity index 100% rename from src/commands/rooms.rs rename to dicebot/src/commands/rooms.rs diff --git a/src/commands/variables.rs b/dicebot/src/commands/variables.rs similarity index 100% rename from src/commands/variables.rs rename to dicebot/src/commands/variables.rs diff --git a/src/config.rs b/dicebot/src/config.rs similarity index 90% rename from src/config.rs rename to dicebot/src/config.rs index 22e9b91..fc842db 100644 --- a/src/config.rs +++ b/dicebot/src/config.rs @@ -4,10 +4,6 @@ use std::fs; use std::path::PathBuf; use thiserror::Error; -/// Shortcut to defining db migration versions. Will probably -/// eventually be moved to a config file. -const MIGRATION_VERSION: u32 = 5; - #[derive(Error, Debug)] pub enum ConfigError { #[error("i/o error: {0}")] @@ -53,10 +49,19 @@ fn db_path_from_env() -> String { } /// The "bot" section of the config file, for bot settings. -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, Default)] struct BotConfig { /// How far back from current time should we process a message? oldest_message_age: Option, + + /// What address and port to run the RPC service on. If not + /// specified, RPC will not be enabled. + rpc_addr: Option, + + /// The shared secret key between the bot and any RPC clients that + /// want to connect to it. The RPC server will reject any clients + /// that don't present the shared key. + rpc_key: Option, } /// The "database" section of the config file. @@ -84,6 +89,18 @@ impl BotConfig { self.oldest_message_age .unwrap_or(DEFAULT_OLDEST_MESSAGE_AGE) } + + #[inline] + #[must_use] + fn rpc_addr(&self) -> Option { + self.rpc_addr.clone() + } + + #[inline] + #[must_use] + fn rpc_key(&self) -> Option { + self.rpc_key.clone() + } } /// Represents the toml config file for the dicebot. The sections of @@ -128,15 +145,6 @@ impl Config { .unwrap_or_else(|| db_path_from_env()) } - /// The current migration version we expect of the database. If - /// this number is higher than the one in the database, we will - /// execute migrations to update the data. - #[inline] - #[must_use] - pub fn migration_version(&self) -> u32 { - MIGRATION_VERSION - } - /// 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 @@ -150,6 +158,18 @@ impl Config { .map(|bc| bc.oldest_message_age()) .unwrap_or(DEFAULT_OLDEST_MESSAGE_AGE) } + + #[inline] + #[must_use] + pub fn rpc_addr(&self) -> Option { + self.bot.as_ref().and_then(|bc| bc.rpc_addr()) + } + + #[inline] + #[must_use] + pub fn rpc_key(&self) -> Option { + self.bot.as_ref().and_then(|bc| bc.rpc_key()) + } } #[cfg(test)] @@ -169,6 +189,7 @@ mod tests { }), bot: Some(BotConfig { oldest_message_age: None, + ..Default::default() }), }; diff --git a/src/context.rs b/dicebot/src/context.rs similarity index 100% rename from src/context.rs rename to dicebot/src/context.rs diff --git a/src/cthulhu/dice.rs b/dicebot/src/cthulhu/dice.rs similarity index 100% rename from src/cthulhu/dice.rs rename to dicebot/src/cthulhu/dice.rs diff --git a/src/cthulhu/mod.rs b/dicebot/src/cthulhu/mod.rs similarity index 100% rename from src/cthulhu/mod.rs rename to dicebot/src/cthulhu/mod.rs diff --git a/src/cthulhu/parser.rs b/dicebot/src/cthulhu/parser.rs similarity index 100% rename from src/cthulhu/parser.rs rename to dicebot/src/cthulhu/parser.rs diff --git a/src/db/errors.rs b/dicebot/src/db/errors.rs similarity index 100% rename from src/db/errors.rs rename to dicebot/src/db/errors.rs diff --git a/src/db/mod.rs b/dicebot/src/db/mod.rs similarity index 100% rename from src/db/mod.rs rename to dicebot/src/db/mod.rs diff --git a/src/db/sqlite/migrator/migrations/V1__variables.rs b/dicebot/src/db/sqlite/migrator/migrations/V1__variables.rs similarity index 100% rename from src/db/sqlite/migrator/migrations/V1__variables.rs rename to dicebot/src/db/sqlite/migrator/migrations/V1__variables.rs diff --git a/src/db/sqlite/migrator/migrations/V2__room_info.rs b/dicebot/src/db/sqlite/migrator/migrations/V2__room_info.rs similarity index 100% rename from src/db/sqlite/migrator/migrations/V2__room_info.rs rename to dicebot/src/db/sqlite/migrator/migrations/V2__room_info.rs diff --git a/src/db/sqlite/migrator/migrations/V3__dbstate.rs b/dicebot/src/db/sqlite/migrator/migrations/V3__dbstate.rs similarity index 100% rename from src/db/sqlite/migrator/migrations/V3__dbstate.rs rename to dicebot/src/db/sqlite/migrator/migrations/V3__dbstate.rs diff --git a/src/db/sqlite/migrator/migrations/V4__room_events.rs b/dicebot/src/db/sqlite/migrator/migrations/V4__room_events.rs similarity index 100% rename from src/db/sqlite/migrator/migrations/V4__room_events.rs rename to dicebot/src/db/sqlite/migrator/migrations/V4__room_events.rs diff --git a/src/db/sqlite/migrator/migrations/V5__room_users.rs b/dicebot/src/db/sqlite/migrator/migrations/V5__room_users.rs similarity index 100% rename from src/db/sqlite/migrator/migrations/V5__room_users.rs rename to dicebot/src/db/sqlite/migrator/migrations/V5__room_users.rs diff --git a/src/db/sqlite/migrator/migrations/V6__user_accounts.rs b/dicebot/src/db/sqlite/migrator/migrations/V6__user_accounts.rs similarity index 100% rename from src/db/sqlite/migrator/migrations/V6__user_accounts.rs rename to dicebot/src/db/sqlite/migrator/migrations/V6__user_accounts.rs diff --git a/src/db/sqlite/migrator/migrations/V7__no_more_room_state.rs b/dicebot/src/db/sqlite/migrator/migrations/V7__no_more_room_state.rs similarity index 100% rename from src/db/sqlite/migrator/migrations/V7__no_more_room_state.rs rename to dicebot/src/db/sqlite/migrator/migrations/V7__no_more_room_state.rs diff --git a/src/db/sqlite/migrator/migrations/V8__user_state.rs b/dicebot/src/db/sqlite/migrator/migrations/V8__user_state.rs similarity index 100% rename from src/db/sqlite/migrator/migrations/V8__user_state.rs rename to dicebot/src/db/sqlite/migrator/migrations/V8__user_state.rs diff --git a/src/db/sqlite/migrator/migrations/V9__nullable_password_and_account_status.rs b/dicebot/src/db/sqlite/migrator/migrations/V9__nullable_password_and_account_status.rs similarity index 100% rename from src/db/sqlite/migrator/migrations/V9__nullable_password_and_account_status.rs rename to dicebot/src/db/sqlite/migrator/migrations/V9__nullable_password_and_account_status.rs diff --git a/src/db/sqlite/migrator/migrations/mod.rs b/dicebot/src/db/sqlite/migrator/migrations/mod.rs similarity index 100% rename from src/db/sqlite/migrator/migrations/mod.rs rename to dicebot/src/db/sqlite/migrator/migrations/mod.rs diff --git a/src/db/sqlite/migrator/mod.rs b/dicebot/src/db/sqlite/migrator/mod.rs similarity index 100% rename from src/db/sqlite/migrator/mod.rs rename to dicebot/src/db/sqlite/migrator/mod.rs diff --git a/src/db/sqlite/mod.rs b/dicebot/src/db/sqlite/mod.rs similarity index 100% rename from src/db/sqlite/mod.rs rename to dicebot/src/db/sqlite/mod.rs diff --git a/src/db/sqlite/rooms.rs b/dicebot/src/db/sqlite/rooms.rs similarity index 100% rename from src/db/sqlite/rooms.rs rename to dicebot/src/db/sqlite/rooms.rs diff --git a/src/db/sqlite/state.rs b/dicebot/src/db/sqlite/state.rs similarity index 100% rename from src/db/sqlite/state.rs rename to dicebot/src/db/sqlite/state.rs diff --git a/src/db/sqlite/users.rs b/dicebot/src/db/sqlite/users.rs similarity index 100% rename from src/db/sqlite/users.rs rename to dicebot/src/db/sqlite/users.rs diff --git a/src/db/sqlite/variables.rs b/dicebot/src/db/sqlite/variables.rs similarity index 100% rename from src/db/sqlite/variables.rs rename to dicebot/src/db/sqlite/variables.rs diff --git a/src/error.rs b/dicebot/src/error.rs similarity index 88% rename from src/error.rs rename to dicebot/src/error.rs index 8ad1914..e8c4a1a 100644 --- a/src/error.rs +++ b/dicebot/src/error.rs @@ -1,7 +1,10 @@ +use std::net::AddrParseError; + use crate::commands::CommandError; use crate::config::ConfigError; use crate::db::errors::DataError; use thiserror::Error; +use tonic::metadata::errors::InvalidMetadataValue; #[derive(Error, Debug)] pub enum BotError { @@ -93,6 +96,15 @@ pub enum BotError { #[error("room name or id does not exist")] RoomDoesNotExist, + + #[error("tonic transport error: {0}")] + TonicTransportError(#[from] tonic::transport::Error), + + #[error("address parsing error: {0}")] + AddressParseError(#[from] AddrParseError), + + #[error("invalid metadata value: {0}")] + TonicInvalidMetadata(#[from] InvalidMetadataValue), } #[derive(Error, Debug)] diff --git a/src/help.rs b/dicebot/src/help.rs similarity index 100% rename from src/help.rs rename to dicebot/src/help.rs diff --git a/src/lib.rs b/dicebot/src/lib.rs similarity index 94% rename from src/lib.rs rename to dicebot/src/lib.rs index eb49213..08bb2f6 100644 --- a/src/lib.rs +++ b/dicebot/src/lib.rs @@ -12,4 +12,5 @@ pub mod logic; pub mod matrix; pub mod models; mod parser; +pub mod rpc; pub mod state; diff --git a/src/logic.rs b/dicebot/src/logic.rs similarity index 100% rename from src/logic.rs rename to dicebot/src/logic.rs diff --git a/src/matrix.rs b/dicebot/src/matrix.rs similarity index 81% rename from src/matrix.rs rename to dicebot/src/matrix.rs index 7bec296..d1fa6c0 100644 --- a/src/matrix.rs +++ b/dicebot/src/matrix.rs @@ -1,6 +1,8 @@ +use std::path::PathBuf; + use futures::stream::{self, StreamExt, TryStreamExt}; use log::error; -use matrix_sdk::{events::room::message::NoticeMessageEventContent, room::Joined}; +use matrix_sdk::{events::room::message::NoticeMessageEventContent, room::Joined, ClientConfig}; use matrix_sdk::{ events::room::message::{InReplyTo, Relation}, events::room::message::{MessageEventContent, MessageType}, @@ -9,6 +11,15 @@ use matrix_sdk::{ Error as MatrixError, }; use matrix_sdk::{identifiers::RoomId, identifiers::UserId, Client}; +use url::Url; + +use crate::{config::Config, error::BotError}; + +fn cache_dir() -> Result { + let mut dir = dirs::cache_dir().ok_or(BotError::NoCacheDirectoryError)?; + dir.push("matrix-dicebot"); + Ok(dir) +} /// Extracts more detailed error messages out of a matrix SDK error. fn extract_error_message(error: MatrixError) -> String { @@ -20,6 +31,15 @@ fn extract_error_message(error: MatrixError) -> String { } } +/// Creates the matrix client. +pub fn create_client(config: &Config) -> Result { + let cache_dir = cache_dir()?; + let client_config = ClientConfig::new().store_path(cache_dir); + let homeserver_url = Url::parse(&config.matrix_homeserver())?; + + Ok(Client::new_with_config(homeserver_url, client_config)?) +} + /// Retrieve a list of users in a given room. pub async fn get_users_in_room( client: &Client, diff --git a/src/models.rs b/dicebot/src/models.rs similarity index 100% rename from src/models.rs rename to dicebot/src/models.rs diff --git a/src/parser/dice.rs b/dicebot/src/parser/dice.rs similarity index 100% rename from src/parser/dice.rs rename to dicebot/src/parser/dice.rs diff --git a/src/parser/mod.rs b/dicebot/src/parser/mod.rs similarity index 100% rename from src/parser/mod.rs rename to dicebot/src/parser/mod.rs diff --git a/src/parser/variables.rs b/dicebot/src/parser/variables.rs similarity index 100% rename from src/parser/variables.rs rename to dicebot/src/parser/variables.rs diff --git a/dicebot/src/rpc/mod.rs b/dicebot/src/rpc/mod.rs new file mode 100644 index 0000000..5a11f7e --- /dev/null +++ b/dicebot/src/rpc/mod.rs @@ -0,0 +1,50 @@ +use crate::error::BotError; +use crate::{config::Config, db::sqlite::Database}; +use log::{info, warn}; +use matrix_sdk::Client; +use service::DicebotRpcService; +use std::sync::Arc; +use tenebrous_rpc::protos::dicebot::dicebot_server::DicebotServer; +use tonic::{metadata::MetadataValue, transport::Server, Request, Status}; + +pub(crate) mod service; + +pub async fn serve_grpc( + config: &Arc, + db: &Database, + client: &Client, +) -> Result<(), BotError> { + match config.rpc_addr().zip(config.rpc_key()) { + Some((addr, rpc_key)) => { + let expected_bearer = MetadataValue::from_str(&format!("Bearer {}", rpc_key))?; + let addr = addr.parse()?; + + let rpc_service = DicebotRpcService { + db: db.clone(), + config: config.clone(), + client: client.clone(), + }; + + info!("Serving Dicebot gRPC service on {}", addr); + + let interceptor = move |req: Request<()>| match req.metadata().get("authorization") { + Some(bearer) if bearer == expected_bearer => Ok(req), + _ => Err(Status::unauthenticated("No valid auth token")), + }; + + let server = DicebotServer::with_interceptor(rpc_service, interceptor); + + Server::builder() + .add_service(server) + .serve(addr) + .await + .map_err(|e| e.into()) + } + _ => noop().await, + } +} + +pub async fn noop() -> Result<(), BotError> { + warn!("RPC address or shared secret not specified. Not enabling gRPC."); + Ok(()) +} diff --git a/dicebot/src/rpc/service.rs b/dicebot/src/rpc/service.rs new file mode 100644 index 0000000..1d3136b --- /dev/null +++ b/dicebot/src/rpc/service.rs @@ -0,0 +1,116 @@ +use crate::db::{errors::DataError, Variables}; +use crate::error::BotError; +use crate::matrix; +use crate::{config::Config, db::sqlite::Database}; +use futures::stream; +use futures::{StreamExt, TryFutureExt, TryStreamExt}; +use matrix_sdk::{identifiers::UserId, room::Joined, Client}; +use std::convert::TryFrom; +use std::sync::Arc; +use tenebrous_rpc::protos::dicebot::{ + dicebot_server::Dicebot, rooms_list_reply::Room, GetAllVariablesReply, GetAllVariablesRequest, + RoomsListReply, SetVariableReply, SetVariableRequest, UserIdRequest, +}; +use tenebrous_rpc::protos::dicebot::{GetVariableReply, GetVariableRequest}; +use tonic::{Code, Request, Response, Status}; + +impl From for Status { + fn from(error: BotError) -> Status { + Status::new(Code::Internal, error.to_string()) + } +} + +impl From for Status { + fn from(error: DataError) -> Status { + Status::new(Code::Internal, error.to_string()) + } +} + +#[derive(Clone)] +pub(super) struct DicebotRpcService { + pub(super) config: Arc, + pub(super) db: Database, + pub(super) client: Client, +} + +#[tonic::async_trait] +impl Dicebot for DicebotRpcService { + async fn set_variable( + &self, + request: Request, + ) -> Result, Status> { + let SetVariableRequest { + user_id, + room_id, + variable_name, + value, + } = request.into_inner(); + + self.db + .set_user_variable(&user_id, &room_id, &variable_name, value) + .await?; + + Ok(Response::new(SetVariableReply { success: true })) + } + + async fn get_variable( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + let value = self + .db + .get_user_variable(&request.user_id, &request.room_id, &request.variable_name) + .await?; + + Ok(Response::new(GetVariableReply { value })) + } + + async fn get_all_variables( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + let variables = self + .db + .get_user_variables(&request.user_id, &request.room_id) + .await?; + + Ok(Response::new(GetAllVariablesReply { variables })) + } + + async fn rooms_for_user( + &self, + request: Request, + ) -> Result, Status> { + let UserIdRequest { user_id } = request.into_inner(); + let user_id = UserId::try_from(user_id).map_err(BotError::from)?; + + let rooms_for_user = matrix::get_rooms_for_user(&self.client, &user_id) + .err_into::() + .await?; + + let mut rooms: Vec = stream::iter(rooms_for_user) + .filter_map(|room: Joined| async move { + let room: Result = room.display_name().await.map(|room_name| Room { + room_id: room.room_id().to_string(), + display_name: room_name, + }); + + Some(room) + }) + .err_into::() + .try_collect() + .await?; + + let sort = |r1: &Room, r2: &Room| { + r1.display_name + .to_lowercase() + .cmp(&r2.display_name.to_lowercase()) + }; + + rooms.sort_by(sort); + + Ok(Response::new(RoomsListReply { rooms })) + } +} diff --git a/src/state.rs b/dicebot/src/state.rs similarity index 100% rename from src/state.rs rename to dicebot/src/state.rs diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml new file mode 100644 index 0000000..f08a205 --- /dev/null +++ b/rpc/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "tenebrous-rpc" +version = "0.1.0" +authors = ["projectmoon "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[build-dependencies] +tonic-build = "0.4" + +[dependencies] +tonic = "0.4" +prost = "0.7" \ No newline at end of file diff --git a/rpc/build.rs b/rpc/build.rs new file mode 100644 index 0000000..bc2d5de --- /dev/null +++ b/rpc/build.rs @@ -0,0 +1,4 @@ +fn main() -> Result<(), Box> { + tonic_build::compile_protos("protos/dicebot.proto")?; + Ok(()) +} diff --git a/rpc/protos/dicebot.proto b/rpc/protos/dicebot.proto new file mode 100644 index 0000000..b27f0f0 --- /dev/null +++ b/rpc/protos/dicebot.proto @@ -0,0 +1,52 @@ +syntax = "proto3"; +package dicebot; + +service Dicebot { + rpc GetVariable(GetVariableRequest) returns (GetVariableReply); + rpc GetAllVariables(GetAllVariablesRequest) returns (GetAllVariablesReply); + rpc SetVariable(SetVariableRequest) returns (SetVariableReply); + rpc RoomsForUser(UserIdRequest) returns (RoomsListReply); +} + +message GetVariableRequest { + string user_id = 1; + string room_id = 2; + string variable_name = 3; +} + +message GetVariableReply { + int32 value = 1; +} + +message GetAllVariablesRequest { + string user_id = 1; + string room_id = 2; +} + +message GetAllVariablesReply { + map variables = 1; +} + +message SetVariableRequest { + string user_id = 1; + string room_id = 2; + string variable_name = 3; + int32 value = 4; +} + +message SetVariableReply { + bool success = 1; +} + +message UserIdRequest { + string user_id = 1; +} + +message RoomsListReply { + message Room { + string room_id = 1; + string display_name = 2; + } + + repeated Room rooms = 1; +} \ No newline at end of file diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs new file mode 100644 index 0000000..1a1bff7 --- /dev/null +++ b/rpc/src/lib.rs @@ -0,0 +1,5 @@ +pub mod protos { + pub mod dicebot { + tonic::include_proto!("dicebot"); + } +} diff --git a/rpc/src/main.rs b/rpc/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/rpc/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +}