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:
@@ -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,
|
||||
|
||||
228
crates/gyandoot-tui/src/platform.rs
Normal file
228
crates/gyandoot-tui/src/platform.rs
Normal 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>,
|
||||
}
|
||||
40
src/main.rs
40
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(())
|
||||
}
|
||||
}
|
||||
/// 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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user