Move IRC stuff to its own package

This commit is contained in:
Chris Sexton 2016-03-10 13:37:07 -05:00
parent 74e981eedf
commit 51d7f7f067
7 changed files with 387 additions and 314 deletions

View File

@ -11,15 +11,10 @@ import (
"time" "time"
"github.com/velour/catbase/config" "github.com/velour/catbase/config"
"github.com/velour/velour/irc"
_ "github.com/mattn/go-sqlite3" _ "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 // 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
@ -32,11 +27,10 @@ type Bot struct {
// Represents the bot // Represents the bot
Me User Me User
// Conn allows us to send messages and modify our connection state
Client *irc.Client
Config *config.Config Config *config.Config
Conn Connector
// SQL DB // SQL DB
// TODO: I think it'd be nice to use https://github.com/jmoiron/sqlx so that // 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 // 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. // NewBot creates a Bot for a given connection and set of handlers.
func NewBot(config *config.Config, c *irc.Client) *Bot { func NewBot(config *config.Config, connector Connector) *Bot {
sqlDB, err := sql.Open("sqlite3", config.DbFile) sqlDB, err := sql.Open("sqlite3", config.DB.File)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -124,9 +118,9 @@ func NewBot(config *config.Config, c *irc.Client) *Bot {
Config: config, Config: config,
Plugins: make(map[string]Handler), Plugins: make(map[string]Handler),
PluginOrdering: make([]string, 0), PluginOrdering: make([]string, 0),
Conn: connector,
Users: users, Users: users,
Me: users[0], Me: users[0],
Client: c,
DB: sqlDB, DB: sqlDB,
logIn: logIn, logIn: logIn,
logOut: logOut, logOut: logOut,
@ -142,6 +136,9 @@ func NewBot(config *config.Config, c *irc.Client) *Bot {
} }
go http.ListenAndServe(config.HttpAddr, nil) go http.ListenAndServe(config.HttpAddr, nil)
connector.RegisterMessageRecieved(bot.MsgRecieved)
connector.RegisterEventRecieved(bot.EventRecieved)
return bot 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 { func (b *Bot) Who(channel string) []User {
out := []User{} out := []User{}
for _, u := range b.Users { for _, u := range b.Users {

View File

@ -12,18 +12,14 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/velour/velour/irc"
) )
// Handles incomming PRIVMSG requests // Handles incomming PRIVMSG requests
func (b *Bot) MsgRecieved(client *irc.Client, inMsg irc.Msg) { func (b *Bot) MsgRecieved(msg Message) {
log.Println("Recieved message: ", inMsg) log.Println("Recieved message: ", msg)
if inMsg.User == "" {
return
}
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 { if strings.HasPrefix(msg.Body, "help") && msg.Command {
parts := strings.Fields(strings.ToLower(msg.Body)) parts := strings.Fields(strings.ToLower(msg.Body))
@ -44,27 +40,23 @@ RET:
} }
// Handle incoming events // Handle incoming events
func (b *Bot) EventRecieved(conn *irc.Client, inMsg irc.Msg) { func (b *Bot) EventRecieved(msg Message) {
log.Println("Recieved event: ", inMsg) log.Println("Recieved event: ", msg)
if inMsg.User == "" { //msg := b.buildMessage(conn, inMsg)
return
}
msg := b.buildMessage(conn, inMsg)
for _, name := range b.PluginOrdering { for _, name := range b.PluginOrdering {
p := b.Plugins[name] p := b.Plugins[name]
if p.Event(inMsg.Cmd, msg) { if p.Event(msg.Body, msg) { // TODO: could get rid of msg.Body
break break
} }
} }
} }
// Interface used for compatibility with the Plugin interface func (b *Bot) SendMessage(channel, message string) {
type Handler interface { b.Conn.SendMessage(channel, message)
Message(message Message) bool }
Event(kind string, message Message) bool
BotMessage(message Message) bool func (b *Bot) SendAction(channel, message string) {
Help(channel string, parts []string) b.Conn.SendAction(channel, message)
RegisterWeb() *string
} }
// 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.
@ -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) { func (b *Bot) LastMessage(channel string) (Message, error) {
log := <-b.logOut log := <-b.logOut
if len(log) == 0 { if len(log) == 0 {

19
bot/interfaces.go Normal file
View File

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

View File

@ -9,13 +9,19 @@ import "io/ioutil"
// Config stores any system-wide startup information that cannot be easily configured via // Config stores any system-wide startup information that cannot be easily configured via
// the database // the database
type Config struct { type Config struct {
DbFile string DB struct {
DbName string File string
DbServer string Name string
Channels []string Server string
MainChannel string }
Plugins []string Channels []string
Nick, Server, Pass string MainChannel string
Plugins []string
Type string
Irc struct {
Server, Pass string
}
Nick string
FullName string FullName string
Version string Version string
CommandChar string CommandChar string
@ -58,6 +64,10 @@ func Readconfig(version, cfile string) *Config {
} }
c.Version = version c.Version = version
if c.Type == "" {
c.Type = "irc"
}
fmt.Printf("godeepintir version %s running.\n", c.Version) fmt.Printf("godeepintir version %s running.\n", c.Version)
return &c return &c

302
irc/irc.go Normal file
View File

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

187
main.go
View File

@ -4,181 +4,44 @@ package main
import ( import (
"flag" "flag"
"io"
"log" "log"
"os"
"time"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/config" "github.com/velour/catbase/config"
"github.com/velour/catbase/irc"
"github.com/velour/catbase/plugins" "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() { func main() {
var err error
var cfile = flag.String("config", "config.json", var cfile = flag.String("config", "config.json",
"Config file to load. (Defaults to 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) c := config.Readconfig(Version, *cfile)
var client bot.Connector
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)
switch c.Type {
case "irc":
client = irc.New(c)
default: default:
cmd := irc.CmdNames[msg.Cmd] log.Fatalf("Unknown connection type: %s", c.Type)
log.Println("(" + cmd + ") " + msg.Raw)
} }
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()
} }

2
slack/slack.go Normal file
View File

@ -0,0 +1,2 @@
// Package to connect to slack service
package slack