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

21 Commits

Author SHA1 Message Date
e0fc61a2e6 update docker files 2024-03-03 16:06:15 -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
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",
"RoomIDList": ["id_1", "id_2", "id_3"],
"RoleToNotify": "@Elysium",
"GameStatus": "current playing game",
"Servers": [
{
"Name": "Elysium PvP",
"Address": "149.202.207.235",
"Port": 8099
},
{
"Name": "Zethkur PvP",
"Address": "151.80.103.221",
"Port": 8093
},
{
"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
}
]
}
"Token": "your bot token",
"RoomIDList": ["room id list goes here"],
"RolesToNotify": ["<@&roleid>", "<@userid>"],
"GameStatus": "current playing game",
"PollingInterval": 10,
"Servers": [
{
"Name": "Your awesome server",
"Address": "game.server.com",
"Port": 80
},
{
"Name": "Another awesome server",
"Address": "awesome.server.com",
"Port": 8080
}
]
}

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

View File

@@ -1,14 +1,16 @@
VERSION := $(shell git describe --tags)
run:
go run ./src/main.go
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:
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:
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:
rm -rf ./dist
@@ -16,4 +18,7 @@ clean:
copyfiles:
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.
This bot will send a chat notification when the status of a server changes.
## Features
I originally made this bot the check if private World of Warcraft servers were up or not.
This bot is actually much more useful than that and can be used for any type of server.
- send channel notifications
- 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
- 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!
## 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 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
### 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.
<img src="https://camo.githubusercontent.com/9f759ec8b45a6e9dd2242bc64c82897c74f84a25/687474703a2f2f692e696d6775722e636f6d2f47684b70424d512e676966"/>
## 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!
```
<img src="./readme_files/screenshot2.gif"/>

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!")
}
}
}
}