2020-12-21 00:32:39 +00:00
package main
import (
"fmt"
"os"
2021-04-03 12:11:47 +00:00
"strings"
2020-12-21 00:32:39 +00:00
"sync"
"time"
2021-04-04 20:06:12 +00:00
"github.com/pkg/errors"
2020-12-21 00:32:39 +00:00
log "github.com/sirupsen/logrus"
"gopkg.in/fsnotify.v1"
"github.com/Luzifer/rconfig/v2"
)
2021-04-03 12:11:47 +00:00
const ircReconnectDelay = 100 * time . Millisecond
2020-12-21 00:32:39 +00:00
var (
cfg = struct {
2021-01-10 21:15:57 +00:00
CommandTimeout time . Duration ` flag:"command-timeout" default:"30s" description:"Timeout for command execution" `
Config string ` flag:"config,c" default:"./config.yaml" description:"Location of configuration file" `
2021-04-21 21:44:13 +00:00
IRCRateLimit time . Duration ` flag:"rate-limit" default:"1500ms" description:"How often to send a message (default: 20/30s=1500ms, if your bot is mod everywhere: 100/30s=300ms, different for known/verified bots)" `
2021-01-10 21:15:57 +00:00
LogLevel string ` flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)" `
StorageFile string ` flag:"storage-file" default:"./storage.json.gz" description:"Where to store the data" `
2021-04-04 20:06:12 +00:00
TwitchClient string ` flag:"twitch-client" default:"" description:"Client ID to act as" `
2021-01-10 21:15:57 +00:00
TwitchToken string ` flag:"twitch-token" default:"" description:"OAuth token valid for client" `
VersionAndExit bool ` flag:"version" default:"false" description:"Prints current version and exits" `
2020-12-21 00:32:39 +00:00
} { }
config * configFile
configLock = new ( sync . RWMutex )
store = newStorageFile ( )
version = "dev"
)
func init ( ) {
2021-04-03 12:11:47 +00:00
for _ , a := range os . Args {
if strings . HasPrefix ( a , "-test." ) {
// Skip initialize for test run
return
}
}
2020-12-21 00:32:39 +00:00
rconfig . AutoEnv ( true )
if err := rconfig . ParseAndValidate ( & cfg ) ; err != nil {
log . Fatalf ( "Unable to parse commandline options: %s" , err )
}
if cfg . VersionAndExit {
fmt . Printf ( "twitch-bot %s\n" , version )
os . Exit ( 0 )
}
if l , err := log . ParseLevel ( cfg . LogLevel ) ; err != nil {
log . WithError ( err ) . Fatal ( "Unable to parse log level" )
} else {
log . SetLevel ( l )
}
}
2021-04-03 12:11:47 +00:00
//nolint: gocognit,gocyclo // Complexity is a little too high but makes no sense to split
2020-12-21 00:32:39 +00:00
func main ( ) {
var err error
2021-04-04 20:06:12 +00:00
if err = startCheck ( ) ; err != nil {
log . WithError ( err ) . Fatal ( "Missing required parameters" )
}
2020-12-21 00:32:39 +00:00
if err = store . Load ( ) ; err != nil {
log . WithError ( err ) . Fatal ( "Unable to load storage file" )
}
if err = loadConfig ( cfg . Config ) ; err != nil {
log . WithError ( err ) . Fatal ( "Initial config load failed" )
}
2021-04-09 16:14:44 +00:00
defer func ( ) { config . CloseRawMessageWriter ( ) } ( )
2020-12-21 00:32:39 +00:00
fswatch , err := fsnotify . NewWatcher ( )
if err != nil {
log . WithError ( err ) . Fatal ( "Unable to create file watcher" )
}
if err = fswatch . Add ( cfg . Config ) ; err != nil {
log . WithError ( err ) . Error ( "Unable to watch config, auto-reload will not work" )
}
var (
2021-03-27 16:59:56 +00:00
irc * ircHandler
ircDisconnected = make ( chan struct { } , 1 )
autoMessageTicker = time . NewTicker ( time . Second )
2020-12-21 00:32:39 +00:00
)
ircDisconnected <- struct { } { }
for {
select {
case <- ircDisconnected :
if irc != nil {
irc . Close ( )
}
if irc , err = newIRCHandler ( ) ; err != nil {
log . WithError ( err ) . Fatal ( "Unable to create IRC client" )
}
go func ( ) {
if err := irc . Run ( ) ; err != nil {
log . WithError ( err ) . Error ( "IRC run exited unexpectedly" )
}
2021-04-03 12:11:47 +00:00
time . Sleep ( ircReconnectDelay )
2020-12-21 00:32:39 +00:00
ircDisconnected <- struct { } { }
} ( )
case evt := <- fswatch . Events :
if evt . Op & fsnotify . Write != fsnotify . Write {
continue
}
if err := loadConfig ( cfg . Config ) ; err != nil {
log . WithError ( err ) . Error ( "Unable to reload config" )
continue
}
2020-12-21 00:52:10 +00:00
irc . ExecuteJoins ( config . Channels )
2020-12-21 00:32:39 +00:00
log . Info ( "Config file reloaded" )
2021-03-27 16:59:56 +00:00
case <- autoMessageTicker . C :
configLock . RLock ( )
for _ , am := range config . AutoMessages {
if ! am . CanSend ( ) {
continue
}
if err := am . Send ( irc . c ) ; err != nil {
log . WithError ( err ) . Error ( "Unable to send automated message" )
}
}
configLock . RUnlock ( )
2020-12-21 00:32:39 +00:00
}
}
}
2021-04-04 20:06:12 +00:00
func startCheck ( ) error {
var errs [ ] string
if cfg . TwitchClient == "" {
errs = append ( errs , "No Twitch-ClientId given" )
}
if cfg . TwitchToken == "" {
errs = append ( errs , "Twitch-Token is unset" )
}
if len ( errs ) > 0 {
fmt . Println ( `
You ' ve not provided a Twitch - ClientId and / or a Twitch - Token .
These parameters are required and you need to provide them . In case
you need help with obtaining those credentials please visit the
following website :
https : //luzifer.github.io/twitch-bot/
You will be guided through the token generation and can afterwards
2021-04-04 20:12:20 +00:00
provide the required configuration parameters . ` )
2021-04-04 20:06:12 +00:00
return errors . New ( strings . Join ( errs , ", " ) )
}
return nil
}