Convert project to workspace with Tonic for gRPC.
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/pr Build is failing Details

This commit adds an RPC service to the dicebot, allowing external
applications to control it. The project was converted to a cargo
workspace to house the protobuf definitions in a common crate
(tenebrous-rpc), so that clients and servers can make use of these
protobuf definitions.
This commit is contained in:
projectmoon 2021-05-31 22:22:03 +00:00
parent b4321721c4
commit 2f60bbc643
69 changed files with 484 additions and 67 deletions

202
Cargo.lock generated
View File

@ -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"

View File

@ -1,47 +1,6 @@
[package]
name = "tenebrous-dicebot"
version = "0.10.0"
authors = ["Taylor C. Richberger <taywee@gmx.com>", "projectmoon <projectmoon@agnos.is>"]
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",
]

53
dicebot/Cargo.toml Normal file
View File

@ -0,0 +1,53 @@
[package]
name = "tenebrous-dicebot"
version = "0.10.0"
authors = ["Taylor C. Richberger <taywee@gmx.com>", "projectmoon <projectmoon@agnos.is>"]
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" ]

View File

@ -1,5 +1,6 @@
//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 std::env;
use std::sync::{Arc, RwLock};
@ -7,15 +8,27 @@ 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<Config>, Database), 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?;
Ok((cfg, db))
}
#[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 +36,9 @@ async fn main() {
match run().await {
Ok(_) => (),
Err(e) => error!("Error: {}", e),
};
}
Ok(())
}
async fn run() -> Result<(), BotError> {
@ -32,12 +47,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) = init(&config_path).await?;
let grpc = rpc::serve_grpc(&cfg, &db);
let bot = run_bot(&cfg, &db);
match try_join!(bot, grpc) {
Ok(_) => (),
Err(e) => error!("Error: {}", e),
};
Ok(())
}
async fn run_bot(cfg: &Arc<Config>, db: &Database) -> Result<(), BotError> {
let state = Arc::new(RwLock::new(DiceBotState::new(&cfg)));
match DiceBot::new(&cfg, &state, &db) {
match DiceBot::new(cfg, &state, db) {
Ok(bot) => bot.run().await?,
Err(e) => println!("Error connecting: {:?}", e),
};

View File

@ -0,0 +1,18 @@
use tenebrous_rpc::protos::dicebot::dicebot_client::DicebotClient;
use tenebrous_rpc::protos::dicebot::UserIdRequest;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = DicebotClient::connect("http://0.0.0.0:9090").await?;
let request = tonic::Request::new(UserIdRequest {
user_id: "Tonic".into(),
});
let response = client.rooms_for_user(request).await?.into_inner();
println!("RESPONSE={:?}", response);
println!("User friendly response is: {:?}", response.room_ids);
Ok(())
}

View File

@ -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}")]
@ -57,6 +53,10 @@ fn db_path_from_env() -> String {
struct BotConfig {
/// How far back from current time should we process a message?
oldest_message_age: Option<u64>,
/// What address and port to run the RPC service on. If not
/// specified, RPC will not be enabled.
rpc_addr: Option<String>,
}
/// The "database" section of the config file.
@ -84,6 +84,12 @@ impl BotConfig {
self.oldest_message_age
.unwrap_or(DEFAULT_OLDEST_MESSAGE_AGE)
}
#[inline]
#[must_use]
fn rpc_addr(&self) -> Option<String> {
self.rpc_addr.clone()
}
}
/// Represents the toml config file for the dicebot. The sections of
@ -128,15 +134,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 +147,12 @@ impl Config {
.map(|bc| bc.oldest_message_age())
.unwrap_or(DEFAULT_OLDEST_MESSAGE_AGE)
}
#[inline]
#[must_use]
pub fn rpc_addr(&self) -> Option<String> {
self.bot.as_ref().and_then(|bc| bc.rpc_addr())
}
}
#[cfg(test)]

View File

@ -1,3 +1,5 @@
use std::net::AddrParseError;
use crate::commands::CommandError;
use crate::config::ConfigError;
use crate::db::errors::DataError;
@ -93,6 +95,12 @@ 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),
}
#[derive(Error, Debug)]

View File

@ -12,4 +12,5 @@ pub mod logic;
pub mod matrix;
pub mod models;
mod parser;
pub mod rpc;
pub mod state;

77
dicebot/src/rpc.rs Normal file
View File

@ -0,0 +1,77 @@
use std::sync::Arc;
use crate::error::BotError;
use crate::{config::Config, db::sqlite::Database};
use log::info;
use tenebrous_rpc::protos::dicebot::{
dicebot_server::{Dicebot, DicebotServer},
GetAllVariablesReply, GetAllVariablesRequest, RoomsListReply, SetVariableReply,
SetVariableRequest, UserIdRequest,
};
use tenebrous_rpc::protos::dicebot::{GetVariableReply, GetVariableRequest};
use tonic::{transport::Server, Request, Response, Status};
pub struct DicebotRpcService {
config: Arc<Config>,
db: Database,
}
#[tonic::async_trait]
impl Dicebot for DicebotRpcService {
async fn set_variable(
&self,
request: Request<SetVariableRequest>,
) -> Result<Response<SetVariableReply>, Status> {
Ok(Response::new(SetVariableReply { success: true }))
}
async fn get_variable(
&self,
request: Request<GetVariableRequest>,
) -> Result<Response<GetVariableReply>, Status> {
Ok(Response::new(GetVariableReply { value: 1 }))
}
async fn get_all_variables(
&self,
request: Request<GetAllVariablesRequest>,
) -> Result<Response<GetAllVariablesReply>, Status> {
Ok(Response::new(GetAllVariablesReply {
variables: std::collections::HashMap::new(),
}))
}
async fn rooms_for_user(
&self,
request: Request<UserIdRequest>,
) -> Result<Response<RoomsListReply>, Status> {
Ok(Response::new(RoomsListReply {
room_ids: Vec::new(),
}))
}
}
pub async fn serve_grpc(config: &Arc<Config>, db: &Database) -> Result<(), BotError> {
match config.rpc_addr() {
Some(addr) => {
let addr = addr.parse()?;
let rpc_service = DicebotRpcService {
db: db.clone(),
config: config.clone(),
};
info!("Serving Dicebot gRPC service on {}", addr);
Server::builder()
.add_service(DicebotServer::new(rpc_service))
.serve(addr)
.await
.map_err(|e| e.into())
}
_ => noop().await,
}
}
pub async fn noop() -> Result<(), BotError> {
info!("RPC address not specified. Not enabling gRPC.");
Ok(())
}

14
rpc/Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "tenebrous-rpc"
version = "0.1.0"
authors = ["projectmoon <projectmoon@agnos.is>"]
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"

4
rpc/build.rs Normal file
View File

@ -0,0 +1,4 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("protos/dicebot.proto")?;
Ok(())
}

47
rpc/protos/dicebot.proto Normal file
View File

@ -0,0 +1,47 @@
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<string, int32> 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 {
repeated string room_ids = 1;
}

5
rpc/src/lib.rs Normal file
View File

@ -0,0 +1,5 @@
pub mod protos {
pub mod dicebot {
tonic::include_proto!("dicebot");
}
}

3
rpc/src/main.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}