1
0
Fork 0
mirror of https://github.com/fluffle/goirc synced 2025-05-12 18:44:50 +00:00

Merge commit 'bedbfc2'

Conflicts:
	client.go
This commit is contained in:
raylu 2010-12-18 17:36:01 -08:00
commit d9267067cd
6 changed files with 6 additions and 6 deletions

14
client/Makefile Normal file
View file

@ -0,0 +1,14 @@
# 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.inc
TARG=github.com/fluffle/goirc/client
GOFILES=\
connection.go\
commands.go\
handlers.go\
nickchan.go
include $(GOROOT)/src/Make.pkg

140
client/commands.go Normal file
View file

@ -0,0 +1,140 @@
package client
import "strings"
// this file contains the various commands you can
// send to the server using an Conn connection
// 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.
// Raw() sends a raw line to the server, should really only be used for
// debugging purposes but may well come in handy.
func (conn *Conn) Raw(rawline string) { conn.out <- rawline }
// Pass() sends a PASS command to the server
func (conn *Conn) Pass(password string) { conn.out <- "PASS "+password }
// Nick() sends a NICK command to the server
func (conn *Conn) Nick(nick string) { conn.out <- "NICK "+nick }
// User() sends a USER command to the server
func (conn *Conn) User(ident, name string) {
conn.out <- "USER "+ident+" 12 * :"+name
}
// Join() sends a JOIN command to the server
func (conn *Conn) Join(channel string) { conn.out <- "JOIN "+channel }
// Part() sends a PART command to the server with an optional part message
func (conn *Conn) Part(channel string, message ...string) {
msg := strings.Join(message, " ")
if msg != "" {
msg = " :" + msg
}
conn.out <- "PART "+channel+msg
}
// Kick() sends a KICK command to remove a nick from a channel
func (conn *Conn) Kick(channel, nick string, message ...string) {
msg := strings.Join(message, " ")
if msg != "" {
msg = " :" + msg
}
conn.out <- "KICK "+channel+" "+nick+msg
}
// Quit() sends a QUIT command to the server with an optional quit message
func (conn *Conn) Quit(message ...string) {
msg := strings.Join(message, " ")
if msg == "" {
msg = "GoBye!"
}
conn.out <- "QUIT :"+msg
}
// Whois() sends a WHOIS command to the server
func (conn *Conn) Whois(nick string) { conn.out <- "WHOIS "+nick }
//Who() sends a WHO command to the server
func (conn *Conn) Who(nick string) { conn.out <- "WHO "+nick }
// Privmsg() sends a PRIVMSG to the target t
func (conn *Conn) Privmsg(t, msg string) { conn.out <- "PRIVMSG "+t+" :"+msg }
// Notice() sends a NOTICE to the target t
func (conn *Conn) Notice(t, msg string) { conn.out <- "NOTICE "+t+" :"+msg }
// Ctcp() sends a (generic) CTCP message to the target t
// with an optional argument
func (conn *Conn) Ctcp(t, ctcp string, arg ...string) {
msg := strings.Join(arg, " ")
if msg != "" {
msg = " " + msg
}
conn.Privmsg(t, "\001"+ctcp+msg+"\001")
}
// CtcpReply() sends a generic CTCP reply to the target t
// with an optional argument
func (conn *Conn) CtcpReply(t, ctcp string, arg ...string) {
msg := strings.Join(arg, " ")
if msg != "" {
msg = " " + msg
}
conn.Notice(t, "\001"+ctcp+msg+"\001")
}
// Version() sends a CTCP "VERSION" to the target t
func (conn *Conn) Version(t string) { conn.Ctcp(t, "VERSION") }
// Action() sends a CTCP "ACTION" to the target t
func (conn *Conn) Action(t, msg string) { conn.Ctcp(t, "ACTION", msg) }
// Topic() sends a TOPIC command to the channel
// Topic(channel) retrieves the current channel topic (see "332" handler)
// Topic(channel, topic) sets the topic for the channel
func (conn *Conn) Topic(channel string, topic ...string) {
t := strings.Join(topic, " ")
if t != "" {
t = " :" + t
}
conn.out <- "TOPIC "+channel+t
}
// Mode() sends a MODE command to the server. This one can get complicated if
// we try to be too clever, so it's deliberately simple:
// Mode(t) retrieves the user or channel modes for target t
// Mode(t, "modestring") sets user or channel modes for target t, where...
// modestring == e.g. "+o <nick>" or "+ntk <key>" or "-is"
// This means you'll need to do your own mode work. It may be linked in with
// the state tracking and ChanMode/NickMode/ChanPrivs objects later...
func (conn *Conn) Mode(t string, modestring ...string) {
mode := strings.Join(modestring, " ")
if mode != "" {
mode = " " + mode
}
conn.out <- "MODE "+t+mode
}
// Away() sends an AWAY command to the server
// Away() resets away status
// Away(message) sets away with the given message
func (conn *Conn) Away(message ...string) {
msg := strings.Join(message, " ")
if msg != "" {
msg = " :" + msg
}
conn.out <- "AWAY"+msg
}
// Invite() sends an INVITE command to the server
func (conn *Conn) Invite(nick, channel string) {
conn.out <- "INVITE "+nick+" "+channel
}
// Oper() sends an OPER command to the server
func (conn *Conn) Oper(user, pass string) {
conn.out <- "OPER "+user+" "+pass
}

295
client/connection.go Normal file
View file

@ -0,0 +1,295 @@
package client
import (
"bufio"
"os"
"net"
"crypto/tls"
"fmt"
"strings"
"time"
)
// An IRC connection is represented by this struct. Once connected, any errors
// encountered are piped down *Conn.Err; this channel is closed on disconnect.
type Conn struct {
// Connection Hostname and Nickname
Host string
Me *Nick
Network string
// I/O stuff to server
sock net.Conn
io *bufio.ReadWriter
in chan *Line
out chan string
connected bool
// Are we connecting via SSL? Do we care about certificate validity?
SSL bool
SSLConfig *tls.Config
// Error channel to transmit any fail back to the user
Err chan os.Error
// Set this to true to disable flood protection and false to re-enable
Flood bool
Debug bool
// 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 parse an incoming line into this struct. Line.Cmd is used as the trigger
// name for incoming event handlers, see *Conn.recv() for details.
// Raw =~ ":nick!user@host cmd args[] :text"
// Src == "nick!user@host"
// Cmd == e.g. PRIVMSG, 332
type Line struct {
Nick, Ident, Host, Src string
Cmd, Text, Raw string
Args []string
}
// Creates a new IRC connection object, but doesn't connect to anything so
// that you can add event handlers to it. See AddHandler() for details.
func New(nick, user, name string) *Conn {
conn := new(Conn)
conn.initialise()
conn.Me = conn.NewNick(nick, user, name, "")
conn.setupEvents()
return conn
}
func (conn *Conn) initialise() {
// allocate meh some memoraaaahh
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.SSL = false
conn.SSLConfig = nil
conn.io = nil
conn.sock = nil
// if this is being called because we are reconnecting, conn.Me
// will still have all the old channels referenced -- nuke them!
if conn.Me != nil {
conn.Me = conn.NewNick(conn.Me.Nick, conn.Me.Ident, conn.Me.Name, "")
}
}
// Connect the IRC connection object to "host[:port]" which should be either
// a hostname or an IP address, with an optional port. To enable explicit SSL
// on the connection to the IRC server, set Conn.SSL to true before calling
// Connect(). The port will default to 6697 if ssl is enabled, and 6667
// otherwise. You can also provide an optional connect password.
func (conn *Conn) Connect(host string, 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 conn.SSL {
if !hasPort(host) {
host += ":6697"
}
// It's unfortunate that tls.Dial doesn't allow a tls.Config arg,
// so we simply replicate it here with the correct Config.
// http://codereview.appspot.com/2883041
if s, err := net.Dial("tcp", "", host); err == nil {
// Passing nil config => certs are validated.
c := tls.Client(s, conn.SSLConfig)
if err = c.Handshake(); err == nil {
conn.sock = c
} else {
s.Close()
return err
}
} else {
return err
}
} else {
if !hasPort(host) {
host += ":6667"
}
if s, err := net.Dial("tcp", "", host); err == nil {
conn.sock = s
} else {
return err
}
}
conn.Host = host
conn.io = bufio.NewReadWriter(
bufio.NewReader(conn.sock),
bufio.NewWriter(conn.sock))
go conn.send()
go conn.recv()
if len(pass) > 0 {
conn.Pass(pass[0])
}
conn.Nick(conn.Me.Nick)
conn.User(conn.Me.Ident, conn.Me.Name)
go conn.runLoop()
return nil
}
// dispatch a nicely formatted os.Error to the error channel
func (conn *Conn) error(s string, a ...interface{}) {
conn.Err <- os.NewError(fmt.Sprintf(s, a...))
}
// copied from http.client for great justice
func hasPort(s string) bool {
return strings.LastIndex(s, ":") > strings.LastIndex(s, "]")
}
// dispatch input from channel as \r\n terminated line to peer
// flood controlled using hybrid's algorithm if conn.Flood is true
func (conn *Conn) send() {
lastsent := time.Nanoseconds()
var badness, linetime, second int64 = 0, 0, 1000000000
for line := range conn.out {
// Hybrid's algorithm allows for 2 seconds per line and an additional
// 1/120 of a second per character on that line.
linetime = 2*second + int64(len(line))*second/120
if !conn.Flood && conn.connected {
// No point in tallying up flood protection stuff until connected
if badness += linetime + lastsent - time.Nanoseconds(); badness < 0 {
// negative badness times are badness...
badness = int64(0)
}
}
lastsent = time.Nanoseconds()
// If we've sent more than 10 second's worth of lines according to the
// calculation above, then we're at risk of "Excess Flood".
if badness > 10*second && !conn.Flood {
// so sleep for the current line's time value before sending it
time.Sleep(linetime)
}
if _, err := conn.io.WriteString(line + "\r\n"); err != nil {
conn.error("irc.send(): %s", err.String())
conn.shutdown()
break
}
conn.io.Flush()
if conn.Debug {
fmt.Println("-> " + line)
}
}
}
// receive one \r\n terminated line from peer, parse and dispatch it
func (conn *Conn) recv() {
for {
s, err := conn.io.ReadString('\n')
if err != nil {
conn.error("irc.recv(): %s", err.String())
conn.shutdown()
break
}
s = strings.Trim(s, "\r\n")
if conn.Debug {
fmt.Println("<- " + s)
}
line := &Line{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)]
conn.in <- line
continue
}
// 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.Ident = 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.Fields(args[0])
line.Cmd = strings.ToUpper(args[0])
if len(args) > 1 {
line.Args = args[1:len(args)]
// some servers (Gamesurge) don't use : properly
// so duplicate Args[0] into Text
if line.Text == "" {
line.Text = args[len(args)-1]
}
} else {
// now duplicate Text into Args[0] if no args
line.Args = []string{line.Text}
}
conn.in <- line
}
}
func (conn *Conn) runLoop() {
for line := range conn.in {
conn.dispatchEvent(line)
}
}
func (conn *Conn) shutdown() {
close(conn.in)
close(conn.out)
close(conn.Err)
conn.connected = false
conn.sock.Close()
// reinit datastructures ready for next connection
// do this here rather than after runLoop()'s for due to race
conn.initialise()
}
// Dumps a load of information about the current state of the connection to a
// string for debugging state tracking and other such things.
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
}

369
client/handlers.go Normal file
View file

@ -0,0 +1,369 @@
package client
// this file contains the basic set of event handlers
// to manage tracking an irc connection etc.
import "strings"
// AddHandler() adds an event handler for a specific IRC command.
//
// Handlers take the form of an anonymous function (currently):
// func(conn *irc.Conn, line *irc.Line) {
// // handler code here
// }
//
// Handlers are triggered on incoming Lines from the server, with the handler
// "name" being equivalent to Line.Cmd. Read the RFCs for details on what
// replies could come from the server. They'll generally be things like
// "PRIVMSG", "JOIN", etc. but all the numeric replies are left as ascii
// strings of digits like "332" (mainly because I really didn't feel like
// putting massive constant tables in).
func (conn *Conn) AddHandler(name string, f func(*Conn, *Line)) {
n := strings.ToUpper(name)
if e, ok := conn.events[n]; ok {
conn.events[n] = append(e, f)
} else {
e := make([]func(*Conn, *Line), 1, 10)
e[0] = f
conn.events[n] = e
}
}
// loops through all event handlers for line.Cmd, running each in a goroutine
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
// 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)
}
}
}
// Basic ping/pong handler
func (conn *Conn) h_PING(line *Line) {
conn.Raw("PONG :" + line.Text)
}
// Handler to trigger a "CONNECTED" event on receipt of numeric 001
func (conn *Conn) h_001(line *Line) {
// we're connected!
conn.connected = true
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)]
}
}
}
// XXX: do we need 005 protocol support message parsing here?
// probably in the future, but I can't quite be arsed yet.
/*
: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"
func (conn *Conn) h_433(line *Line) {
// Args[1] is the new nick we were attempting to acquire
conn.Nick(line.Args[1] + "_")
// if this is happening before we're properly connected (i.e. the nick
// we sent in the initial NICK command is in use) we will not receive
// a NICK message to confirm our change of nick, so ReNick here...
if !conn.connected && line.Args[1] == conn.Me.Nick {
conn.Me.ReNick(line.Args[1] + "_")
}
}
// Handler NICK messages to inform us about nick changes
func (conn *Conn) h_NICK(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
func (conn *Conn) h_CTCP(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
func (conn *Conn) h_JOIN(line *Line) {
// Some IRCds (ircu) send ':n!u@h JOIN #chan' not ':n!u@h JOIN :#chan'
// Unfortunately the RFCs aren't specific about this. In fact the
// examples indicate no colon should be sent, but it's unusual.
var chname string
if len(line.Text) > 0 {
chname = line.Text
} else if len(line.Args) > 0 {
chname = line.Args[0]
}
ch := conn.GetChannel(chname)
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(chname)
// 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)
// sending a WHO for the channel is MUCH more efficient than
// triggering a WHOIS on every nick from the 353 handler
conn.Who(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.Who(n.Nick)
}
// this takes care of both nick and channel linking \o/
ch.AddNick(n)
}
// Handle PARTs from channels to maintain state
func (conn *Conn) h_PART(line *Line) {
// Some IRCds (ircu) send 'PART :#chan' when there's no part message
// instead of 'PART #chan'. This is *questionable* behaviour...
var chname string
if len(line.Args) > 0 {
chname = line.Args[0]
} else if len(line.Text) > 0 {
chname = line.Text
}
ch := conn.GetChannel(chname)
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", chname, line.Nick)
}
}
// Handle KICKs from channels to maintain state
func (conn *Conn) h_KICK(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
func (conn *Conn) h_QUIT(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)
func (conn *Conn) h_MODE(line *Line) {
// channel modes first
if ch := conn.GetChannel(line.Args[0]); ch != nil {
conn.ParseChannelModes(ch, line.Args[1], line.Args[2:len(line.Args)])
} 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.Text, n.Nick)
return
}
conn.ParseNickModes(n, line.Text)
} else {
if line.Text != "" {
conn.error("irc.MODE(): buh? not sure what to do with nick MODE %s %s", line.Args[0], line.Text)
} else {
conn.error("irc.MODE(): buh? not sure what to do with chan MODE %s", strings.Join(line.Args, " "))
}
}
}
// Handle TOPIC changes for channels
func (conn *Conn) h_TOPIC(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
func (conn *Conn) h_311(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
func (conn *Conn) h_324(line *Line) {
// XXX: copypasta from MODE, needs tidying.
if ch := conn.GetChannel(line.Args[1]); ch != nil {
conn.ParseChannelModes(ch, line.Args[2], line.Args[3:len(line.Args)])
} else {
conn.error("irc.324(): buh? received MODE settings for unknown channel %s", line.Args[1])
}
}
// Handle 332 topic reply on join to channel
func (conn *Conn) h_332(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 352 who reply
func (conn *Conn) h_352(line *Line) {
if n := conn.GetNick(line.Args[5]); n != nil {
n.Ident = line.Args[2]
n.Host = line.Args[3]
// XXX: do we care about the actual server the nick is on?
// or the hop count to this server?
// line.Text contains "<hop count> <real name>"
a := strings.Split(line.Text, " ", 2)
n.Name = a[1]
if idx := strings.Index(line.Args[6], "*"); idx != -1 {
n.Modes.Oper = true
}
if idx := strings.Index(line.Args[6], "H"); idx != -1 {
n.Modes.Invisible = true
}
} else {
conn.error("irc.352(): buh? got WHO reply for unknown nick %s", line.Args[5])
}
}
// Handle 353 names reply
func (conn *Conn) h_353(line *Line) {
if ch := conn.GetChannel(line.Args[2]); ch != nil {
nicks := strings.Split(line.Text, " ", -1)
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, "", "", "")
}
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)
func (conn *Conn) h_671(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])
}
}
// sets up the internal event handlers to do useful things with lines
func (conn *Conn) setupEvents() {
conn.events = make(map[string][]func(*Conn, *Line))
conn.AddHandler("CTCP", (*Conn).h_CTCP)
conn.AddHandler("JOIN", (*Conn).h_JOIN)
conn.AddHandler("KICK", (*Conn).h_KICK)
conn.AddHandler("MODE", (*Conn).h_MODE)
conn.AddHandler("NICK", (*Conn).h_NICK)
conn.AddHandler("PART", (*Conn).h_PART)
conn.AddHandler("PING", (*Conn).h_PING)
conn.AddHandler("QUIT", (*Conn).h_QUIT)
conn.AddHandler("TOPIC", (*Conn).h_TOPIC)
conn.AddHandler("001", (*Conn).h_001)
conn.AddHandler("311", (*Conn).h_311)
conn.AddHandler("324", (*Conn).h_324)
conn.AddHandler("332", (*Conn).h_332)
conn.AddHandler("352", (*Conn).h_352)
conn.AddHandler("353", (*Conn).h_353)
conn.AddHandler("433", (*Conn).h_433)
conn.AddHandler("671", (*Conn).h_671)
}

14
client/irc_test.go Normal file
View file

@ -0,0 +1,14 @@
package client
import (
"testing"
)
// Not really sure what or how to test something that basically requires a
// connection to an IRC server to function, but we need some tests or when this
// is present in the go package tree builds fail hard :-(
func TestIRC(t *testing.T) {
if c := New("test", "test", "Testing IRC"); c == nil {
t.FailNow()
}
}

507
client/nickchan.go Normal file
View file

@ -0,0 +1,507 @@
package client
// Here you'll find the Channel and Nick structs
// as well as the internal state maintenance code for the handlers
import (
"fmt"
"reflect"
"strconv"
"strings"
)
// A struct representing an IRC channel
type Channel struct {
Name, Topic string
Modes *ChanMode
Nicks map[*Nick]*ChanPrivs
Bans map[string]string
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 of how this is maintained.
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
\******************************************************************************/
// Creates a new *irc.Nick, initialises it, and stores it in *irc.Conn so it
// can be properly tracked for state management purposes.
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
}
// Returns an *irc.Nick for the nick n, if we're tracking it.
func (conn *Conn) GetNick(n string) *Nick {
if nick, ok := conn.nicks[n]; ok {
return nick
}
return nil
}
// Creates a new *irc.Channel, initialises it, and stores it in *irc.Conn so it
// can be properly tracked for state management purposes.
func (conn *Conn) NewChannel(c string) *Channel {
ch := &Channel{Name: c, conn: conn}
ch.initialise()
conn.chans[ch.Name] = ch
return ch
}
// Returns an *irc.Channel for the channel c, if we're tracking it.
func (conn *Conn) GetChannel(c string) *Channel {
if ch, ok := conn.chans[c]; ok {
return ch
}
return nil
}
// Parses mode strings for a channel
func (conn *Conn) ParseChannelModes(ch *Channel, modes string, modeargs []string) {
var modeop bool // true => add mode, false => remove mode
var modestr string
for i := 0; i < len(modes); i++ {
switch m := modes[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.ParseChanModes(): 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.ParseChanModes(): 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.ParseChanModes(): MODE %s %s%s %s: buh? state tracking failure.", ch.Name, modestr, m, modeargs[0])
}
} else {
conn.error("irc.ParseChanModes(): buh? not enough arguments to process MODE %s %s%s", ch.Name, modestr, m)
}
case 'b':
if len(modeargs) != 0 {
// we only care about host bans
if modeop && strings.HasPrefix(modeargs[0], "*!*@") {
for n, _ := range ch.Nicks {
if modeargs[0][4:] == n.Host {
ch.AddBan(n.Nick, modeargs[0])
}
}
} else if !modeop {
ch.DeleteBan(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)
}
}
}
}
// Parse mode strings for a nick
func (conn *Conn) ParseNickModes(n *Nick, modes string) {
var modeop bool // true => add mode, false => remove mode
for i := 0; i < len(modes); i++ {
switch m := modes[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
}
}
}
/******************************************************************************\
* Channel methods for state management
\******************************************************************************/
func (ch *Channel) initialise() {
ch.Modes = new(ChanMode)
ch.Nicks = make(map[*Nick]*ChanPrivs)
ch.Bans = make(map[string]string)
}
// Associates an *irc.Nick with an *irc.Channel using a shared *irc.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)
}
}
// Disassociates an *irc.Nick from an *irc.Channel. Will call ch.Delete() if
// the *irc.Nick being removed is the connection's nick. Will also call
// n.DelChannel(ch) to remove the association from the perspective of *irc.Nick.
func (ch *Channel) DelNick(n *Nick) {
if _, ok := ch.Nicks[n]; ok {
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) AddBan(nick, ban string) {
ch.Bans[nick] = ban
}
func (ch *Channel) DeleteBan(ban string) {
for n, b := range ch.Bans {
if b == ban {
ch.Bans[n] = "", false // see go issue 1249
}
}
}
// Stops the channel from being tracked by state tracking handlers. Also calls
// n.DelChannel(ch) for all nicks that are associated with the channel.
func (ch *Channel) Delete() {
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)
}
// Associates an *irc.Channel with an *irc.Nick using a shared *irc.ChanPrivs
//
// Very slightly different to irc.Channel.AddNick() in that it tests for a
// pre-existing association within the *irc.Nick object rather than the
// *irc.Channel object before associating the two.
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)
}
}
// Disassociates an *irc.Channel from an *irc.Nick. Will call n.Delete() if
// the *irc.Nick is no longer on any channels we are tracking. Will also call
// ch.DelNick(n) to remove the association from the perspective of *irc.Channel.
func (n *Nick) DelChannel(ch *Channel) {
if _, ok := n.Channels[ch]; ok {
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()
}
}
}
// Signals to the tracking code that the *irc.Nick object should be tracked
// under a "neu" nick rather than the old one.
func (n *Nick) ReNick(neu string) {
n.conn.nicks[n.Nick] = nil, false
n.Nick = neu
n.conn.nicks[n.Nick] = n
}
// Stops the nick from being tracked by state tracking handlers. Also calls
// ch.DelNick(n) for all nicks that are associated with the channel.
func (n *Nick) Delete() {
// we don't ever want to remove *our* nick from conn.nicks...
if n != n.conn.Me {
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.
\******************************************************************************/
// Map *irc.ChanMode fields to IRC mode characters
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",
}
// Map *irc.NickMode fields to IRC mode characters
var NickModeToString = map[string]string{
"Invisible": "i",
"Oper": "o",
"WallOps": "w",
"HiddenHost": "x",
"SSL": "z",
}
// Map *irc.ChanPrivs fields to IRC mode characters
var ChanPrivToString = map[string]string{
"Owner": "q",
"Admin": "a",
"Op": "o",
"HalfOp": "h",
"Voice": "v",
}
// Map *irc.ChanPrivs fields to the symbols used to represent these modes
// in NAMES and WHOIS responses
var ChanPrivToModeChar = map[string]byte{
"Owner": '~',
"Admin": '&',
"Op": '@',
"HalfOp": '%',
"Voice": '+',
}
// Reverse mappings of the above datastructures
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
}
}
// Returns a string representing the channel. Looks like:
// Channel: <channel name> e.g. #moo
// Topic: <channel topic> e.g. Discussing the merits of cows!
// Mode: <channel modes> e.g. +nsti
// Nicks:
// <nick>: <privs> e.g. CowMaster: +o
// ...
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
}
// Returns a string representing the nick. Looks like:
// Nick: <nick name> e.g. CowMaster
// Hostmask: <ident@host> e.g. moo@cows.org
// Real Name: <real name> e.g. Steve "CowMaster" Bush
// Modes: <nick modes> e.g. +z
// Channels:
// <channel>: <privs> e.g. #moo: +o
// ...
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
}
// Returns a string representing the channel modes. Looks like:
// +npk key
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", f.Get())
}
}
}
for _, s := range a {
if s != "" {
str += " " + s
}
}
if str == "+" {
str = "No modes set"
}
return str
}
// Returns a string representing the nick modes. Looks like:
// +iwx
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
}
// Returns a string representing the channel privileges. Looks like:
// +o
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
}