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.
This commit is contained in:
Barrett Ruth 2026-03-07 17:35:20 -05:00
parent 573b335646
commit 0ad7a9614f
Signed by: barrett
GPG key ID: A6C96C9349D2FC81
9 changed files with 55 additions and 25 deletions

View file

@ -221,4 +221,8 @@ M.DEFAULT_VERSIONS = { cpp = 'c++20', python = 'python3' }
M.COOKIE_FILE = vim.fn.expand('~/.cache/cp-nvim/cookies.json') M.COOKIE_FILE = vim.fn.expand('~/.cache/cp-nvim/cookies.json')
M.LOGIN_ERRORS = {
bad_credentials = 'bad credentials',
}
return M return M

View file

@ -11,6 +11,7 @@ local STATUS_MESSAGES = {
installing_browser = 'Installing browser...', installing_browser = 'Installing browser...',
} }
---@param platform string ---@param platform string
---@param display string ---@param display string
local function prompt_and_login(platform, display) local function prompt_and_login(platform, display)
@ -45,7 +46,11 @@ local function prompt_and_login(platform, display)
) )
else else
local err = result.error or 'unknown error' local err = result.error or 'unknown error'
logger.log(display .. ' login failed: ' .. err, { level = vim.log.levels.ERROR }) cache.clear_credentials(platform)
logger.log(
display .. ' login failed: ' .. (constants.LOGIN_ERRORS[err] or err),
{ level = vim.log.levels.ERROR }
)
end end
end) end)
end) end)
@ -83,6 +88,7 @@ function M.login(platform)
{ level = vim.log.levels.INFO, override = true } { level = vim.log.levels.INFO, override = true }
) )
else else
cache.clear_credentials(platform)
prompt_and_login(platform, display) prompt_and_login(platform, display)
end end
end) end)

View file

@ -110,10 +110,13 @@ function M.submit(opts)
logger.log('Submitted successfully', { level = vim.log.levels.INFO, override = true }) logger.log('Submitted successfully', { level = vim.log.levels.INFO, override = true })
else else
local err = result and result.error or 'unknown error' local err = result and result.error or 'unknown error'
if err:match('^Login failed') then if err == 'bad_credentials' or err:match('^Login failed') then
cache.clear_credentials(platform) cache.clear_credentials(platform)
end end
logger.log('Submit failed: ' .. err, { level = vim.log.levels.ERROR }) logger.log(
'Submit failed: ' .. (constants.LOGIN_ERRORS[err] or err),
{ level = vim.log.levels.ERROR }
)
end end
end) end)
end end

View file

@ -400,9 +400,13 @@ def _at_login_action(credentials: dict[str, str]):
page.fill('input[name="username"]', credentials.get("username", "")) page.fill('input[name="username"]', credentials.get("username", ""))
page.fill('input[name="password"]', credentials.get("password", "")) page.fill('input[name="password"]', credentials.get("password", ""))
page.click("#submit") page.click("#submit")
page.wait_for_url( page.wait_for_function(
lambda url: "/login" not in url, timeout=BROWSER_NAV_TIMEOUT "() => !window.location.href.includes('/login') || !!document.querySelector('.alert-danger')",
timeout=BROWSER_NAV_TIMEOUT,
) )
if "/login" in page.url:
login_error = "bad_credentials"
return
except Exception as e: except Exception as e:
login_error = str(e) login_error = str(e)
@ -460,7 +464,9 @@ def _login_headless(credentials: dict[str, str]) -> LoginResult:
solve_cloudflare=True, solve_cloudflare=True,
) )
login_error = get_error() login_error = get_error()
if login_error: if login_error == "bad_credentials":
return LoginResult(success=False, error="bad_credentials")
elif login_error:
return LoginResult(success=False, error=f"Login failed: {login_error}") return LoginResult(success=False, error=f"Login failed: {login_error}")
logged_in = False logged_in = False
@ -474,7 +480,7 @@ def _login_headless(credentials: dict[str, str]) -> LoginResult:
) )
if not logged_in: if not logged_in:
return LoginResult( return LoginResult(
success=False, error="Login failed (bad credentials?)" success=False, error="bad_credentials"
) )
try: try:
@ -570,7 +576,9 @@ def _submit_headless(
solve_cloudflare=True, solve_cloudflare=True,
) )
login_error = get_login_error() login_error = get_login_error()
if login_error: if login_error == "bad_credentials":
return SubmitResult(success=False, error="bad_credentials")
elif login_error:
return SubmitResult( return SubmitResult(
success=False, error=f"Login failed: {login_error}" success=False, error=f"Login failed: {login_error}"
) )

View file

@ -87,7 +87,7 @@ def _login_headless_codechef(credentials: dict[str, str]) -> LoginResult:
try: try:
page.wait_for_url(lambda url: "/login" not in url, timeout=3000) page.wait_for_url(lambda url: "/login" not in url, timeout=3000)
except Exception: except Exception:
login_error = "bad credentials?" login_error = "bad_credentials"
return return
except Exception as e: except Exception as e:
login_error = str(e) login_error = str(e)
@ -106,7 +106,7 @@ def _login_headless_codechef(credentials: dict[str, str]) -> LoginResult:
session.fetch(f"{BASE_URL}/", page_action=check_login, network_idle=True) session.fetch(f"{BASE_URL}/", page_action=check_login, network_idle=True)
if not logged_in: if not logged_in:
return LoginResult( return LoginResult(
success=False, error="Login failed (bad credentials?)" success=False, error="bad_credentials"
) )
try: try:
@ -166,7 +166,7 @@ def _submit_headless_codechef(
try: try:
page.wait_for_url(lambda url: "/login" not in url, timeout=3000) page.wait_for_url(lambda url: "/login" not in url, timeout=3000)
except Exception: except Exception:
login_error = "bad credentials?" login_error = "bad_credentials"
return return
except Exception as e: except Exception as e:
login_error = str(e) login_error = str(e)

View file

@ -353,10 +353,15 @@ def _cf_login_action(credentials: dict[str, str]):
page.wait_for_selector('input[name="handleOrEmail"]', timeout=60000) page.wait_for_selector('input[name="handleOrEmail"]', timeout=60000)
page.fill('input[name="handleOrEmail"]', credentials.get("username", "")) page.fill('input[name="handleOrEmail"]', credentials.get("username", ""))
page.fill('input[name="password"]', credentials.get("password", "")) page.fill('input[name="password"]', credentials.get("password", ""))
page.locator('#enterForm input[name="remember"]').check()
page.locator('#enterForm input[type="submit"]').click() page.locator('#enterForm input[type="submit"]').click()
page.wait_for_url( page.wait_for_function(
lambda url: "/enter" not in url, timeout=BROWSER_NAV_TIMEOUT "() => !window.location.href.includes('/enter') || !!document.querySelector('#enterForm span.error')",
timeout=BROWSER_NAV_TIMEOUT,
) )
if "/enter" in page.url:
login_error = "bad_credentials"
return
except Exception as e: except Exception as e:
login_error = str(e) login_error = str(e)
@ -416,7 +421,9 @@ def _login_headless_cf(credentials: dict[str, str]) -> LoginResult:
solve_cloudflare=True, solve_cloudflare=True,
) )
login_error = get_error() login_error = get_error()
if login_error: if login_error == "bad_credentials":
return LoginResult(success=False, error="bad_credentials")
elif login_error:
return LoginResult(success=False, error=f"Login failed: {login_error}") return LoginResult(success=False, error=f"Login failed: {login_error}")
logged_in = False logged_in = False
@ -428,7 +435,7 @@ def _login_headless_cf(credentials: dict[str, str]) -> LoginResult:
session.fetch(f"{BASE_URL}/", page_action=verify_action, network_idle=True) session.fetch(f"{BASE_URL}/", page_action=verify_action, network_idle=True)
if not logged_in: if not logged_in:
return LoginResult( return LoginResult(
success=False, error="Login failed (bad credentials?)" success=False, error="bad_credentials"
) )
try: try:
@ -540,7 +547,9 @@ def _submit_headless(
solve_cloudflare=True, solve_cloudflare=True,
) )
login_error = _get_login_error() login_error = _get_login_error()
if login_error: if login_error == "bad_credentials":
return SubmitResult(success=False, error="bad_credentials")
elif login_error:
return SubmitResult( return SubmitResult(
success=False, error=f"Login failed: {login_error}" success=False, error=f"Login failed: {login_error}"
) )

View file

@ -266,7 +266,7 @@ class CSESScraper(BaseScraper):
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:
return self._login_error("Login failed (bad credentials?)") return self._login_error("bad_credentials")
return LoginResult( return LoginResult(
success=True, success=True,
@ -434,7 +434,7 @@ class CSESScraper(BaseScraper):
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:
return self._submit_error("Login failed (bad credentials?)") return self._submit_error("bad_credentials")
print( print(
json.dumps( json.dumps(
{ {

View file

@ -344,7 +344,7 @@ class KattisScraper(BaseScraper):
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)
if not ok: if not ok:
return self._submit_error("Login failed (bad credentials?)") return self._submit_error("bad_credentials")
await _save_kattis_cookies(client) await _save_kattis_cookies(client)
print(json.dumps({"status": "submitting"}), flush=True) print(json.dumps({"status": "submitting"}), flush=True)
@ -381,7 +381,7 @@ class KattisScraper(BaseScraper):
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)
if not ok: if not ok:
return self._submit_error("Login failed (bad credentials?)") return self._submit_error("bad_credentials")
await _save_kattis_cookies(client) await _save_kattis_cookies(client)
try: try:
r = await _do_submit() r = await _do_submit()
@ -421,7 +421,7 @@ class KattisScraper(BaseScraper):
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)
if not ok: if not ok:
return self._login_error("Login failed (bad credentials?)") return self._login_error("bad_credentials")
await _save_kattis_cookies(client) await _save_kattis_cookies(client)
return LoginResult( return LoginResult(
success=True, success=True,

View file

@ -439,7 +439,7 @@ class USACOScraper(BaseScraper):
except Exception as e: except Exception as e:
return self._submit_error(f"Login failed: {e}") return self._submit_error(f"Login failed: {e}")
if not ok: if not ok:
return self._submit_error("Login failed (bad credentials?)") return self._submit_error("bad_credentials")
await _save_usaco_cookies(client) await _save_usaco_cookies(client)
else: else:
print(json.dumps({"status": "logging_in"}), flush=True) print(json.dumps({"status": "logging_in"}), flush=True)
@ -448,7 +448,7 @@ class USACOScraper(BaseScraper):
except Exception as e: except Exception as e:
return self._submit_error(f"Login failed: {e}") return self._submit_error(f"Login failed: {e}")
if not ok: if not ok:
return self._submit_error("Login failed (bad credentials?)") return self._submit_error("bad_credentials")
await _save_usaco_cookies(client) await _save_usaco_cookies(client)
result = await self._do_submit(client, problem_id, language_id, source) result = await self._do_submit(client, problem_id, language_id, source)
@ -463,7 +463,7 @@ class USACOScraper(BaseScraper):
except Exception as e: except Exception as e:
return self._submit_error(f"Login failed: {e}") return self._submit_error(f"Login failed: {e}")
if not ok: if not ok:
return self._submit_error("Login failed (bad credentials?)") return self._submit_error("bad_credentials")
await _save_usaco_cookies(client) await _save_usaco_cookies(client)
return await self._do_submit(client, problem_id, language_id, source) return await self._do_submit(client, problem_id, language_id, source)
@ -543,7 +543,7 @@ class USACOScraper(BaseScraper):
return self._login_error(f"Login request failed: {e}") return self._login_error(f"Login request failed: {e}")
if not ok: if not ok:
return self._login_error("Login failed (bad credentials?)") return self._login_error("bad_credentials")
await _save_usaco_cookies(client) await _save_usaco_cookies(client)
return LoginResult( return LoginResult(