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.
This commit is contained in:
2026-06-12 17:09:36 +05:30
parent a694757b6f
commit 9954f28aa6
3 changed files with 270 additions and 1 deletions

View File

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

View File

@@ -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<TelegramPlatformCfg>,
pub discord: Option<DiscordPlatformCfg>,
}
/// Run the platform configuration wizard.
pub fn run_platform_wizard() -> anyhow::Result<Option<PlatformConfig>> {
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::<u64>().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<PathBuf>,
) -> anyhow::Result<PathBuf> {
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<SerializedTelegram>,
#[serde(skip_serializing_if = "Option::is_none")]
pub discord: Option<SerializedDiscord>,
}
#[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<u64>,
}