// © 2016 the CatBase Authors under the WTFPL license. See AUTHORS for the list of authors. package irc import ( "fmt" "io" "log" "os" "strings" "time" "github.com/velour/catbase/bot" "github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/user" "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 eventReceived func(msg.Message) messageReceived func(msg.Message) replyMessageReceived func(msg.Message, string) } func New(c *config.Config) *Irc { i := Irc{} i.config = c return &i } func (i *Irc) RegisterEventReceived(f func(msg.Message)) { i.eventReceived = f } func (i *Irc) RegisterMessageReceived(f func(msg.Message)) { i.messageReceived = f } func (i *Irc) RegisterReplyMessageReceived(f func(msg.Message, string)) { i.replyMessageReceived = f } func (i *Irc) Send(kind bot.Kind, args ...interface{}) (error, string) { switch kind { case bot.Reply: case bot.Message: return i.sendMessage(args[0].(string), args[1].(string)) case bot.Action: return i.sendAction(args[0].(string), args[1].(string)) default: } return nil, "" } 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) (error, 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.GetInt("RatePerSec", 5) throttle = time.Tick(time.Second / time.Duration(ratePerSec)) } <-throttle i.Client.Out <- m } return nil, "NO_IRC_IDENTIFIERS" } // Sends action to channel func (i *Irc) sendAction(channel, message string) (error, string) { message = actionPrefix + " " + message + "\x01" return i.sendMessage(channel, message) } func (i *Irc) GetEmojiList() map[string]string { //we're not going to do anything because it's IRC return make(map[string]string) } func (i *Irc) Serve() error { if i.eventReceived == nil || i.messageReceived == nil { return fmt.Errorf("Missing an event handler") } var err error i.Client, err = irc.DialSSL( i.config.Get("Irc.Server", "localhost"), i.config.Get("Nick", "bot"), i.config.Get("FullName", "bot"), i.config.Get("Irc.Pass", ""), true, ) if err != nil { return fmt.Errorf("%s", err) } for _, c := range i.config.GetArray("channels", []string{}) { i.JoinChannel(c) } i.quit = make(chan bool) go i.handleConnection() <-i.quit return nil } 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.eventReceived(botMsg) case irc.ERR_NOSUCHCHANNEL: i.eventReceived(botMsg) case irc.RPL_MOTD: i.eventReceived(botMsg) case irc.RPL_NAMREPLY: i.eventReceived(botMsg) case irc.RPL_TOPIC: i.eventReceived(botMsg) case irc.KICK: i.eventReceived(botMsg) case irc.TOPIC: i.eventReceived(botMsg) case irc.MODE: i.eventReceived(botMsg) case irc.JOIN: i.eventReceived(botMsg) case irc.PART: i.eventReceived(botMsg) case irc.QUIT: os.Exit(1) case irc.NOTICE: i.eventReceived(botMsg) case irc.PRIVMSG: i.messageReceived(botMsg) case irc.NICK: i.eventReceived(botMsg) case irc.RPL_WHOREPLY: i.eventReceived(botMsg) case irc.RPL_ENDOFWHO: i.eventReceived(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) msg.Message { // Check for the user u := user.User{ Name: inMsg.Origin, } channel := inMsg.Args[0] if channel == i.config.Get("Nick", "bot") { 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 = bot.IsCmd(i.config, message) } msg := msg.Message{ User: &u, Channel: channel, Body: filteredMessage, Raw: message, Command: iscmd, Action: isAction, Time: time.Now(), Host: inMsg.Host, } return msg } func (i Irc) Who(channel string) []string { return []string{} }