use super::markdown::escape_html;
use std::sync::Arc;
use teloxide::types::InlineKeyboardButton;
pub(crate) fn make_approval_callback(
state: Arc<super::TelegramState>,
) -> crate::brain::agent::ApprovalCallback {
use crate::brain::agent::ToolApprovalInfo;
use crate::utils::{check_approval_policy, persist_auto_session_policy};
use teloxide::payloads::SendMessageSetters;
use teloxide::types::{ChatId, InlineKeyboardButton, InlineKeyboardMarkup, ParseMode};
use tokio::sync::oneshot;
Arc::new(move |info: ToolApprovalInfo| {
let state = state.clone();
Box::pin(async move {
if let Some(result) = check_approval_policy() {
return Ok(result);
}
let chat_id = match state.session_chat(info.session_id).await {
Some(id) => id,
None => match state.owner_chat_id().await {
Some(id) => id,
None => {
tracing::warn!(
"Telegram approval: no chat_id for session {}",
info.session_id
);
return Ok((false, false));
}
},
};
let bot = match state.bot().await {
Some(b) => b,
None => {
tracing::warn!("Telegram approval: bot not connected");
return Ok((false, false));
}
};
let approval_id = uuid::Uuid::new_v4().to_string();
let keyboard = InlineKeyboardMarkup::new(vec![
vec![
InlineKeyboardButton::callback("✅ Yes", format!("approve:{}", approval_id)),
InlineKeyboardButton::callback(
"🔁 Always (session)",
format!("always:{}", approval_id),
),
],
vec![
InlineKeyboardButton::callback(
"🔥 YOLO (permanent)",
format!("yolo:{}", approval_id),
),
InlineKeyboardButton::callback("❌ No", format!("deny:{}", approval_id)),
],
]);
let safe_input = crate::utils::redact_tool_input(&info.tool_input);
let mut input_pretty = serde_json::to_string_pretty(&safe_input)
.unwrap_or_else(|_| safe_input.to_string());
if input_pretty.len() > 3500 {
input_pretty.truncate(3500);
input_pretty.push_str("\n... [truncated]");
}
let text = format!(
"🔐 <b>Tool Approval Required</b>\n\nTool: <code>{}</code>\nInput:\n<pre>{}</pre>",
info.tool_name,
escape_html(&input_pretty),
);
let (tx, rx) = oneshot::channel();
state
.register_pending_approval(approval_id.clone(), tx)
.await;
tracing::info!(
"Telegram approval: registered pending id={}, sending to chat={}",
approval_id,
chat_id
);
let topic_id = state
.session_topic(info.session_id)
.await
.map(|tid| teloxide::types::ThreadId(teloxide::types::MessageId(tid)));
match super::send::message_in_thread(&bot, ChatId(chat_id), topic_id, &text)
.parse_mode(ParseMode::Html)
.reply_markup(keyboard)
.await
{
Ok(_) => {
tracing::info!(
"Telegram approval: message sent, waiting for response (id={})",
approval_id
);
}
Err(e) => {
tracing::error!("Telegram approval: failed to send message: {}", e);
return Ok((false, false));
}
}
match tokio::time::timeout(std::time::Duration::from_secs(300), rx).await {
Ok(Ok((approved, always))) => {
tracing::info!(
"Telegram approval: user responded id={}, approved={}, always={}",
approval_id,
approved,
always
);
if always {
persist_auto_session_policy();
}
Ok((approved, always))
}
Ok(Err(_)) => {
tracing::warn!(
"Telegram approval: oneshot channel closed (id={})",
approval_id
);
Ok((false, false))
}
Err(_) => {
tracing::warn!(
"Telegram approval: 5-minute timeout — auto-denying (id={})",
approval_id
);
Ok((false, false))
}
}
})
})
}
pub(crate) fn build_cd_keyboard(
resp: &crate::channels::commands::DirBrowserResponse,
) -> Vec<Vec<InlineKeyboardButton>> {
let mut rows: Vec<Vec<InlineKeyboardButton>> = Vec::new();
for entry in &resp.entries {
let icon = if entry.is_dir { "📁" } else { "📄" };
let display = format!("{} {}", icon, entry.name);
rows.push(vec![InlineKeyboardButton::callback(
display,
format!("cd:sel:{}", entry.index),
)]);
}
let is_root = resp.current_path == "/" || resp.current_path.len() <= 1;
if !is_root {
rows.push(vec![InlineKeyboardButton::callback(
"⬆️ Parent",
"cd:up".to_string(),
)]);
}
if resp.total_pages > 1 {
let mut pag_row = Vec::new();
if resp.page > 0 {
pag_row.push(InlineKeyboardButton::callback(
"◀️ Prev",
format!("cd:pg:{}", resp.page - 1),
));
}
pag_row.push(InlineKeyboardButton::callback(
format!("📄 {}/{}", resp.page + 1, resp.total_pages),
"cd:noop".to_string(),
));
if resp.page + 1 < resp.total_pages {
pag_row.push(InlineKeyboardButton::callback(
"Next ▶️",
format!("cd:pg:{}", resp.page + 1),
));
}
rows.push(pag_row);
}
rows.push(vec![InlineKeyboardButton::callback(
"✅ Select this directory",
"cd:here".to_string(),
)]);
rows
}
pub(crate) fn build_profiles_keyboard(
resp: &crate::channels::commands::ProfilesResponse,
) -> Vec<Vec<InlineKeyboardButton>> {
let mut rows: Vec<Vec<InlineKeyboardButton>> = Vec::new();
for entry in &resp.entries {
let icon = if entry.is_active { "▸" } else { "•" };
let active_tag = if entry.is_active { " ✓" } else { "" };
let display = format!("{} {}{}", icon, entry.name, active_tag);
rows.push(vec![InlineKeyboardButton::callback(
display,
format!("prof:sel:{}", entry.name),
)]);
}
rows.push(vec![InlineKeyboardButton::callback(
"➕ New Profile",
"prof:create".to_string(),
)]);
rows
}