kunt

golang IRC bot
git clone git://git.2f30.org/kunt.git
Log | Files | Refs | LICENSE

commit d8835f83c84876fef279f8d7055e3f1ae8e93e37
parent ffeb3cf38c94e2f3c362b3b2343ac60cfede89fa
Author: sin <sin@2f30.org>
Date:   Mon Apr 15 17:07:25 +0100

Add irc package

Diffstat:
build | 1+
src/irc/events.go | 19+++++++++++++++++++
src/irc/irc.go | 174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/irc/lexer.go | 210+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/irc/message.go | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/irc/utils.go | 17+++++++++++++++++
6 files changed, 535 insertions(+), 0 deletions(-)
diff --git a/build b/build @@ -2,4 +2,5 @@ export GOPATH=$PWD go install fsdb +go install irc go build -o src/kunt/kunt -v kunt diff --git a/src/irc/events.go b/src/irc/events.go @@ -0,0 +1,19 @@ +package irc + +const ( + MAX_EVENTS = 16 +) + +type IrcEvent struct { + Cmd string + Fn func(IrcMessage) +} + +func (i *IrcContext) AddEventHandler(ev IrcEvent) int { + if i.evnum+1 < len(i.ev) { + i.ev[i.evnum] = ev + i.evnum++ + return 0 + } + return -1 +} diff --git a/src/irc/irc.go b/src/irc/irc.go @@ -0,0 +1,174 @@ +package irc + +import ( + "crypto/tls" + "fmt" + "net" + "strings" +) + +type IrcContext struct { + conn net.Conn // Actual connection + outgoingMsg chan IrcMessage // TX messaging channel + incomingMsg chan IrcMessage // RX messaging channel + ev [MAX_EVENTS]IrcEvent // array of callbacks + evnum int // number of callbacks + network ircNetwork // irc network state +} + +type IrcConfig struct { + Network string // network name + Nick string // nickname + User string // username + Pass string // password + Serv string // server address + Port string // server port + Tls bool // enable/disable ssl + Ipv6 bool // enable/disable ipv6 (not implemented) +} + +type ircChan struct { + name string // name of irc channel + joined bool // whether we have joined this channel or not +} + +type ircNetwork struct { + network string // network name + nick string // nickname + user string // username + pass string // password + serv string // server address + port string // server port + tls bool // enable/disable ssl + ipv6 bool // enable/disable ipv6 (not implemented) + channels [32]ircChan // a maximum of 32 channels for now + channum int // number of channels +} + +// Create a new IrcContext +func NewIrcContext(ircConfig IrcConfig) *IrcContext { + network := ircNetwork{ + network: ircConfig.Network, + nick: ircConfig.Nick, + user: ircConfig.User, + pass: ircConfig.Pass, + serv: ircConfig.Serv, + port: ircConfig.Port, + tls: ircConfig.Tls, + ipv6: ircConfig.Ipv6, + } + return &IrcContext{ + outgoingMsg: make(chan IrcMessage), + incomingMsg: make(chan IrcMessage), + network: network, + } +} + +// Add a channel to the set of channels +func (i *IrcContext) AddChannel(s string) int { + for c := 0; c < i.network.channum; c++ { + if i.network.channels[c].name == s { + return -1 + } + } + if i.network.channum+1 < len(i.network.channels) { + i.network.channels[i.network.channum] = ircChan{s, false} + i.network.channum++ + return 0 + } + return -1 +} + +// Join all channels +func (i *IrcContext) JoinChannels() { + for c := 0; c < i.network.channum; c++ { + if !i.network.channels[c].joined { + i.SendJoin(i.network.channels[c].name) + i.network.channels[c].joined = true + } + } +} + +// Connect to the server. Do not join any channels by default. +func (i *IrcContext) Connect() error { + service := i.network.serv + ":" + i.network.port + + if i.network.tls { + conf, err := loadCerts() + if err != nil { + return err + } + conn, err := tls.Dial("tcp", service, conf) + if err != nil { + return err + } + i.conn = conn + } else { + conn, err := net.Dial("tcp", service) + if err != nil { + return err + } + i.conn = conn + } + + // Fire off the goroutines now! + go i.incomingMsgLoop() + go i.outgoingMsgLoop() + go i.rxLoop() + + i.SendNick() + i.SendUser() + return nil +} + +func (i *IrcContext) read(buf []byte) (int, error) { + n, err := i.conn.Read(buf) + if err != nil { + return n, err + } + return n, nil +} + +// This is the actual raw read loop. Here we parse the incoming bytes +// and form messages. +func (i *IrcContext) rxLoop() error { + var msg IrcMessage + for { + var buf [512]byte + + n, err := i.read(buf[0:]) + if err != nil { + return err + } + fmt.Println(string(buf[0 : n-1])) + s := string(buf[0:n]) + + // Handle this here, no need to create + // a message for it + if strings.HasPrefix(s, "PING :") { + r := strings.Replace(s, "PING :", "PONG :", 6) + r = r + "\r\n" + err := i.TxRawMessage([]byte(r)) + if err != nil { + return err + } + continue + } + + // At the moment - handle only PRIVMSG + if strings.Index(s, "PRIVMSG") != -1 { + sf := strings.FieldsFunc(s, func(r rune) bool { + switch r { + case ' ', '\t', '\n': + return true + } + return false + }) + msg.Prefix = sf[0] + msg.Cmd = sf[1] + msg.Params = []string{sf[2], sf[3]} + i.incomingMsg <- msg + } + } + return nil +} diff --git a/src/irc/lexer.go b/src/irc/lexer.go @@ -0,0 +1,210 @@ +package irc + +import ( + "fmt" + "strings" + "unicode/utf8" +) + +const eof = -1 + +type item struct { + typ itemType + val string +} + +type itemType int + +const ( + itemError itemType = iota + + itemEOF + itemMessage + itemStartColon + itemColon + itemPrefix + itemCommand + itemParams + itemMiddle + itemTrailing +) + +func (i item) String() string { + switch i.typ { + case itemEOF: + return "EOF" + case itemError: + return i.val + } + return fmt.Sprintf("%q", i.val) +} + +type stateFn func(*lexer) stateFn + +type lexer struct { + name string + input string + start int + pos int + width int + items chan item +} + +func lex(name, input string) (*lexer, chan item) { + l := &lexer{ + name: name, + input: input, + items: make(chan item), + } + go l.run() + return l, l.items +} + +func (l *lexer) emit(t itemType) { + l.items <- item{t, l.input[l.start:l.pos]} + l.start = l.pos +} + +func (l *lexer) run() { + for state := lexMessage; state != nil; { + state = state(l) + } + close(l.items) +} + +func (l *lexer) next() (r rune) { + if l.pos >= len(l.input) { + l.width = 0 + return eof + } + r, l.width = + utf8.DecodeRuneInString(l.input[l.pos:]) + l.pos += l.width + return r +} + +func (l *lexer) ignore() { + l.start = l.pos +} + +func (l *lexer) backup() { + l.pos -= l.width +} + +func (l *lexer) peek() rune { + r := l.next() + l.backup() + return r +} + +func (l *lexer) eat() { + l.next() + l.ignore() +} + +const colon = ":" + +func (l *lexer) errorf(format string, args ...interface{}) stateFn { + l.items <- item{ + itemError, + fmt.Sprintf(format, args...), + } + return nil +} + +func lexMessage(l *lexer) stateFn { + for { + if strings.HasPrefix(l.input[l.pos:], colon) { + return lexStartColon + } + if l.next() == eof { + break + } + } + l.emit(itemEOF) + return nil +} + +func lexStartColon(l *lexer) stateFn { + l.pos += len(colon) + l.emit(itemStartColon) + return lexPrefix +} + +func lexPrefix(l *lexer) stateFn { + for { + if l.next() != ' ' { + continue + } + l.ignore() + return lexCommand + } + return nil +} + +func lexCommand(l *lexer) stateFn { + for { + if l.next() != ' ' { + continue + } + l.backup() + l.emit(itemCommand) + return lexParams + } + return nil +} + +func lexParams(l *lexer) stateFn { + r := l.next() + if r == ' ' { + l.ignore() + } + if l.peek() != ':' { + return lexMiddle + } else { + l.next() + l.emit(itemColon) + return lexTrailing + } + l.emit(itemParams) + return nil +} + +func lexMiddle(l *lexer) stateFn { + for { + r := l.next() + if r == eof { + break + } + if r == ' ' { + l.backup() + l.emit(itemMiddle) + return lexParams + } + if r == '\r' { + if l.peek() == '\n' { + l.backup() + l.emit(itemMiddle) + return nil + } + } + } + return nil +} + +func lexTrailing(l *lexer) stateFn { + for { + r := l.next() + if r == eof { + break + } + if r == '\r' { + if l.peek() == '\n' { + l.backup() + l.emit(itemTrailing) + return nil + } + } + } + return nil +} diff --git a/src/irc/message.go b/src/irc/message.go @@ -0,0 +1,114 @@ +package irc + +type IrcMessage struct { + Prefix string // prefix part of the message + Cmd string // the actual command + Params []string // the tokenized parameters +} + +// Send the nickname +func (i *IrcContext) SendNick() { + msg := IrcMessage{ + Prefix: "", + Cmd: "NICK", + Params: []string{i.network.nick}, + } + i.outgoingMsg <- msg +} + +// Send the username +func (i *IrcContext) SendUser() { + msg := IrcMessage{ + Prefix: "", + Cmd: "USER", + Params: []string{ + i.network.user, + "* 8", + ":" + i.network.nick, + }, + } + i.outgoingMsg <- msg +} + +// Join a channel +func (i *IrcContext) SendJoin(channel string) { + msg := IrcMessage{ + Prefix: "", + Cmd: "JOIN", + Params: []string{channel}, + } + i.outgoingMsg <- msg +} + +// Send a PRIVMSG +func (i *IrcContext) SendPrivmsg(channel string, text string) { + msg := IrcMessage{ + Prefix: "", + Cmd: "PRIVMSG", + Params: []string{ + channel, + ":" + text, + }, + } + i.outgoingMsg <- msg +} + +// Unpack a message into a byte array +func (i *IrcContext) UnpackMessage(msg IrcMessage) ([]byte, error) { + // No Prefix crap for TX paths + rawMsg := msg.Cmd + " " + for _, v := range msg.Params { + rawMsg += v + " " + } + rawMsg = rawMsg[:len(rawMsg)-1] + "\r\n" + return []byte(rawMsg), nil +} + +// Transmit a raw message +func (i *IrcContext) TxRawMessage(msg []byte) error { + _, err := i.conn.Write(msg[0:]) + if err != nil { + return err + } + return nil +} + +func (i *IrcContext) incomingMsgLoop() error { + for { + select { + case msg, ok := <-i.incomingMsg: + if !ok { + return nil + } + // Check if the user has registered + // a callback for this message + for _, v := range i.ev { + if v.Cmd == msg.Cmd { + v.Fn(msg) + break + } + } + } + } + return nil +} + +func (i *IrcContext) outgoingMsgLoop() error { + for { + select { + case msg, ok := <-i.outgoingMsg: + if !ok { + return nil + } + rawMsg, err := i.UnpackMessage(msg) + if err != nil { + return err + } + err = i.TxRawMessage(rawMsg) + if err != nil { + return err + } + } + } + return nil +} diff --git a/src/irc/utils.go b/src/irc/utils.go @@ -0,0 +1,17 @@ +package irc + +import ( + "crypto/tls" +) + +func loadCerts() (*tls.Config, error) { + cert, err := tls.LoadX509KeyPair("certs/client.pem", + "certs/client.key") + if err != nil { + return nil, err + } + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + InsecureSkipVerify: true, + }, nil +}