Skip to content

Image boards

The HTTP API's server also serves image boards, at /chan/, in the manner of kusaba x and of 4chan that it followed (and Futaba before both): boards by directory (/chan/b/, /chan/g/), threads that start with an image, anonymous replies, bumping, sage and tripcodes. Nothing is needed but the bbs binary: thumbnails are made in Go, and the pages load nothing from elsewhere.

Unlike the web forum, the image boards are a world of their own: posters need no account, and their posts are not messages in the BBS's areas. The BBS's accounts are for staff: sysops are admins of every board, and each board can name its own mods.

bbs chan board add b Random "Anything goes." -nsfw
bbs chan board add g Technology "Computers, terminals and BBSes"
bbs chan board set g mods alice,bob
bbs chan board list
[http]
address = :8443
;chan = yes

[chan]
;dir = chan              ; images and thumbnails, a directory a board
;max_file = 4M           ; the largest image (a board can say otherwise)
;thread_wait = 60        ; seconds between an address's threads
;reply_wait = 15         ; and between its posts
;max_comment = 2000      ; characters
;strip_metadata = yes    ; take EXIF (where a phone took it) out of JPEGs
;style = yotsuba-b       ; the style everyone gets until they choose
;captcha = yes           ; those not signed in answer a captcha to post
;captcha_length = 6      ; its letters, 4 to 10
;captcha_level = normal  ; easy, normal or hard to read
;captcha_lifetime = 600  ; seconds one lasts
;options = yes           ; the post form's Options line (sage, nonoko)
;geoip = /var/lib/bbs/GeoLite2-Country.mmdb   ; for the boards with flags ([general]'s by default)
;geoip_credit = IP geolocation by DB-IP       ; its licence's credit, at each page's foot
;geoip_credit_url = https://db-ip.com
;ffmpeg = ffmpeg         ; videos' thumbnails, a frame (else a card saying what it is)
;pdftoppm = pdftoppm     ; PDFs' thumbnails, the first page
;max_video = 120         ; seconds a video may run (0: any)
;ruffle = /usr/share/ruffle   ; Ruffle's self-hosted files: Flash played in the page
;archive_days = 3        ; days a pruned thread stays in its board's archive (0: always)
Setting Default What it sets
chan (in [http]) yes the image boards at /chan/
dir chan where images and thumbnails are kept, and the boards' secret (dir/secret, made the first time: keep it, or secure tripcodes change)
max_file 4M the largest image; bbs chan board set DIR max_file 8M gives a board its own
thread_wait, reply_wait 60, 15 flood control: staff are not held to it
max_comment 2000 characters in a comment (and 100 lines)
strip_metadata yes JPEGs' EXIF, XMP and comments taken out before they are kept
captcha yes those not signed in answer a captcha to post (below); a board may say otherwise
captcha_length 6 the captcha's letters, 4 to 10
captcha_level normal how hard it is to read: easy (letters barely turned, a line through them), normal, hard (turned further, waved, three lines, more specks)
captcha_lifetime 600 seconds a captcha lasts (60 to a day)
options yes the post form's Options line (sage, nonoko); without it posts always bump. A board may say otherwise
style by the board Yotsuba B on a worksafe board, Yotsuba on the rest, as 4chan does
ffmpeg, pdftoppm none the programs that make videos' and PDFs' thumbnails (by name on the PATH, or a path); without them, a card saying what the file is (below)
max_video 120 seconds a video may run (0: any length)
archive_days 3 days a pruned thread stays in its board's archive (below); 0 keeps them always
ruffle none the directory of Ruffle's self-hosted package (ruffle.js and its .wasm): SWFs and FLVs played in the page (below); without it they are only downloaded

On the terminals

The image boards are on the text BBS and on 3270s too: B on the main menu (or a sysop's menu item boards). The boards, a board's threads by bump order (replies, images, subject), and a thread read a post at a time, as messages: who posted (tripcode, capcode), the thread, and under each post the posts that quote it. The posters are the BBS's callers, posting as anyone on the web does: a name (Name#password for a tripcode), sage, a subject or none, staff as ## Admin or ## Mod; the same bans, flood control (by the caller's address, or by their account where there is none: a modem's), limits and pruning.

A post's picture is shown above it, as well as the terminal can:

Terminal How
kitty, Ghostty kitty's graphics protocol (by what they say they are)
sixel: xterm (-ti vt340), foot, mlterm, WezTerm, Windows Terminal, iTerm2, SyncTERM, the web terminal sixel
iTerm2 before sixel, mintty iTerm2's inline images
any other UTF-8 terminal with colour coloured half blocks (▀, 256 colours): rough, but the picture
the rest, and 3270s what it is, in words: [cat.png, 640x480, 52 KB]

The thumbnail goes above the post (a spoiler's says it is one); V shows the image itself, as large as the screen allows. A caller turns pictures off (or on) on their profile, M, and it stays so. A thread is started with its image sent by the terminal's transfer protocol (ZMODEM, YMODEM, XMODEM, Kermit), where it has one; terminals without (a 3270, the web terminal) reply, or start threads on text_only boards. Staff's tools (deleting, bans, reports) are the web's.

Posting

A board's index has the post form at its top: name, options, subject, comment, an image, and a password to delete the post by (filled in for each browser, which keeps it in a cookie). A thread starts with an image (unless the board is text_only); replies need a comment, an image, or both.

Typed Result
Name#password a tripcode: Name !WokonZwxw2. The same as 2channel's and 4chan's: #tea is !WokonZwxw2 here too
Name##password a secure tripcode, !!..., made with this site's secret: it cannot be found by trying passwords elsewhere
sage in Options the reply does not bump its thread
nonoko in Options back to the board after posting, not the thread
>text greentext
>>123 a link to post 123, (OP) if it starts its thread, struck through if it is gone
>>>/g/123, >>>/g/ a post on another board, or the board
[spoiler]...[/spoiler] hidden until hovered
[code]...[/code] set as code
a link made a link

Spoiler? shows the image's thumbnail as Spoiler Image until it is opened. Names cannot be used on a forced_anon board.

Images are JPEG, PNG, GIF or WebP, told by their bytes, not their names; up to 10000 pixels a side. Thumbnails are JPEGs, 250 pixels for a thread's first image and 125 for a reply's. The same image twice on a board is refused, with a link to where it is (by its MD5, as 4chan does). JPEGs lose their EXIF before they are kept, so a phone's photo does not say where it was taken.

Captchas and members

Posters who are not signed in type the letters of a captcha under the form: drawn here, crooked over noise (no outside service sees the posters), good for one post and ten minutes; New one gives another. Its answer travels sealed in the form (AES-GCM, with the boards' secret), so nothing is kept of it but that it has been used. captcha = no in [chan] turns it off, captcha_length, captcha_level and captcha_lifetime make it longer or shorter, harder or easier; a board's own captcha (yes, no, default) says otherwise than [chan] for it.

The BBS's users sign in at /chan/login (Sign in at the top of each page) and post without one. Their posts are marked as a member's: a ✓ beside the name, "member": 1 in the JSON, whatever name they gave. They may also tick Sign it as NAME: the post then carries their account's name (underlined, a blue ✓, "account" in the JSON), which no one else can post under, as anyone can type a name. On a forced_anon board posts are marked, never signed. On the terminals every poster is signed in: no captcha, [member] by the name, and Sign it as NAME? before the post.

Poster IDs and flags

A board with ids yes gives each poster an ID in each thread, as 4chan's do: eight characters from a keyed hash of their address, the board and the thread, so a poster has the same ID all through a thread and another in the next, and nothing of their address can be had from it. Each ID has a colour of its own; hovering it says how many posts in the thread are by it, and clicking it marks them all. On the terminals it is by the name: Anonymous (ID: aQvqbFdJ).

A board with flags yes shows the flag of each poster's country by their name (an emoji, its name on hovering; [NZ] on the terminals), from a country database the sysop has, [chan] geoip (by default [general] geoip, the one the rest of the BBS uses):

  • a MaxMind DB file (.mmdb): MaxMind's GeoLite2 or GeoIP2 Country or City, DB-IP's IP to Country Lite, IPinfo's and others in its format;
  • or a CSV of ranges, plain or gzipped: DB-IP's (start,end,CC) or IP2Location LITE DB1's ("from","to","CC","Country", by numbers).

The BBS reads them itself (nothing is fetched); replace the file and it is read again within a minute. DB-IP's free one, for instance, is a monthly download (https://download.db-ip.com/free/dbip-country-lite-YYYY-MM.mmdb.gz, gzipped as it comes is fine); it was checked against MaxMind's own reader, 200,000 addresses alike. Its licence (CC BY 4.0), as GeoLite2's, asks for a credit: geoip_credit and geoip_credit_url put it at the foot of each page. Posts from no address (a modem's caller) or one the database does not know have no flag. Behind a reverse proxy name it in trusted_proxies, or every poster is the proxy and has its ID and its flag. Both are kept with each post when it is made: turning them off leaves those already made as they were.

Threads

A reply bumps its thread to the top of the board until the thread has bump_limit replies (300); a thread takes image_limit images (150). A board has pages pages of per_page threads (10 of 10); when a new thread pushes the last one past them, it is pruned: archived, or, on a board with archive no, deleted with its images. Stickies stay at the top and are never pruned; closed threads take no replies (but staff's).

The archive

As 4chan's, a pruned thread goes to its board's archive (/chan/b/archive, [Archive] beside [Catalog]): off the board's pages and catalog, but whole, its files kept, and read-only. No one replies to it (staff neither: it is a record), and its posts are not deleted by their passwords (staff still delete them). Its page says This thread is archived in place of the reply form, and its first post wears an Archived badge. The archive lists the threads, the last archived first: number, subject and the start of the comment, replies, when; archive.json is 4chan's, the numbers, oldest first, and an archived thread's JSON has "archived": 1 and "archived_on".

After [chan] archive_days (3) a thread is gone from the archive too, with its files (looked to when a thread is made and when the archive or the thread is looked at); archive_days = 0 keeps them always. bbs chan board set DIR archive no prunes a board's threads as before, deleted at once. On the terminals the board's thread list has Archive (A): the archived threads, to read.

The index shows each thread's first post and its last five replies (one under a sticky), with how many were left out; a comment longer than 15 lines is cut there. A thread's page has all of it, and under each post the posts that quote it. The catalog shows every thread at a glance: its image, its replies and images, the start of its first post; by bump order or by when it was made.

With JavaScript (the pages work without it): clicking a post's number quotes it in the reply form, clicking a thumbnail opens the image in place, and hovering a >>123 shows that post.

Deleting and reporting

Tick posts, give the password they were posted with (the browser's own is filled in), and Delete; File Only deletes their images and leaves the posts. The ▸ beside a post reports it to staff.

Staff

Sysops and a board's mods sign in at /chan/login with their BBS account and get Manage at the top of each page, an [M] beside each post, and the choice of posting as ## Admin or ## Mod.

  • Reports (/chan/manage): what has been reported on their boards, how often, and why.
  • A post ([M]): its address and that address's other posts; delete the post (a thread's first: the thread) or its file; make a thread sticky or closed; dismiss its reports; delete every post of the address on the board; and ban.
  • Bans: by address or by range (the /24 of IPv4, the /48 of IPv6), on the board or (admins) on every board, for some days or for ever, optionally saying (USER WAS BANNED FOR THIS POST) under the post. The banned see why, until when, and what they posted, when they next post. /chan/manage/bans lists them and lifts them.

From the command line:

bbs chan board add DIR TITLE [DESCRIPTION] [-nsfw] [-forced-anon] [-text-only] [-locked] [-login] [-oekaki]   (-login: only users post)
bbs chan board set DIR KEY VALUE
bbs chan board list
bbs chan board del DIR            ; its posts and images too
bbs chan ban add ADDRESS DAYS [BOARD|-] [REASON...]
bbs chan ban list
bbs chan ban del ID
Board setting Default What it sets
title, description as the board's banner shows them
pages, per_page 10, 10 threads kept: pages of threads
bump_limit, image_limit 300, 150 a thread's
max_file [chan]'s the largest image
default_name Anonymous a post with no name
nsfw no not worksafe: Yotsuba, and left off the front page's images
forced_anon no no names or tripcodes
text_only no threads need no image
oekaki no posters can draw their pictures in the page (below)
ids no each poster has an ID in a thread (below)
flags no posts show their country's flag (with [chan] geoip)
files none files taken besides images: webm, mp4, pdf, swf, flv, as many as wanted (webm,pdf); none for images only (below)
sound no videos and Flash may have sound
archive yes pruned threads are archived, read-only, for archive_days (below); no deletes them at once
locked no only staff post
login no post: only the BBS's users post (signed in at /chan/login); all: only they see the board at all (its pages, images and JSON; others are sent to sign in, and the front page marks it (members))
captcha default yes or no for this board, else [chan]'s
options default yes or no: this board's post form has the Options line (sage, nonoko), else as [chan] says
mods users who moderate it, by name
sort 0 where it comes in the lists of boards

Videos and PDFs

A board can take videos and PDFs besides images, as 4chan's take WebM everywhere and PDF on /po/:

bbs chan board set wsg files webm,mp4
bbs chan board set wsg sound yes
bbs chan board set po files pdf

What a file is is found from its bytes, never its name, and each is read before it is kept:

Format Accepted Refused
WebM VP8, VP9 or AV1 video, Vorbis or Opus sound a Matroska file that is not WebM (browsers do not play it), other codecs, other tracks, two videos in one
MP4 H.264, AV1 or VP9 video, AAC or Opus sound HEVC and other codecs browsers do not all play; one still being written (no movie header)
PDF a whole one (%PDF- to %%EOF) one cut short

A video is at most 4096 pixels a side and max_video seconds long (one that does not say its length is refused while there is a limit), and without sound unless the board's sound says yes. Its post says its size and length (320x240, 0:05); a PDF's its pages where it can count them (PDF, 2 pages). In the page a video's thumbnail plays it in place, with [Close] to stop; a PDF's opens it in a tab of its own, in the browser's viewer (it is served as itself, without the boards' Content Security Policy, which a PDF viewer will not run under; nothing sniffs it as anything else).

Thumbnails: with [chan] ffmpeg a video's is its first frame, and with pdftoppm (poppler-utils) a PDF's is its first page; ffmpeg is told the container (-f matroska, -f mov) and reads only the file. Either one failing on a file refuses it (a browser would not show it either). Without them, the thumbnail is a card saying what it is (WEBM 0:05, PDF 2 pages). On the terminals a video or PDF shows as its thumbnail; the file itself is for the browser.

Flash

A board can take Flash too, as 4chan's /f/ did, played by Ruffle, the Flash Player emulator, in the page:

make ruffle                      # Ruffle's self-hosted package, into ruffle/ (or unpack a release's)
bbs chan board set f files swf,flv
bbs chan board set f sound yes
[chan]
ruffle = /opt/bbs/ruffle         ; the directory with ruffle.js (some 30 MB, not in the BBS)
Format Read for Notes
SWF (FWS, CWS, ZWS) its stage's size, frames and rate, sounds compressed ones are walked as they unpack, nothing kept (a small file that unpacks to much is refused at no cost); an LZMA one (ZWS) Ruffle unpacks, so it is taken as 550x400 with sound
FLV its onMetaData (size, length), its tags (sound, the video's codec: Sorenson H.263, Screen, VP6, VP6 alpha, H.264) without metadata its length is its last tag's time, its size Flash's 320x240

A movie or FLV with sound needs the board's sound; an FLV is held to max_video as videos are. The thumbnail of an FLV is its first frame with [chan] ffmpeg; a movie's is a card (FLASH 550x400): nothing here draws Flash but the browser.

Clicking the thumbnail plays it in place, with [Close]. The player is a frame of /chan/flash, sandboxed (allow-scripts alone, by its Content Security Policy as by the frame): it has no origin of the site's, so no cookies, and nothing of the page. The page fetches the file (with its own cookies, so a board for members only works) and hands its bytes to the frame, which hands them to Ruffle with the network, scripting and links off (allowNetworking: "none", allowScriptAccess: false, openUrlMode: "deny"). Ruffle does not play an FLV by itself, as Flash Player did not: the BBS makes a small movie of its own (/chan/flvplayer.swf?w=&h=, a NetStream played into a Video of the FLV's size) that plays it from a blob: of the frame's. Ruffle's files are served at /chan/ruffle/ to anyone (the frame has no origin to send), only those of the package (.js, .wasm, .map, no directories). Ruffle decodes H.264 with the browser's own decoder (WebCodecs), which Chrome, Edge and Safari have; Sorenson, VP6 and Screen video it decodes itself.

Oekaki

An oekaki board (お絵かき, doodling: bbs chan board set oe oekaki yes) has a drawing board in its post form, as Futaba's boards had their Java painters. Choose the canvas's size (50 to 1200 pixels a side) and Open the drawing board:

  • Pen, Eraser and Fill, a colour (or one from the palette), a brush size;
  • Undo and Redo (Ctrl+Z, Ctrl+Y), Clear;
  • Use this drawing, or just post: the drawing goes as the post's image, a PNG.

The PNG carries how it was drawn: every stroke, with its timing, in a chunk of its own (okST) that other programs pass over, so the picture looks the same anywhere. Under a drawing, Replay draws it again stroke by stroke, at 1× to 32× (pauses cut short), ending as the picture is; the post says how long the drawing took. On an oekaki board Draw over starts a reply from the drawing's strokes, to add to it: the new drawing's replay has both. Since the strokes are in the file, a drawing saved and posted again (on any board) still replays; programs that rewrite PNGs may drop the chunk.

The drawing board and the replays need JavaScript (the boards' only pages that do); without it, an oekaki board is an image board.

What the server takes is checked: a replay must be of this picture's size, its strokes on the canvas, at most a million numbers and 32 MB unpacked, and well formed. One that is not is taken out of the PNG, which is kept as a picture. The format, for programs that want to make or read it, is zlib-compressed JSON:

{"v": 1, "w": 300, "h": 200, "bg": "#ffffff",
 "s": [{"t": "p", "c": "#000000", "z": 4, "p": [0, 80, 180, 33, 80, 150]},
       {"t": "f", "c": "#22b14c", "z": 1, "p": [900, 150, 150]}]}

A stroke is the pen (p), the eraser (e: the pen in the background's colour) or a fill (f, one point); z is its size, and p its points as dt, x, y, dt the milliseconds since the point before (for a stroke's first point, since the stroke before ended). The drawing's time is the sum of them.

Styles

Callers choose one at the top of each page (kept in a cookie):

Style What it looks like
yotsuba 4chan's, on boards not worksafe: cream and maroon
yotsuba-b 4chan's worksafe blue
futaba Futaba's, as kusaba had it
burichan kusaba's blue
photon kusaba's grey and orange
tomorrow 4chan's dark style

JSON

The boards can be read as JSON, in the shapes of 4chan's read-only API (github.com/4chan/4chan-API), so that its readers read these boards too:

File What it holds
/chan/boards.json the boards and how they are run
/chan/b/threads.json each page's threads, when each changed, their replies
/chan/b/catalog.json each page's threads, with their last replies
/chan/b/1.json a page of the index
/chan/b/thread/123.json a thread

Posts have 4chan's id, country and country_name where the board has them, and boards.json its user_ids and country_flags. Beyond 4chan's fields, boards.json says "oekaki": 1 of an oekaki board, and a drawing that replays has "replay": 1 and "draw_ms". A board that takes videos or PDFs has "file_types": ["webm", "pdf"], and with videos 4chan's webm_audio and max_webm_duration; is_archived is 1 of a board with an archive; a video's post has "duration_ms", a PDF's "page_count", beside 4chan's ext, w, h.

A post's file is at /chan/b/src/{tim}{ext} and its thumbnail at /chan/b/thumb/{tim}s.jpg (where 4chan's are at i.4cdn.org/b/). kusaba's addresses go where they should: /chan/b/res/123.html to the thread, /chan/b/1.html to the index.

Behind a reverse proxy, name it in [http]'s trusted_proxies: otherwise every poster has the proxy's address, and flood control and bans reach everyone. See HTTP API: behind a proxy.

See also: Web forum, HTTP API