227 lines
6.5 KiB
JavaScript
227 lines
6.5 KiB
JavaScript
/**
|
|
* 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}`);
|
|
}
|