512 lines
15 KiB
Go
512 lines
15 KiB
Go
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
|
// of this source code is governed by the MIT license that can be found in
|
|
// the LICENSE file.
|
|
|
|
package girc
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/net/context"
|
|
)
|
|
|
|
// Client contains all of the information necessary to run a single IRC
|
|
// client.
|
|
type Client struct {
|
|
// Config represents the configuration
|
|
Config Config
|
|
// rx is a buffer of events waiting to be processed.
|
|
rx chan *Event
|
|
// tx is a buffer of events waiting to be sent.
|
|
tx chan *Event
|
|
|
|
// state represents the throw-away state for the irc session.
|
|
state *state
|
|
// initTime represents the creation time of the client.
|
|
initTime time.Time
|
|
|
|
// Handlers is a handler which manages internal and external handlers.
|
|
Handlers *Caller
|
|
// CTCP is a handler which manages internal and external CTCP handlers.
|
|
CTCP *CTCP
|
|
// Commands contains various helper methods to interact with the server.
|
|
Commands *Commands
|
|
|
|
// conn is a net.Conn reference to the IRC server.
|
|
conn *ircConn
|
|
// tries represents the internal reconnect count to the IRC server.
|
|
tries int
|
|
// reconnecting is true if the client is reconnecting, used so multiple
|
|
// threads aren't trying to reconnect at the same time.
|
|
reconnecting bool
|
|
// cmux is the mux used for connections/disconnections from the server,
|
|
// so multiple threads aren't trying to connect at the same time, and
|
|
// vice versa.
|
|
cmux sync.Mutex
|
|
|
|
// debug is used if a writer is supplied for Client.Config.Debugger.
|
|
debug *log.Logger
|
|
|
|
// Below are functions used to close out goroutines opened by the client.
|
|
closeRead context.CancelFunc
|
|
closeSend context.CancelFunc
|
|
closeExec context.CancelFunc
|
|
closeLoop context.CancelFunc
|
|
}
|
|
|
|
// Config contains configuration options for an IRC client
|
|
type Config struct {
|
|
// Server is a host/ip of the server you want to connect to.
|
|
Server string
|
|
// Port is the port that will be used during server connection.
|
|
Port int
|
|
// Password is the server password used to authenticate.
|
|
Password string
|
|
// Nick is an rfc-valid nickname used during connect.
|
|
Nick string
|
|
// User is the username/ident to use on connect. Ignored if identd server
|
|
// is used.
|
|
User string
|
|
// Name is the "realname" that's used during connect.
|
|
Name string
|
|
// Proxy is a proxy based address, used during the dial process when
|
|
// connecting to the server. Currently, x/net/proxy only supports socks5,
|
|
// however you can add your own proxy functionality using:
|
|
// proxy.RegisterDialerType
|
|
//
|
|
// Examples of how Proxy may be used:
|
|
// socks5://localhost:8080
|
|
// socks5://1.2.3.4:8888
|
|
// customProxy://example.com:8000
|
|
//
|
|
Proxy string
|
|
// Bind is used to bind to a specific host or port during the dial
|
|
// process when connecting to the server. This can be a hostname, however
|
|
// it must resolve to an IPv4/IPv6 address bindable on your system.
|
|
// Otherwise, you can simply use a IPv4/IPv6 address directly.
|
|
Bind string
|
|
// If we should connect via SSL. See TLSConfig to set your own TLS
|
|
// configuration.
|
|
SSL bool
|
|
// TLSConfig is an optional user-supplied tls configuration, used during
|
|
// socket creation to the server. SSL must be enabled for this to be used.
|
|
TLSConfig *tls.Config
|
|
// Retries is the number of times the client will attempt to reconnect
|
|
// to the server after the last disconnect.
|
|
Retries int
|
|
// AllowFlood allows the client to bypass the rate limit of outbound
|
|
// messages.
|
|
AllowFlood bool
|
|
// Debugger is an optional, user supplied location to log the raw lines
|
|
// sent from the server, or other useful debug logs. Defaults to
|
|
// ioutil.Discard. For quick debugging, this could be set to os.Stdout.
|
|
Debugger io.Writer
|
|
// RecoverFunc is called when a handler throws a panic. If RecoverFunc is
|
|
// set, the panic will be considered recovered, otherwise the client will
|
|
// panic. Set this to DefaultRecoverHandler if you don't want the client
|
|
// to panic, however you don't want to handle the panic yourself.
|
|
// DefaultRecoverHandler will log the panic to Debugger or os.Stdout if
|
|
// Debugger is unset.
|
|
RecoverFunc func(c *Client, e *HandlerError)
|
|
// SupportedCaps are the IRCv3 capabilities you would like the client to
|
|
// support. Only use this if DisableTracking and DisableCapTracking are
|
|
// not enabled, otherwise you will need to handle CAP negotiation yourself.
|
|
// The keys value gets passed to the server if supported.
|
|
SupportedCaps map[string][]string
|
|
// Version is the application version information that will be used in
|
|
// response to a CTCP VERSION, if default CTCP replies have not been
|
|
// overwritten or a VERSION handler was already supplied.
|
|
Version string
|
|
// ReconnectDelay is the a duration of time to delay before attempting a
|
|
// reconnection. Defaults to 10s (minimum of 5s). This is ignored if
|
|
// Reconnect() is called directly.
|
|
ReconnectDelay time.Duration
|
|
// HandleError if supplied, is called when one is disconnected from the
|
|
// server, with a given error.
|
|
HandleError func(error)
|
|
|
|
// disableTracking disables all channel and user-level tracking. Useful
|
|
// for highly embedded scripts with single purposes.
|
|
disableTracking bool
|
|
// disableCapTracking disables all network/server capability tracking.
|
|
// This includes determining what feature the IRC server supports, what
|
|
// the "NETWORK=" variables are, and other useful stuff. DisableTracking
|
|
// cannot be enabled if you want to also tracking capabilities.
|
|
disableCapTracking bool
|
|
// disableNickCollision disables the clients auto-response to nickname
|
|
// collisions. For example, if "test" is already in use, or is blocked by
|
|
// the network/a service, the client will try and use "test_", then it
|
|
// will attempt "test__", "test___", and so on.
|
|
disableNickCollision bool
|
|
}
|
|
|
|
// ErrNotConnected is returned if a method is used when the client isn't
|
|
// connected.
|
|
var ErrNotConnected = errors.New("client is not connected to server")
|
|
|
|
// ErrAlreadyConnecting implies that a connection attempt is already happening.
|
|
var ErrAlreadyConnecting = errors.New("a connection attempt is already occurring")
|
|
|
|
// ErrDisconnected is called when Config.Retries is less than 1, and we
|
|
// non-intentionally disconnected from the server.
|
|
var ErrDisconnected = errors.New("unexpectedly disconnected")
|
|
|
|
// ErrInvalidTarget should be returned if the target which you are
|
|
// attempting to send an event to is invalid or doesn't match RFC spec.
|
|
type ErrInvalidTarget struct {
|
|
Target string
|
|
}
|
|
|
|
func (e *ErrInvalidTarget) Error() string { return "invalid target: " + e.Target }
|
|
|
|
// New creates a new IRC client with the specified server, name and config.
|
|
func New(config Config) *Client {
|
|
c := &Client{
|
|
Config: config,
|
|
rx: make(chan *Event, 25),
|
|
tx: make(chan *Event, 25),
|
|
CTCP: newCTCP(),
|
|
initTime: time.Now(),
|
|
}
|
|
|
|
c.Commands = &Commands{c: c}
|
|
|
|
if c.Config.Debugger == nil {
|
|
c.debug = log.New(ioutil.Discard, "", 0)
|
|
} else {
|
|
c.debug = log.New(c.Config.Debugger, "debug:", log.Ltime|log.Lshortfile)
|
|
c.debug.Print("initializing debugging")
|
|
}
|
|
|
|
// Setup the caller.
|
|
c.Handlers = newCaller(c.debug)
|
|
|
|
// Give ourselves a new state.
|
|
c.state = newState()
|
|
|
|
// Register builtin handlers.
|
|
c.registerBuiltins()
|
|
|
|
// Register default CTCP responses.
|
|
c.CTCP.addDefaultHandlers()
|
|
|
|
return c
|
|
}
|
|
|
|
// String returns a brief description of the current client state.
|
|
func (c *Client) String() string {
|
|
var connected bool
|
|
if c.conn != nil {
|
|
connected = c.conn.connected
|
|
}
|
|
|
|
return fmt.Sprintf(
|
|
"<Client init:%q handlers:%d connected:%t reconnecting:%t tries:%d>",
|
|
c.initTime.String(), c.Handlers.Len(), connected, c.reconnecting, c.tries,
|
|
)
|
|
}
|
|
|
|
// cleanup is used to close out all threads used by the client, like read and
|
|
// write loops.
|
|
func (c *Client) cleanup(all bool) {
|
|
c.cmux.Lock()
|
|
|
|
// Close any connections they have open.
|
|
if c.conn != nil {
|
|
c.conn.Close()
|
|
}
|
|
|
|
c.flushTx()
|
|
|
|
if c.closeRead != nil {
|
|
c.closeRead()
|
|
}
|
|
if c.closeSend != nil {
|
|
c.closeSend()
|
|
}
|
|
if c.closeExec != nil {
|
|
c.closeExec()
|
|
}
|
|
|
|
if all {
|
|
if c.closeLoop != nil {
|
|
c.closeLoop()
|
|
}
|
|
}
|
|
|
|
c.cmux.Unlock()
|
|
}
|
|
|
|
// quit is the underlying wrapper to quit from the network and cleanup.
|
|
func (c *Client) quit(sendMessage bool) {
|
|
if sendMessage {
|
|
c.Send(&Event{Command: QUIT, Trailing: "disconnecting..."})
|
|
}
|
|
|
|
c.RunHandlers(&Event{Command: DISCONNECTED, Trailing: c.Server()})
|
|
c.cleanup(false)
|
|
}
|
|
|
|
// Quit disconnects from the server.
|
|
func (c *Client) Quit() {
|
|
c.quit(true)
|
|
}
|
|
|
|
// QuitWithMessage disconnects from the server with a given message.
|
|
func (c *Client) QuitWithMessage(message string) {
|
|
c.Send(&Event{Command: QUIT, Trailing: message})
|
|
c.quit(false)
|
|
}
|
|
|
|
// Stop exits the clients main loop and any other goroutines created by
|
|
// the client itself. This does not include handlers, as they will run for
|
|
// any incoming events prior to when Stop() or Quit() was called, until the
|
|
// event queue is empty and execution has completed for those handlers. This
|
|
// means that you are responsible to ensure that your handlers due not
|
|
// execute forever. Use Client.Quit() first if you want to disconnect the
|
|
// client from the server/connection gracefully.
|
|
func (c *Client) Stop() {
|
|
c.quit(false)
|
|
c.RunHandlers(&Event{Command: STOPPED, Trailing: c.Server()})
|
|
}
|
|
|
|
// Loop reads from the events channel and sends the events to be handled for
|
|
// every message it receives.
|
|
func (c *Client) Loop() {
|
|
var ctx context.Context
|
|
ctx, c.closeLoop = context.WithCancel(context.Background())
|
|
|
|
<-ctx.Done()
|
|
}
|
|
|
|
func (c *Client) execLoop(ctx context.Context) {
|
|
for {
|
|
select {
|
|
case event := <-c.rx:
|
|
c.RunHandlers(event)
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// DisableTracking disables all channel and user-level tracking, and clears
|
|
// all internal handlers. Useful for highly embedded scripts with single
|
|
// purposes. This cannot be un-done.
|
|
func (c *Client) DisableTracking() {
|
|
c.debug.Print("disabling tracking")
|
|
c.Config.disableTracking = true
|
|
c.Handlers.clearInternal()
|
|
c.state.mu.Lock()
|
|
c.state.channels = nil
|
|
c.state.mu.Unlock()
|
|
c.registerBuiltins()
|
|
}
|
|
|
|
// DisableCapTracking disables all network/server capability tracking, and
|
|
// clears all internal handlers. This includes determining what feature the
|
|
// IRC server supports, what the "NETWORK=" variables are, and other useful
|
|
// stuff. DisableTracking() cannot be called if you want to also track
|
|
// capabilities.
|
|
func (c *Client) DisableCapTracking() {
|
|
// No need to mess with internal handlers. That should already be
|
|
// handled by the clear in Client.DisableTracking().
|
|
if c.Config.disableCapTracking {
|
|
return
|
|
}
|
|
|
|
c.debug.Print("disabling CAP tracking")
|
|
c.Config.disableCapTracking = true
|
|
c.Handlers.clearInternal()
|
|
c.registerBuiltins()
|
|
}
|
|
|
|
// DisableNickCollision disables the clients auto-response to nickname
|
|
// collisions. For example, if "test" is already in use, or is blocked by the
|
|
// network/a service, the client will try and use "test_", then it will
|
|
// attempt "test__", "test___", and so on.
|
|
func (c *Client) DisableNickCollision() {
|
|
c.debug.Print("disabling nick collision prevention")
|
|
c.Config.disableNickCollision = true
|
|
c.Handlers.clearInternal()
|
|
c.state.mu.Lock()
|
|
c.state.channels = nil
|
|
c.state.mu.Unlock()
|
|
c.registerBuiltins()
|
|
}
|
|
|
|
// Server returns the string representation of host+port pair for net.Conn.
|
|
func (c *Client) Server() string {
|
|
return fmt.Sprintf("%s:%d", c.Config.Server, c.Config.Port)
|
|
}
|
|
|
|
// Lifetime returns the amount of time that has passed since the client was
|
|
// created.
|
|
func (c *Client) Lifetime() time.Duration {
|
|
return time.Since(c.initTime)
|
|
}
|
|
|
|
// Uptime is the time at which the client successfully connected to the
|
|
// server.
|
|
func (c *Client) Uptime() (up *time.Time, err error) {
|
|
if !c.IsConnected() {
|
|
return nil, ErrNotConnected
|
|
}
|
|
|
|
up = c.conn.connTime
|
|
|
|
return up, nil
|
|
}
|
|
|
|
// ConnSince is the duration that has past since the client successfully
|
|
// connected to the server.
|
|
func (c *Client) ConnSince() (since *time.Duration, err error) {
|
|
if !c.IsConnected() {
|
|
return nil, ErrNotConnected
|
|
}
|
|
|
|
timeSince := time.Since(*c.conn.connTime)
|
|
|
|
return &timeSince, nil
|
|
}
|
|
|
|
// IsConnected returns true if the client is connected to the server.
|
|
func (c *Client) IsConnected() (connected bool) {
|
|
if c.conn == nil {
|
|
return false
|
|
}
|
|
return c.conn.connected
|
|
}
|
|
|
|
// GetNick returns the current nickname of the active connection. Returns
|
|
// empty string if tracking is disabled.
|
|
func (c *Client) GetNick() (nick string) {
|
|
if c.Config.disableTracking {
|
|
panic("GetNick() used when tracking is disabled")
|
|
}
|
|
|
|
c.state.mu.RLock()
|
|
if c.state.nick == "" {
|
|
nick = c.Config.Nick
|
|
} else {
|
|
nick = c.state.nick
|
|
}
|
|
c.state.mu.RUnlock()
|
|
|
|
return nick
|
|
}
|
|
|
|
// Channels returns the active list of channels that the client is in.
|
|
// Panics if tracking is disabled.
|
|
func (c *Client) Channels() []string {
|
|
if c.Config.disableTracking {
|
|
panic("Channels() used when tracking is disabled")
|
|
}
|
|
|
|
channels := make([]string, len(c.state.channels))
|
|
|
|
c.state.mu.RLock()
|
|
var i int
|
|
for channel := range c.state.channels {
|
|
channels[i] = channel
|
|
i++
|
|
}
|
|
c.state.mu.RUnlock()
|
|
|
|
return channels
|
|
}
|
|
|
|
// IsInChannel returns true if the client is in channel. Panics if tracking
|
|
// is disabled.
|
|
func (c *Client) IsInChannel(channel string) bool {
|
|
if c.Config.disableTracking {
|
|
panic("Channels() used when tracking is disabled")
|
|
}
|
|
|
|
c.state.mu.RLock()
|
|
_, inChannel := c.state.channels[strings.ToLower(channel)]
|
|
c.state.mu.RUnlock()
|
|
|
|
return inChannel
|
|
}
|
|
|
|
// GetServerOption retrieves a server capability setting that was retrieved
|
|
// during client connection. This is also known as ISUPPORT (or RPL_PROTOCTL).
|
|
// Will panic if used when tracking has been disabled. Examples of usage:
|
|
//
|
|
// nickLen, success := GetServerOption("MAXNICKLEN")
|
|
//
|
|
func (c *Client) GetServerOption(key string) (result string, ok bool) {
|
|
if c.Config.disableTracking {
|
|
panic("GetServerOption() used when tracking is disabled")
|
|
}
|
|
|
|
c.state.mu.Lock()
|
|
result, ok = c.state.serverOptions[key]
|
|
c.state.mu.Unlock()
|
|
|
|
return result, ok
|
|
}
|
|
|
|
// ServerName returns the server host/name that the server itself identifies
|
|
// as. May be empty if the server does not support RPL_MYINFO. Will panic if
|
|
// used when tracking has been disabled.
|
|
func (c *Client) ServerName() (name string) {
|
|
if c.Config.disableTracking {
|
|
panic("ServerName() used when tracking is disabled")
|
|
}
|
|
|
|
name, _ = c.GetServerOption("SERVER")
|
|
|
|
return name
|
|
}
|
|
|
|
// NetworkName returns the network identifier. E.g. "EsperNet", "ByteIRC".
|
|
// May be empty if the server does not support RPL_ISUPPORT (or RPL_PROTOCTL).
|
|
// Will panic if used when tracking has been disabled.
|
|
func (c *Client) NetworkName() (name string) {
|
|
if c.Config.disableTracking {
|
|
panic("NetworkName() used when tracking is disabled")
|
|
}
|
|
|
|
name, _ = c.GetServerOption("NETWORK")
|
|
|
|
return name
|
|
}
|
|
|
|
// ServerVersion returns the server software version, if the server has
|
|
// supplied this information during connection. May be empty if the server
|
|
// does not support RPL_MYINFO. Will panic if used when tracking has been
|
|
// disabled.
|
|
func (c *Client) ServerVersion() (version string) {
|
|
if c.Config.disableTracking {
|
|
panic("ServerVersion() used when tracking is disabled")
|
|
}
|
|
|
|
version, _ = c.GetServerOption("VERSION")
|
|
|
|
return version
|
|
}
|
|
|
|
// ServerMOTD returns the servers message of the day, if the server has sent
|
|
// it upon connect. Will panic if used when tracking has been disabled.
|
|
func (c *Client) ServerMOTD() (motd string) {
|
|
if c.Config.disableTracking {
|
|
panic("ServerMOTD() used when tracking is disabled")
|
|
}
|
|
|
|
c.state.mu.Lock()
|
|
motd = c.state.motd
|
|
c.state.mu.Unlock()
|
|
|
|
return motd
|
|
}
|