use reqwest::Client;
use serde::Deserialize;
use tokio::time::{sleep, Duration};
const ACCESS_TOKEN_URL: &str = "https://github.com/login/oauth/access_token";
const COPILOT_TOKEN_URL: &str = "https://api.github.com/copilot_internal/v2/token";
const GITHUB_CLIENT_ID: &str = "Iv1.b507a08c87ecfe98";
#[derive(Debug, Deserialize)]
pub struct AccessTokenResponse {
pub access_token: Option<String>,
pub token_type: Option<String>,
pub scope: Option<String>,
pub error: Option<String>,
#[serde(rename = "error_description")]
pub error_description: Option<String>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct CopilotToken {
pub token: String,
#[serde(rename = "expires_at")]
pub expires_at: u64,
#[serde(rename = "chat_enabled")]
pub chat_enabled: bool,
#[serde(rename = "chat_jwt")]
pub chat_jwt: Option<String>,
}
pub async fn poll_access_token(
client: &Client,
device_code: &str,
interval: u64,
expires_in: u64,
) -> Result<String, String> {
let params = [
("client_id", GITHUB_CLIENT_ID),
("device_code", device_code),
("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
];
let start = std::time::Instant::now();
let max_duration = Duration::from_secs(expires_in);
let poll_interval = Duration::from_secs(interval.max(5));
println!(" 🔄 Polling for authorization...");
loop {
if start.elapsed() > max_duration {
return Err("❌ Device code expired. Please try again.".to_string());
}
let response = client
.post(ACCESS_TOKEN_URL)
.header("Accept", "application/json")
.header("User-Agent", "BambooCopilot/1.0")
.form(¶ms)
.send()
.await
.map_err(|e| format!("Failed to poll access token: {}", e))?;
let token_response: AccessTokenResponse = response
.json()
.await
.map_err(|e| format!("Failed to parse access token response: {}", e))?;
if let Some(token) = token_response.access_token {
println!(" ✅ Access token received!");
return Ok(token);
}
if let Some(error) = token_response.error {
match error.as_str() {
"authorization_pending" => {
print!(".");
std::io::Write::flush(&mut std::io::stdout()).ok();
}
"slow_down" => {
println!("\n ⚠️ Server requested slower polling, increasing interval...");
sleep(Duration::from_secs(interval + 5)).await;
continue;
}
"expired_token" => {
return Err("❌ Device code expired. Please try again.".to_string());
}
"access_denied" => {
return Err("❌ Authorization denied by user.".to_string());
}
_ => {
let desc = token_response.error_description.unwrap_or_default();
return Err(format!("❌ Auth error: {} - {}", error, desc));
}
}
}
sleep(poll_interval).await;
}
}
pub async fn get_copilot_token(
client: &Client,
access_token: &str,
) -> Result<CopilotToken, String> {
let response = client
.get(COPILOT_TOKEN_URL)
.header("Authorization", format!("token {}", access_token))
.header("Accept", "application/json")
.header("User-Agent", "BambooCopilot/1.0")
.send()
.await
.map_err(|e| format!("Failed to get copilot token: {}", e))?;
let status = response.status();
if !status.is_success() {
let text = response.text().await.unwrap_or_default();
return Err(format!(
"Copilot token request failed: HTTP {} - {}",
status, text
));
}
let copilot_token: CopilotToken = response
.json()
.await
.map_err(|e| format!("Failed to parse copilot token: {}", e))?;
if !copilot_token.chat_enabled {
return Err("❌ Copilot chat is not enabled for this account.".to_string());
}
println!(" ✅ Copilot token received!");
Ok(copilot_token)
}