Ever cook bacon on your CPU cooler?
This commit is contained in:
parent
771323f162
commit
01ce96b07a
26
README.md
26
README.md
|
@ -1,10 +1,10 @@
|
|||
<p align="center"><a href="https://godoc.org/github.com/lrstanley/girc"><img width="270" src="http://i.imgur.com/DEnyrdB.png"></a></p>
|
||||
<p align="center"><a href="https://godoc.org/git.tcp.direct/kayos/girc-atomic"><img width="270" src="http://i.imgur.com/DEnyrdB.png"></a></p>
|
||||
<p align="center">girc, a flexible IRC library for Go</p>
|
||||
<p align="center">
|
||||
<a href="https://travis-ci.org/lrstanley/girc"><img src="https://travis-ci.org/lrstanley/girc.svg?branch=master" alt="Build Status"></a>
|
||||
<a href="https://codecov.io/gh/lrstanley/girc"><img src="https://codecov.io/gh/lrstanley/girc/branch/master/graph/badge.svg" alt="Coverage Status"></a>
|
||||
<a href="https://godoc.org/github.com/lrstanley/girc"><img src="https://godoc.org/github.com/lrstanley/girc?status.png" alt="GoDoc"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/lrstanley/girc"><img src="https://goreportcard.com/badge/github.com/lrstanley/girc" alt="Go Report Card"></a>
|
||||
<a href="https://godoc.org/git.tcp.direct/kayos/girc-atomic"><img src="https://godoc.org/git.tcp.direct/kayos/girc-atomic?status.png" alt="GoDoc"></a>
|
||||
<a href="https://goreportcard.com/report/git.tcp.direct/kayos/girc-atomic"><img src="https://goreportcard.com/badge/git.tcp.direct/kayos/girc-atomic" alt="Go Report Card"></a>
|
||||
<a href="https://byteirc.org/channel/%23%2Fdev%2Fnull"><img src="https://img.shields.io/badge/ByteIRC-%23%2Fdev%2Fnull-blue.svg" alt="IRC Chat"></a>
|
||||
</p>
|
||||
|
||||
|
@ -17,32 +17,32 @@ you're using won't have breaking changes**
|
|||
## Features
|
||||
|
||||
- Focuses on simplicity, yet tries to still be flexible.
|
||||
- Only requires [standard library packages](https://godoc.org/github.com/lrstanley/girc?imports)
|
||||
- Event based triggering/responses ([example](https://godoc.org/github.com/lrstanley/girc#ex-package--Commands), and [CTCP too](https://godoc.org/github.com/lrstanley/girc#Commands.SendCTCP)!)
|
||||
- [Documentation](https://godoc.org/github.com/lrstanley/girc) is _mostly_ complete.
|
||||
- Only requires [standard library packages](https://godoc.org/git.tcp.direct/kayos/girc-atomic?imports)
|
||||
- Event based triggering/responses ([example](https://godoc.org/git.tcp.direct/kayos/girc-atomic#ex-package--Commands), and [CTCP too](https://godoc.org/git.tcp.direct/kayos/girc-atomic#Commands.SendCTCP)!)
|
||||
- [Documentation](https://godoc.org/git.tcp.direct/kayos/girc-atomic) is _mostly_ complete.
|
||||
- Support for almost all of the [IRCv3 spec](http://ircv3.net/software/libraries.html).
|
||||
- SASL Auth (currently only `PLAIN` and `EXTERNAL` is support by default,
|
||||
however you can simply implement `SASLMech` yourself to support additional
|
||||
mechanisms.)
|
||||
- Message tags (things like `account-tag` on by default)
|
||||
- `account-notify`, `away-notify`, `chghost`, `extended-join`, etc -- all handled seemlessly ([cap.go](https://github.com/lrstanley/girc/blob/master/cap.go) for more info).
|
||||
- `account-notify`, `away-notify`, `chghost`, `extended-join`, etc -- all handled seemlessly ([cap.go](https://git.tcp.direct/kayos/girc-atomic/blob/master/cap.go) for more info).
|
||||
- Channel and user tracking. Easily find what users are in a channel, if a
|
||||
user is away, or if they are authenticated (if the server supports it!)
|
||||
- Client state/capability tracking. Easy methods to access capability data ([LookupChannel](https://godoc.org/github.com/lrstanley/girc#Client.LookupChannel), [LookupUser](https://godoc.org/github.com/lrstanley/girc#Client.LookupUser), [GetServerOption (ISUPPORT)](https://godoc.org/github.com/lrstanley/girc#Client.GetServerOption), etc.)
|
||||
- Client state/capability tracking. Easy methods to access capability data ([LookupChannel](https://godoc.org/git.tcp.direct/kayos/girc-atomic#Client.LookupChannel), [LookupUser](https://godoc.org/git.tcp.direct/kayos/girc-atomic#Client.LookupUser), [GetServerOption (ISUPPORT)](https://godoc.org/git.tcp.direct/kayos/girc-atomic#Client.GetServerOption), etc.)
|
||||
- Built-in support for things you would commonly have to implement yourself.
|
||||
- Nick collision detection and prevention (also see [Config.HandleNickCollide](https://godoc.org/github.com/lrstanley/girc#Config).)
|
||||
- Nick collision detection and prevention (also see [Config.HandleNickCollide](https://godoc.org/git.tcp.direct/kayos/girc-atomic#Config).)
|
||||
- Event/message rate limiting.
|
||||
- Channel, nick, and user validation methods ([IsValidChannel](https://godoc.org/github.com/lrstanley/girc#IsValidChannel), [IsValidNick](https://godoc.org/github.com/lrstanley/girc#IsValidNick), etc.)
|
||||
- CTCP handling and auto-responses ([CTCP](https://godoc.org/github.com/lrstanley/girc#CTCP))
|
||||
- Channel, nick, and user validation methods ([IsValidChannel](https://godoc.org/git.tcp.direct/kayos/girc-atomic#IsValidChannel), [IsValidNick](https://godoc.org/git.tcp.direct/kayos/girc-atomic#IsValidNick), etc.)
|
||||
- CTCP handling and auto-responses ([CTCP](https://godoc.org/git.tcp.direct/kayos/girc-atomic#CTCP))
|
||||
- And more!
|
||||
|
||||
## Installing
|
||||
|
||||
$ go get -u github.com/lrstanley/girc
|
||||
$ go get -u git.tcp.direct/kayos/girc-atomic
|
||||
|
||||
## Examples
|
||||
|
||||
See [the examples](https://godoc.org/github.com/lrstanley/girc#example-package--Bare)
|
||||
See [the examples](https://godoc.org/git.tcp.direct/kayos/girc-atomic#example-package--Bare)
|
||||
within the documentation for real-world usecases. Here are a few real-world
|
||||
usecases/examples/projects which utilize girc:
|
||||
|
||||
|
|
28
builtin.go
28
builtin.go
|
@ -9,6 +9,11 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
stateUnlocked uint32 = iota
|
||||
stateLocked
|
||||
)
|
||||
|
||||
// registerBuiltin sets up built-in handlers, based on client
|
||||
// configuration.
|
||||
func (c *Client) registerBuiltins() {
|
||||
|
@ -85,18 +90,14 @@ func handleConnect(c *Client, e Event) {
|
|||
// the one we supplied during connection, but some networks will rename
|
||||
// users on connect.
|
||||
if len(e.Params) > 0 {
|
||||
c.state.Lock()
|
||||
c.state.nick = e.Params[0]
|
||||
c.state.Unlock()
|
||||
c.state.nick.Store(e.Params[0])
|
||||
|
||||
c.state.notify(c, UPDATE_GENERAL)
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
c.mu.RLock()
|
||||
server := c.server()
|
||||
c.mu.RUnlock()
|
||||
c.RunHandlers(&Event{Command: CONNECTED, Params: []string{server}})
|
||||
}
|
||||
|
||||
|
@ -117,9 +118,7 @@ func handlePING(c *Client, e Event) {
|
|||
}
|
||||
|
||||
func handlePONG(c *Client, e Event) {
|
||||
c.conn.mu.Lock()
|
||||
c.conn.lastPong = time.Now()
|
||||
c.conn.mu.Unlock()
|
||||
c.conn.lastPong.Store(time.Now())
|
||||
}
|
||||
|
||||
// handleJOIN ensures that the state has updated users and channels.
|
||||
|
@ -131,11 +130,11 @@ func handleJOIN(c *Client, e Event) {
|
|||
channelName := e.Params[0]
|
||||
|
||||
c.state.Lock()
|
||||
defer c.state.Unlock()
|
||||
|
||||
channel := c.state.lookupChannel(channelName)
|
||||
if channel == nil {
|
||||
if ok := c.state.createChannel(channelName); !ok {
|
||||
c.state.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -145,7 +144,6 @@ func handleJOIN(c *Client, e Event) {
|
|||
user := c.state.lookupUser(e.Source.Name)
|
||||
if user == nil {
|
||||
if ok := c.state.createUser(e.Source); !ok {
|
||||
c.state.Unlock()
|
||||
return
|
||||
}
|
||||
user = c.state.lookupUser(e.Source.Name)
|
||||
|
@ -166,7 +164,6 @@ func handleJOIN(c *Client, e Event) {
|
|||
user.Extras.Name = e.Params[2]
|
||||
}
|
||||
}
|
||||
c.state.Unlock()
|
||||
|
||||
if e.Source.ID() == c.GetID() {
|
||||
// If it's us, don't just add our user to the list. Run a WHO which
|
||||
|
@ -178,10 +175,8 @@ func handleJOIN(c *Client, e Event) {
|
|||
|
||||
// Update our ident and host too, in state -- since there is no
|
||||
// cleaner method to do this.
|
||||
c.state.Lock()
|
||||
c.state.ident = e.Source.Ident
|
||||
c.state.host = e.Source.Host
|
||||
c.state.Unlock()
|
||||
c.state.ident.Store(e.Source.Ident)
|
||||
c.state.host.Store(e.Source.Host)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -448,7 +443,6 @@ func handleNAMES(c *Client, e Event) {
|
|||
|
||||
var modes, nick string
|
||||
var ok bool
|
||||
s := &Source{}
|
||||
|
||||
c.state.Lock()
|
||||
for i := 0; i < len(parts); i++ {
|
||||
|
@ -457,6 +451,8 @@ func handleNAMES(c *Client, e Event) {
|
|||
continue
|
||||
}
|
||||
|
||||
var s *Source = new(Source)
|
||||
|
||||
// If userhost-in-names.
|
||||
if strings.Contains(nick, "@") {
|
||||
s = ParseSource(nick)
|
||||
|
|
3
cap.go
3
cap.go
|
@ -106,7 +106,7 @@ func parseCap(raw string) map[string]map[string]string {
|
|||
if j < 0 {
|
||||
out[parts[i][:val]][option] = ""
|
||||
} else {
|
||||
out[parts[i][:val]][option[:j]] = option[j+1 : len(option)]
|
||||
out[parts[i][:val]][option[:j]] = option[j+1:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -316,6 +316,7 @@ func handleCHGHOST(c *Client, e Event) {
|
|||
user.Host = e.Params[1]
|
||||
}
|
||||
c.state.Unlock()
|
||||
|
||||
c.state.notify(c, UPDATE_STATE)
|
||||
}
|
||||
|
||||
|
|
68
client.go
68
client.go
|
@ -47,6 +47,9 @@ type Client struct {
|
|||
// so multiple threads aren't trying to connect at the same time, and
|
||||
// vice versa.
|
||||
mu sync.RWMutex
|
||||
|
||||
atom uint32
|
||||
|
||||
// stop is used to communicate with Connect(), letting it know that the
|
||||
// client wishes to cancel/close.
|
||||
stop context.CancelFunc
|
||||
|
@ -229,10 +232,10 @@ func (conf *Config) isValid() error {
|
|||
}
|
||||
|
||||
if !IsValidNick(conf.Nick) {
|
||||
return &ErrInvalidConfig{Conf: *conf, err: errors.New("bad nickname specified")}
|
||||
return &ErrInvalidConfig{Conf: *conf, err: errors.New("bad nickname specified: " + conf.Nick)}
|
||||
}
|
||||
if !IsValidUser(conf.User) {
|
||||
return &ErrInvalidConfig{Conf: *conf, err: errors.New("bad user/ident specified")}
|
||||
return &ErrInvalidConfig{Conf: *conf, err: errors.New("bad user/ident specified: " + conf.User)}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -250,6 +253,7 @@ func New(config Config) *Client {
|
|||
tx: make(chan *Event, 25),
|
||||
CTCP: newCTCP(),
|
||||
initTime: time.Now(),
|
||||
atom: stateUnlocked,
|
||||
}
|
||||
|
||||
c.Cmd = &Commands{c: c}
|
||||
|
@ -313,16 +317,11 @@ func (c *Client) String() string {
|
|||
// connection wasn't established using TLS (see ErrConnNotTLS), or if the
|
||||
// client isn't connected.
|
||||
func (c *Client) TLSConnectionState() (*tls.ConnectionState, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
if c.conn == nil {
|
||||
return nil, ErrNotConnected
|
||||
}
|
||||
|
||||
c.conn.mu.RLock()
|
||||
defer c.conn.mu.RUnlock()
|
||||
|
||||
if !c.conn.connected {
|
||||
if !c.conn.connected.Load().(bool) {
|
||||
return nil, ErrNotConnected
|
||||
}
|
||||
|
||||
|
@ -442,9 +441,6 @@ func (c *Client) DisableTracking() {
|
|||
|
||||
// Server returns the string representation of host+port pair for the connection.
|
||||
func (c *Client) Server() string {
|
||||
c.state.Lock()
|
||||
defer c.state.Lock()
|
||||
|
||||
return c.server()
|
||||
}
|
||||
|
||||
|
@ -465,16 +461,12 @@ func (c *Client) Lifetime() time.Duration {
|
|||
|
||||
// Uptime is the time at which the client successfully connected to the
|
||||
// server.
|
||||
func (c *Client) Uptime() (up *time.Time, err error) {
|
||||
func (c *Client) Uptime() (up time.Time, err error) {
|
||||
if !c.IsConnected() {
|
||||
return nil, ErrNotConnected
|
||||
return time.Now(), ErrNotConnected
|
||||
}
|
||||
|
||||
c.mu.RLock()
|
||||
c.conn.mu.RLock()
|
||||
up = c.conn.connTime
|
||||
c.conn.mu.RUnlock()
|
||||
c.mu.RUnlock()
|
||||
up = c.conn.connTime.Load().(time.Time)
|
||||
|
||||
return up, nil
|
||||
}
|
||||
|
@ -486,29 +478,18 @@ func (c *Client) ConnSince() (since *time.Duration, err error) {
|
|||
return nil, ErrNotConnected
|
||||
}
|
||||
|
||||
c.mu.RLock()
|
||||
c.conn.mu.RLock()
|
||||
timeSince := time.Since(*c.conn.connTime)
|
||||
c.conn.mu.RUnlock()
|
||||
c.mu.RUnlock()
|
||||
timeSince := time.Since(c.conn.connTime.Load().(time.Time))
|
||||
|
||||
return &timeSince, nil
|
||||
}
|
||||
|
||||
// IsConnected returns true if the client is connected to the server.
|
||||
func (c *Client) IsConnected() bool {
|
||||
c.mu.RLock()
|
||||
if c.conn == nil {
|
||||
c.mu.RUnlock()
|
||||
return false
|
||||
}
|
||||
|
||||
c.conn.mu.RLock()
|
||||
connected := c.conn.connected
|
||||
c.conn.mu.RUnlock()
|
||||
c.mu.RUnlock()
|
||||
|
||||
return connected
|
||||
return c.conn.connected.Load().(bool)
|
||||
}
|
||||
|
||||
// GetNick returns the current nickname of the active connection. Panics if
|
||||
|
@ -516,13 +497,10 @@ func (c *Client) IsConnected() bool {
|
|||
func (c *Client) GetNick() string {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
c.state.RLock()
|
||||
defer c.state.RUnlock()
|
||||
|
||||
if c.state.nick == "" {
|
||||
if c.state.nick.Load().(string) == "" {
|
||||
return c.Config.Nick
|
||||
}
|
||||
return c.state.nick
|
||||
return c.state.nick.Load().(string)
|
||||
}
|
||||
|
||||
// GetID returns an RFC1459 compliant version of the current nickname. Panics
|
||||
|
@ -537,13 +515,10 @@ func (c *Client) GetID() string {
|
|||
func (c *Client) GetIdent() string {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
c.state.RLock()
|
||||
defer c.state.RUnlock()
|
||||
|
||||
if c.state.ident == "" {
|
||||
if c.state.ident.Load().(string) == "" {
|
||||
return c.Config.User
|
||||
}
|
||||
return c.state.ident
|
||||
return c.state.ident.Load().(string)
|
||||
}
|
||||
|
||||
// GetHost returns the current host of the active connection. Panics if
|
||||
|
@ -552,9 +527,8 @@ func (c *Client) GetIdent() string {
|
|||
func (c *Client) GetHost() (host string) {
|
||||
c.panicIfNotTracking()
|
||||
|
||||
c.state.RLock()
|
||||
host = c.state.host
|
||||
c.state.RUnlock()
|
||||
host = c.state.host.Load().(string)
|
||||
|
||||
return host
|
||||
}
|
||||
|
||||
|
@ -714,11 +688,7 @@ func (c *Client) ServerMOTD() (motd string) {
|
|||
// by determining the difference in time between when we ping the server, and
|
||||
// when we receive a pong.
|
||||
func (c *Client) Latency() (delta time.Duration) {
|
||||
c.mu.RLock()
|
||||
c.conn.mu.RLock()
|
||||
delta = c.conn.lastPong.Sub(c.conn.lastPing)
|
||||
c.conn.mu.RUnlock()
|
||||
c.mu.RUnlock()
|
||||
delta = c.conn.lastPong.Load().(time.Time).Sub(c.conn.lastPing.Load().(time.Time))
|
||||
|
||||
if delta < 0 {
|
||||
return 0
|
||||
|
|
33
commands.go
33
commands.go
|
@ -5,7 +5,6 @@
|
|||
package girc
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
@ -112,7 +111,7 @@ func (cmd *Commands) Message(target, message string) {
|
|||
// Messagef sends a formated PRIVMSG to target (either channel, service, or
|
||||
// user).
|
||||
func (cmd *Commands) Messagef(target, format string, a ...interface{}) {
|
||||
cmd.Message(target, fmt.Sprintf(format, a...))
|
||||
cmd.Message(target, fmt.Sprintf(Fmt(format), a...))
|
||||
}
|
||||
|
||||
// ErrInvalidSource is returned when a method needs to know the origin of an
|
||||
|
@ -136,11 +135,33 @@ func (cmd *Commands) Reply(event Event, message string) {
|
|||
cmd.Message(event.Source.Name, message)
|
||||
}
|
||||
|
||||
// ReplyKick kicks the source of the event from the channel where the event originated
|
||||
func (cmd *Commands) ReplyKick(event Event, reason string) {
|
||||
if event.Source == nil {
|
||||
panic(ErrInvalidSource)
|
||||
}
|
||||
|
||||
if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
|
||||
cmd.Kick(event.Params[0], event.Source.Name, reason)
|
||||
}
|
||||
}
|
||||
|
||||
// ReplyBan kicks the source of the event from the channel where the event originated
|
||||
func (cmd *Commands) ReplyBan(event Event, reason string) {
|
||||
if event.Source == nil {
|
||||
panic(ErrInvalidSource)
|
||||
}
|
||||
|
||||
if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
|
||||
cmd.Ban(event.Params[0], event.Source.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Replyf sends a reply to channel or user with a format string, based on
|
||||
// where the supplied event originated from. See also ReplyTof(). Panics if
|
||||
// the incoming event has no source.
|
||||
func (cmd *Commands) Replyf(event Event, format string, a ...interface{}) {
|
||||
cmd.Reply(event, fmt.Sprintf(format, a...))
|
||||
cmd.Reply(event, fmt.Sprintf(Fmt(format), a...))
|
||||
}
|
||||
|
||||
// ReplyTo sends a reply to a channel or user, based on where the supplied
|
||||
|
@ -165,7 +186,7 @@ func (cmd *Commands) ReplyTo(event Event, message string) {
|
|||
// from a channel will default to replying with "<user>, <message>". See
|
||||
// also Replyf(). Panics if the incoming event has no source.
|
||||
func (cmd *Commands) ReplyTof(event Event, format string, a ...interface{}) {
|
||||
cmd.ReplyTo(event, fmt.Sprintf(format, a...))
|
||||
cmd.ReplyTo(event, fmt.Sprintf(Fmt(format), a...))
|
||||
}
|
||||
|
||||
// Action sends a PRIVMSG ACTION (/me) to target (either channel, service,
|
||||
|
@ -221,7 +242,7 @@ func (cmd *Commands) SendRawf(format string, a ...interface{}) error {
|
|||
// Topic sets the topic of channel to message. Does not verify the length
|
||||
// of the topic.
|
||||
func (cmd *Commands) Topic(channel, message string) {
|
||||
cmd.c.Send(&Event{Command: TOPIC, Params: []string{channel, message}})
|
||||
cmd.c.Send(&Event{Command: TOPIC, Params: []string{channel, Fmt(message)}})
|
||||
}
|
||||
|
||||
// Who sends a WHO query to the server, which will attempt WHOX by default.
|
||||
|
@ -357,7 +378,7 @@ func (cmd *Commands) List(channels ...string) {
|
|||
// Whowas sends a WHOWAS query to the server. amount is the amount of results
|
||||
// you want back.
|
||||
func (cmd *Commands) Whowas(user string, amount int) {
|
||||
cmd.c.Send(&Event{Command: WHOWAS, Params: []string{user, strconv.Itoa(amount)}})
|
||||
cmd.c.Send(&Event{Command: WHOWAS, Params: []string{user, string(fmt.Sprintf("%d", amount))}})
|
||||
}
|
||||
|
||||
// Monitor sends a MONITOR query to the server. The results of the query
|
||||
|
|
104
conn.go
104
conn.go
|
@ -11,6 +11,7 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -26,25 +27,24 @@ type ircConn struct {
|
|||
io *bufio.ReadWriter
|
||||
sock net.Conn
|
||||
|
||||
mu sync.RWMutex
|
||||
// lastWrite is used to keep track of when we last wrote to the server.
|
||||
lastWrite time.Time
|
||||
lastWrite atomic.Value
|
||||
// lastActive is the last time the client was interacting with the server,
|
||||
// excluding a few background commands (PING, PONG, WHO, etc).
|
||||
lastActive time.Time
|
||||
lastActive atomic.Value
|
||||
// writeDelay is used to keep track of rate limiting of events sent to
|
||||
// the server.
|
||||
writeDelay time.Duration
|
||||
writeDelay atomic.Value
|
||||
// connected is true if we're actively connected to a server.
|
||||
connected bool
|
||||
connected atomic.Value
|
||||
// connTime is the time at which the client has connected to a server.
|
||||
connTime *time.Time
|
||||
connTime atomic.Value
|
||||
// lastPing is the last time that we pinged the server.
|
||||
lastPing time.Time
|
||||
lastPing atomic.Value
|
||||
// lastPong is the last successful time that we pinged the server and
|
||||
// received a successful pong back.
|
||||
lastPong time.Time
|
||||
pingDelay time.Duration
|
||||
lastPong atomic.Value
|
||||
// pingDelay time.Duration
|
||||
}
|
||||
|
||||
// Dialer is an interface implementation of net.Dialer. Use this if you would
|
||||
|
@ -112,25 +112,27 @@ func newConn(conf Config, dialer Dialer, addr string, sts *strictTransport) (*ir
|
|||
conn = tlsConn
|
||||
}
|
||||
|
||||
ctime := time.Now()
|
||||
|
||||
c := &ircConn{
|
||||
sock: conn,
|
||||
connTime: &ctime,
|
||||
connected: true,
|
||||
connTime: atomic.Value{},
|
||||
connected: atomic.Value{},
|
||||
}
|
||||
c.connTime.Store(time.Now())
|
||||
c.connected.Store(true)
|
||||
|
||||
c.newReadWriter()
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func newMockConn(conn net.Conn) *ircConn {
|
||||
ctime := time.Now()
|
||||
c := &ircConn{
|
||||
sock: conn,
|
||||
connTime: &ctime,
|
||||
connected: true,
|
||||
connTime: atomic.Value{},
|
||||
connected: atomic.Value{},
|
||||
}
|
||||
c.connTime.Store(time.Now())
|
||||
c.connected.Store(true)
|
||||
c.newReadWriter()
|
||||
|
||||
return c
|
||||
|
@ -156,6 +158,7 @@ func (c *ircConn) decode() (event *Event, err error) {
|
|||
return event, nil
|
||||
}
|
||||
|
||||
/*
|
||||
func (c *ircConn) encode(event *Event) error {
|
||||
if _, err := c.io.Write(event.Bytes()); err != nil {
|
||||
return err
|
||||
|
@ -166,7 +169,7 @@ func (c *ircConn) encode(event *Event) error {
|
|||
|
||||
return c.io.Flush()
|
||||
}
|
||||
|
||||
*/
|
||||
func (c *ircConn) newReadWriter() {
|
||||
c.io = bufio.NewReadWriter(bufio.NewReader(c.sock), bufio.NewWriter(c.sock))
|
||||
}
|
||||
|
@ -262,8 +265,6 @@ func (c *Client) MockConnect(conn net.Conn) error {
|
|||
|
||||
func (c *Client) internalConnect(mock net.Conn, dialer Dialer) error {
|
||||
startConn:
|
||||
// We want to be the only one handling connects/disconnects right now.
|
||||
c.mu.Lock()
|
||||
|
||||
if c.conn != nil {
|
||||
panic("use of connect more than once")
|
||||
|
@ -284,7 +285,6 @@ startConn:
|
|||
c.RunHandlers(&Event{Command: STS_ERR_FALLBACK})
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -295,7 +295,6 @@ startConn:
|
|||
|
||||
var ctx context.Context
|
||||
ctx, c.stop = context.WithCancel(context.Background())
|
||||
c.mu.Unlock()
|
||||
|
||||
errs := make(chan error, 4)
|
||||
var wg sync.WaitGroup
|
||||
|
@ -352,15 +351,13 @@ startConn:
|
|||
}
|
||||
|
||||
// Make sure that the connection is closed if not already.
|
||||
c.mu.RLock()
|
||||
|
||||
if c.stop != nil {
|
||||
c.stop()
|
||||
}
|
||||
c.conn.mu.Lock()
|
||||
c.conn.connected = false
|
||||
|
||||
c.conn.connected.Store(false)
|
||||
_ = c.conn.Close()
|
||||
c.conn.mu.Unlock()
|
||||
c.mu.RUnlock()
|
||||
|
||||
c.RunHandlers(&Event{Command: DISCONNECTED, Params: []string{addr}})
|
||||
|
||||
|
@ -374,13 +371,11 @@ startConn:
|
|||
// This helps ensure that the end user isn't improperly using the client
|
||||
// more than once. If they want to do this, they should be using multiple
|
||||
// clients, not multiple instances of Connect().
|
||||
c.mu.Lock()
|
||||
c.conn = nil
|
||||
|
||||
if result == nil {
|
||||
if c.state.sts.beginUpgrade {
|
||||
c.state.sts.beginUpgrade = false
|
||||
c.mu.Unlock()
|
||||
goto startConn
|
||||
}
|
||||
|
||||
|
@ -388,7 +383,6 @@ startConn:
|
|||
c.state.sts.persistenceReceived = time.Now()
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
return result
|
||||
}
|
||||
|
@ -432,21 +426,21 @@ func (c *Client) readLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
|
|||
func (c *Client) Send(event *Event) {
|
||||
var delay time.Duration
|
||||
|
||||
for atomic.CompareAndSwapUint32(&c.atom, stateUnlocked, stateLocked) {
|
||||
randSleep()
|
||||
}
|
||||
defer atomic.StoreUint32(&c.atom, stateUnlocked)
|
||||
|
||||
if !c.Config.AllowFlood {
|
||||
c.mu.RLock()
|
||||
|
||||
// Drop the event early as we're disconnected, this way we don't have to wait
|
||||
// the (potentially long) rate limit delay before dropping.
|
||||
if c.conn == nil {
|
||||
c.debugLogEvent(event, true)
|
||||
c.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
c.conn.mu.Lock()
|
||||
delay = c.conn.rate(event.Len())
|
||||
c.conn.mu.Unlock()
|
||||
c.mu.RUnlock()
|
||||
}
|
||||
|
||||
if c.Config.GlobalFormat && len(event.Params) > 0 && event.Params[len(event.Params)-1] != "" &&
|
||||
|
@ -477,11 +471,18 @@ func (c *Client) write(event *Event) {
|
|||
func (c *ircConn) rate(chars int) time.Duration {
|
||||
_time := time.Second + ((time.Duration(chars) * time.Second) / 100)
|
||||
|
||||
if c.writeDelay += _time - time.Now().Sub(c.lastWrite); c.writeDelay < 0 {
|
||||
c.writeDelay = 0
|
||||
if c.writeDelay.Load() == nil {
|
||||
c.writeDelay.Store(time.Duration(0))
|
||||
}
|
||||
wdelay := c.writeDelay.Load().(time.Duration)
|
||||
|
||||
lwrite := c.lastWrite.Load().(time.Time)
|
||||
|
||||
if wdelay += _time - time.Since(lwrite); wdelay < 0 {
|
||||
c.writeDelay.Store(time.Duration(0))
|
||||
}
|
||||
|
||||
if c.writeDelay > (8 * time.Second) {
|
||||
if c.writeDelay.Load().(time.Duration) > (8 * time.Second) {
|
||||
return _time
|
||||
}
|
||||
|
||||
|
@ -500,7 +501,7 @@ func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
|
|||
// Check if tags exist on the event. If they do, and message-tags
|
||||
// isn't a supported capability, remove them from the event.
|
||||
if event.Tags != nil {
|
||||
c.state.RLock()
|
||||
// c.state.RLock()
|
||||
var in bool
|
||||
for i := 0; i < len(c.state.enabledCap); i++ {
|
||||
if _, ok := c.state.enabledCap["message-tags"]; ok {
|
||||
|
@ -508,7 +509,7 @@ func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
|
|||
break
|
||||
}
|
||||
}
|
||||
c.state.RUnlock()
|
||||
// c.state.RUnlock()
|
||||
|
||||
if !in {
|
||||
event.Tags = Tags{}
|
||||
|
@ -517,13 +518,11 @@ func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
|
|||
|
||||
c.debugLogEvent(event, false)
|
||||
|
||||
c.conn.mu.Lock()
|
||||
c.conn.lastWrite = time.Now()
|
||||
c.conn.lastWrite.Store(time.Now())
|
||||
|
||||
if event.Command != PING && event.Command != PONG && event.Command != WHO {
|
||||
c.conn.lastActive = c.conn.lastWrite
|
||||
}
|
||||
c.conn.mu.Unlock()
|
||||
|
||||
// Write the raw line.
|
||||
_, err = c.conn.io.Write(event.Bytes())
|
||||
|
@ -579,10 +578,8 @@ func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
|
|||
c.debug.Print("starting pingLoop")
|
||||
defer c.debug.Print("closing pingLoop")
|
||||
|
||||
c.conn.mu.Lock()
|
||||
c.conn.lastPing = time.Now()
|
||||
c.conn.lastPong = time.Now()
|
||||
c.conn.mu.Unlock()
|
||||
c.conn.lastPing.Store(time.Now())
|
||||
c.conn.lastPong.Store(time.Now())
|
||||
|
||||
tick := time.NewTicker(c.Config.PingDelay)
|
||||
defer tick.Stop()
|
||||
|
@ -603,26 +600,21 @@ func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
|
|||
past = true
|
||||
}
|
||||
|
||||
c.conn.mu.RLock()
|
||||
if time.Since(c.conn.lastPong) > c.Config.PingDelay+(60*time.Second) {
|
||||
if time.Since(c.conn.lastPong.Load().(time.Time)) > c.Config.PingDelay+(120*time.Second) {
|
||||
// It's 60 seconds over what out ping delay is, connection
|
||||
// has probably dropped.
|
||||
errs <- ErrTimedOut{
|
||||
TimeSinceSuccess: time.Since(c.conn.lastPong),
|
||||
LastPong: c.conn.lastPong,
|
||||
LastPing: c.conn.lastPing,
|
||||
TimeSinceSuccess: time.Since(c.conn.lastPong.Load().(time.Time)),
|
||||
LastPong: c.conn.lastPong.Load().(time.Time),
|
||||
LastPing: c.conn.lastPing.Load().(time.Time),
|
||||
Delay: c.Config.PingDelay,
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
c.conn.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
c.conn.mu.RUnlock()
|
||||
|
||||
c.conn.mu.Lock()
|
||||
c.conn.lastPing = time.Now()
|
||||
c.conn.mu.Unlock()
|
||||
c.conn.lastPing.Store(time.Now())
|
||||
|
||||
c.Cmd.Ping(fmt.Sprintf("%d", time.Now().UnixNano()))
|
||||
case <-ctx.Done():
|
||||
|
|
13
ctcp.go
13
ctcp.go
|
@ -193,10 +193,10 @@ func (c *CTCP) Set(cmd string, handler func(client *Client, ctcp CTCPEvent)) {
|
|||
if cmd = c.parseCMD(cmd); cmd == "" {
|
||||
return
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.handlers[cmd] = CTCPHandler(handler)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetBg is much like Set, however the handler is executed in the background,
|
||||
|
@ -270,14 +270,14 @@ func handleCTCPVersion(client *Client, ctcp CTCPEvent) {
|
|||
|
||||
client.Cmd.SendCTCPReplyf(
|
||||
ctcp.Source.ID(), CTCP_VERSION,
|
||||
"girc (github.com/lrstanley/girc) using %s (%s, %s)",
|
||||
"girc (git.tcp.direct/kayos/girc-atomic) using %s (%s, %s)",
|
||||
runtime.Version(), runtime.GOOS, runtime.GOARCH,
|
||||
)
|
||||
}
|
||||
|
||||
// handleCTCPSource replies with the public git location of this library.
|
||||
func handleCTCPSource(client *Client, ctcp CTCPEvent) {
|
||||
client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_SOURCE, "https://github.com/lrstanley/girc")
|
||||
client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_SOURCE, "https://git.tcp.direct/kayos/girc-atomic")
|
||||
}
|
||||
|
||||
// handleCTCPTime replies with a RFC 1123 (Z) formatted version of Go's
|
||||
|
@ -289,9 +289,6 @@ func handleCTCPTime(client *Client, ctcp CTCPEvent) {
|
|||
// handleCTCPFinger replies with the realname and idle time of the user. This
|
||||
// is obsoleted by improvements to the IRC protocol, however still supported.
|
||||
func handleCTCPFinger(client *Client, ctcp CTCPEvent) {
|
||||
client.conn.mu.RLock()
|
||||
active := client.conn.lastActive
|
||||
client.conn.mu.RUnlock()
|
||||
|
||||
active := client.conn.lastActive.Load().(time.Time)
|
||||
client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_FINGER, fmt.Sprintf("%s -- idle %s", client.Config.Name, time.Since(active)))
|
||||
}
|
||||
|
|
13
event.go
13
event.go
|
@ -13,7 +13,7 @@ import (
|
|||
|
||||
const (
|
||||
eventSpace byte = ' ' // Separator.
|
||||
maxLength = 510 // Maximum length is 510 (2 for line endings).
|
||||
maxLength int = 510 // Maximum length is 510 (2 for line endings).
|
||||
)
|
||||
|
||||
// cutCRFunc is used to trim CR characters from prefixes/messages.
|
||||
|
@ -248,7 +248,7 @@ func (e *Event) Len() (length int) {
|
|||
|
||||
// If param contains a space or it's empty, it's trailing, so it should be
|
||||
// prefixed with a colon (:).
|
||||
if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || strings.HasPrefix(e.Params[i], ":") || e.Params[i] == "") {
|
||||
if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || e.Params[i] == "") {
|
||||
length++
|
||||
}
|
||||
}
|
||||
|
@ -268,7 +268,9 @@ func (e *Event) Bytes() []byte {
|
|||
|
||||
// Tags.
|
||||
if e.Tags != nil {
|
||||
e.Tags.writeTo(buffer)
|
||||
if _, err := e.Tags.writeTo(buffer); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Event prefix.
|
||||
|
@ -284,8 +286,9 @@ func (e *Event) Bytes() []byte {
|
|||
// Space separated list of arguments.
|
||||
if len(e.Params) > 0 {
|
||||
// buffer.WriteByte(eventSpace)
|
||||
|
||||
for i := 0; i < len(e.Params); i++ {
|
||||
if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || strings.HasPrefix(e.Params[i], ":") || e.Params[i] == "") {
|
||||
if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || e.Params[i] == "") {
|
||||
buffer.WriteString(string(eventSpace) + string(messagePrefix) + e.Params[i])
|
||||
continue
|
||||
}
|
||||
|
@ -636,6 +639,4 @@ func (s *Source) writeTo(buffer *bytes.Buffer) {
|
|||
buffer.WriteByte(prefixHost)
|
||||
buffer.WriteString(s.Host)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
2
go.mod
2
go.mod
|
@ -1,3 +1,3 @@
|
|||
module github.com/lrstanley/girc
|
||||
module git.tcp.direct/kayos/girc-atomic
|
||||
|
||||
go 1.12
|
||||
|
|
19
handler.go
19
handler.go
|
@ -99,11 +99,11 @@ func newCaller(debugOut *log.Logger) *Caller {
|
|||
func (c *Caller) Len() int {
|
||||
var total int
|
||||
|
||||
c.mu.RLock()
|
||||
// c.mu.RLock()
|
||||
for command := range c.external {
|
||||
total += len(c.external[command])
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
// c.mu.RUnlock()
|
||||
|
||||
return total
|
||||
}
|
||||
|
@ -115,13 +115,13 @@ func (c *Caller) Count(cmd string) int {
|
|||
|
||||
cmd = strings.ToUpper(cmd)
|
||||
|
||||
c.mu.RLock()
|
||||
// c.mu.RLock()
|
||||
for command := range c.external {
|
||||
if command == cmd {
|
||||
total += len(c.external[command])
|
||||
}
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
// c.mu.RUnlock()
|
||||
|
||||
return total
|
||||
}
|
||||
|
@ -176,7 +176,7 @@ func (c *Caller) exec(command string, bg bool, client *Client, event *Event) {
|
|||
// Build a stack of handlers which can be executed concurrently.
|
||||
var stack []execStack
|
||||
|
||||
c.mu.RLock()
|
||||
// c.mu.RLock()
|
||||
// Get internal handlers first.
|
||||
if _, ok := c.internal[command]; ok {
|
||||
for cuid := range c.internal[command] {
|
||||
|
@ -198,7 +198,7 @@ func (c *Caller) exec(command string, bg bool, client *Client, event *Event) {
|
|||
stack = append(stack, execStack{c.external[command][cuid], cuid})
|
||||
}
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
// c.mu.RUnlock()
|
||||
|
||||
// Run all handlers concurrently across the same event. This should
|
||||
// still help prevent mis-ordered events, while speeding up the
|
||||
|
@ -264,9 +264,9 @@ func (c *Caller) Clear(cmd string) {
|
|||
cmd = strings.ToUpper(cmd)
|
||||
|
||||
c.mu.Lock()
|
||||
if _, ok := c.external[cmd]; ok {
|
||||
delete(c.external, cmd)
|
||||
}
|
||||
|
||||
delete(c.external, cmd)
|
||||
|
||||
c.mu.Unlock()
|
||||
|
||||
c.debug.Printf("cleared external handlers for %s", cmd)
|
||||
|
@ -458,7 +458,6 @@ func recoverHandlerPanic(client *Client, event *Event, id string, skip int) {
|
|||
}
|
||||
|
||||
client.Config.RecoverFunc(client, err)
|
||||
return
|
||||
}
|
||||
|
||||
// HandlerError is the error returned when a panic is intentionally recovered
|
||||
|
|
13
state.go
13
state.go
|
@ -8,6 +8,7 @@ import (
|
|||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -17,7 +18,7 @@ import (
|
|||
type state struct {
|
||||
sync.RWMutex
|
||||
// nick, ident, and host are the internal trackers for our user.
|
||||
nick, ident, host string
|
||||
nick, ident, host atomic.Value
|
||||
// channels represents all channels we're active in.
|
||||
channels map[string]*Channel
|
||||
// users represents all of users that we're tracking.
|
||||
|
@ -46,9 +47,9 @@ type state struct {
|
|||
// reset resets the state back to it's original form.
|
||||
func (s *state) reset(initial bool) {
|
||||
s.Lock()
|
||||
s.nick = ""
|
||||
s.ident = ""
|
||||
s.host = ""
|
||||
s.nick.Store("")
|
||||
s.ident.Store("")
|
||||
s.host.Store("")
|
||||
s.channels = make(map[string]*Channel)
|
||||
s.users = make(map[string]*User)
|
||||
s.serverOptions = make(map[string]string)
|
||||
|
@ -481,8 +482,8 @@ func (s *state) renameUser(from, to string) {
|
|||
from = ToRFC1459(from)
|
||||
|
||||
// Update our nickname.
|
||||
if from == ToRFC1459(s.nick) {
|
||||
s.nick = to
|
||||
if from == ToRFC1459(s.nick.Load().(string)) {
|
||||
s.nick.Store(to)
|
||||
}
|
||||
|
||||
user := s.lookupUser(from)
|
||||
|
|
Loading…
Reference in New Issue