refact round 2

This commit is contained in:
Zsolt Tasnadi
2026-03-11 07:28:14 +01:00
parent e837a9a04e
commit d843df816a
23 changed files with 834 additions and 659 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.env
bbs-server
tmp
data/messages.dat

133
content/catalog.go Normal file
View File

@@ -0,0 +1,133 @@
package content
import (
"bbs-server/engine"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
)
const GamesAPIURL = "https://games.teletype.hu/api/software"
type softwareEntry struct {
Software struct {
Title string `json:"title"`
Platform string `json:"platform"`
Author string `json:"author"`
Desc string `json:"desc"`
} `json:"software"`
Releases []interface{} `json:"releases"`
LatestRelease *struct {
Version string `json:"version"`
HTMLFolderPath string `json:"htmlFolderPath"`
CartridgePath string `json:"cartridgePath"`
SourcePath string `json:"sourcePath"`
DocsFolderPath string `json:"docsFolderPath"`
} `json:"latestRelease"`
}
type catalogRepository struct{}
func (r *catalogRepository) fetchGames() ([]softwareEntry, error) {
client := &http.Client{Timeout: 12 * time.Second}
resp, err := client.Get(GamesAPIURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result struct {
Softwares []softwareEntry `json:"softwares"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return result.Softwares, nil
}
// CatalogHandler renders the game catalog
type CatalogHandler struct {
repo *catalogRepository
}
func NewCatalogHandler() *CatalogHandler {
return &CatalogHandler{repo: &catalogRepository{}}
}
// Show displays the game catalog
func (h *CatalogHandler) Show(s *engine.Session) {
s.Printer.BoxHeader(s.Lang["CatTitle"], engine.YL)
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", engine.GY, s.Lang["WikiLoading"], engine.R))
softwares, err := h.repo.fetchGames()
if err != nil {
s.Printer.Send(fmt.Sprintf("\r\n%s%s: %v%s\r\n", engine.RD, s.Lang["WikiConnError"], err, engine.R))
s.Printer.Pause(s.Lang)
return
}
s.Printer.Send("\r\033[A\033[2K")
if len(softwares) == 0 {
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", engine.GY, s.Lang["CatNoGames"], engine.R))
s.Printer.Pause(s.Lang)
return
}
base := "https://games.teletype.hu"
for i, entry := range softwares {
sw := entry.Software
lr := entry.LatestRelease
s.Printer.Send(fmt.Sprintf("\r\n %s%s%s\r\n", engine.YL, strings.Repeat("─", engine.W-4), engine.R))
s.Printer.Send(fmt.Sprintf(" %s%2d.%s %s%s%s%s %s[%s] by %s%s\r\n",
engine.YL, i+1, engine.R,
engine.WH, engine.B, sw.Title, engine.R,
engine.GY, sw.Platform, sw.Author, engine.R))
if sw.Desc != "" {
wrappedDesc := s.Printer.Wrapped(sw.Desc, 7, 1000)
for _, line := range strings.Split(wrappedDesc, "\r\n") {
s.Printer.Send(fmt.Sprintf("%s%s\r\n", engine.GY, line))
}
}
if lr != nil {
badges := []string{}
if lr.HTMLFolderPath != "" {
badges = append(badges, fmt.Sprintf("%s[▶ Play]%s", engine.GR, engine.R))
}
if lr.CartridgePath != "" {
badges = append(badges, fmt.Sprintf("%s[⬇ Download]%s", engine.BL, engine.R))
}
if lr.SourcePath != "" {
badges = append(badges, fmt.Sprintf("%s[Source]%s", engine.MG, engine.R))
}
if lr.DocsFolderPath != "" {
badges = append(badges, fmt.Sprintf("%s[Docs]%s", engine.YL, engine.R))
}
badgeStr := ""
if len(badges) > 0 {
badgeStr = strings.Join(badges, " ")
}
s.Printer.Send(fmt.Sprintf(" %s%s: v%s%s %s\r\n",
engine.GY, s.Lang["CatLatest"], lr.Version, engine.R, badgeStr))
if lr.HTMLFolderPath != "" {
url := base + lr.HTMLFolderPath
s.Printer.Send(fmt.Sprintf(" %s▶ %s%s\r\n", engine.DIM, url, engine.R))
}
}
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n",
engine.GY, fmt.Sprintf(s.Lang["CatVersions"], len(entry.Releases)), engine.R))
}
s.Printer.HR("─", engine.YL)
s.Printer.Send("\r\n")
s.Printer.Send(fmt.Sprintf(" %s%s: %s%s\r\n", engine.GY, s.Lang["CatFull"], base, engine.R))
s.Printer.Pause(s.Lang)
}

121
content/messages.go Normal file
View File

@@ -0,0 +1,121 @@
package content
import (
"bbs-server/engine"
"encoding/csv"
"fmt"
"os"
"strings"
"sync"
"time"
)
type message struct {
Timestamp string
User string
Text string
}
// MessageBoard holds message board data and its handler
type MessageBoard struct {
messages []message
mu sync.Mutex
path string
}
// NewMessageBoard loads messages from the given .dat file (if it exists)
func NewMessageBoard(path string) *MessageBoard {
b := &MessageBoard{path: path}
b.load()
return b
}
func (b *MessageBoard) load() {
f, err := os.Open(b.path)
if err != nil {
return // file does not exist yet, start empty
}
defer f.Close()
r := csv.NewReader(f)
records, err := r.ReadAll()
if err != nil {
return
}
for _, row := range records {
if len(row) != 3 {
continue
}
b.messages = append(b.messages, message{Timestamp: row[0], User: row[1], Text: row[2]})
}
}
func (b *MessageBoard) append(msg message) error {
f, err := os.OpenFile(b.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
w := csv.NewWriter(f)
err = w.Write([]string{msg.Timestamp, msg.User, msg.Text})
w.Flush()
return err
}
// Count returns the number of messages in a thread-safe manner
func (b *MessageBoard) Count() int {
b.mu.Lock()
defer b.mu.Unlock()
return len(b.messages)
}
// Show displays the message board and handles new message input
func (b *MessageBoard) Show(s *engine.Session) {
s.Printer.BoxHeader(s.Lang["MsgBoardTitle"], engine.GR)
b.mu.Lock()
snap := make([]message, len(b.messages))
copy(snap, b.messages)
b.mu.Unlock()
if len(snap) == 0 {
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", engine.GY, s.Lang["MsgNoMessages"], engine.R))
} else {
start := 0
if len(snap) > 30 {
start = len(snap) - 30
}
for i, msg := range snap[start:] {
s.Printer.Send(fmt.Sprintf(" %s%02d%s %s%s%s %s%s:%s %s\r\n",
engine.GR, i+1, engine.R,
engine.GY, msg.Timestamp, engine.R,
engine.WH, msg.User, engine.R, msg.Text))
}
}
s.Printer.Send(fmt.Sprintf("\r\n%s[N]%s %s %s[ENTER]%s %s → ",
engine.GY, engine.R, s.Lang["MsgNew"],
engine.GY, engine.R, s.Lang["MsgBack"]))
choice, _ := s.Printer.ReadLine()
if strings.ToUpper(strings.TrimSpace(choice)) == "N" {
s.Printer.Send(fmt.Sprintf("\r\n%s%s%s ", engine.WH, s.Lang["MsgEnterText"], engine.R))
msgText, _ := s.Printer.ReadLine()
msgText = strings.TrimSpace(msgText)
if msgText != "" {
if len(msgText) > 200 {
msgText = msgText[:200]
}
ts := time.Now().Format("01-02 15:04")
msg := message{Timestamp: ts, User: s.Username, Text: msgText}
b.mu.Lock()
b.messages = append(b.messages, msg)
b.mu.Unlock()
b.append(msg)
s.Printer.Send(fmt.Sprintf("\r\n%s%s%s\r\n", engine.GR, s.Lang["MsgSent"], engine.R))
} else {
s.Printer.Send(fmt.Sprintf("\r\n%s%s%s\r\n", engine.GY, s.Lang["MsgEmpty"], engine.R))
}
}
}

32
content/online.go Normal file
View File

@@ -0,0 +1,32 @@
package content
import (
"bbs-server/engine"
"fmt"
"sort"
)
// Online displays currently logged-in users
func Online(s *engine.Session) {
s.Printer.BoxHeader(s.Lang["OnlineTitle"], engine.CY)
snap := s.State.Snapshot()
keys := make([]string, 0, len(snap))
for k := range snap {
keys = append(keys, k)
}
sort.Strings(keys)
for _, addr := range keys {
user := snap[addr]
marker := ""
if addr == s.Addr {
marker = fmt.Sprintf(" %s%s%s", engine.GR, s.Lang["OnlineYou"], engine.R)
}
s.Printer.Send(fmt.Sprintf(" %s•%s %s%s%s%s\r\n", engine.CY, engine.R, engine.WH, user, engine.R, marker))
}
s.Printer.Send(fmt.Sprintf("\r\n %s%s\r\n", engine.GY,
fmt.Sprintf(s.Lang["OnlineTotal"], engine.WH, len(snap), engine.GY, engine.R)))
s.Printer.Pause(s.Lang)
}

32
content/sysinfo.go Normal file
View File

@@ -0,0 +1,32 @@
package content
import (
"bbs-server/engine"
"fmt"
"strconv"
"time"
)
// Sysinfo returns a HandlerFunc that displays system information
func Sysinfo(board *MessageBoard) engine.HandlerFunc {
return func(s *engine.Session) {
s.Printer.BoxHeader(s.Lang["SysInfoTitle"], engine.GY)
now := time.Now().Format("2006-01-02 15:04:05")
rows := [][]string{
{s.Lang["SysServerTime"], now},
{s.Lang["SysOnlineUsers"], strconv.Itoa(s.State.UserCount())},
{s.Lang["SysMsgCount"], strconv.Itoa(board.Count())},
{s.Lang["SysWikiURL"], WikiJSBaseURL},
{s.Lang["SysGamesAPI"], GamesAPIURL},
{s.Lang["SysPlatform"], "Go BBS v2.0"},
}
for _, row := range rows {
s.Printer.Send(fmt.Sprintf(" %s%-18s%s %s%s%s\r\n",
engine.GY, row[0], engine.R, engine.WH, row[1], engine.R))
}
s.Printer.Pause(s.Lang)
}
}

203
content/wiki.go Normal file
View File

@@ -0,0 +1,203 @@
package content
import (
"bbs-server/engine"
"bytes"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
)
const WikiJSBaseURL = "https://wiki.teletype.hu"
type wikiPage struct {
ID interface{} `json:"id"`
Path string `json:"path"`
Title string `json:"title"`
Description string `json:"description"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
Locale string `json:"locale"`
}
type wikiRepository struct {
token string
}
func (r *wikiRepository) graphql(query string) (map[string]interface{}, error) {
payload := map[string]string{"query": query}
data, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", WikiJSBaseURL+"/graphql", bytes.NewBuffer(data))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
if r.token != "" {
req.Header.Set("Authorization", "Bearer "+r.token)
}
client := &http.Client{Timeout: 12 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return result, nil
}
func (r *wikiRepository) fetchList(tag string) ([]wikiPage, error) {
q := fmt.Sprintf(`{ pages { list(orderBy: CREATED, orderByDirection: DESC, tags: ["%s"]) { id path title description createdAt updatedAt locale } } }`, tag)
res, err := r.graphql(q)
if err != nil {
return nil, err
}
data, ok := res["data"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("invalid response format")
}
pages, ok := data["pages"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("invalid response format")
}
listRaw, ok := pages["list"].([]interface{})
if !ok {
return nil, fmt.Errorf("invalid response format")
}
var list []wikiPage
jsonBytes, _ := json.Marshal(listRaw)
json.Unmarshal(jsonBytes, &list)
return list, nil
}
func (r *wikiRepository) fetchContent(pageID interface{}) (string, error) {
var idStr string
switch v := pageID.(type) {
case string:
idStr = v
case float64:
idStr = fmt.Sprintf("%.0f", v)
default:
idStr = fmt.Sprintf("%v", v)
}
q := fmt.Sprintf(`{ pages { single(id: %s) { content } } }`, idStr)
res, err := r.graphql(q)
if err != nil {
return "", err
}
data := res["data"].(map[string]interface{})
pages := data["pages"].(map[string]interface{})
single := pages["single"].(map[string]interface{})
return single["content"].(string), nil
}
// WikiHandler renders Wiki.js content in the BBS
type WikiHandler struct {
repo *wikiRepository
}
func NewWikiHandler(token string) *WikiHandler {
return &WikiHandler{repo: &wikiRepository{token: token}}
}
// List returns a HandlerFunc that lists pages with the given tag
func (h *WikiHandler) List(tag, title, color string) engine.HandlerFunc {
return func(s *engine.Session) {
s.Printer.BoxHeader(title, color)
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", engine.GY, s.Lang["WikiLoading"], engine.R))
pages, err := h.repo.fetchList(tag)
if err != nil {
s.Printer.Send(fmt.Sprintf("\r\n%s%s: %v%s\r\n", engine.RD, s.Lang["WikiConnError"], err, engine.R))
s.Printer.Pause(s.Lang)
return
}
s.Printer.Send("\r\033[A\033[2K")
if len(pages) == 0 {
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", engine.GY, fmt.Sprintf(s.Lang["WikiNoResults"], tag), engine.R))
s.Printer.Pause(s.Lang)
return
}
maxIdx := 25
if len(pages) < maxIdx {
maxIdx = len(pages)
}
for i, p := range pages[:maxIdx] {
date := s.Printer.FmtDate(p.CreatedAt)
if date == "" {
date = s.Printer.FmtDate(p.UpdatedAt)
}
titleP := p.Title
if titleP == "" {
titleP = p.Path
}
if len(titleP) > 55 {
titleP = titleP[:55]
}
desc := p.Description
if len(desc) > 60 {
desc = desc[:60]
}
s.Printer.Send(fmt.Sprintf(" %s%2d%s %s%s%s\r\n", color, i+1, engine.R, engine.WH, titleP, engine.R))
if desc != "" {
s.Printer.Send(fmt.Sprintf(" %s%s %s%s%s\r\n", engine.GY, date, engine.DIM, desc, engine.R))
} else {
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", engine.GY, date, engine.R))
}
}
s.Printer.Send(fmt.Sprintf("\r\n%s%s%s ", engine.GY, s.Lang["WikiEnterNum"], engine.R))
choice, _ := s.Printer.ReadLine()
choice = strings.TrimSpace(choice)
idx, err := strconv.Atoi(choice)
if err != nil || idx < 1 || idx > len(pages) {
return
}
page := pages[idx-1]
s.Printer.Send(fmt.Sprintf("\r\n%s%s%s", engine.GY, s.Lang["WikiFetchContent"], engine.R))
pageContent, err := h.repo.fetchContent(page.ID)
if err != nil {
s.Printer.Send(fmt.Sprintf("\r\n%sHiba: %v%s\r\n", engine.RD, err, engine.R))
s.Printer.Pause(s.Lang)
return
}
s.Printer.Send("\r\033[A\033[2K")
s.Printer.Send("\r\n")
s.Printer.HR("═", color)
s.Printer.Send("\r\n")
s.Printer.Send(fmt.Sprintf(" %s%s%s%s\r\n", engine.WH, engine.B, page.Title, engine.R))
url := fmt.Sprintf("%s/%s/%s", WikiJSBaseURL, page.Locale, page.Path)
s.Printer.Send(fmt.Sprintf(" %s%s %s%s%s\r\n", engine.GY, s.Printer.FmtDate(page.CreatedAt), engine.DIM, url, engine.R))
s.Printer.HR("─", engine.GY)
s.Printer.Send("\r\n\r\n")
body := s.Printer.Wrapped(pageContent, 2, 5000)
for _, line := range strings.Split(body, "\r\n") {
s.Printer.Send(line + "\r\n")
}
s.Printer.Send("\r\n")
s.Printer.HR("─", engine.GY)
s.Printer.Send("\r\n")
s.Printer.Pause(s.Lang)
}
}

0
data/.keep Normal file
View File

View File

@@ -8,6 +8,9 @@ services:
- "2323:2323"
environment:
- WEBAPP_WIKIJS_TOKEN=${WEBAPP_WIKIJS_TOKEN}
- MESSAGES_PATH=/data/messages.dat
volumes:
- bbs-messages:/data
restart: always
read_only: true
tmpfs:
@@ -16,3 +19,6 @@ services:
- ALL
security_opt:
- no-new-privileges:true
volumes:
bbs-messages:

View File

@@ -8,8 +8,10 @@ services:
- "2323:2323"
volumes:
- ./:/app/
- ./data:/data
environment:
- WEBAPP_WIKIJS_TOKEN=${WEBAPP_WIKIJS_TOKEN:-}
- MESSAGES_PATH=/data/messages.dat
restart: unless-stopped
stdin_open: true
tty: true

View File

@@ -1,22 +1,15 @@
package lib
package engine
// T is the i18n dictionary type
type T map[string]string
// En is the default English dictionary
var En = T{
"Welcome": "Welcome to Teletype's Bulletin Board System!",
"AskName": "Enter your name:",
"Greeting": "Hello, %s%s%s! Welcome to Teletype BBS!",
"MainMenuTitle": "MAIN MENU",
"MenuUzenopal": "Message Board",
"MenuBlog": "Blog Posts",
"MenuHowto": "HowTo Guides",
"MenuCatalog": "Game Catalog",
"MenuOnline": "Online Users",
"MenuSysinfo": "System Info",
"MenuExit": "Exit",
"Choice": "Choice: ",
"Pause": "Press ENTER...",
"Goodbye": "Goodbye, %s! 👋",
"Pause": "Press ENTER...",
"Choice": "Choice: ",
"MsgBoardTitle": "📋 MESSAGE BOARD",
"MsgNoMessages": "(No messages yet — be the first!)",
"MsgNew": "Write new message",
@@ -44,6 +37,4 @@ var En = T{
"SysWikiURL": "Wiki URL",
"SysGamesAPI": "Games API",
"SysPlatform": "Platform",
"SysYes": "yes",
"SysNo": "no",
}

78
engine/menu.go Normal file
View File

@@ -0,0 +1,78 @@
package engine
import (
"fmt"
"strings"
)
// HandlerFunc is the handler type for a menu item
type HandlerFunc func(*Session)
// Exit is a built-in handler that terminates the session
var Exit HandlerFunc = func(s *Session) { s.Quit = true }
type menuItem struct {
Key string
Label string
Color string
Handler HandlerFunc
}
// Menu defines the BBS main menu using a declarative DSL
type Menu struct {
title string
items []menuItem
}
// Title sets the menu header
func (m *Menu) Title(t string) {
m.title = t
}
// Item adds a menu entry (key, display label, ANSI color, handler)
func (m *Menu) Item(key, label, color string, handler HandlerFunc) {
m.items = append(m.items, menuItem{
Key: strings.ToUpper(key),
Label: label,
Color: color,
Handler: handler,
})
}
// Dispatch finds and calls the handler for the given key
func (m *Menu) Dispatch(s *Session, key string) {
key = strings.ToUpper(strings.TrimSpace(key))
for _, item := range m.items {
if item.Key == key {
item.Handler(s)
return
}
}
}
// Render returns the rendered menu string
func (m *Menu) Render(p *Printer, lang T, username string) string {
headerLine := strings.Repeat("═", W)
l1 := p.PadLine(fmt.Sprintf(" %s%s%s %s@%s%s", YL, m.title, R, GY, username, R), W)
var rows []string
for i := 0; i < len(m.items); i += 2 {
if i+1 < len(m.items) {
left := p.PadLine(fmt.Sprintf(" %s[%s]%s %s", m.items[i].Color, m.items[i].Key, R, m.items[i].Label), W/2)
right := p.PadLine(fmt.Sprintf(" %s[%s]%s %s", m.items[i+1].Color, m.items[i+1].Key, R, m.items[i+1].Label), W/2)
rows = append(rows, left+right)
} else {
rows = append(rows, p.PadLine(fmt.Sprintf(" %s[%s]%s %s", m.items[i].Color, m.items[i].Key, R, m.items[i].Label), W))
}
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("\n%s╔%s╗%s\n", WH, headerLine, R))
sb.WriteString(fmt.Sprintf("%s║%s%s║%s\n", WH, l1, WH, R))
sb.WriteString(fmt.Sprintf("%s╠%s╣%s\n", WH, headerLine, R))
for _, row := range rows {
sb.WriteString(fmt.Sprintf("%s║%s%s║%s\n", WH, row, WH, R))
}
sb.WriteString(fmt.Sprintf("%s╚%s╝%s\n%s", WH, headerLine, R, lang["Choice"]))
return sb.String()
}

View File

@@ -1,4 +1,4 @@
package lib
package engine
import (
"bytes"
@@ -10,7 +10,7 @@ import (
"unicode/utf8"
)
// ANSI Colors
// ANSI color codes
const (
R = "\033[0m"
B = "\033[1m"
@@ -25,14 +25,9 @@ const (
GY = "\033[0;37m"
)
// W is the default terminal width
const W = 70
type Message struct {
User string
Timestamp string
Text string
}
type Printer struct {
Conn net.Conn
}
@@ -152,7 +147,7 @@ func (p *Printer) StripMD(text string) string {
reImg := regexp.MustCompile(`!\[.*?\]\(.*?\)|\[(.*?)\]\(.*?\)|#{1,6}\s*|[*_` + "`" + `~>|]`)
text = reImg.ReplaceAllStringFunc(text, func(s string) string {
if strings.HasPrefix(s, "[") {
sub := regexp.MustCompile(`\[(.*?)\]\(.*?\)` )
sub := regexp.MustCompile(`\[(.*?)\]\(.*?\)`)
matches := sub.FindStringSubmatch(s)
if len(matches) > 1 {
return matches[1]

123
engine/server.go Normal file
View File

@@ -0,0 +1,123 @@
package engine
import (
"fmt"
"net"
"os"
"strings"
)
// Config holds the BBS server configuration
type Config struct {
Host string
Port string
Banner string
Lang T
}
// BBS is the main server struct
type BBS struct {
config Config
state *State
menu *Menu
}
// New creates a new BBS server with the given configuration
func New(cfg Config) *BBS {
return &BBS{
config: cfg,
state: newState(),
menu: &Menu{},
}
}
// State returns the shared server state (for use by content handlers)
func (b *BBS) State() *State {
return b.state
}
// Menu configures the main menu using the DSL
func (b *BBS) Menu(fn func(*Menu)) {
fn(b.menu)
}
// Start launches the TCP server and blocks waiting for connections
func (b *BBS) Start() {
ln, err := net.Listen("tcp", b.config.Host+":"+b.config.Port)
if err != nil {
fmt.Printf("Server start error: %v\n", err)
os.Exit(1)
}
defer ln.Close()
fmt.Printf("BBS running → telnet localhost %s\n", b.config.Port)
fmt.Println("Stop: Ctrl+C")
for {
conn, err := ln.Accept()
if err != nil {
fmt.Printf("Accept error: %v\n", err)
continue
}
fmt.Printf("[+] Connected: %s\n", conn.RemoteAddr().String())
go b.handleClient(conn)
}
}
func (b *BBS) handleClient(conn net.Conn) {
defer conn.Close()
addr := conn.RemoteAddr().String()
lang := b.config.Lang
printer := NewPrinter(conn)
// Telnet negotiation (IAC WILL ECHO, IAC WILL SGA, IAC WONT LINEMODE)
printer.Send("\xff\xfb\x01\xff\xfb\x03\xff\xfe\x22")
printer.Send(CY + b.config.Banner + R)
printer.Send(fmt.Sprintf("\n%s%s%s ", WH, lang["AskName"], R))
username, err := printer.ReadLine()
if err != nil {
return
}
username = strings.TrimSpace(username)
if username == "" {
username = "Anonymous"
}
if len(username) > 20 {
username = username[:20]
}
b.state.Mu.Lock()
b.state.OnlineUsers[addr] = username
b.state.Mu.Unlock()
defer func() {
b.state.Mu.Lock()
delete(b.state.OnlineUsers, addr)
b.state.Mu.Unlock()
}()
printer.Send(fmt.Sprintf("\r\n%s%s%s\r\n", GR, fmt.Sprintf(lang["Greeting"], WH, username, GR), R))
session := &Session{
State: b.state,
Printer: printer,
Username: username,
Addr: addr,
Lang: lang,
}
for !session.Quit {
printer.Send(b.menu.Render(printer, lang, username))
choice, err := printer.ReadLine()
if err != nil {
break
}
b.menu.Dispatch(session, choice)
}
if session.Quit {
printer.Send(fmt.Sprintf("\r\n%s%s%s\r\n\r\n", RD, fmt.Sprintf(lang["Goodbye"], username), R))
}
}

43
engine/session.go Normal file
View File

@@ -0,0 +1,43 @@
package engine
import "sync"
// State holds shared server state (thread-safe)
type State struct {
OnlineUsers map[string]string // addr -> username
Mu sync.Mutex
}
func newState() *State {
return &State{
OnlineUsers: make(map[string]string),
}
}
// UserCount returns the number of online users in a thread-safe manner
func (st *State) UserCount() int {
st.Mu.Lock()
defer st.Mu.Unlock()
return len(st.OnlineUsers)
}
// Snapshot returns a thread-safe copy of the online users map
func (st *State) Snapshot() map[string]string {
st.Mu.Lock()
defer st.Mu.Unlock()
snap := make(map[string]string)
for k, v := range st.OnlineUsers {
snap[k] = v
}
return snap
}
// Session represents an active user connection
type Session struct {
State *State
Printer *Printer
Username string
Addr string
Lang T
Quit bool
}

View File

@@ -1,76 +0,0 @@
package lib
import (
"fmt"
"strings"
)
func (s *Session) ShowGames() {
s.Printer.BoxHeader(s.Lang["CatTitle"], YL)
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", GY, s.Lang["WikiLoading"], R))
softwares, err := s.BBS.CatalogRepo.FetchGames()
if err != nil {
s.Printer.Send(fmt.Sprintf("\r\n%s%s: %v%s\r\n", RD, s.Lang["WikiConnError"], err, R))
s.Printer.Pause(s.Lang)
return
}
s.Printer.Send("\r\033[A\033[2K")
if len(softwares) == 0 {
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", GY, s.Lang["CatNoGames"], R))
s.Printer.Pause(s.Lang)
return
}
base := "https://games.teletype.hu"
for i, entry := range softwares {
sw := entry.Software
lr := entry.LatestRelease
s.Printer.Send(fmt.Sprintf("\r\n %s%s%s\r\n", YL, strings.Repeat("─", W-4), R))
s.Printer.Send(fmt.Sprintf(" %s%2d.%s %s%s%s%s %s[%s] by %s%s\r\n", YL, i+1, R, WH, B, sw.Title, R, GY, sw.Platform, sw.Author, R))
if sw.Desc != "" {
wrappedDesc := s.Printer.Wrapped(sw.Desc, 7, 1000)
for _, line := range strings.Split(wrappedDesc, "\r\n") {
s.Printer.Send(fmt.Sprintf("%s%s\r\n", GY, line))
}
}
if lr != nil {
badges := []string{}
if lr.HTMLFolderPath != "" {
badges = append(badges, fmt.Sprintf("%s[▶ Play]%s", GR, R))
}
if lr.CartridgePath != "" {
badges = append(badges, fmt.Sprintf("%s[⬇ Download]%s", BL, R))
}
if lr.SourcePath != "" {
badges = append(badges, fmt.Sprintf("%s[Source]%s", MG, R))
}
if lr.DocsFolderPath != "" {
badges = append(badges, fmt.Sprintf("%s[Docs]%s", YL, R))
}
badgeStr := ""
if len(badges) > 0 {
badgeStr = strings.Join(badges, " ")
}
s.Printer.Send(fmt.Sprintf(" %s%s: v%s%s %s\r\n", GY, s.Lang["CatLatest"], lr.Version, R, badgeStr))
if lr.HTMLFolderPath != "" {
url := base + lr.HTMLFolderPath
s.Printer.Send(fmt.Sprintf(" %s▶ %s%s\r\n", DIM, url, R))
}
}
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", GY, fmt.Sprintf(s.Lang["CatVersions"], len(entry.Releases)), R))
}
s.Printer.HR("─", YL)
s.Printer.Send("\r\n")
s.Printer.Send(fmt.Sprintf(" %s%s: %s%s\r\n", GY, s.Lang["CatFull"], base, R))
s.Printer.Pause(s.Lang)
}

View File

@@ -1,33 +0,0 @@
package lib
import (
"fmt"
"sort"
)
func (s *Session) ShowOnline() {
s.Printer.BoxHeader(s.Lang["OnlineTitle"], CY)
s.BBS.Mu.Lock()
snap := make(map[string]string)
for k, v := range s.BBS.OnlineUsers {
snap[k] = v
}
s.BBS.Mu.Unlock()
keys := make([]string, 0, len(snap))
for k := range snap {
keys = append(keys, k)
}
sort.Strings(keys)
for _, addr := range keys {
user := snap[addr]
marker := ""
if addr == s.Addr {
marker = fmt.Sprintf(" %s%s%s", GR, s.Lang["OnlineYou"], R)
}
s.Printer.Send(fmt.Sprintf(" %s•%s %s%s%s%s\r\n", CY, R, WH, user, R, marker))
}
s.Printer.Send(fmt.Sprintf("\r\n %s%s\r\n", GY, fmt.Sprintf(s.Lang["OnlineTotal"], WH, len(snap), GY, R)))
s.Printer.Pause(s.Lang)
}

View File

@@ -1,31 +0,0 @@
package lib
import (
"fmt"
"strconv"
"time"
)
func (s *Session) ShowSysinfo() {
s.Printer.BoxHeader(s.Lang["SysInfoTitle"], GY)
s.BBS.Mu.Lock()
uc := len(s.BBS.OnlineUsers)
mc := len(s.BBS.Messages)
s.BBS.Mu.Unlock()
now := time.Now().Format("2006-01-02 15:04:05")
rows := [][]string{
{s.Lang["SysServerTime"], now},
{s.Lang["SysOnlineUsers"], strconv.Itoa(uc)},
{s.Lang["SysMsgCount"], strconv.Itoa(mc)},
{s.Lang["SysWikiURL"], WikiJSBaseURL},
{s.Lang["SysGamesAPI"], GamesAPIURL},
{s.Lang["SysPlatform"], "Go BBS v2.0"},
}
for _, row := range rows {
s.Printer.Send(fmt.Sprintf(" %s%-18s%s %s%s%s\r\n", GY, row[0], R, WH, row[1], R))
}
s.Printer.Pause(s.Lang)
}

View File

@@ -1,47 +0,0 @@
package lib
import (
"fmt"
"strings"
"time"
)
func (s *Session) ShowUzenopal() {
s.Printer.BoxHeader(s.Lang["MsgBoardTitle"], GR)
s.BBS.Mu.Lock()
snap := make([]Message, len(s.BBS.Messages))
copy(snap, s.BBS.Messages)
s.BBS.Mu.Unlock()
if len(snap) == 0 {
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", GY, s.Lang["MsgNoMessages"], R))
} else {
start := 0
if len(snap) > 30 {
start = len(snap) - 30
}
for i, msg := range snap[start:] {
s.Printer.Send(fmt.Sprintf(" %s%02d%s %s%s%s %s%s:%s %s\r\n", GR, i+1, R, GY, msg.Timestamp, R, WH, msg.User, R, msg.Text))
}
}
s.Printer.Send(fmt.Sprintf("\r\n%s[N]%s %s %s[ENTER]%s %s → ", GY, R, s.Lang["MsgNew"], GY, R, s.Lang["MsgBack"]))
choice, _ := s.Printer.ReadLine()
if strings.ToUpper(strings.TrimSpace(choice)) == "N" {
s.Printer.Send(fmt.Sprintf("\r\n%s%s%s ", WH, s.Lang["MsgEnterText"], R))
msgText, _ := s.Printer.ReadLine()
msgText = strings.TrimSpace(msgText)
if msgText != "" {
if len(msgText) > 200 {
msgText = msgText[:200]
}
ts := time.Now().Format("01-02 15:04")
s.BBS.Mu.Lock()
s.BBS.Messages = append(s.BBS.Messages, Message{User: s.Username, Timestamp: ts, Text: msgText})
s.BBS.Mu.Unlock()
s.Printer.Send(fmt.Sprintf("\r\n%s%s%s\r\n", GR, s.Lang["MsgSent"], R))
} else {
s.Printer.Send(fmt.Sprintf("\r\n%s%s%s\r\n", GY, s.Lang["MsgEmpty"], R))
}
}
}

View File

@@ -1,93 +0,0 @@
package lib
import (
"fmt"
"strconv"
"strings"
)
func (s *Session) ShowWikiList(tag string, color string, title string) {
s.Printer.BoxHeader(title, color)
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", GY, s.Lang["WikiLoading"], R))
pages, err := s.BBS.WikiRepo.FetchList(tag)
if err != nil {
s.Printer.Send(fmt.Sprintf("\r\n%s%s: %v%s\r\n", RD, s.Lang["WikiConnError"], err, R))
s.Printer.Pause(s.Lang)
return
}
s.Printer.Send("\r\033[A\033[2K")
if len(pages) == 0 {
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", GY, fmt.Sprintf(s.Lang["WikiNoResults"], tag), R))
s.Printer.Pause(s.Lang)
return
}
maxIdx := 25
if len(pages) < maxIdx {
maxIdx = len(pages)
}
for i, p := range pages[:maxIdx] {
date := s.Printer.FmtDate(p.CreatedAt)
if date == "" {
date = s.Printer.FmtDate(p.UpdatedAt)
}
titleP := p.Title
if titleP == "" {
titleP = p.Path
}
if len(titleP) > 55 {
titleP = titleP[:55]
}
desc := p.Description
if len(desc) > 60 {
desc = desc[:60]
}
s.Printer.Send(fmt.Sprintf(" %s%2d%s %s%s%s\r\n", color, i+1, R, WH, titleP, R))
if desc != "" {
s.Printer.Send(fmt.Sprintf(" %s%s %s%s%s\r\n", GY, date, DIM, desc, R))
} else {
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", GY, date, R))
}
}
s.Printer.Send(fmt.Sprintf("\r\n%s%s%s ", GY, s.Lang["WikiEnterNum"], R))
choice, _ := s.Printer.ReadLine()
choice = strings.TrimSpace(choice)
idx, err := strconv.Atoi(choice)
if err != nil || idx < 1 || idx > len(pages) {
return
}
page := pages[idx-1]
s.Printer.Send(fmt.Sprintf("\r\n%s%s%s", GY, s.Lang["WikiFetchContent"], R))
content, err := s.BBS.WikiRepo.FetchContent(page.ID)
if err != nil {
s.Printer.Send(fmt.Sprintf("\r\n%sHiba: %v%s\r\n", RD, err, R))
s.Printer.Pause(s.Lang)
return
}
s.Printer.Send("\r\033[A\033[2K")
s.Printer.Send(fmt.Sprintf("\r\n"))
s.Printer.HR("═", color)
s.Printer.Send("\r\n")
s.Printer.Send(fmt.Sprintf(" %s%s%s%s\r\n", WH, B, page.Title, R))
url := fmt.Sprintf("%s/%s/%s", WikiJSBaseURL, page.Locale, page.Path)
s.Printer.Send(fmt.Sprintf(" %s%s %s%s%s\r\n", GY, s.Printer.FmtDate(page.CreatedAt), DIM, url, R))
s.Printer.HR("─", GY)
s.Printer.Send("\r\n\r\n")
body := s.Printer.Wrapped(content, 2, 5000)
for _, line := range strings.Split(body, "\r\n") {
s.Printer.Send(line+"\r\n")
}
s.Printer.Send("\r\n")
s.Printer.HR("─", GY)
s.Printer.Send("\r\n")
s.Printer.Pause(s.Lang)
}

View File

@@ -1,49 +0,0 @@
package lib
import (
"encoding/json"
"net/http"
"time"
)
const GamesAPIURL = "https://games.teletype.hu/api/software"
type SoftwareEntry struct {
Software struct {
Title string `json:"title"`
Platform string `json:"platform"`
Author string `json:"author"`
Desc string `json:"desc"`
} `json:"software"`
Releases []interface{} `json:"releases"`
LatestRelease *struct {
Version string `json:"version"`
HTMLFolderPath string `json:"htmlFolderPath"`
CartridgePath string `json:"cartridgePath"`
SourcePath string `json:"sourcePath"`
DocsFolderPath string `json:"docsFolderPath"`
} `json:"latestRelease"`
}
type CatalogRepository struct{}
func NewCatalogRepository() *CatalogRepository {
return &CatalogRepository{}
}
func (r *CatalogRepository) FetchGames() ([]SoftwareEntry, error) {
client := &http.Client{Timeout: 12 * time.Second}
resp, err := client.Get(GamesAPIURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result struct {
Softwares []SoftwareEntry `json:"softwares"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return result.Softwares, nil
}

View File

@@ -1,106 +0,0 @@
package lib
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)
const WikiJSBaseURL = "https://wiki.teletype.hu"
type WikiPage struct {
ID interface{} `json:"id"`
Path string `json:"path"`
Title string `json:"title"`
Description string `json:"description"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
Locale string `json:"locale"`
}
type WikiRepository struct {
Token string
}
func NewWikiRepository(token string) *WikiRepository {
return &WikiRepository{Token: token}
}
func (r *WikiRepository) graphql(query string) (map[string]interface{}, error) {
payload := map[string]string{"query": query}
data, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", WikiJSBaseURL+"/graphql", bytes.NewBuffer(data))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
if r.Token != "" {
req.Header.Set("Authorization", "Bearer "+r.Token)
}
client := &http.Client{Timeout: 12 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return result, nil
}
func (r *WikiRepository) FetchList(tag string) ([]WikiPage, error) {
q := fmt.Sprintf(`{ pages { list(orderBy: CREATED, orderByDirection: DESC, tags: ["%s"]) { id path title description createdAt updatedAt locale } } }`, tag)
res, err := r.graphql(q)
if err != nil {
return nil, err
}
data, ok := res["data"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("invalid response format")
}
pages, ok := data["pages"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("invalid response format")
}
listRaw, ok := pages["list"].([]interface{})
if !ok {
return nil, fmt.Errorf("invalid response format")
}
var list []WikiPage
jsonBytes, _ := json.Marshal(listRaw)
json.Unmarshal(jsonBytes, &list)
return list, nil
}
func (r *WikiRepository) FetchContent(pageID interface{}) (string, error) {
var idStr string
switch v := pageID.(type) {
case string:
idStr = v
case float64:
idStr = fmt.Sprintf("%.0f", v)
default:
idStr = fmt.Sprintf("%v", v)
}
q := fmt.Sprintf(`{ pages { single(id: %s) { content } } }`, idStr)
res, err := r.graphql(q)
if err != nil {
return "", err
}
data := res["data"].(map[string]interface{})
pages := data["pages"].(map[string]interface{})
single := pages["single"].(map[string]interface{})
return single["content"].(string), nil
}

View File

@@ -1,102 +0,0 @@
package lib
import (
"fmt"
"strings"
"sync"
)
const Banner = `
████████╗███████╗██╗ ███████╗████████╗██╗ ██╗██████╗ ███████╗
██╔══╝██╔════╝██║ ██╔════╝╚══██╔══╝╚██╗ ██╔╝██╔══██╗██╔════╝
██║ █████╗ ██║ █████╗ ██║ ╚████╔╝ ██████╔╝█████╗
██║ ██╔══╝ ██║ ██╔══╝ ██║ ╚██╔╝ ██╔═══╝ ██╔══╝
██║ ███████╗███████╗███████╗ ██║ ██║ ██║ ███████╗
╚═╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝
██████╗ █████╗ ███╗ ███╗███████╗███████╗
██╔════╝ ██╔══██╗████╗ ████║██╔════╝██╔════╝
██║ ███╗███████║██╔████╔██║█████╗ ███████╗
██║ ██║██╔══██║██║╚██╔╝██║██╔══╝ ╚════██║
╚██████╔╝██║ ██║██║ ╚═╝ ██║███████╗███████║
╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝
░░ BBS v2.0 ░░ teletype.hu ░░
Welcome to the Teletype community bulletin board!
`
type BBS struct {
Messages []Message
OnlineUsers map[string]string // addr -> username
Mu sync.Mutex
WikiToken string
WikiRepo *WikiRepository
CatalogRepo *CatalogRepository
}
func NewBBS(wikiToken string) *BBS {
return &BBS{
Messages: []Message{},
OnlineUsers: make(map[string]string),
WikiToken: wikiToken,
WikiRepo: NewWikiRepository(wikiToken),
CatalogRepo: NewCatalogRepository(),
}
}
type UI struct {
Printer *Printer
Lang T
}
func NewUI(printer *Printer, lang T) *UI {
return &UI{Printer: printer, Lang: lang}
}
type Session struct {
BBS *BBS
Printer *Printer
Username string
Addr string
Lang T
}
func NewSession(bbs *BBS, printer *Printer, username string, addr string, lang T) *Session {
return &Session{
BBS: bbs,
Printer: printer,
Username: username,
Addr: addr,
Lang: lang,
}
}
func (ui *UI) MainMenu(username string) string {
headerLine := strings.Repeat("═", W)
l1 := ui.Printer.PadLine(fmt.Sprintf(" %s%s%s %s@%s%s", YL, ui.Lang["MainMenuTitle"], R, GY, username, R), W)
l2 := ui.Printer.PadLine(fmt.Sprintf(" %s[1]%s %s", GR, R, ui.Lang["MenuUzenopal"]), W/2) + ui.Printer.PadLine(fmt.Sprintf(" %s[2]%s %s", BL, R, ui.Lang["MenuBlog"]), W/2)
l3 := ui.Printer.PadLine(fmt.Sprintf(" %s[3]%s %s", MG, R, ui.Lang["MenuHowto"]), W/2) + ui.Printer.PadLine(fmt.Sprintf(" %s[4]%s %s", YL, R, ui.Lang["MenuCatalog"]), W/2)
l4 := ui.Printer.PadLine(fmt.Sprintf(" %s[5]%s %s", CY, R, ui.Lang["MenuOnline"]), W/2) + ui.Printer.PadLine(fmt.Sprintf(" %s[6]%s %s", GY, R, ui.Lang["MenuSysinfo"]), W/2)
l5 := ui.Printer.PadLine(fmt.Sprintf(" %s[Q]%s %s", RD, R, ui.Lang["MenuExit"]), W)
return fmt.Sprintf(
"\n%s╔%s╗%s\n"+
"%s║%s%s║%s\n"+
"%s╠%s╣%s\n"+
"%s║%s%s║%s\n"+
"%s║%s%s║%s\n"+
"%s║%s%s║%s\n"+
"%s║%s%s║%s\n"+
"%s╚%s╝%s\n%s",
WH, headerLine, R,
WH, l1, WH, R,
WH, headerLine, R,
WH, l2, WH, R,
WH, l3, WH, R,
WH, l4, WH, R,
WH, l5, WH, R,
WH, headerLine, R,
ui.Lang["Choice"],
)
}

144
main.go
View File

@@ -1,116 +1,68 @@
package main
import (
"bbs-server/lib"
"bbs-server/content"
"bbs-server/engine"
"fmt"
"net"
"os"
"strings"
)
const (
Host = "0.0.0.0"
Port = "2323"
)
const banner = `
████████╗███████╗██╗ ███████╗████████╗██╗ ██╗██████╗ ███████╗
██╔══╝██╔════╝██║ ██╔════╝╚══██╔══╝╚██╗ ██╔╝██╔══██╗██╔════╝
██║ █████╗ ██║ █████╗ ██║ ╚████╔╝ ██████╔╝█████╗
██║ ██╔══╝ ██║ ██╔══╝ ██║ ╚██╔╝ ██╔═══╝ ██╔══╝
██║ ███████╗███████╗███████╗ ██║ ██║ ██║ ███████╗
╚═╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝
var bbs *lib.BBS
██████╗ █████╗ ███╗ ███╗███████╗███████╗
██╔════╝ ██╔══██╗████╗ ████║██╔════╝██╔════╝
██║ ███╗███████║██╔████╔██║█████╗ ███████╗
██║ ██║██╔══██║██║╚██╔╝██║██╔══╝ ╚════██║
╚██████╔╝██║ ██║██║ ╚═╝ ██║███████╗███████║
╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝
░░ BBS v2.0 ░░ teletype.hu ░░
Welcome to the Teletype community bulletin board!
`
func main() {
wikiToken := os.Getenv("WEBAPP_WIKIJS_TOKEN")
bbs = lib.NewBBS(wikiToken)
ln, err := net.Listen("tcp", Host+":"+Port)
if err != nil {
fmt.Printf("Hiba a szerver indításakor: %v\n", err)
os.Exit(1)
boardPath := os.Getenv("MESSAGES_PATH")
if boardPath == "" {
boardPath = "messages.dat"
}
defer ln.Close()
fmt.Printf("Teletype BBS running → telnet localhost %s\n", Port)
fmt.Printf("Wiki: %s\n", lib.WikiJSBaseURL)
fmt.Printf("Games API: %s\n", lib.GamesAPIURL)
tokenStatus := "✗ not set"
if wikiToken != "" {
tokenStatus = "✓ set"
}
fmt.Printf("Token: %s\n", tokenStatus)
fmt.Println("Stop: Ctrl+C")
fmt.Printf("Wiki: %s\n", content.WikiJSBaseURL)
fmt.Printf("Games API: %s\n", content.GamesAPIURL)
fmt.Printf("Token: %s\n", tokenStatus)
for {
conn, err := ln.Accept()
if err != nil {
fmt.Printf("Error accepting connection: %v\n", err)
continue
}
fmt.Printf("[+] Connected: %s\n", conn.RemoteAddr().String())
go handleClient(conn)
}
}
func handleClient(conn net.Conn) {
defer conn.Close()
addr := conn.RemoteAddr().String()
printer := lib.NewPrinter(conn)
lang := lib.En
ui := lib.NewUI(printer, lang)
// Telnet negotiation
printer.Send("\xff\xfb\x01\xff\xfb\x03\xff\xfe\x22")
printer.Send(lib.CY + lib.Banner + lib.R)
printer.Send(fmt.Sprintf("\n%s%s%s ", lib.WH, lang["AskName"], lib.R))
username, err := printer.ReadLine()
if err != nil {
return
}
username = strings.TrimSpace(username)
if username == "" {
username = "Anonymous"
}
if len(username) > 20 {
username = username[:20]
}
bbs.Mu.Lock()
bbs.OnlineUsers[addr] = username
bbs.Mu.Unlock()
defer func() {
bbs.Mu.Lock()
delete(bbs.OnlineUsers, addr)
bbs.Mu.Unlock()
}()
printer.Send(fmt.Sprintf("\r\n%s%s%s\r\n", lib.GR, fmt.Sprintf(lang["Greeting"], lib.WH, username, lib.GR), lib.R))
session := lib.NewSession(bbs, printer, username, addr, lang)
for {
printer.Send(ui.MainMenu(username))
choice, err := printer.ReadLine()
if err != nil {
break
}
c := strings.ToUpper(strings.TrimSpace(choice))
switch c {
case "1":
session.ShowUzenopal()
case "2":
session.ShowWikiList("blog", lib.BL, lang["MenuBlog"])
case "3":
session.ShowWikiList("howto", lib.MG, lang["MenuHowto"])
case "4":
session.ShowGames()
case "5":
session.ShowOnline()
case "6":
session.ShowSysinfo()
case "Q":
printer.Send(fmt.Sprintf("\r\n%s%s%s\r\n\r\n", lib.RD, fmt.Sprintf(lang["Goodbye"], username), lib.R))
return
}
}
wiki := content.NewWikiHandler(wikiToken)
cat := content.NewCatalogHandler()
board := content.NewMessageBoard(boardPath)
bbs := engine.New(engine.Config{
Host: "0.0.0.0",
Port: "2323",
Banner: banner,
Lang: engine.En,
})
bbs.Menu(func(m *engine.Menu) {
m.Title("MAIN MENU")
m.Item("1", "Message Board", engine.GR, board.Show)
m.Item("2", "Blog Posts", engine.BL, wiki.List("blog", "Blog Posts", engine.BL))
m.Item("3", "HowTo Guides", engine.MG, wiki.List("howto", "HowTo Guides", engine.MG))
m.Item("4", "Game Catalog", engine.YL, cat.Show)
m.Item("5", "Online Users", engine.CY, content.Online)
m.Item("6", "System Info", engine.GY, content.Sysinfo(board))
m.Item("Q", "Exit", engine.RD, engine.Exit)
})
bbs.Start()
}