mirror of
https://github.com/ancsemi/Haven.git
synced 2026-10-27 00:54:56 +00:00
Admins download the template under Settings > Admin > Server Template, and import one with a preview of what it will create or update. Add missing leaves existing channels, roles and settings alone; Replace layout updates them to match. Neither mode deletes anything. Members, messages, DMs, keys, invite and channel codes, webhook tokens and secret settings are never exported. Settings go through an allowlist, and imports are validated strictly: sizes, names, references, image types and checksums. The channel-name rule, built-in theme list, automod word and escalation checks and image sniffing now each live in one place, since the template code shares them. Co-authored-by: ancsemi <ancsemi@gmail.com>
98 lines
5.9 KiB
JavaScript
98 lines
5.9 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict';
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const USAGE = `Haven server templates
|
|
|
|
node tools/template.js export --data <dir> [--out file.json] [--posts pinned|none] [--post-authors "Guide,Welcome Bot"]
|
|
[--exclude-channel name]... [--no-assets] [--name text] [--description text] [--author text]
|
|
node tools/template.js import <file.json> --data <dir> [--mode merge|replace] [--dry-run] [--as admin-username]
|
|
[--no-posts] [--no-webhooks] [--no-join] [--install-themes]
|
|
node tools/template.js check <file.json>
|
|
|
|
--data is Haven's data folder (the one holding haven.db and uploads/).
|
|
export only reads the database. Stop Haven before import, or restart it afterwards.
|
|
merge adds what is missing and leaves everything already on the server alone.
|
|
replace also updates matching channels, roles and settings to the template. Neither mode deletes anything.`;
|
|
function parseArgs(argv) {
|
|
const out = { _: [], multi: {} };
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const a = argv[i];
|
|
if (!a.startsWith('--')) { out._.push(a); continue; }
|
|
const key = a.slice(2);
|
|
if (['dry-run', 'no-assets', 'no-posts', 'no-webhooks', 'no-join', 'install-themes', 'help'].includes(key)) { out[key] = true; continue; }
|
|
const val = argv[++i];
|
|
if (val === undefined) throw new Error(`--${key} needs a value`);
|
|
key === 'exclude-channel' ? (out.multi[key] = [...(out.multi[key] || []), val]) : (out[key] = val);
|
|
}
|
|
return out;
|
|
}
|
|
function readTemplate(file) {
|
|
const stat = fs.statSync(file);
|
|
const { LIMITS, validateTemplate } = require('../src/serverTemplate');
|
|
if (stat.size > LIMITS.bytes) throw new Error(`${file} is larger than ${LIMITS.bytes / 1048576} MB`);
|
|
let json;
|
|
try { json = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (e) { throw new Error(`${file} is not valid JSON: ${e.message}`); }
|
|
const res = validateTemplate(json);
|
|
if (res.errors) throw new Error(`The template is not valid:\n ${res.errors.join('\n ')}`);
|
|
res.warnings.forEach((w) => console.warn(`warning: ${w}`));
|
|
return res.template;
|
|
}
|
|
function printReport(r) {
|
|
const line = (label, list) => list.length && console.log(`${label} (${list.length}): ${list.join(', ')}`);
|
|
console.log(`${r.dryRun ? 'Dry run, nothing was changed' : 'Applied'} (${r.mode})`);
|
|
line('Created roles', r.created.roles); line('Created channels', r.created.channels); line('Updated roles', r.updated.roles); line('Updated channels', r.updated.channels);
|
|
line('Updated settings', r.updated.settings); line('Already on the server, left alone', [...r.existing.roles, ...r.existing.channels, ...r.existing.settings]);
|
|
line('On the server but not in the template, kept', [...r.extra.roles, ...r.extra.channels]); line('Created webhooks', r.created.webhooks);
|
|
line('Copied files', r.created.files); line('Installed themes', r.created.themes); line('Created emojis', r.created.emojis); line('Created stickers', r.created.stickers);
|
|
console.log(`Posts: ${r.counts.posts}, role menus: ${r.counts.roleMenus}, link rules: ${r.counts.domains}, role channel access rows: ${r.counts.access}`);
|
|
r.warnings.forEach((w) => console.warn(`warning: ${w}`));
|
|
}
|
|
function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
const cmd = args._[0];
|
|
if (!cmd || args.help || !['export', 'import', 'check'].includes(cmd)) return console.log(USAGE);
|
|
if (cmd === 'check') {
|
|
const tpl = readTemplate(args._[1] || '');
|
|
return console.log(JSON.stringify(require('../src/serverTemplate').summarizeTemplate(tpl), null, 2));
|
|
}
|
|
if (!args.data) throw new Error('--data <dir> is required');
|
|
const dataDir = path.resolve(args.data);
|
|
const themesDir = path.join(__dirname, '..', 'themes');
|
|
if (cmd === 'export') {
|
|
const Database = require('better-sqlite3');
|
|
const db = new Database(path.join(dataDir, 'haven.db'), { readonly: true, fileMustExist: true });
|
|
const { exportTemplate } = require('../src/serverTemplate');
|
|
const { template, warnings } = exportTemplate(db, {
|
|
uploadsDir: path.join(dataDir, 'uploads'), themesDir, posts: args.posts === 'none' ? 'none' : 'pinned', assets: !args['no-assets'],
|
|
postAuthors: args['post-authors'] ? args['post-authors'].split(',').map((s) => s.trim()).filter(Boolean) : null,
|
|
excludeChannels: args.multi['exclude-channel'] || [], meta: { name: args.name, description: args.description, author: args.author },
|
|
havenVersion: require('../package.json').version,
|
|
});
|
|
db.close();
|
|
warnings.forEach((w) => console.warn(`warning: ${w}`));
|
|
const json = JSON.stringify(template, null, 2) + '\n';
|
|
args.out ? fs.writeFileSync(args.out, json) : process.stdout.write(json);
|
|
if (args.out) console.error(`Wrote ${args.out}: ${template.channels.length} channels, ${template.roles.length} roles, ${template.posts.length} posts, ${Object.keys(template.assets).length} files`);
|
|
return;
|
|
}
|
|
const file = args._[1];
|
|
if (!file) throw new Error('Give the template file to import');
|
|
const tpl = readTemplate(file);
|
|
process.env.HAVEN_DATA_DIR = dataDir;
|
|
const { initDatabase } = require('../src/database');
|
|
const db = initDatabase();
|
|
const actor = args.as
|
|
? db.prepare('SELECT id FROM users WHERE username = ? COLLATE NOCASE AND is_admin = 1').get(args.as)
|
|
: db.prepare('SELECT id FROM users WHERE is_admin = 1 ORDER BY id LIMIT 1').get();
|
|
if (args.as && !actor) throw new Error(`${args.as} is not an admin on this server`);
|
|
const { UPLOADS_DIR } = require('../src/paths');
|
|
const report = require('../src/serverTemplate').applyTemplate(db, tpl, {
|
|
mode: args.mode === 'replace' ? 'replace' : 'merge', actorId: actor ? actor.id : null, uploadsDir: UPLOADS_DIR, themesDir,
|
|
dryRun: !!args['dry-run'], posts: !args['no-posts'], webhooks: !args['no-webhooks'], joinMembers: !args['no-join'], installThemes: !!args['install-themes'],
|
|
});
|
|
db.close();
|
|
printReport(report);
|
|
}
|
|
try { main(); } catch (err) { console.error(err.message); process.exitCode = 1; }
|