From 543480a4fe0b5a4e2c38f3baedbd935797113dcd Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Fri, 6 Mar 2026 00:09:16 -0500 Subject: [PATCH] feat: implement login and submit for USACO, Kattis, and CodeChef (#325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Login and submit were stub-only for USACO, Kattis, and CodeChef, leaving three platforms without a working solve loop. ## Solution Three commits, one per platform: **USACO** — httpx-based login via `login-session.php` with cookie cache. Submit fetches the problem page and parses the form dynamically (action URL, hidden fields, language select) before POSTing multipart with `sub_file[]`. **Kattis** — httpx-based login via `/login/email` (official Kattis CLI API). Submit is a multipart POST to `/submit`; the `contest` field is included only when `contest_id != problem_id`. `scrape_contest_list` URL updated to filter `kattis_original=on&kattis_recycled=off&user_created=off`. **CodeChef** — StealthySession browser-based login and submit following the AtCoder/CF pattern. Login checks `__NEXT_DATA__` for current user and fills the email/password form. Submit navigates to `/{contest_id}/submit/{problem_id}`, selects language (standard `', + body, + re.DOTALL | re.IGNORECASE, + ): + name, sel_body = sel_m.group(1), sel_m.group(2) + if "lang" in name.lower(): + lang_val = _pick_lang_option(sel_body, language_id) + break + break + return form_action, hidden, lang_val + + +async def _load_usaco_cookies(client: httpx.AsyncClient) -> None: + if not _COOKIE_PATH.exists(): + return + try: + for k, v in json.loads(_COOKIE_PATH.read_text()).items(): + client.cookies.set(k, v) + except Exception: + pass + + +async def _save_usaco_cookies(client: httpx.AsyncClient) -> None: + cookies = {k: v for k, v in client.cookies.items()} + if cookies: + _COOKIE_PATH.parent.mkdir(parents=True, exist_ok=True) + _COOKIE_PATH.write_text(json.dumps(cookies)) + + +async def _check_usaco_login(client: httpx.AsyncClient, username: str) -> bool: + try: + r = await client.get( + f"{_AUTH_BASE}/index.php", + headers=HEADERS, + timeout=HTTP_TIMEOUT, + ) + text = r.text.lower() + return username.lower() in text or "logout" in text + except Exception: + return False + + +async def _do_usaco_login( + client: httpx.AsyncClient, username: str, password: str +) -> bool: + r = await client.post( + f"{_AUTH_BASE}{_LOGIN_PATH}", + data={"user": username, "password": password}, + headers=HEADERS, + timeout=HTTP_TIMEOUT, + ) + r.raise_for_status() + try: + data = r.json() + return bool(data.get("success") or data.get("status") == "success") + except Exception: + return r.status_code == 200 and "error" not in r.text.lower() + + class USACOScraper(BaseScraper): @property def platform_name(self) -> str: @@ -293,15 +409,99 @@ class USACOScraper(BaseScraper): language_id: str, credentials: dict[str, str], ) -> SubmitResult: - return SubmitResult( - success=False, - error="USACO submit not yet implemented", - submission_id="", - verdict="", - ) + source = Path(file_path).read_bytes() + username = credentials.get("username", "") + password = credentials.get("password", "") + if not username or not password: + return self._submit_error("Missing credentials. Use :CP usaco login") + + async with httpx.AsyncClient(follow_redirects=True) as client: + await _load_usaco_cookies(client) + print(json.dumps({"status": "checking_login"}), flush=True) + logged_in = bool(client.cookies) and await _check_usaco_login( + client, username + ) + if not logged_in: + print(json.dumps({"status": "logging_in"}), flush=True) + try: + ok = await _do_usaco_login(client, username, password) + except Exception as e: + return self._submit_error(f"Login failed: {e}") + if not ok: + return self._submit_error("Login failed (bad credentials?)") + await _save_usaco_cookies(client) + + print(json.dumps({"status": "submitting"}), flush=True) + try: + page_r = await client.get( + f"{_AUTH_BASE}/index.php?page=viewproblem2&cpid={problem_id}", + headers=HEADERS, + timeout=HTTP_TIMEOUT, + ) + form_url, hidden_fields, lang_val = _parse_submit_form( + page_r.text, language_id + ) + except Exception: + form_url = _AUTH_BASE + _SUBMIT_PATH + hidden_fields = {} + lang_val = None + + data: dict[str, str] = {"cpid": problem_id, **hidden_fields} + data["language"] = lang_val if lang_val is not None else language_id + ext = "py" if "python" in language_id.lower() else "cpp" + try: + r = await client.post( + form_url, + data=data, + files={"sub_file[]": (f"solution.{ext}", source, "text/plain")}, + headers=HEADERS, + timeout=HTTP_TIMEOUT, + ) + r.raise_for_status() + except Exception as e: + return self._submit_error(f"Submit request failed: {e}") + + try: + resp = r.json() + sid = str(resp.get("submission_id", resp.get("id", ""))) + except Exception: + sid = "" + return SubmitResult( + success=True, error="", submission_id=sid, verdict="submitted" + ) async def login(self, credentials: dict[str, str]) -> LoginResult: - return self._login_error("USACO login not yet implemented") + username = credentials.get("username", "") + password = credentials.get("password", "") + if not username or not password: + return self._login_error("Missing username or password") + + async with httpx.AsyncClient(follow_redirects=True) as client: + await _load_usaco_cookies(client) + if client.cookies: + print(json.dumps({"status": "checking_login"}), flush=True) + if await _check_usaco_login(client, username): + return LoginResult( + success=True, + error="", + credentials={"username": username, "password": password}, + ) + + print(json.dumps({"status": "logging_in"}), flush=True) + try: + ok = await _do_usaco_login(client, username, password) + except Exception as e: + return self._login_error(f"Login request failed: {e}") + + if not ok: + return self._login_error("Login failed (bad credentials?)") + + await _save_usaco_cookies(client) + return LoginResult( + success=True, + error="", + credentials={"username": username, "password": password}, + ) if __name__ == "__main__":