Big overhaul again! Replaced fluffle's goirc library with velour/irc.

Hopefully this new library will provide me with some benefits such as being able
to actually get and respond to a WHO message. Yet to do is to fix sendMessage to
be a channel on Bot with a goroutine that formats and sends messages on. Also,
figuring out how to handle the WHO response and populate users.
This commit is contained in:
Chris Sexton 2013-06-01 21:59:55 -04:00
parent 8cf2b997a2
commit 2c0dc55452
6 changed files with 342 additions and 166 deletions

View File

@ -1,8 +1,8 @@
package bot package bot
import ( import (
"code.google.com/p/velour/irc"
"github.com/chrissexton/alepale/config" "github.com/chrissexton/alepale/config"
irc "github.com/fluffle/goirc/client"
"html/template" "html/template"
"labix.org/v2/mgo" "labix.org/v2/mgo"
"log" "log"
@ -11,6 +11,8 @@ import (
"time" "time"
) )
const actionPrefix = "\x01ACTION"
// Bot type provides storage for bot-wide information, configs, and database connections // Bot type provides storage for bot-wide information, configs, and database connections
type Bot struct { type Bot struct {
// Each plugin must be registered in our plugins handler. To come: a map so that this // 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 Me User
// Conn allows us to send messages and modify our connection state // Conn allows us to send messages and modify our connection state
Conn *irc.Conn Client *irc.Client
Config *config.Config 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 { type Message struct {
User *User User *User
Channel, Body string Channel, Body string
@ -110,7 +95,7 @@ type Variable struct {
} }
// NewBot creates a Bot for a given connection and set of handlers. // 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) session, err := mgo.Dial(config.DbServer)
if err != nil { if err != nil {
panic(err) panic(err)
@ -123,12 +108,9 @@ func NewBot(config *config.Config, c *irc.Conn) *Bot {
RunNewLogger(logIn, logOut) RunNewLogger(logIn, logOut)
config.Nick = c.Me.Nick
users := []User{ users := []User{
User{ User{
Name: config.Nick, Name: config.Nick,
MessageLog: make([]string, 0),
}, },
} }
@ -138,7 +120,7 @@ func NewBot(config *config.Config, c *irc.Conn) *Bot {
PluginOrdering: make([]string, 0), PluginOrdering: make([]string, 0),
Users: users, Users: users,
Me: users[0], Me: users[0],
Conn: c, Client: c,
DbSession: session, DbSession: session,
Db: db, Db: db,
varColl: db.C("variables"), 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) { 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) b.selfSaid(channel, message)
} }
// Sends action to channel // Sends action to channel
func (b *Bot) SendAction(channel, message string) { 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 // Notify plugins that we've said something
b.selfSaid(channel, message) b.selfSaid(channel, message)
} }
// Handles incomming PRIVMSG requests // Handles incomming PRIVMSG requests
func (b *Bot) MsgRecieved(conn *irc.Conn, line *irc.Line) { func (b *Bot) MsgRecieved(client *irc.Client, inMsg irc.Msg) {
msg := b.buildMessage(conn, line) if inMsg.User == "" {
return
}
msg := b.buildMessage(client, inMsg)
if strings.HasPrefix(msg.Body, "help") && msg.Command { if strings.HasPrefix(msg.Body, "help") && msg.Command {
parts := strings.Fields(strings.ToLower(msg.Body)) parts := strings.Fields(strings.ToLower(msg.Body))
@ -204,6 +205,23 @@ RET:
return 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 = ` var rootIndex string = `
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>

View File

@ -1,6 +1,7 @@
package bot package bot
import ( import (
"code.google.com/p/velour/irc"
"errors" "errors"
"fmt" "fmt"
"labix.org/v2/mgo/bson" "labix.org/v2/mgo/bson"
@ -10,7 +11,6 @@ import (
"strings" "strings"
"time" "time"
) )
import irc "github.com/fluffle/goirc/client"
// Interface used for compatibility with the Plugin interface // Interface used for compatibility with the Plugin interface
type Handler interface { type Handler interface {
@ -21,35 +21,6 @@ type Handler interface {
RegisterWeb() *string 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. // 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) { func (b *Bot) checkHelp(channel string, parts []string) {
if len(parts) == 1 { 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 // Checks if message is a command and returns its curtailed version
func (b *Bot) isCmd(message string) (bool, string) { func (b *Bot) isCmd(message string) (bool, string) {
cmdc := b.Config.CommandChar cmdc := b.Config.CommandChar
botnick := strings.ToLower(b.Conn.Me.Nick) botnick := strings.ToLower(b.Config.Nick)
iscmd := false iscmd := false
lowerMessage := strings.ToLower(message) 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 // 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 // Check for the user
user := b.checkuser(line.Nick) user := b.GetUser(inMsg.Origin)
channel := line.Args[0] channel := inMsg.Args[0]
if channel == conn.Me.Nick { if channel == b.Config.Nick {
channel = line.Nick channel = inMsg.Args[0]
} }
isaction := line.Cmd == "ACTION" isAction := false
var message string var message string
if len(line.Args) > 1 { if len(inMsg.Args) > 1 {
message = line.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 iscmd := false
filteredMessage := message filteredMessage := message
if !isaction { if !isAction {
iscmd, filteredMessage = b.isCmd(message) iscmd, filteredMessage = b.isCmd(message)
} }
user.MessageLog = append(user.MessageLog, message)
msg := Message{ msg := Message{
User: user, User: user,
Channel: channel, Channel: channel,
Body: filteredMessage, Body: filteredMessage,
Raw: message, Raw: message,
Command: iscmd, Command: iscmd,
Action: isaction, Action: isAction,
Time: time.Now(), Time: time.Now(),
Host: line.Host, Host: inMsg.Host,
} }
return msg return msg
@ -175,7 +150,8 @@ func (b *Bot) Filter(message Message, input string) string {
} }
if strings.Contains(input, "$someone") { 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) input = strings.Replace(input, "$someone", someone, -1)
} }
@ -232,16 +208,6 @@ func (b *Bot) Help(channel string, parts []string) {
b.SendMessage(channel, msg) 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 // Send our own musings to the plugins
func (b *Bot) selfSaid(channel, message string) { func (b *Bot) selfSaid(channel, message string) {
msg := Message{ msg := Message{

110
bot/users.go Normal file
View File

@ -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
}

View File

@ -5,8 +5,9 @@
"MainChannel": "#AlePaleTest", "MainChannel": "#AlePaleTest",
"Plugins": [], "Plugins": [],
"Server": "127.0.0.1:6666", "Server": "127.0.0.1:6666",
"Nick": "alepaletest", "Nick": "AlePaleTest",
"Pass": "AlePaleTest:test", "Pass": "AlePaleTest:test",
"FullName": "Ale Pale",
"CommandChar": "!", "CommandChar": "!",
"QuoteChance": 0.99, "QuoteChance": 0.99,
"QuoteTime": 1, "QuoteTime": 1,

View File

@ -13,6 +13,7 @@ type Config struct {
MainChannel string MainChannel string
Plugins []string Plugins []string
Nick, Server, Pass string Nick, Server, Pass string
FullName string
Version string Version string
CommandChar string CommandChar string
QuoteChance float64 QuoteChance float64

236
main.go
View File

@ -1,98 +1,178 @@
package main package main
import ( import (
"code.google.com/p/velour/irc"
"flag" "flag"
"fmt"
"github.com/chrissexton/alepale/bot" "github.com/chrissexton/alepale/bot"
"github.com/chrissexton/alepale/config" "github.com/chrissexton/alepale/config"
"github.com/chrissexton/alepale/plugins" "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() { func main() {
var err error
// These belong in the JSON file var cfile = flag.String("config", "config.json",
// var server = flag.String("server", "irc.freenode.net", "Server to connect to.") "Config file to load. (Defaults to config.json)")
// 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)")
flag.Parse() // parses the logging flags. flag.Parse() // parses the logging flags.
config := config.Readconfig(Version, *cfile) Config = config.Readconfig(Version, *cfile)
c := irc.SimpleClient(config.Nick) Client, err = irc.DialServer(Config.Server,
// Optionally, enable SSL Config.Nick,
c.SSL = false 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 // And a signal on disconnect
quit := make(chan bool) 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 // Wait for disconnect
<-quit <-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)
}
}