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 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("as.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}