diff --git a/.env.sample b/.env.sample index 9741867..6d381f8 100644 --- a/.env.sample +++ b/.env.sample @@ -1,8 +1,8 @@ BOT_TOKEN="XXX0XXXxXxX0XXXxXxXxXXX0Xx.XxXXXX.xxxxxXxXXxXxxXxXxXXxX0XxX00xxXx0xxX00x" # Bot token, found in developer portal: https://discord.com/developers/applications GUILD_ID="000000000000000000" # Server ID for bot to join. RCON_ADDRESS="127.0.0.1:25565" # Server RCON Address. Port must be supplied. -RCON_PASSWORD="chicken" # Server RCON Password +RCON_PASSWORD="hunter2" # Server RCON Password ADMIN="000000000000000000" # UserID of Admin user. Only 1 ID accepted -START_SERVER_PATH="./startserversample.bat" # Path to .bat file that launches the game. Currently Windows only SERVER_ADDRESS="" # Optional Server Address argument. If not provided will instead respond with host IP. +SERVER_PORT="" # Optional Server Port argument. PLAYER_LIST=[IGN1:NN1,IGN2:NN2,IGN3:NN3] # Optional player list. Format is In game name(IGN):Nickname(NN). If IGN:NN combo is not provided then it will just print the IGN \ No newline at end of file diff --git a/README.md b/README.md index 587bad6..70b2bcb 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,44 @@ # DiscordMinecraftHelper -This is a small self hosted Discord bot designed to monitor a Minecraft server. It features player count monitoring, server status in the sidebar, structured logging, and automatic daily restarts at 2AM. Currently only Windows compatible. +This is a small self hosted Discord bot designed to monitor a Minecraft server. It features player count monitoring, server status in the sidebar, and structured logging. -## Requirements +## Running the Bot -`go >= 1.21` +If you're just looking for a `docker-compose.yml` or `.exe` to run then head on over to the releases. You'll find ZIPs of both, as well as an `.env.sample` and a README that is a copy of this one. -To use the daily restarts and `/restart-server` command, you need a bat file which automatically restarts the server upon shutdown. The All The Mods 9 modpack has one, but all you need is to add this snippet to your `startserver.bat`, around the existing code that launches the `server.jar` +### Docker Compose -```bat -:START -:: Code that launches server jar -echo Restarting automatically in 10 seconds (press Ctrl + C to cancel) -timeout /t 10 /nobreak > NUL -goto:START -``` - -and you're good to go! - -## Running the bot - -**If you're just looking for an `exe` to run head over to the releases and download the latest zip. It has a README inside that will help you get started.** +I'm not sure the best way to do this so this is my docker-compose.yml that I am currently using. -```go run main.go``` - -## Setup +```YAML +services: + bot: + container_name: bot + image: "scidaj57/minecraft-helper" + ports: + - "8080:8080" + env_file: + - "./.env" +``` -1. Clone this repo ```git clone git@github.com:ScidaJ/DiscordMinecraftHelper.git``` -2. CD into the new directory ```cd DiscordMinecraftHelper``` -3. Install dependencies ```go mod download``` -4. Make a copy of `.env.sample` and rename to `.env`. The variables in that file are explained [further on.](#.env) +You can also define your environment variables within the `docker-compose.yml` like so + +```YAML +services: + bot: + container_name: bot + image: "scidaj57/minecraft-helper" + ports: + - "8080:8080" + environment: + BOT_TOKEN: "XXX0XXXxXxX0XXXxXxXxXXX0Xx" + GUILD_ID: "000000000000000000" + RCON_ADDRESS: "127.0.0.1:25565" + RCON_PASSWORD: "hunter2" + ADMIN: "000000000000000000" +``` -## Everything Else +## Discord Setup This requires making an application with Discord on the Discord Developer Portal, found [here.](https://discord.com/developers/applications) There are a few pieces of information that we need from there, so lets go over what they are. @@ -62,15 +69,14 @@ This will be a quick overview of the variables in the `.env` file. * `RCON_ADDRESS` - This is set in your `server.properties` file or similar. Port must be supplied with the address. * `RCON_PASSWORD` - This is set in your `server.properties` file or similar. * `ADMIN` - The User ID of the "Admin" user for the bot/server. They will be pinged if there is an issue with the server. -* `START_SERVER_PATH` The path to your `startserver.bat` file, needed for `/start` and `/restart` commands, as well as the auto-restarting. * `SERVER_ADDRESS` Optional. The `/address` command just returns the IP of the host machine, as this bot is assuming that the server and bot are running on the same machine. If this variable is filled in then it will instead return this value. +* `SERVER_PORT` Optional. * `PLAYER_LIST` Optional. For use with `/list` command. If value is provided in the format of `[InGameName1:Nickname1,InGameName2:Nickname2,InGameName3:Nickname3]` then it will replace the in game name with the provided nickname in the list. If no nickname is provided then it will print the in game name instead. ## Commands -The bot only has four commands as it is fairly simple in scope. They are listed below +The bot only has three commands as it is fairly simple in scope. They are listed below * `/address` Prints out the address of the server. As the bot assumes that the server is running on the same machine it will return the IP of the host machine. **If you do not want this to be the case then fill in the `SERVER_ADDRESS` variable in the `.env` file. It will print that value instead.** * `/list` List the players currently on the server. If the `.env` variable `PLAYER_LIST` is populated the bot will replace any matching usernames with the corresponding nickname. -* `/restart` Sends the `/stop` command to the server. When paired with the batch script given above the server will restart after 10 seconds. -* `/start` Starts the server by executing the given batch file in `START_SERVER_PATH` +* `/restart` Sends the `/stop` command to the server then checks every 30 seconds for 5 minutes if it has relaunched. You must have some way of restarting your server automatically. diff --git a/bot/bot.go b/bot/bot.go deleted file mode 100644 index 3d24e5b..0000000 --- a/bot/bot.go +++ /dev/null @@ -1,94 +0,0 @@ -package bot - -import ( - botrcon "DiscordMinecraftHelper/server" - "fmt" - - "github.com/bwmarrin/discordgo" - "github.com/go-co-op/gocron/v2" -) - -func AddCronJobs(c gocron.Scheduler, server botrcon.Server) { - c.NewJob( - gocron.DailyJob( - 1, - gocron.NewAtTimes( - gocron.NewAtTime(2, 0, 0), - ), - ), - gocron.NewTask( - func() { server.DailyRestart() }, - ), - ) - c.NewJob( - gocron.DailyJob( - 1, - gocron.NewAtTimes( - gocron.NewAtTime(1, 55, 0), - ), - ), - gocron.NewTask( - func() { - conn, err := server.RconConnect() - if err != nil { - return - } else { - conn.Execute("/say Server will restart in 5 minutes") - } - }, - ), - ) - c.NewJob( - gocron.DailyJob( - 1, - gocron.NewAtTimes( - gocron.NewAtTime(1, 30, 0), - ), - ), - gocron.NewTask( - func() { - conn, err := server.RconConnect() - if err != nil { - return - } else { - conn.Execute("/say Server will restart in 30 minutes") - } - }, - ), - ) -} - -func UpdateBotStatus(s *discordgo.Session, server botrcon.Server) { - playerCount, _ := server.GetPlayerCount() - if server.ServerRunning() { - activity := discordgo.Activity{ - Name: fmt.Sprintf("Players: %v online", playerCount), - Type: discordgo.ActivityTypeWatching, - State: "Online", - Details: fmt.Sprintf("%v player(s) online!", playerCount), - } - presence := discordgo.UpdateStatusData{ - Activities: []*discordgo.Activity{ - &activity, - }, - Status: string(discordgo.StatusOnline), - AFK: false, - } - s.UpdateStatusComplex(presence) - } else { - activity := discordgo.Activity{ - Name: "Server offline", - Type: discordgo.ActivityTypeWatching, - State: "Offline", - Details: "Server offline", - } - presence := discordgo.UpdateStatusData{ - Activities: []*discordgo.Activity{ - &activity, - }, - Status: string(discordgo.StatusOnline), - AFK: false, - } - s.UpdateStatusComplex(presence) - } -} diff --git a/build/Dockerfile b/build/Dockerfile new file mode 100644 index 0000000..8c0787c --- /dev/null +++ b/build/Dockerfile @@ -0,0 +1,16 @@ +# syntax=docker/dockerfile:1 +FROM golang:latest AS build +WORKDIR /app +COPY go.mod go.sum main.go /app/ +COPY internal/ /app/internal +RUN go mod download +RUN CGO_ENABLED=0 GOOS=linux go build -o /bin/bot ./main.go + +FROM alpine:latest AS certs +RUN apk --update add ca-certificates + +FROM scratch +COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=build /bin/bot /bin/bot +EXPOSE 8080 +CMD [ "/bin/bot" ] \ No newline at end of file diff --git a/go.mod b/go.mod index b335e30..c98f391 100644 --- a/go.mod +++ b/go.mod @@ -4,17 +4,12 @@ go 1.22.3 require ( github.com/bwmarrin/discordgo v0.28.1 - github.com/go-co-op/gocron/v2 v2.5.0 github.com/gorcon/rcon v1.3.5 github.com/joho/godotenv v1.5.1 ) require ( - github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect - github.com/jonboulle/clockwork v0.4.0 // indirect - github.com/robfig/cron/v3 v3.0.1 // indirect golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect - golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect ) diff --git a/go.sum b/go.sum index 1a7c174..c2c7267 100644 --- a/go.sum +++ b/go.sum @@ -1,36 +1,16 @@ github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4= github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-co-op/gocron/v2 v2.5.0 h1:ff/TJX9GdTJBDL1il9cyd/Sj3WnS+BB7ZzwHKSNL5p8= -github.com/go-co-op/gocron/v2 v2.5.0/go.mod h1:ckPQw96ZuZLRUGu88vVpd9a6d9HakI14KWahFZtGvNw= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorcon/rcon v1.3.5 h1:YE/Vrw6R99uEP08wp0EjdPAP3Jwz/ys3J8qxI1nYoeU= github.com/gorcon/rcon v1.3.5/go.mod h1:zR1qfKZttF8vAgH1NsP6CdpachOvLDq8jE64NboTpIM= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= -github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= -github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/bot/bot.go b/internal/bot/bot.go new file mode 100644 index 0000000..d04b70c --- /dev/null +++ b/internal/bot/bot.go @@ -0,0 +1,43 @@ +package bot + +import ( + botrcon "DiscordMinecraftHelper/internal/server" + "fmt" + + "github.com/bwmarrin/discordgo" +) + +func UpdateBotStatus(s *discordgo.Session, server *botrcon.Server) { + playerCount, _ := server.GetPlayerCount() + if server.ServerRunning() { + activity := discordgo.Activity{ + Name: fmt.Sprintf("Players: %v online", playerCount), + Type: discordgo.ActivityTypeWatching, + State: "Online", + Details: fmt.Sprintf("%v player(s) online!", playerCount), + } + presence := discordgo.UpdateStatusData{ + Activities: []*discordgo.Activity{ + &activity, + }, + Status: string(discordgo.StatusOnline), + AFK: false, + } + s.UpdateStatusComplex(presence) + } else { + activity := discordgo.Activity{ + Name: "Server offline", + Type: discordgo.ActivityTypeWatching, + State: "Offline", + Details: "Server offline", + } + presence := discordgo.UpdateStatusData{ + Activities: []*discordgo.Activity{ + &activity, + }, + Status: string(discordgo.StatusOnline), + AFK: false, + } + s.UpdateStatusComplex(presence) + } +} diff --git a/commands/commands.go b/internal/commands/commands.go similarity index 75% rename from commands/commands.go rename to internal/commands/commands.go index fca1fc7..fddeeff 100644 --- a/commands/commands.go +++ b/internal/commands/commands.go @@ -1,7 +1,7 @@ package commands import ( - botrcon "DiscordMinecraftHelper/server" + botrcon "DiscordMinecraftHelper/internal/server" "log/slog" "github.com/bwmarrin/discordgo" @@ -14,32 +14,28 @@ type ( Options []*discordgo.ApplicationCommandOption } - HandleFunc func(s *discordgo.Session, i *discordgo.InteractionCreate, g botrcon.Server) + HandleFunc func(s *discordgo.Session, i *discordgo.InteractionCreate, g *botrcon.Server) ) var ( + Address = SlashCommand{ + Name: "address", + Description: "Return the current server IP + port.", + } List = SlashCommand{ Name: "list", Description: "List the players currently active on the server.", } - Start = SlashCommand{ - Name: "start", - Description: "Starts the Minecraft server. Will not restart it if already started.", - } Restart = SlashCommand{ Name: "restart", - Description: "Restarts the Minecraft server manually. This is done every night automatically.", - } - Address = SlashCommand{ - Name: "address", - Description: "Return the current server IP + port.", + Description: "Restarts the Minecraft server manually. Admin user only.", } ) -func AddCommandHandlers(s *discordgo.Session, server botrcon.Server, logger *slog.Logger) { +func AddCommandHandlers(s *discordgo.Session, server *botrcon.Server, logger *slog.Logger) { s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { commandHandlers := GetCommandsHandlers() - logger.Info("command received", "command", i.ApplicationCommandData().Name, "user", i.Member.User.Username) + logger.Info("command received", "command", i.ApplicationCommandData().Name) if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok { h(s, i, server) } @@ -48,10 +44,9 @@ func AddCommandHandlers(s *discordgo.Session, server botrcon.Server, logger *slo func GetCommands() []SlashCommand { return []SlashCommand{ + Address, List, - Start, Restart, - Address, } } @@ -60,10 +55,9 @@ func GetCommands() []SlashCommand { // not be registered. func GetCommandsHandlers() map[string]HandleFunc { return map[string]HandleFunc{ - "list": PlayerListHandler, - "restart": RestartServerHandler, - "start": StartServerHandler, - "address": ServerAddressHandler, + "address": AddressHandler, + "list": ListHandler, + "restart": RestartHandler, } } diff --git a/commands/handlers.go b/internal/commands/handlers.go similarity index 50% rename from commands/handlers.go rename to internal/commands/handlers.go index 2e74e91..dd792aa 100644 --- a/commands/handlers.go +++ b/internal/commands/handlers.go @@ -1,16 +1,45 @@ package commands import ( - "DiscordMinecraftHelper/bot" - botrcon "DiscordMinecraftHelper/server" + "DiscordMinecraftHelper/internal/bot" + botrcon "DiscordMinecraftHelper/internal/server" "fmt" "os" "github.com/bwmarrin/discordgo" ) -func PlayerListHandler(s *discordgo.Session, i *discordgo.InteractionCreate, g botrcon.Server) { - g.Logger = g.Logger.With("command", "list") +func AddressHandler(s *discordgo.Session, i *discordgo.InteractionCreate, g *botrcon.Server) { + message := "" + + conn, err := g.RconConnect() + if err != nil { + message += "The server is not running. " + } else { + conn.Close() + } + address := g.GetServerAddress() + if len(address) == 0 { + message += "Error retrieving server address. Service may be down." + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: message, + }, + }) + notifyAdmin(s, i.ChannelID) + } else { + message += fmt.Sprintf("Server Address: %v", address) + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: message, + }, + }) + } +} + +func ListHandler(s *discordgo.Session, i *discordgo.InteractionCreate, g *botrcon.Server) { if !g.ServerRunning() { s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, @@ -29,6 +58,7 @@ func PlayerListHandler(s *discordgo.Session, i *discordgo.InteractionCreate, g b Content: err.Error(), }, }) + notifyAdmin(s, i.ChannelID) } else { s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, @@ -39,120 +69,62 @@ func PlayerListHandler(s *discordgo.Session, i *discordgo.InteractionCreate, g b } } -func RestartServerHandler(s *discordgo.Session, i *discordgo.InteractionCreate, g botrcon.Server) { - g.Logger = g.Logger.With("command", "restart") +func RestartHandler(s *discordgo.Session, i *discordgo.InteractionCreate, g *botrcon.Server) { bot.UpdateBotStatus(s, g) - conn, err := g.RconConnect() + if i.Member.User.ID == os.Getenv("ADMIN") { - if err != nil { - if err.Error() == "server offline" { + conn, err := g.RconConnect() + + if err != nil { + if err.Error() == "server offline" { + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Server is offline.", + }, + }) + } else { + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: "Unable to restart server.", + }, + }) + } + return + } + + _, err = conn.Execute("/say The server will restart in 10 seconds") + if err != nil { s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ - Content: "Server is offline. Attempting to start server.", + Content: "Unable send warning message.", }, }) - StartServerHandler(s, i, g) - return } else { s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ - Content: "Unable to restart server.", + Content: "Restarting server in 10 seconds. Please wait at least 5 minutes before attempting to restart the server again.", }, }) - } - notifyAdmin(s, i.ChannelID) - return - } - _, err = conn.Execute("/say The server will restart in 10 seconds") - if err != nil { - s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: "Unable to restart server.", - }, - }) - notifyAdmin(s, i.ChannelID) - } else { - s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: "Restarting server in 10 seconds. Please wait at least 5 minutes before attempting to restart the server again. If something went wrong then I'll notify the admin.", - }, - }) + err = g.RestartServer(conn) + if err != nil { + g.Logger.Warn(err.Error()) + } - err = g.RestartServer(conn) - if err != nil { - notifyAdmin(s, i.ChannelID) + conn.Close() + s.ChannelMessageSend(i.ChannelID, "Server has restarted.") } - - conn.Close() - s.ChannelMessageSend(i.ChannelID, "Server has restarted.") - } - bot.UpdateBotStatus(s, g) -} - -func StartServerHandler(s *discordgo.Session, i *discordgo.InteractionCreate, g botrcon.Server) { - g.Logger = g.Logger.With("command", "start") - bot.UpdateBotStatus(s, g) - - conn, _ := g.RconConnect() - if conn != nil { - s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: "Server is already running.", - }, - }) - conn.Close() - return - } - - s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: "Starting server", - }, - }) - - err := g.StartServer() - if err != nil { - notifyAdmin(s, i.ChannelID) - return - } - s.ChannelMessageSend(i.ChannelID, "Server has started.") - bot.UpdateBotStatus(s, g) -} - -func ServerAddressHandler(s *discordgo.Session, i *discordgo.InteractionCreate, g botrcon.Server) { - g.Logger = g.Logger.With("command", "address") - message := "" - - conn, err := g.RconConnect() - if err != nil { - message += "The server is not running. " - } else { - conn.Close() - } - address := g.GetServerAddress() - if len(address) == 0 { - message += "Error retrieving server address. Service may be down." - s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: message, - }, - }) - g.Logger.Warn("error", err) + bot.UpdateBotStatus(s, g) } else { - message += fmt.Sprintf("Server Address: %v:25565", address) s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ - Content: message, + Content: "Command is locked to admin user.", }, }) } @@ -161,7 +133,7 @@ func ServerAddressHandler(s *discordgo.Session, i *discordgo.InteractionCreate, func notifyAdmin(s *discordgo.Session, c string) { admin, ok := os.LookupEnv("ADMIN") if !ok { - s.ChannelMessageSend(c, "There is a problem with the server.") + s.ChannelMessageSend(c, "admin user not found.") return } diff --git a/internal/server/env.go b/internal/server/env.go new file mode 100644 index 0000000..e6cb167 --- /dev/null +++ b/internal/server/env.go @@ -0,0 +1,50 @@ +package botrcon + +import ( + "fmt" + "os" + "strings" +) + +type ServerEnv struct { + PLAYER_LIST map[string]string + RCON_ADDRESS string + RCON_PASSWORD string + SERVER_ADDRESS string +} + +func NewServerEnv() ServerEnv { + return ServerEnv{ + PLAYER_LIST: loadPlayerList(), + RCON_ADDRESS: os.Getenv("RCON_ADDRESS"), + RCON_PASSWORD: os.Getenv("RCON_PASSWORD"), + SERVER_ADDRESS: serverAddressParser(), + } +} + +func serverAddressParser() string { + serverAddress, exists := os.LookupEnv("SERVER_ADDRESS") + if !exists || serverAddress == "" { + return "" + } + serverPort, exists := os.LookupEnv("SERVER_PORT") + if !exists || serverPort == "" { + return serverAddress + } else { + return fmt.Sprintf("%v:%v", serverAddress, serverPort) + } +} + +func loadPlayerList() map[string]string { + playerList := map[string]string{} + + playersString, _ := os.LookupEnv("PLAYER_LIST") + playersSlice := strings.Split(playersString, ",") + + for _, v := range playersSlice { + player := strings.Split(v, ":") + playerList[player[0]] = player[1] + } + + return playerList +} diff --git a/server/server.go b/internal/server/server.go similarity index 70% rename from server/server.go rename to internal/server/server.go index 2445e47..0026470 100644 --- a/server/server.go +++ b/internal/server/server.go @@ -1,5 +1,3 @@ -// @TODO: Capture CMD process on start. Adjust restart function. Add /stop command with admin protection -// @TODO: Add StopServer function which sends SIGINT to Cmd process. V2 of bot package botrcon import ( @@ -8,7 +6,6 @@ import ( "io" "log/slog" "net/http" - "os/exec" "strconv" "strings" "time" @@ -21,22 +18,7 @@ type Server struct { Env ServerEnv } -func (s Server) DailyRestart() { - sLogger := s.Logger.With("function", "daily_restart") - if s.ServerRunning() { - conn, err := s.RconConnect() - if err != nil { - return - } - - err = s.RestartServer(conn) - if err != nil { - sLogger.Warn("error restarting server", "error", err) - } - } -} - -func (s Server) GetPlayerCount() (int, error) { +func (s *Server) GetPlayerCount() (int, error) { sLogger := s.Logger.With("function", "get_player_count") conn, err := s.RconConnect() if err != nil { @@ -58,7 +40,7 @@ func (s Server) GetPlayerCount() (int, error) { } // This assumes that the bot is running on the same machine as the server. If SERVER_ADDRESS is supplied then it will return that. -func (s Server) GetServerAddress() string { +func (s *Server) GetServerAddress() string { sLogger := s.Logger.With("function", "get_server_address") if s.Env.SERVER_ADDRESS != "" { @@ -80,7 +62,7 @@ func (s Server) GetServerAddress() string { return string(body) } -func (s Server) ListPlayers() (string, error) { +func (s *Server) ListPlayers() (string, error) { sLogger := s.Logger.With("function", "get_player_count") conn, err := s.RconConnect() if err != nil { @@ -113,19 +95,19 @@ func (s Server) ListPlayers() (string, error) { } } -func (s Server) RconConnect() (*rcon.Conn, error) { +func (s *Server) RconConnect() (*rcon.Conn, error) { conn, err := rcon.Dial(s.Env.RCON_ADDRESS, s.Env.RCON_PASSWORD) if err != nil { + s.Logger.Info(err.Error()) return nil, errors.New("server offline") } return conn, nil } -func (s Server) RestartServer(conn *rcon.Conn) error { +func (s *Server) RestartServer(conn *rcon.Conn) error { sLogger := s.Logger.With("function", "restart_server") sLogger.Info("restarting server") - time.Sleep(10 * time.Second) _, err := conn.Execute("/stop") if err != nil { @@ -151,7 +133,7 @@ func (s Server) RestartServer(conn *rcon.Conn) error { return nil } -func (s Server) ServerRunning() bool { +func (s *Server) ServerRunning() bool { conn, err := s.RconConnect() if err != nil { return false @@ -162,31 +144,7 @@ func (s Server) ServerRunning() bool { return conn != nil } -func (s Server) StartServer() error { - sLogger := s.Logger.With("function", "start_server") - if !s.ServerRunning() { - c := exec.Command("cmd.exe", "/C", "Start", s.Env.START_SERVER_PATH) - err := c.Start() - if err != nil { - sLogger.Warn("unable to start server", "error", err) - return err - } - - time.Sleep(5 * time.Minute) - - conn, err := s.RconConnect() - if err != nil { - sLogger.Warn("unable to start server", "error", err) - return err - } - conn.Close() - - return nil - } - return nil -} - -func (s Server) nameDecoder(usernameList []string) (string, error) { +func (s *Server) nameDecoder(usernameList []string) (string, error) { var nameList strings.Builder for _, v := range usernameList { diff --git a/main.go b/main.go index c8d7fec..a9bca90 100644 --- a/main.go +++ b/main.go @@ -4,15 +4,15 @@ import ( "log/slog" "os" "os/signal" + "syscall" "time" "github.com/bwmarrin/discordgo" - "github.com/go-co-op/gocron/v2" "github.com/joho/godotenv" - "DiscordMinecraftHelper/bot" - "DiscordMinecraftHelper/commands" - botrcon "DiscordMinecraftHelper/server" + "DiscordMinecraftHelper/internal/bot" + "DiscordMinecraftHelper/internal/commands" + botrcon "DiscordMinecraftHelper/internal/server" ) var GuildID string @@ -61,29 +61,19 @@ func main() { } // Slash command init - commands.AddCommandHandlers(s, server, Logger) + commands.AddCommandHandlers(s, &server, Logger) commands.RegisterCommands(s, GuildID, Logger) defer s.Close() + // Shutdown channel init stop := make(chan os.Signal, 1) - signal.Notify(stop, os.Interrupt) + signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) Logger.Info("press Ctrl+C to exit") - // Cron job init - c, err := gocron.NewScheduler(gocron.WithLocation(time.Local)) - if err != nil { - c.Shutdown() - Logger.Error("error starting cron scheduler", "error", err) - } - - bot.AddCronJobs(c, server) - - c.Start() - // Status init statusTicker := time.NewTicker(10 * time.Minute) - go func(s *discordgo.Session, server botrcon.Server) { + go func(s *discordgo.Session, server *botrcon.Server) { for { select { case <-statusTicker.C: @@ -92,15 +82,13 @@ func main() { return } } - }(s, server) + }(s, &server) - bot.UpdateBotStatus(s, server) + bot.UpdateBotStatus(s, &server) // Shutdown <-stop - c.Shutdown() - Logger.Info("stopping statusTicker") statusTicker.Stop() diff --git a/server/env.go b/server/env.go deleted file mode 100644 index 248c6e8..0000000 --- a/server/env.go +++ /dev/null @@ -1,44 +0,0 @@ -package botrcon - -import ( - "os" - "strings" -) - -type ServerEnv struct { - PLAYER_LIST map[string]string - RCON_ADDRESS string - RCON_PASSWORD string - START_SERVER_PATH string - SERVER_ADDRESS string -} - -func NewServerEnv() ServerEnv { - var env ServerEnv - - serverAddress, _ := os.LookupEnv("SERVER_ADDRESS") - - env = ServerEnv{ - PLAYER_LIST: loadPlayerList(), - RCON_ADDRESS: os.Getenv("RCON_ADDRESS"), - RCON_PASSWORD: os.Getenv("RCON_PASSWORD"), - START_SERVER_PATH: os.Getenv("START_SERVER_PATH"), - SERVER_ADDRESS: serverAddress, - } - - return env -} - -func loadPlayerList() map[string]string { - playerList := map[string]string{} - - playersString, _ := os.LookupEnv("PLAYER_LIST") - playersSlice := strings.Split(playersString, ",") - - for _, v := range playersSlice { - player := strings.Split(v, ":") - playerList[player[0]] = player[1] - } - - return playerList -}