mirror of
https://github.com/fluffle/goirc
synced 2025-05-12 18:44:50 +00:00
commit
d9267067cd
6 changed files with 6 additions and 6 deletions
14
client/Makefile
Normal file
14
client/Makefile
Normal 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
140
client/commands.go
Normal 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
295
client/connection.go
Normal 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
369
client/handlers.go
Normal 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
14
client/irc_test.go
Normal 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
507
client/nickchan.go
Normal 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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue