diff --git a/bot/bot.go b/bot/bot.go index 773ef18..ed4e9bf 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -1,8 +1,8 @@ package bot import ( + "code.google.com/p/velour/irc" "github.com/chrissexton/alepale/config" - irc "github.com/fluffle/goirc/client" "html/template" "labix.org/v2/mgo" "log" @@ -11,6 +11,8 @@ import ( "time" ) +const actionPrefix = "\x01ACTION" + // 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 @@ -24,7 +26,7 @@ type Bot struct { Me User // Conn allows us to send messages and modify our connection state - Conn *irc.Conn + Client *irc.Client Config *config.Config @@ -78,23 +80,6 @@ func (l *Logger) Run() { } } -// User type stores user history. This is a vehicle that will follow the user for the active -// session -type User struct { - // Current nickname known - Name string - - // LastSeen DateTime - - // Alternative nicknames seen - Alts []string - - // Last N messages sent to the user - MessageLog []string - - Admin bool -} - type Message struct { User *User Channel, Body string @@ -110,7 +95,7 @@ type Variable struct { } // NewBot creates a Bot for a given connection and set of handlers. -func NewBot(config *config.Config, c *irc.Conn) *Bot { +func NewBot(config *config.Config, c *irc.Client) *Bot { session, err := mgo.Dial(config.DbServer) if err != nil { panic(err) @@ -123,12 +108,9 @@ func NewBot(config *config.Config, c *irc.Conn) *Bot { RunNewLogger(logIn, logOut) - config.Nick = c.Me.Nick - users := []User{ User{ - Name: config.Nick, - MessageLog: make([]string, 0), + Name: config.Nick, }, } @@ -138,7 +120,7 @@ func NewBot(config *config.Config, c *irc.Conn) *Bot { PluginOrdering: make([]string, 0), Users: users, Me: users[0], - Conn: c, + Client: c, DbSession: session, Db: db, varColl: db.C("variables"), @@ -166,25 +148,44 @@ func (b *Bot) AddHandler(name string, h Handler) { } } -// Sends message to channel func (b *Bot) SendMessage(channel, message string) { - b.Conn.Privmsg(channel, message) + 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 = "" + } + b.Client.Out <- m + } - // Notify plugins that we've said something b.selfSaid(channel, message) } // Sends action to channel func (b *Bot) SendAction(channel, message string) { - b.Conn.Action(channel, message) + // TODO: ADD CTCP ACTION + message = actionPrefix + " " + message + "\x01" + + b.SendMessage(channel, message) // Notify plugins that we've said something b.selfSaid(channel, message) } // Handles incomming PRIVMSG requests -func (b *Bot) MsgRecieved(conn *irc.Conn, line *irc.Line) { - msg := b.buildMessage(conn, line) +func (b *Bot) MsgRecieved(client *irc.Client, inMsg irc.Msg) { + if inMsg.User == "" { + return + } + + msg := b.buildMessage(client, inMsg) if strings.HasPrefix(msg.Body, "help") && msg.Command { parts := strings.Fields(strings.ToLower(msg.Body)) @@ -204,6 +205,23 @@ RET: return } +func (b *Bot) EventRecieved(conn *irc.Client, inMsg irc.Msg) { + if inMsg.User == "" { + return + } + msg := b.buildMessage(conn, inMsg) + for _, name := range b.PluginOrdering { + p := b.Plugins[name] + if p.Event(inMsg.Cmd, msg) { + break + } + } +} + +func (b *Bot) Who(channel string) []User { + return b.Users +} + var rootIndex string = ` diff --git a/bot/handlers.go b/bot/handlers.go index 0dab231..0ae593d 100644 --- a/bot/handlers.go +++ b/bot/handlers.go @@ -1,6 +1,7 @@ package bot import ( + "code.google.com/p/velour/irc" "errors" "fmt" "labix.org/v2/mgo/bson" @@ -10,7 +11,6 @@ import ( "strings" "time" ) -import irc "github.com/fluffle/goirc/client" // Interface used for compatibility with the Plugin interface type Handler interface { @@ -21,35 +21,6 @@ type Handler interface { RegisterWeb() *string } -// Checks to see if our user exists and if any changes have occured to it -// This uses a linear scan for now, oh well. -func (b *Bot) checkuser(nick string) *User { - var user *User = nil - for _, usr := range b.Users { - if usr.Name == nick { - user = &usr - break - } - } - if user == nil { - isadmin := false - for _, u := range b.Config.Admins { - if nick == u { - isadmin = true - } - } - user = &User{ - Name: nick, - Alts: make([]string, 1), - MessageLog: make([]string, 50), - Admin: isadmin, - } - b.Users = append(b.Users, *user) - } - - return user -} - // Checks to see if the user is asking for help, returns true if so and handles the situation. func (b *Bot) checkHelp(channel string, parts []string) { if len(parts) == 1 { @@ -82,7 +53,7 @@ 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.Conn.Me.Nick) + botnick := strings.ToLower(b.Config.Nick) iscmd := false lowerMessage := strings.ToLower(message) @@ -112,39 +83,43 @@ func (b *Bot) isCmd(message string) (bool, string) { } // Builds our internal message type out of a Conn & Line from irc -func (b *Bot) buildMessage(conn *irc.Conn, line *irc.Line) Message { +func (b *Bot) buildMessage(conn *irc.Client, inMsg irc.Msg) Message { // Check for the user - user := b.checkuser(line.Nick) + user := b.GetUser(inMsg.Origin) - channel := line.Args[0] - if channel == conn.Me.Nick { - channel = line.Nick + channel := inMsg.Args[0] + if channel == b.Config.Nick { + channel = inMsg.Args[0] } - isaction := line.Cmd == "ACTION" - + isAction := false var message string - if len(line.Args) > 1 { - message = line.Args[1] + 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 { + if !isAction { iscmd, filteredMessage = b.isCmd(message) } - user.MessageLog = append(user.MessageLog, message) - msg := Message{ User: user, Channel: channel, Body: filteredMessage, Raw: message, Command: iscmd, - Action: isaction, + Action: isAction, Time: time.Now(), - Host: line.Host, + Host: inMsg.Host, } return msg @@ -175,7 +150,8 @@ func (b *Bot) Filter(message Message, input string) string { } if strings.Contains(input, "$someone") { - someone := b.Users[rand.Intn(len(b.Users))].Name + nicks := b.Who(message.Channel) + someone := nicks[rand.Intn(len(nicks))].Name input = strings.Replace(input, "$someone", someone, -1) } @@ -232,16 +208,6 @@ func (b *Bot) Help(channel string, parts []string) { b.SendMessage(channel, msg) } -func (b *Bot) ActionRecieved(conn *irc.Conn, line *irc.Line) { - msg := b.buildMessage(conn, line) - for _, name := range b.PluginOrdering { - p := b.Plugins[name] - if p.Event(line.Cmd, msg) { - break - } - } -} - // Send our own musings to the plugins func (b *Bot) selfSaid(channel, message string) { msg := Message{ diff --git a/bot/users.go b/bot/users.go new file mode 100644 index 0000000..8a868b7 --- /dev/null +++ b/bot/users.go @@ -0,0 +1,110 @@ +package bot + +import ( + // "labix.org/v2/mgo" + "labix.org/v2/mgo/bson" + "log" +) + +// User type stores user history. This is a vehicle that will follow the user for the active +// session +type User struct { + // Current nickname known + Name string + + // LastSeen DateTime + + // Alternative nicknames seen + Alts []string + Parent string + + Admin bool + + bot *Bot +} + +func NewUser(nick string) *User { + return &User{ + Name: nick, + Admin: false, + } +} + +func (b *Bot) GetUser(nick string) *User { + coll := b.Db.C("users") + query := coll.Find(bson.M{"nick": nick}) + var user *User + + if count, err := query.Count(); err != nil { + log.Printf("Error fetching user, %s: %s\n", nick, err) + user = NewUser(nick) + coll.Insert(NewUser(nick)) + } else if count == 1 { + query.One(user) + } else if count == 0 { + // create the user + user = NewUser(nick) + coll.Insert(NewUser(nick)) + } else { + log.Printf("Error: %s appears to have more than one user?\n", nick) + query.One(user) + } + + // grab linked user, if any + if user.Parent != "" { + query := coll.Find(bson.M{"Name": user.Parent}) + if count, err := query.Count(); err != nil && count == 1 { + query.One(user) + } else { + log.Printf("Error: bad linkage on %s -> %s.\n", + user.Name, + user.Parent) + } + } + + user.bot = b + + found := false + for _, u := range b.Users { + if u.Name == user.Name { + found = true + } + } + if !found { + b.Users = append(b.Users, *user) + } + + return user +} + +// Modify user entry to be a link to other, return other +func (u *User) LinkUser(other string) *User { + coll := u.bot.Db.C("users") + user := u.bot.GetUser(u.Name) + otherUser := u.bot.GetUser(other) + + otherUser.Alts = append(otherUser.Alts, user.Alts...) + user.Alts = []string{} + user.Parent = other + + err := coll.Update(bson.M{"Name": u.Name}, u) + if err != nil { + log.Printf("Error updating user: %s\n", u.Name) + } + + err = coll.Update(bson.M{"Name": other}, otherUser) + if err != nil { + log.Printf("Error updating other user: %s\n", other) + } + + return otherUser +} + +func (b *Bot) checkAdmin(nick string) bool { + for _, u := range b.Config.Admins { + if nick == u { + return true + } + } + return false +} diff --git a/config.json b/config.json index 612613d..61477d6 100644 --- a/config.json +++ b/config.json @@ -5,8 +5,9 @@ "MainChannel": "#AlePaleTest", "Plugins": [], "Server": "127.0.0.1:6666", - "Nick": "alepaletest", + "Nick": "AlePaleTest", "Pass": "AlePaleTest:test", + "FullName": "Ale Pale", "CommandChar": "!", "QuoteChance": 0.99, "QuoteTime": 1, diff --git a/config/config.go b/config/config.go index 6d3abb9..477795d 100644 --- a/config/config.go +++ b/config/config.go @@ -13,6 +13,7 @@ type Config struct { MainChannel string Plugins []string Nick, Server, Pass string + FullName string Version string CommandChar string QuoteChance float64 diff --git a/main.go b/main.go index 43ff899..440c80d 100644 --- a/main.go +++ b/main.go @@ -1,98 +1,178 @@ package main import ( + "code.google.com/p/velour/irc" "flag" - "fmt" "github.com/chrissexton/alepale/bot" "github.com/chrissexton/alepale/config" "github.com/chrissexton/alepale/plugins" + "io" + "log" + "os" + "time" +) + +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 ) -import irc "github.com/fluffle/goirc/client" func main() { - - // These belong in the JSON file - // var server = flag.String("server", "irc.freenode.net", "Server to connect to.") - // var nick = flag.String("nick", "CrappyBot", "Nick to set upon connection.") - // var pass = flag.String("pass", "", "IRC server password to use") - // var channel = flag.String("channel", "#AlePaleTest", "Channel to connet to.") - - var cfile = flag.String("config", "config.json", "Config file to load. (Defaults to config.json)") + 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) + Config = config.Readconfig(Version, *cfile) - c := irc.SimpleClient(config.Nick) - // Optionally, enable SSL - c.SSL = false + Client, err = irc.DialServer(Config.Server, + Config.Nick, + Config.FullName, + Config.Pass) + + 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() - // Add handlers to do things here! - // e.g. join a channel on connect. - c.AddHandler("connected", - func(conn *irc.Conn, line *irc.Line) { - for _, channel := range config.Channels { - conn.Join(channel) - fmt.Printf("Now talking in %s.\n", channel) - } - }) // And a signal on disconnect quit := make(chan bool) - c.AddHandler("disconnected", - func(conn *irc.Conn, line *irc.Line) { quit <- true }) - - b := bot.NewBot(config, c) - // 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)) - - c.AddHandler("NICK", func(conn *irc.Conn, line *irc.Line) { - b.ActionRecieved(conn, line) - }) - - c.AddHandler("NAMES", func(conn *irc.Conn, line *irc.Line) { - b.ActionRecieved(conn, line) - }) - - c.AddHandler("MODE", func(conn *irc.Conn, line *irc.Line) { - b.ActionRecieved(conn, line) - }) - - c.AddHandler("PART", func(conn *irc.Conn, line *irc.Line) { - b.ActionRecieved(conn, line) - }) - - c.AddHandler("QUIT", func(conn *irc.Conn, line *irc.Line) { - b.ActionRecieved(conn, line) - }) - - c.AddHandler("JOIN", func(conn *irc.Conn, line *irc.Line) { - b.ActionRecieved(conn, line) - }) - - c.AddHandler("ACTION", func(conn *irc.Conn, line *irc.Line) { - b.MsgRecieved(conn, line) - }) - - c.AddHandler("PRIVMSG", func(conn *irc.Conn, line *irc.Line) { - b.MsgRecieved(conn, line) - }) - - // Tell client to connect - if err := c.Connect(config.Server, config.Pass); err != nil { - fmt.Printf("Connection error: %s\n", err) - } - // 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) + + default: + cmd := irc.CmdNames[msg.Cmd] + log.Println("(" + cmd + ") " + msg.Raw) + } +}