EN /HU | Login

TTG Blog BBS

The Teletype Games blog as a BBS: the latest posts and the categories, a post read a screenful at a time. The editors log in on the same line and write, edit and delete posts.

Live at teletypegames.org:2328, and in the browser on bbsweb, and through TTG Gate BBS

Overview

A Ruby app on the rubbs gem, like the Teletype BBS, Alexandria BBS and Feedia BBS. It has two faces on one port, and both are line by line, without windows:

  • Modern — ANSI colours, up to 80 columns and the caller's window height (SyncTERM, NetRunner, PuTTY, bbsweb).
  • Commodore 64 — PETSCII on a 40×25 screen, for a C64 terminal program (CCGMS, StrikeTerm) behind a WiFi modem.

The menus, the lists, the fields and the full-screen editor are the same on both. A call starts with rubbs' BBS::Terminal question: RETURN takes the guess, 1 picks the modern face, 2 the C64.

Reading

Key Action
L Latest posts, newest first
C Categories with their post counts, then that category's posts
G Go to a post by its number (the web shows it beside the post)
E Editor login
Q Log off

Lists are numbered: the number and RETURN opens a post, RETURN alone turns the page, Q or ← goes back. A post shows its title, category, date and author, the date of its last change, the summary, then the body (Markdown), paged a screenful at a time.

On the C64 every screen fills the whole 40×25: the menus and lists are cursor menus (CRSR moves the bar, RETURN takes it, the keys work too) with a reversed footer bar that tells the screen's keys. The front page lists as many of the newest posts as fit. A post keeps its footer on every page: RETURN, + or CRSR down turns the page, - or CRSR up goes back one, Q or ← leaves; for an editor E edit, P publish and D delete stand on the row over the footer, the questions come in the footer over the post, and after an edit or a publish the post is shown again.

Writing

After logging in, the menu has U unpublished posts, W write a post, P change your password and O log out, and every post has E edit, P publish or unpublish and D delete under it.

A new post is unpublished: only the editors see it (marked so in the lists), the readers and the web do not. Saving a new post keeps it unpublished; P under the post publishes it. The first publishing stamps published_at and rings Discord; taking a post back and publishing it again does not ring it twice. A new post asks for the title (up to 80 characters), the category, a one-line summary (up to 280) and the body, then shows the draft: T title, C category, S summary, B body, V view, W save, Q quit.

Drafts and autosave

What an editor writes is kept as a draft until it is saved, so a dropped line loses nothing: after every field, every 15 seconds in the editor while the text changes (TTG_BLOG_BBS_AUTOSAVE_SECONDS), and when the caller hangs up. The draft is made at the first change, so opening a post and leaving makes none. Nobody but the composer sees a draft: not the lists, the counts, G or the web.

  • W saves: an edit is written into its post and the draft goes; a new post's draft becomes the post itself, unpublished.
  • Q after a change asks: K keeps the draft for later, D discards it, RETURN (on a C64 back) goes back to the draft.
  • E on a post with drafts, and W while there are drafts of new posts, start from a list: the post as it is now (or a new post), then the drafts with their time and editor, the last first. D discards one (on a modern terminal D and then its number). Every editor sees every draft; the menu shows the drafts of new posts beside W.
  • When another editor saved the post after your edit began, W asks before it overwrites their save.

The name, the passwords and the title are typed in a field of rubbs (BBS::Field), the summary and the body in its full-screen editor (BBS::Editor). Both start from the current text, so a title or a post is edited, not typed again: the cursor keys move anywhere, typing inserts at the cursor, DEL deletes before it, and the editor wraps the lines as they are typed. A field ends with RETURN; RUN/STOP or ← (Esc) leaves it.

Key in the editor Action
cursor keys Move; up and down keep the column
HOME / END Start / end of the screen row
PgUp / PgDn A screenful up / down (modern terminal)
RUN/STOP or ← (Esc), then S Save
RUN/STOP or ← (Esc), then Q Quit, keep the text as it was
F1 (Ctrl-S) Save at once (the UltimateTerm of the C64 Ultimate keeps F1 for its help)

The summary is one paragraph: RETURN saves it. In the body RETURN starts a new line and an empty line a new paragraph (Markdown).

On the web

The posts are the blog of teletypegames.org/blog, in both languages of the site. Every post shows its number, the terminal it was typed on (ANSI or C64), and a C64 screen view (?view=c64): the lines a Commodore 64 caller sees, in the C64 palette. A card beside it dials the board in the browser (bbsweb, modern or as a C64), with the telnet command and G plus the post's number. The RSS of the site (/api/v1/rss/blog) carries the posts too.

The board serves them read-only over HTTP, in the same process as the telnet line (Web::Api, port 8080). The container joins the interstack network, and the API of the site reads it there as http://ttg-blog-bbs:8080 (TTG_BLOG_BBS_URL); the port is never published.

Request Answer
GET /posts the published posts newest first, without their bodies
GET /posts/SLUG one post: Markdown, HTML (kramdown, GFM) and the C64 screen
GET /categories the categories with their post counts
GET /health {"ok":true}

A post gets its slug when it is saved: its date and its title in plain ASCII (2026-10-28-hello-world). It stays when the title changes, so links keep working. With TTG_BLOG_BBS_DISCORD_WEBHOOK in the stack's .env, a new post goes to Discord too, with its link.

Categories and editors

The categories come from config/categories.yml (id, name, about): News, Devlog, Releases, Retro, Events. A change is a commit to that file; a push to master deploys it. A post names its category by id.

Editors and posts are in one SQLite file, data/ttgblog.sqlite3 in the stack (users, posts with slug, face, status (draft, unpublished, published), published_at, created_at, updated_at, and for a draft original_id and base_updated_at). Passwords are PBKDF2-SHA256 digests; three wrong logins end the tries of a call. The editors are managed on the server:

BASH
docker exec -it ttg-blog-bbs bin/user add NAME       # asks the password twice
docker exec -it ttg-blog-bbs bin/user passwd NAME
docker exec -it ttg-blog-bbs bin/user remove NAME    # their posts stay, without an author
docker exec ttg-blog-bbs bin/user list

Under the hood

A push to master deploys it from Woodpecker (RuboCop, compose check, ci-deploy ttg-blog-bbs); the container is ttg-blog-bbs, in /srv/stacks/ttg-blog-bbs, data/ stays between deploys. The entrypoint updates rubbs on start.