7.5 KiB
Batch 4 — Telegram Bot + Permissions Verification
File inspected: crates/cgcx-bot/src/main.rs (2318 lines)
Supporting files: crates/cgcx-crypto/src/lib.rs, crates/cgcx-db/src/repos.rs, migrations/003_forward_system.sql
1. finalize_upload — Submission → Review Group
Location: main.rs lines ~1504–1595
| Requirement | Status | Evidence |
|---|---|---|
| Up to 10 items per batch | ✅ PASS | let chunks: Vec<_> = decrypted.chunks(10).collect(); (line 1541) |
| Review text on LAST batch only | ✅ PASS | if is_last { if let Some(last) = batch.last_mut() { ... set caption ... } } (lines 1546–1559) |
| Action button message sent separately BEFORE media | ✅ PASS | bot.send_message(ChatId(review_group_id), review_text.clone()).reply_markup(keyboard) is called at line 1529–1533, before any media batches. set_review_message_id stores the returned message id at line 1535. |
Notes
- The review group text message carries the action buttons (Approve / Ignore / Blacklist / Ban / Ban+BL).
- Media batches are sent after the action message, so moderators can act immediately even while media is still uploading.
- Caption is attached to the last media batch item, not the action message.
2. handle_forward_callback — "approve" Action
Location: main.rs lines ~1940–2040
| Requirement | Status | Evidence |
|---|---|---|
| Up to 10 items per batch | ✅ PASS | let chunks: Vec<_> = decrypted.chunks(10).collect(); (line 1978) |
| Caption on last batch with custom msg + author + direct link + forward link | ✅ PASS | caption built at lines 1966–1970 containing forward_def.forward_message, author_line, link, forward_link. Applied to last batch at lines 1989–2002. |
| Text-only fallback if no files | ✅ PASS | if files.is_empty() sends text-only message at line 1972. Additional fallback if decrypted.is_empty() at line 1982. |
Notes
author_linerespectscontent.show_author(lines 1948–1959).- The
posted_linkis constructed from the first message of the media group or the text message, and is DM’d to the submitter (line 2020).
3. Telegram API Issues
3.1 Videos sent as InputMedia::Document instead of InputMedia::Video
Status: ⚠️ ISSUE FOUND
Evidence:
main.rsline 1555–1560 (review batching):let media = if mime_type.starts_with("image/") { InputMedia::Photo(InputMediaPhoto::new(input_file)) } else { InputMedia::Document(InputMediaDocument::new(input_file)) };- Same pattern in approve batching at lines 1985–1990.
Impact: Videos (video/mp4, etc.) and audio files are sent as generic documents. In Telegram clients this means:
- No inline video player in the chat.
- No audio waveform or music player UI.
- File appears as an attachment rather than native media.
Fix needed: Add mime_type.starts_with("video/") → InputMedia::Video(...) and mime_type.starts_with("audio/") → InputMedia::Audio(...). Requires importing InputMediaVideo, InputMediaAudio from teloxide.
3.2 Audio files sent as documents
Status: ⚠️ ISSUE FOUND (same root cause as 3.1)
Audio (audio/mpeg, etc.) falls through to InputMedia::Document.
3.3 Caption length exceeding Telegram's 1024-character limit
Status: ⚠️ POTENTIAL ISSUE
Evidence:
- Caption built in approve path (lines 1966–1970):
let caption = format!( "{}\n\nSubmitted by: {}\nDirect link: <code>{}</code>\nForward link: <code>{}</code>", escape_html(&forward_def.forward_message), author_line, link, forward_link ); forward_def.forward_messageis admin-controlled and has no length validation.- Direct link + forward link are each ~60–100 chars.
- Author line is moderate.
- Total could exceed 1024 chars if the admin sets a long forward message.
Impact: send_media_group will fail with a Telegram API error if caption > 1024 characters.
Fix needed: Truncate forward_def.forward_message or the final caption to ensure it stays under 1024 characters (e.g., caption.chars().take(1024).collect() or pre-truncate the message component).
3.4 decrypt_bytes loads entire files into memory
Status: ⚠️ ISSUE FOUND
Evidence:
cgcx-crypto/src/lib.rslines 102–126:decrypt_bytestakes&[u8]and returnsVec<u8>, accumulating all plaintext in a singleVec.- In
main.rs(lines 1538–1545 and 1975–1981):match tokio::fs::read(&file.stored_path).await { // entire ciphertext in memory Ok(ciphertext) => { match cgcx_crypto::decrypt_bytes(&ciphertext, ...) { // entire plaintext in memory Ok(bytes) => decrypted.push((file.mime_type.clone(), bytes)), } } } - Then
InputFile::memory(bytes.clone())clones the bytes again for teloxide.
Impact:
- For a single large file (e.g., 500 MB video), the bot will hold:
- Ciphertext (~500 MB)
- Plaintext (~500 MB)
- Teloxide clone (~500 MB)
- Total ~1.5 GB for one file.
- In a 10-item batch, this scales linearly.
- High risk of OOM on constrained deployments.
Fix needed: Use streaming decryption and InputFile::file(path) or a temporary file approach so the bytes are not fully materialized in RAM. This is a larger architectural change.
4. review_message_id Storage & Editing
Status: ✅ PASS
Evidence:
Storage
main.rsline 1535:forward_repo.set_review_message_id(submission_id, sent.id.0).await?;ForwardRepo::set_review_message_idincrates/cgcx-db/src/repos.rsline 679–683:pub async fn set_review_message_id(&self, id: i64, message_id: i32) -> Result<()> { conn.execute( "UPDATE forward_submissions SET review_message_id = ?1 WHERE id = ?2", params![message_id, id], )... }- Schema in
migrations/003_forward_system.sql:review_message_id INTEGER,
Editing on resolution
| Action | Line | Edit behavior |
|---|---|---|
| approve | ~2026 | edit_message_text → <b>[ APPROVED ]</b> #{id}\nApproved by <code>{user_id}</code>, keyboard cleared |
| ignore | ~2054 | edit_message_text → <b>[ IGNORED ]</b> ..., keyboard cleared |
| blacklist (blk) | ~2064 | edit_message_text → <b>[ BLACKLISTED ]</b> ..., keyboard cleared |
| ban | ~2073 | edit_message_text → <b>[ BANNED ]</b> ..., keyboard cleared |
| banblk | ~2094 | edit_message_text → <b>[ BAN/BL ]</b> ..., keyboard cleared |
All paths use submission.review_message_id (retrieved from DB) and call edit_message_text with an empty keyboard, preventing further interaction.
Summary of Fixes Needed
| # | Issue | Severity | Suggested Fix |
|---|---|---|---|
| 1 | Videos sent as InputMedia::Document |
Medium | Add mime_type.starts_with("video/") branch using InputMediaVideo |
| 2 | Audio sent as InputMedia::Document |
Medium | Add mime_type.starts_with("audio/") branch using InputMediaAudio |
| 3 | Caption may exceed 1024 chars | Medium | Truncate caption to 1024 chars before sending |
| 4 | decrypt_bytes + InputFile::memory loads entire files into RAM |
High (OOM risk) | Implement streaming file decryption or write decrypted data to temp files and use InputFile::file |
No fixes needed for: batch size logic, caption placement, action button ordering, text-only fallback, or review_message_id lifecycle.