tuitbot_core/x_api/auth/
mod.rs1use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12
13use crate::error::XApiError;
14
15mod oauth;
16mod refresh;
17mod token;
18
19pub use oauth::{authenticate_callback, authenticate_manual};
20pub use refresh::TokenRefreshResponse;
21pub use token::TokenManager;
22
23pub const AUTH_URL: &str = "https://x.com/i/oauth2/authorize";
27
28pub const TOKEN_URL: &str = "https://api.x.com/2/oauth2/token";
30
31pub const REFRESH_WINDOW_SECS: i64 = 300;
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct Tokens {
39 pub access_token: String,
41 pub refresh_token: String,
43 pub expires_at: DateTime<Utc>,
45 #[serde(default)]
47 pub scopes: Vec<String>,
48}
49
50pub fn save_tokens(tokens: &Tokens, path: &std::path::Path) -> Result<(), String> {
54 if let Some(parent) = path.parent() {
55 std::fs::create_dir_all(parent).map_err(|e| format!("Failed to create directory: {e}"))?;
56 }
57
58 let json = serde_json::to_string_pretty(tokens)
59 .map_err(|e| format!("Failed to serialize tokens: {e}"))?;
60
61 #[cfg(unix)]
63 {
64 use std::io::Write;
65 use std::os::unix::fs::OpenOptionsExt;
66 let mut file = std::fs::OpenOptions::new()
67 .write(true)
68 .create(true)
69 .truncate(true)
70 .mode(0o600)
71 .open(path)
72 .map_err(|e| format!("Failed to create token file: {e}"))?;
73 file.write_all(json.as_bytes())
74 .map_err(|e| format!("Failed to write tokens: {e}"))?;
75 }
76
77 #[cfg(not(unix))]
78 {
79 std::fs::write(path, &json).map_err(|e| format!("Failed to write tokens: {e}"))?;
80 tracing::warn!("Cannot set restrictive file permissions on non-Unix platform");
81 }
82
83 Ok(())
84}
85
86pub fn load_tokens(path: &std::path::Path) -> Result<Option<Tokens>, XApiError> {
88 match std::fs::read_to_string(path) {
89 Ok(contents) => {
90 let tokens: Tokens =
91 serde_json::from_str(&contents).map_err(|e| XApiError::ApiError {
92 status: 0,
93 message: format!("Failed to parse tokens file: {e}"),
94 })?;
95 Ok(Some(tokens))
96 }
97 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
98 Err(e) => Err(XApiError::ApiError {
99 status: 0,
100 message: format!("Failed to read tokens file: {e}"),
101 }),
102 }
103}
104
105#[cfg(test)]
106mod tests;