use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::error::XApiError;
mod oauth;
mod refresh;
mod token;
pub use oauth::{authenticate_callback, authenticate_manual};
pub use refresh::TokenRefreshResponse;
pub use token::TokenManager;
pub const AUTH_URL: &str = "https://x.com/i/oauth2/authorize";
pub const TOKEN_URL: &str = "https://api.x.com/2/oauth2/token";
pub const REFRESH_WINDOW_SECS: i64 = 300;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tokens {
pub access_token: String,
pub refresh_token: String,
pub expires_at: DateTime<Utc>,
#[serde(default)]
pub scopes: Vec<String>,
}
pub fn save_tokens(tokens: &Tokens, path: &std::path::Path) -> Result<(), String> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| format!("Failed to create directory: {e}"))?;
}
let json = serde_json::to_string_pretty(tokens)
.map_err(|e| format!("Failed to serialize tokens: {e}"))?;
#[cfg(unix)]
{
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(path)
.map_err(|e| format!("Failed to create token file: {e}"))?;
file.write_all(json.as_bytes())
.map_err(|e| format!("Failed to write tokens: {e}"))?;
}
#[cfg(not(unix))]
{
std::fs::write(path, &json).map_err(|e| format!("Failed to write tokens: {e}"))?;
tracing::warn!("Cannot set restrictive file permissions on non-Unix platform");
}
Ok(())
}
pub fn load_tokens(path: &std::path::Path) -> Result<Option<Tokens>, XApiError> {
match std::fs::read_to_string(path) {
Ok(contents) => {
let tokens: Tokens =
serde_json::from_str(&contents).map_err(|e| XApiError::ApiError {
status: 0,
message: format!("Failed to parse tokens file: {e}"),
})?;
Ok(Some(tokens))
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(XApiError::ApiError {
status: 0,
message: format!("Failed to read tokens file: {e}"),
}),
}
}
#[cfg(test)]
mod tests;