Skip to main content

seher/copilot/
client.rs

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