Support account registration

Resolves #1.
This commit is contained in:
Trevor Slocum 2023-12-12 22:46:37 -08:00
parent f939d053a8
commit c7d4c1825f
9 changed files with 302 additions and 83 deletions

View file

@ -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.

View file

@ -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

View file

@ -5,8 +5,6 @@ import (
"fmt"
)
// events are always received FROM the server
type Event struct {
Type string
Player string

3
go.mod
View file

@ -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
View file

@ -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=

View file

@ -2,5 +2,7 @@ package server
type account struct {
id int
username string
email []byte
username []byte
password []byte
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)