use std::sync::{Arc, Mutex};
use teloxide::{
Bot,
prelude::Requester,
types::{ChatId, Message},
};
use tg_cli::{bot_from_config_token, load_setup_status, save_bot_config, save_chat_id};
pub(crate) async fn run_setup(profile: Option<&str>) {
let config = load_setup_status(profile).await;
if config.chat_id.is_some() {
match profile {
None => eprintln!("Already configured. Run `tg config reset` to reconfigure."),
Some(name) => eprintln!(
"Profile '{name}' already configured. Run `tg --profile {name} config reset` to reconfigure."
),
}
std::process::exit(1);
}
if let Some(name) = profile {
eprintln!("Setting up profile: {name}\n");
}
eprintln!("Step 1 of 3 — Bot token\n\n");
let (bot, token) = if config.has_token {
match profile {
None => eprintln!("Bot token already configured. Run `tg config reset` to reconfigure."),
Some(name) => eprintln!(
"Bot token already configured. Run `tg --profile {name} config reset` to reconfigure."
),
}
(
bot_from_config_token(profile)
.await
.expect("token reported configured but could not be loaded"),
None,
)
} else {
let token = prompt_token();
(Bot::new(token.clone()), Some(token))
};
let code = pairing_code();
eprintln!(
"\nStep 2 of 3 — Link your account\n\
\n\
Open your bot in Telegram and send it /start.\n\
Waiting..."
);
let chat_id = retrieve_chat_id(bot, &code).await;
eprintln!("\nStep 3 of 3 — Confirm pairing\n\n");
verify_code(&code).await;
if let Some(token) = token {
save_bot_config(&token, chat_id, profile).await;
} else {
save_chat_id(chat_id, profile);
}
match profile {
None => eprintln!("\nAll done! Try it out:\n\n tg \"Hello, World!\""),
Some(name) => {
eprintln!("\nAll done! Try it out:\n\n tg --profile {name} \"Hello, World!\"")
}
}
std::process::exit(0);
}
fn prompt_token() -> String {
let suggested_username = random_bot_username();
let token = prompt(&format!(
"tg needs a Telegram bot to send messages on your behalf.\n\
If you don't have one yet:\n\
1. Open Telegram and start a chat with @BotFather (https://t.me/botfather)\n\
2. Send /newbot — suggested username: {suggested_username}\n\
3. Copy the token BotFather gives you\n\
\n\
Paste your bot token: ",
));
if token.is_empty() {
eprintln!("Token cannot be empty.");
std::process::exit(1);
}
token
}
async fn retrieve_chat_id(bot: Bot, code: &str) -> i64 {
let (tx, rx) = tokio::sync::oneshot::channel::<ChatId>();
let tx = Arc::new(Mutex::new(Some(tx)));
let code_for_repl = code.to_string();
tokio::spawn(async move {
teloxide::repl(bot, move |bot: Bot, msg: Message| {
let tx = Arc::clone(&tx);
let code = code_for_repl.clone();
async move {
if msg.text() == Some("/start") {
let sender = tx.lock().unwrap().take();
if let Some(sender) = sender {
bot.send_message(
msg.chat.id,
format!("Your pairing code is: {code}\n\nEnter it in your terminal to complete setup."),
)
.await?;
let _ = sender.send(msg.chat.id);
}
}
Ok(())
}
})
.await;
});
let chat_id = rx.await.expect("setup listener exited unexpectedly");
chat_id.0
}
async fn verify_code(code: &str) {
let input = prompt(
"Your bot just sent you a 3-digit code in Telegram.\n\
Enter it here: ",
);
if input.trim() != code {
eprintln!("Wrong code. Setup aborted — no config was saved.");
std::process::exit(1);
}
}
fn prompt(label: &str) -> String {
eprint!("{}", label);
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.expect("failed to read input");
input.trim().to_string()
}
fn random_bot_username() -> String {
const CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let mut buf = [0u8; 24];
let mut f = std::fs::File::open("/dev/urandom").expect("failed to open /dev/urandom");
std::io::Read::read_exact(&mut f, &mut buf).expect("failed to read /dev/urandom");
let name: String = buf
.iter()
.map(|&b| CHARS[b as usize % CHARS.len()] as char)
.collect();
format!("{name}_bot")
}
fn pairing_code() -> String {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.subsec_nanos();
format!("{:03}", nanos % 1000)
}