commit d8835f83c84876fef279f8d7055e3f1ae8e93e37
parent ffeb3cf38c94e2f3c362b3b2343ac60cfede89fa
Author: sin <sin@2f30.org>
Date: Mon, 15 Apr 2013 17:07:25 +0100
Add irc package
Diffstat:
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
+}