From 105dceb42e3366fb8172563361d39d15c597a111 Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:59:47 +0100 Subject: [PATCH] refactor(bot): use notes_json and install_methods_json directly, remove extra table dependencies --- .github/workflows/pocketbase-bot.yml | 493 +++++++++++---------------- 1 file changed, 198 insertions(+), 295 deletions(-) diff --git a/.github/workflows/pocketbase-bot.yml b/.github/workflows/pocketbase-bot.yml index 5d0118e15..3849ecf88 100644 --- a/.github/workflows/pocketbase-bot.yml +++ b/.github/workflows/pocketbase-bot.yml @@ -234,36 +234,196 @@ jobs: const setMatch = rest.match(/^set\s+(\S+)/i); if (noteMatch) { - // ── NOTE SUBCOMMAND ────────────────────────────────────────────── - } else if (methodMatch) { - // ── METHOD SUBCOMMAND ──────────────────────────────────────────── - const methodArgs = rest.replace(/^method\s*/i, '').trim(); - const methodListMode = !methodArgs || methodArgs.toLowerCase() === 'list'; + // ── NOTE SUBCOMMAND (reads/writes notes_json on script record) ──── + const noteAction = noteMatch[1].toLowerCase(); + const noteArgsStr = rest.substring(noteMatch[0].length).trim(); - // Helper: format bytes/numbers nicely - function fmtMethod(im) { - const r = im.resources || {}; - const typeLabel = im.expand && im.expand.type ? (im.expand.type.type || im.expand.type.name || im.expand.type.value || im.type) : im.type; - return '**`' + (typeLabel || '(unknown type)') + '`** — CPU: `' + (r.cpu != null ? r.cpu : im.resources_cpu || 0) + '` · RAM: `' + (r.ram != null ? r.ram : im.resources_ram || 0) + ' MB` · HDD: `' + (r.hdd != null ? r.hdd : im.resources_hdd || 0) + ' GB`'; + // Parse notes_json from the already-fetched script record + let notesArr = []; + try { notesArr = JSON.parse(record.notes_json || '[]'); } catch (e) { notesArr = []; } + + // Token parser: unquoted-word OR "quoted string" (supports \" escapes) + function parseNoteTokens(str) { + const tokens = []; + let pos = 0; + while (pos < str.length) { + while (pos < str.length && /\s/.test(str[pos])) pos++; + if (pos >= str.length) break; + if (str[pos] === '"') { + pos++; + let start = pos; + while (pos < str.length && str[pos] !== '"') { + if (str[pos] === '\\') pos++; + pos++; + } + tokens.push(str.substring(start, pos).replace(/\\"/g, '"')); + if (pos < str.length) pos++; + } else { + let start = pos; + while (pos < str.length && !/\s/.test(str[pos])) pos++; + tokens.push(str.substring(start, pos)); + } + } + return tokens; } - // Fetch expanded install_methods - const expRes = await request(recordsUrl + '/' + record.id + '?expand=install_methods,install_methods.type', { headers: { 'Authorization': token } }); - const expRec = JSON.parse(expRes.body); - const installMethods = (expRec.expand && expRec.expand.install_methods) || []; + function formatNotesList(arr) { + if (arr.length === 0) return '*None*'; + return arr.map(function (n, i) { + return (i + 1) + '. **`' + (n.type || '?') + '`**: ' + (n.text || ''); + }).join('\n'); + } + + async function patchNotesJson(arr) { + const res = await request(recordsUrl + '/' + record.id, { + method: 'PATCH', + headers: { 'Authorization': token, 'Content-Type': 'application/json' }, + body: JSON.stringify({ notes_json: JSON.stringify(arr) }) + }); + if (!res.ok) { + await addReaction('-1'); + await postComment('❌ **PocketBase Bot**: Failed to update `notes_json`:\n```\n' + res.body + '\n```'); + process.exit(1); + } + } + + if (noteAction === 'list') { + await addReaction('+1'); + await postComment( + 'ℹ️ **PocketBase Bot**: Notes for **`' + slug + '`** (' + notesArr.length + ' total)\n\n' + + formatNotesList(notesArr) + ); + + } else if (noteAction === 'add') { + const tokens = parseNoteTokens(noteArgsStr); + if (tokens.length < 2) { + await addReaction('-1'); + await postComment( + '❌ **PocketBase Bot**: `note add` requires `` and `""`.\n\n' + + '**Usage:** `/pocketbase ' + slug + ' note add ""`' + ); + process.exit(0); + } + const noteType = tokens[0].toLowerCase(); + const noteText = tokens.slice(1).join(' '); + notesArr.push({ type: noteType, text: noteText }); + await patchNotesJson(notesArr); + await addReaction('+1'); + await postComment( + '✅ **PocketBase Bot**: Added note to **`' + slug + '`**\n\n' + + '- **Type:** `' + noteType + '`\n' + + '- **Text:** ' + noteText + '\n\n' + + '*Executed by @' + actor + '*' + ); + + } else if (noteAction === 'edit') { + const tokens = parseNoteTokens(noteArgsStr); + if (tokens.length < 3) { + await addReaction('-1'); + await postComment( + '❌ **PocketBase Bot**: `note edit` requires ``, `""`, and `""`.\n\n' + + '**Usage:** `/pocketbase ' + slug + ' note edit "" ""`\n\n' + + 'Use `/pocketbase ' + slug + ' note list` to see current notes.' + ); + process.exit(0); + } + const noteType = tokens[0].toLowerCase(); + const oldText = tokens[1]; + const newText = tokens[2]; + const idx = notesArr.findIndex(function (n) { + return n.type.toLowerCase() === noteType && n.text === oldText; + }); + if (idx === -1) { + await addReaction('-1'); + await postComment( + '❌ **PocketBase Bot**: No `' + noteType + '` note found with that exact text.\n\n' + + '**Current notes for `' + slug + '`:**\n' + formatNotesList(notesArr) + ); + process.exit(0); + } + notesArr[idx].text = newText; + await patchNotesJson(notesArr); + await addReaction('+1'); + await postComment( + '✅ **PocketBase Bot**: Edited note in **`' + slug + '`**\n\n' + + '- **Type:** `' + noteType + '`\n' + + '- **Old:** ' + oldText + '\n' + + '- **New:** ' + newText + '\n\n' + + '*Executed by @' + actor + '*' + ); + + } else if (noteAction === 'remove') { + const tokens = parseNoteTokens(noteArgsStr); + if (tokens.length < 2) { + await addReaction('-1'); + await postComment( + '❌ **PocketBase Bot**: `note remove` requires `` and `""`.\n\n' + + '**Usage:** `/pocketbase ' + slug + ' note remove ""`\n\n' + + 'Use `/pocketbase ' + slug + ' note list` to see current notes.' + ); + process.exit(0); + } + const noteType = tokens[0].toLowerCase(); + const noteText = tokens[1]; + const before = notesArr.length; + notesArr = notesArr.filter(function (n) { + return !(n.type.toLowerCase() === noteType && n.text === noteText); + }); + if (notesArr.length === before) { + await addReaction('-1'); + await postComment( + '❌ **PocketBase Bot**: No `' + noteType + '` note found with that exact text.\n\n' + + '**Current notes for `' + slug + '`:**\n' + formatNotesList(notesArr) + ); + process.exit(0); + } + await patchNotesJson(notesArr); + await addReaction('+1'); + await postComment( + '✅ **PocketBase Bot**: Removed note from **`' + slug + '`**\n\n' + + '- **Type:** `' + noteType + '`\n' + + '- **Text:** ' + noteText + '\n\n' + + '*Executed by @' + actor + '*' + ); + } + + } else if (methodMatch) { + // ── METHOD SUBCOMMAND (reads/writes install_methods_json on script record) ── + const methodArgs = rest.replace(/^method\s*/i, '').trim(); + const methodListMode = !methodArgs || methodArgs.toLowerCase() === 'list'; + + // Parse install_methods_json from the already-fetched script record + let methodsArr = []; + try { methodsArr = JSON.parse(record.install_methods_json || '[]'); } catch (e) { methodsArr = []; } + + function formatMethodsList(arr) { + if (arr.length === 0) return '*None*'; + return arr.map(function (im, i) { + const r = im.resources || {}; + return (i + 1) + '. **`' + (im.type || '?') + '`** — CPU: `' + (r.cpu != null ? r.cpu : '?') + + '` · RAM: `' + (r.ram != null ? r.ram : '?') + ' MB` · HDD: `' + (r.hdd != null ? r.hdd : '?') + ' GB`'; + }).join('\n'); + } + + async function patchInstallMethodsJson(arr) { + const res = await request(recordsUrl + '/' + record.id, { + method: 'PATCH', + headers: { 'Authorization': token, 'Content-Type': 'application/json' }, + body: JSON.stringify({ install_methods_json: JSON.stringify(arr) }) + }); + if (!res.ok) { + await addReaction('-1'); + await postComment('❌ **PocketBase Bot**: Failed to update `install_methods_json`:\n```\n' + res.body + '\n```'); + process.exit(1); + } + } if (methodListMode) { await addReaction('+1'); - if (installMethods.length === 0) { - await postComment('ℹ️ **PocketBase Bot**: No install methods found for **`' + slug + '`**.'); - } else { - const lines = installMethods.map(function (im, i) { - const r = im.resources || {}; - const typeLabel = im.expand && im.expand.type ? (im.expand.type.type || im.expand.type.name || im.expand.type.value || '') : ''; - return (i + 1) + '. **`' + (typeLabel || im.type || im.id) + '`** — CPU: `' + (im.resources_cpu || 0) + '` · RAM: `' + (im.resources_ram || 0) + ' MB` · HDD: `' + (im.resources_hdd || 0) + ' GB`'; - }).join('\n'); - await postComment('ℹ️ **PocketBase Bot**: Install methods for **`' + slug + '`** (' + installMethods.length + ' total)\n\n' + lines); - } + await postComment( + 'ℹ️ **PocketBase Bot**: Install methods for **`' + slug + '`** (' + methodsArr.length + ' total)\n\n' + + formatMethodsList(methodsArr) + ); } else { // Parse: cpu=N ram=N hdd=N const methodParts = methodArgs.match(/^(\S+)\s+(.+)$/); @@ -275,7 +435,7 @@ jobs: ); process.exit(0); } - const targetType = methodParts[1].toLowerCase(); + const targetType = methodParts[1].toLowerCase(); const resourcesStr = methodParts[2]; // Parse resource fields (only cpu/ram/hdd allowed) @@ -293,50 +453,34 @@ jobs: process.exit(0); } - // Find matching install method by type name/value - const matchedMethod = installMethods.find(function (im) { - const typeLabel = im.expand && im.expand.type ? - (im.expand.type.type || im.expand.type.name || im.expand.type.value || '') : ''; - return typeLabel.toLowerCase() === targetType || (im.type && im.type.toLowerCase && im.type.toLowerCase() === targetType); + // Find matching method by type name (case-insensitive) + const idx = methodsArr.findIndex(function (im) { + return (im.type || '').toLowerCase() === targetType; }); - - if (!matchedMethod) { + if (idx === -1) { await addReaction('-1'); - const availableTypes = installMethods.map(function (im) { - return im.expand && im.expand.type ? (im.expand.type.type || im.expand.type.name || im.expand.type.value || im.type || im.id) : (im.type || im.id); - }); + const availableTypes = methodsArr.map(function (im) { return im.type || '?'; }); await postComment( '❌ **PocketBase Bot**: No install method with type `' + targetType + '` found for `' + slug + '`.\n\n' + - '**Available types:** `' + availableTypes.join('`, `') + '`\n\n' + + '**Available types:** `' + (availableTypes.length ? availableTypes.join('`, `') : '(none)') + '`\n\n' + 'Use `/pocketbase ' + slug + ' method list` to see all methods.' ); process.exit(0); } - // Build patch payload for script_install_methods record - const imPatch = {}; - if (resourceChanges.cpu != null) imPatch.resources_cpu = resourceChanges.cpu; - if (resourceChanges.ram != null) imPatch.resources_ram = resourceChanges.ram; - if (resourceChanges.hdd != null) imPatch.resources_hdd = resourceChanges.hdd; + if (!methodsArr[idx].resources) methodsArr[idx].resources = {}; + if (resourceChanges.cpu != null) methodsArr[idx].resources.cpu = resourceChanges.cpu; + if (resourceChanges.ram != null) methodsArr[idx].resources.ram = resourceChanges.ram; + if (resourceChanges.hdd != null) methodsArr[idx].resources.hdd = resourceChanges.hdd; + + await patchInstallMethodsJson(methodsArr); - const imPatchRes = await request(apiBase + '/collections/script_install_methods/records/' + matchedMethod.id, { - method: 'PATCH', - headers: { 'Authorization': token, 'Content-Type': 'application/json' }, - body: JSON.stringify(imPatch) - }); - if (!imPatchRes.ok) { - await addReaction('-1'); - await postComment('❌ **PocketBase Bot**: PATCH failed for install method:\n```\n' + imPatchRes.body + '\n```'); - process.exit(1); - } - const typeLabel = matchedMethod.expand && matchedMethod.expand.type ? - (matchedMethod.expand.type.type || matchedMethod.expand.type.name || matchedMethod.expand.type.value || targetType) : targetType; const changesLines = Object.entries(resourceChanges) .map(function ([k, v]) { return '- `' + k + '` → `' + v + (k === 'ram' ? ' MB' : k === 'hdd' ? ' GB' : '') + '`'; }) .join('\n'); await addReaction('+1'); await postComment( - '✅ **PocketBase Bot**: Updated install method **`' + typeLabel + '`** for **`' + slug + '`**\n\n' + + '✅ **PocketBase Bot**: Updated install method **`' + methodsArr[idx].type + '`** for **`' + slug + '`**\n\n' + '**Changes applied:**\n' + changesLines + '\n\n' + '*Executed by @' + actor + '*' ); @@ -386,247 +530,6 @@ jobs: '**Value set:**\n```\n' + preview + '\n```\n\n' + '*Executed by @' + actor + '*' ); - const noteArgsStr = rest.substring(noteMatch[0].length).trim(); - - // Load note types from PocketBase - const noteTypeToId = {}; - const noteTypeToName = {}; - try { - const ntRes = await request(apiBase + '/collections/z_ref_note_types/records?perPage=500', { headers: { 'Authorization': token } }); - if (ntRes.ok) { - JSON.parse(ntRes.body).items.forEach(function (item) { - if (item.type != null) { - noteTypeToId[item.type.toLowerCase()] = item.id; - noteTypeToName[item.id] = item.type; - } - }); - } - } catch (e) { console.warn('z_ref_note_types:', e.message); } - - const VALID_NOTE_TYPES = Object.keys(noteTypeToId); - - // Token parser: unquoted-word OR "quoted string" (supports \" escapes) - function parseNoteTokens(str) { - const tokens = []; - let pos = 0; - while (pos < str.length) { - while (pos < str.length && /\s/.test(str[pos])) pos++; - if (pos >= str.length) break; - if (str[pos] === '"') { - pos++; - let start = pos; - while (pos < str.length && str[pos] !== '"') { - if (str[pos] === '\\') pos++; - pos++; - } - tokens.push(str.substring(start, pos).replace(/\\"/g, '"')); - if (pos < str.length) pos++; - } else { - let start = pos; - while (pos < str.length && !/\s/.test(str[pos])) pos++; - tokens.push(str.substring(start, pos)); - } - } - return tokens; - } - - // Helper: fetch record with expanded notes relation - async function fetchExpandedNotes() { - const r = await request(recordsUrl + '/' + record.id + '?expand=notes', { headers: { 'Authorization': token } }); - const rec = JSON.parse(r.body); - return { rec, notes: (rec.expand && rec.expand.notes) || [] }; - } - - // Helper: format notes list for display - function formatNotesList(notes) { - if (notes.length === 0) return '*None*'; - return notes.map(function (n, i) { - return (i + 1) + '. **`' + (noteTypeToName[n.type] || n.type) + '`**: ' + n.text; - }).join('\n'); - } - - if (noteAction === 'list') { - // ── note list ──────────────────────────────────────────────── - const { notes } = await fetchExpandedNotes(); - await addReaction('+1'); - await postComment( - 'ℹ️ **PocketBase Bot**: Notes for **`' + slug + '`** (' + notes.length + ' total)\n\n' + - formatNotesList(notes) - ); - - } else if (noteAction === 'add') { - // ── note add "" ────────────────────────────────── - const tokens = parseNoteTokens(noteArgsStr); - if (tokens.length < 2) { - await addReaction('-1'); - await postComment( - '❌ **PocketBase Bot**: `note add` requires `` and `""`.\n\n' + - '**Usage:** `/pocketbase ' + slug + ' note add ""`\n' + - '**Valid types:** `' + VALID_NOTE_TYPES.join('`, `') + '`' - ); - process.exit(0); - } - const noteType = tokens[0].toLowerCase(); - const noteText = tokens.slice(1).join(' '); - const typeId = noteTypeToId[noteType]; - if (!typeId) { - await addReaction('-1'); - await postComment( - '❌ **PocketBase Bot**: Unknown note type `' + noteType + '`.\n' + - '**Valid types:** `' + VALID_NOTE_TYPES.join('`, `') + '`' - ); - process.exit(0); - } - // POST new note to script_notes - const postNoteRes = await request(apiBase + '/collections/script_notes/records', { - method: 'POST', - headers: { 'Authorization': token, 'Content-Type': 'application/json' }, - body: JSON.stringify({ text: noteText, type: typeId, script: record.id }) - }); - if (!postNoteRes.ok) { - await addReaction('-1'); - await postComment('❌ **PocketBase Bot**: Failed to create note:\n```\n' + postNoteRes.body + '\n```'); - process.exit(1); - } - const newNoteId = JSON.parse(postNoteRes.body).id; - // PATCH script to include new note in relation - const existingNoteIds = Array.isArray(record.notes) ? record.notes : []; - const patchLinkRes = await request(recordsUrl + '/' + record.id, { - method: 'PATCH', - headers: { 'Authorization': token, 'Content-Type': 'application/json' }, - body: JSON.stringify({ notes: [...existingNoteIds, newNoteId] }) - }); - if (!patchLinkRes.ok) { - await addReaction('-1'); - await postComment('❌ **PocketBase Bot**: Note created but failed to link to script:\n```\n' + patchLinkRes.body + '\n```'); - process.exit(1); - } - await addReaction('+1'); - await postComment( - '✅ **PocketBase Bot**: Added note to **`' + slug + '`**\n\n' + - '- **Type:** `' + noteType + '`\n' + - '- **Text:** ' + noteText + '\n\n' + - '*Executed by @' + actor + '*' - ); - - } else if (noteAction === 'edit') { - // ── note edit "" "" ──────────────── - const tokens = parseNoteTokens(noteArgsStr); - if (tokens.length < 3) { - await addReaction('-1'); - await postComment( - '❌ **PocketBase Bot**: `note edit` requires ``, `""`, and `""`.\n\n' + - '**Usage:** `/pocketbase ' + slug + ' note edit "" ""`\n' + - '**Valid types:** `' + VALID_NOTE_TYPES.join('`, `') + '`\n\n' + - 'Use `/pocketbase ' + slug + ' note list` to see current notes.' - ); - process.exit(0); - } - const noteType = tokens[0].toLowerCase(); - const oldText = tokens[1]; - const newText = tokens[2]; - const typeId = noteTypeToId[noteType]; - if (!typeId) { - await addReaction('-1'); - await postComment( - '❌ **PocketBase Bot**: Unknown note type `' + noteType + '`.\n' + - '**Valid types:** `' + VALID_NOTE_TYPES.join('`, `') + '`' - ); - process.exit(0); - } - const { notes } = await fetchExpandedNotes(); - const matchingNote = notes.find(function (n) { return n.type === typeId && n.text === oldText; }); - if (!matchingNote) { - await addReaction('-1'); - await postComment( - '❌ **PocketBase Bot**: No `' + noteType + '` note found with that exact text.\n\n' + - '**Current notes for `' + slug + '`:**\n' + formatNotesList(notes) - ); - process.exit(0); - } - // PATCH the note record directly - const patchNoteRes = await request(apiBase + '/collections/script_notes/records/' + matchingNote.id, { - method: 'PATCH', - headers: { 'Authorization': token, 'Content-Type': 'application/json' }, - body: JSON.stringify({ text: newText }) - }); - if (!patchNoteRes.ok) { - await addReaction('-1'); - await postComment('❌ **PocketBase Bot**: Failed to update note:\n```\n' + patchNoteRes.body + '\n```'); - process.exit(1); - } - await addReaction('+1'); - await postComment( - '✅ **PocketBase Bot**: Edited note in **`' + slug + '`**\n\n' + - '- **Type:** `' + noteType + '`\n' + - '- **Old:** ' + oldText + '\n' + - '- **New:** ' + newText + '\n\n' + - '*Executed by @' + actor + '*' - ); - - } else if (noteAction === 'remove') { - // ── note remove "" ─────────────────────────────── - const tokens = parseNoteTokens(noteArgsStr); - if (tokens.length < 2) { - await addReaction('-1'); - await postComment( - '❌ **PocketBase Bot**: `note remove` requires `` and `""`.\n\n' + - '**Usage:** `/pocketbase ' + slug + ' note remove ""`\n' + - '**Valid types:** `' + VALID_NOTE_TYPES.join('`, `') + '`\n\n' + - 'Use `/pocketbase ' + slug + ' note list` to see current notes.' - ); - process.exit(0); - } - const noteType = tokens[0].toLowerCase(); - const noteText = tokens[1]; - const typeId = noteTypeToId[noteType]; - if (!typeId) { - await addReaction('-1'); - await postComment( - '❌ **PocketBase Bot**: Unknown note type `' + noteType + '`.\n' + - '**Valid types:** `' + VALID_NOTE_TYPES.join('`, `') + '`' - ); - process.exit(0); - } - const { rec: expandedRecord, notes } = await fetchExpandedNotes(); - const matchingNote = notes.find(function (n) { return n.type === typeId && n.text === noteText; }); - if (!matchingNote) { - await addReaction('-1'); - await postComment( - '❌ **PocketBase Bot**: No `' + noteType + '` note found with that exact text.\n\n' + - '**Current notes for `' + slug + '`:**\n' + formatNotesList(notes) - ); - process.exit(0); - } - // PATCH script to remove note ID from relation, then DELETE the note record - const existingNoteIds = Array.isArray(expandedRecord.notes) ? expandedRecord.notes : []; - const patchRes = await request(recordsUrl + '/' + record.id, { - method: 'PATCH', - headers: { 'Authorization': token, 'Content-Type': 'application/json' }, - body: JSON.stringify({ notes: existingNoteIds.filter(function (id) { return id !== matchingNote.id; }) }) - }); - if (!patchRes.ok) { - await addReaction('-1'); - await postComment('❌ **PocketBase Bot**: Failed to unlink note from script:\n```\n' + patchRes.body + '\n```'); - process.exit(1); - } - const delRes = await request(apiBase + '/collections/script_notes/records/' + matchingNote.id, { - method: 'DELETE', - headers: { 'Authorization': token } - }); - if (!delRes.ok && delRes.statusCode !== 204) { - await addReaction('-1'); - await postComment('❌ **PocketBase Bot**: Note unlinked but could not be deleted:\n```\n' + delRes.body + '\n```'); - process.exit(1); - } - await addReaction('+1'); - await postComment( - '✅ **PocketBase Bot**: Removed note from **`' + slug + '`**\n\n' + - '- **Type:** `' + noteType + '`\n' + - '- **Text:** ' + noteText + '\n\n' + - '*Executed by @' + actor + '*' - ); - } } else { // ── FIELD=VALUE PATH ─────────────────────────────────────────────