1
0
mirror of https://github.com/mgerb/ServerStatus synced 2026-01-11 03:32:50 +00:00

21 Commits

Author SHA1 Message Date
25ab128a58 update docker files 2024-03-03 16:09:20 -06:00
Ethorbit
e4320bcecb Make docker image unprivileged 2024-03-03 16:00:31 -06:00
50a06b98be Updates and maintenance
- switch from dep to go modules
- update to discordgo v0.27.1
- fix offline/online message bug
2024-03-03 11:32:08 -06:00
Ethorbit
ed232fc785 Fix outdated broken bot
discordgo upgraded from 0.20.0 to 0.24.0 which fixed the bot starting with a websocket error and remaining offline. Additionally, the command prefix and MessageHandling commands have been replaced with Discord's newer slash commands.
2024-03-03 11:24:22 -06:00
12c20b928c v0.8.0 2020-06-13 12:53:51 -05:00
2fdc0eddd3 update docker files
closes #27
2020-06-13 12:52:52 -05:00
18b1a0b6e3 fix: add retry to RCON servers
closes #28
2020-06-13 12:52:32 -05:00
bf79d376f3 fix: date format
closes #29
2020-06-13 12:47:00 -05:00
8f430b5982 update readme 2020-06-10 18:53:49 -05:00
56770b7b66 v0.7.0 2020-06-10 18:30:40 -05:00
f8d72bc297 feat: new features/improvements
- update dependencies
- add up/down time #14
- add workers with retry to help with spam
2020-06-10 18:28:41 -05:00
68bfed3f3b update readme 2019-01-19 20:30:27 -06:00
92d94ed4d4 feat(RCON): added RCON support for UDP servers 2019-01-19 20:13:55 -06:00
d0cf0eae78 Merge pull request #23 from mgerb/development
added docker support
2018-10-04 19:10:23 -05:00
642ccc7bf5 added docker support 2018-10-04 19:07:30 -05:00
bcb57c72a3 add polling interval to configurations - resolves #15 2018-07-18 20:22:37 -05:00
1cd9f139c2 Merge pull request #13 from mgerb/mitchell
added embedded messages resolves #9
2018-03-15 21:07:58 -05:00
8d26fba95d added embedded messages resolves #9 2018-03-15 21:06:08 -05:00
217c6c8baa update dependencies - change imports 2018-01-13 12:29:20 -06:00
5a2f81a23a Update readme.md 2017-08-07 23:30:33 -05:00
f8e552caa6 update for felmyst 2017-07-17 19:52:57 -05:00
19 changed files with 433 additions and 196 deletions

2
.dockerignore Normal file
View File

@@ -0,0 +1,2 @@
/vendor
/dist

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
config.json config.json
dist dist
vendor

26
Dockerfile Normal file
View File

@@ -0,0 +1,26 @@
FROM golang:1.22-alpine3.19
WORKDIR /go/src/github.com/mgerb/ServerStatus
ADD . .
RUN apk add --no-cache git alpine-sdk
RUN go get
RUN make linux
FROM alpine:3.19
ARG UNAME="server-status"
ARG GNAME="server-status"
ARG UID=1000
ARG GID=1000
WORKDIR /server-status
COPY --from=0 /go/src/github.com/mgerb/ServerStatus/dist/ServerStatus-linux .
RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/*
RUN addgroup -g ${GID} "${GNAME}"
RUN adduser -D -u ${UID} -G "${GNAME}" "${UNAME}" &&\
chown "${UNAME}":"${GNAME}" -R /server-status/
USER ${UNAME}
ENTRYPOINT ./ServerStatus-linux

View File

@@ -1,33 +1,19 @@
{ {
"Token": "your_bot_token_id", "Token": "your bot token",
"RoomIDList": ["id_1", "id_2", "id_3"], "RoomIDList": ["room id list goes here"],
"RoleToNotify": "@Elysium", "RolesToNotify": ["<@&roleid>", "<@userid>"],
"GameStatus": "current playing game", "GameStatus": "current playing game",
"Servers": [ "PollingInterval": 10,
{ "Servers": [
"Name": "Elysium PvP", {
"Address": "149.202.207.235", "Name": "Your awesome server",
"Port": 8099 "Address": "game.server.com",
}, "Port": 80
{ },
"Name": "Zethkur PvP", {
"Address": "151.80.103.221", "Name": "Another awesome server",
"Port": 8093 "Address": "awesome.server.com",
}, "Port": 8080
{ }
"Name": "Anathema PvP", ]
"Address": "149.202.211.5", }
"Port": 8095
},
{
"Name": "Darrowshire PvE",
"Address": "164.132.233.125",
"Port": 8097
},
{
"Name": "Elysium Authentication Server",
"Address": "logon.elysium-project.org",
"Port": 3724
}
]
}

55
config/config.go Normal file
View File

@@ -0,0 +1,55 @@
package config
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"time"
)
// Variables used for command line parameters
var Config configStruct
type configStruct struct {
Token string `json:"Token"`
RoomIDList []string `json:"RoomIDList"`
RolesToNotify []string `json:"RolesToNotify"`
Servers []Server `json:"Servers"`
GameStatus string `json:"GameStatus"`
PollingInterval time.Duration `json:"PollingInterval"`
}
type Server struct {
Name string `json:"Name"`
Address string `json:"Address"`
Port int `json:"Port"`
Online bool `json:"Online,omitempty"`
// OnlineTimestamp - time of when the server last came online
OnlineTimestamp time.Time
OfflineTimestamp time.Time
}
func Configure() {
fmt.Println("Reading config file...")
file, e := ioutil.ReadFile("./config.json")
if e != nil {
log.Printf("File error: %v\n", e)
os.Exit(1)
}
err := json.Unmarshal(file, &Config)
if err != nil {
log.Println(err)
}
if Config.PollingInterval == 0 {
log.Fatal("Please set your PollingInterval > 0 in your config file.")
}
}

5
docker-build.sh Executable file
View File

@@ -0,0 +1,5 @@
version=$(git describe --tags)
docker build -t mgerb/server-status:latest .
docker tag mgerb/server-status:latest mgerb/server-status:$version

7
docker-compose.yml Normal file
View File

@@ -0,0 +1,7 @@
version: "3"
services:
server-status:
image: mgerb/server-status:latest
volumes:
- ./config.json:/server-status/config.json

5
docker-push.sh Executable file
View File

@@ -0,0 +1,5 @@
version=$(git describe --tags)
docker push mgerb/server-status:latest;
docker push mgerb/server-status:$version;

17
go.mod Normal file
View File

@@ -0,0 +1,17 @@
module github.com/mgerb/ServerStatus
go 1.22.0
require (
github.com/anvie/port-scanner v0.0.0-20180225151059-8159197d3770
github.com/bwmarrin/discordgo v0.27.1
github.com/kidoman/go-steam v0.0.0-20141221015629-2e40e0d508cb
)
require (
github.com/Sirupsen/logrus v1.0.6 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect
)

19
go.sum Normal file
View File

@@ -0,0 +1,19 @@
github.com/Sirupsen/logrus v1.0.6 h1:HCAGQRk48dRVPA5Y+Yh0qdCSTzPOyU1tBJ7Q9YzotII=
github.com/Sirupsen/logrus v1.0.6/go.mod h1:rmk17hk6i8ZSAJkSDa7nOxamrG+SP4P0mm+DAvExv4U=
github.com/anvie/port-scanner v0.0.0-20180225151059-8159197d3770 h1:1KEvfMGAjISVzk3Ti6pfaOgtoC3naoU0LfiJooZDNO8=
github.com/anvie/port-scanner v0.0.0-20180225151059-8159197d3770/go.mod h1:QGzdstKeoHmMWwi9oNHZ7DQzEj9pi7H42171pkj9htk=
github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY=
github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
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/kidoman/go-steam v0.0.0-20141221015629-2e40e0d508cb h1:+3H4rb1CvcN/BEuBlk774uxxab272M6YU9nb9p/GZm8=
github.com/kidoman/go-steam v0.0.0-20141221015629-2e40e0d508cb/go.mod h1:PKiM4eL8SN6mJ38F9m6ZJpVtJKnmSxOxBsi2p3TOru4=
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/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 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
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=

View File

@@ -1,15 +1,18 @@
package main package main
import ( import (
"./bot" "fmt"
"./config"
"./serverstatus" "github.com/mgerb/ServerStatus/bot"
"github.com/mgerb/ServerStatus/config"
"github.com/mgerb/ServerStatus/serverstatus"
) )
// Variables used for command line parameters var version = "undefined"
var (
BotID string func init() {
) fmt.Println("Starting Server Status " + version)
}
func main() { func main() {
//read config file //read config file
@@ -19,13 +22,13 @@ func main() {
bot.Connect(config.Config.Token) bot.Connect(config.Config.Token)
// add handlers // add handlers
bot.AddHandler(serverstatus.MessageHandler) bot.AddHandler(serverstatus.InteractionHandler)
//start websocket to listen for messages //start websocket to listen for messages
bot.Start() bot.Start()
//start server status task //start server status task
serverstatus.Start() serverstatus.Start()
// Simple way to keep program running until CTRL-C is pressed. // Simple way to keep program running until CTRL-C is pressed.
<-make(chan struct{}) <-make(chan struct{})

View File

@@ -1,14 +1,16 @@
VERSION := $(shell git describe --tags)
run: run:
go run ./src/main.go go run ./src/main.go
linux: linux:
go build -o ./dist/ServerStatus-linux ./src/main.go GOOS=linux GOARCH=amd64 go build -o ./dist/ServerStatus-linux -ldflags="-X main.version=${VERSION}" ./main.go
mac: mac:
GOOS=darwin GOARCH=amd64 go build -o ./dist/ServerStatus-mac ./src/main.go GOOS=darwin GOARCH=amd64 go build -o ./dist/ServerStatus-mac -ldflags="-X main.version=${VERSION}" ./main.go
windows: windows:
GOOS=windows GOARCH=386 go build -o ./dist/ServerStatus-windows.exe ./src/main.go GOOS=windows GOARCH=386 go build -o ./dist/ServerStatus-windows.exe -ldflags="-X main.version=${VERSION}" ./main.go
clean: clean:
rm -rf ./dist rm -rf ./dist
@@ -16,4 +18,7 @@ clean:
copyfiles: copyfiles:
cp config.template.json ./dist/config.json cp config.template.json ./dist/config.json
all: linux mac windows copyfiles zip:
zip -r dist.zip dist
all: linux mac windows copyfiles

View File

@@ -1,19 +1,60 @@
## Server Status # Server Status
Monitors a list of servers and sends a chat notification when a server goes on or offline.
Scans a list of servers checking whether the ports are open or not. ## Features
This bot will send a chat notification when the status of a server changes.
I originally made this bot the check if private World of Warcraft servers were up or not. - send channel notifications
This bot is actually much more useful than that and can be used for any type of server. - track server up/down time
- **TCP** - should work with all servers
- **UDP** - [Source RCON Protocol](https://developer.valvesoftware.com/wiki/Source_RCON_Protocol) is supported
- [Docker](https://hub.docker.com/r/mgerb/server-status)
### Want to see more features?
[Submit a new issue](https://github.com/mgerb/ServerStatus/issues/new/choose)
## Configuration ## Configuration
- Download the latest release [here](https://github.com/mgerb/ServerStatus/releases) - Download the latest release [here](https://github.com/mgerb/ServerStatus/releases)
- Add your bot token as well as other configurations to config.json - Add your bot token as well as other configurations to **config.json**
- Execute the OS specific binary! - Execute the OS specific binary!
## Compiling from source ### Mentioning Roles/Users
- list of user/role ID's must be in the following format (see below for obtaining ID's)
- `<@userid>`
- `<@&roleid>`
### Polling Interval
The polling interval is how often the bot will try to ping the servers.
A good interval is 10 seconds, but this may need some adjustment if
it happens to be spamming notifications.
- time in seconds
- configurable in **config.json**
## With Docker
```
docker run -it -v /path/to/your/config.json:/server-status/config.json:ro mgerb/server-status
```
### Docker Compose
```
version: "2"
services:
server-status:
image: mgerb/server-status:latest
volumes:
- /path/to/your/config.json:/server-status/config.json
```
## Usage
To get the current status of your servers simply type `/server-status` in chat.
![Server Status](./readme_files/screenshot1.png)
## Compiling from source
- Make sure Go and Make are installed - Make sure Go and Make are installed
- make all - make all
@@ -21,19 +62,6 @@ This bot is actually much more useful than that and can be used for any type of
https://github.com/reactiflux/discord-irc/wiki/Creating-a-discord-bot-&-getting-a-token https://github.com/reactiflux/discord-irc/wiki/Creating-a-discord-bot-&-getting-a-token
### How to get your room ID ### How to get your room ID
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. 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.
<img src="https://camo.githubusercontent.com/9f759ec8b45a6e9dd2242bc64c82897c74f84a25/687474703a2f2f692e696d6775722e636f6d2f47684b70424d512e676966"/> <img src="./readme_files/screenshot2.gif"/>
## List server status in discord channel
`!ServerStatus`
```
Elysium PvP is online!
Zethkur PvP is online!
Anathema PvP is online!
Darrowshire PvE is online!
Elysium Authentication Server is online!
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 KiB

View File

@@ -0,0 +1,207 @@
package serverstatus
import (
"fmt"
"log"
"strconv"
"strings"
"sync"
"time"
portscanner "github.com/anvie/port-scanner"
"github.com/bwmarrin/discordgo"
steam "github.com/kidoman/go-steam"
"github.com/mgerb/ServerStatus/bot"
"github.com/mgerb/ServerStatus/config"
)
const (
red = 0xf4425c
green = 0x42f477
blue = 0x42adf4
)
// Start - add command, start port scanner and bot listeners
func Start() {
//add command
_, err := bot.Session.ApplicationCommandCreate(bot.Session.State.User.ID, "", &discordgo.ApplicationCommand{
Name: "server-status",
Description: "Get the status of the servers.",
})
if err != nil {
log.Panicf("Cannot create status command '%v'", err)
}
//set each server status as online to start
for i := range config.Config.Servers {
config.Config.Servers[i].Online = true
config.Config.Servers[i].OnlineTimestamp = time.Now()
config.Config.Servers[i].OfflineTimestamp = time.Now()
}
err = bot.Session.UpdateStatusComplex(discordgo.UpdateStatusData{
Status: "online",
Activities: []*discordgo.Activity{
&discordgo.Activity{
Type: discordgo.ActivityTypeGame,
Name: config.Config.GameStatus,
},
},
})
sendMessageToRooms(blue, "Server Status", "Bot started! Type /server-status to see the status of your servers :smiley:", false)
if err != nil {
log.Println(err)
}
//start a new go routine
go scanServers()
}
func scanServers() {
//check if server are in config file
if len(config.Config.Servers) < 1 {
log.Println("No servers in config file.")
return
}
for {
// use waitgroup to scan all servers concurrently
var wg sync.WaitGroup
for index := range config.Config.Servers {
wg.Add(1)
go worker(&config.Config.Servers[index], &wg)
}
wg.Wait()
time.Sleep(time.Second * config.Config.PollingInterval)
}
}
func worker(server *config.Server, wg *sync.WaitGroup) {
defer wg.Done()
prevServerUp := server.Online //set value to previous server status
var serverUp bool
retryCounter := 0
// try reconnecting 5 times if failure persists (every 2 seconds)
for {
serverScanner := portscanner.NewPortScanner(server.Address, time.Second*2, 1)
serverUp = serverScanner.IsOpen(server.Port) //check if the port is open
// if server isn't up check RCON protocol (UDP)
if !serverUp {
host := server.Address + ":" + strconv.Itoa(server.Port)
steamConnection, err := steam.Connect(host)
if err == nil {
defer steamConnection.Close()
_, err := steamConnection.Ping()
if err == nil {
serverUp = true
}
}
}
if serverUp || retryCounter >= 5 {
break
}
retryCounter++
time.Sleep(time.Second * 2)
}
if serverUp && serverUp != prevServerUp {
server.OnlineTimestamp = time.Now()
sendMessageToRooms(green, server.Name, "Is now online :smiley:", true)
} else if !serverUp && serverUp != prevServerUp {
server.OfflineTimestamp = time.Now()
sendMessageToRooms(red, server.Name, "Has gone offline :frowning2:", true)
}
server.Online = serverUp
}
func sendMessageToRooms(color int, title, description string, mentionRoles bool) {
for _, roomID := range config.Config.RoomIDList {
if mentionRoles {
content := strings.Join(config.Config.RolesToNotify, " ")
bot.Session.ChannelMessageSend(roomID, content)
}
sendEmbeddedMessage(roomID, color, title, description)
}
}
func sendEmbeddedMessage(roomID string, color int, title, description string) {
embed := &discordgo.MessageEmbed{
Color: color,
Title: title,
Description: description,
}
bot.Session.ChannelMessageSendEmbed(roomID, embed)
}
// InteractionHandler will be called every time an interaction from a user occurs
// Command interaction handling requires bot command scope
func InteractionHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
// A user is calling us with our status command
if i.ApplicationCommandData().Name == "server-status" {
online := ""
offline := ""
for _, server := range config.Config.Servers {
if server.Online {
online = online + server.Name + " : " + fmtDuration(time.Since(server.OnlineTimestamp)) + "\n"
} else {
offline = offline + server.Name + " : " + fmtDuration(time.Since(server.OfflineTimestamp)) + "\n"
}
}
embeds := []*discordgo.MessageEmbed{}
if online != "" {
embeds = append(embeds, &discordgo.MessageEmbed{
Title: ":white_check_mark: Online",
Color: green,
Description: online,
})
}
if offline != "" {
embeds = append(embeds, &discordgo.MessageEmbed{
Title: ":x: Offline",
Color: red,
Description: offline,
})
}
// Only one message can be an interaction response. Messages can only contain up to 10 embeds.
// Our message will therefore instead be two embeds (online and offline), each with a list of servers in text.
// Embed descriptions can be ~4096 characters, so no limits should get hit with this.
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Embeds: embeds,
},
})
}
}
func fmtDuration(d time.Duration) string {
days := int(d.Hours()) / 24
hours := int(d.Hours()) % 24
minutes := int(d.Minutes()) % 60
seconds := int(d.Seconds()) % 60
return fmt.Sprintf("%dd %dh %dm %ds", days, hours, minutes, seconds)
}

View File

@@ -1,47 +0,0 @@
package config
import (
"encoding/json"
"io/ioutil"
"log"
"os"
)
// Variables used for command line parameters
var Config configStruct
type configStruct struct {
Token string `json:"Token"`
RoomIDList []string `json:"RoomIDList"`
RoleToNotify string `json:"RoleToNotify"`
Servers []server `json:"Servers"`
GameStatus string `json:"GameStatus"`
}
type server struct {
Name string `json:"Name"`
Address string `json:"Address"`
Port int `json:"Port"`
Online bool `json:"Online,omitempty"`
}
func Configure() {
log.Println("Reading config file...")
file, e := ioutil.ReadFile("./config.json")
if e != nil {
log.Printf("File error: %v\n", e)
os.Exit(1)
}
log.Printf("%s\n", string(file))
err := json.Unmarshal(file, &Config)
if err != nil {
log.Println(err)
}
}

View File

@@ -1,82 +0,0 @@
package serverstatus
import (
"../bot"
"../config"
"fmt"
"github.com/anvie/port-scanner"
"github.com/bwmarrin/discordgo"
"log"
"time"
)
func Start() {
//set each server status as online to start
for i, _ := range config.Config.Servers {
config.Config.Servers[i].Online = true
}
err := bot.Session.UpdateStatus(0, config.Config.GameStatus)
if err != nil {
log.Println(err)
}
//start a new go routine
go scanServers()
}
func scanServers() {
//check if server are in config file
if len(config.Config.Servers) < 1 {
fmt.Println("No servers in config file.")
return
}
for {
for index, server := range config.Config.Servers {
prevServerUp := server.Online //set value to previous server status
serverScanner := portscanner.NewPortScanner(server.Address, time.Second*2)
serverUp := serverScanner.IsOpen(server.Port) //check if the port is open
if serverUp && serverUp != prevServerUp {
sendMessage(config.Config.RoleToNotify + " " + server.Name + " is now online!")
} else if !serverUp && serverUp != prevServerUp {
sendMessage(config.Config.RoleToNotify + " " + server.Name + " went offline!")
}
config.Config.Servers[index].Online = serverUp
}
time.Sleep(time.Second * 5)
}
}
func sendMessage(message string) {
for _, roomID := range config.Config.RoomIDList {
bot.Session.ChannelMessageSend(roomID, message)
}
}
// This function will be called every time a new
// message is created on any channel that the autenticated bot has access to.
func MessageHandler(s *discordgo.Session, m *discordgo.MessageCreate) {
// Ignore all messages created by the bot itself
if m.Author.ID == bot.BotID {
return
}
if m.Content == "!ServerStatus" {
for _, server := range config.Config.Servers {
if server.Online {
s.ChannelMessageSend(m.ChannelID, server.Name+" is online!")
} else {
s.ChannelMessageSend(m.ChannelID, server.Name+" is down!")
}
}
}
}