2016-11-14 11:50:14 +00:00
|
|
|
// Copyright 2016 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.
|
|
|
|
|
2016-11-23 18:01:10 +00:00
|
|
|
// Package girc provides a high level, yet flexible IRC library for use with
|
|
|
|
// interacting with IRC servers. girc has support for user/channel tracking,
|
|
|
|
// as well as a few other neat features (like auto-reconnect).
|
2016-11-14 11:50:14 +00:00
|
|
|
//
|
2016-11-23 18:01:10 +00:00
|
|
|
// Much of what girc can do, can also be disabled. The goal is to provide a
|
|
|
|
// solid API that you don't necessarily have to work with out of the box if
|
|
|
|
// you don't want to.
|
2016-11-14 11:50:14 +00:00
|
|
|
//
|
2016-11-23 18:01:10 +00:00
|
|
|
// See "examples/simple/main.go" for a brief and very useful example taking
|
2016-11-14 11:50:14 +00:00
|
|
|
// advantage of girc, that should give you a general idea of how the API
|
|
|
|
// works.
|
2016-11-13 08:30:43 +00:00
|
|
|
package girc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/tls"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"net"
|
2016-11-19 01:11:13 +00:00
|
|
|
"sync"
|
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 {
|
2016-11-14 11:50:14 +00:00
|
|
|
// Config represents the configuration
|
|
|
|
Config Config
|
|
|
|
// 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-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
|
|
|
|
// log is used if a writer is supplied for Client.Config.Logger.
|
|
|
|
log *log.Logger
|
2016-11-19 17:36:33 +00:00
|
|
|
// quitChan is used to stop the client loop. See Client.Stop().
|
2016-11-17 19:59:35 +00:00
|
|
|
quitChan 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
|
|
|
|
// Logger is an optional, user supplied logger to log the raw lines sent
|
|
|
|
// from the server. Useful for debugging. Defaults to ioutil.Discard.
|
|
|
|
Logger io.Writer
|
|
|
|
// 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
|
|
|
|
// 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.
|
|
|
|
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-23 17:40:14 +00:00
|
|
|
// ErrCallbackTimedout is used when we need to wait for temporary callbacks.
|
2016-12-07 00:27:25 +00:00
|
|
|
type ErrCallbackTimedout struct {
|
|
|
|
// ID is the identified of the callback in the callback stack.
|
|
|
|
ID string
|
|
|
|
// Timeout is the time that past before the callback timed out.
|
|
|
|
Timeout time.Duration
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *ErrCallbackTimedout) Error() string {
|
2016-12-07 00:39:54 +00:00
|
|
|
return "callback [" + e.ID + "] timed out while waiting for response from the server: " + e.Timeout.String()
|
2016-12-07 00:27:25 +00:00
|
|
|
}
|
2016-11-23 17:40:14 +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-11-14 11:50:14 +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{
|
|
|
|
Config: config,
|
2016-11-22 20:51:58 +00:00
|
|
|
Events: make(chan *Event, 100), // buffer 100 events
|
2016-11-17 19:59:35 +00:00
|
|
|
quitChan: make(chan struct{}),
|
2016-12-07 10:50:14 +00:00
|
|
|
Callbacks: newCaller(),
|
2016-11-13 08:30:43 +00:00
|
|
|
initTime: time.Now(),
|
|
|
|
}
|
|
|
|
|
2016-11-19 02:38:23 +00:00
|
|
|
if client.Config.Logger == nil {
|
|
|
|
client.Config.Logger = ioutil.Discard
|
|
|
|
}
|
|
|
|
client.log = log.New(client.Config.Logger, "", log.Ldate|log.Ltime|log.Lshortfile)
|
|
|
|
|
2016-11-14 11:50:14 +00:00
|
|
|
// Register builtin helpers.
|
2016-11-13 13:17:41 +00:00
|
|
|
client.registerHelpers()
|
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-11-19 17:36:33 +00:00
|
|
|
c.state.hasQuit = true
|
|
|
|
defer func() {
|
2016-11-23 18:01:10 +00:00
|
|
|
// Unset c.hasQuit, so we can reconnect if we want to.
|
2016-11-19 17:36:33 +00:00
|
|
|
c.state.hasQuit = false
|
|
|
|
}()
|
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-11-23 03:03:48 +00:00
|
|
|
if c.state == nil {
|
|
|
|
return
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
|
|
|
|
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-11-19 17:36:33 +00:00
|
|
|
// Stop exits the clients main loop. Use Client.Quit() if you want to disconnect
|
|
|
|
// the client from the server/connection.
|
|
|
|
func (c *Client) Stop() {
|
2016-11-19 16:13:49 +00:00
|
|
|
// Send to the quit channel, so if Client.Loop() is being used, this will
|
|
|
|
// return.
|
2016-11-17 19:59:35 +00:00
|
|
|
c.quitChan <- struct{}{}
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
|
|
|
|
2016-12-09 10:32:50 +00:00
|
|
|
// Lifetime returns the amount of time that has passed since the client was
|
2016-11-25 00:18:39 +00:00
|
|
|
// created.
|
2016-12-09 10:32:50 +00:00
|
|
|
func (c *Client) Lifetime() time.Duration {
|
2016-11-14 10:20:45 +00:00
|
|
|
return time.Since(c.initTime)
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2016-11-14 11:50:14 +00:00
|
|
|
// Send sends an event to the server. Use Client.RunCallback() if you are
|
|
|
|
// are simply looking to trigger callbacks with an event.
|
2016-11-13 08:30:43 +00:00
|
|
|
func (c *Client) Send(event *Event) error {
|
|
|
|
// log the event
|
|
|
|
if !event.Sensitive {
|
2016-11-14 10:23:30 +00:00
|
|
|
c.log.Print("--> ", event.String())
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
|
|
|
|
2016-11-23 03:03:48 +00:00
|
|
|
return c.state.writer.Encode(event)
|
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.
|
2016-11-22 16:21:25 +00:00
|
|
|
if c.Config.Server == "" {
|
|
|
|
return errors.New("invalid server specified")
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.Config.Port < 21 || c.Config.Port > 65535 {
|
|
|
|
return errors.New("invalid port (21-65535)")
|
|
|
|
}
|
|
|
|
|
|
|
|
if !IsValidNick(c.Config.Nick) || !IsValidUser(c.Config.User) {
|
|
|
|
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
|
|
|
|
2016-11-23 03:03:48 +00:00
|
|
|
c.log.Printf("connecting to %s...", c.Server())
|
|
|
|
|
2016-11-22 16:21:25 +00:00
|
|
|
// Allow the user to specify their own net.Conn.
|
|
|
|
if c.Config.Conn == nil {
|
|
|
|
if c.Config.TLSConfig == nil {
|
|
|
|
conn, err = net.Dial("tcp", c.Server())
|
|
|
|
} else {
|
|
|
|
conn, err = tls.Dial("tcp", c.Server(), c.Config.TLSConfig)
|
|
|
|
}
|
|
|
|
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 {
|
2016-11-23 03:03:48 +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-11-13 08:30:43 +00:00
|
|
|
for _, event := range c.connectMessages() {
|
|
|
|
if err := c.Send(event); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-19 15:55:36 +00:00
|
|
|
go c.readLoop()
|
2016-11-13 08:30:43 +00:00
|
|
|
|
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-11-25 00:18:39 +00:00
|
|
|
c.state.m.Lock()
|
|
|
|
ctime := time.Now()
|
|
|
|
c.state.connTime = &ctime
|
|
|
|
c.state.connected = true
|
|
|
|
c.state.m.Unlock()
|
|
|
|
|
2016-11-13 08:30:43 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-12-09 10:32:50 +00:00
|
|
|
// Uptime is the time at which the client successfully connected to the
|
2016-11-25 00:18:39 +00:00
|
|
|
// server.
|
2016-12-09 10:32:50 +00:00
|
|
|
func (c *Client) Uptime() (*time.Time, error) {
|
2016-11-25 00:18:39 +00:00
|
|
|
c.state.m.RLock()
|
|
|
|
defer c.state.m.RUnlock()
|
|
|
|
|
|
|
|
if !c.state.connected {
|
|
|
|
return nil, ErrNotConnected
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.state.connTime, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ConnSince is the duration that has past since the client successfully
|
|
|
|
// connected to the server.
|
|
|
|
func (c *Client) ConnSince() (*time.Duration, error) {
|
|
|
|
c.state.m.RLock()
|
|
|
|
defer c.state.m.RUnlock()
|
|
|
|
|
|
|
|
if !c.state.connected {
|
|
|
|
return nil, ErrNotConnected
|
|
|
|
}
|
|
|
|
|
|
|
|
since := time.Since(*c.state.connTime)
|
|
|
|
|
|
|
|
return &since, 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.
|
2016-11-13 08:30:43 +00:00
|
|
|
if c.Config.Password != "" {
|
|
|
|
events = append(events, &Event{Command: PASS, Params: []string{c.Config.Password}})
|
|
|
|
}
|
|
|
|
|
2016-11-14 11:50:14 +00:00
|
|
|
// Then nickname.
|
2016-11-13 08:30:43 +00:00
|
|
|
events = append(events, &Event{Command: NICK, Params: []string{c.Config.Nick}})
|
|
|
|
|
2016-11-14 11:50:14 +00:00
|
|
|
// Then username and realname.
|
2016-11-13 08:30:43 +00:00
|
|
|
if c.Config.Name == "" {
|
|
|
|
c.Config.Name = c.Config.User
|
|
|
|
}
|
|
|
|
|
|
|
|
events = append(events, &Event{
|
|
|
|
Command: USER,
|
|
|
|
Params: []string{c.Config.User, "+iw", "*"},
|
|
|
|
Trailing: c.Config.Name,
|
|
|
|
})
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
if c.state.hasQuit {
|
|
|
|
return nil
|
|
|
|
}
|
2016-11-19 16:13:49 +00:00
|
|
|
|
2016-11-14 11:50:14 +00:00
|
|
|
if c.Config.ReconnectDelay < (10 * time.Second) {
|
|
|
|
c.Config.ReconnectDelay = 10 * time.Second
|
|
|
|
}
|
2016-11-13 09:16:01 +00:00
|
|
|
|
2016-11-23 03:03:48 +00:00
|
|
|
if c.state.connected {
|
|
|
|
c.Quit("reconnecting...")
|
2016-11-19 17:36:33 +00:00
|
|
|
}
|
|
|
|
|
2016-11-14 11:50:14 +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.
|
|
|
|
c.log.Printf("reconnecting to %s in %s", c.Server(), c.Config.ReconnectDelay)
|
|
|
|
time.Sleep(c.Config.ReconnectDelay)
|
2016-11-13 08:30:43 +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
|
2016-11-14 11:50:14 +00:00
|
|
|
c.log.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.
|
|
|
|
func (c *Client) readLoop() error {
|
2016-11-13 08:30:43 +00:00
|
|
|
for {
|
2016-12-07 10:50:14 +00:00
|
|
|
if c.state == nil {
|
|
|
|
return ErrNotConnected
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.state.reconnecting || c.state.hasQuit {
|
|
|
|
return ErrNotConnected
|
|
|
|
}
|
|
|
|
|
2016-11-23 03:03:48 +00:00
|
|
|
c.state.conn.SetDeadline(time.Now().Add(300 * time.Second))
|
|
|
|
event, err := c.state.reader.Decode()
|
2016-11-13 08:30:43 +00:00
|
|
|
if err != nil {
|
2016-11-19 17:36:33 +00:00
|
|
|
// And attempt a reconnect (if applicable).
|
2016-11-13 08:30:43 +00:00
|
|
|
return c.Reconnect()
|
|
|
|
}
|
|
|
|
|
|
|
|
c.Events <- event
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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-11-13 08:30:43 +00:00
|
|
|
case <-c.quitChan:
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-14 11:50:14 +00:00
|
|
|
// IsConnected returns true if the client is connected to the server.
|
2016-11-13 08:30:43 +00:00
|
|
|
func (c *Client) IsConnected() bool {
|
2016-11-14 11:59:08 +00:00
|
|
|
c.state.m.RLock()
|
|
|
|
defer c.state.m.RUnlock()
|
2016-11-13 08:30:43 +00:00
|
|
|
|
2016-11-14 11:59:08 +00:00
|
|
|
return c.state.connected
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
|
|
|
|
2016-11-14 11:50:14 +00:00
|
|
|
// GetNick returns the current nickname of the active connection.
|
2016-11-13 13:17:41 +00:00
|
|
|
//
|
2016-11-14 11:50:14 +00:00
|
|
|
// Returns empty string if tracking is disabled.
|
2016-11-13 08:30:43 +00:00
|
|
|
func (c *Client) GetNick() string {
|
2016-11-14 11:50:14 +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-11-14 11:59:08 +00:00
|
|
|
c.state.m.RLock()
|
|
|
|
defer c.state.m.RUnlock()
|
2016-11-13 08:30:43 +00:00
|
|
|
|
2016-11-14 11:59:08 +00:00
|
|
|
if c.state.nick == "" {
|
2016-11-13 08:30:43 +00:00
|
|
|
return c.Config.Nick
|
|
|
|
}
|
|
|
|
|
2016-11-14 11:59:08 +00:00
|
|
|
return c.state.nick
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
|
|
|
|
2016-11-14 11:50:14 +00:00
|
|
|
// SetNick changes the client nickname.
|
2016-12-05 03:09:03 +00:00
|
|
|
func (c *Client) SetNick(name string) error {
|
2016-12-07 00:39:54 +00:00
|
|
|
if !c.state.connected {
|
|
|
|
return ErrNotConnected
|
|
|
|
}
|
|
|
|
|
2016-12-07 00:17:35 +00:00
|
|
|
if !IsValidNick(name) {
|
|
|
|
return &ErrInvalidTarget{Target: name}
|
|
|
|
}
|
|
|
|
|
2016-11-14 11:59:08 +00:00
|
|
|
c.state.m.Lock()
|
|
|
|
defer c.state.m.Unlock()
|
2016-11-13 08:30:43 +00:00
|
|
|
|
2016-11-14 11:59:08 +00:00
|
|
|
c.state.nick = name
|
2016-12-05 03:09:03 +00:00
|
|
|
return c.Send(&Event{Command: NICK, Params: []string{name}})
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
|
|
|
|
2016-11-14 11:50:14 +00:00
|
|
|
// GetChannels returns the active list of channels that the client
|
|
|
|
// is in.
|
2016-11-13 13:17:41 +00:00
|
|
|
//
|
2016-11-14 11:50:14 +00:00
|
|
|
// Returns nil if tracking is disabled.
|
2016-11-13 08:30:43 +00:00
|
|
|
func (c *Client) GetChannels() map[string]*Channel {
|
2016-11-14 11:50:14 +00:00
|
|
|
if c.Config.DisableTracking {
|
2016-11-22 04:55:33 +00:00
|
|
|
panic("GetChannels() used when tracking is disabled")
|
2016-11-14 11:50:14 +00:00
|
|
|
}
|
|
|
|
|
2016-11-14 11:59:08 +00:00
|
|
|
c.state.m.RLock()
|
|
|
|
defer c.state.m.RUnlock()
|
2016-11-13 08:30:43 +00:00
|
|
|
|
2016-11-14 11:59:08 +00:00
|
|
|
return c.state.channels
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
|
|
|
|
2016-11-14 11:50:14 +00:00
|
|
|
// Who tells the client to update it's channel/user records.
|
|
|
|
//
|
|
|
|
// Does not update internal state if tracking is disabled.
|
2016-12-05 03:09:03 +00:00
|
|
|
func (c *Client) Who(target 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: WHO, Params: []string{target, "%tcuhn,1"}})
|
2016-11-13 08:30:43 +00:00
|
|
|
}
|
2016-11-13 11:44:12 +00:00
|
|
|
|
2016-11-14 11:50:14 +00:00
|
|
|
// Join attempts to enter an IRC channel with an optional password.
|
2016-12-05 03:09:03 +00:00
|
|
|
func (c *Client) Join(channel, password string) error {
|
2016-12-07 00:17:35 +00:00
|
|
|
if !IsValidChannel(channel) {
|
|
|
|
return &ErrInvalidTarget{Target: channel}
|
|
|
|
}
|
|
|
|
|
2016-12-07 00:39:54 +00:00
|
|
|
if !c.state.connected {
|
|
|
|
return ErrNotConnected
|
|
|
|
}
|
|
|
|
|
2016-11-13 11:44:12 +00:00
|
|
|
if password != "" {
|
2016-12-05 03:09:03 +00:00
|
|
|
return c.Send(&Event{Command: JOIN, Params: []string{channel, password}})
|
2016-11-13 11:44:12 +00:00
|
|
|
}
|
|
|
|
|
2016-12-05 03:09:03 +00:00
|
|
|
return c.Send(&Event{Command: JOIN, Params: []string{channel}})
|
2016-11-13 11:44:12 +00:00
|
|
|
}
|
|
|
|
|
2016-11-14 11:50:14 +00:00
|
|
|
// Part leaves an IRC channel with an optional leave message.
|
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-07 00:39:54 +00:00
|
|
|
if !c.state.connected {
|
|
|
|
return ErrNotConnected
|
|
|
|
}
|
|
|
|
|
2016-11-13 11:44:12 +00:00
|
|
|
if message != "" {
|
2016-12-05 03:09:03 +00:00
|
|
|
return c.Send(&Event{Command: JOIN, Params: []string{channel}, Trailing: message})
|
2016-11-13 11:44:12 +00:00
|
|
|
}
|
|
|
|
|
2016-12-05 03:09:03 +00:00
|
|
|
return c.Send(&Event{Command: JOIN, Params: []string{channel}})
|
2016-11-13 11:44:12 +00:00
|
|
|
}
|
2016-11-13 13:52:16 +00:00
|
|
|
|
2016-11-14 11:50:14 +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-07 00:39:54 +00:00
|
|
|
if !c.state.connected {
|
|
|
|
return ErrNotConnected
|
|
|
|
}
|
|
|
|
|
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-11-14 11:50:14 +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-11-14 11:50:14 +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-07 00:39:54 +00:00
|
|
|
if !c.state.connected {
|
|
|
|
return ErrNotConnected
|
|
|
|
}
|
|
|
|
|
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-11-14 11:50:14 +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-07 00:39:54 +00:00
|
|
|
if !c.state.connected {
|
|
|
|
return ErrNotConnected
|
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
|
// 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-07 00:39:54 +00:00
|
|
|
if !c.state.connected {
|
|
|
|
return ErrNotConnected
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
|
|
// Whowas sends and waits for a response to a WHOWAS query to the server.
|
|
|
|
// Returns the list of users form the WHOWAS query.
|
|
|
|
func (c *Client) Whowas(nick string) ([]*User, error) {
|
2016-12-07 00:17:35 +00:00
|
|
|
if !IsValidNick(nick) {
|
|
|
|
return nil, &ErrInvalidTarget{Target: nick}
|
|
|
|
}
|
|
|
|
|
2016-12-07 00:39:54 +00:00
|
|
|
if !c.state.connected {
|
|
|
|
return nil, ErrNotConnected
|
|
|
|
}
|
|
|
|
|
2016-11-23 17:46:12 +00:00
|
|
|
var mu sync.Mutex
|
|
|
|
var events []*Event
|
|
|
|
whoDone := make(chan struct{})
|
|
|
|
|
|
|
|
// One callback needs to be added to collect the WHOWAS lines.
|
|
|
|
// <nick> <user> <host> * :<real_name>
|
2016-12-07 10:50:14 +00:00
|
|
|
whoCb := c.Callbacks.AddBg(RPL_WHOWASUSER, func(c *Client, e Event) {
|
2016-11-23 17:46:12 +00:00
|
|
|
if len(e.Params) != 5 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// First check and make sure that this WHOWAS is for us.
|
|
|
|
if e.Params[1] != nick {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
mu.Lock()
|
|
|
|
events = append(events, &e)
|
|
|
|
mu.Unlock()
|
|
|
|
})
|
|
|
|
|
|
|
|
// One more callback needs to be added to let us know when WHOWAS has
|
|
|
|
// finished.
|
|
|
|
// <nick> :<info>
|
2016-12-07 10:50:14 +00:00
|
|
|
whoDoneCb := c.Callbacks.AddBg(RPL_ENDOFWHOWAS, func(c *Client, e Event) {
|
2016-11-23 17:46:12 +00:00
|
|
|
if len(e.Params) != 2 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// First check and make sure that this WHOWAS is for us.
|
|
|
|
if e.Params[1] != nick {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
mu.Lock()
|
|
|
|
whoDone <- struct{}{}
|
|
|
|
mu.Unlock()
|
|
|
|
})
|
|
|
|
|
|
|
|
// Send the WHOWAS query.
|
|
|
|
c.Send(&Event{Command: WHOWAS, Params: []string{nick, "10"}})
|
|
|
|
|
|
|
|
// Wait for everything to finish. Give the server 2 seconds to respond.
|
|
|
|
select {
|
|
|
|
case <-whoDone:
|
|
|
|
close(whoDone)
|
|
|
|
case <-time.After(time.Second * 2):
|
|
|
|
// Remove callbacks and return. Took too long.
|
2016-12-07 10:50:14 +00:00
|
|
|
c.Callbacks.Remove(whoCb)
|
|
|
|
c.Callbacks.Remove(whoDoneCb)
|
2016-11-23 17:46:12 +00:00
|
|
|
|
2016-12-07 00:27:25 +00:00
|
|
|
return nil, &ErrCallbackTimedout{
|
|
|
|
ID: whoCb + " + " + whoDoneCb,
|
|
|
|
Timeout: time.Second * 2,
|
|
|
|
}
|
2016-11-23 17:46:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Remove the temporary callbacks to ensure that nothing else is
|
|
|
|
// received.
|
2016-12-07 10:50:14 +00:00
|
|
|
c.Callbacks.Remove(whoCb)
|
|
|
|
c.Callbacks.Remove(whoDoneCb)
|
2016-11-23 17:46:12 +00:00
|
|
|
|
|
|
|
var users []*User
|
|
|
|
|
|
|
|
for i := 0; i < len(events); i++ {
|
|
|
|
users = append(users, &User{
|
|
|
|
Nick: events[i].Params[1],
|
|
|
|
Ident: events[i].Params[2],
|
|
|
|
Host: events[i].Params[3],
|
|
|
|
Name: events[i].Trailing,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return users, nil
|
|
|
|
}
|