From 9954f28aa67d0f7972906ac626e6282a72557059 Mon Sep 17 00:00:00 2001 From: Tara Menon Date: Fri, 12 Jun 2026 17:09:36 +0530 Subject: [PATCH] feat(platform): interactive wizard to configure Telegram + Discord bots New command: 'gyandoot platform configure' Walks users through: 1. Telegram bot token input (from @BotFather) 2. Discord bot token + guild ID input 3. Summary showing which platforms are configured 4. Saves [platform] section to config.toml Existing 'gyandoot setup' and flat config parser also handle the new [platform] section format. --- crates/gyandoot-tui/src/lib.rs | 3 + crates/gyandoot-tui/src/platform.rs | 228 ++++++++++++++++++++++++++++ src/main.rs | 40 ++++- 3 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 crates/gyandoot-tui/src/platform.rs diff --git a/crates/gyandoot-tui/src/lib.rs b/crates/gyandoot-tui/src/lib.rs index 102399f..e8420ea 100644 --- a/crates/gyandoot-tui/src/lib.rs +++ b/crates/gyandoot-tui/src/lib.rs @@ -5,13 +5,16 @@ //! - `setup` — Interactive setup wizard for configuring providers //! - `terminal` — Terminal helpers (progress spinners, confirm prompts, selection menus) //! - `tui` — Ratatui-based TUI main loop +//! - `platform` — Interactive platform configuration wizard pub mod banner; pub mod setup; pub mod terminal; pub mod tui; +pub mod platform; pub use banner::{print_banner, BANNER_PRIMARY, BANNER_FALLBACK}; +pub use platform::run_platform_wizard; pub use setup::{run_setup_wizard, ProviderConfig, ProviderKind, WizardResult}; pub use terminal::{ confirm_prompt, progress_spinner, selection_menu, show_status, text_input, diff --git a/crates/gyandoot-tui/src/platform.rs b/crates/gyandoot-tui/src/platform.rs new file mode 100644 index 0000000..4968dc5 --- /dev/null +++ b/crates/gyandoot-tui/src/platform.rs @@ -0,0 +1,228 @@ +//! Platform configuration wizard — configure Telegram and Discord bots. +//! +//! Run with: `gyandoot platform configure` + +use std::path::PathBuf; +use serde::Serialize; + +use crate::terminal; +use gyandoot_core::{TelegramPlatformCfg, DiscordPlatformCfg}; + +/// Default config directory. +fn config_dir() -> PathBuf { + dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(".config") + .join("gyandoot") +} + +/// Collected platform configuration. +#[derive(Debug, Default)] +pub struct PlatformConfig { + pub telegram: Option, + pub discord: Option, +} + +/// Run the platform configuration wizard. +pub fn run_platform_wizard() -> anyhow::Result> { + let mut config = PlatformConfig::default(); + + println!(); + terminal::section_header("Platform Configuration"); + println!(" GyanDoot can connect to messaging platforms to receive\n and respond to messages from your users."); + println!(); + + // ── Telegram ──────────────────────────────────────────────────────── + terminal::section_header("Telegram Bot"); + println!(" A Telegram bot lets users message your GyanDoot agent\n directly via Telegram."); + println!(" (Get a bot token from @BotFather: https://t.me/BotFather)"); + println!(); + + if terminal::confirm_prompt("Configure Telegram bot?", Some(false)) { + println!(); + let token = terminal::text_input("Telegram Bot Token", None); + match token { + Some(t) if !t.trim().is_empty() => { + config.telegram = Some(TelegramPlatformCfg { + bot_token: t.trim().to_string(), + }); + terminal::show_status( + terminal::StatusKind::Success, + "Telegram bot token saved", + ); + } + Some(_) | None => { + terminal::show_status( + terminal::StatusKind::Info, + "Telegram configuration skipped", + ); + } + } + } else { + terminal::show_status( + terminal::StatusKind::Info, + "Telegram configuration skipped", + ); + } + + // ── Discord ───────────────────────────────────────────────────────── + println!(); + terminal::section_header("Discord Bot"); + println!(" A Discord bot lets users message your GyanDoot agent\n via Discord. Requires a bot token and guild ID."); + println!(" (Create a bot at: https://discord.com/developers/applications)"); + println!(); + + if terminal::confirm_prompt("Configure Discord bot?", Some(false)) { + println!(); + let token = terminal::text_input("Discord Bot Token", None); + match token { + Some(t) if !t.trim().is_empty() => { + let token = t.trim().to_string(); + + let guild_input = terminal::text_input( + "Discord Guild ID (optional — press Enter to skip)", + None, + ); + let guild_id = guild_input + .and_then(|g| { + let trimmed = g.trim().to_string(); + if trimmed.is_empty() { + None + } else { + trimmed.parse::().ok() + } + }); + + config.discord = Some(DiscordPlatformCfg { + bot_token: token, + guild_id, + }); + terminal::show_status( + terminal::StatusKind::Success, + "Discord bot configuration saved", + ); + } + Some(_) | None => { + terminal::show_status( + terminal::StatusKind::Info, + "Discord configuration skipped", + ); + } + } + } else { + terminal::show_status( + terminal::StatusKind::Info, + "Discord configuration skipped", + ); + } + + // ── Summary ───────────────────────────────────────────────────────── + println!(); + terminal::section_header("Platform Summary"); + match &config.telegram { + Some(tg) => { + let masked = mask_token(&tg.bot_token); + println!(" Telegram: ✅ configured (token: {})", masked); + } + None => println!(" Telegram: ❌ not configured"), + } + match &config.discord { + Some(dc) => { + let masked = mask_token(&dc.bot_token); + let guild = dc.guild_id.map(|g| g.to_string()).unwrap_or_else(|| "none".to_string()); + println!(" Discord: ✅ configured (token: {}, guild: {})", masked, guild); + } + None => println!(" Discord: ❌ not configured"), + } + + println!(); + if !terminal::confirm_prompt("Save platform configuration?", Some(true)) { + terminal::show_status(terminal::StatusKind::Info, "Platform configuration cancelled."); + return Ok(None); + } + + // Write to config file + let config_path = write_platform_config(&config, &None)?; + terminal::show_status( + terminal::StatusKind::Success, + &format!("Platform config saved to {}", config_path.display()), + ); + + println!(); + terminal::section_header("Next Steps"); + println!(" Run: gyandoot serve"); + println!(" This starts the gateway server that listens on all configured"); + println!(" platforms and processes messages through your GyanDoot agent."); + println!(); + + Ok(Some(config)) +} + +/// Write platform config to existing config.toml (append/replace). +fn write_platform_config( + config: &PlatformConfig, + _existing: &Option, +) -> anyhow::Result { + let dir = config_dir(); + std::fs::create_dir_all(&dir)?; + let path = dir.join("config.toml"); + + // Read existing config content if present + let existing_content = if path.exists() { + std::fs::read_to_string(&path)? + } else { + String::new() + }; + + // Build platform TOML section + let mut platform_lines = String::from("\n[platform]\n"); + + if let Some(tg) = &config.telegram { + platform_lines.push_str(&format!("[platform.telegram]\nbot_token = \"{}\"\n\n", tg.bot_token)); + } + if let Some(dc) = &config.discord { + platform_lines.push_str("[platform.discord]\n"); + platform_lines.push_str(&format!("bot_token = \"{}\"\n", dc.bot_token)); + if let Some(guild) = dc.guild_id { + platform_lines.push_str(&format!("guild_id = {}\n\n", guild)); + } + } + + // Append to config file + std::fs::write(&path, format!("{}{}", existing_content.trim(), platform_lines))?; + + Ok(path) +} + +/// Mask a token for display (show first 8 chars). +fn mask_token(token: &str) -> String { + if token.len() > 12 { + format!("{}...{}", &token[..8], &token[token.len()-4..]) + } else if !token.is_empty() { + format!("{}...", &token[..token.len().min(4)]) + } else { + "(empty)".to_string() + } +} + +// ─── Serialized platform section for clean TOML writing ───────────────── + +#[derive(Serialize)] +pub struct SerializedPlatform { + #[serde(skip_serializing_if = "Option::is_none")] + pub telegram: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub discord: Option, +} + +#[derive(Serialize)] +pub struct SerializedTelegram { + pub bot_token: String, +} + +#[derive(Serialize)] +pub struct SerializedDiscord { + pub bot_token: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub guild_id: Option, +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index e7db7ae..f43d2d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -81,6 +81,12 @@ enum Commands { #[command(subcommand)] action: ServiceAction, }, + /// Configure messaging platforms (Telegram, Discord) + #[command(name = "platform")] + Platform { + #[command(subcommand)] + action: PlatformAction, + }, } #[derive(Subcommand, Debug)] @@ -132,6 +138,13 @@ enum ServiceAction { Status, } +#[derive(Subcommand, Debug)] +enum PlatformAction { + /// Interactive wizard to configure Telegram and Discord bots + #[command(name = "configure")] + Configure, +} + #[tokio::main] async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); @@ -180,6 +193,9 @@ async fn main() -> anyhow::Result<()> { Some(Commands::Service { action }) => { cmd_service(action).await?; } + Some(Commands::Platform { action }) => { + cmd_platform(action).await?; + } None => { // Default: run interactive CLI mode cmd_run(&cli.profile, "cli", None).await?; @@ -714,4 +730,26 @@ async fn cmd_service(action: ServiceAction) -> anyhow::Result<()> { } } Ok(()) -} \ No newline at end of file +} +/// Configure messaging platforms (Telegram, Discord) +async fn cmd_platform(action: PlatformAction) -> anyhow::Result<()> { + match action { + PlatformAction::Configure => { + tracing::info!("Starting platform configuration wizard"); + match gyandoot_tui::platform::run_platform_wizard() { + Ok(Some(_config)) => { + println!("✅ Platform configuration saved!"); + println!(" Run 'gyandoot serve' to start the gateway."); + } + Ok(None) => { + println!("Platform configuration cancelled."); + } + Err(e) => { + tracing::error!("Platform wizard failed: {}", e); + eprintln!("Platform setup error: {}", e); + } + } + } + } + Ok(()) +}