Compare commits
No commits in common. "e13eed0c4df193b26b9603982d70ed0c59ca4690" and "8c8e49d75c99bb950141c217a7a7103f6e45f51f" have entirely different histories.
e13eed0c4d
...
8c8e49d75c
9 changed files with 987 additions and 490 deletions
41
flake.nix
41
flake.nix
|
|
@ -26,20 +26,6 @@
|
||||||
ps.requests
|
ps.requests
|
||||||
]);
|
]);
|
||||||
|
|
||||||
mkDevPythonEnv =
|
|
||||||
pkgs:
|
|
||||||
pkgs.python312.withPackages (ps: [
|
|
||||||
ps.backoff
|
|
||||||
ps.beautifulsoup4
|
|
||||||
ps.curl-cffi
|
|
||||||
ps.httpx
|
|
||||||
ps.ndjson
|
|
||||||
ps.pydantic
|
|
||||||
ps.requests
|
|
||||||
ps.pytest
|
|
||||||
ps.pytest-mock
|
|
||||||
]);
|
|
||||||
|
|
||||||
mkSubmitEnv =
|
mkSubmitEnv =
|
||||||
pkgs:
|
pkgs:
|
||||||
pkgs.buildFHSEnv {
|
pkgs.buildFHSEnv {
|
||||||
|
|
@ -58,24 +44,19 @@
|
||||||
glib
|
glib
|
||||||
gtk3
|
gtk3
|
||||||
libdrm
|
libdrm
|
||||||
|
libGL
|
||||||
libxkbcommon
|
libxkbcommon
|
||||||
mesa
|
mesa
|
||||||
libGL
|
|
||||||
nspr
|
nspr
|
||||||
nss
|
nss
|
||||||
pango
|
pango
|
||||||
libx11
|
xorg.libX11
|
||||||
libxcomposite
|
xorg.libXcomposite
|
||||||
libxdamage
|
xorg.libXdamage
|
||||||
libxext
|
xorg.libXext
|
||||||
libxfixes
|
xorg.libXfixes
|
||||||
libxrandr
|
xorg.libXrandr
|
||||||
libxcb
|
xorg.libxcb
|
||||||
at-spi2-core
|
|
||||||
expat
|
|
||||||
libgbm
|
|
||||||
systemdLibs
|
|
||||||
zlib
|
|
||||||
];
|
];
|
||||||
runScript = "${pkgs.uv}/bin/uv";
|
runScript = "${pkgs.uv}/bin/uv";
|
||||||
};
|
};
|
||||||
|
|
@ -119,19 +100,15 @@
|
||||||
submitEnv = mkSubmitEnv (pkgsFor system);
|
submitEnv = mkSubmitEnv (pkgsFor system);
|
||||||
});
|
});
|
||||||
|
|
||||||
formatter = eachSystem (system: (pkgsFor system).nixfmt-tree);
|
|
||||||
|
|
||||||
devShells = eachSystem (system: {
|
devShells = eachSystem (system: {
|
||||||
default = (pkgsFor system).mkShell {
|
default = (pkgsFor system).mkShell {
|
||||||
packages = with (pkgsFor system); [
|
packages = with (pkgsFor system); [
|
||||||
uv
|
uv
|
||||||
(mkDevPythonEnv (pkgsFor system))
|
python312
|
||||||
prettier
|
prettier
|
||||||
ruff
|
|
||||||
stylua
|
stylua
|
||||||
selene
|
selene
|
||||||
lua-language-server
|
lua-language-server
|
||||||
ty
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -44,10 +44,6 @@ local function run_scraper(platform, subcommand, args, opts)
|
||||||
return { success = false, error = msg }
|
return { success = false, error = msg }
|
||||||
end
|
end
|
||||||
|
|
||||||
if subcommand == 'submit' then
|
|
||||||
utils.setup_nix_submit_env()
|
|
||||||
end
|
|
||||||
|
|
||||||
local plugin_path = utils.get_plugin_path()
|
local plugin_path = utils.get_plugin_path()
|
||||||
local cmd
|
local cmd
|
||||||
if subcommand == 'submit' then
|
if subcommand == 'submit' then
|
||||||
|
|
@ -77,10 +73,6 @@ local function run_scraper(platform, subcommand, args, opts)
|
||||||
|
|
||||||
if opts and opts.ndjson then
|
if opts and opts.ndjson then
|
||||||
local uv = vim.uv
|
local uv = vim.uv
|
||||||
local stdin_pipe = nil
|
|
||||||
if opts.stdin then
|
|
||||||
stdin_pipe = uv.new_pipe(false)
|
|
||||||
end
|
|
||||||
local stdout = uv.new_pipe(false)
|
local stdout = uv.new_pipe(false)
|
||||||
local stderr = uv.new_pipe(false)
|
local stderr = uv.new_pipe(false)
|
||||||
local buf = ''
|
local buf = ''
|
||||||
|
|
@ -88,7 +80,7 @@ local function run_scraper(platform, subcommand, args, opts)
|
||||||
local handle
|
local handle
|
||||||
handle = uv.spawn(cmd[1], {
|
handle = uv.spawn(cmd[1], {
|
||||||
args = vim.list_slice(cmd, 2),
|
args = vim.list_slice(cmd, 2),
|
||||||
stdio = { stdin_pipe, stdout, stderr },
|
stdio = { nil, stdout, stderr },
|
||||||
env = spawn_env_list(env),
|
env = spawn_env_list(env),
|
||||||
cwd = plugin_path,
|
cwd = plugin_path,
|
||||||
}, function(code, signal)
|
}, function(code, signal)
|
||||||
|
|
@ -102,9 +94,6 @@ local function run_scraper(platform, subcommand, args, opts)
|
||||||
if opts.on_exit then
|
if opts.on_exit then
|
||||||
opts.on_exit({ success = (code == 0), code = code, signal = signal })
|
opts.on_exit({ success = (code == 0), code = code, signal = signal })
|
||||||
end
|
end
|
||||||
if stdin_pipe and not stdin_pipe:is_closing() then
|
|
||||||
stdin_pipe:close()
|
|
||||||
end
|
|
||||||
if not stdout:is_closing() then
|
if not stdout:is_closing() then
|
||||||
stdout:close()
|
stdout:close()
|
||||||
end
|
end
|
||||||
|
|
@ -117,21 +106,10 @@ local function run_scraper(platform, subcommand, args, opts)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
if not handle then
|
if not handle then
|
||||||
if stdin_pipe and not stdin_pipe:is_closing() then
|
|
||||||
stdin_pipe:close()
|
|
||||||
end
|
|
||||||
logger.log('Failed to start scraper process', vim.log.levels.ERROR)
|
logger.log('Failed to start scraper process', vim.log.levels.ERROR)
|
||||||
return { success = false, error = 'spawn failed' }
|
return { success = false, error = 'spawn failed' }
|
||||||
end
|
end
|
||||||
|
|
||||||
if stdin_pipe then
|
|
||||||
uv.write(stdin_pipe, opts.stdin, function()
|
|
||||||
uv.shutdown(stdin_pipe, function()
|
|
||||||
stdin_pipe:close()
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
uv.read_start(stdout, function(_, data)
|
uv.read_start(stdout, function(_, data)
|
||||||
if data == nil then
|
if data == nil then
|
||||||
if buf ~= '' and opts.on_event then
|
if buf ~= '' and opts.on_event then
|
||||||
|
|
@ -282,41 +260,20 @@ function M.scrape_all_tests(platform, contest_id, callback)
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.submit(
|
function M.submit(platform, contest_id, problem_id, language, source_code, credentials, callback)
|
||||||
platform,
|
local creds_json = vim.json.encode(credentials)
|
||||||
contest_id,
|
|
||||||
problem_id,
|
|
||||||
language,
|
|
||||||
source_code,
|
|
||||||
credentials,
|
|
||||||
on_status,
|
|
||||||
callback
|
|
||||||
)
|
|
||||||
local done = false
|
|
||||||
run_scraper(platform, 'submit', { contest_id, problem_id, language }, {
|
run_scraper(platform, 'submit', { contest_id, problem_id, language }, {
|
||||||
ndjson = true,
|
|
||||||
stdin = source_code,
|
stdin = source_code,
|
||||||
env_extra = { CP_CREDENTIALS = vim.json.encode(credentials) },
|
env_extra = { CP_CREDENTIALS = creds_json },
|
||||||
on_event = function(ev)
|
on_exit = function(result)
|
||||||
if ev.status ~= nil then
|
|
||||||
if type(on_status) == 'function' then
|
|
||||||
on_status(ev.status)
|
|
||||||
end
|
|
||||||
elseif ev.success ~= nil then
|
|
||||||
done = true
|
|
||||||
if type(callback) == 'function' then
|
if type(callback) == 'function' then
|
||||||
callback(ev)
|
if result and result.success then
|
||||||
|
callback(result.data or { success = true })
|
||||||
|
else
|
||||||
|
callback({ success = false, error = result and result.error or 'unknown' })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
on_exit = function(proc)
|
|
||||||
if not done and type(callback) == 'function' then
|
|
||||||
callback({
|
|
||||||
success = false,
|
|
||||||
error = 'submit process exited (code=' .. tostring(proc.code) .. ')',
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,6 @@ local cache = require('cp.cache')
|
||||||
local logger = require('cp.log')
|
local logger = require('cp.log')
|
||||||
local state = require('cp.state')
|
local state = require('cp.state')
|
||||||
|
|
||||||
local STATUS_MSGS = {
|
|
||||||
installing_browser = 'Installing browser (first time setup)...',
|
|
||||||
checking_login = 'Checking login...',
|
|
||||||
logging_in = 'Logging in...',
|
|
||||||
submitting = 'Submitting...',
|
|
||||||
}
|
|
||||||
|
|
||||||
local function prompt_credentials(platform, callback)
|
local function prompt_credentials(platform, callback)
|
||||||
local saved = cache.get_credentials(platform)
|
local saved = cache.get_credentials(platform)
|
||||||
if saved and saved.username and saved.password then
|
if saved and saved.username and saved.password then
|
||||||
|
|
@ -55,8 +48,6 @@ function M.submit(opts)
|
||||||
local source_lines = vim.fn.readfile(source_file)
|
local source_lines = vim.fn.readfile(source_file)
|
||||||
local source_code = table.concat(source_lines, '\n')
|
local source_code = table.concat(source_lines, '\n')
|
||||||
|
|
||||||
vim.notify('[cp.nvim] Submitting...', vim.log.levels.INFO)
|
|
||||||
|
|
||||||
require('cp.scraper').submit(
|
require('cp.scraper').submit(
|
||||||
platform,
|
platform,
|
||||||
contest_id,
|
contest_id,
|
||||||
|
|
@ -64,11 +55,6 @@ function M.submit(opts)
|
||||||
language,
|
language,
|
||||||
source_code,
|
source_code,
|
||||||
creds,
|
creds,
|
||||||
function(status)
|
|
||||||
vim.schedule(function()
|
|
||||||
vim.notify('[cp.nvim] ' .. (STATUS_MSGS[status] or status), vim.log.levels.INFO)
|
|
||||||
end)
|
|
||||||
end,
|
|
||||||
function(result)
|
function(result)
|
||||||
vim.schedule(function()
|
vim.schedule(function()
|
||||||
if result and result.success then
|
if result and result.success then
|
||||||
|
|
|
||||||
|
|
@ -119,76 +119,10 @@ function M.get_python_submit_cmd(module, plugin_path)
|
||||||
if _nix_submit_cmd then
|
if _nix_submit_cmd then
|
||||||
return { _nix_submit_cmd, 'run', '--directory', plugin_path, '-m', 'scrapers.' .. module }
|
return { _nix_submit_cmd, 'run', '--directory', plugin_path, '-m', 'scrapers.' .. module }
|
||||||
end
|
end
|
||||||
return { 'uv', 'run', '--directory', plugin_path, '-m', 'scrapers.' .. module }
|
return M.get_python_cmd(module, plugin_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
local python_env_setup = false
|
local python_env_setup = false
|
||||||
local _nix_submit_attempted = false
|
|
||||||
|
|
||||||
---@return boolean
|
|
||||||
local function discover_nix_submit_cmd()
|
|
||||||
local cache_dir = vim.fn.stdpath('cache') .. '/cp-nvim'
|
|
||||||
local cache_file = cache_dir .. '/nix-submit'
|
|
||||||
|
|
||||||
local f = io.open(cache_file, 'r')
|
|
||||||
if f then
|
|
||||||
local cached = f:read('*l')
|
|
||||||
f:close()
|
|
||||||
if cached and vim.fn.executable(cached) == 1 then
|
|
||||||
_nix_submit_cmd = cached
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local plugin_path = M.get_plugin_path()
|
|
||||||
vim.cmd.redraw()
|
|
||||||
vim.notify('Building submit environment...', vim.log.levels.INFO)
|
|
||||||
vim.cmd.redraw()
|
|
||||||
local result = vim
|
|
||||||
.system(
|
|
||||||
{ 'nix', 'build', plugin_path .. '#submitEnv', '--no-link', '--print-out-paths' },
|
|
||||||
{ text = true }
|
|
||||||
)
|
|
||||||
:wait()
|
|
||||||
|
|
||||||
if result.code ~= 0 then
|
|
||||||
logger.log('nix build #submitEnv failed: ' .. (result.stderr or ''), vim.log.levels.WARN)
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local store_path = result.stdout:gsub('%s+$', '')
|
|
||||||
local submit_cmd = store_path .. '/bin/cp-nvim-submit'
|
|
||||||
|
|
||||||
if vim.fn.executable(submit_cmd) ~= 1 then
|
|
||||||
logger.log('nix submit cmd not executable at ' .. submit_cmd, vim.log.levels.WARN)
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
vim.fn.mkdir(cache_dir, 'p')
|
|
||||||
f = io.open(cache_file, 'w')
|
|
||||||
if f then
|
|
||||||
f:write(submit_cmd)
|
|
||||||
f:close()
|
|
||||||
end
|
|
||||||
|
|
||||||
_nix_submit_cmd = submit_cmd
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
---@return boolean
|
|
||||||
function M.setup_nix_submit_env()
|
|
||||||
if _nix_submit_cmd then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
if _nix_submit_attempted then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
_nix_submit_attempted = true
|
|
||||||
if vim.fn.executable('nix') == 1 then
|
|
||||||
return discover_nix_submit_cmd()
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
---@return boolean
|
---@return boolean
|
||||||
local function discover_nix_python()
|
local function discover_nix_python()
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backoff>=2.2.1",
|
"backoff>=2.2.1",
|
||||||
"beautifulsoup4>=4.13.5",
|
"beautifulsoup4>=4.13.5",
|
||||||
"scrapling[fetchers]>=0.4",
|
"camoufox[geoip]",
|
||||||
"curl-cffi>=0.13.0",
|
"curl-cffi>=0.13.0",
|
||||||
"httpx>=0.28.1",
|
"httpx>=0.28.1",
|
||||||
"ndjson>=0.3.1",
|
"ndjson>=0.3.1",
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,7 @@ import asyncio
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
@ -235,183 +233,6 @@ def _extract_samples(html: str) -> list[TestCase]:
|
||||||
return cases
|
return cases
|
||||||
|
|
||||||
|
|
||||||
_TURNSTILE_JS = "() => { const el = document.querySelector('[name=\"cf-turnstile-response\"]'); return el && el.value.length > 0; }"
|
|
||||||
|
|
||||||
|
|
||||||
def _solve_turnstile(page) -> None:
|
|
||||||
for _ in range(6):
|
|
||||||
has_token = page.evaluate(_TURNSTILE_JS)
|
|
||||||
if has_token:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
box = page.locator(
|
|
||||||
'iframe[src*="challenges.cloudflare.com"]'
|
|
||||||
).first.bounding_box()
|
|
||||||
if box:
|
|
||||||
page.mouse.click(
|
|
||||||
box["x"] + box["width"] * 0.15,
|
|
||||||
box["y"] + box["height"] * 0.5,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
page.wait_for_function(_TURNSTILE_JS, timeout=5000)
|
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
raise RuntimeError("Turnstile not solved after multiple attempts")
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_browser() -> None:
|
|
||||||
try:
|
|
||||||
from patchright._impl._driver import compute_driver_executable # type: ignore[import-untyped,unresolved-import]
|
|
||||||
|
|
||||||
node, cli = compute_driver_executable()
|
|
||||||
browser_info = subprocess.run(
|
|
||||||
[node, cli, "install", "--dry-run", "chromium"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
for line in browser_info.stdout.splitlines():
|
|
||||||
if "Install location:" in line:
|
|
||||||
install_dir = line.split(":", 1)[1].strip()
|
|
||||||
if not os.path.isdir(install_dir):
|
|
||||||
print(json.dumps({"status": "installing_browser"}), flush=True)
|
|
||||||
subprocess.run(
|
|
||||||
[node, cli, "install", "chromium"],
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _submit_headless(
|
|
||||||
contest_id: str,
|
|
||||||
problem_id: str,
|
|
||||||
source_code: str,
|
|
||||||
language_id: str,
|
|
||||||
credentials: dict[str, str],
|
|
||||||
) -> "SubmitResult":
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
try:
|
|
||||||
from scrapling.fetchers import StealthySession # type: ignore[import-untyped,unresolved-import]
|
|
||||||
except ImportError:
|
|
||||||
return SubmitResult(
|
|
||||||
success=False,
|
|
||||||
error="scrapling is required for AtCoder submit. Install it: uv add 'scrapling[fetchers]>=0.4'",
|
|
||||||
)
|
|
||||||
|
|
||||||
_ensure_browser()
|
|
||||||
|
|
||||||
cookie_cache = Path.home() / ".cache" / "cp-nvim" / "atcoder-cookies.json"
|
|
||||||
cookie_cache.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
saved_cookies: list[dict[str, Any]] = []
|
|
||||||
if cookie_cache.exists():
|
|
||||||
try:
|
|
||||||
saved_cookies = json.loads(cookie_cache.read_text())
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
logged_in = False
|
|
||||||
login_error: str | None = None
|
|
||||||
submit_error: str | None = None
|
|
||||||
|
|
||||||
def check_login(page):
|
|
||||||
nonlocal logged_in
|
|
||||||
logged_in = page.evaluate(
|
|
||||||
"() => Array.from(document.querySelectorAll('a')).some(a => a.textContent.trim() === 'Sign Out')"
|
|
||||||
)
|
|
||||||
|
|
||||||
def login_action(page):
|
|
||||||
nonlocal login_error
|
|
||||||
try:
|
|
||||||
_solve_turnstile(page)
|
|
||||||
page.fill('input[name="username"]', credentials.get("username", ""))
|
|
||||||
page.fill('input[name="password"]', credentials.get("password", ""))
|
|
||||||
page.click("#submit")
|
|
||||||
page.wait_for_url(lambda url: "/login" not in url, timeout=60000)
|
|
||||||
except Exception as e:
|
|
||||||
login_error = str(e)
|
|
||||||
|
|
||||||
def submit_action(page):
|
|
||||||
nonlocal submit_error
|
|
||||||
try:
|
|
||||||
_solve_turnstile(page)
|
|
||||||
page.select_option(
|
|
||||||
'select[name="data.TaskScreenName"]',
|
|
||||||
f"{contest_id}_{problem_id}",
|
|
||||||
)
|
|
||||||
page.locator(
|
|
||||||
f'select[name="data.LanguageId"] option[value="{language_id}"]'
|
|
||||||
).wait_for(state="attached", timeout=15000)
|
|
||||||
page.select_option('select[name="data.LanguageId"]', language_id)
|
|
||||||
with tempfile.NamedTemporaryFile(
|
|
||||||
mode="w", suffix=".cpp", delete=False, prefix="atcoder_"
|
|
||||||
) as tf:
|
|
||||||
tf.write(source_code)
|
|
||||||
tmp_path = tf.name
|
|
||||||
try:
|
|
||||||
page.set_input_files("#input-open-file", tmp_path)
|
|
||||||
page.wait_for_timeout(500)
|
|
||||||
finally:
|
|
||||||
os.unlink(tmp_path)
|
|
||||||
page.locator('button[type="submit"]').click()
|
|
||||||
page.wait_for_url(lambda url: "/submissions/me" in url, timeout=60000)
|
|
||||||
except Exception as e:
|
|
||||||
submit_error = str(e)
|
|
||||||
|
|
||||||
try:
|
|
||||||
with StealthySession(
|
|
||||||
headless=True,
|
|
||||||
timeout=60000,
|
|
||||||
google_search=False,
|
|
||||||
cookies=saved_cookies,
|
|
||||||
) as session:
|
|
||||||
print(json.dumps({"status": "checking_login"}), flush=True)
|
|
||||||
session.fetch(
|
|
||||||
f"{BASE_URL}/home",
|
|
||||||
page_action=check_login,
|
|
||||||
network_idle=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not logged_in:
|
|
||||||
print(json.dumps({"status": "logging_in"}), flush=True)
|
|
||||||
session.fetch(
|
|
||||||
f"{BASE_URL}/login",
|
|
||||||
page_action=login_action,
|
|
||||||
solve_cloudflare=True,
|
|
||||||
)
|
|
||||||
if login_error:
|
|
||||||
return SubmitResult(
|
|
||||||
success=False, error=f"Login failed: {login_error}"
|
|
||||||
)
|
|
||||||
|
|
||||||
print(json.dumps({"status": "submitting"}), flush=True)
|
|
||||||
session.fetch(
|
|
||||||
f"{BASE_URL}/contests/{contest_id}/submit",
|
|
||||||
page_action=submit_action,
|
|
||||||
solve_cloudflare=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
browser_cookies = session.context.cookies()
|
|
||||||
if any(c["name"] == "REVEL_SESSION" for c in browser_cookies):
|
|
||||||
cookie_cache.write_text(json.dumps(browser_cookies))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if submit_error:
|
|
||||||
return SubmitResult(success=False, error=submit_error)
|
|
||||||
|
|
||||||
return SubmitResult(
|
|
||||||
success=True, error="", submission_id="", verdict="submitted"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return SubmitResult(success=False, error=str(e))
|
|
||||||
|
|
||||||
|
|
||||||
def _scrape_tasks_sync(contest_id: str) -> list[dict[str, str]]:
|
def _scrape_tasks_sync(contest_id: str) -> list[dict[str, str]]:
|
||||||
html = _fetch(f"{BASE_URL}/contests/{contest_id}/tasks")
|
html = _fetch(f"{BASE_URL}/contests/{contest_id}/tasks")
|
||||||
return _parse_tasks_list(html)
|
return _parse_tasks_list(html)
|
||||||
|
|
@ -558,15 +379,84 @@ class AtcoderScraper(BaseScraper):
|
||||||
language_id: str,
|
language_id: str,
|
||||||
credentials: dict[str, str],
|
credentials: dict[str, str],
|
||||||
) -> SubmitResult:
|
) -> SubmitResult:
|
||||||
return await asyncio.to_thread(
|
def _submit_sync() -> SubmitResult:
|
||||||
_submit_headless,
|
try:
|
||||||
contest_id,
|
from camoufox.sync_api import Camoufox
|
||||||
problem_id,
|
except ImportError:
|
||||||
source_code,
|
return SubmitResult(
|
||||||
language_id,
|
success=False,
|
||||||
credentials,
|
error="camoufox is required for AtCoder login. Install it: uv add 'camoufox[geoip]'",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from curl_cffi import requests as curl_requests
|
||||||
|
|
||||||
|
try:
|
||||||
|
with Camoufox(headless=True) as browser:
|
||||||
|
page = browser.new_page()
|
||||||
|
page.goto(f"{BASE_URL}/login", wait_until="domcontentloaded")
|
||||||
|
page.wait_for_load_state("networkidle")
|
||||||
|
page.fill('input[name="username"]', credentials.get("username", ""))
|
||||||
|
page.fill('input[name="password"]', credentials.get("password", ""))
|
||||||
|
page.click('#submit')
|
||||||
|
page.wait_for_url(lambda url: "/login" not in url, timeout=30000)
|
||||||
|
cookies = page.context.cookies()
|
||||||
|
|
||||||
|
session = curl_requests.Session(impersonate="chrome")
|
||||||
|
for cookie in cookies:
|
||||||
|
session.cookies.set(
|
||||||
|
cookie["name"],
|
||||||
|
cookie["value"],
|
||||||
|
domain=cookie.get("domain", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
submit_page = session.get(
|
||||||
|
f"{BASE_URL}/contests/{contest_id}/submit",
|
||||||
|
timeout=TIMEOUT_SECONDS,
|
||||||
|
)
|
||||||
|
submit_page.raise_for_status()
|
||||||
|
soup = BeautifulSoup(submit_page.text, "html.parser")
|
||||||
|
csrf_input = soup.find("input", {"name": "csrf_token"})
|
||||||
|
if not csrf_input or not hasattr(csrf_input, "get"):
|
||||||
|
return SubmitResult(
|
||||||
|
success=False, error="Could not find CSRF token on submit page"
|
||||||
|
)
|
||||||
|
csrf_token = csrf_input.get("value", "") or "" # type: ignore[union-attr]
|
||||||
|
|
||||||
|
task_screen_name = f"{contest_id}_{problem_id}"
|
||||||
|
submit_resp = session.post(
|
||||||
|
f"{BASE_URL}/contests/{contest_id}/submit",
|
||||||
|
data={
|
||||||
|
"data.TaskScreenName": task_screen_name,
|
||||||
|
"data.LanguageId": language_id,
|
||||||
|
"sourceCode": source_code,
|
||||||
|
"csrf_token": csrf_token,
|
||||||
|
},
|
||||||
|
timeout=TIMEOUT_SECONDS,
|
||||||
|
allow_redirects=False,
|
||||||
|
)
|
||||||
|
if submit_resp.status_code in (301, 302):
|
||||||
|
location = submit_resp.headers.get("Location", "")
|
||||||
|
if "/submissions/me" in location:
|
||||||
|
return SubmitResult(
|
||||||
|
success=True,
|
||||||
|
error="",
|
||||||
|
submission_id="",
|
||||||
|
verdict="submitted",
|
||||||
|
)
|
||||||
|
return SubmitResult(
|
||||||
|
success=False,
|
||||||
|
error=f"Submit may have failed: redirected to {location}",
|
||||||
|
)
|
||||||
|
submit_resp.raise_for_status()
|
||||||
|
return SubmitResult(
|
||||||
|
success=False,
|
||||||
|
error="Unexpected response from submit (expected redirect)",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return SubmitResult(success=False, error=str(e))
|
||||||
|
|
||||||
|
return await asyncio.to_thread(_submit_sync)
|
||||||
|
|
||||||
|
|
||||||
async def main_async() -> int:
|
async def main_async() -> int:
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
LANGUAGE_IDS = {
|
LANGUAGE_IDS = {
|
||||||
"atcoder": {
|
"atcoder": {
|
||||||
"cpp": "6017",
|
"cpp": "5028",
|
||||||
"python": "6082",
|
"python": "5078",
|
||||||
},
|
},
|
||||||
"codeforces": {
|
"codeforces": {
|
||||||
"cpp": "89",
|
"cpp": "89",
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,8 @@ set -eu
|
||||||
nix develop --command stylua --check .
|
nix develop --command stylua --check .
|
||||||
git ls-files '*.lua' | xargs nix develop --command selene --display-style quiet
|
git ls-files '*.lua' | xargs nix develop --command selene --display-style quiet
|
||||||
nix develop --command prettier --check .
|
nix develop --command prettier --check .
|
||||||
nix fmt
|
|
||||||
git diff --exit-code -- '*.nix'
|
|
||||||
nix develop --command lua-language-server --check . --checklevel=Warning
|
nix develop --command lua-language-server --check . --checklevel=Warning
|
||||||
nix develop --command ruff format --check .
|
nix develop --command uvx ruff format --check .
|
||||||
nix develop --command ruff check .
|
nix develop --command uvx ruff check .
|
||||||
nix develop --command ty check .
|
nix develop --command uvx ty check .
|
||||||
nix develop --command python -m pytest tests/ -v
|
nix develop --command uv run pytest tests/ -v
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue