initial go IRC library, dirty hax abound :-)

This commit is contained in:
Alex 2009-11-29 20:23:15 +00:00
commit 545a88fea0
7 changed files with 418 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/gobot
*.[568]
_obj/
*.swp
*~

12
Makefile Normal file
View File

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

35
client.go Normal file
View File

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

13
irc/Makefile Normal file
View File

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

102
irc/commands.go Normal file
View File

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

163
irc/connection.go Normal file
View File

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

88
irc/handlers.go Normal file
View File

@ -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<len(e); i++ {
ne[i] = e[i];
}
e = ne;
}
e = e[0:len(e)+1];
e[len(e)-1] = f;
} else {
e := make([]func (*IRCConn, *IRCLine), 1, 10);
e[0] = f;
conn.events[n] = e;
}
}
// loops through all event handlers for line.Cmd, running each in a goroutine
func (conn *IRCConn) dispatchEvent(line *IRCLine) {
// So, I think CTCP and (in particular) CTCP ACTION are better handled as
// separate events as opposed to forcing people to have gargantuan PRIVMSG
// handlers to cope with the possibilities.
if line.Cmd == "PRIVMSG" && len(line.Text) > 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<len(line.Args); i++ {
a[i+1] = line.Args[i];
}
line.Args = a;
}
if len(t) > 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 + "_");
});
}