Skip to main content

seher/copilot/
client.rs

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