From 4236e8a06ff35ee9f32eb719ba9fbd95faa4f153 Mon Sep 17 00:00:00 2001 From: AH-dark Date: Fri, 15 Mar 2024 01:20:10 +0800 Subject: [PATCH 1/5] feat: added handler for webm stickers --- .idea/.gitignore | 2 + Cargo.lock | 12 +++++ .../stickers-export-handler/Cargo.toml | 1 + .../stickers-export-handler/src/convert.rs | 13 +++++ .../stickers-export-handler/src/handlers.rs | 48 ++++++++++++------- .../stickers-export-handler/src/main.rs | 1 + 6 files changed, 61 insertions(+), 16 deletions(-) create mode 100644 rust-components/stickers-export-handler/src/convert.rs diff --git a/.idea/.gitignore b/.idea/.gitignore index 13566b8..a9d7db9 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -6,3 +6,5 @@ # Datasource local storage ignored files /dataSources/ /dataSources.local.xml +# GitHub Copilot persisted chat sessions +/copilot/chatSessions diff --git a/Cargo.lock b/Cargo.lock index 4cbc914..664622b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ac-ffmpeg" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d1d77cd9038693a0551d28ccb8bd5c542e54017673bf9af780f7a3ca6da37c" +dependencies = [ + "cc", + "lazy_static", + "pkg-config", +] + [[package]] name = "addr2line" version = "0.21.0" @@ -2951,6 +2962,7 @@ dependencies = [ name = "stickers-export-handler" version = "0.1.0" dependencies = [ + "ac-ffmpeg", "anyhow", "image", "log", diff --git a/rust-components/stickers-export-handler/Cargo.toml b/rust-components/stickers-export-handler/Cargo.toml index ba5c093..41a9e46 100644 --- a/rust-components/stickers-export-handler/Cargo.toml +++ b/rust-components/stickers-export-handler/Cargo.toml @@ -20,3 +20,4 @@ teloxide = { workspace = true, features = ["macros"] } serde = { workspace = true, features = ["derive"] } image = { version = "0.25", features = [] } anyhow = "1.0" +ac-ffmpeg = "0.18" diff --git a/rust-components/stickers-export-handler/src/convert.rs b/rust-components/stickers-export-handler/src/convert.rs new file mode 100644 index 0000000..a238e31 --- /dev/null +++ b/rust-components/stickers-export-handler/src/convert.rs @@ -0,0 +1,13 @@ +use std::io::Cursor; + +pub(crate) fn convert_webp_to_png(image_data: &Vec) -> anyhow::Result> { + let img = image::load_from_memory(image_data)?; + let mut buffer = Vec::new(); + let mut cursor = Cursor::new(&mut buffer); + img.write_to(&mut cursor, image::ImageFormat::Png)?; + Ok(buffer) +} + +pub(crate) fn convert_webm_to_gif(image_data: &Vec) -> anyhow::Result> { + todo!() +} diff --git a/rust-components/stickers-export-handler/src/handlers.rs b/rust-components/stickers-export-handler/src/handlers.rs index 161f4bf..40d7e0c 100644 --- a/rust-components/stickers-export-handler/src/handlers.rs +++ b/rust-components/stickers-export-handler/src/handlers.rs @@ -1,12 +1,11 @@ -use std::io::Cursor; - -use image::ImageError; -use image::ImageFormat::Png; +use image::guess_format; use teloxide::net::Download; use teloxide::prelude::*; use teloxide::types::{InputFile, MediaKind, MessageKind}; use teloxide::utils::command::BotCommands; +use crate::convert::{convert_webm_to_gif, convert_webp_to_png}; + #[derive(BotCommands, Clone)] #[command(rename_rule = "lowercase")] pub(crate) enum Command { @@ -44,23 +43,40 @@ pub(crate) async fn export_sticker_handler(bot: Bot, message: Message) -> anyhow }; // convert sticker to png - let file = bot.get_file(&media_sticker.sticker.file.id).send().await?; let mut buffer = Vec::new(); - bot.download_file(&file.path, &mut buffer).await?; + log::debug!( + "Downloading sticker: {}, chat id: {}", + file.path, + message.chat.id + ); + + match bot.download_file(&file.path, &mut buffer).await { + Ok(_) => {} + Err(err) => { + send_error_message!( + bot, + message, + &format!("Failed to download sticker: {}", err) + ); + return Err(err.into()); + } + }; - let img = image::load_from_memory(&buffer)?; - let output_data = tokio::task::spawn_blocking(move || -> Result, ImageError> { - let mut bytes: Vec = Vec::new(); - let mut cursor = Cursor::new(&mut bytes); - img.write_to(&mut cursor, Png)?; - Ok(bytes) - }) - .await??; + let bytes = match if guess_format(&buffer).is_ok() { + convert_webp_to_png(&mut buffer) + } else { + convert_webm_to_gif(&mut buffer) + } { + Ok(buf) => buf, + Err(err) => { + send_error_message!(bot, message, &format!("Failed to convert sticker: {}", err)); + return Err(err.into()); + } + }; // send png - - bot.send_photo(message.chat.id, InputFile::memory(output_data)) + bot.send_photo(message.chat.id, InputFile::memory(bytes)) .reply_to_message_id(message.id) .send() .await?; diff --git a/rust-components/stickers-export-handler/src/main.rs b/rust-components/stickers-export-handler/src/main.rs index f8e3098..062a391 100644 --- a/rust-components/stickers-export-handler/src/main.rs +++ b/rust-components/stickers-export-handler/src/main.rs @@ -9,6 +9,7 @@ use pegasus_common::{observability, settings}; use crate::run::run; +mod convert; mod handlers; mod run; From 6ed0aa549a398544d096dc471debde357d0ba209 Mon Sep 17 00:00:00 2001 From: AH-dark Date: Sat, 16 Mar 2024 14:49:24 +0800 Subject: [PATCH 2/5] feat: upload image data directly without conversion --- Cargo.lock | 12 ----------- .../stickers-export-handler/Cargo.toml | 1 - .../stickers-export-handler/src/convert.rs | 13 ------------ .../stickers-export-handler/src/handlers.rs | 21 ++++--------------- .../stickers-export-handler/src/main.rs | 1 - 5 files changed, 4 insertions(+), 44 deletions(-) delete mode 100644 rust-components/stickers-export-handler/src/convert.rs diff --git a/Cargo.lock b/Cargo.lock index 664622b..4cbc914 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,17 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "ac-ffmpeg" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d1d77cd9038693a0551d28ccb8bd5c542e54017673bf9af780f7a3ca6da37c" -dependencies = [ - "cc", - "lazy_static", - "pkg-config", -] - [[package]] name = "addr2line" version = "0.21.0" @@ -2962,7 +2951,6 @@ dependencies = [ name = "stickers-export-handler" version = "0.1.0" dependencies = [ - "ac-ffmpeg", "anyhow", "image", "log", diff --git a/rust-components/stickers-export-handler/Cargo.toml b/rust-components/stickers-export-handler/Cargo.toml index 41a9e46..ba5c093 100644 --- a/rust-components/stickers-export-handler/Cargo.toml +++ b/rust-components/stickers-export-handler/Cargo.toml @@ -20,4 +20,3 @@ teloxide = { workspace = true, features = ["macros"] } serde = { workspace = true, features = ["derive"] } image = { version = "0.25", features = [] } anyhow = "1.0" -ac-ffmpeg = "0.18" diff --git a/rust-components/stickers-export-handler/src/convert.rs b/rust-components/stickers-export-handler/src/convert.rs deleted file mode 100644 index a238e31..0000000 --- a/rust-components/stickers-export-handler/src/convert.rs +++ /dev/null @@ -1,13 +0,0 @@ -use std::io::Cursor; - -pub(crate) fn convert_webp_to_png(image_data: &Vec) -> anyhow::Result> { - let img = image::load_from_memory(image_data)?; - let mut buffer = Vec::new(); - let mut cursor = Cursor::new(&mut buffer); - img.write_to(&mut cursor, image::ImageFormat::Png)?; - Ok(buffer) -} - -pub(crate) fn convert_webm_to_gif(image_data: &Vec) -> anyhow::Result> { - todo!() -} diff --git a/rust-components/stickers-export-handler/src/handlers.rs b/rust-components/stickers-export-handler/src/handlers.rs index 40d7e0c..1268d72 100644 --- a/rust-components/stickers-export-handler/src/handlers.rs +++ b/rust-components/stickers-export-handler/src/handlers.rs @@ -4,8 +4,6 @@ use teloxide::prelude::*; use teloxide::types::{InputFile, MediaKind, MessageKind}; use teloxide::utils::command::BotCommands; -use crate::convert::{convert_webm_to_gif, convert_webp_to_png}; - #[derive(BotCommands, Clone)] #[command(rename_rule = "lowercase")] pub(crate) enum Command { @@ -46,9 +44,10 @@ pub(crate) async fn export_sticker_handler(bot: Bot, message: Message) -> anyhow let file = bot.get_file(&media_sticker.sticker.file.id).send().await?; let mut buffer = Vec::new(); log::debug!( - "Downloading sticker: {}, chat id: {}", + "Downloading sticker: {}({}), chat id: {}", + media_sticker.sticker.file.id, file.path, - message.chat.id + message.chat.id, ); match bot.download_file(&file.path, &mut buffer).await { @@ -63,20 +62,8 @@ pub(crate) async fn export_sticker_handler(bot: Bot, message: Message) -> anyhow } }; - let bytes = match if guess_format(&buffer).is_ok() { - convert_webp_to_png(&mut buffer) - } else { - convert_webm_to_gif(&mut buffer) - } { - Ok(buf) => buf, - Err(err) => { - send_error_message!(bot, message, &format!("Failed to convert sticker: {}", err)); - return Err(err.into()); - } - }; - // send png - bot.send_photo(message.chat.id, InputFile::memory(bytes)) + bot.send_photo(message.chat.id, InputFile::memory(buffer)) .reply_to_message_id(message.id) .send() .await?; diff --git a/rust-components/stickers-export-handler/src/main.rs b/rust-components/stickers-export-handler/src/main.rs index 062a391..f8e3098 100644 --- a/rust-components/stickers-export-handler/src/main.rs +++ b/rust-components/stickers-export-handler/src/main.rs @@ -9,7 +9,6 @@ use pegasus_common::{observability, settings}; use crate::run::run; -mod convert; mod handlers; mod run; From 57524c642d8df296fc2ce3730a33c0f87826bf78 Mon Sep 17 00:00:00 2001 From: AH-dark Date: Sat, 16 Mar 2024 17:21:00 +0800 Subject: [PATCH 3/5] feat: convert webm via ffmpeg cli --- Cargo.lock | 10 ++ .../stickers-export-handler/Cargo.toml | 1 + .../stickers-export-handler/src/convert.rs | 91 +++++++++++++++++++ .../stickers-export-handler/src/handlers.rs | 21 ++++- .../stickers-export-handler/src/main.rs | 1 + 5 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 rust-components/stickers-export-handler/src/convert.rs diff --git a/Cargo.lock b/Cargo.lock index 4cbc914..6d5ef35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -283,6 +283,15 @@ version = "4.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" +[[package]] +name = "async-tempfile" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17d4a3286e6518d84be399efb8901aa74a183753b0bf4a8a4afe5bcfedcbb08" +dependencies = [ + "tokio", +] + [[package]] name = "async-trait" version = "0.1.77" @@ -2952,6 +2961,7 @@ name = "stickers-export-handler" version = "0.1.0" dependencies = [ "anyhow", + "async-tempfile", "image", "log", "opentelemetry", diff --git a/rust-components/stickers-export-handler/Cargo.toml b/rust-components/stickers-export-handler/Cargo.toml index ba5c093..509410c 100644 --- a/rust-components/stickers-export-handler/Cargo.toml +++ b/rust-components/stickers-export-handler/Cargo.toml @@ -20,3 +20,4 @@ teloxide = { workspace = true, features = ["macros"] } serde = { workspace = true, features = ["derive"] } image = { version = "0.25", features = [] } anyhow = "1.0" +async-tempfile = "0.5" diff --git a/rust-components/stickers-export-handler/src/convert.rs b/rust-components/stickers-export-handler/src/convert.rs new file mode 100644 index 0000000..0e85e41 --- /dev/null +++ b/rust-components/stickers-export-handler/src/convert.rs @@ -0,0 +1,91 @@ +use std::io::Cursor; + +use image::codecs::{png, webp}; +use image::ImageFormat; + +pub(crate) fn convert_webp_to_png(input_buffer: Vec) -> anyhow::Result> { + let input_image = image::load_from_memory(&input_buffer) + .map_err(|err| anyhow::anyhow!("Failed to load image from memory: {}", err))?; + + let mut output_buffer = Vec::new(); + let mut output_cursor = Cursor::new(&mut output_buffer); + input_image + .write_to(&mut output_cursor, ImageFormat::Png) + .map_err(|err| anyhow::anyhow!("Failed to write image to buffer: {}", err))?; + + Ok(output_buffer) +} + +pub(crate) async fn convert_webm_to_gif(input_buffer: Vec) -> anyhow::Result> { + let mut input_file = async_tempfile::TempFile::new_with_name("input.webm") + .await + .map_err(|err| anyhow::anyhow!("Failed to create input file: {}", err))?; + + let palette_file = async_tempfile::TempFile::new_with_name("palette.png") + .await + .map_err(|err| anyhow::anyhow!("Failed to create palette file: {}", err))?; + + let mut output_file = async_tempfile::TempFile::new_with_name("output.gif") + .await + .map_err(|err| anyhow::anyhow!("Failed to create output file: {}", err))?; + + log::debug!( + "input_file: {}, palette_file: {}, output_file: {}", + input_file.file_path().to_str().unwrap(), + palette_file.file_path().to_str().unwrap(), + output_file.file_path().to_str().unwrap() + ); + + tokio::io::AsyncWriteExt::write_all(&mut input_file, &input_buffer) + .await + .map_err(|err| anyhow::anyhow!("Failed to write input file: {}", err))?; + + // create a palette from the input file + let out = tokio::process::Command::new("ffmpeg") + .arg("-i") + .arg(input_file.file_path()) + .arg("-vf") + .arg("palettegen") + .arg(palette_file.file_path()) + .arg("-y") + .output() + .await + .map_err(|err| anyhow::anyhow!("Failed to generate palette: {}", err))?; + if !out.status.success() { + return Err(anyhow::anyhow!( + "Failed to generate palette: {}", + String::from_utf8_lossy(&out.stderr) + )); + } + + // convert the input file to a gif using the palette + let out = tokio::process::Command::new("ffmpeg") + .arg("-i") + .arg(input_file.file_path()) + .arg("-i") + .arg(palette_file.file_path()) + .arg("-filter_complex") + .arg("paletteuse") + .arg(output_file.file_path()) + .arg("-y") + .output() + .await + .map_err(|err| anyhow::anyhow!("Failed to convert webm to gif: {}", err))?; + if !out.status.success() { + return Err(anyhow::anyhow!( + "Failed to convert webm to gif: {}", + String::from_utf8_lossy(&out.stderr) + )); + } + + let mut output_buffer = Vec::new(); + tokio::io::AsyncReadExt::read_to_end(&mut output_file, &mut output_buffer) + .await + .map_err(|err| anyhow::anyhow!("Failed to read output file: {}", err))?; + + if output_buffer.is_empty() { + return Err(anyhow::anyhow!("Empty output buffer")); + } + + Ok(output_buffer) +} diff --git a/rust-components/stickers-export-handler/src/handlers.rs b/rust-components/stickers-export-handler/src/handlers.rs index 1268d72..8dfd9a9 100644 --- a/rust-components/stickers-export-handler/src/handlers.rs +++ b/rust-components/stickers-export-handler/src/handlers.rs @@ -4,6 +4,8 @@ use teloxide::prelude::*; use teloxide::types::{InputFile, MediaKind, MessageKind}; use teloxide::utils::command::BotCommands; +use crate::convert::{convert_webm_to_gif, convert_webp_to_png}; + #[derive(BotCommands, Clone)] #[command(rename_rule = "lowercase")] pub(crate) enum Command { @@ -62,8 +64,25 @@ pub(crate) async fn export_sticker_handler(bot: Bot, message: Message) -> anyhow } }; + let input_file = match guess_format(&buffer) { + Ok(_) => match convert_webp_to_png(buffer) { + Ok(buf) => InputFile::memory(buf).file_name("sticker.png"), + Err(err) => { + send_error_message!(bot, message, &format!("Failed to convert sticker: {}", err)); + return Err(err.into()); + } + }, + Err(_) => match convert_webm_to_gif(buffer).await { + Ok(buf) => InputFile::memory(buf).file_name("sticker.gif"), + Err(err) => { + send_error_message!(bot, message, &format!("Failed to convert sticker: {}", err)); + return Err(err.into()); + } + }, + }; + // send png - bot.send_photo(message.chat.id, InputFile::memory(buffer)) + bot.send_document(message.chat.id, input_file) .reply_to_message_id(message.id) .send() .await?; diff --git a/rust-components/stickers-export-handler/src/main.rs b/rust-components/stickers-export-handler/src/main.rs index f8e3098..062a391 100644 --- a/rust-components/stickers-export-handler/src/main.rs +++ b/rust-components/stickers-export-handler/src/main.rs @@ -9,6 +9,7 @@ use pegasus_common::{observability, settings}; use crate::run::run; +mod convert; mod handlers; mod run; From 0f0dd486a5002fc1bd723cdb76351681925c18fd Mon Sep 17 00:00:00 2001 From: AH-dark Date: Sat, 16 Mar 2024 17:35:53 +0800 Subject: [PATCH 4/5] feat: add ffmpeg to docker image --- docker/rust-basic/Dockerfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docker/rust-basic/Dockerfile b/docker/rust-basic/Dockerfile index d0542da..fbfbe76 100644 --- a/docker/rust-basic/Dockerfile +++ b/docker/rust-basic/Dockerfile @@ -15,9 +15,10 @@ WORKDIR /app ARG COMPONENT -RUN apt update && \ - apt install -y openssl libssl-dev ca-certificates && \ - rm -rf /var/lib/apt/lists/* +RUN apt update +RUN apt install -y openssl libssl-dev ca-certificates +RUN if [ "${COMPONENT}" = "stickers-export-handler" ]; then apt install -y ffmpeg; fi +RUN rm -rf /var/lib/apt/lists/* COPY --from=builder /usr/src/pegasus/target/release/${COMPONENT} /app/entry From 2c13da36f06b7446f9e595a2080dd1c94c9f713b Mon Sep 17 00:00:00 2001 From: AH-dark Date: Sat, 16 Mar 2024 17:40:15 +0800 Subject: [PATCH 5/5] feat: send pending message --- .../stickers-export-handler/src/handlers.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/rust-components/stickers-export-handler/src/handlers.rs b/rust-components/stickers-export-handler/src/handlers.rs index 8dfd9a9..9391524 100644 --- a/rust-components/stickers-export-handler/src/handlers.rs +++ b/rust-components/stickers-export-handler/src/handlers.rs @@ -35,6 +35,12 @@ pub(crate) async fn export_sticker_handler(bot: Bot, message: Message) -> anyhow return Ok(()); }; + let pending_message = bot + .send_message(message.chat.id, "Processing...") + .reply_to_message_id(message.id) + .send() + .await?; + let media_sticker = if let MediaKind::Sticker(media_sticker) = &reply_to_message.media_kind { media_sticker } else { @@ -87,5 +93,9 @@ pub(crate) async fn export_sticker_handler(bot: Bot, message: Message) -> anyhow .send() .await?; + bot.delete_message(pending_message.chat.id, pending_message.id) + .send() + .await?; + Ok(()) }