Commit graph

71 commits

Author SHA1 Message Date
f420eadff2
ci: format 2026-03-07 17:55:57 -05:00
0ad7a9614f
fix(scrapers): bad credentials detection and error message cleanup
Problem: Wrong credentials during login produced two bugs: scrapers
wrapped the `bad_credentials` error code in `"Login failed: ..."`,
causing double-prefixed messages in the UI; and `credentials.lua`
did not clear cached credentials before re-prompting or on failure,
leaving stale bad creds in the cache.

Solution: Standardize all scrapers to emit `"bad_credentials"` as
the raw error code. Add `LOGIN_ERRORS` map in `constants.lua` to
translate it to a human-readable string in both `credentials.lua`
and `submit.lua`. Fix `credentials.lua` to clear credentials on
failure in both the fresh-prompt and cached-creds-fail paths.
For AtCoder and Codeforces, replace `wait_for_url` with
`wait_for_function` to detect the login error element immediately
rather than waiting the full 10s navigation timeout. Also add
"Remember me" checkbox check on Codeforces login.
2026-03-07 17:35:20 -05:00
Barrett Ruth
573b335646
feat: file conflict prompt, empty submit guard, and lint fixes (#366)
## Problem

Loading a problem whose source file already exists silently overwrites
user code. Submitting an empty file sends a blank submission to the
platform. Two ruff lint violations existed in the scrapers.

## Solution

- `setup.lua`: when the target source file exists on the filesystem
(`vim.uv.fs_stat`), show an inline `Overwrite? [y/N]:` prompt. Declining
keeps the existing file open and registers state normally. Skipped when
the file is already loaded in a buffer.
- `submit.lua`: resolve path to absolute, use `vim.uv.fs_stat` to verify
existence, abort with WARN if `stat.size == 0` ("Submit aborted: source
file has no content").
- `scrapers/atcoder.py`: remove unused `pathlib.Path` import (F401).
- `scrapers/base.py`: move local imports to top of file (E402).

Closes #364, #365.
2026-03-07 16:30:51 -05:00
Barrett Ruth
b7ddf4c253
fix(scrapers): cookie fast paths, centralized storage, and reauth hardening (#363)
## Problem

Scraper cookie handling was fragmented across per-platform files with no
shared access, httpx scrapers lacked `checking_login` fast paths on
login, and several re-auth edge cases (CodeChef submit, CF cookie guard,
AtCoder cookie persistence) caused unnecessary full re-logins or silent
failures.

## Solution

Centralize all cookie storage into a single `cookies.json` via helpers
in `base.py`. Add `checking_login` fast paths to `kattis.py` (using the
`x-username` response header as a session probe), `usaco.py`, and
`cses.py` login flows. Fix `kattis.py` submit to emit `checking_login`
only after loading cookies. Remove AtCoder cookie persistence from login
entirely — always do a fresh session. Harden CodeChef and CF reauth
with consistent status logging and cookie guard checks.
2026-03-07 16:10:51 -05:00
Barrett Ruth
592f977296
fix(login): remove cookie fast-path from login subcommand (#344)
## Problem

`:CP <platform> login` short-circuited on cached cookies/tokens — if an
old session was still valid, the new credentials were never tested. The
user got "login successful" even with wrong input. USACO submit also
wasted a roundtrip on `_check_usaco_login` every time.

## Solution

Always validate credentials in the login path. Remove cookie/token
loading from `_login_headless` (AtCoder), `_login_headless_cf` (CF),
`_login_headless_codechef` (CodeChef), and `login` (CSES). For USACO
submit, replace the `_check_usaco_login` roundtrip with cookie trust +
retry-on-auth-failure (the Kattis pattern). Submit paths are unchanged —
cookie fast-paths remain for contest speed.

Closes #331
2026-03-06 17:53:22 -05:00
Barrett Ruth
0b40e0f33e
fix: replace curl_cffi with scrapling in codeforces metadata (#334)
## Problem

`codeforces.py` used `curl_cffi` to bypass Cloudflare when fetching
contest problem HTML, making it unavailable in the nix python env and
requiring an extra dependency across `pyproject.toml` and `flake.nix`.

## Solution

Rewrite `_fetch_problems_html` to use scrapling `StealthySession` with
`solve_cloudflare=True`, matching the existing CF submit pattern. Extend
`needs_browser` in `scraper.lua` to route CF `metadata` and `tests`
through the FHS env on NixOS. Remove `curl-cffi` from `pyproject.toml`,
`flake.nix`, and test mocks.
2026-03-06 13:25:44 -05:00
Barrett Ruth
7bf4cf2538
feat(commands): implement :CP open [problem|contest|standings] (#319)
## Problem

There was no way to open a problem, contest, or standings page in the
browser from within the plugin.

## Solution

Add `contest_url` and `standings_url` to `MetadataResult` and persist
them in the cache. Add `cache.get_open_urls` to resolve all three URLs.
Wire up `:CP open [problem|contest|standings]` in `commands/init.lua`
to call `vim.ui.open`, warning when a URL is unavailable (e.g. CSES
has no standings). Closes #315.
2026-03-05 19:16:05 -05:00
Barrett Ruth
b959f29bd4
fix(scrapers): harden CSES and CF submit edge cases (#295) (#318)
## Problem

CSES `_web_login` used bare dict indexing on the API response, raising
an opaque `KeyError` on missing fields. `_check_token` swallowed all
exceptions as `False`, conflating transient network errors with invalid
tokens. CF persisted cookies unconditionally and silently swallowed
`_solve_turnstile` failures in `submit_action`.

## Solution

CSES API fields now use `.get()` with a descriptive `RuntimeError` on
absence. `_check_token` re-raises `httpx` network/timeout exceptions
so callers see real failures. CF cookie writes are guarded by an
`X-User-Handle` check (the CF auth cookie). `_solve_turnstile` errors
propagate to the outer error handler instead of being silenced.
2026-03-05 19:00:35 -05:00
Barrett Ruth
924601ce99
fix(codeforces): persist cookies after submit regardless of name (#312)
## Problem

The submit path in `codeforces.py` guarded `cookie_cache.write_text` on
the presence of a `JSESSIONID` cookie. Codeforces does not use
`JSESSIONID`, so the cookie file was never written after submit,
breaking the fast-path on subsequent submits.

## Solution

Replace the name-specific guard with a non-empty check (`if
browser_cookies:`), matching the unconditional save already used in the
login path.

Closes #301.
2026-03-05 15:31:29 -05:00
Barrett Ruth
2c119774df
feat: validate credentials on :CP <platform> login (#310)
## Problem

`:CP <platform> login` blindly caches username/password without
server-side
validation. Bad credentials are only discovered at submit time, which is
confusing and wastes a browser session.

## Solution

Wire `:CP <platform> login` through the scraper pipeline so each
platform
actually authenticates before persisting credentials. On failure, the
user
sees an error and nothing is cached.

- CSES: reuses `_check_token` (fast path) and `_web_login`; returns API
token
  in `LoginResult.credentials` so subsequent submits skip re-auth.
- AtCoder/Codeforces: new `_login_headless` functions open a
StealthySession,
solve Turnstile/Cloudflare, fill the login form, and validate success by
  checking for the logout link. Cookies only persist on confirmed login.
- CodeChef/Kattis/USACO: return "not yet implemented" errors.
- `scraper.lua`: generalizes submit-only guards (`needs_browser` flag)
to
  cover both `submit` and `login` subcommands.
- `credentials.lua`: prompts for username/password, passes cached token
for
CSES fast path, shows ndjson status notifications, only caches on
success.
2026-03-05 15:12:09 -05:00
Barrett Ruth
a202725cc5
fix(submit): use file path over stdin; fix CF CodeMirror textarea (#305)
## Problem

After the initial submit hardening, two issues remained: source code was
read in Lua and piped as stdin to the scraper (unnecessary roundtrip
since
the file exists on disk), and CF's `page.fill()` timed out on the hidden
`textarea[name="source"]` because CodeMirror owns the editor state.

## Solution

Pass the source file path as a CLI arg instead — AtCoder calls
`page.set_input_files(file_path)` directly, CF reads it with
`Path(file_path).read_text()`. Fix CF source injection via
`page.evaluate()`
into the CodeMirror instance. Extract `BROWSER_SUBMIT_NAV_TIMEOUT` as a
per-platform `defaultdict` (CF defaults to 2× nav timeout). Save the
buffer
with `vim.cmd.update()` before submitting.
2026-03-05 14:34:14 -05:00
Barrett Ruth
127089c57f
fix(submit): harden atcoder and codeforces submit flow (#304)
## Problem

AtCoder file upload always wrote a `.cpp` temp file regardless of
language. CF submit used `solve_cloudflare=True` on the submit page,
causing a spurious "No Cloudflare challenge found" error;
`_wait_for_gate_reload` in `login_action` was dead code. Stale cookies
caused silent auth failures with no recovery path. The `uv.spawn` ndjson
path for submit had no overall timeout.

## Solution

Replace AtCoder's temp file with `page.set_input_files` using an
in-memory buffer and correct extension via `_LANGUAGE_ID_EXTENSION`.
Replace CF's temp-file/fallback dance with a direct
`textarea[name="source"]` fill and set `solve_cloudflare=False` on the
submit fetch. Add a login fast-path that skips the homepage check when
cookies exist, with automatic stale-cookie recovery via `_retried` flag
on redirect-to-login detection. Remove `_wait_for_gate_reload`. Fix
`_ensure_browser` to propagate install errors. Add a 120s kill timer to
the ndjson `uv.spawn` submit path in `scraper.lua`.
2026-03-05 11:18:34 -05:00
Barrett Ruth
6fcb5d1bbc
feat(codeforces): implement submit; cache CSES token (#300)
## Problem

Codeforces submit was a stub. CSES submit re-ran the full login flow on
every invocation (~1.5s overhead).

## Solution

**Codeforces**: headless browser submit via StealthySession (same
pattern as AtCoder). Solves Cloudflare Turnstile on login, uploads
source via file input, caches cookies at
`~/.cache/cp-nvim/codeforces-cookies.json` so repeat submits skip login.

**CSES**: persist the API token in credentials via a `credentials`
ndjson event. Subsequent submits validate the cached token with a single
GET before falling back to full login.

Also includes a vimdoc table of contents.
2026-03-05 10:37:39 -05:00
bad219e578 ci: format 2026-03-03 15:09:41 -05:00
90bd13580b feat(scraper): add precision extraction, start_time, and submit support
Problem: problem pages contain floating-point precision requirements and
contest start timestamps that were not being extracted or stored. The
submit workflow also needed a foundation in the scraper layer.

Solution: add extract_precision() to base.py and propagate through all
scrapers into cache. Add start_time to ContestSummary and extract it
from AtCoder and Codeforces. Add SubmitResult model, abstract submit()
method, submit CLI case with get_language_id() resolution, stdin/env_extra
support in run_scraper, and a full AtCoder submit implementation; stub
the remaining platforms.
2026-03-03 15:09:41 -05:00
b6f3398bbc fix(ci): formatting and typing 2026-02-18 14:13:37 -05:00
1162e7046b try to fix the setup 2026-02-18 14:13:37 -05:00
89c1a3c683 fix(ci): more fixes 2026-01-27 15:56:34 -05:00
5293515aca feat(scrapers): refactor 2026-01-27 14:44:08 -05:00
dfd8275421 fix: use a diff scraper for now 2025-12-08 19:46:14 -06:00
Barrett Ruth
aab211902e feat: multi-test case view 2025-11-04 21:32:40 -05:00
Barrett Ruth
fef73887e4 feat(io): multi-test case view 2025-11-04 08:15:08 -05:00
Barrett Ruth
3654748632 fix(scrapers): fix multi-test case codeforces running 2025-11-02 22:42:05 -05:00
7bfa839c84 fix(codeforces): correct problem url 2025-10-31 21:47:15 -04:00
352f98f26f fix: open problem-specific url 2025-10-15 11:00:31 -04:00
c0e175d84b feat(config): open url option 2025-10-12 16:19:02 -04:00
c509102b37 feat(tests): basic tests 2025-10-05 21:58:43 -04:00
ee88450b3b feat(scrapers): make scrapers softer 2025-10-05 13:40:56 -04:00
b9a2c7a4ff fix(scrapers): fix 2025-10-04 15:00:37 -04:00
179b333505 update pyproject 2025-10-03 22:38:24 -04:00
b8c79401da fix(scrapers/codeforces): suppress scrapling logs 2025-10-03 21:14:28 -04:00
f48acb4672 fix(scrapers/codeforces): scrape time 2025-10-03 21:06:20 -04:00
4498c4a7fa fix scrapers 2025-10-03 19:19:02 -04:00
67c23c4d69 better scraper config 2025-09-30 22:33:36 -04:00
49ba922ff7 fix(scraper): use scrapling 2025-09-30 20:16:59 -04:00
ae2f8b94cf feat: interactive problem finer-tuning 2025-09-27 10:05:58 -04:00
53562eb6a8 fix(scrapers): reorg codeforces scraper 2025-09-22 22:48:24 -04:00
db391da52c feat(scrapers): total refactor 2025-09-22 22:00:20 -04:00
eb3f7762de fix(ci): typing 2025-09-22 20:46:27 -04:00
afb15150af fix(ci): format 2025-09-21 15:11:10 -04:00
78fb4f8f4b feat(cache): cache clearing, updating and resetting 2025-09-21 15:08:55 -04:00
9761cded88 fix(scrapers): dont limit results to 100 contests 2025-09-21 14:23:48 -04:00
fe158aa65f fix(qol): remove ai-like comments 2025-09-21 14:00:38 -04:00
0dd145b71e feat(doc): make docs more concise 2025-09-21 12:06:45 -04:00
1f38dba57f fix(scrape): proper vars 2025-09-21 00:31:10 -04:00
9deedec15a fix(scraper): comments 2025-09-21 00:10:10 -04:00
7b8aae7921 fix(ci): move imports 2025-09-20 23:52:32 -04:00
8e13b8c61d feat(cses): update cses with concept of a category 2025-09-20 14:01:18 -04:00
ff9a3d1abb fix(ci): run as modukle 2025-09-19 21:20:31 -04:00
ddff996ee2 fix(ci): test 2025-09-19 21:11:12 -04:00