parent
955912e903
commit
0ebc7d006b
9 changed files with 201 additions and 32 deletions
|
@ -118,6 +118,12 @@ formatted responses are more easily parsed by computers.
|
|||
- This command is not normally used, as the match state is provided in JSON format.
|
||||
- Aliases: `b`
|
||||
|
||||
- `follow <username>`
|
||||
- Follow a player. A notification is shown whenever a followed player goes online or offline.
|
||||
|
||||
- `unfollow <username>`
|
||||
- Un-follow a player.
|
||||
|
||||
- `pong <message>`
|
||||
- Sent in response to server `ping` event to prevent the connection from timing out.
|
||||
- Whether the client sends a `pong` command, or any other command, clients
|
||||
|
|
|
@ -26,6 +26,8 @@ const (
|
|||
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.
|
||||
CommandFollow = "follow" // Follow a player.
|
||||
CommandUnfollow = "unfollow" // Un-follow a player.
|
||||
CommandBoard = "board" // Print current board state in human-readable form.
|
||||
CommandPong = "pong" // Response to server ping.
|
||||
CommandDisconnect = "disconnect" // Disconnect from server.
|
||||
|
@ -80,6 +82,8 @@ var HelpText = map[string]string{
|
|||
CommandReset: "- Reset pending checker movement.",
|
||||
CommandOk: "[1-6] - Accept double offer or confirm checker movement. The parameter for this command only applies in acey-deucey games.",
|
||||
CommandRematch: "- Request (or accept) a rematch after a match has been finished.",
|
||||
CommandFollow: "- Follow a player. A notification is shown whenever a followed player goes online or offline.",
|
||||
CommandUnfollow: "- Un-follow a player.",
|
||||
CommandBoard: "- Request current match state.",
|
||||
CommandPong: "<message> - Sent in response to server ping event to prevent the connection from timing out.",
|
||||
CommandDisconnect: "- Disconnect from the server.",
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
package server
|
||||
|
||||
type account struct {
|
||||
id int
|
||||
email []byte
|
||||
username []byte
|
||||
password []byte
|
||||
|
||||
icon int
|
||||
icons []byte
|
||||
|
||||
autoplay bool
|
||||
highlight bool
|
||||
pips bool
|
||||
moves bool
|
||||
flip bool
|
||||
traditional bool
|
||||
advanced bool
|
||||
muteJoinLeave bool
|
||||
muteChat bool
|
||||
muteRoll bool
|
||||
muteMove bool
|
||||
muteBearOff bool
|
||||
speed int8
|
||||
|
||||
casual *clientRating
|
||||
competitive *clientRating
|
||||
}
|
|
@ -78,6 +78,17 @@ CREATE TABLE game (
|
|||
wintype integer NOT NULL,
|
||||
replay TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
CREATE TABLE follow (
|
||||
account integer NOT NULL,
|
||||
target integer NOT NULL,
|
||||
UNIQUE (account, target),
|
||||
CONSTRAINT follow_user
|
||||
FOREIGN KEY(account)
|
||||
REFERENCES account(id),
|
||||
CONSTRAINT follow_target
|
||||
FOREIGN KEY(target)
|
||||
REFERENCES account(id)
|
||||
);
|
||||
`
|
||||
|
||||
var (
|
||||
|
@ -461,6 +472,19 @@ func loginAccount(passwordSalt string, username []byte, password []byte) (*accou
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
var follows []byte
|
||||
err = tx.QueryRow(context.Background(), "select string_agg(target::text, ',') FROM follow WHERE account = $1", a.id).Scan(&follows)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
for _, target := range bytes.Split(follows, []byte(",")) {
|
||||
v, err := strconv.Atoi(string(target))
|
||||
if err != nil || v <= 0 {
|
||||
continue
|
||||
}
|
||||
a.follows = append(a.follows, v)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(context.Background(), "UPDATE account SET active = $1 WHERE id = $2", time.Now().Unix(), a.id)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
|
@ -532,6 +556,30 @@ func setAccountSetting(id int, name string, value int) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func setAccountFollows(id int, target int, follows bool) error {
|
||||
dbLock.Lock()
|
||||
defer dbLock.Unlock()
|
||||
|
||||
if db == nil {
|
||||
return nil
|
||||
} else if id == 0 || target == 0 {
|
||||
return fmt.Errorf("invalid id or target: %d/%d", id, target)
|
||||
}
|
||||
|
||||
tx, err := begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Commit(context.Background())
|
||||
|
||||
if !follows {
|
||||
_, err = tx.Exec(context.Background(), "DELETE FROM follow WHERE account = $1 AND target = $2", id, target)
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(context.Background(), "INSERT INTO follow VALUES ($1, $2)", id, target)
|
||||
return err
|
||||
}
|
||||
|
||||
func recordGameResult(g *serverGame, winType int8, replay [][]byte) error {
|
||||
dbLock.Lock()
|
||||
defer dbLock.Unlock()
|
||||
|
|
|
@ -5,6 +5,35 @@ const (
|
|||
matchTypeRated
|
||||
)
|
||||
|
||||
type account struct {
|
||||
id int
|
||||
email []byte
|
||||
username []byte
|
||||
password []byte
|
||||
|
||||
follows []int
|
||||
|
||||
icon int
|
||||
icons []byte
|
||||
|
||||
autoplay bool
|
||||
highlight bool
|
||||
pips bool
|
||||
moves bool
|
||||
flip bool
|
||||
traditional bool
|
||||
advanced bool
|
||||
muteJoinLeave bool
|
||||
muteChat bool
|
||||
muteRoll bool
|
||||
muteMove bool
|
||||
muteBearOff bool
|
||||
speed int8
|
||||
|
||||
casual *clientRating
|
||||
competitive *clientRating
|
||||
}
|
||||
|
||||
type leaderboardEntry struct {
|
||||
User string
|
||||
Rating int
|
||||
|
|
|
@ -51,6 +51,10 @@ func setAccountSetting(id int, name string, value int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func setAccountFollows(id int, target int, follows bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func matchInfo(id int) (timestamp int64, player1 string, player2 string, replay []byte, err error) {
|
||||
return 0, "", "", nil, nil
|
||||
}
|
||||
|
|
|
@ -10,15 +10,21 @@ msgstr ""
|
|||
msgid "%s accepted double."
|
||||
msgstr ""
|
||||
|
||||
msgid "%s resigned."
|
||||
msgstr ""
|
||||
|
||||
msgid "%s declined double offer."
|
||||
msgstr ""
|
||||
|
||||
msgid "%s disconnected."
|
||||
msgstr ""
|
||||
|
||||
msgid "%s is online."
|
||||
msgstr ""
|
||||
|
||||
msgid "%s offers a double (%d points)."
|
||||
msgstr ""
|
||||
|
||||
msgid "%s resigned."
|
||||
msgstr ""
|
||||
|
||||
msgid "Accepted double."
|
||||
msgstr ""
|
||||
|
||||
|
@ -160,12 +166,18 @@ msgstr ""
|
|||
msgid "Waiting for response from opponent."
|
||||
msgstr ""
|
||||
|
||||
msgid "You are no longer following %s."
|
||||
msgstr ""
|
||||
|
||||
msgid "You are not allowed to use that command."
|
||||
msgstr ""
|
||||
|
||||
msgid "You are not currently in a match."
|
||||
msgstr ""
|
||||
|
||||
msgid "You are now following %s."
|
||||
msgstr ""
|
||||
|
||||
msgid "You are spectating this match. Chat messages are not relayed."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -257,6 +257,19 @@ func (s *server) removeClient(c *serverClient) {
|
|||
return
|
||||
}
|
||||
}
|
||||
|
||||
if c.accountID != 0 {
|
||||
for _, sc := range s.clients {
|
||||
if sc.accountID <= 0 {
|
||||
continue
|
||||
}
|
||||
for _, target := range sc.account.follows {
|
||||
if c.accountID == target {
|
||||
sc.sendNotice(fmt.Sprintf(gotext.GetD(c.language, "%s disconnected."), c.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) handleGames() {
|
||||
|
|
|
@ -255,6 +255,27 @@ COMMANDS:
|
|||
cmd.client.sendNotice("Help translate this application into your preferred language at bgammon.org/translate")
|
||||
}
|
||||
|
||||
c := cmd.client
|
||||
if c.accountID != 0 {
|
||||
s.clientsLock.Lock()
|
||||
for _, sc := range s.clients {
|
||||
if sc.accountID <= 0 {
|
||||
continue
|
||||
}
|
||||
for _, target := range c.account.follows {
|
||||
if sc.accountID == target {
|
||||
c.sendNotice(fmt.Sprintf(gotext.GetD(c.language, "%s is online."), sc.name))
|
||||
}
|
||||
}
|
||||
for _, target := range sc.account.follows {
|
||||
if c.accountID == target {
|
||||
sc.sendNotice(fmt.Sprintf(gotext.GetD(c.language, "%s is online."), c.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
s.clientsLock.Unlock()
|
||||
}
|
||||
|
||||
// Rejoin match in progress.
|
||||
s.gamesLock.RLock()
|
||||
for _, g := range s.games {
|
||||
|
@ -284,7 +305,7 @@ COMMANDS:
|
|||
clientGame := s.gameByClient(cmd.client)
|
||||
if clientGame != nil && clientGame.client1 != cmd.client && clientGame.client2 != cmd.client {
|
||||
switch keyword {
|
||||
case bgammon.CommandHelp, "h", bgammon.CommandJSON, bgammon.CommandList, "ls", bgammon.CommandBoard, "b", bgammon.CommandLeave, "l", bgammon.CommandReplay, bgammon.CommandSet, bgammon.CommandPassword, bgammon.CommandPong, bgammon.CommandDisconnect, bgammon.CommandMOTD, bgammon.CommandBroadcast, bgammon.CommandShutdown:
|
||||
case bgammon.CommandHelp, "h", bgammon.CommandJSON, bgammon.CommandList, "ls", bgammon.CommandBoard, "b", bgammon.CommandLeave, "l", bgammon.CommandReplay, bgammon.CommandSet, bgammon.CommandPassword, bgammon.CommandFollow, bgammon.CommandUnfollow, bgammon.CommandPong, bgammon.CommandDisconnect, bgammon.CommandMOTD, bgammon.CommandBroadcast, bgammon.CommandShutdown:
|
||||
// These commands are allowed to be used by spectators.
|
||||
default:
|
||||
cmd.client.sendNotice(gotext.GetD(cmd.client.language, "Command ignored: You are spectating this match."))
|
||||
|
@ -1148,6 +1169,56 @@ COMMANDS:
|
|||
cmd.client.sendNotice(gotext.GetD(cmd.client.language, "Rematch offer sent."))
|
||||
continue
|
||||
}
|
||||
case bgammon.CommandFollow:
|
||||
if len(params) < 1 {
|
||||
cmd.client.sendNotice("Please specify a player: follow <username>")
|
||||
continue
|
||||
} else if cmd.client.accountID == 0 {
|
||||
cmd.client.sendNotice("Failed to follow player: Please log in before following.")
|
||||
continue
|
||||
}
|
||||
|
||||
target, err := accountByUsername(string(params[0]))
|
||||
if err != nil || target == nil || target.id == 0 {
|
||||
cmd.client.sendNotice("Failed to follow player: Invalid username.")
|
||||
continue
|
||||
} else if target.id == cmd.client.accountID {
|
||||
cmd.client.sendNotice("Following yourself will get you nowhere quickly.")
|
||||
continue
|
||||
}
|
||||
|
||||
err = setAccountFollows(cmd.client.accountID, target.id, true)
|
||||
if err != nil {
|
||||
cmd.client.sendNotice(fmt.Sprintf("You are already following %s.", target.username))
|
||||
continue
|
||||
}
|
||||
cmd.client.account.follows = append(cmd.client.account.follows, target.id)
|
||||
cmd.client.sendNotice(fmt.Sprintf(gotext.GetD(cmd.client.language, "You are now following %s."), target.username))
|
||||
case bgammon.CommandUnfollow:
|
||||
if len(params) < 1 {
|
||||
cmd.client.sendNotice("Please specify a player: unfollow <username>")
|
||||
continue
|
||||
} else if cmd.client.accountID == 0 {
|
||||
cmd.client.sendNotice("Failed to un-follow player: Please log in before un-following.")
|
||||
continue
|
||||
}
|
||||
|
||||
target, err := accountByUsername(string(params[0]))
|
||||
if err != nil || target == nil || target.id == 0 {
|
||||
cmd.client.sendNotice("Failed to un-follow player: Invalid username.")
|
||||
continue
|
||||
} else if target.id == cmd.client.accountID {
|
||||
cmd.client.sendNotice("Un-following yourself will get you somewhere slowly.")
|
||||
continue
|
||||
}
|
||||
|
||||
err = setAccountFollows(cmd.client.accountID, target.id, false)
|
||||
if err != nil {
|
||||
cmd.client.sendNotice(fmt.Sprintf("You are not following %s.", target.username))
|
||||
continue
|
||||
}
|
||||
cmd.client.account.follows = removeInt(cmd.client.account.follows, target.id)
|
||||
cmd.client.sendNotice(fmt.Sprintf(gotext.GetD(cmd.client.language, "You are no longer following %s."), target.username))
|
||||
case bgammon.CommandBoard, "b":
|
||||
if clientGame == nil {
|
||||
cmd.client.sendNotice(gotext.GetD(cmd.client.language, "You are not currently in a match."))
|
||||
|
@ -1378,3 +1449,13 @@ COMMANDS:
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeInt(s []int, v int) []int {
|
||||
for i, sv := range s {
|
||||
if sv == v {
|
||||
s[i] = s[len(s)-1]
|
||||
return s[:len(s)-1]
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue