tImplement HTML score page generation - scoreboard - Interactive scoreboard for CTF-like games
 (HTM) git clone git://git.z3bra.org/scoreboard.git
 (DIR) Log
 (DIR) Files
 (DIR) Refs
       ---
 (DIR) commit 528d94f22d463e41294bd92095b4953f52dc1fdd
 (DIR) parent e060760bef0f2e589d993fa17bbf11cc6762f3dc
 (HTM) Author: Willy Goiffon <dev@z3bra.org>
       Date:   Wed,  7 Dec 2022 12:45:04 +0100
       
       Implement HTML score page generation
       
       Diffstat:
         M db.go                               |       4 ++--
         A html.go                             |      97 ++++++++++++++++++++++++++++++
         M main.go                             |      16 +++++++++++-----
         M player.go                           |      15 ++++++++++-----
         M playerbox.go                        |       1 +
       
       5 files changed, 121 insertions(+), 12 deletions(-)
       ---
 (DIR) diff --git a/db.go b/db.go
       t@@ -104,7 +104,7 @@ func db_id(db *sql.DB, nick string) bool {
        
        func db_ranked_players(db *sql.DB, offset, limit int) ([]Player, error) {
                query := `SELECT
       -          name,flag,score
       +          name,flag,score,ts
                  FROM score
                  ORDER BY
                    score DESC,
       t@@ -122,7 +122,7 @@ func db_ranked_players(db *sql.DB, offset, limit int) ([]Player, error) {
                players := make([]Player, 0)
                for rows.Next() {
                        var p Player
       -                err := rows.Scan(&p.name, &p.flag, &p.score)
       +                err := rows.Scan(&p.name, &p.flag, &p.score, &p.ts)
                        if err != nil {
                                return nil, err
                        }
 (DIR) diff --git a/html.go b/html.go
       t@@ -0,0 +1,97 @@
       +package main
       +
       +import (
       +        "os"
       +        "fmt"
       +        "html/template"
       +        "github.com/dustin/go-humanize"
       +)
       +
       +type Boardline struct {
       +        Name  string
       +        Rank  string
       +        Flag  string
       +        Score string
       +}
       +
       +type Template struct {
       +        Players []Boardline
       +        Placeholders []Boardline
       +}
       +
       +var html string = `
       +<!DOCTYPE HTML>
       +<html>
       +<head>
       +        <meta charset="utf-8">
       +        <meta name="author" content="z3bra">
       +        <meta name="viewport" content="width=device-width">
       +        <link rel="stylesheet" type="text/css" href="/arcade.css">
       +        <link rel="stylesheet" type="text/css" href="/glitch.css">
       +        <link rel="stylesheet" type="text/css" href="/board.css">
       +        <link rel="icon" type="image/ico" href="/favicon.ico" />
       +        <title>High scores</title>
       +</head>
       +<body>
       +<h1 class="title glitch" data-text="CYB3R HUNT">CYB3R HUNT</h1>
       +<h2>HIGH SCORES</h2>
       +<div class="board">
       +<table>
       +        <thead><tr><th>RANK</th><th>NAME</th><th>FLAGS</th><th>SCORE</th></tr></thead>
       +        <tbody>
       +{{range .Players}}                <tr><td>{{.Rank}}<td>{{.Name}}</td><td>{{.Flag}}</td><td>{{.Score}}</td></tr>
       +{{end}}
       +{{range .Placeholders}}                <tr class='unset'><td>{{.Rank}}<td>{{.Name}}</td><td>{{.Flag}}</td><td>{{.Score}}</td></tr>
       +{{end}}
       +        </tbody>
       +</table>
       +</div>
       +</body>
       +</html>
       +`
       +
       +func (a *Application) GenerateHTML() {
       +        players, err := db_ranked_players(a.db, 0, -1)
       +        if err != nil {
       +                panic(err)
       +        }
       +
       +        data := Template{}
       +        for i:=0; i<len(players); i++ {
       +                players[i].db = a.db
       +                data.Players = append(data.Players, Boardline{
       +                        Name: players[i].name,
       +                        Rank: players[i].RankStr(),
       +                        Flag: players[i].FlagStr(),
       +                        Score: fmt.Sprintf("%d", players[i].score),
       +                })
       +        }
       +
       +        /* fill with placeholder data */
       +        if len(data.Players) < 10 {
       +                for i:=len(data.Players); i<10; i++ {
       +                        data.Placeholders = append(data.Placeholders, Boardline{
       +                                Name: "AAA",
       +                                Rank: humanize.Ordinal(i+1),
       +                                Flag: ".....",
       +                                Score: "00000",
       +                        })
       +                }
       +        }
       +
       +        tmpl, err := template.New("board").Parse(html)
       +        if err != nil {
       +                panic(err)
       +        }
       +
       +        f, err := os.OpenFile(a.html, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
       +        if err != nil {
       +                panic(err)
       +        }
       +        defer f.Close()
       +
       +        err = tmpl.Execute(f, data)
       +        if err != nil {
       +                panic(err)
       +        }
       +}
 (DIR) diff --git a/main.go b/main.go
       t@@ -31,12 +31,11 @@ import (
        const (
                BOARD_WIDTH int = 26
                BOARD_HEIGHT int = 15
       -        DB string = "leaderboard.db"
       +        HTML string = "score.html"
       +        DB string = "score.db"
                TOKEN_REMINDER string = `TOKEN FOR %s: %s
       -
        This token will be requested when you submit flag #%d.
        Save it carefully, and do not share it with anyone.
       -
        Good bye hunter. Good luck.
        `
        )
       t@@ -45,6 +44,7 @@ type Application struct {
                flag int
                db *sql.DB
                app *tview.Application
       +        html string
                pages *tview.Pages
                frame *tview.Grid
                board *tview.Flex
       t@@ -122,6 +122,7 @@ func pageToken() tview.Primitive {
                                }
                                cyboard.HighlightBoard(cyboard.player.ScoreRank())
                                cyboard.pages.SwitchToPage("board")
       +                        cyboard.GenerateHTML()
                        })
        
                return center(40, 1, input)
       t@@ -137,6 +138,11 @@ func pageBoard() tview.Primitive {
        func main() {
                var err error
        
       +        html := flag.String("o", HTML, "Output HTML file")
       +        db := flag.String("d", DB, "Database file")
       +
       +        flag.Parse()
       +
                // Override default borders
                tview.Borders.HorizontalFocus  = tview.BoxDrawingsLightHorizontal
                tview.Borders.VerticalFocus    = tview.BoxDrawingsLightVertical
       t@@ -152,13 +158,14 @@ func main() {
                tview.Styles.GraphicsColor            = tcell.ColorDefault
                tview.Styles.PrimaryTextColor         = tcell.ColorDefault
        
       -        cyboard.db, err = db_init(DB)
       +        cyboard.db, err = db_init(*db)
                if err != nil {
                        panic(err)
                }
                defer cyboard.db.Close()
        
                cyboard.flag = 0
       +        cyboard.html = *html
                cyboard.app = tview.NewApplication()
                cyboard.pages = tview.NewPages()
                cyboard.frame = tview.NewGrid()
       t@@ -170,7 +177,6 @@ func main() {
                cyboard.pages.AddPage("token",  pageToken(), true, false)
                cyboard.pages.AddPage("board",  pageBoard(), true, false)
        
       -        flag.Parse()
                args := flag.Args()
        
                if len(args) > 1 || (len(args) == 1 && args[0] == "help") {
 (DIR) diff --git a/player.go b/player.go
       t@@ -49,8 +49,6 @@ func tokenize(name string) (string, string, error) {
                key := randbuf(12)
                salt := base32.StdEncoding.EncodeToString([]byte(name))
        
       -        //token := []byte(name)
       -        //token = append(token, key...)
                token := key
                token = append(token, []byte(name)...)
        
       t@@ -104,10 +102,10 @@ func (p *Player) ScoreRank() int {
                  count(*)
                  FROM score
                  WHERE
       -            score >= ? OR (score == ? AND ts <= ?)
       +            name != ? AND (score > ? OR (score == ? AND ts <= ?))
                ;`
        
       -        row := p.db.QueryRow(query, p.score, p.score, p.ts)
       +        row := p.db.QueryRow(query, p.name, p.score, p.score, p.ts)
                row.Scan(&count)
                return count
        }
       t@@ -139,6 +137,10 @@ func (p *Player) FlagStr() string {
                return fmt.Sprintf("%s", str)
        }
        
       +func (p *Player) RankStr() string {
       +        return humanize.Ordinal(p.ScoreRank() + 1)
       +}
       +
        func (p *Player) Exists() bool {
                forbidden := []string{"KKK"}
                for i := 0; i<len(forbidden); i++ {
       t@@ -177,7 +179,10 @@ func (p *Player) Submit(flag int) error {
                        p.score += 10 * flag
                }
        
       -        p.Update()
       +        err := p.Update()
       +        if err != nil {
       +                return err
       +        }
        
                return nil
        }
 (DIR) diff --git a/playerbox.go b/playerbox.go
       t@@ -76,6 +76,7 @@ func PlayerBoxName(p *Player) *tview.TextView {
                                                        cyboard.Fatal(err)
                                                }
                                                cyboard.HighlightBoard(p.ScoreRank())
       +                                        cyboard.GenerateHTML()
                                        } else {
                                                cyboard.Popup("NOPE", "Player name unavailable\nPlease pick another one")
                                        }