use std::time::Duration;
const WORKER_URL: &str = "https://tokensave-counter.enzinol.workers.dev";
const GITHUB_RELEASES_URL: &str =
"https://api.github.com/repos/aovestdipaperino/tokensave/releases/latest";
const GITHUB_RELEASES_LIST_URL: &str =
"https://api.github.com/repos/aovestdipaperino/tokensave/releases?per_page=10";
const FLUSH_TIMEOUT: Duration = Duration::from_secs(2);
const FETCH_TIMEOUT: Duration = Duration::from_secs(1);
#[derive(serde::Deserialize)]
struct WorkerResponse {
total: u64,
}
pub fn agent_with_timeout(timeout: Duration) -> ureq::Agent {
ureq::Agent::config_builder()
.timeout_global(Some(timeout))
.build()
.into()
}
pub fn flush_pending(amount: u64) -> Option<u64> {
if amount == 0 {
return None;
}
let body = serde_json::json!({ "amount": amount });
let agent = agent_with_timeout(FLUSH_TIMEOUT);
let parsed: WorkerResponse = agent
.post(&format!("{WORKER_URL}/increment"))
.send_json(&body)
.ok()?
.body_mut()
.read_json()
.ok()?;
Some(parsed.total)
}
pub fn fetch_worldwide_total() -> Option<u64> {
let agent = agent_with_timeout(FETCH_TIMEOUT);
let parsed: WorkerResponse = agent
.get(&format!("{WORKER_URL}/total"))
.call()
.ok()?
.body_mut()
.read_json()
.ok()?;
Some(parsed.total)
}
#[derive(serde::Deserialize)]
struct CountriesResponse {
flags: Vec<String>,
}
pub fn fetch_country_flags() -> Vec<String> {
let agent = agent_with_timeout(Duration::from_millis(500));
let mut resp = match agent.get(&format!("{WORKER_URL}/countries")).call() {
Ok(r) => r,
Err(_) => return Vec::new(),
};
let parsed: CountriesResponse = match resp.body_mut().read_json() {
Ok(r) => r,
Err(_) => return Vec::new(),
};
parsed.flags
}
#[derive(serde::Deserialize)]
struct GitHubRelease {
tag_name: String,
#[serde(default)]
prerelease: bool,
}
pub fn fetch_latest_version() -> Option<String> {
if is_beta() {
fetch_latest_beta_version()
} else {
fetch_latest_stable_version()
}
}
pub fn fetch_latest_stable_version() -> Option<String> {
let agent = agent_with_timeout(FETCH_TIMEOUT);
let release: GitHubRelease = agent
.get(GITHUB_RELEASES_URL)
.header("User-Agent", "tokensave")
.call()
.ok()?
.body_mut()
.read_json()
.ok()?;
Some(release.tag_name.trim_start_matches('v').to_string())
}
pub fn fetch_latest_beta_version() -> Option<String> {
let agent = agent_with_timeout(FETCH_TIMEOUT);
let releases: Vec<GitHubRelease> = agent
.get(GITHUB_RELEASES_LIST_URL)
.header("User-Agent", "tokensave")
.call()
.ok()?
.body_mut()
.read_json()
.ok()?;
releases
.into_iter()
.find(|r| r.prerelease)
.map(|r| r.tag_name.trim_start_matches('v').to_string())
}
pub fn is_beta() -> bool {
env!("CARGO_PKG_VERSION").contains('-')
}
pub fn is_newer_version(current: &str, latest: &str) -> bool {
fn parse(v: &str) -> Option<(u64, u64, u64, Option<&str>)> {
let (base, pre) = match v.split_once('-') {
Some((b, p)) => (b, Some(p)),
None => (v, None),
};
let mut parts = base.split('.');
let major = parts.next()?.parse().ok()?;
let minor = parts.next()?.parse().ok()?;
let patch = parts.next()?.parse().ok()?;
Some((major, minor, patch, pre))
}
match (parse(current), parse(latest)) {
(Some((cm, cn, cp, cpre)), Some((lm, ln, lp, lpre))) => {
if cpre.is_some() != lpre.is_some() {
return false;
}
let c_base = (cm, cn, cp);
let l_base = (lm, ln, lp);
if l_base != c_base {
return l_base > c_base;
}
match (cpre, lpre) {
(None, None) => false,
(Some(a), Some(b)) => b > a,
_ => false,
}
}
_ => false,
}
}
pub fn is_newer_minor_version(current: &str, latest: &str) -> bool {
fn parse(v: &str) -> Option<(u64, u64)> {
let base = v.split_once('-').map_or(v, |(b, _)| b);
let mut parts = base.split('.');
let major = parts.next()?.parse().ok()?;
let minor = parts.next()?.parse().ok()?;
Some((major, minor))
}
is_newer_version(current, latest)
&& match (parse(current), parse(latest)) {
(Some(c), Some(l)) => l > c,
_ => true,
}
}
pub enum InstallMethod {
Cargo,
Brew,
Scoop,
Unknown,
}
pub fn detect_install_method() -> InstallMethod {
let Ok(exe) = std::env::current_exe() else {
return InstallMethod::Unknown;
};
let path = exe.to_string_lossy();
if path.contains(".cargo/bin") || path.contains(".cargo\\bin") {
InstallMethod::Cargo
} else if path.contains("/homebrew/") || path.contains("/Cellar/") {
InstallMethod::Brew
} else if path.contains("\\scoop\\") || path.contains("/scoop/") {
InstallMethod::Scoop
} else {
InstallMethod::Unknown
}
}
pub fn upgrade_command(_method: &InstallMethod) -> &'static str {
"tokensave upgrade"
}