mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 16:42:41 +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:
@@ -8,9 +8,13 @@ import (
|
||||
|
||||
// ── Arena Tier Menu ─────────────────────────────────────────────────────────
|
||||
|
||||
func renderArenaTierMenu(char *AdventureCharacter, stats *ArenaPersonalStats) string {
|
||||
func renderArenaStreakEntry(char *AdventureCharacter, stats *ArenaPersonalStats, tier *ArenaTier, firstMonster *ArenaMonster) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("⚔️ **THE ARENA**\n\n")
|
||||
b.WriteString("The Arena is a streak. You start at Tier 1 and fight your way down.\n")
|
||||
b.WriteString("After each tier, you have 30 seconds to bail or you auto-advance.\n")
|
||||
b.WriteString("Death forfeits all accumulated rewards.\n\n")
|
||||
|
||||
b.WriteString(fmt.Sprintf("Combat Level: %d\n\n", char.CombatLevel))
|
||||
|
||||
for i := range arenaTiers {
|
||||
@@ -18,12 +22,13 @@ func renderArenaTierMenu(char *AdventureCharacter, stats *ArenaPersonalStats) st
|
||||
eligible := char.CombatLevel >= t.MinLevel
|
||||
icon := "🔒"
|
||||
if eligible {
|
||||
icon = "⬚" // eligible but not cleared
|
||||
icon = "⬚"
|
||||
}
|
||||
if stats != nil && stats.HighestTier >= t.Number {
|
||||
icon = "✅" // cleared
|
||||
icon = "✅"
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("%s **Tier %d — %s** (Lv.%d+)\n", icon, t.Number, t.Name, t.MinLevel))
|
||||
mult := arenaStreakEuroMultiplier[t.Number]
|
||||
b.WriteString(fmt.Sprintf("%s **Tier %d — %s** (Lv.%d+) — %.1f× euros\n", icon, t.Number, t.Name, t.MinLevel, mult))
|
||||
}
|
||||
|
||||
if stats != nil && stats.TotalRuns > 0 {
|
||||
@@ -34,9 +39,11 @@ func renderArenaTierMenu(char *AdventureCharacter, stats *ArenaPersonalStats) st
|
||||
b.WriteString("\n⛑️ _Today's helmets are provided by Brim & Battle — celebrated makers of baseball hats — and proud sponsors of the Arena. ")
|
||||
b.WriteString("Their new VeriFort line of field-evaluated combat headgear represents an exciting expansion beyond their area of expertise. ")
|
||||
b.WriteString("Winners may receive a complimentary sample. Brim & Battle thanks you for your participation in their ongoing verification process._\n\n")
|
||||
b.WriteString("`!arena tier <1-5>` — Enter a tier\n")
|
||||
b.WriteString("`!arena stats` — Your arena stats\n")
|
||||
b.WriteString("`!arena leaderboard` — Top arena players\n")
|
||||
|
||||
b.WriteString(fmt.Sprintf("**Round 1 opponent: %s**\n", firstMonster.Name))
|
||||
b.WriteString(fmt.Sprintf("_%s_\n\n", firstMonster.Flavor))
|
||||
b.WriteString("`!arena fight` — Enter and fight Round 1\n")
|
||||
b.WriteString("`!arena cancel` — Back out")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
@@ -48,60 +55,23 @@ func renderArenaRoundStart(tier *ArenaTier, round int, monster *ArenaMonster, ru
|
||||
b.WriteString(fmt.Sprintf("**%s**\n", monster.Name))
|
||||
b.WriteString(fmt.Sprintf("_%s_\n\n", monster.Flavor))
|
||||
|
||||
if run.Earnings > 0 {
|
||||
b.WriteString(fmt.Sprintf("Run earnings: €%d (at risk)\n\n", run.Earnings))
|
||||
sessionTotal := run.Earnings + run.TierEarnings
|
||||
if sessionTotal > 0 {
|
||||
b.WriteString(fmt.Sprintf("Session earnings: €%d (at risk)\n\n", sessionTotal))
|
||||
}
|
||||
|
||||
b.WriteString("`!arena fight` — Face this opponent\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ── Survival ────────────────────────────────────────────────────────────────
|
||||
|
||||
func renderArenaSurvival(tier *ArenaTier, round int, monster *ArenaMonster, reward int64, xp int, totalEarnings int64) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(fmt.Sprintf("✅ **%s defeated.**\n\n", monster.Name))
|
||||
b.WriteString(fmt.Sprintf("Round reward: €%d\n", reward))
|
||||
b.WriteString(fmt.Sprintf("Battle XP: +%d\n", xp))
|
||||
b.WriteString(fmt.Sprintf("Run total: €%d\n", totalEarnings))
|
||||
return b.String()
|
||||
}
|
||||
// renderArenaSurvival removed — survival text is built by renderArenaCombatLog + inline formatting.
|
||||
|
||||
// ── Tier Complete (Transition Prompt) ───────────────────────────────────────
|
||||
|
||||
func renderArenaTierComplete(tier *ArenaTier, completionBonus int64, totalEarnings int64) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(fmt.Sprintf("🏆 **Tier %d — %s cleared!**\n\n", tier.Number, tier.Name))
|
||||
b.WriteString(fmt.Sprintf("Completion bonus: €%d\n", completionBonus))
|
||||
b.WriteString(fmt.Sprintf("Run total: €%d\n\n", totalEarnings))
|
||||
// renderArenaTierComplete is no longer used — tier complete text is built inline
|
||||
// in resolveArenaSurvival and the countdown DM handles the opt-out flow.
|
||||
|
||||
if tier.Number < 5 {
|
||||
nextTier := arenaGetTier(tier.Number + 1)
|
||||
b.WriteString(fmt.Sprintf("Descend to Tier %d — %s? Your earnings are at risk.\n\n", nextTier.Number, nextTier.Name))
|
||||
b.WriteString(fmt.Sprintf("`!arena descend` — Enter Tier %d (earnings carry over, still at risk)\n", nextTier.Number))
|
||||
b.WriteString(fmt.Sprintf("`!arena cashout` — Take your €%d and leave\n\n", totalEarnings))
|
||||
b.WriteString("_You have 10 minutes to decide. After that, GogoBee collects on your behalf._")
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ── Tier 5 Complete ─────────────────────────────────────────────────────────
|
||||
|
||||
func renderArenaTier5Complete(totalEarnings int64, startTier int) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("🏆🏆🏆 **THE ARENA HAS BEEN CONQUERED.**\n\n")
|
||||
b.WriteString("That Which Has Always Been has fallen. The machine has logged a loss.\n")
|
||||
b.WriteString("The crowd is silent. Not out of respect — out of disbelief.\n\n")
|
||||
b.WriteString(fmt.Sprintf("**Total earnings: €%d**\n\n", totalEarnings))
|
||||
b.WriteString("Your euros have been credited. Your name has been etched. The Arena remembers.")
|
||||
|
||||
if startTier == 1 {
|
||||
b.WriteString("\n\n_All the way down. From Tier 1 to Tier 5 in a single run. Statistically impossible. Empirically: you._")
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
// renderArenaTier5Complete removed — T5 completion is part of arenaCompleteSession payout summary.
|
||||
|
||||
// ── Death ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -117,25 +87,7 @@ func renderArenaDeath(tier *ArenaTier, round int, monster *ArenaMonster, lostEar
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ── Cashout ─────────────────────────────────────────────────────────────────
|
||||
|
||||
func renderArenaCashout(totalEarnings int64, lastTier int) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(fmt.Sprintf("💰 **Cashed out: €%d**\n\n", totalEarnings))
|
||||
b.WriteString(fmt.Sprintf("You cleared through Tier %d and lived to spend it. ", lastTier))
|
||||
b.WriteString("Wisdom or cowardice — the euros don't care which.")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ── Auto-Cashout ────────────────────────────────────────────────────────────
|
||||
|
||||
func renderArenaAutoCashout(totalEarnings int64) string {
|
||||
return fmt.Sprintf(
|
||||
"⏰ **Auto-cashout: €%d**\n\n"+
|
||||
"You took too long to decide. GogoBee collected your winnings on your behalf "+
|
||||
"and is annoyed about it. The money is in your account. You're welcome.",
|
||||
totalEarnings)
|
||||
}
|
||||
// renderArenaCashout and renderArenaAutoCashout removed — payout handled by arenaCompleteSession.
|
||||
|
||||
// ── Status ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -146,9 +98,11 @@ func renderArenaStatus(run *ArenaRun, char *AdventureCharacter) string {
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("⚔️ **Arena Run Status**\n\n")
|
||||
b.WriteString("⚔️ **Arena Streak Status**\n\n")
|
||||
b.WriteString(fmt.Sprintf("Tier: %d — %s\n", tier.Number, tier.Name))
|
||||
|
||||
sessionTotal := run.Earnings + run.TierEarnings
|
||||
|
||||
switch run.Status {
|
||||
case "active":
|
||||
monster := arenaGetMonster(run.Tier, run.Round)
|
||||
@@ -156,14 +110,14 @@ func renderArenaStatus(run *ArenaRun, char *AdventureCharacter) string {
|
||||
if monster != nil {
|
||||
b.WriteString(fmt.Sprintf("Opponent: %s\n", monster.Name))
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("Earnings: €%d (at risk)\n", run.Earnings))
|
||||
b.WriteString(fmt.Sprintf("Session total: €%d (at risk)\n", sessionTotal))
|
||||
b.WriteString(fmt.Sprintf("Rounds survived: %d\n", run.RoundsSurvived))
|
||||
b.WriteString("\n`!arena fight` to continue")
|
||||
case "awaiting":
|
||||
b.WriteString(fmt.Sprintf("Tier %d cleared — awaiting decision\n", run.Tier))
|
||||
b.WriteString(fmt.Sprintf("Earnings: €%d (at risk)\n", run.Earnings))
|
||||
b.WriteString(fmt.Sprintf("Tier %d cleared — advancing shortly\n", run.Tier))
|
||||
b.WriteString(fmt.Sprintf("Session total: €%d (at risk)\n", sessionTotal))
|
||||
b.WriteString(fmt.Sprintf("Rounds survived: %d\n", run.RoundsSurvived))
|
||||
b.WriteString("\n`!arena descend` or `!arena cashout`")
|
||||
b.WriteString("\n`!bail` to collect your rewards")
|
||||
}
|
||||
|
||||
return b.String()
|
||||
@@ -207,19 +161,6 @@ func renderArenaLeaderboard(entries []ArenaLeaderboardEntry) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ── Tier Entry Confirmation ──────────────────────────────────────────────────
|
||||
|
||||
func renderArenaTierConfirm(tier *ArenaTier, firstMonster *ArenaMonster) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(fmt.Sprintf("⚔️ **Tier %d — %s**\n\n", tier.Number, tier.Name))
|
||||
b.WriteString(fmt.Sprintf("4 rounds. You cannot leave mid-tier. Death forfeits all earnings.\n\n"))
|
||||
b.WriteString(fmt.Sprintf("Round 1 opponent: **%s**\n", firstMonster.Name))
|
||||
b.WriteString(fmt.Sprintf("_%s_\n\n", firstMonster.Flavor))
|
||||
b.WriteString("`!arena fight` — Enter and fight Round 1\n")
|
||||
b.WriteString("`!arena cancel` — Back out")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ── Personal Stats ──────────────────────────────────────────────────────────
|
||||
|
||||
type ArenaPersonalStats struct {
|
||||
@@ -305,12 +246,11 @@ func renderArenaAlreadyInRun(run *ArenaRun) string {
|
||||
switch run.Status {
|
||||
case "awaiting":
|
||||
return fmt.Sprintf(
|
||||
"You have a pending arena decision. Tier %d cleared, €%d at risk.\n\n"+
|
||||
"`!arena descend` or `!arena cashout`",
|
||||
"Tier %d cleared, €%d accumulated. Waiting for next tier or `!bail` to collect.\n",
|
||||
run.Tier, run.Earnings)
|
||||
default:
|
||||
return fmt.Sprintf(
|
||||
"You're already in an arena run. Tier %d, Round %d.\n\n"+
|
||||
"You're in an arena streak. Tier %d, Round %d.\n\n"+
|
||||
"`!arena fight` to continue.",
|
||||
run.Tier, run.Round)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user