feat: Phase 9 — developer experience, extensibility, and community growth
New crates: - quicproquo-bot: Bot SDK with polling API + JSON pipe mode - quicproquo-kt: Key Transparency Merkle log (RFC 9162 subset) - quicproquo-plugin-api: no_std C-compatible plugin vtable API - quicproquo-gen: scaffolding tool (qpq-gen plugin/bot/rpc/hook) Server features: - ServerHooks trait wired into all RPC handlers (enqueue, fetch, auth, channel, registration) with plugin rejection support - Dynamic plugin loader (libloading) with --plugin-dir config - Delivery proof canary tokens (Ed25519 server signatures on enqueue) - Key Transparency Merkle log with inclusion proofs on resolveUser Core library: - Safety numbers (60-digit HMAC-SHA256 key verification codes) - Verifiable transcript archive (CBOR + ChaCha20-Poly1305 + hash chain) - Delivery proof verification utility - Criterion benchmarks (hybrid KEM, MLS, identity, sealed sender, padding) Client: - /verify REPL command for out-of-band key verification - Full-screen TUI via Ratatui (feature-gated --features tui) - qpq export / qpq export-verify CLI subcommands - KT inclusion proof verification on user resolution Also: ROADMAP Phase 9 added, bot SDK docs, server hooks docs, crate-responsibilities updated, example plugins (rate_limit, logging).
This commit is contained in:
212
crates/quicproquo-gen/src/generators/bot.rs
Normal file
212
crates/quicproquo-gen/src/generators/bot.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn generate(name: &str, output: &Path) -> Result<(), String> {
|
||||
let crate_name = sanitize_name(name);
|
||||
let dir = output.join(&crate_name);
|
||||
|
||||
if dir.exists() {
|
||||
return Err(format!("directory already exists: {}", dir.display()));
|
||||
}
|
||||
|
||||
let src_dir = dir.join("src");
|
||||
fs::create_dir_all(&src_dir).map_err(|e| format!("create dir: {e}"))?;
|
||||
|
||||
// Cargo.toml
|
||||
let cargo_toml = format!(
|
||||
r#"[package]
|
||||
name = "{crate_name}"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "quicproquo bot: {name}"
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
quicproquo-bot = {{ git = "https://github.com/nickvidal/quicproquo" }}
|
||||
tokio = {{ version = "1", features = ["macros", "rt-multi-thread"] }}
|
||||
anyhow = "1"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = {{ version = "0.3", features = ["env-filter"] }}
|
||||
"#,
|
||||
crate_name = crate_name,
|
||||
name = name,
|
||||
);
|
||||
write_file(&dir.join("Cargo.toml"), &cargo_toml)?;
|
||||
|
||||
// src/main.rs
|
||||
let main_rs = format!(
|
||||
r#"//! quicproquo bot: {name}
|
||||
//!
|
||||
//! A bot that connects to a quicproquo server and responds to messages.
|
||||
//!
|
||||
//! Usage:
|
||||
//! {crate_name} --server 127.0.0.1:7000 --username my-bot --password secret
|
||||
//!
|
||||
//! Environment variables (alternative to CLI args):
|
||||
//! QPQ_SERVER, QPQ_USERNAME, QPQ_PASSWORD, QPQ_CA_CERT, QPQ_STATE_PATH
|
||||
|
||||
use quicproquo_bot::{{Bot, BotConfig}};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {{
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| "info".into()),
|
||||
)
|
||||
.init();
|
||||
|
||||
// --- Configuration ---
|
||||
let server = env_or("QPQ_SERVER", "127.0.0.1:7000");
|
||||
let username = env_or("QPQ_USERNAME", "{crate_name}");
|
||||
let password = env_or("QPQ_PASSWORD", "changeme");
|
||||
let ca_cert = env_or("QPQ_CA_CERT", "server-cert.der");
|
||||
let state_path = env_or("QPQ_STATE_PATH", "{crate_name}-state.bin");
|
||||
|
||||
let config = BotConfig::new(&server, &username, &password)
|
||||
.ca_cert(&ca_cert)
|
||||
.state_path(&state_path);
|
||||
|
||||
// --- Connect and authenticate ---
|
||||
tracing::info!("connecting to {{server}} as {{username}}...");
|
||||
let bot = Bot::connect(config).await?;
|
||||
tracing::info!("authenticated as {{}} (key: {{}})", bot.username(), &bot.identity_key_hex()[..16]);
|
||||
|
||||
// --- Main loop: poll for messages and respond ---
|
||||
tracing::info!("listening for messages (Ctrl+C to stop)...");
|
||||
loop {{
|
||||
let messages = bot.receive(5000).await?;
|
||||
for msg in messages {{
|
||||
tracing::info!("[{{}}] {{}}", msg.sender, msg.text);
|
||||
|
||||
// --- Add your command handlers here ---
|
||||
if let Some(response) = handle_message(&msg.sender, &msg.text) {{
|
||||
bot.send_dm(&msg.sender, &response).await?;
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
/// Process an incoming message and optionally return a response.
|
||||
///
|
||||
/// Add your bot's command logic here.
|
||||
fn handle_message(sender: &str, text: &str) -> Option<String> {{
|
||||
let text = text.trim();
|
||||
|
||||
// !help — list available commands
|
||||
if text == "!help" {{
|
||||
return Some(
|
||||
"Available commands:\n\
|
||||
!help — show this message\n\
|
||||
!echo <text> — echo back the text\n\
|
||||
!whoami — show your username\n\
|
||||
!ping — pong!"
|
||||
.to_string(),
|
||||
);
|
||||
}}
|
||||
|
||||
// !echo <text> — echo back
|
||||
if let Some(rest) = text.strip_prefix("!echo ") {{
|
||||
return Some(rest.to_string());
|
||||
}}
|
||||
|
||||
// !whoami — tell the sender their username
|
||||
if text == "!whoami" {{
|
||||
return Some(format!("You are {{sender}}"));
|
||||
}}
|
||||
|
||||
// !ping — respond with pong
|
||||
if text == "!ping" {{
|
||||
return Some("pong!".to_string());
|
||||
}}
|
||||
|
||||
// Unknown command or regular message — no response
|
||||
None
|
||||
}}
|
||||
|
||||
fn env_or(key: &str, default: &str) -> String {{
|
||||
std::env::var(key).unwrap_or_else(|_| default.to_string())
|
||||
}}
|
||||
"#,
|
||||
name = name,
|
||||
crate_name = crate_name,
|
||||
);
|
||||
write_file(&src_dir.join("main.rs"), &main_rs)?;
|
||||
|
||||
// README
|
||||
let readme = format!(
|
||||
r#"# {name} — quicproquo bot
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# Build
|
||||
cargo build
|
||||
|
||||
# Run (make sure a qpq server is running)
|
||||
QPQ_SERVER=127.0.0.1:7000 \
|
||||
QPQ_USERNAME={crate_name} \
|
||||
QPQ_PASSWORD=changeme \
|
||||
QPQ_CA_CERT=path/to/server-cert.der \
|
||||
cargo run
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `!help` | Show available commands |
|
||||
| `!echo <text>` | Echo back the text |
|
||||
| `!whoami` | Show your username |
|
||||
| `!ping` | Respond with "pong!" |
|
||||
|
||||
## Adding commands
|
||||
|
||||
Edit the `handle_message` function in `src/main.rs`:
|
||||
|
||||
```rust
|
||||
fn handle_message(sender: &str, text: &str) -> Option<String> {{
|
||||
if text == "!mycommand" {{
|
||||
return Some("my response".to_string());
|
||||
}}
|
||||
None
|
||||
}}
|
||||
```
|
||||
|
||||
## Pipe mode
|
||||
|
||||
For shell integration, use the Bot SDK's JSON pipe mode:
|
||||
|
||||
```bash
|
||||
echo '{{"action":"send","to":"alice","text":"hello"}}' | my-bot
|
||||
echo '{{"action":"recv","timeout_ms":5000}}' | my-bot
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Bot SDK docs](https://github.com/nickvidal/quicproquo/blob/main/docs/src/getting-started/bot-sdk.md)
|
||||
- [Server Hooks](https://github.com/nickvidal/quicproquo/blob/main/docs/src/internals/server-hooks.md)
|
||||
"#,
|
||||
name = name,
|
||||
crate_name = crate_name,
|
||||
);
|
||||
write_file(&dir.join("README.md"), &readme)?;
|
||||
|
||||
println!("Created bot project: {}", dir.display());
|
||||
println!();
|
||||
println!(" cd {crate_name}");
|
||||
println!(" # Edit src/main.rs to add your commands");
|
||||
println!(" QPQ_SERVER=127.0.0.1:7000 QPQ_PASSWORD=secret cargo run");
|
||||
println!();
|
||||
println!("The bot responds to !help, !echo, !whoami, !ping out of the box.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sanitize_name(name: &str) -> String {
|
||||
name.replace(['-', ' '], "_")
|
||||
}
|
||||
|
||||
fn write_file(path: &Path, content: &str) -> Result<(), String> {
|
||||
fs::write(path, content).map_err(|e| format!("write {}: {e}", path.display()))
|
||||
}
|
||||
Reference in New Issue
Block a user