From 545a88fea0292315141bd07014d7ff101545211e Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 29 Nov 2009 20:23:15 +0000 Subject: [PATCH] initial go IRC library, dirty hax abound :-) --- .gitignore | 5 ++ Makefile | 12 ++++ client.go | 35 ++++++++++ irc/Makefile | 13 ++++ irc/commands.go | 102 +++++++++++++++++++++++++++++ irc/connection.go | 163 ++++++++++++++++++++++++++++++++++++++++++++++ irc/handlers.go | 88 +++++++++++++++++++++++++ 7 files changed, 418 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 client.go create mode 100644 irc/Makefile create mode 100644 irc/commands.go create mode 100644 irc/connection.go create mode 100644 irc/handlers.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f75fbc3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/gobot +*.[568] +_obj/ +*.swp +*~ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..97bfc7a --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +# Copyright 2009 The Go Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +include $(GOROOT)/src/Make.$(GOARCH) + +TARG=gobot +GOFILES=\ + client.go\ + +include $(GOROOT)/src/Make.cmd + diff --git a/client.go b/client.go new file mode 100644 index 0000000..ad5865d --- /dev/null +++ b/client.go @@ -0,0 +1,35 @@ +package main + +import ( + "./irc/_obj/irc"; + "fmt"; + "os"; +) + +func main() { + c := irc.New("GoTest", "gotest", "GoBot"); + c.AddHandler("connected", + func(conn *irc.IRCConn, line *irc.IRCLine) { + conn.Join("#"); + } + ); + c.AddHandler("join", + func(conn *irc.IRCConn, line *irc.IRCLine) { + if line.Nick == conn.Me { + conn.Privmsg(line.Text, "I LIVE, BITCHES"); + } + } + ); + if err := c.Connect("irc.pl0rt.org", ""); err != nil { + fmt.Printf("Connection error: %v\n", err); + return; + } + + // if we get here, we're successfully connected and should have just + // dispatched the "CONNECTED" event to it's handlers \o/ + control := make(chan os.Error, 1); + go c.RunLoop(control); + if err := <-control; err != nil { + fmt.Printf("IRCConn.RunLoop terminated: %v\n", err); + } +} diff --git a/irc/Makefile b/irc/Makefile new file mode 100644 index 0000000..07ef058 --- /dev/null +++ b/irc/Makefile @@ -0,0 +1,13 @@ +# Copyright 2009 The Go Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +include $(GOROOT)/src/Make.$(GOARCH) + +TARG=irc +GOFILES=\ + connection.go\ + commands.go\ + handlers.go\ + +include $(GOROOT)/src/Make.pkg diff --git a/irc/commands.go b/irc/commands.go new file mode 100644 index 0000000..b5d21e5 --- /dev/null +++ b/irc/commands.go @@ -0,0 +1,102 @@ +package irc + +// this file contains the various commands you can +// send to the server using an IRCConn connection + +import ( + "fmt"; + "reflect"; +) + +// This could be a lot less ugly with the ability to manipulate +// the symbol table and add methods/functions on the fly +// [ CMD, FMT, FMTARGS ] etc. + +// send a PASS command to the server +func (conn *IRCConn) Pass(p string) { + conn.send(fmt.Sprintf("PASS %s", p)); +} + +// send a NICK command to the server +func (conn *IRCConn) Nick(n string) { + conn.send(fmt.Sprintf("NICK %s", n)); +} + +// send a USER command to the server +func (conn *IRCConn) User(u, n string) { + conn.send(fmt.Sprintf("USER %s 12 * :%s", u, n)); +} + +// send a JOIN command to the server +func (conn *IRCConn) Join(c string) { + conn.send(fmt.Sprintf("JOIN %s", c)); +} + +// send a PART command to the server +func (conn *IRCConn) Part(c string, a ...) { + msg := getStringMsg(a); + if msg != "" { + msg = " :" + msg + } + conn.send(fmt.Sprintf("PART %s%s", c, msg)); +} + +// send a QUIT command to the server +func (conn *IRCConn) Quit(a ...) { + msg := getStringMsg(a); + if msg == "" { + msg = "GoBye!" + } + conn.send(fmt.Sprintf("QUIT :%s", msg)); +} + +// send a PRIVMSG to the target t +func (conn *IRCConn) Privmsg(t, msg string) { + conn.send(fmt.Sprintf("PRIVMSG %s :%s", t, msg)); +} + +// send a NOTICE to the target t +func (conn *IRCConn) Notice(t, msg string) { + conn.send(fmt.Sprintf("NOTICE %s :%s", t, msg)); +} + +// send a (generic) CTCP to the target t +func (conn *IRCConn) Ctcp(t, ctcp string, a ...) { + msg := getStringMsg(a); + if msg != "" { + msg = " " + msg + } + conn.Privmsg(t, fmt.Sprintf("\001%s%s\001", ctcp, msg)); +} + +// send a generic CTCP reply to the target t +func (conn *IRCConn) CtcpReply(t, ctcp string, a ...) { + msg := getStringMsg(a); + if msg != "" { + msg = " " + msg + } + conn.Notice(t, fmt.Sprintf("\001%s%s\001", ctcp, msg)); +} + + +// send a CTCP "VERSION" to the target t +func (conn *IRCConn) Version(t string) { + conn.Ctcp(t, "VERSION"); +} + +// send a CTCP "ACTION" to the target t -- /me does stuff! +func (conn *IRCConn) Action(t, msg string) { + conn.Ctcp(t, "ACTION", msg); +} + +func getStringMsg(a ...) (msg string) { + // dealing with functions with a variable parameter list is nasteeh :-( + // the below stolen and munged from fmt/print.go + if v := reflect.NewValue(a).(*reflect.StructValue); v.NumField() == 1 { + // XXX: should we check that this looks at least vaguely stringy first? + msg = fmt.Sprintf("%v", v.Field(1)); + } else { + msg = "" + } + return +} diff --git a/irc/connection.go b/irc/connection.go new file mode 100644 index 0000000..c3b97be --- /dev/null +++ b/irc/connection.go @@ -0,0 +1,163 @@ +// Some IRC testing code! + +package irc + +import ( + "bufio"; + "os"; + "net"; + "fmt"; + "strings"; +) + +// the IRC connection object +type IRCConn struct { + sock *bufio.ReadWriter; + Host string; + Me string; + Ident string; + Name string; + con bool; + reg bool; + events map[string] []func (*IRCConn, *IRCLine); + chans map[string] *IRCChan; + nicks map[string] *IRCNick; +} + +// We'll parse an incoming line into this struct +// raw =~ ":nick!user@host cmd args[] :text" +// src == "nick!user@host" +type IRCLine struct { + Nick string; + User string; + Host string; + Src string; + Cmd string; + Args []string; + Text string; + Raw string; +} + +// A struct representing an IRC channel +type IRCChan struct { + Name string; + Topic string; + Modes map[string] string; + Nicks map[string] *IRCNick; +} + +// A struct representing an IRC nick +type IRCNick struct { + Name string; + Chans map[string] *IRCChan; +} + +// construct a new IRC Connection object +func New(nick, user, name string) (conn *IRCConn) { + conn = &IRCConn{Me: nick, Ident: user, Name: name}; + // allocate meh some memoraaaahh + conn.nicks = make(map[string] *IRCNick); + conn.chans = make(map[string] *IRCChan); + conn.events = make(map[string] []func(*IRCConn, *IRCLine)); + conn.setupEvents(); + return conn +} + +// connect the IRC connection object to a host +func (conn *IRCConn) Connect(host, pass string) (err os.Error) { + if !hasPort(host) { + host += ":6667"; + } + sock, err := net.Dial("tcp", "", host); + if err != nil { + return err + } + conn.sock = bufio.NewReadWriter(bufio.NewReader(sock), bufio.NewWriter(sock)); + conn.con = true; + conn.Host = host; + + // initial connection set-up + // verify valid nick/user/name here? + if pass != "" { + conn.Pass(pass) + } + conn.Nick(conn.Me); + conn.User(conn.Ident, conn.Name); + + for line, err := conn.recv(); err == nil; line, err = conn.recv() { + // initial loop to get us to the point where we're connected + conn.dispatchEvent(line); + if line.Cmd == "001" { + break; + } + } + return err; +} + +func (conn *IRCConn) RunLoop(c chan os.Error) { + var err os.Error; + for line, err := conn.recv(); err == nil; line, err = conn.recv() { + conn.dispatchEvent(line); + } + c <- err; + return; +} + +// copied from http.client for great justice +func hasPort(s string) bool { + return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") +} + +// send \r\n terminated line to peer, propagate errors +func (conn *IRCConn) send(line string) (err os.Error) { + err = conn.sock.WriteString(line + "\r\n"); + conn.sock.Flush(); + fmt.Println("-> " + line); + return err +} + +// receive one \r\n terminated line from peer and parse it, propagate errors +func (conn *IRCConn) recv() (line *IRCLine, err os.Error) { + s, err := conn.sock.ReadString('\n'); + if err != nil { + return line, err + } + // chop off \r\n + s = s[0:len(s)-2]; + fmt.Println("<- " + s); + + line = &IRCLine{Raw: s}; + if s[0] == ':' { + // remove a source and parse it + if idx := strings.Index(s, " "); idx != -1 { + line.Src, s = s[1:idx], s[idx+1:len(s)]; + } else { + // pretty sure we shouldn't get here ... + line.Src = s[1:len(s)]; + return line, nil; + } + + // src can be the hostname of the irc server or a nick!user@host + line.Host = line.Src; + nidx, uidx := strings.Index(line.Src, "!"), strings.Index(line.Src, "@"); + if uidx != -1 && nidx != -1 { + line.Nick = line.Src[0:nidx]; + line.User = line.Src[nidx+1:uidx]; + line.Host = line.Src[uidx+1:len(line.Src)]; + } + } + + // now we're here, we've parsed a :nick!user@host or :server off + // s should contain "cmd args[] :text" + args := strings.Split(s, " :", 2); + if len(args) > 1 { + line.Text = args[1]; + } + args = strings.Split(args[0], " ", 0); + line.Cmd = strings.ToUpper(args[0]); + if len(args) > 1 { + line.Args = args[1:len(args)]; + } + return line, nil +} + diff --git a/irc/handlers.go b/irc/handlers.go new file mode 100644 index 0000000..700cf14 --- /dev/null +++ b/irc/handlers.go @@ -0,0 +1,88 @@ +package irc + +// this file contains the basic set of event handlers +// to manage tracking an irc connection etc. + +import ( + "fmt"; + "strings"; +) + +// Add an event handler for a specific IRC command +func (conn *IRCConn) AddHandler(name string, f func (*IRCConn, *IRCLine)) { + n := strings.ToUpper(name); + if e, ok := conn.events[n]; ok { + if len(e) == cap(e) { + // crap, we're full. expand e by another 10 handler slots + ne := make([]func (*IRCConn, *IRCLine), len(e), len(e)+10); + for i := 0; i 2 + && line.Text[0] == '\001' && line.Text[len(line.Text)-1] == '\001' { + // WOO, it's a CTCP message + t := strings.Split(line.Text[1:len(line.Text)-1], " ", 2); + if c := strings.ToUpper(t[0]); c == "ACTION" { + // make a CTCP ACTION it's own event a-la PRIVMSG + line.Cmd = c; + } else { + // otherwise, dispatch a generic CTCP event that + // contains the type of CTCP in line.Args[0] + line.Cmd = "CTCP"; + a := make([]string, len(line.Args)+1); + a[0] = c; + for i:=0; i 1 { + // for some CTCP messages this could make more sense + // in line.Args[], but meh. MEH, I say. + line.Text = t[1]; + } + } + if funcs, ok := conn.events[line.Cmd]; ok { + for _, f := range funcs { + go f(conn, line) + } + } +} + +// sets up the internal event handlers to do useful things with lines +// XXX: is there a better way of doing this? +func (conn *IRCConn) setupEvents() { + // Basic ping/pong handler + conn.AddHandler("PING", func(conn *IRCConn, line *IRCLine) { + conn.send(fmt.Sprintf("PONG :%s", line.Text)); + }); + + // Handler to trigger a "CONNECTED" event on receipt of numeric 001 + conn.AddHandler("001", func(conn *IRCConn, line *IRCLine) { + l := new(IRCLine); + l.Cmd = "CONNECTED"; + conn.dispatchEvent(l); + }); + + // Handler to deal with "433 :Nickname already in use" on connection + conn.AddHandler("433", func(conn *IRCConn, line *IRCLine) { + conn.Nick(conn.Me + "_"); + }); +} +