From 7334fb3d0930c06db6dffc2d6756f941cab41f1b Mon Sep 17 00:00:00 2001 From: VC Date: Mon, 1 Dec 2025 11:37:58 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F:=20better=20handle=20of=20qu?= =?UTF-8?q?otes=20and=20media=20embedded=20into=20quotes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 62 +++++++++++++++++++++---------------------- Cargo.toml | 2 +- README.md | 5 +++- src/bsky.rs | 60 ++++++++++++++++-------------------------- src/lib.rs | 76 ++++++++++++++++++++++++++++++++++++++++++----------- 5 files changed, 119 insertions(+), 86 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 892b1e5..1ab9e1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -391,9 +391,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.2.47" +version = "1.2.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" dependencies = [ "find-msvc-tools", "jobserver", @@ -1511,9 +1511,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -1736,9 +1736,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6" +checksum = "80986bbbcf925ebd3be54c26613d861255284584501595cf418320c078945608" dependencies = [ "num-traits", "pxfm", @@ -1926,7 +1926,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "oolatoocs" -version = "4.4.1" +version = "4.4.2" dependencies = [ "atrium-api", "bsky-sdk", @@ -2165,9 +2165,9 @@ dependencies = [ [[package]] name = "psl" -version = "2.1.165" +version = "2.1.166" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e410950750bcf102e8849416a57e4d54bc5dd35ac3831964bf3a0406d2b5ff0e" +checksum = "2085080c7de45d70a59d96aa7d5b0870fc1ccbd27adb780ee1e1ec905da42035" dependencies = [ "psl-types", ] @@ -2180,9 +2180,9 @@ checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" [[package]] name = "pxfm" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" +checksum = "b3502d6155304a4173a5f2c34b52b7ed0dd085890326cb50fd625fdf39e86b3b" dependencies = [ "num-traits", ] @@ -2579,9 +2579,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" dependencies = [ "web-time", "zeroize", @@ -3213,9 +3213,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -3417,9 +3417,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", @@ -3430,9 +3430,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.55" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", @@ -3443,9 +3443,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3453,9 +3453,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ "bumpalo", "proc-macro2", @@ -3466,9 +3466,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] @@ -3488,9 +3488,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", @@ -3809,18 +3809,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.30" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.30" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf955aa904d6040f70dc8e9384444cb1030aed272ba3cb09bbc4ab9e7c1f34f5" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 29e315c..01feb5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oolatoocs" -version = "4.4.1" +version = "4.4.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/README.md b/README.md index a0f13c0..9b37291 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,13 @@ Since 2025-01-20, Twitter is now longer supported. What it can do: * Reproduces the Toot content into the Record; * Cuts (poorly) the Toot in half in it’s too long for Bluesky and thread it (this is cut using a word count, not the best method, but it gets the job done); -* Reuploads images/gifs/videos from Mastodon to Bluesky +* Reuploads images/gifs/videos/webcards from Mastodon to Bluesky * ⚠️ Bluesky does not support mixing images and videos. You can have up to 4 images on a Bsky record **or** 1 video but not mix around. If you do so, only the video will be posted on Bluesky. * ⚠️ Bluesky does not support images greater than 1Mb (that is 1,000,000 bytes or 976.6 KiB), so Oolatoocs converts the image to WebP and progressively reduces the quality to fit that limitation. + * ⚠️ Bluesky does not support webcards with any other media/quote, so webcards have the last priority * Can reproduce threads from Mastodon to Bluesky +* Can reproduce (self-)quotes from Mastodon to Bluesky + * ⚠️ Bluesky can’t do quotes with webcards, you can only embed images **or** a video with quotes * ⚠️ Bluesky does support polls for now. So the poll itself is just presented as text from Mastodon instead which is not the most elegant. * Can prevent a Toot from being recorded to Bluesky by using the #NoTweet (case-insensitive) hashtag in Mastodon diff --git a/src/bsky.rs b/src/bsky.rs index dc8f155..35b2802 100644 --- a/src/bsky.rs +++ b/src/bsky.rs @@ -1,4 +1,4 @@ -use crate::config::BlueskyConfig; +use crate::{config::BlueskyConfig, OolatoocsError}; use atrium_api::{ app::bsky::feed::post::RecordData, com::atproto::repo::upload_blob::Output, types::string::Datetime, types::string::Language, types::string::RecordKey, @@ -148,18 +148,16 @@ async fn get_record( Ok(record) } -/// Generate an quote embed record into Bsky +/// Generate an quote embed record +/// it is encapsulated in Option to prevent this function from failing pub async fn generate_quote_records( config: &BlueskyConfig, quote_id: &str, -) -> Option> { +) -> Result> { // if we can’t match the quote_id, simply return None - let quote_record = match get_record(&config.handle, &rkey(quote_id)).await { - Ok(a) => a, - Err(_) => return None, - }; + let quote_record = get_record(&config.handle, &rkey(quote_id)).await?; - Some(atrium_api::types::Union::Refs( + Ok( atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedRecordMain(Box::new( atrium_api::app::bsky::embed::record::MainData { record: atrium_api::com::atproto::repo::strong_ref::MainData { @@ -170,24 +168,18 @@ pub async fn generate_quote_records( } .into(), )), - )) + ) } -/// Generate an embed card record into Bsky +/// Generate an embed webcard record into Bsky /// If the preview image does not exist or fails to upload, it is simply ignored -pub async fn generate_embed_records( +pub async fn generate_webcard_records( bsky: &BskyAgent, card: &Card, -) -> Option> { - // uploads the image card, if it fails, simply ignore everything - let blob = if let Some(url) = &card.image { - if let Ok(image_blob) = upload_media(true, bsky, url).await { - Some(image_blob.blob.clone()) - } else { - None - } - } else { - None +) -> Result> { + let blob = match &card.image { + Some(url) => upload_media(true, bsky, url).await?.blob.clone().into(), + None => None, }; let record_card = atrium_api::app::bsky::embed::external::ExternalData { @@ -197,14 +189,14 @@ pub async fn generate_embed_records( uri: card.url.clone(), }; - Some(atrium_api::types::Union::Refs( + Ok( atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedExternalMain(Box::new( atrium_api::app::bsky::embed::external::MainData { external: record_card.into(), } .into(), )), - )) + ) } /// Generate an array of Bsky media records @@ -213,11 +205,7 @@ pub async fn generate_embed_records( pub async fn generate_media_records( bsky: &BskyAgent, media_attach: &[Attachment], -) -> Option> { - let mut embed: Option< - atrium_api::types::Union, - > = None; - +) -> Result> { let image_media_attach: Vec<_> = media_attach .iter() .filter(|x| x.r#type == AttachmentType::Image) @@ -233,9 +221,9 @@ pub async fn generate_media_records( if !video_media_attach.is_empty() { // treat only the very first video, ignore the rest let media = &video_media_attach[0]; - let blob = upload_media(false, bsky, &media.url).await.unwrap(); + let blob = upload_media(false, bsky, &media.url).await?; - embed = Some(atrium_api::types::Union::Refs( + return Ok( atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedVideoMain(Box::new( atrium_api::app::bsky::embed::video::MainData { alt: media.description.clone(), @@ -245,12 +233,10 @@ pub async fn generate_media_records( } .into(), )), - )); - - // returns immediately, we don’t want to treat the other medias - return embed; + ); } + // It wasn’t a video, then it’s an image or a gallery of 4 images let mut stream = stream::iter(image_media_attach) .map(|media| { let bsky = bsky.clone(); @@ -281,14 +267,14 @@ pub async fn generate_media_records( } if !images.is_empty() { - embed = Some(atrium_api::types::Union::Refs( + return Ok( atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedImagesMain(Box::new( atrium_api::app::bsky::embed::images::MainData { images }.into(), )), - )); + ); } - embed + Err(OolatoocsError::new("Cannot embed media").into()) } async fn upload_media( diff --git a/src/lib.rs b/src/lib.rs index e561fa5..e22bbc2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,7 +19,7 @@ use utils::{generate_multi_tweets, strip_everything}; mod bsky; use bsky::{ - build_post_record, generate_embed_records, generate_media_records, generate_quote_records, + build_post_record, generate_media_records, generate_quote_records, generate_webcard_records, get_session, BskyReply, }; @@ -161,22 +161,66 @@ pub async fn run(config: &Config) { }); }; - // Don’t know how to union things so… - // cards have the least priority (you cannot embed card and images anyway) - // images have a higher priority - // quotes have the highest priority - - let record_embed = if toot.reblog.is_some() { - let quote_record = - read_state(&conn, Some(toot.reblog.unwrap().id.parse::().unwrap())); - match quote_record { - Ok(Some(q)) => generate_quote_records(&config.bluesky, &q.record_uri).await, - _ => None, + // handle quote if any + let quote_embed = match toot.reblog { + Some(r) => { + let quote_record = read_state(&conn, Some(r.id.parse::().unwrap())); + match quote_record { + Ok(Some(q)) => generate_quote_records(&config.bluesky, &q.record_uri) + .await + .ok(), + _ => None, + } } - } else if toot.media_attachments.len() > usize::from(0u8) { - generate_media_records(&bluesky, &toot.media_attachments).await - } else if toot.card.is_some() { - generate_embed_records(&bluesky, &toot.card.unwrap()).await + None => None, + }; + + // handle medias if any + let media_embed = if toot.media_attachments.len() > usize::from(0u8) { + generate_media_records(&bluesky, &toot.media_attachments) + .await + .ok() + } else { + None + }; + + // handle webcard if any + let webcard_embed = match toot.card { + Some(t) => generate_webcard_records(&bluesky, &t).await.ok(), + None => None, + }; + + let record_embed = if quote_embed.is_some() { + if media_embed.is_some() { + let medias_mapped = match media_embed.unwrap() { + atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedImagesMain(a) => atrium_api::app::bsky::embed::record_with_media::MainMediaRefs::AppBskyEmbedImagesMain(a), + atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedVideoMain(a) => atrium_api::app::bsky::embed::record_with_media::MainMediaRefs::AppBskyEmbedVideoMain(a), + _ => continue, // this should NEVER happen as Media are either Video or + // Images at this point + }; + let quote_mapped = match quote_embed.unwrap() { + atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedRecordMain( + a, + ) => a, + _ => continue, // again, this should NEVER happen + }; + Some(atrium_api::types::Union::Refs( + atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedRecordWithMediaMain( + Box::new( + atrium_api::app::bsky::embed::record_with_media::MainData { + media: atrium_api::types::Union::Refs(medias_mapped), + record: (*quote_mapped), + }.into() + ) + ) + )) + } else { + quote_embed.map(atrium_api::types::Union::Refs) + } + } else if media_embed.is_some() { + media_embed.map(atrium_api::types::Union::Refs) + } else if webcard_embed.is_some() { + webcard_embed.map(atrium_api::types::Union::Refs) } else { None };