scaffold bluesky client

This commit is contained in:
Denis-Cosmin NUTIU 2024-12-30 14:07:03 +02:00
parent 0d931a143e
commit 8512c0e253
6 changed files with 104 additions and 13 deletions

View file

@ -15,3 +15,4 @@ anyhow = "1.0.95"
serde = { version = "1.0.216", features = ["derive"] }
base64 = "0.22.1"
serde_json = "1.0.134"
reqwest = { version = "0.12.11", features = ["json"] }

View file

@ -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<Self, anyhow::Error> {
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<T>(&mut self, body: T) -> Result<(), anyhow::Error>
where
T: Into<Body>,
{
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(())
}
}

View file

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

View file

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

View file

@ -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}")

View file

@ -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<bool, anyhow::Error> {
pub fn is_expired(&self) -> Result<bool, anyhow::Error> {
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