mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 00:52:40 +00:00
Full codebase audit: 21 security/robustness fixes, 328 tests across 4 packages
Security & economy: - Credit()/Debit() reject non-positive amounts (closes infinite-money exploit) - Lottery ticket purchase uses in-transaction count check (closes TOCTOU race) - Arena bail channel ownership prevents double-close panic - Entry.ID validated before exec.Command in fetch-esteemed - Internal errors no longer leaked to users (forex, esteemed) Robustness: - safeGo() panic recovery added to 17 goroutine launch sites across 14 plugins - db.Close() added and called on shutdown - Miniflux mutex pattern fixed (snapshot-under-lock) - Silently ignored DB/JSON errors now logged (lottery_db) Performance: - Added indexes: idx_arena_runs_user(user_id, status), idx_euro_bal_user(user_id) UX: - Empty !buy args shows usage instead of "No item matching ''" - "Failed to load character" now suggests !adventure Test coverage: - internal/util/parser_test.go (19 tests: XP, levels, progress bar, commands, parsing) - internal/crypto/crypto_test.go (12 tests: encrypt/decrypt, HMAC, ParseKey) - internal/plugin/helpers_test.go (30 tests: formatNumber, calc, lottery, chat perks) - internal/plugin/audit_fixes_test.go (25 tests: safeGo, bail channels, error leaks, overflow) - internal/db/db_test.go (4 tests: Init, Close, schema indexes) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ type AdventurePlugin struct {
|
||||
dmMenuSentAt sync.Map // userID string -> time.Time (last time actionable menu was DM'd)
|
||||
arenaDeadlines sync.Map // userID string -> time.Time (auto-cashout deadline)
|
||||
arenaPending sync.Map // userID string -> int (pending tier number awaiting confirmation)
|
||||
arenaBailCh sync.Map // userID string -> chan struct{} (bail signal for countdown goroutine)
|
||||
shopSessions sync.Map // userID string -> *advShopSession
|
||||
hospitalNudges sync.Map // userID string -> time.Time (when to send nudge)
|
||||
morningHour int
|
||||
@@ -128,7 +129,7 @@ func (p *AdventurePlugin) Init() error {
|
||||
go p.summaryTicker()
|
||||
go p.midnightTicker()
|
||||
go p.eventTicker()
|
||||
go p.arenaAutoCashoutTicker()
|
||||
// Arena auto-cashout ticker removed — replaced by per-session countdown goroutines
|
||||
go p.rivalChallengeTicker()
|
||||
go p.robbieTicker()
|
||||
go p.hospitalNudgeTicker()
|
||||
@@ -164,6 +165,9 @@ func (p *AdventurePlugin) OnReaction(_ ReactionContext) error { return nil }
|
||||
|
||||
func (p *AdventurePlugin) OnMessage(ctx MessageContext) error {
|
||||
// 1. Arena commands (work in rooms and DMs)
|
||||
if p.IsCommand(ctx.Body, "bail") {
|
||||
return p.handleArenaBail(ctx)
|
||||
}
|
||||
if p.IsCommand(ctx.Body, "arena") {
|
||||
return p.dispatchArenaCommand(ctx)
|
||||
}
|
||||
@@ -398,12 +402,16 @@ func (p *AdventurePlugin) handleShopCmd(ctx MessageContext, args string) error {
|
||||
func (p *AdventurePlugin) handleBuyCmd(ctx MessageContext, itemName string) error {
|
||||
char, equip, err := p.ensureCharacter(ctx.Sender)
|
||||
if err != nil {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to load your character.")
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to load your character. Try `!adventure` to create one first.")
|
||||
}
|
||||
if !char.Alive {
|
||||
return p.SendDM(ctx.Sender, "You're dead. Shopping can wait until you've respawned.")
|
||||
}
|
||||
|
||||
if itemName == "" {
|
||||
return p.SendDM(ctx.Sender, "Usage: `!buy <item name>`. Type `!adventure shop` to browse.")
|
||||
}
|
||||
|
||||
slot, def, found := advFindShopItem(itemName)
|
||||
if !found {
|
||||
return p.SendDM(ctx.Sender, fmt.Sprintf("No item matching '%s' found in the shop. Type `!adventure shop` to see what's available.", itemName))
|
||||
@@ -416,7 +424,7 @@ func (p *AdventurePlugin) handleBuyCmd(ctx MessageContext, itemName string) erro
|
||||
func (p *AdventurePlugin) handleSellCmd(ctx MessageContext, args string) error {
|
||||
char, _, err := p.ensureCharacter(ctx.Sender)
|
||||
if err != nil {
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to load your character.")
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, "Failed to load your character. Try `!adventure` to create one first.")
|
||||
}
|
||||
if !char.Alive {
|
||||
return p.SendDM(ctx.Sender, "You're dead. No haggling from beyond the grave.")
|
||||
|
||||
Reference in New Issue
Block a user