895 lines
32 KiB
JavaScript
895 lines
32 KiB
JavaScript
const fs = require("fs");
|
|
const {
|
|
Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell,
|
|
Header, Footer, AlignmentType, LevelFormat, HeadingLevel,
|
|
BorderStyle, WidthType, ShadingType, PageNumber, PageBreak,
|
|
TabStopType, TabStopPosition
|
|
} = require("docx");
|
|
|
|
// --- Color palette ---
|
|
const CLR = {
|
|
title: "1A5276",
|
|
heading: "1A5276",
|
|
accent: "2E75B6",
|
|
dimText: "555555",
|
|
tblHead: "D6EAF8",
|
|
tblAlt: "F2F8FC",
|
|
border: "B0C4DE",
|
|
black: "000000",
|
|
white: "FFFFFF",
|
|
};
|
|
|
|
const border = { style: BorderStyle.SINGLE, size: 1, color: CLR.border };
|
|
const borders = { top: border, bottom: border, left: border, right: border };
|
|
const cellMargins = { top: 60, bottom: 60, left: 100, right: 100 };
|
|
const noBorders = {
|
|
top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
|
bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
|
left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
|
right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
|
|
};
|
|
|
|
// --- Helpers ---
|
|
function h1(text) {
|
|
return new Paragraph({
|
|
heading: HeadingLevel.HEADING_1,
|
|
children: [new TextRun(text)],
|
|
spacing: { before: 360, after: 200 },
|
|
});
|
|
}
|
|
|
|
function h2(text) {
|
|
return new Paragraph({
|
|
heading: HeadingLevel.HEADING_2,
|
|
children: [new TextRun(text)],
|
|
spacing: { before: 280, after: 160 },
|
|
});
|
|
}
|
|
|
|
function h3(text) {
|
|
return new Paragraph({
|
|
heading: HeadingLevel.HEADING_3,
|
|
children: [new TextRun(text)],
|
|
spacing: { before: 200, after: 120 },
|
|
});
|
|
}
|
|
|
|
function para(text, opts = {}) {
|
|
const runs = [];
|
|
if (typeof text === "string") {
|
|
runs.push(new TextRun({ text, ...opts }));
|
|
} else {
|
|
// Array of TextRun configs
|
|
for (const t of text) {
|
|
runs.push(new TextRun(t));
|
|
}
|
|
}
|
|
return new Paragraph({
|
|
children: runs,
|
|
spacing: { after: 140 },
|
|
});
|
|
}
|
|
|
|
function bold(text) {
|
|
return { text, bold: true };
|
|
}
|
|
|
|
function code(text) {
|
|
return { text, font: "Courier New", size: 20, color: "333333" };
|
|
}
|
|
|
|
function codeBlock(lines) {
|
|
return lines.map(line =>
|
|
new Paragraph({
|
|
children: [new TextRun({ text: line || " ", font: "Courier New", size: 19, color: "333333" })],
|
|
spacing: { after: 20 },
|
|
indent: { left: 400 },
|
|
shading: { type: ShadingType.CLEAR, fill: "F5F5F5" },
|
|
})
|
|
);
|
|
}
|
|
|
|
function bullet(text, opts = {}) {
|
|
const runs = [];
|
|
if (typeof text === "string") {
|
|
runs.push(new TextRun(text));
|
|
} else {
|
|
for (const t of text) runs.push(new TextRun(t));
|
|
}
|
|
return new Paragraph({
|
|
numbering: { reference: "bullets", level: opts.level || 0 },
|
|
children: runs,
|
|
spacing: { after: 80 },
|
|
});
|
|
}
|
|
|
|
function makeRow(cells, opts = {}) {
|
|
return new TableRow({
|
|
children: cells.map((text, i) => {
|
|
const widths = opts.widths || [4680, 4680];
|
|
return new TableCell({
|
|
borders,
|
|
width: { size: widths[i], type: WidthType.DXA },
|
|
margins: cellMargins,
|
|
shading: opts.shading ? { fill: opts.shading, type: ShadingType.CLEAR } : undefined,
|
|
children: [
|
|
new Paragraph({
|
|
children: [new TextRun({
|
|
text: String(text),
|
|
bold: opts.header || false,
|
|
font: "Arial",
|
|
size: opts.header ? 21 : 20,
|
|
color: opts.header ? CLR.heading : CLR.black,
|
|
})],
|
|
}),
|
|
],
|
|
});
|
|
}),
|
|
});
|
|
}
|
|
|
|
function makeTable(headers, rows, widths) {
|
|
const totalWidth = widths.reduce((a, b) => a + b, 0);
|
|
return new Table({
|
|
width: { size: totalWidth, type: WidthType.DXA },
|
|
columnWidths: widths,
|
|
rows: [
|
|
makeRow(headers, { widths, header: true, shading: CLR.tblHead }),
|
|
...rows.map((row, i) =>
|
|
makeRow(row, { widths, shading: i % 2 === 1 ? CLR.tblAlt : undefined })
|
|
),
|
|
],
|
|
});
|
|
}
|
|
|
|
function spacer() {
|
|
return new Paragraph({ spacing: { after: 80 }, children: [] });
|
|
}
|
|
|
|
// --- Document content ---
|
|
const children = [];
|
|
|
|
// ==================== TITLE PAGE ====================
|
|
children.push(new Paragraph({ spacing: { before: 3000 }, children: [] }));
|
|
children.push(new Paragraph({
|
|
alignment: AlignmentType.CENTER,
|
|
children: [new TextRun({ text: "URIT BBS", size: 72, bold: true, color: CLR.title, font: "Arial" })],
|
|
}));
|
|
children.push(new Paragraph({
|
|
alignment: AlignmentType.CENTER,
|
|
spacing: { after: 400 },
|
|
children: [new TextRun({ text: "Sysop Guide", size: 48, color: CLR.accent, font: "Arial" })],
|
|
}));
|
|
children.push(new Paragraph({
|
|
alignment: AlignmentType.CENTER,
|
|
spacing: { after: 200 },
|
|
children: [new TextRun({ text: "Version 0.2.0", size: 24, color: CLR.dimText, font: "Arial" })],
|
|
}));
|
|
children.push(new Paragraph({
|
|
alignment: AlignmentType.CENTER,
|
|
children: [new TextRun({
|
|
text: "A modern reimplementation of T.A.G.-BBS (1986) in Go",
|
|
size: 22, color: CLR.dimText, font: "Arial", italics: true,
|
|
})],
|
|
}));
|
|
children.push(new Paragraph({ children: [new PageBreak()] }));
|
|
|
|
// ==================== 1. INTRODUCTION ====================
|
|
children.push(h1("Introduction"));
|
|
children.push(para(
|
|
"URIT is a multi-node BBS written in Go, inspired by T.A.G.-BBS (The Amiga Gazette BBS, 1986). " +
|
|
"It preserves the classic BBS experience\u2014ANSI screens, message boards, private mail, file libraries, " +
|
|
"bulletins\u2014while replacing the Amiga serial port with telnet, flat files with SQLite, and ZMODEM " +
|
|
"with HTTP file transfers. Features that the single-node original never had, such as multi-node " +
|
|
"support and inter-node chat, are included."
|
|
));
|
|
children.push(para(
|
|
"This guide covers everything a sysop needs to install, configure, and manage a URIT BBS. It assumes " +
|
|
"familiarity with BBS concepts (nodes, security levels, message boards, file areas) but not with " +
|
|
"URIT or TAG specifically."
|
|
));
|
|
|
|
// ==================== 2. BUILDING AND INSTALLING ====================
|
|
children.push(h1("Building and Installing"));
|
|
children.push(para("URIT is a single Go binary with no runtime dependencies beyond SQLite (bundled via CGo). You need:"));
|
|
children.push(bullet([bold("Go 1.22+"), { text: " \u2014 " }, code("go version"), { text: " to verify" }]));
|
|
children.push(bullet([bold("GCC"), { text: " \u2014 required by the SQLite CGo binding (gcc or musl-gcc)" }]));
|
|
children.push(spacer());
|
|
children.push(para("Clone and build:"));
|
|
children.push(...codeBlock([
|
|
"git clone https://github.com/urit/urit.git",
|
|
"cd urit",
|
|
"go build -o urit ./cmd/urit/",
|
|
]));
|
|
children.push(para(
|
|
"This produces a single binary. Dependencies are vendored, so no network access is needed during the build. " +
|
|
"Copy the binary wherever you like; all runtime paths are configurable."
|
|
));
|
|
|
|
// ==================== 3. QUICK START ====================
|
|
children.push(h1("Quick Start"));
|
|
children.push(para("Initialize a new BBS instance and start it:"));
|
|
children.push(...codeBlock([
|
|
"./urit init",
|
|
"./urit",
|
|
]));
|
|
children.push(para([
|
|
{ text: "The " }, code("init"), { text: " command walks you through naming the BBS and creating the sysop account. " +
|
|
"It creates a config.toml, a data directory with the SQLite database, seed content (boards, a bulletin, " +
|
|
"a file library), and a screens directory with starter ANSI files." }
|
|
]));
|
|
children.push(para([
|
|
{ text: "Once running, connect via telnet on port 2323 (" }, code("telnet localhost 2323"),
|
|
{ text: ") and visit " }, code("http://localhost:8080"),
|
|
{ text: " in a browser to see the landing page." }
|
|
]));
|
|
|
|
// ==================== 4. COMMAND-LINE USAGE ====================
|
|
children.push(h1("Command-Line Usage"));
|
|
children.push(para("URIT has two modes: server and initialization."));
|
|
children.push(spacer());
|
|
children.push(makeTable(
|
|
["Command", "Description"],
|
|
[
|
|
["urit", "Start the BBS server"],
|
|
["urit init", "Initialize a new BBS instance"],
|
|
["urit version", "Print version and exit"],
|
|
["urit help", "Show help"],
|
|
],
|
|
[3000, 6360],
|
|
));
|
|
children.push(spacer());
|
|
children.push(para([
|
|
{ text: "Both " }, code("urit"), { text: " and " }, code("urit init"),
|
|
{ text: " accept " }, code("-config <path>"),
|
|
{ text: " to specify a configuration file (default: config.toml in the current directory). " +
|
|
"The init command also accepts " }, code("-force"), { text: " to overwrite an existing database." },
|
|
]));
|
|
|
|
// ==================== 5. CONFIGURATION ====================
|
|
children.push(h1("Configuration"));
|
|
children.push(para(
|
|
"URIT uses a single TOML file for all configuration. A default config.toml is created during " +
|
|
"initialization. Below is a reference of every section."
|
|
));
|
|
|
|
// --- [system] ---
|
|
children.push(h2("[system]"));
|
|
children.push(makeTable(
|
|
["Key", "Default", "Description"],
|
|
[
|
|
["name", '"URIT BBS"', "BBS name shown on banners and the HTTP landing page"],
|
|
["sysop", '"Sysop"', "Sysop display name"],
|
|
["location", '"./data/"', "Base data directory"],
|
|
["screens", '"./screens/"', "Path to ANSI screen files"],
|
|
],
|
|
[2000, 2200, 5160],
|
|
));
|
|
|
|
// --- [telnet] ---
|
|
children.push(h2("[telnet]"));
|
|
children.push(makeTable(
|
|
["Key", "Default", "Description"],
|
|
[
|
|
["enabled", "true", "Enable or disable the telnet listener"],
|
|
["address", '":2323"', "Listen address and port"],
|
|
],
|
|
[2000, 2200, 5160],
|
|
));
|
|
|
|
// --- [http] ---
|
|
children.push(h2("[http]"));
|
|
children.push(makeTable(
|
|
["Key", "Default", "Description"],
|
|
[
|
|
["enabled", "true", "Enable or disable the HTTP file server"],
|
|
["address", '":8080"', "Listen address and port"],
|
|
],
|
|
[2000, 2200, 5160],
|
|
));
|
|
children.push(para(
|
|
"The HTTP server provides the system landing page, file library browsing, file uploads and downloads, " +
|
|
"and a health-check endpoint at /health."
|
|
));
|
|
|
|
// --- [storage] ---
|
|
children.push(h2("[storage]"));
|
|
children.push(makeTable(
|
|
["Key", "Default", "Description"],
|
|
[
|
|
["driver", '"sqlite"', "Storage backend (only sqlite is supported)"],
|
|
["sqlite_path", '"./data/urit.db"', "Path to the SQLite database file"],
|
|
],
|
|
[2000, 2800, 4560],
|
|
));
|
|
|
|
// --- [users] ---
|
|
children.push(h2("[users]"));
|
|
children.push(makeTable(
|
|
["Key", "Default", "Description"],
|
|
[
|
|
["max_accounts", "500", "Maximum registered accounts"],
|
|
["guest_time_limit", "1800", "Guest session time limit in seconds (30 min)"],
|
|
["new_time_limit", "3600", "New user session time limit (60 min)"],
|
|
["valid_time_limit", "7200", "Validated user time limit (120 min)"],
|
|
],
|
|
[2600, 1600, 5160],
|
|
));
|
|
children.push(spacer());
|
|
children.push(para(
|
|
"Time limits are enforced per calendar day. If a user reconnects within 12 hours, their remaining " +
|
|
"time carries over from the previous session. After 12 hours, the timer resets. The sysop account " +
|
|
"(SecStatus 255) always gets a full reset on every login."
|
|
));
|
|
|
|
// --- [users.*_security] ---
|
|
children.push(h2("[users.guest_security] / [users.new_security] / [users.valid_security]"));
|
|
children.push(makeTable(
|
|
["Key", "Description"],
|
|
[
|
|
["status", "SecStatus tier assigned to users in this class"],
|
|
["board", "SecBoard level assigned to users in this class"],
|
|
["library", "SecLibrary level assigned to users in this class"],
|
|
["bulletin", "SecBulletin level assigned to users in this class"],
|
|
],
|
|
[2000, 7360],
|
|
));
|
|
children.push(spacer());
|
|
children.push(para(
|
|
"These define the default security levels for each user class. When someone registers, they receive " +
|
|
"the new_security values. When the sysop validates them, they receive the valid_security values. " +
|
|
"See the Security Model section for details on what the levels mean."
|
|
));
|
|
|
|
// --- [logging] ---
|
|
children.push(h2("[logging]"));
|
|
children.push(makeTable(
|
|
["Key", "Default", "Description"],
|
|
[
|
|
["level", '"info"', "Log verbosity"],
|
|
["file", '""', "Log file path (empty means stdout)"],
|
|
],
|
|
[2000, 2200, 5160],
|
|
));
|
|
|
|
// ==================== 6. SECURITY MODEL ====================
|
|
children.push(h1("Security Model"));
|
|
children.push(para(
|
|
"URIT uses a four-axis security system inherited from the original TAG-BBS. Every user has four " +
|
|
"independent security levels that control what they can access."
|
|
));
|
|
|
|
children.push(h2("SecStatus \u2014 Account Tier"));
|
|
children.push(para(
|
|
"This is the overall account classification. It determines which main menu commands are available " +
|
|
"and gates access to the sysop menu."
|
|
));
|
|
children.push(spacer());
|
|
children.push(makeTable(
|
|
["Range", "Label", "Description"],
|
|
[
|
|
["0", "Guest", "Unauthenticated browser; limited to public commands"],
|
|
["1", "New", "Registered but awaiting sysop validation"],
|
|
["2\u201399", "Valid", "Normal validated user"],
|
|
["100\u2013149", "BoardOp", "Can moderate message boards"],
|
|
["150\u2013254", "LibOp", "Can manage file libraries"],
|
|
["255", "Sysop", "Full administrative access"],
|
|
],
|
|
[1600, 1400, 6360],
|
|
));
|
|
|
|
children.push(h2("SecBoard, SecLibrary, SecBulletin"));
|
|
children.push(para(
|
|
"These are numeric levels (typically 0\u201310) checked against the Low/High access range on each board, " +
|
|
"library, or bulletin. For example, a board with ReadLow=2 and ReadHigh=10 is visible to any user " +
|
|
"whose SecBoard is between 2 and 10 inclusive."
|
|
));
|
|
children.push(para(
|
|
"This lets you create tiered content: public boards at level 0, member boards at level 2, staff " +
|
|
"boards at level 5, and so on. The same pattern applies to libraries (with separate ranges for " +
|
|
"upload and download access) and bulletins."
|
|
));
|
|
children.push(para(
|
|
"The sysop can edit all four security levels per user from the sysop menu."
|
|
));
|
|
|
|
// ==================== 7. SCREEN FILES ====================
|
|
children.push(h1("Screen Files"));
|
|
children.push(para(
|
|
"Screen files are ANSI art text files displayed at various points during a session. They live in " +
|
|
"the directory specified by system.screens (default: ./screens/). Replace them with your own " +
|
|
"ANSI art to customize the look and feel of your BBS."
|
|
));
|
|
children.push(spacer());
|
|
children.push(makeTable(
|
|
["File", "When Displayed"],
|
|
[
|
|
["welcome.ans", "Before the login prompt (pre-auth banner)"],
|
|
["login.ans", "Alongside the login prompt"],
|
|
["guest.ans", "After guest login"],
|
|
["newuser.ans", "After first-time registration"],
|
|
["logon.ans", "After successful login for returning users"],
|
|
["notime.ans", "When a user has no remaining time (then disconnected)"],
|
|
["mainmenu.ans", "Above the main menu (blank = show generated command list)"],
|
|
["help.ans", "For the [?] help command (blank = generated list)"],
|
|
["join.ans", "Before the account creation prompts"],
|
|
["joined.ans", "After successful account creation"],
|
|
["goodbye.ans", "At logoff"],
|
|
],
|
|
[2400, 6960],
|
|
));
|
|
children.push(spacer());
|
|
children.push(para(
|
|
"Bulletin display files are stored wherever the bulletin\u2019s FilePath points. These are managed " +
|
|
"through the sysop bulletin menu."
|
|
));
|
|
children.push(para(
|
|
"Screen files use standard ANSI escape codes. Classic BBS ANSI editors such as PabloDraw, Moebius, " +
|
|
"and SyncDraw produce compatible output. Keep line widths to 80 columns for the best terminal experience."
|
|
));
|
|
|
|
// ==================== 8. THE SYSOP MENU ====================
|
|
children.push(h1("The Sysop Menu"));
|
|
children.push(para([
|
|
{ text: "Log in as the sysop account (created during " }, code("urit init"),
|
|
{ text: ") and press " }, bold("E"), { text: " at the main menu to enter the sysop management area." },
|
|
]));
|
|
children.push(spacer());
|
|
children.push(makeTable(
|
|
["Key", "Command", "Description"],
|
|
[
|
|
["L", "List all users", "Paginated user listing with status and last login"],
|
|
["N", "New/unvalidated", "Shows only users with SecStatus=1 awaiting validation"],
|
|
["V", "View/edit user", "Full account editor (security, stats, password, active flag)"],
|
|
["B", "Board management", "Create, edit, delete message boards"],
|
|
["U", "Bulletin management", "Create, edit, delete bulletins and their display files"],
|
|
["F", "File library mgmt", "Create, edit, delete file libraries with access ranges"],
|
|
["C", "Call log", "Last 30 connection events with timestamps, IPs, and details"],
|
|
["X", "Force disconnect", "Kick a node by number (sends courtesy message first)"],
|
|
["Q", "Return", "Back to the main menu"],
|
|
],
|
|
[800, 2800, 5760],
|
|
));
|
|
|
|
children.push(h2("Validating New Users"));
|
|
children.push(para(
|
|
"When someone registers, they receive SecStatus=1 (New). They can use the BBS but with limited " +
|
|
"access defined by the new_security configuration. To validate a user, navigate to the sysop menu " +
|
|
"and press V to view/edit their account, then press 2 to validate. This promotes the user to " +
|
|
"the valid_security levels defined in your config. You can also manually adjust their security " +
|
|
"levels with keys F through I."
|
|
));
|
|
|
|
children.push(h2("User Account Editor"));
|
|
children.push(para(
|
|
"From the sysop menu, press V and enter a user ID. The editor displays all account fields and " +
|
|
"accepts single-key commands."
|
|
));
|
|
children.push(spacer());
|
|
children.push(makeTable(
|
|
["Key", "Action"],
|
|
[
|
|
["1", "Save changes"],
|
|
["ESC", "Cancel (discard changes)"],
|
|
["2", "Validate user (set to valid security levels)"],
|
|
["3", "Toggle active/deactivated"],
|
|
["D", "Permanently delete user"],
|
|
["A", "Change username"],
|
|
["B", "Reset password"],
|
|
["C", "Edit comments"],
|
|
["F", "Set SecStatus"],
|
|
["G", "Set SecBoard"],
|
|
["H", "Set SecLibrary"],
|
|
["I", "Set SecBulletin"],
|
|
["J\u2013N", "Edit stats (messages posted, mail sent/received, uploads, downloads)"],
|
|
["Q", "Set time limit"],
|
|
["R", "Set time used"],
|
|
],
|
|
[1200, 8160],
|
|
));
|
|
|
|
children.push(h2("Board Management"));
|
|
children.push(para(
|
|
"The board management submenu (sysop menu \u2192 B) lets you list, create, edit, and delete " +
|
|
"message boards. Each board has a name and four access boundaries: ReadLow, ReadHigh, WriteLow, " +
|
|
"and WriteHigh, all checked against the user\u2019s SecBoard level."
|
|
));
|
|
children.push(para(
|
|
"A board with ReadLow=0 and ReadHigh=255 is visible to everyone. Setting WriteLow=2 means guests " +
|
|
"and new users can read but not post. MaxPosts sets the board\u2019s capacity; when reached, the " +
|
|
"oldest messages are purged as new ones arrive."
|
|
));
|
|
|
|
children.push(h2("Library Management"));
|
|
children.push(para(
|
|
"The library management submenu (sysop menu \u2192 F) manages file libraries. Libraries have " +
|
|
"separate upload and download access ranges, both checked against the user\u2019s SecLibrary level."
|
|
));
|
|
children.push(para([
|
|
{ text: "A library with DownloadLow=0 is publicly browsable via the HTTP server without login. " +
|
|
"Set DownloadLow higher to restrict access to authenticated users. UploadLow and UploadHigh " +
|
|
"control who can upload files. MaxFiles sets the capacity limit. " },
|
|
bold("FilePath"),
|
|
{ text: " is the directory on disk where uploaded files are stored; the server creates it " +
|
|
"automatically on the first upload." },
|
|
]));
|
|
|
|
children.push(h2("Bulletin Management"));
|
|
children.push(para(
|
|
"Bulletins (sysop menu \u2192 U) are ANSI text files displayed to users from the bulletin menu. " +
|
|
"Each bulletin has a name, a file path pointing to the display file on disk, and a " +
|
|
"ReadLow/ReadHigh range checked against SecBulletin."
|
|
));
|
|
|
|
// ==================== 9. HTTP FILE SERVER ====================
|
|
children.push(h1("HTTP File Server"));
|
|
children.push(para(
|
|
"The HTTP server (port 8080 by default) is the modern replacement for ZMODEM and other file " +
|
|
"transfer protocols. It provides library browsing, file downloads, and file uploads through " +
|
|
"a browser interface styled to match the BBS\u2019s terminal aesthetic."
|
|
));
|
|
|
|
children.push(h2("Routes"));
|
|
children.push(makeTable(
|
|
["Path", "Purpose"],
|
|
[
|
|
["/", "Landing page with system name, stats, and telnet address"],
|
|
["/health", "JSON health check for monitoring"],
|
|
["/login", "Browser-based login (BBS username/password)"],
|
|
["/logout", "Clear session"],
|
|
["/libraries", "List of accessible file libraries"],
|
|
["/libraries/{id}", "File listing for a specific library"],
|
|
["/libraries/{id}/files/{n}", "File download"],
|
|
["/libraries/{id}/upload", "File upload form"],
|
|
],
|
|
[3800, 5560],
|
|
));
|
|
|
|
children.push(h2("Authentication"));
|
|
children.push(para("The HTTP server supports two authentication methods."));
|
|
children.push(para([
|
|
bold("Browser login"),
|
|
{ text: " \u2014 Visit /login and enter your BBS username and password. This creates a cookie-based " +
|
|
"session lasting 24 hours. The same bcrypt password check as the telnet side is used." },
|
|
]));
|
|
children.push(para([
|
|
bold("Telnet tokens"),
|
|
{ text: " \u2014 From the BBS main menu, press D to generate a one-time URL. Open it in your browser " +
|
|
"to get authenticated with your BBS security levels. Tokens expire after one hour. This is " +
|
|
"convenient for telnet users who want to download files without re-entering their password." },
|
|
]));
|
|
children.push(para(
|
|
"Anonymous visitors see only public libraries (DownloadLow=0). Authenticated users see all " +
|
|
"libraries matching their SecLibrary level."
|
|
));
|
|
|
|
children.push(h2("File Uploads"));
|
|
children.push(para(
|
|
"Authenticated users can upload to libraries where their SecLibrary falls within the library\u2019s " +
|
|
"UploadLow\u2013UploadHigh range. The upload form is accessible from the file listing page. " +
|
|
"Files are limited to 50 MB. Duplicate filenames within the same library are rejected. The " +
|
|
"library\u2019s directory on disk is created automatically if it does not exist."
|
|
));
|
|
|
|
// ==================== 10. INTER-NODE CHAT ====================
|
|
children.push(h1("Inter-Node Chat"));
|
|
children.push(para([
|
|
{ text: "URIT supports real-time communication between connected users. Press " },
|
|
bold("C"),
|
|
{ text: " at the main menu to enter the chat system. Three modes are available." },
|
|
]));
|
|
children.push(para([
|
|
bold("Page"),
|
|
{ text: " \u2014 Send a one-line message to another node. The message appears on their terminal " +
|
|
"immediately, regardless of what they are doing. No chat mode is required on either end." },
|
|
]));
|
|
children.push(para([
|
|
bold("Chat"),
|
|
{ text: " \u2014 Enter real-time line-by-line messaging with another node. When you start a chat, " +
|
|
"the target user receives a notification. If they also press C and select your node, the " +
|
|
"channels link and messages flow bidirectionally. Type /quit to exit. If either user " +
|
|
"disconnects, the partner is notified." },
|
|
]));
|
|
children.push(para([
|
|
bold("Broadcast"),
|
|
{ text: " (sysop only) \u2014 Send a message to every connected node at once. Useful for " +
|
|
"maintenance announcements or server shutdown warnings." },
|
|
]));
|
|
|
|
// ==================== 11. MAIN MENU COMMANDS ====================
|
|
children.push(h1("Main Menu Commands"));
|
|
children.push(para(
|
|
"The following commands are available from the main menu. Guest users see only commands marked " +
|
|
"as guest-accessible. Registered users see all non-sysop commands."
|
|
));
|
|
children.push(spacer());
|
|
children.push(makeTable(
|
|
["Key", "Name", "Description"],
|
|
[
|
|
["I", "Info", "System information"],
|
|
["S", "Stats", "System statistics (calls, messages, time)"],
|
|
["T", "Time", "Session time remaining"],
|
|
["W", "Who", "Who\u2019s online (node list)"],
|
|
["A", "Account", "View your account details"],
|
|
["P", "Mail", "Private mail system"],
|
|
["M", "Messages", "Message boards"],
|
|
["B", "Bulletins", "Bulletin listings"],
|
|
["L", "Library", "File library browser (telnet side)"],
|
|
["D", "Download", "Generate a web download token URL"],
|
|
["C", "Chat", "Page/chat with other users"],
|
|
["U", "Users", "User listings"],
|
|
["F", "Feedback", "Send a note to the sysop (delivered as mail)"],
|
|
["J", "Join", "Create a permanent account (guest only)"],
|
|
["?", "Help", "Command list"],
|
|
["G", "Goodbye", "Log off"],
|
|
["E", "Sysop", "Sysop management menu (sysop only)"],
|
|
],
|
|
[800, 1600, 6960],
|
|
));
|
|
|
|
// ==================== 12. STATISTICS AND CALL LOG ====================
|
|
children.push(h1("Statistics and Call Log"));
|
|
children.push(para("URIT tracks system activity in two ways."));
|
|
children.push(para([
|
|
bold("Stats counters"),
|
|
{ text: " are atomic key-value counters stored in the database. The system tracks: total_calls, " +
|
|
"guest_calls, new_calls, valid_calls, new_accounts, messages_posted, mail_sent, total_time_secs, " +
|
|
"files_downloaded, and files_uploaded. Users can view a summary from the main menu with S." },
|
|
]));
|
|
children.push(para([
|
|
bold("The call log"),
|
|
{ text: " records timestamped events (login, logoff, kicked) with the user name, node number, " +
|
|
"remote IP, and detail text. The sysop call log (sysop menu \u2192 C) shows the last 30 events " +
|
|
"with full detail. The user-facing stats screen shows the last 10 login/logoff events without " +
|
|
"IP addresses." },
|
|
]));
|
|
|
|
// ==================== 13. SESSION LIFECYCLE ====================
|
|
children.push(h1("Session Lifecycle"));
|
|
children.push(para(
|
|
"Understanding the session flow helps when troubleshooting or customizing screens. The following " +
|
|
"is the sequence from connection to logoff."
|
|
));
|
|
children.push(spacer());
|
|
children.push(makeTable(
|
|
["Step", "Action", "Screen File"],
|
|
[
|
|
["1", "Telnet connection; node allocated", "welcome.ans"],
|
|
["2", "Login prompt; username/password checked (3 attempts, then guest)", "login.ans"],
|
|
["3a", "New user: registration flow", "newuser.ans"],
|
|
["3b", "Guest login", "guest.ans"],
|
|
["3c", "Returning user login", "logon.ans"],
|
|
["4", "Time check: if >12 hrs since last login, reset; otherwise carry over", "(none)"],
|
|
["4x", "No time remaining", "notime.ans"],
|
|
["5", "Last caller display, unread mail check, validation notice", "(none)"],
|
|
["6", "Main menu loop", "mainmenu.ans"],
|
|
["7", "Logoff; time/stats saved; node freed", "goodbye.ans"],
|
|
],
|
|
[900, 5660, 2800],
|
|
));
|
|
children.push(spacer());
|
|
children.push(para(
|
|
"The logoff handler runs in a deferred function, so stats and time are saved even if the " +
|
|
"connection drops unexpectedly."
|
|
));
|
|
|
|
// ==================== 14. DATABASE ====================
|
|
children.push(h1("Database"));
|
|
children.push(para(
|
|
"All persistent state lives in a single SQLite database (default: ./data/urit.db). The schema " +
|
|
"is created automatically on first run."
|
|
));
|
|
children.push(spacer());
|
|
children.push(makeTable(
|
|
["Table", "Contents"],
|
|
[
|
|
["users", "User accounts (name, password hash, security levels, stats)"],
|
|
["boards", "Message boards (name, topic, access ranges)"],
|
|
["messages", "Board messages (author, subject, body, timestamps)"],
|
|
["mail", "Private mail between users"],
|
|
["libraries", "File libraries (name, path, access ranges, capacity)"],
|
|
["library_files", "File metadata (name, size, uploader, download count)"],
|
|
["bulletins", "Bulletin definitions (name, display file path)"],
|
|
["call_log", "Timestamped connection events"],
|
|
["stats", "Key-value counters"],
|
|
["web_sessions", "HTTP authentication sessions"],
|
|
],
|
|
[2200, 7160],
|
|
));
|
|
children.push(spacer());
|
|
children.push(para([
|
|
{ text: "The database can be backed up by copying the file while the server is stopped. For a " +
|
|
"running system, use " },
|
|
code('sqlite3 urit.db ".backup backup.db"'),
|
|
{ text: " which is safe for concurrent reads." },
|
|
]));
|
|
|
|
// ==================== 15. DEPLOYMENT TIPS ====================
|
|
children.push(h1("Deployment Tips"));
|
|
|
|
children.push(h2("Reverse Proxy"));
|
|
children.push(para(
|
|
"Put nginx or Caddy in front of the HTTP server for TLS. The telnet server does not support TLS " +
|
|
"natively; consider a stunnel or haproxy wrapper if you need encrypted telnet connections."
|
|
));
|
|
|
|
children.push(h2("Systemd"));
|
|
children.push(para(
|
|
"Create a service unit that runs the binary with your config file. Set Restart=on-failure for " +
|
|
"automatic recovery. Use a dedicated system user for isolation. A minimal unit file:"
|
|
));
|
|
children.push(...codeBlock([
|
|
"[Unit]",
|
|
"Description=URIT BBS",
|
|
"After=network.target",
|
|
"",
|
|
"[Service]",
|
|
"Type=simple",
|
|
"User=urit",
|
|
"WorkingDirectory=/opt/urit",
|
|
"ExecStart=/opt/urit/urit -config /opt/urit/config.toml",
|
|
"Restart=on-failure",
|
|
"",
|
|
"[Install]",
|
|
"WantedBy=multi-user.target",
|
|
]));
|
|
|
|
children.push(h2("Backups"));
|
|
children.push(para(
|
|
"Back up two things: the SQLite database and the screens directory. File libraries are stored on " +
|
|
"disk at the paths configured per library, so include those directories as well. " +
|
|
"The config.toml should be kept in version control or backed up separately."
|
|
));
|
|
|
|
children.push(h2("Monitoring"));
|
|
children.push(para([
|
|
{ text: "The " }, code("/health"),
|
|
{ text: " endpoint returns JSON with the system name and current timestamp. " +
|
|
"Point your monitoring tool at it for basic availability checks." },
|
|
]));
|
|
|
|
// ==================== 16. APPENDIX: CONFIG DEFAULTS ====================
|
|
children.push(h1("Appendix: Default Configuration"));
|
|
children.push(para("For reference, here is the complete default config.toml produced by initialization:"));
|
|
children.push(...codeBlock([
|
|
'[system]',
|
|
'name = "URIT BBS"',
|
|
'sysop = "Sysop"',
|
|
'location = "./data/"',
|
|
'screens = "./screens/"',
|
|
'',
|
|
'[telnet]',
|
|
'enabled = true',
|
|
'address = ":2323"',
|
|
'',
|
|
'[http]',
|
|
'enabled = true',
|
|
'address = ":8080"',
|
|
'',
|
|
'[storage]',
|
|
'driver = "sqlite"',
|
|
'sqlite_path = "./data/urit.db"',
|
|
'',
|
|
'[users]',
|
|
'max_accounts = 500',
|
|
'guest_time_limit = 1800',
|
|
'new_time_limit = 3600',
|
|
'valid_time_limit = 7200',
|
|
'',
|
|
'[users.guest_security]',
|
|
'status = 0',
|
|
'board = 0',
|
|
'library = 0',
|
|
'bulletin = 0',
|
|
'',
|
|
'[users.new_security]',
|
|
'status = 1',
|
|
'board = 1',
|
|
'library = 1',
|
|
'bulletin = 1',
|
|
'',
|
|
'[users.valid_security]',
|
|
'status = 2',
|
|
'board = 2',
|
|
'library = 2',
|
|
'bulletin = 2',
|
|
'',
|
|
'[logging]',
|
|
'level = "info"',
|
|
'file = ""',
|
|
]));
|
|
|
|
// ==================== BUILD DOCUMENT ====================
|
|
const doc = new Document({
|
|
styles: {
|
|
default: {
|
|
document: {
|
|
run: { font: "Arial", size: 22, color: CLR.black },
|
|
},
|
|
},
|
|
paragraphStyles: [
|
|
{
|
|
id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true,
|
|
run: { size: 36, bold: true, font: "Arial", color: CLR.title },
|
|
paragraph: {
|
|
spacing: { before: 360, after: 200 },
|
|
outlineLevel: 0,
|
|
border: { bottom: { style: BorderStyle.SINGLE, size: 4, color: CLR.accent, space: 4 } },
|
|
},
|
|
},
|
|
{
|
|
id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true,
|
|
run: { size: 28, bold: true, font: "Arial", color: CLR.heading },
|
|
paragraph: { spacing: { before: 240, after: 160 }, outlineLevel: 1 },
|
|
},
|
|
{
|
|
id: "Heading3", name: "Heading 3", basedOn: "Normal", next: "Normal", quickFormat: true,
|
|
run: { size: 24, bold: true, font: "Arial", color: CLR.heading },
|
|
paragraph: { spacing: { before: 200, after: 120 }, outlineLevel: 2 },
|
|
},
|
|
],
|
|
},
|
|
numbering: {
|
|
config: [
|
|
{
|
|
reference: "bullets",
|
|
levels: [
|
|
{
|
|
level: 0, format: LevelFormat.BULLET, text: "\u2022",
|
|
alignment: AlignmentType.LEFT,
|
|
style: { paragraph: { indent: { left: 720, hanging: 360 } } },
|
|
},
|
|
{
|
|
level: 1, format: LevelFormat.BULLET, text: "\u2013",
|
|
alignment: AlignmentType.LEFT,
|
|
style: { paragraph: { indent: { left: 1440, hanging: 360 } } },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
sections: [
|
|
{
|
|
properties: {
|
|
page: {
|
|
size: { width: 12240, height: 15840 },
|
|
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 },
|
|
},
|
|
},
|
|
headers: {
|
|
default: new Header({
|
|
children: [
|
|
new Paragraph({
|
|
alignment: AlignmentType.RIGHT,
|
|
border: { bottom: { style: BorderStyle.SINGLE, size: 1, color: CLR.border, space: 4 } },
|
|
children: [
|
|
new TextRun({ text: "URIT BBS Sysop Guide", font: "Arial", size: 18, color: CLR.dimText }),
|
|
],
|
|
}),
|
|
],
|
|
}),
|
|
},
|
|
footers: {
|
|
default: new Footer({
|
|
children: [
|
|
new Paragraph({
|
|
alignment: AlignmentType.CENTER,
|
|
border: { top: { style: BorderStyle.SINGLE, size: 1, color: CLR.border, space: 4 } },
|
|
children: [
|
|
new TextRun({ text: "Page ", font: "Arial", size: 18, color: CLR.dimText }),
|
|
new TextRun({ children: [PageNumber.CURRENT], font: "Arial", size: 18, color: CLR.dimText }),
|
|
],
|
|
}),
|
|
],
|
|
}),
|
|
},
|
|
children,
|
|
},
|
|
],
|
|
});
|
|
|
|
Packer.toBuffer(doc).then(buffer => {
|
|
fs.writeFileSync("/mnt/user-data/outputs/URIT-BBS-Sysop-Guide.docx", buffer);
|
|
console.log("Done: URIT-BBS-Sysop-Guide.docx");
|
|
});
|