├── data └── test_cases │ ├── 1 │ ├── 1.in │ ├── info │ └── spj_src.cpp │ └── 2 │ ├── 1.in │ ├── 1.out │ ├── 2.in │ ├── 2.out │ ├── 3.in │ ├── 3.out │ └── info ├── migrations ├── .gitkeep └── 00000000000000_diesel_initial_setup │ ├── down.sql │ └── up.sql ├── src ├── auth │ ├── mod.rs │ └── region.rs ├── services │ ├── rank │ │ ├── mod.rs │ │ └── utils.rs │ ├── judge_server │ │ ├── mod.rs │ │ ├── info.rs │ │ └── heartbeat.rs │ ├── mod.rs │ ├── region │ │ ├── utils.rs │ │ └── mod.rs │ ├── contest │ │ └── utils.rs │ ├── problem_set │ │ └── mod.rs │ ├── sample │ │ └── mod.rs │ ├── submission │ │ └── mod.rs │ ├── problem │ │ └── utils.rs │ └── user │ │ └── mod.rs ├── judge_actor │ ├── statistics │ │ ├── mod.rs │ │ └── common_region.rs │ ├── mod.rs │ ├── utils.rs │ └── handler.rs ├── models │ ├── utils.rs │ ├── region_access_settings.rs │ ├── access_control_list.rs │ ├── mod.rs │ ├── problem_sets.rs │ ├── regions.rs │ ├── ranks.rs │ ├── judge_servers.rs │ ├── samples.rs │ ├── region_links.rs │ ├── users.rs │ ├── problems.rs │ ├── contests.rs │ ├── statistics.rs │ ├── languages.rs │ └── submissions.rs ├── controllers │ ├── mod.rs │ ├── judge_servers │ │ ├── mod.rs │ │ └── handler.rs │ ├── submissions │ │ ├── mod.rs │ │ └── handler.rs │ ├── samples │ │ ├── mod.rs │ │ └── handler.rs │ ├── problem_sets │ │ ├── mod.rs │ │ └── handler.rs │ ├── contests │ │ ├── mod.rs │ │ └── handler.rs │ ├── regions │ │ ├── mod.rs │ │ └── handler.rs │ ├── problems │ │ ├── mod.rs │ │ └── handler.rs │ └── users │ │ ├── mod.rs │ │ └── handler.rs ├── statics.rs ├── main.rs └── schema.rs ├── crates ├── server-core │ ├── src │ │ ├── utils │ │ │ ├── mod.rs │ │ │ ├── time.rs │ │ │ └── encryption.rs │ │ ├── lib.rs │ │ ├── database │ │ │ ├── mod.rs │ │ │ └── pool.rs │ │ ├── cli_args.rs │ │ └── errors.rs │ ├── README.md │ └── Cargo.toml └── shupdtp-db │ ├── src │ ├── user │ │ ├── mod.rs │ │ ├── operations.rs │ │ └── models.rs │ ├── lib.rs │ └── schema.rs │ ├── README.md │ ├── Cargo.toml │ └── tests │ └── user.rs ├── .gitignore ├── executable └── send_judge_request ├── .env.example ├── diesel.toml ├── auth.toml ├── document ├── Authentication.md ├── API │ ├── IMPORTANT.md │ ├── Problems.md │ ├── Users.md │ └── Samples.md └── RUN.md ├── .github └── workflows │ └── rust.yml ├── docker-compose.yml ├── docker-compose ├── rsync-master.yml └── judge-server.yml ├── README.md ├── Cargo.toml └── examples └── send_judge_request.rs /data/test_cases/1/1.in: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /migrations/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/test_cases/2/1.in: -------------------------------------------------------------------------------- 1 | 2 2 | -------------------------------------------------------------------------------- /data/test_cases/2/1.out: -------------------------------------------------------------------------------- 1 | 2 2 | -------------------------------------------------------------------------------- /data/test_cases/2/2.in: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /data/test_cases/2/2.out: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /data/test_cases/2/3.in: -------------------------------------------------------------------------------- 1 | test123 2 | -------------------------------------------------------------------------------- /data/test_cases/2/3.out: -------------------------------------------------------------------------------- 1 | test123 2 | -------------------------------------------------------------------------------- /src/auth/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod region; 2 | -------------------------------------------------------------------------------- /src/services/rank/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod utils; 2 | -------------------------------------------------------------------------------- /src/judge_actor/statistics/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod common_region; 2 | -------------------------------------------------------------------------------- /crates/server-core/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod encryption; 2 | pub mod time; 3 | -------------------------------------------------------------------------------- /crates/shupdtp-db/src/user/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod models; 2 | pub mod operations; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /crates/shupdtp-db/target 3 | /data/rsync_master/rsyncd.log 4 | .env 5 | *.pem -------------------------------------------------------------------------------- /executable/send_judge_request: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slhmy/SHUpdtp/HEAD/executable/send_judge_request -------------------------------------------------------------------------------- /data/test_cases/1/info: -------------------------------------------------------------------------------- 1 | {"test_case_number":1,"spj":true,"test_cases":{"0":{"input_name":"1.in","input_size":0}}} -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgres://username:password@localhost:port/schema 2 | JUDGE_ACTOR_COUNT=2 3 | RUST_LOG=info -------------------------------------------------------------------------------- /crates/shupdtp-db/README.md: -------------------------------------------------------------------------------- 1 | # SHUpdtp DB 2 | 3 | ## How to run test 4 | 5 | ```shell 6 | cargo test -- --nocapture 7 | ``` 8 | -------------------------------------------------------------------------------- /crates/shupdtp-db/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod schema; 2 | pub mod user; 3 | 4 | #[macro_use] 5 | extern crate diesel; 6 | #[macro_use] 7 | extern crate serde; 8 | -------------------------------------------------------------------------------- /src/services/judge_server/mod.rs: -------------------------------------------------------------------------------- 1 | mod heartbeat; 2 | pub(crate) use heartbeat::record_server_info; 3 | 4 | mod info; 5 | pub(crate) use info::server_info; 6 | -------------------------------------------------------------------------------- /src/models/utils.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Serialize)] 2 | pub struct SizedList { 3 | pub total: i64, 4 | pub list: Vec, 5 | } 6 | -------------------------------------------------------------------------------- /diesel.toml: -------------------------------------------------------------------------------- 1 | # For documentation on how to configure this file, 2 | # see diesel.rs/guides/configuring-diesel-cli 3 | 4 | [print_schema] 5 | file = "src/schema.rs" 6 | -------------------------------------------------------------------------------- /auth.toml: -------------------------------------------------------------------------------- 1 | ['/'] 2 | sup = ['manage'] 3 | admin = ['manage'] 4 | other = [] 5 | 6 | ['/manage'] 7 | sup = ['user_manage', 'problem_manage'] 8 | admin = ['problem_manage'] 9 | others = [] -------------------------------------------------------------------------------- /src/controllers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod contests; 2 | pub mod judge_servers; 3 | pub mod problem_sets; 4 | pub mod problems; 5 | pub mod regions; 6 | pub mod samples; 7 | pub mod submissions; 8 | pub mod users; 9 | -------------------------------------------------------------------------------- /src/services/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod contest; 2 | pub mod judge_server; 3 | pub mod problem; 4 | pub mod problem_set; 5 | pub mod rank; 6 | pub mod region; 7 | pub mod sample; 8 | pub mod submission; 9 | pub mod user; 10 | -------------------------------------------------------------------------------- /document/Authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | This project follows a starter repository [canduma](https://github.com/clifinger/canduma). 3 | It's a prefect example, and many dependencies are new enough. So if you are building a new project, I highly recommend this example. -------------------------------------------------------------------------------- /document/API/IMPORTANT.md: -------------------------------------------------------------------------------- 1 | # Doc moved to YuQue 2 | For project reason the API Document is moved to **[SHUpdtp Doc](https://www.yuque.com/books/share/2242e791-9354-4e37-8695-35997db2c0a6?#)**. 3 | 4 | You can find the latest version in "API文档", I will try to ask my member to keep it a english version. -------------------------------------------------------------------------------- /crates/server-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde_derive; 3 | 4 | pub mod cli_args; 5 | pub mod database; 6 | pub mod errors; 7 | pub mod utils; 8 | 9 | #[cfg(test)] 10 | mod tests { 11 | #[test] 12 | fn it_works() { 13 | assert_eq!(2 + 2, 4); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/controllers/judge_servers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod handler; 2 | 3 | use actix_web::web; 4 | 5 | pub fn route(cfg: &mut web::ServiceConfig) { 6 | cfg.service( 7 | web::scope("/judge_servers") 8 | .service(handler::handle_heartbeat) 9 | .service(handler::get_server_info), 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/models/region_access_settings.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::*; 2 | 3 | #[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable)] 4 | #[table_name = "region_access_settings"] 5 | pub struct RegionAccessSetting { 6 | pub region: String, 7 | pub salt: Option, 8 | pub hash: Option>, 9 | } 10 | -------------------------------------------------------------------------------- /src/controllers/submissions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod handler; 2 | 3 | use actix_web::web; 4 | 5 | pub fn route(cfg: &mut web::ServiceConfig) { 6 | cfg.service( 7 | web::scope("/submissions") 8 | .service(handler::create) 9 | .service(handler::get) 10 | .service(handler::get_list), 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/models/access_control_list.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::*; 2 | 3 | #[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable)] 4 | #[table_name = "access_control_list"] 5 | pub struct AccessControlListColumn { 6 | pub user_id: i32, 7 | pub region: String, 8 | pub is_unrated: Option, 9 | pub is_manager: bool, 10 | } 11 | -------------------------------------------------------------------------------- /src/controllers/samples/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod handler; 2 | 3 | use actix_web::web; 4 | 5 | pub fn route(cfg: &mut web::ServiceConfig) { 6 | cfg.service( 7 | web::scope("/samples") 8 | .service(handler::create) 9 | .service(handler::get_list) 10 | .service(handler::get) 11 | .service(handler::delete), 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod access_control_list; 2 | pub mod contests; 3 | pub mod judge_servers; 4 | pub mod languages; 5 | pub mod problem_sets; 6 | pub mod problems; 7 | pub mod ranks; 8 | pub mod region_access_settings; 9 | pub mod region_links; 10 | pub mod regions; 11 | pub mod samples; 12 | pub mod statistics; 13 | pub mod submissions; 14 | pub mod users; 15 | pub mod utils; 16 | -------------------------------------------------------------------------------- /migrations/00000000000000_diesel_initial_setup/down.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); 6 | DROP FUNCTION IF EXISTS diesel_set_updated_at(); 7 | -------------------------------------------------------------------------------- /src/controllers/problem_sets/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod handler; 2 | 3 | use actix_web::web; 4 | 5 | pub fn route(cfg: &mut web::ServiceConfig) { 6 | cfg.service( 7 | web::scope("/problem_sets") 8 | .service(handler::create) 9 | .service(handler::get_set_list) 10 | .service(handler::delete) 11 | .service(handler::update), 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/services/region/utils.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | use server_core::errors::ServiceResult; 3 | 4 | pub fn get_self_type(region: String, db_connection: &PgConnection) -> ServiceResult { 5 | use crate::schema::regions as regions_schema; 6 | 7 | Ok(regions_schema::table 8 | .filter(regions_schema::name.eq(region)) 9 | .select(regions_schema::self_type) 10 | .first(db_connection)?) 11 | } 12 | -------------------------------------------------------------------------------- /crates/server-core/src/utils/time.rs: -------------------------------------------------------------------------------- 1 | use chrono::*; 2 | 3 | pub fn get_cur_naive_date_time() -> NaiveDateTime { 4 | let local: DateTime = Local::now(); 5 | let year = local.year(); 6 | let month = local.month(); 7 | let day = local.day(); 8 | let hour = local.hour(); 9 | let minute = local.minute(); 10 | let second = local.second(); 11 | NaiveDate::from_ymd(year, month, day).and_hms(hour, minute, second) 12 | } 13 | -------------------------------------------------------------------------------- /src/services/contest/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::models::contests::*; 2 | use server_core::errors::{ServiceError, ServiceResult}; 3 | 4 | pub fn check_settings_legal(settings: ContestSettings) -> ServiceResult<()> { 5 | if !settings.view_after_end && settings.public_after_end { 6 | let hint = "Can not allow public_after_end if view_after end is true".to_owned(); 7 | return Err(ServiceError::BadRequest(hint)); 8 | } 9 | Ok(()) 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main, server-core, pg-db ] 6 | pull_request: 7 | branches: [ main, server-core, pg-db ] 8 | types: [assigned, opened, synchronize, reopened, updated] 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Build 21 | run: cargo build 22 | -------------------------------------------------------------------------------- /data/test_cases/2/info: -------------------------------------------------------------------------------- 1 | {"test_case_number":3,"spj":false,"test_cases":{"1":{"input_name":"1.in","input_size":3,"output_name":"1.out","output_size":3,"stripped_output_md5":"c81e728d9d4c2f636f067f89cc14862c"},"2":{"input_name":"2.in","input_size":6,"output_name":"2.out","output_size":6,"stripped_output_md5":"098f6bcd4621d373cade4e832627b4f6"},"3":{"input_name":"3.in","input_size":9,"output_name":"3.out","output_size":9,"stripped_output_md5":"cc03e747a6afbbcbf8be7668acfebee5"}}} -------------------------------------------------------------------------------- /src/controllers/contests/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod handler; 2 | 3 | use actix_web::web; 4 | 5 | pub fn route(cfg: &mut web::ServiceConfig) { 6 | cfg.service( 7 | web::scope("/contests") 8 | .service(handler::create) 9 | .service(handler::get_contest_list) 10 | .service(handler::register) 11 | .service(handler::get_acm_rank) 12 | .service(handler::delete) 13 | .service(handler::update), 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/models/problem_sets.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::*; 2 | 3 | #[derive(Debug, Clone, Serialize, Deserialize, Insertable, Queryable)] 4 | #[table_name = "problem_sets"] 5 | pub struct ProblemSetInfo { 6 | pub region: String, 7 | pub title: String, 8 | pub introduction: Option, 9 | } 10 | 11 | #[derive(AsChangeset)] 12 | #[table_name = "problem_sets"] 13 | pub struct ProblemSetForm { 14 | pub title: Option, 15 | pub introduction: Option, 16 | } 17 | -------------------------------------------------------------------------------- /document/RUN.md: -------------------------------------------------------------------------------- 1 | # How to run this project 2 | ## Environment prepare 3 | We use Ubuntu as the environment. 4 | 5 | To use diesel_cli, make sure that you have installed `libpq-dev`, `libsqlite3-dev` and `libmysqlclient-dev`. Then run the below command: 6 | ``` 7 | cargo install diesel_cli 8 | ``` 9 | After installation you can migrate the tables into your database. 10 | 11 | And we use `unzip` Command to unzip zip files, so you may need to install `unzip` by: 12 | ``` 13 | sudo apt install unzip 14 | ``` -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | oj-rsync-master: 4 | image: registry.cn-hangzhou.aliyuncs.com/onlinejudge/oj_rsync 5 | container_name: oj-rsync-master 6 | volumes: 7 | - $PWD/data/test_cases:/test_case:ro 8 | - $PWD/data/rsync_master:/log 9 | environment: 10 | - RSYNC_MODE=master 11 | - RSYNC_USER=ojrsync 12 | - RSYNC_PASSWORD=CHANGE_THIS_PASSWORD 13 | ports: 14 | - "0.0.0.0:873:873" -------------------------------------------------------------------------------- /docker-compose/rsync-master.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | oj-rsync-master: 4 | image: registry.cn-hangzhou.aliyuncs.com/onlinejudge/oj_rsync 5 | container_name: oj-rsync-master 6 | volumes: 7 | - $PWD/data/test_cases:/test_case:ro 8 | - $PWD/data/rsync_master:/log 9 | environment: 10 | - RSYNC_MODE=master 11 | - RSYNC_USER=ojrsync 12 | - RSYNC_PASSWORD=CHANGE_THIS_PASSWORD 13 | ports: 14 | - "0.0.0.0:873:873" -------------------------------------------------------------------------------- /src/controllers/regions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod handler; 2 | 3 | use actix_web::web; 4 | 5 | pub fn route(cfg: &mut web::ServiceConfig) { 6 | cfg.service( 7 | web::scope("/regions") 8 | .service(handler::get_list) 9 | .service(handler::insert_problems) 10 | .service(handler::get_linked_problem_column_list) 11 | .service(handler::get_linked_problem) 12 | .service(handler::create_submission) 13 | .service(handler::delete_problem), 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/models/regions.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::*; 2 | 3 | #[derive(Debug, Clone, Serialize, Deserialize, Insertable, Queryable)] 4 | #[table_name = "regions"] 5 | pub struct Region { 6 | pub name: String, 7 | pub self_type: String, 8 | pub title: String, 9 | pub has_access_setting: bool, 10 | pub introduction: Option, 11 | } 12 | 13 | #[derive(AsChangeset)] 14 | #[table_name = "regions"] 15 | pub struct RegionForm { 16 | pub title: Option, 17 | pub introduction: Option, 18 | } 19 | -------------------------------------------------------------------------------- /crates/server-core/src/database/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::ServiceError; 2 | 3 | pub mod pool; 4 | 5 | use diesel::r2d2::PoolError; 6 | 7 | type ConnectionManager = diesel::r2d2::ConnectionManager; 8 | pub type Pool = diesel::r2d2::Pool; 9 | pub type PooledConnection = diesel::r2d2::PooledConnection; 10 | 11 | pub fn db_connection(pool: &Pool) -> Result { 12 | Ok(pool.get().map_err(|_| ServiceError::UnableToConnectToDb)?) 13 | } 14 | -------------------------------------------------------------------------------- /crates/server-core/README.md: -------------------------------------------------------------------------------- 1 | # Server Core 2 | 3 | Every time you tried to build a web application, it will take some time to build the structure and prepare some basic function. **This is why we have server-core.** 4 | 5 | If it is taken for your own use, feel free to edit it. 6 | But we also have something you may need, and can save some of your time: 7 | 8 | - A basic errors handling struct ServiceError 9 | - Utils with some useful function, such as password encryption 10 | - Database functions, which enable you quickly add connection to server 11 | -------------------------------------------------------------------------------- /src/controllers/problems/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod handler; 2 | 3 | use actix_web::web; 4 | 5 | pub fn route(cfg: &mut web::ServiceConfig) { 6 | cfg.service( 7 | web::scope("/problems") 8 | .service(handler::batch_create) 9 | .service(handler::change_release_state) 10 | .service(handler::get_list) 11 | .service(handler::get_title) 12 | .service(handler::get) 13 | .service(handler::delete) 14 | .service(handler::create) 15 | .service(handler::update), 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/services/judge_server/info.rs: -------------------------------------------------------------------------------- 1 | use crate::models::judge_servers::OutJudgeServerInfo; 2 | use crate::statics::JUDGE_SERVER_INFOS; 3 | use actix_identity::Identity; 4 | use server_core::errors::ServiceResult; 5 | 6 | pub async fn server_info(_id: Identity) -> ServiceResult> { 7 | let lock = JUDGE_SERVER_INFOS.read().unwrap(); 8 | let mut info_vec: Vec = Vec::new(); 9 | for (_url, info) in lock.iter() { 10 | info_vec.push(OutJudgeServerInfo::from(info.clone())); 11 | } 12 | Ok(info_vec) 13 | } 14 | -------------------------------------------------------------------------------- /crates/server-core/src/database/pool.rs: -------------------------------------------------------------------------------- 1 | use super::{ConnectionManager, Pool, PoolError}; 2 | 3 | pub fn init_pool(database_url: &str, max_size: u32) -> Result { 4 | let manager = ConnectionManager::new(database_url); 5 | Pool::builder().max_size(max_size).build(manager) 6 | } 7 | 8 | pub fn establish_connection_with_count(database_url: &str, conn_count: u32) -> Pool { 9 | let max_size = conn_count; 10 | log::info!("Initing Database Pool with max_size={}", max_size); 11 | init_pool(database_url, max_size).expect("Failed to create pool") 12 | } 13 | -------------------------------------------------------------------------------- /crates/shupdtp-db/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shupdtp-db" 3 | version = "0.1.0" 4 | authors = ["slhmy <1484836413@qq.com>"] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | server-core = { path = "../server-core" } 11 | # ORM and Query Builder 12 | diesel = { version = "1.4.8", features = ["postgres", "uuidv07", "chrono", "r2d2"] } 13 | 14 | # Framework for serializing and deserializing 15 | serde = { version = "1.0", features = ["derive"] } 16 | serde_json = "1.0" 17 | 18 | chrono = { version = "0.4", features = ["serde"] } 19 | 20 | dotenv = "0.15" -------------------------------------------------------------------------------- /src/judge_actor/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod handler; 2 | mod statistics; 3 | mod utils; 4 | 5 | use actix::prelude::*; 6 | use server_core::database::Pool; 7 | 8 | pub struct JudgeActor { 9 | pub pool: Pool, 10 | } 11 | 12 | impl Actor for JudgeActor { 13 | type Context = SyncContext; 14 | } 15 | 16 | pub struct JudgeActorAddr { 17 | pub addr: Addr, 18 | } 19 | 20 | pub(crate) fn start_judge_actor(opt: server_core::cli_args::Opt, pool: Pool) -> Addr { 21 | info!( 22 | "Spawning {} JudgeActor in SyncArbiter", 23 | opt.judge_actor_count 24 | ); 25 | 26 | SyncArbiter::start(opt.judge_actor_count, move || JudgeActor { 27 | pool: pool.clone(), 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /document/API/Problems.md: -------------------------------------------------------------------------------- 1 | # Problems 2 | 3 | ## Get `Get /problems/{id}` 4 | ### Return `Json` 5 | Problem 6 | - `id` int 7 | - `info` ProblemInfo 8 | - `title` string 9 | - `tags` [string] 10 | - `difficulty` double 11 | - `contents` ProblemContents 12 | - `description` nullable string 13 | - `example_count` int 14 | - `examples` [Example] 15 | - `input` string 16 | - `output` string 17 | - `settings` ProblemSettings 18 | - `is_spj` bool 19 | - `high_performance_max_cpu_time` int 20 | - `high_performance_max_memory` int 21 | - `other_max_cpu_time` int 22 | - `other_max_memory` other_max_memory 23 | - `opaque_output` bool 24 | - `test_case_count` nullable int 25 | - `is_released` bool 26 | ### Explain 27 | WIP -------------------------------------------------------------------------------- /src/controllers/users/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod handler; 2 | 3 | use actix_web::web; 4 | 5 | pub fn route(cfg: &mut web::ServiceConfig) { 6 | cfg.service( 7 | web::scope("/users") 8 | .service(handler::me) 9 | .service(handler::get_permitted_methods) 10 | .service(handler::get_name) 11 | .service(handler::get) 12 | .service(handler::create) 13 | .service(handler::update) 14 | .service(handler::get_list) 15 | .service(handler::login) 16 | .service(handler::logout) 17 | .service(handler::delete) 18 | .service(handler::get_submissions_count) 19 | .service(handler::get_submissions_time), 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /crates/server-core/src/utils/encryption.rs: -------------------------------------------------------------------------------- 1 | use argon2rs::argon2i_simple; 2 | 3 | pub fn make_salt() -> String { 4 | use rand::Rng; 5 | const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\ 6 | abcdefghijklmnopqrstuvwxyz\ 7 | 0123456789)(*&^%$#@!~"; 8 | const PASSWORD_LEN: usize = 128; 9 | let mut rng = rand::thread_rng(); 10 | 11 | let password: String = (0..PASSWORD_LEN) 12 | .map(|_| { 13 | let idx = rng.gen_range(0..CHARSET.len()); 14 | CHARSET[idx] as char 15 | }) 16 | .collect(); 17 | password 18 | } 19 | 20 | pub fn make_hash(password: &str, salt: &str) -> [u8; argon2rs::defaults::LENGTH] { 21 | argon2i_simple(password, salt) 22 | } 23 | -------------------------------------------------------------------------------- /document/API/Users.md: -------------------------------------------------------------------------------- 1 | # Users 2 | 3 | ## Create `POST /users` 4 | ### Body `Json` 5 | - `account`! 6 | - `password`! 7 | - `mobile` 8 | - `role`! 9 | ### Return `null` 10 | 200 means success 11 | ### Explain 12 | Method which enables you to create a new user. 13 | 14 | *Currently mobile login is not supported. And server won't reject bad mobile strings. 15 | 16 | ## Login `POST /users/login` 17 | ### Body `Json` 18 | - `account`! 19 | - `password`! 20 | ### Return `Json` 21 | SlimUser 22 | - `id` int 23 | - `role` string 24 | 25 | ## Check Oline Info `GET /users/{id}` 26 | ### Return `Json` 27 | OutUser 28 | - `id` int 29 | - `account` string 30 | - `mobile` string 31 | - `role` string 32 | 33 | ## Logout `POST /users/logout` 34 | ### Return `null` 35 | 200 means success -------------------------------------------------------------------------------- /crates/server-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "server-core" 3 | version = "0.1.0" 4 | authors = ["slhmy <1484836413@qq.com>"] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | chrono = { version = "0.4", features = ["serde"] } 11 | argon2rs = "0.2" 12 | rand = "0.8" 13 | # ORM and Query Builder 14 | diesel = { version = "1.4", features = ["postgres", "uuidv07", "chrono", "r2d2"] } 15 | # Web framework 16 | actix-web = "3" 17 | log = "0.4" 18 | 19 | # Framework for serializing and deserializing 20 | serde = "1.0" 21 | serde_derive = "1.0" 22 | serde_json = "1.0" 23 | 24 | thiserror = "1.0" 25 | 26 | uuid = { version = "0.8", features = ["serde", "v4"] } 27 | 28 | structopt = "0.3" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Shanghai University Program Design and Training Platform 2 | 3 | ## Introduction 4 | 5 | Currently `SHUpdtp` is a simple Restful API server written in Rust. 6 | It assures running efficiency by using one of the fastest web application framework. 7 | 8 | ## Crates 9 | 10 | ### server-core 11 | 12 | It's a **reuseable crate for actix-web project**, see more in [server-core README](crates/server-core/README.md) 13 | 14 | ### shupdtp-db 15 | 16 | DB operation collection for SHUpdtp. 17 | 18 | ## Related Project 19 | 20 | [online_judge](https://github.com/slhmy/online_judge) 21 | Previous version of `SHUpdtp`. Tried some new things such as GrapQL etc. 22 | But not well structured. 23 | 24 | [ojFront](https://github.com/slhmy/ojFront) 25 | Front end server for `online_judge`. More likely to be a mobile version. 26 | Missing functionalities in creating problems or contest. 27 | -------------------------------------------------------------------------------- /src/models/ranks.rs: -------------------------------------------------------------------------------- 1 | use chrono::*; 2 | 3 | #[derive(Debug, Clone, Serialize, Deserialize)] 4 | pub struct ACMRank { 5 | pub region: String, 6 | pub last_updated_time: NaiveDateTime, 7 | pub columns: Vec, 8 | } 9 | 10 | #[derive(Debug, Clone, Serialize, Deserialize)] 11 | pub struct ACMRankColumn { 12 | pub rank: Option, 13 | pub user_id: i32, 14 | //temporary use account to show username 15 | pub account: String, 16 | pub total_accepted: i32, 17 | pub time_cost: i64, 18 | pub is_unrated: Option, 19 | pub problem_block: Vec, 20 | } 21 | 22 | #[derive(Debug, Clone, Serialize, Deserialize)] 23 | pub struct ACMProblemBlock { 24 | pub inner_id: i32, 25 | pub is_accepted: Option, 26 | pub is_first_accepted: bool, 27 | pub is_sealed: bool, 28 | pub try_times: i32, 29 | pub last_submit_time: Option, 30 | } 31 | -------------------------------------------------------------------------------- /data/test_cases/1/spj_src.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define AC 0 4 | #define WA 1 5 | #define ERROR -1 6 | 7 | int spj(FILE *input, FILE *user_output); 8 | 9 | void close_file(FILE *f){ 10 | if(f != NULL){ 11 | fclose(f); 12 | } 13 | } 14 | 15 | int main(int argc, char *args[]){ 16 | FILE *input = NULL, *user_output = NULL; 17 | int result; 18 | if(argc != 3){ 19 | printf("Usage: spj x.in x.out\n"); 20 | return ERROR; 21 | } 22 | input = fopen(args[1], "r"); 23 | user_output = fopen(args[2], "r"); 24 | if(input == NULL || user_output == NULL){ 25 | printf("Failed to open output file\n"); 26 | close_file(input); 27 | close_file(user_output); 28 | return ERROR; 29 | } 30 | 31 | result = spj(input, user_output); 32 | printf("result: %d\n", result); 33 | 34 | close_file(input); 35 | close_file(user_output); 36 | return result; 37 | } 38 | 39 | int spj(FILE *input, FILE *user_output){ 40 | return AC; 41 | } -------------------------------------------------------------------------------- /docker-compose/judge-server.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | judge_server: 4 | image: registry.cn-hangzhou.aliyuncs.com/onlinejudge/judge_server 5 | read_only: true 6 | cap_drop: 7 | - SETPCAP 8 | - MKNOD 9 | - NET_BIND_SERVICE 10 | - SYS_CHROOT 11 | - SETFCAP 12 | - FSETID 13 | tmpfs: 14 | - /tmp 15 | volumes: 16 | - $PWD/data/backend/test_cases:/test_case:ro 17 | - $PWD/data/judge_server/log:/log 18 | - $PWD/data/judge_server/run:/judger 19 | environment: 20 | - BACKEND_URL=http://172.17.0.1:8080/judge_servers/heartbeat 21 | - SERVICE_URL=http://127.0.0.1:12345 22 | - TOKEN=YOUR_TOKEN_HERE 23 | ports: 24 | - "0.0.0.0:12345:8080" 25 | 26 | oj-rsync-slave: 27 | image: registry.cn-hangzhou.aliyuncs.com/onlinejudge/oj_rsync 28 | container_name: oj-rsync-slave1 29 | volumes: 30 | - $PWD/data/backend/test_cases:/test_case 31 | - $PWD/data/rsync_slave:/log 32 | environment: 33 | - RSYNC_MODE=slave 34 | - RSYNC_USER=ojrsync 35 | - RSYNC_PASSWORD=CHANGE_THIS_PASSWORD 36 | - RSYNC_MASTER_ADDR=172.17.0.1 -------------------------------------------------------------------------------- /migrations/00000000000000_diesel_initial_setup/up.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | 6 | 7 | 8 | -- Sets up a trigger for the given table to automatically set a column called 9 | -- `updated_at` whenever the row is modified (unless `updated_at` was included 10 | -- in the modified columns) 11 | -- 12 | -- # Example 13 | -- 14 | -- ```sql 15 | -- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); 16 | -- 17 | -- SELECT diesel_manage_updated_at('users'); 18 | -- ``` 19 | CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ 20 | BEGIN 21 | EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s 22 | FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); 23 | END; 24 | $$ LANGUAGE plpgsql; 25 | 26 | CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ 27 | BEGIN 28 | IF ( 29 | NEW IS DISTINCT FROM OLD AND 30 | NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at 31 | ) THEN 32 | NEW.updated_at := current_timestamp; 33 | END IF; 34 | RETURN NEW; 35 | END; 36 | $$ LANGUAGE plpgsql; 37 | -------------------------------------------------------------------------------- /crates/shupdtp-db/tests/user.rs: -------------------------------------------------------------------------------- 1 | use server_core::database::*; 2 | use server_core::errors::ServiceResult; 3 | use server_core::utils::encryption; 4 | use shupdtp_db::user; 5 | use shupdtp_db::user::models::*; 6 | 7 | fn get_conn() -> ServiceResult { 8 | dotenv::dotenv().ok(); 9 | let database_url: String = dotenv::var("DATABASE_URL").unwrap(); 10 | let pool = server_core::database::pool::establish_connection_with_count(&database_url, 1); 11 | Ok(db_connection(&pool)?) 12 | } 13 | 14 | #[test] 15 | fn single_operations() { 16 | let conn = get_conn().unwrap(); 17 | 18 | let insertable_user = InsertableUser { 19 | salt: None, 20 | hash: None, 21 | account: "somebody".to_string(), 22 | mobile: None, 23 | role: "super".to_string(), 24 | }; 25 | user::operations::insert(&conn, &insertable_user).unwrap(); 26 | let user_form = UserForm { 27 | salt: None, 28 | hash: None, 29 | account: None, 30 | mobile: None, 31 | role: Some("student".to_string()), 32 | }; 33 | user::operations::update_by_account(&conn, "somebody".to_string(), user_form).unwrap(); 34 | let user = user::operations::get_by_account(&conn, "somebody".to_string()).unwrap(); 35 | println!("{:?}", user); 36 | user::operations::delete_by_id(&conn, user.id).unwrap(); 37 | } 38 | -------------------------------------------------------------------------------- /crates/server-core/src/cli_args.rs: -------------------------------------------------------------------------------- 1 | use structopt::StructOpt; 2 | 3 | /// Juniper (GraphQl API), Diesel PostgreSQL, session authentication and JWT boilerplate server 4 | #[derive(StructOpt, Debug, Clone)] 5 | #[structopt(name = "SHUpdtp")] 6 | pub struct Opt { 7 | /// Port to listen to 8 | #[structopt(short, long, env = "PORT", default_value = "3000")] 9 | pub port: u16, 10 | 11 | /// Count of judge actor to spawn 12 | #[structopt(short, long, env = "JUDGE_ACTOR_COUNT", default_value = "1")] 13 | pub judge_actor_count: usize, 14 | 15 | /// Domain 16 | #[structopt(long, env = "DOMAIN", default_value = "localhost")] 17 | pub domain: String, 18 | 19 | /// Database URL 20 | #[structopt(long, env = "DATABASE_URL")] 21 | pub database_url: String, 22 | 23 | /// Secret Key for Auth Cookie 24 | #[structopt( 25 | long, 26 | env = "AUTH_SECRET_KEY", 27 | default_value = "01230123012301230123012301230123" 28 | )] 29 | pub auth_secret_key: String, 30 | 31 | /// Use secure cookie (HTTPS), 32 | /// this can only be set if you have https 33 | #[structopt(long, env = "HTTPS_COOKIE")] 34 | pub secure_cookie: bool, 35 | 36 | /// Auth duration in hours, 37 | /// this is used for cookie and JWT 38 | #[structopt(long, env = "AUTH_DURATION_IN_HOUR", default_value = "24")] 39 | pub auth_duration_in_hour: u16, 40 | } 41 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["crates/server-core", "crates/shupdtp-db"] 3 | 4 | [package] 5 | name = "shu_pdtp" 6 | version = "0.1.0" 7 | authors = ["slhmy"] 8 | edition = "2018" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | # Actor model 14 | actix = "0.10.0" 15 | 16 | # Web framework 17 | actix-web = "3" 18 | # Middleware 19 | actix-identity = "0.3" 20 | actix-cors = "0.5" 21 | # Extension 22 | actix-multipart = "0.3" 23 | actix-http = "2.2" 24 | 25 | # ORM and Query Builder 26 | diesel = { version = "1.4.8", features = ["postgres", "uuidv07", "chrono", "r2d2"] } 27 | 28 | # Framework for serializing and deserializing 29 | serde = "1.0" 30 | serde_derive = "1.0" 31 | serde_json = "1.0" 32 | 33 | # Other libraries 34 | thiserror = "1.0" 35 | chrono = { version = "0.4", features = ["serde"] } 36 | uuid = { version = "0.8", features = ["serde", "v4"] } 37 | structopt = "0.3" 38 | dotenv = "0.15" 39 | log = "0.4" 40 | env_logger = "0.9" 41 | argon2rs = "0.2" 42 | rand = "0.8" 43 | time = "0.2" 44 | shrinkwraprs = "0.2" 45 | futures = "0.3" 46 | toml = "0.5" 47 | sha2 = "0.10.2" 48 | sha-1 = "0.10.0" 49 | md-5 = "0.9.1" 50 | hex = "0.4" 51 | hmac = "0.12.1" 52 | base64 = "0.13.0" 53 | digest = "0.9.0" 54 | lazy_static = "1.1" 55 | regex = "1.4" 56 | 57 | server-core = { path = "crates/server-core" } 58 | shupdtp-db = { path = "crates/shupdtp-db" } -------------------------------------------------------------------------------- /src/statics.rs: -------------------------------------------------------------------------------- 1 | use crate::models::{ 2 | judge_servers::JudgeServerInfo, ranks::ACMRank, statistics::SubmissionStatistics, 3 | users::AuthConfig, 4 | }; 5 | use regex::Regex; 6 | use std::io::Read; 7 | use std::{ 8 | collections::{HashMap, VecDeque}, 9 | sync::RwLock, 10 | }; 11 | use uuid::Uuid; 12 | 13 | lazy_static! { 14 | pub static ref WAITING_QUEUE: RwLock> = RwLock::new(VecDeque::new()); 15 | pub static ref RESULT_STATISTICS_CACHE: RwLock> = 16 | RwLock::new(HashMap::new()); 17 | pub static ref ACM_RANK_CACHE: RwLock> = RwLock::new(HashMap::new()); 18 | pub static ref JUDGE_SERVER_INFOS: RwLock> = 19 | RwLock::new(HashMap::new()); 20 | pub static ref RE_EMAIL: Regex = 21 | Regex::new(r"^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$").unwrap(); 22 | pub static ref RE_MOBILE: Regex = 23 | Regex::new(r"^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(18[0,5-9]))\d{8}$").unwrap(); 24 | pub static ref RE_PASSWORD: Regex = Regex::new(r"^\S{6,20}$").unwrap(); 25 | pub static ref AUTH_CONFIG: HashMap = { 26 | let mut file = std::fs::File::open("auth.toml").unwrap(); 27 | let mut content = String::new(); 28 | file.read_to_string(&mut content).unwrap(); 29 | toml::from_str(&content).unwrap() 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/models/judge_servers.rs: -------------------------------------------------------------------------------- 1 | use std::time::SystemTime; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct JudgeServerInfo { 5 | pub judger_version: String, 6 | pub hostname: String, 7 | pub cpu_core: i32, 8 | pub memory: f32, 9 | pub cpu: f32, 10 | pub task_number: i32, 11 | pub service_url: String, 12 | pub token: String, 13 | pub heartbeat_time: SystemTime, 14 | pub is_deprecated: bool, 15 | } 16 | 17 | #[derive(Debug, Clone, Serialize, Deserialize)] 18 | pub struct OutJudgeServerInfo { 19 | pub judger_version: String, 20 | pub hostname: String, 21 | pub cpu_core: i32, 22 | pub memory: f32, 23 | pub cpu: f32, 24 | pub task_number: i32, 25 | pub service_url: String, 26 | pub token: String, 27 | pub heartbeat_time: SystemTime, 28 | pub last_heartbeat: i32, 29 | pub is_deprecated: bool, 30 | } 31 | 32 | impl From for OutJudgeServerInfo { 33 | fn from(raw: JudgeServerInfo) -> Self { 34 | Self { 35 | judger_version: raw.judger_version, 36 | hostname: raw.hostname, 37 | cpu_core: raw.cpu_core, 38 | memory: raw.memory, 39 | cpu: raw.cpu, 40 | task_number: raw.task_number, 41 | service_url: raw.service_url, 42 | token: raw.token, 43 | heartbeat_time: raw.heartbeat_time, 44 | last_heartbeat: raw.heartbeat_time.elapsed().unwrap().as_secs() as i32, 45 | is_deprecated: raw.is_deprecated, 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/models/samples.rs: -------------------------------------------------------------------------------- 1 | use super::submissions; 2 | use crate::schema::*; 3 | use chrono::*; 4 | use uuid::Uuid; 5 | 6 | #[derive(Debug, Clone, Queryable)] 7 | pub struct RawSample { 8 | pub submission_id: Uuid, 9 | pub description: Option, 10 | } 11 | 12 | #[derive(Debug, Clone, Insertable)] 13 | #[table_name = "samples"] 14 | pub struct InsertableSample { 15 | pub submission_id: Uuid, 16 | pub description: Option, 17 | } 18 | 19 | #[derive(Debug, Clone, Serialize, Deserialize)] 20 | pub struct Sample { 21 | pub submission_id: Uuid, 22 | pub description: Option, 23 | pub submission: submissions::Submission, 24 | } 25 | 26 | #[derive(Debug, Clone, Serialize, Deserialize)] 27 | pub struct SlimSample { 28 | pub submission_id: Uuid, 29 | pub problem_id: i32, 30 | pub language: Option, 31 | pub description: Option, 32 | pub submission_state: String, 33 | pub is_accepted: Option, 34 | pub submit_time: NaiveDateTime, 35 | pub err: Option, 36 | } 37 | 38 | impl From for SlimSample { 39 | fn from(raw: Sample) -> Self { 40 | Self { 41 | submission_id: raw.submission_id, 42 | problem_id: raw.submission.problem_id, 43 | language: raw.submission.language, 44 | description: raw.description, 45 | submission_state: raw.submission.state, 46 | is_accepted: raw.submission.is_accepted, 47 | submit_time: raw.submission.submit_time, 48 | err: raw.submission.err, 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/judge_actor/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::statics::JUDGE_SERVER_INFOS; 2 | use std::io::{BufRead, BufReader, Write}; 3 | use std::process::{Command, Stdio}; 4 | 5 | pub fn choose_judge_server() -> Option<(String, String)> { 6 | let lock = JUDGE_SERVER_INFOS.read().unwrap(); 7 | for (url, info) in lock.iter() { 8 | let last_heartbeat = info.heartbeat_time.elapsed().unwrap().as_secs() as i32; 9 | if !info.is_deprecated && info.task_number + 1 <= info.cpu_core * 2 && last_heartbeat <= 5 { 10 | return Some((url.to_owned(), info.token.clone())); 11 | } 12 | } 13 | None 14 | } 15 | 16 | pub fn run_judge_client(token: String, url: String, judge_setting: String) -> String { 17 | // 启动子进程 18 | let mut p = Command::new("./executable/send_judge_request") 19 | .stdin(Stdio::piped()) // 将子进程的标准输入重定向到管道 20 | .stdout(Stdio::piped()) // 将子进程的标准输出重定向到管道 21 | .spawn() 22 | .unwrap(); 23 | 24 | let p_stdin = p.stdin.as_mut().unwrap(); 25 | let mut p_stdout = BufReader::new(p.stdout.as_mut().unwrap()); 26 | let mut line = String::new(); 27 | 28 | p_stdin.write(token.as_bytes()).unwrap(); 29 | p_stdin.write("\n".as_bytes()).unwrap(); // 发送\n,子进程的read_line才会响应 30 | 31 | p_stdin.write(url.as_bytes()).unwrap(); 32 | p_stdin.write("\n".as_bytes()).unwrap(); 33 | 34 | p_stdin.write(judge_setting.as_bytes()).unwrap(); 35 | p_stdin.write("\n".as_bytes()).unwrap(); 36 | 37 | // 接收消息 38 | line.clear(); // 需要清空,否则会保留上次的结果 39 | p_stdout.read_line(&mut line).unwrap(); 40 | // 等待子进程结束 41 | p.wait().unwrap(); 42 | 43 | line.trim().to_owned() 44 | } 45 | -------------------------------------------------------------------------------- /crates/shupdtp-db/src/user/operations.rs: -------------------------------------------------------------------------------- 1 | use super::models::*; 2 | use crate::schema::users as users_schema; 3 | use diesel::pg::PgConnection; 4 | use diesel::prelude::*; 5 | use server_core::errors::ServiceResult; 6 | 7 | pub fn insert(conn: &PgConnection, insertable_user: &InsertableUser) -> ServiceResult<()> { 8 | diesel::insert_into(users_schema::table) 9 | .values(insertable_user) 10 | .execute(conn)?; 11 | 12 | Ok(()) 13 | } 14 | 15 | pub fn get_by_id(conn: &PgConnection, id: i32) -> ServiceResult { 16 | Ok(users_schema::table 17 | .filter(users_schema::id.eq(id)) 18 | .first::(conn)?) 19 | } 20 | 21 | pub fn get_by_account(conn: &PgConnection, account: String) -> ServiceResult { 22 | Ok(users_schema::table 23 | .filter(users_schema::account.eq(account)) 24 | .first::(conn)?) 25 | } 26 | 27 | pub fn update_by_id(conn: &PgConnection, id: i32, user_form: UserForm) -> ServiceResult<()> { 28 | diesel::update(users_schema::table.filter(users_schema::id.eq(id))) 29 | .set(user_form) 30 | .execute(conn)?; 31 | 32 | Ok(()) 33 | } 34 | 35 | pub fn update_by_account( 36 | conn: &PgConnection, 37 | account: String, 38 | user_form: UserForm, 39 | ) -> ServiceResult<()> { 40 | diesel::update(users_schema::table.filter(users_schema::account.eq(account))) 41 | .set(user_form) 42 | .execute(conn)?; 43 | 44 | Ok(()) 45 | } 46 | 47 | pub fn delete_by_id(conn: &PgConnection, id: i32) -> ServiceResult<()> { 48 | diesel::delete(users_schema::table.filter(users_schema::id.eq(id))).execute(conn)?; 49 | 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /src/controllers/judge_servers/handler.rs: -------------------------------------------------------------------------------- 1 | use crate::judge_actor::JudgeActorAddr; 2 | use crate::services::judge_server::*; 3 | use actix_identity::Identity; 4 | use actix_web::{delete, get, post, put, web, HttpRequest, HttpResponse}; 5 | use server_core::errors::ServiceError; 6 | 7 | #[derive(Debug, Clone, Deserialize, Serialize)] 8 | pub struct HeartbeatBody { 9 | pub judger_version: String, 10 | pub hostname: String, 11 | pub cpu_core: i32, 12 | pub memory: f32, 13 | pub cpu: f32, 14 | pub service_url: Option, 15 | } 16 | 17 | #[derive(Debug, Clone, Serialize)] 18 | struct HeartbeatResponse { 19 | data: String, 20 | error: Option, 21 | } 22 | 23 | #[post("/heartbeat")] 24 | pub async fn handle_heartbeat( 25 | body: web::Json, 26 | req: HttpRequest, 27 | judge_actor: web::Data, 28 | ) -> Result { 29 | let token = req 30 | .headers() 31 | .get("x-judge-server-token") 32 | .unwrap() 33 | .to_str() 34 | .unwrap() 35 | .to_string(); 36 | 37 | record_server_info( 38 | body.judger_version.clone(), 39 | body.hostname.clone(), 40 | body.cpu_core, 41 | body.memory, 42 | body.cpu, 43 | body.service_url.clone(), 44 | token.clone(), 45 | judge_actor, 46 | ) 47 | .await?; 48 | 49 | Ok(HttpResponse::Ok() 50 | .set_header("X-Judge-Server-Token", token) 51 | .set_header("Content-Type", "application/json") 52 | .json(HeartbeatResponse { 53 | data: "success".to_owned(), 54 | error: None, 55 | })) 56 | } 57 | 58 | #[get("/info")] 59 | pub async fn get_server_info(id: Identity) -> Result { 60 | server_info(id) 61 | .await 62 | .map(|res| HttpResponse::Ok().json(&res)) 63 | } 64 | -------------------------------------------------------------------------------- /src/models/region_links.rs: -------------------------------------------------------------------------------- 1 | use super::problems; 2 | use crate::schema::*; 3 | use server_core::errors::ServiceResult; 4 | 5 | #[derive(Debug, Clone, Serialize, Deserialize, Insertable, Queryable)] 6 | #[table_name = "region_links"] 7 | pub struct RegionLink { 8 | pub region: String, 9 | pub inner_id: i32, 10 | pub problem_id: i32, 11 | pub score: Option, 12 | } 13 | 14 | #[derive(Debug, Clone, Serialize, Deserialize)] 15 | pub struct CreateRegionLinksResult { 16 | pub problem_id: i32, 17 | pub inner_id: Option, 18 | pub is_success: bool, 19 | } 20 | 21 | #[derive(Debug, Clone, Serialize, Deserialize, Queryable)] 22 | pub struct RawLinkedProblemColumn { 23 | pub region: String, 24 | pub inner_id: i32, 25 | pub problem_id: i32, 26 | pub problem_title: String, 27 | pub problem_tags: Vec, 28 | pub problem_difficulty: f64, 29 | pub is_released: bool, 30 | } 31 | 32 | #[derive(Debug, Clone, Serialize, Deserialize)] 33 | pub struct LinkedProblemColumn { 34 | pub region: String, 35 | pub inner_id: i32, 36 | pub out_problem: problems::OutProblem, 37 | pub submit_times: i32, 38 | pub accept_times: i32, 39 | pub error_times: i32, 40 | } 41 | 42 | use crate::models::statistics::get_results; 43 | use server_core::database::*; 44 | pub fn get_column_from_raw( 45 | conn: &PooledConnection, 46 | raw: RawLinkedProblemColumn, 47 | ) -> ServiceResult { 48 | let statistic = get_results(conn, raw.region.clone(), raw.problem_id)?; 49 | 50 | Ok(LinkedProblemColumn { 51 | region: raw.region, 52 | inner_id: raw.inner_id, 53 | out_problem: problems::OutProblem { 54 | id: raw.problem_id, 55 | info: problems::ProblemInfo { 56 | title: raw.problem_title, 57 | tags: raw.problem_tags, 58 | difficulty: raw.problem_difficulty, 59 | }, 60 | is_released: raw.is_released, 61 | }, 62 | submit_times: statistic.submit_times, 63 | accept_times: statistic.accept_times, 64 | error_times: statistic.error_times, 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /src/services/judge_server/heartbeat.rs: -------------------------------------------------------------------------------- 1 | use crate::judge_actor::{handler::StartJudge, JudgeActorAddr}; 2 | use crate::models::judge_servers::JudgeServerInfo; 3 | use crate::statics::JUDGE_SERVER_INFOS; 4 | use actix_web::client::Client; 5 | use actix_web::web; 6 | use server_core::errors::ServiceResult; 7 | use std::time::SystemTime; 8 | 9 | pub async fn record_server_info( 10 | judger_version: String, 11 | hostname: String, 12 | cpu_core: i32, 13 | memory: f32, 14 | cpu: f32, 15 | service_url: Option, 16 | token: String, 17 | judge_actor: web::Data, 18 | ) -> ServiceResult<()> { 19 | if !service_url.is_none() { 20 | let url = service_url.clone().unwrap(); 21 | let task_number = { 22 | let lock = JUDGE_SERVER_INFOS.read().unwrap(); 23 | if lock.get(&url).is_none() { 24 | 0 25 | } else { 26 | let target = lock.get(&url).unwrap(); 27 | target.task_number 28 | } 29 | }; 30 | 31 | let response = Client::new() 32 | .post(format!("{}/ping", url)) 33 | .set_header("X-Judge-Server-Token", token.clone()) 34 | .set_header("Content-Type", "application/json") 35 | .send() 36 | .await; 37 | 38 | let is_deprecated = { 39 | if !response.is_ok() { 40 | info!("setting is_deprecated to true"); 41 | true 42 | } else { 43 | false 44 | } 45 | }; 46 | 47 | let now = SystemTime::now(); 48 | let judge_server_info = JudgeServerInfo { 49 | judger_version: judger_version.clone(), 50 | hostname: hostname.clone(), 51 | cpu_core: cpu_core, 52 | memory: memory, 53 | cpu: cpu, 54 | task_number: task_number, 55 | service_url: url, 56 | token: token.clone(), 57 | heartbeat_time: now, 58 | is_deprecated: is_deprecated, 59 | }; 60 | let mut lock = JUDGE_SERVER_INFOS.write().unwrap(); 61 | lock.insert(service_url.clone().unwrap(), judge_server_info); 62 | 63 | if !is_deprecated { 64 | judge_actor.addr.do_send(StartJudge()); 65 | } 66 | } 67 | 68 | Ok(()) 69 | } 70 | -------------------------------------------------------------------------------- /crates/shupdtp-db/src/user/models.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::*; 2 | use chrono::NaiveDate; 3 | 4 | #[derive(Debug, Clone, Serialize, Deserialize, Queryable)] 5 | pub struct User { 6 | pub id: i32, 7 | pub salt: Option, 8 | pub hash: Option>, 9 | pub account: String, 10 | pub mobile: Option, 11 | pub role: String, 12 | } 13 | 14 | #[derive(Debug, Insertable)] 15 | #[table_name = "users"] 16 | pub struct InsertableUser { 17 | pub salt: Option, 18 | pub hash: Option>, 19 | pub account: String, 20 | pub mobile: Option, 21 | pub role: String, 22 | } 23 | 24 | #[derive(Serialize)] 25 | pub struct OutUser { 26 | pub id: i32, 27 | pub account: String, 28 | pub mobile: Option, 29 | pub role: String, 30 | } 31 | 32 | impl From for OutUser { 33 | fn from(user: User) -> Self { 34 | Self { 35 | id: user.id, 36 | account: user.account, 37 | mobile: user.mobile, 38 | role: user.role, 39 | } 40 | } 41 | } 42 | 43 | #[derive(AsChangeset)] 44 | #[table_name = "users"] 45 | pub struct UserForm { 46 | pub salt: Option, 47 | pub hash: Option>, 48 | pub account: Option, 49 | pub mobile: Option, 50 | pub role: Option, 51 | } 52 | 53 | #[derive(Debug, Clone, Serialize, Deserialize)] 54 | pub struct SlimUser { 55 | pub id: i32, 56 | pub role: String, 57 | } 58 | 59 | impl From for SlimUser { 60 | fn from(user: User) -> Self { 61 | Self { 62 | id: user.id, 63 | role: user.role, 64 | } 65 | } 66 | } 67 | 68 | #[derive(Debug, Clone, Serialize, Deserialize)] 69 | pub struct AuthConfig { 70 | pub sup: Option>, 71 | pub admin: Option>, 72 | pub teacher: Option>, 73 | pub student: Option>, 74 | pub net_friend: Option>, 75 | pub others: Option>, 76 | } 77 | 78 | #[derive(Debug, Clone, Serialize, Deserialize)] 79 | pub struct UserSubmissionCount { 80 | pub total_submit_times: i32, 81 | pub total_accept_times: i32, 82 | pub navie_submit_times: i32, 83 | pub navie_accept_times: i32, 84 | pub easy_submit_times: i32, 85 | pub easy_accept_times: i32, 86 | pub middle_submit_times: i32, 87 | pub middle_accept_times: i32, 88 | pub hard_submit_times: i32, 89 | pub hard_accept_times: i32, 90 | } 91 | 92 | #[derive(Debug, Clone, Serialize, Deserialize)] 93 | pub struct UserSubmissionTime { 94 | pub date: NaiveDate, 95 | pub count: i32, 96 | } 97 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde_derive; 3 | #[macro_use] 4 | extern crate lazy_static; 5 | #[macro_use] 6 | extern crate diesel; 7 | #[macro_use] 8 | extern crate log; 9 | 10 | mod auth; 11 | mod controllers; 12 | mod judge_actor; 13 | mod models; 14 | mod schema; 15 | mod services; 16 | mod statics; 17 | 18 | use actix_cors::Cors; 19 | use actix_identity::{CookieIdentityPolicy, IdentityService}; 20 | use actix_web::middleware::Logger; 21 | use actix_web::{App, HttpResponse, HttpServer}; 22 | 23 | #[actix_web::get("/")] 24 | async fn hello() -> impl actix_web::Responder { 25 | HttpResponse::Ok().body("Hello world!") 26 | } 27 | 28 | #[actix_web::main] 29 | async fn main() -> std::io::Result<()> { 30 | dotenv::dotenv().ok(); 31 | 32 | env_logger::init(); 33 | 34 | // Get options 35 | let opt = { 36 | use structopt::StructOpt; 37 | server_core::cli_args::Opt::from_args() 38 | }; 39 | 40 | let pool = server_core::database::pool::establish_connection_with_count( 41 | &opt.database_url, 42 | opt.judge_actor_count as u32 + 10, 43 | ); 44 | let _domain = opt.domain.clone(); 45 | let cookie_secret_key = opt.auth_secret_key.clone(); 46 | let _secure_cookie = opt.secure_cookie; 47 | let auth_duration = time::Duration::hours(i64::from(opt.auth_duration_in_hour)); 48 | 49 | let judge_actor_addr = judge_actor::start_judge_actor(opt.clone(), pool.clone()); 50 | 51 | HttpServer::new(move || { 52 | App::new() 53 | .data(pool.clone()) 54 | .data(judge_actor::JudgeActorAddr { 55 | addr: judge_actor_addr.clone(), 56 | }) 57 | .wrap(Logger::default()) 58 | .wrap(Cors::permissive()) 59 | .wrap(IdentityService::new( 60 | CookieIdentityPolicy::new(cookie_secret_key.as_bytes()) 61 | .name("auth") 62 | .path("/") 63 | // .domain(&domain) 64 | // Time from creation that cookie remains valid 65 | .max_age_time(auth_duration) 66 | // .same_site(actix_web::cookie::SameSite::None) 67 | // Restricted to https? 68 | .secure(false), 69 | )) 70 | .service(hello) 71 | .configure(controllers::users::route) 72 | .configure(controllers::problems::route) 73 | .configure(controllers::judge_servers::route) 74 | .configure(controllers::submissions::route) 75 | .configure(controllers::samples::route) 76 | .configure(controllers::regions::route) 77 | .configure(controllers::problem_sets::route) 78 | .configure(controllers::contests::route) 79 | }) 80 | .bind(("0.0.0.0", opt.port)) 81 | .unwrap() 82 | .run() 83 | .await 84 | } 85 | -------------------------------------------------------------------------------- /src/schema.rs: -------------------------------------------------------------------------------- 1 | table! { 2 | access_control_list (region, user_id) { 3 | user_id -> Int4, 4 | region -> Text, 5 | is_unrated -> Nullable, 6 | is_manager -> Bool, 7 | } 8 | } 9 | 10 | table! { 11 | contests (region) { 12 | region -> Text, 13 | title -> Text, 14 | introduction -> Nullable, 15 | start_time -> Timestamp, 16 | end_time -> Nullable, 17 | seal_time -> Nullable, 18 | settings -> Text, 19 | } 20 | } 21 | 22 | table! { 23 | problem_sets (region) { 24 | region -> Text, 25 | title -> Text, 26 | introduction -> Nullable, 27 | } 28 | } 29 | 30 | table! { 31 | problems (id) { 32 | id -> Int4, 33 | title -> Text, 34 | tags -> Array, 35 | difficulty -> Float8, 36 | contents -> Text, 37 | settings -> Text, 38 | is_released -> Bool, 39 | } 40 | } 41 | 42 | table! { 43 | region_access_settings (region) { 44 | region -> Text, 45 | salt -> Nullable, 46 | hash -> Nullable, 47 | } 48 | } 49 | 50 | table! { 51 | region_links (inner_id, region) { 52 | region -> Text, 53 | inner_id -> Int4, 54 | problem_id -> Int4, 55 | score -> Nullable, 56 | } 57 | } 58 | 59 | table! { 60 | regions (name, self_type) { 61 | name -> Text, 62 | self_type -> Text, 63 | title -> Text, 64 | has_access_setting -> Bool, 65 | introduction -> Nullable, 66 | } 67 | } 68 | 69 | table! { 70 | samples (submission_id) { 71 | submission_id -> Uuid, 72 | description -> Nullable, 73 | } 74 | } 75 | 76 | table! { 77 | submissions (id) { 78 | id -> Uuid, 79 | problem_id -> Int4, 80 | user_id -> Int4, 81 | region -> Nullable, 82 | state -> Text, 83 | settings -> Text, 84 | result -> Nullable, 85 | submit_time -> Timestamp, 86 | is_accepted -> Nullable, 87 | finish_time -> Nullable, 88 | max_time -> Nullable, 89 | max_memory -> Nullable, 90 | language -> Nullable, 91 | err -> Nullable, 92 | out_results -> Nullable>, 93 | } 94 | } 95 | 96 | table! { 97 | users (id) { 98 | id -> Int4, 99 | salt -> Nullable, 100 | hash -> Nullable, 101 | account -> Text, 102 | mobile -> Nullable, 103 | role -> Text, 104 | } 105 | } 106 | 107 | allow_tables_to_appear_in_same_query!( 108 | access_control_list, 109 | contests, 110 | problem_sets, 111 | problems, 112 | region_access_settings, 113 | region_links, 114 | regions, 115 | samples, 116 | submissions, 117 | users, 118 | ); 119 | -------------------------------------------------------------------------------- /crates/shupdtp-db/src/schema.rs: -------------------------------------------------------------------------------- 1 | table! { 2 | access_control_list (region, user_id) { 3 | user_id -> Int4, 4 | region -> Text, 5 | is_unrated -> Nullable, 6 | is_manager -> Bool, 7 | } 8 | } 9 | 10 | table! { 11 | contests (region) { 12 | region -> Text, 13 | title -> Text, 14 | introduction -> Nullable, 15 | start_time -> Timestamp, 16 | end_time -> Nullable, 17 | seal_time -> Nullable, 18 | settings -> Text, 19 | } 20 | } 21 | 22 | table! { 23 | problem_sets (region) { 24 | region -> Text, 25 | title -> Text, 26 | introduction -> Nullable, 27 | } 28 | } 29 | 30 | table! { 31 | problems (id) { 32 | id -> Int4, 33 | title -> Text, 34 | tags -> Array, 35 | difficulty -> Float8, 36 | contents -> Text, 37 | settings -> Text, 38 | is_released -> Bool, 39 | } 40 | } 41 | 42 | table! { 43 | region_access_settings (region) { 44 | region -> Text, 45 | salt -> Nullable, 46 | hash -> Nullable, 47 | } 48 | } 49 | 50 | table! { 51 | region_links (inner_id, region) { 52 | region -> Text, 53 | inner_id -> Int4, 54 | problem_id -> Int4, 55 | score -> Nullable, 56 | } 57 | } 58 | 59 | table! { 60 | regions (name, self_type) { 61 | name -> Text, 62 | self_type -> Text, 63 | title -> Text, 64 | has_access_setting -> Bool, 65 | introduction -> Nullable, 66 | } 67 | } 68 | 69 | table! { 70 | samples (submission_id) { 71 | submission_id -> Uuid, 72 | description -> Nullable, 73 | } 74 | } 75 | 76 | table! { 77 | submissions (id) { 78 | id -> Uuid, 79 | problem_id -> Int4, 80 | user_id -> Int4, 81 | region -> Nullable, 82 | state -> Text, 83 | settings -> Text, 84 | result -> Nullable, 85 | submit_time -> Timestamp, 86 | is_accepted -> Nullable, 87 | finish_time -> Nullable, 88 | max_time -> Nullable, 89 | max_memory -> Nullable, 90 | language -> Nullable, 91 | err -> Nullable, 92 | out_results -> Nullable>, 93 | } 94 | } 95 | 96 | table! { 97 | users (id) { 98 | id -> Int4, 99 | salt -> Nullable, 100 | hash -> Nullable, 101 | account -> Text, 102 | mobile -> Nullable, 103 | role -> Text, 104 | } 105 | } 106 | 107 | allow_tables_to_appear_in_same_query!( 108 | access_control_list, 109 | contests, 110 | problem_sets, 111 | problems, 112 | region_access_settings, 113 | region_links, 114 | regions, 115 | samples, 116 | submissions, 117 | users, 118 | ); 119 | -------------------------------------------------------------------------------- /examples/send_judge_request.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde_derive; 3 | 4 | use actix_http::Error; 5 | use actix_web::client::Client; 6 | use std::io; 7 | use std::str; 8 | use std::time::Duration; 9 | 10 | #[derive(Debug, Clone, Serialize, Deserialize)] 11 | pub struct CompileConfig { 12 | pub src_name: String, 13 | pub exe_name: String, 14 | pub max_cpu_time: i32, 15 | pub max_real_time: i32, 16 | pub max_memory: i32, 17 | pub compile_command: String, 18 | } 19 | 20 | #[derive(Debug, Clone, Serialize, Deserialize)] 21 | pub struct RunConfig { 22 | pub command: String, 23 | pub seccomp_rule: Option, 24 | pub env: Vec, 25 | pub memory_limit_check_only: i32, 26 | } 27 | 28 | #[derive(Debug, Clone, Serialize, Deserialize)] 29 | pub struct LanguageConfig { 30 | pub compile: CompileConfig, 31 | pub run: RunConfig, 32 | } 33 | 34 | #[derive(Debug, Clone, Serialize, Deserialize)] 35 | pub struct SpjConfig { 36 | pub exe_name: String, 37 | pub command: String, 38 | pub seccomp_rule: String, 39 | } 40 | 41 | #[derive(Debug, Clone, Serialize, Deserialize)] 42 | pub struct SpjCompileConfig { 43 | pub src_name: String, 44 | pub exe_name: String, 45 | pub max_cpu_time: i32, 46 | pub max_real_time: i32, 47 | pub max_memory: i32, 48 | pub compile_command: String, 49 | } 50 | 51 | #[derive(Debug, Clone, Serialize, Deserialize)] 52 | pub struct TestCase { 53 | pub input: String, 54 | pub output: String, 55 | } 56 | 57 | #[derive(Debug, Clone, Serialize, Deserialize)] 58 | struct JudgeSetting { 59 | language_config: LanguageConfig, 60 | src: String, 61 | max_cpu_time: i32, 62 | max_memory: i32, 63 | test_case_id: Option, 64 | test_case: Option>, 65 | spj_version: Option, 66 | spj_config: Option, 67 | spj_compile_config: Option, 68 | spj_src: Option, 69 | output: bool, 70 | } 71 | 72 | #[actix_web::main] 73 | async fn main() -> Result<(), Error> { 74 | let stdin = io::stdin(); 75 | let mut token = String::new(); 76 | stdin.read_line(&mut token)?; 77 | let mut url = String::new(); 78 | stdin.read_line(&mut url)?; 79 | let mut judge_setting_string = String::new(); 80 | stdin.read_line(&mut judge_setting_string)?; 81 | let judge_setting: JudgeSetting = serde_json::from_str(&judge_setting_string.trim())?; 82 | let time_out = (120) as u64; 83 | 84 | // Create request builder, configure request and send 85 | let mut response = Client::new() 86 | .post(format!("{}/judge", url.trim())) 87 | .set_header("X-Judge-Server-Token", token.trim()) 88 | .set_header("Content-Type", "application/json") 89 | .timeout(Duration::new(time_out, 0)) 90 | .send_json(&judge_setting) 91 | .await?; 92 | 93 | let result_vec = response.body().await?.to_vec(); 94 | let result_str = str::from_utf8(&result_vec)?; 95 | println!("{}", result_str.trim()); 96 | 97 | Ok(()) 98 | } 99 | -------------------------------------------------------------------------------- /crates/server-core/src/errors.rs: -------------------------------------------------------------------------------- 1 | use actix_web::error::BlockingError; 2 | use actix_web::{error::ResponseError, HttpResponse}; 3 | use diesel::result::Error as DBError; 4 | use std::convert::From; 5 | use std::io::Error as IOError; 6 | use thiserror::Error; 7 | 8 | #[derive(Debug, Error, Serialize)] 9 | pub enum ServiceError { 10 | #[error("Internal Server Error")] 11 | InternalServerError, 12 | 13 | #[error("Internal Server Error: {0}")] 14 | InternalServerErrorWithHint(String), 15 | 16 | #[error("BadRequest: {0}")] 17 | BadRequest(String), 18 | 19 | #[error("Unauthorized")] 20 | Unauthorized, 21 | 22 | #[error("Unauthorized: {0}")] 23 | UnauthorizedWithHint(String), 24 | 25 | #[error("Unable to connect to DB")] 26 | UnableToConnectToDb, 27 | } 28 | 29 | // impl ResponseError trait allows to convert our errors into http responses with appropriate data 30 | impl ResponseError for ServiceError { 31 | fn error_response(&self) -> HttpResponse { 32 | match self { 33 | ServiceError::InternalServerError => { 34 | HttpResponse::InternalServerError().json("Internal Server Error, Please try later") 35 | } 36 | ServiceError::InternalServerErrorWithHint(ref message) => { 37 | HttpResponse::InternalServerError().json(message) 38 | } 39 | ServiceError::UnableToConnectToDb => HttpResponse::InternalServerError() 40 | .json("Unable to connect to DB, Please try later"), 41 | ServiceError::BadRequest(ref message) => HttpResponse::BadRequest().json(message), 42 | ServiceError::UnauthorizedWithHint(ref message) => { 43 | HttpResponse::Unauthorized().json(message) 44 | } 45 | ServiceError::Unauthorized => HttpResponse::Unauthorized().json("Unauthorized"), 46 | } 47 | } 48 | } 49 | 50 | // we can return early in our handlers if UUID is not valid 51 | // and provide a custom message 52 | impl From for ServiceError { 53 | fn from(_: uuid::Error) -> ServiceError { 54 | ServiceError::BadRequest("Invalid UUID".into()) 55 | } 56 | } 57 | 58 | impl From for ServiceError { 59 | fn from(error: DBError) -> ServiceError { 60 | // Right now we just care about UniqueViolation from diesel 61 | // But this would be helpful to easily map errors as our app grows 62 | let message = format!("{:?}", error); 63 | ServiceError::InternalServerErrorWithHint(message) 64 | } 65 | } 66 | 67 | impl From for ServiceError { 68 | fn from(error: IOError) -> ServiceError { 69 | match error.kind() { 70 | std::io::ErrorKind::NotFound => { 71 | let message = "An entity was not found, often a file.".to_string(); 72 | ServiceError::BadRequest(message) 73 | } 74 | _ => { 75 | let message = "Something went wrong with file analysis, please check your format." 76 | .to_string(); 77 | ServiceError::BadRequest(message) 78 | } 79 | } 80 | } 81 | } 82 | 83 | impl From> for ServiceError { 84 | fn from(error: BlockingError) -> ServiceError { 85 | match error { 86 | // If not canceled, then return the raw error. 87 | BlockingError::Error(e) => e, 88 | BlockingError::Canceled => ServiceError::InternalServerError, 89 | } 90 | } 91 | } 92 | 93 | pub type ServiceResult = std::result::Result; 94 | -------------------------------------------------------------------------------- /src/controllers/submissions/handler.rs: -------------------------------------------------------------------------------- 1 | use crate::judge_actor::JudgeActorAddr; 2 | use crate::models::users::LoggedUser; 3 | use crate::services::submission; 4 | use actix_web::{get, post, put, web, HttpResponse}; 5 | use server_core::database::{db_connection, Pool}; 6 | use server_core::errors::ServiceError; 7 | use uuid::Uuid; 8 | 9 | #[derive(Deserialize)] 10 | pub struct CreateSubmissionBody { 11 | region: Option, 12 | problem_id: i32, 13 | src: String, 14 | language: String, 15 | } 16 | 17 | #[post("")] 18 | pub async fn create( 19 | body: web::Json, 20 | pool: web::Data, 21 | logged_user: LoggedUser, 22 | judge_actor: web::Data, 23 | ) -> Result { 24 | info!("{:?}", logged_user.0); 25 | if logged_user.0.is_none() { 26 | return Err(ServiceError::Unauthorized); 27 | } 28 | 29 | let res = web::block(move || { 30 | submission::create( 31 | body.region.clone(), 32 | body.problem_id, 33 | logged_user.0.unwrap().id, 34 | body.src.clone(), 35 | body.language.clone(), 36 | pool, 37 | judge_actor, 38 | ) 39 | }) 40 | .await 41 | .map_err(|e| { 42 | eprintln!("{}", e); 43 | e 44 | })?; 45 | 46 | Ok(HttpResponse::Ok().json(&res)) 47 | } 48 | 49 | #[get("/{id}")] 50 | pub async fn get( 51 | web::Path(submission_id): web::Path, 52 | logged_user: LoggedUser, 53 | pool: web::Data, 54 | ) -> Result { 55 | if logged_user.0.is_none() { 56 | return Err(ServiceError::Unauthorized); 57 | } 58 | let cur_user = logged_user.0.unwrap(); 59 | 60 | let conn = &db_connection(&pool)?; 61 | 62 | use crate::schema::submissions as submissions_schema; 63 | use diesel::prelude::*; 64 | 65 | let user_id: i32 = submissions_schema::table 66 | .filter(submissions_schema::id.eq(submission_id)) 67 | .select(submissions_schema::user_id) 68 | .first(conn)?; 69 | 70 | if cur_user.id != user_id && cur_user.role != "sup" && cur_user.role != "admin" { 71 | let hint = "No permission.".to_string(); 72 | return Err(ServiceError::BadRequest(hint)); 73 | } 74 | 75 | let res = web::block(move || submission::get(submission_id, pool)) 76 | .await 77 | .map_err(|e| { 78 | eprintln!("{}", e); 79 | e 80 | })?; 81 | 82 | Ok(HttpResponse::Ok().json(&res)) 83 | } 84 | 85 | #[derive(Deserialize)] 86 | pub struct GetSubmissionListParams { 87 | region_filter: Option, 88 | problem_id_filter: Option, 89 | user_id_filter: Option, 90 | limit: i32, 91 | offset: i32, 92 | } 93 | 94 | #[get("")] 95 | pub async fn get_list( 96 | query: web::Query, 97 | logged_user: LoggedUser, 98 | pool: web::Data, 99 | ) -> Result { 100 | if logged_user.0.is_none() { 101 | return Err(ServiceError::Unauthorized); 102 | } 103 | 104 | let res = web::block(move || { 105 | submission::get_list( 106 | query.region_filter.clone(), 107 | query.problem_id_filter.clone(), 108 | query.user_id_filter.clone(), 109 | query.limit, 110 | query.offset, 111 | pool, 112 | ) 113 | }) 114 | .await 115 | .map_err(|e| { 116 | eprintln!("{}", e); 117 | e 118 | })?; 119 | 120 | Ok(HttpResponse::Ok().json(&res)) 121 | } 122 | -------------------------------------------------------------------------------- /src/models/users.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::*; 2 | use actix_identity::RequestIdentity; 3 | use actix_web::dev::Payload; 4 | use actix_web::{Error, FromRequest, HttpRequest}; 5 | use chrono::NaiveDate; 6 | use shrinkwraprs::Shrinkwrap; 7 | 8 | #[derive(Debug, Clone, Serialize, Deserialize, Queryable)] 9 | pub struct User { 10 | pub id: i32, 11 | pub salt: Option, 12 | pub hash: Option>, 13 | pub account: String, 14 | pub mobile: Option, 15 | pub role: String, 16 | } 17 | 18 | #[derive(Debug, Insertable)] 19 | #[table_name = "users"] 20 | pub struct InsertableUser { 21 | pub salt: Option, 22 | pub hash: Option>, 23 | pub account: String, 24 | pub mobile: Option, 25 | pub role: String, 26 | } 27 | 28 | #[derive(Serialize)] 29 | pub struct OutUser { 30 | pub id: i32, 31 | pub account: String, 32 | pub mobile: Option, 33 | pub role: String, 34 | } 35 | 36 | impl From for OutUser { 37 | fn from(user: User) -> Self { 38 | Self { 39 | id: user.id, 40 | account: user.account, 41 | mobile: user.mobile, 42 | role: user.role, 43 | } 44 | } 45 | } 46 | 47 | #[derive(AsChangeset)] 48 | #[table_name = "users"] 49 | pub struct UserForm { 50 | pub salt: Option, 51 | pub hash: Option>, 52 | pub account: Option, 53 | pub mobile: Option, 54 | pub role: Option, 55 | } 56 | 57 | #[derive(Debug, Clone, Serialize, Deserialize)] 58 | pub struct SlimUser { 59 | pub id: i32, 60 | pub role: String, 61 | } 62 | 63 | #[derive(Shrinkwrap, Clone, Default)] 64 | pub struct LoggedUser(pub Option); 65 | 66 | impl From for SlimUser { 67 | fn from(user: User) -> Self { 68 | Self { 69 | id: user.id, 70 | role: user.role, 71 | } 72 | } 73 | } 74 | 75 | impl From for LoggedUser { 76 | fn from(slim_user: SlimUser) -> Self { 77 | LoggedUser(Some(slim_user)) 78 | } 79 | } 80 | 81 | impl FromRequest for LoggedUser { 82 | type Error = Error; 83 | type Future = futures::future::Ready>; 84 | type Config = (); 85 | 86 | fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { 87 | let identity = req.get_identity(); 88 | 89 | let slim_user = if let Some(identity) = identity { 90 | match serde_json::from_str::(&identity) { 91 | Err(e) => return futures::future::err(e.into()), 92 | Ok(y) => Ok(Some(y)), 93 | } 94 | } else { 95 | Ok(None) 96 | }; 97 | 98 | futures::future::ready(slim_user.map(LoggedUser)) 99 | } 100 | } 101 | 102 | #[derive(Debug, Clone, Serialize, Deserialize)] 103 | pub struct AuthConfig { 104 | pub sup: Option>, 105 | pub admin: Option>, 106 | pub teacher: Option>, 107 | pub student: Option>, 108 | pub net_friend: Option>, 109 | pub others: Option>, 110 | } 111 | 112 | #[derive(Debug, Clone, Serialize, Deserialize)] 113 | pub struct UserSubmissionCount { 114 | pub total_submit_times: i32, 115 | pub total_accept_times: i32, 116 | pub navie_submit_times: i32, 117 | pub navie_accept_times: i32, 118 | pub easy_submit_times: i32, 119 | pub easy_accept_times: i32, 120 | pub middle_submit_times: i32, 121 | pub middle_accept_times: i32, 122 | pub hard_submit_times: i32, 123 | pub hard_accept_times: i32, 124 | } 125 | 126 | #[derive(Debug, Clone, Serialize, Deserialize)] 127 | pub struct UserSubmissionTime { 128 | pub date: NaiveDate, 129 | pub count: i32, 130 | } 131 | -------------------------------------------------------------------------------- /document/API/Samples.md: -------------------------------------------------------------------------------- 1 | # Samples 2 | 3 | ## Create `POST /samples` 4 | ### Body `Json` 5 | - `problem_id`! 6 | - `src`! code string 7 | - `language`! content of this string can be `c` `cpp` `java` `py2` `py3` 8 | - `description` describe what is the sample be used for 9 | ### Return `Json` 10 | Uuid 11 | ### Explain 12 | Create a sample, sample code will be judged. 13 | The Uuid return refers to sample's id. 14 | 15 | ## Get Sample Details `GET /samples/{id}` 16 | ### Return `Json` 17 | Sample 18 | - `submission_id` Uuid 19 | - `description` Option 20 | - `submission` Submission 21 | - `id` Uuid 22 | - `problem_id` i32 23 | - `user_id` i32 24 | - `region` Option 25 | - `state` String 26 | - `settings` JudgeSettings 27 | - `language_config` LanguageConfig 28 | - `compile` CompileConfig 29 | - `src_name` String 30 | - `exe_name` String 31 | - `max_cpu_time` i32 32 | - `max_real_time` i32 33 | - `max_memory` i32 34 | - `compile_command` String 35 | - `run` RunConfig 36 | - `command` String 37 | - `seccomp_rule` Option 38 | - `env` Vec 39 | - `memory_limit_check_only` i32 40 | - `src` String 41 | - `max_cpu_time` i32 42 | - `max_memory` i32 43 | - `test_case_id` Option 44 | - `test_case` Option> Additional TestCases, we don't expect to have this. So it's always `null`. 45 | - `input` string 46 | - `output` string 47 | - `spj_version` Option 48 | - `spj_config` Option 49 | - `exe_name` String 50 | - `command` String 51 | - `seccomp_rule` String 52 | - `spj_compile_config` Option 53 | - `src_name` String 54 | - `exe_name` String 55 | - `max_cpu_time` i32 56 | - `max_real_time` i32 57 | - `max_memory` i32 58 | - `compile_command` String 59 | - `spj_src` Option 60 | - `output` bool 61 | - `result` Option This where the result saved. 62 | - `err` Option If it is `null`, then the result is effective. 63 | - `err_reason` Option If err is not `null`, then this column is used to describe the err. 64 | - `is_accepted` Option 65 | - `details` Option> 66 | - `cpu_time` i32 67 | - `real_time` i32 68 | - `memory` i32 69 | - `signal` i32 70 | - `exit_code` i32 71 | - `error` String See more in Explain 72 | - `result` String See more in Explain 73 | - `test_case` String 74 | - `output_md5` Option 75 | - `output` Option 76 | - `submit_time` NaiveDateTime 77 | - `is_accepted` Option 78 | - `finish_time` Option 79 | ### Explain 80 | `result` 81 | - WRONG_ANSWER = -1 (this means the process exited normally, but the answer is wrong) 82 | - SUCCESS = 0 (this means the answer is accepted) 83 | - CPU_TIME_LIMIT_EXCEEDED = 1 84 | - REAL_TIME_LIMIT_EXCEEDED = 2 85 | - MEMORY_LIMIT_EXCEEDED = 3 86 | - RUNTIME_ERROR = 4 87 | - SYSTEM_ERROR = 5 88 | 89 | `error` 90 | - SUCCESS = 0 91 | - INVALID_CONFIG = -1 92 | - CLONE_FAILED = -2 93 | - PTHREAD_FAILED = -3 94 | - WAIT_FAILED = -4 95 | - ROOT_REQUIRED = -5 96 | - LOAD_SECCOMP_FAILED = -6 97 | - SETRLIMIT_FAILED = -7 98 | - DUP2_FAILED = -8 99 | - SETUID_FAILED = -9 100 | - EXECVE_FAILED = -10 101 | - SPJ_ERROR = -11 102 | 103 | ## Get List `GET /samples` 104 | ### Params 105 | - `description_filter` Option 106 | - `limit` i32 107 | - `offset` i32 108 | ### Return `Json` 109 | [SlimSample] 110 | - `submission_id` Uuid 111 | - `description` Option 112 | - `submission_state` String 113 | - `is_accepted` Option 114 | - `submit_time` NaiveDateTime 115 | 116 | ## Delete a Sample `DELETE /samples/{id}` 117 | ### Return `null` 118 | 200 means success -------------------------------------------------------------------------------- /src/controllers/problem_sets/handler.rs: -------------------------------------------------------------------------------- 1 | use crate::models::users::LoggedUser; 2 | use crate::services::problem_set; 3 | use actix_web::{delete, get, post, put, web, HttpResponse}; 4 | use server_core::database::Pool; 5 | use server_core::errors::ServiceError; 6 | 7 | #[derive(Deserialize)] 8 | pub struct CreateProblemSetBody { 9 | region: String, 10 | title: String, 11 | introduction: Option, 12 | } 13 | 14 | #[post("")] 15 | pub async fn create( 16 | body: web::Json, 17 | pool: web::Data, 18 | logged_user: LoggedUser, 19 | ) -> Result { 20 | info!("{:?}", logged_user.0); 21 | if logged_user.0.is_none() { 22 | return Err(ServiceError::Unauthorized); 23 | } 24 | let cur_user = logged_user.0.unwrap(); 25 | if cur_user.role != "sup" && cur_user.role != "admin" { 26 | let hint = "No permission.".to_string(); 27 | return Err(ServiceError::BadRequest(hint)); 28 | } 29 | 30 | let res = web::block(move || { 31 | problem_set::create( 32 | body.region.clone(), 33 | body.title.clone(), 34 | body.introduction.clone(), 35 | pool, 36 | ) 37 | }) 38 | .await 39 | .map_err(|e| { 40 | eprintln!("{}", e); 41 | e 42 | })?; 43 | 44 | Ok(HttpResponse::Ok().json(&res)) 45 | } 46 | 47 | #[derive(Deserialize)] 48 | pub struct GetProblemSetListParams { 49 | title_filter: Option, 50 | limit: i32, 51 | offset: i32, 52 | } 53 | 54 | #[get("")] 55 | pub async fn get_set_list( 56 | query: web::Query, 57 | pool: web::Data, 58 | ) -> Result { 59 | let res = web::block(move || { 60 | problem_set::get_set_list(query.title_filter.clone(), query.limit, query.offset, pool) 61 | }) 62 | .await 63 | .map_err(|e| { 64 | eprintln!("{}", e); 65 | e 66 | })?; 67 | 68 | Ok(HttpResponse::Ok().json(&res)) 69 | } 70 | 71 | #[delete("/{region}")] 72 | pub async fn delete( 73 | web::Path(region): web::Path, 74 | pool: web::Data, 75 | logged_user: LoggedUser, 76 | ) -> Result { 77 | if logged_user.0.is_none() { 78 | return Err(ServiceError::Unauthorized); 79 | } 80 | let cur_user = logged_user.0.unwrap(); 81 | if cur_user.role != "sup" && cur_user.role != "admin" { 82 | let hint = "No permission.".to_string(); 83 | return Err(ServiceError::BadRequest(hint)); 84 | } 85 | 86 | let res = web::block(move || problem_set::delete(region, pool)) 87 | .await 88 | .map_err(|e| { 89 | eprintln!("{}", e); 90 | e 91 | })?; 92 | 93 | Ok(HttpResponse::Ok().json(&res)) 94 | } 95 | 96 | #[derive(Deserialize)] 97 | pub struct UpdateProblemSetBody { 98 | new_title: Option, 99 | new_introduction: Option, 100 | } 101 | 102 | #[put("/{region}")] 103 | pub async fn update( 104 | web::Path(region): web::Path, 105 | body: web::Json, 106 | pool: web::Data, 107 | logged_user: LoggedUser, 108 | ) -> Result { 109 | info!("{:?}", logged_user.0); 110 | if logged_user.0.is_none() { 111 | return Err(ServiceError::Unauthorized); 112 | } 113 | let cur_user = logged_user.0.unwrap(); 114 | if cur_user.role != "sup" && cur_user.role != "admin" { 115 | let hint = "No permission.".to_string(); 116 | return Err(ServiceError::BadRequest(hint)); 117 | } 118 | 119 | let res = web::block(move || { 120 | problem_set::update( 121 | region.clone(), 122 | body.new_title.clone(), 123 | body.new_introduction.clone(), 124 | pool, 125 | ) 126 | }) 127 | .await 128 | .map_err(|e| { 129 | eprintln!("{}", e); 130 | e 131 | })?; 132 | 133 | Ok(HttpResponse::Ok().json(&res)) 134 | } 135 | -------------------------------------------------------------------------------- /src/models/problems.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::*; 2 | 3 | #[derive(Debug, Clone, Serialize, Deserialize, Queryable)] 4 | pub struct RawProblem { 5 | pub id: i32, 6 | pub title: String, 7 | pub tags: Vec, 8 | pub difficulty: f64, 9 | pub contents: String, 10 | pub settings: String, 11 | pub is_released: bool, 12 | } 13 | 14 | #[derive(Debug, Clone, Insertable)] 15 | #[table_name = "problems"] 16 | pub struct InsertableProblem { 17 | pub title: String, 18 | pub tags: Vec, 19 | pub difficulty: f64, 20 | pub contents: String, 21 | pub settings: String, 22 | pub is_released: bool, 23 | } 24 | 25 | #[derive(Debug, Clone, Serialize, Deserialize)] 26 | pub struct ProblemInfo { 27 | pub title: String, 28 | pub tags: Vec, 29 | pub difficulty: f64, 30 | } 31 | 32 | #[derive(Debug, Clone, Serialize, Deserialize)] 33 | pub struct Example { 34 | pub input: String, 35 | pub output: String, 36 | } 37 | 38 | #[derive(Debug, Clone, Serialize, Deserialize)] 39 | pub struct ProblemContents { 40 | pub description: Option, 41 | pub example_count: i32, 42 | pub examples: Vec, 43 | } 44 | 45 | #[derive(Debug, Clone, Serialize, Deserialize)] 46 | pub struct ProblemSettings { 47 | pub is_spj: bool, 48 | pub high_performance_max_cpu_time: i32, 49 | pub high_performance_max_memory: i32, 50 | pub other_max_cpu_time: i32, 51 | pub other_max_memory: i32, 52 | pub opaque_output: bool, 53 | pub test_case_count: Option, 54 | } 55 | 56 | #[derive(Debug, Clone, Serialize, Deserialize, Queryable)] 57 | pub struct Problem { 58 | pub id: i32, 59 | pub info: ProblemInfo, 60 | pub contents: ProblemContents, 61 | pub settings: ProblemSettings, 62 | pub is_released: bool, 63 | } 64 | 65 | impl From for Problem { 66 | fn from(raw: RawProblem) -> Self { 67 | Self { 68 | id: raw.id, 69 | info: ProblemInfo { 70 | title: raw.title, 71 | tags: raw.tags, 72 | difficulty: raw.difficulty, 73 | }, 74 | contents: serde_json::from_str::(&raw.contents).unwrap(), 75 | settings: serde_json::from_str::(&raw.settings).unwrap(), 76 | is_released: raw.is_released, 77 | } 78 | } 79 | } 80 | 81 | #[derive(Debug, Clone, Serialize, Deserialize, Queryable)] 82 | pub struct SlimProblem { 83 | pub id: i32, 84 | pub info: ProblemInfo, 85 | pub is_released: bool, 86 | pub is_effective: bool, 87 | } 88 | 89 | impl From for SlimProblem { 90 | fn from(raw: RawProblem) -> Self { 91 | Self { 92 | id: raw.id, 93 | info: ProblemInfo { 94 | title: raw.title, 95 | tags: raw.tags, 96 | difficulty: raw.difficulty, 97 | }, 98 | is_released: raw.is_released, 99 | is_effective: false, 100 | } 101 | } 102 | } 103 | 104 | #[derive(Debug, Clone, Serialize, Deserialize, Queryable)] 105 | pub struct OutProblem { 106 | pub id: i32, 107 | pub info: ProblemInfo, 108 | pub is_released: bool, 109 | } 110 | 111 | impl From for OutProblem { 112 | fn from(raw: RawProblem) -> Self { 113 | Self { 114 | id: raw.id, 115 | info: ProblemInfo { 116 | title: raw.title, 117 | tags: raw.tags, 118 | difficulty: raw.difficulty, 119 | }, 120 | is_released: raw.is_released, 121 | } 122 | } 123 | } 124 | 125 | #[derive(Debug, Clone, Serialize, Deserialize)] 126 | pub struct CreateProblemsResult { 127 | pub title: String, 128 | pub is_success: bool, 129 | pub id: Option, 130 | } 131 | 132 | #[derive(AsChangeset)] 133 | #[table_name = "problems"] 134 | pub struct ProblemForm { 135 | pub title: Option, 136 | pub tags: Option>, 137 | pub difficulty: Option, 138 | pub contents: Option, 139 | pub settings: Option, 140 | } 141 | -------------------------------------------------------------------------------- /src/controllers/samples/handler.rs: -------------------------------------------------------------------------------- 1 | use crate::judge_actor::JudgeActorAddr; 2 | use crate::models::users::LoggedUser; 3 | use crate::services::sample; 4 | use actix_web::{delete, get, post, put, web, HttpResponse}; 5 | use server_core::database::Pool; 6 | use server_core::errors::ServiceError; 7 | use uuid::Uuid; 8 | 9 | #[derive(Deserialize)] 10 | pub struct CreateSampleBody { 11 | problem_id: i32, 12 | src: String, 13 | language: String, 14 | description: Option, 15 | } 16 | 17 | #[post("")] 18 | pub async fn create( 19 | body: web::Json, 20 | pool: web::Data, 21 | logged_user: LoggedUser, 22 | judge_actor: web::Data, 23 | ) -> Result { 24 | info!("{:?}", logged_user.0); 25 | if logged_user.0.is_none() { 26 | return Err(ServiceError::Unauthorized); 27 | } 28 | 29 | let res = web::block(move || { 30 | sample::create( 31 | body.problem_id, 32 | logged_user.0.unwrap().id, 33 | body.src.clone(), 34 | body.language.clone(), 35 | body.description.clone(), 36 | pool, 37 | judge_actor, 38 | ) 39 | }) 40 | .await 41 | .map_err(|e| { 42 | eprintln!("{}", e); 43 | e 44 | })?; 45 | 46 | Ok(HttpResponse::Ok().json(&res)) 47 | } 48 | 49 | #[derive(Deserialize)] 50 | pub struct GetSampleListParams { 51 | description_filter: Option, 52 | problem_id_filter: Option, 53 | language_filter: Option, 54 | submit_time_order: Option, 55 | limit: i32, 56 | offset: i32, 57 | } 58 | 59 | #[get("")] 60 | pub async fn get_list( 61 | query: web::Query, 62 | logged_user: LoggedUser, 63 | pool: web::Data, 64 | ) -> Result { 65 | if logged_user.0.is_none() { 66 | return Err(ServiceError::Unauthorized); 67 | } 68 | let cur_user = logged_user.0.unwrap(); 69 | 70 | if cur_user.role != "sup" && cur_user.role != "admin" { 71 | let hint = "No permission.".to_string(); 72 | return Err(ServiceError::BadRequest(hint)); 73 | } 74 | 75 | let res = web::block(move || { 76 | sample::get_list( 77 | query.description_filter.clone(), 78 | query.problem_id_filter.clone(), 79 | query.language_filter.clone(), 80 | query.submit_time_order, 81 | query.limit, 82 | query.offset, 83 | pool, 84 | ) 85 | }) 86 | .await 87 | .map_err(|e| { 88 | eprintln!("{}", e); 89 | e 90 | })?; 91 | 92 | Ok(HttpResponse::Ok().json(&res)) 93 | } 94 | 95 | #[get("/{id}")] 96 | pub async fn get( 97 | web::Path(id): web::Path, 98 | logged_user: LoggedUser, 99 | pool: web::Data, 100 | ) -> Result { 101 | if logged_user.0.is_none() { 102 | return Err(ServiceError::Unauthorized); 103 | } 104 | let cur_user = logged_user.0.unwrap(); 105 | 106 | if cur_user.role != "sup" && cur_user.role != "admin" { 107 | let hint = "No permission.".to_string(); 108 | return Err(ServiceError::BadRequest(hint)); 109 | } 110 | 111 | let res = web::block(move || sample::get(id, pool)) 112 | .await 113 | .map_err(|e| { 114 | eprintln!("{}", e); 115 | e 116 | })?; 117 | 118 | Ok(HttpResponse::Ok().json(&res)) 119 | } 120 | 121 | #[delete("/{id}")] 122 | pub async fn delete( 123 | web::Path(id): web::Path, 124 | logged_user: LoggedUser, 125 | pool: web::Data, 126 | ) -> Result { 127 | if logged_user.0.is_none() { 128 | return Err(ServiceError::Unauthorized); 129 | } 130 | let cur_user = logged_user.0.unwrap(); 131 | if cur_user.role != "sup" && cur_user.role != "admin" { 132 | let hint = "No permission.".to_string(); 133 | return Err(ServiceError::BadRequest(hint)); 134 | } 135 | 136 | let res = web::block(move || sample::delete(id, pool)) 137 | .await 138 | .map_err(|e| { 139 | eprintln!("{}", e); 140 | e 141 | })?; 142 | 143 | Ok(HttpResponse::Ok().json(&res)) 144 | } 145 | -------------------------------------------------------------------------------- /src/services/problem_set/mod.rs: -------------------------------------------------------------------------------- 1 | use server_core::database::{db_connection, Pool}; 2 | use server_core::errors::ServiceResult; 3 | 4 | use crate::models::problem_sets::*; 5 | 6 | use crate::models::regions::*; 7 | use crate::models::utils::SizedList; 8 | use actix_web::web; 9 | use diesel::prelude::*; 10 | 11 | pub fn create( 12 | region: String, 13 | title: String, 14 | introduction: Option, 15 | pool: web::Data, 16 | ) -> ServiceResult<()> { 17 | let conn = &db_connection(&pool)?; 18 | 19 | use crate::schema::regions as regions_schema; 20 | diesel::insert_into(regions_schema::table) 21 | .values(&Region { 22 | name: region.clone(), 23 | self_type: "problem_set".to_owned(), 24 | title: title.clone(), 25 | has_access_setting: false, 26 | introduction: introduction.clone(), 27 | }) 28 | .execute(conn)?; 29 | 30 | use crate::schema::problem_sets as problem_sets_schema; 31 | diesel::insert_into(problem_sets_schema::table) 32 | .values(&ProblemSetInfo { 33 | region: region, 34 | title: title, 35 | introduction: introduction, 36 | }) 37 | .execute(conn)?; 38 | 39 | Ok(()) 40 | } 41 | 42 | pub fn get_set_list( 43 | title_filter: Option, 44 | limit: i32, 45 | offset: i32, 46 | pool: web::Data, 47 | ) -> ServiceResult> { 48 | let conn = &db_connection(&pool)?; 49 | 50 | let title_filter = if let Some(inner_data) = title_filter { 51 | Some(String::from("%") + &inner_data.as_str().replace(" ", "%") + "%") 52 | } else { 53 | None 54 | }; 55 | 56 | use crate::schema::problem_sets as problem_sets_schema; 57 | let target = problem_sets_schema::table.filter( 58 | problem_sets_schema::title 59 | .nullable() 60 | .like(title_filter.clone()) 61 | .or(title_filter.is_none()), 62 | ); 63 | 64 | let total: i64 = target.clone().count().get_result(conn)?; 65 | 66 | let res = target 67 | .offset(offset.into()) 68 | .limit(limit.into()) 69 | .load(conn)?; 70 | 71 | Ok(SizedList { 72 | total: total, 73 | list: res, 74 | }) 75 | } 76 | 77 | pub fn delete(region: String, pool: web::Data) -> ServiceResult<()> { 78 | let conn = &db_connection(&pool)?; 79 | 80 | use crate::schema::regions as regions_schema; 81 | diesel::delete( 82 | regions_schema::table.filter( 83 | regions_schema::name 84 | .eq(region.clone()) 85 | .and(regions_schema::self_type.eq("problem_set")), 86 | ), 87 | ) 88 | .execute(conn)?; 89 | 90 | use crate::schema::problem_sets as problem_sets_schema; 91 | diesel::delete( 92 | problem_sets_schema::table.filter(problem_sets_schema::region.eq(region.clone())), 93 | ) 94 | .execute(conn)?; 95 | 96 | use crate::schema::region_access_settings as region_access_settings_schema; 97 | diesel::delete( 98 | region_access_settings_schema::table 99 | .filter(region_access_settings_schema::region.eq(region.clone())), 100 | ) 101 | .execute(conn)?; 102 | 103 | use crate::schema::region_links as region_links_schema; 104 | diesel::delete( 105 | region_links_schema::table.filter(region_links_schema::region.eq(region.clone())), 106 | ) 107 | .execute(conn)?; 108 | 109 | Ok(()) 110 | } 111 | 112 | pub fn update( 113 | region: String, 114 | new_title: Option, 115 | new_introduction: Option, 116 | pool: web::Data, 117 | ) -> ServiceResult<()> { 118 | let conn = &db_connection(&pool)?; 119 | 120 | use crate::schema::regions as regions_schema; 121 | diesel::update(regions_schema::table.filter(regions_schema::name.eq(region.clone()))) 122 | .set(RegionForm { 123 | title: new_title.clone(), 124 | introduction: new_introduction.clone(), 125 | }) 126 | .execute(conn)?; 127 | 128 | use crate::schema::problem_sets as problem_sets_schema; 129 | diesel::update( 130 | problem_sets_schema::table.filter(problem_sets_schema::region.eq(region.clone())), 131 | ) 132 | .set(ProblemSetForm { 133 | title: new_title, 134 | introduction: new_introduction, 135 | }) 136 | .execute(conn)?; 137 | 138 | Ok(()) 139 | } 140 | -------------------------------------------------------------------------------- /src/judge_actor/statistics/common_region.rs: -------------------------------------------------------------------------------- 1 | use crate::models::statistics::*; 2 | use crate::models::submissions::*; 3 | use crate::models::*; 4 | use crate::statics::RESULT_STATISTICS_CACHE; 5 | use diesel::prelude::*; 6 | use server_core::database::PooledConnection; 7 | use server_core::errors::*; 8 | 9 | pub fn update_results( 10 | conn: &PooledConnection, 11 | submission: submissions::Submission, 12 | ) -> ServiceResult<()> { 13 | // if not sample submission 14 | if let Some(region) = submission.region.clone() { 15 | let problem_id = submission.problem_id; 16 | let has_cache = { 17 | let result_statistics = RESULT_STATISTICS_CACHE.read().unwrap(); 18 | result_statistics 19 | .get(&(region.clone(), problem_id)) 20 | .is_some() 21 | }; 22 | 23 | if !has_cache { 24 | count_results(conn, ®ion, submission.problem_id)?; 25 | } else { 26 | let mut result_statistics = RESULT_STATISTICS_CACHE.write().unwrap(); 27 | let mut statistic = result_statistics 28 | .get_mut(&(region.clone(), problem_id)) 29 | .unwrap(); 30 | update_submission_statistics(&mut statistic, submission); 31 | } 32 | } 33 | 34 | Ok(()) 35 | } 36 | 37 | fn count_results(conn: &PooledConnection, region: &str, problem_id: i32) -> ServiceResult<()> { 38 | let mut statistics = SubmissionStatistics { 39 | problem_id, 40 | region: region.to_owned(), 41 | submit_times: 0, 42 | accept_times: 0, 43 | error_times: 0, 44 | avg_max_time: 0, 45 | avg_max_memory: 0, 46 | result_count: ResultCount { 47 | wrong_answer: 0, 48 | success: 0, 49 | cpu_time_limit_exceeded: 0, 50 | real_time_limit_exceeded: 0, 51 | memory_limit_exceeded: 0, 52 | runtime_error: 0, 53 | system_error: 0, 54 | unknown_error: 0, 55 | }, 56 | }; 57 | 58 | use crate::schema::submissions as submissions_schema; 59 | let raw_submissions: Vec = submissions_schema::table 60 | .filter(submissions_schema::region.eq(region.to_string())) 61 | .filter(submissions_schema::problem_id.eq(problem_id)) 62 | .load(conn)?; 63 | 64 | for raw_submission in raw_submissions { 65 | let submission = Submission::from(raw_submission); 66 | update_submission_statistics(&mut statistics, submission); 67 | } 68 | 69 | { 70 | let mut result_statistics = RESULT_STATISTICS_CACHE.write().unwrap(); 71 | result_statistics.insert((region.to_owned(), problem_id), statistics); 72 | } 73 | 74 | Ok(()) 75 | } 76 | 77 | fn update_submission_statistics(statistics: &mut SubmissionStatistics, submission: Submission) { 78 | if let Some(result) = submission.result { 79 | if let Some(is_accepted) = result.is_accepted { 80 | if is_accepted { 81 | statistics.accept_times += 1; 82 | } 83 | } 84 | 85 | if let Some(_) = result.err { 86 | statistics.error_times += 1; 87 | } 88 | 89 | if let Some(max_time) = result.max_time { 90 | let effective_time = statistics.submit_times - statistics.error_times; 91 | statistics.avg_max_time = 92 | (statistics.avg_max_time * effective_time + max_time) / (effective_time + 1); 93 | } 94 | 95 | if let Some(max_memory) = result.max_memory { 96 | let effective_time = statistics.submit_times - statistics.error_times; 97 | statistics.avg_max_memory = 98 | (statistics.avg_max_memory * effective_time + max_memory) / (effective_time + 1); 99 | } 100 | 101 | statistics.submit_times += 1; 102 | } 103 | 104 | if let Some(result_set) = submission.out_results { 105 | for result in result_set { 106 | match result.as_str() { 107 | "WRONG_ANSWER" => { 108 | statistics.result_count.wrong_answer += 1; 109 | } 110 | "SUCCESS" => { 111 | statistics.result_count.success += 1; 112 | } 113 | "CPU_TIME_LIMIT_EXCEEDED" => { 114 | statistics.result_count.cpu_time_limit_exceeded += 1; 115 | } 116 | "REAL_TIME_LIMIT_EXCEEDED" => { 117 | statistics.result_count.real_time_limit_exceeded += 1; 118 | } 119 | "MEMORY_LIMIT_EXCEEDED" => { 120 | statistics.result_count.memory_limit_exceeded += 1; 121 | } 122 | "RUNTIME_ERROR" => { 123 | statistics.result_count.runtime_error += 1; 124 | } 125 | "SYSTEM_ERROR" => { 126 | statistics.result_count.system_error += 1; 127 | } 128 | "UNKNOWN_ERROR" => { 129 | statistics.result_count.unknown_error += 1; 130 | } 131 | _ => {} 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/services/sample/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::judge_actor::JudgeActorAddr; 2 | use crate::models::utils::SizedList; 3 | use crate::models::*; 4 | use crate::services::submission; 5 | use actix_web::web; 6 | use diesel::prelude::*; 7 | use server_core::database::{db_connection, Pool}; 8 | use server_core::errors::ServiceResult; 9 | use uuid::Uuid; 10 | 11 | pub fn create( 12 | problem_id: i32, 13 | user_id: i32, 14 | src: String, 15 | language: String, 16 | description: Option, 17 | pool: web::Data, 18 | judge_actor: web::Data, 19 | ) -> ServiceResult { 20 | let submission_id = submission::create( 21 | None, 22 | problem_id, 23 | user_id, 24 | src, 25 | language, 26 | pool.clone(), 27 | judge_actor, 28 | )?; 29 | 30 | let conn = &db_connection(&pool)?; 31 | use crate::schema::samples as samples_schema; 32 | 33 | diesel::insert_into(samples_schema::table) 34 | .values(&samples::InsertableSample { 35 | submission_id: submission_id, 36 | description: description, 37 | }) 38 | .execute(conn)?; 39 | 40 | Ok(submission_id) 41 | } 42 | 43 | pub fn get_list( 44 | description_filter: Option, 45 | problem_id_filter: Option, 46 | language_filter: Option, 47 | submit_time_order: Option, 48 | limit: i32, 49 | offset: i32, 50 | pool: web::Data, 51 | ) -> ServiceResult> { 52 | let description_filter = if let Some(inner_data) = description_filter { 53 | Some(String::from("%") + &inner_data.as_str().replace(" ", "%") + "%") 54 | } else { 55 | None 56 | }; 57 | 58 | let conn = &db_connection(&pool)?; 59 | 60 | use crate::schema::samples as samples_schema; 61 | use crate::schema::submissions as submissions_schema; 62 | 63 | let target = samples_schema::table 64 | .inner_join( 65 | submissions_schema::table.on(samples_schema::submission_id.eq(submissions_schema::id)), 66 | ) 67 | .filter( 68 | samples_schema::description 69 | .nullable() 70 | .like(description_filter.clone()) 71 | .or(description_filter.is_none()), 72 | ) 73 | .filter( 74 | submissions_schema::problem_id 75 | .nullable() 76 | .eq(problem_id_filter) 77 | .or(problem_id_filter.is_none()), 78 | ) 79 | .filter( 80 | submissions_schema::language 81 | .nullable() 82 | .eq(language_filter.clone()) 83 | .or(language_filter.is_none()), 84 | ); 85 | 86 | let total: i64 = target.clone().count().get_result(conn)?; 87 | 88 | let target = target.offset(offset.into()).limit(limit.into()); 89 | 90 | let raw: Vec<(samples::RawSample, submissions::RawSubmission)> = match submit_time_order { 91 | None => target 92 | .order(submissions_schema::submit_time.desc()) 93 | .load(conn)?, 94 | Some(true) => target 95 | .order(submissions_schema::submit_time.asc()) 96 | .load(conn)?, 97 | Some(false) => target 98 | .order(submissions_schema::submit_time.desc()) 99 | .load(conn)?, 100 | }; 101 | 102 | let mut res = Vec::new(); 103 | for (raw_sample, raw_submission) in raw { 104 | let slim_sample = samples::SlimSample::from(samples::Sample { 105 | submission_id: raw_sample.submission_id, 106 | description: raw_sample.description, 107 | submission: submissions::Submission::from(raw_submission), 108 | }); 109 | res.push(slim_sample); 110 | } 111 | 112 | Ok(SizedList { 113 | total: total, 114 | list: res, 115 | }) 116 | } 117 | 118 | pub fn get(id: Uuid, pool: web::Data) -> ServiceResult { 119 | let conn = &db_connection(&pool)?; 120 | 121 | use crate::schema::samples as samples_schema; 122 | use crate::schema::submissions as submissions_schema; 123 | 124 | let (raw_sample, raw_submission): (samples::RawSample, submissions::RawSubmission) = 125 | samples_schema::table 126 | .filter(samples_schema::submission_id.eq(id)) 127 | .inner_join( 128 | submissions_schema::table 129 | .on(samples_schema::submission_id.eq(submissions_schema::id)), 130 | ) 131 | .first(conn)?; 132 | 133 | Ok(samples::Sample { 134 | submission_id: raw_sample.submission_id, 135 | description: raw_sample.description, 136 | submission: submissions::Submission::from(raw_submission), 137 | }) 138 | } 139 | 140 | pub fn delete(id: Uuid, pool: web::Data) -> ServiceResult<()> { 141 | let conn = &db_connection(&pool)?; 142 | 143 | use crate::schema::samples as samples_schema; 144 | use crate::schema::submissions as submissions_schema; 145 | 146 | diesel::delete(samples_schema::table.filter(samples_schema::submission_id.eq(id))) 147 | .execute(conn)?; 148 | 149 | diesel::delete(submissions_schema::table.filter(submissions_schema::id.eq(id))) 150 | .execute(conn)?; 151 | 152 | Ok(()) 153 | } 154 | -------------------------------------------------------------------------------- /src/models/contests.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::*; 2 | use chrono::*; 3 | use server_core::utils::time::get_cur_naive_date_time; 4 | 5 | #[derive(Debug, Clone, Serialize, Deserialize, Insertable, Queryable)] 6 | #[table_name = "contests"] 7 | pub struct RawContest { 8 | pub region: String, 9 | pub title: String, 10 | pub introduction: Option, 11 | pub start_time: NaiveDateTime, 12 | pub end_time: Option, 13 | pub seal_time: Option, 14 | pub settings: String, 15 | } 16 | 17 | #[derive(Debug, Clone, Serialize, Deserialize)] 18 | pub struct Contest { 19 | pub region: String, 20 | pub title: String, 21 | pub introduction: Option, 22 | pub start_time: NaiveDateTime, 23 | pub end_time: Option, 24 | pub seal_time: Option, 25 | pub settings: ContestSettings, 26 | pub state: String, 27 | } 28 | 29 | impl From for Contest { 30 | fn from(raw: RawContest) -> Self { 31 | let mut res = Self { 32 | region: raw.region, 33 | title: raw.title, 34 | introduction: raw.introduction, 35 | start_time: raw.start_time, 36 | end_time: raw.end_time, 37 | seal_time: raw.seal_time, 38 | settings: serde_json::from_str(&raw.settings).unwrap(), 39 | state: format!("{}", ContestState::Ended), 40 | }; 41 | res.state = format!( 42 | "{}", 43 | get_contest_state(res.clone(), get_cur_naive_date_time()) 44 | ); 45 | res 46 | } 47 | } 48 | 49 | #[derive(Debug, Clone, Serialize, Deserialize)] 50 | pub struct SlimContest { 51 | pub region: String, 52 | pub title: String, 53 | pub introduction: Option, 54 | pub start_time: NaiveDateTime, 55 | pub end_time: Option, 56 | pub seal_time: Option, 57 | pub state: String, 58 | pub is_registered: bool, 59 | pub need_pass: bool, 60 | pub settings: ContestSettings, 61 | } 62 | 63 | impl From for SlimContest { 64 | fn from(raw: RawContest) -> Self { 65 | let contest = Contest::from(raw); 66 | 67 | Self { 68 | region: contest.region, 69 | title: contest.title, 70 | introduction: contest.introduction, 71 | start_time: contest.start_time, 72 | end_time: contest.end_time, 73 | seal_time: contest.seal_time, 74 | state: contest.state, 75 | is_registered: false, 76 | need_pass: false, 77 | settings: contest.settings, 78 | } 79 | } 80 | } 81 | 82 | #[derive(Debug, Clone, Serialize, Deserialize)] 83 | pub struct ContestSettings { 84 | pub register_after_start: bool, 85 | pub unrate_after_start: bool, 86 | pub view_before_start: bool, 87 | pub view_after_end: bool, 88 | pub public_after_end: bool, 89 | pub submit_after_end: bool, 90 | } 91 | 92 | impl Default for ContestSettings { 93 | fn default() -> Self { 94 | Self { 95 | register_after_start: true, 96 | unrate_after_start: true, 97 | view_before_start: true, 98 | view_after_end: true, 99 | public_after_end: false, 100 | submit_after_end: true, 101 | } 102 | } 103 | } 104 | 105 | #[derive(PartialEq)] 106 | pub enum ContestState { 107 | Preparing, 108 | Running, 109 | SealedRunning, 110 | Ended, 111 | } 112 | 113 | use std::fmt; 114 | impl fmt::Display for ContestState { 115 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 116 | match self { 117 | ContestState::Preparing => f.write_str("Preparing"), 118 | ContestState::Running => f.write_str("Running"), 119 | ContestState::SealedRunning => f.write_str("SealedRunning"), 120 | ContestState::Ended => f.write_str("Ended"), 121 | } 122 | } 123 | } 124 | 125 | pub fn get_contest_state(contest: Contest, cur_time: NaiveDateTime) -> ContestState { 126 | if cur_time < contest.start_time { 127 | ContestState::Preparing 128 | } else { 129 | if let Some(seal_time) = contest.seal_time { 130 | if cur_time < seal_time { 131 | ContestState::Running 132 | } else { 133 | if let Some(end_time) = contest.end_time { 134 | if cur_time < end_time { 135 | ContestState::SealedRunning 136 | } else { 137 | ContestState::Ended 138 | } 139 | } else { 140 | ContestState::SealedRunning 141 | } 142 | } 143 | } else { 144 | if let Some(end_time) = contest.end_time { 145 | if cur_time < end_time { 146 | ContestState::Running 147 | } else { 148 | ContestState::Ended 149 | } 150 | } else { 151 | ContestState::Running 152 | } 153 | } 154 | } 155 | } 156 | 157 | #[derive(AsChangeset)] 158 | #[table_name = "contests"] 159 | pub struct ContestForm { 160 | pub title: Option, 161 | pub introduction: Option, 162 | pub start_time: Option, 163 | pub end_time: Option, 164 | pub seal_time: Option, 165 | pub settings: Option, 166 | } 167 | -------------------------------------------------------------------------------- /src/models/statistics.rs: -------------------------------------------------------------------------------- 1 | use crate::models::submissions::*; 2 | use crate::statics::RESULT_STATISTICS_CACHE; 3 | use diesel::prelude::*; 4 | use server_core::database::*; 5 | use server_core::errors::ServiceResult; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct SubmissionStatistics { 9 | pub problem_id: i32, 10 | pub region: String, 11 | pub submit_times: i32, 12 | pub accept_times: i32, 13 | pub error_times: i32, 14 | pub avg_max_time: i32, 15 | pub avg_max_memory: i32, 16 | pub result_count: ResultCount, 17 | } 18 | 19 | #[derive(Debug, Clone, Serialize, Deserialize)] 20 | pub struct ResultCount { 21 | pub wrong_answer: i32, 22 | pub success: i32, 23 | pub cpu_time_limit_exceeded: i32, 24 | pub real_time_limit_exceeded: i32, 25 | pub memory_limit_exceeded: i32, 26 | pub runtime_error: i32, 27 | pub system_error: i32, 28 | pub unknown_error: i32, 29 | } 30 | 31 | pub fn get_results( 32 | conn: &PooledConnection, 33 | region: String, 34 | problem_id: i32, 35 | ) -> ServiceResult { 36 | let has_cache = { 37 | let result_statistics = RESULT_STATISTICS_CACHE.read().unwrap(); 38 | result_statistics 39 | .get(&(region.clone(), problem_id)) 40 | .is_some() 41 | }; 42 | 43 | if !has_cache { 44 | count_results(conn, ®ion, problem_id)?; 45 | } 46 | 47 | Ok({ 48 | let result_statistics = RESULT_STATISTICS_CACHE.read().unwrap(); 49 | result_statistics 50 | .get(&(region.clone(), problem_id)) 51 | .unwrap() 52 | .to_owned() 53 | }) 54 | } 55 | 56 | fn count_results(conn: &PooledConnection, region: &str, problem_id: i32) -> ServiceResult<()> { 57 | let mut statistics = SubmissionStatistics { 58 | problem_id, 59 | region: region.to_owned(), 60 | submit_times: 0, 61 | accept_times: 0, 62 | error_times: 0, 63 | avg_max_time: 0, 64 | avg_max_memory: 0, 65 | result_count: ResultCount { 66 | wrong_answer: 0, 67 | success: 0, 68 | cpu_time_limit_exceeded: 0, 69 | real_time_limit_exceeded: 0, 70 | memory_limit_exceeded: 0, 71 | runtime_error: 0, 72 | system_error: 0, 73 | unknown_error: 0, 74 | }, 75 | }; 76 | 77 | use crate::schema::submissions as submissions_schema; 78 | let raw_submissions: Vec = submissions_schema::table 79 | .filter(submissions_schema::region.eq(region.to_string())) 80 | .filter(submissions_schema::problem_id.eq(problem_id)) 81 | .load(conn)?; 82 | 83 | for raw_submission in raw_submissions { 84 | let submission = Submission::from(raw_submission); 85 | update_submission_statistics(&mut statistics, submission); 86 | } 87 | 88 | { 89 | let mut result_statistics = RESULT_STATISTICS_CACHE.write().unwrap(); 90 | result_statistics.insert((region.to_owned(), problem_id), statistics); 91 | } 92 | 93 | Ok(()) 94 | } 95 | 96 | fn update_submission_statistics(statistics: &mut SubmissionStatistics, submission: Submission) { 97 | if let Some(result) = submission.result { 98 | if let Some(is_accepted) = result.is_accepted { 99 | if is_accepted { 100 | statistics.accept_times += 1; 101 | } 102 | } 103 | 104 | if let Some(_) = result.err { 105 | statistics.error_times += 1; 106 | } 107 | 108 | if let Some(max_time) = result.max_time { 109 | let effective_time = statistics.submit_times - statistics.error_times; 110 | statistics.avg_max_time = 111 | (statistics.avg_max_time * effective_time + max_time) / (effective_time + 1); 112 | } 113 | 114 | if let Some(max_memory) = result.max_memory { 115 | let effective_time = statistics.submit_times - statistics.error_times; 116 | statistics.avg_max_memory = 117 | (statistics.avg_max_memory * effective_time + max_memory) / (effective_time + 1); 118 | } 119 | 120 | statistics.submit_times += 1; 121 | } 122 | 123 | if let Some(result_set) = submission.out_results { 124 | for result in result_set { 125 | match result.as_str() { 126 | "WRONG_ANSWER" => { 127 | statistics.result_count.wrong_answer += 1; 128 | } 129 | "SUCCESS" => { 130 | statistics.result_count.success += 1; 131 | } 132 | "CPU_TIME_LIMIT_EXCEEDED" => { 133 | statistics.result_count.cpu_time_limit_exceeded += 1; 134 | } 135 | "REAL_TIME_LIMIT_EXCEEDED" => { 136 | statistics.result_count.real_time_limit_exceeded += 1; 137 | } 138 | "MEMORY_LIMIT_EXCEEDED" => { 139 | statistics.result_count.memory_limit_exceeded += 1; 140 | } 141 | "RUNTIME_ERROR" => { 142 | statistics.result_count.runtime_error += 1; 143 | } 144 | "SYSTEM_ERROR" => { 145 | statistics.result_count.system_error += 1; 146 | } 147 | "UNKNOWN_ERROR" => { 148 | statistics.result_count.unknown_error += 1; 149 | } 150 | _ => {} 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/services/submission/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::judge_actor::{handler::StartJudge, JudgeActorAddr}; 2 | use crate::models::utils::SizedList; 3 | use crate::models::*; 4 | use crate::statics::WAITING_QUEUE; 5 | use actix_web::web; 6 | use diesel::prelude::*; 7 | use server_core::database::{db_connection, Pool}; 8 | use server_core::errors::ServiceResult; 9 | use server_core::utils::time::get_cur_naive_date_time; 10 | use std::fs::File; 11 | use std::io::prelude::*; 12 | use uuid::Uuid; 13 | 14 | pub fn create( 15 | region: Option, 16 | problem_id: i32, 17 | user_id: i32, 18 | src: String, 19 | language: String, 20 | pool: web::Data, 21 | judge_actor: web::Data, 22 | ) -> ServiceResult { 23 | let id = Uuid::new_v4(); 24 | let language_config = languages::get_lang_config(&language); 25 | 26 | let conn = &db_connection(&pool)?; 27 | use crate::schema::problems as problems_schema; 28 | use crate::schema::submissions as submissions_schema; 29 | 30 | let raw_problem: problems::RawProblem = problems_schema::table 31 | .filter(problems_schema::id.eq(problem_id)) 32 | .first(conn)?; 33 | let problem = problems::Problem::from(raw_problem); 34 | let mut spj_src = None; 35 | if problem.settings.is_spj { 36 | let mut file = File::open(format!("data/test_cases/{}/spj_src.cpp", problem.id))?; 37 | let mut contents = String::new(); 38 | file.read_to_string(&mut contents)?; 39 | spj_src = Some(contents); 40 | } 41 | 42 | let settings = submissions::JudgeSettings { 43 | language_config: language_config, 44 | src: src, 45 | max_cpu_time: if &language == "c" || &language == "cpp" { 46 | problem.settings.high_performance_max_cpu_time 47 | } else { 48 | problem.settings.other_max_cpu_time 49 | }, 50 | max_memory: if &language == "c" || &language == "cpp" { 51 | problem.settings.high_performance_max_memory 52 | } else { 53 | problem.settings.other_max_memory 54 | }, 55 | test_case_id: Some(problem.id.to_string()), 56 | test_case: None, 57 | spj_version: Some("1".to_owned()), 58 | spj_config: if problem.settings.is_spj { 59 | Some(languages::spj_config()) 60 | } else { 61 | None 62 | }, 63 | spj_compile_config: if problem.settings.is_spj { 64 | Some(languages::spj_compile_config()) 65 | } else { 66 | None 67 | }, 68 | spj_src: spj_src, 69 | output: !problem.settings.opaque_output, 70 | }; 71 | 72 | let settings_string = serde_json::to_string(&settings).unwrap(); 73 | 74 | diesel::insert_into(submissions_schema::table) 75 | .values(&submissions::InsertableSubmission { 76 | id: id, 77 | problem_id: problem_id, 78 | region: region, 79 | user_id: user_id, 80 | state: String::from("Waiting"), 81 | settings: settings_string, 82 | result: None, 83 | submit_time: get_cur_naive_date_time(), 84 | is_accepted: None, 85 | finish_time: None, 86 | max_time: None, 87 | max_memory: None, 88 | language: Some(language), 89 | err: None, 90 | }) 91 | .execute(conn)?; 92 | 93 | { 94 | let mut lock = WAITING_QUEUE.write().unwrap(); 95 | lock.push_back(id); 96 | } 97 | 98 | judge_actor.addr.do_send(StartJudge()); 99 | 100 | Ok(id) 101 | } 102 | 103 | pub fn get(id: Uuid, pool: web::Data) -> ServiceResult { 104 | let conn = &db_connection(&pool)?; 105 | 106 | use crate::schema::submissions as submissions_schema; 107 | 108 | let raw: submissions::RawSubmission = submissions_schema::table 109 | .filter(submissions_schema::id.eq(id)) 110 | .first(conn)?; 111 | 112 | Ok(submissions::Submission::from(raw)) 113 | } 114 | 115 | pub fn get_list( 116 | region_filter: Option, 117 | problem_id_filter: Option, 118 | user_id_filter: Option, 119 | limit: i32, 120 | offset: i32, 121 | pool: web::Data, 122 | ) -> ServiceResult> { 123 | let conn = &db_connection(&pool)?; 124 | 125 | use crate::schema::submissions as submissions_schema; 126 | 127 | let target = submissions_schema::table 128 | .filter( 129 | submissions_schema::region 130 | .nullable() 131 | .eq(region_filter.clone()) 132 | .or(region_filter.is_none()), 133 | ) 134 | .filter( 135 | submissions_schema::problem_id 136 | .nullable() 137 | .eq(problem_id_filter) 138 | .or(problem_id_filter.is_none()), 139 | ) 140 | .filter( 141 | submissions_schema::user_id 142 | .nullable() 143 | .eq(user_id_filter) 144 | .or(user_id_filter.is_none()), 145 | ); 146 | 147 | let total: i64 = target.clone().count().get_result(conn)?; 148 | 149 | let raw_submissions: Vec = target 150 | .offset(offset.into()) 151 | .limit(limit.into()) 152 | .order(submissions_schema::submit_time.desc()) 153 | .load(conn)?; 154 | 155 | let mut res = Vec::new(); 156 | for raw_submission in raw_submissions { 157 | res.push(submissions::SlimSubmission::from(raw_submission)); 158 | } 159 | 160 | Ok(SizedList { 161 | total: total, 162 | list: res, 163 | }) 164 | } 165 | -------------------------------------------------------------------------------- /src/controllers/regions/handler.rs: -------------------------------------------------------------------------------- 1 | use crate::auth::region::*; 2 | use crate::judge_actor::JudgeActorAddr; 3 | use crate::models::users::LoggedUser; 4 | use crate::services::region; 5 | use actix_web::{delete, get, post, put, web, HttpResponse}; 6 | use server_core::database::Pool; 7 | use server_core::errors::ServiceError; 8 | 9 | #[derive(Deserialize)] 10 | pub struct GetRegionListParams { 11 | self_type: Option, 12 | limit: i32, 13 | offset: i32, 14 | } 15 | 16 | #[get("")] 17 | pub async fn get_list( 18 | query: web::Query, 19 | pool: web::Data, 20 | ) -> Result { 21 | let res = web::block(move || { 22 | region::get_list(query.self_type.clone(), query.limit, query.offset, pool) 23 | }) 24 | .await 25 | .map_err(|e| { 26 | eprintln!("{}", e); 27 | e 28 | })?; 29 | 30 | Ok(HttpResponse::Ok().json(&res)) 31 | } 32 | 33 | #[derive(Deserialize)] 34 | pub struct InsertProblemToRegionBody { 35 | problem_ids: Vec, 36 | } 37 | 38 | #[post("/{region}")] 39 | pub async fn insert_problems( 40 | web::Path(region): web::Path, 41 | body: web::Json, 42 | pool: web::Data, 43 | logged_user: LoggedUser, 44 | ) -> Result { 45 | info!("{:?}", logged_user.0); 46 | if logged_user.0.is_none() { 47 | return Err(ServiceError::Unauthorized); 48 | } 49 | let cur_user = logged_user.0.unwrap(); 50 | if cur_user.role != "sup" && cur_user.role != "admin" { 51 | let hint = "No permission.".to_string(); 52 | return Err(ServiceError::BadRequest(hint)); 53 | } 54 | 55 | let res = 56 | web::block(move || region::insert_problems(region, body.problem_ids.clone(), None, pool)) 57 | .await 58 | .map_err(|e| { 59 | eprintln!("{}", e); 60 | e 61 | })?; 62 | 63 | Ok(HttpResponse::Ok().json(&res)) 64 | } 65 | 66 | #[derive(Deserialize)] 67 | pub struct GetLinkedProblemColumnParams { 68 | inner_id_filter: Option, 69 | problem_id_filter: Option, 70 | title_filter: Option, 71 | tag_filter: Option>, 72 | difficulty_filter: Option, 73 | inner_id_order: Option, 74 | problem_id_order: Option, 75 | difficulty_order: Option, 76 | limit: i32, 77 | offset: i32, 78 | } 79 | 80 | #[get("/{region}")] 81 | pub async fn get_linked_problem_column_list( 82 | web::Path(region): web::Path, 83 | query: web::Query, 84 | pool: web::Data, 85 | logged_user: LoggedUser, 86 | ) -> Result { 87 | check_view_right(pool.clone(), logged_user.clone(), region.clone())?; 88 | 89 | let res = web::block(move || { 90 | region::get_linked_problem_column_list( 91 | region, 92 | query.inner_id_filter, 93 | query.problem_id_filter, 94 | query.title_filter.clone(), 95 | query.tag_filter.clone(), 96 | query.difficulty_filter.clone(), 97 | query.inner_id_order.clone(), 98 | query.problem_id_order.clone(), 99 | query.difficulty_order.clone(), 100 | query.limit, 101 | query.offset, 102 | pool, 103 | ) 104 | }) 105 | .await 106 | .map_err(|e| { 107 | eprintln!("{}", e); 108 | e 109 | })?; 110 | 111 | Ok(HttpResponse::Ok().json(&res)) 112 | } 113 | 114 | #[get("/{region}/{inner_id}")] 115 | pub async fn get_linked_problem( 116 | web::Path((region, inner_id)): web::Path<(String, i32)>, 117 | logged_user: LoggedUser, 118 | pool: web::Data, 119 | ) -> Result { 120 | check_solve_right(pool.clone(), logged_user.clone(), region.clone())?; 121 | 122 | let res = web::block(move || region::get_linked_problem(region, inner_id, pool)) 123 | .await 124 | .map_err(|e| { 125 | eprintln!("{}", e); 126 | e 127 | })?; 128 | 129 | Ok(HttpResponse::Ok().json(&res)) 130 | } 131 | 132 | #[derive(Deserialize)] 133 | pub struct CreateRegionSubmissionBody { 134 | src: String, 135 | language: String, 136 | } 137 | 138 | #[post("/{region}/{inner_id}/submission")] 139 | pub async fn create_submission( 140 | web::Path((region, inner_id)): web::Path<(String, i32)>, 141 | body: web::Json, 142 | pool: web::Data, 143 | logged_user: LoggedUser, 144 | judge_actor: web::Data, 145 | ) -> Result { 146 | check_solve_right(pool.clone(), logged_user.clone(), region.clone())?; 147 | 148 | let res = web::block(move || { 149 | region::create_submission( 150 | region, 151 | inner_id, 152 | logged_user.0.unwrap().id, 153 | body.src.clone(), 154 | body.language.clone(), 155 | pool, 156 | judge_actor, 157 | ) 158 | }) 159 | .await 160 | .map_err(|e| { 161 | eprintln!("{}", e); 162 | e 163 | })?; 164 | 165 | Ok(HttpResponse::Ok().json(&res)) 166 | } 167 | 168 | #[delete("/{region}/{inner_id}")] 169 | pub async fn delete_problem( 170 | web::Path((region, inner_id)): web::Path<(String, i32)>, 171 | logged_user: LoggedUser, 172 | pool: web::Data, 173 | ) -> Result { 174 | info!("{:?}", logged_user.0); 175 | if logged_user.0.is_none() { 176 | return Err(ServiceError::Unauthorized); 177 | } 178 | let cur_user = logged_user.0.unwrap(); 179 | if cur_user.role != "sup" && cur_user.role != "admin" { 180 | let hint = "No permission.".to_string(); 181 | return Err(ServiceError::BadRequest(hint)); 182 | } 183 | 184 | let res = web::block(move || region::delete_problem(region, inner_id, pool)) 185 | .await 186 | .map_err(|e| { 187 | eprintln!("{}", e); 188 | e 189 | })?; 190 | 191 | Ok(HttpResponse::Ok().json(&res)) 192 | } 193 | -------------------------------------------------------------------------------- /src/models/languages.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Serialize, Deserialize)] 2 | pub struct CompileConfig { 3 | pub src_name: String, 4 | pub exe_name: String, 5 | pub max_cpu_time: i32, 6 | pub max_real_time: i32, 7 | pub max_memory: i32, 8 | pub compile_command: String, 9 | } 10 | 11 | #[derive(Debug, Clone, Serialize, Deserialize)] 12 | pub struct RunConfig { 13 | pub command: String, 14 | pub seccomp_rule: Option, 15 | pub env: Vec, 16 | pub memory_limit_check_only: i32, 17 | } 18 | 19 | #[derive(Debug, Clone, Serialize, Deserialize)] 20 | pub struct LanguageConfig { 21 | pub compile: CompileConfig, 22 | pub run: RunConfig, 23 | } 24 | 25 | #[derive(Debug, Clone, Serialize, Deserialize)] 26 | pub struct SpjConfig { 27 | pub exe_name: String, 28 | pub command: String, 29 | pub seccomp_rule: String, 30 | } 31 | 32 | #[derive(Debug, Clone, Serialize, Deserialize)] 33 | pub struct SpjCompileConfig { 34 | pub src_name: String, 35 | pub exe_name: String, 36 | pub max_cpu_time: i32, 37 | pub max_real_time: i32, 38 | pub max_memory: i32, 39 | pub compile_command: String, 40 | } 41 | 42 | fn default_env() -> Vec { 43 | vec![ 44 | "LANG=en_US.UTF-8".to_owned(), 45 | "LANGUAGE=en_US:en".to_owned(), 46 | "LC_ALL=en_US.UTF-8".to_owned(), 47 | ] 48 | } 49 | 50 | fn c_lang_config() -> LanguageConfig { 51 | LanguageConfig { 52 | compile: CompileConfig { 53 | src_name: "main.c".to_owned(), 54 | exe_name: "main".to_owned(), 55 | max_cpu_time: 3000, 56 | max_real_time: 5000, 57 | max_memory: 128 * 1024 * 1024, 58 | compile_command: "/usr/bin/gcc -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c99 {src_path} -lm -o {exe_path}".to_owned(), 59 | }, 60 | run: RunConfig { 61 | command: "{exe_path}".to_owned(), 62 | seccomp_rule: Some("c_cpp".to_owned()), 63 | env: default_env(), 64 | memory_limit_check_only: 0, 65 | } 66 | } 67 | } 68 | 69 | pub fn spj_compile_config() -> SpjCompileConfig { 70 | SpjCompileConfig { 71 | src_name: "spj-{spj_version}.cpp".to_owned(), 72 | exe_name: "spj-{spj_version}".to_owned(), 73 | max_cpu_time: 3000, 74 | max_real_time: 5000, 75 | max_memory: 1024 * 1024 * 1024, 76 | compile_command: "/usr/bin/g++ -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c99 {src_path} -lm -L /test_case/include -o {exe_path}".to_owned(), 77 | } 78 | } 79 | 80 | pub fn spj_config() -> SpjConfig { 81 | SpjConfig { 82 | exe_name: "spj-{spj_version}".to_owned(), 83 | command: "{exe_path} {in_file_path} {user_out_file_path}".to_owned(), 84 | seccomp_rule: "c_cpp".to_owned(), 85 | } 86 | } 87 | 88 | fn cpp_lang_config() -> LanguageConfig { 89 | LanguageConfig { 90 | compile: CompileConfig { 91 | src_name: "main.cpp".to_owned(), 92 | exe_name: "main".to_owned(), 93 | max_cpu_time: 3000, 94 | max_real_time: 5000, 95 | max_memory: 128 * 1024 * 1024, 96 | compile_command: "/usr/bin/g++ -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c++11 {src_path} -lm -o {exe_path}".to_owned(), 97 | }, 98 | run: RunConfig { 99 | command: "{exe_path}".to_owned(), 100 | seccomp_rule: Some("c_cpp".to_owned()), 101 | env: default_env(), 102 | memory_limit_check_only: 0, 103 | } 104 | } 105 | } 106 | 107 | fn java_lang_config() -> LanguageConfig { 108 | LanguageConfig { 109 | compile: CompileConfig { 110 | src_name: "Main.java".to_owned(), 111 | exe_name: "Main".to_owned(), 112 | max_cpu_time: 5000, 113 | max_real_time: 10000, 114 | max_memory: -1, 115 | compile_command: "/usr/bin/javac {src_path} -d {exe_dir} -encoding UTF8".to_owned(), 116 | }, 117 | run: RunConfig { 118 | command: "/usr/bin/java -cp {exe_dir} -XX:MaxRAM={max_memory}k -Djava.security.manager -Dfile.encoding=UTF-8 -Djava.security.policy==/etc/java_policy -Djava.awt.headless=true Main".to_owned(), 119 | seccomp_rule: None, 120 | env: default_env(), 121 | memory_limit_check_only: 1, 122 | } 123 | } 124 | } 125 | 126 | fn py2_lang_config() -> LanguageConfig { 127 | LanguageConfig { 128 | compile: CompileConfig { 129 | src_name: "solution.py".to_owned(), 130 | exe_name: "solution.pyc".to_owned(), 131 | max_cpu_time: 3000, 132 | max_real_time: 5000, 133 | max_memory: 128 * 1024 * 1024, 134 | compile_command: "/usr/bin/python -m py_compile {src_path}".to_owned(), 135 | }, 136 | run: RunConfig { 137 | command: "/usr/bin/python {exe_path}".to_owned(), 138 | seccomp_rule: Some("general".to_owned()), 139 | env: default_env(), 140 | memory_limit_check_only: 0, 141 | }, 142 | } 143 | } 144 | 145 | fn py3_lang_config() -> LanguageConfig { 146 | LanguageConfig { 147 | compile: CompileConfig { 148 | src_name: "solution.py".to_owned(), 149 | exe_name: "__pycache__/solution.cpython-36.pyc".to_owned(), 150 | max_cpu_time: 3000, 151 | max_real_time: 5000, 152 | max_memory: 128 * 1024 * 1024, 153 | compile_command: "/usr/bin/python3 -m py_compile {src_path}".to_owned(), 154 | }, 155 | run: RunConfig { 156 | command: "/usr/bin/python3 {exe_path}".to_owned(), 157 | seccomp_rule: Some("general".to_owned()), 158 | env: { 159 | let mut default_env = default_env(); 160 | default_env.push("PYTHONIOENCODING=UTF-8".to_owned()); 161 | default_env 162 | }, 163 | memory_limit_check_only: 0, 164 | }, 165 | } 166 | } 167 | 168 | pub fn get_lang_config(language: &str) -> LanguageConfig { 169 | match language { 170 | "c" => c_lang_config(), 171 | "cpp" => cpp_lang_config(), 172 | "java" => java_lang_config(), 173 | "py2" => py2_lang_config(), 174 | "py3" => py3_lang_config(), 175 | _ => c_lang_config(), 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/auth/region.rs: -------------------------------------------------------------------------------- 1 | use crate::models::region_access_settings::RegionAccessSetting; 2 | use crate::models::users::LoggedUser; 3 | use crate::services::region::utils::get_self_type; 4 | use actix_web::web; 5 | use diesel::prelude::*; 6 | use server_core::database::{db_connection, Pool}; 7 | use server_core::errors::*; 8 | use server_core::utils::time::get_cur_naive_date_time; 9 | 10 | pub fn has_access_setting(conn: &PgConnection, region: String) -> ServiceResult { 11 | use crate::schema::regions as regions_schema; 12 | 13 | if regions_schema::table 14 | .filter(regions_schema::name.eq(region)) 15 | .select(regions_schema::has_access_setting) 16 | .first::(conn)? 17 | { 18 | Ok(true) 19 | } else { 20 | Ok(false) 21 | } 22 | } 23 | 24 | pub fn read_access_setting( 25 | conn: &PgConnection, 26 | region: String, 27 | ) -> ServiceResult { 28 | use crate::schema::region_access_settings as region_access_settings_schema; 29 | 30 | Ok(region_access_settings_schema::table 31 | .filter(region_access_settings_schema::region.eq(region)) 32 | .first::(conn)?) 33 | } 34 | 35 | pub fn check_acl(conn: &PgConnection, user_id: i32, region: String) -> ServiceResult<()> { 36 | use crate::schema::access_control_list as access_control_list_schema; 37 | 38 | if access_control_list_schema::table 39 | .filter(access_control_list_schema::user_id.eq(user_id)) 40 | .filter(access_control_list_schema::region.eq(region)) 41 | .count() 42 | .get_result::(conn)? 43 | == 1 44 | { 45 | Ok(()) 46 | } else { 47 | let hint = "Not in ACL.".to_owned(); 48 | Err(ServiceError::UnauthorizedWithHint(hint)) 49 | } 50 | } 51 | 52 | pub fn is_manager(conn: &PgConnection, user_id: i32, region: String) -> ServiceResult { 53 | use crate::schema::access_control_list as access_control_list_schema; 54 | 55 | if access_control_list_schema::table 56 | .filter(access_control_list_schema::user_id.eq(user_id)) 57 | .filter(access_control_list_schema::region.eq(region)) 58 | .filter(access_control_list_schema::is_manager.eq(true)) 59 | .count() 60 | .get_result::(conn)? 61 | == 1 62 | { 63 | Ok(true) 64 | } else { 65 | Ok(false) 66 | } 67 | } 68 | 69 | // have right to get colume to see problem list 70 | pub fn check_view_right( 71 | pool: web::Data, 72 | logged_user: LoggedUser, 73 | region: String, 74 | ) -> ServiceResult<()> { 75 | let conn = &db_connection(&pool)?; 76 | if let Some(user) = logged_user.0.clone() { 77 | if is_manager(conn, user.id, region.clone())? { 78 | return Ok(()); 79 | } 80 | } 81 | 82 | let region_type = get_self_type(region.clone(), conn)?; 83 | if ®ion_type == "contest" { 84 | if logged_user.0.is_none() { 85 | return Err(ServiceError::Unauthorized); 86 | } 87 | 88 | use crate::models::contests; 89 | use crate::schema::contests as contests_schema; 90 | 91 | let contest = contests::Contest::from( 92 | contests_schema::table 93 | .filter(contests_schema::region.eq(region.clone())) 94 | .first::(conn)?, 95 | ); 96 | 97 | use contests::ContestState::*; 98 | match contests::get_contest_state(contest.clone(), get_cur_naive_date_time()) { 99 | Preparing => { 100 | if !contest.settings.view_before_start { 101 | let hint = "Contest do not allows viewing before start.".to_owned(); 102 | return Err(ServiceError::UnauthorizedWithHint(hint)); 103 | } 104 | } 105 | Ended => { 106 | if !contest.settings.view_after_end { 107 | let hint = "Contest do not allows viewing after end.".to_owned(); 108 | return Err(ServiceError::UnauthorizedWithHint(hint)); 109 | } else if contest.settings.public_after_end { 110 | return Ok(()); 111 | } 112 | } 113 | _ => (), 114 | } 115 | } 116 | if has_access_setting(conn, region.clone())? { 117 | if let Some(user) = logged_user.0 { 118 | check_acl(conn, user.id, region) 119 | } else { 120 | let hint = "Veiwing region which has access settings need to be logged in.".to_owned(); 121 | return Err(ServiceError::UnauthorizedWithHint(hint)); 122 | } 123 | } else { 124 | Ok(()) 125 | } 126 | } 127 | 128 | // have right to see problem detail and submit in region 129 | pub fn check_solve_right( 130 | pool: web::Data, 131 | logged_user: LoggedUser, 132 | region: String, 133 | ) -> ServiceResult<()> { 134 | let conn = &db_connection(&pool)?; 135 | 136 | if let Some(user) = logged_user.0.clone() { 137 | if is_manager(conn, user.id, region.clone())? { 138 | return Ok(()); 139 | } 140 | } else { 141 | return Err(ServiceError::Unauthorized); 142 | } 143 | 144 | let region_type = get_self_type(region.clone(), conn)?; 145 | if ®ion_type == "contest" { 146 | use crate::models::contests; 147 | use crate::schema::contests as contests_schema; 148 | 149 | let contest = contests::Contest::from( 150 | contests_schema::table 151 | .filter(contests_schema::region.eq(region.clone())) 152 | .first::(conn)?, 153 | ); 154 | 155 | use contests::ContestState::*; 156 | match contests::get_contest_state(contest.clone(), get_cur_naive_date_time()) { 157 | Preparing => { 158 | let hint = "Contest do not allows visiting problems before start.".to_owned(); 159 | return Err(ServiceError::UnauthorizedWithHint(hint)); 160 | } 161 | Ended => { 162 | if !contest.settings.submit_after_end { 163 | let hint = "Contest do not allows visiting problems before start.".to_owned(); 164 | return Err(ServiceError::UnauthorizedWithHint(hint)); 165 | } 166 | } 167 | _ => (), 168 | } 169 | } 170 | if has_access_setting(conn, region.clone())? { 171 | let user = logged_user.0.unwrap(); 172 | check_acl(conn, user.id, region) 173 | } else { 174 | Ok(()) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/controllers/contests/handler.rs: -------------------------------------------------------------------------------- 1 | use crate::auth::region::*; 2 | use crate::models::contests::*; 3 | use crate::models::users::LoggedUser; 4 | use crate::services::contest; 5 | use actix_web::{delete, get, post, put, web, HttpResponse}; 6 | use chrono::*; 7 | use server_core::database::Pool; 8 | use server_core::errors::ServiceError; 9 | 10 | #[derive(Deserialize)] 11 | pub struct CreateContestBody { 12 | region: String, 13 | title: String, 14 | introduction: Option, 15 | start_time: NaiveDateTime, 16 | end_time: Option, 17 | seal_time: Option, 18 | settings: Option, 19 | password: Option, 20 | } 21 | 22 | #[post("")] 23 | pub async fn create( 24 | body: web::Json, 25 | pool: web::Data, 26 | logged_user: LoggedUser, 27 | ) -> Result { 28 | info!("{:?}", logged_user.0); 29 | if logged_user.0.is_none() { 30 | return Err(ServiceError::Unauthorized); 31 | } 32 | let cur_user = logged_user.0.unwrap(); 33 | if cur_user.role != "sup" && cur_user.role != "admin" { 34 | let hint = "No permission.".to_string(); 35 | return Err(ServiceError::BadRequest(hint)); 36 | } 37 | 38 | let res = web::block(move || { 39 | contest::create( 40 | body.region.clone(), 41 | body.title.clone(), 42 | body.introduction.clone(), 43 | body.start_time.clone(), 44 | body.end_time.clone(), 45 | body.seal_time.clone(), 46 | if let Some(settings) = body.settings.clone() { 47 | settings 48 | } else { 49 | ContestSettings::default() 50 | }, 51 | body.password.clone(), 52 | cur_user.id, 53 | pool, 54 | ) 55 | }) 56 | .await 57 | .map_err(|e| { 58 | eprintln!("{}", e); 59 | e 60 | })?; 61 | 62 | Ok(HttpResponse::Ok().json(&res)) 63 | } 64 | 65 | #[derive(Deserialize)] 66 | pub struct GetContestListParams { 67 | title_filter: Option, 68 | include_ended: Option, 69 | limit: i32, 70 | offset: i32, 71 | } 72 | 73 | #[get("")] 74 | pub async fn get_contest_list( 75 | query: web::Query, 76 | pool: web::Data, 77 | logged_user: LoggedUser, 78 | ) -> Result { 79 | let res = web::block(move || { 80 | contest::get_contest_list( 81 | query.title_filter.clone(), 82 | query.include_ended.unwrap_or(true), 83 | query.limit, 84 | query.offset, 85 | if let Some(user) = logged_user.0 { 86 | Some(user.id) 87 | } else { 88 | None 89 | }, 90 | pool, 91 | ) 92 | }) 93 | .await 94 | .map_err(|e| { 95 | eprintln!("{}", e); 96 | e 97 | })?; 98 | 99 | Ok(HttpResponse::Ok().json(&res)) 100 | } 101 | 102 | #[derive(Deserialize)] 103 | pub struct RegisterToRegionBody { 104 | password: Option, 105 | } 106 | 107 | #[post("/{region}/register")] 108 | pub async fn register( 109 | web::Path(region): web::Path, 110 | body: web::Json, 111 | pool: web::Data, 112 | logged_user: LoggedUser, 113 | ) -> Result { 114 | info!("{:?}", logged_user.0); 115 | if logged_user.0.is_none() { 116 | return Err(ServiceError::Unauthorized); 117 | } 118 | 119 | let res = web::block(move || { 120 | contest::register( 121 | region, 122 | body.password.clone(), 123 | logged_user.0.unwrap().id, 124 | pool, 125 | ) 126 | }) 127 | .await 128 | .map_err(|e| { 129 | eprintln!("{}", e); 130 | e 131 | })?; 132 | 133 | Ok(HttpResponse::Ok().json(&res)) 134 | } 135 | 136 | #[get("/{region}/rank_acm")] 137 | pub async fn get_acm_rank( 138 | web::Path(region): web::Path, 139 | pool: web::Data, 140 | logged_user: LoggedUser, 141 | ) -> Result { 142 | check_view_right(pool.clone(), logged_user.clone(), region.clone())?; 143 | 144 | let res = web::block(move || contest::get_acm_rank(region, pool)) 145 | .await 146 | .map_err(|e| { 147 | eprintln!("{}", e); 148 | e 149 | })?; 150 | 151 | Ok(HttpResponse::Ok().json(&res)) 152 | } 153 | 154 | #[delete("/{region}")] 155 | pub async fn delete( 156 | web::Path(region): web::Path, 157 | pool: web::Data, 158 | logged_user: LoggedUser, 159 | ) -> Result { 160 | if logged_user.0.is_none() { 161 | return Err(ServiceError::Unauthorized); 162 | } 163 | let cur_user = logged_user.0.unwrap(); 164 | if cur_user.role != "sup" && cur_user.role != "admin" { 165 | let hint = "No permission.".to_string(); 166 | return Err(ServiceError::BadRequest(hint)); 167 | } 168 | 169 | let res = web::block(move || contest::delete(region, pool)) 170 | .await 171 | .map_err(|e| { 172 | eprintln!("{}", e); 173 | e 174 | })?; 175 | 176 | Ok(HttpResponse::Ok().json(&res)) 177 | } 178 | 179 | #[derive(Deserialize)] 180 | pub struct UpdateContestBody { 181 | new_title: Option, 182 | new_introduction: Option, 183 | new_start_time: Option, 184 | new_end_time: Option, 185 | new_seal_time: Option, 186 | new_settings: Option, 187 | new_password: Option, 188 | } 189 | 190 | #[put("/{region}")] 191 | pub async fn update( 192 | web::Path(region): web::Path, 193 | body: web::Json, 194 | pool: web::Data, 195 | logged_user: LoggedUser, 196 | ) -> Result { 197 | info!("{:?}", logged_user.0); 198 | if logged_user.0.is_none() { 199 | return Err(ServiceError::Unauthorized); 200 | } 201 | let cur_user = logged_user.0.unwrap(); 202 | if cur_user.role != "sup" && cur_user.role != "admin" { 203 | let hint = "No permission.".to_string(); 204 | return Err(ServiceError::BadRequest(hint)); 205 | } 206 | 207 | let res = web::block(move || { 208 | contest::update( 209 | region.clone(), 210 | body.new_title.clone(), 211 | body.new_introduction.clone(), 212 | body.new_start_time.clone(), 213 | body.new_end_time.clone(), 214 | body.new_seal_time.clone(), 215 | body.new_settings.clone(), 216 | body.new_password.clone(), 217 | pool, 218 | ) 219 | }) 220 | .await 221 | .map_err(|e| { 222 | eprintln!("{}", e); 223 | e 224 | })?; 225 | 226 | Ok(HttpResponse::Ok().json(&res)) 227 | } 228 | -------------------------------------------------------------------------------- /src/services/rank/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::models::access_control_list::*; 2 | use crate::models::contests::*; 3 | use crate::models::ranks::*; 4 | use crate::models::region_links::*; 5 | use crate::models::submissions::*; 6 | use crate::services::region::utils::*; 7 | use crate::statics::ACM_RANK_CACHE; 8 | use diesel::prelude::*; 9 | use server_core::errors::ServiceResult; 10 | use server_core::utils::time::get_cur_naive_date_time; 11 | 12 | pub fn update_acm_rank_cache( 13 | region: String, 14 | conn: &PgConnection, 15 | is_final: bool, 16 | ) -> ServiceResult<()> { 17 | log::info!("Updating acm rank"); 18 | if &get_self_type(region.clone(), conn)? != "contest" { 19 | return Ok(()); 20 | } 21 | 22 | use crate::schema::contests as contests_schema; 23 | let contest = Contest::from( 24 | contests_schema::table 25 | .filter(contests_schema::region.eq(region.clone())) 26 | .first::(conn)?, 27 | ); 28 | 29 | let mut rank = ACMRank { 30 | region: region.clone(), 31 | last_updated_time: get_cur_naive_date_time(), 32 | columns: Vec::new(), 33 | }; 34 | 35 | use crate::schema::access_control_list as access_control_list_schema; 36 | 37 | let access_control_list: Vec = access_control_list_schema::table 38 | .filter(access_control_list_schema::region.eq(region.clone())) 39 | .load(conn)?; 40 | 41 | for access_control_list_colunm in access_control_list { 42 | rank.columns.push(build_acm_rank_column( 43 | contest.clone(), 44 | access_control_list_colunm, 45 | conn, 46 | is_final, 47 | )?); 48 | } 49 | 50 | let slice = rank.columns.as_mut_slice(); 51 | slice.sort_by(|colume_a, colume_b| { 52 | if colume_a.total_accepted != colume_b.total_accepted { 53 | colume_a 54 | .total_accepted 55 | .cmp(&colume_b.total_accepted) 56 | .reverse() 57 | } else { 58 | colume_a.time_cost.cmp(&colume_b.time_cost) 59 | } 60 | }); 61 | 62 | // assgin rank 63 | let mut rank_count = 0; 64 | use crate::schema::region_links as region_links_schema; 65 | let mut last_total_accepted = region_links_schema::table 66 | .filter(region_links_schema::region.eq(region.clone())) 67 | .count() 68 | .get_result::(conn)? as i32 69 | + 1; 70 | let mut last_time_cost = 0; 71 | for colume in slice.iter_mut() { 72 | if colume.is_unrated == Some(true) { 73 | continue; 74 | } 75 | if colume.total_accepted < last_total_accepted { 76 | rank_count += 1; 77 | } else if colume.time_cost > last_time_cost { 78 | rank_count += 1; 79 | } 80 | 81 | last_total_accepted = colume.total_accepted; 82 | last_time_cost = colume.time_cost; 83 | colume.rank = Some(rank_count); 84 | } 85 | rank.columns = slice.to_vec(); 86 | 87 | ACM_RANK_CACHE.write().unwrap().insert(region, rank); 88 | 89 | Ok(()) 90 | } 91 | 92 | fn build_acm_rank_column( 93 | contest: Contest, 94 | access_control_list_column: AccessControlListColumn, 95 | conn: &PgConnection, 96 | is_final: bool, 97 | ) -> ServiceResult { 98 | let region = access_control_list_column.region; 99 | let user_id = access_control_list_column.user_id; 100 | let is_unrated = access_control_list_column.is_unrated; 101 | 102 | use crate::schema::users as users_schema; 103 | let account = users_schema::table 104 | .filter(users_schema::id.eq(user_id)) 105 | .select(users_schema::account) 106 | .first::(conn)?; 107 | 108 | let mut rank_column = ACMRankColumn { 109 | rank: None, 110 | user_id, 111 | account, 112 | total_accepted: 0, 113 | time_cost: 0, 114 | is_unrated, 115 | problem_block: Vec::new(), 116 | }; 117 | 118 | use crate::schema::region_links as region_links_schema; 119 | let region_links: Vec = region_links_schema::table 120 | .filter(region_links_schema::region.eq(region.clone())) 121 | .order(region_links_schema::inner_id.asc()) 122 | .load(conn)?; 123 | 124 | for region_link in region_links { 125 | let mut problem_block = ACMProblemBlock { 126 | inner_id: region_link.inner_id, 127 | is_accepted: None, 128 | is_first_accepted: false, 129 | is_sealed: false, 130 | try_times: 0, 131 | last_submit_time: None, 132 | }; 133 | 134 | use crate::schema::submissions as submissions_schema; 135 | let submissions: Vec = submissions_schema::table 136 | .filter(submissions_schema::user_id.eq(user_id)) 137 | .filter(submissions_schema::region.eq(region.clone())) 138 | .filter(submissions_schema::problem_id.eq(region_link.problem_id)) 139 | .filter(submissions_schema::is_accepted.is_not_null()) 140 | .order(submissions_schema::submit_time.asc()) 141 | .load(conn)?; 142 | 143 | for submission in submissions { 144 | let submit_state = get_contest_state(contest.clone(), submission.submit_time); 145 | 146 | if submit_state == ContestState::Preparing { 147 | continue; 148 | } else if submit_state == ContestState::SealedRunning && !is_final { 149 | problem_block.is_accepted = None; 150 | problem_block.is_sealed = true; 151 | problem_block.try_times += 1; 152 | problem_block.last_submit_time = Some(submission.submit_time); 153 | if submission.is_accepted.unwrap() { 154 | break; 155 | } 156 | } else if submit_state == ContestState::Running 157 | || (submit_state == ContestState::SealedRunning && is_final) 158 | { 159 | problem_block.is_accepted = submission.is_accepted; 160 | problem_block.is_sealed = false; 161 | problem_block.try_times += 1; 162 | problem_block.last_submit_time = Some(submission.submit_time); 163 | if submission.is_accepted.unwrap() { 164 | rank_column.total_accepted += 1; 165 | rank_column.time_cost += 20 * 60 * (problem_block.try_times as i64 - 1) 166 | + submission.submit_time.timestamp() 167 | - contest.start_time.timestamp(); 168 | 169 | if submissions_schema::table 170 | .filter(submissions_schema::region.eq(region.clone())) 171 | .filter(submissions_schema::problem_id.eq(region_link.problem_id)) 172 | .filter(submissions_schema::is_accepted.eq(Some(true))) 173 | .filter(submissions_schema::submit_time.lt(submission.submit_time)) 174 | .count() 175 | .get_result::(conn)? 176 | == 0 177 | { 178 | problem_block.is_first_accepted = true; 179 | } 180 | break; 181 | } 182 | } else { 183 | break; 184 | } 185 | } 186 | 187 | rank_column.problem_block.push(problem_block); 188 | } 189 | 190 | Ok(rank_column) 191 | } 192 | -------------------------------------------------------------------------------- /src/controllers/users/handler.rs: -------------------------------------------------------------------------------- 1 | use crate::models::users::LoggedUser; 2 | use crate::services::user; 3 | use actix_identity::Identity; 4 | use actix_web::{delete, get, post, put, web, HttpResponse}; 5 | use server_core::database::Pool; 6 | use server_core::errors::ServiceError; 7 | 8 | #[derive(Deserialize)] 9 | pub struct GetUserListParams { 10 | id_filter: Option, 11 | account_filter: Option, 12 | mobile_filter: Option, 13 | role_filter: Option, 14 | id_order: Option, 15 | limit: i32, 16 | offset: i32, 17 | } 18 | 19 | #[get("")] 20 | pub async fn get_list( 21 | query: web::Query, 22 | pool: web::Data, 23 | ) -> Result { 24 | let res = web::block(move || { 25 | user::get_list( 26 | query.id_filter, 27 | query.account_filter.clone(), 28 | query.mobile_filter.clone(), 29 | query.role_filter.clone(), 30 | query.id_order.clone(), 31 | query.limit, 32 | query.offset, 33 | pool, 34 | ) 35 | }) 36 | .await 37 | .map_err(|e| { 38 | eprintln!("{}", e); 39 | e 40 | })?; 41 | 42 | Ok(HttpResponse::Ok().json(&res)) 43 | } 44 | 45 | #[derive(Deserialize)] 46 | pub struct CreateUserBody { 47 | account: String, 48 | password: String, 49 | mobile: Option, 50 | role: String, 51 | } 52 | 53 | #[post("")] 54 | pub async fn create( 55 | body: web::Json, 56 | pool: web::Data, 57 | ) -> Result { 58 | let res = web::block(move || { 59 | user::create( 60 | body.account.clone(), 61 | body.password.clone(), 62 | body.mobile.clone(), 63 | body.role.clone(), 64 | pool, 65 | ) 66 | }) 67 | .await 68 | .map_err(|e| { 69 | eprintln!("{}", e); 70 | e 71 | })?; 72 | 73 | Ok(HttpResponse::Ok().json(&res)) 74 | } 75 | 76 | #[get("/{id}/name")] 77 | pub async fn get_name( 78 | web::Path(id): web::Path, 79 | pool: web::Data, 80 | ) -> Result { 81 | let res = web::block(move || user::get_name(id, pool)) 82 | .await 83 | .map_err(|e| { 84 | eprintln!("{}", e); 85 | e 86 | })?; 87 | 88 | Ok(HttpResponse::Ok().json(&res)) 89 | } 90 | 91 | #[get("/{id}")] 92 | pub async fn get( 93 | web::Path(id): web::Path, 94 | pool: web::Data, 95 | ) -> Result { 96 | let res = web::block(move || user::get(id, pool)).await.map_err(|e| { 97 | eprintln!("{}", e); 98 | e 99 | })?; 100 | 101 | Ok(HttpResponse::Ok().json(&res)) 102 | } 103 | 104 | #[derive(Deserialize)] 105 | pub struct UpdateUserBody { 106 | new_account: Option, 107 | new_password: Option, 108 | new_mobile: Option, 109 | new_role: Option, 110 | } 111 | 112 | #[put("/{id}")] 113 | pub async fn update( 114 | web::Path(id): web::Path, 115 | body: web::Json, 116 | logged_user: LoggedUser, 117 | pool: web::Data, 118 | ) -> Result { 119 | if logged_user.0.is_none() { 120 | return Err(ServiceError::Unauthorized); 121 | } 122 | let cur_user = logged_user.0.unwrap(); 123 | if cur_user.id != id && cur_user.role != "sup" && cur_user.role != "admin" { 124 | let hint = "No permission.".to_string(); 125 | return Err(ServiceError::BadRequest(hint)); 126 | } 127 | 128 | let res = web::block(move || { 129 | user::update( 130 | id, 131 | body.new_account.clone(), 132 | body.new_password.clone(), 133 | body.new_mobile.clone(), 134 | body.new_role.clone(), 135 | pool, 136 | ) 137 | }) 138 | .await 139 | .map_err(|e| { 140 | eprintln!("{}", e); 141 | e 142 | })?; 143 | 144 | Ok(HttpResponse::Ok().json(&res)) 145 | } 146 | 147 | #[derive(Deserialize)] 148 | pub struct LoginBody { 149 | account: String, 150 | password: String, 151 | } 152 | 153 | #[post("/login")] 154 | pub async fn login( 155 | body: web::Json, 156 | identity: Identity, 157 | pool: web::Data, 158 | ) -> Result { 159 | let res = web::block(move || user::login(body.account.clone(), body.password.clone(), pool)) 160 | .await 161 | .map_err(|e| { 162 | eprintln!("{}", e); 163 | e 164 | })?; 165 | 166 | let user_string = serde_json::to_string(&res).map_err(|_| ServiceError::InternalServerError)?; 167 | info!("user_string={}", user_string); 168 | identity.remember(user_string); 169 | Ok(HttpResponse::Ok().json(res)) 170 | } 171 | 172 | #[post("/logout")] 173 | pub fn logout(identity: Identity) -> HttpResponse { 174 | identity.forget(); 175 | HttpResponse::Ok().finish() 176 | } 177 | 178 | #[get("/me")] 179 | pub async fn me(logged_user: LoggedUser) -> Result { 180 | if let Some(res) = logged_user.0 { 181 | Ok(HttpResponse::Ok().json(&res)) 182 | } else { 183 | Err(ServiceError::Unauthorized) 184 | } 185 | } 186 | 187 | #[derive(Deserialize)] 188 | pub struct GetPermittedMethodsParams { 189 | path: String, 190 | } 191 | 192 | #[get("/permitted_methods")] 193 | pub async fn get_permitted_methods( 194 | query: web::Query, 195 | logged_user: LoggedUser, 196 | ) -> Result { 197 | if logged_user.0.is_none() { 198 | return Err(ServiceError::Unauthorized); 199 | } 200 | let cur_user = logged_user.0.unwrap(); 201 | 202 | let res = web::block(move || user::get_permitted_methods(cur_user.role, query.path.clone())) 203 | .await 204 | .map_err(|e| { 205 | eprintln!("{}", e); 206 | e 207 | })?; 208 | 209 | Ok(HttpResponse::Ok().json(&res)) 210 | } 211 | 212 | #[delete("/{id}")] 213 | pub async fn delete( 214 | web::Path(id): web::Path, 215 | logged_user: LoggedUser, 216 | pool: web::Data, 217 | ) -> Result { 218 | if logged_user.0.is_none() { 219 | return Err(ServiceError::Unauthorized); 220 | } 221 | let cur_user = logged_user.0.unwrap(); 222 | if cur_user.id != id && cur_user.role != "sup" && cur_user.role != "admin" { 223 | let hint = "No permission.".to_string(); 224 | return Err(ServiceError::BadRequest(hint)); 225 | } 226 | 227 | let res = web::block(move || user::delete(id, pool)) 228 | .await 229 | .map_err(|e| { 230 | eprintln!("{}", e); 231 | e 232 | })?; 233 | 234 | Ok(HttpResponse::Ok().json(&res)) 235 | } 236 | 237 | #[get("/{id}/submissions_count")] 238 | pub async fn get_submissions_count( 239 | web::Path(user_id): web::Path, 240 | logged_user: LoggedUser, 241 | pool: web::Data, 242 | ) -> Result { 243 | if logged_user.0.is_none() { 244 | return Err(ServiceError::Unauthorized); 245 | } 246 | 247 | let res = web::block(move || user::get_submissions_count(user_id, pool)) 248 | .await 249 | .map_err(|e| { 250 | eprintln!("{}", e); 251 | e 252 | })?; 253 | 254 | Ok(HttpResponse::Ok().json(&res)) 255 | } 256 | 257 | #[get("/{id}/submissions_time")] 258 | pub async fn get_submissions_time( 259 | web::Path(user_id): web::Path, 260 | logged_user: LoggedUser, 261 | pool: web::Data, 262 | ) -> Result { 263 | if logged_user.0.is_none() { 264 | return Err(ServiceError::Unauthorized); 265 | } 266 | 267 | let res = web::block(move || user::get_submissions_time(user_id, pool)) 268 | .await 269 | .map_err(|e| { 270 | eprintln!("{}", e); 271 | e 272 | })?; 273 | 274 | Ok(HttpResponse::Ok().json(&res)) 275 | } 276 | -------------------------------------------------------------------------------- /src/controllers/problems/handler.rs: -------------------------------------------------------------------------------- 1 | use crate::models::problems::{ProblemContents, ProblemInfo, ProblemSettings}; 2 | use crate::models::users::LoggedUser; 3 | use crate::services::problem; 4 | use actix_multipart::Multipart; 5 | use actix_web::{delete, get, post, put, web, HttpResponse}; 6 | use futures::{StreamExt, TryStreamExt}; 7 | use server_core::database::Pool; 8 | use server_core::errors::ServiceError; 9 | 10 | #[post("")] 11 | pub async fn batch_create( 12 | //logged_user: LoggedUser, 13 | mut payload: Multipart, 14 | pool: web::Data, 15 | ) -> Result { 16 | let mut bytes = web::BytesMut::new(); 17 | // iterate over multipart stream 18 | let mut filename = None; 19 | while let Ok(Some(mut field)) = payload.try_next().await { 20 | let content_type = field.content_disposition().unwrap(); 21 | if filename.is_none() { 22 | filename = Some(content_type.get_filename().unwrap().to_owned()); 23 | } else { 24 | // only accept one file 25 | if filename.clone().unwrap() != content_type.get_filename().unwrap() { 26 | continue; 27 | } 28 | } 29 | 30 | // Field in turn is stream of *Bytes* object 31 | while let Some(chunk) = field.next().await { 32 | let data = chunk.unwrap(); 33 | // filesystem operations are blocking, we have to use threadpool 34 | bytes.extend_from_slice(&data); 35 | } 36 | } 37 | 38 | let res = web::block(move || problem::batch_create(&bytes, pool)) 39 | .await 40 | .map_err(|e| { 41 | eprintln!("{}", e); 42 | e 43 | })?; 44 | 45 | Ok(HttpResponse::Ok().json(res)) 46 | } 47 | 48 | #[derive(Deserialize)] 49 | pub struct ChangeReleaseStateBody { 50 | target_state: bool, 51 | } 52 | 53 | #[post("/{id}/change_release_state")] 54 | pub async fn change_release_state( 55 | web::Path(id): web::Path, 56 | body: web::Json, 57 | logged_user: LoggedUser, 58 | pool: web::Data, 59 | ) -> Result { 60 | if logged_user.0.is_none() { 61 | return Err(ServiceError::Unauthorized); 62 | } 63 | let cur_user = logged_user.0.unwrap(); 64 | if cur_user.role != "sup" && cur_user.role != "admin" { 65 | let hint = "No permission.".to_string(); 66 | return Err(ServiceError::BadRequest(hint)); 67 | } 68 | 69 | web::block(move || problem::change_release_state(id, body.target_state, pool)) 70 | .await 71 | .map_err(|e| { 72 | eprintln!("{}", e); 73 | e 74 | })?; 75 | 76 | Ok(HttpResponse::Ok().finish()) 77 | } 78 | 79 | #[derive(Deserialize)] 80 | pub struct GetProblemListParams { 81 | id_filter: Option, 82 | title_filter: Option, 83 | tag_filter: Option>, 84 | difficulty_filter: Option, 85 | release_filter: Option, 86 | id_order: Option, 87 | difficulty_order: Option, 88 | limit: i32, 89 | offset: i32, 90 | } 91 | 92 | #[get("")] 93 | pub async fn get_list( 94 | query: web::Query, 95 | pool: web::Data, 96 | ) -> Result { 97 | let res = web::block(move || { 98 | problem::get_list( 99 | query.id_filter, 100 | query.title_filter.clone(), 101 | query.tag_filter.clone(), 102 | query.difficulty_filter.clone(), 103 | query.release_filter.clone(), 104 | query.id_order.clone(), 105 | query.difficulty_order.clone(), 106 | query.limit, 107 | query.offset, 108 | pool, 109 | ) 110 | }) 111 | .await 112 | .map_err(|e| { 113 | eprintln!("{}", e); 114 | e 115 | })?; 116 | 117 | Ok(HttpResponse::Ok().json(&res)) 118 | } 119 | 120 | #[get("/{id}/title")] 121 | pub async fn get_title( 122 | web::Path(id): web::Path, 123 | pool: web::Data, 124 | ) -> Result { 125 | let res = web::block(move || problem::get_title(id, pool)) 126 | .await 127 | .map_err(|e| { 128 | eprintln!("{}", e); 129 | e 130 | })?; 131 | 132 | Ok(HttpResponse::Ok().json(&res)) 133 | } 134 | 135 | #[get("/{id}")] 136 | pub async fn get( 137 | web::Path(id): web::Path, 138 | logged_user: LoggedUser, 139 | pool: web::Data, 140 | ) -> Result { 141 | if logged_user.0.is_none() { 142 | return Err(ServiceError::Unauthorized); 143 | } 144 | let cur_user = logged_user.0.unwrap(); 145 | if cur_user.role != "sup" && cur_user.role != "admin" { 146 | let hint = "No permission.".to_string(); 147 | return Err(ServiceError::BadRequest(hint)); 148 | } 149 | 150 | let res = web::block(move || problem::get(id, pool)) 151 | .await 152 | .map_err(|e| { 153 | eprintln!("{}", e); 154 | e 155 | })?; 156 | 157 | Ok(HttpResponse::Ok().json(&res)) 158 | } 159 | 160 | #[delete("/{id}")] 161 | pub async fn delete( 162 | web::Path(id): web::Path, 163 | logged_user: LoggedUser, 164 | pool: web::Data, 165 | ) -> Result { 166 | if logged_user.0.is_none() { 167 | return Err(ServiceError::Unauthorized); 168 | } 169 | let cur_user = logged_user.0.unwrap(); 170 | if cur_user.role != "sup" && cur_user.role != "admin" { 171 | let hint = "No permission.".to_string(); 172 | return Err(ServiceError::BadRequest(hint)); 173 | } 174 | 175 | let res = web::block(move || problem::delete(id, pool)) 176 | .await 177 | .map_err(|e| { 178 | eprintln!("{}", e); 179 | e 180 | })?; 181 | 182 | Ok(HttpResponse::Ok().json(&res)) 183 | } 184 | 185 | #[derive(Deserialize)] 186 | pub struct CreateProblemBody { 187 | info: ProblemInfo, 188 | contents: ProblemContents, 189 | settings: ProblemSettings, 190 | } 191 | 192 | #[post("/create")] 193 | pub async fn create( 194 | body: web::Json, 195 | pool: web::Data, 196 | logged_user: LoggedUser, 197 | ) -> Result { 198 | if logged_user.0.is_none() { 199 | return Err(ServiceError::Unauthorized); 200 | } 201 | let cur_user = logged_user.0.unwrap(); 202 | if cur_user.role != "sup" && cur_user.role != "admin" { 203 | let hint = "No permission.".to_string(); 204 | return Err(ServiceError::BadRequest(hint)); 205 | } 206 | 207 | let res = web::block(move || { 208 | problem::create( 209 | body.info.clone(), 210 | body.contents.clone(), 211 | body.settings.clone(), 212 | pool, 213 | ) 214 | }) 215 | .await 216 | .map_err(|e| { 217 | eprintln!("{}", e); 218 | e 219 | })?; 220 | 221 | Ok(HttpResponse::Ok().json(&res)) 222 | } 223 | 224 | #[derive(Deserialize)] 225 | pub struct UpdateProblemBody { 226 | new_info: Option, 227 | new_contents: Option, 228 | new_settings: Option, 229 | } 230 | 231 | #[put("/{id}")] 232 | pub async fn update( 233 | web::Path(id): web::Path, 234 | body: web::Json, 235 | logged_user: LoggedUser, 236 | pool: web::Data, 237 | ) -> Result { 238 | if logged_user.0.is_none() { 239 | return Err(ServiceError::Unauthorized); 240 | } 241 | let cur_user = logged_user.0.unwrap(); 242 | if cur_user.role != "sup" && cur_user.role != "admin" { 243 | let hint = "No permission.".to_string(); 244 | return Err(ServiceError::BadRequest(hint)); 245 | } 246 | 247 | let res = web::block(move || { 248 | problem::update( 249 | id, 250 | body.new_info.clone(), 251 | body.new_contents.clone(), 252 | body.new_settings.clone(), 253 | pool, 254 | ) 255 | }) 256 | .await 257 | .map_err(|e| { 258 | eprintln!("{}", e); 259 | e 260 | })?; 261 | 262 | Ok(HttpResponse::Ok().json(&res)) 263 | } 264 | -------------------------------------------------------------------------------- /src/services/problem/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::models::problems; 2 | use digest::Digest; 3 | use hex::ToHex; 4 | use md5::Md5; 5 | use server_core::errors::{ServiceError, ServiceResult}; 6 | use std::collections::BTreeMap; 7 | use std::fs::File; 8 | use std::io::prelude::*; 9 | 10 | fn read_settings(path: &str) -> std::io::Result { 11 | let mut file = File::open(path)?; 12 | let mut contents = String::new(); 13 | file.read_to_string(&mut contents)?; 14 | 15 | let settings: problems::ProblemSettings = toml::from_str(&contents)?; 16 | 17 | Ok(settings) 18 | } 19 | 20 | fn read_info(path: &str) -> std::io::Result { 21 | let mut file = File::open(path)?; 22 | let mut contents = String::new(); 23 | file.read_to_string(&mut contents)?; 24 | 25 | let info: problems::ProblemInfo = toml::from_str(&contents)?; 26 | 27 | Ok(info) 28 | } 29 | 30 | fn read_description(path: &str) -> std::io::Result> { 31 | let mut file = match File::open(path) { 32 | Ok(file) => file, 33 | Err(_) => { 34 | return Ok(None); 35 | } 36 | }; 37 | let mut contents = String::new(); 38 | file.read_to_string(&mut contents)?; 39 | 40 | Ok(Some(contents)) 41 | } 42 | 43 | fn read_examples(path: &str) -> std::io::Result<(Vec, i32)> { 44 | let mut examples = Vec::new(); 45 | 46 | let mut count = 0; 47 | loop { 48 | let id = count + 1; 49 | let input_name = id.to_string() + ".in"; 50 | let mut input_file = match File::open(path.to_string() + "/" + &input_name) { 51 | Ok(file) => file, 52 | Err(_) => { 53 | break; 54 | } 55 | }; 56 | let mut input_content = String::new(); 57 | input_file.read_to_string(&mut input_content)?; 58 | 59 | let output_name = id.to_string() + ".out"; 60 | let mut output_file = match File::open(path.to_string() + "/" + &output_name) { 61 | Ok(file) => file, 62 | Err(_) => { 63 | break; 64 | } 65 | }; 66 | let mut output_content = String::new(); 67 | output_file.read_to_string(&mut output_content)?; 68 | 69 | examples.push(problems::Example { 70 | input: input_content, 71 | output: output_content, 72 | }); 73 | 74 | count += 1; 75 | } 76 | 77 | Ok((examples, count)) 78 | } 79 | 80 | pub fn read_insertable_problem(path: &str) -> ServiceResult { 81 | let info_path = path.to_string() + "/Info.toml"; 82 | let description_path = path.to_string() + "/Description.md"; 83 | let examples_path = path.to_string() + "/Examples"; 84 | let settings_path = path.to_string() + "/Settings.toml"; 85 | 86 | let info = read_info(&info_path)?; 87 | let description = read_description(&description_path)?; 88 | let (examples, example_count) = read_examples(&examples_path)?; 89 | let settings = read_settings(&settings_path)?; 90 | 91 | let contents = problems::ProblemContents { 92 | description: description, 93 | example_count: example_count, 94 | examples: examples, 95 | }; 96 | 97 | Ok(problems::InsertableProblem { 98 | title: info.title, 99 | tags: info.tags, 100 | difficulty: info.difficulty, 101 | contents: serde_json::to_string(&contents).unwrap(), 102 | settings: serde_json::to_string(&settings).unwrap(), 103 | is_released: false, 104 | }) 105 | } 106 | 107 | fn hash_token(key: &str, output: &mut [u8]) { 108 | let mut hasher = D::new(); 109 | hasher.update(key.as_bytes()); 110 | output.copy_from_slice(&hasher.finalize()) 111 | } 112 | 113 | fn get_stripped_md5_output(output: &str) -> String { 114 | let mut buf = [0u8; 16]; 115 | hash_token::(output.trim_end(), &mut buf); 116 | (&buf[..]).to_vec().encode_hex::() 117 | } 118 | 119 | #[derive(Debug, Clone, Serialize)] 120 | struct NormalTestCaseInfo { 121 | input_name: String, 122 | input_size: i32, 123 | output_name: String, 124 | output_size: i32, 125 | stripped_output_md5: String, 126 | } 127 | 128 | #[derive(Debug, Clone, Serialize)] 129 | struct SpjTestCaseInfo { 130 | input_name: String, 131 | input_size: i32, 132 | } 133 | 134 | fn prepare_normal_test_cases(path: &str) -> ServiceResult { 135 | let mut count = 0; 136 | let mut test_cases: BTreeMap = BTreeMap::new(); 137 | 138 | loop { 139 | let id = count + 1; 140 | let input_name = id.to_string() + ".in"; 141 | let mut input_file = match File::open(path.to_string() + "/" + &input_name) { 142 | Ok(file) => file, 143 | Err(_) => { 144 | info!("Can't find file {}", path.to_string() + "/" + &input_name); 145 | break; 146 | } 147 | }; 148 | let mut input_content = String::new(); 149 | input_file.read_to_string(&mut input_content)?; 150 | 151 | let output_name = id.to_string() + ".out"; 152 | let mut output_file = match File::open(path.to_string() + "/" + &output_name) { 153 | Ok(file) => file, 154 | Err(_) => { 155 | info!("Can't find file {}", path.to_string() + "/" + &output_name); 156 | break; 157 | } 158 | }; 159 | let mut output_content = String::new(); 160 | output_file.read_to_string(&mut output_content)?; 161 | 162 | test_cases.insert( 163 | id.to_string(), 164 | NormalTestCaseInfo { 165 | input_name: input_name, 166 | input_size: input_content.len() as i32, 167 | output_name: output_name, 168 | output_size: output_content.len() as i32, 169 | stripped_output_md5: get_stripped_md5_output(&output_content), 170 | }, 171 | ); 172 | 173 | count += 1; 174 | } 175 | 176 | if count == 0 { 177 | let hint = String::from("Need at least one test case."); 178 | return Err(ServiceError::BadRequest(hint)); 179 | } 180 | 181 | let info = serde_json::json!({ 182 | "test_case_number": count, 183 | "spj": false, 184 | "test_cases": test_cases, 185 | }); 186 | 187 | let mut file = File::create(&(path.to_string() + "/info"))?; 188 | file.write_all(info.to_string().as_bytes())?; 189 | 190 | Ok(count) 191 | } 192 | 193 | fn prepare_spj_test_case(path: &str) -> ServiceResult { 194 | let mut count = 0; 195 | let mut test_cases: BTreeMap = BTreeMap::new(); 196 | 197 | loop { 198 | // check if spj_src.c exists 199 | if count == 0 { 200 | File::open(path.to_string() + "/spj_src.c")?; 201 | } 202 | let name = (count + 1).to_string() + ".in"; 203 | let mut file = match File::open(path.to_string() + "/" + &name) { 204 | Ok(file) => file, 205 | Err(_) => { 206 | info!("Can't find file {}", path.to_string() + "/" + &name); 207 | break; 208 | } 209 | }; 210 | let mut content = String::new(); 211 | file.read_to_string(&mut content).unwrap(); 212 | test_cases.insert( 213 | count.to_string(), 214 | SpjTestCaseInfo { 215 | input_name: name, 216 | input_size: content.len() as i32, 217 | }, 218 | ); 219 | 220 | count += 1; 221 | } 222 | 223 | if count == 0 { 224 | let hint = String::from("Need at least one test case."); 225 | return Err(ServiceError::BadRequest(hint)); 226 | } 227 | 228 | let info = serde_json::json!({ 229 | "test_case_number": count, 230 | "spj": true, 231 | "test_cases": test_cases, 232 | }); 233 | 234 | let mut file = File::create(path.to_string() + "/" + "info").expect("Error creating info"); 235 | file.write_all(info.to_string().as_bytes()) 236 | .expect("Error writing info"); 237 | 238 | Ok(count) 239 | } 240 | 241 | pub fn prepare_test_cases(path: &str, is_spj: bool) -> ServiceResult { 242 | if is_spj { 243 | Ok(prepare_spj_test_case(path)?) 244 | } else { 245 | Ok(prepare_normal_test_cases(path)?) 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/judge_actor/handler.rs: -------------------------------------------------------------------------------- 1 | use super::statistics::*; 2 | use super::utils::*; 3 | use super::JudgeActor; 4 | use crate::models::*; 5 | use crate::services::rank::utils::update_acm_rank_cache; 6 | use crate::services::region::utils::get_self_type; 7 | use crate::statics::JUDGE_SERVER_INFOS; 8 | use crate::statics::WAITING_QUEUE; 9 | use actix::prelude::*; 10 | use diesel::prelude::*; 11 | use server_core::database::db_connection; 12 | use server_core::utils::time::get_cur_naive_date_time; 13 | 14 | #[derive(Debug, Clone, Deserialize)] 15 | pub struct StartJudge(); 16 | 17 | impl Message for StartJudge { 18 | type Result = (); 19 | } 20 | 21 | impl Handler for JudgeActor { 22 | type Result = (); 23 | 24 | fn handle(&mut self, _msg: StartJudge, _: &mut Self::Context) -> Self::Result { 25 | use crate::schema::submissions as submissions_schema; 26 | 27 | let conn = match db_connection(&self.pool) { 28 | Ok(conn) => conn, 29 | Err(_) => { 30 | return; 31 | } 32 | }; 33 | 34 | let mut queue_size = { 35 | let lock = WAITING_QUEUE.read().unwrap(); 36 | lock.len() 37 | }; 38 | log::info!("queue_size: {}", queue_size); 39 | while queue_size != 0 { 40 | let server = choose_judge_server(); 41 | if server.is_none() { 42 | return; 43 | } 44 | let (server_url, server_token) = server.unwrap(); 45 | 46 | let task_uuid = { 47 | let mut lock = WAITING_QUEUE.write().unwrap(); 48 | lock.pop_front().clone().unwrap() 49 | }; 50 | 51 | let cur_state = match submissions_schema::table 52 | .filter(submissions_schema::id.eq(task_uuid)) 53 | .select(submissions_schema::state) 54 | .first::(&conn) 55 | { 56 | Ok(cur_state) => cur_state, 57 | Err(_) => { 58 | log::error!("Error loading setting_data from submissions."); 59 | return; 60 | } 61 | }; 62 | 63 | if cur_state == *"Waiting" { 64 | // run judge 65 | let setting_string = match submissions_schema::table 66 | .filter(submissions_schema::id.eq(task_uuid)) 67 | .select(submissions_schema::settings) 68 | .first::(&conn) 69 | { 70 | Ok(setting_string) => setting_string, 71 | Err(_) => { 72 | log::error!("Error loading setting_data from submissions."); 73 | return; 74 | } 75 | }; 76 | 77 | let target = submissions_schema::table.filter(submissions_schema::id.eq(task_uuid)); 78 | match diesel::update(target) 79 | .set((submissions_schema::state.eq("Pending".to_owned()),)) 80 | .execute(&conn) 81 | { 82 | Ok(_) => (), 83 | Err(_) => { 84 | log::error!("Error changing submissions's state to Pending."); 85 | return; 86 | } 87 | }; 88 | 89 | info!("sending request to {}", server_url); 90 | { 91 | let mut lock = JUDGE_SERVER_INFOS.write().unwrap(); 92 | let mut server_info = lock.get_mut(&server_url).unwrap(); 93 | server_info.task_number += 1; 94 | } 95 | let result_string = 96 | run_judge_client(server_token, server_url.clone(), setting_string); 97 | info!("{}", result_string); 98 | 99 | { 100 | let mut lock = JUDGE_SERVER_INFOS.write().unwrap(); 101 | let mut server_info = lock.get_mut(&server_url).unwrap(); 102 | server_info.task_number -= 1; 103 | } 104 | 105 | if result_string == *"" { 106 | let target = 107 | submissions_schema::table.filter(submissions_schema::id.eq(task_uuid)); 108 | match diesel::update(target) 109 | .set((submissions_schema::state.eq("Waiting".to_owned()),)) 110 | .execute(&conn) 111 | { 112 | Ok(_) => (), 113 | Err(_) => { 114 | log::error!("Error changing submissions's state to Waiting."); 115 | return; 116 | } 117 | }; 118 | 119 | { 120 | let mut lock = WAITING_QUEUE.write().unwrap(); 121 | lock.push_front(task_uuid); 122 | } 123 | 124 | info!("pushed {} back to queue", task_uuid); 125 | continue; 126 | } 127 | 128 | let raw_result = 129 | serde_json::from_str::(&result_string).unwrap(); 130 | let result = submissions::JudgeResult::from(raw_result); 131 | 132 | // update submissions 133 | let target = submissions_schema::table.filter(submissions_schema::id.eq(task_uuid)); 134 | let mut result_set = std::collections::HashSet::new(); 135 | match diesel::update(target) 136 | .set(( 137 | submissions_schema::state.eq("Finished".to_owned()), 138 | submissions_schema::result.eq(serde_json::to_string(&result).unwrap()), 139 | submissions_schema::is_accepted.eq(result.is_accepted), 140 | submissions_schema::finish_time.eq(get_cur_naive_date_time()), 141 | submissions_schema::max_time.eq(result.max_time), 142 | submissions_schema::max_memory.eq(result.max_memory), 143 | submissions_schema::err.eq(result.err), 144 | submissions_schema::out_results.eq({ 145 | if let Some(details) = result.details { 146 | let mut res = Vec::new(); 147 | for detail in details { 148 | result_set.insert(detail.result); 149 | } 150 | for e in result_set.clone() { 151 | res.push(e); 152 | } 153 | Some(res) 154 | } else { 155 | None 156 | } 157 | }), 158 | )) 159 | .execute(&conn) 160 | { 161 | Ok(_) => (), 162 | Err(_) => { 163 | log::error!("Error changing submissions's data."); 164 | return; 165 | } 166 | }; 167 | 168 | let submission = submissions::Submission::from( 169 | match submissions_schema::table 170 | .filter(submissions_schema::id.eq(task_uuid)) 171 | .first::(&conn) 172 | { 173 | Ok(raw_submission) => raw_submission, 174 | Err(_) => { 175 | log::error!("Error querying submission."); 176 | return; 177 | } 178 | }, 179 | ); 180 | 181 | match common_region::update_results(&conn, submission.clone()) { 182 | Ok(_) => {} 183 | Err(_) => { 184 | log::error!("Error updating results type."); 185 | return; 186 | } 187 | }; 188 | 189 | if let Some(region) = submission.region.clone() { 190 | if match get_self_type(region, &conn) { 191 | Ok(region_type) => region_type, 192 | Err(_) => { 193 | log::error!("Error getting region type."); 194 | return; 195 | } 196 | } == "contest" 197 | { 198 | match update_acm_rank_cache(submission.region.unwrap(), &conn, false) { 199 | Ok(_) => (), 200 | Err(_) => { 201 | log::error!("Error updating acm rank cache."); 202 | return; 203 | } 204 | }; 205 | } 206 | } 207 | } 208 | 209 | queue_size = { 210 | let lock = WAITING_QUEUE.read().unwrap(); 211 | lock.len() 212 | }; 213 | } 214 | 215 | () 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/services/region/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod utils; 2 | 3 | use crate::judge_actor::JudgeActorAddr; 4 | use crate::models::problems::*; 5 | use crate::models::region_links::*; 6 | use crate::models::regions::*; 7 | use crate::models::utils::SizedList; 8 | use actix_web::web; 9 | use diesel::prelude::*; 10 | use server_core::database::{db_connection, Pool}; 11 | use server_core::errors::{ServiceError, ServiceResult}; 12 | use uuid::Uuid; 13 | 14 | pub fn get_list( 15 | self_type: Option, 16 | limit: i32, 17 | offset: i32, 18 | pool: web::Data, 19 | ) -> ServiceResult> { 20 | let conn = &db_connection(&pool)?; 21 | 22 | use crate::schema::regions as regions_schema; 23 | let target = regions_schema::table.filter( 24 | regions_schema::self_type 25 | .nullable() 26 | .eq(self_type.clone()) 27 | .or(self_type.is_none()), 28 | ); 29 | 30 | let total: i64 = target.clone().count().get_result(conn)?; 31 | 32 | let regions: Vec = target 33 | .offset(offset.into()) 34 | .limit(limit.into()) 35 | .load(conn)?; 36 | 37 | Ok(SizedList { 38 | total: total, 39 | list: regions, 40 | }) 41 | } 42 | 43 | pub fn insert_problems( 44 | region: String, 45 | problem_ids: Vec, 46 | score: Option, 47 | pool: web::Data, 48 | ) -> ServiceResult> { 49 | let conn = &db_connection(&pool)?; 50 | 51 | use crate::schema::problems as problems_schema; 52 | use crate::schema::region_links as region_links_schema; 53 | 54 | let mut target_id: i32 = region_links_schema::table 55 | .select(region_links_schema::inner_id) 56 | .filter(region_links_schema::region.eq(region.clone())) 57 | .order(region_links_schema::inner_id.desc()) 58 | .first(conn) 59 | .unwrap_or(0); 60 | target_id += 1; 61 | 62 | let mut res = Vec::new(); 63 | for problem_id in problem_ids { 64 | match diesel::insert_into(region_links_schema::table) 65 | .values(&RegionLink { 66 | region: region.clone(), 67 | inner_id: target_id, 68 | problem_id: problem_id, 69 | score: Some(score.unwrap_or(100)), 70 | }) 71 | .execute(conn) 72 | { 73 | Ok(_) => { 74 | diesel::update(problems_schema::table.filter(problems_schema::id.eq(problem_id))) 75 | .set(problems_schema::is_released.eq(true)) 76 | .execute(conn) 77 | .expect("Error changing problem's release state."); 78 | res.push(CreateRegionLinksResult { 79 | problem_id: problem_id, 80 | inner_id: Some(target_id), 81 | is_success: true, 82 | }); 83 | target_id += 1; 84 | } 85 | Err(_) => { 86 | res.push(CreateRegionLinksResult { 87 | problem_id: problem_id, 88 | inner_id: None, 89 | is_success: false, 90 | }); 91 | } 92 | } 93 | } 94 | 95 | Ok(res) 96 | } 97 | 98 | pub fn get_linked_problem_column_list( 99 | region: String, 100 | inner_id_filter: Option, 101 | problem_id_filter: Option, 102 | title_filter: Option, 103 | tag_filter: Option>, 104 | difficulty_filter: Option, 105 | id_order: Option, 106 | problem_id_order: Option, 107 | difficulty_order: Option, 108 | limit: i32, 109 | offset: i32, 110 | pool: web::Data, 111 | ) -> ServiceResult> { 112 | let conn = &db_connection(&pool)?; 113 | 114 | let title_filter = if let Some(inner_data) = title_filter { 115 | Some(String::from("%") + &inner_data.as_str().replace(" ", "%") + "%") 116 | } else { 117 | None 118 | }; 119 | 120 | let tag_filter: Vec = if let Some(inner_data) = tag_filter { 121 | inner_data.clone() 122 | } else { 123 | Vec::::new() 124 | }; 125 | 126 | let (min_difficulty, max_difficulty) = if difficulty_filter.is_none() { 127 | (0.0, 10.0) 128 | } else { 129 | match difficulty_filter.unwrap().as_str() { 130 | "Navie" => (0.0, 2.5), 131 | "Easy" => (2.5, 5.0), 132 | "Middle" => (5.0, 7.5), 133 | "Hard" => (7.5, 10.0), 134 | _ => (0.0, 10.0), 135 | } 136 | }; 137 | 138 | use crate::schema::problems as problems_schema; 139 | use crate::schema::region_links as region_links_schema; 140 | let target = region_links_schema::table 141 | .inner_join( 142 | problems_schema::table.on(problems_schema::id.eq(region_links_schema::problem_id)), 143 | ) 144 | .filter(region_links_schema::region.eq(region)) 145 | .filter( 146 | region_links_schema::inner_id 147 | .nullable() 148 | .eq(inner_id_filter) 149 | .or(inner_id_filter.is_none()), 150 | ) 151 | .filter( 152 | problems_schema::id 153 | .nullable() 154 | .eq(problem_id_filter) 155 | .or(problem_id_filter.is_none()), 156 | ) 157 | .filter( 158 | problems_schema::tags 159 | .overlaps_with(tag_filter.clone()) 160 | .or(tag_filter.is_empty()), 161 | ) 162 | .filter( 163 | problems_schema::title 164 | .nullable() 165 | .like(title_filter.clone()) 166 | .or(title_filter.is_none()), 167 | ) 168 | .filter(problems_schema::difficulty.between(min_difficulty, max_difficulty)); 169 | 170 | let total: i64 = target.clone().count().get_result(conn)?; 171 | 172 | let target = target.offset(offset.into()).limit(limit.into()).select(( 173 | region_links_schema::region, 174 | region_links_schema::inner_id, 175 | problems_schema::id, 176 | problems_schema::title, 177 | problems_schema::tags, 178 | problems_schema::difficulty, 179 | problems_schema::is_released, 180 | )); 181 | 182 | let columns: Vec = match id_order { 183 | None => match problem_id_order { 184 | None => match difficulty_order { 185 | None => target.load(conn)?, 186 | Some(true) => target.order(problems_schema::difficulty.asc()).load(conn)?, 187 | Some(false) => target 188 | .order(problems_schema::difficulty.desc()) 189 | .load(conn)?, 190 | }, 191 | Some(true) => target.order(problems_schema::id.asc()).load(conn)?, 192 | Some(false) => target.order(problems_schema::id.desc()).load(conn)?, 193 | }, 194 | Some(true) => target 195 | .order(region_links_schema::inner_id.asc()) 196 | .load(conn)?, 197 | Some(false) => target 198 | .order(region_links_schema::inner_id.desc()) 199 | .load(conn)?, 200 | }; 201 | 202 | let out_columns = { 203 | let mut res = Vec::new(); 204 | for column in columns { 205 | res.push(get_column_from_raw(conn, column)?); 206 | } 207 | res 208 | }; 209 | 210 | Ok(SizedList { 211 | total: total, 212 | list: out_columns, 213 | }) 214 | } 215 | 216 | pub fn get_linked_problem( 217 | region: String, 218 | inner_id: i32, 219 | pool: web::Data, 220 | ) -> ServiceResult { 221 | let conn = &db_connection(&pool)?; 222 | 223 | use crate::schema::region_links as region_links_schema; 224 | 225 | let problem_id: i32 = region_links_schema::table 226 | .filter(region_links_schema::region.eq(region)) 227 | .filter(region_links_schema::inner_id.eq(inner_id)) 228 | .select(region_links_schema::problem_id) 229 | .first(conn)?; 230 | 231 | use crate::schema::problems as problems_schema; 232 | 233 | let problem: RawProblem = problems_schema::table 234 | .filter(problems_schema::id.eq(problem_id)) 235 | .first(conn)?; 236 | 237 | Ok(Problem::from(problem)) 238 | } 239 | 240 | pub fn create_submission( 241 | region: String, 242 | inner_id: i32, 243 | user_id: i32, 244 | src: String, 245 | language: String, 246 | pool: web::Data, 247 | judge_actor: web::Data, 248 | ) -> ServiceResult { 249 | let conn = &db_connection(&pool)?; 250 | 251 | use crate::schema::region_links as region_links_schema; 252 | 253 | let problem_id: i32 = region_links_schema::table 254 | .filter(region_links_schema::region.eq(region.clone())) 255 | .filter(region_links_schema::inner_id.eq(inner_id)) 256 | .select(region_links_schema::problem_id) 257 | .first(conn)?; 258 | 259 | use crate::schema::problems as problems_schema; 260 | 261 | let is_released: bool = problems_schema::table 262 | .filter(problems_schema::id.eq(problem_id)) 263 | .select(problems_schema::is_released) 264 | .first(conn)?; 265 | 266 | if !is_released { 267 | let hint = "Problem is not released.".to_string(); 268 | return Err(ServiceError::BadRequest(hint)); 269 | } 270 | 271 | use crate::services::submission::create as inner_create; 272 | 273 | inner_create( 274 | Some(region), 275 | problem_id, 276 | user_id, 277 | src, 278 | language, 279 | pool, 280 | judge_actor, 281 | ) 282 | } 283 | 284 | pub fn delete_problem(region: String, inner_id: i32, pool: web::Data) -> ServiceResult<()> { 285 | let conn = &db_connection(&pool)?; 286 | 287 | use crate::schema::region_links as region_links_schema; 288 | 289 | let target = region_links_schema::table.filter( 290 | region_links_schema::region 291 | .eq(region.clone()) 292 | .and(region_links_schema::inner_id.eq(inner_id.clone())), 293 | ); 294 | 295 | let problem_id: i32 = target 296 | .clone() 297 | .select(region_links_schema::problem_id) 298 | .first(conn)?; 299 | 300 | diesel::delete(target).execute(conn)?; 301 | 302 | if region_links_schema::table 303 | .filter(region_links_schema::problem_id.eq(problem_id.clone())) 304 | .count() 305 | .get_result::(conn)? as i32 306 | == 0 307 | { 308 | use crate::schema::problems as problems_schema; 309 | diesel::update(problems_schema::table.filter(problems_schema::id.eq(problem_id))) 310 | .set(problems_schema::is_released.eq(false)) 311 | .execute(conn) 312 | .expect("Error changing problem's release state."); 313 | } 314 | 315 | use crate::schema::submissions as submissions_schema; 316 | diesel::delete( 317 | submissions_schema::table.filter( 318 | submissions_schema::region 319 | .eq(region.clone()) 320 | .and(submissions_schema::problem_id.eq(problem_id.clone())), 321 | ), 322 | ) 323 | .execute(conn)?; 324 | 325 | Ok(()) 326 | } 327 | -------------------------------------------------------------------------------- /src/services/user/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::models::users::*; 2 | use crate::models::utils::SizedList; 3 | use actix_web::web; 4 | use diesel::prelude::*; 5 | use server_core::database::{db_connection, Pool}; 6 | use server_core::errors::{ServiceError, ServiceResult}; 7 | use server_core::utils::encryption; 8 | 9 | pub fn create( 10 | account: String, 11 | password: String, 12 | mobile: Option, 13 | role: String, 14 | pool: web::Data, 15 | ) -> ServiceResult<()> { 16 | let (salt, hash) = { 17 | let salt = encryption::make_salt(); 18 | let hash = encryption::make_hash(&password, &salt).to_vec(); 19 | (Some(salt), Some(hash)) 20 | }; 21 | 22 | let conn = &db_connection(&pool)?; 23 | 24 | use crate::schema::users as users_schema; 25 | diesel::insert_into(users_schema::table) 26 | .values(&InsertableUser { 27 | salt: salt, 28 | hash: hash, 29 | account: account, 30 | mobile: mobile, 31 | role: role, 32 | }) 33 | .execute(conn)?; 34 | 35 | Ok(()) 36 | } 37 | 38 | pub fn get_name(id: i32, pool: web::Data) -> ServiceResult { 39 | let conn = &db_connection(&pool)?; 40 | 41 | use crate::schema::users as users_schema; 42 | 43 | let name: String = users_schema::table 44 | .filter(users_schema::id.eq(id)) 45 | .select(users_schema::account) 46 | .first(conn)?; 47 | 48 | Ok(name) 49 | } 50 | 51 | pub fn get(id: i32, pool: web::Data) -> ServiceResult { 52 | let conn = &db_connection(&pool)?; 53 | 54 | use crate::schema::users as users_schema; 55 | let user: User = users_schema::table 56 | .filter(users_schema::id.eq(id)) 57 | .first(conn)?; 58 | 59 | Ok(OutUser::from(user)) 60 | } 61 | 62 | pub fn update( 63 | id: i32, 64 | new_account: Option, 65 | new_password: Option, 66 | new_mobile: Option, 67 | new_role: Option, 68 | pool: web::Data, 69 | ) -> ServiceResult<()> { 70 | let conn = &db_connection(&pool)?; 71 | 72 | let (new_salt, new_hash) = if let Some(inner_data) = new_password { 73 | let salt = encryption::make_salt(); 74 | let hash = encryption::make_hash(&inner_data, &salt).to_vec(); 75 | (Some(salt), Some(hash)) 76 | } else { 77 | (None, None) 78 | }; 79 | 80 | use crate::schema::users as users_schema; 81 | diesel::update(users_schema::table.filter(users_schema::id.eq(id))) 82 | .set(UserForm { 83 | salt: new_salt, 84 | hash: new_hash, 85 | account: new_account, 86 | mobile: new_mobile, 87 | role: new_role, 88 | }) 89 | .execute(conn)?; 90 | 91 | Ok(()) 92 | } 93 | 94 | pub fn get_list( 95 | id_filter: Option, 96 | account_filter: Option, 97 | mobile_filter: Option, 98 | role_filter: Option, 99 | id_order: Option, 100 | limit: i32, 101 | offset: i32, 102 | pool: web::Data, 103 | ) -> ServiceResult> { 104 | let account_filter = if let Some(inner_data) = account_filter { 105 | Some(String::from("%") + &inner_data.as_str().replace(" ", "%") + "%") 106 | } else { 107 | None 108 | }; 109 | 110 | let mobile_filter = if let Some(inner_data) = mobile_filter { 111 | Some(String::from("%") + &inner_data.as_str().replace(" ", "%") + "%") 112 | } else { 113 | None 114 | }; 115 | 116 | let conn = &db_connection(&pool)?; 117 | 118 | use crate::schema::users as users_schema; 119 | let target = users_schema::table 120 | .filter( 121 | users_schema::id 122 | .nullable() 123 | .eq(id_filter) 124 | .or(id_filter.is_none()), 125 | ) 126 | .filter( 127 | users_schema::account 128 | .nullable() 129 | .like(account_filter.clone()) 130 | .or(account_filter.is_none()), 131 | ) 132 | .filter( 133 | users_schema::mobile 134 | .like(mobile_filter.clone()) 135 | .or(mobile_filter.is_none()), 136 | ) 137 | .filter( 138 | users_schema::role 139 | .nullable() 140 | .eq(role_filter.clone()) 141 | .or(role_filter.is_none()), 142 | ); 143 | 144 | let total: i64 = target.clone().count().get_result(conn)?; 145 | 146 | let target = target.offset(offset.into()).limit(limit.into()); 147 | 148 | let users: Vec = match id_order { 149 | None => target.load(conn)?, 150 | Some(true) => target.order(users_schema::id.asc()).load(conn)?, 151 | Some(false) => target.order(users_schema::id.desc()).load(conn)?, 152 | }; 153 | 154 | let out_users = { 155 | let mut res = Vec::new(); 156 | for user in users { 157 | res.push(OutUser::from(user)); 158 | } 159 | res 160 | }; 161 | 162 | Ok(SizedList { 163 | total: total, 164 | list: out_users, 165 | }) 166 | } 167 | 168 | pub fn login(account: String, password: String, pool: web::Data) -> ServiceResult { 169 | let conn = &db_connection(&pool)?; 170 | 171 | use crate::schema::users as users_schema; 172 | let user: User = users_schema::table 173 | .filter(users_schema::account.eq(account)) 174 | .first(conn)?; 175 | 176 | if user.hash.is_none() || user.salt.is_none() { 177 | let hint = "Password was not set.".to_string(); 178 | Err(ServiceError::BadRequest(hint)) 179 | } else { 180 | let hash = encryption::make_hash(&password, &user.clone().salt.unwrap()).to_vec(); 181 | if Some(hash) == user.hash { 182 | Ok(SlimUser::from(user)) 183 | } else { 184 | let hint = "Password is wrong.".to_string(); 185 | Err(ServiceError::BadRequest(hint)) 186 | } 187 | } 188 | } 189 | 190 | pub fn get_permitted_methods(role: String, path: String) -> ServiceResult> { 191 | use crate::statics::AUTH_CONFIG; 192 | match AUTH_CONFIG.get(&path) { 193 | Some(config) => match role.as_str() { 194 | "sup" => Ok(config.sup.clone().unwrap_or_default()), 195 | "admin" => Ok(config.admin.clone().unwrap_or_default()), 196 | "student" => Ok(config.student.clone().unwrap_or_default()), 197 | "teacher" => Ok(config.teacher.clone().unwrap_or_default()), 198 | _ => Ok(config.others.clone().unwrap_or_default()), 199 | }, 200 | None => { 201 | let hint = "Path not set in config.".to_string(); 202 | Err(ServiceError::BadRequest(hint)) 203 | } 204 | } 205 | } 206 | 207 | pub fn delete(id: i32, pool: web::Data) -> ServiceResult<()> { 208 | let conn = &db_connection(&pool)?; 209 | 210 | use crate::schema::users as users_schema; 211 | diesel::delete(users_schema::table.filter(users_schema::id.eq(id))).execute(conn)?; 212 | 213 | Ok(()) 214 | } 215 | 216 | pub fn get_submissions_count( 217 | user_id: i32, 218 | pool: web::Data, 219 | ) -> ServiceResult { 220 | let conn = &db_connection(&pool)?; 221 | 222 | use crate::schema::problems as problems_schema; 223 | use crate::schema::submissions as submissions_schema; 224 | 225 | let target = submissions_schema::table 226 | .filter(submissions_schema::user_id.eq(user_id)) 227 | .inner_join( 228 | problems_schema::table.on(submissions_schema::problem_id.eq(problems_schema::id)), 229 | ); 230 | 231 | let navie = target.filter(problems_schema::difficulty.lt(2.5)); 232 | let easy = target.filter( 233 | problems_schema::difficulty 234 | .ge(2.5) 235 | .and(problems_schema::difficulty.lt(5.0)), 236 | ); 237 | let middle = target.filter( 238 | problems_schema::difficulty 239 | .ge(5.0) 240 | .and(problems_schema::difficulty.lt(7.5)), 241 | ); 242 | let hard = target.filter(problems_schema::difficulty.ge(7.5)); 243 | 244 | let navie_submit_times: i64 = navie.count().get_result(conn)?; 245 | let navie_accept_times: i64 = navie 246 | .filter(submissions_schema::is_accepted.eq(true)) 247 | .count() 248 | .get_result(conn)?; 249 | 250 | let easy_submit_times: i64 = easy.count().get_result(conn)?; 251 | let easy_accept_times: i64 = easy 252 | .filter(submissions_schema::is_accepted.eq(true)) 253 | .count() 254 | .get_result(conn)?; 255 | 256 | let middle_submit_times: i64 = middle.count().get_result(conn)?; 257 | let middle_accept_times: i64 = middle 258 | .filter(submissions_schema::is_accepted.eq(true)) 259 | .count() 260 | .get_result(conn)?; 261 | 262 | let hard_submit_times: i64 = hard.count().get_result(conn)?; 263 | let hard_accept_times: i64 = hard 264 | .filter(submissions_schema::is_accepted.eq(true)) 265 | .count() 266 | .get_result(conn)?; 267 | 268 | let total_submit_times: i64 = 269 | navie_submit_times + easy_submit_times + middle_submit_times + hard_submit_times; 270 | let total_accept_times: i64 = 271 | navie_accept_times + easy_accept_times + middle_accept_times + hard_accept_times; 272 | 273 | Ok(UserSubmissionCount { 274 | total_submit_times: total_submit_times as i32, 275 | total_accept_times: total_accept_times as i32, 276 | navie_submit_times: navie_submit_times as i32, 277 | navie_accept_times: navie_accept_times as i32, 278 | easy_submit_times: easy_submit_times as i32, 279 | easy_accept_times: easy_accept_times as i32, 280 | middle_submit_times: middle_submit_times as i32, 281 | middle_accept_times: middle_accept_times as i32, 282 | hard_submit_times: hard_submit_times as i32, 283 | hard_accept_times: hard_accept_times as i32, 284 | }) 285 | } 286 | 287 | pub fn get_submissions_time( 288 | user_id: i32, 289 | pool: web::Data, 290 | ) -> ServiceResult> { 291 | let conn = &db_connection(&pool)?; 292 | 293 | use crate::schema::submissions as submissions_schema; 294 | 295 | let raw_times: Vec = submissions_schema::table 296 | .filter(submissions_schema::user_id.eq(user_id)) 297 | .select(submissions_schema::submit_time) 298 | .order(submissions_schema::submit_time.desc()) 299 | .load(conn)?; 300 | 301 | let mut time_count: Vec = Vec::new(); 302 | let mut last = 0; 303 | let mut first: bool = true; 304 | for time in raw_times { 305 | if first { 306 | time_count.push(UserSubmissionTime { 307 | date: time.date(), 308 | count: 1, 309 | }); 310 | first = false; 311 | } else if time.date() == time_count[last].date { 312 | time_count[last].count += 1; 313 | } else { 314 | time_count.push(UserSubmissionTime { 315 | date: time.date(), 316 | count: 1, 317 | }); 318 | last += 1; 319 | } 320 | } 321 | 322 | Ok(time_count) 323 | } 324 | -------------------------------------------------------------------------------- /src/models/submissions.rs: -------------------------------------------------------------------------------- 1 | use super::languages::*; 2 | use crate::schema::submissions; 3 | use chrono::NaiveDateTime; 4 | use std::collections::HashSet; 5 | use uuid::Uuid; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct TestCase { 9 | pub input: String, 10 | pub output: String, 11 | } 12 | 13 | #[derive(Debug, Clone, Serialize, Deserialize)] 14 | pub struct JudgeSettings { 15 | pub language_config: LanguageConfig, 16 | pub src: String, 17 | pub max_cpu_time: i32, 18 | pub max_memory: i32, 19 | pub test_case_id: Option, 20 | pub test_case: Option>, 21 | pub spj_version: Option, 22 | pub spj_config: Option, 23 | pub spj_compile_config: Option, 24 | pub spj_src: Option, 25 | pub output: bool, 26 | } 27 | 28 | #[derive(Debug, Clone, Deserialize)] 29 | pub struct RawJudgeResult { 30 | pub err: Option, 31 | pub data: serde_json::Value, 32 | } 33 | 34 | #[derive(Debug, Clone, Serialize, Deserialize)] 35 | pub struct RawJudgeResultData { 36 | pub cpu_time: i32, 37 | pub real_time: i32, 38 | pub memory: i32, 39 | pub signal: i32, 40 | pub exit_code: i32, 41 | pub error: i32, 42 | pub result: i32, 43 | pub test_case: String, 44 | pub output_md5: Option, 45 | pub output: Option, 46 | } 47 | 48 | #[derive(Debug, Clone, Serialize, Deserialize)] 49 | pub struct JudgeResultData { 50 | pub cpu_time: i32, 51 | pub real_time: i32, 52 | pub memory: i32, 53 | pub signal: i32, 54 | pub exit_code: i32, 55 | pub error: String, 56 | pub result: String, 57 | pub test_case: String, 58 | pub output_md5: Option, 59 | pub output: Option, 60 | } 61 | 62 | impl From for JudgeResultData { 63 | fn from(raw: RawJudgeResultData) -> Self { 64 | Self { 65 | cpu_time: raw.cpu_time, 66 | real_time: raw.real_time, 67 | memory: raw.memory, 68 | signal: raw.signal, 69 | exit_code: raw.exit_code, 70 | error: { 71 | match raw.error { 72 | 0 => "SUCCESS".to_owned(), 73 | -1 => "INVALID_CONFIG".to_owned(), 74 | -2 => "CLONE_FAILED".to_owned(), 75 | -3 => "PTHREAD_FAILED".to_owned(), 76 | -4 => "WAIT_FAILED".to_owned(), 77 | -5 => "ROOT_REQUIRED".to_owned(), 78 | -6 => "LOAD_SECCOMP_FAILED".to_owned(), 79 | -7 => "SETRLIMIT_FAILED".to_owned(), 80 | -8 => "DUP2_FAILED".to_owned(), 81 | -9 => "SETUID_FAILED".to_owned(), 82 | -10 => "EXECVE_FAILED".to_owned(), 83 | -11 => "SPJ_ERROR".to_owned(), 84 | _ => "UNKNOWN_ERROR".to_owned(), 85 | } 86 | }, 87 | result: { 88 | match raw.result { 89 | -1 => "WRONG_ANSWER".to_owned(), 90 | 0 => "SUCCESS".to_owned(), 91 | 1 => "CPU_TIME_LIMIT_EXCEEDED".to_owned(), 92 | 2 => "REAL_TIME_LIMIT_EXCEEDED".to_owned(), 93 | 3 => "MEMORY_LIMIT_EXCEEDED".to_owned(), 94 | 4 => "RUNTIME_ERROR".to_owned(), 95 | 5 => "SYSTEM_ERROR".to_owned(), 96 | _ => "UNKNOWN_ERROR".to_owned(), 97 | } 98 | }, 99 | test_case: raw.test_case, 100 | output_md5: raw.output_md5, 101 | output: raw.output, 102 | } 103 | } 104 | } 105 | 106 | #[derive(Debug, Clone, Serialize, Deserialize)] 107 | pub struct JudgeResult { 108 | pub err: Option, 109 | pub err_reason: Option, 110 | pub is_accepted: Option, 111 | pub max_time: Option, 112 | pub max_memory: Option, 113 | pub details: Option>, 114 | } 115 | 116 | impl From for JudgeResult { 117 | fn from(raw: RawJudgeResult) -> Self { 118 | let mut is_accepted = true; 119 | let raw_details: Option> = if raw.err.is_none() { 120 | Some(serde_json::from_value::>(raw.data.clone()).unwrap()) 121 | } else { 122 | None 123 | }; 124 | 125 | let details: Option> = if raw_details.is_some() { 126 | let mut tmp = Vec::new(); 127 | for raw_detail in raw_details.unwrap() { 128 | if raw_detail.result != 0 { 129 | is_accepted = false; 130 | } 131 | tmp.push(JudgeResultData::from(raw_detail)) 132 | } 133 | Some(tmp) 134 | } else { 135 | None 136 | }; 137 | 138 | let max_time: Option = if let Some(inner_data) = details.clone() { 139 | Some( 140 | inner_data 141 | .clone() 142 | .iter() 143 | .map(|detail| detail.cpu_time) 144 | .max() 145 | .unwrap_or(0), 146 | ) 147 | } else { 148 | None 149 | }; 150 | 151 | let max_memory: Option = if let Some(inner_data) = details.clone() { 152 | Some( 153 | inner_data 154 | .clone() 155 | .iter() 156 | .map(|detail| detail.memory) 157 | .max() 158 | .unwrap_or(0), 159 | ) 160 | } else { 161 | None 162 | }; 163 | 164 | Self { 165 | err: raw.err.clone(), 166 | err_reason: if raw.err.is_some() { 167 | Some(serde_json::from_value::(raw.data.clone()).unwrap()) 168 | } else { 169 | None 170 | }, 171 | is_accepted: if raw.err.is_none() { 172 | Some(is_accepted) 173 | } else { 174 | None 175 | }, 176 | max_time: max_time, 177 | max_memory: max_memory, 178 | details: details, 179 | } 180 | } 181 | } 182 | 183 | #[derive(Debug, Clone, Serialize, Queryable)] 184 | pub struct RawSubmission { 185 | pub id: Uuid, 186 | pub problem_id: i32, 187 | pub user_id: i32, 188 | pub region: Option, 189 | pub state: String, 190 | pub settings: String, 191 | pub result: Option, 192 | pub submit_time: NaiveDateTime, 193 | pub is_accepted: Option, 194 | pub finish_time: Option, 195 | pub max_time: Option, 196 | pub max_memory: Option, 197 | pub language: Option, 198 | pub err: Option, 199 | pub out_results: Option>, 200 | } 201 | 202 | #[derive(Debug, Clone, Deserialize, Insertable, Queryable)] 203 | #[table_name = "submissions"] 204 | pub struct InsertableSubmission { 205 | pub id: Uuid, 206 | pub problem_id: i32, 207 | pub user_id: i32, 208 | pub region: Option, 209 | pub state: String, 210 | pub settings: String, 211 | pub result: Option, 212 | pub submit_time: NaiveDateTime, 213 | pub is_accepted: Option, 214 | pub finish_time: Option, 215 | pub max_time: Option, 216 | pub max_memory: Option, 217 | pub language: Option, 218 | pub err: Option, 219 | } 220 | 221 | #[derive(Debug, Clone, Serialize, Deserialize)] 222 | pub struct Submission { 223 | pub id: Uuid, 224 | pub problem_id: i32, 225 | pub user_id: i32, 226 | pub region: Option, 227 | pub state: String, 228 | pub settings: JudgeSettings, 229 | pub result: Option, 230 | pub submit_time: NaiveDateTime, 231 | pub is_accepted: Option, 232 | pub finish_time: Option, 233 | pub max_time: Option, 234 | pub max_memory: Option, 235 | pub language: Option, 236 | pub err: Option, 237 | pub out_results: Option>, 238 | } 239 | 240 | impl From for Submission { 241 | fn from(raw: RawSubmission) -> Self { 242 | Self { 243 | id: raw.id, 244 | problem_id: raw.problem_id, 245 | user_id: raw.user_id, 246 | region: raw.region, 247 | state: raw.state, 248 | settings: serde_json::from_str::(&raw.settings).unwrap(), 249 | result: if let Some(result) = raw.result.clone() { 250 | Some(serde_json::from_str::(&result).unwrap()) 251 | } else { 252 | None 253 | }, 254 | submit_time: raw.submit_time, 255 | is_accepted: raw.is_accepted, 256 | finish_time: raw.finish_time, 257 | max_time: raw.max_time, 258 | max_memory: raw.max_memory, 259 | language: raw.language, 260 | err: raw.err, 261 | out_results: { 262 | if let Some(result) = raw.result { 263 | let result = serde_json::from_str::(&result).unwrap(); 264 | if let Some(details) = result.details { 265 | let mut set = std::collections::HashSet::new(); 266 | for detail in details { 267 | set.insert(detail.result); 268 | } 269 | Some(set) 270 | } else { 271 | None 272 | } 273 | } else { 274 | None 275 | } 276 | }, 277 | } 278 | } 279 | } 280 | 281 | #[derive(Debug, Clone, Serialize, Deserialize)] 282 | pub struct SlimSubmission { 283 | pub id: Uuid, 284 | pub problem_id: i32, 285 | pub user_id: i32, 286 | pub state: String, 287 | pub submit_time: NaiveDateTime, 288 | pub is_accepted: Option, 289 | pub out_results: Option>, 290 | pub max_time: Option, 291 | pub max_memory: Option, 292 | pub language: Option, 293 | pub err: Option, 294 | } 295 | 296 | impl From for SlimSubmission { 297 | fn from(raw: RawSubmission) -> Self { 298 | Self { 299 | id: raw.id, 300 | problem_id: raw.problem_id, 301 | user_id: raw.user_id, 302 | state: raw.state, 303 | submit_time: raw.submit_time, 304 | is_accepted: raw.is_accepted, 305 | out_results: { 306 | if let Some(result) = raw.result { 307 | let result = serde_json::from_str::(&result).unwrap(); 308 | if let Some(details) = result.details { 309 | let mut set = std::collections::HashSet::new(); 310 | for detail in details { 311 | set.insert(detail.result); 312 | } 313 | Some(set) 314 | } else { 315 | None 316 | } 317 | } else { 318 | None 319 | } 320 | }, 321 | max_time: raw.max_time, 322 | max_memory: raw.max_memory, 323 | language: raw.language, 324 | err: raw.err, 325 | } 326 | } 327 | } 328 | --------------------------------------------------------------------------------