parent
f939d053a8
commit
c7d4c1825f
9 changed files with 302 additions and 83 deletions
21
PROTOCOL.md
21
PROTOCOL.md
|
@ -16,14 +16,25 @@ Players always perceive games from the perspective of player number 1 (black).
|
|||
|
||||
### Client commands
|
||||
|
||||
- `login [username] [password]`
|
||||
- Log in to bgammon. A random username is assigned when none is provided.
|
||||
Clients must send a register command or login command before sending any other commands.
|
||||
|
||||
- `register <email> <username> <password>`
|
||||
- Register an account. A valid email address must be provided.
|
||||
- Usernames must contain at least one non-numeric character.
|
||||
|
||||
- `registerjson <client name> <email> <username> <password>`
|
||||
- Register an account and enable JSON formatted responses.
|
||||
- All client applications should use the `registerjson` command to register, as JSON
|
||||
formatted responses are more easily parsed by computers.
|
||||
- The name of the client must be specified.
|
||||
- Aliases: `rj`
|
||||
|
||||
- `login [username] [password]`
|
||||
- Log in. A random username is assigned when none is provided.
|
||||
- Usernames must contain at least one non-numeric character.
|
||||
- This (or `loginjson`) must be the first command sent when a client connects to bgammon.
|
||||
- Aliases: `l`
|
||||
|
||||
- `loginjson <client name> [username] [password]`
|
||||
- Log in to bgammon and enable JSON formatted responses.
|
||||
- Log in and enable JSON formatted responses.
|
||||
- All client applications should use the `loginjson` command to log in, as JSON
|
||||
formatted responses are more easily parsed by computers.
|
||||
- The name of the client must be specified.
|
||||
|
|
42
command.go
42
command.go
|
@ -1,29 +1,29 @@
|
|||
package bgammon
|
||||
|
||||
// commands are always sent TO the server
|
||||
|
||||
type Command string
|
||||
|
||||
const (
|
||||
CommandLogin = "login" // Log in with username and password, or as a guest.
|
||||
CommandLoginJSON = "loginjson" // Log in with username and password, or as a guest, and enable JSON messages.
|
||||
CommandHelp = "help" // Print help information.
|
||||
CommandJSON = "json" // Enable or disable JSON formatted messages.
|
||||
CommandSay = "say" // Send chat message.
|
||||
CommandList = "list" // List available matches.
|
||||
CommandCreate = "create" // Create match.
|
||||
CommandJoin = "join" // Join match.
|
||||
CommandLeave = "leave" // Leave match.
|
||||
CommandDouble = "double" // Offer double to opponent.
|
||||
CommandResign = "resign" // Decline double offer and resign game.
|
||||
CommandRoll = "roll" // Roll dice.
|
||||
CommandMove = "move" // Move checkers.
|
||||
CommandReset = "reset" // Reset checker movement.
|
||||
CommandOk = "ok" // Confirm checker movement and pass turn to next player.
|
||||
CommandRematch = "rematch" // Confirm checker movement and pass turn to next player.
|
||||
CommandBoard = "board" // Print current board state in human-readable form.
|
||||
CommandPong = "pong" // Response to server ping.
|
||||
CommandDisconnect = "disconnect" // Disconnect from server.
|
||||
CommandLogin = "login" // Log in with username and password, or as a guest.
|
||||
CommandLoginJSON = "loginjson" // Log in with username and password, or as a guest, and enable JSON messages.
|
||||
CommandRegister = "register" // Register an account.
|
||||
CommandRegisterJSON = "registerjson" // Register an account and enable JSON messages.
|
||||
CommandHelp = "help" // Print help information.
|
||||
CommandJSON = "json" // Enable or disable JSON formatted messages.
|
||||
CommandSay = "say" // Send chat message.
|
||||
CommandList = "list" // List available matches.
|
||||
CommandCreate = "create" // Create match.
|
||||
CommandJoin = "join" // Join match.
|
||||
CommandLeave = "leave" // Leave match.
|
||||
CommandDouble = "double" // Offer double to opponent.
|
||||
CommandResign = "resign" // Decline double offer and resign game.
|
||||
CommandRoll = "roll" // Roll dice.
|
||||
CommandMove = "move" // Move checkers.
|
||||
CommandReset = "reset" // Reset checker movement.
|
||||
CommandOk = "ok" // Confirm checker movement and pass turn to next player.
|
||||
CommandRematch = "rematch" // Confirm checker movement and pass turn to next player.
|
||||
CommandBoard = "board" // Print current board state in human-readable form.
|
||||
CommandPong = "pong" // Response to server ping.
|
||||
CommandDisconnect = "disconnect" // Disconnect from server.
|
||||
)
|
||||
|
||||
type EventType string
|
||||
|
|
2
event.go
2
event.go
|
@ -5,8 +5,6 @@ import (
|
|||
"fmt"
|
||||
)
|
||||
|
||||
// events are always received FROM the server
|
||||
|
||||
type Event struct {
|
||||
Type string
|
||||
Player string
|
||||
|
|
3
go.mod
3
go.mod
|
@ -3,8 +3,9 @@ module code.rocket9labs.com/tslocum/bgammon
|
|||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/alexedwards/argon2id v1.0.0
|
||||
github.com/gobwas/ws v1.3.1
|
||||
github.com/jackc/pgx/v5 v5.5.0
|
||||
github.com/jackc/pgx/v5 v5.5.1
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
44
go.sum
44
go.sum
|
@ -1,3 +1,5 @@
|
|||
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
||||
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
|
@ -10,8 +12,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
|||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw=
|
||||
github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
||||
github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI=
|
||||
github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
|
@ -19,14 +21,52 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
|
||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
|
|
@ -2,5 +2,7 @@ package server
|
|||
|
||||
type account struct {
|
||||
id int
|
||||
username string
|
||||
email []byte
|
||||
username []byte
|
||||
password []byte
|
||||
}
|
||||
|
|
|
@ -3,30 +3,51 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"code.rocket9labs.com/tslocum/bgammon"
|
||||
"github.com/alexedwards/argon2id"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
const databaseSchema = `
|
||||
CREATE TABLE account (
|
||||
id serial PRIMARY KEY,
|
||||
created bigint NOT NULL,
|
||||
active bigint NOT NULL,
|
||||
email text NOT NULL,
|
||||
username text NOT NULL,
|
||||
password text NOT NULL
|
||||
);
|
||||
CREATE TABLE game (
|
||||
id serial PRIMARY KEY,
|
||||
acey integer NOT NULL,
|
||||
started bigint NOT NULL,
|
||||
ended bigint NOT NULL,
|
||||
player1 text NOT NULL,
|
||||
player2 text NOT NULL,
|
||||
points integer NOT NULL,
|
||||
winner integer NOT NULL,
|
||||
id serial PRIMARY KEY,
|
||||
acey integer NOT NULL,
|
||||
started bigint NOT NULL,
|
||||
ended bigint NOT NULL,
|
||||
player1 text NOT NULL,
|
||||
account1 integer NOT NULL,
|
||||
player2 text NOT NULL,
|
||||
account2 integer NOT NULL,
|
||||
points integer NOT NULL,
|
||||
winner integer NOT NULL,
|
||||
wintype integer NOT NULL
|
||||
);
|
||||
`
|
||||
|
||||
var db *pgx.Conn
|
||||
|
||||
var passwordArgon2id = &argon2id.Params{
|
||||
Memory: 128 * 1024,
|
||||
Iterations: 16,
|
||||
Parallelism: 4,
|
||||
SaltLength: 16,
|
||||
KeyLength: 64,
|
||||
}
|
||||
|
||||
func connectDB(dataSource string) error {
|
||||
var err error
|
||||
db, err = pgx.Connect(context.Background(), dataSource)
|
||||
|
@ -73,7 +94,87 @@ func initDB() {
|
|||
log.Println("Initialized database schema")
|
||||
}
|
||||
|
||||
func recordGameResult(g *bgammon.Game, winType int) error {
|
||||
func registerAccount(a *account) error {
|
||||
if db == nil {
|
||||
return nil
|
||||
} else if len(bytes.TrimSpace(a.username)) == 0 {
|
||||
return fmt.Errorf("please enter a username")
|
||||
} else if len(bytes.TrimSpace(a.email)) == 0 {
|
||||
return fmt.Errorf("please enter an email address")
|
||||
} else if len(bytes.TrimSpace(a.password)) == 0 {
|
||||
return fmt.Errorf("please enter a password")
|
||||
} else if !bytes.ContainsRune(a.email, '@') || !bytes.ContainsRune(a.email, '.') {
|
||||
return fmt.Errorf("please enter a valid email address")
|
||||
} else if !alphaNumericUnderscore.Match(a.username) {
|
||||
return fmt.Errorf("please enter a username containing only letters, numbers and underscores")
|
||||
} else if bytes.HasPrefix(bytes.ToLower(a.username), []byte("guest_")) {
|
||||
return fmt.Errorf("please enter a valid username")
|
||||
}
|
||||
|
||||
tx, err := begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Commit(context.Background())
|
||||
|
||||
var result int
|
||||
err = tx.QueryRow(context.Background(), "SELECT COUNT(*) FROM account WHERE email = $1", bytes.ToLower(bytes.TrimSpace(a.email))).Scan(&result)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
} else if result > 0 {
|
||||
return fmt.Errorf("email address already in use")
|
||||
}
|
||||
|
||||
err = tx.QueryRow(context.Background(), "SELECT COUNT(*) FROM account WHERE username = $1", bytes.ToLower(bytes.TrimSpace(a.username))).Scan(&result)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
} else if result > 0 {
|
||||
return fmt.Errorf("username already in use")
|
||||
}
|
||||
|
||||
passwordHash, err := argon2id.CreateHash(string(a.password), passwordArgon2id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
_, err = tx.Exec(context.Background(), "INSERT INTO account (created, active, email, username, password) VALUES ($1, $2, $3, $4, $5)", timestamp, timestamp, bytes.ToLower(bytes.TrimSpace(a.email)), bytes.ToLower(bytes.TrimSpace(a.username)), passwordHash)
|
||||
return err
|
||||
}
|
||||
|
||||
func loginAccount(username []byte, password []byte) (*account, error) {
|
||||
if db == nil {
|
||||
return nil, nil
|
||||
} else if len(bytes.TrimSpace(username)) == 0 {
|
||||
return nil, fmt.Errorf("please enter an email address")
|
||||
} else if len(bytes.TrimSpace(password)) == 0 {
|
||||
return nil, fmt.Errorf("please enter a password")
|
||||
}
|
||||
|
||||
tx, err := begin()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Commit(context.Background())
|
||||
|
||||
account := &account{}
|
||||
err = tx.QueryRow(context.Background(), "SELECT id, email, username, password FROM account WHERE username = $1 OR email = $2", bytes.ToLower(bytes.TrimSpace(username)), bytes.ToLower(bytes.TrimSpace(username))).Scan(&account.id, &account.email, &account.username, &account.password)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
} else if len(account.password) == 0 {
|
||||
return nil, fmt.Errorf("account disabled")
|
||||
}
|
||||
|
||||
match, err := argon2id.ComparePasswordAndHash(string(password), string(account.password))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !match {
|
||||
return nil, nil
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func recordGameResult(g *bgammon.Game, winType int, account1 int, account2 int) error {
|
||||
if db == nil || g.Started.IsZero() || g.Ended.IsZero() || g.Winner == 0 {
|
||||
return nil
|
||||
}
|
||||
|
@ -88,7 +189,7 @@ func recordGameResult(g *bgammon.Game, winType int) error {
|
|||
if g.Acey {
|
||||
acey = 1
|
||||
}
|
||||
_, err = tx.Exec(context.Background(), "INSERT INTO game (acey, started, ended, player1, player2, points, winner, wintype) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", acey, g.Started.Unix(), g.Ended.Unix(), g.Player1.Name, g.Player2.Name, g.Points, g.Winner, winType)
|
||||
_, err = tx.Exec(context.Background(), "INSERT INTO game (acey, started, ended, player1, account1, player2, account2, points, winner, wintype) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", acey, g.Started.Unix(), g.Ended.Unix(), g.Player1.Name, account1, g.Player2.Name, account2, g.Points, g.Winner, winType)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,15 @@ func testDBConnection() error {
|
|||
func initDB() {
|
||||
}
|
||||
|
||||
func recordGameResult(g *bgammon.Game, winType int) error {
|
||||
func registerAccount(a *account) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func loginAccount(username []byte, password []byte) (*account, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func recordGameResult(g *bgammon.Game, winType int, account1 int, account2 int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -23,8 +23,9 @@ const clientTimeout = 40 * time.Second
|
|||
var allowDebugCommands bool
|
||||
|
||||
var (
|
||||
onlyNumbers = regexp.MustCompile(`^[0-9]+$`)
|
||||
guestName = regexp.MustCompile(`^guest[0-9]+$`)
|
||||
onlyNumbers = regexp.MustCompile(`^[0-9]+$`)
|
||||
guestName = regexp.MustCompile(`^guest[0-9]+$`)
|
||||
alphaNumericUnderscore = regexp.MustCompile(`^[A-Za-z0-9_]+$`)
|
||||
)
|
||||
|
||||
type serverCommand struct {
|
||||
|
@ -404,7 +405,7 @@ func (s *server) handleNewClientIDs() {
|
|||
// randomUsername returns a random guest username, and assumes clients are already locked.
|
||||
func (s *server) randomUsername() []byte {
|
||||
for {
|
||||
name := []byte(fmt.Sprintf("Guest%d", 100+RandInt(900)))
|
||||
name := []byte(fmt.Sprintf("Guest_%d", 100+RandInt(900)))
|
||||
|
||||
if s.clientByUsername(name) == nil {
|
||||
return name
|
||||
|
@ -466,55 +467,112 @@ COMMANDS:
|
|||
|
||||
// Require users to send login command first.
|
||||
if cmd.client.account == -1 {
|
||||
if keyword == bgammon.CommandLogin || keyword == bgammon.CommandLoginJSON || keyword == "l" || keyword == "lj" {
|
||||
if keyword == bgammon.CommandLoginJSON || keyword == "lj" {
|
||||
loginCommand := keyword == bgammon.CommandLogin || keyword == bgammon.CommandLoginJSON || keyword == "lj"
|
||||
registerCommand := keyword == bgammon.CommandRegister || keyword == bgammon.CommandRegisterJSON || keyword == "rj"
|
||||
if loginCommand || registerCommand {
|
||||
if keyword == bgammon.CommandLoginJSON || keyword == bgammon.CommandRegisterJSON || keyword == "lj" || keyword == "rj" {
|
||||
cmd.client.json = true
|
||||
}
|
||||
|
||||
s.clientsLock.Lock()
|
||||
|
||||
var username []byte
|
||||
var password []byte
|
||||
readUsername := func() bool {
|
||||
if cmd.client.json {
|
||||
if len(params) > 1 {
|
||||
username = params[1]
|
||||
}
|
||||
} else {
|
||||
if len(params) > 0 {
|
||||
username = params[0]
|
||||
}
|
||||
var randomUsername bool
|
||||
if registerCommand {
|
||||
sendUsage := func() {
|
||||
cmd.client.Terminate("Please enter an email, username and password.")
|
||||
}
|
||||
var randomUsername bool
|
||||
if len(bytes.TrimSpace(username)) == 0 {
|
||||
username = s.randomUsername()
|
||||
randomUsername = true
|
||||
|
||||
var email []byte
|
||||
if keyword == bgammon.CommandRegisterJSON || keyword == "rj" {
|
||||
if len(params) < 4 {
|
||||
sendUsage()
|
||||
continue
|
||||
}
|
||||
email = params[1]
|
||||
username = params[2]
|
||||
password = bytes.Join(params[3:], []byte("_"))
|
||||
} else {
|
||||
if len(params) < 3 {
|
||||
sendUsage()
|
||||
continue
|
||||
}
|
||||
email = params[0]
|
||||
username = params[1]
|
||||
password = bytes.Join(params[2:], []byte("_"))
|
||||
}
|
||||
if onlyNumbers.Match(username) {
|
||||
cmd.client.Terminate("Invalid username: must contain at least one non-numeric character.")
|
||||
return false
|
||||
} else if s.clientByUsername(username) != nil || (!randomUsername && !s.nameAllowed(username)) {
|
||||
cmd.client.Terminate("That username is already in use.")
|
||||
return false
|
||||
cmd.client.Terminate("Failed to register: Invalid username: must contain at least one non-numeric character.")
|
||||
continue
|
||||
}
|
||||
return true
|
||||
}
|
||||
if !readUsername() {
|
||||
s.clientsLock.Unlock()
|
||||
continue
|
||||
}
|
||||
if len(params) > 2 {
|
||||
password = bytes.ReplaceAll(bytes.Join(params[2:], []byte(" ")), []byte("_"), []byte(" "))
|
||||
}
|
||||
password = bytes.ReplaceAll(password, []byte("_"), []byte(" "))
|
||||
a := &account{
|
||||
email: email,
|
||||
username: username,
|
||||
password: password,
|
||||
}
|
||||
err := registerAccount(a)
|
||||
if err != nil {
|
||||
cmd.client.Terminate(fmt.Sprintf("Failed to register: %s", err))
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
s.clientsLock.Lock()
|
||||
|
||||
s.clientsLock.Unlock()
|
||||
readUsername := func() bool {
|
||||
if cmd.client.json {
|
||||
if len(params) > 1 {
|
||||
username = params[1]
|
||||
}
|
||||
} else {
|
||||
if len(params) > 0 {
|
||||
username = params[0]
|
||||
}
|
||||
}
|
||||
if len(bytes.TrimSpace(username)) == 0 {
|
||||
username = s.randomUsername()
|
||||
randomUsername = true
|
||||
} else if !alphaNumericUnderscore.Match(username) {
|
||||
cmd.client.Terminate("Invalid username: must contain only letters, numbers and underscores.")
|
||||
return false
|
||||
}
|
||||
if onlyNumbers.Match(username) {
|
||||
cmd.client.Terminate("Invalid username: must contain at least one non-numeric character.")
|
||||
return false
|
||||
} else if s.clientByUsername(username) != nil || s.clientByUsername(append([]byte("Guest_"), username...)) != nil || (!randomUsername && !s.nameAllowed(username)) {
|
||||
cmd.client.Terminate("That username is already in use.")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
if !readUsername() {
|
||||
s.clientsLock.Unlock()
|
||||
continue
|
||||
}
|
||||
if len(params) > 2 {
|
||||
password = bytes.ReplaceAll(bytes.Join(params[2:], []byte(" ")), []byte("_"), []byte(" "))
|
||||
}
|
||||
|
||||
s.clientsLock.Unlock()
|
||||
}
|
||||
|
||||
if len(password) > 0 {
|
||||
cmd.client.account = 1
|
||||
a, err := loginAccount(username, password)
|
||||
if err != nil {
|
||||
cmd.client.Terminate(fmt.Sprintf("Failed to log in: %s", err))
|
||||
continue
|
||||
} else if a == nil {
|
||||
cmd.client.Terminate("No account was found with the provided username and password. To log in as a guest, do not enter a password.")
|
||||
continue
|
||||
}
|
||||
cmd.client.account = a.id
|
||||
cmd.client.name = a.username
|
||||
} else {
|
||||
cmd.client.account = 0
|
||||
if !randomUsername && !bytes.HasPrefix(username, []byte("BOT_")) {
|
||||
username = append([]byte("Guest_"), username...)
|
||||
}
|
||||
cmd.client.name = username
|
||||
}
|
||||
cmd.client.name = username
|
||||
|
||||
cmd.client.sendEvent(&bgammon.EventWelcome{
|
||||
PlayerName: string(cmd.client.name),
|
||||
|
@ -897,7 +955,7 @@ COMMANDS:
|
|||
winEvent.Player = clientGame.Player2.Name
|
||||
}
|
||||
|
||||
err := recordGameResult(clientGame.Game, 4)
|
||||
err := recordGameResult(clientGame.Game, 4, clientGame.client1.account, clientGame.client2.account)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to record game result: %s", err)
|
||||
}
|
||||
|
@ -1126,7 +1184,7 @@ COMMANDS:
|
|||
}
|
||||
}
|
||||
|
||||
err := recordGameResult(clientGame.Game, winPoints)
|
||||
err := recordGameResult(clientGame.Game, winPoints, clientGame.client1.account, clientGame.client2.account)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to record game result: %s", err)
|
||||
}
|
||||
|
@ -1380,7 +1438,7 @@ COMMANDS:
|
|||
clientGame.Turn = 2
|
||||
clientGame.Roll1 = 6
|
||||
clientGame.Roll2 = 6
|
||||
clientGame.Board = []int{1, 2, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, -2, 0, 0, 0}
|
||||
clientGame.Board = []int{1, 2, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, -2, 0, 0, 0}
|
||||
|
||||
clientGame.eachClient(func(client *serverClient) {
|
||||
clientGame.sendBoard(client)
|
||||
|
|
Loading…
Reference in a new issue