use std::fs;
use crate::auth::LeetCodeCredentials;
use crate::error::{EngineError, Result};
use crate::models::{GraphQLQuery, Question, SubmissionCheckResult, SubmitPayload, SubmitResponse};
use reqwest::Client;
use reqwest::header::{COOKIE, HeaderMap, HeaderValue, USER_AGENT};
use serde_json::json;
#[derive(Clone, Debug)]
pub struct LeetCodeClient {
http_client: Client,
}
impl LeetCodeClient {
pub fn new(creds: LeetCodeCredentials) -> Result<Self> {
let mut headers = HeaderMap::new();
let cookie_str = format!(
"LEETCODE_SESSION={}; csrftoken={}",
creds.session_cookie, creds.csrf_token
);
headers.insert(COOKIE, HeaderValue::from_str(&cookie_str).unwrap());
headers.insert(
"x-csrftoken",
HeaderValue::from_str(&creds.csrf_token).unwrap(),
);
headers.insert(
USER_AGENT,
HeaderValue::from_static(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
),
);
headers.insert("Referer", HeaderValue::from_static("https://leetcode.com/"));
let client = Client::builder()
.default_headers(headers)
.cookie_store(true)
.build()?;
Ok(Self {
http_client: client,
})
}
pub async fn execute_graphql<T: serde::de::DeserializeOwned>(
&self,
query: GraphQLQuery,
) -> Result<T> {
let response = self
.http_client
.post("https://leetcode.com/graphql")
.json(&query)
.send()
.await?;
if !response.status().is_success() {
return Err(EngineError::Network(
response.error_for_status().unwrap_err(),
));
}
let json_data: serde_json::Value = response.json().await?;
if let Some(errors) = json_data.get("errors") {
return Err(EngineError::GraphQL(errors.to_string()));
}
let data = json_data
.get("data")
.ok_or_else(|| EngineError::GraphQL("Missing data field".into()))?;
serde_json::from_value(data.clone()).map_err(EngineError::from)
}
pub async fn get_question_by_slug(&self, title_slug: &str) -> Result<Question> {
let query_string = r#"
query questionData($titleSlug: String!) {
question(titleSlug: $titleSlug) {
questionId
title
titleSlug
content
codeSnippets {
langSlug
code
}
}
}
"#;
let query = GraphQLQuery {
query: query_string.to_string(),
variables: json!({ "titleSlug": title_slug }),
operation_name: Some("questionData".to_string()),
};
#[derive(serde::Deserialize)]
struct QuestionWrapper {
question: Question,
}
let response: QuestionWrapper = self.execute_graphql(query).await?;
Ok(response.question)
}
pub async fn get_question_by_id(&self, id: u64) -> Result<Question> {
let url = "https://leetcode.com/api/problems/all/";
let response = self.http_client.get(url).send().await?;
if !response.status().is_success() {
return Err(EngineError::GraphQL(format!(
"Failed to fetch problem list: {}",
response.status()
)));
}
let json_data: serde_json::Value = response.json().await?;
let mut target_slug = None;
if let Some(pairs) = json_data
.get("stat_status_pairs")
.and_then(|v| v.as_array())
{
for pair in pairs {
if let Some(stat) = pair.get("stat") {
let current_id = stat.get("frontend_question_id").and_then(|v| v.as_u64());
if current_id == Some(id) {
if let Some(slug) =
stat.get("question__title_slug").and_then(|v| v.as_str())
{
target_slug = Some(slug.to_string());
break;
}
}
}
}
}
let slug = target_slug
.ok_or_else(|| EngineError::GraphQL(format!("Problem with ID {} not found", id)))?;
self.get_question_by_slug(&slug).await
}
pub async fn submit_code(
&self,
title_slug: &str,
question_id: &str,
lang: &str,
code: &str,
) -> Result<u64> {
let url = format!("https://leetcode.com/problems/{}/submit/", title_slug);
let payload = SubmitPayload {
lang: lang.to_string(),
question_id: question_id.to_string(),
typed_code: code.to_string(),
};
let response = self
.http_client
.post(&url)
.json(&payload)
.header(
"Referer",
format!("https://leetcode.com/problems/{}/", title_slug),
)
.send()
.await?;
if !response.status().is_success() {
return Err(EngineError::GraphQL(format!(
"Submission failed: {}",
response.status()
)));
}
let result: SubmitResponse = response.json().await?;
Ok(result.submission_id)
}
pub async fn check_submission(&self, submission_id: u64) -> Result<SubmissionCheckResult> {
let url = format!(
"https://leetcode.com/submissions/detail/{}/check/",
submission_id
);
loop {
let response = self.http_client.get(&url).send().await?;
if !response.status().is_success() {
return Err(EngineError::GraphQL(format!(
"Check failed: {}",
response.status()
)));
}
let result: SubmissionCheckResult = response.json().await?;
if result.state == "SUCCESS" {
return Ok(result);
}
tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
}
}
pub async fn get_problem_list(&self) -> Result<Vec<crate::models::ProblemSummary>> {
let url = "https://leetcode.com/api/problems/all/";
let response = self.http_client.get(url).send().await?;
if !response.status().is_success() {
return Err(crate::error::EngineError::GraphQL(format!(
"Failed to fetch problem list: {}",
response.status()
)));
}
let json_data: serde_json::Value = response.json().await?;
let mut problems = Vec::new();
if let Some(pairs) = json_data
.get("stat_status_pairs")
.and_then(|v| v.as_array())
{
for pair in pairs {
if let (Some(stat), Some(difficulty)) = (pair.get("stat"), pair.get("difficulty")) {
let id = stat
.get("frontend_question_id")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let title = stat
.get("question__title")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let slug = stat
.get("question__title_slug")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let level = difficulty
.get("level")
.and_then(|v| v.as_u64())
.unwrap_or(0) as u8;
let accepted =
stat.get("total_acs").and_then(|v| v.as_u64()).unwrap_or(0) as u64;
let submitted = stat
.get("total_submitted")
.and_then(|v| v.as_u64())
.unwrap_or(0) as u64;
let acceptance = accepted as f64 / submitted as f64;
problems.push(crate::models::ProblemSummary {
id,
title,
slug,
difficulty: level,
accepted,
submitted,
acceptance,
});
}
}
}
problems.sort_by_key(|p| p.id);
Ok(problems)
}
}