Sync src/VikunjaClient.gs

This commit is contained in:
2026-04-24 17:41:45 +00:00
parent 5ad13796b1
commit 795f85416d
+226
View File
@@ -0,0 +1,226 @@
/**
* VikunjaClient - Low-level API wrapper for Vikunja Task Management
*/
/**
* Helper to ensure the base URL always points to the Vikunja API root (/api/v1)
* @return {string} Normalized API base URL
*/
function getVikunjaApiBaseUrl_() {
const config = getVikunjaConfig();
let baseUrl = config.baseUrl.replace(/\/$/, ''); // Remove trailing slash
if (!baseUrl.endsWith('/api/v1')) {
baseUrl += '/api/v1';
}
return baseUrl;
}
function vikunjaRequest_(method, path, payload = null) {
const apiBaseUrl = getVikunjaApiBaseUrl_();
const normalizedPath = path.startsWith('/') ? path : '/' + path;
const url = `${apiBaseUrl}${normalizedPath}`;
console.log(`Vikunja Request: [${method}] ${url}`);
const config = getVikunjaConfig();
if (!config.token) {
throw new Error('Vikunja Token is missing from configuration.');
}
const options = {
method: method,
headers: {
'Authorization': `Bearer ${config.token}`,
'Content-Type': 'application/json'
},
muteHttpExceptions: true
};
console.log('Vikunja Auth Header Present:', !!options.headers.Authorization);
if (payload) {
options.payload = JSON.stringify(payload);
}
try {
const response = UrlFetchApp.fetch(url, options);
const code = response.getResponseCode();
const rawBody = response.getContentText();
let json = null;
if (rawBody) {
try {
json = JSON.parse(rawBody);
} catch (e) {
console.warn(`Non-JSON response received from Vikunja: ${rawBody.substring(0, 200)}...`);
}
}
if (code >= 200 && code < 300) {
return {
ok: true,
code: code,
data: json,
rawBody: rawBody,
message: 'Request successful'
};
}
return {
ok: false,
code: code,
data: json,
rawBody: rawBody,
message: json && json.message ? json.message : `Vikunja API Error ${code}`
};
} catch (e) {
return {
ok: false,
code: 500,
data: null,
rawBody: null,
message: `Vikunja Connection Error: ${e.message}`
};
}
}
function getVikunjaTask(taskId) {
return vikunjaRequest_('GET', `/tasks/${taskId}`);
}
function createVikunjaComment(taskId, commentText) {
const payload = { comment: commentText };
const path = `/tasks/${taskId}/comments`;
console.log(`Vikunja Comment Create Attempt: [PUT] ${path} | Payload: ${JSON.stringify(payload)}`);
const res = vikunjaRequest_('PUT', path, payload);
console.log(`Vikunja Comment Response: Code ${res.code} | Body: ${res.rawBody}`);
return res;
}
function updateVikunjaTask(taskId, data) {
console.log(`Vikunja Task Update Attempt: [POST] /tasks/${taskId} | Payload: ${JSON.stringify(data)}`);
const res = vikunjaRequest_('POST', `/tasks/${taskId}`, data);
console.log(`Vikunja Task Update Response: Code ${res.code} | Body: ${res.rawBody}`);
return res;
}
function setVikunjaTaskDone(taskId, isDone) {
return updateVikunjaTask(taskId, { done: isDone });
}
function createVikunjaTask(taskData) {
const projectId = taskData.projectId || taskData.project_id;
if (!projectId) {
return { ok: false, message: 'projectId is required to create a task in Vikunja.' };
}
// Construct full payload including priority and description
const payload = {
title: taskData.title,
description: taskData.description || '',
priority: typeof taskData.priority === 'number' ? taskData.priority : 0
};
const endpoint = `/projects/${projectId}/tasks`;
console.log(`Vikunja Create Attempt: [PUT] ${endpoint} | Payload: ${JSON.stringify(payload)}`);
// Using PUT as per instance-specific evidence (POST returns 405)
const res = vikunjaRequest_('PUT', endpoint, payload);
console.log(`Vikunja Response: Code ${res.code} | Body: ${res.rawBody}`);
if (res.ok && res.data && res.data.id) {
return {
ok: true,
code: res.code,
data: res.data,
rawBody: res.rawBody,
message: 'Task created successfully'
};
}
return {
ok: false,
code: res.code || 500,
data: res.data || null,
rawBody: res.rawBody || '',
message: `Task creation failed: ${res.message || 'Unknown error'}. Raw body: ${res.rawBody || 'No body'}`
};
}
function searchVikunjaTasks(query) {
// API v1 /tasks returns list of tasks.
// We fetch and filter client-side to avoid complex filter syntax errors.
const res = vikunjaRequest_('GET', '/tasks');
if (!res.ok) return res;
const filtered = (res.data || []).filter(t =>
t.title && t.title.toLowerCase().includes(query.toLowerCase())
);
return { ok: true, data: filtered };
}
function getLabelIdByName_(labelName) {
const res = vikunjaRequest_('GET', '/labels');
if (!res.ok) return null;
// Vikunja labels typically use 'title' for the display name
const label = (res.data || []).find(l =>
(l.title && l.title.toLowerCase() === labelName.toLowerCase()) ||
(l.name && l.name.toLowerCase() === labelName.toLowerCase())
);
return label ? label.id : null;
}
function createVikunjaLabel(labelName) {
// Using PUT as per instance evidence for label creation
const payload = { title: labelName };
console.log(`Vikunja Label Create Attempt: [PUT] /labels | Payload: ${JSON.stringify(payload)}`);
const res = vikunjaRequest_('PUT', '/labels', payload);
if (res.ok && res.data && res.data.id) {
return { ok: true, id: res.data.id, code: res.code };
}
console.error(`Vikunja Label Create Failed: Code ${res.code} | Body: ${res.rawBody}`);
return { ok: false, message: res.message || 'Failed to create label', code: res.code };
}
function addVikunjaLabel(taskId, labelName) {
let labelId = getLabelIdByName_(labelName);
// Auto-create label if it doesn't exist
if (!labelId) {
const createRes = createVikunjaLabel(labelName);
if (!createRes.ok) {
return {
ok: false,
message: `Label resolution failed: ${createRes.message}`,
code: createRes.code
};
}
labelId = createRes.id;
}
// Attach label to task using PUT as per instance evidence
const payload = { label_id: labelId };
console.log(`Vikunja Label Attach Attempt: [PUT] /tasks/${taskId}/labels | Payload: ${JSON.stringify(payload)}`);
const res = vikunjaRequest_('PUT', `/tasks/${taskId}/labels`, payload);
if (!res.ok) {
console.error(`Vikunja Label Attach Failed: Code ${res.code} | Body: ${res.rawBody}`);
}
return res;
}
function deleteVikunjaTask(taskId) {
return vikunjaRequest_('DELETE', `/tasks/${taskId}`);
}