local oauth = require('pending.sync.oauth') local config = require('pending.config') local M = {} M.name = 'gcal' local BASE_URL = 'https://www.googleapis.com/calendar/v3' local SCOPE = 'https://www.googleapis.com/auth/calendar' local client = oauth.new({ name = 'gcal', scope = SCOPE, port = 18392, config_key = 'gcal', }) ---@return string? calendar_id ---@return string? err local function find_or_create_calendar(access_token) local cfg = config.get() local gc = (cfg.sync and cfg.sync.gcal) or {} local cal_name = gc.calendar or 'Pendings' local data, err = oauth.curl_request('GET', BASE_URL .. '/users/me/calendarList', oauth.auth_headers(access_token)) if err then return nil, err end for _, item in ipairs(data and data.items or {}) do if item.summary == cal_name then return item.id, nil end end local body = vim.json.encode({ summary = cal_name }) local created, create_err = oauth.curl_request('POST', BASE_URL .. '/calendars', oauth.auth_headers(access_token), body) if create_err then return nil, create_err end return created and created.id, nil end ---@param date_str string ---@return string local function next_day(date_str) local y, m, d = date_str:match('^(%d%d%d%d)-(%d%d)-(%d%d)$') local t = os.time({ year = tonumber(y) or 0, month = tonumber(m) or 0, day = tonumber(d) or 0 }) + 86400 return os.date('%Y-%m-%d', t) --[[@as string]] end ---@param access_token string ---@param calendar_id string ---@param task pending.Task ---@return string? event_id ---@return string? err local function create_event(access_token, calendar_id, task) local event = { summary = task.description, start = { date = task.due }, ['end'] = { date = next_day(task.due or '') }, transparency = 'transparent', extendedProperties = { private = { taskId = tostring(task.id) }, }, } local data, err = oauth.curl_request( 'POST', BASE_URL .. '/calendars/' .. oauth.url_encode(calendar_id) .. '/events', oauth.auth_headers(access_token), vim.json.encode(event) ) if err then return nil, err end return data and data.id, nil end ---@param access_token string ---@param calendar_id string ---@param event_id string ---@param task pending.Task ---@return string? err local function update_event(access_token, calendar_id, event_id, task) local event = { summary = task.description, start = { date = task.due }, ['end'] = { date = next_day(task.due or '') }, } local _, err = oauth.curl_request( 'PATCH', BASE_URL .. '/calendars/' .. oauth.url_encode(calendar_id) .. '/events/' .. oauth.url_encode(event_id), oauth.auth_headers(access_token), vim.json.encode(event) ) return err end ---@param access_token string ---@param calendar_id string ---@param event_id string ---@return string? err local function delete_event(access_token, calendar_id, event_id) local _, err = oauth.curl_request( 'DELETE', BASE_URL .. '/calendars/' .. oauth.url_encode(calendar_id) .. '/events/' .. oauth.url_encode(event_id), oauth.auth_headers(access_token) ) return err end function M.auth() client:auth() end function M.sync() local access_token = client:get_access_token() if not access_token then return end local calendar_id, err = find_or_create_calendar(access_token) if err or not calendar_id then vim.notify('pending.nvim: ' .. (err or 'calendar not found'), vim.log.levels.ERROR) return end local tasks = require('pending').store():tasks() local created, updated, deleted = 0, 0, 0 for _, task in ipairs(tasks) do local extra = task._extra or {} local event_id = extra['_gcal_event_id'] --[[@as string?]] local should_delete = event_id ~= nil and ( task.status == 'done' or task.status == 'deleted' or (task.status == 'pending' and not task.due) ) if should_delete and event_id then local del_err = delete_event(access_token, calendar_id, event_id) --[[@as string]] if not del_err then extra['_gcal_event_id'] = nil if next(extra) == nil then task._extra = nil else task._extra = extra end task.modified = os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]] deleted = deleted + 1 end elseif task.status == 'pending' and task.due then if event_id then local upd_err = update_event(access_token, calendar_id, event_id, task) if not upd_err then updated = updated + 1 end else local new_id, create_err = create_event(access_token, calendar_id, task) if not create_err and new_id then if not task._extra then task._extra = {} end task._extra['_gcal_event_id'] = new_id task.modified = os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]] created = created + 1 end end end end require('pending').store():save() require('pending')._recompute_counts() vim.notify( string.format( 'pending.nvim: Synced to Google Calendar (created: %d, updated: %d, deleted: %d)', created, updated, deleted ) ) end ---@return nil function M.health() oauth.health(M.name) end return M