├── .dockerignore ├── .gitignore ├── Dockerfile ├── bot └── bot.go ├── config.template.json ├── config └── config.go ├── docker-build.sh ├── docker-compose.yml ├── docker-push.sh ├── go.mod ├── go.sum ├── main.go ├── makefile ├── readme.md ├── readme_files ├── screenshot1.png └── screenshot2.gif └── serverstatus └── serverstatus.go /.dockerignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /dist 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | dist 3 | vendor 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22-alpine3.19 2 | 3 | WORKDIR /go/src/github.com/mgerb/ServerStatus 4 | ADD . . 5 | RUN apk add --no-cache git alpine-sdk 6 | RUN go get 7 | RUN make linux 8 | 9 | 10 | FROM alpine:3.19 11 | 12 | ARG UNAME="server-status" 13 | ARG GNAME="server-status" 14 | ARG UID=1000 15 | ARG GID=1000 16 | 17 | WORKDIR /server-status 18 | COPY --from=0 /go/src/github.com/mgerb/ServerStatus/dist/ServerStatus-linux . 19 | RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/* 20 | RUN addgroup -g ${GID} "${GNAME}" 21 | RUN adduser -D -u ${UID} -G "${GNAME}" "${UNAME}" &&\ 22 | chown "${UNAME}":"${GNAME}" -R /server-status/ 23 | 24 | USER ${UNAME} 25 | 26 | ENTRYPOINT ./ServerStatus-linux 27 | -------------------------------------------------------------------------------- /bot/bot.go: -------------------------------------------------------------------------------- 1 | package bot 2 | 3 | import ( 4 | "fmt" 5 | "github.com/bwmarrin/discordgo" 6 | ) 7 | 8 | // Variables used for command line parameters 9 | var ( 10 | BotID string 11 | Session *discordgo.Session 12 | ) 13 | 14 | func Connect(token string) { 15 | // Create a new Discord session using the provided bot token. 16 | var err error 17 | Session, err = discordgo.New("Bot " + token) 18 | 19 | if err != nil { 20 | fmt.Println("error creating Discord session,", err) 21 | return 22 | } 23 | 24 | // Get the account information. 25 | u, err := Session.User("@me") 26 | if err != nil { 27 | fmt.Println("error obtaining account details,", err) 28 | } 29 | 30 | // Store the account ID for later use. 31 | BotID = u.ID 32 | 33 | fmt.Println("Bot connected") 34 | } 35 | 36 | func Start() { 37 | // Open the websocket and begin listening. 38 | err := Session.Open() 39 | if err != nil { 40 | fmt.Println("error opening connection,", err) 41 | return 42 | } 43 | 44 | fmt.Println("Bot is now running. Press CTRL-C to exit.") 45 | 46 | return 47 | } 48 | 49 | func AddHandler(handler interface{}) { 50 | Session.AddHandler(handler) 51 | } 52 | -------------------------------------------------------------------------------- /config.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Token": "your bot token", 3 | "RoomIDList": ["room id list goes here"], 4 | "RolesToNotify": ["<@&roleid>", "<@userid>"], 5 | "GameStatus": "current playing game", 6 | "PollingInterval": 10, 7 | "Servers": [ 8 | { 9 | "Name": "Your awesome server", 10 | "Address": "game.server.com", 11 | "Port": 80 12 | }, 13 | { 14 | "Name": "Another awesome server", 15 | "Address": "awesome.server.com", 16 | "Port": 8080 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "time" 10 | ) 11 | 12 | // Variables used for command line parameters 13 | var Config configStruct 14 | 15 | type configStruct struct { 16 | Token string `json:"Token"` 17 | RoomIDList []string `json:"RoomIDList"` 18 | RolesToNotify []string `json:"RolesToNotify"` 19 | Servers []Server `json:"Servers"` 20 | GameStatus string `json:"GameStatus"` 21 | PollingInterval time.Duration `json:"PollingInterval"` 22 | } 23 | 24 | type Server struct { 25 | Name string `json:"Name"` 26 | Address string `json:"Address"` 27 | Port int `json:"Port"` 28 | Online bool `json:"Online,omitempty"` 29 | // OnlineTimestamp - time of when the server last came online 30 | OnlineTimestamp time.Time 31 | OfflineTimestamp time.Time 32 | } 33 | 34 | func Configure() { 35 | 36 | fmt.Println("Reading config file...") 37 | 38 | file, e := ioutil.ReadFile("./config.json") 39 | 40 | if e != nil { 41 | log.Printf("File error: %v\n", e) 42 | os.Exit(1) 43 | } 44 | 45 | err := json.Unmarshal(file, &Config) 46 | 47 | if err != nil { 48 | log.Println(err) 49 | } 50 | 51 | if Config.PollingInterval == 0 { 52 | log.Fatal("Please set your PollingInterval > 0 in your config file.") 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /docker-build.sh: -------------------------------------------------------------------------------- 1 | version=$(git describe --tags) 2 | 3 | docker build -t mgerb/server-status:latest . 4 | docker tag mgerb/server-status:latest mgerb/server-status:$version 5 | 6 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | server-status: 5 | image: mgerb/server-status:latest 6 | volumes: 7 | - ./config.json:/server-status/config.json 8 | -------------------------------------------------------------------------------- /docker-push.sh: -------------------------------------------------------------------------------- 1 | version=$(git describe --tags) 2 | 3 | docker push mgerb/server-status:latest; 4 | docker push mgerb/server-status:$version; 5 | 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mgerb/ServerStatus 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | github.com/anvie/port-scanner v0.0.0-20180225151059-8159197d3770 7 | github.com/bwmarrin/discordgo v0.27.1 8 | github.com/kidoman/go-steam v0.0.0-20141221015629-2e40e0d508cb 9 | ) 10 | 11 | require ( 12 | github.com/Sirupsen/logrus v1.0.6 // indirect 13 | github.com/gorilla/websocket v1.4.2 // indirect 14 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect 15 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect 16 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Sirupsen/logrus v1.0.6 h1:HCAGQRk48dRVPA5Y+Yh0qdCSTzPOyU1tBJ7Q9YzotII= 2 | github.com/Sirupsen/logrus v1.0.6/go.mod h1:rmk17hk6i8ZSAJkSDa7nOxamrG+SP4P0mm+DAvExv4U= 3 | github.com/anvie/port-scanner v0.0.0-20180225151059-8159197d3770 h1:1KEvfMGAjISVzk3Ti6pfaOgtoC3naoU0LfiJooZDNO8= 4 | github.com/anvie/port-scanner v0.0.0-20180225151059-8159197d3770/go.mod h1:QGzdstKeoHmMWwi9oNHZ7DQzEj9pi7H42171pkj9htk= 5 | github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY= 6 | github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= 7 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 8 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 9 | github.com/kidoman/go-steam v0.0.0-20141221015629-2e40e0d508cb h1:+3H4rb1CvcN/BEuBlk774uxxab272M6YU9nb9p/GZm8= 10 | github.com/kidoman/go-steam v0.0.0-20141221015629-2e40e0d508cb/go.mod h1:PKiM4eL8SN6mJ38F9m6ZJpVtJKnmSxOxBsi2p3TOru4= 11 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= 12 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 13 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 14 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= 15 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 16 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= 17 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 18 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 19 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 20 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/mgerb/ServerStatus/bot" 7 | "github.com/mgerb/ServerStatus/config" 8 | "github.com/mgerb/ServerStatus/serverstatus" 9 | ) 10 | 11 | var version = "undefined" 12 | 13 | func init() { 14 | fmt.Println("Starting Server Status " + version) 15 | } 16 | 17 | func main() { 18 | //read config file 19 | config.Configure() 20 | 21 | //connect bot to account with token 22 | bot.Connect(config.Config.Token) 23 | 24 | // add handlers 25 | bot.AddHandler(serverstatus.InteractionHandler) 26 | 27 | //start websocket to listen for messages 28 | bot.Start() 29 | 30 | //start server status task 31 | serverstatus.Start() 32 | 33 | // Simple way to keep program running until CTRL-C is pressed. 34 | <-make(chan struct{}) 35 | } 36 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | VERSION := $(shell git describe --tags) 2 | 3 | run: 4 | go run ./src/main.go 5 | 6 | linux: 7 | GOOS=linux GOARCH=amd64 go build -o ./dist/ServerStatus-linux -ldflags="-X main.version=${VERSION}" ./main.go 8 | 9 | mac: 10 | GOOS=darwin GOARCH=amd64 go build -o ./dist/ServerStatus-mac -ldflags="-X main.version=${VERSION}" ./main.go 11 | 12 | windows: 13 | GOOS=windows GOARCH=386 go build -o ./dist/ServerStatus-windows.exe -ldflags="-X main.version=${VERSION}" ./main.go 14 | 15 | clean: 16 | rm -rf ./dist 17 | 18 | copyfiles: 19 | cp config.template.json ./dist/config.json 20 | 21 | zip: 22 | zip -r dist.zip dist 23 | 24 | all: linux mac windows copyfiles 25 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Server Status 2 | Monitors a list of servers and sends a chat notification when a server goes on or offline. 3 | 4 | ## Features 5 | 6 | - send channel notifications 7 | - track server up/down time 8 | - **TCP** - should work with all servers 9 | - **UDP** - [Source RCON Protocol](https://developer.valvesoftware.com/wiki/Source_RCON_Protocol) is supported 10 | - [Docker](https://hub.docker.com/r/mgerb/server-status) 11 | 12 | ### Want to see more features? 13 | 14 | [Submit a new issue](https://github.com/mgerb/ServerStatus/issues/new/choose) 15 | 16 | ## Configuration 17 | - Download the latest release [here](https://github.com/mgerb/ServerStatus/releases) 18 | - Add your bot token as well as other configurations to **config.json** 19 | - Execute the OS specific binary! 20 | 21 | ### Mentioning Roles/Users 22 | - list of user/role ID's must be in the following format (see below for obtaining ID's) 23 | - `<@userid>` 24 | - `<@&roleid>` 25 | 26 | ### Polling Interval 27 | The polling interval is how often the bot will try to ping the servers. 28 | A good interval is 10 seconds, but this may need some adjustment if 29 | it happens to be spamming notifications. 30 | 31 | - time in seconds 32 | - configurable in **config.json** 33 | 34 | ## With Docker 35 | 36 | ``` 37 | docker run -it -v /path/to/your/config.json:/server-status/config.json:ro mgerb/server-status 38 | ``` 39 | 40 | ### Docker Compose 41 | 42 | ``` 43 | version: "2" 44 | 45 | services: 46 | server-status: 47 | image: mgerb/server-status:latest 48 | volumes: 49 | - /path/to/your/config.json:/server-status/config.json 50 | ``` 51 | 52 | ## Usage 53 | To get the current status of your servers simply type `/server-status` in chat. 54 | 55 | ![Server Status](./readme_files/screenshot1.png) 56 | 57 | ## Compiling from source 58 | - Make sure Go and Make are installed 59 | - make all 60 | 61 | ### How to get the bot token 62 | https://github.com/reactiflux/discord-irc/wiki/Creating-a-discord-bot-&-getting-a-token 63 | 64 | ### How to get your room ID 65 | To get IDs, turn on Developer Mode in the Discord client (User Settings -> Appearance) and then right-click your name/icon anywhere in the client and select Copy ID. 66 | 67 | 68 | -------------------------------------------------------------------------------- /readme_files/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgerb/ServerStatus/25ab128a5876985cf336233e21cf7faca893abf7/readme_files/screenshot1.png -------------------------------------------------------------------------------- /readme_files/screenshot2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgerb/ServerStatus/25ab128a5876985cf336233e21cf7faca893abf7/readme_files/screenshot2.gif -------------------------------------------------------------------------------- /serverstatus/serverstatus.go: -------------------------------------------------------------------------------- 1 | package serverstatus 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strconv" 7 | "strings" 8 | "sync" 9 | "time" 10 | 11 | portscanner "github.com/anvie/port-scanner" 12 | "github.com/bwmarrin/discordgo" 13 | steam "github.com/kidoman/go-steam" 14 | "github.com/mgerb/ServerStatus/bot" 15 | "github.com/mgerb/ServerStatus/config" 16 | ) 17 | 18 | const ( 19 | red = 0xf4425c 20 | green = 0x42f477 21 | blue = 0x42adf4 22 | ) 23 | 24 | // Start - add command, start port scanner and bot listeners 25 | func Start() { 26 | //add command 27 | _, err := bot.Session.ApplicationCommandCreate(bot.Session.State.User.ID, "", &discordgo.ApplicationCommand{ 28 | Name: "server-status", 29 | Description: "Get the status of the servers.", 30 | }) 31 | 32 | if err != nil { 33 | log.Panicf("Cannot create status command '%v'", err) 34 | } 35 | 36 | //set each server status as online to start 37 | for i := range config.Config.Servers { 38 | config.Config.Servers[i].Online = true 39 | config.Config.Servers[i].OnlineTimestamp = time.Now() 40 | config.Config.Servers[i].OfflineTimestamp = time.Now() 41 | } 42 | 43 | err = bot.Session.UpdateStatusComplex(discordgo.UpdateStatusData{ 44 | Status: "online", 45 | Activities: []*discordgo.Activity{ 46 | &discordgo.Activity{ 47 | Type: discordgo.ActivityTypeGame, 48 | Name: config.Config.GameStatus, 49 | }, 50 | }, 51 | }) 52 | 53 | sendMessageToRooms(blue, "Server Status", "Bot started! Type /server-status to see the status of your servers :smiley:", false) 54 | 55 | if err != nil { 56 | log.Println(err) 57 | } 58 | 59 | //start a new go routine 60 | go scanServers() 61 | } 62 | 63 | func scanServers() { 64 | 65 | //check if server are in config file 66 | if len(config.Config.Servers) < 1 { 67 | log.Println("No servers in config file.") 68 | return 69 | } 70 | 71 | for { 72 | 73 | // use waitgroup to scan all servers concurrently 74 | var wg sync.WaitGroup 75 | 76 | for index := range config.Config.Servers { 77 | wg.Add(1) 78 | go worker(&config.Config.Servers[index], &wg) 79 | } 80 | 81 | wg.Wait() 82 | 83 | time.Sleep(time.Second * config.Config.PollingInterval) 84 | } 85 | } 86 | 87 | func worker(server *config.Server, wg *sync.WaitGroup) { 88 | defer wg.Done() 89 | 90 | prevServerUp := server.Online //set value to previous server status 91 | 92 | var serverUp bool 93 | retryCounter := 0 94 | 95 | // try reconnecting 5 times if failure persists (every 2 seconds) 96 | for { 97 | serverScanner := portscanner.NewPortScanner(server.Address, time.Second*2, 1) 98 | serverUp = serverScanner.IsOpen(server.Port) //check if the port is open 99 | 100 | // if server isn't up check RCON protocol (UDP) 101 | if !serverUp { 102 | host := server.Address + ":" + strconv.Itoa(server.Port) 103 | steamConnection, err := steam.Connect(host) 104 | if err == nil { 105 | defer steamConnection.Close() 106 | _, err := steamConnection.Ping() 107 | if err == nil { 108 | serverUp = true 109 | } 110 | } 111 | } 112 | 113 | if serverUp || retryCounter >= 5 { 114 | break 115 | } 116 | 117 | retryCounter++ 118 | time.Sleep(time.Second * 2) 119 | } 120 | 121 | if serverUp && serverUp != prevServerUp { 122 | server.OnlineTimestamp = time.Now() 123 | sendMessageToRooms(green, server.Name, "Is now online :smiley:", true) 124 | } else if !serverUp && serverUp != prevServerUp { 125 | server.OfflineTimestamp = time.Now() 126 | sendMessageToRooms(red, server.Name, "Has gone offline :frowning2:", true) 127 | } 128 | 129 | server.Online = serverUp 130 | } 131 | 132 | func sendMessageToRooms(color int, title, description string, mentionRoles bool) { 133 | for _, roomID := range config.Config.RoomIDList { 134 | if mentionRoles { 135 | content := strings.Join(config.Config.RolesToNotify, " ") 136 | bot.Session.ChannelMessageSend(roomID, content) 137 | } 138 | sendEmbeddedMessage(roomID, color, title, description) 139 | } 140 | } 141 | 142 | func sendEmbeddedMessage(roomID string, color int, title, description string) { 143 | 144 | embed := &discordgo.MessageEmbed{ 145 | Color: color, 146 | Title: title, 147 | Description: description, 148 | } 149 | 150 | bot.Session.ChannelMessageSendEmbed(roomID, embed) 151 | } 152 | 153 | // InteractionHandler will be called every time an interaction from a user occurs 154 | // Command interaction handling requires bot command scope 155 | func InteractionHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { 156 | // A user is calling us with our status command 157 | if i.ApplicationCommandData().Name == "server-status" { 158 | online := "" 159 | offline := "" 160 | 161 | for _, server := range config.Config.Servers { 162 | if server.Online { 163 | online = online + server.Name + " : " + fmtDuration(time.Since(server.OnlineTimestamp)) + "\n" 164 | } else { 165 | offline = offline + server.Name + " : " + fmtDuration(time.Since(server.OfflineTimestamp)) + "\n" 166 | } 167 | } 168 | 169 | embeds := []*discordgo.MessageEmbed{} 170 | 171 | if online != "" { 172 | embeds = append(embeds, &discordgo.MessageEmbed{ 173 | Title: ":white_check_mark: Online", 174 | Color: green, 175 | Description: online, 176 | }) 177 | } 178 | 179 | if offline != "" { 180 | embeds = append(embeds, &discordgo.MessageEmbed{ 181 | Title: ":x: Offline", 182 | Color: red, 183 | Description: offline, 184 | }) 185 | } 186 | 187 | // Only one message can be an interaction response. Messages can only contain up to 10 embeds. 188 | // Our message will therefore instead be two embeds (online and offline), each with a list of servers in text. 189 | // Embed descriptions can be ~4096 characters, so no limits should get hit with this. 190 | s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ 191 | Type: discordgo.InteractionResponseChannelMessageWithSource, 192 | Data: &discordgo.InteractionResponseData{ 193 | Embeds: embeds, 194 | }, 195 | }) 196 | } 197 | } 198 | 199 | func fmtDuration(d time.Duration) string { 200 | 201 | days := int(d.Hours()) / 24 202 | hours := int(d.Hours()) % 24 203 | minutes := int(d.Minutes()) % 60 204 | seconds := int(d.Seconds()) % 60 205 | 206 | return fmt.Sprintf("%dd %dh %dm %ds", days, hours, minutes, seconds) 207 | } 208 | --------------------------------------------------------------------------------