Convert project to workspace with Tonic for gRPC. (#84)

Convert project to workspace with Tonic for gRPC.

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.
Co-Authored-By: projectmoon <projectmoon@noreply.git.agnos.is>
Co-Committed-By: projectmoon <projectmoon@noreply.git.agnos.is>
This commit is contained in:
projectmoon 2021-06-02 21:09:58 +00:00
parent b4321721c4
commit 764426382a
69 changed files with 688 additions and 90 deletions

View File

@ -7,6 +7,7 @@ steps:
commands: commands:
- apt-get update - apt-get update
- apt-get install -y cmake - apt-get install -y cmake
- rustup component add rustfmt
- cargo build --verbose --all - cargo build --verbose --all
- cargo test --verbose --all - cargo test --verbose --all

202
Cargo.lock generated
View File

@ -101,6 +101,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "anyhow"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b"
[[package]] [[package]]
name = "arrayref" name = "arrayref"
version = "0.3.6" version = "0.3.6"
@ -119,6 +125,27 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f093eed78becd229346bf859eec0aa4dd7ddde0757287b2b4107a1f09c80002" 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]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.50" version = "0.1.50"
@ -529,6 +556,12 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fixedbitset"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d"
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -991,6 +1024,15 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135"
[[package]]
name = "itertools"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.10.0" version = "0.10.0"
@ -1263,6 +1305,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "multimap"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
[[package]] [[package]]
name = "native-tls" name = "native-tls"
version = "0.2.7" version = "0.2.7"
@ -1462,6 +1510,16 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 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]] [[package]]
name = "phf" name = "phf"
version = "0.8.0" version = "0.8.0"
@ -1618,6 +1676,57 @@ dependencies = [
"unicode-xid", "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]] [[package]]
name = "quote" name = "quote"
version = "1.0.9" version = "1.0.9"
@ -2557,11 +2666,12 @@ dependencies = [
"futures", "futures",
"html2text", "html2text",
"indoc", "indoc",
"itertools", "itertools 0.10.0",
"log", "log",
"matrix-sdk", "matrix-sdk",
"nom 5.1.2", "nom 5.1.2",
"phf", "phf",
"prost",
"rand 0.8.3", "rand 0.8.3",
"refinery", "refinery",
"rust-argon2", "rust-argon2",
@ -2569,13 +2679,25 @@ dependencies = [
"sqlx", "sqlx",
"substring", "substring",
"tempfile", "tempfile",
"tenebrous-rpc",
"thiserror", "thiserror",
"tokio", "tokio",
"toml", "toml",
"tonic",
"tonic-build",
"tracing-subscriber", "tracing-subscriber",
"url", "url",
] ]
[[package]]
name = "tenebrous-rpc"
version = "0.1.0"
dependencies = [
"prost",
"tonic",
"tonic-build",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.24" version = "1.0.24"
@ -2743,6 +2865,73 @@ dependencies = [
"serde", "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]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.1" version = "0.3.1"
@ -2756,6 +2945,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"log",
"pin-project-lite", "pin-project-lite",
"tracing-attributes", "tracing-attributes",
"tracing-core", "tracing-core",
@ -3055,6 +3245,16 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "whoami" name = "whoami"
version = "1.1.2" version = "1.1.2"

View File

@ -1,47 +1,6 @@
[package] [workspace]
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"]
[dependencies] members = [
log = "0.4" "dicebot",
tracing-subscriber = "0.2" "rpc",
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" ]

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

35
dicebot/Dockerfile Normal file
View File

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

View File

@ -1,21 +1,36 @@
//Needed for nested Result handling from tokio. Probably can go away after 1.47.0. //Needed for nested Result handling from tokio. Probably can go away after 1.47.0.
#![type_length_limit = "7605144"] #![type_length_limit = "7605144"]
use futures::try_join;
use log::error; use log::error;
use matrix_sdk::Client;
use std::env; use std::env;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use tenebrous_dicebot::bot::DiceBot; use tenebrous_dicebot::bot::DiceBot;
use tenebrous_dicebot::config::*; use tenebrous_dicebot::config::*;
use tenebrous_dicebot::db::sqlite::Database; use tenebrous_dicebot::db::sqlite::Database;
use tenebrous_dicebot::error::BotError; use tenebrous_dicebot::error::BotError;
use tenebrous_dicebot::rpc;
use tenebrous_dicebot::state::DiceBotState; use tenebrous_dicebot::state::DiceBotState;
use tracing_subscriber::filter::EnvFilter; 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, 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] #[tokio::main]
async fn main() { async fn main() -> Result<(), BotError> {
let filter = if env::var("RUST_LOG").is_ok() { let filter = if env::var("RUST_LOG").is_ok() {
EnvFilter::from_default_env() EnvFilter::from_default_env()
} else { } 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(); tracing_subscriber::fmt().with_env_filter(filter).init();
@ -23,7 +38,9 @@ async fn main() {
match run().await { match run().await {
Ok(_) => (), Ok(_) => (),
Err(e) => error!("Error: {}", e), Err(e) => error!("Error: {}", e),
}; }
Ok(())
} }
async fn run() -> Result<(), BotError> { async fn run() -> Result<(), BotError> {
@ -32,12 +49,22 @@ async fn run() -> Result<(), BotError> {
.next() .next()
.expect("Need a config as an argument"); .expect("Need a config as an argument");
let cfg = Arc::new(read_config(config_path)?); let (cfg, db, client) = init(&config_path).await?;
let sqlite_path = format!("{}/dicebot.sqlite", cfg.database_path()); let grpc = rpc::serve_grpc(&cfg, &db, &client);
let db = Database::new(&sqlite_path).await?; 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<Config>, db: &Database, client: &Client) -> Result<(), BotError> {
let state = Arc::new(RwLock::new(DiceBotState::new(&cfg))); 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?, Ok(bot) => bot.run().await?,
Err(e) => println!("Error connecting: {:?}", e), Err(e) => println!("Error connecting: {:?}", e),
}; };

View File

@ -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<DicebotClient<Channel>, Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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(())
}

View File

@ -4,13 +4,10 @@ use crate::db::sqlite::Database;
use crate::db::DbState; use crate::db::DbState;
use crate::error::BotError; use crate::error::BotError;
use crate::state::DiceBotState; use crate::state::DiceBotState;
use dirs;
use log::info; 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::clone::Clone;
use std::path::PathBuf;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use url::Url;
mod command_execution; mod command_execution;
pub mod event_handlers; pub mod event_handlers;
@ -35,22 +32,6 @@ pub struct DiceBot {
db: Database, db: Database,
} }
fn cache_dir() -> Result<PathBuf, BotError> {
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<Client, BotError> {
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 { impl DiceBot {
/// Create a new dicebot with the given configuration and state /// Create a new dicebot with the given configuration and state
/// actor. This function returns a Result because it is possible /// actor. This function returns a Result because it is possible
@ -60,9 +41,10 @@ impl DiceBot {
config: &Arc<Config>, config: &Arc<Config>,
state: &Arc<RwLock<DiceBotState>>, state: &Arc<RwLock<DiceBotState>>,
db: &Database, db: &Database,
client: &Client,
) -> Result<Self, BotError> { ) -> Result<Self, BotError> {
Ok(DiceBot { Ok(DiceBot {
client: create_client(&config)?, client: client.clone(),
config: config.clone(), config: config.clone(),
state: state.clone(), state: state.clone(),
db: db.clone(), db: db.clone(),

View File

@ -4,10 +4,6 @@ use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use thiserror::Error; 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)] #[derive(Error, Debug)]
pub enum ConfigError { pub enum ConfigError {
#[error("i/o error: {0}")] #[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. /// The "bot" section of the config file, for bot settings.
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug, Default)]
struct BotConfig { struct BotConfig {
/// How far back from current time should we process a message? /// How far back from current time should we process a message?
oldest_message_age: Option<u64>, 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 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<String>,
} }
/// The "database" section of the config file. /// The "database" section of the config file.
@ -84,6 +89,18 @@ impl BotConfig {
self.oldest_message_age self.oldest_message_age
.unwrap_or(DEFAULT_OLDEST_MESSAGE_AGE) .unwrap_or(DEFAULT_OLDEST_MESSAGE_AGE)
} }
#[inline]
#[must_use]
fn rpc_addr(&self) -> Option<String> {
self.rpc_addr.clone()
}
#[inline]
#[must_use]
fn rpc_key(&self) -> Option<String> {
self.rpc_key.clone()
}
} }
/// Represents the toml config file for the dicebot. The sections of /// Represents the toml config file for the dicebot. The sections of
@ -128,15 +145,6 @@ impl Config {
.unwrap_or_else(|| db_path_from_env()) .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 /// Figure out the allowed oldest message age, in seconds. This will
/// be the defined oldest message age in the bot config, if the bot /// be the defined oldest message age in the bot config, if the bot
/// configuration and associated "oldest_message_age" setting are /// configuration and associated "oldest_message_age" setting are
@ -150,6 +158,18 @@ impl Config {
.map(|bc| bc.oldest_message_age()) .map(|bc| bc.oldest_message_age())
.unwrap_or(DEFAULT_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())
}
#[inline]
#[must_use]
pub fn rpc_key(&self) -> Option<String> {
self.bot.as_ref().and_then(|bc| bc.rpc_key())
}
} }
#[cfg(test)] #[cfg(test)]
@ -169,6 +189,7 @@ mod tests {
}), }),
bot: Some(BotConfig { bot: Some(BotConfig {
oldest_message_age: None, oldest_message_age: None,
..Default::default()
}), }),
}; };

View File

@ -1,7 +1,10 @@
use std::net::AddrParseError;
use crate::commands::CommandError; use crate::commands::CommandError;
use crate::config::ConfigError; use crate::config::ConfigError;
use crate::db::errors::DataError; use crate::db::errors::DataError;
use thiserror::Error; use thiserror::Error;
use tonic::metadata::errors::InvalidMetadataValue;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum BotError { pub enum BotError {
@ -93,6 +96,15 @@ pub enum BotError {
#[error("room name or id does not exist")] #[error("room name or id does not exist")]
RoomDoesNotExist, 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)] #[derive(Error, Debug)]

View File

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

View File

@ -1,6 +1,8 @@
use std::path::PathBuf;
use futures::stream::{self, StreamExt, TryStreamExt}; use futures::stream::{self, StreamExt, TryStreamExt};
use log::error; 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::{ use matrix_sdk::{
events::room::message::{InReplyTo, Relation}, events::room::message::{InReplyTo, Relation},
events::room::message::{MessageEventContent, MessageType}, events::room::message::{MessageEventContent, MessageType},
@ -9,6 +11,15 @@ use matrix_sdk::{
Error as MatrixError, Error as MatrixError,
}; };
use matrix_sdk::{identifiers::RoomId, identifiers::UserId, Client}; use matrix_sdk::{identifiers::RoomId, identifiers::UserId, Client};
use url::Url;
use crate::{config::Config, error::BotError};
fn cache_dir() -> Result<PathBuf, BotError> {
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. /// Extracts more detailed error messages out of a matrix SDK error.
fn extract_error_message(error: MatrixError) -> String { 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<Client, BotError> {
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. /// Retrieve a list of users in a given room.
pub async fn get_users_in_room( pub async fn get_users_in_room(
client: &Client, client: &Client,

50
dicebot/src/rpc/mod.rs Normal file
View File

@ -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<Config>,
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(())
}

116
dicebot/src/rpc/service.rs Normal file
View File

@ -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<BotError> for Status {
fn from(error: BotError) -> Status {
Status::new(Code::Internal, error.to_string())
}
}
impl From<DataError> for Status {
fn from(error: DataError) -> Status {
Status::new(Code::Internal, error.to_string())
}
}
#[derive(Clone)]
pub(super) struct DicebotRpcService {
pub(super) config: Arc<Config>,
pub(super) db: Database,
pub(super) client: Client,
}
#[tonic::async_trait]
impl Dicebot for DicebotRpcService {
async fn set_variable(
&self,
request: Request<SetVariableRequest>,
) -> Result<Response<SetVariableReply>, 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<GetVariableRequest>,
) -> Result<Response<GetVariableReply>, 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<GetAllVariablesRequest>,
) -> Result<Response<GetAllVariablesReply>, 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<UserIdRequest>,
) -> Result<Response<RoomsListReply>, 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::<BotError>()
.await?;
let mut rooms: Vec<Room> = stream::iter(rooms_for_user)
.filter_map(|room: Joined| async move {
let room: Result<Room, _> = room.display_name().await.map(|room_name| Room {
room_id: room.room_id().to_string(),
display_name: room_name,
});
Some(room)
})
.err_into::<BotError>()
.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 }))
}
}

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(())
}

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

@ -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<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 {
message Room {
string room_id = 1;
string display_name = 2;
}
repeated Room rooms = 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!");
}