2017-01-03 16:05:27 +00:00
|
|
|
// Copyright 2016-2017 Liam Stanley <me@liamstanley.io>. All rights reserved.
|
2016-11-14 11:50:14 +00:00
|
|
|
// Use of this source code is governed by the MIT license that can be
|
|
|
|
// found in the LICENSE file.
|
|
|
|
|
2016-11-13 08:30:43 +00:00
|
|
|
package girc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/tls"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"net"
|
2016-12-10 09:14:03 +00:00
|
|
|
"strings"
|
2016-11-13 08:30:43 +00:00
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
2016-11-14 11:50:14 +00:00
|
|
|
// Client contains all of the information necessary to run a single IRC
|
|
|
|
// client.
|
2016-11-13 08:30:43 +00:00
|
|
|
type Client struct {
|
2017-02-05 12:11:59 +00:00
|
|
|
// config represents the configuration
|
|
|
|
config Config
|
2016-11-14 11:50:14 +00:00
|
|
|
// Events is a buffer of events waiting to be processed.
|
|
|
|
Events chan *Event
|
2016-11-14 11:59:08 +00:00
|
|
|
|
2016-11-19 01:11:13 +00:00
|
|
|
// state represents the throw-away state for the irc session.
|
2016-11-14 11:59:08 +00:00
|
|
|
state *state
|
2016-11-14 11:50:14 +00:00
|
|
|
// initTime represents the creation time of the client.
|
|
|
|
initTime time.Time
|
2016-11-19 01:11:13 +00:00
|
|
|
|
2016-12-07 10:50:14 +00:00
|
|
|
// Callbacks is a handler which manages internal and external callbacks.
|
|
|
|
Callbacks *Caller
|
2016-12-26 08:06:15 +00:00
|
|
|
// CTCP is a handler which manages internal and external CTCP handlers.
|
|
|
|
CTCP *CTCP
|
2016-11-19 01:11:13 +00:00
|
|
|
|
2016-11-14 11:50:14 +00:00
|
|
|
// tries represents the internal reconnect count to the IRC server.
|
|
|
|
tries int
|
2017-01-12 09:19:35 +00:00
|
|
|
// limiter is a configurable EventLimiter by the end user.
|
|
|
|
limiter *EventLimiter
|
2017-02-03 13:54:44 +00:00
|
|
|
// debug is used if a writer is supplied for Client.Config.Debugger.
|
|
|
|
debug *log.Logger
|
2016-12-13 15:23:06 +00:00
|
|
|
// quitChan is used to stop the read loop. See Client.Quit().
|
2016-11-17 19:59:35 +00:00
|
|
|
quitChan chan struct{}
|
2016-12-13 15:23:06 +00:00
|
|
|
// stopChan is used to stop the client loop. See Client.Stop().
|
|
|
|
stopChan chan struct{}
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Config contains configuration options for an IRC client
|
|
|
|
type Config struct {
|
2016-11-14 11:50:14 +00:00
|
|
|
// 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
|
2016-11-22 16:21:25 +00:00
|
|
|
// Conn is an optional network connection to use (overrides TLSConfig).
|
|
|
|
Conn *net.Conn
|
2016-11-14 11:50:14 +00:00
|
|
|
// TLSConfig is an optional user-supplied tls configuration, used during
|
|
|
|
// socket creation to the server.
|
|
|
|
TLSConfig *tls.Config
|
|
|
|
// MaxRetries is the number of times the client will attempt to reconnect
|
|
|
|
// to the server after the last disconnect.
|
|
|
|
MaxRetries int
|
2017-01-12 09:19:35 +00:00
|
|
|
// RateLimit is the delay in seconds between events sent to the server,
|
|
|
|
// with a burst of 4 messages. Set to -1 to disable.
|
|
|
|
RateLimit int
|
2017-02-03 13:54:44 +00:00
|
|
|
// 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.
|
|
|
|
Debugger io.Writer
|
2017-01-06 14:00:29 +00:00
|
|
|
// 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.
|
2017-01-19 11:58:08 +00:00
|
|
|
// The keys value gets passed to the server if supported.
|
|
|
|
SupportedCaps map[string][]string
|
2017-01-12 07:34:29 +00:00
|
|
|
// 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
|
2016-11-14 11:50:14 +00:00
|
|
|
// ReconnectDelay is the a duration of time to delay before attempting a
|
|
|
|
// reconnection. Defaults to 10s (minimum of 10s).
|
|
|
|
ReconnectDelay time.Duration
|
|
|
|
// DisableTracking disables all channel and user-level tracking. Useful
|
|
|
|
// for highly embedded scripts with single purposes.
|
|
|
|
DisableTracking bool
|
2016-12-26 08:51:42 +00:00
|
|
|
// DisableDefaultCTCP disables all default CTCP responses. Though, any
|
|
|
|
// set CTCP's will override any pre-set ones, by default.
|
|
|
|
DisableDefaultCTCP bool
|
2016-11-14 11:50:14 +00:00
|
|
|
// DisableCapTracking disables all network/server capability tracking.
|
|
|
|
// This includes determining what feature the IRC server supports, what
|
2017-01-06 14:00:29 +00:00
|
|
|
// the "NETWORK=" variables are, and other useful stuff. DisableTracking
|
|
|
|
// cannot be enabled if you want to also tracking capabilities.
|
2016-11-14 11:50:14 +00:00
|
|
|
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
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
|
|
|
|
2016-11-25 00:18:39 +00:00
|
|
|
// ErrNotConnected is returned if a method is used when the client isn't
|
|
|
|
// connected.
|
2016-12-07 00:42:00 +00:00
|
|
|
var ErrNotConnected = errors.New("client is not connected to server")
|
2016-11-25 00:18:39 +00:00
|
|
|
|
|
|
|
// ErrAlreadyConnecting implies that a connection attempt is already happening.
|
2016-12-07 00:17:35 +00:00
|
|
|
var ErrAlreadyConnecting = errors.New("a connection attempt is already occurring")
|
|
|
|
|
|
|
|
// 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 }
|
2016-11-25 00:18:39 +00:00
|
|
|
|
2016-12-09 10:58:18 +00:00
|
|
|
// New creates a new IRC client with the specified server, name and config.
|
2016-11-13 08:30:43 +00:00
|
|
|
func New(config Config) *Client {
|
|
|
|
client := &Client{
|
2017-02-05 12:11:59 +00:00
|
|
|
config: config,
|
2017-02-05 11:09:35 +00:00
|
|
|
Events: make(chan *Event, 100), // buffer 100 events
|
|
|
|
quitChan: make(chan struct{}, 1),
|
|
|
|
stopChan: make(chan struct{}, 1),
|
|
|
|
CTCP: newCTCP(),
|
|
|
|
initTime: time.Now(),
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
|
|
|
|
2017-02-05 12:11:59 +00:00
|
|
|
if client.config.Debugger == nil {
|
|
|
|
client.config.Debugger = ioutil.Discard
|
2017-02-03 13:54:44 +00:00
|
|
|
}
|
2017-02-05 12:11:59 +00:00
|
|
|
client.debug = log.New(client.config.Debugger, "debug:", log.Ltime|log.Lshortfile)
|
2017-02-05 11:09:35 +00:00
|
|
|
client.debug.Print("initializing debugging")
|
|
|
|
|
|
|
|
// Setup the caller.
|
|
|
|
client.Callbacks = newCaller(client.debug)
|
2016-11-19 02:38:23 +00:00
|
|
|
|
2017-01-12 09:19:35 +00:00
|
|
|
// Setup a rate limiter if they requested one.
|
2017-02-05 12:11:59 +00:00
|
|
|
if client.config.RateLimit == 0 {
|
2017-01-12 09:19:35 +00:00
|
|
|
client.limiter = NewEventLimiter(4, 1*time.Second, client.write)
|
2017-02-05 12:11:59 +00:00
|
|
|
} else if client.config.RateLimit > 0 {
|
|
|
|
client.limiter = NewEventLimiter(4, time.Duration(client.config.RateLimit)*time.Second, client.write)
|
2017-01-12 09:19:35 +00:00
|
|
|
}
|
|
|
|
|
2016-12-09 10:37:01 +00:00
|
|
|
// Give ourselves a new state.
|
|
|
|
client.state = newState()
|
|
|
|
|
2017-01-04 02:13:05 +00:00
|
|
|
// Register builtin handlers.
|
|
|
|
client.registerHandlers()
|
2016-11-13 08:30:43 +00:00
|
|
|
|
2016-12-26 08:51:42 +00:00
|
|
|
// Register default CTCP responses.
|
2017-02-05 12:11:59 +00:00
|
|
|
client.CTCP.disableDefault = client.config.DisableDefaultCTCP
|
2016-12-26 08:51:42 +00:00
|
|
|
client.CTCP.addDefaultHandlers()
|
|
|
|
|
2016-11-13 08:30:43 +00:00
|
|
|
return client
|
|
|
|
}
|
|
|
|
|
2016-11-19 17:36:33 +00:00
|
|
|
// Quit disconnects from the server.
|
2016-11-13 11:29:00 +00:00
|
|
|
func (c *Client) Quit(message string) {
|
2016-12-13 15:23:06 +00:00
|
|
|
c.state.quitting = true
|
|
|
|
c.quitChan <- struct{}{}
|
2016-11-19 17:36:33 +00:00
|
|
|
defer func() {
|
2016-12-13 15:23:06 +00:00
|
|
|
// Unset c.quitting, so we can reconnect if we want to.
|
|
|
|
c.state.quitting = false
|
2016-11-19 17:36:33 +00:00
|
|
|
}()
|
2016-11-13 11:29:00 +00:00
|
|
|
|
2016-11-19 17:36:33 +00:00
|
|
|
c.Send(&Event{Command: QUIT, Trailing: message})
|
2016-11-13 11:29:00 +00:00
|
|
|
|
2016-12-13 15:23:06 +00:00
|
|
|
c.state.connected = false
|
2016-11-23 03:03:48 +00:00
|
|
|
if c.state.conn != nil {
|
|
|
|
c.state.conn.Close()
|
|
|
|
}
|
2016-11-19 17:36:33 +00:00
|
|
|
}
|
2016-11-19 16:13:49 +00:00
|
|
|
|
2016-12-13 15:23:06 +00:00
|
|
|
// Stop exits the clients main loop. Use Client.Quit() first if you want to
|
2016-12-09 10:58:18 +00:00
|
|
|
// disconnect the client from the server/connection.
|
2016-11-19 17:36:33 +00:00
|
|
|
func (c *Client) Stop() {
|
2017-01-12 09:19:35 +00:00
|
|
|
// Close and limiters they have, otherwise the client could be easily
|
|
|
|
// held in memory.
|
|
|
|
if c.limiter != nil {
|
|
|
|
c.limiter.Stop()
|
|
|
|
}
|
|
|
|
|
2016-12-13 15:23:06 +00:00
|
|
|
// Send to the stop channel, so if Client.Loop() is being used, this will
|
2016-11-19 16:13:49 +00:00
|
|
|
// return.
|
2016-12-13 15:23:06 +00:00
|
|
|
c.stopChan <- struct{}{}
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Connect attempts to connect to the given IRC server
|
|
|
|
func (c *Client) Connect() error {
|
|
|
|
var conn net.Conn
|
|
|
|
var err error
|
|
|
|
|
2016-11-14 11:50:14 +00:00
|
|
|
// Sanity check a few options.
|
2017-02-05 12:11:59 +00:00
|
|
|
if c.config.Server == "" {
|
2016-11-22 16:21:25 +00:00
|
|
|
return errors.New("invalid server specified")
|
|
|
|
}
|
|
|
|
|
2017-02-05 12:11:59 +00:00
|
|
|
if c.config.Port < 21 || c.config.Port > 65535 {
|
2016-11-22 16:21:25 +00:00
|
|
|
return errors.New("invalid port (21-65535)")
|
|
|
|
}
|
|
|
|
|
2017-02-05 12:11:59 +00:00
|
|
|
if !IsValidNick(c.config.Nick) || !IsValidUser(c.config.User) {
|
2016-11-22 16:21:25 +00:00
|
|
|
return errors.New("invalid nickname or user")
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
|
|
|
|
2016-11-14 11:50:14 +00:00
|
|
|
// Reset the state.
|
2016-11-14 11:59:08 +00:00
|
|
|
c.state = newState()
|
2016-11-13 08:30:43 +00:00
|
|
|
|
2017-02-03 13:54:44 +00:00
|
|
|
c.debug.Printf("connecting to %s...", c.Server())
|
2016-11-23 03:03:48 +00:00
|
|
|
|
2016-11-22 16:21:25 +00:00
|
|
|
// Allow the user to specify their own net.Conn.
|
2017-02-05 12:11:59 +00:00
|
|
|
if c.config.Conn == nil {
|
|
|
|
if c.config.TLSConfig == nil {
|
2016-11-22 16:21:25 +00:00
|
|
|
conn, err = net.Dial("tcp", c.Server())
|
|
|
|
} else {
|
2017-02-05 12:11:59 +00:00
|
|
|
conn, err = tls.Dial("tcp", c.Server(), c.config.TLSConfig)
|
2016-11-22 16:21:25 +00:00
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-11-23 03:03:48 +00:00
|
|
|
c.state.conn = conn
|
2016-11-13 08:30:43 +00:00
|
|
|
} else {
|
2017-02-05 12:11:59 +00:00
|
|
|
c.state.conn = *c.config.Conn
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
|
|
|
|
2016-11-23 03:03:48 +00:00
|
|
|
c.state.reader = newDecoder(c.state.conn)
|
|
|
|
c.state.writer = newEncoder(c.state.conn)
|
2016-12-30 13:51:31 +00:00
|
|
|
|
2016-11-13 08:30:43 +00:00
|
|
|
for _, event := range c.connectMessages() {
|
2017-01-19 07:33:19 +00:00
|
|
|
if err := c.write(event); err != nil {
|
2016-11-13 08:30:43 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-06 06:09:18 +00:00
|
|
|
// List the IRCv3 capabilities, specifically with the max protocol we
|
|
|
|
// support.
|
2017-01-19 08:33:49 +00:00
|
|
|
if err := c.listCAP(); err != nil {
|
|
|
|
return err
|
2016-12-30 13:51:31 +00:00
|
|
|
}
|
|
|
|
|
2017-01-19 06:56:30 +00:00
|
|
|
// Start read loop to process messages from the server.
|
|
|
|
go c.readLoop()
|
|
|
|
|
2016-11-14 11:50:14 +00:00
|
|
|
// Consider the connection a success at this point.
|
2016-11-23 03:03:48 +00:00
|
|
|
c.tries = 0
|
2016-11-13 08:30:43 +00:00
|
|
|
|
2016-12-10 11:43:26 +00:00
|
|
|
c.state.mu.Lock()
|
2016-11-25 00:18:39 +00:00
|
|
|
ctime := time.Now()
|
|
|
|
c.state.connTime = &ctime
|
|
|
|
c.state.connected = true
|
2016-12-10 11:43:26 +00:00
|
|
|
c.state.mu.Unlock()
|
2016-11-25 00:18:39 +00:00
|
|
|
|
2016-11-13 08:30:43 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-11-19 15:55:36 +00:00
|
|
|
// connectMessages is a list of IRC messages to send when attempting to
|
|
|
|
// connect to the IRC server.
|
2016-11-13 13:17:41 +00:00
|
|
|
func (c *Client) connectMessages() (events []*Event) {
|
2016-11-14 11:50:14 +00:00
|
|
|
// Passwords first.
|
2017-02-05 12:11:59 +00:00
|
|
|
if c.config.Password != "" {
|
|
|
|
events = append(events, &Event{Command: PASS, Params: []string{c.config.Password}})
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
|
|
|
|
2016-11-14 11:50:14 +00:00
|
|
|
// Then nickname.
|
2017-02-05 12:11:59 +00:00
|
|
|
events = append(events, &Event{Command: NICK, Params: []string{c.config.Nick}})
|
2016-11-13 08:30:43 +00:00
|
|
|
|
2016-11-14 11:50:14 +00:00
|
|
|
// Then username and realname.
|
2017-02-05 12:11:59 +00:00
|
|
|
if c.config.Name == "" {
|
|
|
|
c.config.Name = c.config.User
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
|
|
|
|
2017-02-05 12:11:59 +00:00
|
|
|
events = append(events, &Event{Command: USER, Params: []string{c.config.User, "+iw", "*"}, Trailing: c.config.Name})
|
2016-11-13 08:30:43 +00:00
|
|
|
|
|
|
|
return events
|
|
|
|
}
|
|
|
|
|
2016-11-19 15:55:36 +00:00
|
|
|
// Reconnect checks to make sure we want to, and then attempts to reconnect
|
|
|
|
// to the server.
|
2016-11-14 11:50:14 +00:00
|
|
|
func (c *Client) Reconnect() (err error) {
|
2016-11-19 17:36:33 +00:00
|
|
|
if c.state.reconnecting {
|
2016-11-25 00:18:39 +00:00
|
|
|
return ErrAlreadyConnecting
|
2016-11-13 11:29:00 +00:00
|
|
|
}
|
|
|
|
|
2016-12-07 10:50:14 +00:00
|
|
|
// Doesn't need to be set to false because a connect should reset it.
|
2016-11-19 17:36:33 +00:00
|
|
|
c.state.reconnecting = true
|
|
|
|
|
2016-12-13 15:23:06 +00:00
|
|
|
if c.state.quitting {
|
2016-11-19 17:36:33 +00:00
|
|
|
return nil
|
|
|
|
}
|
2016-11-19 16:13:49 +00:00
|
|
|
|
2017-02-05 12:11:59 +00:00
|
|
|
if c.config.ReconnectDelay < (10 * time.Second) {
|
|
|
|
c.config.ReconnectDelay = 25 * time.Second
|
2016-11-14 11:50:14 +00:00
|
|
|
}
|
2016-11-13 09:16:01 +00:00
|
|
|
|
2016-12-09 11:30:06 +00:00
|
|
|
if c.IsConnected() {
|
2016-11-23 03:03:48 +00:00
|
|
|
c.Quit("reconnecting...")
|
2016-11-19 17:36:33 +00:00
|
|
|
}
|
|
|
|
|
2017-02-05 12:11:59 +00:00
|
|
|
if c.config.MaxRetries > 0 {
|
2016-11-13 08:30:43 +00:00
|
|
|
var err error
|
|
|
|
|
2016-11-14 11:50:14 +00:00
|
|
|
// Delay so we're not slaughtering the server with a bunch of
|
|
|
|
// connections.
|
2017-02-05 12:11:59 +00:00
|
|
|
c.debug.Printf("reconnecting to %s in %s", c.Server(), c.config.ReconnectDelay)
|
|
|
|
time.Sleep(c.config.ReconnectDelay)
|
2016-11-13 08:30:43 +00:00
|
|
|
|
2017-02-05 12:11:59 +00:00
|
|
|
for err = c.Connect(); err != nil && c.tries < c.config.MaxRetries; c.tries++ {
|
2016-12-07 10:50:14 +00:00
|
|
|
c.state.reconnecting = true
|
2017-02-05 12:11:59 +00:00
|
|
|
c.debug.Printf("reconnecting to %s in %s (%d tries)", c.Server(), c.config.ReconnectDelay, c.tries)
|
|
|
|
time.Sleep(c.config.ReconnectDelay)
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
|
|
|
|
2016-11-23 03:03:48 +00:00
|
|
|
if err != nil {
|
|
|
|
// Too many errors. Stop the client.
|
|
|
|
c.Stop()
|
|
|
|
}
|
|
|
|
|
2016-11-13 08:30:43 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
close(c.Events)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-11-19 15:55:36 +00:00
|
|
|
// readLoop sets a timeout of 300 seconds, and then attempts to read from the
|
|
|
|
// IRC server. If there is an error, it calls Reconnect.
|
2016-12-13 15:23:06 +00:00
|
|
|
func (c *Client) readLoop() {
|
2016-11-13 08:30:43 +00:00
|
|
|
for {
|
2016-12-13 15:23:06 +00:00
|
|
|
select {
|
|
|
|
case <-c.quitChan:
|
|
|
|
return
|
|
|
|
default:
|
|
|
|
c.state.conn.SetDeadline(time.Now().Add(300 * time.Second))
|
|
|
|
event, err := c.state.reader.Decode()
|
|
|
|
if err != nil {
|
|
|
|
// And attempt a reconnect (if applicable).
|
|
|
|
c.Reconnect()
|
|
|
|
<-c.quitChan
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c.Events <- event
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-19 15:55:36 +00:00
|
|
|
// Loop reads from the events channel and sends the events to be handled for
|
|
|
|
// every message it receives.
|
2016-11-13 13:17:41 +00:00
|
|
|
func (c *Client) Loop() {
|
2016-11-13 08:30:43 +00:00
|
|
|
for {
|
|
|
|
select {
|
2016-11-13 10:27:53 +00:00
|
|
|
case event := <-c.Events:
|
2016-11-23 17:40:14 +00:00
|
|
|
c.RunCallbacks(event)
|
2016-12-13 15:23:06 +00:00
|
|
|
case <-c.stopChan:
|
2016-11-13 08:30:43 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-13 15:24:51 +00:00
|
|
|
// Server returns the string representation of host+port pair for net.Conn.
|
|
|
|
func (c *Client) Server() string {
|
2017-02-05 12:11:59 +00:00
|
|
|
return fmt.Sprintf("%s:%d", c.config.Server, c.config.Port)
|
2016-12-13 15:24:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Send sends an event to the server. Use Client.RunCallback() if you are
|
|
|
|
// simply looking to trigger callbacks with an event.
|
|
|
|
func (c *Client) Send(event *Event) error {
|
2017-01-12 09:19:35 +00:00
|
|
|
// if the client wants us to rate limit incoming events, do so, otherwise
|
|
|
|
// simply use the underlying send functionality.
|
|
|
|
if c.limiter != nil {
|
|
|
|
return c.limiter.Send(event)
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.write(event)
|
|
|
|
}
|
|
|
|
|
|
|
|
// write is the lower level function to write an event.
|
|
|
|
func (c *Client) write(event *Event) error {
|
2016-12-13 15:24:51 +00:00
|
|
|
// log the event
|
|
|
|
if !event.Sensitive {
|
2017-02-03 13:54:44 +00:00
|
|
|
c.debug.Print("> ", StripRaw(event.String()))
|
2016-12-13 15:24:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return c.state.writer.Encode(event)
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
|
|
|
c.state.mu.RLock()
|
|
|
|
up = c.state.connTime
|
|
|
|
c.state.mu.RUnlock()
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
c.state.mu.RLock()
|
|
|
|
timeSince := time.Since(*c.state.connTime)
|
|
|
|
c.state.mu.RUnlock()
|
|
|
|
|
|
|
|
return &timeSince, nil
|
|
|
|
}
|
|
|
|
|
2016-11-14 11:50:14 +00:00
|
|
|
// IsConnected returns true if the client is connected to the server.
|
2016-12-09 11:30:06 +00:00
|
|
|
func (c *Client) IsConnected() (connected bool) {
|
2016-12-10 11:43:26 +00:00
|
|
|
c.state.mu.RLock()
|
2016-12-09 11:30:06 +00:00
|
|
|
connected = c.state.connected
|
2016-12-10 11:43:26 +00:00
|
|
|
c.state.mu.RUnlock()
|
2016-11-13 08:30:43 +00:00
|
|
|
|
2016-12-09 11:30:06 +00:00
|
|
|
return connected
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
|
|
|
|
2016-12-09 10:58:18 +00:00
|
|
|
// GetNick returns the current nickname of the active connection. Returns
|
|
|
|
// empty string if tracking is disabled.
|
2016-12-09 11:30:06 +00:00
|
|
|
func (c *Client) GetNick() (nick string) {
|
2017-02-05 12:11:59 +00:00
|
|
|
if c.config.DisableTracking {
|
2016-11-22 04:55:33 +00:00
|
|
|
panic("GetNick() used when tracking is disabled")
|
2016-11-14 11:50:14 +00:00
|
|
|
}
|
|
|
|
|
2016-12-10 11:43:26 +00:00
|
|
|
c.state.mu.RLock()
|
2016-11-14 11:59:08 +00:00
|
|
|
if c.state.nick == "" {
|
2017-02-05 12:11:59 +00:00
|
|
|
nick = c.config.Nick
|
2016-12-09 11:30:06 +00:00
|
|
|
} else {
|
|
|
|
nick = c.state.nick
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
2016-12-10 11:43:26 +00:00
|
|
|
c.state.mu.RUnlock()
|
2016-11-13 08:30:43 +00:00
|
|
|
|
2016-12-09 11:30:06 +00:00
|
|
|
return nick
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
|
|
|
|
2016-12-10 10:58:42 +00:00
|
|
|
// Nick changes the client nickname.
|
|
|
|
func (c *Client) Nick(name string) error {
|
2016-12-07 00:17:35 +00:00
|
|
|
if !IsValidNick(name) {
|
|
|
|
return &ErrInvalidTarget{Target: name}
|
|
|
|
}
|
|
|
|
|
2016-12-10 11:43:26 +00:00
|
|
|
c.state.mu.Lock()
|
2016-11-14 11:59:08 +00:00
|
|
|
c.state.nick = name
|
2016-12-09 11:30:06 +00:00
|
|
|
err := c.Send(&Event{Command: NICK, Params: []string{name}})
|
2016-12-10 11:43:26 +00:00
|
|
|
c.state.mu.Unlock()
|
2016-12-09 11:30:06 +00:00
|
|
|
|
|
|
|
return err
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
|
|
|
|
2016-12-10 10:22:06 +00:00
|
|
|
// Channels returns the active list of channels that the client is in.
|
2017-02-05 12:09:48 +00:00
|
|
|
// Panics if tracking is disabled.
|
2016-12-10 10:22:06 +00:00
|
|
|
func (c *Client) Channels() []string {
|
2017-02-05 12:11:59 +00:00
|
|
|
if c.config.DisableTracking {
|
2016-12-10 10:22:06 +00:00
|
|
|
panic("Channels() used when tracking is disabled")
|
2016-11-14 11:50:14 +00:00
|
|
|
}
|
|
|
|
|
2016-12-10 10:22:06 +00:00
|
|
|
channels := make([]string, len(c.state.channels))
|
|
|
|
|
2016-12-10 11:43:26 +00:00
|
|
|
c.state.mu.RLock()
|
2016-12-10 10:22:06 +00:00
|
|
|
var i int
|
|
|
|
for channel := range c.state.channels {
|
|
|
|
channels[i] = channel
|
|
|
|
i++
|
|
|
|
}
|
2016-12-10 11:43:26 +00:00
|
|
|
c.state.mu.RUnlock()
|
2016-11-13 08:30:43 +00:00
|
|
|
|
2016-12-10 10:22:06 +00:00
|
|
|
return channels
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
|
|
|
|
2017-02-05 12:09:48 +00:00
|
|
|
// IsInChannel returns true if the client is in channel. Panics if tracking
|
|
|
|
// is disabled.
|
2016-12-10 09:14:03 +00:00
|
|
|
func (c *Client) IsInChannel(channel string) bool {
|
2017-02-05 12:11:59 +00:00
|
|
|
if c.config.DisableTracking {
|
2017-02-05 12:09:48 +00:00
|
|
|
panic("Channels() used when tracking is disabled")
|
|
|
|
}
|
|
|
|
|
2016-12-10 11:43:26 +00:00
|
|
|
c.state.mu.RLock()
|
2016-12-10 09:14:03 +00:00
|
|
|
_, inChannel := c.state.channels[strings.ToLower(channel)]
|
2016-12-10 11:43:26 +00:00
|
|
|
c.state.mu.RUnlock()
|
2016-12-10 09:14:03 +00:00
|
|
|
|
|
|
|
return inChannel
|
|
|
|
}
|
|
|
|
|
2017-02-05 12:09:48 +00:00
|
|
|
// Join attempts to enter a list of IRC channels, at bulk if possible to
|
|
|
|
// prevent sending extensive JOIN commands.
|
2016-12-13 15:45:08 +00:00
|
|
|
func (c *Client) Join(channels ...string) error {
|
|
|
|
// We can join multiple channels at once, however we need to ensure that
|
|
|
|
// we are not exceeding the line length. (see maxLength)
|
|
|
|
max := maxLength - len(JOIN) - 1
|
|
|
|
|
|
|
|
var buffer string
|
|
|
|
var err error
|
|
|
|
|
|
|
|
for i := 0; i < len(channels); i++ {
|
|
|
|
if !IsValidChannel(channels[i]) {
|
|
|
|
return &ErrInvalidTarget{Target: channels[i]}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(buffer+","+channels[i]) > max {
|
|
|
|
err = c.Send(&Event{Command: JOIN, Params: []string{buffer}})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
buffer = ""
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(buffer) == 0 {
|
|
|
|
buffer = channels[i]
|
|
|
|
} else {
|
|
|
|
buffer += "," + channels[i]
|
|
|
|
}
|
|
|
|
|
|
|
|
if i == len(channels)-1 {
|
|
|
|
return c.Send(&Event{Command: JOIN, Params: []string{buffer}})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2016-12-13 14:34:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// JoinKey attempts to enter an IRC channel with a password.
|
|
|
|
func (c *Client) JoinKey(channel, password string) error {
|
|
|
|
if !IsValidChannel(channel) {
|
|
|
|
return &ErrInvalidTarget{Target: channel}
|
2016-11-13 11:44:12 +00:00
|
|
|
}
|
|
|
|
|
2016-12-13 14:34:08 +00:00
|
|
|
return c.Send(&Event{Command: JOIN, Params: []string{channel, password}})
|
2016-11-13 11:44:12 +00:00
|
|
|
}
|
|
|
|
|
2016-12-13 14:34:08 +00:00
|
|
|
// Part leaves an IRC channel.
|
2016-12-05 03:09:03 +00:00
|
|
|
func (c *Client) Part(channel, message string) error {
|
2016-12-07 00:17:35 +00:00
|
|
|
if !IsValidChannel(channel) {
|
|
|
|
return &ErrInvalidTarget{Target: channel}
|
|
|
|
}
|
|
|
|
|
2016-12-13 14:34:08 +00:00
|
|
|
return c.Send(&Event{Command: JOIN, Params: []string{channel}})
|
|
|
|
}
|
|
|
|
|
|
|
|
// PartMessage leaves an IRC channel with a specified leave message.
|
|
|
|
func (c *Client) PartMessage(channel, message string) error {
|
|
|
|
if !IsValidChannel(channel) {
|
|
|
|
return &ErrInvalidTarget{Target: channel}
|
2016-11-13 11:44:12 +00:00
|
|
|
}
|
|
|
|
|
2016-12-13 14:34:08 +00:00
|
|
|
return c.Send(&Event{Command: JOIN, Params: []string{channel}, Trailing: message})
|
2016-11-13 11:44:12 +00:00
|
|
|
}
|
2016-11-13 13:52:16 +00:00
|
|
|
|
2016-12-26 08:51:42 +00:00
|
|
|
// SendCTCP sends a CTCP request to target. Note that this method uses
|
|
|
|
// PRIVMSG specifically.
|
2016-12-24 05:15:41 +00:00
|
|
|
func (c *Client) SendCTCP(target, ctcpType, message string) error {
|
|
|
|
out := encodeCTCPRaw(ctcpType, message)
|
|
|
|
if out == "" {
|
|
|
|
return errors.New("invalid CTCP")
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.Message(target, out)
|
|
|
|
}
|
|
|
|
|
2016-12-26 08:51:42 +00:00
|
|
|
// SendCTCPf sends a CTCP request to target using a specific format. Note that
|
|
|
|
// this method uses PRIVMSG specifically.
|
2016-12-24 05:15:41 +00:00
|
|
|
func (c *Client) SendCTCPf(target, ctcpType, format string, a ...interface{}) error {
|
|
|
|
return c.SendCTCP(target, ctcpType, fmt.Sprintf(format, a...))
|
|
|
|
}
|
|
|
|
|
2016-12-26 08:51:42 +00:00
|
|
|
// SendCTCPReplyf sends a CTCP response to target using a specific format.
|
|
|
|
// Note that this method uses NOTICE specifically.
|
|
|
|
func (c *Client) SendCTCPReplyf(target, ctcpType, format string, a ...interface{}) error {
|
|
|
|
return c.SendCTCPReply(target, ctcpType, fmt.Sprintf(format, a...))
|
|
|
|
}
|
|
|
|
|
|
|
|
// SendCTCPReply sends a CTCP response to target. Note that this method uses
|
|
|
|
// NOTICE specifically.
|
|
|
|
func (c *Client) SendCTCPReply(target, ctcpType, message string) error {
|
|
|
|
out := encodeCTCPRaw(ctcpType, message)
|
|
|
|
if out == "" {
|
|
|
|
return errors.New("invalid CTCP")
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.Notice(target, out)
|
|
|
|
}
|
|
|
|
|
2016-12-09 10:58:18 +00:00
|
|
|
// Message sends a PRIVMSG to target (either channel, service, or user).
|
2016-12-05 03:09:03 +00:00
|
|
|
func (c *Client) Message(target, message string) error {
|
2016-12-07 00:17:35 +00:00
|
|
|
if !IsValidNick(target) && !IsValidChannel(target) {
|
|
|
|
return &ErrInvalidTarget{Target: target}
|
|
|
|
}
|
|
|
|
|
2016-12-05 03:09:03 +00:00
|
|
|
return c.Send(&Event{Command: PRIVMSG, Params: []string{target}, Trailing: message})
|
2016-11-13 13:52:16 +00:00
|
|
|
}
|
|
|
|
|
2016-12-09 10:58:18 +00:00
|
|
|
// Messagef sends a formated PRIVMSG to target (either channel, service, or
|
|
|
|
// user).
|
2016-12-05 03:09:03 +00:00
|
|
|
func (c *Client) Messagef(target, format string, a ...interface{}) error {
|
|
|
|
return c.Message(target, fmt.Sprintf(format, a...))
|
2016-11-13 13:52:16 +00:00
|
|
|
}
|
2016-11-13 14:00:39 +00:00
|
|
|
|
2016-12-09 10:58:18 +00:00
|
|
|
// Action sends a PRIVMSG ACTION (/me) to target (either channel, service,
|
|
|
|
// or user).
|
2016-12-05 03:09:03 +00:00
|
|
|
func (c *Client) Action(target, message string) error {
|
2016-12-07 00:17:35 +00:00
|
|
|
if !IsValidNick(target) && !IsValidChannel(target) {
|
|
|
|
return &ErrInvalidTarget{Target: target}
|
|
|
|
}
|
|
|
|
|
2016-12-05 03:09:03 +00:00
|
|
|
return c.Send(&Event{
|
|
|
|
Command: PRIVMSG,
|
|
|
|
Params: []string{target},
|
|
|
|
Trailing: fmt.Sprintf("\001ACTION %s\001", message),
|
|
|
|
})
|
2016-11-13 14:00:39 +00:00
|
|
|
}
|
|
|
|
|
2016-12-09 10:58:18 +00:00
|
|
|
// Actionf sends a formated PRIVMSG ACTION (/me) to target (either channel,
|
|
|
|
// service, or user).
|
2016-12-05 03:09:03 +00:00
|
|
|
func (c *Client) Actionf(target, format string, a ...interface{}) error {
|
|
|
|
return c.Action(target, fmt.Sprintf(format, a...))
|
2016-11-13 14:00:39 +00:00
|
|
|
}
|
2016-11-17 15:26:33 +00:00
|
|
|
|
|
|
|
// Notice sends a NOTICE to target (either channel, service, or user).
|
2016-12-05 03:09:03 +00:00
|
|
|
func (c *Client) Notice(target, message string) error {
|
2016-12-07 00:17:35 +00:00
|
|
|
if !IsValidNick(target) && !IsValidChannel(target) {
|
|
|
|
return &ErrInvalidTarget{Target: target}
|
|
|
|
}
|
|
|
|
|
2016-12-05 03:09:03 +00:00
|
|
|
return c.Send(&Event{Command: NOTICE, Params: []string{target}, Trailing: message})
|
2016-11-17 15:26:33 +00:00
|
|
|
}
|
|
|
|
|
2016-12-09 10:58:18 +00:00
|
|
|
// Noticef sends a formated NOTICE to target (either channel, service, or
|
|
|
|
// user).
|
2016-12-05 03:09:03 +00:00
|
|
|
func (c *Client) Noticef(target, format string, a ...interface{}) error {
|
|
|
|
return c.Notice(target, fmt.Sprintf(format, a...))
|
2016-11-17 15:26:33 +00:00
|
|
|
}
|
2016-11-17 19:55:09 +00:00
|
|
|
|
2016-12-09 10:58:18 +00:00
|
|
|
// SendRaw sends a raw string back to the server, without carriage returns
|
|
|
|
// or newlines.
|
2016-12-05 03:09:03 +00:00
|
|
|
func (c *Client) SendRaw(raw string) error {
|
2016-11-17 19:55:09 +00:00
|
|
|
e := ParseEvent(raw)
|
|
|
|
if e == nil {
|
2016-12-05 03:09:03 +00:00
|
|
|
return errors.New("invalid event: " + raw)
|
2016-11-17 19:55:09 +00:00
|
|
|
}
|
|
|
|
|
2016-12-05 03:09:03 +00:00
|
|
|
return c.Send(e)
|
2016-11-17 19:55:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// SendRawf sends a formated string back to the server, without carriage
|
|
|
|
// returns or newlines.
|
2016-12-05 03:09:03 +00:00
|
|
|
func (c *Client) SendRawf(format string, a ...interface{}) error {
|
|
|
|
return c.SendRaw(fmt.Sprintf(format, a...))
|
2016-11-17 19:55:09 +00:00
|
|
|
}
|
2016-11-23 17:46:12 +00:00
|
|
|
|
2016-12-10 10:54:41 +00:00
|
|
|
// Topic sets the topic of channel to message. Does not verify the length
|
|
|
|
// of the topic.
|
|
|
|
func (c *Client) Topic(channel, message string) error {
|
|
|
|
return c.Send(&Event{Command: TOPIC, Params: []string{channel}, Trailing: message})
|
|
|
|
}
|
2016-12-14 06:49:43 +00:00
|
|
|
|
2016-12-30 11:00:44 +00:00
|
|
|
// Who sends a WHO query to the server, which will attempt WHOX by default.
|
|
|
|
// See http://faerion.sourceforge.net/doc/irc/whox.var for more details. This
|
|
|
|
// sends "%tcuhnr,2" per default. Do not use "1" as this will conflict with
|
|
|
|
// girc's builtin tracking functionality.
|
2017-02-05 11:12:49 +00:00
|
|
|
func (c *Client) Who(target string) error {
|
|
|
|
if !IsValidNick(target) && !IsValidChannel(target) && !IsValidUser(target) {
|
|
|
|
return &ErrInvalidTarget{Target: target}
|
2016-12-14 06:49:43 +00:00
|
|
|
}
|
|
|
|
|
2017-02-05 12:09:48 +00:00
|
|
|
return c.Send(&Event{Command: WHO, Params: []string{target, "%tcuhnr,2"}})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Whois sends a WHOIS query to the server, targetted at a specific user.
|
|
|
|
// as WHOIS is a bit slower, you may want to use WHO for brief user info.
|
|
|
|
func (c *Client) Whois(nick string) error {
|
|
|
|
if !IsValidNick(nick) {
|
|
|
|
return &ErrInvalidTarget{Target: nick}
|
2016-12-14 06:49:43 +00:00
|
|
|
}
|
|
|
|
|
2017-02-05 12:09:48 +00:00
|
|
|
return c.Send(&Event{Command: WHOIS, Params: []string{nick}})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ping sends a PING query to the server, with a specific identifier that
|
|
|
|
// the server should respond with.
|
|
|
|
func (c *Client) Ping(id string) error {
|
|
|
|
return c.Send(&Event{Command: PING, Params: []string{id}})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Pong sends a PONG query to the server, with an identifier which was
|
|
|
|
// received from a previous PING query received by the client.
|
|
|
|
func (c *Client) Pong(id string) error {
|
|
|
|
return c.Send(&Event{Command: PONG, Params: []string{id}})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Oper sends a OPER authentication query to the server, with a username
|
|
|
|
// and password.
|
|
|
|
func (c *Client) Oper(user, pass string) error {
|
|
|
|
return c.Send(&Event{Command: OPER, Params: []string{user, pass}, Sensitive: true})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Kick sends a KICK query to the server, attempting to kick nick from
|
|
|
|
// channel, with reason. If reason is blank, one will not be sent to the
|
|
|
|
// server.
|
|
|
|
func (c *Client) Kick(channel, nick, reason string) error {
|
|
|
|
if !IsValidChannel(channel) {
|
|
|
|
return &ErrInvalidTarget{Target: channel}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !IsValidNick(nick) {
|
|
|
|
return &ErrInvalidTarget{Target: nick}
|
|
|
|
}
|
|
|
|
|
|
|
|
if reason != "" {
|
|
|
|
return c.Send(&Event{Command: KICK, Params: []string{channel, nick}, Trailing: reason})
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.Send(&Event{Command: KICK, Params: []string{channel, nick}})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Invite sends a INVITE query to the server, to invite nick to channel.
|
|
|
|
func (c *Client) Invite(channel, nick string) error {
|
|
|
|
if !IsValidChannel(channel) {
|
|
|
|
return &ErrInvalidTarget{Target: channel}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !IsValidNick(nick) {
|
|
|
|
return &ErrInvalidTarget{Target: nick}
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.Send(&Event{Command: INVITE, Params: []string{nick, channel}})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Away sends a AWAY query to the server, suggesting that the client is no
|
|
|
|
// longer active. If reason is blank, Client.Back() is called. Also see
|
|
|
|
// Client.Back().
|
|
|
|
func (c *Client) Away(reason string) error {
|
|
|
|
if reason == "" {
|
|
|
|
return c.Back()
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.Send(&Event{Command: AWAY, Params: []string{reason}})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Back sends a AWAY query to the server, however the query is blank,
|
|
|
|
// suggesting that the client is active once again. Also see Client.Away().
|
|
|
|
func (c *Client) Back() error {
|
|
|
|
return c.Send(&Event{Command: AWAY})
|
|
|
|
}
|
|
|
|
|
|
|
|
// LIST sends a LIST query to the server, which will list channels and topics.
|
|
|
|
// Supports multiple channels at once, in hopes it will reduce extensive
|
|
|
|
// LIST queries to the server. Supply no channels to run a list against the
|
|
|
|
// entire server (warning, that may mean LOTS of channels!)
|
|
|
|
func (c *Client) List(channels ...string) error {
|
|
|
|
if len(channels) == 0 {
|
|
|
|
return c.Send(&Event{Command: LIST})
|
|
|
|
}
|
|
|
|
|
|
|
|
// We can LIST multiple channels at once, however we need to ensure that
|
|
|
|
// we are not exceeding the line length. (see maxLength)
|
|
|
|
max := maxLength - len(JOIN) - 1
|
|
|
|
|
|
|
|
var buffer string
|
|
|
|
var err error
|
|
|
|
|
|
|
|
for i := 0; i < len(channels); i++ {
|
|
|
|
if !IsValidChannel(channels[i]) {
|
|
|
|
return &ErrInvalidTarget{Target: channels[i]}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(buffer+","+channels[i]) > max {
|
|
|
|
err = c.Send(&Event{Command: LIST, Params: []string{buffer}})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
buffer = ""
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(buffer) == 0 {
|
|
|
|
buffer = channels[i]
|
|
|
|
} else {
|
|
|
|
buffer += "," + channels[i]
|
|
|
|
}
|
|
|
|
|
|
|
|
if i == len(channels)-1 {
|
|
|
|
return c.Send(&Event{Command: LIST, Params: []string{buffer}})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2016-12-14 06:49:43 +00:00
|
|
|
}
|
|
|
|
|
2016-12-30 11:00:44 +00:00
|
|
|
// Whowas sends a WHOWAS query to the server. amount is the amount of results
|
|
|
|
// you want back.
|
|
|
|
func (c *Client) Whowas(nick string, amount int) error {
|
2016-12-14 06:49:43 +00:00
|
|
|
if !IsValidNick(nick) {
|
2016-12-30 11:00:44 +00:00
|
|
|
return &ErrInvalidTarget{Target: nick}
|
2016-12-14 06:49:43 +00:00
|
|
|
}
|
|
|
|
|
2016-12-30 11:00:44 +00:00
|
|
|
return c.Send(&Event{Command: WHOWAS, Params: []string{nick, string(amount)}})
|
2016-12-14 06:49:43 +00:00
|
|
|
}
|
2017-01-06 13:30:09 +00:00
|
|
|
|
|
|
|
// GetServerOption retrieves a server capability setting that was retrieved
|
2017-01-06 13:53:41 +00:00
|
|
|
// during client connection. This is also known as ISUPPORT (or RPL_PROTOCTL).
|
|
|
|
// Will panic if used when tracking has been disabled. Examples of usage:
|
2017-01-06 13:30:09 +00:00
|
|
|
//
|
|
|
|
// nickLen, success := GetServerOption("MAXNICKLEN")
|
|
|
|
//
|
|
|
|
func (c *Client) GetServerOption(key string) (result string, success bool) {
|
2017-02-05 12:11:59 +00:00
|
|
|
if c.config.DisableTracking {
|
2017-01-06 13:30:09 +00:00
|
|
|
panic("GetServerOption() used when tracking is disabled")
|
|
|
|
}
|
|
|
|
|
|
|
|
c.state.mu.Lock()
|
|
|
|
result, success = c.state.serverOptions[key]
|
|
|
|
c.state.mu.Unlock()
|
|
|
|
|
|
|
|
return result, success
|
|
|
|
}
|
|
|
|
|
|
|
|
// ServerName returns the server host/name that the server itself identifies
|
2017-01-06 13:40:01 +00:00
|
|
|
// as. May be empty if the server does not support RPL_MYINFO. Will panic if
|
|
|
|
// used when tracking has been disabled.
|
2017-01-06 13:30:09 +00:00
|
|
|
func (c *Client) ServerName() (name string) {
|
2017-02-05 12:11:59 +00:00
|
|
|
if c.config.DisableTracking {
|
2017-01-06 13:53:41 +00:00
|
|
|
panic("ServerName() used when tracking is disabled")
|
2017-01-06 13:40:01 +00:00
|
|
|
}
|
|
|
|
|
2017-01-06 13:30:09 +00:00
|
|
|
name, _ = c.GetServerOption("SERVER")
|
|
|
|
|
|
|
|
return name
|
|
|
|
}
|
|
|
|
|
|
|
|
// NetworkName returns the network identifier. E.g. "EsperNet", "ByteIRC".
|
2017-01-06 13:53:41 +00:00
|
|
|
// May be empty if the server does not support RPL_ISUPPORT (or RPL_PROTOCTL).
|
|
|
|
// Will panic if used when tracking has been disabled.
|
2017-01-06 13:30:09 +00:00
|
|
|
func (c *Client) NetworkName() (name string) {
|
2017-02-05 12:11:59 +00:00
|
|
|
if c.config.DisableTracking {
|
2017-01-06 13:53:41 +00:00
|
|
|
panic("NetworkName() used when tracking is disabled")
|
2017-01-06 13:40:01 +00:00
|
|
|
}
|
|
|
|
|
2017-01-06 13:30:09 +00:00
|
|
|
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
|
2017-01-06 13:40:01 +00:00
|
|
|
// does not support RPL_MYINFO. Will panic if used when tracking has been
|
|
|
|
// disabled.
|
2017-01-06 13:30:09 +00:00
|
|
|
func (c *Client) ServerVersion() (version string) {
|
2017-02-05 12:11:59 +00:00
|
|
|
if c.config.DisableTracking {
|
2017-01-06 13:53:41 +00:00
|
|
|
panic("ServerVersion() used when tracking is disabled")
|
2017-01-06 13:40:01 +00:00
|
|
|
}
|
|
|
|
|
2017-01-06 13:30:09 +00:00
|
|
|
version, _ = c.GetServerOption("VERSION")
|
|
|
|
|
|
|
|
return version
|
|
|
|
}
|
2017-01-06 13:40:01 +00:00
|
|
|
|
|
|
|
// ServerMOTD returns the servers message of the day, if the server has sent
|
2017-01-06 13:53:41 +00:00
|
|
|
// it upon connect. Will panic if used when tracking has been disabled.
|
2017-01-06 13:40:01 +00:00
|
|
|
func (c *Client) ServerMOTD() (motd string) {
|
2017-02-05 12:11:59 +00:00
|
|
|
if c.config.DisableTracking {
|
2017-01-06 13:53:41 +00:00
|
|
|
panic("ServerMOTD() used when tracking is disabled")
|
|
|
|
}
|
|
|
|
|
2017-01-06 13:40:01 +00:00
|
|
|
c.state.mu.Lock()
|
|
|
|
motd = c.state.motd
|
|
|
|
c.state.mu.Unlock()
|
|
|
|
|
|
|
|
return motd
|
|
|
|
}
|