2020-12-21 00:32:39 +00:00
package main
import (
"fmt"
2021-08-25 18:53:04 +00:00
"net/http"
2020-12-21 00:32:39 +00:00
"os"
2021-04-03 12:11:47 +00:00
"strings"
2020-12-21 00:32:39 +00:00
"sync"
"time"
2021-08-06 22:00:22 +00:00
"github.com/go-irc/irc"
2021-08-25 18:53:04 +00:00
"github.com/gorilla/mux"
2021-04-04 20:06:12 +00:00
"github.com/pkg/errors"
2021-08-24 22:44:49 +00:00
"github.com/robfig/cron/v3"
2020-12-21 00:32:39 +00:00
log "github.com/sirupsen/logrus"
2021-06-14 21:42:40 +00:00
"github.com/Luzifer/go_helpers/v2/str"
2020-12-21 00:32:39 +00:00
"github.com/Luzifer/rconfig/v2"
2021-08-19 13:33:56 +00:00
"github.com/Luzifer/twitch-bot/twitch"
2020-12-21 00:32:39 +00:00
)
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)" `
2021-08-19 13:33:56 +00:00
PluginDir string ` flag:"plugin-dir" default:"/usr/lib/twitch-bot" description:"Where to find and load plugins" `
2021-01-10 21:15:57 +00:00
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" `
2021-07-12 12:52:30 +00:00
ValidateConfig bool ` flag:"validate-config,v" default:"false" description:"Loads the config, logs any errors and quits with status 0 on success" `
2021-01-10 21:15:57 +00:00
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 )
2021-08-24 22:44:49 +00:00
cronService * cron . Cron
2021-09-02 21:26:39 +00:00
ircHdl * ircHandler
2021-08-28 15:27:24 +00:00
router = mux . NewRouter ( )
2021-08-24 22:44:49 +00:00
2021-08-06 22:00:22 +00:00
sendMessage func ( m * irc . Message ) error
2021-08-19 13:33:56 +00:00
store = newStorageFile ( false )
twitchClient * twitch . Client
2020-12-21 00:32:39 +00:00
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
2021-06-28 22:01:26 +00:00
store = newStorageFile ( true ) // Use in-mem-store for tests
2021-04-03 12:11:47 +00:00
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-08-19 13:33:56 +00:00
//nolint: funlen,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-08-24 22:44:49 +00:00
cronService = cron . New ( )
twitchClient = twitch . New ( cfg . TwitchClient , cfg . TwitchToken )
2021-09-02 15:09:30 +00:00
twitchWatch := newTwitchWatcher ( )
2021-09-02 21:26:39 +00:00
cronService . AddFunc ( "@every 10s" , twitchWatch . Check ) // Query may run that often as the twitchClient has an internal cache
2021-09-02 15:09:30 +00:00
2021-08-28 15:27:24 +00:00
router . HandleFunc ( "/" , handleSwaggerHTML )
router . HandleFunc ( "/openapi.json" , handleSwaggerRequest )
2021-09-10 17:45:31 +00:00
if err = initCorePlugins ( ) ; err != nil {
log . WithError ( err ) . Fatal ( "Unable to load core plugins" )
}
2021-08-19 13:33:56 +00:00
if err = loadPlugins ( cfg . PluginDir ) ; err != nil {
log . WithError ( err ) . Fatal ( "Unable to load plugins" )
}
2021-07-12 12:52:30 +00:00
if err = loadConfig ( cfg . Config ) ; err != nil {
log . WithError ( err ) . Fatal ( "Initial config load failed" )
}
defer func ( ) { config . CloseRawMessageWriter ( ) } ( )
if cfg . ValidateConfig {
// We were asked to only validate the config, this was successful
log . Info ( "Config validated successfully" )
return
}
2021-09-02 15:54:28 +00:00
for _ , c := range config . Channels {
if err := twitchWatch . AddChannel ( c ) ; err != nil {
log . WithError ( err ) . WithField ( "channel" , c ) . Error ( "Unable to add channel to watcher" )
}
}
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" )
}
2021-05-24 15:55:12 +00:00
fsEvents := make ( chan configChangeEvent , 1 )
go watchConfigChanges ( cfg . Config , fsEvents )
2020-12-21 00:32:39 +00:00
var (
2021-03-27 16:59:56 +00:00
ircDisconnected = make ( chan struct { } , 1 )
autoMessageTicker = time . NewTicker ( time . Second )
2020-12-21 00:32:39 +00:00
)
2021-08-24 22:44:49 +00:00
cronService . Start ( )
2021-08-25 18:53:04 +00:00
if config . HTTPListen != "" {
// If listen address is configured start HTTP server
go http . ListenAndServe ( config . HTTPListen , router )
}
2020-12-21 00:32:39 +00:00
ircDisconnected <- struct { } { }
for {
select {
case <- ircDisconnected :
2021-09-02 21:26:39 +00:00
if ircHdl != nil {
2021-08-06 22:00:22 +00:00
sendMessage = nil
2021-09-02 21:26:39 +00:00
ircHdl . Close ( )
2020-12-21 00:32:39 +00:00
}
2021-09-02 21:26:39 +00:00
if ircHdl , err = newIRCHandler ( ) ; err != nil {
2020-12-21 00:32:39 +00:00
log . WithError ( err ) . Fatal ( "Unable to create IRC client" )
}
go func ( ) {
2021-09-02 21:26:39 +00:00
sendMessage = ircHdl . SendMessage
if err := ircHdl . Run ( ) ; err != nil {
2020-12-21 00:32:39 +00:00
log . WithError ( err ) . Error ( "IRC run exited unexpectedly" )
}
2021-08-06 22:00:22 +00:00
sendMessage = nil
2021-04-03 12:11:47 +00:00
time . Sleep ( ircReconnectDelay )
2020-12-21 00:32:39 +00:00
ircDisconnected <- struct { } { }
} ( )
2021-05-24 15:55:12 +00:00
case evt := <- fsEvents :
switch evt {
case configChangeEventUnkown :
2020-12-21 00:32:39 +00:00
continue
2021-05-24 15:55:12 +00:00
case configChangeEventNotExist :
log . Error ( "Config file is not available, not reloading config" )
continue
case configChangeEventModified :
// Fine, reload
2020-12-21 00:32:39 +00:00
}
2021-06-14 21:42:40 +00:00
previousChannels := append ( [ ] string { } , config . Channels ... )
2020-12-21 00:32:39 +00:00
if err := loadConfig ( cfg . Config ) ; err != nil {
log . WithError ( err ) . Error ( "Unable to reload config" )
continue
}
2021-09-02 21:26:39 +00:00
ircHdl . ExecuteJoins ( config . Channels )
2021-09-02 15:09:30 +00:00
for _ , c := range config . Channels {
if err := twitchWatch . AddChannel ( c ) ; err != nil {
log . WithError ( err ) . WithField ( "channel" , c ) . Error ( "Unable to add channel to watcher" )
}
}
2020-12-21 00:52:10 +00:00
2021-06-14 21:42:40 +00:00
for _ , c := range previousChannels {
if ! str . StringInSlice ( c , config . Channels ) {
log . WithField ( "channel" , c ) . Info ( "Leaving removed channel..." )
2021-09-02 21:26:39 +00:00
ircHdl . ExecutePart ( c )
2021-09-02 15:09:30 +00:00
if err := twitchWatch . RemoveChannel ( c ) ; err != nil {
log . WithError ( err ) . WithField ( "channel" , c ) . Error ( "Unable to remove channel from watcher" )
}
2021-06-14 21:42:40 +00:00
}
}
2021-03-27 16:59:56 +00:00
case <- autoMessageTicker . C :
configLock . RLock ( )
for _ , am := range config . AutoMessages {
if ! am . CanSend ( ) {
continue
}
2021-09-02 21:26:39 +00:00
if err := am . Send ( ircHdl . c ) ; err != nil {
2021-03-27 16:59:56 +00:00
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
}