diff --git a/.vscode/settings.json b/.vscode/settings.json index cadcf79..7210b81 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,7 @@ "Backend", "Judger", "Testsuit", - "Grpc" + "Grpc", + "Tools" ] } \ No newline at end of file diff --git a/tools/Cargo.toml b/tools/Cargo.toml new file mode 100644 index 0000000..b3cd302 --- /dev/null +++ b/tools/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "tools" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "mdoj" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0.86" +serde_json = "1.0.127" +zip = "2.2.0" + + +[dependencies.tonic] +workspace = true +features = ["transport", "codegen", "prost", "channel"] + +[dependencies.grpc] +path = "../grpc" +features = ["backend", "client", "extra_trait", "transport"] + +[dependencies.serde] +workspace = true +features = ["derive"] + +[dependencies.tokio] +workspace = true +features = ["macros", "rt-multi-thread"] + +[dependencies.reqwest] +version = "0.12.7" +features = ["json"] + +[dependencies.clap] +version = "4.5.16" +features = ["derive"] + +[dependencies.futures] +version = "0.3.30" +default-features = false +features = ["std"] diff --git a/tools/src/grpc.rs b/tools/src/grpc.rs new file mode 100644 index 0000000..f4996a1 --- /dev/null +++ b/tools/src/grpc.rs @@ -0,0 +1,23 @@ +pub use grpc::backend::*; +use tonic::{metadata::MetadataMap, IntoRequest, Request}; + +pub trait WithToken: Sized { + /// this will add token to request. + fn with_token(self, token: impl AsRef) -> Request; +} + +impl WithToken for T +where + T: IntoRequest, +{ + fn with_token(self, token: impl AsRef) -> Request { + let mut req = self.into_request(); + let Ok(token) = token.as_ref().parse() else { + return req; + }; + let mut metadata = MetadataMap::new(); + metadata.insert("token", token); + *req.metadata_mut() = metadata; + req + } +} diff --git a/tools/src/main.rs b/tools/src/main.rs new file mode 100644 index 0000000..9dd5848 --- /dev/null +++ b/tools/src/main.rs @@ -0,0 +1,21 @@ +mod grpc; +mod quoj; +mod quoj2mdoj; + +use anyhow::Result; +use clap::Parser; + +#[derive(Debug, Parser)] +enum Cli { + Quoj2mdoj(quoj2mdoj::Quoj2mdoj), +} + +#[tokio::main] +async fn main() -> Result<()> { + let cli = Cli::parse(); + match cli { + Cli::Quoj2mdoj(v) => quoj2mdoj::quoj2mdoj(v).await?, + }; + + Ok(()) +} diff --git a/tools/src/quoj/mod.rs b/tools/src/quoj/mod.rs new file mode 100644 index 0000000..b5a2b86 --- /dev/null +++ b/tools/src/quoj/mod.rs @@ -0,0 +1,42 @@ +pub mod problem; +pub mod testcases; + +use anyhow::Result; +use reqwest::{ + header::{self, HeaderMap}, + Client, IntoUrl, Url, +}; +use std::io::{Read, Seek}; + +#[derive(Debug, Clone)] +pub struct QuojClient { + base_url: Url, + client: Client, +} + +impl QuojClient { + pub fn new(base_url: impl IntoUrl, session: String) -> Result { + let base_url = base_url.into_url()?; + + let mut headers = HeaderMap::new(); + headers.insert(header::COOKIE, format!("sessionid={session}").parse()?); + + let client = reqwest::ClientBuilder::new() + .default_headers(headers) + .build()?; + + Ok(Self { base_url, client }) + } + + pub async fn problem(&self, id: usize) -> Result { + problem::problem(&self.client, &self.base_url, id).await + } + + pub async fn problems(&self) -> Result> { + problem::problems(&self.client, &self.base_url).await + } + + pub async fn testcases(&self, id: u64) -> Result> { + testcases::testcases(&self.client, &self.base_url, id).await + } +} diff --git a/tools/src/quoj/problem.rs b/tools/src/quoj/problem.rs new file mode 100644 index 0000000..b3b2704 --- /dev/null +++ b/tools/src/quoj/problem.rs @@ -0,0 +1,99 @@ +use anyhow::Result; +use reqwest::{Client, Url}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Problem { + pub data: ProblemData, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Problems { + pub data: ProblemsData, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProblemsData { + pub results: Vec, + pub total: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProblemData { + pub id: u64, + pub tags: Vec, + pub title: String, + pub description: String, + pub input_description: String, + pub output_description: String, + pub samples: Vec, + pub test_case_id: String, + pub test_case_score: Vec, + pub hint: String, + pub languages: Vec, + pub create_time: String, + pub last_update_time: Option, + pub time_limit: u64, + pub memory_limit: u64, + pub io_mode: IoMode, + pub rule_type: String, + pub difficulty: Difficulty, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum Difficulty { + Low, + Mid, + High, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IoMode { + pub input: String, + pub output: String, + pub io_mode: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Sample { + pub input: String, + pub output: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TestCaseScore { + pub score: i64, + pub input_name: String, + pub output_name: String, +} + +pub async fn problem(client: &Client, base_url: &Url, id: usize) -> Result { + let problem: Problem = client + .get(base_url.join("admin/problem")?) + .query(&[("id", id)]) + .send() + .await? + .json() + .await?; + Ok(problem.data) +} + +pub async fn problems(client: &Client, base_url: &Url) -> Result> { + const PAGE_SIZE: u64 = 250; + let mut ret = vec![]; + for i in 0.. { + let problems: Problems = client + .get(base_url.join("admin/problem")?) + .query(&[("limit", PAGE_SIZE), ("offset", PAGE_SIZE * i)]) + .send() + .await? + .json() + .await?; + + ret.extend(problems.data.results); + if problems.data.total <= PAGE_SIZE * i { + break; + } + } + Ok(ret) +} diff --git a/tools/src/quoj/testcases.rs b/tools/src/quoj/testcases.rs new file mode 100644 index 0000000..a6b65f8 --- /dev/null +++ b/tools/src/quoj/testcases.rs @@ -0,0 +1,35 @@ +use std::io::{Cursor, Read, Seek}; + +use anyhow::Result; +use reqwest::{Client, Url}; +use zip::ZipArchive; + +pub async fn testcases( + client: &Client, + base_url: &Url, + id: u64, +) -> Result> { + let bytes = Cursor::new( + client + .get(base_url.join("admin/test_case")?) + .query(&[("problem_id", id)]) + .send() + .await? + .bytes() + .await?, + ); + let testcases = ZipArchive::new(bytes)?; + Ok(Testcases(testcases)) +} + +pub struct Testcases(ZipArchive); + +impl Testcases { + pub fn testcase(&mut self, name: impl AsRef) -> Result> { + let mut buf = vec![]; + let mut file = self.0.by_name(name.as_ref())?; + buf.reserve_exact(file.size() as usize); + file.read_to_end(&mut buf)?; + Ok(buf) + } +} diff --git a/tools/src/quoj2mdoj.rs b/tools/src/quoj2mdoj.rs new file mode 100644 index 0000000..80ca658 --- /dev/null +++ b/tools/src/quoj2mdoj.rs @@ -0,0 +1,135 @@ +use crate::{ + grpc::{self, WithToken}, + quoj, +}; +use anyhow::Result; +use clap::Parser; + +#[derive(Debug, Parser)] +pub struct Quoj2mdoj { + #[arg(long)] + duoj_api: String, + #[arg(long)] + duoj_session: String, + + #[arg(long)] + mdoj_api: String, + #[arg(long)] + mdoj_session: String, + + #[arg(long)] + problem_id: Option, +} + +pub async fn quoj2mdoj(v: Quoj2mdoj) -> Result<()> { + let quoj_client = quoj::QuojClient::new(v.duoj_api, v.duoj_session)?; + let mut mdoj_client = grpc::problem_client::ProblemClient::connect(v.mdoj_api.clone()).await?; + let mut mdoj_testcase_client = + grpc::testcase_client::TestcaseClient::connect(v.mdoj_api).await?; + + if let Some(problem_id) = v.problem_id { + let p = quoj_client.problem(problem_id).await?; + return problem( + p, + &v.mdoj_session, + &quoj_client, + &mut mdoj_client, + &mut mdoj_testcase_client, + ) + .await; + } + + let ps = quoj_client.problems().await?; + for p in ps { + problem( + p, + &v.mdoj_session, + &quoj_client, + &mut mdoj_client, + &mut mdoj_testcase_client, + ) + .await?; + } + + Ok(()) +} + +async fn problem( + problem: quoj::problem::ProblemData, + session: &str, + quoj_client: &quoj::QuojClient, + mdoj_client: &mut grpc::problem_client::ProblemClient, + mdoj_testcase_client: &mut grpc::testcase_client::TestcaseClient, +) -> Result<()> { + let id = mdoj_client + .create( + grpc::CreateProblemRequest { + info: grpc::create_problem_request::Info { + title: problem.title, + difficulty: match problem.difficulty { + quoj::problem::Difficulty::Low => 1000, + quoj::problem::Difficulty::Mid => 2000, + quoj::problem::Difficulty::High => 3000, + }, + time: problem.time_limit * 1000, + memory: problem.memory_limit * 1024 * 1024, + content: format!( + "{}\n\n## 輸入\n\n{}\n\n## 輸出\n\n{}\n\n## 提示\n\n{}", + problem.description, + problem.input_description, + problem.output_description, + problem.hint + ), + match_rule: grpc::MatchRule::MatchruleIgnoreSnl.into(), + order: 0.0, + tags: problem.tags, + }, + request_id: None, + } + .with_token(session), + ) + .await? + .into_inner() + .id; + + let mut testcases = quoj_client.testcases(problem.id).await?; + + for testcase in problem.test_case_score { + let testcase_id = mdoj_testcase_client + .create( + grpc::CreateTestcaseRequest { + info: grpc::create_testcase_request::Info { + score: testcase.score as u32, + input: testcases.testcase(testcase.input_name)?, + output: testcases.testcase(testcase.output_name)?, + }, + request_id: None, + } + .with_token(session), + ) + .await? + .into_inner() + .id; + mdoj_testcase_client + .add_to_problem( + grpc::AddTestcaseToProblemRequest { + testcase_id, + problem_id: id, + request_id: None, + } + .with_token(session), + ) + .await?; + } + + mdoj_client + .publish( + grpc::PublishRequest { + id, + request_id: None, + } + .with_token(session), + ) + .await?; + Ok(()) +}