Compare commits

..

No commits in common. "57f57f931f0651e73d23af51f1d5fc92faa37347" and "543480a4fe0b5a4e2c38f3baedbd935797113dcd" have entirely different histories.

4 changed files with 53 additions and 42 deletions

View file

@ -54,7 +54,6 @@ function M.submit(opts)
logger.log('Source file not found', { level = vim.log.levels.ERROR }) logger.log('Source file not found', { level = vim.log.levels.ERROR })
return return
end end
source_file = vim.fn.fnamemodify(source_file, ':p')
prompt_credentials(platform, function(creds) prompt_credentials(platform, function(creds)
vim.cmd.update() vim.cmd.update()

View file

@ -221,17 +221,25 @@ async def _save_kattis_cookies(client: httpx.AsyncClient) -> None:
_COOKIE_PATH.write_text(json.dumps(cookies)) _COOKIE_PATH.write_text(json.dumps(cookies))
async def _check_kattis_login(client: httpx.AsyncClient) -> bool:
try:
r = await client.get(BASE_URL + "/", headers=HEADERS, timeout=HTTP_TIMEOUT)
text = r.text.lower()
return "sign out" in text or "logout" in text or "my profile" in text
except Exception:
return False
async def _do_kattis_login( async def _do_kattis_login(
client: httpx.AsyncClient, username: str, password: str client: httpx.AsyncClient, username: str, password: str
) -> bool: ) -> bool:
client.cookies.clear()
r = await client.post( r = await client.post(
f"{BASE_URL}/login", f"{BASE_URL}/login/email",
data={"user": username, "password": password, "script": "true"}, data={"user": username, "password": password, "script": "true"},
headers=HEADERS, headers=HEADERS,
timeout=HTTP_TIMEOUT, timeout=HTTP_TIMEOUT,
) )
return r.status_code == 200 return r.status_code == 200 and "login failed" not in r.text.lower()
class KattisScraper(BaseScraper): class KattisScraper(BaseScraper):
@ -322,7 +330,9 @@ class KattisScraper(BaseScraper):
async with httpx.AsyncClient(follow_redirects=True) as client: async with httpx.AsyncClient(follow_redirects=True) as client:
await _load_kattis_cookies(client) await _load_kattis_cookies(client)
if not client.cookies: print(json.dumps({"status": "checking_login"}), flush=True)
logged_in = bool(client.cookies) and await _check_kattis_login(client)
if not logged_in:
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:
@ -341,35 +351,18 @@ class KattisScraper(BaseScraper):
} }
if contest_id != problem_id: if contest_id != problem_id:
data["contest"] = contest_id data["contest"] = contest_id
try:
async def _do_submit() -> httpx.Response: r = await client.post(
return await client.post(
f"{BASE_URL}/submit", f"{BASE_URL}/submit",
data=data, data=data,
files={"sub_file[]": (f"solution.{ext}", source, "text/plain")}, files={"sub_file[]": (f"solution.{ext}", source, "text/plain")},
headers=HEADERS, headers=HEADERS,
timeout=HTTP_TIMEOUT, timeout=HTTP_TIMEOUT,
) )
try:
r = await _do_submit()
r.raise_for_status() r.raise_for_status()
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":
_COOKIE_PATH.unlink(missing_ok=True)
print(json.dumps({"status": "logging_in"}), flush=True)
ok = await _do_kattis_login(client, username, password)
if not ok:
return self._submit_error("Login failed (bad credentials?)")
await _save_kattis_cookies(client)
try:
r = await _do_submit()
r.raise_for_status()
except Exception as e:
return self._submit_error(f"Submit request failed: {e}")
sid_m = re.search(r"Submission ID:\s*(\d+)", r.text, re.IGNORECASE) sid_m = re.search(r"Submission ID:\s*(\d+)", r.text, re.IGNORECASE)
sid = sid_m.group(1) if sid_m else "" sid = sid_m.group(1) if sid_m else ""
return SubmitResult( return SubmitResult(
@ -383,10 +376,21 @@ class KattisScraper(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_kattis_cookies(client)
if client.cookies:
print(json.dumps({"status": "checking_login"}), flush=True)
if await _check_kattis_login(client):
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)
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("Login failed (bad credentials?)")
await _save_kattis_cookies(client) await _save_kattis_cookies(client)
return LoginResult( return LoginResult(
success=True, success=True,

View file

@ -16,7 +16,7 @@ LANGUAGE_IDS = {
"python": "python", "python": "python",
}, },
"kattis": { "kattis": {
"cpp": "C++", "cpp": "C++17",
"python": "Python 3", "python": "Python 3",
}, },
"codechef": { "codechef": {

View file

@ -29,7 +29,7 @@ CONNECTIONS = 4
_COOKIE_PATH = Path.home() / ".cache" / "cp-nvim" / "usaco-cookies.json" _COOKIE_PATH = Path.home() / ".cache" / "cp-nvim" / "usaco-cookies.json"
_LOGIN_PATH = "/current/tpcm/login-session.php" _LOGIN_PATH = "/current/tpcm/login-session.php"
_SUBMIT_PATH = "/current/tpcm/submit-solution.php" _SUBMIT_PATH = "/current/tpcm/submitproblem.php"
_LANG_KEYWORDS: dict[str, list[str]] = { _LANG_KEYWORDS: dict[str, list[str]] = {
"cpp": ["c++17", "c++ 17", "g++17", "c++", "cpp"], "cpp": ["c++17", "c++ 17", "g++17", "c++", "cpp"],
@ -141,16 +141,13 @@ def _parse_problem_page(html: str) -> dict[str, Any]:
def _pick_lang_option(select_body: str, language_id: str) -> str | None: def _pick_lang_option(select_body: str, language_id: str) -> str | None:
keywords = _LANG_KEYWORDS.get(language_id.lower(), [language_id.lower()]) keywords = _LANG_KEYWORDS.get(language_id.lower(), [language_id.lower()])
options = [ for m in re.finditer(
(m.group(1), m.group(2).strip().lower()) r'<option\b[^>]*\bvalue=["\']([^"\']*)["\'][^>]*>([^<]+)',
for m in re.finditer( select_body,
r'<option\b[^>]*\bvalue=["\']([^"\']*)["\'][^>]*>([^<]+)', re.IGNORECASE,
select_body, ):
re.IGNORECASE, val, text = m.group(1), m.group(2).strip().lower()
) for kw in keywords:
]
for kw in keywords:
for val, text in options:
if kw in text: if kw in text:
return val return val
return None return None
@ -168,7 +165,7 @@ def _parse_submit_form(
re.DOTALL | re.IGNORECASE, re.DOTALL | re.IGNORECASE,
): ):
action, body = form_m.group(1), form_m.group(2) action, body = form_m.group(1), form_m.group(2)
if "sourcefile" not in body.lower(): if "sub_file" not in body.lower():
continue continue
if action.startswith("http"): if action.startswith("http"):
form_action = action form_action = action
@ -185,7 +182,7 @@ def _parse_submit_form(
name_m = re.search(r'\bname=["\']([^"\']+)["\']', tag, re.IGNORECASE) name_m = re.search(r'\bname=["\']([^"\']+)["\']', tag, re.IGNORECASE)
val_m = re.search(r'\bvalue=["\']([^"\']*)["\']', tag, re.IGNORECASE) val_m = re.search(r'\bvalue=["\']([^"\']*)["\']', tag, re.IGNORECASE)
if name_m and val_m: if name_m and val_m:
hidden[name_m.group(1)] = val_m.group(1) hidden[name_m.group(1)] = val_m.group(2)
for sel_m in re.finditer( for sel_m in re.finditer(
r'<select\b[^>]*\bname=["\']([^"\']+)["\'][^>]*>(.*?)</select>', r'<select\b[^>]*\bname=["\']([^"\']+)["\'][^>]*>(.*?)</select>',
body, body,
@ -234,15 +231,16 @@ async def _do_usaco_login(
) -> bool: ) -> bool:
r = await client.post( r = await client.post(
f"{_AUTH_BASE}{_LOGIN_PATH}", f"{_AUTH_BASE}{_LOGIN_PATH}",
data={"uname": username, "password": password}, data={"user": username, "password": password},
headers=HEADERS, headers=HEADERS,
timeout=HTTP_TIMEOUT, timeout=HTTP_TIMEOUT,
) )
r.raise_for_status() r.raise_for_status()
try: try:
return r.json().get("code") == 1 data = r.json()
return bool(data.get("success") or data.get("status") == "success")
except Exception: except Exception:
return False return r.status_code == 200 and "error" not in r.text.lower()
class USACOScraper(BaseScraper): class USACOScraper(BaseScraper):
@ -455,7 +453,7 @@ class USACOScraper(BaseScraper):
r = await client.post( r = await client.post(
form_url, form_url,
data=data, data=data,
files={"sourcefile": (f"solution.{ext}", source, "text/plain")}, files={"sub_file[]": (f"solution.{ext}", source, "text/plain")},
headers=HEADERS, headers=HEADERS,
timeout=HTTP_TIMEOUT, timeout=HTTP_TIMEOUT,
) )
@ -479,6 +477,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)