tRearrange pages to reuse token box - scoreboard - Interactive scoreboard for CTF-like games
 (HTM) git clone git://git.z3bra.org/scoreboard.git
 (DIR) Log
 (DIR) Files
 (DIR) Refs
       ---
 (DIR) commit 4ccfc4d4d29f20d328168684842cf8ea1ac31581
 (DIR) parent 37c53b0f0c56f05121f7fd0a3626eb278efa5f76
 (HTM) Author: Willy Goiffon <contact@z3bra.org>
       Date:   Fri, 27 Sep 2024 00:01:25 +0200
       
       Rearrange pages to reuse token box
       
       Diffstat:
         M main.go                             |      81 ++++++++++---------------------
         M player.go                           |      30 +++++++++++++++++++++++++++++-
         M playerbox.go                        |      19 +++++++++----------
         M ui.go                               |      46 +++++++++++++++++++++++++++++++
       
       4 files changed, 109 insertions(+), 67 deletions(-)
       ---
 (DIR) diff --git a/main.go b/main.go
       t@@ -18,7 +18,6 @@ import (
                "flag"
                "fmt"
                "os"
       -        "regexp"
                "strings"
                "time"
                "database/sql"
       t@@ -33,7 +32,7 @@ const (
                BOARD_HEIGHT int = 15
                HTML string = "score.html"
                DB string = "score.db"
       -        TOKEN_REMINDER string = `%s, use the token below to submit your next flag.
       +        TOKEN_REMINDER string = `%s, use the token below to submit your flags.
        Save it carefully, do not share it.
        
          🔑%s
       t@@ -76,57 +75,6 @@ func flagid(hash string) int {
                return -1
        }
        
       -func pageToken() tview.Primitive {
       -        input := tview.NewInputField().
       -                SetLabel("TOKEN ").
       -                SetPlaceholder("").
       -                SetFieldStyle(tcell.StyleDefault.Reverse(true)).
       -                SetFieldWidth(30)
       -
       -        input.SetAcceptanceFunc(func(text string, ch rune) bool {
       -                if len(text) > 24 {
       -                        return false
       -                }
       -
       -                // tokens are base32 strings
       -                matched, err := regexp.Match(`^[A-Z2-7]+$`, []byte(text))
       -                if err != nil {
       -                        panic(err)
       -                }
       -                return matched
       -        })
       -        input.SetDoneFunc(func(key tcell.Key) {
       -                        if key != tcell.KeyEnter {
       -                                return
       -                        }
       -
       -                        token := input.GetText()
       -
       -                        if len(token) != 24 {
       -                                scoreboard.Popup("ERROR", "Invalid token format")
       -                                return
       -                        }
       -                        err := scoreboard.player.FromToken(token)
       -                        if err != nil {
       -                                scoreboard.Fatal(err)
       -                                return
       -                        }
       -
       -                        scoreboard.player.token = token;
       -
       -                        err = scoreboard.player.Submit(scoreboard.flag)
       -                        if err != nil {
       -                                scoreboard.Fatal(err)
       -                                return
       -                        }
       -                        scoreboard.HighlightBoard(scoreboard.player.ScoreRank() + 1)
       -                        scoreboard.pages.SwitchToPage("board")
       -                        scoreboard.GenerateHTML()
       -                })
       -
       -        return center(40, 1, input)
       -}
       -
        func pageBoard() tview.Primitive {
                scoreboard.SetupFrame()
                scoreboard.DrawBoard()
       t@@ -137,6 +85,7 @@ func pageBoard() tview.Primitive {
        
        func main() {
                var err error
       +        var reminder bool = false
        
                html := flag.String("o", HTML, "Output HTML file")
                db := flag.String("d", DB, "Database file")
       t@@ -175,7 +124,6 @@ func main() {
        
                scoreboard.pages.SetBackgroundColor(tcell.ColorDefault)
        
       -        scoreboard.pages.AddPage("token",  pageToken(), true, false)
                scoreboard.pages.AddPage("board",  pageBoard(), true, false)
        
                args := flag.Args()
       t@@ -204,6 +152,14 @@ func main() {
                                rank, _ := db_count_players(scoreboard.db)
                                scoreboard.NewPlayer(rank + 1)
                                scoreboard.pages.SwitchToPage("board")
       +                        reminder = true
       +
       +                case "badges":
       +                        badgepage := scoreboard.Token(func () {
       +                                scoreboard.app.Stop()
       +                                fmt.Printf("Collection: %d/%d\n\n%s", scoreboard.player.flag, len(scoreboard.flag_ref), scoreboard.player.BadgeStr())
       +                        })
       +                        scoreboard.pages.AddAndSwitchToPage("badge", badgepage, true)
        
                        /* anything not a command is treated as a flag */
                        default:
       t@@ -212,7 +168,20 @@ func main() {
                                        fmt.Println("Incorrect flag")
                                        return
                                }
       -                        scoreboard.pages.SwitchToPage("token")
       +
       +                        submitpage := scoreboard.Token(func () {
       +                                err = scoreboard.player.Submit(scoreboard.flag)
       +                                if err != nil {
       +                                        scoreboard.Fatal(err)
       +                                        return
       +                                }
       +                                scoreboard.HighlightBoard(scoreboard.player.ScoreRank() + 1)
       +                                scoreboard.pages.RemovePage("submit")
       +                                scoreboard.pages.ShowPage("board")
       +                                scoreboard.GenerateHTML()
       +                        })
       +
       +                        scoreboard.pages.AddAndSwitchToPage("submit", submitpage, true)
                        }
                }
        
       t@@ -223,7 +192,7 @@ func main() {
                }
        
                /* Print a token reminder on exit in case one has been generated or provided */
       -        if scoreboard.player.token != "" {
       +        if reminder {
                        fmt.Printf(TOKEN_REMINDER, scoreboard.player.name, scoreboard.player.token)
                }
        }
 (DIR) diff --git a/player.go b/player.go
       t@@ -20,6 +20,7 @@ import (
                "encoding/base32"
                "errors"
                "fmt"
       +        "strings"
                "time"
                "golang.org/x/crypto/scrypt"
                "github.com/dustin/go-humanize"
       t@@ -146,6 +147,33 @@ func (p *Player) FlagStr() string {
                return fmt.Sprintf("%2d/%d", p.flag, len(scoreboard.flag_ref))
        }
        
       +func (p *Player) BadgeStr() string {
       +        var badges strings.Builder
       +
       +        query := `SELECT
       +          flag.badge, flag.value, flag.score
       +          FROM flag
       +          INNER JOIN score ON score.flag = flag.value
       +          WHERE score.name = ?;`
       +
       +        rows, err := p.db.Query(query, p.name)
       +        if err != nil {
       +                return ""
       +        }
       +
       +        for rows.Next() {
       +                var b, v string
       +                var s int
       +                err := rows.Scan(&b, &v, &s)
       +                if err != nil {
       +                        return ""
       +                }
       +                badges.WriteString(fmt.Sprintf("%s %s (%d pts)\n", b, v, s))
       +        }
       +
       +        return badges.String();
       +}
       +
        func (p *Player) RankStr() string {
                return humanize.Ordinal(p.ScoreRank() + 1)
        }
       t@@ -221,7 +249,7 @@ func (p *Player) FromToken(token string) error {
                        return err
                }
                hash := base32.StdEncoding.EncodeToString(dk)
       -        query := `SELECT name,flag,score,ts FROM score WHERE hash = ?`
       +        query := `SELECT name,flag,score,ts FROM user WHERE hash = ?`
        
                row := p.db.QueryRow(query, hash)
                err = row.Scan(&p.name, &p.flag, &p.score, &p.ts)
 (DIR) diff --git a/playerbox.go b/playerbox.go
       t@@ -16,19 +16,18 @@ type PlayerBox struct {
        }
        
        const (
       -        TOKEN_WELCOME string = `%s, your progression has
       -been saved. To update it,
       -you will need this token:
       +        TOKEN_WELCOME string = `
       +%s, your registration is
       +now complete. To update
       +your progression, you
       +will need this token:
        
        🔑%s
        
       -Save it.
       -Do not share it.
        
       -Tokens are single-use.
       -A new token is generated
       -and displayed each time
       -you submit a flag.
       +
       +Save it carefully.
       +Do not share it.
        `
        )
        
       t@@ -125,7 +124,7 @@ func PlayerBoxGrid(p *Player, rank int) *tview.Grid {
                playerbox.score = gridcell(fmt.Sprintf("%5d ", p.score))
        
                grid := tview.NewGrid().
       -                SetColumns(4,4,6,7).
       +                SetColumns(5,4,6,7).
                        SetGap(0, 2).
                        SetRows(1).
                        AddItem(gridcell(rankstr), 0, 0, 1, 1, 0, 0, false).
 (DIR) diff --git a/ui.go b/ui.go
       t@@ -3,6 +3,7 @@ package main
        import (
                "fmt"
                "math"
       +        "regexp"
                "github.com/gdamore/tcell/v2"
                "github.com/rivo/tview"
                "github.com/dustin/go-humanize"
       t@@ -171,3 +172,48 @@ func (a *Application) Fatal(err error) {
        
                a.pages.AddAndSwitchToPage("popup", p, true)
        }
       +
       +func (a *Application) Token(callback func()) tview.Primitive {
       +        input := tview.NewInputField().
       +                SetLabel("TOKEN ").
       +                SetPlaceholder("").
       +                SetFieldStyle(tcell.StyleDefault.Reverse(true)).
       +                SetFieldWidth(30)
       +
       +        input.SetAcceptanceFunc(func(text string, ch rune) bool {
       +                if len(text) > 24 {
       +                        return false
       +                }
       +
       +                // tokens are base32 strings
       +                matched, err := regexp.Match(`^[A-Z2-7]+$`, []byte(text))
       +                if err != nil {
       +                        panic(err)
       +                }
       +                return matched
       +        })
       +        input.SetDoneFunc(func(key tcell.Key) {
       +                        if key != tcell.KeyEnter {
       +                                return
       +                        }
       +
       +                        token := input.GetText()
       +
       +                        if len(token) != 24 {
       +                                scoreboard.Popup("ERROR", "Invalid token format")
       +                                return
       +                        }
       +                        err := scoreboard.player.FromToken(token)
       +                        if err != nil {
       +                                scoreboard.Fatal(err)
       +                                return
       +                        }
       +
       +                        scoreboard.player.token = token;
       +                        scoreboard.player.Fetch()
       +                        scoreboard.pages.RemovePage("token");
       +                        callback()
       +                })
       +
       +        return center(40, 1, input)
       +}