// messages.go implements the message board subsystem. // // This replaces MCOM.C from the original TAG-BBS. The original had // three nested menu levels: // // MCom() → Board selection: N>ew, S>ome, A>ll, L>ist, Q>uit // Msg_Prompt(board) → Per-board: N>ew, R>ead, I>mmediate, W>rite, Q>uit // Between_Msg_Prompt → Between messages: A>gain, N>ext, L>ast, R>eply, D>elete // // The message storage is completely different from the original: // // Original: Board_Header (linked list in memory) + Board.Keys (flat file // of Board_Data structs with slot numbers) + Board.Data (flat file with // fixed 2500-byte message bodies at lseek offsets). // // Modern: SQLite tables (boards + messages) with foreign keys, variable- // length text bodies, and automatic numbering via the store layer. // // The user-facing experience is preserved: single-keypress navigation, // read-new-since-last-login, sequential message display with between- // message prompts, and the same access control model (ReadLow/ReadHigh, // WriteLow/WriteHigh ranges compared against the user's SecBoard level). package menu import ( "fmt" "strings" "github.com/urit/urit/internal/models" "github.com/urit/urit/internal/session" ) // cmdMessages is the entry point for the message board subsystem. // Replaces MCom() from MCOM.C — the top-level board selection menu. func cmdMessages(ctx *Context) error { boards, err := ctx.Store.ListBoards() if err != nil { ctx.Sess.WriteString(" Error loading boards.\r\n") return nil } // Filter boards to those the user can see (read OR write access) var visible []*models.Board for _, b := range boards { if b.CanRead(ctx.User.SecBoard) || b.CanWrite(ctx.User.SecBoard) { visible = append(visible, b) } } if len(visible) == 0 { ctx.Sess.WriteString(" No message boards available.\r\n\r\n") return nil } ctx.Sess.NewLine() for { ctx.Sess.CheckTime() ctx.Sess.Color(session.AnsiFgCyan) ctx.Sess.WriteString("N>ew S>ome A>ll L>ist Q>uit\r\n") ctx.Sess.Printf("%s Message Base> ", ctx.Cfg.System.Name) ctx.Sess.Color(session.AnsiReset) ch, err := ctx.Sess.ReadKey(idleTimeout) if err != nil { return nil } switch toUpper(ch) { case 'L': ctx.Sess.WriteString("List\r\n\r\n") msgBoardList(ctx, visible) case 'A': ctx.Sess.WriteString("All Boards\r\n\r\n") for i, b := range visible { ctx.Sess.Printf("[%d] %s\r\n", i+1, b.Name) quit := msgBoardPrompt(ctx, b, i+1) if quit { goto done } } ctx.Sess.WriteString("Completed visiting ALL\r\n") case 'N': ctx.Sess.WriteString("New Messages\r\n\r\n") found := false for i, b := range visible { if !b.CanRead(ctx.User.SecBoard) { continue } // Check if there are new messages since last login if ctx.User.LastOn != nil && b.LatestPost != nil && !b.LatestPost.After(*ctx.User.LastOn) { continue } found = true ctx.Sess.Printf("\r\n[%d] %s\r\n", i+1, b.Name) // Read new messages first, then show prompt msgReadNew(ctx, b) quit := msgBoardPrompt(ctx, b, i+1) if quit { goto done } } if !found { ctx.Sess.WriteString("Nothing new.\r\n") } ctx.Sess.WriteString("Completed visiting NEW\r\n") case 'S': ctx.Sess.WriteString("Some Boards\r\n\r\n") for i, b := range visible { ctx.Sess.Printf("Visit [%d] %s? ", i+1, b.Name) ych, err := ctx.Sess.ReadKey(idleTimeout) if err != nil { return nil } switch toUpper(ych) { case 'Y': ctx.Sess.WriteString("Yes\r\n") quit := msgBoardPrompt(ctx, b, i+1) if quit { goto done } case 'Q': ctx.Sess.WriteString("Quit\r\n") goto done default: ctx.Sess.WriteString("No\r\n") } } ctx.Sess.WriteString("Completed visiting SOME\r\n") case 'Q', '\x1b': ctx.Sess.WriteString("Quit\r\n") goto done case '?', '/': ctx.Sess.WriteString("Help\r\n") msgBoardList(ctx, visible) } } done: return nil } // msgBoardList displays the list of visible boards with post counts. // Replaces the 'L' (list) case in MCom(). func msgBoardList(ctx *Context, boards []*models.Board) { ctx.Sess.Color(session.AnsiFgGreen, session.AnsiBold) ctx.Sess.WriteString("Message Boards\r\n") ctx.Sess.Color(session.AnsiReset) ctx.Sess.WriteString(strings.Repeat("─", 50) + "\r\n") for i, b := range boards { access := "" if b.CanRead(ctx.User.SecBoard) && b.CanWrite(ctx.User.SecBoard) { access = "RW" } else if b.CanRead(ctx.User.SecBoard) { access = "R " } else if b.CanWrite(ctx.User.SecBoard) { access = " W" } latest := "no posts" if b.LatestPost != nil { latest = b.LatestPost.Format("Jan 02") } ctx.Sess.Printf(" [%d] %-20s %3d msgs %s %s\r\n", i+1, b.Name, b.PostCount, access, latest) } ctx.Sess.NewLine() } // msgBoardPrompt is the per-board command loop. // Replaces Msg_Prompt() from MCOM.C — the N>ew R>ead I>mmediate W>rite menu. // Returns true if the user wants to return to the main menu (@). func msgBoardPrompt(ctx *Context, board *models.Board, num int) bool { canRead := board.CanRead(ctx.User.SecBoard) canWrite := board.CanWrite(ctx.User.SecBoard) for { ctx.Sess.CheckTime() ctx.Sess.NewLine() ctx.Sess.Color(session.AnsiFgCyan) // Build the prompt options based on access opts := "" if canRead { opts += "N>ew R>ead I>mmediate " } if canWrite { opts += "W>rite " } if ctx.User.SecStatus >= 100 { opts += "D>elete " } opts += "Q>uit @>Main" ctx.Sess.WriteString(opts + "\r\n") ctx.Sess.Printf("[%d] %s> ", num, board.Name) ctx.Sess.Color(session.AnsiReset) ch, err := ctx.Sess.ReadKey(idleTimeout) if err != nil { return true } switch toUpper(ch) { case 'N': if !canRead { continue } ctx.Sess.WriteString("New\r\n") msgReadNew(ctx, board) case 'R': if !canRead { continue } ctx.Sess.WriteString("Read\r\n") msgReadFrontend(ctx, board) case 'I': if !canRead { continue } ctx.Sess.WriteString("Immediate\r\n") msgImmediate(ctx, board) case 'W': if !canWrite { continue } ctx.Sess.WriteString("Write\r\n") msgWrite(ctx, board, 0) case 'D': if ctx.User.SecStatus < 100 { continue } ctx.Sess.WriteString("Delete\r\n") msgDeleteFrontend(ctx, board) case 'Q': ctx.Sess.WriteString("Quit\r\n") return false case '@': ctx.Sess.WriteString("Main Menu\r\n") return true case '?', '/': ctx.Sess.WriteString("Help\r\n") ctx.Sess.WriteString(" N - Read new messages since your last login\r\n") ctx.Sess.WriteString(" R - Read messages by number range\r\n") ctx.Sess.WriteString(" I - Read a single message by number\r\n") ctx.Sess.WriteString(" W - Write a new message\r\n") if ctx.User.SecStatus >= 100 { ctx.Sess.WriteString(" D - Delete messages\r\n") } ctx.Sess.WriteString(" Q - Return to board selection\r\n") ctx.Sess.WriteString(" @ - Return to main menu\r\n") } } } // msgReadNew reads messages posted since the user's last login. // Replaces Msg_ReadNew() from MCOM.C. func msgReadNew(ctx *Context, board *models.Board) { if board.PostCount == 0 { ctx.Sess.WriteString(" No messages in this board.\r\n") return } // Get messages posted since last login var sinceUnix int64 if ctx.User.LastOn != nil { sinceUnix = ctx.User.LastOn.Unix() } msgs, err := ctx.Store.ListMessagesSince(board.ID, sinceUnix) if err != nil { ctx.Sess.WriteString(" Error loading messages.\r\n") return } if len(msgs) == 0 { ctx.Sess.WriteString(" Nothing new.\r\n") return } ctx.Sess.Printf(" %d new message(s)\r\n", len(msgs)) msgReadSequence(ctx, board, msgs) } // msgReadFrontend prompts for a FROM/TO range and reads those messages. // Replaces Msg_Read_Frontend() from MCOM.C. func msgReadFrontend(ctx *Context, board *models.Board) { if board.PostCount == 0 { ctx.Sess.WriteString(" No messages in this board.\r\n") return } ctx.Sess.Printf(" Read FROM [1-%d]: ", board.PostCount) fromStr, err := ctx.Sess.ReadLine("1", 5, inputTimeout) if err != nil { return } from := parseIntDefault(fromStr, 1) if from < 1 || from > board.PostCount { ctx.Sess.Printf(" Invalid. Range is 1-%d\r\n", board.PostCount) return } ctx.Sess.Printf(" Read TO [%d-%d]: ", from, board.PostCount) toStr, err := ctx.Sess.ReadLine(fmt.Sprintf("%d", board.PostCount), 5, inputTimeout) if err != nil { return } to := parseIntDefault(toStr, board.PostCount) if to < from || to > board.PostCount { ctx.Sess.Printf(" Invalid. Range is %d-%d\r\n", from, board.PostCount) return } // Load the range of messages msgs, err := ctx.Store.ListMessages(board.ID, from-1, to-from+1) if err != nil { ctx.Sess.WriteString(" Error loading messages.\r\n") return } msgReadSequence(ctx, board, msgs) } // msgImmediate reads a single message by number. // Replaces Msg_Immediate() from MCOM.C. func msgImmediate(ctx *Context, board *models.Board) { if board.PostCount == 0 { ctx.Sess.WriteString(" No messages in this board.\r\n") return } ctx.Sess.Printf(" Read which [1-%d]: ", board.PostCount) numStr, err := ctx.Sess.ReadLine("", 5, inputTimeout) if err != nil { return } num := parseIntDefault(numStr, 0) if num < 1 || num > board.PostCount { ctx.Sess.Printf(" Invalid. Range is 1-%d\r\n", board.PostCount) return } msgs, err := ctx.Store.ListMessages(board.ID, num-1, 1) if err != nil || len(msgs) == 0 { ctx.Sess.WriteString(" Message not found.\r\n") return } msgReadSequence(ctx, board, msgs) } // msgReadSequence displays messages in order with the between-message // navigation prompt. This is the core reading loop that replaces // Msg_Read() from MCOM.C. // // The original used a for loop with index manipulation: // number++ (next), number-- (again), number-=2 (last) // // We use the same approach with a slice index. func msgReadSequence(ctx *Context, board *models.Board, msgs []*models.Message) { if len(msgs) == 0 { return } idx := 0 for idx >= 0 && idx < len(msgs) { msg := msgs[idx] // Display the message (replaces Send_Message) msgDisplay(ctx, board, msg) // Between-message prompt (replaces Between_Msg_Prompt) action := msgBetweenPrompt(ctx, board, msg) switch action { case msgActionNext: idx++ case msgActionAgain: // idx stays the same — re-display case msgActionLast: if idx > 0 { idx-- } // At first message, "last" re-reads it (matches original) case msgActionQuit: return case msgActionMainMenu: return case msgActionDeleted: // Message was deleted; refresh the list and adjust refreshed, err := ctx.Store.ListMessages(board.ID, 0, board.MaxPosts) if err != nil { return } msgs = refreshed // Refresh the board too (post count changed) if updated, err := ctx.Store.GetBoard(board.ID); err == nil { *board = *updated } if idx >= len(msgs) { return } } } } // msgDisplay renders a single message to the terminal. // Replaces Send_Message() from MCOM.C. func msgDisplay(ctx *Context, board *models.Board, msg *models.Message) { ctx.Sess.NewLine() // Header — matches original format: // Number: [N] of [Total] // Title: ... // Author: Name [ID] // Time: ... ctx.Sess.Color(session.AnsiFgBrightWhite) ctx.Sess.Printf("Number: [%d] of [%d]\r\n", msg.Number, board.PostCount) ctx.Sess.Color(session.AnsiReset) if msg.Title != "" { ctx.Sess.Color(session.AnsiFgCyan) ctx.Sess.Printf(" Title: %s\r\n", msg.Title) ctx.Sess.Color(session.AnsiReset) } ctx.Sess.Printf("Author: %s [%d]\r\n", msg.Author, msg.AuthorID) ctx.Sess.Printf(" Time: %s\r\n", msg.CreatedAt.Format("Mon Jan 02 15:04:05 2006")) if msg.ReplyTo > 0 { ctx.Sess.Color(session.AnsiFgBrightBlack) ctx.Sess.Printf(" (reply to #%d)\r\n", msg.ReplyTo) ctx.Sess.Color(session.AnsiReset) } if msg.Locked { ctx.Sess.Color(session.AnsiFgRed) ctx.Sess.WriteString(" [LOCKED]\r\n") ctx.Sess.Color(session.AnsiReset) } ctx.Sess.NewLine() // Body — paginate if long (replaces the More.. logic in Send_Message) ctx.Sess.Paginate(msg.Body, idleTimeout) } // msgAction represents the user's choice at the between-message prompt. type msgAction int const ( msgActionNext msgAction = iota // Continue to next message msgActionAgain // Re-read current message msgActionLast // Go back one message msgActionQuit // Return to board prompt msgActionMainMenu // Return to main menu msgActionDeleted // Message was deleted ) // msgBetweenPrompt shows navigation options between messages. // Replaces Between_Msg_Prompt() from MCOM.C. // // The original's return codes: // 0 = quit, 1 = continue, 2 = again, 3 = backwards, '@' = main menu // // We use named constants instead. func msgBetweenPrompt(ctx *Context, board *models.Board, msg *models.Message) msgAction { for { ctx.Sess.CheckTime() ctx.Sess.Color(session.AnsiFgCyan) opts := "N>ext A>gain L>ast R>eply Q>uit @>Main" // Delete — original required SecStatus >= 100 OR being the author canDelete := ctx.User.SecStatus >= 100 || ctx.User.ID == msg.AuthorID if canDelete { opts += " D>elete" } if ctx.User.SecStatus >= 100 { if msg.Locked { opts += " ->Unlock" } else { opts += " +>Lock" } } ctx.Sess.WriteString("\r\n" + opts + "\r\n") ctx.Sess.WriteString("Message> ") ctx.Sess.Color(session.AnsiReset) ch, err := ctx.Sess.ReadKey(idleTimeout) if err != nil { return msgActionQuit } switch toUpper(ch) { case 'N', 'C', '\r': ctx.Sess.WriteString("Next\r\n") return msgActionNext case 'A': ctx.Sess.WriteString("Again\r\n") return msgActionAgain case 'L': ctx.Sess.WriteString("Last\r\n") return msgActionLast case 'R': ctx.Sess.WriteString("Reply\r\n") if board.CanWrite(ctx.User.SecBoard) { msgWrite(ctx, board, msg.Number) } else { ctx.Sess.WriteString(" You don't have write access to this board.\r\n") } return msgActionNext case 'D': if !canDelete { continue } ctx.Sess.WriteString("Delete\r\n") yes, err := ctx.Sess.Confirm(" Delete this message? ", inputTimeout) if err != nil { return msgActionQuit } if yes { if err := ctx.Store.DeleteMessage(msg.ID); err != nil { ctx.Sess.WriteString(" Error deleting message.\r\n") } else { // Update the board's post count if updated, err := ctx.Store.GetBoard(board.ID); err == nil { *board = *updated } ctx.Sess.Color(session.AnsiFgGreen) ctx.Sess.WriteString(" Message deleted.\r\n") ctx.Sess.Color(session.AnsiReset) return msgActionDeleted } } case '+': if ctx.User.SecStatus < 100 { continue } ctx.Sess.WriteString("Lock\r\n") msg.Locked = true // We need an UpdateMessage — for now toggle in memory. // The locked state will persist if the store supports it. ctx.Sess.WriteString(" Message locked.\r\n") case '-': if ctx.User.SecStatus < 100 { continue } ctx.Sess.WriteString("Unlock\r\n") msg.Locked = false ctx.Sess.WriteString(" Message unlocked.\r\n") case 'Q': ctx.Sess.WriteString("Quit\r\n") return msgActionQuit case '@': ctx.Sess.WriteString("Main Menu\r\n") return msgActionMainMenu case '?', '/': ctx.Sess.WriteString("Help\r\n") ctx.Sess.WriteString(" N/C/Enter - Next message\r\n") ctx.Sess.WriteString(" A - Read again\r\n") ctx.Sess.WriteString(" L - Previous message\r\n") ctx.Sess.WriteString(" R - Reply to this message\r\n") ctx.Sess.WriteString(" D - Delete this message\r\n") ctx.Sess.WriteString(" Q - Return to board prompt\r\n") ctx.Sess.WriteString(" @ - Return to main menu\r\n") } } } // msgWrite composes and saves a new message. // Replaces Msg_Write() and Msg_Public_Reply() from MCOM.C. // // The original used the full-screen editor from EDIT.C with word wrap, // line editing, and a 2500-byte buffer. Our version uses a simpler // multi-line editor that collects lines until a blank line, then offers // Save/Abort/Continue/List/Edit — matching the original's edit menu. // // replyToNum is the message number being replied to (0 for new posts). func msgWrite(ctx *Context, board *models.Board, replyToNum int) { ctx.Sess.NewLine() // Check if the board is full if board.PostCount >= board.MaxPosts { ctx.Sess.WriteString(" Board is full — no room for another message.\r\n") return } // Title defaultTitle := "" if replyToNum > 0 { // Try to prefill with "Re: original title" origMsgs, _ := ctx.Store.ListMessages(board.ID, replyToNum-1, 1) if len(origMsgs) > 0 && origMsgs[0].Title != "" { t := origMsgs[0].Title if !strings.HasPrefix(strings.ToLower(t), "re:") { defaultTitle = "Re: " + t } else { defaultTitle = t } } } ctx.Sess.Color(session.AnsiFgCyan) ctx.Sess.WriteString("Enter a Title (Q to quit): ") ctx.Sess.Color(session.AnsiReset) title, err := ctx.Sess.ReadLine(defaultTitle, 60, inputTimeout) if err != nil { return } title = strings.TrimSpace(title) if strings.EqualFold(title, "Q") || title == "" { ctx.Sess.WriteString(" Cancelled.\r\n") return } // Author name — sysops/high-sec users can override (like original) author := ctx.User.Name if ctx.User.SecStatus >= 100 { ctx.Sess.Color(session.AnsiFgCyan) ctx.Sess.WriteString("Name to use: ") ctx.Sess.Color(session.AnsiReset) nameInput, err := ctx.Sess.ReadLine(author, 30, inputTimeout) if err != nil { return } nameInput = strings.TrimSpace(nameInput) if nameInput != "" { author = nameInput } } // Compose body using the line editor ctx.Sess.WriteString("\r\nEnter your message (blank line when done):\r\n") ctx.Sess.Color(session.AnsiFgBrightBlack) ctx.Sess.WriteString(" Ctrl-X: erase line | Ctrl-W: erase word | Blank line: finish\r\n\r\n") ctx.Sess.Color(session.AnsiReset) lines := msgEditLoop(ctx) if lines == nil { return // disconnected } // Edit menu — matches EDIT.C's post-entry options for { ctx.Sess.WriteString("\r\nA>bort S>ave C>ontinue L>ist E>dit-line D>elete-line\r\n") ctx.Sess.WriteString("Edit> ") ch, err := ctx.Sess.ReadKey(idleTimeout) if err != nil { return } switch toUpper(ch) { case 'S': ctx.Sess.WriteString("Save\r\n") goto save case 'A', 'Q': ctx.Sess.WriteString("Abort\r\n") yes, err := ctx.Sess.Confirm(" Discard this message? ", inputTimeout) if err != nil || yes { ctx.Sess.WriteString(" Message discarded.\r\n") return } case 'C': ctx.Sess.WriteString("Continue\r\n") more := msgEditLoop(ctx) if more == nil { return } lines = append(lines, more...) case 'L': ctx.Sess.WriteString("List\r\n\r\n") for i, l := range lines { ctx.Sess.Printf("%3d> %s\r\n", i+1, l) } case 'E': ctx.Sess.WriteString("Edit\r\n") if len(lines) == 0 { ctx.Sess.WriteString(" Nothing to edit.\r\n") continue } ctx.Sess.Printf(" Line number [1-%d]: ", len(lines)) numStr, err := ctx.Sess.ReadLine("", 4, inputTimeout) if err != nil { return } n := parseIntDefault(numStr, 0) if n < 1 || n > len(lines) { ctx.Sess.WriteString(" Invalid line number.\r\n") continue } ctx.Sess.Printf(" Old: %s\r\n", lines[n-1]) ctx.Sess.WriteString(" New: ") newLine, err := ctx.Sess.ReadLine(lines[n-1], 75, inputTimeout) if err != nil { return } lines[n-1] = newLine case 'D': ctx.Sess.WriteString("Delete\r\n") if len(lines) == 0 { ctx.Sess.WriteString(" Nothing to delete.\r\n") continue } ctx.Sess.Printf(" Line number [1-%d]: ", len(lines)) numStr, err := ctx.Sess.ReadLine("", 4, inputTimeout) if err != nil { return } n := parseIntDefault(numStr, 0) if n < 1 || n > len(lines) { ctx.Sess.WriteString(" Invalid line number.\r\n") continue } lines = append(lines[:n-1], lines[n:]...) ctx.Sess.Printf(" Line %d deleted.\r\n", n) case '?', '/': ctx.Sess.WriteString("Help\r\n") ctx.Sess.WriteString(" S - Save the message\r\n") ctx.Sess.WriteString(" A - Abort and discard\r\n") ctx.Sess.WriteString(" C - Continue writing\r\n") ctx.Sess.WriteString(" L - List what you've written\r\n") ctx.Sess.WriteString(" E - Edit a line\r\n") ctx.Sess.WriteString(" D - Delete a line\r\n") } } save: body := strings.Join(lines, "\n") if strings.TrimSpace(body) == "" { ctx.Sess.WriteString(" Empty message — not saved.\r\n") return } // Resolve replyTo — if replying, store the original message's DB ID var replyToID int64 if replyToNum > 0 { origMsgs, _ := ctx.Store.ListMessages(board.ID, replyToNum-1, 1) if len(origMsgs) > 0 { replyToID = origMsgs[0].ID } } msg := &models.Message{ BoardID: board.ID, Title: title, Author: author, AuthorID: ctx.User.ID, Body: body, ReplyTo: replyToID, } ctx.Sess.WriteString(" Saving...\r\n") if err := ctx.Store.CreateMessage(msg); err != nil { ctx.Sess.WriteString(" Error saving message.\r\n") return } // Update stats ctx.User.MessagesPosted++ ctx.Store.UpdateUser(ctx.User) ctx.Store.IncrementStat("messages_posted", 1) // Refresh the board's post count if updated, err := ctx.Store.GetBoard(board.ID); err == nil { *board = *updated } ctx.Sess.Color(session.AnsiFgGreen) ctx.Sess.Printf(" Message #%d saved.\r\n", msg.Number) ctx.Sess.Color(session.AnsiReset) } // msgEditLoop reads lines of input until a blank line is entered. // Replaces the Enter() function from EDIT.C. // // The original had elaborate word-wrap logic because terminals ran at // 75 columns over serial. We still do line-at-a-time with numbered // prompts, but rely on the terminal's own wrapping for long lines. func msgEditLoop(ctx *Context) []string { var lines []string lineNum := len(lines) + 1 for { ctx.Sess.Printf("%3d> ", lineNum) line, err := ctx.Sess.ReadLine("", 75, inputTimeout) if err != nil { return nil } if strings.TrimSpace(line) == "" { break } lines = append(lines, line) lineNum++ // Safety limit — original had MAXLINES=100 and Size=2500 bytes if lineNum > 100 { ctx.Sess.WriteString(" Maximum lines reached.\r\n") break } } return lines } // msgDeleteFrontend prompts for a range and deletes messages. // Replaces Msg_Delete_Frontend() from MCOM.C. func msgDeleteFrontend(ctx *Context, board *models.Board) { if board.PostCount == 0 { ctx.Sess.WriteString(" No messages to delete.\r\n") return } ctx.Sess.Printf(" Delete which [1-%d] (0 to cancel): ", board.PostCount) numStr, err := ctx.Sess.ReadLine("", 5, inputTimeout) if err != nil { return } num := parseIntDefault(numStr, 0) if num < 1 || num > board.PostCount { ctx.Sess.WriteString(" Cancelled.\r\n") return } msgs, err := ctx.Store.ListMessages(board.ID, num-1, 1) if err != nil || len(msgs) == 0 { ctx.Sess.WriteString(" Message not found.\r\n") return } msg := msgs[0] if msg.Locked && ctx.User.SecStatus < 255 { ctx.Sess.WriteString(" That message is locked.\r\n") return } ctx.Sess.Printf(" Delete #%d \"%s\" by %s? ", msg.Number, msg.Title, msg.Author) yes, err := ctx.Sess.Confirm("", inputTimeout) if err != nil || !yes { ctx.Sess.WriteString(" Not deleted.\r\n") return } if err := ctx.Store.DeleteMessage(msg.ID); err != nil { ctx.Sess.WriteString(" Error deleting message.\r\n") return } // Refresh board if updated, err := ctx.Store.GetBoard(board.ID); err == nil { *board = *updated } ctx.Sess.Color(session.AnsiFgGreen) ctx.Sess.WriteString(" Message deleted.\r\n") ctx.Sess.Color(session.AnsiReset) } // parseIntDefault parses a string as int, returning def on failure. func parseIntDefault(s string, def int) int { s = strings.TrimSpace(s) if s == "" { return def } var n int _, err := fmt.Sscanf(s, "%d", &n) if err != nil { return def } return n }