// 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() }