♻️: better handling of embedded records

This commit is contained in:
VC
2025-12-02 13:37:53 +01:00
parent 639582ba59
commit 5ee64014eb
4 changed files with 135 additions and 107 deletions

2
Cargo.lock generated
View File

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

View File

@@ -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

View File

@@ -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<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
/// 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<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
/// 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<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
/// 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<Attachment>,
) -> Result<atrium_api::app::bsky::feed::post::RecordEmbedRefs, Box<dyn Error + Send + Sync>> {
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 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)
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<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(
is_image: bool,
bsky: &BskyAgent,

View File

@@ -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::<u64>().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::<u64>().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(