vastly updated bot framework, now with state tracking etc.

This commit is contained in:
Alex Bramley 2009-12-17 17:22:31 +00:00
parent 4d610d1718
commit 79822340b5
8 changed files with 977 additions and 175 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@
_obj/ _obj/
*.swp *.swp
*~ *~
*.out

View File

@ -4,32 +4,89 @@ import (
"./irc/_obj/irc"; "./irc/_obj/irc";
"fmt"; "fmt";
"os"; "os";
"bufio";
"strings";
) )
func main() { func main() {
// create new IRC connection
c := irc.New("GoTest", "gotest", "GoBot"); c := irc.New("GoTest", "gotest", "GoBot");
c.AddHandler("connected", c.AddHandler("connected",
func(conn *irc.IRCConn, line *irc.IRCLine) { func(conn *irc.Conn, line *irc.Line) {
conn.Join("#"); conn.Join("#");
} }
); );
c.AddHandler("join",
func(conn *irc.IRCConn, line *irc.IRCLine) { // connect to server
if line.Nick == conn.Me {
conn.Privmsg(line.Text, "I LIVE, BITCHES");
}
}
);
if err := c.Connect("irc.pl0rt.org", ""); err != nil { if err := c.Connect("irc.pl0rt.org", ""); err != nil {
fmt.Printf("Connection error: %v\n", err); fmt.Printf("Connection error: %s\n", err);
return; return;
} }
// if we get here, we're successfully connected and should have just // set up a goroutine to read commands from stdin
// dispatched the "CONNECTED" event to it's handlers \o/ in := make(chan string, 4);
control := make(chan os.Error, 1); reallyquit := false;
go c.RunLoop(control); go func() {
if err := <-control; err != nil { con := bufio.NewReader(os.Stdin);
fmt.Printf("IRCConn.RunLoop terminated: %v\n", err); for {
s, err := con.ReadString('\n');
if err != nil {
// wha?, maybe ctrl-D...
close(in);
break;
}
// no point in sending empty lines down the channel
if len(s) > 2 {
in <- s[0:len(s)-1]
}
}
}();
// set up a goroutine to do parsey things with the stuff from stdin
go func() {
for {
if closed(in) {
break;
}
cmd := <-in;
if cmd[0] == ':' {
switch idx := strings.Index(cmd, " "); {
case idx == -1:
continue;
case cmd[1] == 'q':
reallyquit = true;
c.Quit(cmd[idx+1:len(cmd)]);
case cmd[1] == 'j':
c.Join(cmd[idx+1:len(cmd)]);
case cmd[1] == 'p':
c.Part(cmd[idx+1:len(cmd)]);
case cmd[1] == 'd':
fmt.Printf(c.String());
}
} else {
c.Raw(cmd)
}
}
}();
// stall here waiting for asplode on error channel
for {
if closed(c.Err) {
// c.Err being closed indicates we've been disconnected from the
// server for some reason (e.g. quit, kill or ping timeout)
// if we don't really want to quit, reconnect!
if !reallyquit {
fmt.Println("Reconnecting...");
if err := c.Connect("irc.pl0rt.org", ""); err != nil {
fmt.Printf("Connection error: %s\n", err);
break;
}
continue;
}
break;
}
if err := <-c.Err; err != nil {
fmt.Printf("goirc error: %s\n", err);
}
} }
} }

View File

@ -9,5 +9,6 @@ GOFILES=\
connection.go\ connection.go\
commands.go\ commands.go\
handlers.go\ handlers.go\
nickchan.go
include $(GOROOT)/src/Make.pkg include $(GOROOT)/src/Make.pkg

View File

@ -1,10 +1,10 @@
package irc package irc
// this file contains the various commands you can // this file contains the various commands you can
// send to the server using an IRCConn connection // send to the server using an Conn connection
import ( import (
"fmt"; // "fmt";
"reflect"; "reflect";
) )
@ -12,91 +12,127 @@ import (
// the symbol table and add methods/functions on the fly // the symbol table and add methods/functions on the fly
// [ CMD, FMT, FMTARGS ] etc. // [ CMD, FMT, FMTARGS ] etc.
// send a raw line to the server for debugging etc
func (conn *Conn) Raw(s string) {
conn.out <- s
}
// send a PASS command to the server // send a PASS command to the server
func (conn *IRCConn) Pass(p string) { func (conn *Conn) Pass(p string) {
conn.send(fmt.Sprintf("PASS %s", p)); conn.out <- "PASS " + p
} }
// send a NICK command to the server // send a NICK command to the server
func (conn *IRCConn) Nick(n string) { func (conn *Conn) Nick(n string) {
conn.send(fmt.Sprintf("NICK %s", n)); conn.out <- "NICK " + n
} }
// send a USER command to the server // send a USER command to the server
func (conn *IRCConn) User(u, n string) { func (conn *Conn) User(u, n string) {
conn.send(fmt.Sprintf("USER %s 12 * :%s", u, n)); conn.out <- "USER " + u + " 12 * :" + n
} }
// send a JOIN command to the server // send a JOIN command to the server
func (conn *IRCConn) Join(c string) { func (conn *Conn) Join(c string) {
conn.send(fmt.Sprintf("JOIN %s", c)); conn.out <- "JOIN " + c
} }
// send a PART command to the server // send a PART command to the server
func (conn *IRCConn) Part(c string, a ...) { func (conn *Conn) Part(c string, a ...) {
msg := getStringMsg(a); msg := getStringMsg(a);
if msg != "" { if msg != "" {
msg = " :" + msg msg = " :" + msg
} }
conn.send(fmt.Sprintf("PART %s%s", c, msg)); conn.out <- "PART " + c + msg
} }
// send a QUIT command to the server // send a QUIT command to the server
func (conn *IRCConn) Quit(a ...) { func (conn *Conn) Quit(a ...) {
msg := getStringMsg(a); msg := getStringMsg(a);
if msg == "" { if msg == "" {
msg = "GoBye!" msg = "GoBye!"
} }
conn.send(fmt.Sprintf("QUIT :%s", msg)); conn.out <- "QUIT :" + msg
}
// send a WHOIS command to the server
func (conn *Conn) Whois(t string) {
conn.out <- "WHOIS " + t
}
// send a WHO command to the server
func (conn *Conn) Who(t string) {
conn.out <- "WHO " + t
} }
// send a PRIVMSG to the target t // send a PRIVMSG to the target t
func (conn *IRCConn) Privmsg(t, msg string) { func (conn *Conn) Privmsg(t, msg string) {
conn.send(fmt.Sprintf("PRIVMSG %s :%s", t, msg)); conn.out <- "PRIVMSG " + t + " :" + msg
} }
// send a NOTICE to the target t // send a NOTICE to the target t
func (conn *IRCConn) Notice(t, msg string) { func (conn *Conn) Notice(t, msg string) {
conn.send(fmt.Sprintf("NOTICE %s :%s", t, msg)); conn.out <- "NOTICE " + t + " :" +msg
} }
// send a (generic) CTCP to the target t // send a (generic) CTCP to the target t
func (conn *IRCConn) Ctcp(t, ctcp string, a ...) { func (conn *Conn) Ctcp(t, ctcp string, a ...) {
msg := getStringMsg(a); msg := getStringMsg(a);
if msg != "" { if msg != "" {
msg = " " + msg msg = " " + msg
} }
conn.Privmsg(t, fmt.Sprintf("\001%s%s\001", ctcp, msg)); conn.Privmsg(t, "\001" + ctcp + msg + "\001")
} }
// send a generic CTCP reply to the target t // send a generic CTCP reply to the target t
func (conn *IRCConn) CtcpReply(t, ctcp string, a ...) { func (conn *Conn) CtcpReply(t, ctcp string, a ...) {
msg := getStringMsg(a); msg := getStringMsg(a);
if msg != "" { if msg != "" {
msg = " " + msg msg = " " + msg
} }
conn.Notice(t, fmt.Sprintf("\001%s%s\001", ctcp, msg)); conn.Notice(t, "\001" + ctcp + msg + "\001")
} }
// send a CTCP "VERSION" to the target t // send a CTCP "VERSION" to the target t
func (conn *IRCConn) Version(t string) { func (conn *Conn) Version(t string) {
conn.Ctcp(t, "VERSION"); conn.Ctcp(t, "VERSION")
} }
// send a CTCP "ACTION" to the target t -- /me does stuff! // send a CTCP "ACTION" to the target t -- /me does stuff!
func (conn *IRCConn) Action(t, msg string) { func (conn *Conn) Action(t, msg string) {
conn.Ctcp(t, "ACTION", msg); conn.Ctcp(t, "ACTION", msg)
}
// send a TOPIC command to the channel c
func (conn *Conn) Topic(c string, a ...) {
topic := getStringMsg(a);
if topic != "" {
topic = " :" + topic
}
conn.out <- "TOPIC " + c + topic
}
// send a MODE command (this one gets complicated)
// Mode(t) retrieves the user or channel modes for target t
// Mode(t, "string"
func (conn *Conn) Mode(t string, a ...) {
mode := getStringMsg(a);
if mode != "" {
mode = " " + mode
}
conn.out <- "MODE " + t + mode
} }
func getStringMsg(a ...) (msg string) { func getStringMsg(a ...) (msg string) {
// dealing with functions with a variable parameter list is nasteeh :-( // dealing with functions with a variable parameter list is nasteeh :-(
// the below stolen and munged from fmt/print.go // the below stolen and munged from fmt/print.go func getString()
if v := reflect.NewValue(a).(*reflect.StructValue); v.NumField() == 1 { if v := reflect.NewValue(a).(*reflect.StructValue); v.NumField() > 0 {
// XXX: should we check that this looks at least vaguely stringy first? if s, ok := v.Field(0).(*reflect.StringValue); ok {
msg = fmt.Sprintf("%v", v.Field(1)); return s.Get();
} else {
msg = ""
} }
return if b, ok := v.Interface().([]byte); ok {
return string(b)
}
}
return ""
} }

View File

@ -1,5 +1,3 @@
// Some IRC testing code!
package irc package irc
import ( import (
@ -11,96 +9,99 @@ import (
) )
// the IRC connection object // the IRC connection object
type IRCConn struct { type Conn struct {
sock *bufio.ReadWriter; // Hostname, Nickname, etc.
Host string; Host string;
Me string; Me *Nick;
Ident string;
Name string; // I/O stuff to server
con bool; sock *net.TCPConn;
reg bool; io *bufio.ReadWriter;
events map[string] []func (*IRCConn, *IRCLine); in chan *Line;
chans map[string] *IRCChan; out chan string;
nicks map[string] *IRCNick; connected bool;
// Error channel to transmit any fail back to the user
Err chan os.Error;
// Event handler mapping
events map[string] []func (*Conn, *Line);
// Map of channels we're on
chans map[string] *Channel;
// Map of nicks we know about
nicks map[string] *Nick;
} }
// We'll parse an incoming line into this struct // We'll parse an incoming line into this struct
// raw =~ ":nick!user@host cmd args[] :text" // raw =~ ":nick!user@host cmd args[] :text"
// src == "nick!user@host" // src == "nick!user@host"
type IRCLine struct { type Line struct {
Nick string; Nick, Ident, Host, Src string;
User string; Cmd, Text, Raw string;
Host string;
Src string;
Cmd string;
Args []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 // construct a new IRC Connection object
func New(nick, user, name string) (conn *IRCConn) { func New(nick, user, name string) *Conn {
conn = &IRCConn{Me: nick, Ident: user, Name: name}; conn := new(Conn);
// allocate meh some memoraaaahh conn.initialise();
conn.nicks = make(map[string] *IRCNick); conn.Me = conn.NewNick(nick, user, name, "");
conn.chans = make(map[string] *IRCChan);
conn.events = make(map[string] []func(*IRCConn, *IRCLine));
conn.setupEvents(); conn.setupEvents();
return conn return conn;
}
func (conn *Conn) initialise() {
// allocate meh some memoraaaahh
fmt.Println("irc.initialise(): initialising...");
conn.nicks = make(map[string] *Nick);
conn.chans = make(map[string] *Channel);
conn.in = make(chan *Line, 32);
conn.out = make(chan string, 32);
conn.Err = make(chan os.Error, 4);
conn.io = nil;
conn.sock = nil;
} }
// connect the IRC connection object to a host // connect the IRC connection object to a host
func (conn *IRCConn) Connect(host, pass string) (err os.Error) { func (conn *Conn) Connect(host, pass string) os.Error {
if conn.connected {
return os.NewError(fmt.Sprintf("irc.Connect(): already connected to %s, cannot connect to %s", conn.Host, host));
}
if !hasPort(host) { if !hasPort(host) {
host += ":6667"; host += ":6667";
} }
sock, err := net.Dial("tcp", "", host);
if err != nil { if addr, err := net.ResolveTCPAddr(host); err != nil {
return err
} else if conn.sock, err = net.DialTCP("tcp", nil, addr); err != nil {
return err return err
} }
conn.sock = bufio.NewReadWriter(bufio.NewReader(sock), bufio.NewWriter(sock)); fmt.Println("irc.Connect(): connected happily...");
conn.con = true;
conn.Host = host; conn.Host = host;
// initial connection set-up conn.io = bufio.NewReadWriter(
// verify valid nick/user/name here? bufio.NewReader(conn.sock),
bufio.NewWriter(conn.sock)
);
go conn.send();
go conn.recv();
if pass != "" { if pass != "" {
conn.Pass(pass) conn.Pass(pass)
} }
conn.Nick(conn.Me); conn.Nick(conn.Me.Nick);
conn.User(conn.Ident, conn.Name); conn.User(conn.Me.Ident, conn.Me.Name);
for line, err := conn.recv(); err == nil; line, err = conn.recv() { go conn.runLoop();
// initial loop to get us to the point where we're connected fmt.Println("irc.Connect(): launched runLoop() goroutine.");
conn.dispatchEvent(line); return nil;
if line.Cmd == "001" {
break;
}
}
return err;
} }
func (conn *IRCConn) RunLoop(c chan os.Error) { // dispatch a nicely formatted os.Error to the error channel
var err os.Error; func (conn *Conn) error(s string, a ...) {
for line, err := conn.recv(); err == nil; line, err = conn.recv() { conn.Err <- os.NewError(fmt.Sprintf(s, a));
conn.dispatchEvent(line);
}
c <- err;
return;
} }
// copied from http.client for great justice // copied from http.client for great justice
@ -108,25 +109,37 @@ func hasPort(s string) bool {
return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") return strings.LastIndex(s, ":") > strings.LastIndex(s, "]")
} }
// send \r\n terminated line to peer, propagate errors // dispatch input from channel as \r\n terminated line to peer
func (conn *IRCConn) send(line string) (err os.Error) { func (conn *Conn) send() {
err = conn.sock.WriteString(line + "\r\n"); for {
conn.sock.Flush(); line := <-conn.out;
if closed(conn.out) {
break;
}
if err := conn.io.WriteString(line + "\r\n"); err != nil {
conn.error("irc.send(): %s", err.String());
conn.shutdown();
break;
}
conn.io.Flush();
fmt.Println("-> " + line); fmt.Println("-> " + line);
return err }
} }
// receive one \r\n terminated line from peer and parse it, propagate errors // receive one \r\n terminated line from peer, parse and dispatch it
func (conn *IRCConn) recv() (line *IRCLine, err os.Error) { func (conn *Conn) recv() {
s, err := conn.sock.ReadString('\n'); for {
s, err := conn.io.ReadString('\n');
if err != nil { if err != nil {
return line, err conn.error("irc.recv(): %s", err.String());
conn.shutdown();
break;
} }
// chop off \r\n // chop off \r\n
s = s[0:len(s)-2]; s = s[0:len(s)-2];
fmt.Println("<- " + s); fmt.Println("<- " + s);
line = &IRCLine{Raw: s}; line := &Line{Raw: s};
if s[0] == ':' { if s[0] == ':' {
// remove a source and parse it // remove a source and parse it
if idx := strings.Index(s, " "); idx != -1 { if idx := strings.Index(s, " "); idx != -1 {
@ -134,7 +147,8 @@ func (conn *IRCConn) recv() (line *IRCLine, err os.Error) {
} else { } else {
// pretty sure we shouldn't get here ... // pretty sure we shouldn't get here ...
line.Src = s[1:len(s)]; line.Src = s[1:len(s)];
return line, nil; conn.in <- line;
continue;
} }
// src can be the hostname of the irc server or a nick!user@host // src can be the hostname of the irc server or a nick!user@host
@ -142,7 +156,7 @@ func (conn *IRCConn) recv() (line *IRCLine, err os.Error) {
nidx, uidx := strings.Index(line.Src, "!"), strings.Index(line.Src, "@"); nidx, uidx := strings.Index(line.Src, "!"), strings.Index(line.Src, "@");
if uidx != -1 && nidx != -1 { if uidx != -1 && nidx != -1 {
line.Nick = line.Src[0:nidx]; line.Nick = line.Src[0:nidx];
line.User = line.Src[nidx+1:uidx]; line.Ident = line.Src[nidx+1:uidx];
line.Host = line.Src[uidx+1:len(line.Src)]; line.Host = line.Src[uidx+1:len(line.Src)];
} }
} }
@ -158,6 +172,57 @@ func (conn *IRCConn) recv() (line *IRCLine, err os.Error) {
if len(args) > 1 { if len(args) > 1 {
line.Args = args[1:len(args)]; line.Args = args[1:len(args)];
} }
return line, nil conn.in <- line
}
}
func (conn *Conn) runLoop() {
for {
if closed(conn.in) {
break;
}
select {
case line := <-conn.in:
conn.dispatchEvent(line);
}
}
fmt.Println("irc.runLoop(): Exited runloop...");
// if we fall off the end here due to shutdown,
// reinit everything once the runloop is done
// so that Connect() can be called again.
conn.initialise();
}
func (conn *Conn) shutdown() {
close(conn.in);
close(conn.out);
close(conn.Err);
conn.connected = false;
conn.sock.Close();
fmt.Println("irc.shutdown(): shut down sockets and channels!");
}
func (conn *Conn) String() string {
str := "GoIRC Connection\n";
str += "----------------\n\n";
if conn.connected {
str += "Connected to " + conn.Host + "\n\n"
} else {
str += "Not currently connected!\n\n";
}
str += conn.Me.String() + "\n";
str += "GoIRC Channels\n";
str += "--------------\n\n";
for _, ch := range conn.chans {
str += ch.String() + "\n"
}
str += "GoIRC NickNames\n";
str += "---------------\n\n";
for _, n := range conn.nicks {
if n != conn.Me {
str += n.String() + "\n"
}
}
return str;
} }

View File

@ -4,17 +4,17 @@ package irc
// to manage tracking an irc connection etc. // to manage tracking an irc connection etc.
import ( import (
"fmt";
"strings"; "strings";
"strconv";
) )
// Add an event handler for a specific IRC command // Add an event handler for a specific IRC command
func (conn *IRCConn) AddHandler(name string, f func (*IRCConn, *IRCLine)) { func (conn *Conn) AddHandler(name string, f func (*Conn, *Line)) {
n := strings.ToUpper(name); n := strings.ToUpper(name);
if e, ok := conn.events[n]; ok { if e, ok := conn.events[n]; ok {
if len(e) == cap(e) { if len(e) == cap(e) {
// crap, we're full. expand e by another 10 handler slots // crap, we're full. expand e by another 10 handler slots
ne := make([]func (*IRCConn, *IRCLine), len(e), len(e)+10); ne := make([]func (*Conn, *Line), len(e), len(e)+10);
for i := 0; i<len(e); i++ { for i := 0; i<len(e); i++ {
ne[i] = e[i]; ne[i] = e[i];
} }
@ -23,14 +23,21 @@ func (conn *IRCConn) AddHandler(name string, f func (*IRCConn, *IRCLine)) {
e = e[0:len(e)+1]; e = e[0:len(e)+1];
e[len(e)-1] = f; e[len(e)-1] = f;
} else { } else {
e := make([]func (*IRCConn, *IRCLine), 1, 10); e := make([]func (*Conn, *Line), 1, 10);
e[0] = f; e[0] = f;
conn.events[n] = e; conn.events[n] = e;
} }
} }
// loops through all event handlers for line.Cmd, running each in a goroutine // loops through all event handlers for line.Cmd, running each in a goroutine
func (conn *IRCConn) dispatchEvent(line *IRCLine) { func (conn *Conn) dispatchEvent(line *Line) {
// seems that we end up dispatching an event with a nil line when receiving
// EOF from the server. Until i've tracked down why....
if line == nil {
conn.error("irc.dispatchEvent(): buh? line == nil :-(");
return
}
// So, I think CTCP and (in particular) CTCP ACTION are better handled as // So, I think CTCP and (in particular) CTCP ACTION are better handled as
// separate events as opposed to forcing people to have gargantuan PRIVMSG // separate events as opposed to forcing people to have gargantuan PRIVMSG
// handlers to cope with the possibilities. // handlers to cope with the possibilities.
@ -67,22 +74,329 @@ func (conn *IRCConn) dispatchEvent(line *IRCLine) {
// sets up the internal event handlers to do useful things with lines // sets up the internal event handlers to do useful things with lines
// XXX: is there a better way of doing this? // XXX: is there a better way of doing this?
func (conn *IRCConn) setupEvents() { // Turns out there may be but it's not actually implemented in the language yet
// according to the people on freenode/#go-nuts ... :-(
// see: http://golang.org/doc/go_spec.html#Method_expressions for details
// I think this means we should be able to do something along the lines of:
// conn.AddHandler("event", (*Conn).h_handler);
// where h_handler is declared in the irc package as:
// func (conn *Conn) h_handler(line *Line) {}
// in the future, but for now the compiler throws a hissy fit.
func (conn *Conn) setupEvents() {
conn.events = make(map[string] []func(*Conn, *Line));
// Basic ping/pong handler // Basic ping/pong handler
conn.AddHandler("PING", func(conn *IRCConn, line *IRCLine) { conn.AddHandler("PING", func(conn *Conn, line *Line) {
conn.send(fmt.Sprintf("PONG :%s", line.Text)); conn.Raw("PONG :" + line.Text);
}); });
// Handler to trigger a "CONNECTED" event on receipt of numeric 001 // Handler to trigger a "CONNECTED" event on receipt of numeric 001
conn.AddHandler("001", func(conn *IRCConn, line *IRCLine) { conn.AddHandler("001", func(conn *Conn, line *Line) {
l := new(IRCLine); // we're connected!
l.Cmd = "CONNECTED"; conn.connected = true;
conn.dispatchEvent(l); conn.dispatchEvent(&Line{Cmd: "CONNECTED"});
// and we're being given our hostname (from the server's perspective)
if ridx := strings.LastIndex(line.Text, " "); ridx != -1 {
h := line.Text[ridx+1:len(line.Text)];
if idx := strings.Index(h, "@"); idx != -1 {
conn.Me.Host = h[idx+1:len(h)];
}
}
}); });
// Handler to deal with "433 :Nickname already in use" on connection // XXX: do we need 005 protocol support message parsing here?
conn.AddHandler("433", func(conn *IRCConn, line *IRCLine) { // probably in the future, but I can't quite be arsed yet.
conn.Nick(conn.Me + "_"); /*
:irc.pl0rt.org 005 GoTest CMDS=KNOCK,MAP,DCCALLOW,USERIP UHNAMES NAMESX SAFELIST HCN MAXCHANNELS=20 CHANLIMIT=#:20 MAXLIST=b:60,e:60,I:60 NICKLEN=30 CHANNELLEN=32 TOPICLEN=307 KICKLEN=307 AWAYLEN=307 :are supported by this server
:irc.pl0rt.org 005 GoTest MAXTARGETS=20 WALLCHOPS WATCH=128 WATCHOPTS=A SILENCE=15 MODES=12 CHANTYPES=# PREFIX=(qaohv)~&@%+ CHANMODES=beI,kfL,lj,psmntirRcOAQKVCuzNSMT NETWORK=bb101.net CASEMAPPING=ascii EXTBAN=~,cqnr ELIST=MNUCT :are supported by this server
:irc.pl0rt.org 005 GoTest STATUSMSG=~&@%+ EXCEPTS INVEX :are supported by this server
*/
// Handler to deal with "433 :Nickname already in use"
conn.AddHandler("433", func(conn *Conn, line *Line) {
// Args[1] is the new nick we were attempting to acquire
conn.Nick(line.Args[1] + "_");
});
// Handler NICK messages to inform us about nick changes
conn.AddHandler("NICK", func(conn *Conn, line *Line) {
// all nicks should be handled the same way, our own included
if n := conn.GetNick(line.Nick); n != nil {
n.ReNick(line.Text);
} else {
conn.error("irc.NICK(): buh? unknown nick %s.", line.Nick);
}
});
// Handle VERSION requests and CTCP PING
conn.AddHandler("CTCP", func(conn *Conn, line *Line) {
if line.Args[0] == "VERSION" {
conn.CtcpReply(line.Nick, "VERSION", "powered by goirc...");
} else if line.Args[0] == "PING" {
conn.CtcpReply(line.Nick, "PING", line.Text);
}
});
// Handle JOINs to channels to maintain state
conn.AddHandler("JOIN", func(conn *Conn, line *Line) {
ch := conn.GetChannel(line.Text);
n := conn.GetNick(line.Nick);
if ch == nil {
// first we've seen of this channel, so should be us joining it
// NOTE this will also take care of n == nil && ch == nil
if n != conn.Me {
conn.error("irc.JOIN(): buh? JOIN to unknown channel %s recieved from (non-me) nick %s", line.Text, line.Nick);
return;
}
ch = conn.NewChannel(line.Text);
// since we don't know much about this channel, ask server for info
// we get the channel users automatically in 353 and the channel
// topic in 332 on join, so we just need to get the modes
conn.Mode(ch.Name);
}
if n == nil {
// this is the first we've seen of this nick
n = conn.NewNick(line.Nick, line.Ident, "", line.Host);
// since we don't know much about this nick, ask server for info
conn.Whois(n.Nick);
}
// this takes care of both nick and channel linking \o/
ch.AddNick(n);
});
// Handle PARTs from channels to maintain state
conn.AddHandler("PART", func(conn *Conn, line *Line) {
ch := conn.GetChannel(line.Args[0]);
n := conn.GetNick(line.Nick);
if ch != nil && n != nil {
ch.DelNick(n);
} else {
conn.error("irc.PART(): buh? PART of channel %s by nick %s", line.Args[0], line.Nick);
}
});
// Handle KICKs from channels to maintain state
conn.AddHandler("KICK", func(conn *Conn, line *Line) {
// XXX: this won't handle autorejoining channels on KICK
// it's trivial to do this in a seperate handler...
ch := conn.GetChannel(line.Args[0]);
n := conn.GetNick(line.Args[1]);
if ch != nil && n != nil {
ch.DelNick(n);
} else {
conn.error("irc.KICK(): buh? KICK from channel %s of nick %s", line.Args[0], line.Args[1]);
}
});
// Handle other people's QUITs
conn.AddHandler("QUIT", func(conn *Conn, line *Line) {
if n := conn.GetNick(line.Nick); n != nil {
n.Delete();
} else {
conn.error("irc.QUIT(): buh? QUIT from unknown nick %s", line.Nick);
}
});
// Handle MODE changes for channels we know about (and our nick personally)
// this is moderately ugly. suggestions for improvement welcome
conn.AddHandler("MODE", func(conn *Conn, line *Line) {
// channel modes first
if ch := conn.GetChannel(line.Args[0]); ch != nil {
modeargs := line.Args[2:len(line.Args)];
var modeop bool; // true => add mode, false => remove mode
var modestr string;
for i := 0; i < len(line.Args[1]); i++ {
switch m := line.Args[1][i]; m {
case '+':
modeop = true;
modestr = string(m);
case '-':
modeop = false;
modestr = string(m);
case 'i': ch.Modes.InviteOnly = modeop;
case 'm': ch.Modes.Moderated = modeop;
case 'n': ch.Modes.NoExternalMsg = modeop;
case 'p': ch.Modes.Private = modeop;
case 's': ch.Modes.Secret = modeop;
case 't': ch.Modes.ProtectedTopic = modeop;
case 'z': ch.Modes.SSLOnly = modeop;
case 'O': ch.Modes.OperOnly = modeop;
case 'k':
if len(modeargs) != 0 {
ch.Modes.Key, modeargs = modeargs[0], modeargs[1:len(modeargs)]
} else {
conn.error("irc.MODE(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m)
}
case 'l':
if len(modeargs) != 0 {
ch.Modes.Limit, _ = strconv.Atoi(modeargs[0]);
modeargs = modeargs[1:len(modeargs)];
} else {
conn.error("irc.MODE(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m)
}
case 'q', 'a', 'o', 'h', 'v':
if len(modeargs) != 0 {
n := conn.GetNick(modeargs[0]);
if p, ok := ch.Nicks[n]; ok && n != nil {
switch m {
case 'q': p.Owner = modeop;
case 'a': p.Admin = modeop;
case 'o': p.Op = modeop;
case 'h': p.HalfOp = modeop;
case 'v': p.Voice = modeop;
}
modeargs = modeargs[1:len(modeargs)];
} else {
conn.error("irc.MODE(): MODE %s %s%s %s: buh? state tracking failure.", ch.Name, modestr, m, modeargs[0]);
}
} else {
conn.error("irc.MODE(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m);
}
}
}
} else if n := conn.GetNick(line.Args[0]); n != nil {
// nick mode change, should be us
if n != conn.Me {
conn.error("irc.MODE(): buh? recieved MODE %s for (non-me) nick %s", line.Args[1], n.Nick);
return;
}
var modeop bool; // true => add mode, false => remove mode
for i := 0; i < len(line.Text); i++ {
switch m := line.Text[i]; m {
case '+':
modeop = true;
case '-':
modeop = false;
case 'i': n.Modes.Invisible = modeop;
case 'o': n.Modes.Oper = modeop;
case 'w': n.Modes.WallOps = modeop;
case 'x': n.Modes.HiddenHost = modeop;
case 'z': n.Modes.SSL = modeop;
}
}
} else {
conn.error("irc.MODE(): buh? not sure what to do with MODE %s %s", line.Args[0], line.Args[1]);
}
});
// Handle TOPIC changes for channels
conn.AddHandler("TOPIC", func(conn *Conn, line *Line) {
if ch := conn.GetChannel(line.Args[0]); ch != nil {
ch.Topic = line.Text;
} else {
conn.error("irc.TOPIC(): buh? topic change on unknown channel %s", line.Args[0]);
}
});
// Handle 311 whois reply
conn.AddHandler("311", func(conn *Conn, line *Line) {
if n := conn.GetNick(line.Args[1]); n != nil {
n.Ident = line.Args[2];
n.Host = line.Args[3];
n.Name = line.Text;
} else {
conn.error("irc.311(): buh? received WHOIS info for unknown nick %s", line.Args[1]);
}
});
// Handle 324 mode reply
conn.AddHandler("324", func(conn *Conn, line *Line) {
// XXX: copypasta from MODE, needs tidying.
if ch := conn.GetChannel(line.Args[1]); ch != nil {
modeargs := line.Args[3:len(line.Args)];
var modeop bool; // true => add mode, false => remove mode
var modestr string;
for i := 0; i < len(line.Args[2]); i++ {
switch m := line.Args[2][i]; m {
case '+':
modeop = true;
modestr = string(m);
case '-':
modeop = false;
modestr = string(m);
case 'i': ch.Modes.InviteOnly = modeop;
case 'm': ch.Modes.Moderated = modeop;
case 'n': ch.Modes.NoExternalMsg = modeop;
case 'p': ch.Modes.Private = modeop;
case 's': ch.Modes.Secret = modeop;
case 't': ch.Modes.ProtectedTopic = modeop;
case 'z': ch.Modes.SSLOnly = modeop;
case 'O': ch.Modes.OperOnly = modeop;
case 'k':
if len(modeargs) != 0 {
ch.Modes.Key, modeargs = modeargs[0], modeargs[1:len(modeargs)]
} else {
conn.error("irc.324(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m)
}
case 'l':
if len(modeargs) != 0 {
ch.Modes.Limit, _ = strconv.Atoi(modeargs[0]);
modeargs = modeargs[1:len(modeargs)];
} else {
conn.error("irc.324(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m)
}
}
}
} else {
conn.error("irc.324(): buh? received MODE settings for unknown channel %s", line.Args[1]);
}
});
// Handle 332 topic reply on join to channel
conn.AddHandler("332", func(conn *Conn, line *Line) {
if ch := conn.GetChannel(line.Args[1]); ch != nil {
ch.Topic = line.Text;
} else {
conn.error("irc.332(): buh? received TOPIC value for unknown channel %s", line.Args[1]);
}
});
// Handle 353 names reply
conn.AddHandler("353", func(conn *Conn, line *Line) {
if ch := conn.GetChannel(line.Args[2]); ch != nil {
nicks := strings.Split(line.Text, " ", 0);
for _, nick := range nicks {
// UnrealIRCd's coders are lazy and leave a trailing space
if nick == "" {
continue
}
switch c := nick[0]; c {
case '~', '&', '@', '%', '+':
nick = nick[1:len(nick)];
fallthrough;
default:
n := conn.GetNick(nick);
if n == nil {
// we don't know this nick yet!
n = conn.NewNick(nick, "", "", "");
conn.Whois(nick);
}
if n != conn.Me {
// we will be in the names list, but should also be in
// the channel's nick list from the JOIN handler above
ch.AddNick(n);
}
p := ch.Nicks[n];
switch c {
case '~': p.Owner = true;
case '&': p.Admin = true;
case '@': p.Op = true;
case '%': p.HalfOp = true;
case '+': p.Voice = true;
}
}
}
} else {
conn.error("irc.353(): buh? received NAMES list for unknown channel %s", line.Args[2]);
}
});
// Handle 671 whois reply (nick connected via SSL)
conn.AddHandler("671", func(conn *Conn, line *Line) {
if n := conn.GetNick(line.Args[1]); n != nil {
n.Modes.SSL = true;
} else {
conn.error("irc.671(): buh? received WHOIS SSL info for unknown nick %s", line.Args[1]);
}
}); });
} }

327
irc/nickchan.go Normal file
View File

@ -0,0 +1,327 @@
package irc
// Here you'll find the Channel and Nick structs
// as well as the internal state maintenance code for the handlers
import (
"fmt";
"reflect";
)
// A struct representing an IRC channel
type Channel struct {
Name, Topic string;
Modes *ChanMode;
// MODE +q, +a, +o, +h, +v
Nicks map[*Nick] *ChanPrivs;
conn *Conn;
}
// A struct representing an IRC nick
type Nick struct {
Nick, Ident, Host, Name string;
Modes *NickMode;
Channels map[*Channel] *ChanPrivs;
conn *Conn;
}
// A struct representing the modes of an IRC Channel
// (the ones we care about, at least)
// see the MODE handler in setupEvents() for details
type ChanMode struct {
// MODE +p, +s, +t, +n, +m
Private, Secret, ProtectedTopic, NoExternalMsg, Moderated bool;
// MODE +i, +O, +z
InviteOnly, OperOnly, SSLOnly bool;
// MODE +k
Key string;
// MODE +l
Limit int;
}
// A struct representing the modes of an IRC Nick (User Modes)
// (again, only the ones we care about)
// This is only really useful for conn.Me, as we can't see other people's modes
// without IRC operator privileges (and even then only on some IRCd's).
type NickMode struct {
// MODE +i, +o, +w, +x, +z
Invisible, Oper, WallOps, HiddenHost, SSL bool;
}
// A struct representing the modes a Nick can have on a Channel
type ChanPrivs struct {
// MODE +q, +a, +o, +h, +v
Owner, Admin, Op, HalfOp, Voice bool;
}
/******************************************************************************\
* Conn methods to create/look up nicks/channels
\******************************************************************************/
func (conn *Conn) NewNick(nick, ident, name, host string) *Nick {
n := &Nick{Nick: nick, Ident: ident, Name: name, Host: host, conn: conn};
n.initialise();
conn.nicks[n.Nick] = n;
return n;
}
func (conn *Conn) GetNick(n string) *Nick {
if nick, ok := conn.nicks[n]; ok {
return nick
}
return nil
}
func (conn *Conn) NewChannel(c string) *Channel {
ch := &Channel{Name: c, conn: conn};
ch.initialise();
conn.chans[ch.Name] = ch;
return ch
}
func (conn *Conn) GetChannel(c string) *Channel {
if ch, ok := conn.chans[c]; ok {
return ch
}
return nil
}
/******************************************************************************\
* Channel methods for state management
\******************************************************************************/
func (ch *Channel) initialise() {
ch.Modes = new(ChanMode);
ch.Nicks = make(map[*Nick] *ChanPrivs);
}
func (ch *Channel) AddNick(n *Nick) {
if _, ok := ch.Nicks[n]; !ok {
ch.Nicks[n] = new(ChanPrivs);
n.Channels[ch] = ch.Nicks[n];
} else {
ch.conn.error("irc.Channel.AddNick() warning: trying to add already-present nick %s to channel %s", n.Nick, ch.Name);
}
}
func (ch *Channel) DelNick(n *Nick) {
if _, ok := ch.Nicks[n]; ok {
fmt.Printf("irc.Channel.DelNick(): deleting %s from %s\n", n.Nick, ch.Name);
if n == n.conn.Me {
// we're leaving the channel, so remove all state we have about it
ch.Delete();
} else {
ch.Nicks[n] = nil, false;
n.DelChannel(ch);
}
} // no else here ...
// we call Channel.DelNick() and Nick.DelChan() from each other to ensure
// consistency, and this would mean spewing an error message every delete
}
func (ch *Channel) Delete() {
fmt.Printf("irc.Channel.Delete(): deleting %s\n", ch.Name);
for n, _ := range ch.Nicks {
n.DelChannel(ch);
}
ch.conn.chans[ch.Name] = nil, false;
}
/******************************************************************************\
* Nick methods for state management
\******************************************************************************/
func (n *Nick) initialise() {
n.Modes = new(NickMode);
n.Channels = make(map[*Channel] *ChanPrivs);
}
// very slightly different to Channel.AddNick() ...
func (n *Nick) AddChannel(ch *Channel) {
if _, ok := n.Channels[ch]; !ok {
ch.Nicks[n] = new(ChanPrivs);
n.Channels[ch] = ch.Nicks[n];
} else {
n.conn.error("irc.Nick.AddChannel() warning: trying to add already-present channel %s to nick %s", ch.Name, n.Nick);
}
}
func (n *Nick) DelChannel(ch *Channel) {
if _, ok := n.Channels[ch]; ok {
fmt.Printf("irc.Nick.DelChannel(): deleting %s from %s\n", n.Nick, ch.Name);
n.Channels[ch] = nil, false;
ch.DelNick(n);
if len(n.Channels) == 0 {
// nick is no longer in any channels we inhabit, stop tracking it
n.Delete();
}
}
}
func (n *Nick) ReNick(neu string) {
n.conn.nicks[n.Nick] = nil, false;
n.Nick = neu;
n.conn.nicks[n.Nick] = n;
}
func (n *Nick) Delete() {
// we don't ever want to remove *our* nick from conn.nicks...
if n != n.conn.Me {
fmt.Printf("irc.Nick.Delete(): deleting %s\n", n.Nick);
for ch, _ := range n.Channels {
ch.DelNick(n);
}
n.conn.nicks[n.Nick] = nil, false;
}
}
/******************************************************************************\
* String() methods for all structs in this file for ease of debugging.
\******************************************************************************/
var ChanModeToString = map[string]string{
"Private":"p",
"Secret":"s",
"ProtectedTopic":"t",
"NoExternalMsg":"n",
"Moderated":"m",
"InviteOnly":"i",
"OperOnly":"O",
"SSLOnly":"z",
"Key":"k",
"Limit":"l",
};
var NickModeToString = map[string]string{
"Invisible":"i",
"Oper":"o",
"WallOps":"w",
"HiddenHost":"x",
"SSL":"z",
};
var ChanPrivToString = map[string]string{
"Owner":"q",
"Admin":"a",
"Op":"o",
"HalfOp":"h",
"Voice":"v",
};
var ChanPrivToModeChar = map[string]byte{
"Owner":'~',
"Admin":'&',
"Op":'@',
"HalfOp":'%',
"Voice":'+',
};
var StringToChanMode, StringToNickMode, StringToChanPriv map[string]string;
var ModeCharToChanPriv map[byte]string;
// Init function to fill in reverse mappings for *toString constants etc.
func init() {
StringToChanMode = make(map[string]string);
for k,v := range ChanModeToString {
StringToChanMode[v] = k;
}
StringToNickMode = make(map[string]string);
for k,v := range NickModeToString {
StringToNickMode[v] = k;
}
StringToChanPriv = make(map[string]string);
for k,v := range ChanPrivToString {
StringToChanPriv[v] = k;
}
ModeCharToChanPriv = make(map[byte]string);
for k,v := range ChanPrivToModeChar {
ModeCharToChanPriv[v] = k;
}
}
func (ch *Channel) String() string {
str := "Channel: " + ch.Name + "\n\t";
str += "Topic: " + ch.Topic + "\n\t";
str += "Modes: " + ch.Modes.String() + "\n\t";
str += "Nicks: \n";
for n, p := range ch.Nicks {
str += "\t\t" + n.Nick + ": " + p.String() + "\n";
}
return str;
}
func (n *Nick) String() string {
str := "Nick: "+ n.Nick + "\n\t";
str += "Hostmask: " + n.Ident + "@" + n.Host + "\n\t";
str += "Real Name: " + n.Name + "\n\t";
str += "Modes: " + n.Modes.String() + "\n\t";
str += "Channels: \n";
for ch, p := range n.Channels {
str += "\t\t" + ch.Name + ": " + p.String() + "\n";
}
return str;
}
func (cm *ChanMode) String() string {
str := "+";
a := make([]string, 2);
v := reflect.Indirect(reflect.NewValue(cm)).(*reflect.StructValue);
t := v.Type().(*reflect.StructType);
for i := 0; i < v.NumField(); i++ {
switch f := v.Field(i).(type) {
case *reflect.BoolValue:
if f.Get() {
str += ChanModeToString[t.Field(i).Name];
}
case *reflect.StringValue:
if f.Get() != "" {
str += ChanModeToString[t.Field(i).Name];
a[0] = f.Get();
}
case *reflect.IntValue:
if f.Get() != 0 {
str += ChanModeToString[t.Field(i).Name];
a[1] = fmt.Sprintf("%d", cm.Limit);
}
}
}
for _, s := range a {
if s != "" {
str += " " + s;
}
}
if str == "+" {
str = "No modes set";
}
return str;
}
func (nm *NickMode) String() string {
str := "+";
v := reflect.Indirect(reflect.NewValue(nm)).(*reflect.StructValue);
t := v.Type().(*reflect.StructType);
for i := 0; i < v.NumField(); i++ {
switch f := v.Field(i).(type) {
// only bools here at the mo!
case *reflect.BoolValue:
if f.Get() {
str += NickModeToString[t.Field(i).Name];
}
}
}
if str == "+" {
str = "No modes set";
}
return str;
}
func (p *ChanPrivs) String() string {
str := "+";
v := reflect.Indirect(reflect.NewValue(p)).(*reflect.StructValue);
t := v.Type().(*reflect.StructType);
for i := 0; i < v.NumField(); i++ {
switch f := v.Field(i).(type) {
// only bools here at the mo too!
case *reflect.BoolValue:
if f.Get() {
str += ChanPrivToString[t.Field(i).Name];
}
}
}
if str == "+" {
str = "No modes set";
}
return str;
}

1
vims Normal file
View File

@ -0,0 +1 @@
find . -name \*.go | xargs gvim -p