use super::{CURRENT_VERSION, https_agent};
pub(super) fn github_token() -> Option<String> {
["GITHUB_TOKEN", "GH_TOKEN", "LEAN_CTX_GITHUB_TOKEN"]
.iter()
.filter_map(|name| std::env::var(name).ok())
.map(|value| value.trim().to_string())
.find(|value| !value.is_empty())
}
pub(super) fn rate_limit_message(reset: Option<i64>, authenticated: bool, now: i64) -> String {
let when = reset
.and_then(|seconds| chrono::DateTime::from_timestamp(seconds, 0))
.map(|at| {
let minutes = ((at.timestamp() - now).max(0) + 59) / 60;
format!(
"; it resets at {} UTC (in {minutes} min)",
at.format("%H:%M")
)
})
.unwrap_or_default();
if authenticated {
format!("GitHub API rate limit reached for your token{when}.")
} else {
format!(
"GitHub API rate limit reached (60 requests per hour per IP address without a token){when}. \
Set GITHUB_TOKEN or GH_TOKEN to a GitHub token (no scopes needed) to raise it to 5000."
)
}
}
pub(crate) fn github_api_json(url: &str) -> Result<serde_json::Value, String> {
if !url.starts_with("https://api.github.com/") {
return Err(format!("refusing non-GitHub API URL: {url}"));
}
let token = github_token();
let mut request = https_agent()
.get(url)
.config()
.http_status_as_error(false)
.max_redirects(0)
.build()
.header("User-Agent", &format!("lean-ctx/{CURRENT_VERSION}"))
.header("Accept", "application/vnd.github.v3+json");
if let Some(token) = &token {
request = request.header("Authorization", &format!("Bearer {token}"));
}
let response = request.call().map_err(|e| e.to_string())?;
let status = response.status().as_u16();
let header = |name: &str| {
response
.headers()
.get(name)
.and_then(|value| value.to_str().ok())
.map(str::to_string)
};
let remaining = header("x-ratelimit-remaining");
let reset = header("x-ratelimit-reset").and_then(|value| value.parse::<i64>().ok());
let body = response
.into_body()
.read_to_string()
.map_err(|e| e.to_string())?;
match status {
200 => serde_json::from_str(&body).map_err(|e| e.to_string()),
403 | 429 if remaining.as_deref() == Some("0") || body.contains("rate limit") => Err(
rate_limit_message(reset, token.is_some(), chrono::Utc::now().timestamp()),
),
401 if token.is_some() => Err(
"GitHub rejected the token from GITHUB_TOKEN/GH_TOKEN (401 Bad credentials); fix or unset it."
.to_string(),
),
404 => Err("not found on GitHub (http status: 404)".to_string()),
other => Err(format!("GitHub API returned http status: {other}")),
}
}