girc-atomic/main.go

496 lines
14 KiB
Go
Raw Normal View History

// 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.
// 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).
//
// 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.
//
// See "example/main.go" for a brief and very useful example taking
// 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 (
2016-11-18 22:10:58 +00:00
"bytes"
2016-11-13 08:30:43 +00:00
"crypto/tls"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"time"
)
// Client contains all of the information necessary to run a single IRC
// client.
2016-11-13 08:30:43 +00:00
type Client struct {
// Config represents the configuration
Config Config
// Events is a buffer of events waiting to be processed.
Events chan *Event
// Sender is a Sender{} interface implementation.
Sender Sender
2016-11-14 11:59:08 +00:00
// state represents the internal state
state *state
// initTime represents the creation time of the client.
initTime time.Time
// callbacks is an internal mapping of COMMAND -> callback.
callbacks map[string][]Callback
// reader is the socket buffer reader from the IRC server.
reader *Decoder
// reader is the socket buffer write to the IRC server.
writer *Encoder
// conn is a net.Conn reference to the IRC server.
conn net.Conn
// 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
// quitChan is used to close the connection to the IRC server.
2016-11-17 19:59:35 +00:00
quitChan chan struct{}
// hasQuit is used to determine if we've finished quitting/cleaning up.
hasQuit bool
2016-11-13 08:30:43 +00:00
}
// 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
// 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
}
// 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-13 13:17:41 +00:00
Events: make(chan *Event, 40), // buffer 40 events
2016-11-17 19:59:35 +00:00
quitChan: make(chan struct{}),
2016-11-13 08:30:43 +00:00
callbacks: make(map[string][]Callback),
tries: 0,
initTime: time.Now(),
}
// Register builtin helpers.
2016-11-13 13:17:41 +00:00
client.registerHelpers()
2016-11-13 08:30:43 +00:00
return client
}
// Quit disconnects from the server.s
func (c *Client) Quit(message string) {
c.Send(&Event{Command: QUIT, Trailing: message})
c.hasQuit = true
2016-11-13 08:30:43 +00:00
if c.conn != nil {
c.conn.Close()
}
2016-11-17 19:59:35 +00:00
c.quitChan <- struct{}{}
2016-11-13 08:30:43 +00:00
}
// Uptime returns the amount of time that has passed since the
// client was created.
2016-11-13 08:30:43 +00:00
func (c *Client) Uptime() 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)
}
// 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
}
return c.Sender.Send(event)
}
// Connect attempts to connect to the given IRC server
func (c *Client) Connect() error {
var conn net.Conn
var err error
// Sanity check a few options.
2016-11-13 08:30:43 +00:00
if c.Config.Server == "" || c.Config.Port == 0 || c.Config.Nick == "" || c.Config.User == "" {
return errors.New("invalid configuration (server/port/nick/user)")
}
// Reset the state.
2016-11-14 11:59:08 +00:00
c.state = newState()
2016-11-13 08:30:43 +00:00
if c.Config.Logger == nil {
c.Config.Logger = ioutil.Discard
}
c.log = log.New(c.Config.Logger, "", log.Ldate|log.Ltime|log.Lshortfile)
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
}
c.conn = conn
c.reader = NewDecoder(conn)
c.writer = NewEncoder(conn)
c.Sender = serverSender{writer: c.writer}
for _, event := range c.connectMessages() {
if err := c.Send(event); err != nil {
return err
}
}
c.tries = 0
go c.ReadLoop()
// Consider the connection a success at this point.
2016-11-14 11:59:08 +00:00
c.state.connected = true
2016-11-13 08:30:43 +00:00
return nil
}
// 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) {
// 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}})
}
// Then nickname.
2016-11-13 08:30:43 +00:00
events = append(events, &Event{Command: NICK, Params: []string{c.Config.Nick}})
// 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
}
// Reconnect checks to make sure we want to, and then attempts to
// reconnect to the server.
func (c *Client) Reconnect() (err error) {
if c.hasQuit {
return nil
}
if c.Config.ReconnectDelay < (10 * time.Second) {
c.Config.ReconnectDelay = 10 * time.Second
}
2016-11-13 09:16:01 +00:00
if c.Config.MaxRetries > 0 {
2016-11-13 08:30:43 +00:00
var err error
c.conn.Close()
2016-11-13 08:30:43 +00:00
// Re-setup events.
2016-11-13 13:17:41 +00:00
c.Events = make(chan *Event, 40)
2016-11-13 09:16:01 +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++ {
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
}
return err
}
close(c.Events)
return nil
}
// 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-11-13 08:30:43 +00:00
func (c *Client) ReadLoop() error {
for {
c.conn.SetDeadline(time.Now().Add(300 * time.Second))
event, err := c.reader.Decode()
if err != nil {
return c.Reconnect()
}
c.Events <- event
}
}
// 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-13 10:46:44 +00:00
c.handleEvent(event)
2016-11-13 08:30:43 +00:00
case <-c.quitChan:
return
}
}
}
// 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
}
// GetNick returns the current nickname of the active connection.
2016-11-13 13:17:41 +00:00
//
// Returns empty string if tracking is disabled.
2016-11-13 08:30:43 +00:00
func (c *Client) GetNick() string {
if c.Config.DisableTracking {
return ""
}
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
}
// SetNick changes the client nickname.
2016-11-13 08:30:43 +00:00
func (c *Client) SetNick(name string) {
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-11-13 08:30:43 +00:00
c.Send(&Event{Command: NICK, Params: []string{name}})
}
// GetChannels returns the active list of channels that the client
// is in.
2016-11-13 13:17:41 +00:00
//
// Returns nil if tracking is disabled.
2016-11-13 08:30:43 +00:00
func (c *Client) GetChannels() map[string]*Channel {
if c.Config.DisableTracking {
return nil
}
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
}
// Who tells the client to update it's channel/user records.
//
// Does not update internal state if tracking is disabled.
func (c *Client) Who(target string) {
c.Send(&Event{Command: WHO, Params: []string{target, "%tcuhn,1"}})
2016-11-13 08:30:43 +00:00
}
// Join attempts to enter an IRC channel with an optional password.
func (c *Client) Join(channel, password string) {
if password != "" {
c.Send(&Event{Command: JOIN, Params: []string{channel, password}})
return
}
c.Send(&Event{Command: JOIN, Params: []string{channel}})
}
// Part leaves an IRC channel with an optional leave message.
func (c *Client) Part(channel, message string) {
if message != "" {
c.Send(&Event{Command: JOIN, Params: []string{channel}, Trailing: message})
return
}
c.Send(&Event{Command: JOIN, Params: []string{channel}})
}
// Message sends a PRIVMSG to target (either channel, service, or
// user).
func (c *Client) Message(target, message string) {
c.Send(&Event{Command: PRIVMSG, Params: []string{target}, Trailing: message})
}
// Messagef sends a formated PRIVMSG to target (either channel,
// service, or user).
func (c *Client) Messagef(target, format string, a ...interface{}) {
c.Message(target, fmt.Sprintf(format, a...))
}
// Action sends a PRIVMSG ACTION (/me) to target (either channel,
// service, or user).
func (c *Client) Action(target, message string) {
c.Send(&Event{Command: PRIVMSG, Params: []string{target}, Trailing: fmt.Sprintf("\001ACTION %s\001", message)})
}
// Actionf sends a formated PRIVMSG ACTION (/me) to target (either
// channel, service, or user).
func (c *Client) Actionf(target, format string, a ...interface{}) {
c.Action(target, fmt.Sprintf(format, a...))
}
// Notice sends a NOTICE to target (either channel, service, or user).
func (c *Client) Notice(target, message string) {
c.Send(&Event{Command: NOTICE, Params: []string{target}, Trailing: message})
}
// Noticef sends a formated NOTICE to target (either channel, service, or user).
func (c *Client) Noticef(target, format string, a ...interface{}) {
c.Notice(target, fmt.Sprintf(format, a...))
}
// SendRaw sends a raw string back to the server, without carriage returns or
// newlines.
func (c *Client) SendRaw(raw string) {
e := ParseEvent(raw)
if e == nil {
c.log.Printf("invalid event: %q", raw)
return
}
c.Send(e)
}
// SendRawf sends a formated string back to the server, without carriage
// returns or newlines.
func (c *Client) SendRawf(format string, a ...interface{}) {
c.SendRaw(fmt.Sprintf(format, a...))
}
2016-11-18 20:17:09 +00:00
2016-11-18 22:10:58 +00:00
// contains '*', even though this isn't RFC compliant, it's commonly used
var validChannelPrefixes = [...]string{"&", "#", "+", "!", "*"}
2016-11-18 20:17:09 +00:00
// IsValidChannel checks if channel is an RFC complaint channel or not
2016-11-18 22:10:58 +00:00
//
// channel = ( "#" / "+" / ( "!" channelid ) / "&" ) chanstring
// [ ":" chanstring ]
// chanstring = 0x01-0x07 / 0x08-0x09 / 0x0B-0x0C / 0x0E-0x1F / 0x21-0x2B
// chanstring = / 0x2D-0x39 / 0x3B-0xFF
// ; any octet except NUL, BELL, CR, LF, " ", "," and ":"
// channelid = 5( 0x41-0x5A / digit ) ; 5( A-Z / 0-9 )
2016-11-18 20:17:09 +00:00
func IsValidChannel(channel string) bool {
if len(channel) <= 1 || len(channel) > 50 {
return false
}
2016-11-18 22:10:58 +00:00
// #, +, !<channelid>, or &
// Including "*" in the prefix list, as this is commonly used (e.g. ZNC)
if bytes.IndexByte([]byte{0x21, 0x23, 0x26, 0x2A, 0x2B}, channel[0]) == -1 {
2016-11-18 20:17:09 +00:00
return false
}
2016-11-18 22:10:58 +00:00
// !<channelid> -- not very commonly supported, but we'll check it anyway.
// The ID must be 5 chars. This means min-channel size should be:
// 1 (prefix) + 5 (id) + 1 (+, channel name)
if channel[0] == 0x21 {
if len(channel) < 7 {
return false
}
// check for valid ID
for i := 1; i < 6; i++ {
if (channel[i] < 0x30 || channel[i] > 0x39) && (channel[i] < 0x41 || channel[i] > 0x5A) {
return false
}
}
}
// Check for invalid octets here.
bad := []byte{0x00, 0x07, 0x0D, 0x0A, 0x20, 0x2C, 0x3A}
for i := 1; i < len(channel); i++ {
if bytes.IndexByte(bad, channel[i]) != -1 {
return false
}
2016-11-18 20:17:09 +00:00
}
return true
}
// IsValidNick valids an IRC nickame. Note that this does not valid IRC
// nickname length.
2016-11-18 22:10:58 +00:00
//
// nickname = ( letter / special ) *8( letter / digit / special / "-" )
// letter = 0x41-0x5A / 0x61-0x7A
// digit = 0x30-0x39
// special = 0x5B-0x60 / 0x7B-0x7D
2016-11-18 20:17:09 +00:00
func IsValidNick(nick string) bool {
if len(nick) <= 0 {
return false
}
// Check the first index. Some characters aren't allowed for the first
// index of an IRC nickname.
if nick[0] < 0x41 || nick[0] > 0x7D {
// a-z, A-Z, and _\[]{}^|
return false
}
for i := 1; i < len(nick); i++ {
if (nick[i] < 0x41 || nick[i] > 0x7D) && (nick[i] < 0x30 || nick[i] > 0x39) && nick[i] != 0x2D {
// a-z, A-Z, 0-9, -, and _\[]{}^|
return false
}
}
return true
}