mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2026-04-25 11:25:05 +02:00
notes_json was sent as JSON.stringify(arr) inside JSON.stringify(), causing PocketBase to receive a string instead of a JSON array. patchMethods already does it correctly — align patchNotes.
802 lines
40 KiB
YAML
Generated
802 lines
40 KiB
YAML
Generated
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 <slug> info\n```\n\n' +
|
||
'**Field update (simple):** `/pocketbase <slug> field=value [field=value ...]`\n\n' +
|
||
'**Field update (HTML/multiline) — value from code block:**\n' +
|
||
'````\n' +
|
||
'/pocketbase <slug> set description\n' +
|
||
'```html\n' +
|
||
'<p>Your <b>HTML</b> or multi-line content here</p>\n' +
|
||
'```\n' +
|
||
'````\n\n' +
|
||
'**Note management:**\n' +
|
||
'```\n' +
|
||
'/pocketbase <slug> note list\n' +
|
||
'/pocketbase <slug> note add <type> "<text>"\n' +
|
||
'/pocketbase <slug> note edit <type> "<old text>" "<new text>"\n' +
|
||
'/pocketbase <slug> note remove <type> "<text>"\n' +
|
||
'```\n\n' +
|
||
'**Install method management:**\n' +
|
||
'```\n' +
|
||
'/pocketbase <slug> method list\n' +
|
||
'/pocketbase <slug> method <type> cpu=4 ram=2048 hdd=20\n' +
|
||
'/pocketbase <slug> method <type> config_path="/opt/app/.env"\n' +
|
||
'/pocketbase <slug> method <type> os=debian version=13\n' +
|
||
'/pocketbase <slug> method add <type> cpu=2 ram=2048 hdd=8 os=debian version=13\n' +
|
||
'/pocketbase <slug> method remove <type>\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: 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 `<type>` and `"<text>"`.\n\n' +
|
||
'**Usage:** `/pocketbase ' + slug + ' note add <type> "<text>"`'
|
||
);
|
||
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 `<type>`, `"<old text>"`, and `"<new text>"`.\n\n' +
|
||
'**Usage:** `/pocketbase ' + slug + ' note edit <type> "<old text>" "<new text>"`\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 `<type>` and `"<text>"`.\n\n' +
|
||
'**Usage:** `/pocketbase ' + slug + ' note remove <type> "<text>"`\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 <type> cpu=4 ram=2048 hdd=20\n' +
|
||
'/pocketbase ' + slug + ' method <type> config_path="/opt/app/.env"\n' +
|
||
'/pocketbase ' + slug + ' method add <type> cpu=2 ram=2048 hdd=8\n' +
|
||
'/pocketbase ' + slug + ' method remove <type>\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
|