Add follow and unfollow commands

Resolves #24.
This commit is contained in:
Trevor Slocum 2024-08-20 20:09:42 -07:00
parent 955912e903
commit 0ebc7d006b
9 changed files with 201 additions and 32 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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