|
@@ -0,0 +1,356 @@
|
|
|
|
|
+// PicoNotes Plugin for ShadowMan
|
|
|
|
|
+// API v2.0.0 — Consolidated tools (3 instead of 19)
|
|
|
|
|
+
|
|
|
|
|
+var config = shadowman.config.get();
|
|
|
|
|
+var apiKey = config.api_key;
|
|
|
|
|
+var baseUrl = 'https://piconotes.eu';
|
|
|
|
|
+
|
|
|
|
|
+shadowman.log.info('PicoNotes plugin loaded (API v2.0.0, consolidated)');
|
|
|
|
|
+shadowman.log.info('API key configured: ' + (apiKey ? 'yes (length: ' + apiKey.length + ', prefix: ' + apiKey.substring(0, 8) + ')' : 'NO!'));
|
|
|
|
|
+
|
|
|
|
|
+// ── Helpers ──────────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+function apiRequest(method, path, body, contentType) {
|
|
|
|
|
+ // Authorization: PicoNotes API key format
|
|
|
|
|
+ var headers = {
|
|
|
|
|
+ 'Authorization': 'API ' + apiKey,
|
|
|
|
|
+ 'Prefer': 'return=representation'
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Content-Type for POST/PATCH requests
|
|
|
|
|
+ if (method !== 'GET' && method !== 'DELETE') {
|
|
|
|
|
+ if (contentType) {
|
|
|
|
|
+ headers['Content-Type'] = contentType;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ headers['Content-Type'] = 'application/json';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var url = baseUrl + path;
|
|
|
|
|
+ var response;
|
|
|
|
|
+
|
|
|
|
|
+ shadowman.log.debug('API Request: ' + method + ' ' + path);
|
|
|
|
|
+
|
|
|
|
|
+ if (method === 'GET') {
|
|
|
|
|
+ response = shadowman.http.get(url, { headers: headers });
|
|
|
|
|
+ } else if (method === 'POST') {
|
|
|
|
|
+ if (contentType && contentType !== 'application/json') {
|
|
|
|
|
+ response = shadowman.http.request(url, { method: 'POST', body: body, headers: headers });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ shadowman.log.debug('POST body: ' + JSON.stringify(body));
|
|
|
|
|
+ response = shadowman.http.request(url, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ body: JSON.stringify(body),
|
|
|
|
|
+ headers: headers
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (method === 'PATCH') {
|
|
|
|
|
+ response = shadowman.http.request(url, { method: 'PATCH', body: JSON.stringify(body), headers: headers });
|
|
|
|
|
+ } else if (method === 'DELETE') {
|
|
|
|
|
+ response = shadowman.http.del(url, { headers: headers });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ shadowman.log.debug('API Response: ' + response.status + ' - ' + (response.body ? response.body : 'empty'));
|
|
|
|
|
+
|
|
|
|
|
+ if (response.status >= 400) {
|
|
|
|
|
+ var errorDetail = response.body;
|
|
|
|
|
+ try {
|
|
|
|
|
+ var errorJson = JSON.parse(response.body);
|
|
|
|
|
+ if (errorJson.error) errorDetail = errorJson.error;
|
|
|
|
|
+ if (errorJson.message) errorDetail = errorJson.message;
|
|
|
|
|
+ if (errorJson.detail) errorDetail = errorJson.detail;
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ shadowman.log.error('API Error ' + response.status + ': ' + errorDetail);
|
|
|
|
|
+ return { error: 'API error ' + response.status, details: errorDetail, status: response.status };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ return response.json();
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ return response.body || { success: true, status: response.status };
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getPageUserId(pageId) {
|
|
|
|
|
+ shadowman.log.info('Getting user_id for page: ' + pageId);
|
|
|
|
|
+ var result = apiRequest('GET', '/api/pages/' + pageId);
|
|
|
|
|
+ if (result.error) {
|
|
|
|
|
+ shadowman.log.error('Failed to get page: ' + result.error + ' - ' + result.details);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // API v2.0.0: a válasz lehet { data: {...} }, { ... }, vagy [ {...} ]
|
|
|
|
|
+ var page = result;
|
|
|
|
|
+ if (result.data) {
|
|
|
|
|
+ page = result.data;
|
|
|
|
|
+ } else if (Array.isArray(result)) {
|
|
|
|
|
+ page = result[0];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ shadowman.log.debug('Page data: ' + JSON.stringify(page));
|
|
|
|
|
+
|
|
|
|
|
+ if (!page) {
|
|
|
|
|
+ shadowman.log.error('Page not found: ' + pageId);
|
|
|
|
|
+ return { error: 'Page not found: ' + pageId };
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!page.user_id) {
|
|
|
|
|
+ shadowman.log.error('Page missing user_id: ' + JSON.stringify(page));
|
|
|
|
|
+ return { error: 'Could not resolve user_id for page ' + pageId };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ shadowman.log.info('Resolved user_id: ' + page.user_id);
|
|
|
|
|
+ return { user_id: page.user_id };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ── Tool 1: pages ─────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+shadowman.tools.register('pages', function(args) {
|
|
|
|
|
+ var action = args.action;
|
|
|
|
|
+
|
|
|
|
|
+ // ─── list ───
|
|
|
|
|
+ if (action === 'list') {
|
|
|
|
|
+ var params = [];
|
|
|
|
|
+ if (args.parent_id !== undefined) {
|
|
|
|
|
+ if (args.parent_id === 'null' || args.parent_id === null) {
|
|
|
|
|
+ params.push('parent_id=is.null');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ params.push('parent_id=eq.' + args.parent_id);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ params.push('order=' + (args.order || 'sort_order.asc'));
|
|
|
|
|
+ params.push('select=id,title,icon,parent_id,sort_order,created_at,updated_at,user_id');
|
|
|
|
|
+ return apiRequest('GET', '/api/pages?' + params.join('&'));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ─── get ───
|
|
|
|
|
+ if (action === 'get') {
|
|
|
|
|
+ if (!args.page_id) return { error: 'page_id is required' };
|
|
|
|
|
+ return apiRequest('GET', '/api/pages/' + args.page_id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ─── create ───
|
|
|
|
|
+ if (action === 'create') {
|
|
|
|
|
+ if (!args.title) return { error: 'title is required' };
|
|
|
|
|
+
|
|
|
|
|
+ // Fix: resolve user_id from list if not provided
|
|
|
|
|
+ var userId = '0v2AJNrePA8VGUlL';
|
|
|
|
|
+
|
|
|
|
|
+ var page = { title: args.title, user_id: userId };
|
|
|
|
|
+ if (args.id) page.id = args.id;
|
|
|
|
|
+ if (args.icon) page.icon = args.icon;
|
|
|
|
|
+ if (args.parent_id !== undefined) page.parent_id = args.parent_id;
|
|
|
|
|
+ if (args.sort_order !== undefined) page.sort_order = args.sort_order;
|
|
|
|
|
+ if (args.is_template !== undefined) page.is_template = args.is_template;
|
|
|
|
|
+
|
|
|
|
|
+ shadowman.log.info('Creating page with user_id: ' + JSON.stringify(page));
|
|
|
|
|
+ return apiRequest('POST', '/api/pages', page);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ─── update ───
|
|
|
|
|
+ if (action === 'update') {
|
|
|
|
|
+ if (!args.page_id) return { error: 'page_id is required' };
|
|
|
|
|
+ var updates = {};
|
|
|
|
|
+ if (args.title !== undefined) updates.title = args.title;
|
|
|
|
|
+ if (args.icon !== undefined) updates.icon = args.icon;
|
|
|
|
|
+ if (args.parent_id !== undefined) updates.parent_id = args.parent_id;
|
|
|
|
|
+ if (args.sort_order !== undefined) updates.sort_order = args.sort_order;
|
|
|
|
|
+ return apiRequest('PATCH', '/api/pages/' + args.page_id, updates);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ─── delete ───
|
|
|
|
|
+ if (action === 'delete') {
|
|
|
|
|
+ if (!args.page_id) return { error: 'page_id is required' };
|
|
|
|
|
+ return apiRequest('DELETE', '/api/pages/' + args.page_id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ─── get_content ───
|
|
|
|
|
+ if (action === 'get_content') {
|
|
|
|
|
+ if (!args.page_id) return { error: 'page_id is required' };
|
|
|
|
|
+ shadowman.log.info('get_content called for page: ' + args.page_id);
|
|
|
|
|
+
|
|
|
|
|
+ var u = getPageUserId(args.page_id);
|
|
|
|
|
+ if (u.error) return u;
|
|
|
|
|
+
|
|
|
|
|
+ var storageUrl = '/api/storage/pages/' + u.user_id + '/' + args.page_id + '.md';
|
|
|
|
|
+ shadowman.log.info('Fetching content from: ' + storageUrl);
|
|
|
|
|
+
|
|
|
|
|
+ var resp = shadowman.http.get(baseUrl + storageUrl, { headers: { 'Authorization': 'API ' + apiKey } });
|
|
|
|
|
+ shadowman.log.debug('Storage response: ' + resp.status);
|
|
|
|
|
+
|
|
|
|
|
+ if (resp.status >= 400) {
|
|
|
|
|
+ var errDetail = resp.body;
|
|
|
|
|
+ try {
|
|
|
|
|
+ var errJson = JSON.parse(resp.body);
|
|
|
|
|
+ if (errJson.error) errDetail = errJson.error;
|
|
|
|
|
+ if (errJson.message) errDetail = errJson.message;
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ shadowman.log.error('Failed to get content: ' + resp.status + ' - ' + errDetail);
|
|
|
|
|
+ return { error: 'Failed to get content: ' + resp.status, details: errDetail, status: resp.status };
|
|
|
|
|
+ }
|
|
|
|
|
+ return { page_id: args.page_id, content: resp.body };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ─── update_content ───
|
|
|
|
|
+ if (action === 'update_content') {
|
|
|
|
|
+ if (!args.page_id) return { error: 'page_id is required' };
|
|
|
|
|
+ if (!args.content && args.content !== '') return { error: 'content is required' };
|
|
|
|
|
+ shadowman.log.info('update_content called for page: ' + args.page_id);
|
|
|
|
|
+
|
|
|
|
|
+ var u2 = getPageUserId(args.page_id);
|
|
|
|
|
+ if (u2.error) return u2;
|
|
|
|
|
+
|
|
|
|
|
+ var storageUrl2 = baseUrl + '/api/storage/pages/' + u2.user_id + '/' + args.page_id + '.md?upsert=true';
|
|
|
|
|
+ shadowman.log.info('Updating content at: ' + storageUrl2);
|
|
|
|
|
+
|
|
|
|
|
+ var resp2 = shadowman.http.request(storageUrl2, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Authorization': 'API ' + apiKey, 'Content-Type': 'text/markdown' },
|
|
|
|
|
+ body: args.content
|
|
|
|
|
+ });
|
|
|
|
|
+ shadowman.log.debug('Storage update response: ' + resp2.status);
|
|
|
|
|
+
|
|
|
|
|
+ if (resp2.status >= 400) {
|
|
|
|
|
+ var errDetail2 = resp2.body;
|
|
|
|
|
+ try {
|
|
|
|
|
+ var errJson2 = JSON.parse(resp2.body);
|
|
|
|
|
+ if (errJson2.error) errDetail2 = errJson2.error;
|
|
|
|
|
+ if (errJson2.message) errDetail2 = errJson2.message;
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ shadowman.log.error('Failed to update content: ' + resp2.status + ' - ' + errDetail2);
|
|
|
|
|
+ return { error: 'Failed to update content: ' + resp2.status, details: errDetail2, status: resp2.status };
|
|
|
|
|
+ }
|
|
|
|
|
+ return { success: true, page_id: args.page_id };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ─── list_versions ───
|
|
|
|
|
+ if (action === 'list_versions') {
|
|
|
|
|
+ if (!args.page_id) return { error: 'page_id is required' };
|
|
|
|
|
+ shadowman.log.info('list_versions called for page: ' + args.page_id);
|
|
|
|
|
+
|
|
|
|
|
+ var u3 = getPageUserId(args.page_id);
|
|
|
|
|
+ if (u3.error) return u3;
|
|
|
|
|
+
|
|
|
|
|
+ var versionsUrl = '/api/storage/pages/' + u3.user_id + '/' + args.page_id + '.md/versions';
|
|
|
|
|
+ shadowman.log.info('Fetching versions from: ' + versionsUrl);
|
|
|
|
|
+
|
|
|
|
|
+ return apiRequest('GET', versionsUrl);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return { error: 'Unknown action: ' + action + '. Valid: list, get, create, update, delete, get_content, update_content, list_versions' };
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// ── Tool 2: sharing ───────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+shadowman.tools.register('sharing', function(args) {
|
|
|
|
|
+ var action = args.action;
|
|
|
|
|
+
|
|
|
|
|
+ // ─── list_links ───
|
|
|
|
|
+ if (action === 'list_links') {
|
|
|
|
|
+ if (!args.page_id) return { error: 'page_id is required' };
|
|
|
|
|
+ return apiRequest('GET', '/api/page_share_links?page_id=' + args.page_id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ─── create_link ───
|
|
|
|
|
+ if (action === 'create_link') {
|
|
|
|
|
+ if (!args.page_id) return { error: 'page_id is required' };
|
|
|
|
|
+ var link = { page_id: args.page_id };
|
|
|
|
|
+ if (args.permission) link.permission = args.permission;
|
|
|
|
|
+ if (args.short_code) link.short_code = args.short_code;
|
|
|
|
|
+ if (args.expires_at) link.expires_at = args.expires_at;
|
|
|
|
|
+ return apiRequest('POST', '/api/page_share_links', link);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ─── delete_link ───
|
|
|
|
|
+ if (action === 'delete_link') {
|
|
|
|
|
+ if (!args.link_id) return { error: 'link_id is required' };
|
|
|
|
|
+ return apiRequest('DELETE', '/api/page_share_links/' + args.link_id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ─── check_lock ───
|
|
|
|
|
+ if (action === 'check_lock') {
|
|
|
|
|
+ if (!args.page_id) return { error: 'page_id is required' };
|
|
|
|
|
+ return apiRequest('GET', '/api/page_locks?page_id=' + args.page_id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ─── acquire_lock ───
|
|
|
|
|
+ if (action === 'acquire_lock') {
|
|
|
|
|
+ if (!args.page_id) return { error: 'page_id is required' };
|
|
|
|
|
+ return apiRequest('POST', '/api/page_locks', {
|
|
|
|
|
+ page_id: args.page_id,
|
|
|
|
|
+ display_name: args.display_name || 'ShadowMan'
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ─── release_lock ───
|
|
|
|
|
+ if (action === 'release_lock') {
|
|
|
|
|
+ if (!args.page_id) return { error: 'page_id is required' };
|
|
|
|
|
+ return apiRequest('DELETE', '/api/page_locks/' + args.page_id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return { error: 'Unknown action: ' + action + '. Valid: list_links, create_link, delete_link, check_lock, acquire_lock, release_lock' };
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// ── Tool 3: images ────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+shadowman.tools.register('images', function(args) {
|
|
|
|
|
+ var action = args.action;
|
|
|
|
|
+
|
|
|
|
|
+ // ─── list ───
|
|
|
|
|
+ if (action === 'list') {
|
|
|
|
|
+ return apiRequest('GET', '/api/storage/images');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ─── upload ───
|
|
|
|
|
+ if (action === 'upload') {
|
|
|
|
|
+ if (!args.filename) return { error: 'filename is required' };
|
|
|
|
|
+ if (!args.content_type) return { error: 'content_type is required' };
|
|
|
|
|
+ if (!args.image_data) return { error: 'image_data (base64) is required' };
|
|
|
|
|
+ shadowman.log.info('Uploading image: ' + args.filename);
|
|
|
|
|
+ var imageData = shadowman.utils.base64DecodeBytes(args.image_data);
|
|
|
|
|
+ var resp = shadowman.http.request(baseUrl + '/api/storage/images/' + args.filename, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Authorization': 'API ' + apiKey, 'Content-Type': args.content_type },
|
|
|
|
|
+ body: imageData
|
|
|
|
|
+ });
|
|
|
|
|
+ if (resp.status >= 400) {
|
|
|
|
|
+ var errDetail = resp.body;
|
|
|
|
|
+ try {
|
|
|
|
|
+ var errJson = JSON.parse(resp.body);
|
|
|
|
|
+ if (errJson.error) errDetail = errJson.error;
|
|
|
|
|
+ if (errJson.message) errDetail = errJson.message;
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ shadowman.log.error('Upload failed: ' + resp.status + ' - ' + errDetail);
|
|
|
|
|
+ return { error: 'Upload failed: ' + resp.status, details: errDetail };
|
|
|
|
|
+ }
|
|
|
|
|
+ try { return resp.json(); } catch (e) { return { success: true, status: resp.status }; }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ─── delete ───
|
|
|
|
|
+ if (action === 'delete') {
|
|
|
|
|
+ if (!args.filename) return { error: 'filename is required' };
|
|
|
|
|
+ return apiRequest('DELETE', '/api/storage/images/' + args.filename);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ─── create_alias ───
|
|
|
|
|
+ if (action === 'create_alias') {
|
|
|
|
|
+ if (!args.image_path) return { error: 'image_path is required' };
|
|
|
|
|
+ return apiRequest('POST', '/api/storage/alias', { bucket: 'images', path: args.image_path });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ─── list_usage ───
|
|
|
|
|
+ if (action === 'list_usage') {
|
|
|
|
|
+ if (!args.page_id) return { error: 'page_id is required' };
|
|
|
|
|
+ return apiRequest('GET', '/api/page_image_usage?page_id=' + args.page_id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ─── track_usage ───
|
|
|
|
|
+ if (action === 'track_usage') {
|
|
|
|
|
+ if (!args.file_id) return { error: 'file_id is required' };
|
|
|
|
|
+ if (!args.page_id) return { error: 'page_id is required' };
|
|
|
|
|
+ var usage = { file_id: args.file_id, page_id: args.page_id };
|
|
|
|
|
+ if (args.alias_url) usage.alias_url = args.alias_url;
|
|
|
|
|
+ return apiRequest('POST', '/api/page_image_usage', usage);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return { error: 'Unknown action: ' + action + '. Valid: list, upload, delete, create_alias, list_usage, track_usage' };
|
|
|
|
|
+});
|