@@ -5,7 +5,10 @@ use bytes::Bytes;
55use http:: Uri ;
66use image:: { imageops:: FilterType as ImageFilterType , DynamicImage , GenericImageView , ImageFormat } ;
77use itertools:: Itertools ;
8- use reqwest:: multipart:: { Form , Part } ;
8+ use reqwest:: {
9+ multipart:: { Form , Part } ,
10+ Url ,
11+ } ;
912use serde:: {
1013 de:: { DeserializeOwned , IgnoredAny } ,
1114 Deserialize ,
@@ -17,6 +20,7 @@ use super::super::{ConfigApiServer, ConfigChat};
1720use crate :: {
1821 config:: Config ,
1922 helper:: { self , VideoResolution } ,
23+ platform:: twitter:: source:: TWITTER_IMAGE_URL_END_TAG ,
2024 source:: { PostAttachmentImage , PostAttachmentVideo , PostContent , PostContentPart } ,
2125} ;
2226
@@ -483,24 +487,62 @@ impl<'a> Media<'a> {
483487 Self :: Document ( document) => document. input ,
484488 }
485489 }
490+
491+ fn convert_to_document ( & mut self ) {
492+ let dropped = MediaInput :: Url ( "* should never happen *" ) ;
493+ match self {
494+ Self :: Document ( _) => { }
495+ Self :: Photo ( photo) => {
496+ * self = Self :: Document ( MediaDocument {
497+ input : mem:: replace ( & mut photo. input , dropped) ,
498+ } ) ;
499+ }
500+ Self :: Video ( video) => {
501+ * self = Self :: Document ( MediaDocument {
502+ input : mem:: replace ( & mut video. input , dropped) ,
503+ } ) ;
504+ }
505+ }
506+ }
486507}
487508
488509#[ derive( Clone ) ]
489510pub enum MediaInput < ' a > {
490511 Url ( & ' a str ) ,
491512 Memory {
492513 data : Bytes ,
493- filename : Option < & ' a str > ,
514+ filename : Option < Cow < ' a , str > > ,
494515 } ,
495516}
496517
497- impl MediaInput < ' _ > {
518+ impl < ' a > MediaInput < ' a > {
519+ fn as_memory ( & self ) -> Option < & Bytes > {
520+ match self {
521+ Self :: Memory { data, .. } => Some ( data) ,
522+ _ => None ,
523+ }
524+ }
525+
498526 fn to_url ( & self , index : usize ) -> Cow < ' _ , str > {
499527 match self {
500528 Self :: Url ( url) => Cow :: Borrowed ( url) ,
501529 Self :: Memory { .. } => Cow :: Owned ( format ! ( "attach://{index}" ) ) ,
502530 }
503531 }
532+
533+ fn extract_filename < ' b > ( & ' a self ) -> Option < Cow < ' b , str > >
534+ where
535+ ' a : ' b ,
536+ {
537+ match self {
538+ Self :: Url ( url) => Url :: parse ( url) . ok ( ) . and_then ( |url| {
539+ url. path_segments ( )
540+ . and_then ( |mut segments| segments. next_back ( ) )
541+ . map ( |filename| Cow :: Owned ( filename. to_string ( ) ) )
542+ } ) ,
543+ Self :: Memory { filename, .. } => filename. as_deref ( ) . map ( Cow :: Borrowed ) ,
544+ }
545+ }
504546}
505547
506548impl fmt:: Debug for MediaInput < ' _ > {
@@ -613,30 +655,53 @@ impl<'a> SendMedia<'a> {
613655 }
614656 }
615657
616- pub async fn send ( self ) -> anyhow:: Result < Response < ResultMessage > > {
658+ pub async fn send ( mut self ) -> anyhow:: Result < Response < ResultMessage > > {
617659 let mut body = json ! (
618660 {
619661 "chat_id" : self . chat. to_json( ) ,
620662 "message_thread_id" : self . thread_id,
621663 "disable_notification" : self . disable_notification
622664 }
623665 ) ;
624- let ( method, url, retry_multipart) = match & self . media {
625- Media :: Photo ( photo) => {
626- let url = photo. input . to_url ( 0 ) ;
627- body[ "photo" ] = url. clone ( ) . into ( ) ;
628- body[ "has_spoiler" ] = photo. has_spoiler . into ( ) ;
629- ( "sendPhoto" , url, matches ! ( photo. input, MediaInput :: Url ( _) ) )
630- }
666+
667+ let mut convert_to_document = false ;
668+ let ( mut method, url, retry_multipart) = match & mut self . media {
669+ Media :: Photo ( photo) => match & photo. input {
670+ MediaInput :: Url ( _) => {
671+ let url = photo. input . to_url ( 0 ) . to_string ( ) ;
672+ body[ "photo" ] = url. clone ( ) . into ( ) ;
673+ body[ "has_spoiler" ] = photo. has_spoiler . into ( ) ;
674+ ( "sendPhoto" , url, true )
675+ }
676+ MediaInput :: Memory { data, .. } => {
677+ // Width and height ratio must be at most 20.
678+ if check_image_aspect_radio ( data) {
679+ let url = photo. input . to_url ( 0 ) . to_string ( ) ;
680+ body[ "photo" ] = url. clone ( ) . into ( ) ;
681+ body[ "has_spoiler" ] = photo. has_spoiler . into ( ) ;
682+ ( "sendPhoto" , url, false )
683+ } else {
684+ // If the ratio is not satisfied, send as document (image without
685+ // compression)
686+ //
687+ // TODO: Do the same thing for send_media_group
688+ warn ! ( "photo aspect ratio exceeds limit, sending as document instead @1" ) ;
689+ convert_to_document = true ;
690+ let url = photo. input . to_url ( 0 ) . to_string ( ) ;
691+ body[ "document" ] = url. clone ( ) . into ( ) ;
692+ ( "sendDocument" , url, false )
693+ }
694+ }
695+ } ,
631696 Media :: Video ( video) => {
632- let url = video. input . to_url ( 0 ) ;
697+ let url = video. input . to_url ( 0 ) . to_string ( ) ;
633698 body[ "video" ] = url. clone ( ) . into ( ) ;
634699 body[ "supports_streaming" ] = true . into ( ) ;
635700 body[ "has_spoiler" ] = video. has_spoiler . into ( ) ;
636701 ( "sendVideo" , url, matches ! ( video. input, MediaInput :: Url ( _) ) )
637702 }
638703 Media :: Document ( document) => {
639- let url = document. input . to_url ( 0 ) ;
704+ let url = document. input . to_url ( 0 ) . to_string ( ) ;
640705 body[ "document" ] = url. clone ( ) . into ( ) ;
641706 (
642707 "sendDocument" ,
@@ -645,6 +710,9 @@ impl<'a> SendMedia<'a> {
645710 )
646711 }
647712 } ;
713+ if convert_to_document {
714+ self . media . convert_to_document ( ) ;
715+ }
648716 if let Some ( text) = self . text {
649717 let ( text, entities) = text. into_json ( ) ;
650718 let body = body. as_object_mut ( ) . unwrap ( ) ;
@@ -662,10 +730,20 @@ impl<'a> SendMedia<'a> {
662730 if retry_multipart && is_media_failure ( & resp) {
663731 warn ! ( "failed to send media with URL, retrying with HTTP multipart. url '{url}', description '{}'" , resp. description. as_deref( ) . unwrap_or( "*no description*" ) ) ;
664732
665- let downloaded = download_file ( self . media ) . await ?;
733+ let mut downloaded = download_file ( self . media ) . await ?;
666734 match & downloaded {
667735 Media :: Photo ( photo) => {
668- body[ "photo" ] = photo. input . to_url ( 0 ) . into ( ) ;
736+ if check_image_aspect_radio ( photo. input . as_memory ( ) . unwrap ( ) ) {
737+ body[ "photo" ] = photo. input . to_url ( 0 ) . into ( ) ;
738+ } else {
739+ warn ! ( "photo aspect ratio exceeds limit, sending as document instead @2" ) ;
740+ let body = body. as_object_mut ( ) . unwrap ( ) ;
741+ body. remove ( "photo" ) ;
742+ body. remove ( "has_spoiler" ) ;
743+ body. insert ( "document" . into ( ) , dbg ! ( & photo. input) . to_url ( 0 ) . into ( ) ) ;
744+ method = "sendDocument" ;
745+ downloaded. convert_to_document ( ) ;
746+ }
669747 }
670748 Media :: Video ( video) => {
671749 body[ "video" ] = video. input . to_url ( 0 ) . into ( ) ;
@@ -1214,10 +1292,17 @@ async fn download_file<'a>(mut file: Media<'a>) -> anyhow::Result<Media<'a>> {
12141292 anyhow ! ( "{rustfmt_bug}: {err}, status: {status} from url '{url}'" )
12151293 } ) ?;
12161294
1217- * input = MediaInput :: Memory {
1218- data,
1219- filename : None ,
1220- } ;
1295+ let filename = Url :: parse ( url) . ok ( ) . and_then ( |url| {
1296+ url. path_segments ( )
1297+ . and_then ( |mut segments| segments. next_back ( ) )
1298+ . map ( |filename| {
1299+ // Workaround for Twitter image URLs
1300+ let filename = filename. trim_end_matches ( TWITTER_IMAGE_URL_END_TAG ) ;
1301+ Cow :: Owned ( filename. to_string ( ) )
1302+ } )
1303+ } ) ;
1304+
1305+ * input = MediaInput :: Memory { data, filename } ;
12211306 // TODO: Replace failed image with a fallback image
12221307 Ok ( file)
12231308}
@@ -1233,20 +1318,25 @@ async fn download_files<'a>(
12331318 Ok ( ret)
12341319}
12351320
1321+ fn image ( bytes : & Bytes ) -> anyhow:: Result < ( DynamicImage , Option < ImageFormat > ) > {
1322+ let image_reader = image:: ImageReader :: new ( Cursor :: new ( & bytes) )
1323+ . with_guessed_format ( )
1324+ . map_err ( |err| anyhow ! ( "failed to guess format for downloaded image: {err}" ) ) ?;
1325+
1326+ let format = image_reader. format ( ) ;
1327+ let image = image_reader
1328+ . decode ( )
1329+ . map_err ( |err| anyhow ! ( "failed to decode downloaded image: {err}" ) ) ?;
1330+ Ok ( ( image, format) )
1331+ }
1332+
12361333macro_rules! workaround_rustfmt_bug {
12371334 ( trying) => { "photo #{} bytes size exceeds the limit, try scaling down with ratio {} from {},{} to {},{}, now the binary size is {}, attempt {}" } ;
12381335 ( giving_up) => { "photo #{} bytes size still exceeds the limit after 10 iterations of scaling down, giving up" } ;
12391336}
12401337fn media_into_part ( i : usize , bytes : Bytes , is_photo : bool ) -> anyhow:: Result < Part > {
12411338 let part = if is_photo {
1242- let image_reader = image:: ImageReader :: new ( Cursor :: new ( & bytes) )
1243- . with_guessed_format ( )
1244- . map_err ( |err| anyhow ! ( "failed to guess format for downloaded image: {err}" ) ) ?;
1245-
1246- let format = image_reader. format ( ) ;
1247- let image = image_reader
1248- . decode ( )
1249- . map_err ( |err| anyhow ! ( "failed to decode downloaded image: {err}" ) ) ?;
1339+ let ( image, format) = image ( & bytes) ?;
12501340
12511341 fn adjust_dimensions ( i : usize , mut image : DynamicImage ) -> DynamicImage {
12521342 // Based on my testing, the actual limit is <=10001 :)
@@ -1332,6 +1422,17 @@ fn media_into_part(i: usize, bytes: Bytes, is_photo: bool) -> anyhow::Result<Par
13321422 Ok ( part)
13331423}
13341424
1425+ fn check_image_aspect_radio ( bytes : & Bytes ) -> bool {
1426+ fn check_image_radio_impl ( bytes : & Bytes ) -> anyhow:: Result < bool > {
1427+ let ( image, _) = image ( bytes) ?;
1428+ let ( width, height) = image. dimensions ( ) ;
1429+ Ok ( width / height <= 20 && height / width <= 20 )
1430+ }
1431+ check_image_radio_impl ( bytes)
1432+ . inspect_err ( |err| warn ! ( "failed to check image aspect radio: {err}, assuming satisfied" ) )
1433+ . unwrap_or ( true )
1434+ }
1435+
13351436#[ cfg( test) ]
13361437mod tests {
13371438 use super :: * ;
0 commit comments