Send browser cookies/UA for live auth
Client builds the Cookie header as 'auth=<value>; oc_locale=en' from the bare
auth value the config supplies, so oc_locale defaults to English and the
scrape selectors match. Legacy full-header configs pass through. Default
User-Agent is a desktop Chrome string; --user-agent still overrides. Fix
relative-time past tense ('1m ago' not 'ago1m'). Verified live against a real
OpenCode account: balance, three usage frames, and reset times all parse.
This commit is contained in:
parent
41f35aec34
commit
4463a9b51d
@ -22,6 +22,24 @@ pub fn go_url(workspace: &str) -> String {
|
|||||||
GO_BASE_URL.replace("{workspace}", workspace)
|
GO_BASE_URL.replace("{workspace}", workspace)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build the Cookie header from the bare `auth` value. OpenCode expects an
|
||||||
|
/// `auth` cookie plus an `oc_locale` cookie. We pin `oc_locale=en` so the page
|
||||||
|
/// renders in English and the scrape selectors match the captured pages.
|
||||||
|
///
|
||||||
|
/// If `cookie` already looks like a full header (contains `=` and `;`), pass
|
||||||
|
/// it through unchanged so legacy configs keep working.
|
||||||
|
fn cookie_header(cookie: &str) -> String {
|
||||||
|
let trimmed = cookie.trim();
|
||||||
|
if trimmed.contains('=') && (trimmed.contains(';') || trimmed.starts_with("auth=")) {
|
||||||
|
// Caller supplied a full header. Use as-is, but ensure oc_locale.
|
||||||
|
if trimmed.contains("oc_locale") {
|
||||||
|
return trimmed.to_string();
|
||||||
|
}
|
||||||
|
return format!("{trimmed}; oc_locale=en");
|
||||||
|
}
|
||||||
|
format!("auth={trimmed}; oc_locale=en")
|
||||||
|
}
|
||||||
|
|
||||||
pub struct HttpClient {
|
pub struct HttpClient {
|
||||||
client: reqwest::Client,
|
client: reqwest::Client,
|
||||||
retries: u8,
|
retries: u8,
|
||||||
@ -59,8 +77,9 @@ impl HttpClient {
|
|||||||
let resp = self
|
let resp = self
|
||||||
.client
|
.client
|
||||||
.get(url)
|
.get(url)
|
||||||
.header("cookie", cookie)
|
.header("cookie", cookie_header(cookie))
|
||||||
.header("accept", "text/html,application/xhtml+xml,*/*;q=0.8")
|
.header("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||||
|
.header("accept-language", "en-US,en;q=0.9")
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
|
|||||||
@ -18,6 +18,11 @@ use clap::Parser;
|
|||||||
use cli::{CacheAction, Cli, Command, ConfigAction, RawFormat};
|
use cli::{CacheAction, Cli, Command, ConfigAction, RawFormat};
|
||||||
use model::{AccountReport, ReportStatus};
|
use model::{AccountReport, ReportStatus};
|
||||||
|
|
||||||
|
/// User-Agent sent on every request. Looks like a normal desktop browser so
|
||||||
|
/// OpenCode serves the same HTML the scrape selectors were built against. The
|
||||||
|
/// user can override with `--user-agent`.
|
||||||
|
const DEFAULT_USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let rt = match tokio::runtime::Builder::new_multi_thread()
|
let rt = match tokio::runtime::Builder::new_multi_thread()
|
||||||
@ -131,7 +136,7 @@ fn load_and_options(cli: &Cli) -> anyhow::Result<(config::Config, fetch::FetchOp
|
|||||||
let user_agent = cli
|
let user_agent = cli
|
||||||
.user_agent
|
.user_agent
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| format!("ocd/{}", env!("CARGO_PKG_VERSION")));
|
.unwrap_or_else(|| DEFAULT_USER_AGENT.to_string());
|
||||||
let ttl = cli.ttl.or(cfg.default_ttl_secs).unwrap_or(180);
|
let ttl = cli.ttl.or(cfg.default_ttl_secs).unwrap_or(180);
|
||||||
let opts = fetch::FetchOptions {
|
let opts = fetch::FetchOptions {
|
||||||
ttl_secs: ttl,
|
ttl_secs: ttl,
|
||||||
|
|||||||
@ -14,9 +14,8 @@ pub fn relative(iso: &str) -> String {
|
|||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
let d = t.signed_duration_since(now);
|
let d = t.signed_duration_since(now);
|
||||||
let secs = d.num_seconds();
|
let secs = d.num_seconds();
|
||||||
let sign = if secs >= 0 { "in " } else { " ago" };
|
|
||||||
let body = dur_words(secs.abs());
|
let body = dur_words(secs.abs());
|
||||||
format!("{sign}{body}")
|
if secs >= 0 { format!("in {body}") } else { format!("{body} ago") }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Absolute UTC string like "2025-08-31 17:00 UTC".
|
/// Absolute UTC string like "2025-08-31 17:00 UTC".
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user