Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 26 additions & 14 deletions src/i18n.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,31 +32,43 @@ pub struct Messages {
pub prompt_text: &'static str,
pub approved_callback: &'static str,
pub rejected_callback: &'static str,
pub reject_feedback_prompt: &'static str,
pub reject_feedback_callback: &'static str,
pub timeout_notice: &'static str,
}

impl Locale {
pub fn messages(self) -> Messages {
match self {
Self::En => Messages {
approve_button: "\u{2705} Approve",
reject_button: "\u{274C} Reject",
approve_button: " Approve",
reject_button: " Reject",
prompt_text: "Please approve or reject this request.",
approved_callback: "Approved \u{2714}",
rejected_callback: "Rejected \u{2714}",
approved_callback: "Approved ✔",
rejected_callback: "Rejected ✔",
reject_feedback_prompt: "❌ <b>Rejected.</b> Reply to this message within 60 seconds to add a reason (or ignore to skip).",
reject_feedback_callback: "Rejected with feedback ✔",
timeout_notice: "⏰ Request timed out — no response received.",
},
Self::ZhCN => Messages {
approve_button: "\u{2705} \u{6279}\u{51C6}",
reject_button: "\u{274C} \u{62D2}\u{7EDD}",
prompt_text: "\u{8BF7}\u{6279}\u{51C6}\u{6216}\u{62D2}\u{7EDD}\u{6B64}\u{8BF7}\u{6C42}\u{3002}",
approved_callback: "\u{5DF2}\u{6279}\u{51C6} \u{2714}",
rejected_callback: "\u{5DF2}\u{62D2}\u{7EDD} \u{2714}",
approve_button: "✅ 批准",
reject_button: "❌ 拒绝",
prompt_text: "请批准或拒绝此请求。",
approved_callback: "已批准 ✔",
rejected_callback: "已拒绝 ✔",
reject_feedback_prompt: "❌ <b>已拒绝。</b>请在 60 秒内回复此消息以添加原因(忽略则跳过)。",
reject_feedback_callback: "已拒绝并附上原因 ✔",
timeout_notice: "⏰ 请求已超时 — 未收到响应。",
},
Self::ZhTW => Messages {
approve_button: "\u{2705} \u{6279}\u{51C6}",
reject_button: "\u{274C} \u{62D2}\u{7D55}",
prompt_text: "\u{8ACB}\u{6279}\u{51C6}\u{6216}\u{62D2}\u{7D55}\u{6B64}\u{8ACB}\u{6C42}\u{3002}",
approved_callback: "\u{5DF2}\u{6279}\u{51C6} \u{2714}",
rejected_callback: "\u{5DF2}\u{62D2}\u{7D55} \u{2714}",
approve_button: "✅ 批准",
reject_button: "❌ 拒絕",
prompt_text: "請批准或拒絕此請求。",
approved_callback: "已批准 ✔",
rejected_callback: "已拒絕 ✔",
reject_feedback_prompt: "❌ <b>已拒絕。</b>請在 60 秒內回覆此訊息以附上原因(忽略則跳過)。",
reject_feedback_callback: "已拒絕並附上原因 ✔",
timeout_notice: "⏰ 請求已超時 — 未收到回應。",
},
}
}
Expand Down
103 changes: 102 additions & 1 deletion src/providers/telegram.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,82 @@ impl TelegramProvider {
Ok(())
}

async fn send_notice(&self, text: &str) -> Result<()> {
#[derive(Serialize)]
struct Req {
chat_id: i64,
text: String,
parse_mode: String,
}
self.client
.post(format!("{}/sendMessage", self.base_url))
.json(&Req {
chat_id: self.config.chat_id,
text: text.to_string(),
parse_mode: "HTML".to_string(),
})
.send()
.await?;
Ok(())
}

/// After a Reject button click, wait up to 60 seconds for a text reply
/// to the original message. Returns Some(feedback) or None if no reply arrives.
async fn wait_for_reject_feedback(
&self,
sent_message_id: i64,
mut offset: Option<i64>,
) -> Result<Option<String>> {
let deadline = tokio::time::Instant::now() + Duration::from_secs(60);

loop {
if tokio::time::Instant::now() >= deadline {
return Ok(None);
}

let remaining = deadline - tokio::time::Instant::now();
let poll_timeout = remaining.min(Duration::from_secs(10));

let mut url = format!(
"{}/getUpdates?timeout={}&allowed_updates=[\"message\"]",
self.base_url,
poll_timeout.as_secs()
);
if let Some(off) = offset {
url.push_str(&format!("&offset={off}"));
}

let resp: TelegramResponse<Vec<Update>> =
self.client.get(&url).send().await?.json().await?;

let updates = match resp.result {
Some(u) => u,
None => continue,
};

for update in updates {
offset = Some(update.update_id + 1);

if let Some(msg) = update.message {
if msg.chat.id != self.config.chat_id {
continue;
}
if let Some(ref reply_to) = msg.reply_to_message {
if reply_to.message_id == sent_message_id {
let user_id = msg.from.as_ref().map_or(0, |u| u.id);
if !self.is_trusted(user_id) {
continue;
}
return Ok(msg.text.clone());
}
}
}
}

tokio::time::sleep(POLL_INTERVAL).await;
}
}

async fn poll_for_response(
&self,
sent_message_id: i64,
Expand All @@ -229,6 +305,11 @@ impl TelegramProvider {
loop {
if tokio::time::Instant::now() >= deadline {
info!("Timeout reached, no response received");
// Remove buttons and send timeout notice
self.edit_message_reply_markup(self.config.chat_id, sent_message_id)
.await
.ok();
self.send_notice(self.messages.timeout_notice).await.ok();
return Ok(FeedbackResponse::timeout(title));
}

Expand Down Expand Up @@ -300,11 +381,31 @@ impl TelegramProvider {
"Response received"
);

// For reject: prompt for optional feedback and wait 60s
let feedback = if decision == Decision::Rejected {
self.send_notice(self.messages.reject_feedback_prompt)
.await
.ok();
let fb = self
.wait_for_reject_feedback(sent_message_id, offset)
.await
.unwrap_or(None);
if fb.is_some() {
info!(feedback = ?fb, "Reject feedback received");
self.send_notice(self.messages.reject_feedback_callback)
.await
.ok();
}
fb
} else {
None
};

return Ok(FeedbackResponse {
decision,
user: cb.from.display_name(),
user_id: cb.from.id,
feedback: None,
feedback,
timestamp: Utc::now(),
request_title: title.to_string(),
});
Expand Down