From 51d7f7f0674bb1640cfd896c959c39c28b77f577 Mon Sep 17 00:00:00 2001 From: Chris Sexton Date: Thu, 10 Mar 2016 13:37:07 -0500 Subject: [PATCH] Move IRC stuff to its own package --- bot/bot.go | 58 ++------- bot/handlers.go | 109 +++-------------- bot/interfaces.go | 19 +++ config/config.go | 24 ++-- irc/irc.go | 302 ++++++++++++++++++++++++++++++++++++++++++++++ main.go | 187 ++++------------------------ slack/slack.go | 2 + 7 files changed, 387 insertions(+), 314 deletions(-) create mode 100644 bot/interfaces.go create mode 100644 irc/irc.go create mode 100644 slack/slack.go diff --git a/bot/bot.go b/bot/bot.go index 9c07bda..01905e4 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -11,15 +11,10 @@ import ( "time" "github.com/velour/catbase/config" - "github.com/velour/velour/irc" _ "github.com/mattn/go-sqlite3" ) -const actionPrefix = "\x01ACTION" - -var throttle <-chan time.Time - // Bot type provides storage for bot-wide information, configs, and database connections type Bot struct { // Each plugin must be registered in our plugins handler. To come: a map so that this @@ -32,11 +27,10 @@ type Bot struct { // Represents the bot Me User - // Conn allows us to send messages and modify our connection state - Client *irc.Client - Config *config.Config + Conn Connector + // SQL DB // TODO: I think it'd be nice to use https://github.com/jmoiron/sqlx so that // the select/update/etc statements could be simplified with struct @@ -103,8 +97,8 @@ type Variable struct { } // NewBot creates a Bot for a given connection and set of handlers. -func NewBot(config *config.Config, c *irc.Client) *Bot { - sqlDB, err := sql.Open("sqlite3", config.DbFile) +func NewBot(config *config.Config, connector Connector) *Bot { + sqlDB, err := sql.Open("sqlite3", config.DB.File) if err != nil { log.Fatal(err) } @@ -124,9 +118,9 @@ func NewBot(config *config.Config, c *irc.Client) *Bot { Config: config, Plugins: make(map[string]Handler), PluginOrdering: make([]string, 0), + Conn: connector, Users: users, Me: users[0], - Client: c, DB: sqlDB, logIn: logIn, logOut: logOut, @@ -142,6 +136,9 @@ func NewBot(config *config.Config, c *irc.Client) *Bot { } go http.ListenAndServe(config.HttpAddr, nil) + connector.RegisterMessageRecieved(bot.MsgRecieved) + connector.RegisterEventRecieved(bot.EventRecieved) + return bot } @@ -197,45 +194,6 @@ func (b *Bot) AddHandler(name string, h Handler) { } } -func (b *Bot) SendMessage(channel, message string) { - if !strings.HasPrefix(message, actionPrefix) { - b.selfSaid(channel, message, false) - } - for len(message) > 0 { - m := irc.Msg{ - Cmd: "PRIVMSG", - Args: []string{channel, message}, - } - _, err := m.RawString() - if err != nil { - mtl := err.(irc.MsgTooLong) - m.Args[1] = message[:mtl.NTrunc] - message = message[mtl.NTrunc:] - } else { - message = "" - } - - if throttle == nil { - ratePerSec := b.Config.RatePerSec - throttle = time.Tick(time.Second / time.Duration(ratePerSec)) - } - - <-throttle - - b.Client.Out <- m - } -} - -// Sends action to channel -func (b *Bot) SendAction(channel, message string) { - // Notify plugins that we've said something - b.selfSaid(channel, message, true) - - message = actionPrefix + " " + message + "\x01" - - b.SendMessage(channel, message) -} - func (b *Bot) Who(channel string) []User { out := []User{} for _, u := range b.Users { diff --git a/bot/handlers.go b/bot/handlers.go index 36f557f..bf0165f 100644 --- a/bot/handlers.go +++ b/bot/handlers.go @@ -12,18 +12,14 @@ import ( "strconv" "strings" "time" - - "github.com/velour/velour/irc" ) // Handles incomming PRIVMSG requests -func (b *Bot) MsgRecieved(client *irc.Client, inMsg irc.Msg) { - log.Println("Recieved message: ", inMsg) - if inMsg.User == "" { - return - } +func (b *Bot) MsgRecieved(msg Message) { + log.Println("Recieved message: ", msg) - msg := b.buildMessage(client, inMsg) + // msg := b.buildMessage(client, inMsg) + // do need to look up user and fix it if strings.HasPrefix(msg.Body, "help") && msg.Command { parts := strings.Fields(strings.ToLower(msg.Body)) @@ -44,27 +40,23 @@ RET: } // Handle incoming events -func (b *Bot) EventRecieved(conn *irc.Client, inMsg irc.Msg) { - log.Println("Recieved event: ", inMsg) - if inMsg.User == "" { - return - } - msg := b.buildMessage(conn, inMsg) +func (b *Bot) EventRecieved(msg Message) { + log.Println("Recieved event: ", msg) + //msg := b.buildMessage(conn, inMsg) for _, name := range b.PluginOrdering { p := b.Plugins[name] - if p.Event(inMsg.Cmd, msg) { + if p.Event(msg.Body, msg) { // TODO: could get rid of msg.Body break } } } -// Interface used for compatibility with the Plugin interface -type Handler interface { - Message(message Message) bool - Event(kind string, message Message) bool - BotMessage(message Message) bool - Help(channel string, parts []string) - RegisterWeb() *string +func (b *Bot) SendMessage(channel, message string) { + b.Conn.SendMessage(channel, message) +} + +func (b *Bot) SendAction(channel, message string) { + b.Conn.SendAction(channel, message) } // Checks to see if the user is asking for help, returns true if so and handles the situation. @@ -96,79 +88,6 @@ func (b *Bot) checkHelp(channel string, parts []string) { } } -// Checks if message is a command and returns its curtailed version -func (b *Bot) isCmd(message string) (bool, string) { - cmdc := b.Config.CommandChar - botnick := strings.ToLower(b.Config.Nick) - iscmd := false - lowerMessage := strings.ToLower(message) - - if strings.HasPrefix(lowerMessage, cmdc) && len(cmdc) > 0 { - iscmd = true - message = message[len(cmdc):] - // } else if match, _ := regexp.MatchString(rex, lowerMessage); match { - } else if strings.HasPrefix(lowerMessage, botnick) && - len(lowerMessage) > len(botnick) && - (lowerMessage[len(botnick)] == ',' || lowerMessage[len(botnick)] == ':') { - - iscmd = true - message = message[len(botnick):] - - // trim off the customary addressing punctuation - if message[0] == ':' || message[0] == ',' { - message = message[1:] - } - } - - // trim off any whitespace left on the message - message = strings.TrimSpace(message) - - return iscmd, message -} - -// Builds our internal message type out of a Conn & Line from irc -func (b *Bot) buildMessage(conn *irc.Client, inMsg irc.Msg) Message { - // Check for the user - user := b.GetUser(inMsg.Origin) - - channel := inMsg.Args[0] - if channel == b.Config.Nick { - channel = inMsg.Args[0] - } - - isAction := false - var message string - if len(inMsg.Args) > 1 { - message = inMsg.Args[1] - - isAction = strings.HasPrefix(message, actionPrefix) - if isAction { - message = strings.TrimRight(message[len(actionPrefix):], "\x01") - message = strings.TrimSpace(message) - } - - } - - iscmd := false - filteredMessage := message - if !isAction { - iscmd, filteredMessage = b.isCmd(message) - } - - msg := Message{ - User: user, - Channel: channel, - Body: filteredMessage, - Raw: message, - Command: iscmd, - Action: isAction, - Time: time.Now(), - Host: inMsg.Host, - } - - return msg -} - func (b *Bot) LastMessage(channel string) (Message, error) { log := <-b.logOut if len(log) == 0 { diff --git a/bot/interfaces.go b/bot/interfaces.go new file mode 100644 index 0000000..b38fe16 --- /dev/null +++ b/bot/interfaces.go @@ -0,0 +1,19 @@ +package bot + +type Connector interface { + RegisterEventRecieved(func(message Message)) + RegisterMessageRecieved(func(message Message)) + + SendMessage(channel, message string) + SendAction(channel, message string) + Serve() +} + +// Interface used for compatibility with the Plugin interface +type Handler interface { + Message(message Message) bool + Event(kind string, message Message) bool + BotMessage(message Message) bool + Help(channel string, parts []string) + RegisterWeb() *string +} diff --git a/config/config.go b/config/config.go index 18b0d62..a946823 100644 --- a/config/config.go +++ b/config/config.go @@ -9,13 +9,19 @@ import "io/ioutil" // Config stores any system-wide startup information that cannot be easily configured via // the database type Config struct { - DbFile string - DbName string - DbServer string - Channels []string - MainChannel string - Plugins []string - Nick, Server, Pass string + DB struct { + File string + Name string + Server string + } + Channels []string + MainChannel string + Plugins []string + Type string + Irc struct { + Server, Pass string + } + Nick string FullName string Version string CommandChar string @@ -58,6 +64,10 @@ func Readconfig(version, cfile string) *Config { } c.Version = version + if c.Type == "" { + c.Type = "irc" + } + fmt.Printf("godeepintir version %s running.\n", c.Version) return &c diff --git a/irc/irc.go b/irc/irc.go new file mode 100644 index 0000000..63614e4 --- /dev/null +++ b/irc/irc.go @@ -0,0 +1,302 @@ +package irc + +import ( + "io" + "log" + "os" + "strings" + "time" + + "github.com/velour/catbase/bot" + "github.com/velour/catbase/config" + "github.com/velour/velour/irc" +) + +const ( + // DefaultPort is the port used to connect to + // the server if one is not specified. + defaultPort = "6667" + + // InitialTimeout is the initial amount of time + // to delay before reconnecting. Each failed + // reconnection doubles the timout until + // a connection is made successfully. + initialTimeout = 2 * time.Second + + // PingTime is the amount of inactive time + // to wait before sending a ping to the server. + pingTime = 120 * time.Second + + actionPrefix = "\x01ACTION" +) + +var throttle <-chan time.Time + +type Irc struct { + Client *irc.Client + config *config.Config + quit chan bool + + eventRecieved func(bot.Message) + messageRecieved func(bot.Message) +} + +func New(c *config.Config) *Irc { + i := Irc{} + i.config = c + + return &i +} + +func (i *Irc) RegisterEventRecieved(f func(bot.Message)) { + i.eventRecieved = f +} + +func (i *Irc) RegisterMessageRecieved(f func(bot.Message)) { + i.messageRecieved = f +} + +func (i *Irc) JoinChannel(channel string) { + log.Printf("Joining channel: %s", channel) + i.Client.Out <- irc.Msg{Cmd: irc.JOIN, Args: []string{channel}} +} + +func (i *Irc) SendMessage(channel, message string) { + for len(message) > 0 { + m := irc.Msg{ + Cmd: "PRIVMSG", + Args: []string{channel, message}, + } + _, err := m.RawString() + if err != nil { + mtl := err.(irc.MsgTooLong) + m.Args[1] = message[:mtl.NTrunc] + message = message[mtl.NTrunc:] + } else { + message = "" + } + + if throttle == nil { + ratePerSec := i.config.RatePerSec + throttle = time.Tick(time.Second / time.Duration(ratePerSec)) + } + + <-throttle + + i.Client.Out <- m + } +} + +// Sends action to channel +func (i *Irc) SendAction(channel, message string) { + message = actionPrefix + " " + message + "\x01" + + i.SendMessage(channel, message) +} + +func (i *Irc) Serve() { + if i.eventRecieved == nil || i.messageRecieved == nil { + log.Fatal("Missing an event handler") + } + + var err error + i.Client, err = irc.DialSSL( + i.config.Irc.Server, + i.config.Nick, + i.config.FullName, + i.config.Irc.Pass, + true, + ) + if err != nil { + log.Fatal(err) + } + + for _, c := range i.config.Channels { + i.JoinChannel(c) + } + + i.quit = make(chan bool) + go i.handleConnection() + <-i.quit +} + +func (i *Irc) handleConnection() { + t := time.NewTimer(pingTime) + + defer func() { + t.Stop() + close(i.Client.Out) + for err := range i.Client.Errors { + if err != io.EOF { + log.Println(err) + } + } + }() + + for { + select { + case msg, ok := <-i.Client.In: + if !ok { // disconnect + i.quit <- true + return + } + t.Stop() + t = time.NewTimer(pingTime) + i.handleMsg(msg) + + case <-t.C: + i.Client.Out <- irc.Msg{Cmd: irc.PING, Args: []string{i.Client.Server}} + t = time.NewTimer(pingTime) + + case err, ok := <-i.Client.Errors: + if ok && err != io.EOF { + log.Println(err) + i.quit <- true + return + } + } + } +} + +// HandleMsg handles IRC messages from the server. +func (i *Irc) handleMsg(msg irc.Msg) { + botMsg := i.buildMessage(msg) + + switch msg.Cmd { + case irc.ERROR: + log.Println(1, "Received error: "+msg.Raw) + + case irc.PING: + i.Client.Out <- irc.Msg{Cmd: irc.PONG} + + case irc.PONG: + // OK, ignore + + case irc.ERR_NOSUCHNICK: + i.eventRecieved(botMsg) + + case irc.ERR_NOSUCHCHANNEL: + i.eventRecieved(botMsg) + + case irc.RPL_MOTD: + i.eventRecieved(botMsg) + + case irc.RPL_NAMREPLY: + i.eventRecieved(botMsg) + + case irc.RPL_TOPIC: + i.eventRecieved(botMsg) + + case irc.KICK: + i.eventRecieved(botMsg) + + case irc.TOPIC: + i.eventRecieved(botMsg) + + case irc.MODE: + i.eventRecieved(botMsg) + + case irc.JOIN: + i.eventRecieved(botMsg) + + case irc.PART: + i.eventRecieved(botMsg) + + case irc.QUIT: + os.Exit(1) + + case irc.NOTICE: + i.eventRecieved(botMsg) + + case irc.PRIVMSG: + i.messageRecieved(botMsg) + + case irc.NICK: + i.eventRecieved(botMsg) + + case irc.RPL_WHOREPLY: + i.eventRecieved(botMsg) + + case irc.RPL_ENDOFWHO: + i.eventRecieved(botMsg) + + default: + cmd := irc.CmdNames[msg.Cmd] + log.Println("(" + cmd + ") " + msg.Raw) + } +} + +// Builds our internal message type out of a Conn & Line from irc +func (i *Irc) buildMessage(inMsg irc.Msg) bot.Message { + // Check for the user + user := bot.User{ + Name: inMsg.Origin, + } + + channel := inMsg.Args[0] + if channel == i.config.Nick { + channel = inMsg.Args[0] + } + + isAction := false + var message string + if len(inMsg.Args) > 1 { + message = inMsg.Args[1] + + isAction = strings.HasPrefix(message, actionPrefix) + if isAction { + message = strings.TrimRight(message[len(actionPrefix):], "\x01") + message = strings.TrimSpace(message) + } + + } + + iscmd := false + filteredMessage := message + if !isAction { + iscmd, filteredMessage = i.isCmd(message) + } + + msg := bot.Message{ + User: &user, + Channel: channel, + Body: filteredMessage, + Raw: message, + Command: iscmd, + Action: isAction, + Time: time.Now(), + Host: inMsg.Host, + } + + return msg +} + +// Checks if message is a command and returns its curtailed version +func (i *Irc) isCmd(message string) (bool, string) { + cmdc := i.config.CommandChar + botnick := strings.ToLower(i.config.Nick) + iscmd := false + lowerMessage := strings.ToLower(message) + + if strings.HasPrefix(lowerMessage, cmdc) && len(cmdc) > 0 { + iscmd = true + message = message[len(cmdc):] + // } else if match, _ := regexp.MatchString(rex, lowerMessage); match { + } else if strings.HasPrefix(lowerMessage, botnick) && + len(lowerMessage) > len(botnick) && + (lowerMessage[len(botnick)] == ',' || lowerMessage[len(botnick)] == ':') { + + iscmd = true + message = message[len(botnick):] + + // trim off the customary addressing punctuation + if message[0] == ':' || message[0] == ',' { + message = message[1:] + } + } + + // trim off any whitespace left on the message + message = strings.TrimSpace(message) + + return iscmd, message +} diff --git a/main.go b/main.go index 26e2653..032b508 100644 --- a/main.go +++ b/main.go @@ -4,181 +4,44 @@ package main import ( "flag" - "io" "log" - "os" - "time" "github.com/velour/catbase/bot" "github.com/velour/catbase/config" + "github.com/velour/catbase/irc" "github.com/velour/catbase/plugins" - "github.com/velour/velour/irc" -) - -const ( - // DefaultPort is the port used to connect to - // the server if one is not specified. - defaultPort = "6667" - - // InitialTimeout is the initial amount of time - // to delay before reconnecting. Each failed - // reconnection doubles the timout until - // a connection is made successfully. - initialTimeout = 2 * time.Second - - // PingTime is the amount of inactive time - // to wait before sending a ping to the server. - pingTime = 120 * time.Second -) - -var ( - Client *irc.Client - Bot *bot.Bot - Config *config.Config ) func main() { - var err error var cfile = flag.String("config", "config.json", "Config file to load. (Defaults to config.json)") flag.Parse() // parses the logging flags. - Config = config.Readconfig(Version, *cfile) - - Client, err = irc.DialSSL( - Config.Server, - Config.Nick, - Config.FullName, - Config.Pass, - true, - ) - - if err != nil { - log.Fatal(err) - } - - Bot = bot.NewBot(Config, Client) - // Bot.AddHandler(plugins.NewTestPlugin(Bot)) - Bot.AddHandler("admin", plugins.NewAdminPlugin(Bot)) - Bot.AddHandler("first", plugins.NewFirstPlugin(Bot)) - Bot.AddHandler("downtime", plugins.NewDowntimePlugin(Bot)) - Bot.AddHandler("talker", plugins.NewTalkerPlugin(Bot)) - Bot.AddHandler("dice", plugins.NewDicePlugin(Bot)) - Bot.AddHandler("beers", plugins.NewBeersPlugin(Bot)) - Bot.AddHandler("counter", plugins.NewCounterPlugin(Bot)) - Bot.AddHandler("remember", plugins.NewRememberPlugin(Bot)) - Bot.AddHandler("skeleton", plugins.NewSkeletonPlugin(Bot)) - Bot.AddHandler("your", plugins.NewYourPlugin(Bot)) - // catches anything left, will always return true - Bot.AddHandler("factoid", plugins.NewFactoidPlugin(Bot)) - - handleConnection() - - // And a signal on disconnect - quit := make(chan bool) - - // Wait for disconnect - <-quit -} - -func handleConnection() { - t := time.NewTimer(pingTime) - - defer func() { - t.Stop() - close(Client.Out) - for err := range Client.Errors { - if err != io.EOF { - log.Println(err) - } - } - }() - - for { - select { - case msg, ok := <-Client.In: - if !ok { // disconnect - return - } - t.Stop() - t = time.NewTimer(pingTime) - handleMsg(msg) - - case <-t.C: - Client.Out <- irc.Msg{Cmd: irc.PING, Args: []string{Client.Server}} - t = time.NewTimer(pingTime) - - case err, ok := <-Client.Errors: - if ok && err != io.EOF { - log.Println(err) - return - } - } - } -} - -// HandleMsg handles IRC messages from the server. -func handleMsg(msg irc.Msg) { - switch msg.Cmd { - case irc.ERROR: - log.Println(1, "Received error: "+msg.Raw) - - case irc.PING: - Client.Out <- irc.Msg{Cmd: irc.PONG} - - case irc.PONG: - // OK, ignore - - case irc.ERR_NOSUCHNICK: - Bot.EventRecieved(Client, msg) - - case irc.ERR_NOSUCHCHANNEL: - Bot.EventRecieved(Client, msg) - - case irc.RPL_MOTD: - Bot.EventRecieved(Client, msg) - - case irc.RPL_NAMREPLY: - Bot.EventRecieved(Client, msg) - - case irc.RPL_TOPIC: - Bot.EventRecieved(Client, msg) - - case irc.KICK: - Bot.EventRecieved(Client, msg) - - case irc.TOPIC: - Bot.EventRecieved(Client, msg) - - case irc.MODE: - Bot.EventRecieved(Client, msg) - - case irc.JOIN: - Bot.EventRecieved(Client, msg) - - case irc.PART: - Bot.EventRecieved(Client, msg) - - case irc.QUIT: - os.Exit(1) - - case irc.NOTICE: - Bot.EventRecieved(Client, msg) - - case irc.PRIVMSG: - Bot.MsgRecieved(Client, msg) - - case irc.NICK: - Bot.EventRecieved(Client, msg) - - case irc.RPL_WHOREPLY: - Bot.EventRecieved(Client, msg) - - case irc.RPL_ENDOFWHO: - Bot.EventRecieved(Client, msg) + c := config.Readconfig(Version, *cfile) + var client bot.Connector + switch c.Type { + case "irc": + client = irc.New(c) default: - cmd := irc.CmdNames[msg.Cmd] - log.Println("(" + cmd + ") " + msg.Raw) + log.Fatalf("Unknown connection type: %s", c.Type) } + + b := bot.NewBot(c, client) + + // b.AddHandler(plugins.NewTestPlugin(b)) + b.AddHandler("admin", plugins.NewAdminPlugin(b)) + b.AddHandler("first", plugins.NewFirstPlugin(b)) + b.AddHandler("downtime", plugins.NewDowntimePlugin(b)) + b.AddHandler("talker", plugins.NewTalkerPlugin(b)) + b.AddHandler("dice", plugins.NewDicePlugin(b)) + b.AddHandler("beers", plugins.NewBeersPlugin(b)) + b.AddHandler("counter", plugins.NewCounterPlugin(b)) + b.AddHandler("remember", plugins.NewRememberPlugin(b)) + b.AddHandler("skeleton", plugins.NewSkeletonPlugin(b)) + b.AddHandler("your", plugins.NewYourPlugin(b)) + // catches anything left, will always return true + b.AddHandler("factoid", plugins.NewFactoidPlugin(b)) + + client.Serve() } diff --git a/slack/slack.go b/slack/slack.go new file mode 100644 index 0000000..9766782 --- /dev/null +++ b/slack/slack.go @@ -0,0 +1,2 @@ +// Package to connect to slack service +package slack