diff --git a/Cargo.lock b/Cargo.lock index 2b6b11f..04b17bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1926,7 +1926,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "oolatoocs" -version = "4.5.0" +version = "4.5.1" dependencies = [ "atrium-api", "bsky-sdk", diff --git a/Cargo.toml b/Cargo.toml index 012d14b..8041b46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oolatoocs" -version = "4.5.0" +version = "4.5.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/bsky.rs b/src/bsky.rs index a849989..739d55c 100644 --- a/src/bsky.rs +++ b/src/bsky.rs @@ -148,9 +148,96 @@ async fn get_record( Ok(record) } +/// Generate a Union of embed records to be built-in into records +/// In case an embed cannot be uploaded/created, this calling function silently gets Option instead +/// of failing +pub async fn generate_embed_records( + config: &BlueskyConfig, + bsky: &BskyAgent, + qid: Option<&str>, + media_attach: &[Attachment], + card: &Option, +) -> Result< + Option>, + Box, +> { + // handle quote if any + let quote_embed = match qid { + Some(q) => generate_quote_records(config, q).await.ok(), + _ => None, + }; + + // handle medias if any + let media_embed = if media_attach.len() > usize::from(0u8) { + let image_media_attach: Vec<_> = media_attach + .iter() + .filter(|x| x.r#type == AttachmentType::Image) + .cloned() + .collect(); + let video_media_attach: Vec<_> = media_attach + .iter() + .filter(|x| x.r#type == AttachmentType::Video || x.r#type == AttachmentType::Gifv) + .cloned() + .collect(); + + if !video_media_attach.is_empty() { + generate_video_record(bsky, video_media_attach).await.ok() + } else if !image_media_attach.is_empty() { + generate_images_records(bsky, image_media_attach).await.ok() + } else { + return Err(OolatoocsError::new("A media attached is not an image nor a video").into()); + } + } else { + None + }; + + // handle webcard if any + let webcard_embed = match card { + Some(t) => generate_webcard_records(bsky, t).await.ok(), + None => None, + }; + + if let Some(q) = quote_embed { + if let Some(m) = media_embed { + let medias_mapped = match m { + 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), + _ => return Err(OolatoocsError::new("Something went terribly wrong when trying to add image/video to quote record: can’t decapsulate media").into()), + }; + let quote_mapped = match q { + atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedRecordMain( + a, + ) => a, + _ => return Err(OolatoocsError::new("Something went terribly wrong when trying to add image/video to quote record: can’t decapsulate quote").into()), + }; + 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 { + Some(atrium_api::types::Union::Refs(q)) + } + } 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 + }; + + Ok(None) +} + /// Generate an quote embed record /// it is encapsulated in Option to prevent this function from failing -pub async fn generate_quote_records( +async fn generate_quote_records( config: &BlueskyConfig, quote_id: &str, ) -> Result> { @@ -173,7 +260,7 @@ pub async fn generate_quote_records( /// 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_webcard_records( +async fn generate_webcard_records( bsky: &BskyAgent, card: &Card, ) -> Result> { @@ -199,47 +286,12 @@ pub async fn generate_webcard_records( ) } -/// Generate an array of Bsky media records -/// As Bsky does not support multiple video in a record or mix of video and images, video has the -/// highest priority -pub async fn generate_media_records( +/// Generate an array of Bsky image media records +async fn generate_images_records( bsky: &BskyAgent, - media_attach: &[Attachment], + media_attach: Vec, ) -> Result> { - let image_media_attach: Vec<_> = media_attach - .iter() - .filter(|x| x.r#type == AttachmentType::Image) - .cloned() - .collect(); - let video_media_attach: Vec<_> = media_attach - .iter() - .filter(|x| x.r#type == AttachmentType::Video || x.r#type == AttachmentType::Gifv) - .cloned() - .collect(); - - // Bsky only tasks 1 video per post, so we’ll try to treat that first and exit - 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?; - - return Ok( - atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedVideoMain(Box::new( - atrium_api::app::bsky::embed::video::MainData { - alt: media.description.clone(), - aspect_ratio: convert_aspect_ratio( - &media.meta.as_ref().and_then(|m| m.original.clone()), - ), - captions: None, - video: blob.data.blob, - } - .into(), - )), - ); - } - - // It wasn’t a video, then it’s an image or a gallery of 4 images - let mut stream = stream::iter(image_media_attach) + let mut stream = stream::iter(media_attach) .map(|media| { let bsky = bsky.clone(); tokio::task::spawn(async move { @@ -281,6 +333,30 @@ pub async fn generate_media_records( Err(OolatoocsError::new("Cannot embed media").into()) } +/// Generate a video Bsky media record +async fn generate_video_record( + bsky: &BskyAgent, + media_attach: Vec, +) -> Result> { + // treat only the very first video, ignore the rest + let media = &media_attach[0]; + let blob = upload_media(false, bsky, &media.url).await?; + + Ok( + atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedVideoMain(Box::new( + atrium_api::app::bsky::embed::video::MainData { + alt: media.description.clone(), + aspect_ratio: convert_aspect_ratio( + &media.meta.as_ref().and_then(|m| m.original.clone()), + ), + captions: None, + video: blob.data.blob, + } + .into(), + )), + ) +} + async fn upload_media( is_image: bool, bsky: &BskyAgent, diff --git a/src/lib.rs b/src/lib.rs index e22bbc2..4806a7a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,10 +18,7 @@ mod utils; use utils::{generate_multi_tweets, strip_everything}; mod bsky; -use bsky::{ - build_post_record, generate_media_records, generate_quote_records, generate_webcard_records, - get_session, BskyReply, -}; +use bsky::{build_post_record, generate_embed_records, get_session, BskyReply}; use rusqlite::Connection; @@ -161,69 +158,24 @@ pub async fn run(config: &Config) { }); }; - // 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, - } - } + // get quote_id if any + let quote_id = match toot.reblog { + Some(r) => match read_state(&conn, Some(r.id.parse::().unwrap())) { + Ok(q) => q.map(|x| x.record_uri.to_owned()), + _ => None, + }, 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 - }; + let record_embed = generate_embed_records( + &config.bluesky, + &bluesky, + quote_id.as_deref(), + &toot.media_attachments, + &toot.card, + ) + .await + .unwrap_or_else(|e| panic!("Cannot embed record for {}: {}", &toot.id, e)); // posts corresponding tweet let record = build_post_record(