local config = require('todo.config') local store = require('todo.store') local M = {} local BASE_URL = 'https://www.googleapis.com/calendar/v3' local TOKEN_URL = 'https://oauth2.googleapis.com/token' local AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth' local SCOPE = 'https://www.googleapis.com/auth/calendar' local function gcal_config() local cfg = config.get() return cfg.gcal or {} end local function token_path() return vim.fn.stdpath('data') .. '/todo/gcal_tokens.json' end local function credentials_path() local gc = gcal_config() return gc.credentials_path or (vim.fn.stdpath('data') .. '/todo/gcal_credentials.json') end local function load_json_file(path) local f = io.open(path, 'r') if not f then return nil end local content = f:read('*a') f:close() if content == '' then return nil end local ok, decoded = pcall(vim.json.decode, content) if not ok then return nil end return decoded end local function save_json_file(path, data) local dir = vim.fn.fnamemodify(path, ':h') if vim.fn.isdirectory(dir) == 0 then vim.fn.mkdir(dir, 'p') end local f = io.open(path, 'w') if not f then return false end f:write(vim.json.encode(data)) f:close() vim.fn.setfperm(path, 'rw-------') return true end local function load_credentials() local creds = load_json_file(credentials_path()) if not creds then return nil end if creds.installed then return creds.installed end return creds end local function load_tokens() return load_json_file(token_path()) end local function save_tokens(tokens) return save_json_file(token_path(), tokens) end local function url_encode(str) return str:gsub('([^%w%-%.%_%~])', function(c) return string.format('%%%02X', string.byte(c)) end) end local function curl_request(method, url, headers, body) local args = { 'curl', '-s', '-X', method } for _, h in ipairs(headers or {}) do table.insert(args, '-H') table.insert(args, h) end if body then table.insert(args, '-d') table.insert(args, body) end table.insert(args, url) local result = vim.system(args, { text = true }):wait() if result.code ~= 0 then return nil, 'curl failed: ' .. (result.stderr or '') end if not result.stdout or result.stdout == '' then return {}, nil end local ok, decoded = pcall(vim.json.decode, result.stdout) if not ok then return nil, 'failed to parse response: ' .. result.stdout end if decoded.error then return nil, 'API error: ' .. (decoded.error.message or vim.json.encode(decoded.error)) end return decoded, nil end local function auth_headers(access_token) return { 'Authorization: Bearer ' .. access_token, 'Content-Type: application/json', } end local function refresh_access_token(creds, tokens) local body = 'client_id=' .. url_encode(creds.client_id) .. '&client_secret=' .. url_encode(creds.client_secret) .. '&grant_type=refresh_token' .. '&refresh_token=' .. url_encode(tokens.refresh_token) local result = vim .system({ 'curl', '-s', '-X', 'POST', '-H', 'Content-Type: application/x-www-form-urlencoded', '-d', body, TOKEN_URL, }, { text = true }) :wait() if result.code ~= 0 then return nil end local ok, decoded = pcall(vim.json.decode, result.stdout or '') if not ok or not decoded.access_token then return nil end tokens.access_token = decoded.access_token tokens.expires_in = decoded.expires_in tokens.obtained_at = os.time() save_tokens(tokens) return tokens end local function get_access_token() local creds = load_credentials() if not creds then vim.notify( 'todo.nvim: No Google Calendar credentials found at ' .. credentials_path(), vim.log.levels.ERROR ) return nil end local tokens = load_tokens() if not tokens or not tokens.refresh_token then M.authorize() tokens = load_tokens() if not tokens then return nil end end local now = os.time() local obtained = tokens.obtained_at or 0 local expires = tokens.expires_in or 3600 if now - obtained > expires - 60 then tokens = refresh_access_token(creds, tokens) if not tokens then vim.notify('todo.nvim: Failed to refresh access token.', vim.log.levels.ERROR) return nil end end return tokens.access_token end function M.authorize() local creds = load_credentials() if not creds then vim.notify( 'todo.nvim: No Google Calendar credentials found at ' .. credentials_path(), vim.log.levels.ERROR ) return end local port = 18392 local verifier_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~' local verifier = {} math.randomseed(os.time()) for _ = 1, 64 do local idx = math.random(1, #verifier_chars) table.insert(verifier, verifier_chars:sub(idx, idx)) end local code_verifier = table.concat(verifier) local sha_result = vim.system({ 'printf', '%s', code_verifier }, { text = true }):wait() local sha_pipe = vim .system( { 'sh', '-c', 'printf "%s" "' .. code_verifier .. '" | openssl dgst -sha256 -binary | openssl base64 -A | tr "+/" "-_" | tr -d "="', }, { text = true } ) :wait() local code_challenge = sha_pipe.stdout or '' local auth_url = AUTH_URL .. '?client_id=' .. url_encode(creds.client_id) .. '&redirect_uri=' .. url_encode('http://127.0.0.1:' .. port) .. '&response_type=code' .. '&scope=' .. url_encode(SCOPE) .. '&access_type=offline' .. '&prompt=consent' .. '&code_challenge=' .. url_encode(code_challenge) .. '&code_challenge_method=S256' vim.ui.open(auth_url) vim.notify('todo.nvim: Opening browser for Google authorization...') local server = vim.uv.new_tcp() server:bind('127.0.0.1', port) server:listen(1, function(err) if err then return end local client = vim.uv.new_tcp() server:accept(client) client:read_start(function(read_err, data) if read_err or not data then return end local code = data:match('[?&]code=([^&%s]+)') local response_body = code and '
You can close this tab.
' or '