From f9f4d9e1dedc215bee5c19c9e8bf19cae611ee13 Mon Sep 17 00:00:00 2001 From: lew Date: Thu, 9 Apr 2026 20:28:31 +0100 Subject: [PATCH] docs: readme --- .env.example | 42 ++--- README.md | 339 +++++++++++++++++++++++++++++++++++++++++ module.nix | 11 ++ templates/default.html | 2 +- 4 files changed, 372 insertions(+), 22 deletions(-) create mode 100644 README.md diff --git a/.env.example b/.env.example index e70a646..451521b 100644 --- a/.env.example +++ b/.env.example @@ -1,11 +1,11 @@ # Port to listen on (binds to 127.0.0.1). -BOOK_PORT=8123 +# BOOK_PORT=8123 # Directory for guestbook entry files. -BOOK_DATA_DIR=./data +# BOOK_DATA_DIR=./data # Site title shown in nav and page title. -BOOK_SITE_TITLE=guestbook +# BOOK_SITE_TITLE=guestbook # Telegram bot token. Optional โ€” if unset, telegram moderation is disabled. # BOOK_TELEGRAM_BOT_TOKEN=your-bot-token-here @@ -14,21 +14,21 @@ BOOK_SITE_TITLE=guestbook # BOOK_TELEGRAM_CHAT_ID=0 # Enable honeypot field for spam prevention. -BOOK_ENABLE_HONEYPOT=true +# BOOK_ENABLE_HONEYPOT=true # Allow new guestbook submissions. When false, the form is hidden and submissions are rejected. -BOOK_ENABLE_SUBMISSIONS=true +# BOOK_ENABLE_SUBMISSIONS=true # Show website field in form and render website links in entries. # When false, the input is hidden, submitted values are ignored, and existing links are not displayed. -BOOK_ENABLE_WEBSITE_LINKS=true +# BOOK_ENABLE_WEBSITE_LINKS=true # Allow raw HTML/JS in entry names and message bodies. When false, HTML is escaped. # Website URLs are always escaped regardless of this setting. -BOOK_ENABLE_HTML_INJECTION=false +# BOOK_ENABLE_HTML_INJECTION=false # Enable captcha on submission form. -BOOK_ENABLE_CAPTCHA=false +# BOOK_ENABLE_CAPTCHA=false # Captcha question displayed as a label. # BOOK_CAPTCHA_QUESTION=What is my name? @@ -37,22 +37,22 @@ BOOK_ENABLE_CAPTCHA=false # BOOK_CAPTCHA_ANSWER=lew # Require exact match (true) or just "contains" (false). -BOOK_CAPTCHA_EXACT=false +# BOOK_CAPTCHA_EXACT=false # Require case-sensitive match. -BOOK_CAPTCHA_CASESENSITIVE=false +# BOOK_CAPTCHA_CASESENSITIVE=false # Maximum length for names. 0 for unlimited. -BOOK_MAX_NAME_LENGTH=0 +# BOOK_MAX_NAME_LENGTH=0 # Maximum length for messages. 0 for unlimited. -BOOK_MAX_MESSAGE_LENGTH=0 +# BOOK_MAX_MESSAGE_LENGTH=0 # Maximum length for website URLs. 0 for unlimited. -BOOK_MAX_WEBSITE_LENGTH=0 +# BOOK_MAX_WEBSITE_LENGTH=0 # Separator between guestbook entries. -BOOK_SEPARATOR=------------------------------------------------------------ +# BOOK_SEPARATOR=------------------------------------------------------------ # Path to a CSS file. Takes precedence over BOOK_STYLE. # BOOK_STYLE_FILE=./templates/default.css @@ -64,25 +64,25 @@ BOOK_SEPARATOR=------------------------------------------------------------ # BOOK_STYLE= # Text shown above the form. -BOOK_FORM_PROMPT=Thanks for visiting. Sign the guestbook! +# BOOK_FORM_PROMPT=Thanks for visiting. Sign the guestbook! # Submit button text. -BOOK_BUTTON_TEXT=sign +# BOOK_BUTTON_TEXT=sign # Label for the name field. -BOOK_LABEL_NAME=Your name: +# BOOK_LABEL_NAME=Your name: # Label for the website field. -BOOK_LABEL_WEBSITE=Your website (optional): +# BOOK_LABEL_WEBSITE=Your website (optional): # Label for the message field. -BOOK_LABEL_MESSAGE=Your message: +# BOOK_LABEL_MESSAGE=Your message: # Number of rows for the message textarea. -BOOK_TEXTAREA_ROWS=8 +# BOOK_TEXTAREA_ROWS=8 # Number of columns for the message textarea. -BOOK_TEXTAREA_COLS=60 +# BOOK_TEXTAREA_COLS=60 # Custom HTML template file with {{title}}, {{form}}, {{entries}}, and {{style}} placeholders. # Uses built-in default if unset. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4d9af81 --- /dev/null +++ b/README.md @@ -0,0 +1,339 @@ +`guestbook` is a self-hosted guestbook web service with: +- entries stored in plaintext, +- notifications and moderation via [Telegram](#telegram), +- spam prevention via honeypot and/or [captcha](#captcha), +- completely customisable [styling](#customisation), + +and more, written in Rust, and inspired by [t0.vc/g](https://t0.vc/g). + +`guestbook` is a single binary that serves a single-page guestbook aimed at personal sites. There's a form for visitors to submit a name, message, and optionally a link to their own site. Entries are written to plain text files with TOML frontmatter, and are initially marked as pending. The frontmatter can be manually edited to mark entries as approved or denied, or a Telegram bot can be hooked up for notifications and moderation. Running the Telegram bot just requires handing over a bot token, and it'll run off the same binary. + +Everything is configured through environment variables (see [`.env.example`](#default-config) for the defaults). If you're hosting with Nix, there's a flake that can set up the `guestbook` service end-to-end, running on a systemd service with a Caddy reverse proxy. Optionally, just ignore the flake and set up all the extra stuff yourself. + +Aesthetically, essentially all of the HTML and CSS can be configured. There's a default template included for both, but you can take them and change both to your liking. Just point the template and/or style variables at your replacements. + +--- + +### Installation + +

+ + Build ยท + NixOS + +

+ +#### Build + +`guestbook` is written in [Rust](https://www.rust-lang.org). The easiest way to install it is via `cargo`. + +```bash +cargo install guestbook +``` + +#### NixOS + +[NixOS](https://nixos.org) users can use the included flake, which builds the binary via [crane](https://github.com/ipetkov/crane) and exports a module that sets up the systemd service, user, and optionally a [Caddy](https://caddyserver.com) reverse proxy. + +```nix +# flake.nix +{ + inputs.guestbook.url = "github:llywelwyn/guestbook"; + + outputs = { self, nixpkgs, guestbook, ... }: { + nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { + modules = [ + guestbook.nixosModules.default + { + services.guestbook = { + enable = true; + package = guestbook.packages.x86_64-linux.default; + siteTitle = "my guestbook"; + telegram = { + enable = true; + botTokenFile = "/run/secrets/guestbook-bot-token"; + chatId = 12345; + }; + caddy = { + enable = true; + domain = "guestbook.example.com"; + }; + }; + } + ]; + }; + }; +} +``` + +--- + +### Configuration + +`guestbook` is configured entirely through environment variables. For local development, copy `.env.example` to `.env`. For NixOS, the [module](#nixos-module) maps all options to environment variables for you. + +Running `guestbook` with no env vars will give you a working guestbook on `localhost:8123` with the default config below. Notably, no Telegram moderation. That requires a bot token, and is probably the most important thing to set up. + +#### Default Config + +```bash +# Port to listen on (binds to 127.0.0.1). +# BOOK_PORT=8123 + +# Directory for guestbook entry files. +# BOOK_DATA_DIR=./data + +# Site title shown in nav and page title. +# BOOK_SITE_TITLE=guestbook + +# Telegram bot token. Optional โ€” if unset, telegram moderation is disabled. +# BOOK_TELEGRAM_BOT_TOKEN=your-bot-token-here + +# Telegram chat ID for moderation messages. Required if bot token is set. +# BOOK_TELEGRAM_CHAT_ID=0 + +# Enable honeypot field for spam prevention. +# BOOK_ENABLE_HONEYPOT=true + +# Allow new guestbook submissions. When false, the form is hidden and submissions are rejected. +# BOOK_ENABLE_SUBMISSIONS=true + +# Show website field in form and render website links in entries. +# When false, the input is hidden, submitted values are ignored, and existing links are not displayed. +# BOOK_ENABLE_WEBSITE_LINKS=true + +# Allow raw HTML/JS in entry names and message bodies. When false, HTML is escaped. +# Website URLs are always escaped regardless of this setting. +# BOOK_ENABLE_HTML_INJECTION=false + +# Enable captcha on submission form. +# BOOK_ENABLE_CAPTCHA=false + +# Captcha question displayed as a label. +# BOOK_CAPTCHA_QUESTION=What is my name? + +# Captcha answer to validate against. +# BOOK_CAPTCHA_ANSWER=lew + +# Require exact match (true) or just "contains" (false). +# BOOK_CAPTCHA_EXACT=false + +# Require case-sensitive match. +# BOOK_CAPTCHA_CASESENSITIVE=false + +# Maximum length for names. 0 for unlimited. +# BOOK_MAX_NAME_LENGTH=0 + +# Maximum length for messages. 0 for unlimited. +# BOOK_MAX_MESSAGE_LENGTH=0 + +# Maximum length for website URLs. 0 for unlimited. +# BOOK_MAX_WEBSITE_LENGTH=0 + +# Separator between guestbook entries. +# BOOK_SEPARATOR=------------------------------------------------------------ + +# Path to a CSS file. Takes precedence over BOOK_STYLE. Uses built-in default if unset. +# BOOK_STYLE_FILE=./templates/default.css + +# Custom CSS injected into a style tag. +# Classes: .guestbook-form, .guestbook-prompt, .guestbook-label, .guestbook-input, +# .guestbook-textarea, .guestbook-button, .entry-header, .entry-name, +# .entry-website, .entry-body, .entry-separator +# BOOK_STYLE= + +# Text shown above the form. +# BOOK_FORM_PROMPT=Thanks for visiting. Sign the guestbook! + +# Submit button text. +# BOOK_BUTTON_TEXT=sign + +# Label for the name field. +# BOOK_LABEL_NAME=Your name: + +# Label for the website field. +# BOOK_LABEL_WEBSITE=Your website (optional): + +# Label for the message field. +# BOOK_LABEL_MESSAGE=Your message: + +# Number of rows for the message textarea. +# BOOK_TEXTAREA_ROWS=8 + +# Number of columns for the message textarea. +# BOOK_TEXTAREA_COLS=60 + +# Custom HTML template file with {{title}}, {{form}}, {{entries}}, and {{style}} placeholders. +# Uses built-in default if unset. +# BOOK_TEMPLATE=./templates/default.html +``` + +#### NixOS Module + +```nix +services.guestbook = { + enable = false; + # package = ; -- required when enabled + port = 8123; + dataDir = "/srv/guestbook/data"; + siteTitle = "guestbook"; + user = "guestbook"; + group = "guestbook"; + + caddy = { + enable = false; + # domain = ; -- required when enabled + forwardAuth = null; # e.g. "localhost:9090" + }; + + security = { + enableSubmissions = true; + enableHtmlInjection = false; + enableWebsiteLinks = true; + enableHoneypot = true; + captcha = { + enable = false; + question = ""; + answer = ""; + exact = false; + caseSensitive = false; + }; + }; + + telegram = { + enable = false; + # botTokenFile = ; -- required when enabled + # chatId = ; -- required when enabled + }; + + limits = { + name = 0; + message = 0; + website = 0; + }; + + styles = { + css = ""; + cssFile = null; + templateFile = null; + separator = "------------------------------------------------------------"; + greeting = "Thanks for visiting. Sign the guestbook!"; + labels = { + submit = "sign"; + name = "Your name:"; + website = "Your website (optional):"; + message = "Your message:"; + }; + message = { + rows = 8; + cols = 60; + }; + }; +}; +``` + +--- + +### Telegram + +To enable Telegram moderation, create a bot via [@BotFather](https://t.me/BotFather) and set `BOOK_TELEGRAM_BOT_TOKEN` to the token it gives you. Set `BOOK_TELEGRAM_CHAT_ID` to the chat ID where you want notifications sent โ€” the easiest way to find this is to message the bot and check the [getUpdates](https://api.telegram.org/bot/getUpdates) endpoint. + +When a visitor submits an entry, the bot sends a message with the entry details and `/allow_` and `/deny_` commands. Tap either to approve or deny. + +--- + +### Entry Format + +Each entry is a plain text file in `{data_dir}/entries/`. The filename is `{date}-{short_id}.txt`. + +``` ++++ +name = "someone" +date = "2026-04-09" +website = "https://example.com" +status = "pending" ++++ +Message body here. +``` + +The `status` field can be `pending`, `approved`, or `denied`. Only approved entries are displayed. To moderate without Telegram, just edit the file and change `status` to `approved` or `denied`. + +--- + +### Customisation + +#### Default Template + +```html + + + + + + + {{title}} + {{style}} + + +
+{{title}} + +guestbook +========= + +{{form}} + +entries +======= +{{entries}} +
+ + +``` + +#### Default CSS + +```css +/* Page container */ +.page-container { + max-width: 70ch; + margin: 0 auto; + padding: 1rem; + white-space: pre-wrap; + word-wrap: break-word; +} + +/* Form */ +.guestbook-prompt {} +.guestbook-form {} +.guestbook-label {} +.guestbook-input {} +.guestbook-textarea {} +.guestbook-button {} + +/* Entries */ +.entry-header {} +.entry-name {} +.entry-website {} +.entry-body {} +.entry-separator {} +``` diff --git a/module.nix b/module.nix index bd23747..09647c3 100644 --- a/module.nix +++ b/module.nix @@ -49,6 +49,12 @@ in type = types.str; description = "Domain for the Caddy virtual host."; }; + + forwardAuth = mkOption { + type = types.nullOr types.str; + default = null; + description = "URL for forward_auth (e.g. localhost:9090). When set, all requests are authenticated via forward_auth before proxying."; + }; }; security = { @@ -282,6 +288,11 @@ in (mkIf cfg.caddy.enable { services.caddy.virtualHosts.${cfg.caddy.domain}.extraConfig = '' + ${lib.optionalString (cfg.caddy.forwardAuth != null) '' + forward_auth ${cfg.caddy.forwardAuth} { + uri /api/auth + } + ''} reverse_proxy localhost:${toString cfg.port} encode zstd gzip ''; diff --git a/templates/default.html b/templates/default.html index e1f6074..e07d457 100644 --- a/templates/default.html +++ b/templates/default.html @@ -10,7 +10,7 @@ form - The submission form (labels, inputs, button). Controlled by BOOK_FORM_PROMPT, BOOK_LABEL_NAME, BOOK_LABEL_WEBSITE, BOOK_LABEL_MESSAGE, BOOK_BUTTON_TEXT, BOOK_TEXTAREA_ROWS, - BOOK_TEXTAREA_COLS. Empty when BOOK_OPEN_REGISTRATION=false. + BOOK_TEXTAREA_COLS. Empty when BOOK_ENABLE_SUBMISSIONS=false. entries - Approved guestbook entries, newest first. Entry separator controlled by BOOK_SEPARATOR. style - Custom CSS from BOOK_STYLE or BOOK_STYLE_FILE, wrapped in