refactor(send): model a payload's media as MediaRef, not media + file_id

MediaItemPayload carried one String field with two meanings and a file_id
bool beside it to say which, in all three variants; every reader re-checked
the flag (input_file had a three-arm pattern just to find the file-id case).
MediaRef::Source/FileId says it once, and the readers now match on it —
local_media_paths, item_url, input_file and the download path each shrank to
the one branch they care about.

Fixes a real failure that the flag was hiding: send_animation built its
InputFile with input_file_for(media_url), which read a cached file id as a
local path and answered "local media file missing" — permanent. So the
second request for a single-gif post always failed (the third worked, from
the degraded entry). It now uses the payload's own input_file, and
a_cached_animation_sends_by_file_id fails without that line.

Wire shape: a queued row from before this change no longer parses, and the
queue already handles that shape (handle_task dead-letters it as an invalid
payload, and dead_letter_notify still names the post and drops the stale
cache entry).
This commit is contained in:
2026-09-21 18:44:26 +08:00
parent 70d4f2ec88
commit aa70b45ae6
7 changed files with 164 additions and 110 deletions
+12 -5
View File
@@ -3,7 +3,9 @@
//! that exceed Telegram's limits and uploads the batch via multipart.
use super::input_media::{input_file_for, item_url, media_from};
use super::{MediaItemPayload, SendError, Task, classify_to_send_error, retry_delay_seconds};
use super::{
MediaItemPayload, MediaRef, SendError, Task, classify_to_send_error, retry_delay_seconds,
};
use crate::media_sender::MediaSender;
use crate::photo::{self, MAX_UPLOAD_BYTES, PhotoPrep};
use std::sync::LazyLock;
@@ -68,10 +70,15 @@ pub(super) enum FallbackError {
async fn download_to_temp(
item: &MediaItemPayload,
) -> Result<(NamedTempFile, bytes::Bytes), FallbackError> {
let media_url = match item {
MediaItemPayload::Photo { media, .. }
| MediaItemPayload::Video { media, .. }
| MediaItemPayload::Animation { media, .. } => media,
// Only a URL/path item is ever downloaded: a file id is sent as-is (see
// `MediaItemPayload::input_file`), so this path cannot see one.
let media_url = match item.media_ref() {
MediaRef::Source(media) => media,
MediaRef::FileId(id) => {
return Err(FallbackError::Permanent {
message: format!("file id reached the download path: {id}"),
});
}
};
// Photos are downloaded even over the upload cap so `prepare_photo` can
// downscale / transcode them, up to their own download cap; videos and