A rubbs legalja: hogyan lesz egy kapcsolatból
BBS::Session, mit csinál a telnet-egyeztetés, az ablakméret (NAWS), areadkeyösszes lehetséges billentyűje, areadline, az írás, az egérjelentés, az idle-korlátok és a bontás (lib/bbs/server.rb,session.rb,telnet.rb); valamint aBBS::Store, a tábla, amelyben a board a hívói adatait tartja, a 0.33.0 óta SQLite-fájlban (lib/bbs/store.rb). A többi építőelem, ami egy tartalmas boardnak kell (SQLite, HTTP, cache, rate limit, naplózás), az Adat és hálózat oldalon van. A legtöbb board ezekkel csak egy Flow-n vagy egy Applicationön keresztül találkozik; közvetlenül egy Flowcalllépésében, egy doorban vagy egy C64-es arcban van rájuk szükség.
BBS.start és BBS::ServerBBS.start # port from BBS.config.port (ENV['BBS_PORT'] or 2323)
BBS.start(port: 2400)
A BBS.start(port: config.port) a BBS::Server.new(port:).run-t futtatja, ami blokkol:
0.0.0.0:PORT-on figyel (kiírja: BBS listening on port …);max_sessions hívó van bent, kiírja, hogy Too many callers right now, try again later., naplózza: BBS busy: …, és bont; ha arról a címről már max_sessions_per_ip hívás fut, azt írja ki, hogy Too many calls from your address, try again later., és BBS crowded: …-ot naplóz;idle_seconds-ra állítja (nil esetén 900 s; ehhez a Ruby 3.2 IO#timeout=-ja kell), és egy új BBS::Session-t futtat;Session#run: negotiate, enable_mouse, ha a config.mouse és a config.mouse_on_connect is be van kapcsolva, majd FlowRunner.new(session, session_id, config.flow).run; utána mindig kikapcsolja az egeret és lezárja a socketet;config.on_session_end-et.Minden sor a BBS.log-on át megy, és a config.log_tag-gel kezdődik (lásd Adat és hálózat). A bontás vagy az elnémult vonal (IOError, EPIPE, ECONNRESET, ETIMEDOUT, IO::TimeoutError) feedia-bbs: session: dropped by the client (…) formában kerül a naplóba, nem hibaként. Minden más kivételt a session címke alatt naplóz, az osztályával, az üzenetével és három sor backtrace-szel. Magának az accept-nek a hibáinál (ECONNABORTED, EMFILE, ENFILE) a ciklus naplóz, és fél másodpercet vár ahelyett, hogy pörögne. Ctrl-C-re és SIGTERM-re (docker stop) kiírja: Server stopped..
| Mező | Alapértelmezés | Jelentés |
|---|---|---|
port |
ENV['BBS_PORT'] vagy 2323 |
A BBS.start TCP-portja. |
idle_seconds |
nil |
A socket olvasási/írási időkorlátja (nil esetén 900 s) és az Application idle-bontása. Lásd Idle-korlátok. |
max_sessions |
ENV['BBS_MAX_SESSIONS'] vagy 100 |
Egyszerre kiszolgált hívók. |
max_sessions_per_ip |
ENV['BBS_MAX_SESSIONS_PER_IP'] vagy 6 |
Egy címről egyszerre kiszolgált hívások. A privát, loopback és link-local címekre (a bbs-web a Docker-hálózatból sok hívót proxyz) nem vonatkozik. A 0.33.0 óta. |
log_tag |
ENV['BBS_LOG_TAG'] vagy bbs |
Amivel minden naplósor kezdődik: a board technikai neve, pl. feedia-bbs. Az 1.0.0 óta. |
mouse |
false |
Használ-e a board egyáltalán egeret. |
mouse_on_connect |
true |
A mouse mellett: kapcsolódáskor bekapcsolja-e a jelentést. A kétarcú board false-ra állítja (lásd Commodore 64). |
on_session_end |
nil |
->(session) { … }, a szerver szálából hívva minden session után, hiba után is. |
require 'bbs'
BBS.configure do |c|
c.port = 2400
c.idle_seconds = 600
c.max_sessions = 20
c.on_session_end = ->(session) { puts "#{session.session_id} left (#{session.term_cols}x#{session.term_rows})" }
c.flow = BBS::Flow.define do
say 'Hello!'
end
end
BBS.start
Közvetlenül a kapcsolódás után a session négy telnet-parancsot küld, és amíg mást nem hall, 80×24-es ablakot feltételez:
| Bájtok | Parancs | Miért |
|---|---|---|
ff fb 03 |
IAC WILL SGA |
Go-ahead elnyomása: karakterenként, nem soronként. |
ff fb 01 |
IAC WILL ECHO |
A szerver visszhangoz, így a readline dönti el, mi jelenik meg (pl. egy maszk). |
ff fd 03 |
IAC DO SGA |
Arra kéri a klienst, hogy ő is nyomja el a go-ahead-et. |
ff fd 1f |
IAC DO NAWS |
Arra kéri a klienst, hogy jelentse az ablakméretét. |
A kliens válaszait (WILL/WONT/DO/DONT + opció) ott olvassa be és dobja el, ahol épp érkeznek: a readkey-ben, a readline-ban, a settle-ben. Más opciókezelés nincs (se BINARY, se TTYPE). Mindezt egyetlen IAC-értelmező végzi, a BBS::Telnet::Parser (1.0.0 óta), ugyanaz, amelyet egy gateway mindkét irányban használ. Egy telnetet egyáltalán nem beszélő kliens (WiFi-modem mögötti C64, nc) egyszerűen sosem válaszol.
Amikor a kliens jelenti a méretét (IAC SB NAWS w1 w2 h1 h2 IAC SE), a session frissíti a term_cols-t és a term_rows-t:
IAC IAC) 255-ként olvasódik;petscii! után) a méret 40×25 marad, de a naws_reported? ettől még igaz lesz.| Metódus | Visszaad | Jelentés |
|---|---|---|
term_cols |
Integer | Oszlopok: 80, amíg nem jön jelentés, utána a korlátok közé szorított érték; PETSCII-vonalon 40. |
term_rows |
Integer | Sorok: 24, amíg nem jön jelentés; PETSCII-vonalon 25. |
naws_reported? |
Boolean | Igaz, ha a kliens már jelentette az ablakméretét. Az igazi terminálok jelentik; a portszkennerek és a jelszópróbálgató botok általában nem. |
settle(timeout: 1.5) |
a session vagy nil |
Legfeljebb timeout másodpercig vár az ablakméretre, és minden mást, ami közben jön, beolvas és eldob (a bot prompt előtt begépelt root-ja nem jut sehová). Ha megjött a méret, a sessiont adja vissza, ha nem, nil-t (az 1.0 előtt true-t vagy false-t). Ha a méret már ismert, azonnal visszatér. |
resize!(cols, rows) |
— | Úgy állítja be a méretet, mint egy NAWS-jelentés: ugyanúgy korlátok közé szorítja, a nullát figyelmen kívül hagyja, PETSCII-vonalon nem változtat semmit, és a naws_reported? igaz lesz. A telnetes NAWS-kezelés ezt hívja, és a BBS::Gateway is, ha a hívó átkelés közben méretez át (0.17.1 óta). |
A settle-t használja a BBS::Terminal.guess is, hogy egy modern terminált megkülönböztessen egy C64-től. Olcsó botszűrő is lesz belőle:
BOT_FILTER = BBS::Flow.define do
call do |_ctx, runner|
session = runner.session
next nil if session.settle(timeout: 2)
session.write "This board needs a telnet client that reports its window size.\r\n"
:halt
end
say 'Welcome, human.'
end
A :halt-ot visszaadó call lépés befejezi a flow-t; a next egy értékkel lép ki a blokkból.
readkeykey = session.readkey # blocks until a key (or the socket timeout)
key = session.readkey(timeout: 1.0) # :idle when nothing came in 1 s
Ezek egyikét adja vissza:
"a", " ", "é", "ő");BBS::Telnet::MouseEvent — egérjelentés (csak bekapcsolt jelentésnél);:idle — letelt a timeout, és nem jött billentyű;nil — a hívó bontott.A közben érkező telnet-parancsokat és NAWS-jelentéseket feldolgozza, sosem adja vissza. timeout nélkül az olvasás addig vár, amíg a socket saját időkorlátja (idle_seconds, alapból 900 s) IO::TimeoutError-t nem dob, ami egy IOError.
| Billentyű | Symbol | Amilyen bájtokból jön |
|---|---|---|
| Enter / Return | :enter |
CR, LF, CR LF, CR NUL (mindegyik egy Enter) |
| Tab | :tab |
09 |
| Shift-Tab | :shift_tab |
ESC [ Z |
| Backspace | :backspace |
08, 7f |
| Esc | :escape |
magányos ESC (0,15 s-on belül nem jön semmi), ESC ESC, vagy bármilyen escape-szekvencia, amit a rubbs nem ismer |
| Ctrl-C | :interrupt |
03 |
| Ctrl-A … Ctrl-Z | :ctrl_a … :ctrl_z |
01–1a, kivéve a Ctrl-C-t, Ctrl-H-t, Ctrl-I-t, Ctrl-J-t és Ctrl-M-et, amelyek a fenti billentyűk |
| Nyilak | :up, :down, :right, :left |
ESC [ A…D, ESC O A…D |
| Home / End | :home, :end |
ESC [ H / F, ESC O H / F, ESC [ 1 ~ / 7 ~, ESC [ 4 ~ / 8 ~ |
| Insert / Delete | :insert, :delete |
ESC [ 2 ~, ESC [ 3 ~ |
| Page Up / Down | :page_up, :page_down |
ESC [ 5 ~, ESC [ 6 ~ |
| F1 – F4 | :f1 … :f4 |
ESC O P…S, ESC [ 11 ~…14 ~ |
| F5 – F12 | :f5 … :f12 |
ESC [ 15 ~, 17 ~, 18 ~, 19 ~, 20 ~, 21 ~, 23 ~, 24 ~ |
| Alt + Enter | :alt_enter |
ESC CR |
| Alt + nyomtatható | :alt_a, :alt_x, :alt_1, … |
ESC + a karakter, kisbetűsítve (az Alt-Shift-A is :alt_a) |
| Módosított billentyűk | :shift_up, :ctrl_f5, :alt_home, :ctrl_shift_right, … |
az xterm második paramétere, lásd lent |
| Időben nem jött semmi | :idle |
letelt a timeout: |
Módosítók. Az xterm a Shiftet, az Altot és a Ctrl-t egy második CSI-paraméterként jelenti: az ESC [ 1 ; 2 A a Shift-Fel, az ESC [ 15 ; 5 ~ a Ctrl-F5. A rubbs előtagot tesz a billentyű szimbóluma elé:
| Paraméter | Előtag | Példa |
|---|---|---|
| 2 | shift_ |
:shift_up, :shift_f6 |
| 3 | alt_ |
:alt_home, :alt_page_down |
| 5 | ctrl_ |
:ctrl_right, :ctrl_f5 |
| 6 | ctrl_shift_ |
:ctrl_shift_left |
| 4, 7, 8 | nincs | a sima billentyű (az ESC [ 1 ; 4 A :up) |
A CSI-alakokra vonatkozik: nyilak, Home/End, Insert/Delete, Page Up/Down és a ~-os funkcióbillentyűk. Az xterm ESC [ 1 ; 5 P alakú módosított F1–F4-ét nem dekódolja, az :escape-ként olvasódik.
Más bájtok (NUL, 1c–1f, egy törött UTF-8 szekvencia) kimaradnak. PETSCII-vonalon a billentyűk a C64-éi: lásd Commodore 64.
Bekapcsolt egérjelentésnél (enable_mouse) a kattintások, a húzások és a görgő BBS::Telnet::MouseEvent-ként jönnek vissza, ami egy Struct:
| Mező | Értékek |
|---|---|
kind |
:press, :release, :move (mozgás lenyomott gombbal), :wheel |
button |
:left, :middle, :right, :none; :wheel-nél :up vagy :down |
x, y |
1-től számolt oszlop és sor |
mods |
:shift, :meta, :ctrl tömbje |
Mindkét jelentésformátumot olvassa: az SGR-t (ESC [ < b ; x ; y M lenyomás, … m felengedés) és a régi X10-et (ESC [ M b x y, mindegyik plusz 32).
| Metódus | Mit csinál |
|---|---|
enable_mouse |
Kiírja: \e[?1002h\e[?1006h: xterm gombesemény-követés (lenyomás, felengedés, mozgás lenyomott gombbal) SGR-formátumban. Nem csinál semmit, ha már be van kapcsolva, vagy PETSCII-vonalon. |
disable_mouse |
Kiírja: \e[?1006l\e[?1002l\e[?1000l, ha be volt kapcsolva. A Session#run minden session végén meghívja. |
Egy Application a MouseEventekből BBS::Event-eket csinál a widgeteknek; egy Door a programja idejére kikapcsolja a jelentést. Nyers call lépésben úgy kapod meg őket, ahogy jönnek.
Egy call lépés, amíg fut, az övé a vonal. Ez kiír minden billentyűt, amit olvas, Esc-re vagy 30 tétlen másodperc után megáll, és túléli a bontást:
require 'bbs'
KEYS = BBS::Flow.define do
call do |_ctx, runner|
s = runner.session
s.settle # wait for NAWS, or term_cols is still the default 80
s.enable_mouse
s.write "#{s.term_cols}x#{s.term_rows}, NAWS: #{s.naws_reported?}. Press keys, Esc ends.\r\n"
idle = 0
loop do
key = s.readkey(timeout: 1)
break if key.nil? || key == :escape
if key == :idle
idle += 1
break if idle >= 30
next
end
idle = 0
text = key.is_a?(BBS::Telnet::MouseEvent) ? "#{key.kind} #{key.button} at #{key.x},#{key.y}" : key.inspect
s.write "\e[1;33m#{text}\e[0m\r\n"
end
s.disable_mouse
nil
end
say 'Bye.'
end
BBS.configure { |c| c.flow = KEYS }
BBS.start
A hívás első lépése rögtön az egyeztetés elküldése után fut, mielőtt a kliens válaszai megérkeznének: settle nélkül a term_cols és a term_rows még 80×24 lenne, a naws_reported? pedig hamis. A ciklusból break-kel lépj ki, ne return-nel: a blokkon belüli return LocalJumpError-t dob (a FlowRunner hibaként kiírja, és megy tovább).
Amit néhány, tesztszkriptből küldött bemenetre kiírt (100×30-as NAWS-szal):
a "a"
ESC [ A :up
ESC [ 1 ; 2 A :shift_up
ESC [ 15 ; 5 ~ :ctrl_f5
ESC O P :f1
ESC [ Z :shift_tab
ESC x :alt_x
ESC CR :alt_enter
01 :ctrl_a
03 :interrupt
7f :backspace
c3 a1 "á"
ESC [ < 0;10;5 M press left at 10,5
ESC [ < 64;3;4 M wheel up at 3,4
ESC [ < 2;7;8 m release right at 7,8
ESC [ 1 ; 6 C :ctrl_shift_right
ESC [ 24 ~ :f12
require 'bbs'
MENU = BBS::Flow.define do
call do |ctx, runner|
s = runner.session
items = %w[News Files Chat Logoff]
pick = 0
loop do
s.write "\e[2J\e[H"
items.each_with_index do |item, i|
s.write(i == pick ? "\e[7m > #{item} \e[0m\r\n" : " #{item}\r\n")
end
key = s.readkey
case key
when nil then break
when :up then pick = (pick - 1) % items.size
when :down, :tab then pick = (pick + 1) % items.size
when :enter
ctx[:choice] = items[pick]
break
when String
hit = items.index { |i| i.downcase.start_with?(key.downcase) }
pick = hit if hit
end
end
nil
end
say 'Done.'
end
BBS.configure { |c| c.flow = MENU }
BBS.start
readlinename = session.readline # echoes what is typed
password = session.readline(mask: '*') # echoes a * for each character
| Viselkedés | |
|---|---|
| Visszaad | A sort UTF-8 stringként, sorvég nélkül; az érvénytelen bájtok kimaradnak. nil, ha a hívó bont. |
| Vége | CR, LF, CR LF vagy CR NUL (egy sorvég). A végén kiír egy \r\n-t. |
| Visszhang | Minden karakter, ahogy begépelték, vagy karakterenként a mask (egy UTF-8 karakter egy maszkot kap). |
| Backspace | A 08 vagy a 7f egy teljes karaktert töröl, és \b \b-t ír. |
| Korlátok | Legfeljebb 1024 bájt (Telnet::MAX_LINE); a további billentyűk elvesznek. A vezérlőbájtok is. |
| Időkorlát | Nincs sajátja: a socket időkorlátjáig (idle_seconds) vár. |
| PETSCII | C64-vonalon a C64 billentyűit olvassa, és PETSCII-ben visszhangoz. |
A Flow ask lépése ezt használja; egy call lépésben:
LOGIN = BBS::Flow.define do
call do |ctx, runner|
s = runner.session
s.write 'Name: '
ctx[:name] = s.readline or next :halt
s.write 'Password: '
password = s.readline(mask: '*') or next :halt
next :halt unless password == 'secret'
nil
end
say 'Logged in.'
end
Kipróbálva: a Zsé, Backspace, o, Enter "Zso"-t ad vissza (UTF-8); a maszkolt abc visszhangja ***.
| Metódus | Mit csinál |
|---|---|
write(data) |
Stringet ír a hívónak. PETSCII-vonalon előbb az Encoderen megy át. Sosem dob kivételt: hibánál (lezárt socket) nil-t ad, és a bontás a következő olvasásnál derül ki. |
line |
Maga a hívó socketje, egy doornak, amely változatlanul továbbadja a vonalat (BBS::Gateway), vagy olyan bájtokhoz, amelyeknek ki kell kerülniük a PETSCII-enkódert. |
read_byte(timeout = nil) |
Egy nyers bájt Stringként, :timeout vagy nil; telnet-kezelés nélkül. |
read_raw_byte(timeout) |
Egy bájt Integerként, bináris protokollokhoz (ZMODEM): a telnet-parancsok kiszűrve, a duplázott IAC 255-ként jön vissza; bontáskor :timeout vagy nil. |
A send_raw létezik, de privát (ezzel kerüli meg az egyeztetés és a petscii! az Encodert); kívülről a session.line-ra írj.
session.write "\e[1;32mOK\e[0m\r\n"
session.line.write "\x07".b # a bell, past any encoder
| Metódus | Jelentés |
|---|---|
session_id |
16 hexa karakter (SecureRandom.hex(8)), minden kapcsolatnak új. A FlowRunner ctx[:session_id]-ként beteszi a contextbe, és a persist ezt használja store-kulcsnak. |
term_cols, term_rows |
Lásd NAWS. |
idle_seconds, idle_seconds= |
Ennek a hívónak a tétlenségi limitje, kapcsolódáskor a config.idle_seconds-ból másolódik. Az Application-hurok ezt olvassa; az állítása a socket timeoutját is átírja (nil esetén 900 s), így a session.idle_seconds = 3600 egy hívónak (egy sysopnak) egy órát ad. A 0.19.0 óta. |
petscii!, petscii? |
A C64-vonal, lásd Commodore 64. |
Idle. A config.idle_seconds két helyen hat:
| Hol | Mi történik |
|---|---|
| A socket | Olvasási/írási időkorlát (nil esetén 900 s). Az ennyi ideig néma vonal, akár egy promptnál, egy call lépésben vagy egy doorban, IO::TimeoutError-t dob: a session megszakad. |
BBS::Application |
Számolja a billentyűk közti tétlen másodperceket; a korlátnál kiírja: Disconnected after N s idle., és véget ér. |
Max sessions. Egyszerre config.max_sessions (BBS_MAX_SESSIONS, különben 100) hívó, és egy nyilvános címről config.max_sessions_per_ip (BBS_MAX_SESSIONS_PER_IP, különben 6); a következő egy sort kap, a szerver bont, és BBS busy-t vagy BBS crowded-ot naplóz a két korláttal és a címmel (a stderr-re, puffer nélkül, így azonnal megjelenik a docker logs-ban). A max_sessions-t a konténer memóriájához kell méretezni: egy session nagyjából 0,5–1,7 MB a board saját 20–40 MB-ja fölött.
Bontás. Amikor a hívó bont:
readkey és a readline nil-t ad, a write csendben nil-t;IOError-t, EPIPE-ot vagy ECONNRESET-et dobó call lépése csendben leállítja a flow-t; nézd a nil-t és lépj ki break-kel, vagy dobj magad IOError-t (ahogy a C64-es képernyőosztályok teszik);Session#run kikapcsolja az egeret és lezárja a socketet; a szerver session dropped-ot logol, és meghívja az on_session_end-et.BBS::StoreA BBS::Store egy kis tábla, kulcsonként (általában session-azonosítónként) egy sorral: thread-safe, és ellenáll a bájtoknak, amiket egy terminál küldhet. A 0.33.0 óta egy SQLite-fájlban él (egy BBS::SQLite-ban, lásd Adat és hálózat); előtte CSV-fájl volt, és egy régebbi board CSV-jét az első használatkor beimportálja.
SESSIONS = BBS::Store.new(path: 'data/sessions.csv', headers: %w[session_id timestamp player_name code])
BBS::Store.new(path:, headers:)| Argumentum | Jelentés |
|---|---|
path: |
A fájl. A .csv végű útvonal a mellette lévő .sqlite3 fájlt jelenti (data/sessions.csv → data/sessions.sqlite3), így egy régebbi board megtarthatja az útvonalát; bármely más útvonal maga az SQLite-fájl. A mappa első használatkor jön létre. |
headers: |
Az oszlopnevek tömbje (Stringek). A session_id a kulcsoszlop; a timestamp oszlop, ha van, magától kitöltődik. A később felvett fejlécből új oszlop lesz. |
A store.path, a store.database_path és a store.headers visszaadja őket.
| Metódus | Visszaad | Mit csinál |
|---|---|---|
upsert(session_id:, **fields) |
a store-t | Beszúr egy sort a session_id-hoz, vagy frissíti a meglévőt, egy tranzakcióban. |
find(session_id) |
Hash vagy nil |
A sor Hashként, String kulcsokkal, csak a fejléc oszlopaival. |
all |
Hash-ek tömbje | Minden sor, a legrégebbivel kezdve. |
delete(session_id) |
nil |
Törli a sort; ha nincs ilyen, nem történik semmi. |
open! |
a store-t | Azonnal megnyitja a fájlt (és beimportálja a régi CSV-t), hogy a board ezt induláskor tegye meg, ne az első hívója sessionjében. |
Beszúrás (még nincs sor ezzel a kulccsal): a session_id a kulcs, a timestamp a pillanatnyi UTC-idő ISO 8601-ben (2026-10-27T22:15:45Z), a többi oszlop az azonos nevű mező (username: → username), üres (nil), ha nincs megadva.
Frissítés (a sor létezik): csak a megadott mezők változnak. A timestamp a beszúrás idejét őrzi. A nil-re állított mező üres string lesz.
A fejlécben nem szereplő mezőket figyelmen kívül hagyja, beszúrásnál és frissítésnél egyaránt.
require 'bbs'
USERS = BBS::Store.new(path: 'data/users.sqlite3', headers: %w[session_id timestamp username email])
USERS.upsert(session_id: 'a1', username: 'joe', email: 'joe@example.com')
USERS.upsert(session_id: 'b2', username: 'ann')
USERS.upsert(session_id: 'a1', email: 'joe@new.example')
USERS.find('a1')
# => {"session_id"=>"a1", "timestamp"=>"2026-10-27T22:15:45Z", "username"=>"joe", "email"=>"joe@new.example"}
USERS.find('zz') # => nil
USERS.all.size # => 2
USERS.delete('b2')
USERS.all.map { |r| r['username'] } # => ["joe"]
A sorok a data/users.sqlite3 store táblájában vannak; a sqlite3 data/users.sqlite3 'SELECT * FROM store' a board futása közben is megmutatja őket.
Ha a tábla még üres, és létezik az útvonal CSV-fájlja, a Store egy tranzakcióban beimportálja, majd átnevezi users.csv.imported-ra. Az a sor, amely nem érvényes CSV, több mezője van, mint a fejlécnek, vagy nincs session_id-ja, nem állítja meg az importot: a fejléc alá kerül a users.csv.rejected fájlba a sysopnak, és a naplósor megszámolja (store: 412 rows of data/users.csv imported into data/users.sqlite3, 2 set aside in data/users.csv.rejected). Hívd meg induláskor az open!-t, hogy ez még az első hívó előtt lezajoljon.
Ezekbe a táblákba a hívók gépelnek, a terminál pedig bármit küldhet. Minden érték Stringgé alakul, és a BBS::Text.clean tisztítja meg: az érvénytelen UTF-8 bájtok kimaradnak, ahogy a C0 vezérlőkarakterek (a tab és az újsor marad), a DEL, a C1 vezérlők és a kétirányú szöveg felülíró karakterei is. Egy ANSI-szekvenciából csak az ESC bájt tűnik el: az "ann\e[31m" ann[31m-ként tárolódik.
s = BBS::Store.new(path: 'data/log.sqlite3', headers: %w[session_id name note])
s.upsert(session_id: 's1', name: "ann\e[31m\x00 bé", note: "two\nlines")
s.find('s1')
# => {"session_id"=>"s1", "name"=>"ann[31m bé", "note"=>"two\nlines"}
Minden hívás egyetlen, zár mögötti közös kapcsolaton át éri el az SQLite-fájlt, így egy Store objektumot az összes session-szál megoszthat. A fájl WAL módban van: a board egy parancssori eszköze futás közben is olvashatja (és írhatja), a board pedig a következő hívásánál már látja a változást. Memóriabeli cache nincs többé.
persistA Flow persist lépése context-kulcsokat ír egy store-ba, a session-azonosítóval mint kulccsal:
persist :player_name, :code, store: SESSIONS
A megnevezett kulcsok közül azokat veszi, amelyek benne vannak a contextben (a még be nem állítottak kimaradnak), és meghívja a store.upsert(session_id: a context session-azonosítója, **ezek)-et; ha egyik sincs beállítva, nem csinál semmit. Egy későbbi persist ugyanazt a sort frissíti. Lásd Flow.
Az Impostor BBS (bbs/impostor-bbs/lib/impostor.rb) így tart minden kihallgatást egyetlen store-ban:
module Impostor
SESSION_HEADERS = %w[session_id timestamp player_name code feedback email].freeze
def self.sessions_store(path) = BBS::Store.new(path: path, headers: SESSION_HEADERS)
def self.debrief(sessions:, screens_dir:)
BBS::Flow.define(screens_dir: screens_dir) do
ask :player_name, prompt: 'Player Name', transform: :upcase
ask :code, prompt: 'Code'
persist :player_name, :code, store: sessions
# ...
menu '>', loop: true do
option 'Leave feedback' do
ask :feedback, prompt: 'Feedback'
persist :feedback, store: sessions
end
option 'Subscribe to updates' do
ask :email, prompt: 'Email address', validate: :email
persist :email, store: sessions
end
option('Exit') { exit_menu }
end
end
end
end
# bbs.rb: one store, shared by both faces and every session
sessions = Impostor.sessions_store(File.expand_path('data/sessions.csv', __dir__)).open!
A kulcs csak egy string: a hívó nevével kulcsolt store hívásokon át megőrzi az adatot.
PROFILES = BBS::Store.new(path: 'data/profiles.sqlite3', headers: %w[session_id timestamp calls last_seen])
def count_call(name)
calls = PROFILES.find(name)&.fetch('calls').to_i
PROFILES.upsert(session_id: name, calls: calls + 1, last_seen: Time.now.utc.iso8601)
end
Egy Application-handlerben vagy egy call lépésben a session-azonosító a ctx[:session_id] (Applicationön belül a context[:session_id]).
A Store egyetlen, szöveges oszlopokból álló tábla, egyetlen stringgel kulcsolva. Egy board, amelynek posztjai, szálai vagy több táblája van, közvetlenül a BBS::SQLite-ot használja, saját sémával és számozott migrációkkal: lásd Adat és hálózat.
BBS.configure és az első boardask, persist, call