fix(scrapers): bad credentials detection and error message cleanup (#367)
## Problem Wrong credentials produced garbled error messages (`"login failed: Login failed: bad_credentials"`) and stale credentials remained cached after failure, causing silent re-use on the next invocation. ## Solution Standardize all scrapers to emit `"bad_credentials"` as a plain error code, mapped to a human-readable string via `LOGIN_ERRORS` in `constants.lua`. Fix `credentials.lua` to clear cached credentials on failure in both the fresh-prompt and re-prompt paths. For AtCoder and Codeforces, replace `wait_for_url` with `wait_for_function` to detect the login error element immediately rather than sitting the full 10s navigation timeout. Add "Remember me" checkbox on Codeforces login.
This commit is contained in:
parent
573b335646
commit
771dbc7753
9 changed files with 67 additions and 40 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,13 @@ local STATUS_MESSAGES = {
|
||||||
---@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)
|
||||||
vim.ui.input({ prompt = display .. ' username: ' }, function(username)
|
vim.ui.input({ prompt = '[cp.nvim]: ' .. 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 })
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
vim.fn.inputsave()
|
vim.fn.inputsave()
|
||||||
local password = vim.fn.inputsecret(display .. ' password: ')
|
local password = vim.fn.inputsecret('[cp.nvim]: ' .. display .. ' password: ')
|
||||||
vim.fn.inputrestore()
|
vim.fn.inputrestore()
|
||||||
if not password or password == '' then
|
if not password or password == '' then
|
||||||
logger.log('Cancelled', { level = vim.log.levels.WARN })
|
logger.log('Cancelled', { level = vim.log.levels.WARN })
|
||||||
|
|
@ -45,7 +45,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 +87,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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -473,9 +479,7 @@ def _login_headless(credentials: dict[str, str]) -> LoginResult:
|
||||||
f"{BASE_URL}/home", page_action=verify_action, network_idle=True
|
f"{BASE_URL}/home", page_action=verify_action, network_idle=True
|
||||||
)
|
)
|
||||||
if not logged_in:
|
if not logged_in:
|
||||||
return LoginResult(
|
return LoginResult(success=False, error="bad_credentials")
|
||||||
success=False, error="Login failed (bad credentials?)"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
browser_cookies = session.context.cookies()
|
browser_cookies = session.context.cookies()
|
||||||
|
|
@ -570,7 +574,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}"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ from .base import (
|
||||||
load_platform_cookies,
|
load_platform_cookies,
|
||||||
save_platform_cookies,
|
save_platform_cookies,
|
||||||
)
|
)
|
||||||
from .timeouts import BROWSER_SESSION_TIMEOUT, HTTP_TIMEOUT
|
from .timeouts import BROWSER_NAV_TIMEOUT, BROWSER_SESSION_TIMEOUT, HTTP_TIMEOUT
|
||||||
from .models import (
|
from .models import (
|
||||||
ContestListResult,
|
ContestListResult,
|
||||||
ContestSummary,
|
ContestSummary,
|
||||||
|
|
@ -84,10 +84,12 @@ def _login_headless_codechef(credentials: dict[str, str]) -> LoginResult:
|
||||||
page.locator('input[name="name"]').fill(credentials.get("username", ""))
|
page.locator('input[name="name"]').fill(credentials.get("username", ""))
|
||||||
page.locator('input[name="pass"]').fill(credentials.get("password", ""))
|
page.locator('input[name="pass"]').fill(credentials.get("password", ""))
|
||||||
page.locator("input.cc-login-btn").click()
|
page.locator("input.cc-login-btn").click()
|
||||||
try:
|
page.wait_for_function(
|
||||||
page.wait_for_url(lambda url: "/login" not in url, timeout=3000)
|
"() => !window.location.href.includes('/login') || !!document.querySelector('div.error')",
|
||||||
except Exception:
|
timeout=BROWSER_NAV_TIMEOUT,
|
||||||
login_error = "bad credentials?"
|
)
|
||||||
|
if "/login" in page.url:
|
||||||
|
login_error = "bad_credentials"
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
login_error = str(e)
|
login_error = str(e)
|
||||||
|
|
@ -105,9 +107,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="bad_credentials")
|
||||||
success=False, error="Login failed (bad credentials?)"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
browser_cookies = session.context.cookies()
|
browser_cookies = session.context.cookies()
|
||||||
|
|
@ -163,10 +163,12 @@ def _submit_headless_codechef(
|
||||||
page.locator('input[name="name"]').fill(credentials.get("username", ""))
|
page.locator('input[name="name"]').fill(credentials.get("username", ""))
|
||||||
page.locator('input[name="pass"]').fill(credentials.get("password", ""))
|
page.locator('input[name="pass"]').fill(credentials.get("password", ""))
|
||||||
page.locator("input.cc-login-btn").click()
|
page.locator("input.cc-login-btn").click()
|
||||||
try:
|
page.wait_for_function(
|
||||||
page.wait_for_url(lambda url: "/login" not in url, timeout=3000)
|
"() => !window.location.href.includes('/login') || !!document.querySelector('div.error')",
|
||||||
except Exception:
|
timeout=BROWSER_NAV_TIMEOUT,
|
||||||
login_error = "bad credentials?"
|
)
|
||||||
|
if "/login" in page.url:
|
||||||
|
login_error = "bad_credentials"
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
login_error = str(e)
|
login_error = str(e)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -427,9 +434,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="bad_credentials")
|
||||||
success=False, error="Login failed (bad credentials?)"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
browser_cookies = session.context.cookies()
|
browser_cookies = session.context.cookies()
|
||||||
|
|
@ -540,7 +545,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}"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue