Add broadcast and shutdown commands

Resolves #14.
This commit is contained in:
Trevor Slocum 2024-07-08 11:46:41 -07:00
parent 7930427638
commit 85e092b8cb
6 changed files with 121 additions and 6 deletions

View file

@ -123,6 +123,14 @@ must write some data to the server at least once every 40 seconds.
- `disconnect`
- Disconnect from the server.
- `broadcast`
- Send a message to all players.
- This command is only available to server administrators.
- `shutdown <minutes> <reason>`
- Prevent the creation of new matches and periodically warn players about the server shutting down.
- This command is only available to server administrators.
## Server events
All events are sent in either JSON or human-readable format. The structure of

View file

@ -29,6 +29,8 @@ const (
CommandBoard = "board" // Print current board state in human-readable form.
CommandPong = "pong" // Response to server ping.
CommandDisconnect = "disconnect" // Disconnect from server.
CommandBroadcast = "broadcast" // Send a message to all players.
CommandShutdown = "shutdown" // Prevent the creation of new matches.
)
type EventType string

View file

@ -9,6 +9,7 @@ import (
"time"
"code.rocket9labs.com/tslocum/bgammon"
"github.com/leonelquinteros/gotext"
)
type clientRating struct {
@ -84,6 +85,10 @@ type serverClient struct {
bgammon.Client
}
func (c *serverClient) Admin() bool {
return c.accountID == 1
}
func (c *serverClient) sendEvent(e interface{}) {
// JSON formatted messages.
if c.json {
@ -205,6 +210,12 @@ func (c *serverClient) sendNotice(message string) {
})
}
func (c *serverClient) sendBroadcast(message string) {
c.sendEvent(&bgammon.EventNotice{
Message: gotext.GetD(c.language, "SERVER BROADCAST:") + " " + message,
})
}
func (c *serverClient) label() string {
if len(c.name) > 0 {
return string(c.name)

View file

@ -55,6 +55,9 @@ msgstr ""
msgid "Failed to create match: Please leave the match you are in before creating another."
msgstr ""
msgid "Failed to create match: The server is shutting down. Reason: %s"
msgstr ""
msgid "Failed to log in: %s"
msgstr ""
@ -109,6 +112,9 @@ msgstr ""
msgid "Please enter an email, username and password."
msgstr ""
msgid "Please finish your match as soon as possible."
msgstr ""
msgid "Please leave the match you are in before joining another."
msgstr ""
@ -121,6 +127,9 @@ msgstr ""
msgid "Resigned."
msgstr ""
msgid "SERVER BROADCAST:"
msgstr ""
msgid "Server error"
msgstr ""
@ -133,6 +142,15 @@ msgstr ""
msgid "The match you are in is still in progress."
msgstr ""
msgid "The server is shutting down. Reason:"
msgstr ""
msgid "The server is shutting down in 1 minute. Reason:"
msgstr ""
msgid "The server is shutting down in %d minutes. Reason:"
msgstr ""
msgid "Unknown command: %s"
msgstr ""

View file

@ -86,6 +86,9 @@ type server struct {
relayChat bool // Chats are not relayed normally. This option is only used by local servers.
verbose bool
shutdownTime time.Time
shutdownReason string
}
func NewServer(tz string, dataSource string, mailServer string, passwordSalt string, resetSalt string, relayChat bool, verbose bool, allowDebug bool) *server {
@ -539,6 +542,44 @@ func (s *server) Analyze(g *bgammon.Game) {
os.Exit(0)
}
func (s *server) handleShutdown() {
var mins time.Duration
var minutes int
t := time.NewTicker(time.Minute)
for {
mins = time.Until(s.shutdownTime)
if mins > 0 {
minutes = int(mins.Minutes()) + 1
}
s.clientsLock.Lock()
for _, sc := range s.clients {
switch minutes {
case 0:
sc.sendBroadcast(gotext.GetD(sc.language, "The server is shutting down. Reason:"))
case 1:
sc.sendBroadcast(gotext.GetD(sc.language, "The server is shutting down in 1 minute. Reason:"))
default:
sc.sendBroadcast(gotext.GetD(sc.language, "The server is shutting down in %d minutes. Reason:", minutes))
}
sc.sendBroadcast(s.shutdownReason)
sc.sendBroadcast(gotext.GetD(sc.language, "Please finish your match as soon as possible."))
}
s.clientsLock.Unlock()
<-t.C
}
}
func (s *server) shutdown(delay time.Duration, reason string) {
if !s.shutdownTime.IsZero() {
return
}
s.shutdownTime = time.Now().Add(delay)
s.shutdownReason = reason
go s.handleShutdown()
}
func RandInt(max int) int {
i, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
if err != nil {

View file

@ -245,7 +245,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.CommandDisconnect, bgammon.CommandPong:
case bgammon.CommandHelp, "h", bgammon.CommandJSON, bgammon.CommandList, "ls", bgammon.CommandBoard, "b", bgammon.CommandLeave, "l", bgammon.CommandReplay, bgammon.CommandSet, bgammon.CommandPong, bgammon.CommandDisconnect, 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."))
@ -320,6 +320,9 @@ COMMANDS:
if clientGame != nil {
cmd.client.sendNotice(gotext.GetD(cmd.client.language, "Failed to create match: Please leave the match you are in before creating another."))
continue
} else if !s.shutdownTime.IsZero() {
cmd.client.sendNotice(gotext.GetD(cmd.client.language, "Failed to create match: The server is shutting down. Reason: %s", s.shutdownReason))
continue
}
sendUsage := func() {
@ -1232,20 +1235,52 @@ COMMANDS:
ev.CasualTabulaMulti = a.casual.tabulaMulti / 100
}
cmd.client.sendEvent(ev)
case bgammon.CommandPong:
// Do nothing.
case bgammon.CommandDisconnect:
if clientGame != nil {
clientGame.removeClient(cmd.client)
}
cmd.client.Terminate("Client disconnected")
case bgammon.CommandPong:
// Do nothing.
case bgammon.CommandBroadcast:
if !cmd.client.Admin() {
cmd.client.sendNotice("Access denied.")
continue
} else if len(params) == 0 {
cmd.client.sendNotice("Please specify a message to broadcast.")
continue
}
message := string(bytes.Join(params, []byte(" ")))
s.clientsLock.Lock()
for _, sc := range s.clients {
sc.sendBroadcast(message)
}
s.clientsLock.Unlock()
case bgammon.CommandShutdown:
if !cmd.client.Admin() {
cmd.client.sendNotice("Access denied.")
continue
} else if len(params) < 2 {
cmd.client.sendNotice("Please specify the number of minutes until shutdown and the reason.")
continue
} else if !s.shutdownTime.IsZero() {
cmd.client.sendNotice("Server shutdown already in progress.")
continue
}
minutes, err := strconv.Atoi(string(params[0]))
if err != nil || minutes <= 0 {
cmd.client.sendNotice("Error: Invalid shutdown delay.")
continue
}
s.shutdown(time.Duration(minutes)*time.Minute, string(bytes.Join(params[1:], []byte(" "))))
case "endgame":
if !allowDebugCommands {
cmd.client.sendNotice(gotext.GetD(cmd.client.language, "You are not allowed to use that command."))
continue
}
if clientGame == nil {
} else if clientGame == nil {
cmd.client.sendNotice(gotext.GetD(cmd.client.language, "You are not currently in a match."))
continue
}