name: PocketBase Bot on: issue_comment: types: [created] permissions: issues: write pull-requests: write contents: read jobs: pocketbase-bot: runs-on: self-hosted # Only act on /pocketbase commands if: startsWith(github.event.comment.body, '/pocketbase') steps: - name: Execute PocketBase bot command env: POCKETBASE_URL: ${{ secrets.POCKETBASE_URL }} POCKETBASE_COLLECTION: ${{ secrets.POCKETBASE_COLLECTION }} POCKETBASE_ADMIN_EMAIL: ${{ secrets.POCKETBASE_ADMIN_EMAIL }} POCKETBASE_ADMIN_PASSWORD: ${{ secrets.POCKETBASE_ADMIN_PASSWORD }} COMMENT_BODY: ${{ github.event.comment.body }} COMMENT_ID: ${{ github.event.comment.id }} ISSUE_NUMBER: ${{ github.event.issue.number }} REPO_OWNER: ${{ github.repository_owner }} REPO_NAME: ${{ github.event.repository.name }} ACTOR: ${{ github.event.comment.user.login }} ACTOR_ASSOCIATION: ${{ github.event.comment.author_association }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} FRONTEND_URL: ${{ secrets.FRONTEND_URL }} REVALIDATE_SECRET: ${{ secrets.REVALIDATE_SECRET }} run: | node << 'ENDSCRIPT' (async function () { const https = require('https'); const http = require('http'); const url = require('url'); // ── HTTP helper with redirect following ──────────────────────────── function request(fullUrl, opts, redirectCount) { redirectCount = redirectCount || 0; return new Promise(function (resolve, reject) { const u = url.parse(fullUrl); const isHttps = u.protocol === 'https:'; const body = opts.body; const options = { hostname: u.hostname, port: u.port || (isHttps ? 443 : 80), path: u.path, method: opts.method || 'GET', headers: opts.headers || {} }; if (body) options.headers['Content-Length'] = Buffer.byteLength(body); const lib = isHttps ? https : http; const req = lib.request(options, function (res) { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { if (redirectCount >= 5) return reject(new Error('Too many redirects from ' + fullUrl)); const redirectUrl = url.resolve(fullUrl, res.headers.location); res.resume(); resolve(request(redirectUrl, opts, redirectCount + 1)); return; } let data = ''; res.on('data', function (chunk) { data += chunk; }); res.on('end', function () { resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, statusCode: res.statusCode, body: data }); }); }); req.on('error', reject); if (body) req.write(body); req.end(); }); } // ── GitHub API helpers ───────────────────────────────────────────── const owner = process.env.REPO_OWNER; const repo = process.env.REPO_NAME; const issueNumber = parseInt(process.env.ISSUE_NUMBER, 10); const commentId = parseInt(process.env.COMMENT_ID, 10); const actor = process.env.ACTOR; function ghRequest(path, method, body) { const headers = { 'Authorization': 'Bearer ' + process.env.GITHUB_TOKEN, 'Accept': 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28', 'User-Agent': 'PocketBase-Bot' }; const bodyStr = body ? JSON.stringify(body) : undefined; if (bodyStr) headers['Content-Type'] = 'application/json'; return request('https://api.github.com' + path, { method: method || 'GET', headers, body: bodyStr }); } async function addReaction(content) { try { await ghRequest( '/repos/' + owner + '/' + repo + '/issues/comments/' + commentId + '/reactions', 'POST', { content } ); } catch (e) { console.warn('Could not add reaction:', e.message); } } async function postComment(text) { const res = await ghRequest( '/repos/' + owner + '/' + repo + '/issues/' + issueNumber + '/comments', 'POST', { body: text } ); if (!res.ok) console.warn('Could not post comment:', res.body); } // ── Permission check ─────────────────────────────────────────────── const association = process.env.ACTOR_ASSOCIATION; if (association !== 'OWNER' && association !== 'MEMBER') { await addReaction('-1'); await postComment( '❌ **PocketBase Bot**: @' + actor + ' is not authorized to use this command.\n' + 'Only org members (Contributors team) can use `/pocketbase`.' ); process.exit(0); } // ── Acknowledge ──────────────────────────────────────────────────── await addReaction('eyes'); // ── Parse command ────────────────────────────────────────────────── const commentBody = process.env.COMMENT_BODY || ''; const lines = commentBody.trim().split('\n'); const firstLine = lines[0].trim(); const withoutCmd = firstLine.replace(/^\/pocketbase\s+/, '').trim(); function extractCodeBlock(body) { const m = body.match(/```[^\n]*\n([\s\S]*?)```/); return m ? m[1].trim() : null; } const codeBlockValue = extractCodeBlock(commentBody); const HELP_TEXT = '**Show current state:**\n' + '```\n/pocketbase info\n```\n\n' + '**Field update (simple):** `/pocketbase field=value [field=value ...]`\n\n' + '**Field update (HTML/multiline) — value from code block:**\n' + '````\n' + '/pocketbase set description\n' + '```html\n' + '

Your HTML or multi-line content here

\n' + '```\n' + '````\n\n' + '**Note management:**\n' + '```\n' + '/pocketbase note list\n' + '/pocketbase note add ""\n' + '/pocketbase note edit "" ""\n' + '/pocketbase note remove ""\n' + '```\n\n' + '**Install method management:**\n' + '```\n' + '/pocketbase method list\n' + '/pocketbase method cpu=4 ram=2048 hdd=20\n' + '/pocketbase method config_path="/opt/app/.env"\n' + '/pocketbase method os=debian version=13\n' + '/pocketbase method add cpu=2 ram=2048 hdd=8 os=debian version=13\n' + '/pocketbase method remove \n' + '```\n' + 'Method fields: `cpu` `ram` `hdd` `os` `version` `config_path` `script`\n\n' + '**Editable fields:** `name` `description` `logo` `documentation` `website` `project_url` `github` ' + '`config_path` `port` `default_user` `default_passwd` ' + '`updateable` `privileged` `has_arm` `is_dev` ' + '`is_disabled` `disable_message` `is_deleted` `deleted_message`'; if (!withoutCmd) { await addReaction('-1'); await postComment('❌ **PocketBase Bot**: No slug or command specified.\n\n' + HELP_TEXT); process.exit(0); } const spaceIdx = withoutCmd.indexOf(' '); const slug = (spaceIdx === -1 ? withoutCmd : withoutCmd.substring(0, spaceIdx)).trim(); const rest = spaceIdx === -1 ? '' : withoutCmd.substring(spaceIdx + 1).trim(); if (!rest) { await addReaction('-1'); await postComment('❌ **PocketBase Bot**: No command specified for slug `' + slug + '`.\n\n' + HELP_TEXT); process.exit(0); } // ── PocketBase: authenticate ─────────────────────────────────────── const raw = process.env.POCKETBASE_URL.replace(/\/$/, ''); const apiBase = /\/api$/i.test(raw) ? raw : raw + '/api'; const coll = process.env.POCKETBASE_COLLECTION; const authRes = await request(apiBase + '/collections/users/auth-with-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identity: process.env.POCKETBASE_ADMIN_EMAIL, password: process.env.POCKETBASE_ADMIN_PASSWORD }) }); if (!authRes.ok) { await addReaction('-1'); await postComment('❌ **PocketBase Bot**: PocketBase authentication failed. CC @' + owner + '/maintainers'); process.exit(1); } const token = JSON.parse(authRes.body).token; // ── PocketBase: find record by slug ──────────────────────────────── const recordsUrl = apiBase + '/collections/' + encodeURIComponent(coll) + '/records'; const filter = "(slug='" + slug.replace(/'/g, "''") + "')"; const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1', { headers: { 'Authorization': token } }); const list = JSON.parse(listRes.body); const record = list.items && list.items[0]; if (!record) { await addReaction('-1'); await postComment( '❌ **PocketBase Bot**: No record found for slug `' + slug + '`.\n\n' + 'Make sure the script was already pushed to PocketBase (JSON must exist and have been synced).' ); process.exit(0); } // ── Shared helpers ───────────────────────────────────────────────── // Key=value parser: handles unquoted and "quoted" values function parseKVPairs(str) { const fields = {}; let pos = 0; while (pos < str.length) { while (pos < str.length && /\s/.test(str[pos])) pos++; if (pos >= str.length) break; let keyStart = pos; while (pos < str.length && str[pos] !== '=' && !/\s/.test(str[pos])) pos++; const key = str.substring(keyStart, pos).trim(); if (!key || pos >= str.length || str[pos] !== '=') { pos++; continue; } pos++; let value; if (pos < str.length && str[pos] === '"') { pos++; let valStart = pos; while (pos < str.length && str[pos] !== '"') { if (str[pos] === '\\') pos++; pos++; } value = str.substring(valStart, pos).replace(/\\"/g, '"'); if (pos < str.length) pos++; } else { let valStart = pos; while (pos < str.length && !/\s/.test(str[pos])) pos++; value = str.substring(valStart, pos); } fields[key] = value; } return fields; } // Token parser for note commands: unquoted-word OR "quoted string" function parseTokens(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; } // Read JSON blob from record (handles parsed objects and strings) function readJsonBlob(val) { if (Array.isArray(val)) return val; try { return JSON.parse(val || '[]'); } catch (e) { return []; } } // Frontend cache revalidation (silent, best-effort) async function revalidate(s) { const frontendUrl = process.env.FRONTEND_URL; const secret = process.env.REVALIDATE_SECRET; if (!frontendUrl || !secret) return; try { await request(frontendUrl.replace(/\/$/, '') + '/api/revalidate', { method: 'POST', headers: { 'Authorization': 'Bearer ' + secret, 'Content-Type': 'application/json' }, body: JSON.stringify({ tags: ['scripts', 'script-' + s] }) }); } catch (e) { console.warn('Revalidation skipped:', e.message); } } // Format notes list for display function formatNotesList(arr) { if (arr.length === 0) return '*None*'; return arr.map(function (n, i) { return (i + 1) + '. **`' + (n.type || '?') + '`**: ' + (n.text || ''); }).join('\n'); } // Format install methods list for display function formatMethodsList(arr) { if (arr.length === 0) return '*None*'; return arr.map(function (im, i) { const r = im.resources || {}; const parts = [ (r.os || '?') + ' ' + (r.version || '?'), (r.cpu != null ? r.cpu : '?') + 'C / ' + (r.ram != null ? r.ram : '?') + ' MB / ' + (r.hdd != null ? r.hdd : '?') + ' GB' ]; if (im.config_path) parts.push('config: `' + im.config_path + '`'); if (im.script) parts.push('script: `' + im.script + '`'); return (i + 1) + '. **`' + (im.type || '?') + '`** — ' + parts.join(', '); }).join('\n'); } // ── Route: dispatch to subcommand handler ────────────────────────── const infoMatch = rest.match(/^info$/i); const noteMatch = rest.match(/^note\s+(list|add|edit|remove)\b/i); const methodMatch = rest.match(/^method\b/i); const setMatch = rest.match(/^set\s+(\S+)/i); if (infoMatch) { // ── INFO SUBCOMMAND ────────────────────────────────────────────── const notesArr = readJsonBlob(record.notes_json); const methodsArr = readJsonBlob(record.install_methods_json); const out = []; out.push('ℹ️ **PocketBase Bot**: Info for **`' + slug + '`**\n'); out.push('**Basic info:**'); out.push('- **Name:** ' + (record.name || '—')); out.push('- **Slug:** `' + slug + '`'); out.push('- **Port:** ' + (record.port != null ? '`' + record.port + '`' : '—')); out.push('- **Updateable:** ' + (record.updateable ? 'Yes' : 'No')); out.push('- **Privileged:** ' + (record.privileged ? 'Yes' : 'No')); out.push('- **ARM:** ' + (record.has_arm ? 'Yes' : 'No')); if (record.is_dev) out.push('- **Dev:** Yes'); if (record.is_disabled) out.push('- **Disabled:** Yes' + (record.disable_message ? ' — ' + record.disable_message : '')); if (record.is_deleted) out.push('- **Deleted:** Yes' + (record.deleted_message ? ' — ' + record.deleted_message : '')); out.push(''); out.push('**Links:**'); out.push('- **Website:** ' + (record.website || '—')); out.push('- **Docs:** ' + (record.documentation || '—')); out.push('- **Logo:** ' + (record.logo ? '[link](' + record.logo + ')' : '—')); out.push('- **GitHub:** ' + (record.github || '—')); if (record.config_path) out.push('- **Config:** `' + record.config_path + '`'); out.push(''); out.push('**Credentials:**'); out.push('- **User:** ' + (record.default_user || '—')); out.push('- **Password:** ' + (record.default_passwd ? '*(set)*' : '—')); out.push(''); out.push('**Install methods** (' + methodsArr.length + '):'); out.push(formatMethodsList(methodsArr)); out.push(''); out.push('**Notes** (' + notesArr.length + '):'); out.push(formatNotesList(notesArr)); await addReaction('+1'); await postComment(out.join('\n')); } else if (noteMatch) { // ── NOTE SUBCOMMAND ────────────────────────────────────────────── const noteAction = noteMatch[1].toLowerCase(); const noteArgsStr = rest.substring(noteMatch[0].length).trim(); let notesArr = readJsonBlob(record.notes_json); async function patchNotes(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:\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 = parseTokens(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 patchNotes(notesArr); await revalidate(slug); 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 = parseTokens(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 patchNotes(notesArr); await revalidate(slug); 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 = parseTokens(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 patchNotes(notesArr); await revalidate(slug); 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 ──────────────────────────────────────────── const methodArgs = rest.replace(/^method\s*/i, '').trim(); const methodListMode = !methodArgs || methodArgs.toLowerCase() === 'list'; let methodsArr = readJsonBlob(record.install_methods_json); // Method field classification const RESOURCE_KEYS = { cpu: 'number', ram: 'number', hdd: 'number', os: 'string', version: 'string' }; const METHOD_KEYS = { config_path: 'string', script: 'string' }; const ALL_METHOD_KEYS = Object.assign({}, RESOURCE_KEYS, METHOD_KEYS); function applyMethodChanges(method, parsed) { if (!method.resources) method.resources = {}; for (const [k, v] of Object.entries(parsed)) { if (RESOURCE_KEYS[k]) { method.resources[k] = RESOURCE_KEYS[k] === 'number' ? parseInt(v, 10) : v; } else if (METHOD_KEYS[k]) { method[k] = v === '' ? null : v; } } } async function patchMethods(arr) { const res = await request(recordsUrl + '/' + record.id, { method: 'PATCH', headers: { 'Authorization': token, 'Content-Type': 'application/json' }, body: JSON.stringify({ install_methods_json: arr }) }); if (!res.ok) { await addReaction('-1'); await postComment('❌ **PocketBase Bot**: Failed to update install methods:\n```\n' + res.body + '\n```'); process.exit(1); } } if (methodListMode) { await addReaction('+1'); await postComment( 'ℹ️ **PocketBase Bot**: Install methods for **`' + slug + '`** (' + methodsArr.length + ' total)\n\n' + formatMethodsList(methodsArr) ); } else { // Check for add / remove sub-actions const addMatch = methodArgs.match(/^add\s+(\S+)(?:\s+(.+))?$/i); const removeMatch = methodArgs.match(/^remove\s+(\S+)$/i); if (addMatch) { // ── METHOD ADD ─────────────────────────────────────────────── const newType = addMatch[1]; if (methodsArr.some(function (im) { return (im.type || '').toLowerCase() === newType.toLowerCase(); })) { await addReaction('-1'); await postComment('❌ **PocketBase Bot**: Install method `' + newType + '` already exists for `' + slug + '`.\n\nUse `/pocketbase ' + slug + ' method list` to see all methods.'); process.exit(0); } const newMethod = { type: newType, resources: { cpu: 1, ram: 512, hdd: 4, os: 'debian', version: '13' } }; if (addMatch[2]) { const parsed = parseKVPairs(addMatch[2]); const unknown = Object.keys(parsed).filter(function (k) { return !ALL_METHOD_KEYS[k]; }); if (unknown.length > 0) { await addReaction('-1'); await postComment('❌ **PocketBase Bot**: Unknown method field(s): `' + unknown.join('`, `') + '`\n\n**Allowed:** `' + Object.keys(ALL_METHOD_KEYS).join('`, `') + '`'); process.exit(0); } applyMethodChanges(newMethod, parsed); } methodsArr.push(newMethod); await patchMethods(methodsArr); await revalidate(slug); await addReaction('+1'); await postComment( '✅ **PocketBase Bot**: Added install method **`' + newType + '`** to **`' + slug + '`**\n\n' + formatMethodsList([newMethod]) + '\n\n' + '*Executed by @' + actor + '*' ); } else if (removeMatch) { // ── METHOD REMOVE ──────────────────────────────────────────── const removeType = removeMatch[1].toLowerCase(); const removed = methodsArr.filter(function (im) { return (im.type || '').toLowerCase() === removeType; }); if (removed.length === 0) { await addReaction('-1'); const available = methodsArr.map(function (im) { return im.type || '?'; }); await postComment('❌ **PocketBase Bot**: No install method `' + removeType + '` found.\n\n**Available:** `' + (available.length ? available.join('`, `') : '(none)') + '`'); process.exit(0); } methodsArr = methodsArr.filter(function (im) { return (im.type || '').toLowerCase() !== removeType; }); await patchMethods(methodsArr); await revalidate(slug); await addReaction('+1'); await postComment( '✅ **PocketBase Bot**: Removed install method **`' + removed[0].type + '`** from **`' + slug + '`**\n\n' + '*Executed by @' + actor + '*' ); } else { // ── METHOD EDIT ────────────────────────────────────────────── const editParts = methodArgs.match(/^(\S+)\s+(.+)$/); if (!editParts) { await addReaction('-1'); await postComment( '❌ **PocketBase Bot**: Invalid `method` syntax.\n\n' + '**Usage:**\n```\n/pocketbase ' + slug + ' method list\n' + '/pocketbase ' + slug + ' method cpu=4 ram=2048 hdd=20\n' + '/pocketbase ' + slug + ' method config_path="/opt/app/.env"\n' + '/pocketbase ' + slug + ' method add cpu=2 ram=2048 hdd=8\n' + '/pocketbase ' + slug + ' method remove \n```' ); process.exit(0); } const targetType = editParts[1].toLowerCase(); const parsed = parseKVPairs(editParts[2]); const unknown = Object.keys(parsed).filter(function (k) { return !ALL_METHOD_KEYS[k]; }); if (unknown.length > 0) { await addReaction('-1'); await postComment('❌ **PocketBase Bot**: Unknown method field(s): `' + unknown.join('`, `') + '`\n\n**Allowed:** `' + Object.keys(ALL_METHOD_KEYS).join('`, `') + '`'); process.exit(0); } if (Object.keys(parsed).length === 0) { await addReaction('-1'); await postComment('❌ **PocketBase Bot**: No valid `key=value` pairs found.\n\n**Allowed:** `' + Object.keys(ALL_METHOD_KEYS).join('`, `') + '`'); process.exit(0); } const idx = methodsArr.findIndex(function (im) { return (im.type || '').toLowerCase() === targetType; }); if (idx === -1) { await addReaction('-1'); const available = methodsArr.map(function (im) { return im.type || '?'; }); await postComment( '❌ **PocketBase Bot**: No install method `' + targetType + '` found for `' + slug + '`.\n\n' + '**Available:** `' + (available.length ? available.join('`, `') : '(none)') + '`\n\n' + 'Use `/pocketbase ' + slug + ' method list` to see all methods.' ); process.exit(0); } applyMethodChanges(methodsArr[idx], parsed); await patchMethods(methodsArr); await revalidate(slug); const changesLines = Object.entries(parsed) .map(function ([k, v]) { const unit = k === 'ram' ? ' MB' : k === 'hdd' ? ' GB' : ''; return '- `' + k + '` → `' + v + unit + '`'; }).join('\n'); await addReaction('+1'); await postComment( '✅ **PocketBase Bot**: Updated install method **`' + methodsArr[idx].type + '`** for **`' + slug + '`**\n\n' + '**Changes applied:**\n' + changesLines + '\n\n' + '*Executed by @' + actor + '*' ); } } } else if (setMatch) { // ── SET SUBCOMMAND (value from code block) ─────────────────────── const fieldName = setMatch[1].toLowerCase(); const SET_ALLOWED = { name: 'string', description: 'string', logo: 'string', documentation: 'string', website: 'string', project_url: 'string', github: 'string', config_path: 'string', disable_message: 'string', deleted_message: 'string' }; if (!SET_ALLOWED[fieldName]) { await addReaction('-1'); await postComment( '❌ **PocketBase Bot**: `set` only supports text fields.\n\n' + '**Allowed:** `' + Object.keys(SET_ALLOWED).join('`, `') + '`\n\n' + 'For boolean/number fields use `field=value` syntax instead.' ); process.exit(0); } if (!codeBlockValue) { await addReaction('-1'); await postComment( '❌ **PocketBase Bot**: `set` requires a code block with the value.\n\n' + '**Usage:**\n````\n/pocketbase ' + slug + ' set ' + fieldName + '\n```\nYour content here (HTML, multiline, special chars all fine)\n```\n````' ); process.exit(0); } const setPayload = {}; setPayload[fieldName] = codeBlockValue; const setPatchRes = await request(recordsUrl + '/' + record.id, { method: 'PATCH', headers: { 'Authorization': token, 'Content-Type': 'application/json' }, body: JSON.stringify(setPayload) }); if (!setPatchRes.ok) { await addReaction('-1'); await postComment('❌ **PocketBase Bot**: PATCH failed for `' + slug + '`:\n```\n' + setPatchRes.body + '\n```'); process.exit(1); } await revalidate(slug); const preview = codeBlockValue.length > 300 ? codeBlockValue.substring(0, 300) + '…' : codeBlockValue; await addReaction('+1'); await postComment( '✅ **PocketBase Bot**: Set `' + fieldName + '` for **`' + slug + '`**\n\n' + '**Value set:**\n```\n' + preview + '\n```\n\n' + '*Executed by @' + actor + '*' ); } else { // ── FIELD=VALUE PATH ───────────────────────────────────────────── const ALLOWED_FIELDS = { name: 'string', description: 'string', logo: 'string', documentation: 'string', website: 'string', project_url: 'string', github: 'string', config_path: 'string', port: 'number', default_user: 'nullable_string', default_passwd: 'nullable_string', updateable: 'boolean', privileged: 'boolean', has_arm: 'boolean', is_dev: 'boolean', is_disabled: 'boolean', disable_message: 'string', is_deleted: 'boolean', deleted_message: 'string', }; const parsedFields = parseKVPairs(rest); const unknownFields = Object.keys(parsedFields).filter(function (f) { return !ALLOWED_FIELDS[f]; }); if (unknownFields.length > 0) { await addReaction('-1'); await postComment( '❌ **PocketBase Bot**: Unknown field(s): `' + unknownFields.join('`, `') + '`\n\n' + '**Allowed fields:** `' + Object.keys(ALLOWED_FIELDS).join('`, `') + '`' ); process.exit(0); } if (Object.keys(parsedFields).length === 0) { await addReaction('-1'); await postComment('❌ **PocketBase Bot**: Could not parse any valid `field=value` pairs.\n\n' + HELP_TEXT); process.exit(0); } // Cast values to correct types const payload = {}; for (const [key, rawVal] of Object.entries(parsedFields)) { const type = ALLOWED_FIELDS[key]; if (type === 'boolean') { if (rawVal === 'true') payload[key] = true; else if (rawVal === 'false') payload[key] = false; else { await addReaction('-1'); await postComment('❌ **PocketBase Bot**: `' + key + '` must be `true` or `false`, got: `' + rawVal + '`'); process.exit(0); } } else if (type === 'number') { const n = parseInt(rawVal, 10); if (isNaN(n)) { await addReaction('-1'); await postComment('❌ **PocketBase Bot**: `' + key + '` must be a number, got: `' + rawVal + '`'); process.exit(0); } payload[key] = n; } else if (type === 'nullable_string') { payload[key] = rawVal === '' ? null : rawVal; } else { payload[key] = rawVal; } } const patchRes = await request(recordsUrl + '/' + record.id, { method: 'PATCH', headers: { 'Authorization': token, 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!patchRes.ok) { await addReaction('-1'); await postComment('❌ **PocketBase Bot**: PATCH failed for `' + slug + '`:\n```\n' + patchRes.body + '\n```'); process.exit(1); } await revalidate(slug); await addReaction('+1'); const changesLines = Object.entries(payload) .map(function ([k, v]) { return '- `' + k + '` → `' + JSON.stringify(v) + '`'; }) .join('\n'); await postComment( '✅ **PocketBase Bot**: Updated **`' + slug + '`** successfully!\n\n' + '**Changes applied:**\n' + changesLines + '\n\n' + '*Executed by @' + actor + '*' ); } console.log('Done.'); })().catch(function (e) { console.error('Fatal error:', e.message || e); process.exit(1); }); ENDSCRIPT shell: bash