urit/internal/menu/bulletins.go
2026-05-02 21:11:50 -04:00

144 lines
4 KiB
Go

// bulletins.go implements the bulletin subsystem.
//
// This replaces BCOM.C from the original TAG-BBS. Bulletins are
// read-only text (or ANSI art) files displayed to users. The sysop
// creates them outside the BBS and registers them in the database.
//
// The original stored bulletins as a linked list of Bulletin_Header
// structs loaded from System.Data at startup. Each had a Filename
// and Location (directory path) plus ReadLow/ReadHigh access levels.
// Reading a bulletin called MenuSend() which opened the file and
// sent it line-by-line with flow control.
//
// Our version stores bulletin metadata in SQLite and reads the actual
// files from the FilePath column. The session's SendFile method
// handles ANSI display and flow control.
package menu
import (
"fmt"
"os"
"strings"
"github.com/urit/urit/internal/models"
"github.com/urit/urit/internal/session"
)
// cmdBulletins is the entry point for the bulletin subsystem.
// Replaces BCom() from BCOM.C.
func cmdBulletins(ctx *Context) error {
bulletins, err := ctx.Store.ListBulletins()
if err != nil {
ctx.Sess.WriteString(" Error loading bulletins.\r\n")
return nil
}
// Filter to those the user can read
var visible []*models.Bulletin
for _, b := range bulletins {
if ctx.User.SecBulletin >= b.ReadLow && ctx.User.SecBulletin <= b.ReadHigh {
visible = append(visible, b)
}
}
if len(visible) == 0 {
ctx.Sess.WriteString(" No bulletins available.\r\n\r\n")
return nil
}
ctx.Sess.NewLine()
for {
ctx.Sess.CheckTime()
ctx.Sess.Color(session.AnsiFgCyan)
ctx.Sess.WriteString("L>ist Q>uit\r\n")
ctx.Sess.Printf("Bulletin to read [1-%d]: ", len(visible))
ctx.Sess.Color(session.AnsiReset)
// Read a line (not a single key) so users can type a number
// — matches original: LineInput("",string,5,120L)
input, err := ctx.Sess.ReadLine("", 5, inputTimeout)
if err != nil {
return nil
}
input = strings.TrimSpace(input)
if input == "" {
continue
}
switch {
case strings.EqualFold(input, "L"):
bulletinList(ctx, visible)
case strings.EqualFold(input, "Q") || input == "@":
return nil
case input == "?" || input == "/":
bulletinList(ctx, visible)
default:
num := parseIntDefault(input, 0)
if num < 1 || num > len(visible) {
ctx.Sess.Printf(" Enter 1-%d, L to list, Q to quit.\r\n", len(visible))
continue
}
bulletinRead(ctx, visible[num-1], num)
}
}
}
// bulletinList shows all accessible bulletins.
// Replaces Bulletin_Listings() from BCOM.C.
func bulletinList(ctx *Context, bulletins []*models.Bulletin) {
ctx.Sess.NewLine()
ctx.Sess.Color(session.AnsiFgGreen, session.AnsiBold)
ctx.Sess.WriteString("Bulletin Listings\r\n")
ctx.Sess.Color(session.AnsiReset)
ctx.Sess.WriteString(strings.Repeat("─", 40) + "\r\n")
for i, b := range bulletins {
ctx.Sess.Printf(" [%d] %s\r\n", i+1, b.Name)
}
ctx.Sess.NewLine()
}
// bulletinRead displays a single bulletin file.
// Replaces Read_Bulletin() from BCOM.C — the original just called
// MenuSend(location + filename) which sent the file with flow control.
func bulletinRead(ctx *Context, b *models.Bulletin, num int) {
ctx.Sess.NewLine()
ctx.Sess.Color(session.AnsiFgBrightWhite)
ctx.Sess.Printf("Bulletin #%d: %s\r\n", num, b.Name)
ctx.Sess.Color(session.AnsiReset)
ctx.Sess.WriteString(strings.Repeat("─", 40) + "\r\n")
// Check if file exists
if _, err := os.Stat(b.FilePath); err != nil {
ctx.Sess.Color(session.AnsiFgRed)
ctx.Sess.Printf(" File not found: %s\r\n", b.FilePath)
ctx.Sess.Color(session.AnsiReset)
return
}
// Read and display the file
data, err := os.ReadFile(b.FilePath)
if err != nil {
ctx.Sess.WriteString(" Error reading bulletin file.\r\n")
return
}
// Display with pagination
content := string(data)
ctx.Sess.Paginate(content, idleTimeout)
ctx.Sess.NewLine()
// Pause after display — classic BBS "press any key"
ctx.Sess.WaitForKey(
fmt.Sprintf("%s--- End of Bulletin #%d ---%s Press any key... ",
session.AnsiFgBrightBlack, num, session.AnsiReset),
idleTimeout,
)
ctx.Sess.NewLine()
}