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 {{ let text = text.trim(); // !help — list available commands if text == "!help" {{ return Some( "Available commands:\n\ !help — show this message\n\ !echo — echo back the text\n\ !whoami — show your username\n\ !ping — pong!" .to_string(), ); }} // !echo — 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 ` | 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 {{ 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())) }