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
|
//! - `setup` — Interactive setup wizard for configuring providers
|
||||||
//! - `terminal` — Terminal helpers (progress spinners, confirm prompts, selection menus)
|
//! - `terminal` — Terminal helpers (progress spinners, confirm prompts, selection menus)
|
||||||
//! - `tui` — Ratatui-based TUI main loop
|
//! - `tui` — Ratatui-based TUI main loop
|
||||||
|
//! - `platform` — Interactive platform configuration wizard
|
||||||
|
|
||||||
pub mod banner;
|
pub mod banner;
|
||||||
pub mod setup;
|
pub mod setup;
|
||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
pub mod tui;
|
pub mod tui;
|
||||||
|
pub mod platform;
|
||||||
|
|
||||||
pub use banner::{print_banner, BANNER_PRIMARY, BANNER_FALLBACK};
|
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 setup::{run_setup_wizard, ProviderConfig, ProviderKind, WizardResult};
|
||||||
pub use terminal::{
|
pub use terminal::{
|
||||||
confirm_prompt, progress_spinner, selection_menu, show_status, text_input,
|
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)]
|
#[command(subcommand)]
|
||||||
action: ServiceAction,
|
action: ServiceAction,
|
||||||
},
|
},
|
||||||
|
/// Configure messaging platforms (Telegram, Discord)
|
||||||
|
#[command(name = "platform")]
|
||||||
|
Platform {
|
||||||
|
#[command(subcommand)]
|
||||||
|
action: PlatformAction,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
@@ -132,6 +138,13 @@ enum ServiceAction {
|
|||||||
Status,
|
Status,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
enum PlatformAction {
|
||||||
|
/// Interactive wizard to configure Telegram and Discord bots
|
||||||
|
#[command(name = "configure")]
|
||||||
|
Configure,
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
@@ -180,6 +193,9 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
Some(Commands::Service { action }) => {
|
Some(Commands::Service { action }) => {
|
||||||
cmd_service(action).await?;
|
cmd_service(action).await?;
|
||||||
}
|
}
|
||||||
|
Some(Commands::Platform { action }) => {
|
||||||
|
cmd_platform(action).await?;
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
// Default: run interactive CLI mode
|
// Default: run interactive CLI mode
|
||||||
cmd_run(&cli.profile, "cli", None).await?;
|
cmd_run(&cli.profile, "cli", None).await?;
|
||||||
@@ -714,4 +730,26 @@ async fn cmd_service(action: ServiceAction) -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
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