govendor
This commit is contained in:
303
vendor/github.com/fluffle/goirc/client/commands.go
generated
vendored
Normal file
303
vendor/github.com/fluffle/goirc/client/commands.go
generated
vendored
Normal file
@@ -0,0 +1,303 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
REGISTER = "REGISTER"
|
||||
CONNECTED = "CONNECTED"
|
||||
DISCONNECTED = "DISCONNECTED"
|
||||
ACTION = "ACTION"
|
||||
AWAY = "AWAY"
|
||||
CAP = "CAP"
|
||||
CTCP = "CTCP"
|
||||
CTCPREPLY = "CTCPREPLY"
|
||||
INVITE = "INVITE"
|
||||
JOIN = "JOIN"
|
||||
KICK = "KICK"
|
||||
MODE = "MODE"
|
||||
NICK = "NICK"
|
||||
NOTICE = "NOTICE"
|
||||
OPER = "OPER"
|
||||
PART = "PART"
|
||||
PASS = "PASS"
|
||||
PING = "PING"
|
||||
PONG = "PONG"
|
||||
PRIVMSG = "PRIVMSG"
|
||||
QUIT = "QUIT"
|
||||
TOPIC = "TOPIC"
|
||||
USER = "USER"
|
||||
VERSION = "VERSION"
|
||||
VHOST = "VHOST"
|
||||
WHO = "WHO"
|
||||
WHOIS = "WHOIS"
|
||||
defaultSplit = 450
|
||||
)
|
||||
|
||||
// cutNewLines() pares down a string to the part before the first "\r" or "\n".
|
||||
func cutNewLines(s string) string {
|
||||
r := strings.SplitN(s, "\r", 2)
|
||||
r = strings.SplitN(r[0], "\n", 2)
|
||||
return r[0]
|
||||
}
|
||||
|
||||
// indexFragment looks for the last sentence split-point (defined as one of
|
||||
// the punctuation characters .:;,!?"' followed by a space) in the string s
|
||||
// and returns the index in the string after that split-point. If no split-
|
||||
// point is found it returns the index after the last space in s, or -1.
|
||||
func indexFragment(s string) int {
|
||||
max := -1
|
||||
for _, sep := range []string{". ", ": ", "; ", ", ", "! ", "? ", "\" ", "' "} {
|
||||
if idx := strings.LastIndex(s, sep); idx > max {
|
||||
max = idx
|
||||
}
|
||||
}
|
||||
if max > 0 {
|
||||
return max + 2
|
||||
}
|
||||
if idx := strings.LastIndex(s, " "); idx > 0 {
|
||||
return idx + 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// splitMessage splits a message > splitLen chars at:
|
||||
// 1. the end of the last sentence fragment before splitLen
|
||||
// 2. the end of the last word before splitLen
|
||||
// 3. splitLen itself
|
||||
func splitMessage(msg string, splitLen int) (msgs []string) {
|
||||
// This is quite short ;-)
|
||||
if splitLen < 13 {
|
||||
splitLen = defaultSplit
|
||||
}
|
||||
for len(msg) > splitLen {
|
||||
idx := indexFragment(msg[:splitLen-3])
|
||||
if idx < 0 {
|
||||
idx = splitLen - 3
|
||||
}
|
||||
msgs = append(msgs, msg[:idx]+"...")
|
||||
msg = msg[idx:]
|
||||
}
|
||||
return append(msgs, msg)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Avoid command injection by enforcing one command per line.
|
||||
conn.out <- cutNewLines(rawline)
|
||||
}
|
||||
|
||||
// Pass sends a PASS command to the server.
|
||||
// PASS password
|
||||
func (conn *Conn) Pass(password string) { conn.Raw(PASS + " " + password) }
|
||||
|
||||
// Nick sends a NICK command to the server.
|
||||
// NICK nick
|
||||
func (conn *Conn) Nick(nick string) { conn.Raw(NICK + " " + nick) }
|
||||
|
||||
// User sends a USER command to the server.
|
||||
// USER ident 12 * :name
|
||||
func (conn *Conn) User(ident, name string) {
|
||||
conn.Raw(USER + " " + ident + " 12 * :" + name)
|
||||
}
|
||||
|
||||
// Join sends a JOIN command to the server with an optional key.
|
||||
// JOIN channel [key]
|
||||
func (conn *Conn) Join(channel string, key ...string) {
|
||||
k := ""
|
||||
if len(key) > 0 {
|
||||
k = " " + key[0]
|
||||
}
|
||||
conn.Raw(JOIN + " " + channel + k)
|
||||
}
|
||||
|
||||
// Part sends a PART command to the server with an optional part message.
|
||||
// PART channel [:message]
|
||||
func (conn *Conn) Part(channel string, message ...string) {
|
||||
msg := strings.Join(message, " ")
|
||||
if msg != "" {
|
||||
msg = " :" + msg
|
||||
}
|
||||
conn.Raw(PART + " " + channel + msg)
|
||||
}
|
||||
|
||||
// Kick sends a KICK command to remove a nick from a channel.
|
||||
// KICK channel nick [:message]
|
||||
func (conn *Conn) Kick(channel, nick string, message ...string) {
|
||||
msg := strings.Join(message, " ")
|
||||
if msg != "" {
|
||||
msg = " :" + msg
|
||||
}
|
||||
conn.Raw(KICK + " " + channel + " " + nick + msg)
|
||||
}
|
||||
|
||||
// Quit sends a QUIT command to the server with an optional quit message.
|
||||
// QUIT [:message]
|
||||
func (conn *Conn) Quit(message ...string) {
|
||||
msg := strings.Join(message, " ")
|
||||
if msg == "" {
|
||||
msg = conn.cfg.QuitMessage
|
||||
}
|
||||
conn.Raw(QUIT + " :" + msg)
|
||||
}
|
||||
|
||||
// Whois sends a WHOIS command to the server.
|
||||
// WHOIS nick
|
||||
func (conn *Conn) Whois(nick string) { conn.Raw(WHOIS + " " + nick) }
|
||||
|
||||
// Who sends a WHO command to the server.
|
||||
// WHO nick
|
||||
func (conn *Conn) Who(nick string) { conn.Raw(WHO + " " + nick) }
|
||||
|
||||
// Privmsg sends a PRIVMSG to the target nick or channel t.
|
||||
// If msg is longer than Config.SplitLen characters, multiple PRIVMSGs
|
||||
// will be sent to the target containing sequential parts of msg.
|
||||
// PRIVMSG t :msg
|
||||
func (conn *Conn) Privmsg(t, msg string) {
|
||||
prefix := PRIVMSG + " " + t + " :"
|
||||
for _, s := range splitMessage(msg, conn.cfg.SplitLen) {
|
||||
conn.Raw(prefix + s)
|
||||
}
|
||||
}
|
||||
|
||||
// Privmsgln is the variadic version of Privmsg that formats the message
|
||||
// that is sent to the target nick or channel t using the
|
||||
// fmt.Sprintln function.
|
||||
// Note: Privmsgln doesn't add the '\n' character at the end of the message.
|
||||
func (conn *Conn) Privmsgln(t string, a ...interface{}) {
|
||||
msg := fmt.Sprintln(a...)
|
||||
// trimming the new-line character added by the fmt.Sprintln function,
|
||||
// since it's irrelevant.
|
||||
msg = msg[:len(msg)-1]
|
||||
conn.Privmsg(t, msg)
|
||||
}
|
||||
|
||||
// Privmsgf is the variadic version of Privmsg that formats the message
|
||||
// that is sent to the target nick or channel t using the
|
||||
// fmt.Sprintf function.
|
||||
func (conn *Conn) Privmsgf(t, format string, a ...interface{}) {
|
||||
msg := fmt.Sprintf(format, a...)
|
||||
conn.Privmsg(t, msg)
|
||||
}
|
||||
|
||||
// Notice sends a NOTICE to the target nick or channel t.
|
||||
// If msg is longer than Config.SplitLen characters, multiple NOTICEs
|
||||
// will be sent to the target containing sequential parts of msg.
|
||||
// NOTICE t :msg
|
||||
func (conn *Conn) Notice(t, msg string) {
|
||||
for _, s := range splitMessage(msg, conn.cfg.SplitLen) {
|
||||
conn.Raw(NOTICE + " " + t + " :" + s)
|
||||
}
|
||||
}
|
||||
|
||||
// Ctcp sends a (generic) CTCP message to the target nick
|
||||
// or channel t, with an optional argument.
|
||||
// PRIVMSG t :\001CTCP arg\001
|
||||
func (conn *Conn) Ctcp(t, ctcp string, arg ...string) {
|
||||
// We need to split again here to ensure
|
||||
for _, s := range splitMessage(strings.Join(arg, " "), conn.cfg.SplitLen) {
|
||||
if s != "" {
|
||||
s = " " + s
|
||||
}
|
||||
// Using Raw rather than PRIVMSG here to avoid double-split problems.
|
||||
conn.Raw(PRIVMSG + " " + t + " :\001" + strings.ToUpper(ctcp) + s + "\001")
|
||||
}
|
||||
}
|
||||
|
||||
// CtcpReply sends a (generic) CTCP reply to the target nick
|
||||
// or channel t, with an optional argument.
|
||||
// NOTICE t :\001CTCP arg\001
|
||||
func (conn *Conn) CtcpReply(t, ctcp string, arg ...string) {
|
||||
for _, s := range splitMessage(strings.Join(arg, " "), conn.cfg.SplitLen) {
|
||||
if s != "" {
|
||||
s = " " + s
|
||||
}
|
||||
// Using Raw rather than NOTICE here to avoid double-split problems.
|
||||
conn.Raw(NOTICE + " " + t + " :\001" + strings.ToUpper(ctcp) + s + "\001")
|
||||
}
|
||||
}
|
||||
|
||||
// Version sends a CTCP "VERSION" to the target nick or channel t.
|
||||
func (conn *Conn) Version(t string) { conn.Ctcp(t, VERSION) }
|
||||
|
||||
// Action sends a CTCP "ACTION" to the target nick or channel t.
|
||||
func (conn *Conn) Action(t, msg string) { conn.Ctcp(t, ACTION, msg) }
|
||||
|
||||
// Topic() sends a TOPIC command for a channel.
|
||||
// If no topic is provided this requests that a 332 response is sent by the
|
||||
// server for that channel, which can then be handled to retrieve the current
|
||||
// channel topic. If a topic is provided the channel's topic will be set.
|
||||
// TOPIC channel
|
||||
// TOPIC channel :topic
|
||||
func (conn *Conn) Topic(channel string, topic ...string) {
|
||||
t := strings.Join(topic, " ")
|
||||
if t != "" {
|
||||
t = " :" + t
|
||||
}
|
||||
conn.Raw(TOPIC + " " + channel + t)
|
||||
}
|
||||
|
||||
// Mode sends a MODE command for a target nick or channel t.
|
||||
// If no mode strings are provided this requests that a 324 response is sent
|
||||
// by the server for the target. Otherwise the mode strings are concatenated
|
||||
// with spaces and sent to the server. This allows e.g.
|
||||
// conn.Mode("#channel", "+nsk", "mykey")
|
||||
//
|
||||
// MODE t
|
||||
// MODE t modestring
|
||||
func (conn *Conn) Mode(t string, modestring ...string) {
|
||||
mode := strings.Join(modestring, " ")
|
||||
if mode != "" {
|
||||
mode = " " + mode
|
||||
}
|
||||
conn.Raw(MODE + " " + t + mode)
|
||||
}
|
||||
|
||||
// Away sends an AWAY command to the server.
|
||||
// If a message is provided it sets the client's away status with that message,
|
||||
// otherwise it resets the client's away status.
|
||||
// AWAY
|
||||
// AWAY :message
|
||||
func (conn *Conn) Away(message ...string) {
|
||||
msg := strings.Join(message, " ")
|
||||
if msg != "" {
|
||||
msg = " :" + msg
|
||||
}
|
||||
conn.Raw(AWAY + msg)
|
||||
}
|
||||
|
||||
// Invite sends an INVITE command to the server.
|
||||
// INVITE nick channel
|
||||
func (conn *Conn) Invite(nick, channel string) {
|
||||
conn.Raw(INVITE + " " + nick + " " + channel)
|
||||
}
|
||||
|
||||
// Oper sends an OPER command to the server.
|
||||
// OPER user pass
|
||||
func (conn *Conn) Oper(user, pass string) { conn.Raw(OPER + " " + user + " " + pass) }
|
||||
|
||||
// VHost sends a VHOST command to the server.
|
||||
// VHOST user pass
|
||||
func (conn *Conn) VHost(user, pass string) { conn.Raw(VHOST + " " + user + " " + pass) }
|
||||
|
||||
// Ping sends a PING command to the server, which should PONG.
|
||||
// PING :message
|
||||
func (conn *Conn) Ping(message string) { conn.Raw(PING + " :" + message) }
|
||||
|
||||
// Pong sends a PONG command to the server.
|
||||
// PONG :message
|
||||
func (conn *Conn) Pong(message string) { conn.Raw(PONG + " :" + message) }
|
||||
|
||||
// Cap sends a CAP command to the server.
|
||||
// CAP subcommand
|
||||
// CAP subcommand :message
|
||||
func (conn *Conn) Cap(subcommmand string, capabilities ...string) {
|
||||
if len(capabilities) == 0 {
|
||||
conn.Raw(CAP + " " + subcommmand)
|
||||
} else {
|
||||
conn.Raw(CAP + " " + subcommmand + " :" + strings.Join(capabilities, " "))
|
||||
}
|
||||
}
|
570
vendor/github.com/fluffle/goirc/client/connection.go
generated
vendored
Normal file
570
vendor/github.com/fluffle/goirc/client/connection.go
generated
vendored
Normal file
@@ -0,0 +1,570 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fluffle/goirc/logging"
|
||||
"github.com/fluffle/goirc/state"
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
// Conn encapsulates a connection to a single IRC server. Create
|
||||
// one with Client or SimpleClient.
|
||||
type Conn struct {
|
||||
// For preventing races on (dis)connect.
|
||||
mu sync.RWMutex
|
||||
|
||||
// Contains parameters that people can tweak to change client behaviour.
|
||||
cfg *Config
|
||||
|
||||
// Handlers
|
||||
intHandlers *hSet
|
||||
fgHandlers *hSet
|
||||
bgHandlers *hSet
|
||||
|
||||
// State tracker for nicks and channels
|
||||
st state.Tracker
|
||||
stRemovers []Remover
|
||||
|
||||
// I/O stuff to server
|
||||
dialer *net.Dialer
|
||||
proxyDialer proxy.Dialer
|
||||
sock net.Conn
|
||||
io *bufio.ReadWriter
|
||||
in chan *Line
|
||||
out chan string
|
||||
connected bool
|
||||
|
||||
// Control channel and WaitGroup for goroutines
|
||||
die chan struct{}
|
||||
wg sync.WaitGroup
|
||||
|
||||
// Internal counters for flood protection
|
||||
badness time.Duration
|
||||
lastsent time.Time
|
||||
}
|
||||
|
||||
// Config contains options that can be passed to Client to change the
|
||||
// behaviour of the library during use. It is recommended that NewConfig
|
||||
// is used to create this struct rather than instantiating one directly.
|
||||
// Passing a Config with no Nick in the Me field to Client will result
|
||||
// in unflattering consequences.
|
||||
type Config struct {
|
||||
// Set this to provide the Nick, Ident and Name for the client to use.
|
||||
// It is recommended to call Conn.Me to get up-to-date information
|
||||
// about the current state of the client's IRC nick after connecting.
|
||||
Me *state.Nick
|
||||
|
||||
// Hostname to connect to and optional connect password.
|
||||
// Changing these after connection will have no effect until the
|
||||
// client reconnects.
|
||||
Server, Pass string
|
||||
|
||||
// Are we connecting via SSL? Do we care about certificate validity?
|
||||
// Changing these after connection will have no effect until the
|
||||
// client reconnects.
|
||||
SSL bool
|
||||
SSLConfig *tls.Config
|
||||
|
||||
// To connect via proxy set the proxy url here.
|
||||
// Changing these after connection will have no effect until the
|
||||
// client reconnects.
|
||||
Proxy string
|
||||
|
||||
// Local address to bind to when connecting to the server.
|
||||
LocalAddr string
|
||||
|
||||
// Replaceable function to customise the 433 handler's new nick.
|
||||
// By default an underscore "_" is appended to the current nick.
|
||||
NewNick func(string) string
|
||||
|
||||
// Client->server ping frequency, in seconds. Defaults to 3m.
|
||||
// Set to 0 to disable client-side pings.
|
||||
PingFreq time.Duration
|
||||
|
||||
// The duration before a connection timeout is triggered. Defaults to 1m.
|
||||
// Set to 0 to wait indefinitely.
|
||||
Timeout time.Duration
|
||||
|
||||
// Set this to true to disable flood protection and false to re-enable.
|
||||
Flood bool
|
||||
|
||||
// Sent as the reply to a CTCP VERSION message.
|
||||
Version string
|
||||
|
||||
// Sent as the default QUIT message if Quit is called with no args.
|
||||
QuitMessage string
|
||||
|
||||
// Configurable panic recovery for all handlers.
|
||||
// Defaults to logging an error, see LogPanic.
|
||||
Recover func(*Conn, *Line)
|
||||
|
||||
// Split PRIVMSGs, NOTICEs and CTCPs longer than SplitLen characters
|
||||
// over multiple lines. Default to 450 if not set.
|
||||
SplitLen int
|
||||
}
|
||||
|
||||
// NewConfig creates a Config struct containing sensible defaults.
|
||||
// It takes one required argument: the nick to use for the client.
|
||||
// Subsequent string arguments set the client's ident and "real"
|
||||
// name, but these are optional.
|
||||
func NewConfig(nick string, args ...string) *Config {
|
||||
cfg := &Config{
|
||||
Me: &state.Nick{Nick: nick},
|
||||
PingFreq: 3 * time.Minute,
|
||||
NewNick: func(s string) string { return s + "_" },
|
||||
Recover: (*Conn).LogPanic, // in dispatch.go
|
||||
SplitLen: defaultSplit,
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
cfg.Me.Ident = "goirc"
|
||||
if len(args) > 0 && args[0] != "" {
|
||||
cfg.Me.Ident = args[0]
|
||||
}
|
||||
cfg.Me.Name = "Powered by GoIRC"
|
||||
if len(args) > 1 && args[1] != "" {
|
||||
cfg.Me.Name = args[1]
|
||||
}
|
||||
cfg.Version = "Powered by GoIRC"
|
||||
cfg.QuitMessage = "GoBye!"
|
||||
return cfg
|
||||
}
|
||||
|
||||
// SimpleClient creates a new Conn, passing its arguments to NewConfig.
|
||||
// If you don't need to change any client options and just want to get
|
||||
// started quickly, this is a convenient shortcut.
|
||||
func SimpleClient(nick string, args ...string) *Conn {
|
||||
conn := Client(NewConfig(nick, args...))
|
||||
return conn
|
||||
}
|
||||
|
||||
// Client takes a Config struct and returns a new Conn ready to have
|
||||
// handlers added and connect to a server.
|
||||
func Client(cfg *Config) *Conn {
|
||||
if cfg == nil {
|
||||
cfg = NewConfig("__idiot__")
|
||||
}
|
||||
if cfg.Me == nil || cfg.Me.Nick == "" || cfg.Me.Ident == "" {
|
||||
cfg.Me = &state.Nick{Nick: "__idiot__"}
|
||||
cfg.Me.Ident = "goirc"
|
||||
cfg.Me.Name = "Powered by GoIRC"
|
||||
}
|
||||
|
||||
dialer := new(net.Dialer)
|
||||
dialer.Timeout = cfg.Timeout
|
||||
if cfg.LocalAddr != "" {
|
||||
if !hasPort(cfg.LocalAddr) {
|
||||
cfg.LocalAddr += ":0"
|
||||
}
|
||||
|
||||
local, err := net.ResolveTCPAddr("tcp", cfg.LocalAddr)
|
||||
if err == nil {
|
||||
dialer.LocalAddr = local
|
||||
} else {
|
||||
logging.Error("irc.Client(): Cannot resolve local address %s: %s", cfg.LocalAddr, err)
|
||||
}
|
||||
}
|
||||
|
||||
conn := &Conn{
|
||||
cfg: cfg,
|
||||
dialer: dialer,
|
||||
intHandlers: handlerSet(),
|
||||
fgHandlers: handlerSet(),
|
||||
bgHandlers: handlerSet(),
|
||||
stRemovers: make([]Remover, 0, len(stHandlers)),
|
||||
lastsent: time.Now(),
|
||||
}
|
||||
conn.addIntHandlers()
|
||||
return conn
|
||||
}
|
||||
|
||||
// Connected returns true if the client is successfully connected to
|
||||
// an IRC server. It becomes true when the TCP connection is established,
|
||||
// and false again when the connection is closed.
|
||||
func (conn *Conn) Connected() bool {
|
||||
conn.mu.RLock()
|
||||
defer conn.mu.RUnlock()
|
||||
return conn.connected
|
||||
}
|
||||
|
||||
// Config returns a pointer to the Config struct used by the client.
|
||||
// Many of the elements of Config may be changed at any point to
|
||||
// affect client behaviour. To disable flood protection temporarily,
|
||||
// for example, a handler could do:
|
||||
//
|
||||
// conn.Config().Flood = true
|
||||
// // Send many lines to the IRC server, risking "excess flood"
|
||||
// conn.Config().Flood = false
|
||||
//
|
||||
func (conn *Conn) Config() *Config {
|
||||
return conn.cfg
|
||||
}
|
||||
|
||||
// Me returns a state.Nick that reflects the client's IRC nick at the
|
||||
// time it is called. If state tracking is enabled, this comes from
|
||||
// the tracker, otherwise it is equivalent to conn.cfg.Me.
|
||||
func (conn *Conn) Me() *state.Nick {
|
||||
if conn.st != nil {
|
||||
conn.cfg.Me = conn.st.Me()
|
||||
}
|
||||
return conn.cfg.Me
|
||||
}
|
||||
|
||||
// StateTracker returns the state tracker being used by the client,
|
||||
// if tracking is enabled, and nil otherwise.
|
||||
func (conn *Conn) StateTracker() state.Tracker {
|
||||
return conn.st
|
||||
}
|
||||
|
||||
// EnableStateTracking causes the client to track information about
|
||||
// all channels it is joined to, and all the nicks in those channels.
|
||||
// This can be rather handy for a number of bot-writing tasks. See
|
||||
// the state package for more details.
|
||||
//
|
||||
// NOTE: Calling this while connected to an IRC server may cause the
|
||||
// state tracker to become very confused all over STDERR if logging
|
||||
// is enabled. State tracking should enabled before connecting or
|
||||
// at a pinch while the client is not joined to any channels.
|
||||
func (conn *Conn) EnableStateTracking() {
|
||||
conn.mu.Lock()
|
||||
defer conn.mu.Unlock()
|
||||
if conn.st == nil {
|
||||
n := conn.cfg.Me
|
||||
conn.st = state.NewTracker(n.Nick)
|
||||
conn.st.NickInfo(n.Nick, n.Ident, n.Host, n.Name)
|
||||
conn.cfg.Me = conn.st.Me()
|
||||
conn.addSTHandlers()
|
||||
}
|
||||
}
|
||||
|
||||
// DisableStateTracking causes the client to stop tracking information
|
||||
// about the channels and nicks it knows of. It will also wipe current
|
||||
// state from the state tracker.
|
||||
func (conn *Conn) DisableStateTracking() {
|
||||
conn.mu.Lock()
|
||||
defer conn.mu.Unlock()
|
||||
if conn.st != nil {
|
||||
conn.cfg.Me = conn.st.Me()
|
||||
conn.delSTHandlers()
|
||||
conn.st.Wipe()
|
||||
conn.st = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Per-connection state initialisation.
|
||||
func (conn *Conn) initialise() {
|
||||
conn.io = nil
|
||||
conn.sock = nil
|
||||
conn.in = make(chan *Line, 32)
|
||||
conn.out = make(chan string, 32)
|
||||
conn.die = make(chan struct{})
|
||||
if conn.st != nil {
|
||||
conn.st.Wipe()
|
||||
}
|
||||
}
|
||||
|
||||
// ConnectTo connects the IRC client to "host[:port]", which should be either
|
||||
// a hostname or an IP address, with an optional port. It sets the client's
|
||||
// Config.Server to host, Config.Pass to pass if one is provided, and then
|
||||
// calls Connect.
|
||||
func (conn *Conn) ConnectTo(host string, pass ...string) error {
|
||||
conn.cfg.Server = host
|
||||
if len(pass) > 0 {
|
||||
conn.cfg.Pass = pass[0]
|
||||
}
|
||||
return conn.Connect()
|
||||
}
|
||||
|
||||
// Connect connects the IRC client to the server configured in Config.Server.
|
||||
// To enable explicit SSL on the connection to the IRC server, set Config.SSL
|
||||
// to true before calling Connect(). The port will default to 6697 if SSL is
|
||||
// enabled, and 6667 otherwise.
|
||||
// To enable connecting via a proxy server, set Config.Proxy to the proxy URL
|
||||
// (example socks5://localhost:9000) before calling Connect().
|
||||
//
|
||||
// Upon successful connection, Connected will return true and a REGISTER event
|
||||
// will be fired. This is mostly for internal use; it is suggested that a
|
||||
// handler for the CONNECTED event is used to perform any initial client work
|
||||
// like joining channels and sending messages.
|
||||
func (conn *Conn) Connect() error {
|
||||
conn.mu.Lock()
|
||||
defer conn.mu.Unlock()
|
||||
conn.initialise()
|
||||
|
||||
if conn.cfg.Server == "" {
|
||||
return fmt.Errorf("irc.Connect(): cfg.Server must be non-empty")
|
||||
}
|
||||
if conn.connected {
|
||||
return fmt.Errorf("irc.Connect(): Cannot connect to %s, already connected.", conn.cfg.Server)
|
||||
}
|
||||
|
||||
if !hasPort(conn.cfg.Server) {
|
||||
if conn.cfg.SSL {
|
||||
conn.cfg.Server = net.JoinHostPort(conn.cfg.Server, "6697")
|
||||
} else {
|
||||
conn.cfg.Server = net.JoinHostPort(conn.cfg.Server, "6667")
|
||||
}
|
||||
}
|
||||
|
||||
if conn.cfg.Proxy != "" {
|
||||
proxyURL, err := url.Parse(conn.cfg.Proxy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn.proxyDialer, err = proxy.FromURL(proxyURL, conn.dialer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server)
|
||||
if s, err := conn.proxyDialer.Dial("tcp", conn.cfg.Server); err == nil {
|
||||
conn.sock = s
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
logging.Info("irc.Connect(): Connecting to %s.", conn.cfg.Server)
|
||||
if s, err := conn.dialer.Dial("tcp", conn.cfg.Server); err == nil {
|
||||
conn.sock = s
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if conn.cfg.SSL {
|
||||
logging.Info("irc.Connect(): Performing SSL handshake.")
|
||||
s := tls.Client(conn.sock, conn.cfg.SSLConfig)
|
||||
if err := s.Handshake(); err != nil {
|
||||
return err
|
||||
}
|
||||
conn.sock = s
|
||||
}
|
||||
|
||||
conn.postConnect(true)
|
||||
conn.connected = true
|
||||
conn.dispatch(&Line{Cmd: REGISTER, Time: time.Now()})
|
||||
return nil
|
||||
}
|
||||
|
||||
// postConnect performs post-connection setup, for ease of testing.
|
||||
func (conn *Conn) postConnect(start bool) {
|
||||
conn.io = bufio.NewReadWriter(
|
||||
bufio.NewReader(conn.sock),
|
||||
bufio.NewWriter(conn.sock))
|
||||
if start {
|
||||
conn.wg.Add(3)
|
||||
go conn.send()
|
||||
go conn.recv()
|
||||
go conn.runLoop()
|
||||
if conn.cfg.PingFreq > 0 {
|
||||
conn.wg.Add(1)
|
||||
go conn.ping()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hasPort returns true if the string hostname has a :port suffix.
|
||||
// It was copied from net/http for great justice.
|
||||
func hasPort(s string) bool {
|
||||
return strings.LastIndex(s, ":") > strings.LastIndex(s, "]")
|
||||
}
|
||||
|
||||
// send is started as a goroutine after a connection is established.
|
||||
// It shuttles data from the output channel to write(), and is killed
|
||||
// when Conn.die is closed.
|
||||
func (conn *Conn) send() {
|
||||
for {
|
||||
select {
|
||||
case line := <-conn.out:
|
||||
if err := conn.write(line); err != nil {
|
||||
logging.Error("irc.send(): %s", err.Error())
|
||||
// We can't defer this, because Close() waits for it.
|
||||
conn.wg.Done()
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
case <-conn.die:
|
||||
// control channel closed, bail out
|
||||
conn.wg.Done()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// recv is started as a goroutine after a connection is established.
|
||||
// It receives "\r\n" terminated lines from the server, parses them into
|
||||
// Lines, and sends them to the input channel.
|
||||
func (conn *Conn) recv() {
|
||||
for {
|
||||
s, err := conn.io.ReadString('\n')
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
logging.Error("irc.recv(): %s", err.Error())
|
||||
}
|
||||
// We can't defer this, because Close() waits for it.
|
||||
conn.wg.Done()
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
s = strings.Trim(s, "\r\n")
|
||||
logging.Debug("<- %s", s)
|
||||
|
||||
if line := ParseLine(s); line != nil {
|
||||
line.Time = time.Now()
|
||||
conn.in <- line
|
||||
} else {
|
||||
logging.Warn("irc.recv(): problems parsing line:\n %s", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ping is started as a goroutine after a connection is established, as
|
||||
// long as Config.PingFreq >0. It pings the server every PingFreq seconds.
|
||||
func (conn *Conn) ping() {
|
||||
defer conn.wg.Done()
|
||||
tick := time.NewTicker(conn.cfg.PingFreq)
|
||||
for {
|
||||
select {
|
||||
case <-tick.C:
|
||||
conn.Ping(fmt.Sprintf("%d", time.Now().UnixNano()))
|
||||
case <-conn.die:
|
||||
// control channel closed, bail out
|
||||
tick.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runLoop is started as a goroutine after a connection is established.
|
||||
// It pulls Lines from the input channel and dispatches them to any
|
||||
// handlers that have been registered for that IRC verb.
|
||||
func (conn *Conn) runLoop() {
|
||||
defer conn.wg.Done()
|
||||
for {
|
||||
select {
|
||||
case line := <-conn.in:
|
||||
conn.dispatch(line)
|
||||
case <-conn.die:
|
||||
// control channel closed, bail out
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// write writes a \r\n terminated line of output to the connected server,
|
||||
// using Hybrid's algorithm to rate limit if conn.cfg.Flood is false.
|
||||
func (conn *Conn) write(line string) error {
|
||||
if !conn.cfg.Flood {
|
||||
if t := conn.rateLimit(len(line)); t != 0 {
|
||||
// sleep for the current line's time value before sending it
|
||||
logging.Info("irc.rateLimit(): Flood! Sleeping for %.2f secs.",
|
||||
t.Seconds())
|
||||
<-time.After(t)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := conn.io.WriteString(line + "\r\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := conn.io.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.HasPrefix(line, "PASS") {
|
||||
line = "PASS **************"
|
||||
}
|
||||
logging.Debug("-> %s", line)
|
||||
return nil
|
||||
}
|
||||
|
||||
// rateLimit implements Hybrid's flood control algorithm for outgoing lines.
|
||||
func (conn *Conn) rateLimit(chars int) time.Duration {
|
||||
// Hybrid's algorithm allows for 2 seconds per line and an additional
|
||||
// 1/120 of a second per character on that line.
|
||||
linetime := 2*time.Second + time.Duration(chars)*time.Second/120
|
||||
elapsed := time.Now().Sub(conn.lastsent)
|
||||
if conn.badness += linetime - elapsed; conn.badness < 0 {
|
||||
// negative badness times are badness...
|
||||
conn.badness = 0
|
||||
}
|
||||
conn.lastsent = time.Now()
|
||||
// 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 conn.badness > 10*time.Second {
|
||||
return linetime
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Close tears down all connection-related state. It is called when either
|
||||
// the sending or receiving goroutines encounter an error.
|
||||
// It may also be used to forcibly shut down the connection to the server.
|
||||
func (conn *Conn) Close() error {
|
||||
// Guard against double-call of Close() if we get an error in send()
|
||||
// as calling sock.Close() will cause recv() to receive EOF in readstring()
|
||||
conn.mu.Lock()
|
||||
if !conn.connected {
|
||||
conn.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
logging.Info("irc.Close(): Disconnected from server.")
|
||||
conn.connected = false
|
||||
err := conn.sock.Close()
|
||||
close(conn.die)
|
||||
// Drain both in and out channels to avoid a deadlock if the buffers
|
||||
// have filled. See TestSendDeadlockOnFullBuffer in connection_test.go.
|
||||
conn.drainIn()
|
||||
conn.drainOut()
|
||||
conn.wg.Wait()
|
||||
conn.mu.Unlock()
|
||||
// Dispatch after closing connection but before reinit
|
||||
// so event handlers can still access state information.
|
||||
conn.dispatch(&Line{Cmd: DISCONNECTED, Time: time.Now()})
|
||||
return err
|
||||
}
|
||||
|
||||
// drainIn sends all data buffered in conn.in to /dev/null.
|
||||
func (conn *Conn) drainIn() {
|
||||
for {
|
||||
select {
|
||||
case <-conn.in:
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// drainOut does the same for conn.out. Generics!
|
||||
func (conn *Conn) drainOut() {
|
||||
for {
|
||||
select {
|
||||
case <-conn.out:
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.cfg.Server + "\n\n"
|
||||
} else {
|
||||
str += "Not currently connected!\n\n"
|
||||
}
|
||||
str += conn.Me().String() + "\n"
|
||||
if conn.st != nil {
|
||||
str += conn.st.String() + "\n"
|
||||
}
|
||||
return str
|
||||
}
|
192
vendor/github.com/fluffle/goirc/client/dispatch.go
generated
vendored
Normal file
192
vendor/github.com/fluffle/goirc/client/dispatch.go
generated
vendored
Normal file
@@ -0,0 +1,192 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"github.com/fluffle/goirc/logging"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// 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).
|
||||
//
|
||||
// Foreground handlers have a guarantee of protocol consistency: all the
|
||||
// handlers for one event will have finished before the handlers for the
|
||||
// next start processing. They are run in parallel but block the event
|
||||
// loop, so care should be taken to ensure these handlers are quick :-)
|
||||
//
|
||||
// Background handlers are run in parallel and do not block the event loop.
|
||||
// This is useful for things that may need to do significant work.
|
||||
type Handler interface {
|
||||
Handle(*Conn, *Line)
|
||||
}
|
||||
|
||||
// Removers allow for a handler that has been previously added to the client
|
||||
// to be removed.
|
||||
type Remover interface {
|
||||
Remove()
|
||||
}
|
||||
|
||||
// HandlerFunc allows a bare function with this signature to implement the
|
||||
// Handler interface. It is used by Conn.HandleFunc.
|
||||
type HandlerFunc func(*Conn, *Line)
|
||||
|
||||
func (hf HandlerFunc) Handle(conn *Conn, line *Line) {
|
||||
hf(conn, line)
|
||||
}
|
||||
|
||||
// Handlers are organised using a map of linked-lists, with each map
|
||||
// key representing an IRC verb or numeric, and the linked list values
|
||||
// being handlers that are executed in parallel when a Line from the
|
||||
// server with that verb or numeric arrives.
|
||||
type hSet struct {
|
||||
set map[string]*hList
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
type hList struct {
|
||||
start, end *hNode
|
||||
}
|
||||
|
||||
// Storing the forward and backward links in the node allows O(1) removal.
|
||||
// This probably isn't strictly necessary but I think it's kinda nice.
|
||||
type hNode struct {
|
||||
next, prev *hNode
|
||||
set *hSet
|
||||
event string
|
||||
handler Handler
|
||||
}
|
||||
|
||||
// A hNode implements both Handler (with configurable panic recovery)...
|
||||
func (hn *hNode) Handle(conn *Conn, line *Line) {
|
||||
defer conn.cfg.Recover(conn, line)
|
||||
hn.handler.Handle(conn, line)
|
||||
}
|
||||
|
||||
// ... and Remover.
|
||||
func (hn *hNode) Remove() {
|
||||
hn.set.remove(hn)
|
||||
}
|
||||
|
||||
func handlerSet() *hSet {
|
||||
return &hSet{set: make(map[string]*hList)}
|
||||
}
|
||||
|
||||
// When a new Handler is added for an event, it is wrapped in a hNode and
|
||||
// returned as a Remover so the caller can remove it at a later time.
|
||||
func (hs *hSet) add(ev string, h Handler) Remover {
|
||||
hs.Lock()
|
||||
defer hs.Unlock()
|
||||
ev = strings.ToLower(ev)
|
||||
l, ok := hs.set[ev]
|
||||
if !ok {
|
||||
l = &hList{}
|
||||
}
|
||||
hn := &hNode{
|
||||
set: hs,
|
||||
event: ev,
|
||||
handler: h,
|
||||
}
|
||||
if !ok {
|
||||
l.start = hn
|
||||
} else {
|
||||
hn.prev = l.end
|
||||
l.end.next = hn
|
||||
}
|
||||
l.end = hn
|
||||
hs.set[ev] = l
|
||||
return hn
|
||||
}
|
||||
|
||||
func (hs *hSet) remove(hn *hNode) {
|
||||
hs.Lock()
|
||||
defer hs.Unlock()
|
||||
l, ok := hs.set[hn.event]
|
||||
if !ok {
|
||||
logging.Error("Removing node for unknown event '%s'", hn.event)
|
||||
return
|
||||
}
|
||||
if hn.next == nil {
|
||||
l.end = hn.prev
|
||||
} else {
|
||||
hn.next.prev = hn.prev
|
||||
}
|
||||
if hn.prev == nil {
|
||||
l.start = hn.next
|
||||
} else {
|
||||
hn.prev.next = hn.next
|
||||
}
|
||||
hn.next = nil
|
||||
hn.prev = nil
|
||||
hn.set = nil
|
||||
if l.start == nil || l.end == nil {
|
||||
delete(hs.set, hn.event)
|
||||
}
|
||||
}
|
||||
|
||||
func (hs *hSet) dispatch(conn *Conn, line *Line) {
|
||||
hs.RLock()
|
||||
defer hs.RUnlock()
|
||||
ev := strings.ToLower(line.Cmd)
|
||||
list, ok := hs.set[ev]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
wg := &sync.WaitGroup{}
|
||||
for hn := list.start; hn != nil; hn = hn.next {
|
||||
wg.Add(1)
|
||||
go func(hn *hNode) {
|
||||
hn.Handle(conn, line.Copy())
|
||||
wg.Done()
|
||||
}(hn)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Handle adds the provided handler to the foreground set for the named event.
|
||||
// It will return a Remover that allows that handler to be removed again.
|
||||
func (conn *Conn) Handle(name string, h Handler) Remover {
|
||||
return conn.fgHandlers.add(name, h)
|
||||
}
|
||||
|
||||
// HandleBG adds the provided handler to the background set for the named
|
||||
// event. It may go away in the future.
|
||||
// It will return a Remover that allows that handler to be removed again.
|
||||
func (conn *Conn) HandleBG(name string, h Handler) Remover {
|
||||
return conn.bgHandlers.add(name, h)
|
||||
}
|
||||
|
||||
func (conn *Conn) handle(name string, h Handler) Remover {
|
||||
return conn.intHandlers.add(name, h)
|
||||
}
|
||||
|
||||
// HandleFunc adds the provided function as a handler in the foreground set
|
||||
// for the named event.
|
||||
// It will return a Remover that allows that handler to be removed again.
|
||||
func (conn *Conn) HandleFunc(name string, hf HandlerFunc) Remover {
|
||||
return conn.Handle(name, hf)
|
||||
}
|
||||
|
||||
func (conn *Conn) dispatch(line *Line) {
|
||||
// We run the internal handlers first, including all state tracking ones.
|
||||
// This ensures that user-supplied handlers that use the tracker have a
|
||||
// consistent view of the connection state in handlers that mutate it.
|
||||
conn.intHandlers.dispatch(conn, line)
|
||||
go conn.bgHandlers.dispatch(conn, line)
|
||||
conn.fgHandlers.dispatch(conn, line)
|
||||
}
|
||||
|
||||
// LogPanic is used as the default panic catcher for the client. If, like me,
|
||||
// you are not good with computer, and you'd prefer your bot not to vanish into
|
||||
// the ether whenever you make unfortunate programming mistakes, you may find
|
||||
// this useful: it will recover panics from handler code and log the errors.
|
||||
func (conn *Conn) LogPanic(line *Line) {
|
||||
if err := recover(); err != nil {
|
||||
_, f, l, _ := runtime.Caller(2)
|
||||
logging.Error("%s:%d: panic: %v", f, l, err)
|
||||
}
|
||||
}
|
34
vendor/github.com/fluffle/goirc/client/doc.go
generated
vendored
Normal file
34
vendor/github.com/fluffle/goirc/client/doc.go
generated
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
// Package client implements an IRC client. It handles protocol basics
|
||||
// such as initial connection and responding to server PINGs, and has
|
||||
// optional state tracking support which will keep tabs on every nick
|
||||
// present in the same channels as the client. Other features include
|
||||
// SSL support, automatic splitting of long lines, and panic recovery
|
||||
// for handlers.
|
||||
//
|
||||
// Incoming IRC messages are parsed into client.Line structs and trigger
|
||||
// events based on the IRC verb (e.g. PRIVMSG) of the message. Handlers
|
||||
// for these events conform to the client.Handler interface; a HandlerFunc
|
||||
// type to wrap bare functions is provided a-la the net/http package.
|
||||
//
|
||||
// Creating a client, adding a handler and connecting to a server looks
|
||||
// soemthing like this, for the simple case:
|
||||
//
|
||||
// // Create a new client, which will connect with the nick "myNick"
|
||||
// irc := client.SimpleClient("myNick")
|
||||
//
|
||||
// // Add a handler that waits for the "disconnected" event and
|
||||
// // closes a channel to signal everything is done.
|
||||
// disconnected := make(chan struct{})
|
||||
// c.HandleFunc("disconnected", func(c *client.Conn, l *client.Line) {
|
||||
// close(disconnected)
|
||||
// })
|
||||
//
|
||||
// // Connect to an IRC server.
|
||||
// if err := c.ConnectTo("irc.freenode.net"); err != nil {
|
||||
// log.Fatalf("Connection error: %v\n", err)
|
||||
// }
|
||||
//
|
||||
// // Wait for disconnection.
|
||||
// <-disconnected
|
||||
//
|
||||
package client
|
105
vendor/github.com/fluffle/goirc/client/handlers.go
generated
vendored
Normal file
105
vendor/github.com/fluffle/goirc/client/handlers.go
generated
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
package client
|
||||
|
||||
// this file contains the basic set of event handlers
|
||||
// to manage tracking an irc connection etc.
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// sets up the internal event handlers to do essential IRC protocol things
|
||||
var intHandlers = map[string]HandlerFunc{
|
||||
REGISTER: (*Conn).h_REGISTER,
|
||||
"001": (*Conn).h_001,
|
||||
"433": (*Conn).h_433,
|
||||
CTCP: (*Conn).h_CTCP,
|
||||
NICK: (*Conn).h_NICK,
|
||||
PING: (*Conn).h_PING,
|
||||
}
|
||||
|
||||
func (conn *Conn) addIntHandlers() {
|
||||
for n, h := range intHandlers {
|
||||
// internal handlers are essential for the IRC client
|
||||
// to function, so we don't save their Removers here
|
||||
conn.handle(n, h)
|
||||
}
|
||||
}
|
||||
|
||||
// Basic ping/pong handler
|
||||
func (conn *Conn) h_PING(line *Line) {
|
||||
conn.Pong(line.Args[0])
|
||||
}
|
||||
|
||||
// Handler for initial registration with server once tcp connection is made.
|
||||
func (conn *Conn) h_REGISTER(line *Line) {
|
||||
if conn.cfg.Pass != "" {
|
||||
conn.Pass(conn.cfg.Pass)
|
||||
}
|
||||
conn.Nick(conn.cfg.Me.Nick)
|
||||
conn.User(conn.cfg.Me.Ident, conn.cfg.Me.Name)
|
||||
}
|
||||
|
||||
// Handler to trigger a CONNECTED event on receipt of numeric 001
|
||||
func (conn *Conn) h_001(line *Line) {
|
||||
// we're connected!
|
||||
conn.dispatch(&Line{Cmd: CONNECTED, Time: time.Now()})
|
||||
// and we're being given our hostname (from the server's perspective)
|
||||
t := line.Args[len(line.Args)-1]
|
||||
if idx := strings.LastIndex(t, " "); idx != -1 {
|
||||
t = t[idx+1:]
|
||||
if idx = strings.Index(t, "@"); idx != -1 {
|
||||
if conn.st != nil {
|
||||
me := conn.Me()
|
||||
conn.st.NickInfo(me.Nick, me.Ident, t[idx+1:], me.Name)
|
||||
} else {
|
||||
conn.cfg.Me.Host = t[idx+1:]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
me := conn.Me()
|
||||
neu := conn.cfg.NewNick(line.Args[1])
|
||||
conn.Nick(neu)
|
||||
if !line.argslen(1) {
|
||||
return
|
||||
}
|
||||
// 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 line.Args[1] == me.Nick {
|
||||
if conn.st != nil {
|
||||
conn.cfg.Me = conn.st.ReNick(me.Nick, neu)
|
||||
} else {
|
||||
conn.cfg.Me.Nick = neu
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle VERSION requests and CTCP PING
|
||||
func (conn *Conn) h_CTCP(line *Line) {
|
||||
if line.Args[0] == VERSION {
|
||||
conn.CtcpReply(line.Nick, VERSION, conn.cfg.Version)
|
||||
} else if line.Args[0] == PING && line.argslen(2) {
|
||||
conn.CtcpReply(line.Nick, PING, line.Args[2])
|
||||
}
|
||||
}
|
||||
|
||||
// Handle updating our own NICK if we're not using the state tracker
|
||||
func (conn *Conn) h_NICK(line *Line) {
|
||||
if conn.st == nil && line.Nick == conn.cfg.Me.Nick {
|
||||
conn.cfg.Me.Nick = line.Args[0]
|
||||
}
|
||||
}
|
216
vendor/github.com/fluffle/goirc/client/line.go
generated
vendored
Normal file
216
vendor/github.com/fluffle/goirc/client/line.go
generated
vendored
Normal file
@@ -0,0 +1,216 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fluffle/goirc/logging"
|
||||
)
|
||||
|
||||
var tagsReplacer = strings.NewReplacer("\\:", ";", "\\s", " ", "\\r", "\r", "\\n", "\n")
|
||||
|
||||
// We parse an incoming line into this struct. Line.Cmd is used as the trigger
|
||||
// name for incoming event handlers and is the IRC verb, the first sequence
|
||||
// of non-whitespace characters after ":nick!user@host", e.g. PRIVMSG.
|
||||
// Raw =~ ":nick!user@host cmd args[] :text"
|
||||
// Src == "nick!user@host"
|
||||
// Cmd == e.g. PRIVMSG, 332
|
||||
type Line struct {
|
||||
Tags map[string]string
|
||||
Nick, Ident, Host, Src string
|
||||
Cmd, Raw string
|
||||
Args []string
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
// Copy returns a deep copy of the Line.
|
||||
func (l *Line) Copy() *Line {
|
||||
nl := *l
|
||||
nl.Args = make([]string, len(l.Args))
|
||||
copy(nl.Args, l.Args)
|
||||
if l.Tags != nil {
|
||||
nl.Tags = make(map[string]string)
|
||||
for k, v := range l.Tags {
|
||||
nl.Tags[k] = v
|
||||
}
|
||||
}
|
||||
return &nl
|
||||
}
|
||||
|
||||
// Text returns the contents of the text portion of a line. This only really
|
||||
// makes sense for lines with a :text part, but there are a lot of them.
|
||||
func (line *Line) Text() string {
|
||||
if len(line.Args) > 0 {
|
||||
return line.Args[len(line.Args)-1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Target returns the contextual target of the line, usually the first Arg
|
||||
// for the IRC verb. If the line was broadcast from a channel, the target
|
||||
// will be that channel. If the line was sent directly by a user, the target
|
||||
// will be that user.
|
||||
func (line *Line) Target() string {
|
||||
// TODO(fluffle): Add 005 CHANTYPES parsing for this?
|
||||
switch line.Cmd {
|
||||
case PRIVMSG, NOTICE, ACTION:
|
||||
if !line.Public() {
|
||||
return line.Nick
|
||||
}
|
||||
case CTCP, CTCPREPLY:
|
||||
if !line.Public() {
|
||||
return line.Nick
|
||||
}
|
||||
return line.Args[1]
|
||||
}
|
||||
if len(line.Args) > 0 {
|
||||
return line.Args[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Public returns true if the line is the result of an IRC user sending
|
||||
// a message to a channel the client has joined instead of directly
|
||||
// to the client.
|
||||
//
|
||||
// NOTE: This is very permissive, allowing all 4 RFC channel types even if
|
||||
// your server doesn't technically support them.
|
||||
func (line *Line) Public() bool {
|
||||
switch line.Cmd {
|
||||
case PRIVMSG, NOTICE, ACTION:
|
||||
switch line.Args[0][0] {
|
||||
case '#', '&', '+', '!':
|
||||
return true
|
||||
}
|
||||
case CTCP, CTCPREPLY:
|
||||
// CTCP prepends the CTCP verb to line.Args, thus for the message
|
||||
// :nick!user@host PRIVMSG #foo :\001BAR baz\001
|
||||
// line.Args contains: []string{"BAR", "#foo", "baz"}
|
||||
// TODO(fluffle): Arguably this is broken, and we should have
|
||||
// line.Args containing: []string{"#foo", "BAR", "baz"}
|
||||
// ... OR change conn.Ctcp()'s argument order to be consistent.
|
||||
switch line.Args[1][0] {
|
||||
case '#', '&', '+', '!':
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ParseLine creates a Line from an incoming message from the IRC server.
|
||||
//
|
||||
// It contains special casing for CTCP messages, most notably CTCP ACTION.
|
||||
// All CTCP messages have the \001 bytes stripped from the message and the
|
||||
// CTCP command separated from any subsequent text. Then, CTCP ACTIONs are
|
||||
// rewritten such that Line.Cmd == ACTION. Other CTCP messages have Cmd
|
||||
// set to CTCP or CTCPREPLY, and the CTCP command prepended to line.Args.
|
||||
//
|
||||
// ParseLine also parses IRCv3 tags, if received. If a line does not have
|
||||
// the tags section, Line.Tags will be nil. Tags are optional, and will
|
||||
// only be included after the correct CAP command.
|
||||
//
|
||||
// http://ircv3.net/specs/core/capability-negotiation-3.1.html
|
||||
// http://ircv3.net/specs/core/message-tags-3.2.html
|
||||
func ParseLine(s string) *Line {
|
||||
line := &Line{Raw: s}
|
||||
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if s[0] == '@' {
|
||||
var rawTags string
|
||||
line.Tags = make(map[string]string)
|
||||
if idx := strings.Index(s, " "); idx != -1 {
|
||||
rawTags, s = s[1:idx], s[idx+1:]
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ; is represented as \: in a tag, so it's safe to split on ;
|
||||
for _, tag := range strings.Split(rawTags, ";") {
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
pair := strings.SplitN(tagsReplacer.Replace(tag), "=", 2)
|
||||
if len(pair) < 2 {
|
||||
line.Tags[tag] = ""
|
||||
} else {
|
||||
line.Tags[pair[0]] = pair[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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:]
|
||||
} else {
|
||||
// pretty sure we shouldn't get here ...
|
||||
return nil
|
||||
}
|
||||
|
||||
// src can be the hostname of the irc server or a nick!user@host
|
||||
line.Host = line.Src
|
||||
nidx, uidx := strings.Index(line.Src, "!"), strings.Index(line.Src, "@")
|
||||
if uidx != -1 && nidx != -1 {
|
||||
line.Nick = line.Src[:nidx]
|
||||
line.Ident = line.Src[nidx+1 : uidx]
|
||||
line.Host = line.Src[uidx+1:]
|
||||
}
|
||||
}
|
||||
|
||||
// now we're here, we've parsed a :nick!user@host or :server off
|
||||
// s should contain "cmd args[] :text"
|
||||
args := strings.SplitN(s, " :", 2)
|
||||
if len(args) > 1 {
|
||||
args = append(strings.Fields(args[0]), args[1])
|
||||
} else {
|
||||
args = strings.Fields(args[0])
|
||||
}
|
||||
line.Cmd = strings.ToUpper(args[0])
|
||||
if len(args) > 1 {
|
||||
line.Args = args[1:]
|
||||
}
|
||||
|
||||
// So, I think CTCP and (in particular) CTCP ACTION are better handled as
|
||||
// separate events as opposed to forcing people to have gargantuan
|
||||
// handlers to cope with the possibilities.
|
||||
if (line.Cmd == PRIVMSG || line.Cmd == NOTICE) &&
|
||||
len(line.Args[1]) > 2 &&
|
||||
strings.HasPrefix(line.Args[1], "\001") &&
|
||||
strings.HasSuffix(line.Args[1], "\001") {
|
||||
// WOO, it's a CTCP message
|
||||
t := strings.SplitN(strings.Trim(line.Args[1], "\001"), " ", 2)
|
||||
if len(t) > 1 {
|
||||
// Replace the line with the unwrapped CTCP
|
||||
line.Args[1] = t[1]
|
||||
}
|
||||
if c := strings.ToUpper(t[0]); c == ACTION && line.Cmd == PRIVMSG {
|
||||
// make a CTCP ACTION it's own event a-la PRIVMSG
|
||||
line.Cmd = c
|
||||
} else {
|
||||
// otherwise, dispatch a generic CTCP/CTCPREPLY event that
|
||||
// contains the type of CTCP in line.Args[0]
|
||||
if line.Cmd == PRIVMSG {
|
||||
line.Cmd = CTCP
|
||||
} else {
|
||||
line.Cmd = CTCPREPLY
|
||||
}
|
||||
line.Args = append([]string{c}, line.Args...)
|
||||
}
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
func (line *Line) argslen(minlen int) bool {
|
||||
pc, _, _, _ := runtime.Caller(1)
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if len(line.Args) <= minlen {
|
||||
logging.Warn("%s: too few arguments: %s", fn.Name(), strings.Join(line.Args, " "))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
262
vendor/github.com/fluffle/goirc/client/state_handlers.go
generated
vendored
Normal file
262
vendor/github.com/fluffle/goirc/client/state_handlers.go
generated
vendored
Normal file
@@ -0,0 +1,262 @@
|
||||
package client
|
||||
|
||||
// this file contains the extra set of event handlers
|
||||
// to manage tracking state for an IRC connection
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/fluffle/goirc/logging"
|
||||
)
|
||||
|
||||
var stHandlers = map[string]HandlerFunc{
|
||||
"JOIN": (*Conn).h_JOIN,
|
||||
"KICK": (*Conn).h_KICK,
|
||||
"MODE": (*Conn).h_MODE,
|
||||
"NICK": (*Conn).h_STNICK,
|
||||
"PART": (*Conn).h_PART,
|
||||
"QUIT": (*Conn).h_QUIT,
|
||||
"TOPIC": (*Conn).h_TOPIC,
|
||||
"311": (*Conn).h_311,
|
||||
"324": (*Conn).h_324,
|
||||
"332": (*Conn).h_332,
|
||||
"352": (*Conn).h_352,
|
||||
"353": (*Conn).h_353,
|
||||
"671": (*Conn).h_671,
|
||||
}
|
||||
|
||||
func (conn *Conn) addSTHandlers() {
|
||||
for n, h := range stHandlers {
|
||||
conn.stRemovers = append(conn.stRemovers, conn.handle(n, h))
|
||||
}
|
||||
}
|
||||
|
||||
func (conn *Conn) delSTHandlers() {
|
||||
for _, h := range conn.stRemovers {
|
||||
h.Remove()
|
||||
}
|
||||
conn.stRemovers = conn.stRemovers[:0]
|
||||
}
|
||||
|
||||
// Handle NICK messages that need to update the state tracker
|
||||
func (conn *Conn) h_STNICK(line *Line) {
|
||||
// all nicks should be handled the same way, our own included
|
||||
conn.st.ReNick(line.Nick, line.Args[0])
|
||||
}
|
||||
|
||||
// Handle JOINs to channels to maintain state
|
||||
func (conn *Conn) h_JOIN(line *Line) {
|
||||
ch := conn.st.GetChannel(line.Args[0])
|
||||
nk := conn.st.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 nk == nil && ch == nil
|
||||
if !conn.Me().Equals(nk) {
|
||||
logging.Warn("irc.JOIN(): JOIN to unknown channel %s received "+
|
||||
"from (non-me) nick %s", line.Args[0], line.Nick)
|
||||
return
|
||||
}
|
||||
conn.st.NewChannel(line.Args[0])
|
||||
// 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(line.Args[0])
|
||||
// sending a WHO for the channel is MUCH more efficient than
|
||||
// triggering a WHOIS on every nick from the 353 handler
|
||||
conn.Who(line.Args[0])
|
||||
}
|
||||
if nk == nil {
|
||||
// this is the first we've seen of this nick
|
||||
conn.st.NewNick(line.Nick)
|
||||
conn.st.NickInfo(line.Nick, line.Ident, line.Host, "")
|
||||
// since we don't know much about this nick, ask server for info
|
||||
conn.Who(line.Nick)
|
||||
}
|
||||
// this takes care of both nick and channel linking \o/
|
||||
conn.st.Associate(line.Args[0], line.Nick)
|
||||
}
|
||||
|
||||
// Handle PARTs from channels to maintain state
|
||||
func (conn *Conn) h_PART(line *Line) {
|
||||
conn.st.Dissociate(line.Args[0], line.Nick)
|
||||
}
|
||||
|
||||
// Handle KICKs from channels to maintain state
|
||||
func (conn *Conn) h_KICK(line *Line) {
|
||||
if !line.argslen(1) {
|
||||
return
|
||||
}
|
||||
// XXX: this won't handle autorejoining channels on KICK
|
||||
// it's trivial to do this in a seperate handler...
|
||||
conn.st.Dissociate(line.Args[0], line.Args[1])
|
||||
}
|
||||
|
||||
// Handle other people's QUITs
|
||||
func (conn *Conn) h_QUIT(line *Line) {
|
||||
conn.st.DelNick(line.Nick)
|
||||
}
|
||||
|
||||
// Handle MODE changes for channels we know about (and our nick personally)
|
||||
func (conn *Conn) h_MODE(line *Line) {
|
||||
if !line.argslen(1) {
|
||||
return
|
||||
}
|
||||
if ch := conn.st.GetChannel(line.Args[0]); ch != nil {
|
||||
// channel modes first
|
||||
conn.st.ChannelModes(line.Args[0], line.Args[1], line.Args[2:]...)
|
||||
} else if nk := conn.st.GetNick(line.Args[0]); nk != nil {
|
||||
// nick mode change, should be us
|
||||
if !conn.Me().Equals(nk) {
|
||||
logging.Warn("irc.MODE(): recieved MODE %s for (non-me) nick %s",
|
||||
line.Args[1], line.Args[0])
|
||||
return
|
||||
}
|
||||
conn.st.NickModes(line.Args[0], line.Args[1])
|
||||
} else {
|
||||
logging.Warn("irc.MODE(): not sure what to do with MODE %s",
|
||||
strings.Join(line.Args, " "))
|
||||
}
|
||||
}
|
||||
|
||||
// Handle TOPIC changes for channels
|
||||
func (conn *Conn) h_TOPIC(line *Line) {
|
||||
if !line.argslen(1) {
|
||||
return
|
||||
}
|
||||
if ch := conn.st.GetChannel(line.Args[0]); ch != nil {
|
||||
conn.st.Topic(line.Args[0], line.Args[1])
|
||||
} else {
|
||||
logging.Warn("irc.TOPIC(): topic change on unknown channel %s",
|
||||
line.Args[0])
|
||||
}
|
||||
}
|
||||
|
||||
// Handle 311 whois reply
|
||||
func (conn *Conn) h_311(line *Line) {
|
||||
if !line.argslen(5) {
|
||||
return
|
||||
}
|
||||
if nk := conn.st.GetNick(line.Args[1]); (nk != nil) && !conn.Me().Equals(nk) {
|
||||
conn.st.NickInfo(line.Args[1], line.Args[2], line.Args[3], line.Args[5])
|
||||
} else {
|
||||
logging.Warn("irc.311(): received WHOIS info for unknown nick %s",
|
||||
line.Args[1])
|
||||
}
|
||||
}
|
||||
|
||||
// Handle 324 mode reply
|
||||
func (conn *Conn) h_324(line *Line) {
|
||||
if !line.argslen(2) {
|
||||
return
|
||||
}
|
||||
if ch := conn.st.GetChannel(line.Args[1]); ch != nil {
|
||||
conn.st.ChannelModes(line.Args[1], line.Args[2], line.Args[3:]...)
|
||||
} else {
|
||||
logging.Warn("irc.324(): 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 !line.argslen(2) {
|
||||
return
|
||||
}
|
||||
if ch := conn.st.GetChannel(line.Args[1]); ch != nil {
|
||||
conn.st.Topic(line.Args[1], line.Args[2])
|
||||
} else {
|
||||
logging.Warn("irc.332(): received TOPIC value for unknown channel %s",
|
||||
line.Args[1])
|
||||
}
|
||||
}
|
||||
|
||||
// Handle 352 who reply
|
||||
func (conn *Conn) h_352(line *Line) {
|
||||
if !line.argslen(5) {
|
||||
return
|
||||
}
|
||||
nk := conn.st.GetNick(line.Args[5])
|
||||
if nk == nil {
|
||||
logging.Warn("irc.352(): received WHO reply for unknown nick %s",
|
||||
line.Args[5])
|
||||
return
|
||||
}
|
||||
if conn.Me().Equals(nk) {
|
||||
return
|
||||
}
|
||||
// XXX: do we care about the actual server the nick is on?
|
||||
// or the hop count to this server?
|
||||
// last arg contains "<hop count> <real name>"
|
||||
a := strings.SplitN(line.Args[len(line.Args)-1], " ", 2)
|
||||
conn.st.NickInfo(nk.Nick, line.Args[2], line.Args[3], a[1])
|
||||
if !line.argslen(6) {
|
||||
return
|
||||
}
|
||||
if idx := strings.Index(line.Args[6], "*"); idx != -1 {
|
||||
conn.st.NickModes(nk.Nick, "+o")
|
||||
}
|
||||
if idx := strings.Index(line.Args[6], "B"); idx != -1 {
|
||||
conn.st.NickModes(nk.Nick, "+B")
|
||||
}
|
||||
if idx := strings.Index(line.Args[6], "H"); idx != -1 {
|
||||
conn.st.NickModes(nk.Nick, "+i")
|
||||
}
|
||||
}
|
||||
|
||||
// Handle 353 names reply
|
||||
func (conn *Conn) h_353(line *Line) {
|
||||
if !line.argslen(2) {
|
||||
return
|
||||
}
|
||||
if ch := conn.st.GetChannel(line.Args[2]); ch != nil {
|
||||
nicks := strings.Split(line.Args[len(line.Args)-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:]
|
||||
fallthrough
|
||||
default:
|
||||
if conn.st.GetNick(nick) == nil {
|
||||
// we don't know this nick yet!
|
||||
conn.st.NewNick(nick)
|
||||
}
|
||||
if _, ok := conn.st.IsOn(ch.Name, nick); !ok {
|
||||
// This nick isn't associated with this channel yet!
|
||||
conn.st.Associate(ch.Name, nick)
|
||||
}
|
||||
switch c {
|
||||
case '~':
|
||||
conn.st.ChannelModes(ch.Name, "+q", nick)
|
||||
case '&':
|
||||
conn.st.ChannelModes(ch.Name, "+a", nick)
|
||||
case '@':
|
||||
conn.st.ChannelModes(ch.Name, "+o", nick)
|
||||
case '%':
|
||||
conn.st.ChannelModes(ch.Name, "+h", nick)
|
||||
case '+':
|
||||
conn.st.ChannelModes(ch.Name, "+v", nick)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logging.Warn("irc.353(): 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 !line.argslen(1) {
|
||||
return
|
||||
}
|
||||
if nk := conn.st.GetNick(line.Args[1]); nk != nil {
|
||||
conn.st.NickModes(nk.Nick, "+z")
|
||||
} else {
|
||||
logging.Warn("irc.671(): received WHOIS SSL info for unknown nick %s",
|
||||
line.Args[1])
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user