diff --git a/src/main.rs b/src/main.rs index fdbfed230fe498b01976b5c20a4ee974659d40d6..f166174ed6e71ad5a4a924abbc5e45ae278d6e79 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,65 +1,35 @@ -use std::collections::HashMap; -use std::convert::TryFrom; +use std::env; use std::error::Error; -use std::fs::{create_dir_all, File}; -use std::future::Future; -use std::io::{Cursor, Read, Write}; -use std::iter::FromIterator; -use std::path::PathBuf; -use std::pin::Pin; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::time::Duration; -use std::{env, fs, str}; -use actix_files::{Files, NamedFile}; -use actix_identity::{Identity, IdentityMiddleware}; -use actix_multipart::Multipart; +use actix_files::Files; +use actix_identity::IdentityMiddleware; use actix_session::storage::CookieSessionStore; -use actix_session::{Session, SessionMiddleware}; +use actix_session::SessionMiddleware; use actix_web::cookie::Key; -use actix_web::dev::Payload; -use actix_web::http::header::{ContentType, HeaderValue}; -use actix_web::http::{header, StatusCode}; -use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers}; +use actix_web::middleware::ErrorHandlers; use actix_web::web::Data; -use actix_web::{ - dev, get, http, middleware, post, web, App, FromRequest, HttpMessage, HttpRequest, - HttpResponse, HttpServer, -}; +use actix_web::{http, middleware, web, App, HttpServer}; use actix_web_flash_messages::storage::CookieMessageStore; -use actix_web_flash_messages::{ - FlashMessage, FlashMessagesFramework, IncomingFlashMessages, Level, -}; -use async_channel::Sender; -use base64; +use actix_web_flash_messages::{FlashMessagesFramework, Level}; use broadcaster::Broadcaster; -use chrono::prelude::*; use chrono_tz::Tz; -use contest::{Contest, ContestWithAcs}; use dashmap::DashMap; use diesel::pg::PgConnection; use diesel::r2d2::ConnectionManager; use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; -use futures::{StreamExt, TryFutureExt, TryStreamExt}; -use handlebars::{Handlebars, RenderError}; -use itertools::Itertools; -use lazy_static::lazy_static; +use futures::TryFutureExt; +use handlebars::Handlebars; use listenfd::ListenFd; -use log::{error, info}; -use models::problem::ProblemByContest; use models::{contest, problem, submission, user}; -use problem::{ProblemByContestMetadata, ProblemByContestWithScore}; +use problem::ProblemByContestMetadata; use queue::job_protocol::job_queue_server::JobQueueServer; use queue::job_protocol::{job, job_result, Job, JobResult, Language}; use queue::JobQueuer; -use regex::Regex; -use serde::{Deserialize, Serialize}; -use submission::{ContestProblem, Submission, SubmissionCompletion}; -use thiserror::Error; +use submission::{Submission, SubmissionCompletion}; use tokio::sync::broadcast; use tonic::transport::Server; -use user::{PasswordMatched, User, UserHashingError}; -use uuid::Uuid; mod broadcaster; mod import_contest; @@ -68,13 +38,14 @@ mod models; mod queue; mod schema; mod setup; +mod pages; type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>; async fn update_database( mut job_result_receiver: broadcast::Receiver<JobResult>, pool: DbPool, -) -> Result<(), PageError> { +) -> Result<(), pages::PageError> { loop { let job_result = job_result_receiver.recv().await.unwrap(); if let JobResult { @@ -204,8 +175,8 @@ async fn main() -> Result<(), Box<dyn Error>> { .app_data(Data::new(job_result_sender_data.clone())) .app_data(Data::new(languages_data.clone())) .app_data(Data::new(tz.clone())) - .wrap(ErrorHandlers::new().handler(http::StatusCode::UNAUTHORIZED, render_401)) - .wrap(ErrorHandlers::new().handler(http::StatusCode::BAD_REQUEST, render_400)) + .wrap(ErrorHandlers::new().handler(http::StatusCode::UNAUTHORIZED, pages::render_401)) + .wrap(ErrorHandlers::new().handler(http::StatusCode::BAD_REQUEST, pages::render_400)) .wrap( FlashMessagesFramework::builder( CookieMessageStore::builder(Key::from(&private_key)).build(), @@ -225,29 +196,29 @@ async fn main() -> Result<(), Box<dyn Error>> { .app_data(handlebars_ref.clone()) .service( web::scope("/jughisto") - .service(get_login) - .service(get_me) - .service(change_password) - .service(post_login) - .service(post_logout) - .service(get_main) - .service(get_contests) - .service(get_contest_by_id) - .service(get_contest_scoreboard_by_id) - .service(get_contest_problem_by_id_label) - .service(get_submissions_me) - .service(get_submission) - .service(get_submissions) - .service(get_submissions_me_by_contest_id) - .service(get_submissions_me_by_contest_id_problem_label) - .service(rejudge_submission) - .service(create_submission) - .service(create_contest) - .service(create_user) - .service(impersonate_user) - .service(submission_updates) - .service(Files::new("/static/", "./static/")) - .service(get_problem_by_id_assets), + .service(pages::get_login) + .service(pages::get_me) + .service(pages::change_password) + .service(pages::post_login) + .service(pages::post_logout) + .service(pages::get_main) + .service(pages::get_contests) + .service(pages::get_contest_by_id) + .service(pages::get_contest_scoreboard_by_id) + .service(pages::get_contest_problem_by_id_label) + .service(pages::get_submissions_me) + .service(pages::get_submission) + .service(pages::get_submissions) + .service(pages::get_submissions_me_by_contest_id) + .service(pages::get_submissions_me_by_contest_id_problem_label) + .service(pages::rejudge_submission) + .service(pages::create_submission) + .service(pages::create_contest) + .service(pages::create_user) + .service(pages::impersonate_user) + .service(pages::submission_updates) + .service(pages::get_problem_by_id_assets) + .service(Files::new("/static/", "./static/")), ) }); @@ -283,1496 +254,3 @@ async fn main() -> Result<(), Box<dyn Error>> { Ok(()) } - -#[derive(Error, Debug)] -enum PageError { - #[error("Unauthorized")] - Unauthorized(), - #[error("Couldn't render: {0}")] - Render(#[from] handlebars::RenderError), - #[error(transparent)] - SessionGet(#[from] actix_session::SessionGetError), - #[error("{0}")] - Custom(String), - #[error("{0}")] - Forbidden(String), - #[error("{0}")] - Validation(String), - #[error("Couldn't get connection from pool")] - ConnectionPool(#[from] r2d2::Error), - #[error("Couldn't hash")] - UserHashing(#[from] user::UserHashingError), - #[error(transparent)] - Web(#[from] actix_web::Error), - #[error(transparent)] - Queue(#[from] async_channel::SendError<Job>), - #[error("Couldn't fetch result from database")] - Database(#[from] diesel::result::Error), - #[error("couldn't insert session")] - SessionInsert(#[from] actix_session::SessionInsertError), - #[error("couldn't work with the filesystem")] - Io(#[from] std::io::Error), - #[error("couldn't work with the zip")] - Zip(#[from] zip::result::ZipError), -} - -fn error_response_and_log(me: &impl actix_web::error::ResponseError) -> HttpResponse { - error!("{}", me); - HttpResponse::build(me.status_code()) - .insert_header(ContentType::plaintext()) - .body(me.to_string()) -} - -impl actix_web::error::ResponseError for PageError { - fn error_response(&self) -> HttpResponse { - error_response_and_log(self) - } - - fn status_code(&self) -> StatusCode { - match *self { - PageError::Unauthorized() => StatusCode::UNAUTHORIZED, - PageError::Validation(_) => StatusCode::BAD_REQUEST, - PageError::Forbidden(_) => StatusCode::FORBIDDEN, - PageError::Custom(_) - | PageError::SessionInsert(_) - | PageError::ConnectionPool(_) - | PageError::Web(_) - | PageError::Render(_) - | PageError::Queue(_) - | PageError::SessionGet(_) - | PageError::Database(_) - | PageError::Io(_) - | PageError::UserHashing(_) - | PageError::Zip(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -type PageResult = Result<HttpResponse, PageError>; - -#[derive(Serialize)] -struct BaseContext { - logged_user: Option<LoggedUser>, - flash_messages: IncomingFlashMessages, - base_url: String, -} - -impl FromRequest for BaseContext { - type Error = actix_web::Error; - type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>; - - fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { - let req = req.clone(); - Box::pin(async move { - let identity = Option::<Identity>::from_request(&req, &mut Payload::None).await; - let logged_user = identity?.and_then(|identity| get_identity(&Some(&identity))); - let flash_messages = - IncomingFlashMessages::from_request(&req, &mut Payload::None).await?; - Ok(BaseContext { - logged_user, - flash_messages, - base_url: env::var("BASE_URL").expect("BASE_URL environment variable is not set"), - }) - }) - } -} - -fn render<Err: From<RenderError>, Ctx: serde::Serialize>( - hb: &Handlebars, - name: &str, - context: &Ctx, -) -> Result<HttpResponse, Err> { - Ok(HttpResponse::Ok().body(hb.render(name, context)?)) -} - -#[get("/login")] -async fn get_login(base: BaseContext, hb: web::Data<Handlebars<'_>>) -> PageResult { - #[derive(Serialize)] - struct Context { - base: BaseContext, - } - render(&hb, "login", &Context { base }) -} - -#[get("/me")] -async fn get_me(base: BaseContext, identity: Identity, hb: web::Data<Handlebars<'_>>) -> PageResult { - require_identity(&identity)?; - #[derive(Serialize)] - struct Context { - base: BaseContext, - } - render(&hb, "me", &Context { base }) -} - -#[get("/problems/{id}/assets/{filename}")] -async fn get_problem_by_id_assets( - identity: Identity, - pool: web::Data<DbPool>, - path: web::Path<(i32, String)>, -) -> Result<NamedFile, PageError> { - let logged_user = require_identity(&identity)?; - let (problem_id, asset_filename) = path.into_inner(); - let mut connection = pool.get()?; - let problem = problem::get_problem_by_contest_id_metadata(&mut connection, problem_id)?; - let contest = contest::get_contest_by_id(&mut connection, problem.contest_id)?; - assert_contest_not_started(&logged_user, &contest)?; - let file_path = PathBuf::from("/data/") - .join(problem.id) - .join("statements/.html/portuguese/") - .join(asset_filename); - Ok(NamedFile::open(file_path)?) -} - -fn render_401<B>(mut res: dev::ServiceResponse<B>) -> actix_web::Result<ErrorHandlerResponse<B>> { - FlashMessage::error("Você precisa estar logado para acessar esta página").send(); - res.response_mut().headers_mut().insert( - header::LOCATION, - HeaderValue::from_str(&format!( - "{}login", - &env::var("BASE_URL").expect("BASE_URL environment variable is not set") - )) - .unwrap(), - ); - *res.response_mut().status_mut() = StatusCode::SEE_OTHER; - Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) -} - -fn render_400<B>(res: dev::ServiceResponse<B>) -> actix_web::Result<ErrorHandlerResponse<B>> { - FlashMessage::error("Entrada inválida").send(); - Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) -} - -#[derive(Serialize, Deserialize)] -struct LoginForm { - name: String, - password: String, -} - -fn get_identity(identity: &Option<&Identity>) -> Option<LoggedUser> { - identity.as_ref().and_then(|identity| { - let identity = identity.id(); - identity - .ok() - .and_then(|identity| serde_json::from_str(&identity).ok()) - }) -} - -fn require_identity(identity: &Identity) -> Result<LoggedUser, PageError> { - get_identity(&Some(identity)).ok_or(PageError::Unauthorized()) -} - -fn format_duration(duration: chrono::Duration) -> String { - format!( - "{}{}{:02}:{:02}", - if duration.num_milliseconds() >= 0 { - "" - } else { - "-" - }, - if duration.num_days() != 0 { - format!("{}:", duration.num_days().abs()) - } else { - "".into() - }, - duration.num_hours().abs() % 24, - duration.num_minutes().abs() % 60 - ) -} - -#[derive(Serialize)] -struct FormattedProblemByContestWithScore { - pub first_ac_submission_time: String, - pub first_ac_submission_minutes: Option<i64>, - pub user_accepted_count: i32, - pub failed_submissions: i32, - pub id: i32, - pub name: String, - pub label: String, - pub memory_limit_mib: i32, - pub time_limit: String, -} - -fn get_formatted_problem_by_contest_with_score( - p: &ProblemByContestWithScore, - contest: &Contest, -) -> FormattedProblemByContestWithScore { - FormattedProblemByContestWithScore { - first_ac_submission_time: p - .first_ac_submission_instant - .map(|t| match contest.start_instant { - Some(cs) => format_duration(t - cs), - None => "*".into(), - }) - .unwrap_or("".into()), - first_ac_submission_minutes: p.first_ac_submission_instant.and_then(|t| { - match contest.start_instant { - Some(cs) => Some((t - cs).num_minutes()), - None => None, - } - }), - failed_submissions: p.failed_submissions, - id: p.id, - name: p.name.clone(), - label: p.label.clone(), - memory_limit_mib: p.memory_limit_bytes / 1_024 / 1_024, - time_limit: format!("{}", f64::from(p.time_limit_ms) / 1000.0).replacen(".", ",", 1), - user_accepted_count: p.user_accepted_count, - } -} - -#[get("/contests/{id}")] -async fn get_contest_by_id( - base: BaseContext, - identity: Identity, - pool: web::Data<DbPool>, - hb: web::Data<Handlebars<'_>>, - path: web::Path<(i32,)>, - tz: web::Data<Tz>, -) -> PageResult { - let logged_user = require_identity(&identity)?; - let (contest_id,) = path.into_inner(); - - #[derive(Serialize)] - struct Context { - base: BaseContext, - contest: FormattedContest, - problems: Vec<FormattedProblemByContestWithScore>, - submissions: Vec<FormattedSubmission>, - } - - let mut connection = pool.get()?; - let contest = contest::get_contest_by_id(&mut connection, contest_id)?; - assert_contest_not_started(&logged_user, &contest)?; - - let problems = problem::get_problems_user_by_contest_id_with_score( - &mut connection, - logged_user.id, - contest_id, - )?; - let submissions = - submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?; - - render( - &hb, - "contest", - &Context { - base, - contest: get_formatted_contest(&tz, &contest), - problems: problems - .iter() - .map(|p| get_formatted_problem_by_contest_with_score(p, &contest)) - .collect(), - submissions: get_formatted_submissions(&tz, &submissions), - }, - ) -} - -#[get("/contests/{id}/scoreboard")] -async fn get_contest_scoreboard_by_id( - base: BaseContext, - identity: Identity, - pool: web::Data<DbPool>, - hb: web::Data<Handlebars<'_>>, - path: web::Path<(i32,)>, - tz: web::Data<Tz>, -) -> PageResult { - let logged_user = require_identity(&identity)?; - let (contest_id,) = path.into_inner(); - - #[derive(Serialize)] - struct Score { - pub user_name: Option<String>, - pub problems: Vec<FormattedProblemByContestWithScore>, - pub solved_count: i64, - pub penalty: i64, - } - - #[derive(Serialize)] - struct Context { - base: BaseContext, - contest: FormattedContest, - scores: Vec<Score>, - submissions: Vec<FormattedSubmission>, - } - - let mut connection = pool.get()?; - let contest = contest::get_contest_by_id(&mut connection, contest_id)?; - assert_contest_not_started(&logged_user, &contest)?; - let scores = problem::get_problems_by_contest_id_with_score(&mut connection, contest_id)?; - let submissions = - submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?; - let mut scores: Vec<_> = scores.into_iter().group_by(|e| e.user_name.as_ref().map(|s| s.clone())).into_iter().map(|(user_name, problems)| { - let problems: Vec<_> = problems.map(|p| get_formatted_problem_by_contest_with_score(&p, &contest)).collect(); - - Score { - user_name, - solved_count: i64::try_from(problems.iter().filter(|p| p.first_ac_submission_time != "").count()).unwrap(), - penalty: problems.iter().filter(|p| p.first_ac_submission_time != "") - .map(|p| match p.first_ac_submission_minutes { - Some(x) if x >= 0 => x, - _ => 0, - } + i64::from(20*p.failed_submissions)) - .sum(), - problems - } - }).collect(); - scores.sort_by(|a, b| { - ( - a.user_name != None, - -a.solved_count, - a.penalty, - &a.user_name, - ) - .cmp(&( - b.user_name != None, - -b.solved_count, - b.penalty, - &b.user_name, - )) - }); - - render( - &hb, - "scoreboard", - &Context { - base, - contest: get_formatted_contest(&tz, &contest), - scores, - submissions: get_formatted_submissions(&tz, &submissions), - }, - ) -} - -fn assert_contest_not_started(logged_user: &LoggedUser, contest: &Contest) -> Result<(), PageError> { - if contest - .start_instant - .map(|s| s > Local::now().naive_utc()) - .unwrap_or(false) - && !logged_user.is_admin - { - return Err(PageError::Forbidden( - "Essa competição ainda não começou".into(), - )); - } - Ok(()) -} - -#[get("/contests/{id}/{label}")] -async fn get_contest_problem_by_id_label( - base: BaseContext, - identity: Identity, - pool: web::Data<DbPool>, - hb: web::Data<Handlebars<'_>>, - languages: web::Data<Arc<DashMap<String, Language>>>, - session: Session, - path: web::Path<(i32, String)>, - tz: web::Data<Tz>, -) -> PageResult { - let logged_user = require_identity(&identity)?; - let (contest_id, problem_label) = path.into_inner(); - - #[derive(Serialize, Debug)] - struct LanguageContext { - order: i32, - name: String, - value: String, - } - - #[derive(Serialize)] - struct Context { - base: BaseContext, - languages: Vec<LanguageContext>, - language: Option<String>, - contest: FormattedContest, - problems: Vec<ProblemByContest>, - problem: ProblemByContest, - submissions: Vec<FormattedSubmission>, - } - - let mut languages = languages - .iter() - .filter(|kv| { - if logged_user.is_admin { - true - } else { - kv.key() == "cpp.17.g++" - } - }) - .map(|kv| LanguageContext { - order: kv.value().order, - value: kv.key().into(), - name: kv.value().name.clone(), - }) - .collect::<Vec<_>>(); - languages.sort_by(|a, b| a.order.cmp(&b.order)); - - let mut connection = pool.get()?; - let contest = contest::get_contest_by_id(&mut connection, contest_id)?; - assert_contest_not_started(&logged_user, &contest)?; - let problems = problem::get_problems_by_contest_id(&mut connection, contest_id)?; - let problem = - problem::get_problem_by_contest_id_label(&mut connection, contest_id, &problem_label)?; - let submissions = submission::get_submissions_user_by_contest_problem( - &mut connection, - logged_user.id, - contest_id, - &problem_label, - )?; - - render( - &hb, - "contest_problem", - &Context { - base, - contest: get_formatted_contest(&tz, &contest), - languages, - problems, - problem, - language: session.get("language")?, - submissions: get_formatted_submissions(&tz, &submissions), - }, - ) -} - -#[derive(Serialize, Deserialize, Clone)] -struct LoggedUser { - id: i32, - name: String, - is_admin: bool, -} - -#[post("/logout")] -async fn post_logout(identity: Identity) -> PageResult { - identity.logout(); - Ok(redirect_to_root()) -} - -#[post("/login")] -async fn post_login( - pool: web::Data<DbPool>, - form: web::Form<LoginForm>, - request: HttpRequest, -) -> PageResult { - let mut connection = pool.get()?; - - match web::block(move || { - user::check_matching_password(&mut connection, &form.name, &form.password) - }) - .await - .map_err(|e| PageError::Web(e.into()))? - .map_err(|e| match e { - UserHashingError::Database(e) => PageError::Database(e), - UserHashingError::Hash(_) => PageError::Validation("Senha inválida".into()), - })? { - PasswordMatched::UserDoesntExist => { - Err(PageError::Validation("Usuário inexistente".into())) - } - PasswordMatched::PasswordDoesntMatch => { - Err(PageError::Validation("Senha incorreta".into())) - } - PasswordMatched::PasswordMatches(logged_user) => { - Identity::login( - &request.extensions(), - serde_json::to_string(&LoggedUser { - id: logged_user.id, - name: (&logged_user.name).into(), - is_admin: logged_user.is_admin, - }) - .map_err(|_| PageError::Custom("Usuário no banco de dados inconsistente".into()))?, - ) - .map_err(|_| PageError::Custom("Impossível fazer login".into()))?; - Ok(redirect_to_root()) - } - } -} - -#[get("/submission_updates/")] -async fn submission_updates(broadcaster: web::Data<Mutex<Broadcaster>>) -> HttpResponse { - let rx = broadcaster - .lock() - .expect("Submission broadcaster is not active") - .new_client(); - - HttpResponse::Ok() - .append_header(("content-type", "text/event-stream")) - .streaming(rx) -} - -#[derive(Serialize)] -struct FormattedSubmission { - uuid: String, - verdict: String, - problem_label: String, - submission_instant: String, - error_output: Option<String>, - user_name: String, - time_ms: Option<i32>, - memory: String, - failed_test: Option<i32>, -} - -fn format_utc_date_time(tz: &Tz, input: NaiveDateTime) -> String { - tz.from_utc_datetime(&input) - .format("%d/%m/%Y %H:%M:%S") - .to_string() -} - -fn get_formatted_submissions( - tz: &Tz, - vec: &Vec<(Submission, ContestProblem, User)>, -) -> Vec<FormattedSubmission> { - vec.iter() - .map(|(submission, contest_problem, user)| FormattedSubmission { - uuid: (&submission.uuid).into(), - verdict: submission - .verdict - .as_ref() - .map(|s| String::from(s)) - .unwrap_or("WJ".into()) - .into(), - problem_label: contest_problem.label.clone(), - submission_instant: format_utc_date_time(tz, submission.submission_instant), - error_output: submission.error_output.as_ref().map(|s| s.into()), - user_name: user.name.clone(), - time_ms: submission.time_ms, - memory: match submission.memory_kib { - None | Some(0) => "".into(), - Some(k) if k < 1_024 => format!("{}KiB", k), - Some(k) => format!("{}MiB", k / 1_024), - }, - failed_test: submission.failed_test, - }) - .collect() -} - -#[get("/submissions/")] -async fn get_submissions( - base: BaseContext, - identity: Identity, - pool: web::Data<DbPool>, - hb: web::Data<Handlebars<'_>>, - tz: web::Data<Tz>, -) -> PageResult { - let logged_user = require_identity(&identity)?; - let mut connection = pool.get()?; - - #[derive(Serialize)] - struct Context { - base: BaseContext, - submissions: Vec<FormattedSubmission>, - } - - let submissions = if logged_user.is_admin { - submission::get_submissions(&mut connection)? - } else { - submission::get_submissions_user(&mut connection, logged_user.id)? - }; - - render( - &hb, - "submissions", - &Context { - base, - submissions: get_formatted_submissions(&tz, &submissions), - }, - ) -} - -#[get("/submissions/me/")] -async fn get_submissions_me( - base: BaseContext, - identity: Identity, - pool: web::Data<DbPool>, - hb: web::Data<Handlebars<'_>>, - tz: web::Data<Tz>, -) -> PageResult { - let logged_user = require_identity(&identity)?; - let mut connection = pool.get()?; - - #[derive(Serialize)] - struct Context { - base: BaseContext, - submissions: Vec<FormattedSubmission>, - } - - let submissions = submission::get_submissions_user(&mut connection, logged_user.id)?; - - render( - &hb, - "submissions", - &Context { - base, - submissions: get_formatted_submissions(&tz, &submissions), - }, - ) -} - -#[get("/submissions/{uuid}")] -async fn get_submission( - base: BaseContext, - identity: Identity, - pool: web::Data<DbPool>, - hb: web::Data<Handlebars<'_>>, - path: web::Path<(String,)>, - tz: web::Data<Tz>, -) -> PageResult { - let logged_user = require_identity(&identity)?; - let (submission_uuid,) = path.into_inner(); - let mut connection = pool.get()?; - - #[derive(Serialize)] - struct Submission { - pub uuid: String, - pub verdict: Option<String>, - pub source_text: String, - pub language: String, - pub memory_kib: Option<i32>, - pub time_ms: Option<i32>, - pub time_wall_ms: Option<i32>, - pub error_output: Option<String>, - pub user_name: String, - pub problem_label: String, - pub contest_name: String, - pub failed_test: Option<i32>, - } - - #[derive(Serialize)] - struct Context { - base: BaseContext, - submission: Submission, - } - let (submission, user, contest_problem, contest) = - submission::get_submission_by_uuid(&mut connection, submission_uuid)?; - - if user.id != logged_user.id && !logged_user.is_admin { - return Err(PageError::Forbidden( - "Não é possível acessar uma submissão de outro usuário".into(), - )); - } - - render( - &hb, - "submission", - &Context { - base, - submission: Submission { - uuid: submission.uuid, - verdict: submission.verdict, - source_text: submission.source_text, - language: submission.language, - memory_kib: submission.memory_kib, - time_ms: submission.time_ms, - time_wall_ms: submission.time_wall_ms, - error_output: submission.error_output, - user_name: user.name, - problem_label: contest_problem.label, - contest_name: contest.name, - failed_test: submission.failed_test, - }, - }, - ) -} - -#[get("/submissions/me/contests/{id}")] -async fn get_submissions_me_by_contest_id( - base: BaseContext, - identity: Identity, - pool: web::Data<DbPool>, - hb: web::Data<Handlebars<'_>>, - path: web::Path<(i32,)>, - tz: web::Data<Tz>, -) -> PageResult { - let logged_user = require_identity(&identity)?; - let (contest_id,) = path.into_inner(); - let mut connection = pool.get()?; - - #[derive(Serialize)] - struct Context { - base: BaseContext, - submissions: Vec<FormattedSubmission>, - } - - let submissions = - submission::get_submissions_user_by_contest(&mut connection, logged_user.id, contest_id)?; - - render( - &hb, - "submissions", - &Context { - base, - submissions: get_formatted_submissions(&tz, &submissions), - }, - ) -} - -#[get("/submissions/me/contests/{id}/{label}")] -async fn get_submissions_me_by_contest_id_problem_label( - base: BaseContext, - identity: Identity, - pool: web::Data<DbPool>, - hb: web::Data<Handlebars<'_>>, - path: web::Path<(i32, String)>, - tz: web::Data<Tz>, -) -> PageResult { - let logged_user = require_identity(&identity)?; - let mut connection = pool.get()?; - - #[derive(Serialize)] - struct Context { - base: BaseContext, - submissions: Vec<FormattedSubmission>, - } - - let (contest_id, problem_label) = path.into_inner(); - let submissions = submission::get_submissions_user_by_contest_problem( - &mut connection, - logged_user.id, - contest_id, - &problem_label, - )?; - - render( - &hb, - "submissions", - &Context { - base, - submissions: get_formatted_submissions(&tz, &submissions), - }, - ) -} - -#[derive(Serialize, Deserialize)] -struct SubmissionForm { - contest_problem_id: i32, - language: String, - source_text: String, -} - -fn redirect_to_root() -> HttpResponse { - HttpResponse::SeeOther() - .append_header(( - header::LOCATION, - HeaderValue::from_str( - &env::var("BASE_URL").expect("BASE_URL environment variable is not set"), - ) - .unwrap(), - )) - .finish() -} - -fn redirect_to_referer(message: String, request: &HttpRequest) -> HttpResponse { - let referer = request - .headers() - .get("Referer") - .and_then(|h| h.to_str().ok()) - .map(|s| s.into()) - .unwrap_or(env::var("BASE_URL").expect("BASE_URL environment variable is not set")); - FlashMessage::info(message).send(); - HttpResponse::SeeOther() - .append_header((header::LOCATION, HeaderValue::from_str(&referer).unwrap())) - .finish() -} - -#[derive(Serialize, Deserialize)] -struct ChangePasswordForm { - old_password: String, - new_password: String, - new_password_repeat: String, -} - -#[post("/me/password")] -async fn change_password( - identity: Identity, - form: web::Form<ChangePasswordForm>, - pool: web::Data<DbPool>, - request: HttpRequest, -) -> PageResult { - let identity = require_identity(&identity)?; - if form.new_password != form.new_password_repeat { - return Err(PageError::Validation("Senhas são diferentes".into())); - } - - let mut connection = pool.get()?; - - match user::change_password( - &mut connection, - identity.id, - &form.old_password, - &form.new_password, - )? { - PasswordMatched::PasswordMatches(_) => Ok(redirect_to_referer( - "Senha alterada com sucesso".into(), - &request, - )), - _ => Ok(redirect_to_referer( - "Senha antiga incorreta".into(), - &request, - )), - } -} - -#[derive(Serialize, Deserialize)] -struct CreateUserForm { - name: String, - password: String, - is_admin: Option<bool>, -} - -#[post("/users/")] -async fn create_user( - identity: Identity, - pool: web::Data<DbPool>, - form: web::Form<CreateUserForm>, - request: HttpRequest, -) -> PageResult { - let identity = require_identity(&identity)?; - if !identity.is_admin { - return Err(PageError::Forbidden( - "Apenas administradores podem fazer isso".into(), - )); - } - - let mut connection = pool.get()?; - - user::insert_new_user( - &mut connection, - user::NewUser { - name: &form.name, - password: &form.password, - is_admin: form.is_admin.unwrap_or(false), - creation_instant: Local::now().naive_utc(), - creation_user_id: Some(identity.id), - }, - )?; - - Ok(redirect_to_referer( - "Usuário criado com sucesso".into(), - &request, - )) -} - -#[derive(Serialize, Deserialize)] -struct ImpersonateUserForm { - name: String, -} - -#[post("/impersonate/")] -async fn impersonate_user( - identity: Identity, - pool: web::Data<DbPool>, - form: web::Form<ImpersonateUserForm>, - request: HttpRequest, -) -> PageResult { - let my_identity = require_identity(&identity)?; - if !my_identity.is_admin { - return Err(PageError::Forbidden( - "Apenas administradores podem fazer isso".into(), - )); - } - - let mut connection = pool.get()?; - - let user = user::get_user_by_name(&mut connection, &form.name)?; - Identity::login( - &request.extensions(), - serde_json::to_string(&LoggedUser { - id: user.id, - name: (&user.name).into(), - is_admin: user.is_admin, - }) - .map_err(|_| PageError::Custom("Usuário no banco de dados inconsistente".into()))?, - ) - .map_err(|_| PageError::Custom("Impossível fazer login".into()))?; - - Ok(redirect_to_referer( - "Personificado com sucesso".into(), - &request, - )) -} - -#[post("/submissions/")] -async fn create_submission( - identity: Identity, - form: web::Form<SubmissionForm>, - pool: web::Data<DbPool>, - job_sender: web::Data<Sender<Job>>, - languages: web::Data<Arc<DashMap<String, Language>>>, - session: Session, - request: HttpRequest, -) -> PageResult { - let logged_user = require_identity(&identity)?; - let mut connection = pool.get()?; - - languages - .get(&form.language) - .ok_or(PageError::Validation("Linguagem inexistente".into()))?; - - if !logged_user.is_admin && form.language != "cpp.17.g++" { - return Err(PageError::Validation( - "Somente é possível submeter em C++".into(), - )); - } - - let uuid = Uuid::new_v4(); - submission::insert_submission( - &mut connection, - submission::NewSubmission { - uuid: uuid.to_string(), - source_text: (&form.source_text).into(), - language: (&form.language).into(), - submission_instant: Local::now().naive_utc(), - contest_problem_id: form.contest_problem_id, - user_id: logged_user.id, - }, - )?; - - let contest = - contest::get_contest_by_contest_problem_id(&mut connection, form.contest_problem_id)?; - assert_contest_not_started(&logged_user, &contest)?; - - let metadata = - problem::get_problem_by_contest_id_metadata(&mut connection, form.contest_problem_id)?; - - job_sender - .send(Job { - uuid: uuid.to_string(), - language: (&form.language).into(), - time_limit_ms: metadata.time_limit_ms, - memory_limit_kib: metadata.memory_limit_bytes / 1_024, - - which: Some(job::Which::Judgement(job::Judgement { - source_text: (&form.source_text).into(), - test_count: metadata.test_count, - test_pattern: format!("./{}/{}", metadata.id, metadata.test_pattern).into(), - checker_language: metadata.checker_language, - checker_source_path: format!("./{}/{}", metadata.id, metadata.checker_path).into(), - })), - }) - .await?; - - session.insert("language", &form.language)?; - Ok(redirect_to_referer( - format!("Submetido {} com sucesso!", uuid), - &request, - )) -} - -#[post("/submissions/{uuid}/rejudge")] -async fn rejudge_submission( - identity: Identity, - pool: web::Data<DbPool>, - job_sender: web::Data<Sender<Job>>, - request: HttpRequest, - path: web::Path<(String,)>, -) -> PageResult { - let logged_user = require_identity(&identity)?; - let mut connection = pool.get()?; - - if !logged_user.is_admin { - return Err(PageError::Forbidden( - "Apenas administradores podem fazer isso".into(), - )); - } - - let (submission_uuid,) = path.into_inner(); - let (submission, _, _, _) = - submission::get_submission_by_uuid(&mut connection, submission_uuid.clone())?; - let metadata = problem::get_problem_by_contest_id_metadata( - &mut connection, - submission.contest_problem_id, - )?; - - job_sender - .send(create_job_from_submission(submission, metadata)) - .await?; - - Ok(redirect_to_referer( - format!("Rejulgando {}", submission_uuid), - &request, - )) -} - -#[derive(Serialize)] -struct FormattedContest { - pub id: i32, - pub name: String, - pub start_instant: Option<String>, - pub end_instant: Option<String>, - pub creation_instant: String, - pub grade_ratio: Option<i32>, - pub grade_after_ratio: Option<i32>, - pub accepted_count: i32, - pub accepted_after_count: i32, - pub accepted_total_count: i32, - pub problem_count: i32, - pub grade: String, -} - -fn get_formatted_contest(tz: &Tz, contest: &Contest) -> FormattedContest { - FormattedContest { - id: contest.id, - name: contest.name.clone(), - start_instant: contest.start_instant.map(|i| format_utc_date_time(&tz, i)), - end_instant: contest.end_instant.map(|i| format_utc_date_time(&tz, i)), - creation_instant: format_utc_date_time(&tz, contest.creation_instant), - grade_ratio: contest.grade_ratio, - grade_after_ratio: contest.grade_after_ratio, - accepted_count: 0, - accepted_after_count: 0, - accepted_total_count: 0, - problem_count: 0, - grade: "".into(), - } -} - -fn get_formatted_contest_acs(tz: &Tz, contest: &ContestWithAcs) -> FormattedContest { - FormattedContest { - id: contest.id, - name: contest.name.clone(), - start_instant: contest.start_instant.map(|i| format_utc_date_time(&tz, i)), - end_instant: contest.end_instant.map(|i| format_utc_date_time(&tz, i)), - creation_instant: format_utc_date_time(&tz, contest.creation_instant), - grade_ratio: contest.grade_ratio, - grade_after_ratio: contest.grade_after_ratio, - accepted_count: contest.accepted_count, - accepted_after_count: contest.accepted_after_count, - accepted_total_count: contest.accepted_count + contest.accepted_after_count, - problem_count: contest.problem_count, - grade: match contest.grade_ratio { - Some(grade_ratio) => format!( - "{:.2}", - 10.0 * (f64::from(contest.accepted_count) * 1.0 / f64::from(grade_ratio) - + match contest.grade_after_ratio { - Some(grade_after_ratio) => - f64::from(contest.accepted_after_count) * 1.0 - / f64::from(grade_after_ratio), - None => 0.0, - }) - ) - .replacen(".", ",", 1), - None => "".into(), - }, - } -} - -fn get_formatted_contests( - connection: &mut PgConnection, - user_id: Option<i32>, - tz: &Tz, -) -> Result<Vec<FormattedContest>, PageError> { - Ok(match user_id { - Some(user_id) => contest::get_contests_with_acs(connection, user_id)? - .iter() - .map(|c| get_formatted_contest_acs(tz, c)) - .collect(), - None => contest::get_contests(connection)? - .iter() - .map(|c| get_formatted_contest(tz, c)) - .collect(), - }) -} - -#[get("/")] -async fn get_main( - base: BaseContext, - identity: Option<Identity>, - pool: web::Data<DbPool>, - hb: web::Data<Handlebars<'_>>, - tz: web::Data<Tz>, -) -> PageResult { - let logged_user = get_identity(&identity.as_ref()); - - #[derive(Serialize)] - struct Context { - base: BaseContext, - contests: Vec<FormattedContest>, - submissions: Vec<FormattedSubmission>, - } - - let mut connection = pool.get()?; - let submissions = submission::get_submissions(&mut connection)?; - - render( - &hb, - "main", - &Context { - base, - contests: get_formatted_contests(&mut connection, logged_user.map(|u| u.id), &tz)?, - submissions: get_formatted_submissions(&tz, &submissions), - }, - ) -} - -#[get("/contests/")] -async fn get_contests( - base: BaseContext, - identity: Identity, - pool: web::Data<DbPool>, - hb: web::Data<Handlebars<'_>>, - tz: web::Data<Tz>, -) -> PageResult { - let logged_user = require_identity(&identity)?; - - #[derive(Serialize)] - struct Context { - base: BaseContext, - contests: Vec<FormattedContest>, - } - - let mut connection = pool.get()?; - render( - &hb, - "contests", - &Context { - base, - contests: get_formatted_contests(&mut connection, Some(logged_user.id), &tz)?, - }, - ) -} - -#[post("/contests/")] -async fn create_contest( - identity: Identity, - pool: web::Data<DbPool>, - mut payload: Multipart, - job_sender: web::Data<Sender<Job>>, - job_result_sender: web::Data<broadcast::Sender<JobResult>>, - tz: web::Data<Tz>, - request: HttpRequest, -) -> PageResult { - let logged_user = require_identity(&identity)?; - if !logged_user.is_admin { - return Err(PageError::Forbidden( - "Apenas administradores podem fazer isso".into(), - )); - } - - #[derive(Debug)] - struct Form { - name: Option<String>, - start_instant: Option<String>, - end_instant: Option<String>, - polygon_zip: Option<Cursor<Vec<u8>>>, - grade_ratio: Option<i32>, - grade_after_ratio: Option<i32>, - } - - let mut form = Form { - name: None, - start_instant: None, - end_instant: None, - polygon_zip: None, - grade_ratio: None, - grade_after_ratio: None, - }; - - while let Ok(Some(mut field)) = payload.try_next().await { - let mut cursor = Cursor::new(vec![]); - while let Some(chunk) = field.next().await { - let data = chunk.unwrap(); - cursor - .write(&data) - .map_err(|_| PageError::Validation("Corpo inválido".into()))?; - } - - cursor.set_position(0); - - fn parse_field(field: &str, cursor: &mut Cursor<Vec<u8>>) -> Result<String, PageError> { - let mut value = String::new(); - cursor - .read_to_string(&mut value) - .map_err(|_| PageError::Validation(format!("Campo {} inválido", field)))?; - Ok(value) - } - - match field.content_disposition().get_name() { - Some("name") => form.name = Some(parse_field("name", &mut cursor)?), - Some("start_instant") => { - form.start_instant = Some(parse_field("start_instant", &mut cursor)?) - } - Some("end_instant") => { - form.end_instant = Some(parse_field("end_instant", &mut cursor)?) - } - Some("grade_ratio") => { - form.grade_ratio = parse_field("grade_ratio", &mut cursor)?.parse().ok() - } - Some("grade_after_ratio") => { - form.grade_after_ratio = parse_field("grade_after_ratio", &mut cursor)?.parse().ok() - } - Some("polygon_zip") => form.polygon_zip = Some(cursor), - _ => {} - } - } - - let polygon_zip = form - .polygon_zip - .ok_or(PageError::Validation("Arquivo não informado".into()))?; - let imported = import_contest::import_file(polygon_zip) - .map_err(|e| PageError::Validation(format!("Não foi possível importar: {}", e)))?; - let mut connection = pool.get()?; - - let contest = if form.name.as_ref().unwrap() != "" { - Some(contest::insert_contest( - &mut connection, - contest::NewContest { - name: form.name.clone().unwrap(), - start_instant: form - .start_instant - .and_then(|s| tz.datetime_from_str(&s, "%Y-%m-%d %H:%M:%S").ok()) - .map(|d| d.naive_utc()), - end_instant: form - .end_instant - .and_then(|s| tz.datetime_from_str(&s, "%Y-%m-%d %H:%M:%S").ok()) - .map(|d| d.naive_utc()), - creation_instant: Local::now().naive_utc(), - creation_user_id: logged_user.id, - grade_ratio: form.grade_ratio, - grade_after_ratio: form.grade_after_ratio, - }, - )?) - } else { - None - }; - - fn polygon_url_to_id_without_revision(url: String) -> String { - url.replace("https://polygon.codeforces.com/", "polygon.") - .replace("/", ".") - } - - let problem_label: HashMap<String, String> = - HashMap::from_iter(imported.0.problems.problem.iter().map(|problem| { - ( - polygon_url_to_id_without_revision(problem.url.clone()), - problem.index.clone(), - ) - })); - - let mut zip = imported.2; - - lazy_static! { - static ref CODEFORCES_LANGUAGE_TO_JUGHISTO: HashMap<String, String> = { - let mut m = HashMap::new(); - m.insert("cpp.g++17".into(), "cpp.17.g++".into()); - m.insert("cpp.msys2-mingw64-9-g++17".into(), "cpp.17.g++".into()); - m.insert("java.8".into(), "java.8".into()); - m.insert("testlib".into(), "cpp.17.g++".into()); - m - }; - } - - for (name, metadata) in imported.1 { - let problem_id_without_revision = polygon_url_to_id_without_revision(metadata.url); - let problem_id = format!("{}.r{}", problem_id_without_revision, &metadata.revision); - - let files_regex: Regex = Regex::new(&format!( - concat!( - "^{}/(", - r"files/$|", - r"files/.*\.cpp$|", - r"files/.*\.h$|", - r"files/tests/$|", - r"files/tests/validator-tests/$|", - r"files/tests/validator-tests/.*$|", - r"files/tests/validator-tests/.*$|", - r"solutions/$|", - r"solutions/.*.cc$|", - r"solutions/.*.cpp$|", - r"statements/$|", - r"statements/.html/.*$|", - r"tests/$", - ")" - ), - name - )) - .unwrap(); - let mut filenames = zip - .file_names() - .filter(|name| files_regex.is_match(name)) - .map(|s| s.to_string()) - .collect::<Vec<_>>(); - filenames.sort(); - for name in filenames { - let relative_path = files_regex - .captures(&name) - .unwrap() - .get(1) - .unwrap() - .as_str(); - let data_path = format!("/data/{}/{}", problem_id, relative_path); - - if name.ends_with("/") { - info!("Creating directory {} into {}", name, data_path); - create_dir_all(data_path)?; - continue; - } - - info!("Putting file {} into {}", name, data_path); - std::io::copy(&mut zip.by_name(&name)?, &mut File::create(data_path)?)?; - } - - fn map_codeforces_language(input: &String) -> Result<String, PageError> { - Ok(CODEFORCES_LANGUAGE_TO_JUGHISTO - .get(input) - .ok_or_else(|| PageError::Validation(format!("Linguagem {} não suportada", input)))? - .into()) - } - - let main_solution = &metadata - .assets - .solutions - .solution - .iter() - .find(|s| s.tag == "main") - .ok_or(PageError::Validation("No main solution".into()))? - .source; - - let problem = problem::upsert_problem( - &mut connection, - problem::NewProblem { - id: problem_id.clone(), - name: metadata.names.name[0].value.clone(), - memory_limit_bytes: metadata.judging.testset[0] - .memory_limit - .value - .parse() - .unwrap(), - time_limit_ms: metadata.judging.testset[0] - .time_limit - .value - .parse() - .unwrap(), - checker_path: metadata.assets.checker.source.path.clone(), - checker_language: map_codeforces_language(&metadata.assets.checker.r#type)?, - validator_path: metadata.assets.validators.validator[0].source.path.clone(), - validator_language: map_codeforces_language( - &metadata.assets.validators.validator[0].source.r#type, - )?, - main_solution_path: main_solution.path.clone(), - main_solution_language: map_codeforces_language(&main_solution.r#type)?, - test_pattern: metadata.judging.testset[0].input_path_pattern.value.clone(), - test_count: metadata.judging.testset[0] - .test_count - .value - .parse() - .unwrap(), - status: "compiled".into(), - creation_instant: Local::now().naive_utc(), - creation_user_id: logged_user.id, - }, - )?; - - for (i, test) in metadata.judging.testset[0].tests.test.iter().enumerate() { - let i = i + 1; - let test_path = format!( - "./{}/{}", - problem_id, - import_contest::format_width(&problem.test_pattern, i) - ); - - info!( - "Iterating through test {} to {:#?}, which is {}", - i, - test_path, - test.method.as_ref().unwrap() - ); - if test.method.as_ref().unwrap() == "manual" { - let test_name = PathBuf::from(&name) - .join(import_contest::format_width(&problem.test_pattern, i)); - info!("Extracting {:#?} from zip", test_name); - std::io::copy( - &mut zip.by_name(&test_name.to_str().unwrap())?, - &mut File::create(PathBuf::from("/data/").join(&test_path))?, - )?; - } else { - let cmd: Vec<_> = test.cmd.as_ref().unwrap().split(" ").collect(); - let run_stats = language::run_cached( - &job_sender, - &job_result_sender, - &"cpp.17.g++".into(), - format!("./{}/files/{}.cpp", problem.id, cmd.get(0).unwrap()), - cmd[1..].iter().map(|s| s.clone().into()).collect(), - None, - Some(test_path.clone()), - problem.memory_limit_bytes / 1_024, - 10_000, - ) - .await - .map_err(|_| { - PageError::Validation("Couldn't use an intermediate program".into()) - })?; - - if run_stats.result != i32::from(job_result::run_cached::Result::Ok) { - return Err(PageError::Validation( - "Couldn't run an intermediate program".into(), - )); - } - } - - let run_stats = language::run_cached( - &job_sender, - &job_result_sender, - &problem.main_solution_language, - format!("./{}/{}", problem.id, problem.main_solution_path), - vec![], - Some(test_path.clone()), - Some(format!("{}.a", test_path)), - problem.memory_limit_bytes / 1_024, - problem.time_limit_ms, - ) - .await - .map_err(|_| PageError::Validation("Couldn't run solution on test".into()))?; - if run_stats.exit_code != 0 { - return Err(PageError::Validation( - "Couldn't run solution on test".into(), - )); - } - } - - language::judge( - &job_sender, - &job_result_sender, - &problem.main_solution_language, - fs::read_to_string(PathBuf::from(format!( - "/data/{}/{}", - problem.id, problem.main_solution_path - )))?, - problem.test_count, - format!("./{}/{}", problem.id, problem.test_pattern).into(), - problem.checker_language, - format!("./{}/{}", problem.id, problem.checker_path).into(), - problem.memory_limit_bytes / 1_024, - problem.time_limit_ms, - ) - .await - .map_err(|_| PageError::Validation("Couldn't judge main solution".into()))?; - - if form.name.as_ref().unwrap() != "" { - contest::relate_problem( - &mut connection, - contest::NewContestProblems { - label: problem_label - .get(&problem_id_without_revision) - .ok_or(PageError::Validation( - "Arquivo não contém problemas listados".into(), - ))? - .to_string() - .to_uppercase(), - contest_id: contest.as_ref().unwrap().id, - problem_id, - }, - )?; - } - } - - if form.name.as_ref().unwrap() != "" { - Ok(redirect_to_referer( - "Competição criada com sucesso".into(), - &request, - )) - } else { - Ok(redirect_to_referer( - "Problemas adicionados com sucesso".into(), - &request, - )) - } -} diff --git a/src/pages/mod.rs b/src/pages/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..cff4464660a6e6856d3f5cc665fd4e467dde1aa6 --- /dev/null +++ b/src/pages/mod.rs @@ -0,0 +1,1566 @@ +use std::collections::HashMap; +use std::convert::TryFrom; +use std::fs::{create_dir_all, File}; +use std::future::Future; +use std::io::{Cursor, Read, Write}; +use std::iter::FromIterator; +use std::path::PathBuf; +use std::pin::Pin; +use std::sync::{Arc, Mutex}; +use std::{env, fs, str}; + +use actix_files::NamedFile; +use actix_identity::Identity; +use actix_multipart::Multipart; +use actix_session::Session; +use actix_web::dev::Payload; +use actix_web::http::header::{ContentType, HeaderValue}; +use actix_web::http::{header, StatusCode}; +use actix_web::middleware::ErrorHandlerResponse; +use actix_web::{dev, get, post, web, FromRequest, HttpMessage, HttpRequest, HttpResponse}; +use actix_web_flash_messages::{FlashMessage, IncomingFlashMessages}; +use async_channel::Sender; +use chrono::prelude::*; +use chrono_tz::Tz; +use contest::{Contest, ContestWithAcs}; +use dashmap::DashMap; +use diesel::pg::PgConnection; +use futures::{StreamExt, TryStreamExt}; +use handlebars::{Handlebars, RenderError}; +use itertools::Itertools; +use lazy_static::lazy_static; +use log::{error, info}; +use problem::ProblemByContestWithScore; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use submission::{ContestProblem, Submission}; +use thiserror::Error; +use tokio::sync::broadcast; +use user::{PasswordMatched, User, UserHashingError}; +use uuid::Uuid; + +use crate::broadcaster::Broadcaster; +use crate::models::problem::ProblemByContest; +use crate::models::{contest, problem, submission, user}; +use crate::queue::job_protocol::{job, job_result, Job, JobResult, Language}; +use crate::{import_contest, language, DbPool}; + +#[derive(Error, Debug)] +pub enum PageError { + #[error("Unauthorized")] + Unauthorized(), + #[error("Couldn't render: {0}")] + Render(#[from] handlebars::RenderError), + #[error(transparent)] + SessionGet(#[from] actix_session::SessionGetError), + #[error("{0}")] + Custom(String), + #[error("{0}")] + Forbidden(String), + #[error("{0}")] + Validation(String), + #[error("Couldn't get connection from pool")] + ConnectionPool(#[from] r2d2::Error), + #[error("Couldn't hash")] + UserHashing(#[from] user::UserHashingError), + #[error(transparent)] + Web(#[from] actix_web::Error), + #[error(transparent)] + Queue(#[from] async_channel::SendError<Job>), + #[error("Couldn't fetch result from database")] + Database(#[from] diesel::result::Error), + #[error("couldn't insert session")] + SessionInsert(#[from] actix_session::SessionInsertError), + #[error("couldn't work with the filesystem")] + Io(#[from] std::io::Error), + #[error("couldn't work with the zip")] + Zip(#[from] zip::result::ZipError), +} + +fn error_response_and_log(me: &impl actix_web::error::ResponseError) -> HttpResponse { + error!("{}", me); + HttpResponse::build(me.status_code()) + .insert_header(ContentType::plaintext()) + .body(me.to_string()) +} + +impl actix_web::error::ResponseError for PageError { + fn error_response(&self) -> HttpResponse { + error_response_and_log(self) + } + + fn status_code(&self) -> StatusCode { + match *self { + PageError::Unauthorized() => StatusCode::UNAUTHORIZED, + PageError::Validation(_) => StatusCode::BAD_REQUEST, + PageError::Forbidden(_) => StatusCode::FORBIDDEN, + PageError::Custom(_) + | PageError::SessionInsert(_) + | PageError::ConnectionPool(_) + | PageError::Web(_) + | PageError::Render(_) + | PageError::Queue(_) + | PageError::SessionGet(_) + | PageError::Database(_) + | PageError::Io(_) + | PageError::UserHashing(_) + | PageError::Zip(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +type PageResult = Result<HttpResponse, PageError>; + +#[derive(Serialize)] +pub struct BaseContext { + logged_user: Option<LoggedUser>, + flash_messages: IncomingFlashMessages, + base_url: String, +} + +impl FromRequest for BaseContext { + type Error = actix_web::Error; + type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>; + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + let req = req.clone(); + Box::pin(async move { + let identity = Option::<Identity>::from_request(&req, &mut Payload::None).await; + let logged_user = identity?.and_then(|identity| get_identity(&Some(&identity))); + let flash_messages = + IncomingFlashMessages::from_request(&req, &mut Payload::None).await?; + Ok(BaseContext { + logged_user, + flash_messages, + base_url: env::var("BASE_URL") + .expect("BASE_URL environment variable is not set"), + }) + }) + } +} + +fn render<Err: From<RenderError>, Ctx: serde::Serialize>( + hb: &Handlebars, + name: &str, + context: &Ctx, +) -> Result<HttpResponse, Err> { + Ok(HttpResponse::Ok().body(hb.render(name, context)?)) +} + +#[get("/login")] +async fn get_login(base: BaseContext, hb: web::Data<Handlebars<'_>>) -> PageResult { + #[derive(Serialize)] + struct Context { + base: BaseContext, + } + render(&hb, "login", &Context { base }) +} + +#[get("/me")] +async fn get_me( + base: BaseContext, + identity: Identity, + hb: web::Data<Handlebars<'_>>, +) -> PageResult { + require_identity(&identity)?; + #[derive(Serialize)] + struct Context { + base: BaseContext, + } + render(&hb, "me", &Context { base }) +} + +#[get("/problems/{id}/assets/{filename}")] +async fn get_problem_by_id_assets( + identity: Identity, + pool: web::Data<DbPool>, + path: web::Path<(i32, String)>, +) -> Result<NamedFile, PageError> { + let logged_user = require_identity(&identity)?; + let (problem_id, asset_filename) = path.into_inner(); + let mut connection = pool.get()?; + let problem = problem::get_problem_by_contest_id_metadata(&mut connection, problem_id)?; + let contest = contest::get_contest_by_id(&mut connection, problem.contest_id)?; + assert_contest_not_started(&logged_user, &contest)?; + let file_path = PathBuf::from("/data/") + .join(problem.id) + .join("statements/.html/portuguese/") + .join(asset_filename); + Ok(NamedFile::open(file_path)?) +} + +pub fn render_401<B>( + mut res: dev::ServiceResponse<B>, +) -> actix_web::Result<ErrorHandlerResponse<B>> { + FlashMessage::error("Você precisa estar logado para acessar esta página").send(); + res.response_mut().headers_mut().insert( + header::LOCATION, + HeaderValue::from_str(&format!( + "{}login", + &env::var("BASE_URL").expect("BASE_URL environment variable is not set") + )) + .unwrap(), + ); + *res.response_mut().status_mut() = StatusCode::SEE_OTHER; + Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) +} + +pub fn render_400<B>( + res: dev::ServiceResponse<B>, +) -> actix_web::Result<ErrorHandlerResponse<B>> { + FlashMessage::error("Entrada inválida").send(); + Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) +} + +#[derive(Serialize, Deserialize)] +struct LoginForm { + name: String, + password: String, +} + +fn get_identity(identity: &Option<&Identity>) -> Option<LoggedUser> { + identity.as_ref().and_then(|identity| { + let identity = identity.id(); + identity + .ok() + .and_then(|identity| serde_json::from_str(&identity).ok()) + }) +} + +fn require_identity(identity: &Identity) -> Result<LoggedUser, PageError> { + get_identity(&Some(identity)).ok_or(PageError::Unauthorized()) +} + +fn format_duration(duration: chrono::Duration) -> String { + format!( + "{}{}{:02}:{:02}", + if duration.num_milliseconds() >= 0 { + "" + } else { + "-" + }, + if duration.num_days() != 0 { + format!("{}:", duration.num_days().abs()) + } else { + "".into() + }, + duration.num_hours().abs() % 24, + duration.num_minutes().abs() % 60 + ) +} + +#[derive(Serialize)] +struct FormattedProblemByContestWithScore { + pub first_ac_submission_time: String, + pub first_ac_submission_minutes: Option<i64>, + pub user_accepted_count: i32, + pub failed_submissions: i32, + pub id: i32, + pub name: String, + pub label: String, + pub memory_limit_mib: i32, + pub time_limit: String, +} + +fn get_formatted_problem_by_contest_with_score( + p: &ProblemByContestWithScore, + contest: &Contest, +) -> FormattedProblemByContestWithScore { + FormattedProblemByContestWithScore { + first_ac_submission_time: p + .first_ac_submission_instant + .map(|t| match contest.start_instant { + Some(cs) => format_duration(t - cs), + None => "*".into(), + }) + .unwrap_or("".into()), + first_ac_submission_minutes: p.first_ac_submission_instant.and_then(|t| match contest + .start_instant + { + Some(cs) => Some((t - cs).num_minutes()), + None => None, + }), + failed_submissions: p.failed_submissions, + id: p.id, + name: p.name.clone(), + label: p.label.clone(), + memory_limit_mib: p.memory_limit_bytes / 1_024 / 1_024, + time_limit: format!("{}", f64::from(p.time_limit_ms) / 1000.0).replacen(".", ",", 1), + user_accepted_count: p.user_accepted_count, + } +} + +#[get("/contests/{id}")] +pub async fn get_contest_by_id( + base: BaseContext, + identity: Identity, + pool: web::Data<DbPool>, + hb: web::Data<Handlebars<'_>>, + path: web::Path<(i32,)>, + tz: web::Data<Tz>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let (contest_id,) = path.into_inner(); + + #[derive(Serialize)] + struct Context { + base: BaseContext, + contest: FormattedContest, + problems: Vec<FormattedProblemByContestWithScore>, + submissions: Vec<FormattedSubmission>, + } + + let mut connection = pool.get()?; + let contest = contest::get_contest_by_id(&mut connection, contest_id)?; + assert_contest_not_started(&logged_user, &contest)?; + + let problems = problem::get_problems_user_by_contest_id_with_score( + &mut connection, + logged_user.id, + contest_id, + )?; + let submissions = submission::get_submissions_user_by_contest( + &mut connection, + logged_user.id, + contest_id, + )?; + + render( + &hb, + "contest", + &Context { + base, + contest: get_formatted_contest(&tz, &contest), + problems: problems + .iter() + .map(|p| get_formatted_problem_by_contest_with_score(p, &contest)) + .collect(), + submissions: get_formatted_submissions(&tz, &submissions), + }, + ) +} + +#[get("/contests/{id}/scoreboard")] +async fn get_contest_scoreboard_by_id( + base: BaseContext, + identity: Identity, + pool: web::Data<DbPool>, + hb: web::Data<Handlebars<'_>>, + path: web::Path<(i32,)>, + tz: web::Data<Tz>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let (contest_id,) = path.into_inner(); + + #[derive(Serialize)] + struct Score { + pub user_name: Option<String>, + pub problems: Vec<FormattedProblemByContestWithScore>, + pub solved_count: i64, + pub penalty: i64, + } + + #[derive(Serialize)] + struct Context { + base: BaseContext, + contest: FormattedContest, + scores: Vec<Score>, + submissions: Vec<FormattedSubmission>, + } + + let mut connection = pool.get()?; + let contest = contest::get_contest_by_id(&mut connection, contest_id)?; + assert_contest_not_started(&logged_user, &contest)?; + let scores = problem::get_problems_by_contest_id_with_score(&mut connection, contest_id)?; + let submissions = submission::get_submissions_user_by_contest( + &mut connection, + logged_user.id, + contest_id, + )?; + let mut scores: Vec<_> = scores.into_iter().group_by(|e| e.user_name.as_ref().map(|s| s.clone())).into_iter().map(|(user_name, problems)| { + let problems: Vec<_> = problems.map(|p| get_formatted_problem_by_contest_with_score(&p, &contest)).collect(); + + Score { + user_name, + solved_count: i64::try_from(problems.iter().filter(|p| p.first_ac_submission_time != "").count()).unwrap(), + penalty: problems.iter().filter(|p| p.first_ac_submission_time != "") + .map(|p| match p.first_ac_submission_minutes { + Some(x) if x >= 0 => x, + _ => 0, + } + i64::from(20*p.failed_submissions)) + .sum(), + problems + } +}).collect(); + scores.sort_by(|a, b| { + ( + a.user_name != None, + -a.solved_count, + a.penalty, + &a.user_name, + ) + .cmp(&( + b.user_name != None, + -b.solved_count, + b.penalty, + &b.user_name, + )) + }); + + render( + &hb, + "scoreboard", + &Context { + base, + contest: get_formatted_contest(&tz, &contest), + scores, + submissions: get_formatted_submissions(&tz, &submissions), + }, + ) +} + +fn assert_contest_not_started( + logged_user: &LoggedUser, + contest: &Contest, +) -> Result<(), PageError> { + if contest + .start_instant + .map(|s| s > Local::now().naive_utc()) + .unwrap_or(false) + && !logged_user.is_admin + { + return Err(PageError::Forbidden( + "Essa competição ainda não começou".into(), + )); + } + Ok(()) +} + +#[get("/contests/{id}/{label}")] +async fn get_contest_problem_by_id_label( + base: BaseContext, + identity: Identity, + pool: web::Data<DbPool>, + hb: web::Data<Handlebars<'_>>, + languages: web::Data<Arc<DashMap<String, Language>>>, + session: Session, + path: web::Path<(i32, String)>, + tz: web::Data<Tz>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let (contest_id, problem_label) = path.into_inner(); + + #[derive(Serialize, Debug)] + struct LanguageContext { + order: i32, + name: String, + value: String, + } + + #[derive(Serialize)] + struct Context { + base: BaseContext, + languages: Vec<LanguageContext>, + language: Option<String>, + contest: FormattedContest, + problems: Vec<ProblemByContest>, + problem: ProblemByContest, + submissions: Vec<FormattedSubmission>, + } + + let mut languages = languages + .iter() + .filter(|kv| { + if logged_user.is_admin { + true + } else { + kv.key() == "cpp.17.g++" + } + }) + .map(|kv| LanguageContext { + order: kv.value().order, + value: kv.key().into(), + name: kv.value().name.clone(), + }) + .collect::<Vec<_>>(); + languages.sort_by(|a, b| a.order.cmp(&b.order)); + + let mut connection = pool.get()?; + let contest = contest::get_contest_by_id(&mut connection, contest_id)?; + assert_contest_not_started(&logged_user, &contest)?; + let problems = problem::get_problems_by_contest_id(&mut connection, contest_id)?; + let problem = + problem::get_problem_by_contest_id_label(&mut connection, contest_id, &problem_label)?; + let submissions = submission::get_submissions_user_by_contest_problem( + &mut connection, + logged_user.id, + contest_id, + &problem_label, + )?; + + render( + &hb, + "contest_problem", + &Context { + base, + contest: get_formatted_contest(&tz, &contest), + languages, + problems, + problem, + language: session.get("language")?, + submissions: get_formatted_submissions(&tz, &submissions), + }, + ) +} + +#[derive(Serialize, Deserialize, Clone)] +struct LoggedUser { + id: i32, + name: String, + is_admin: bool, +} + +#[post("/logout")] +async fn post_logout(identity: Identity) -> PageResult { + identity.logout(); + Ok(redirect_to_root()) +} + +#[post("/login")] +async fn post_login( + pool: web::Data<DbPool>, + form: web::Form<LoginForm>, + request: HttpRequest, +) -> PageResult { + let mut connection = pool.get()?; + + match web::block(move || { + user::check_matching_password(&mut connection, &form.name, &form.password) + }) + .await + .map_err(|e| PageError::Web(e.into()))? + .map_err(|e| match e { + UserHashingError::Database(e) => PageError::Database(e), + UserHashingError::Hash(_) => PageError::Validation("Senha inválida".into()), + })? { + PasswordMatched::UserDoesntExist => { + Err(PageError::Validation("Usuário inexistente".into())) + } + PasswordMatched::PasswordDoesntMatch => { + Err(PageError::Validation("Senha incorreta".into())) + } + PasswordMatched::PasswordMatches(logged_user) => { + Identity::login( + &request.extensions(), + serde_json::to_string(&LoggedUser { + id: logged_user.id, + name: (&logged_user.name).into(), + is_admin: logged_user.is_admin, + }) + .map_err(|_| { + PageError::Custom("Usuário no banco de dados inconsistente".into()) + })?, + ) + .map_err(|_| PageError::Custom("Impossível fazer login".into()))?; + Ok(redirect_to_root()) + } + } +} + +#[get("/submission_updates/")] +async fn submission_updates(broadcaster: web::Data<Mutex<Broadcaster>>) -> HttpResponse { + let rx = broadcaster + .lock() + .expect("Submission broadcaster is not active") + .new_client(); + + HttpResponse::Ok() + .append_header(("content-type", "text/event-stream")) + .streaming(rx) +} + +#[derive(Serialize)] +struct FormattedSubmission { + uuid: String, + verdict: String, + problem_label: String, + submission_instant: String, + error_output: Option<String>, + user_name: String, + time_ms: Option<i32>, + memory: String, + failed_test: Option<i32>, +} + +fn format_utc_date_time(tz: &Tz, input: NaiveDateTime) -> String { + tz.from_utc_datetime(&input) + .format("%d/%m/%Y %H:%M:%S") + .to_string() +} + +fn get_formatted_submissions( + tz: &Tz, + vec: &Vec<(Submission, ContestProblem, User)>, +) -> Vec<FormattedSubmission> { + vec.iter() + .map(|(submission, contest_problem, user)| FormattedSubmission { + uuid: (&submission.uuid).into(), + verdict: submission + .verdict + .as_ref() + .map(|s| String::from(s)) + .unwrap_or("WJ".into()) + .into(), + problem_label: contest_problem.label.clone(), + submission_instant: format_utc_date_time(tz, submission.submission_instant), + error_output: submission.error_output.as_ref().map(|s| s.into()), + user_name: user.name.clone(), + time_ms: submission.time_ms, + memory: match submission.memory_kib { + None | Some(0) => "".into(), + Some(k) if k < 1_024 => format!("{}KiB", k), + Some(k) => format!("{}MiB", k / 1_024), + }, + failed_test: submission.failed_test, + }) + .collect() +} + +#[get("/submissions/")] +async fn get_submissions( + base: BaseContext, + identity: Identity, + pool: web::Data<DbPool>, + hb: web::Data<Handlebars<'_>>, + tz: web::Data<Tz>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let mut connection = pool.get()?; + + #[derive(Serialize)] + struct Context { + base: BaseContext, + submissions: Vec<FormattedSubmission>, + } + + let submissions = if logged_user.is_admin { + submission::get_submissions(&mut connection)? + } else { + submission::get_submissions_user(&mut connection, logged_user.id)? + }; + + render( + &hb, + "submissions", + &Context { + base, + submissions: get_formatted_submissions(&tz, &submissions), + }, + ) +} + +#[get("/submissions/me/")] +async fn get_submissions_me( + base: BaseContext, + identity: Identity, + pool: web::Data<DbPool>, + hb: web::Data<Handlebars<'_>>, + tz: web::Data<Tz>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let mut connection = pool.get()?; + + #[derive(Serialize)] + struct Context { + base: BaseContext, + submissions: Vec<FormattedSubmission>, + } + + let submissions = submission::get_submissions_user(&mut connection, logged_user.id)?; + + render( + &hb, + "submissions", + &Context { + base, + submissions: get_formatted_submissions(&tz, &submissions), + }, + ) +} + +#[get("/submissions/{uuid}")] +async fn get_submission( + base: BaseContext, + identity: Identity, + pool: web::Data<DbPool>, + hb: web::Data<Handlebars<'_>>, + path: web::Path<(String,)>, + tz: web::Data<Tz>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let (submission_uuid,) = path.into_inner(); + let mut connection = pool.get()?; + + #[derive(Serialize)] + struct Submission { + pub uuid: String, + pub verdict: Option<String>, + pub source_text: String, + pub language: String, + pub memory_kib: Option<i32>, + pub time_ms: Option<i32>, + pub time_wall_ms: Option<i32>, + pub error_output: Option<String>, + pub user_name: String, + pub problem_label: String, + pub contest_name: String, + pub failed_test: Option<i32>, + } + + #[derive(Serialize)] + struct Context { + base: BaseContext, + submission: Submission, + } + let (submission, user, contest_problem, contest) = + submission::get_submission_by_uuid(&mut connection, submission_uuid)?; + + if user.id != logged_user.id && !logged_user.is_admin { + return Err(PageError::Forbidden( + "Não é possível acessar uma submissão de outro usuário".into(), + )); + } + + render( + &hb, + "submission", + &Context { + base, + submission: Submission { + uuid: submission.uuid, + verdict: submission.verdict, + source_text: submission.source_text, + language: submission.language, + memory_kib: submission.memory_kib, + time_ms: submission.time_ms, + time_wall_ms: submission.time_wall_ms, + error_output: submission.error_output, + user_name: user.name, + problem_label: contest_problem.label, + contest_name: contest.name, + failed_test: submission.failed_test, + }, + }, + ) +} + +#[get("/submissions/me/contests/{id}")] +async fn get_submissions_me_by_contest_id( + base: BaseContext, + identity: Identity, + pool: web::Data<DbPool>, + hb: web::Data<Handlebars<'_>>, + path: web::Path<(i32,)>, + tz: web::Data<Tz>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let (contest_id,) = path.into_inner(); + let mut connection = pool.get()?; + + #[derive(Serialize)] + struct Context { + base: BaseContext, + submissions: Vec<FormattedSubmission>, + } + + let submissions = submission::get_submissions_user_by_contest( + &mut connection, + logged_user.id, + contest_id, + )?; + + render( + &hb, + "submissions", + &Context { + base, + submissions: get_formatted_submissions(&tz, &submissions), + }, + ) +} + +#[get("/submissions/me/contests/{id}/{label}")] +async fn get_submissions_me_by_contest_id_problem_label( + base: BaseContext, + identity: Identity, + pool: web::Data<DbPool>, + hb: web::Data<Handlebars<'_>>, + path: web::Path<(i32, String)>, + tz: web::Data<Tz>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let mut connection = pool.get()?; + + #[derive(Serialize)] + struct Context { + base: BaseContext, + submissions: Vec<FormattedSubmission>, + } + + let (contest_id, problem_label) = path.into_inner(); + let submissions = submission::get_submissions_user_by_contest_problem( + &mut connection, + logged_user.id, + contest_id, + &problem_label, + )?; + + render( + &hb, + "submissions", + &Context { + base, + submissions: get_formatted_submissions(&tz, &submissions), + }, + ) +} + +#[derive(Serialize, Deserialize)] +struct SubmissionForm { + contest_problem_id: i32, + language: String, + source_text: String, +} + +fn redirect_to_root() -> HttpResponse { + HttpResponse::SeeOther() + .append_header(( + header::LOCATION, + HeaderValue::from_str( + &env::var("BASE_URL").expect("BASE_URL environment variable is not set"), + ) + .unwrap(), + )) + .finish() +} + +fn redirect_to_referer(message: String, request: &HttpRequest) -> HttpResponse { + let referer = request + .headers() + .get("Referer") + .and_then(|h| h.to_str().ok()) + .map(|s| s.into()) + .unwrap_or(env::var("BASE_URL").expect("BASE_URL environment variable is not set")); + FlashMessage::info(message).send(); + HttpResponse::SeeOther() + .append_header((header::LOCATION, HeaderValue::from_str(&referer).unwrap())) + .finish() +} + +#[derive(Serialize, Deserialize)] +struct ChangePasswordForm { + old_password: String, + new_password: String, + new_password_repeat: String, +} + +#[post("/me/password")] +async fn change_password( + identity: Identity, + form: web::Form<ChangePasswordForm>, + pool: web::Data<DbPool>, + request: HttpRequest, +) -> PageResult { + let identity = require_identity(&identity)?; + if form.new_password != form.new_password_repeat { + return Err(PageError::Validation("Senhas são diferentes".into())); + } + + let mut connection = pool.get()?; + + match user::change_password( + &mut connection, + identity.id, + &form.old_password, + &form.new_password, + )? { + PasswordMatched::PasswordMatches(_) => Ok(redirect_to_referer( + "Senha alterada com sucesso".into(), + &request, + )), + _ => Ok(redirect_to_referer( + "Senha antiga incorreta".into(), + &request, + )), + } +} + +#[derive(Serialize, Deserialize)] +struct CreateUserForm { + name: String, + password: String, + is_admin: Option<bool>, +} + +#[post("/users/")] +async fn create_user( + identity: Identity, + pool: web::Data<DbPool>, + form: web::Form<CreateUserForm>, + request: HttpRequest, +) -> PageResult { + let identity = require_identity(&identity)?; + if !identity.is_admin { + return Err(PageError::Forbidden( + "Apenas administradores podem fazer isso".into(), + )); + } + + let mut connection = pool.get()?; + + user::insert_new_user( + &mut connection, + user::NewUser { + name: &form.name, + password: &form.password, + is_admin: form.is_admin.unwrap_or(false), + creation_instant: Local::now().naive_utc(), + creation_user_id: Some(identity.id), + }, + )?; + + Ok(redirect_to_referer( + "Usuário criado com sucesso".into(), + &request, + )) +} + +#[derive(Serialize, Deserialize)] +struct ImpersonateUserForm { + name: String, +} + +#[post("/impersonate/")] +async fn impersonate_user( + identity: Identity, + pool: web::Data<DbPool>, + form: web::Form<ImpersonateUserForm>, + request: HttpRequest, +) -> PageResult { + let my_identity = require_identity(&identity)?; + if !my_identity.is_admin { + return Err(PageError::Forbidden( + "Apenas administradores podem fazer isso".into(), + )); + } + + let mut connection = pool.get()?; + + let user = user::get_user_by_name(&mut connection, &form.name)?; + Identity::login( + &request.extensions(), + serde_json::to_string(&LoggedUser { + id: user.id, + name: (&user.name).into(), + is_admin: user.is_admin, + }) + .map_err(|_| PageError::Custom("Usuário no banco de dados inconsistente".into()))?, + ) + .map_err(|_| PageError::Custom("Impossível fazer login".into()))?; + + Ok(redirect_to_referer( + "Personificado com sucesso".into(), + &request, + )) +} + +#[post("/submissions/")] +async fn create_submission( + identity: Identity, + form: web::Form<SubmissionForm>, + pool: web::Data<DbPool>, + job_sender: web::Data<Sender<Job>>, + languages: web::Data<Arc<DashMap<String, Language>>>, + session: Session, + request: HttpRequest, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let mut connection = pool.get()?; + + languages + .get(&form.language) + .ok_or(PageError::Validation("Linguagem inexistente".into()))?; + + if !logged_user.is_admin && form.language != "cpp.17.g++" { + return Err(PageError::Validation( + "Somente é possível submeter em C++".into(), + )); + } + + let uuid = Uuid::new_v4(); + submission::insert_submission( + &mut connection, + submission::NewSubmission { + uuid: uuid.to_string(), + source_text: (&form.source_text).into(), + language: (&form.language).into(), + submission_instant: Local::now().naive_utc(), + contest_problem_id: form.contest_problem_id, + user_id: logged_user.id, + }, + )?; + + let contest = + contest::get_contest_by_contest_problem_id(&mut connection, form.contest_problem_id)?; + assert_contest_not_started(&logged_user, &contest)?; + + let metadata = + problem::get_problem_by_contest_id_metadata(&mut connection, form.contest_problem_id)?; + + job_sender + .send(Job { + uuid: uuid.to_string(), + language: (&form.language).into(), + time_limit_ms: metadata.time_limit_ms, + memory_limit_kib: metadata.memory_limit_bytes / 1_024, + + which: Some(job::Which::Judgement(job::Judgement { + source_text: (&form.source_text).into(), + test_count: metadata.test_count, + test_pattern: format!("./{}/{}", metadata.id, metadata.test_pattern).into(), + checker_language: metadata.checker_language, + checker_source_path: format!("./{}/{}", metadata.id, metadata.checker_path) + .into(), + })), + }) + .await?; + + session.insert("language", &form.language)?; + Ok(redirect_to_referer( + format!("Submetido {} com sucesso!", uuid), + &request, + )) +} + +#[post("/submissions/{uuid}/rejudge")] +async fn rejudge_submission( + identity: Identity, + pool: web::Data<DbPool>, + job_sender: web::Data<Sender<Job>>, + request: HttpRequest, + path: web::Path<(String,)>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + let mut connection = pool.get()?; + + if !logged_user.is_admin { + return Err(PageError::Forbidden( + "Apenas administradores podem fazer isso".into(), + )); + } + + let (submission_uuid,) = path.into_inner(); + let (submission, _, _, _) = + submission::get_submission_by_uuid(&mut connection, submission_uuid.clone())?; + let metadata = problem::get_problem_by_contest_id_metadata( + &mut connection, + submission.contest_problem_id, + )?; + + job_sender + .send(crate::create_job_from_submission(submission, metadata)) + .await?; + + Ok(redirect_to_referer( + format!("Rejulgando {}", submission_uuid), + &request, + )) +} + +#[derive(Serialize)] +struct FormattedContest { + pub id: i32, + pub name: String, + pub start_instant: Option<String>, + pub end_instant: Option<String>, + pub creation_instant: String, + pub grade_ratio: Option<i32>, + pub grade_after_ratio: Option<i32>, + pub accepted_count: i32, + pub accepted_after_count: i32, + pub accepted_total_count: i32, + pub problem_count: i32, + pub grade: String, +} + +fn get_formatted_contest(tz: &Tz, contest: &Contest) -> FormattedContest { + FormattedContest { + id: contest.id, + name: contest.name.clone(), + start_instant: contest.start_instant.map(|i| format_utc_date_time(&tz, i)), + end_instant: contest.end_instant.map(|i| format_utc_date_time(&tz, i)), + creation_instant: format_utc_date_time(&tz, contest.creation_instant), + grade_ratio: contest.grade_ratio, + grade_after_ratio: contest.grade_after_ratio, + accepted_count: 0, + accepted_after_count: 0, + accepted_total_count: 0, + problem_count: 0, + grade: "".into(), + } +} + +fn get_formatted_contest_acs(tz: &Tz, contest: &ContestWithAcs) -> FormattedContest { + FormattedContest { + id: contest.id, + name: contest.name.clone(), + start_instant: contest.start_instant.map(|i| format_utc_date_time(&tz, i)), + end_instant: contest.end_instant.map(|i| format_utc_date_time(&tz, i)), + creation_instant: format_utc_date_time(&tz, contest.creation_instant), + grade_ratio: contest.grade_ratio, + grade_after_ratio: contest.grade_after_ratio, + accepted_count: contest.accepted_count, + accepted_after_count: contest.accepted_after_count, + accepted_total_count: contest.accepted_count + contest.accepted_after_count, + problem_count: contest.problem_count, + grade: match contest.grade_ratio { + Some(grade_ratio) => format!( + "{:.2}", + 10.0 * (f64::from(contest.accepted_count) * 1.0 / f64::from(grade_ratio) + + match contest.grade_after_ratio { + Some(grade_after_ratio) => + f64::from(contest.accepted_after_count) * 1.0 + / f64::from(grade_after_ratio), + None => 0.0, + }) + ) + .replacen(".", ",", 1), + None => "".into(), + }, + } +} + +fn get_formatted_contests( + connection: &mut PgConnection, + user_id: Option<i32>, + tz: &Tz, +) -> Result<Vec<FormattedContest>, PageError> { + Ok(match user_id { + Some(user_id) => contest::get_contests_with_acs(connection, user_id)? + .iter() + .map(|c| get_formatted_contest_acs(tz, c)) + .collect(), + None => contest::get_contests(connection)? + .iter() + .map(|c| get_formatted_contest(tz, c)) + .collect(), + }) +} + +#[get("/")] +async fn get_main( + base: BaseContext, + identity: Option<Identity>, + pool: web::Data<DbPool>, + hb: web::Data<Handlebars<'_>>, + tz: web::Data<Tz>, +) -> PageResult { + let logged_user = get_identity(&identity.as_ref()); + + #[derive(Serialize)] + struct Context { + base: BaseContext, + contests: Vec<FormattedContest>, + submissions: Vec<FormattedSubmission>, + } + + let mut connection = pool.get()?; + let submissions = submission::get_submissions(&mut connection)?; + + render( + &hb, + "main", + &Context { + base, + contests: get_formatted_contests(&mut connection, logged_user.map(|u| u.id), &tz)?, + submissions: get_formatted_submissions(&tz, &submissions), + }, + ) +} + +#[get("/contests/")] +async fn get_contests( + base: BaseContext, + identity: Identity, + pool: web::Data<DbPool>, + hb: web::Data<Handlebars<'_>>, + tz: web::Data<Tz>, +) -> PageResult { + let logged_user = require_identity(&identity)?; + + #[derive(Serialize)] + struct Context { + base: BaseContext, + contests: Vec<FormattedContest>, + } + + let mut connection = pool.get()?; + render( + &hb, + "contests", + &Context { + base, + contests: get_formatted_contests(&mut connection, Some(logged_user.id), &tz)?, + }, + ) +} + +#[post("/contests/")] +async fn create_contest( + identity: Identity, + pool: web::Data<DbPool>, + mut payload: Multipart, + job_sender: web::Data<Sender<Job>>, + job_result_sender: web::Data<broadcast::Sender<JobResult>>, + tz: web::Data<Tz>, + request: HttpRequest, +) -> PageResult { + let logged_user = require_identity(&identity)?; + if !logged_user.is_admin { + return Err(PageError::Forbidden( + "Apenas administradores podem fazer isso".into(), + )); + } + + #[derive(Debug)] + struct Form { + name: Option<String>, + start_instant: Option<String>, + end_instant: Option<String>, + polygon_zip: Option<Cursor<Vec<u8>>>, + grade_ratio: Option<i32>, + grade_after_ratio: Option<i32>, + } + + let mut form = Form { + name: None, + start_instant: None, + end_instant: None, + polygon_zip: None, + grade_ratio: None, + grade_after_ratio: None, + }; + + while let Ok(Some(mut field)) = payload.try_next().await { + let mut cursor = Cursor::new(vec![]); + while let Some(chunk) = field.next().await { + let data = chunk.unwrap(); + cursor + .write(&data) + .map_err(|_| PageError::Validation("Corpo inválido".into()))?; + } + + cursor.set_position(0); + + fn parse_field(field: &str, cursor: &mut Cursor<Vec<u8>>) -> Result<String, PageError> { + let mut value = String::new(); + cursor + .read_to_string(&mut value) + .map_err(|_| PageError::Validation(format!("Campo {} inválido", field)))?; + Ok(value) + } + + match field.content_disposition().get_name() { + Some("name") => form.name = Some(parse_field("name", &mut cursor)?), + Some("start_instant") => { + form.start_instant = Some(parse_field("start_instant", &mut cursor)?) + } + Some("end_instant") => { + form.end_instant = Some(parse_field("end_instant", &mut cursor)?) + } + Some("grade_ratio") => { + form.grade_ratio = parse_field("grade_ratio", &mut cursor)?.parse().ok() + } + Some("grade_after_ratio") => { + form.grade_after_ratio = + parse_field("grade_after_ratio", &mut cursor)?.parse().ok() + } + Some("polygon_zip") => form.polygon_zip = Some(cursor), + _ => {} + } + } + + let polygon_zip = form + .polygon_zip + .ok_or(PageError::Validation("Arquivo não informado".into()))?; + let imported = import_contest::import_file(polygon_zip) + .map_err(|e| PageError::Validation(format!("Não foi possível importar: {}", e)))?; + let mut connection = pool.get()?; + + let contest = if form.name.as_ref().unwrap() != "" { + Some(contest::insert_contest( + &mut connection, + contest::NewContest { + name: form.name.clone().unwrap(), + start_instant: form + .start_instant + .and_then(|s| tz.datetime_from_str(&s, "%Y-%m-%d %H:%M:%S").ok()) + .map(|d| d.naive_utc()), + end_instant: form + .end_instant + .and_then(|s| tz.datetime_from_str(&s, "%Y-%m-%d %H:%M:%S").ok()) + .map(|d| d.naive_utc()), + creation_instant: Local::now().naive_utc(), + creation_user_id: logged_user.id, + grade_ratio: form.grade_ratio, + grade_after_ratio: form.grade_after_ratio, + }, + )?) + } else { + None + }; + + fn polygon_url_to_id_without_revision(url: String) -> String { + url.replace("https://polygon.codeforces.com/", "polygon.") + .replace("/", ".") + } + + let problem_label: HashMap<String, String> = + HashMap::from_iter(imported.0.problems.problem.iter().map(|problem| { + ( + polygon_url_to_id_without_revision(problem.url.clone()), + problem.index.clone(), + ) + })); + + let mut zip = imported.2; + + lazy_static! { + static ref CODEFORCES_LANGUAGE_TO_JUGHISTO: HashMap<String, String> = { + let mut m = HashMap::new(); + m.insert("cpp.g++17".into(), "cpp.17.g++".into()); + m.insert("cpp.msys2-mingw64-9-g++17".into(), "cpp.17.g++".into()); + m.insert("java.8".into(), "java.8".into()); + m.insert("testlib".into(), "cpp.17.g++".into()); + m + }; + } + + for (name, metadata) in imported.1 { + let problem_id_without_revision = polygon_url_to_id_without_revision(metadata.url); + let problem_id = format!("{}.r{}", problem_id_without_revision, &metadata.revision); + + let files_regex: Regex = Regex::new(&format!( + concat!( + "^{}/(", + r"files/$|", + r"files/.*\.cpp$|", + r"files/.*\.h$|", + r"files/tests/$|", + r"files/tests/validator-tests/$|", + r"files/tests/validator-tests/.*$|", + r"files/tests/validator-tests/.*$|", + r"solutions/$|", + r"solutions/.*.cc$|", + r"solutions/.*.cpp$|", + r"statements/$|", + r"statements/.html/.*$|", + r"tests/$", + ")" + ), + name + )) + .unwrap(); + let mut filenames = zip + .file_names() + .filter(|name| files_regex.is_match(name)) + .map(|s| s.to_string()) + .collect::<Vec<_>>(); + filenames.sort(); + for name in filenames { + let relative_path = files_regex + .captures(&name) + .unwrap() + .get(1) + .unwrap() + .as_str(); + let data_path = format!("/data/{}/{}", problem_id, relative_path); + + if name.ends_with("/") { + info!("Creating directory {} into {}", name, data_path); + create_dir_all(data_path)?; + continue; + } + + info!("Putting file {} into {}", name, data_path); + std::io::copy(&mut zip.by_name(&name)?, &mut File::create(data_path)?)?; + } + + fn map_codeforces_language(input: &String) -> Result<String, PageError> { + Ok(CODEFORCES_LANGUAGE_TO_JUGHISTO + .get(input) + .ok_or_else(|| { + PageError::Validation(format!("Linguagem {} não suportada", input)) + })? + .into()) + } + + let main_solution = &metadata + .assets + .solutions + .solution + .iter() + .find(|s| s.tag == "main") + .ok_or(PageError::Validation("No main solution".into()))? + .source; + + let problem = problem::upsert_problem( + &mut connection, + problem::NewProblem { + id: problem_id.clone(), + name: metadata.names.name[0].value.clone(), + memory_limit_bytes: metadata.judging.testset[0] + .memory_limit + .value + .parse() + .unwrap(), + time_limit_ms: metadata.judging.testset[0] + .time_limit + .value + .parse() + .unwrap(), + checker_path: metadata.assets.checker.source.path.clone(), + checker_language: map_codeforces_language(&metadata.assets.checker.r#type)?, + validator_path: metadata.assets.validators.validator[0].source.path.clone(), + validator_language: map_codeforces_language( + &metadata.assets.validators.validator[0].source.r#type, + )?, + main_solution_path: main_solution.path.clone(), + main_solution_language: map_codeforces_language(&main_solution.r#type)?, + test_pattern: metadata.judging.testset[0].input_path_pattern.value.clone(), + test_count: metadata.judging.testset[0] + .test_count + .value + .parse() + .unwrap(), + status: "compiled".into(), + creation_instant: Local::now().naive_utc(), + creation_user_id: logged_user.id, + }, + )?; + + for (i, test) in metadata.judging.testset[0].tests.test.iter().enumerate() { + let i = i + 1; + let test_path = format!( + "./{}/{}", + problem_id, + import_contest::format_width(&problem.test_pattern, i) + ); + + info!( + "Iterating through test {} to {:#?}, which is {}", + i, + test_path, + test.method.as_ref().unwrap() + ); + if test.method.as_ref().unwrap() == "manual" { + let test_name = PathBuf::from(&name) + .join(import_contest::format_width(&problem.test_pattern, i)); + info!("Extracting {:#?} from zip", test_name); + std::io::copy( + &mut zip.by_name(&test_name.to_str().unwrap())?, + &mut File::create(PathBuf::from("/data/").join(&test_path))?, + )?; + } else { + let cmd: Vec<_> = test.cmd.as_ref().unwrap().split(" ").collect(); + let run_stats = language::run_cached( + &job_sender, + &job_result_sender, + &"cpp.17.g++".into(), + format!("./{}/files/{}.cpp", problem.id, cmd.get(0).unwrap()), + cmd[1..].iter().map(|s| s.clone().into()).collect(), + None, + Some(test_path.clone()), + problem.memory_limit_bytes / 1_024, + 10_000, + ) + .await + .map_err(|_| { + PageError::Validation("Couldn't use an intermediate program".into()) + })?; + + if run_stats.result != i32::from(job_result::run_cached::Result::Ok) { + return Err(PageError::Validation( + "Couldn't run an intermediate program".into(), + )); + } + } + + let run_stats = language::run_cached( + &job_sender, + &job_result_sender, + &problem.main_solution_language, + format!("./{}/{}", problem.id, problem.main_solution_path), + vec![], + Some(test_path.clone()), + Some(format!("{}.a", test_path)), + problem.memory_limit_bytes / 1_024, + problem.time_limit_ms, + ) + .await + .map_err(|_| PageError::Validation("Couldn't run solution on test".into()))?; + if run_stats.exit_code != 0 { + return Err(PageError::Validation( + "Couldn't run solution on test".into(), + )); + } + } + + language::judge( + &job_sender, + &job_result_sender, + &problem.main_solution_language, + fs::read_to_string(PathBuf::from(format!( + "/data/{}/{}", + problem.id, problem.main_solution_path + )))?, + problem.test_count, + format!("./{}/{}", problem.id, problem.test_pattern).into(), + problem.checker_language, + format!("./{}/{}", problem.id, problem.checker_path).into(), + problem.memory_limit_bytes / 1_024, + problem.time_limit_ms, + ) + .await + .map_err(|_| PageError::Validation("Couldn't judge main solution".into()))?; + + if form.name.as_ref().unwrap() != "" { + contest::relate_problem( + &mut connection, + contest::NewContestProblems { + label: problem_label + .get(&problem_id_without_revision) + .ok_or(PageError::Validation( + "Arquivo não contém problemas listados".into(), + ))? + .to_string() + .to_uppercase(), + contest_id: contest.as_ref().unwrap().id, + problem_id, + }, + )?; + } + } + + if form.name.as_ref().unwrap() != "" { + Ok(redirect_to_referer( + "Competição criada com sucesso".into(), + &request, + )) + } else { + Ok(redirect_to_referer( + "Problemas adicionados com sucesso".into(), + &request, + )) + } +}