2 Commits

Author SHA1 Message Date
VC
498095d3a8 Merge branch '17-better-handling-of-embedded-records' into 'main'
♻️: better handling of embedded records

Closes #17

See merge request veretcle/oolatoocs!41
2025-12-02 14:03:04 +00:00
VC
5ee64014eb ♻️: better handling of embedded records 2025-12-02 13:37:53 +01:00
4 changed files with 135 additions and 107 deletions

2
Cargo.lock generated
View File

@@ -1926,7 +1926,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]] [[package]]
name = "oolatoocs" name = "oolatoocs"
version = "4.5.0" version = "4.5.1"
dependencies = [ dependencies = [
"atrium-api", "atrium-api",
"bsky-sdk", "bsky-sdk",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "oolatoocs" name = "oolatoocs"
version = "4.5.0" version = "4.5.1"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -148,9 +148,96 @@ async fn get_record(
Ok(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<Card>,
) -> Result<
Option<atrium_api::types::Union<atrium_api::app::bsky::feed::post::RecordEmbedRefs>>,
Box<dyn Error + Send + Sync>,
> {
// 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: cant 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: cant 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 /// Generate an quote embed record
/// it is encapsulated in Option to prevent this function from failing /// it is encapsulated in Option to prevent this function from failing
pub async fn generate_quote_records( async fn generate_quote_records(
config: &BlueskyConfig, config: &BlueskyConfig,
quote_id: &str, quote_id: &str,
) -> Result<atrium_api::app::bsky::feed::post::RecordEmbedRefs, Box<dyn Error>> { ) -> Result<atrium_api::app::bsky::feed::post::RecordEmbedRefs, Box<dyn Error>> {
@@ -173,7 +260,7 @@ pub async fn generate_quote_records(
/// Generate an embed webcard 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 /// 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, bsky: &BskyAgent,
card: &Card, card: &Card,
) -> Result<atrium_api::app::bsky::feed::post::RecordEmbedRefs, Box<dyn Error + Send + Sync>> { ) -> Result<atrium_api::app::bsky::feed::post::RecordEmbedRefs, Box<dyn Error + Send + Sync>> {
@@ -199,47 +286,12 @@ pub async fn generate_webcard_records(
) )
} }
/// Generate an array of Bsky media records /// Generate an array of Bsky image media records
/// As Bsky does not support multiple video in a record or mix of video and images, video has the async fn generate_images_records(
/// highest priority
pub async fn generate_media_records(
bsky: &BskyAgent, bsky: &BskyAgent,
media_attach: &[Attachment], media_attach: Vec<Attachment>,
) -> Result<atrium_api::app::bsky::feed::post::RecordEmbedRefs, Box<dyn Error + Send + Sync>> { ) -> Result<atrium_api::app::bsky::feed::post::RecordEmbedRefs, Box<dyn Error + Send + Sync>> {
let image_media_attach: Vec<_> = media_attach let mut stream = stream::iter(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 well 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 wasnt a video, then its an image or a gallery of 4 images
let mut stream = stream::iter(image_media_attach)
.map(|media| { .map(|media| {
let bsky = bsky.clone(); let bsky = bsky.clone();
tokio::task::spawn(async move { tokio::task::spawn(async move {
@@ -281,6 +333,30 @@ pub async fn generate_media_records(
Err(OolatoocsError::new("Cannot embed media").into()) Err(OolatoocsError::new("Cannot embed media").into())
} }
/// Generate a video Bsky media record
async fn generate_video_record(
bsky: &BskyAgent,
media_attach: Vec<Attachment>,
) -> Result<atrium_api::app::bsky::feed::post::RecordEmbedRefs, Box<dyn Error + Send + Sync>> {
// 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( async fn upload_media(
is_image: bool, is_image: bool,
bsky: &BskyAgent, bsky: &BskyAgent,

View File

@@ -18,10 +18,7 @@ mod utils;
use utils::{generate_multi_tweets, strip_everything}; use utils::{generate_multi_tweets, strip_everything};
mod bsky; mod bsky;
use bsky::{ use bsky::{build_post_record, generate_embed_records, get_session, BskyReply};
build_post_record, generate_media_records, generate_quote_records, generate_webcard_records,
get_session, BskyReply,
};
use rusqlite::Connection; use rusqlite::Connection;
@@ -161,69 +158,24 @@ pub async fn run(config: &Config) {
}); });
}; };
// handle quote if any // get quote_id if any
let quote_embed = match toot.reblog { let quote_id = match toot.reblog {
Some(r) => { Some(r) => match read_state(&conn, Some(r.id.parse::<u64>().unwrap())) {
let quote_record = read_state(&conn, Some(r.id.parse::<u64>().unwrap())); Ok(q) => q.map(|x| x.record_uri.to_owned()),
match quote_record {
Ok(Some(q)) => generate_quote_records(&config.bluesky, &q.record_uri)
.await
.ok(),
_ => None, _ => None,
} },
}
None => None, None => None,
}; };
// handle medias if any let record_embed = generate_embed_records(
let media_embed = if toot.media_attachments.len() > usize::from(0u8) { &config.bluesky,
generate_media_records(&bluesky, &toot.media_attachments) &bluesky,
quote_id.as_deref(),
&toot.media_attachments,
&toot.card,
)
.await .await
.ok() .unwrap_or_else(|e| panic!("Cannot embed record for {}: {}", &toot.id, e));
} 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
};
// posts corresponding tweet // posts corresponding tweet
let record = build_post_record( let record = build_post_record(