--- jasper/api.lua --- Low-level HTTP calls to the Jasper API, executed via curl. local M = {} local JASPER_URL = "https://jasper.4sigma.it" --- Run a curl command and return the parsed JSON body + HTTP status code. --- @param args string[] full curl argument list (without the URL, which is last) --- @return table|nil decoded JSON body --- @return number HTTP status code (0 on curl error) --- @return string|nil error message local function curl(args) -- Append -w to get the HTTP status on a separate last line local full_args = vim.list_extend(vim.list_slice(args, 1), { "-s", "-w", "\n%{http_code}" }) -- args already contains the URL; rebuild properly: -- Actually we receive the full arg list already. Add -w trick. table.insert(args, "-w") table.insert(args, "\n%{http_code}") local result = vim.system(args, { text = true }):wait() if result.code ~= 0 then return nil, 0, "curl error (exit " .. result.code .. ")" end -- Split body and status code local last_newline = result.stdout:match(".*\n()") local status_code = tonumber(result.stdout:sub(last_newline)) or 0 local body = result.stdout:sub(1, last_newline - 2) -- strip trailing \n + status line local ok, data = pcall(vim.fn.json_decode, body) if not ok then return nil, status_code, "JSON decode error" end return data, status_code, nil end --- @param path string API path, e.g. "/api/v1/timer/" --- @param token string --- @return table|nil, number, string|nil local function get(path, token) return curl({ "curl", "-H", "Authorization: Token " .. token, JASPER_URL .. path, }) end --- @param path string --- @param token string --- @param form table|nil key/value pairs sent as form data --- @return table|nil, number, string|nil local function post(path, token, form) local args = { "curl", "-X", "POST", "-H", "Authorization: Token " .. token, } if form then for k, v in pairs(form) do table.insert(args, "--data-urlencode") table.insert(args, k .. "=" .. tostring(v)) end end table.insert(args, JASPER_URL .. path) return curl(args) end -- --------------------------------------------------------------------------- -- Public API -- --------------------------------------------------------------------------- --- Fetch all timers for the current user. --- @param token string --- @return table[]|nil list of timer objects --- @return string|nil error message function M.get_timers(token) local data, status, err = get("/api/v1/timer/", token) if err then return nil, err end if status == 401 then return nil, "unauthorized" end if status ~= 200 then return nil, "unexpected HTTP status " .. status end local timers_dict = data.data and data.data.timers if type(timers_dict) ~= "table" then return nil, "unexpected response shape" end local timers = {} for _, v in pairs(timers_dict) do table.insert(timers, v) end return timers, nil end --- Start (play) a timer. --- @param token string --- @param timer_id number --- @return boolean ok --- @return string|nil error function M.start_timer(token, timer_id) local data, status, err = post("/api/v1/timer/" .. timer_id .. "/play/", token) if err then return false, err end if status ~= 200 then return false, "HTTP " .. status end if data.error ~= nil and data.error ~= vim.NIL then return false, tostring(data.error) end return true, nil end --- Pause a timer. --- @param token string --- @param timer_id number --- @return boolean ok --- @return string|nil error function M.stop_timer(token, timer_id) local data, status, err = post("/api/v1/timer/" .. timer_id .. "/pausa/", token) if err then return false, err end if status ~= 200 then return false, "HTTP " .. status end if data.error ~= nil and data.error ~= vim.NIL then return false, tostring(data.error) end return true, nil end --- Associate an attivita with a (new) timer slot. --- @param token string --- @param timer_id number desired slot id (smallest free integer) --- @param attivita_id number --- @return boolean ok --- @return string|nil error function M.create_timer(token, timer_id, attivita_id) local data, status, err = post( "/api/v1/timer/" .. timer_id .. "/attivita/", token, { attivita_id = attivita_id } ) if err then return false, err end if status ~= 200 then return false, "HTTP " .. status end if data.error ~= nil and data.error ~= vim.NIL then return false, tostring(data.error) end return true, nil end return M