use crate::{
CopilotError,
constant::{COPILOT_CLIENT_ID, GITHUB_DEVICE_CODE_URL, GITHUB_TOKEN_URL},
};
use aither_core::time::sleep;
use serde::{Deserialize, Serialize};
use std::time::{Duration, SystemTime};
use zenwave::{Client, client};
#[derive(Debug, Clone, Deserialize)]
pub struct DeviceCodeResponse {
pub device_code: String,
pub user_code: String,
pub verification_uri: String,
pub expires_in: u64,
pub interval: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CopilotToken {
pub access_token: String,
pub token_type: String,
pub scope: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<SystemTime>,
}
#[derive(Serialize)]
struct DeviceCodeRequest<'a> {
client_id: &'a str,
scope: &'a str,
}
#[derive(Serialize)]
struct TokenRequest<'a> {
client_id: &'a str,
device_code: &'a str,
grant_type: &'a str,
}
#[derive(Deserialize)]
struct TokenResponse {
access_token: Option<String>,
token_type: Option<String>,
scope: Option<String>,
error: Option<String>,
error_description: Option<String>,
}
pub async fn request_device_code() -> Result<DeviceCodeResponse, CopilotError> {
let mut backend = client();
let request = DeviceCodeRequest {
client_id: COPILOT_CLIENT_ID,
scope: "copilot",
};
let response: DeviceCodeResponse = backend
.post(GITHUB_DEVICE_CODE_URL)
.map_err(CopilotError::Http)?
.header("Accept", "application/json")
.map_err(CopilotError::Http)?
.json_body(&request)
.map_err(CopilotError::Http)?
.json()
.await
.map_err(CopilotError::Http)?;
Ok(response)
}
pub async fn poll_for_token(
device_code: &str,
interval: u64,
) -> Result<CopilotToken, CopilotError> {
let poll_interval = Duration::from_secs(interval.max(5));
loop {
sleep(poll_interval).await;
match try_get_token(device_code).await {
Ok(token) => return Ok(token),
Err(CopilotError::AuthorizationPending) => {
tracing::debug!("Authorization pending, polling again...");
}
Err(e) => return Err(e),
}
}
}
pub async fn try_get_token(device_code: &str) -> Result<CopilotToken, CopilotError> {
let mut backend = client();
let request = TokenRequest {
client_id: COPILOT_CLIENT_ID,
device_code,
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
};
let response: TokenResponse = backend
.post(GITHUB_TOKEN_URL)
.map_err(CopilotError::Http)?
.header("Accept", "application/json")
.map_err(CopilotError::Http)?
.json_body(&request)
.map_err(CopilotError::Http)?
.json()
.await
.map_err(CopilotError::Http)?;
if let Some(error) = response.error {
return match error.as_str() {
"authorization_pending" | "slow_down" => Err(CopilotError::AuthorizationPending),
"expired_token" => Err(CopilotError::DeviceCodeExpired),
"access_denied" => Err(CopilotError::AccessDenied),
_ => Err(CopilotError::Api(
response.error_description.unwrap_or(error),
)),
};
}
let access_token = response
.access_token
.ok_or_else(|| CopilotError::Api("Missing access_token in response".to_string()))?;
let token_type = response.token_type.unwrap_or_else(|| "bearer".to_string());
let scope = response.scope.unwrap_or_else(|| "copilot".to_string());
Ok(CopilotToken {
access_token,
token_type,
scope,
expires_at: None, })
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionToken {
pub token: String,
pub api_endpoint: String,
pub expires_at: i64,
}
#[derive(Debug, Deserialize)]
struct EndpointsResponse {
api: String,
}
#[derive(Debug, Deserialize)]
struct SessionTokenResponse {
token: String,
endpoints: EndpointsResponse,
expires_at: i64,
}
pub async fn get_session_token(oauth_token: &str) -> Result<SessionToken, CopilotError> {
use crate::constant::{COPILOT_TOKEN_URL, EDITOR_VERSION};
let mut backend = client();
let response: SessionTokenResponse = backend
.get(COPILOT_TOKEN_URL)
.map_err(CopilotError::Http)?
.header("Authorization", format!("token {oauth_token}"))
.map_err(CopilotError::Http)?
.header("Accept", "application/json")
.map_err(CopilotError::Http)?
.header(
"User-Agent",
format!("aither-copilot/0.1 ({EDITOR_VERSION})"),
)
.map_err(CopilotError::Http)?
.json()
.await
.map_err(CopilotError::Http)?;
Ok(SessionToken {
token: response.token,
api_endpoint: response.endpoints.api,
expires_at: response.expires_at,
})
}