Skip to content

Commit

Permalink
Rewrite of log tail, added commands, bot now uses Discord status mess…
Browse files Browse the repository at this point in the history
…ages

Thanks to https://github.com/Eeems for adding the features in #6!

* Add status message and simple commands

* Fix crash

* Better handle join/parts

* Switch to tail

* Periodically update player count

* Remove fsnotify

* Cleanup mod files

* Fixed the tests by consuming on the channels

* Seek to end of log and keep track of players without polling

* Refactored a bit to handle errors

* Added test for the tail function to prove it doesn't work (well it does work haha)

* Changed timeout

* Changed rocket launch status to watching instead of playing

* And also with the player death message

Co-authored-by: Matthijs <[email protected]>
  • Loading branch information
Eeems and Mattie112 authored Jan 13, 2023
1 parent 18081b5 commit 19ecc8a
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 107 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ RCON_PORT=xx # The rcon port
RCON_PASSWORD=xx # The rcon password
FACTORIO_LOG=C:\Users\xx\AppData\Roaming\Factorio\console.log (Unix path also supported) # Path to the chat log (--console-log)
MOD_LOG=C:\Users\xx\AppData\Roaming\Factorio\script-output\factorigo-chat-bot\factorigo-chat-bot.log (Optional) # If you use the companion mod supply the path to that log file here
POLL_LOG=1 # If this is set, logs will be polled instead of using inotify. This may be required with docker depending on your storage setup
```
When using docker: don't forget to also mount/bind the log-files to your container.

Expand Down
15 changes: 9 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ module github.com/Mattie112/FactoriGOChatBot
go 1.17

require (
github.com/bwmarrin/discordgo v0.23.2
github.com/bwmarrin/discordgo v0.26.1
github.com/forPelevin/gomoji v1.1.2
github.com/forewing/csgo-rcon v1.3.0
github.com/fsnotify/fsnotify v1.5.1
github.com/joho/godotenv v1.4.0
github.com/nxadm/tail v1.4.8
github.com/sirupsen/logrus v1.8.1
)

require (
github.com/forPelevin/gomoji v1.1.2 // indirect
github.com/gorilla/websocket v1.4.0 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hpcloud/tail v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 // indirect
golang.org/x/sys v0.0.0-20210921065528-437939a70204 // indirect
golang.org/x/crypto v0.4.0 // indirect
golang.org/x/sys v0.3.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
)
28 changes: 28 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4=
github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M=
github.com/bwmarrin/discordgo v0.26.1 h1:AIrM+g3cl+iYBr4yBxCBp9tD9jR3K7upEjl0d89FRkE=
github.com/bwmarrin/discordgo v0.26.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
Expand All @@ -10,12 +12,23 @@ github.com/forPelevin/gomoji v1.1.2/go.mod h1:Z5cUlNvnrRQPxwMxc8hmn+ZAm0p8WhqE0F
github.com/forewing/csgo-rcon v1.3.0 h1:y1BwWL3mLZeXQkOY1ZGD8cq1jOYBi9UXMrBSafNj+zg=
github.com/forewing/csgo-rcon v1.3.0/go.mod h1:hD/ht7ta+ub49Jx+FiP3tI7vKRt/bDEc7vrj3/iOrjw=
github.com/forewing/gobuild v0.4.0/go.mod h1:HRJckNq8s6UAK2r5PsXhFQS+FGk0v5hvO7TmEVrAP5w=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/nxadm/tail v1.0.0 h1:UIkD4DPa09b3qCzVmPib3sQ7IeCry0KIiRnOLZq8YWw=
github.com/nxadm/tail v1.0.0/go.mod h1:1MZIjlaX6+FGNxRj+q5CixiLiqzqeGM688FekZFtNpw=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
Expand All @@ -26,7 +39,22 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210921065528-437939a70204 h1:JJhkWtBuTQKyz2bd5WG9H8iUsJRU3En/KRfN8B2RnDs=
golang.org/x/sys v0.0.0-20210921065528-437939a70204/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
174 changes: 135 additions & 39 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import (
"github.com/forPelevin/gomoji"
"github.com/forewing/csgo-rcon"
"github.com/joho/godotenv"
"github.com/nxadm/tail"
"github.com/sirupsen/logrus"
"io"
"os"
"os/signal"
"regexp"
Expand All @@ -20,8 +22,12 @@ var (
log *logrus.Logger
messagesToDiscord chan string
messagesToFactorio chan string
readLogFile chan string
discordActivities chan discordgo.Activity
commands chan string
discordChannelId string
playersOnline int
seed string
tailFile *tail.Tail
// VERSION These can be injected at build time -ldflags "-InputArgs main.VERSION=dev main.BUILD_TIME=201610251410"
VERSION = "Undefined"
// BUILDTIME These can be injected at build time -ldflags "-InputArgs main.VERSION=dev main.BUILD_TIME=201610251410"
Expand All @@ -43,40 +49,46 @@ func main() {
config = loadConfig() // Load optional config

discordChannelId = os.Getenv("DISCORD_CHANNEL_ID")
playersOnline = 0

messagesToDiscord = make(chan string)
messagesToFactorio = make(chan string)
readLogFile = make(chan string)
commands = make(chan string)

discord := setUpDiscord()
rconClient := setUpRCON()

//Setup file watcher
var paths []string
paths = append(paths, os.Getenv("FACTORIO_LOG"))
// Setup file watchers
go readFactorioLogFile(os.Getenv("FACTORIO_LOG"))
if os.Getenv("MOD_LOG") != "" {
paths = append(paths, os.Getenv("MOD_LOG"))
go readFactorioLogFile(os.Getenv("MOD_LOG"))
}
watcher := setupFileWatcher(paths)

// Start functions that handle the dataflow
go sendMessageToFactorio(rconClient)
go readFactorioLogFile()
go sendMessageToDiscord(discord)
go handleCommands(rconClient)

// Setup recurring tasks
periodicTasks := schedule(60*time.Second, func() {
updatePlayerCount(rconClient)
})

// Keep running until getting exit signal
sc := make(chan os.Signal, 1)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
<-sc

// Cleanup
close(periodicTasks)
close(messagesToDiscord)
close(messagesToFactorio)
close(commands)
_ = discord.Close()
_ = watcher.Close()
}

func loadConfig() sConfig {
var c = sConfig{allRocketLaunches: getenvBool("ALL_ROCKET_LAUNCHES")}
return c
return sConfig{allRocketLaunches: getenvBool("ALL_ROCKET_LAUNCHES")}
}

// Parse the message and format it in a way for Discord
Expand All @@ -101,13 +113,14 @@ func parseAndFormatMessage(message string) string {
if strings.Contains(messageContent, "[gps=") {
return ""
}

return fmt.Sprintf(":speech_left: | `%s`: %s", match[1], messageContent)
case "JOIN":
playersOnline += 1
var re = regexp.MustCompile(`(?m)] (\w*)`)
match := re.FindStringSubmatch(message)
return fmt.Sprintf(":green_circle: | `%s` joined the game!", match[1])
case "LEAVE":
playersOnline -= 1
var re = regexp.MustCompile(`(?m)] (\w*)`)
match := re.FindStringSubmatch(message)
return fmt.Sprintf(":red_circle: | `%s` left the game!", match[1])
Expand All @@ -134,11 +147,13 @@ func parseModLogEntries(message string) string {
case "RESEARCH_FINISHED":
var re = regexp.MustCompile(`(?m):(\S*)]`)
match := re.FindStringSubmatch(message)
updateDiscordStatus(discordgo.ActivityTypeListening, match[1])
return fmt.Sprintf(":microscope: | Research finished: `%s`", match[1])
case "PLAYER_DIED":
var re = regexp.MustCompile(`(?m):([\w -]*)+`)
match := re.FindAllStringSubmatch(message, -1)

updateDiscordStatus(discordgo.ActivityTypeWatching, match[1][1]+" dying")
// No cause
if len(match) == 2 {
return fmt.Sprintf(":skull: | Player died: `%s` (unknown cause)", match[1][1])
Expand Down Expand Up @@ -171,6 +186,7 @@ func parseModLogEntries(message string) string {

return ""
case "ROCKET_LAUNCHED":
updateDiscordStatus(discordgo.ActivityTypeWatching, "a rocket launch")
var re = regexp.MustCompile(`(?m):(\d*)]`)
match := re.FindStringSubmatch(message)
launchAmount, _ := strconv.Atoi(match[1])
Expand Down Expand Up @@ -247,6 +263,9 @@ func onReceiveDiscordMessage(s *discordgo.Session, m *discordgo.MessageCreate) {
messages := parseDiscordMessage(m.Content)
log.WithFields(logrus.Fields{"messages": messages}).Debugf("Sending Discord message to output channel")
for _, message := range messages {
if len(message) > 0 && message[0:1] == "!" {
commands <- message
}
messagesToFactorio <- fmt.Sprintf("[%s]: %s", nick, message)
}
}
Expand All @@ -263,26 +282,99 @@ func parseDiscordMessage(message string) []string {
}

// Read the last line of a file and puts the parsed message on our output channel
func readFactorioLogFile() {
for fileName := range readLogFile {
log.Debug("Trigger to read Factorio logfile")
line := getLastLineWithSeek(fileName)
log.WithFields(logrus.Fields{"line": line}).Debug("Read line from Factorio log")
message := parseAndFormatMessage(line)
func readFactorioLogFile(filename string) {
seek := tail.SeekInfo{
Offset: 0,
Whence: io.SeekEnd,
}
var err error
tailFile, err = tail.TailFile(filename, tail.Config{
Follow: true,
ReOpen: true,
MustExist: true,
Poll: os.Getenv("POLL_LOG") != "",
Location: &seek,
})
if err != nil {
log.WithError(err).Error("Failed to open mod log file")
return
}
for line := range tailFile.Lines {
if line.Err != nil {
log.WithError(line.Err).Error("Error while tailing log file")
continue
}
log.WithFields(logrus.Fields{"line": line.Text}).Debug("Read line from Factorio log")
message := parseAndFormatMessage(line.Text)
if message != "" {
messagesToDiscord <- message
}
}
}

func updateDiscordStatus(activityType discordgo.ActivityType, name string) {
discordActivities <- discordgo.Activity{
Name: name,
Type: activityType,
}
}

func sendDiscordStatusUpdates(discord *discordgo.Session) {
for activity := range discordActivities {
// Set game status
idle := 0
err := discord.UpdateStatusComplex(discordgo.UpdateStatusData{
IdleSince: &idle,
Activities: []*discordgo.Activity{&activity},
AFK: false,
})
if err != nil {
log.WithError(err).Errorln("Failed to update Discord status")
}
log.Debugln("Updated status to " + activityToStatus(&activity))
}
}

func setUpRCON() *rcon.Client {
rconIp := os.Getenv("RCON_IP")
rconPort := os.Getenv("RCON_PORT")
rconPassword := os.Getenv("RCON_PASSWORD")
rconClient := rcon.New(rconIp+":"+rconPort, rconPassword, time.Second*2)
updatePlayerCount(rconClient)
return rconClient
}

func getSeedFromFactorio(rconClient *rcon.Client) string {
if seed != "" {
return seed
}
msg, err := rconClient.Execute("/seed")
if err != nil {
log.WithFields(logrus.Fields{"err": err}).Error("Could not get seed from Factorio")
return "Unknown"
}
seed = msg
return seed
}

func updatePlayerCount(rconClient *rcon.Client) {
msg, err := rconClient.Execute("/players online count")
if err != nil {
log.WithFields(logrus.Fields{"err": err}).Error("Could not get player count from Factorio")
return
}
playersOnline, err = strconv.Atoi(strings.Split(strings.Split(msg, "(")[1], ")")[0])
if err != nil {
log.WithFields(logrus.Fields{"err": err}).Panic("Could not parse player count from Factorio")
return
}
if playersOnline > 0 {
updateDiscordStatus(discordgo.ActivityTypeWatching, "the factory grow")
} else {
updateDiscordStatus(discordgo.ActivityTypeWatching, "the world burn")
}
}

func setUpDiscord() *discordgo.Session {
discordToken := os.Getenv("DISCORD_TOKEN")

Expand All @@ -301,9 +393,34 @@ func setUpDiscord() *discordgo.Session {
}

log.Infoln("Bot registered by Discord and is now listening for messagesToDiscord")
discordActivities = make(chan discordgo.Activity)
go sendDiscordStatusUpdates(discord)
// Set initial status
updateDiscordStatus(discordgo.ActivityTypeWatching, "the world burn")
return discord
}

func handleCommands(rconClient *rcon.Client) {
for command := range commands {
switch command {
case "!online":
messagesToDiscord <- strconv.Itoa(playersOnline) + " players online"
break
case "!seed":
messagesToDiscord <- getSeedFromFactorio(rconClient)
break
case "!evolution":
msg, err := rconClient.Execute("/evolution")
if err != nil {
log.WithFields(logrus.Fields{"err": err}).Error("Could not get evolution from Factorio")
msg = "Unknown"
}
messagesToDiscord <- msg
break
}
}
}

func checkRequiredEnvVariables() {
vars := []string{"DISCORD_TOKEN", "DISCORD_CHANNEL_ID", "RCON_IP", "RCON_PORT", "RCON_PASSWORD", "FACTORIO_LOG"}
for _, envVar := range vars {
Expand All @@ -312,27 +429,6 @@ func checkRequiredEnvVariables() {
}
}
}
func getenvStr(key string) (string, error) {
v := os.Getenv(key)
return v, nil
}

func getenvBool(key string) bool {
s, err := getenvStr(key)
if err != nil {
log.WithField("envVar", key).WithError(err).Error("Cannot parse env variable as boolean")
return false
}
if s == "" {
return false // No env var is false
}
v, err := strconv.ParseBool(s)
if err != nil {
log.WithField("envVar", key).WithError(err).Error("Cannot parse env variable as boolean")
return false
}
return v
}

type sConfig struct {
allRocketLaunches bool
Expand Down
Loading

0 comments on commit 19ecc8a

Please sign in to comment.