Amit a tartalmas boardjaink korábban mind maguknak írtak meg, az a 0.33.0 és az 1.0.0 óta a rubbs része: egységes naplóformátum, háttérmunka, SQLite-fájl migrációkkal, korlátok közé szorított HTTP, cache, rate limit, ütemezett Wikipédia-kliens, vezérlőkaraktermentes szöveg, és a szabály, hogy hol kezeljük egy hiba következményeit. Mindegyik elem néhány sorral bevethető; a teljes szignatúrák az API-referenciában vannak.
A BBS.configure alábbi mezői tartoznak ide (a többi az Első lépések és a Telnet és a store oldalon van):
| Mező | Alapértelmezés | Jelentés |
|---|---|---|
log_tag |
ENV['BBS_LOG_TAG'] vagy bbs |
Amivel minden naplósor kezdődik: a board technikai neve, pl. feedia-bbs. |
user_agent |
rubbs/VERSION (+https://teletypegames.org) |
A BBS::HTTP kérések User-Agent-je. |
max_sessions_per_ip |
ENV['BBS_MAX_SESSIONS_PER_IP'] vagy 6 |
Egy nyilvános címről egyszerre futó hívások; a következőnek szól, és bont (a naplóban BBS crowded). |
ai_concurrency |
2 |
A folyamatban egyszerre futó BBS::AI::Client kérések; a következő legfeljebb 5 s-ot vár, aztán BBS::AI::Busy-t kap (lásd AI-karakterek). |
BBS.configure do |c|
c.log_tag = 'feedia-bbs'
c.user_agent = 'Feedia BBS (+https://teletypegames.org)'
end
Minden sor, amit a könyvtár vagy a board a naplóba ír, a BBS.log(tag, message)-en át megy a stderr-re, így tizenegy board sorai egy naplóban is egyformán olvashatók. Ha az üzenet kivétel, az osztálya és az üzenete kerül a sorba.
BBS.log('catalog refresh', 'done in 3.2 s') # feedia-bbs: catalog refresh: done in 3.2 s
BBS.log('catalog refresh', error) # feedia-bbs: catalog refresh: SocketError: …
A könyvtár saját sorai is ezt a formát követik: session, door …, gateway …, store, BBS busy, BBS crowded. Egy BBS::DiscordWebhook label:-je is a log tag lett, úgyhogy már nem kell bele a board neve.
Ami a hívók útjából kifér (az induláskor betöltött index, az óránként frissülő katalógus), az egy BBS::Background szálban fut. A hibát naplózza, és retry_after (5) másodperc múlva újrapróbálja, a szünetet max_retry-ig (300) duplázva: egy leállt szolgáltatás így pár naplósorba kerül, nem egy összeomlott boardba vagy egy pörgő ciklusba.
BBS.background('index') { wiki.load_index } # until it succeeds once
BBS.background('catalog refresh', every: 3600) { library.refresh } # every hour
Mindkettő a Thread-et adja vissza; a BBS::Background.new(name, …) { … }.run ugyanezt a hívó szálban futtatja (egy tesztben, egy parancssori eszközben).
BBS::SQLiteEgy SQLite-fájl és az egyetlen kapcsolata, amelyen a hívók szálai egy zár mögött osztoznak: WAL mód (a board egy parancssori eszköze közben is használhatja a fájlt), busy timeout, bekapcsolt idegen kulcsok. A séma migrációk listája, a PRAGMA user_version számozza őket: egy fájl csak azokat futtatja, amelyeken még nem ment át, mindegyiket egy tranzakcióban. A migráció SQL-string vagy lambda; a már lefutottat sose írd át, inkább fűzz hozzá újat.
DB = BBS::SQLite.new('data/board.sqlite3') # creates data/ when needed
DB.migrate(
'CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT NOT NULL)',
'ALTER TABLE posts ADD COLUMN body TEXT',
->(db) { db.insert('INSERT INTO posts (title) VALUES (?)', 'Welcome') }
)
DB.query('SELECT * FROM posts') # => [{ 'id' => 1, 'title' => 'Welcome', 'body' => nil }]
DB.get('SELECT * FROM posts WHERE id = ?', 1) # the first row or nil
DB.insert('INSERT INTO posts (title) VALUES (?)', 'Hi') # => the new rowid
DB.change('DELETE FROM posts WHERE id = ?', 2) # => rows changed
DB.transaction { … } # a nested call joins the outer one
BBS::StoreA Flow persist lépésének egytáblás, kulcs szerint upsertelő store-ja a 0.33.0 óta BBS::SQLite-ra épül. A .csv útvonal a mellette lévő .sqlite3 fájlt jelenti, egy régebbi board CSV-jét pedig az első használatkor beimportálja (a hibás sorokat egy .rejected fájlba teszi félre); a store.open! ezt induláskor elvégzi. Lásd Telnet és a store.
BBS::HTTPKorlátok közé szorított kérések a szolgáltatások felé, amelyeket a board megmutat. Minden hiba BBS::HTTP::Error-t dob, amelynek az üzenete a címet a query string nélkül nevezi meg (így token nem kerül a naplóba); a max_bytes-nál nagyobb törzs az alosztályát, a BBS::HTTP::TooLarge-ot dobja. A nem 2xx státuszokat visszaadja, nem dob rájuk.
response = BBS::HTTP.get('https://example.org/feed.xml', headers: { 'Accept' => 'application/rss+xml' })
response.ok? # => true for 2xx
response.status # => 200
response.body # => binary String
BBS::HTTP.post(url, body: URI.encode_www_form(query: q),
headers: { 'Content-Type' => 'application/x-www-form-urlencoded' })
File.open(path, 'wb') { |file| BBS::HTTP.get(url, to: file, max_bytes: 64 * 1024 * 1024) }
| Opció | Alapértelmezés | |
|---|---|---|
open_timeout / read_timeout |
5 / 15 s | kapcsolatonként / olvasásonként |
deadline |
30 s | az egész kérés, az átirányításokkal együtt |
max_bytes |
4 MiB | a törzs folyamosan jön, és e fölött a kérés elbukik |
redirects |
3 (post: 0) |
csak http/https címre követi |
public_only |
true |
elutasítja a loopback, privát, link-local, CGNAT, ULA és más nem nyilvános címre feloldódó hostokat; az ellenőrzött címre kapcsolódik |
to |
nil |
egy IO, amelybe a 2xx törzs érkezés közben íródik (a body ilyenkor nil) |
BBS::CacheTTL, a legrégebben használt elem kidobása, bájtkorlát, kulcsonként egyetlen számítás (az ugyanazt a kulcsot egyszerre hiányoló hívók megvárják, és osztoznak az eredményén), és ha a számítás hibát dob, a lejárt értéket adja.
ARTICLES = BBS::Cache.new(ttl: 600, max_entries: 500, max_bytes: 8 * 1024 * 1024)
ARTICLES.fetch([lang, title]) { repository.article(lang, title) }
BBS.background('catalog', every: 3600) { CATALOG.refresh(:all) { repository.load } }
A refresh(key) { … } csak akkor cseréli le a tárolt értéket, ha a blokk sikerrel fut, így egy háttérfrissítés sosem üríti ki a cache-t. Továbbá: delete(key), clear, size.
BBS::RateLimitKulcsonkénti csúszó ablak, thread-safe; a csendes kulcsokat elfelejti. A caller_key(session) interneten át a hívó IP-címe, egy helyi proxy (bbs-web) mögött pedig a vonal session-azonosítója, mert ott mindenki ugyanarról a címről jön.
TIPS = BBS::RateLimit.new(limit: 3, per: 3600)
key = BBS::RateLimit.caller_key(session)
TIPS.allow?(key) # => true, and counted; false when over the limit
TIPS.wait(key) # => seconds until the next event is allowed
PER_CALLER = BBS::RateLimit.new(limit: 5, per: 600)
PER_BOARD = BBS::RateLimit.new(limit: 100, per: 600)
BBS::RateLimit.allow_all?([[PER_CALLER, key], [PER_BOARD, :all]]) # counted in all or none
LOGINS = BBS::RateLimit.new(limit: 10, per: 600, store: DB, name: 'logins')
return locked_out unless LOGINS.room?(key)
LOGINS.record(key) unless password_ok?(name, password)
allow_all? egyszerre több korlátot néz (a hívóét és az egész boardét): az eseményt vagy mindegyikbe beszámolja, vagy ha valamelyikben nincs hely, egyikbe sem.room? csak ellenőriz, a record csak számol: arra az esetre, amikor egy esemény csak akkor számít, ha rosszul sült el (rossz jelszó).store:-ral (egy BBS::SQLite) az események a rate_limit_events táblájába kerülnek name: alatt, így egy belépési tiltás az újraindítást is túléli.BBS::WikipediaA Wikipédia Action API-ja úgy, ahogy egy board kérdezi: BBS::HTTP-n át, a board User-Agentjével (anélkül a Wikipédia 403-at adhat), ütemezve, hogy a hívók együtt se küldjenek másodpercenként rate-nél több kérést; amelyik kérés 3 s-nál tovább várna a sorára, el sem megy. A válasz feldolgozva jön (a format=json és a formatversion=2 magától hozzáadódik); a hiba BBS::Wikipedia::Error, ami egy BBS::HTTP::Error. Az Alexandria BBS és a Commodex BBS használja.
WIKIPEDIA = BBS::Wikipedia.new(user_agent: 'Alexandria BBS (+https://teletypegames.org)', rate: 5)
WIKIPEDIA.get('en', action: 'query', list: 'search', srsearch: 'Commodore 64')
# => { "query" => { "search" => [...] } }
A BBS::Gateway (lásd Doorok) egyszer oldja fel a hostot, és az 1.0.0 óta csak nyilvános címet hív: egy listázás, amely 127.0.0.1-et vagy egy privát címet ad meg, nem teheti a boardot a saját hálózatába vezető átjáróvá. Az a board, amely a saját szomszédait tárcsázza, ezt ki is mondja; egy C64 pedig translate: true-val ASCII- és ANSI-terminálokra készült boardot is felhívhat (0.34.0 óta).
BBS::Gateway.new(host: 'host.docker.internal', port: 2327, name: 'Feedia', public_only: false)
BBS::Gateway.new(host: board.host, port: board.port, translate: !board.petscii?).call(session)
BBS::Text.clean(text): érvényes UTF-8, C0 vezérlőkarakterek (a tab és a soremelés marad), DEL, C1 és kétirányú felülíró karakterek nélkül. keep_newlines: false-szal minden sorvég szóköz lesz, így két sor szavai nem ragadnak össze.BBS::Text.line(text): egyetlen tiszta sor, a whitespace-sorozatok (a sorvégek és a tabok is) egy szóközzé válnak, a két végén nincs szóköz. Címhez, névhez, összefoglalóhoz.BBS::Text.printable?(char): minden beviteli mező szabálya.BBS::Address.public?(ip) (a nyilvános interneten van) és BBS::Address.local?(ip) (privát, loopback, link-local).A frame buffer sem tárol vezérlőkaraktert, így egy szolgáltatástól jött szöveg nem mozgathatja és nem színezheti át a hívó képernyőjét.
Egy board rétegei egy szolgáltatás hibáját egyetlen helyen kezelik:
lib/domain/repository) beszél a külvilággal, és a hibáit továbbengedi: a BBS::HTTP::Error-t (és a BBS::Wikipedia::Error-t) úgy, ahogy jön, vagy a board egy domain-hibáját, ha magával a válasszal van baj. Nem alakítja őket nil-lé vagy üres listává, és nem ír szöveget a hívónak;BBS.log sort, és azt adja vissza, amit a UI megmutat (egy cache-elt választ, egy üres eredményt, egy saját üzenetet);BBS::HTTP::Error-t; azt mutatja, amit a service adott.Így URL-t vagy tokent tartalmazó hibaüzenet sosem jut el a hívóig, és minden hiba egyszer kerül a naplóba.
class Library
def search(query)
repository.search(query)
rescue BBS::HTTP::Error => e
BBS.log('search', e)
[]
end
end
BBS::Store