@@ -16,5 +16,8 @@ | |||
go.sum | |||
# build binary | |||
./leadercraft-s | |||
leadercraft-s | |||
# sqlite default db | |||
test.sqlite | |||
@@ -21,6 +21,10 @@ func initArgs() { | |||
flag.StringVar(&port, "port", defaultPort, "Port to listen on") | |||
flag.StringVar(&root, "root", defaultRoot, "Root working directory") | |||
flag.BoolVar(&printVersionAndExit, "version", false, "Print version and exit") | |||
flag.StringVar(&sqlConnection, "conn", sqlConnectionDefault, "Database connection string") | |||
flag.StringVar(&sqlServer, "sql", sqlServerDefault, "SQL Database type") | |||
flag.BoolVar(&buildTables, "build-db", true /*false*/, "Build database tables on startup") | |||
flag.BoolVar(&populateTables, "populate-db", true /*false*/, "Populate database with test data") | |||
} | |||
func parseArgs() { | |||
@@ -1,3 +1,5 @@ | |||
module git.exmods.org/NGnius/leadercraft-s | |||
go 1.13 | |||
require github.com/mattn/go-sqlite3 v2.0.3+incompatible |
@@ -24,26 +24,32 @@ func boardHandler(w http.ResponseWriter, r *http.Request) { | |||
} | |||
board := args.Get("board") | |||
if !checkArgExists(args, "count", w) || !checkArgInt(args, "count", w, 0) { | |||
w.WriteHeader(400) | |||
return | |||
} | |||
count, _ := strconv.Atoi(args.Get("count")) | |||
if !checkArgExists(args, "start", w) || !checkArgInt(args, "start", w, 0) { | |||
w.WriteHeader(400) | |||
return | |||
} | |||
start, _ := strconv.Atoi(args.Get("start")) | |||
// execute query | |||
result := NewResult("", r.URL.String()) | |||
b, ok := boards[board] | |||
if !ok { | |||
b, err := boardByName(board) | |||
//b, ok := boards[board] | |||
if err != nil { | |||
fmt.Println(err) | |||
w.WriteHeader(404) | |||
return | |||
} | |||
if len(b.Entries) < start+count { | |||
count = len(b.Entries) - start | |||
bEntries, loadErr := b.SomeEntries(int64(start), int64(start+count)) | |||
if loadErr != nil { | |||
fmt.Println(loadErr) | |||
w.WriteHeader(404) | |||
return | |||
} | |||
// result.Items = b.Entries[start : start+count] | |||
for _, entry := range b.Entries[start : start+count] { | |||
item := entry | |||
for _, entry := range bEntries { | |||
item, _ := entry.JsonObject() | |||
result.Items = append(result.Items, &item) | |||
} | |||
result.Query = fmt.Sprintf("load board[%s] from %d to %d", board, start, count+start) | |||
@@ -8,14 +8,14 @@ import ( | |||
) | |||
// Board a leaderboard | |||
type Board struct { | |||
type BoardJSON struct { | |||
ID int64 | |||
Entries []Entry | |||
Entries []EntryJSON | |||
Name string | |||
} | |||
// Entry an entry in a leaderboard | |||
type Entry struct { | |||
type EntryJSON struct { | |||
ID int64 | |||
Rank int64 | |||
Score int64 | |||
@@ -24,14 +24,14 @@ type Entry struct { | |||
} | |||
// Player a player | |||
type Player struct { | |||
type PlayerJSON struct { | |||
ID int64 | |||
Name string | |||
Entries []Entry | |||
Entries []EntryJSON | |||
} | |||
// URL get the player's URL | |||
func (p *Player) URL() string { | |||
func (p *PlayerJSON) URL() string { | |||
return "/player/" + strconv.Itoa(int(p.ID)) | |||
} | |||
@@ -5,6 +5,8 @@ package main // leadercraft-server | |||
import ( | |||
"fmt" | |||
"net/http" | |||
"os" | |||
"os/signal" | |||
) | |||
const ( | |||
@@ -15,11 +17,11 @@ const ( | |||
) | |||
var ( | |||
server *http.Server | |||
handler http.Handler | |||
port string | |||
root string | |||
boards map[string]Board | |||
server *http.Server | |||
handler http.Handler | |||
port string | |||
root string | |||
isClosing bool | |||
) | |||
func init() { | |||
@@ -27,32 +29,33 @@ func init() { | |||
serverMux := http.NewServeMux() | |||
serverMux.HandleFunc("/load", boardHandler) | |||
handler = serverMux | |||
boards = make(map[string]Board) | |||
// test data | |||
boards["test1"] = Board{ | |||
ID: 1, | |||
Entries: []Entry{ | |||
Entry{ID: 1, Rank: 1, Score: 1000, PlayerName: "NGnius", PlayerURL: "/player/1"}, | |||
Entry{ID: 2, Rank: 2, Score: 900, PlayerName: "Also NGnius", PlayerURL: "/player/2"}, | |||
Entry{ID: 4, Rank: 3, Score: 400, PlayerName: ".xX||eDgY TeeNaGeR||Xx.", PlayerURL: "/player/3"}, | |||
Entry{ID: 3, Rank: 4, Score: 350, PlayerName: "New username who dis?", PlayerURL: "/player/4"}, | |||
Entry{ID: 7, Rank: 5, Score: 350, PlayerName: "Extremely Ridiculously Long Name Please don't break things", PlayerURL: "/player/5"}, | |||
Entry{ID: 5, Rank: 6, Score: 250, PlayerName: "P|P3 |o|z", PlayerURL: "/player/3333"}, | |||
Entry{ID: 6, Rank: 7, Score: 200, PlayerName: "Simon", PlayerURL: "/player/456"}, | |||
Entry{ID: 24, Rank: 8, Score: 175, PlayerName: "Zettagram.com", PlayerURL: "/player/32"}, | |||
Entry{ID: 42, Rank: 9, Score: 150, PlayerName: "The Doctor", PlayerURL: "/player/9283"}, | |||
Entry{ID: 43, Rank: 10, Score: 140, PlayerName: "Marvin the Paranoid Android", PlayerURL: "/player/6"}, | |||
Entry{ID: 8, Rank: 11, Score: 100, PlayerName: "IDK HOW", PlayerURL: "/player/666"}, | |||
}, | |||
} | |||
} | |||
func main() { | |||
parseArgs() | |||
sqlInitErr := sqlInit() | |||
if sqlInitErr != nil { | |||
fmt.Printf("Failed to initialise SQL connection: %s\n", sqlInitErr) | |||
os.Exit(1) | |||
} | |||
// handle interrupt (terminate) signal | |||
signalChan := make(chan os.Signal, 1) | |||
signal.Notify(signalChan, os.Interrupt) | |||
go func() { | |||
s := <-signalChan | |||
fmt.Println("Received terminate signal " + s.String()) | |||
isClosing = true | |||
sqlClose() | |||
server.Close() | |||
}() | |||
server = &http.Server{ | |||
Addr: ":" + port, | |||
Handler: handler, | |||
} | |||
fmt.Println("Starting on " + server.Addr) | |||
fmt.Println(server.ListenAndServe()) | |||
//fmt.Println(server.ListenAndServe()) | |||
err := server.ListenAndServe() | |||
if err != nil && !isClosing { | |||
fmt.Println(err) | |||
} | |||
} |
@@ -0,0 +1,135 @@ | |||
// NGnius 2020-02-11 | |||
package main | |||
import ( | |||
"database/sql" | |||
"fmt" | |||
_ "github.com/mattn/go-sqlite3" | |||
) | |||
const ( | |||
sqlServerDefault = "sqlite3" | |||
sqlConnectionDefault = "test.sqlite" | |||
) | |||
var ( | |||
// command line arguments | |||
sqlServer string | |||
sqlConnection string | |||
buildTables bool | |||
populateTables bool | |||
// internal variables | |||
db *sql.DB | |||
) | |||
func sqlInit() error { | |||
var dbOpenErr error | |||
db, dbOpenErr = sql.Open(sqlServer, sqlConnection) | |||
if dbOpenErr != nil { | |||
return dbOpenErr | |||
} | |||
if buildTables { | |||
fmt.Println("Building tables in database...") | |||
sqlBuildTables() | |||
} | |||
if populateTables { | |||
fmt.Println("Populating tables in database...") | |||
sqlPopulateTables() | |||
} | |||
return nil | |||
} | |||
func sqlClose() error { | |||
if db != nil { | |||
err := db.Close() | |||
if err != nil { | |||
return err | |||
} | |||
db = nil | |||
} | |||
return nil | |||
} | |||
func boardByName(name string) (*Board, error) { | |||
b := &Board{} | |||
return b, db.QueryRow("SELECT * FROM Boards WHERE name=? LIMIT 1", name).Scan(b.Intake()...) | |||
} | |||
func playerByName(name string) (*Player, error) { | |||
p := &Player{} | |||
return p, db.QueryRow("SELECT * FROM Players WHERE name=? LIMTI 1", name).Scan(p.Intake()...) | |||
} | |||
// internal operations | |||
func sqlBuildTables() { | |||
transaction, txErr := db.Begin() | |||
if txErr != nil { | |||
return | |||
} | |||
// test table | |||
//transaction.Exec("CREATE TABLE IF NOT EXISTS Test (Sometext VARCHAR, Somenumber);") | |||
//transaction.Exec("INSERT INTO Test (Sometext, Somenumber) VALUES (?,?);", "Hello sqlite", 123) | |||
// build real tables | |||
transaction.Exec("CREATE TABLE IF NOT EXISTS Players (id INTEGER PRIMARY KEY, name TEXT NOT NULL);") | |||
transaction.Exec("CREATE TABLE IF NOT EXISTS Boards (id INTEGER PRIMARY KEY, name TEXT NOT NULL, description TEXT);") | |||
transaction.Exec("CREATE TABLE IF NOT EXISTS Entries (id INTEGER PRIMARY KEY, rank INTEGER, score INTEGER NOT NULL, player INTEGER NOT NULL, board INTEGER NOT NULL, FOREIGN KEY(player) REFERENCES Players(id), FOREIGN KEY(board) REFERENCES Boards(id));") | |||
transaction.Commit() | |||
} | |||
func sqlPopulateTables() { | |||
boards := []*Board{ | |||
&Board{ID: 1, Name: "main-test", Description: "Overall best (test data)"}, | |||
&Board{ID: 2, Name: "coolest-test", Description: "Coolest score (test data)"}, | |||
&Board{ID: 3, Name: "fastest-test", Description: "Fastest time (test data)"}, | |||
} | |||
for _, b := range boards { | |||
err := b.Commit() | |||
if err != nil { | |||
fmt.Printf("Error creating board %d: %s\n", b.ID, err) | |||
} | |||
} | |||
players := []*Player{ | |||
&Player{ID: 1, Name: "NGnius (test)"}, | |||
&Player{ID: 2, Name: "Also NGnius (test)"}, | |||
&Player{ID: 3, Name: ".xX||eDgY TeeNaGeR||Xx. (test)"}, | |||
&Player{ID: 4, Name: "New username who dis? (test)"}, | |||
&Player{ID: 5, Name: "Extremely Ridiculously Long Name to break things (test)"}, | |||
&Player{ID: 6, Name: "P|P3 |o|z (test)"}, | |||
&Player{ID: 7, Name: "Waldo (test)"}, | |||
&Player{ID: 8, Name: "Zettagram.com (test)"}, | |||
&Player{ID: 9, Name: "The Doctor (test)"}, | |||
&Player{ID: 10, Name: "Marvin the Paranoid Android (test)"}, | |||
&Player{ID: 11, Name: "IDK HOW (test)"}, | |||
&Player{ID: 12, Name: "If you can read this your API may be wrong (test)"}, | |||
&Player{ID: 13, Name: "Unlucky 7 (test)"}, | |||
} | |||
for _, p := range players { | |||
err := p.Commit() | |||
if err != nil { | |||
fmt.Printf("Error creating player %d: %s\n", p.ID, err) | |||
} | |||
} | |||
entries := []*Entry{ | |||
&Entry{ID: 1, Rank: 1, Score: 1000, Player: players[0].ID, Board: boards[0].ID}, | |||
&Entry{ID: 2, Rank: 2, Score: 900, Player: players[1].ID, Board: boards[0].ID}, | |||
&Entry{ID: 3, Rank: 3, Score: 400, Player: players[2].ID, Board: boards[0].ID}, | |||
&Entry{ID: 4, Rank: 4, Score: 350, Player: players[3].ID, Board: boards[0].ID}, | |||
&Entry{ID: 5, Rank: 5, Score: 350, Player: players[4].ID, Board: boards[0].ID}, | |||
&Entry{ID: 6, Rank: 6, Score: 250, Player: players[5].ID, Board: boards[0].ID}, | |||
&Entry{ID: 7, Rank: 7, Score: 200, Player: players[6].ID, Board: boards[0].ID}, | |||
&Entry{ID: 8, Rank: 8, Score: 175, Player: players[7].ID, Board: boards[0].ID}, | |||
&Entry{ID: 9, Rank: 9, Score: 150, Player: players[8].ID, Board: boards[0].ID}, | |||
&Entry{ID: 10, Rank: 10, Score: 140, Player: players[9].ID, Board: boards[0].ID}, | |||
&Entry{ID: 11, Rank: 11, Score: 10, Player: players[10].ID, Board: boards[0].ID}, | |||
&Entry{ID: 12, Rank: 12, Score: 60, Player: players[11].ID, Board: boards[0].ID}, | |||
&Entry{ID: 13, Rank: 13, Score: 13, Player: players[12].ID, Board: boards[0].ID}, | |||
} | |||
for _, e := range entries { | |||
err := e.Commit() | |||
if err != nil { | |||
fmt.Printf("Error creating entry %d: %s", e.ID, err) | |||
} | |||
} | |||
} |
@@ -0,0 +1,223 @@ | |||
// NGnius 2020-02-12 | |||
package main | |||
import ( | |||
"encoding/json" | |||
"strconv" | |||
) | |||
type Jsonable interface { | |||
Json() ([]byte, error) | |||
JsonPretty() ([]byte, error) | |||
Unjson([]byte) error | |||
JsonObject() (interface{}, error) | |||
} | |||
type Rower interface { | |||
Intake() []interface{} | |||
Output() []interface{} | |||
} | |||
type Board struct { | |||
ID int64 | |||
Name string | |||
Description string | |||
} | |||
func LoadBoard(id int64) *Board { | |||
b := &Board{ID: id} | |||
b.Load() | |||
return b | |||
} | |||
func (b *Board) Load() error { | |||
return db.QueryRow("SELECT * FROM Boards WHERE id=?", b.ID).Scan(b.Intake()...) | |||
} | |||
func (b *Board) Commit() error { | |||
tx, _ := db.Begin() | |||
statement, _ := tx.Prepare("INSERT OR REPLACE INTO Boards(id, name, description) VALUES (?, ?, ?);") | |||
_, err := statement.Exec(b.Output()...) | |||
if err != nil { | |||
tx.Rollback() | |||
return err | |||
} | |||
return tx.Commit() | |||
} | |||
func (b *Board) Entries() ([]*Entry, error) { | |||
var entries []*Entry | |||
rows, err := db.Query("SELECT * FROM Entries WHERE board=?", b.ID) | |||
if err != nil { | |||
return entries, err | |||
} | |||
count := 0 | |||
for rows.Next() { | |||
entries = append(entries, &Entry{}) | |||
rows.Scan(entries[count].Intake()...) | |||
count++ | |||
} | |||
return entries, nil | |||
} | |||
func (b *Board) SomeEntries(start, end int64) ([]*Entry, error) { | |||
var entries []*Entry | |||
rows, err := db.Query("SELECT * FROM Entries WHERE board=? and rank >= ? and rank <= ? ORDER BY rank ASC;", b.ID, start, end) | |||
if err != nil { | |||
return entries, err | |||
} | |||
count := 0 | |||
for rows.Next() { | |||
entries = append(entries, &Entry{}) | |||
rows.Scan(entries[count].Intake()...) | |||
count++ | |||
} | |||
return entries, nil | |||
} | |||
func (b *Board) Intake() []interface{} { | |||
return []interface{}{&b.ID, &b.Name, &b.Description} | |||
} | |||
func (b *Board) Output() []interface{} { | |||
return []interface{}{b.ID, b.Name, b.Description} | |||
} | |||
type Player struct { | |||
ID int64 | |||
Name string | |||
} | |||
func LoadPlayer(id int64) *Player { | |||
p := &Player{ID: id} | |||
p.Load() | |||
return p | |||
} | |||
func (p *Player) Load() error { | |||
return db.QueryRow("SELECT * FROM Players WHERE id=? LIMIT 1;", p.ID).Scan(p.Intake()...) | |||
} | |||
func (p *Player) Commit() error { | |||
tx, _ := db.Begin() | |||
statement, _ := tx.Prepare("INSERT OR REPLACE INTO Players(id, name) VALUES (?, ?);") | |||
_, err := statement.Exec(p.Output()...) | |||
if err != nil { | |||
tx.Rollback() | |||
return err | |||
} | |||
return tx.Commit() | |||
} | |||
func (p *Player) Entries() ([]*Entry, error) { | |||
var entries []*Entry | |||
rows, err := db.Query("SELECT * FROM Entries WHERE player=?", p.ID) | |||
if err != nil { | |||
return entries, err | |||
} | |||
count := 0 | |||
for rows.Next() { | |||
entries = append(entries, &Entry{}) | |||
rows.Scan(entries[count].Intake()...) | |||
count++ | |||
} | |||
return entries, nil | |||
} | |||
func (p *Player) SomeEntries(limit int64) ([]*Entry, error) { | |||
var entries []*Entry | |||
rows, err := db.Query("SELECT * FROM Entries WHERE player=? ORDER BY rank ASC LIMIT ?;", p.ID, limit) | |||
if err != nil { | |||
return entries, err | |||
} | |||
count := 0 | |||
for rows.Next() { | |||
entries = append(entries, &Entry{}) | |||
rows.Scan(entries[count].Intake()...) | |||
count++ | |||
} | |||
return entries, nil | |||
} | |||
func (p *Player) Url() string { | |||
return "/player/" + strconv.Itoa(int(p.ID)) | |||
} | |||
// implementation of Rower | |||
func (p *Player) Intake() []interface{} { | |||
return []interface{}{&p.ID, &p.Name} | |||
} | |||
func (p *Player) Output() []interface{} { | |||
return []interface{}{p.ID, p.Name} | |||
} | |||
type Entry struct { | |||
ID int64 | |||
Rank int64 | |||
Score int64 | |||
Player int64 | |||
Board int64 | |||
} | |||
func LoadEntry(id int64) *Entry { | |||
e := &Entry{ID: id} | |||
e.Load() | |||
return e | |||
} | |||
func (e *Entry) Load() error { | |||
return db.QueryRow("SELECT * FROM Entries WHERE id=? LIMIT 1;", e.ID).Scan(e.Intake()...) | |||
} | |||
func (e *Entry) Commit() error { | |||
tx, _ := db.Begin() | |||
statement, _ := tx.Prepare("INSERT OR REPLACE INTO Entries(id, rank, score, player, board) VALUES (?, ?, ?, ?, ?);") | |||
_, err := statement.Exec(e.Output()...) | |||
if err != nil { | |||
tx.Rollback() | |||
return err | |||
} | |||
return tx.Commit() | |||
} | |||
// implementation of Jsonable | |||
func (e *Entry) Json() ([]byte, error) { | |||
var data []byte | |||
jsonObj, err := e.JsonObject() | |||
if err != nil { | |||
return data, err | |||
} | |||
return json.Marshal(jsonObj) | |||
} | |||
func (e *Entry) JsonPretty() ([]byte, error) { | |||
var data []byte | |||
jsonObj, err := e.JsonObject() | |||
if err != nil { | |||
return data, err | |||
} | |||
return json.MarshalIndent(jsonObj, "", " ") | |||
} | |||
func (e *Entry) JsonObject() (interface{}, error) { | |||
jsonObj := EntryJSON{ID: e.ID, Rank: e.Rank, Score: e.Score} | |||
ePlayer := &Player{ID: e.Player} | |||
err := ePlayer.Load() | |||
if err != nil { | |||
return jsonObj, err | |||
} | |||
jsonObj.PlayerName = ePlayer.Name | |||
jsonObj.PlayerURL = ePlayer.Url() | |||
return jsonObj, nil | |||
} | |||
// implementation of Rower | |||
func (e *Entry) Intake() []interface{} { | |||
return []interface{}{&e.ID, &e.Rank, &e.Score, &e.Player, &e.Board} | |||
} | |||
func (e *Entry) Output() []interface{} { | |||
return []interface{}{e.ID, e.Rank, e.Score, e.Player, e.Board} | |||
} |