From 8512c0e2538c962fadd41647ec834ac081f00ed4 Mon Sep 17 00:00:00 2001 From: Denis-Cosmin NUTIU Date: Mon, 30 Dec 2024 14:07:03 +0200 Subject: [PATCH] scaffold bluesky client --- bot/Cargo.toml | 3 +- bot/src/bluesky.rs | 69 ++++++++++++++++++++++++++++++++++++++ bot/src/bluesky/atproto.rs | 7 ++++ bot/src/cli.rs | 2 +- bot/src/main.rs | 12 +++++-- bot/src/token.rs | 24 ++++++++----- 6 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 bot/src/bluesky/atproto.rs diff --git a/bot/Cargo.toml b/bot/Cargo.toml index 601fe32..a7cfea3 100644 --- a/bot/Cargo.toml +++ b/bot/Cargo.toml @@ -14,4 +14,5 @@ env_logger = "0.11.6" anyhow = "1.0.95" serde = { version = "1.0.216", features = ["derive"] } base64 = "0.22.1" -serde_json = "1.0.134" \ No newline at end of file +serde_json = "1.0.134" +reqwest = { version = "0.12.11", features = ["json"] } \ No newline at end of file diff --git a/bot/src/bluesky.rs b/bot/src/bluesky.rs index 8b13789..0a50162 100644 --- a/bot/src/bluesky.rs +++ b/bot/src/bluesky.rs @@ -1 +1,70 @@ +mod atproto; +use crate::bluesky::atproto::ATProtoServerCreateSession; +use crate::token::Token; +use reqwest::Body; + +/// The BlueSky client used to interact with the platform. +pub struct BlueSkyClient { + auth_token: Token, + client: reqwest::Client, +} + +impl BlueSkyClient { + pub async fn new(user_handle: &str, user_password: &str) -> Result { + let client = reqwest::Client::new(); + let server_create_session = ATProtoServerCreateSession { + identifier: user_handle.to_string(), + password: user_password.to_string(), + }; + let body = serde_json::to_string(&server_create_session)?; + let token: Token = client + .post("https://bsky.social/xrpc/com.atproto.repo.createRecord") + .body(body) + .send() + .await? + .json() + .await?; + + Ok(BlueSkyClient { + auth_token: token, + client, + }) + } + + pub async fn post(&mut self, body: T) -> Result<(), anyhow::Error> + where + T: Into, + { + let token_expired = self.auth_token.is_expired()?; + if token_expired { + self.renew_token().await?; + } + self.client + .post("https://bsky.social/xrpc/com.atproto.repo.createRecord") + .header( + "Authorization", + format!("Bearer, {}", self.auth_token.access_jwt), + ) + .body(body) + .send() + .await?; + Ok(()) + } + + async fn renew_token(&mut self) -> Result<(), anyhow::Error> { + let result: Token = self + .client + .post("https://bsky.social/xrpc/com.atproto.server.refreshSession") + .header( + "Authorization", + format!("Bearer, {}", self.auth_token.refresh_jwt), + ) + .send() + .await? + .json() + .await?; + self.auth_token = result; + Ok(()) + } +} diff --git a/bot/src/bluesky/atproto.rs b/bot/src/bluesky/atproto.rs new file mode 100644 index 0000000..7cf1c4c --- /dev/null +++ b/bot/src/bluesky/atproto.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] +pub struct ATProtoServerCreateSession { + pub(crate) identifier: String, + pub(crate) password: String, +} diff --git a/bot/src/cli.rs b/bot/src/cli.rs index 39ebe22..6df8096 100644 --- a/bot/src/cli.rs +++ b/bot/src/cli.rs @@ -25,5 +25,5 @@ pub struct CliArgs { /// The bluesky bot user's password. #[arg(short = 'p', long)] - pub bluesky_password: String + pub bluesky_password: String, } diff --git a/bot/src/main.rs b/bot/src/main.rs index dbfcfff..1ba91d7 100644 --- a/bot/src/main.rs +++ b/bot/src/main.rs @@ -1,3 +1,4 @@ +use crate::bluesky::BlueSkyClient; use crate::cli::CliArgs; use clap::Parser; use infrastructure::RedisService; @@ -53,6 +54,9 @@ async fn main() -> Result<(), anyhow::Error> { warn!("{}", err); } + let mut bluesky_client = + BlueSkyClient::new(&args.bluesky_handle, &args.bluesky_password).await?; + // Read from stream while running.load(Ordering::SeqCst) { match redis_service @@ -64,9 +68,11 @@ async fn main() -> Result<(), anyhow::Error> { ) .await { - Ok(data) => { - // TODO: Implement - dbg!(data); + Ok(post) => { + let data = ""; // TODO + if let Err(err) = bluesky_client.post(data).await { + error!("failed to post: {post:?} {err}") + } } Err(err) => { error!("error reading stream: {err}") diff --git a/bot/src/token.rs b/bot/src/token.rs index ce711d1..7b2a867 100644 --- a/bot/src/token.rs +++ b/bot/src/token.rs @@ -14,18 +14,18 @@ struct TokenPayloadInternal { } /// Token represents a bluesky authentication token. -#[derive(Serialize, Deserialize, Debug, PartialOrd, PartialEq, Default)] -struct Token { - handle: String, +#[derive(Serialize, Deserialize, Debug, PartialOrd, PartialEq)] +pub(crate) struct Token { + pub handle: String, #[serde(rename(serialize = "accessJwt", deserialize = "accessJwt"))] - access_jwt: String, + pub access_jwt: String, #[serde(rename(serialize = "refreshJwt", deserialize = "refreshJwt"))] - refresh_jwt: String, + pub refresh_jwt: String, } impl Token { /// Returns true if the token is expired, false otherwise. - fn is_expired(&self) -> Result { + pub fn is_expired(&self) -> Result { let parts: Vec<&str> = self.access_jwt.split('.').collect(); let payload_part = parts.get(1).ok_or(anyhow!("Missing payload from token"))?; @@ -66,7 +66,11 @@ mod tests { let json_data = serde_json::to_string(&payload)?; let base64_data = BASE64_STANDARD_NO_PAD.encode(json_data); - let mut token = Token::default(); + let mut token = Token { + handle: "".to_string(), + access_jwt: "".to_string(), + refresh_jwt: "".to_string(), + }; token.access_jwt = format!("eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJFUzI1NksifQ.{}.oWhKfhGWv6omS3oFQ21GX29uzsd5WrfPJyotJMCQ8V44GF1UN2et7sf_JKVB5jkSuJa6kVWERGuKVGgj8AWScA", base64_data); // Test @@ -91,7 +95,11 @@ mod tests { let json_data = serde_json::to_string(&payload)?; let base64_data = BASE64_STANDARD_NO_PAD.encode(json_data); - let mut token = Token::default(); + let mut token = Token { + handle: "".to_string(), + access_jwt: "".to_string(), + refresh_jwt: "".to_string(), + }; token.access_jwt = format!("eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJFUzI1NksifQ.{}.oWhKfhGWv6omS3oFQ21GX29uzsd5WrfPJyotJMCQ8V44GF1UN2et7sf_JKVB5jkSuJa6kVWERGuKVGgj8AWScA", base64_data); // Test