: add bluesky support

This commit is contained in:
VC
2024-09-26 16:32:39 +02:00
parent f7e2aafa7b
commit ac8af5ce95
8 changed files with 1090 additions and 245 deletions

206
src/bsky.rs Normal file
View File

@@ -0,0 +1,206 @@
use crate::config::BlueskyConfig;
use atrium_api::{
app::bsky::feed::post::RecordData, com::atproto::repo::upload_blob::Output,
types::string::Datetime, types::string::Language,
};
use bsky_sdk::{rich_text::RichText, BskyAgent};
use log::error;
use megalodon::entities::attachment::{Attachment, AttachmentType};
use regex::Regex;
use std::error::Error;
/// Intermediary struct to deal with replies more easily
#[derive(Debug)]
pub struct BskyReply {
pub record_uri: String,
pub root_record_uri: String,
}
pub async fn get_session(user: &str, pass: &str) -> Result<BskyAgent, Box<dyn Error>> {
let agent = BskyAgent::builder().build().await?;
agent.login(user, pass).await?;
Ok(agent)
}
pub async fn build_post_record(
config: &BlueskyConfig,
text: &str,
language: &Option<String>,
embed: Option<atrium_api::types::Union<atrium_api::app::bsky::feed::post::RecordEmbedRefs>>,
reply_to: &Option<BskyReply>,
) -> Result<RecordData, Box<dyn Error>> {
let mut rt = RichText::new_with_detect_facets(text).await?;
let re = Regex::new(r#"(https?://)(\S{1,29})(\S*)"#).unwrap();
while let Some(found) = re.captures(&rt.text.clone()) {
if let Some(group) = found.get(3) {
rt.delete(group.start(), group.start() + group.len());
}
if let Some(group) = found.get(1) {
rt.delete(group.start(), group.start() + group.len());
}
}
let langs = language.clone().map(|s| vec![Language::new(s).unwrap()]);
let reply = if let Some(x) = reply_to {
let root_record = get_record(&config.handle, &rkey(&x.root_record_uri)).await?;
let parent_record = get_record(&config.handle, &rkey(&x.record_uri)).await?;
Some(
atrium_api::app::bsky::feed::post::ReplyRefData {
parent: atrium_api::com::atproto::repo::strong_ref::MainData {
cid: parent_record.data.cid.unwrap(),
uri: parent_record.data.uri.to_owned(),
}
.into(),
root: atrium_api::com::atproto::repo::strong_ref::MainData {
cid: root_record.data.cid.unwrap(),
uri: root_record.data.uri.to_owned(),
}
.into(),
}
.into(),
)
} else {
None
};
Ok(RecordData {
created_at: Datetime::now(),
embed,
entities: None,
facets: rt.facets,
labels: None,
langs,
reply,
tags: None,
text: rt.text,
})
}
async fn get_record(
config: &str,
rkey: &str,
) -> Result<
atrium_api::types::Object<atrium_api::com::atproto::repo::get_record::OutputData>,
Box<dyn Error>,
> {
let bsky = BskyAgent::builder().build().await?;
let record = bsky
.api
.com
.atproto
.repo
.get_record(
atrium_api::com::atproto::repo::get_record::ParametersData {
cid: None,
collection: atrium_api::types::string::Nsid::new("app.bsky.feed.post".to_string())?,
repo: atrium_api::types::string::Handle::new(config.to_string())?.into(),
rkey: rkey.to_string(),
}
.into(),
)
.await?;
Ok(record)
}
// its ugly af but it gets the job done for now
pub async fn generate_media_records(
bsky: &BskyAgent,
media_attach: &[Attachment],
) -> Option<atrium_api::types::Union<atrium_api::app::bsky::feed::post::RecordEmbedRefs>> {
let mut embed: Option<
atrium_api::types::Union<atrium_api::app::bsky::feed::post::RecordEmbedRefs>,
> = None;
let mut images = Vec::new();
let mut videos: Vec<atrium_api::app::bsky::embed::video::MainData> = Vec::new();
for media in media_attach.iter() {
let blob = upload_media(bsky, &media.url).await.unwrap();
match media.r#type {
AttachmentType::Image => {
images.push(
atrium_api::app::bsky::embed::images::ImageData {
alt: media
.description
.clone()
.map_or("".to_string(), |v| v.to_owned()),
aspect_ratio: None,
image: blob.data.blob,
}
.into(),
);
}
AttachmentType::Gifv | AttachmentType::Video => {
videos.push(atrium_api::app::bsky::embed::video::MainData {
alt: media.description.clone(),
aspect_ratio: None,
captions: None,
video: blob.data.blob,
});
}
_ => {
error!("Not an image, not a video, what happened here?");
}
}
}
if !images.is_empty() {
embed = Some(atrium_api::types::Union::Refs(
atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedImagesMain(Box::new(
atrium_api::app::bsky::embed::images::MainData { images }.into(),
)),
));
}
// if a video has been uploaded, it takes priority as you can only have 1 video per post
if !videos.is_empty() {
embed = Some(atrium_api::types::Union::Refs(
atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedVideoMain(Box::new(
videos[0].clone().into(),
)),
))
}
embed
}
async fn upload_media(bsky: &BskyAgent, u: &str) -> Result<Output, Box<dyn Error>> {
let dl = reqwest::get(u).await?;
let bytes = dl.bytes().await?;
let record = bsky.api.com.atproto.repo.upload_blob(bytes.into()).await?;
Ok(record)
}
fn rkey(record_id: &str) -> String {
record_id.split('/').nth(4).unwrap().to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_build_post_record() {
let text = "@factornews@piaille.fr Retrouvez-nous ici https://www.nintendojo.fr/articles/editos/le-mod-renovation-de-8bitdo-pour-manette-n64 et là https://www.nintendojo.fr/articles/analyses/vite-vu/vite-vu-morbid-the-lords-of-ire";
let expected_text = "@factornews@piaille.fr Retrouvez-nous ici www.nintendojo.fr/articles/ed et là www.nintendojo.fr/articles/an";
let bsky_conf = BlueskyConfig {
handle: "tamerelol.bsky.social".to_string(),
password: "dtc".to_string(),
};
let created_record_data = build_post_record(&bsky_conf, text, &None, None, &None)
.await
.unwrap();
assert_eq!(expected_text, &created_record_data.text);
}
}