mirror of
https://github.com/TheFunny/TelegramTwitterMediaBot.git
synced 2026-09-23 23:32:05 +00:00
fix(send): order photos first in mixed media groups
Telegram's sendMediaGroup requires the first item to be a photo when a group mixes photos and videos; the previous code kept the source-site order, so a mixed post with a video first (twitter media order is not guaranteed) would 400 permanently. photos_first() stable-sorts photos ahead of videos/animations before chunking; within-kind order is kept.
This commit is contained in:
@@ -569,7 +569,9 @@ fn build_send_task(
|
||||
chat_id,
|
||||
reply_to_message_id: message.id.0 as i64,
|
||||
caption,
|
||||
media_batches: send::chunk_media_items(items),
|
||||
// Photos first so a mixed photo+video group starts with a photo
|
||||
// (Telegram's sendMediaGroup rule); order within each kind is kept.
|
||||
media_batches: send::chunk_media_items(send::photos_first(items)),
|
||||
batch_index: 0,
|
||||
sent_message_ids: vec![],
|
||||
source_url,
|
||||
|
||||
@@ -296,6 +296,19 @@ pub fn chunk_media_items<T: Clone>(items: Vec<T>) -> Vec<Vec<T>> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Orders media for a Telegram media group: when photos and videos are
|
||||
/// mixed, the first item must be a photo (Telegram's sendMediaGroup rule).
|
||||
/// Stable sort keeps the source order within each kind; a lone animation is
|
||||
/// untouched (it takes the SendAnimation path before this runs).
|
||||
pub fn photos_first(items: Vec<MediaItemPayload>) -> Vec<MediaItemPayload> {
|
||||
let mut items = items;
|
||||
items.sort_by_key(|item| match item {
|
||||
MediaItemPayload::Photo { .. } => 0,
|
||||
MediaItemPayload::Video { .. } | MediaItemPayload::Animation { .. } => 1,
|
||||
});
|
||||
items
|
||||
}
|
||||
|
||||
/// Exponential backoff with jitter, capped at 30s.
|
||||
pub fn retry_delay_seconds(attempts: u32) -> f64 {
|
||||
let jitter: f64 = rand::thread_rng().gen_range(0.2..0.8);
|
||||
@@ -1355,6 +1368,44 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn photos_first_orders_photos_before_videos() {
|
||||
use MediaItemPayload::{Animation, Photo, Video};
|
||||
let photo = |u: &str| Photo {
|
||||
media: u.into(),
|
||||
has_spoiler: false,
|
||||
fallback_url: None,
|
||||
file_id: false,
|
||||
};
|
||||
let video = |u: &str| Video {
|
||||
media: u.into(),
|
||||
has_spoiler: false,
|
||||
thumbnail: None,
|
||||
fallback_url: None,
|
||||
file_id: false,
|
||||
};
|
||||
let items = vec![
|
||||
video("https://v/1.mp4"),
|
||||
photo("https://p/1.jpg"),
|
||||
video("https://v/2.mp4"),
|
||||
photo("https://p/2.jpg"),
|
||||
];
|
||||
let ordered = photos_first(items);
|
||||
// All photos first (stable: p1 before p2), then all videos in order.
|
||||
let kinds: Vec<&str> = ordered.iter().map(|i| match i {
|
||||
Photo { media, .. } => media.as_str(),
|
||||
Video { media, .. } => media.as_str(),
|
||||
Animation { .. } => unreachable!(),
|
||||
}).collect();
|
||||
assert_eq!(
|
||||
kinds,
|
||||
["https://p/1.jpg", "https://p/2.jpg", "https://v/1.mp4", "https://v/2.mp4"]
|
||||
);
|
||||
// Already-photos-first input is unchanged.
|
||||
let items = vec![photo("https://p/1.jpg"), video("https://v/1.mp4")];
|
||||
assert!(matches!(photos_first(items)[0], Photo { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retry_delay_seconds_bounds() {
|
||||
for attempts in 0..10 {
|
||||
|
||||
Reference in New Issue
Block a user