feat: git credential backend for credential storage (#371)

## Problem

Credentials were stored as plaintext JSON in
`stdpath('data')/cp-nvim.json`, with no integration with system
credential managers.

## Solution

Replace file-based credential storage with `git credential
fill/approve/reject`, delegating to whatever credential helper the user
has configured (`cache`, `store`, `libsecret`, macOS Keychain, etc.).

- New `lua/cp/git_credential.lua` module wrapping the git credential
protocol
- All credential consumers (`credentials.lua`, `submit.lua`,
`scraper.lua`) use `git_credential` directly — `cache.lua` no longer
handles credentials
- CSES API token packed into the password field (`password<TAB>token`)
so it works with helpers that ignore the `path` field
- `has_helper()` guard on `:CP login`, `:CP logout`, and `:CP submit`
with an error message if no helper is configured
- Healthcheck split into `[required]`/`[optional]` sections; git version
and credential helper status shown
- `git` checked at startup in `check_required_runtime()`
- Cache version system (`CACHE_VERSION`, v1→v2 migration) removed — the
cache file is now a plain JSON blob
- `:CP` command gets `bar = true`
This commit is contained in:
Barrett Ruth 2026-03-07 20:15:06 -05:00 committed by GitHub
parent 27d7a4e6b5
commit da4e2ebeba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 283 additions and 150 deletions

View file

@ -10,14 +10,15 @@ https://github.com/user-attachments/assets/e81d8dfb-578f-4a79-9989-210164fc0148
## Features ## Features
- **Multi-platform support**: AtCoder, CodeChef, Codeforces, USACO, CSES, Kattis - **Multi-platform support**: AtCoder, CodeChef, Codeforces, USACO, CSES, Kattis
- **Automatic problem setup**: Scrape test cases and metadata in seconds - **Online Judge Integration**: Submit problems and view contest standings
- **Dual view modes**: Lightweight I/O view for quick feedback, full panel for - **Live Contest Support**: Participate in real-time contests
detailed analysis - **Automatic setup**: Scrape test cases and metadata in seconds
- **Test case management**: Quickly view, edit, add, & remove test cases - **Streamlined Editing**: Configure coding view, edit test cases,
- **Rich test output**: 256 color ANSI support for compiler errors and program stress-test solutions, run interactive problems, and more
- **Rich output**: 256 color ANSI support for compiler errors and program
output output
- **Language agnostic**: Works with any language - **Language agnosticism**: Configure with any language
- **Diff viewer**: Compare expected vs actual output with 3 diff modes - **Security**: Passwords go untampered
## Installation ## Installation
@ -37,15 +38,16 @@ luarocks install cp.nvim
## Quick Start ## Quick Start
cp.nvim follows a simple principle: **solve locally, submit remotely**. 1. Find a contest:
### Basic Usage
1. Find a contest or problem
2. Set up contests locally
``` ```
:CP codeforces 1848 :CP pick
```
2. View the problem:
```
:CP open
``` ```
3. Code and test 3. Code and test
@ -69,7 +71,17 @@ cp.nvim follows a simple principle: **solve locally, submit remotely**.
:CP panel --debug :CP panel --debug
``` ```
5. Submit on the original website 6. Submit:
```
:CP submit
```
7. View contest standings:
```
:CP open standings
```
## Documentation ## Documentation
@ -78,7 +90,7 @@ cp.nvim follows a simple principle: **solve locally, submit remotely**.
``` ```
See See
[my config](https://github.com/barrettruth/dots/blob/main/.config/nvim/lua/plugins/cp.lua) [my config](https://github.com/barrettruth/nix/blob/5d0ede3668eb7f5ad2b4475267fc0458f9fa4527/config/nvim/lua/plugins/dev.lua#L165)
for the setup in the video shown above. for the setup in the video shown above.
## Motivation ## Motivation

View file

@ -49,7 +49,8 @@ REQUIREMENTS *cp-requirements*
- Neovim 0.10.0+ - Neovim 0.10.0+
- Unix-like operating system - Unix-like operating system
- uv package manager (https://docs.astral.sh/uv/) - (Optional) git 1.7.9+ (credential storage)
- (Optional) uv package manager (https://docs.astral.sh/uv/)
============================================================================== ==============================================================================
SETUP *cp-setup* SETUP *cp-setup*
@ -998,8 +999,15 @@ CREDENTIALS *cp-credentials*
Manage stored login credentials for platform submission. Manage stored login credentials for platform submission.
Credentials are stored under _credentials in the main cache file Credentials are stored via git-credential(1), using whatever credential
(stdpath('data')/cp-nvim.json). Use :CP cache read to inspect them. helper is configured in your git config (macOS Keychain, libsecret,
credential-store, etc.). Git is required. Cookie files
(~/.cache/cp-nvim/cookies.json) are unaffected.
To inspect stored credentials:
>sh
printf 'protocol=https\nhost=cses.fi\n\n' | git credential fill
<
:CP login [platform] :CP login [platform]
Set or update credentials for a platform. Prompts for username Set or update credentials for a platform. Prompts for username

View file

@ -42,13 +42,10 @@
local M = {} local M = {}
local CACHE_VERSION = 2
local cache_file = vim.fn.stdpath('data') .. '/cp-nvim.json' local cache_file = vim.fn.stdpath('data') .. '/cp-nvim.json'
local cache_data = {} local cache_data = {}
local loaded = false local loaded = false
--- Load the cache from disk if not done already
---@return nil ---@return nil
function M.load() function M.load()
if loaded then if loaded then
@ -56,8 +53,11 @@ function M.load()
end end
if vim.fn.filereadable(cache_file) == 0 then if vim.fn.filereadable(cache_file) == 0 then
vim.fn.writefile({}, cache_file) vim.fn.mkdir(vim.fn.fnamemodify(cache_file, ':h'), 'p')
vim.fn.setfperm(cache_file, 'rw-------') local tmpfile = vim.fn.tempname()
vim.fn.writefile({}, tmpfile)
vim.fn.setfperm(tmpfile, 'rw-------')
vim.uv.fs_rename(tmpfile, cache_file)
loaded = true loaded = true
return return
end end
@ -70,26 +70,7 @@ function M.load()
end end
local ok, decoded = pcall(vim.json.decode, table.concat(content, '\n')) local ok, decoded = pcall(vim.json.decode, table.concat(content, '\n'))
if not ok then if ok and type(decoded) == 'table' then
cache_data = {}
M.save()
loaded = true
return
end
if decoded._version == 1 then
local old_creds = decoded._credentials
decoded._credentials = nil
if old_creds then
for platform, creds in pairs(old_creds) do
decoded[platform] = decoded[platform] or {}
decoded[platform]._credentials = creds
end
end
decoded._version = CACHE_VERSION
cache_data = decoded
M.save()
elseif decoded._version == CACHE_VERSION then
cache_data = decoded cache_data = decoded
else else
cache_data = {} cache_data = {}
@ -98,17 +79,16 @@ function M.load()
loaded = true loaded = true
end end
--- Save the cache to disk, overwriting existing contents
---@return nil ---@return nil
function M.save() function M.save()
vim.schedule(function() vim.schedule(function()
vim.fn.mkdir(vim.fn.fnamemodify(cache_file, ':h'), 'p') vim.fn.mkdir(vim.fn.fnamemodify(cache_file, ':h'), 'p')
cache_data._version = CACHE_VERSION
local encoded = vim.json.encode(cache_data) local encoded = vim.json.encode(cache_data)
local lines = vim.split(encoded, '\n') local lines = vim.split(encoded, '\n')
vim.fn.writefile(lines, cache_file) local tmpfile = vim.fn.tempname()
vim.fn.setfperm(cache_file, 'rw-------') vim.fn.writefile(lines, tmpfile)
vim.fn.setfperm(tmpfile, 'rw-------')
vim.uv.fs_rename(tmpfile, cache_file)
end) end)
end end
@ -445,31 +425,6 @@ function M.get_contest_display_name(platform, contest_id)
return cache_data[platform][contest_id].display_name return cache_data[platform][contest_id].display_name
end end
---@param platform string
---@return table?
function M.get_credentials(platform)
if not cache_data[platform] then
return nil
end
return cache_data[platform]._credentials
end
---@param platform string
---@param creds table
function M.set_credentials(platform, creds)
cache_data[platform] = cache_data[platform] or {}
cache_data[platform]._credentials = creds
M.save()
end
---@param platform string
function M.clear_credentials(platform)
if cache_data[platform] then
cache_data[platform]._credentials = nil
end
M.save()
end
---@return nil ---@return nil
function M.clear_all() function M.clear_all()
cache_data = {} cache_data = {}

View file

@ -1,7 +1,7 @@
local M = {} local M = {}
local cache = require('cp.cache')
local constants = require('cp.constants') local constants = require('cp.constants')
local git_credential = require('cp.git_credential')
local logger = require('cp.log') local logger = require('cp.log')
local state = require('cp.state') local state = require('cp.state')
@ -40,14 +40,14 @@ local function prompt_and_login(platform, display)
end, function(result) end, function(result)
vim.schedule(function() vim.schedule(function()
if result.success then if result.success then
cache.set_credentials(platform, credentials) git_credential.store(platform, credentials)
logger.log( logger.log(
display .. ' login successful', display .. ' login successful',
{ level = vim.log.levels.INFO, override = true } { level = vim.log.levels.INFO, override = true }
) )
else else
local err = result.error or 'unknown error' local err = result.error or 'unknown error'
cache.clear_credentials(platform) git_credential.reject(platform, credentials)
logger.log( logger.log(
display .. ' login failed: ' .. (constants.LOGIN_ERRORS[err] or err), display .. ' login failed: ' .. (constants.LOGIN_ERRORS[err] or err),
{ level = vim.log.levels.ERROR } { level = vim.log.levels.ERROR }
@ -71,10 +71,17 @@ function M.login(platform)
return return
end end
if not git_credential.has_helper() then
logger.log(
'No git credential helper configured. See :help cp-credentials',
{ level = vim.log.levels.ERROR }
)
return
end
local display = constants.PLATFORM_DISPLAY_NAMES[platform] or platform local display = constants.PLATFORM_DISPLAY_NAMES[platform] or platform
cache.load() local existing = git_credential.get(platform) or {}
local existing = cache.get_credentials(platform) or {}
if existing.username and existing.password then if existing.username and existing.password then
local scraper = require('cp.scraper') local scraper = require('cp.scraper')
@ -91,7 +98,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) git_credential.reject(platform, existing)
prompt_and_login(platform, display) prompt_and_login(platform, display)
end end
end) end)
@ -112,16 +119,28 @@ function M.logout(platform)
) )
return return
end end
if not git_credential.has_helper() then
logger.log(
'No git credential helper configured. See :help cp-credentials',
{ level = vim.log.levels.ERROR }
)
return
end
local display = constants.PLATFORM_DISPLAY_NAMES[platform] or platform local display = constants.PLATFORM_DISPLAY_NAMES[platform] or platform
cache.load() local existing = git_credential.get(platform)
cache.clear_credentials(platform) if existing then
git_credential.reject(platform, existing)
end
local cookie_file = constants.COOKIE_FILE local cookie_file = constants.COOKIE_FILE
if vim.fn.filereadable(cookie_file) == 1 then if vim.fn.filereadable(cookie_file) == 1 then
local ok, data = pcall(vim.fn.json_decode, vim.fn.readfile(cookie_file, 'b')) local ok, data = pcall(vim.fn.json_decode, vim.fn.readfile(cookie_file, 'b'))
if ok and type(data) == 'table' then if ok and type(data) == 'table' then
data[platform] = nil data[platform] = nil
vim.fn.writefile({ vim.fn.json_encode(data) }, cookie_file) local tmpfile = vim.fn.tempname()
vim.fn.setfperm(cookie_file, 'rw-------') vim.fn.writefile({ vim.fn.json_encode(data) }, tmpfile)
vim.fn.setfperm(tmpfile, 'rw-------')
vim.uv.fs_rename(tmpfile, cookie_file)
end end
end end
logger.log(display .. ' credentials cleared', { level = vim.log.levels.INFO, override = true }) logger.log(display .. ' credentials cleared', { level = vim.log.levels.INFO, override = true })

107
lua/cp/git_credential.lua Normal file
View file

@ -0,0 +1,107 @@
---@class cp.Credentials
---@field username string
---@field password string
local M = {}
local HOSTS = {
atcoder = 'atcoder.jp',
codechef = 'www.codechef.com',
codeforces = 'codeforces.com',
cses = 'cses.fi',
kattis = 'open.kattis.com',
usaco = 'usaco.org',
}
local _helper_checked = false
local _helper_ok = false
---@return boolean
function M.has_helper()
if not _helper_checked then
local r = vim
.system({ 'git', 'config', 'credential.helper' }, { text = true, timeout = 5000 })
:wait()
_helper_ok = r.code == 0 and r.stdout ~= nil and vim.trim(r.stdout) ~= ''
_helper_checked = true
end
return _helper_ok
end
---@param host string
---@param extra? table<string, string>
---@return string
local function _build_input(host, extra)
local lines = { 'protocol=https', 'host=' .. host }
if extra then
for k, v in pairs(extra) do
table.insert(lines, k .. '=' .. v)
end
end
table.insert(lines, '')
table.insert(lines, '')
return table.concat(lines, '\n')
end
---@param stdout string
---@return table<string, string>
local function _parse_output(stdout)
local result = {}
for line in stdout:gmatch('[^\n]+') do
local k, v = line:match('^(%S+)=(.+)$')
if k and v then
result[k] = v
end
end
return result
end
---@param platform string
---@return cp.Credentials?
function M.get(platform)
local host = HOSTS[platform]
if not host then
return nil
end
local input = _build_input(host)
local obj = vim
.system({ 'git', 'credential', 'fill' }, { stdin = input, text = true, timeout = 5000 })
:wait()
if obj.code ~= 0 then
return nil
end
local parsed = _parse_output(obj.stdout or '')
if not parsed.username or not parsed.password then
return nil
end
return { username = parsed.username, password = parsed.password }
end
---@param platform string
---@param creds cp.Credentials
function M.store(platform, creds)
local host = HOSTS[platform]
if not host then
return
end
local input = _build_input(host, { username = creds.username, password = creds.password })
vim.system({ 'git', 'credential', 'approve' }, { stdin = input, text = true }):wait()
end
---@param platform string
---@param creds cp.Credentials
function M.reject(platform, creds)
local host = HOSTS[platform]
if not host or not creds then
return
end
local input = _build_input(host, { username = creds.username, password = creds.password })
vim.system({ 'git', 'credential', 'reject' }, { stdin = input, text = true }):wait()
end
return M

View file

@ -5,12 +5,12 @@ local utils = require('cp.utils')
local function check() local function check()
vim.health.start('cp.nvim [required] ~') vim.health.start('cp.nvim [required] ~')
utils.setup_python_env() local nvim_ver = vim.version()
local nvim_str = ('%d.%d.%d'):format(nvim_ver.major, nvim_ver.minor, nvim_ver.patch)
if vim.fn.has('nvim-0.10.0') == 1 then if vim.fn.has('nvim-0.10.0') == 1 then
vim.health.ok('Neovim 0.10.0+ detected') vim.health.ok('Neovim >= 0.10.0: ' .. nvim_str)
else else
vim.health.error('cp.nvim requires Neovim 0.10.0+') vim.health.error('Neovim >= 0.10.0 required, found ' .. nvim_str)
end end
local uname = vim.uv.os_uname() local uname = vim.uv.os_uname()
@ -18,6 +18,24 @@ local function check()
vim.health.error('Windows is not supported') vim.health.error('Windows is not supported')
end end
local time_cap = utils.time_capability()
if time_cap.ok then
vim.health.ok('GNU time found: ' .. time_cap.path)
else
vim.health.error('GNU time not found: ' .. (time_cap.reason or ''))
end
local timeout_cap = utils.timeout_capability()
if timeout_cap.ok then
vim.health.ok('GNU timeout found: ' .. timeout_cap.path)
else
vim.health.error('GNU timeout not found: ' .. (timeout_cap.reason or ''))
end
vim.health.start('cp.nvim [optional] ~')
utils.setup_python_env()
if utils.is_nix_build() then if utils.is_nix_build() then
local source = utils.is_nix_discovered() and 'runtime discovery' or 'flake install' local source = utils.is_nix_discovered() and 'runtime discovery' or 'flake install'
vim.health.ok('Nix Python environment detected (' .. source .. ')') vim.health.ok('Nix Python environment detected (' .. source .. ')')
@ -51,18 +69,30 @@ local function check()
end end
end end
local time_cap = utils.time_capability() if vim.fn.executable('git') == 1 then
if time_cap.ok then local r = vim.system({ 'git', '--version' }, { text = true }):wait()
vim.health.ok('GNU time found: ' .. time_cap.path) if r.code == 0 then
local major, minor, patch = r.stdout:match('(%d+)%.(%d+)%.(%d+)')
major, minor, patch = tonumber(major), tonumber(minor), tonumber(patch or 0)
local ver_str = ('%d.%d.%d'):format(major or 0, minor or 0, patch or 0)
if
major
and (major > 1 or (major == 1 and minor > 7) or (major == 1 and minor == 7 and patch >= 9))
then
vim.health.ok('git >= 1.7.9: ' .. ver_str)
else else
vim.health.error('GNU time not found: ' .. (time_cap.reason or '')) vim.health.warn('git >= 1.7.9 required for credential storage, found ' .. ver_str)
end
end end
local timeout_cap = utils.timeout_capability() local helper = vim.system({ 'git', 'config', 'credential.helper' }, { text = true }):wait()
if timeout_cap.ok then if helper.code == 0 and helper.stdout and vim.trim(helper.stdout) ~= '' then
vim.health.ok('GNU timeout found: ' .. timeout_cap.path) vim.health.ok('git credential helper: ' .. vim.trim(helper.stdout))
else else
vim.health.error('GNU timeout not found: ' .. (timeout_cap.reason or '')) vim.health.warn('no git credential helper configured (required for login/submit)')
end
else
vim.health.warn('git not found (required for credential storage)')
end end
end end

View file

@ -347,7 +347,9 @@ function M.login(platform, credentials, on_status, callback)
stdin = vim.json.encode(credentials), stdin = vim.json.encode(credentials),
on_event = function(ev) on_event = function(ev)
if ev.credentials ~= nil and next(ev.credentials) ~= nil then if ev.credentials ~= nil and next(ev.credentials) ~= nil then
require('cp.cache').set_credentials(platform, ev.credentials) vim.schedule(function()
require('cp.git_credential').store(platform, ev.credentials)
end)
end end
if ev.status ~= nil then if ev.status ~= nil then
if type(on_status) == 'function' then if type(on_status) == 'function' then
@ -395,7 +397,9 @@ function M.submit(
stdin = vim.json.encode(credentials), stdin = vim.json.encode(credentials),
on_event = function(ev) on_event = function(ev)
if ev.credentials ~= nil and next(ev.credentials) ~= nil then if ev.credentials ~= nil and next(ev.credentials) ~= nil then
require('cp.cache').set_credentials(platform, ev.credentials) vim.schedule(function()
require('cp.git_credential').store(platform, ev.credentials)
end)
end end
if ev.status ~= nil then if ev.status ~= nil then
if type(on_status) == 'function' then if type(on_status) == 'function' then

View file

@ -1,8 +1,8 @@
local M = {} local M = {}
local cache = require('cp.cache')
local config = require('cp.config') local config = require('cp.config')
local constants = require('cp.constants') local constants = require('cp.constants')
local git_credential = require('cp.git_credential')
local logger = require('cp.log') local logger = require('cp.log')
local state = require('cp.state') local state = require('cp.state')
@ -14,7 +14,7 @@ local STATUS_MSGS = {
} }
local function prompt_credentials(platform, callback) local function prompt_credentials(platform, callback)
local saved = cache.get_credentials(platform) local saved = git_credential.get(platform)
if saved and saved.username and saved.password then if saved and saved.username and saved.password then
callback(saved) callback(saved)
return return
@ -42,6 +42,14 @@ end
---@param opts { language?: string }? ---@param opts { language?: string }?
function M.submit(opts) function M.submit(opts)
if not git_credential.has_helper() then
logger.log(
'No git credential helper configured. See :help cp-credentials',
{ level = vim.log.levels.ERROR }
)
return
end
local platform = state.get_platform() local platform = state.get_platform()
local contest_id = state.get_contest_id() local contest_id = state.get_contest_id()
local problem_id = state.get_problem_id() local problem_id = state.get_problem_id()
@ -109,12 +117,12 @@ function M.submit(opts)
function(result) function(result)
vim.schedule(function() vim.schedule(function()
if result and result.success then if result and result.success then
cache.set_credentials(platform, creds) git_credential.store(platform, creds)
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 == 'bad_credentials' or err:match('^Login failed') then if err == 'bad_credentials' or err:match('^Login failed') then
cache.clear_credentials(platform) git_credential.reject(platform, creds)
logger.log( logger.log(
'Submit failed: ' .. (constants.LOGIN_ERRORS[err] or err), 'Submit failed: ' .. (constants.LOGIN_ERRORS[err] or err),
{ level = vim.log.levels.ERROR } { level = vim.log.levels.ERROR }

View file

@ -366,6 +366,10 @@ function M.check_required_runtime()
return false, timeout.reason return false, timeout.reason
end end
if vim.fn.executable('git') ~= 1 then
return false, 'git is required for credential storage'
end
return true return true
end end

View file

@ -8,6 +8,7 @@ vim.api.nvim_create_user_command('CP', function(opts)
cp.handle_command(opts) cp.handle_command(opts)
end, { end, {
nargs = '*', nargs = '*',
bar = true,
desc = 'Competitive programming helper', desc = 'Competitive programming helper',
complete = function(ArgLead, CmdLine, _) complete = function(ArgLead, CmdLine, _)
local constants = require('cp.constants') local constants = require('cp.constants')

View file

@ -3,6 +3,7 @@ import json
import os import os
import re import re
import sys import sys
import tempfile
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -20,6 +21,18 @@ from .models import (
_COOKIE_FILE = Path.home() / ".cache" / "cp-nvim" / "cookies.json" _COOKIE_FILE = Path.home() / ".cache" / "cp-nvim" / "cookies.json"
def _atomic_write(path: Path, content: str) -> None:
fd, tmp = tempfile.mkstemp(dir=path.parent, prefix=".tmp-")
try:
os.fchmod(fd, 0o600)
with os.fdopen(fd, "w") as f:
f.write(content)
os.replace(tmp, path)
except BaseException:
os.unlink(tmp)
raise
def load_platform_cookies(platform: str) -> Any | None: def load_platform_cookies(platform: str) -> Any | None:
try: try:
data = json.loads(_COOKIE_FILE.read_text()) data = json.loads(_COOKIE_FILE.read_text())
@ -29,22 +42,20 @@ def load_platform_cookies(platform: str) -> Any | None:
def save_platform_cookies(platform: str, data: Any) -> None: def save_platform_cookies(platform: str, data: Any) -> None:
_COOKIE_FILE.parent.mkdir(parents=True, exist_ok=True) _COOKIE_FILE.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
try: try:
existing = json.loads(_COOKIE_FILE.read_text()) existing = json.loads(_COOKIE_FILE.read_text())
except Exception: except Exception:
existing = {} existing = {}
existing[platform] = data existing[platform] = data
_COOKIE_FILE.write_text(json.dumps(existing)) _atomic_write(_COOKIE_FILE, json.dumps(existing))
_COOKIE_FILE.chmod(0o600)
def clear_platform_cookies(platform: str) -> None: def clear_platform_cookies(platform: str) -> None:
try: try:
existing = json.loads(_COOKIE_FILE.read_text()) existing = json.loads(_COOKIE_FILE.read_text())
existing.pop(platform, None) existing.pop(platform, None)
_COOKIE_FILE.write_text(json.dumps(existing)) _atomic_write(_COOKIE_FILE, json.dumps(existing))
_COOKIE_FILE.chmod(0o600)
except Exception: except Exception:
pass pass

View file

@ -8,7 +8,7 @@ from typing import Any
import httpx import httpx
from .base import BaseScraper, extract_precision from .base import BaseScraper, extract_precision, load_platform_cookies, save_platform_cookies
from .timeouts import HTTP_TIMEOUT from .timeouts import HTTP_TIMEOUT
from .models import ( from .models import (
ContestListResult, ContestListResult,
@ -248,35 +248,20 @@ 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") token = load_platform_cookies("cses")
async with httpx.AsyncClient(follow_redirects=True) as client: async with httpx.AsyncClient(follow_redirects=True) as client:
if token: if token:
print(json.dumps({"status": "checking_login"}), flush=True) print(json.dumps({"status": "checking_login"}), flush=True)
if await self._check_token(client, token): if await self._check_token(client, token):
return LoginResult( return LoginResult(success=True, error="")
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:
return self._login_error("bad_credentials") return self._login_error("bad_credentials")
return LoginResult( save_platform_cookies("cses", token)
success=True, return LoginResult(success=True, error="")
error="",
credentials={
"username": username,
"password": password,
"token": token,
},
)
async def stream_tests_for_category_async(self, category_id: str) -> None: async def stream_tests_for_category_async(self, category_id: str) -> None:
async with httpx.AsyncClient( async with httpx.AsyncClient(
@ -423,7 +408,7 @@ class CSESScraper(BaseScraper):
return self._submit_error("Missing credentials. Use :CP login cses") return self._submit_error("Missing credentials. Use :CP login cses")
async with httpx.AsyncClient(follow_redirects=True) as client: async with httpx.AsyncClient(follow_redirects=True) as client:
token = credentials.get("token") token = load_platform_cookies("cses")
if token: if token:
print(json.dumps({"status": "checking_login"}), flush=True) print(json.dumps({"status": "checking_login"}), flush=True)
@ -435,18 +420,7 @@ class CSESScraper(BaseScraper):
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("bad_credentials") return self._submit_error("bad_credentials")
print( save_platform_cookies("cses", token)
json.dumps(
{
"credentials": {
"username": username,
"password": password,
"token": token,
}
}
),
flush=True,
)
print(json.dumps({"status": "submitting"}), flush=True) print(json.dumps({"status": "submitting"}), flush=True)