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