use serde::{Deserialize, Serialize};
use std::fmt;
use crate::x_api::scopes::{self, ScopeAnalysis};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApiTier {
Free,
Basic,
Pro,
}
impl fmt::Display for ApiTier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ApiTier::Free => write!(f, "Free"),
ApiTier::Basic => write!(f, "Basic"),
ApiTier::Pro => write!(f, "Pro"),
}
}
}
#[derive(Debug, Clone)]
pub struct TierCapabilities {
pub mentions: bool,
pub discovery: bool,
pub posting: bool,
pub search: bool,
}
impl TierCapabilities {
pub fn for_tier(tier: ApiTier) -> Self {
match tier {
ApiTier::Free => Self {
mentions: false,
discovery: false,
posting: true,
search: false,
},
ApiTier::Basic | ApiTier::Pro => Self {
mentions: true,
discovery: true,
posting: true,
search: true,
},
}
}
pub fn enabled_loop_names(&self) -> Vec<&'static str> {
let mut loops = Vec::new();
if self.mentions {
loops.push("mentions");
}
if self.discovery {
loops.push("discovery");
}
loops.push("content");
loops.push("threads");
loops
}
pub fn format_status(&self) -> String {
let status = |enabled: bool| if enabled { "enabled" } else { "DISABLED" };
format!(
"Mentions: {}, Discovery: {}, Content: enabled, Threads: enabled",
status(self.mentions),
status(self.discovery),
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredTokens {
pub access_token: String,
#[serde(default)]
pub refresh_token: Option<String>,
#[serde(default)]
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
#[serde(default)]
pub scopes: Vec<String>,
}
impl StoredTokens {
pub fn is_expired(&self) -> bool {
match self.expires_at {
Some(expires) => chrono::Utc::now() >= expires,
None => false,
}
}
pub fn time_until_expiry(&self) -> Option<chrono::TimeDelta> {
self.expires_at.map(|expires| expires - chrono::Utc::now())
}
pub fn format_expiry(&self) -> String {
match self.time_until_expiry() {
Some(duration) if duration.num_seconds() > 0 => {
let hours = duration.num_hours();
let minutes = duration.num_minutes() % 60;
if hours > 0 {
format!("{hours}h {minutes}m")
} else {
format!("{minutes}m")
}
}
Some(_) => "expired".to_string(),
None => "no expiry set".to_string(),
}
}
pub fn has_scope_info(&self) -> bool {
!self.scopes.is_empty()
}
pub fn has_scope(&self, scope: &str) -> bool {
self.scopes.iter().any(|granted| granted == scope)
}
pub fn analyze_scopes(&self) -> ScopeAnalysis {
scopes::analyze_scopes(&self.scopes)
}
}
#[derive(Debug, thiserror::Error)]
pub enum StartupError {
#[error("configuration error: {0}")]
Config(String),
#[error("authentication required: run `tuitbot auth` first")]
AuthRequired,
#[error("authentication expired: run `tuitbot auth` to re-authenticate")]
AuthExpired,
#[error("token refresh failed: {0}")]
TokenRefreshFailed(String),
#[error("database error: {0}")]
Database(String),
#[error("LLM provider error: {0}")]
LlmError(String),
#[error("X API error: {0}")]
XApiError(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
Other(String),
}