small bsky embedder @ boobsky.app - kinda mid but works - mirror of git.fomx.gay/rooot/embedthing

feat: initial bsky support :3

Signed-off-by: rooot <hey@rooot.gay>

+276 -20
+3
Cargo.lock
··· 419 419 "ipld-core", 420 420 "lazy_static", 421 421 "rocket", 422 + "serde", 423 + "serde_json", 424 + "thiserror", 422 425 ] 423 426 424 427 [[package]]
+3
Cargo.toml
··· 10 10 dotenvy = "0.15.7" 11 11 bsky-sdk = "0.1.11" 12 12 ipld-core = "*" 13 + thiserror = "1.0.65" 14 + serde = "1.0.213" 15 + serde_json = "1.0.132"
+126 -20
src/bsky.rs
··· 1 + use crate::meow::{EmbedEmbed, EmbedThingy, GenericError, MissingElementError}; 1 2 use crate::ManagedBskyAgent; 3 + use bsky_sdk::api::app::bsky::embed::images::ImageData; 4 + use bsky_sdk::api::app::bsky::embed::video; 5 + use bsky_sdk::api::types::TypedBlobRef::Blob; 2 6 use bsky_sdk::api::types::Union::Refs; 3 - use bsky_sdk::api::types::{DataModel, Object, Unknown}; 7 + use bsky_sdk::api::types::{DataModel, TryFromUnknown, Unknown}; 4 8 use ipld_core::ipld::Ipld; 5 - use rocket::serde::Deserializer; 6 9 use rocket::{get, State}; 7 10 use std::ops::Deref; 8 11 ··· 11 14 agent: &State<ManagedBskyAgent>, 12 15 name: &str, 13 16 post: &str, 14 - ) -> String { 17 + ) -> Result<EmbedThingy, GenericError> { 15 18 let atproto_uri = format!("at://{}/app.bsky.feed.post/{}", name, post); 16 - let sex = agent 19 + let post_thread = agent 17 20 .agent 18 21 .api 19 22 .app ··· 27 30 } 28 31 .into(), 29 32 ) 30 - .await 31 - .unwrap(); 32 - 33 - let post = sex.data; 33 + .await? 34 + .data; 34 35 35 - if let Refs(thread) = post.thread { 36 + if let Refs(thread) = post_thread.thread { 36 37 match thread { 37 38 bsky_sdk::api::app::bsky::feed::get_post_thread::OutputThreadRefs::AppBskyFeedDefsThreadViewPost(thread) => { 38 39 // println!("{:#?}", &thread.post); 40 + let post_uri = thread.post.uri.clone(); 41 + 42 + let did_start_index = post_uri.find("did:plc:"); 43 + let did_start = match did_start_index { 44 + Some(index) => index, 45 + None => { 46 + return Err(GenericError::DIDNotFound); 47 + } 48 + }; 49 + let did_end_index = match post_uri[did_start..].find("/") { 50 + Some(index) => index, 51 + None => { 52 + return Err(GenericError::DIDNotFound); 53 + } 54 + }; 55 + 56 + let did = &post_uri[did_start..did_start + did_end_index]; 57 + println!("did: {:?}", did); 39 58 40 59 let data = thread.post.data.clone(); 41 60 42 61 if let Unknown::Object(record) = data.record { 43 - if ipld_to_string(record.get_key_value("$type").unwrap().1.deref()) == Ok("app.bsky.feed.post".to_string()) { 62 + if ipld_to_string(record.get_key_value("$type").ok_or(MissingElementError("$type".into()))?.1.deref()) == Ok("app.bsky.feed.post".to_string()) { 44 63 println!("this is a post"); 45 64 46 - let text = ipld_to_string(record.get_key_value("text").unwrap().1.deref()); 65 + let text = ipld_to_string(record.get_key_value("text").ok_or(MissingElementError("text".into()))?.1.deref()); 47 66 println!("post text: {:?}", text); 48 67 49 - let langs = record.get_key_value("langs").unwrap().1.deref(); 50 - println!("post langs: {:?}", langs); 68 + let embeds = record.get_key_value("embed").ok_or(MissingElementError("embed".into()))?.1; 69 + //println!("post embed(s): {:?}", embeds); 51 70 52 - println!("whole post thing {:#?}", record) 53 - } 71 + // multiple? images 72 + let images = match embeds.get("images") { 73 + Ok(Some(Ipld::List(images))) => { 74 + let mut vec = Vec::new(); 75 + for image in images { 76 + vec.push(image.clone()); 77 + } 78 + vec 79 + }, 80 + _ => { 81 + Vec::<Ipld>::new() 82 + } 83 + }; 54 84 55 - // println!("{:?}", a); 85 + let unknown_gay_sex = Unknown::Other(embeds.clone()); 86 + let mut blobs = Vec::new(); 87 + 88 + //dbg!(&unknown_gay_sex); 89 + if let Ipld::Map(map) = embeds.clone().deref() { 90 + //println!("map: {:?}", map); 91 + if ipld_to_string(map.get_key_value("$type").ok_or(MissingElementError("$type".into()))?.1) == Ok("app.bsky.embed.video".to_string()) { 92 + // bug: this crashes at runtime. error handling my asshole 93 + // this is also why we need those 2 checks above 94 + let bite_me = video::Main::try_from_unknown(unknown_gay_sex.clone()); 95 + //dbg!("sex", &bite_me); 96 + 97 + let video_blob = match bite_me { 98 + Ok(video) => { 99 + match &video.clone().video { 100 + bsky_sdk::api::types::BlobRef::Typed(blob) => { 101 + println!("video blob: {:?}", blob); 102 + match blob { 103 + Blob(blob) => Some(blob.clone()), 104 + } 105 + }, 106 + bsky_sdk::api::types::BlobRef::Untyped(_) => { 107 + println!("image is not a blob wtf"); 108 + None 109 + } 110 + } 111 + }, 112 + Err(_) => { 113 + None 114 + } 115 + }; 116 + if let Some(blob) = video_blob { 117 + blobs.push(blob); 118 + } 119 + } 120 + } 121 + 122 + for image in images { 123 + let image_embed = ImageData::try_from_unknown(Unknown::Other(DataModel::try_from(image)?)); 124 + 125 + if image_embed.is_err() { 126 + println!("image is not an image wtf"); 127 + } 128 + 129 + let image_embed = image_embed?; 130 + 131 + let blob = match image_embed.image { 132 + bsky_sdk::api::types::BlobRef::Typed(blob) => { 133 + //println!("image blob: {:?}", blob); 134 + match blob { 135 + Blob(blob) => Some(blob), 136 + } 137 + }, 138 + bsky_sdk::api::types::BlobRef::Untyped(_) => { 139 + None 140 + } 141 + }; 142 + if let Some(blob) = blob { 143 + blobs.push(blob); 144 + } 145 + }; 146 + 147 + let mut embed_embeds = Vec::new(); 148 + for blob in blobs { 149 + println!("blob: {:?}", blob); 150 + // todo: get blob url? - might have to cook our own "API" for this as bsky's require auth 151 + // for images we might get away with a simple CDN url, although those have bad quality 152 + } 153 + 154 + println!("embed embeds aaa: {:?}", &embed_embeds); 155 + 156 + return Ok(EmbedThingy { 157 + author: None, 158 + text: text.ok(), 159 + embeds: embed_embeds, 160 + }); 161 + } 56 162 } else { 57 - return "what da hell this shit aint havin a record !!".to_string(); 163 + println!("this is not a post"); 58 164 } 59 165 } 60 166 bsky_sdk::api::app::bsky::feed::get_post_thread::OutputThreadRefs::AppBskyFeedDefsNotFoundPost(_) => { 61 - return "not found".to_string(); 167 + return Err(GenericError::TooTired); 62 168 } 63 169 bsky_sdk::api::app::bsky::feed::get_post_thread::OutputThreadRefs::AppBskyFeedDefsBlockedPost(_) => { 64 - return format!("whoops, {} blocked us!", name); 170 + return Err(GenericError::BlockedBy(name.to_string())); 65 171 } 66 172 } 67 173 } 68 174 69 - format!("bsky: {}", atproto_uri) 175 + Err(GenericError::TooTired) 70 176 } 71 177 72 178 fn ipld_to_string(ipld: &Ipld) -> Result<String, ()> {
+1
src/main.rs
··· 1 1 mod bsky; 2 + mod meow; 2 3 mod srv; 3 4 4 5 use bsky_sdk::agent::config::FileStore;
+143
src/meow.rs
··· 1 + use rocket::http::{ContentType, Status}; 2 + use rocket::response::Responder; 3 + use rocket::{response, Request, Response}; 4 + use serde::ser::SerializeMap; 5 + use serde::Serialize; 6 + use thiserror::Error; 7 + 8 + #[derive(Debug)] 9 + pub struct EmbedAuthor { 10 + pub name: String, 11 + pub handle: String, 12 + pub profile_url: String, 13 + pub avatar_url: String, 14 + } 15 + 16 + // banger name 17 + #[derive(Debug)] 18 + pub enum EmbedEmbed { 19 + Video(String), 20 + Image(String), 21 + } 22 + 23 + #[derive(Debug)] 24 + pub struct EmbedThingy { 25 + pub author: Option<EmbedAuthor>, 26 + pub text: Option<String>, 27 + pub embeds: Vec<EmbedEmbed>, 28 + } 29 + 30 + impl<'r, 'o: 'r> Responder<'r, 'o> for EmbedThingy { 31 + fn respond_to(self, _req: &'r Request<'_>) -> response::Result<'o> { 32 + let mut html = String::new(); 33 + html.push_str("<!DOCTYPE html>\n<html>\n<head>\n"); 34 + html.push_str("<meta charset=\"UTF-8\">\n"); 35 + html.push_str("<title>boobs</title>\n"); 36 + html.push_str("<meta property=\"og:title\" content=\"boobs\">\n"); 37 + html.push_str("<meta property=\"theme-color\" content=\"#f5c2e7\">\n"); 38 + 39 + if !self.embeds.is_empty() { 40 + for embed in self.embeds { 41 + match embed { 42 + EmbedEmbed::Image(url) => { 43 + println!("hehe image ! {}", url); 44 + html.push_str(&format!(r#"<meta property="og:image" content="{}">"#, url)); 45 + html.push('\n'); 46 + } 47 + EmbedEmbed::Video(url) => { 48 + html.push_str(r#"<meta property="og:type" content="video.other">"#); 49 + html.push('\n'); 50 + html.push_str(&format!(r#"<meta property="og:video" content="{}">"#, url)); 51 + html.push('\n'); 52 + html.push_str(&format!( 53 + r#"<meta property="og:video:url" content="{}">"#, 54 + url 55 + )); 56 + html.push('\n'); 57 + html.push_str(&format!( 58 + r#"<meta property="og:video:secure_url" content="{}">"#, 59 + url 60 + )); 61 + } 62 + } 63 + } 64 + println!("sex"); 65 + } 66 + /* 67 + <meta property="og:type" content="video.other"> 68 + <meta property="og:video" content=""> 69 + <meta property="og:video:url" content=""> 70 + <meta property="og:video:secure_url" content=""> 71 + */ 72 + 73 + html.push_str("</head>\n<body>meow</body>\n</html>"); 74 + Ok(Response::build() 75 + .header(ContentType::HTML) 76 + .status(Status::Ok) 77 + .streamed_body(std::io::Cursor::new(html)) 78 + .finalize()) 79 + } 80 + } 81 + 82 + #[derive(Error, Debug)] 83 + #[error("{0}")] 84 + pub struct MissingElementError(pub String); 85 + 86 + #[derive(Error, Debug)] 87 + #[error(transparent)] 88 + pub enum GenericError { 89 + #[error("Missing element or invalid selector: {0}")] 90 + MissingElement(#[from] MissingElementError), 91 + 92 + #[error("DID not found")] 93 + DIDNotFound, 94 + 95 + #[error("too tired to make an actuall error type")] 96 + TooTired, 97 + 98 + #[error("blocked by {0}")] 99 + BlockedBy(String), 100 + 101 + #[error("atrium error: {0}")] 102 + Atrium(#[from] bsky_sdk::api::error::Error), 103 + 104 + #[error("get post thread error: {0}")] 105 + GetPostThread( 106 + #[from] bsky_sdk::api::xrpc::Error<bsky_sdk::api::app::bsky::feed::get_post_thread::Error>, 107 + ), 108 + 109 + #[error("get blob error: {0}")] 110 + GetBlob(#[from] bsky_sdk::api::xrpc::Error<bsky_sdk::api::com::atproto::sync::get_blob::Error>), 111 + 112 + #[error("invalid cid")] 113 + InvalidCID(#[from] ipld_core::cid::Error), 114 + } 115 + 116 + impl Serialize for GenericError { 117 + fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { 118 + let error_message = match self { 119 + GenericError::MissingElement(e) => e.to_string(), 120 + GenericError::Atrium(e) => e.to_string(), 121 + GenericError::GetPostThread(e) => e.to_string(), 122 + GenericError::DIDNotFound => "DID not found".to_string(), 123 + GenericError::TooTired => self.to_string(), 124 + GenericError::BlockedBy(_) => self.to_string(), 125 + GenericError::GetBlob(e) => e.to_string(), 126 + GenericError::InvalidCID(e) => e.to_string(), 127 + }; 128 + let mut map = serializer.serialize_map(Some(2))?; 129 + map.serialize_entry("error", &format!("{}: {}", self, &error_message))?; 130 + map.serialize_entry("status", &"error")?; 131 + map.end() 132 + } 133 + } 134 + 135 + impl<'r, 'o: 'r> Responder<'r, 'o> for GenericError { 136 + fn respond_to(self, _req: &'r Request<'_>) -> response::Result<'o> { 137 + Ok(Response::build() 138 + .header(ContentType::JSON) 139 + .status(Status::InternalServerError) 140 + .streamed_body(std::io::Cursor::new(serde_json::to_string(&self).unwrap())) 141 + .finalize()) 142 + } 143 + }