diff --git a/PROTOCOL.md b/PROTOCOL.md index 163919e..36363fd 100644 --- a/PROTOCOL.md +++ b/PROTOCOL.md @@ -47,6 +47,10 @@ formatted responses are more easily parsed by computers. - `password ` - Change account password. +- `set ` + - Change account setting. + - Available settings: `highlight`, `pips` and `moves`. + - `json ` - Turn JSON formatted messages on or off. JSON messages are not sent by default. diff --git a/command.go b/command.go index f13d484..cf805a9 100644 --- a/command.go +++ b/command.go @@ -9,6 +9,7 @@ const ( CommandRegisterJSON = "registerjson" // Register an account and enable JSON messages. CommandResetPassword = "resetpassword" // Request password reset link via email. CommandPassword = "password" // Change password. + CommandSet = "set" // Change account setting. CommandHelp = "help" // Print help information. CommandJSON = "json" // Enable or disable JSON formatted messages. CommandSay = "say" // Send chat message. @@ -48,4 +49,5 @@ const ( EventTypeFailedMove = "failedmove" EventTypeFailedOk = "failedok" EventTypeWin = "win" + EventTypeSettings = "settings" ) diff --git a/event.go b/event.go index fe83a24..91753ba 100644 --- a/event.go +++ b/event.go @@ -110,6 +110,13 @@ type EventWin struct { Points int } +type EventSettings struct { + Event + Highlight bool + Pips bool + Moves bool +} + func DecodeEvent(message []byte) (interface{}, error) { e := &Event{} err := json.Unmarshal(message, e) @@ -153,6 +160,8 @@ func DecodeEvent(message []byte) (interface{}, error) { ev = &EventFailedOk{} case EventTypeWin: ev = &EventWin{} + case EventTypeSettings: + ev = &EventSettings{} default: return nil, fmt.Errorf("failed to decode event: unknown event type: %s", e.Type) } diff --git a/pkg/server/account.go b/pkg/server/account.go index 9599bfb..bb9daa2 100644 --- a/pkg/server/account.go +++ b/pkg/server/account.go @@ -1,8 +1,11 @@ package server type account struct { - id int - email []byte - username []byte - password []byte + id int + email []byte + username []byte + password []byte + highlight bool + pips bool + moves bool } diff --git a/pkg/server/client.go b/pkg/server/client.go index eb6db74..e779ffe 100644 --- a/pkg/server/client.go +++ b/pkg/server/client.go @@ -63,6 +63,8 @@ func (c *serverClient) sendEvent(e interface{}) { ev.Type = bgammon.EventTypeFailedOk case *bgammon.EventWin: ev.Type = bgammon.EventTypeWin + case *bgammon.EventSettings: + ev.Type = bgammon.EventTypeSettings default: log.Panicf("unknown event type %+v", ev) } diff --git a/pkg/server/database.go b/pkg/server/database.go index 476b5bc..ba7793e 100644 --- a/pkg/server/database.go +++ b/pkg/server/database.go @@ -32,7 +32,10 @@ CREATE TABLE account ( reset bigint NOT NULL DEFAULT 0, email text NOT NULL, username text NOT NULL, - password text NOT NULL + password text NOT NULL, + highlight smallint NOT NULL DEFAULT 1, + pips smallint NOT NULL DEFAULT 1, + moves smallint NOT NULL DEFAULT 0 ); CREATE TABLE game ( id serial PRIMARY KEY, @@ -105,7 +108,7 @@ func initDB() { log.Println("Initialized database schema") } -func registerAccount(a *account) error { +func registerAccount(passwordSalt string, a *account) error { if db == nil { return nil } else if len(bytes.TrimSpace(a.username)) == 0 { @@ -143,7 +146,7 @@ func registerAccount(a *account) error { return fmt.Errorf("username already in use") } - passwordHash, err := argon2id.CreateHash(string(a.password), passwordArgon2id) + passwordHash, err := argon2id.CreateHash(string(a.password)+passwordSalt, passwordArgon2id) if err != nil { return err } @@ -192,7 +195,7 @@ func resetAccount(mailServer string, resetSalt string, email []byte) error { timestamp := time.Now().Unix() h := sha256.New() - h.Write([]byte(fmt.Sprintf("%d", timestamp) + resetSalt)) + h.Write([]byte(fmt.Sprintf("%d/%d", id, timestamp) + resetSalt)) hash := fmt.Sprintf("%x", h.Sum(nil))[0:16] emailConfig := hermes.Hermes{ @@ -274,7 +277,7 @@ func confirmResetAccount(resetSalt string, passwordSalt string, id int, key stri } h := sha256.New() - h.Write([]byte(fmt.Sprintf("%d", reset) + resetSalt)) + h.Write([]byte(fmt.Sprintf("%d/%d", id, reset) + resetSalt)) hash := fmt.Sprintf("%x", h.Sum(nil))[0:16] if key != hash { return "", nil @@ -291,7 +294,7 @@ func confirmResetAccount(resetSalt string, passwordSalt string, id int, key stri return newPassword, err } -func loginAccount(username []byte, password []byte) (*account, error) { +func loginAccount(passwordSalt string, username []byte, password []byte) (*account, error) { if db == nil { return nil, nil } else if len(bytes.TrimSpace(username)) == 0 { @@ -306,21 +309,25 @@ func loginAccount(username []byte, password []byte) (*account, error) { } 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) + a := &account{} + var highlight, pips, moves int + err = tx.QueryRow(context.Background(), "SELECT id, email, username, password, highlight, pips, moves FROM account WHERE username = $1 OR email = $2", bytes.ToLower(bytes.TrimSpace(username)), bytes.ToLower(bytes.TrimSpace(username))).Scan(&a.id, &a.email, &a.username, &a.password, &highlight, &pips, &moves) if err != nil { return nil, nil - } else if len(account.password) == 0 { + } else if len(a.password) == 0 { return nil, fmt.Errorf("account disabled") } + a.highlight = highlight == 1 + a.pips = pips == 1 + a.moves = moves == 1 - match, err := argon2id.ComparePasswordAndHash(string(password), string(account.password)) + match, err := argon2id.ComparePasswordAndHash(string(password)+passwordSalt, string(a.password)) if err != nil { return nil, err } else if !match { return nil, nil } - return account, nil + return a, nil } func setAccountPassword(passwordSalt string, id int, password string) error { @@ -355,6 +362,31 @@ func setAccountPassword(passwordSalt string, id int, password string) error { return err } +func setAccountSetting(id int, name string, value int) error { + if db == nil { + return nil + } else if name == "" { + return fmt.Errorf("no setting name provided") + } + + 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 id = $1", id).Scan(&result) + if err != nil { + return err + } else if result == 0 { + return nil + } + + _, err = tx.Exec(context.Background(), "UPDATE account SET "+name+" = $1 WHERE id = $2", value, id) + return err +} + 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 diff --git a/pkg/server/database_disabled.go b/pkg/server/database_disabled.go index 8c17966..af21cc3 100644 --- a/pkg/server/database_disabled.go +++ b/pkg/server/database_disabled.go @@ -19,7 +19,7 @@ func testDBConnection() error { func initDB() { } -func registerAccount(a *account) error { +func registerAccount(passwordSalt string, a *account) error { return nil } @@ -31,7 +31,7 @@ func confirmResetAccount(resetSalt string, passwordSalt string, id int, key stri return "", nil } -func loginAccount(username []byte, password []byte) (*account, error) { +func loginAccount(passwordSalt string, username []byte, password []byte) (*account, error) { return nil, nil } @@ -39,6 +39,10 @@ func setAccountPassword(passwordSalt string, id int, password string) error { return nil } +func setAccountSetting(id int, name string, value int) error { + return nil +} + func recordGameResult(g *bgammon.Game, winType int, account1 int, account2 int) error { return nil } diff --git a/pkg/server/server.go b/pkg/server/server.go index a2adb7c..4dfc069 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -550,13 +550,13 @@ COMMANDS: cmd.client.Terminate("Failed to register: Invalid username: must contain at least one non-numeric character.") continue } - password = bytes.ReplaceAll(password, []byte("_"), []byte(" ")) + password = bytes.ReplaceAll(password, []byte(" "), []byte("_")) a := &account{ email: email, username: username, - password: append(password, []byte(s.passwordSalt)...), + password: password, } - err := registerAccount(a) + err := registerAccount(s.passwordSalt, a) if err != nil { cmd.client.Terminate(fmt.Sprintf("Failed to register: %s", err)) continue @@ -595,14 +595,14 @@ COMMANDS: continue } if len(params) > 2 { - password = bytes.ReplaceAll(bytes.Join(params[2:], []byte(" ")), []byte("_"), []byte(" ")) + password = bytes.ReplaceAll(bytes.Join(params[2:], []byte(" ")), []byte(" "), []byte("_")) } s.clientsLock.Unlock() } if len(password) > 0 { - a, err := loginAccount(username, append(password, []byte(s.passwordSalt)...)) + a, err := loginAccount(s.passwordSalt, username, password) if err != nil { cmd.client.Terminate(fmt.Sprintf("Failed to log in: %s", err)) continue @@ -612,6 +612,11 @@ COMMANDS: } cmd.client.account = a.id cmd.client.name = a.username + cmd.client.sendEvent(&bgammon.EventSettings{ + Highlight: a.highlight, + Pips: a.pips, + Moves: a.moves, + }) } else { cmd.client.account = 0 if !randomUsername && !bytes.HasPrefix(username, []byte("BOT_")) && !bytes.HasPrefix(username, []byte("Guest_")) { @@ -1486,7 +1491,7 @@ COMMANDS: continue } - a, err := loginAccount(cmd.client.name, append(params[0], []byte(s.passwordSalt)...)) + a, err := loginAccount(s.passwordSalt, cmd.client.name, params[0]) if err != nil || a == nil || a.id == 0 { cmd.client.sendNotice("Failed to change password: incorrect existing password.") continue @@ -1498,6 +1503,26 @@ COMMANDS: continue } cmd.client.sendNotice("Password changed successfully.") + case bgammon.CommandSet: + if cmd.client.account == 0 { + continue + } else if len(params) < 2 { + cmd.client.sendNotice("Please specify the setting name and value as follows: set ") + continue + } + + name := string(bytes.ToLower(params[0])) + if name != "highlight" && name != "pips" && name != "moves" { + cmd.client.sendNotice("Please specify the setting name and value as follows: set ") + continue + } + + value, err := strconv.Atoi(string(params[1])) + if err != nil || value < 0 { + cmd.client.sendNotice("Invalid setting value provided.") + continue + } + _ = setAccountSetting(cmd.client.account, name, value) case bgammon.CommandDisconnect: if clientGame != nil { clientGame.removeClient(cmd.client)