#include "csp/controllers/submission_controller.h" #include "csp/app_state.h" #include "csp/domain/enum_strings.h" #include "csp/domain/json.h" #include "csp/services/contest_service.h" #include "csp/services/problem_service.h" #include "csp/services/solution_access_service.h" #include "csp/services/submission_feedback_runner.h" #include "csp/services/submission_feedback_service.h" #include "csp/services/submission_service.h" #include "http_auth.h" #include #include #include #include #include #include namespace csp::controllers { namespace { drogon::HttpResponsePtr JsonError(drogon::HttpStatusCode code, const std::string& msg) { Json::Value j; j["ok"] = false; j["error"] = msg; auto resp = drogon::HttpResponse::newHttpJsonResponse(j); resp->setStatusCode(code); return resp; } drogon::HttpResponsePtr JsonOk(const Json::Value& data) { Json::Value j; j["ok"] = true; j["data"] = data; auto resp = drogon::HttpResponse::newHttpJsonResponse(j); resp->setStatusCode(drogon::k200OK); return resp; } int ParseClampedInt(const std::string& s, int default_value, int min_value, int max_value) { if (s.empty()) return default_value; const int v = std::stoi(s); return std::max(min_value, std::min(max_value, v)); } std::optional ParseOptionalInt64(const std::string& s) { if (s.empty()) return std::nullopt; return std::stoll(s); } bool ParseBoolLike(const std::string& s, bool default_value) { if (s.empty()) return default_value; std::string v = s; std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (v == "1" || v == "true" || v == "yes" || v == "on") return true; if (v == "0" || v == "false" || v == "no" || v == "off") return false; return default_value; } Json::Value ParseLinksArray(const std::string& links_json) { Json::CharReaderBuilder builder; Json::Value parsed; std::string errs; std::unique_ptr reader(builder.newCharReader()); if (!reader->parse(links_json.data(), links_json.data() + links_json.size(), &parsed, &errs) || !parsed.isArray()) { return Json::Value(Json::arrayValue); } return parsed; } } // namespace void SubmissionController::submitProblem( const drogon::HttpRequestPtr& req, std::function&& cb, int64_t problem_id) { try { std::string auth_error; const auto user_id = GetAuthedUserId(req, auth_error); if (!user_id.has_value()) { cb(JsonError(drogon::k401Unauthorized, auth_error)); return; } const auto json = req->getJsonObject(); if (!json) { cb(JsonError(drogon::k400BadRequest, "body must be json")); return; } services::SubmissionCreateRequest create; create.user_id = *user_id; create.problem_id = problem_id; create.language = (*json).get("language", "cpp").asString(); create.code = (*json).get("code", "").asString(); if ((*json).isMember("contest_id") && !(*json)["contest_id"].isNull()) { create.contest_id = (*json)["contest_id"].asInt64(); services::ContestService contest(csp::AppState::Instance().db()); if (!contest.GetContest(*create.contest_id).has_value()) { cb(JsonError(drogon::k400BadRequest, "contest not found")); return; } if (!contest.ContainsProblem(*create.contest_id, problem_id)) { cb(JsonError(drogon::k400BadRequest, "problem not in contest")); return; } if (!contest.IsRegistered(*create.contest_id, *user_id)) { cb(JsonError(drogon::k403Forbidden, "user is not registered for contest")); return; } if (!contest.IsRunning(*create.contest_id)) { cb(JsonError(drogon::k403Forbidden, "contest is not running")); return; } } services::SubmissionService svc(csp::AppState::Instance().db()); auto s = svc.CreateAndJudge(create); // Auto-enqueue LLM feedback generation in background. services::SubmissionFeedbackRunner::Instance().Enqueue(s.id); cb(JsonOk(domain::ToJson(s))); } catch (const std::invalid_argument&) { cb(JsonError(drogon::k400BadRequest, "invalid numeric field")); } catch (const std::out_of_range&) { cb(JsonError(drogon::k400BadRequest, "numeric field out of range")); } catch (const std::runtime_error& e) { cb(JsonError(drogon::k400BadRequest, e.what())); } catch (const std::exception& e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } void SubmissionController::listSubmissions( const drogon::HttpRequestPtr& req, std::function&& cb) { try { const auto user_id = ParseOptionalInt64(req->getParameter("user_id")); const auto problem_id = ParseOptionalInt64(req->getParameter("problem_id")); const auto contest_id = ParseOptionalInt64(req->getParameter("contest_id")); const int page = ParseClampedInt(req->getParameter("page"), 1, 1, 100000); const int page_size = ParseClampedInt(req->getParameter("page_size"), 20, 1, 200); services::SubmissionService svc(csp::AppState::Instance().db()); const auto rows = svc.List(user_id, problem_id, contest_id, page, page_size); Json::Value arr(Json::arrayValue); for (const auto& s : rows) arr.append(domain::ToJson(s)); Json::Value payload; payload["items"] = arr; payload["page"] = page; payload["page_size"] = page_size; cb(JsonOk(payload)); } catch (const std::invalid_argument&) { cb(JsonError(drogon::k400BadRequest, "invalid query parameter")); } catch (const std::out_of_range&) { cb(JsonError(drogon::k400BadRequest, "query parameter out of range")); } catch (const std::exception& e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } void SubmissionController::getSubmission( const drogon::HttpRequestPtr& /*req*/, std::function&& cb, int64_t submission_id) { try { services::SubmissionService svc(csp::AppState::Instance().db()); const auto s = svc.GetById(submission_id); if (!s.has_value()) { cb(JsonError(drogon::k404NotFound, "submission not found")); return; } Json::Value payload = domain::ToJson(*s); payload["code"] = s->code; // Attach problem title for frontend linking. { services::ProblemService psvc(csp::AppState::Instance().db()); if (const auto p = psvc.GetById(s->problem_id); p.has_value()) { payload["problem_title"] = p->title; } } services::SolutionAccessService access_svc(csp::AppState::Instance().db()); const auto stats = access_svc.QueryUserProblemViewStats(s->user_id, s->problem_id); payload["has_viewed_answer"] = stats.has_viewed; payload["answer_view_count"] = stats.total_views; payload["answer_view_total_cost"] = stats.total_cost; if (stats.last_viewed_at.has_value()) { payload["last_answer_view_at"] = Json::Int64(*stats.last_viewed_at); } else { payload["last_answer_view_at"] = Json::nullValue; } services::SubmissionFeedbackService feedback_svc(csp::AppState::Instance().db()); if (const auto feedback = feedback_svc.GetBySubmissionId(s->id); feedback.has_value()) { Json::Value analysis; analysis["feedback_md"] = feedback->feedback_md; analysis["links"] = ParseLinksArray(feedback->links_json); analysis["model_name"] = feedback->model_name; analysis["status"] = feedback->status; analysis["created_at"] = Json::Int64(feedback->created_at); analysis["updated_at"] = Json::Int64(feedback->updated_at); payload["analysis"] = analysis; } else { payload["analysis"] = Json::nullValue; } cb(JsonOk(payload)); } catch (const std::exception& e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } void SubmissionController::analyzeSubmission( const drogon::HttpRequestPtr& req, std::function&& cb, int64_t submission_id) { try { std::string auth_error; if (!GetAuthedUserId(req, auth_error).has_value()) { cb(JsonError(drogon::k401Unauthorized, auth_error)); return; } bool force_refresh = ParseBoolLike(req->getParameter("refresh"), false); if (const auto json = req->getJsonObject(); json && (*json).isMember("refresh")) { force_refresh = (*json)["refresh"].asBool(); } services::SubmissionService submission_svc(csp::AppState::Instance().db()); const auto submission = submission_svc.GetById(submission_id); if (!submission.has_value()) { cb(JsonError(drogon::k404NotFound, "submission not found")); return; } services::ProblemService problem_svc(csp::AppState::Instance().db()); const auto problem = problem_svc.GetById(submission->problem_id); if (!problem.has_value()) { cb(JsonError(drogon::k404NotFound, "problem not found")); return; } services::SubmissionFeedbackService feedback_svc(csp::AppState::Instance().db()); const auto feedback = feedback_svc.GenerateAndSave(*submission, *problem, force_refresh); Json::Value payload; payload["submission_id"] = Json::Int64(feedback.submission_id); payload["feedback_md"] = feedback.feedback_md; payload["links"] = ParseLinksArray(feedback.links_json); payload["model_name"] = feedback.model_name; payload["status"] = feedback.status; payload["created_at"] = Json::Int64(feedback.created_at); payload["updated_at"] = Json::Int64(feedback.updated_at); payload["refresh"] = force_refresh; cb(JsonOk(payload)); } catch (const std::invalid_argument&) { cb(JsonError(drogon::k400BadRequest, "invalid request field")); } catch (const std::runtime_error& e) { cb(JsonError(drogon::k400BadRequest, e.what())); } catch (const std::exception& e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } void SubmissionController::runCpp( const drogon::HttpRequestPtr& req, std::function&& cb) { try { const auto json = req->getJsonObject(); if (!json) { cb(JsonError(drogon::k400BadRequest, "body must be json")); return; } const std::string code = (*json).get("code", "").asString(); const std::string input = (*json).get("input", "").asString(); services::SubmissionService svc(csp::AppState::Instance().db()); const auto r = svc.RunOnlyCpp(code, input); Json::Value payload; payload["status"] = domain::ToString(r.status); payload["time_ms"] = r.time_ms; payload["stdout"] = r.stdout_text; payload["stderr"] = r.stderr_text; payload["compile_log"] = r.compile_log; cb(JsonOk(payload)); } catch (const std::runtime_error& e) { cb(JsonError(drogon::k400BadRequest, e.what())); } catch (const std::exception& e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } } // namespace csp::controllers