scaffold bluesky client
This commit is contained in:
parent
0d931a143e
commit
8512c0e253
6 changed files with 104 additions and 13 deletions
|
@ -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"
|
||||
serde_json = "1.0.134"
|
||||
reqwest = { version = "0.12.11", features = ["json"] }
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
7
bot/src/bluesky/atproto.rs
Normal file
7
bot/src/bluesky/atproto.rs
Normal 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,
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue