package irc import ( "bufio" "encoding/json" "errors" "fmt" "io" "io/ioutil" "math/rand" "net" "net/textproto" "regexp" "strings" "time" rgb "github.com/foresthoffman/rgblog" ) const PSTFormat = "Jan 2 15:04:05 PST" // Regex for parsing PRIVMSG strings. // // First matched group is the user's name and the second matched group is the content of the // user's message. var MsgRegex *regexp.Regexp = regexp.MustCompile(`^:(\w+)!\w+@\w+\.tmi\.twitch\.tv (PRIVMSG) #\w+(?: :(.*))?$`) // Regex for parsing user commands, from already parsed PRIVMSG strings. // // First matched group is the command name and the second matched group is the argument for the // command. var CmdRegex *regexp.Regexp = regexp.MustCompile(`^!(\w+)\s?(\w+)?`) type OAuthCred struct { // The bot account's OAuth password. Password string `json:"password,omitempty"` // The developer application client ID. Used for API calls to Twitch. ClientID string `json:"client_id,omitempty"` } type KardBot struct { Channel string conn net.Conn Credentials *OAuthCred MsgRate time.Duration Name string Port string PrivatePath string Server string startTime time.Time Prompts []string } // Connects the bot to the Twitch IRC server. The bot will continue to try to connect until it // succeeds or is forcefully shutdown. func (bb *KardBot) Connect() { var err error rgb.YPrintf("[%s] Connecting to %s...\n", TimeStamp(), bb.Server) // makes connection to Twitch IRC server bb.conn, err = net.Dial("tcp", bb.Server+":"+bb.Port) if nil != err { rgb.YPrintf("[%s] Cannot connect to %s, retrying.\n", TimeStamp(), bb.Server) bb.Connect() return } rgb.YPrintf("[%s] Connected to %s!\n", TimeStamp(), bb.Server) bb.startTime = time.Now() } // Officially disconnects the bot from the Twitch IRC server. func (bb *KardBot) Disconnect() { bb.conn.Close() upTime := time.Now().Sub(bb.startTime).Seconds() rgb.YPrintf("[%s] Closed connection from %s! | Live for: %fs\n", TimeStamp(), bb.Server, upTime) } // Listens for and logs messages from chat. Responds to commands from the channel owner. The bot // continues until it gets disconnected, told to shutdown, or forcefully shutdown. func (bb *KardBot) HandleChat() error { rgb.YPrintf("[%s] Watching #%s...\n", TimeStamp(), bb.Channel) // reads from connection tp := textproto.NewReader(bufio.NewReader(bb.conn)) // listens for chat messages for { line, err := tp.ReadLine() if nil != err { // officially disconnects the bot from the server bb.Disconnect() return errors.New("bb.Bot.HandleChat: Failed to read line from channel. Disconnected.") } // logs the response from the IRC server rgb.YPrintf("[%s] %s\n", TimeStamp(), line) if "PING :tmi.twitch.tv" == line { // respond to PING message with a PONG message, to maintain the connection bb.conn.Write([]byte("PONG :tmi.twitch.tv\r\n")) continue } else { // handle a PRIVMSG message matches := MsgRegex.FindStringSubmatch(line) if nil != matches { userName := matches[1] msgType := matches[2] switch msgType { case "PRIVMSG": msg := matches[3] rgb.GPrintf("[%s] %s: %s\n", TimeStamp(), userName, msg) // parse commands from user message cmdMatches := CmdRegex.FindStringSubmatch(msg) if nil != cmdMatches { cmd := cmdMatches[1] switch cmd { case "card": rgb.CPrintf("[%s] Card asked for!\n", TimeStamp()) bb.Say("Your prompt is : " + bb.Prompts[rand.Intn(len(bb.Prompts))]) } // channel-owner specific commands if userName == bb.Channel { switch cmd { case "tbdown": rgb.CPrintf( "[%s] Shutdown command received. Shutting down now...\n", TimeStamp(), ) bb.Disconnect() return nil default: // do nothing } } } default: // do nothing } } } time.Sleep(bb.MsgRate) } } // Makes the bot join its pre-specified channel. func (bb *KardBot) JoinChannel() { rgb.YPrintf("[%s] Joining #%s...\n", TimeStamp(), bb.Channel) bb.conn.Write([]byte("PASS " + bb.Credentials.Password + "\r\n")) bb.conn.Write([]byte("NICK " + bb.Name + "\r\n")) bb.conn.Write([]byte("JOIN #" + bb.Channel + "\r\n")) rgb.YPrintf("[%s] Joined #%s as @%s!\n", TimeStamp(), bb.Channel, bb.Name) } // Reads from the private credentials file and stores the data in the bot's Credentials field. func (bb *KardBot) ReadCredentials() error { // reads from the file credFile, err := ioutil.ReadFile(bb.PrivatePath) if nil != err { return err } bb.Credentials = &OAuthCred{} // parses the file contents dec := json.NewDecoder(strings.NewReader(string(credFile))) if err = dec.Decode(bb.Credentials); nil != err && io.EOF != err { return err } return nil } // Makes the bot send a message to the chat channel. func (bb *KardBot) Say(msg string) error { if "" == msg { return errors.New("BasicBot.Say: msg was empty.") } // check if message is too large for IRC if len(msg) > 512 { return errors.New("BasicBot.Say: msg exceeded 512 bytes") } _, err := bb.conn.Write([]byte(fmt.Sprintf("PRIVMSG #%s :%s\r\n", bb.Channel, msg))) if nil != err { return err } return nil } // Starts a loop where the bot will attempt to connect to the Twitch IRC server, then connect to the // pre-specified channel, and then handle the chat. It will attempt to reconnect until it is told to // shut down, or is forcefully shutdown. func (bb *KardBot) Start() { err := bb.ReadCredentials() if nil != err { fmt.Println(err) fmt.Println("Aborting...") return } for { bb.Connect() bb.JoinChannel() err = bb.HandleChat() if nil != err { // attempts to reconnect upon unexpected chat error time.Sleep(1000 * time.Millisecond) fmt.Println(err) fmt.Println("Starting bot again...") } else { return } } } func TimeStamp() string { return TimeStampFmt(PSTFormat) } func TimeStampFmt(format string) string { return time.Now().Format(format) }