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