Compare commits

..

9 commits

Author SHA1 Message Date
4f307e323d
fix(config): avoid doubling slug in default_filename for single-problem mode
Problem: Kattis single-problem mode sets both `contest_id` and
`problem_id` to the same slug, causing `default_filename` to
concatenate them (e.g. `addtwonumbersaddtwonumbers.cc`).

Solution: only concatenate when `problem_id ~= contest_id`; otherwise
return just `problem_id` (or `contest_id` if nil).
2026-03-07 02:32:52 -05:00
cf175a80c1
ci: format 2026-03-07 02:23:28 -05:00
3c86af869f
fix(submit): remove premature Submitting log before scraper starts 2026-03-07 02:21:00 -05:00
e6441ad630
fix(credentials): try cached credentials before prompting on login
Problem: `:CP <platform> login` always prompted for username and
password, even when valid credentials were already cached.

Solution: if cached credentials exist, attempt `scraper.login()`
with them first. On success, return immediately with no prompt. On
failure, fall through to `prompt_and_login()` for fresh input.
2026-03-07 02:20:54 -05:00
ffd40564bc
fix(usaco): add proactive cookie validation to login and submit
Problem: `login()` always ran a fresh web login even with valid
cached cookies. `submit()` only checked cookie existence, not
validity, relying solely on a reactive retry after auth failure.
Auth redirect detection used `page_r.url.path` which could miss
non-path login redirects.

Solution: `login()` and `submit()` now load cookies and call
`_check_usaco_login()` upfront; re-login only if the check fails.
Auth detection in `_do_submit()` uses `str(page_r.url)` for a more
robust redirect match.
2026-03-07 02:20:20 -05:00
a47decf81b
fix(kattis): emit checking_login at submit start for consistency 2026-03-07 02:19:54 -05:00
923dc7293f
fix(kattis): harden reactive re-auth trigger on submit
Problem: stale cookie detection only matched the exact text
`"Request validation failed"`, missing cases where Kattis returns
a 400 or 403 status instead.

Solution: also trigger re-login when `r.status_code in (400, 403)`.
2026-03-07 02:15:42 -05:00
397576ad93
fix(cses): add token fast path to login and improve error surfacing
Problem: `login()` always ran the full web login flow even with a
valid cached token, prompting the user unnecessarily. Submit errors
only checked `message`, missing `error` field.

Solution: check the cached token via `_check_token` at the start of
`login()`; return immediately if valid. Error body now checks
`body.get("error") or body.get("message")` before falling back to
raw text.
2026-03-07 02:14:54 -05:00
65d119cdfc
fix: remove bag files 2026-03-07 02:14:18 -05:00
7 changed files with 90 additions and 50 deletions

View file

@ -605,10 +605,10 @@ end
---@param problem_id? string ---@param problem_id? string
---@return string ---@return string
local function default_filename(contest_id, problem_id) local function default_filename(contest_id, problem_id)
if problem_id then if problem_id and problem_id ~= contest_id then
return (contest_id .. problem_id):lower() return (contest_id .. problem_id):lower()
end end
return contest_id:lower() return (problem_id or contest_id):lower()
end end
M.default_filename = default_filename M.default_filename = default_filename

View file

@ -11,19 +11,9 @@ local STATUS_MESSAGES = {
installing_browser = 'Installing browser...', installing_browser = 'Installing browser...',
} }
---@param platform string? ---@param platform string
function M.login(platform) ---@param display string
platform = platform or state.get_platform() local function prompt_and_login(platform, display)
if not platform then
logger.log(
'No platform specified. Usage: :CP <platform> login',
{ level = vim.log.levels.ERROR }
)
return
end
local display = constants.PLATFORM_DISPLAY_NAMES[platform] or platform
vim.ui.input({ prompt = display .. ' username: ' }, function(username) vim.ui.input({ prompt = display .. ' username: ' }, function(username)
if not username or username == '' then if not username or username == '' then
logger.log('Cancelled', { level = vim.log.levels.WARN }) logger.log('Cancelled', { level = vim.log.levels.WARN })
@ -37,15 +27,7 @@ function M.login(platform)
return return
end end
cache.load() local credentials = { username = username, password = password }
local existing = cache.get_credentials(platform) or {}
local credentials = {
username = username,
password = password,
}
if existing.token then
credentials.token = existing.token
end
local scraper = require('cp.scraper') local scraper = require('cp.scraper')
scraper.login(platform, credentials, function(ev) scraper.login(platform, credentials, function(ev)
@ -69,6 +51,47 @@ function M.login(platform)
end) end)
end end
---@param platform string?
function M.login(platform)
platform = platform or state.get_platform()
if not platform then
logger.log(
'No platform specified. Usage: :CP <platform> login',
{ level = vim.log.levels.ERROR }
)
return
end
local display = constants.PLATFORM_DISPLAY_NAMES[platform] or platform
cache.load()
local existing = cache.get_credentials(platform) or {}
if existing.username and existing.password then
local scraper = require('cp.scraper')
scraper.login(platform, existing, function(ev)
vim.schedule(function()
local msg = STATUS_MESSAGES[ev.status] or ev.status
logger.log(display .. ': ' .. msg, { level = vim.log.levels.INFO, override = true })
end)
end, function(result)
vim.schedule(function()
if result.success then
logger.log(
display .. ' login successful',
{ level = vim.log.levels.INFO, override = true }
)
else
prompt_and_login(platform, display)
end
end)
end)
return
end
prompt_and_login(platform, display)
end
---@param platform string? ---@param platform string?
function M.logout(platform) function M.logout(platform)
platform = platform or state.get_platform() platform = platform or state.get_platform()

View file

@ -79,7 +79,6 @@ function M.submit(opts)
prompt_credentials(platform, function(creds) prompt_credentials(platform, function(creds)
vim.cmd.update() vim.cmd.update()
logger.log('Submitting...', { level = vim.log.levels.INFO, override = true })
require('cp.scraper').submit( require('cp.scraper').submit(
platform, platform,

View file

@ -248,7 +248,21 @@ class CSESScraper(BaseScraper):
if not username or not password: if not username or not password:
return self._login_error("Missing username or password") return self._login_error("Missing username or password")
token = credentials.get("token")
async with httpx.AsyncClient(follow_redirects=True) as client: async with httpx.AsyncClient(follow_redirects=True) as client:
if token:
print(json.dumps({"status": "checking_login"}), flush=True)
if await self._check_token(client, token):
return LoginResult(
success=True,
error="",
credentials={
"username": username,
"password": password,
"token": token,
},
)
print(json.dumps({"status": "logging_in"}), flush=True) print(json.dumps({"status": "logging_in"}), flush=True)
token = await self._web_login(client, username, password) token = await self._web_login(client, username, password)
if not token: if not token:
@ -460,7 +474,8 @@ class CSESScraper(BaseScraper):
if r.status_code not in range(200, 300): if r.status_code not in range(200, 300):
try: try:
err = r.json().get("message", r.text) body = r.json()
err = body.get("error") or body.get("message") or r.text
except Exception: except Exception:
err = r.text err = r.text
return self._submit_error(f"Submit request failed: {err}") return self._submit_error(f"Submit request failed: {err}")

View file

@ -329,6 +329,7 @@ class KattisScraper(BaseScraper):
return self._submit_error("Missing credentials. Use :CP kattis login") return self._submit_error("Missing credentials. Use :CP kattis login")
async with httpx.AsyncClient(follow_redirects=True) as client: async with httpx.AsyncClient(follow_redirects=True) as client:
print(json.dumps({"status": "checking_login"}), flush=True)
await _load_kattis_cookies(client) await _load_kattis_cookies(client)
if not client.cookies: if not client.cookies:
print(json.dumps({"status": "logging_in"}), flush=True) print(json.dumps({"status": "logging_in"}), flush=True)
@ -366,7 +367,7 @@ class KattisScraper(BaseScraper):
except Exception as e: except Exception as e:
return self._submit_error(f"Submit request failed: {e}") return self._submit_error(f"Submit request failed: {e}")
if r.text == "Request validation failed": if r.status_code in (400, 403) or r.text == "Request validation failed":
_COOKIE_PATH.unlink(missing_ok=True) _COOKIE_PATH.unlink(missing_ok=True)
print(json.dumps({"status": "logging_in"}), flush=True) print(json.dumps({"status": "logging_in"}), flush=True)
ok = await _do_kattis_login(client, username, password) ok = await _do_kattis_login(client, username, password)

View file

@ -429,7 +429,19 @@ class USACOScraper(BaseScraper):
async with httpx.AsyncClient(follow_redirects=True) as client: async with httpx.AsyncClient(follow_redirects=True) as client:
await _load_usaco_cookies(client) await _load_usaco_cookies(client)
if not client.cookies: if client.cookies:
print(json.dumps({"status": "checking_login"}), flush=True)
if not await _check_usaco_login(client, username):
client.cookies.clear()
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)
else:
print(json.dumps({"status": "logging_in"}), flush=True) print(json.dumps({"status": "logging_in"}), flush=True)
try: try:
ok = await _do_usaco_login(client, username, password) ok = await _do_usaco_login(client, username, password)
@ -470,7 +482,8 @@ class USACOScraper(BaseScraper):
headers=HEADERS, headers=HEADERS,
timeout=HTTP_TIMEOUT, timeout=HTTP_TIMEOUT,
) )
if "login" in page_r.url.path.lower() or "Login" in page_r.text[:2000]: page_url = str(page_r.url)
if "/login" in page_url or "Login" in page_r.text[:2000]:
return self._submit_error("auth_failure") return self._submit_error("auth_failure")
form_url, hidden_fields, lang_val = _parse_submit_form( form_url, hidden_fields, lang_val = _parse_submit_form(
page_r.text, language_id page_r.text, language_id
@ -513,6 +526,16 @@ class USACOScraper(BaseScraper):
return self._login_error("Missing username or password") return self._login_error("Missing username or password")
async with httpx.AsyncClient(follow_redirects=True) as client: 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) print(json.dumps({"status": "logging_in"}), flush=True)
try: try:
ok = await _do_usaco_login(client, username, password) ok = await _do_usaco_login(client, username, password)

View file

@ -1,21 +0,0 @@
vim.opt.runtimepath:prepend(vim.fn.expand('~/dev/cp.nvim'))
vim.opt.runtimepath:prepend(vim.fn.expand('~/dev/fzf-lua'))
vim.g.cp = {
languages = {
cpp = {
extension = 'cc',
commands = {
build = { 'g++', '-std=c++23', '-O2', '{source}', '-o', '{binary}' },
run = { '{binary}' },
},
},
},
platforms = {
codechef = {
enabled_languages = { 'cpp' },
default_language = 'cpp',
},
},
ui = { picker = 'fzf-lua' },
}