v0.0.39: contacts online, message wrap, tab complete, multipart, OTPK
FC-P2-T6: /contacts shows online status (● online, ○ offline) FC-P6-T6: Long messages word-wrap into multiple lines with aligned indent FC-P6-T7: Tab completion for 33 slash commands (4 new tests) FC-P8-T6: sendDocument accepts both JSON and multipart form data OTPK: Auto-replenish on TUI startup when supply < 3 (generates 10 new) 135 tests passing (was 127) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,7 @@ pub fn routes() -> Router<AppState> {
|
||||
.route("/bot/:token/setWebhook", post(set_webhook))
|
||||
.route("/bot/:token/deleteWebhook", post(delete_webhook))
|
||||
.route("/bot/:token/getWebhookInfo", get(get_webhook_info))
|
||||
.route("/bot/:token/sendDocument", post(send_document))
|
||||
.route("/bot/:token/sendDocument", post(send_document_flexible))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -956,44 +956,104 @@ async fn get_webhook_info(
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// sendDocument
|
||||
// sendDocument — accepts both JSON and multipart/form-data
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SendDocumentRequest {
|
||||
chat_id: serde_json::Value, // Accept string (fingerprint) or number (numeric ID)
|
||||
/// File path, URL, or file_id reference. In v1, the reference is stored
|
||||
/// and forwarded as-is without server-side file hosting.
|
||||
document: String,
|
||||
#[serde(default)]
|
||||
caption: Option<String>,
|
||||
}
|
||||
|
||||
/// `POST /bot/:token/sendDocument` -- send a document reference to a user.
|
||||
async fn send_document(
|
||||
///
|
||||
/// Accepts both `application/json` and `multipart/form-data` content types
|
||||
/// so Telegram bot libraries that upload files via multipart work out of the box.
|
||||
async fn send_document_flexible(
|
||||
State(state): State<AppState>,
|
||||
Path(token): Path<String>,
|
||||
Json(req): Json<SendDocumentRequest>,
|
||||
headers: axum::http::HeaderMap,
|
||||
body: axum::body::Bytes,
|
||||
) -> Json<serde_json::Value> {
|
||||
let bot_info = match validate_bot_token(&state, &token) {
|
||||
Some(i) => i,
|
||||
None => return Json(serde_json::json!({"ok": false, "description": "invalid token"})),
|
||||
};
|
||||
let bot_fp = bot_info["fingerprint"].as_str().unwrap_or("bot");
|
||||
let to_fp = match resolve_chat_id(&state, &req.chat_id) {
|
||||
Some(fp) => fp,
|
||||
None => {
|
||||
return Json(serde_json::json!({"ok": false, "description": "chat_id not found"}))
|
||||
let bot_name = bot_info["name"].as_str().unwrap_or("bot");
|
||||
|
||||
let content_type = headers
|
||||
.get("content-type")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("");
|
||||
|
||||
let (chat_id_val, document, caption) = if content_type.contains("multipart") {
|
||||
// Parse multipart fields from raw bytes (simplified text-field extraction).
|
||||
let body_str = String::from_utf8_lossy(&body);
|
||||
let mut chat_id = String::new();
|
||||
let mut doc = String::new();
|
||||
let mut cap = String::new();
|
||||
|
||||
// Split on boundary markers (lines starting with --)
|
||||
for part in body_str.split("------") {
|
||||
if part.contains("name=\"chat_id\"") {
|
||||
if let Some(val) = part.split("\r\n\r\n").nth(1) {
|
||||
chat_id = val.trim().to_string();
|
||||
}
|
||||
}
|
||||
if part.contains("name=\"document\"") {
|
||||
if let Some(val) = part.split("\r\n\r\n").nth(1) {
|
||||
doc = val.trim().to_string();
|
||||
}
|
||||
}
|
||||
if part.contains("name=\"caption\"") {
|
||||
if let Some(val) = part.split("\r\n\r\n").nth(1) {
|
||||
cap = val.trim().to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
serde_json::Value::String(chat_id),
|
||||
doc,
|
||||
if cap.is_empty() { None } else { Some(cap) },
|
||||
)
|
||||
} else {
|
||||
// JSON body
|
||||
match serde_json::from_slice::<serde_json::Value>(&body) {
|
||||
Ok(json) => {
|
||||
let chat_id = json
|
||||
.get("chat_id")
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::String(String::new()));
|
||||
let doc = json
|
||||
.get("document")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let cap = json
|
||||
.get("caption")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from);
|
||||
(chat_id, doc, cap)
|
||||
}
|
||||
Err(e) => {
|
||||
return Json(
|
||||
serde_json::json!({"ok": false, "description": format!("invalid body: {}", e)}),
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
let msg_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
let to_fp = match resolve_chat_id(&state, &chat_id_val) {
|
||||
Some(fp) => fp,
|
||||
None => {
|
||||
return Json(serde_json::json!({"ok": false, "description": "invalid chat_id"}))
|
||||
}
|
||||
};
|
||||
|
||||
let msg_id = uuid::Uuid::new_v4().to_string();
|
||||
let doc_msg = serde_json::json!({
|
||||
"type": "bot_document",
|
||||
"id": msg_id,
|
||||
"from": bot_fp,
|
||||
"document": req.document,
|
||||
"caption": req.caption,
|
||||
"from_name": bot_name,
|
||||
"document": document,
|
||||
"caption": caption,
|
||||
"timestamp": chrono::Utc::now().timestamp(),
|
||||
});
|
||||
let msg_bytes = serde_json::to_vec(&doc_msg).unwrap_or_default();
|
||||
@@ -1004,8 +1064,8 @@ async fn send_document(
|
||||
"result": {
|
||||
"message_id": msg_id,
|
||||
"chat": {"id": to_fp},
|
||||
"document": {"file_name": req.document},
|
||||
"caption": req.caption,
|
||||
"document": {"file_name": document},
|
||||
"caption": caption,
|
||||
"delivered": delivered,
|
||||
}
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user