scaffold bluesky client
This commit is contained in:
parent
0d931a143e
commit
8512c0e253
6 changed files with 104 additions and 13 deletions
|
@ -15,3 +15,4 @@ anyhow = "1.0.95"
|
||||||
serde = { version = "1.0.216", features = ["derive"] }
|
serde = { version = "1.0.216", features = ["derive"] }
|
||||||
base64 = "0.22.1"
|
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.
|
/// The bluesky bot user's password.
|
||||||
#[arg(short = 'p', long)]
|
#[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 crate::cli::CliArgs;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use infrastructure::RedisService;
|
use infrastructure::RedisService;
|
||||||
|
@ -53,6 +54,9 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||||
warn!("{}", err);
|
warn!("{}", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut bluesky_client =
|
||||||
|
BlueSkyClient::new(&args.bluesky_handle, &args.bluesky_password).await?;
|
||||||
|
|
||||||
// Read from stream
|
// Read from stream
|
||||||
while running.load(Ordering::SeqCst) {
|
while running.load(Ordering::SeqCst) {
|
||||||
match redis_service
|
match redis_service
|
||||||
|
@ -64,9 +68,11 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(data) => {
|
Ok(post) => {
|
||||||
// TODO: Implement
|
let data = ""; // TODO
|
||||||
dbg!(data);
|
if let Err(err) = bluesky_client.post(data).await {
|
||||||
|
error!("failed to post: {post:?} {err}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("error reading stream: {err}")
|
error!("error reading stream: {err}")
|
||||||
|
|
|
@ -14,18 +14,18 @@ struct TokenPayloadInternal {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Token represents a bluesky authentication token.
|
/// Token represents a bluesky authentication token.
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialOrd, PartialEq, Default)]
|
#[derive(Serialize, Deserialize, Debug, PartialOrd, PartialEq)]
|
||||||
struct Token {
|
pub(crate) struct Token {
|
||||||
handle: String,
|
pub handle: String,
|
||||||
#[serde(rename(serialize = "accessJwt", deserialize = "accessJwt"))]
|
#[serde(rename(serialize = "accessJwt", deserialize = "accessJwt"))]
|
||||||
access_jwt: String,
|
pub access_jwt: String,
|
||||||
#[serde(rename(serialize = "refreshJwt", deserialize = "refreshJwt"))]
|
#[serde(rename(serialize = "refreshJwt", deserialize = "refreshJwt"))]
|
||||||
refresh_jwt: String,
|
pub refresh_jwt: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Token {
|
impl Token {
|
||||||
/// Returns true if the token is expired, false otherwise.
|
/// 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 parts: Vec<&str> = self.access_jwt.split('.').collect();
|
||||||
let payload_part = parts.get(1).ok_or(anyhow!("Missing payload from token"))?;
|
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 json_data = serde_json::to_string(&payload)?;
|
||||||
let base64_data = BASE64_STANDARD_NO_PAD.encode(json_data);
|
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);
|
token.access_jwt = format!("eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJFUzI1NksifQ.{}.oWhKfhGWv6omS3oFQ21GX29uzsd5WrfPJyotJMCQ8V44GF1UN2et7sf_JKVB5jkSuJa6kVWERGuKVGgj8AWScA", base64_data);
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
|
@ -91,7 +95,11 @@ mod tests {
|
||||||
let json_data = serde_json::to_string(&payload)?;
|
let json_data = serde_json::to_string(&payload)?;
|
||||||
let base64_data = BASE64_STANDARD_NO_PAD.encode(json_data);
|
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);
|
token.access_jwt = format!("eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJFUzI1NksifQ.{}.oWhKfhGWv6omS3oFQ21GX29uzsd5WrfPJyotJMCQ8V44GF1UN2et7sf_JKVB5jkSuJa6kVWERGuKVGgj8AWScA", base64_data);
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
|
|
Loading…
Reference in a new issue