diff --git a/doc/cp.nvim.txt b/doc/cp.nvim.txt index 4fc684c..670bdab 100644 --- a/doc/cp.nvim.txt +++ b/doc/cp.nvim.txt @@ -453,6 +453,7 @@ COMMANDS *cp-commands* any previously saved credentials. If [platform] is omitted, uses the active platform. Examples: > + :CP login :CP login atcoder :CP login codeforces < @@ -460,6 +461,7 @@ COMMANDS *cp-commands* Remove stored credentials for a platform. If [platform] is omitted, uses the active platform. Examples: > + :CP logout :CP logout atcoder < :CP {platform} signup diff --git a/lua/cp/cache.lua b/lua/cp/cache.lua index 9448910..7ff1824 100644 --- a/lua/cp/cache.lua +++ b/lua/cp/cache.lua @@ -57,6 +57,7 @@ function M.load() if vim.fn.filereadable(cache_file) == 0 then vim.fn.writefile({}, cache_file) + vim.fn.setfperm(cache_file, 'rw-------') loaded = true return end @@ -107,6 +108,7 @@ function M.save() local encoded = vim.json.encode(cache_data) local lines = vim.split(encoded, '\n') vim.fn.writefile(lines, cache_file) + vim.fn.setfperm(cache_file, 'rw-------') end) end diff --git a/lua/cp/commands/init.lua b/lua/cp/commands/init.lua index 9ea34a4..c13b196 100644 --- a/lua/cp/commands/init.lua +++ b/lua/cp/commands/init.lua @@ -326,6 +326,10 @@ local function parse_command(args) end end + if (first == 'login' or first == 'logout' or first == 'signup') and #args == 1 then + return { type = 'action', action = first, requires_context = true, platform = nil } + end + if #args == 1 then return { type = 'problem_jump', @@ -378,6 +382,7 @@ function M.handle_command(opts) if not restore.restore_from_current_file() then return end + vim.cmd.redraw() end local setup = require('cp.setup') @@ -421,24 +426,45 @@ function M.handle_command(opts) end vim.ui.open(url) elseif cmd.action == 'login' then - if not check_platform_enabled(cmd.platform) then - return - end - require('cp.credentials').login(cmd.platform) - elseif cmd.action == 'logout' then - if not check_platform_enabled(cmd.platform) then - return - end - require('cp.credentials').logout(cmd.platform) - elseif cmd.action == 'signup' then - local url = constants.SIGNUP_URLS[cmd.platform] - if not url then + local p = cmd.platform or state.get_platform() + if not p then logger.log( - ("No signup URL available for '%s'"):format(cmd.platform), - { level = vim.log.levels.WARN } + 'No platform active. Usage: :CP login', + { level = vim.log.levels.ERROR } ) return end + if not check_platform_enabled(p) then + return + end + require('cp.credentials').login(p) + elseif cmd.action == 'logout' then + local p = cmd.platform or state.get_platform() + if not p then + logger.log( + 'No platform active. Usage: :CP logout', + { level = vim.log.levels.ERROR } + ) + return + end + if not check_platform_enabled(p) then + return + end + require('cp.credentials').logout(p) + elseif cmd.action == 'signup' then + local p = cmd.platform or state.get_platform() + if not p then + logger.log( + 'No platform active. Usage: :CP signup', + { level = vim.log.levels.ERROR } + ) + return + end + local url = constants.SIGNUP_URLS[p] + if not url then + logger.log(("No signup URL available for '%s'"):format(p), { level = vim.log.levels.WARN }) + return + end vim.ui.open(url) end elseif cmd.type == 'problem_jump' then diff --git a/lua/cp/credentials.lua b/lua/cp/credentials.lua index a1258c4..4f22038 100644 --- a/lua/cp/credentials.lua +++ b/lua/cp/credentials.lua @@ -14,46 +14,50 @@ local STATUS_MESSAGES = { ---@param platform string ---@param display string local function prompt_and_login(platform, display) - vim.ui.input({ prompt = '[cp.nvim]: ' .. display .. ' username: ' }, function(username) - if not username or username == '' then - logger.log('Cancelled', { level = vim.log.levels.WARN }) - return - end - vim.fn.inputsave() - local password = vim.fn.inputsecret('[cp.nvim]: ' .. display .. ' password: ') - vim.fn.inputrestore() - if not password or password == '' then - logger.log('Cancelled', { level = vim.log.levels.WARN }) - return - end + vim.ui.input( + { prompt = '[cp.nvim]: ' .. display .. ' username ( to cancel): ' }, + function(username) + if not username or username == '' then + logger.log(display .. ' login cancelled', { level = vim.log.levels.WARN }) + return + end + vim.fn.inputsave() + local password = vim.fn.inputsecret('[cp.nvim]: ' .. display .. ' password: ') + vim.fn.inputrestore() + if not password or password == '' then + logger.log(display .. ' login cancelled', { level = vim.log.levels.WARN }) + return + end - local credentials = { username = username, password = password } + local credentials = { username = username, password = password } - local scraper = require('cp.scraper') - scraper.login(platform, credentials, function(ev) - vim.schedule(function() - local msg = STATUS_MESSAGES[ev.status] or ev.status - logger.log(display .. ': ' .. msg, { level = vim.log.levels.INFO, override = true }) + local scraper = require('cp.scraper') + scraper.login(platform, credentials, function(ev) + vim.schedule(function() + local msg = STATUS_MESSAGES[ev.status] or ev.status + logger.log(display .. ': ' .. msg, { level = vim.log.levels.INFO, override = true }) + end) + end, function(result) + vim.schedule(function() + if result.success then + cache.set_credentials(platform, credentials) + logger.log( + display .. ' login successful', + { level = vim.log.levels.INFO, override = true } + ) + else + local err = result.error or 'unknown error' + cache.clear_credentials(platform) + logger.log( + display .. ' login failed: ' .. (constants.LOGIN_ERRORS[err] or err), + { level = vim.log.levels.ERROR } + ) + prompt_and_login(platform, display) + end + end) end) - end, function(result) - vim.schedule(function() - if result.success then - cache.set_credentials(platform, credentials) - logger.log( - display .. ' login successful', - { level = vim.log.levels.INFO, override = true } - ) - else - local err = result.error or 'unknown 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 ---@param platform string? @@ -117,6 +121,7 @@ function M.logout(platform) if ok and type(data) == 'table' then data[platform] = nil vim.fn.writefile({ vim.fn.json_encode(data) }, cookie_file) + vim.fn.setfperm(cookie_file, 'rw-------') end end logger.log(display .. ' credentials cleared', { level = vim.log.levels.INFO, override = true }) diff --git a/lua/cp/scraper.lua b/lua/cp/scraper.lua index 02f20b3..4ad46bc 100644 --- a/lua/cp/scraper.lua +++ b/lua/cp/scraper.lua @@ -344,7 +344,7 @@ function M.login(platform, credentials, on_status, callback) local done = false run_scraper(platform, 'login', {}, { ndjson = true, - env_extra = { CP_CREDENTIALS = vim.json.encode(credentials) }, + stdin = vim.json.encode(credentials), on_event = function(ev) if ev.credentials ~= nil and next(ev.credentials) ~= nil then require('cp.cache').set_credentials(platform, ev.credentials) @@ -392,9 +392,9 @@ function M.submit( local done = false run_scraper(platform, 'submit', { contest_id, problem_id, language, source_file }, { ndjson = true, - env_extra = { CP_CREDENTIALS = vim.json.encode(credentials) }, + stdin = vim.json.encode(credentials), on_event = function(ev) - if ev.credentials ~= nil then + if ev.credentials ~= nil and next(ev.credentials) ~= nil then require('cp.cache').set_credentials(platform, ev.credentials) end if ev.status ~= nil then diff --git a/lua/cp/submit.lua b/lua/cp/submit.lua index 68b9b90..5a661ec 100644 --- a/lua/cp/submit.lua +++ b/lua/cp/submit.lua @@ -19,23 +19,27 @@ local function prompt_credentials(platform, callback) callback(saved) return end - vim.ui.input({ prompt = platform .. ' username: ' }, function(username) - if not username or username == '' then - logger.log('Submit cancelled', { level = vim.log.levels.WARN }) - return + local display = constants.PLATFORM_DISPLAY_NAMES[platform] or platform + vim.ui.input( + { prompt = '[cp.nvim]: ' .. display .. ' username ( to cancel): ' }, + function(username) + if not username or username == '' then + logger.log('Submit cancelled', { level = vim.log.levels.WARN }) + return + end + vim.fn.inputsave() + local password = vim.fn.inputsecret('[cp.nvim]: ' .. display .. ' password: ') + vim.fn.inputrestore() + vim.cmd.redraw() + if not password or password == '' then + logger.log('Submit cancelled', { level = vim.log.levels.WARN }) + return + end + local creds = { username = username, password = password } + cache.set_credentials(platform, creds) + callback(creds) end - vim.fn.inputsave() - local password = vim.fn.inputsecret(platform .. ' password: ') - vim.fn.inputrestore() - vim.cmd.redraw() - if not password or password == '' then - logger.log('Submit cancelled', { level = vim.log.levels.WARN }) - return - end - local creds = { username = username, password = password } - cache.set_credentials(platform, creds) - callback(creds) - end) + ) end ---@param opts { language?: string }? @@ -86,7 +90,7 @@ function M.submit(opts) end end - prompt_credentials(platform, function(creds) + local function do_submit(creds) vim.cmd.update() require('cp.scraper').submit( @@ -112,16 +116,24 @@ function M.submit(opts) local err = result and result.error or 'unknown error' if err == 'bad_credentials' or err:match('^Login failed') then cache.clear_credentials(platform) + logger.log( + 'Submit failed: ' .. (constants.LOGIN_ERRORS[err] or err), + { level = vim.log.levels.ERROR } + ) + prompt_credentials(platform, do_submit) + else + logger.log( + 'Submit failed: ' .. (constants.LOGIN_ERRORS[err] or err), + { level = vim.log.levels.ERROR } + ) end - logger.log( - 'Submit failed: ' .. (constants.LOGIN_ERRORS[err] or err), - { level = vim.log.levels.ERROR } - ) end end) end ) - end) + end + + prompt_credentials(platform, do_submit) end return M diff --git a/plugin/cp.lua b/plugin/cp.lua index 531a5f3..b1430f2 100644 --- a/plugin/cp.lua +++ b/plugin/cp.lua @@ -43,6 +43,10 @@ end, { vim.list_extend(candidates, platforms) table.insert(candidates, 'cache') table.insert(candidates, 'pick') + if platform then + table.insert(candidates, 'login') + table.insert(candidates, 'logout') + end if platform and contest_id then vim.list_extend( candidates, diff --git a/scrapers/base.py b/scrapers/base.py index 035495a..e98990c 100644 --- a/scrapers/base.py +++ b/scrapers/base.py @@ -36,6 +36,7 @@ def save_platform_cookies(platform: str, data: Any) -> None: existing = {} existing[platform] = data _COOKIE_FILE.write_text(json.dumps(existing)) + _COOKIE_FILE.chmod(0o600) def clear_platform_cookies(platform: str) -> None: @@ -43,6 +44,7 @@ def clear_platform_cookies(platform: str) -> None: existing = json.loads(_COOKIE_FILE.read_text()) existing.pop(platform, None) _COOKIE_FILE.write_text(json.dumps(existing)) + _COOKIE_FILE.chmod(0o600) except Exception: pass @@ -160,7 +162,7 @@ class BaseScraper(ABC): ).model_dump_json() ) return 1 - creds_raw = os.environ.get("CP_CREDENTIALS", "{}") + creds_raw = sys.stdin.read() try: credentials = json.loads(creds_raw) except json.JSONDecodeError: @@ -173,7 +175,7 @@ class BaseScraper(ABC): return 0 if result.success else 1 case "login": - creds_raw = os.environ.get("CP_CREDENTIALS", "{}") + creds_raw = sys.stdin.read() try: credentials = json.loads(creds_raw) except json.JSONDecodeError: diff --git a/scrapers/kattis.py b/scrapers/kattis.py index 8b0099f..4417628 100644 --- a/scrapers/kattis.py +++ b/scrapers/kattis.py @@ -415,7 +415,6 @@ class KattisScraper(BaseScraper): return LoginResult( success=True, error="", - credentials={"username": username, "password": password}, ) print(json.dumps({"status": "logging_in"}), flush=True) @@ -426,7 +425,6 @@ class KattisScraper(BaseScraper): return LoginResult( success=True, error="", - credentials={"username": username, "password": password}, ) diff --git a/scrapers/usaco.py b/scrapers/usaco.py index e463881..d878886 100644 --- a/scrapers/usaco.py +++ b/scrapers/usaco.py @@ -533,7 +533,6 @@ class USACOScraper(BaseScraper): return LoginResult( success=True, error="", - credentials={"username": username, "password": password}, ) print(json.dumps({"status": "logging_in"}), flush=True) @@ -549,7 +548,6 @@ class USACOScraper(BaseScraper): return LoginResult( success=True, error="", - credentials={"username": username, "password": password}, )