Skip to main content

seher/copilot/
client.rs

1use crate::Cookie;
2use chrono::{DateTime, NaiveDate, Utc};
3use serde::Deserialize;
4
5#[derive(Debug, Deserialize)]
6pub struct QuotaRemaining {
7    #[serde(rename = "chatPercentage")]
8    pub chat_percentage: f64,
9    #[serde(rename = "premiumInteractionsPercentage")]
10    pub premium_interactions_percentage: f64,
11}
12
13#[derive(Debug, Deserialize)]
14pub struct Quotas {
15    pub remaining: QuotaRemaining,
16    #[serde(rename = "resetDate")]
17    pub reset_date: String,
18}
19
20#[derive(Debug, Deserialize)]
21pub struct CopilotQuotaResponse {
22    pub quotas: Quotas,
23}
24
25#[derive(Debug)]
26pub struct CopilotQuota {
27    pub chat_utilization: f64,
28    pub premium_utilization: f64,
29    pub reset_time: Option<DateTime<Utc>>,
30}
31
32impl CopilotQuota {
33    pub fn is_limited(&self) -> bool {
34        self.chat_utilization >= 100.0 || self.premium_utilization >= 100.0
35    }
36
37    pub fn next_reset_time(&self) -> Option<DateTime<Utc>> {
38        self.reset_time
39    }
40}
41
42const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
43    AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
44
45pub struct CopilotClient;
46
47impl CopilotClient {
48    pub async fn fetch_quota(
49        cookies: &[Cookie],
50    ) -> Result<CopilotQuota, Box<dyn std::error::Error>> {
51        let cookie_header = Self::build_cookie_header(cookies);
52
53        let client = reqwest::Client::builder()
54            .timeout(std::time::Duration::from_secs(30))
55            .build()?;
56
57        let response = client
58            .get("https://github.com/github-copilot/chat")
59            .header("Cookie", &cookie_header)
60            .header("User-Agent", USER_AGENT)
61            .header("github-verified-fetch", "true")
62            .header("x-requested-with", "XMLHttpRequest")
63            .header("accept", "application/json")
64            .send()
65            .await?;
66
67        let status = response.status();
68        if !status.is_success() {
69            let body = response.text().await?;
70            return Err(format!("GitHub Copilot API error: {} - {}", status, body).into());
71        }
72
73        let quota_response: CopilotQuotaResponse = response.json().await?;
74        let quotas = quota_response.quotas;
75
76        let _now = Utc::now();
77        let reset_time = NaiveDate::parse_from_str(&quotas.reset_date, "%Y-%m-%d")
78            .ok()
79            .and_then(|d| d.and_hms_opt(0, 0, 0))
80            .map(|dt| dt.and_utc());
81
82        let chat_utilization = 100.0 - quotas.remaining.chat_percentage;
83        let premium_utilization = 100.0 - quotas.remaining.premium_interactions_percentage;
84
85        Ok(CopilotQuota {
86            chat_utilization,
87            premium_utilization,
88            reset_time,
89        })
90    }
91
92    fn build_cookie_header(cookies: &[Cookie]) -> String {
93        cookies
94            .iter()
95            .filter(|c| !c.value.bytes().any(|b| b < 0x20 || b == 0x7f))
96            .map(|c| format!("{}={}", c.name, c.value))
97            .collect::<Vec<_>>()
98            .join("; ")
99    }
100}