summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/lrstanley
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/lrstanley')
-rw-r--r--vendor/github.com/lrstanley/girc/.editorconfig5
-rw-r--r--vendor/github.com/lrstanley/girc/.golangci.yml191
-rw-r--r--vendor/github.com/lrstanley/girc/builtin.go42
-rw-r--r--vendor/github.com/lrstanley/girc/cap.go4
-rw-r--r--vendor/github.com/lrstanley/girc/cap_sasl.go6
-rw-r--r--vendor/github.com/lrstanley/girc/cap_tags.go7
-rw-r--r--vendor/github.com/lrstanley/girc/client.go89
-rw-r--r--vendor/github.com/lrstanley/girc/commands.go8
-rw-r--r--vendor/github.com/lrstanley/girc/conn.go208
-rw-r--r--vendor/github.com/lrstanley/girc/event.go146
-rw-r--r--vendor/github.com/lrstanley/girc/format.go216
-rw-r--r--vendor/github.com/lrstanley/girc/internal/ctxgroup/ctxgroup.go67
-rw-r--r--vendor/github.com/lrstanley/girc/modes.go15
-rw-r--r--vendor/github.com/lrstanley/girc/state.go15
14 files changed, 838 insertions, 181 deletions
diff --git a/vendor/github.com/lrstanley/girc/.editorconfig b/vendor/github.com/lrstanley/girc/.editorconfig
index 32ecf3ee..4ae34fde 100644
--- a/vendor/github.com/lrstanley/girc/.editorconfig
+++ b/vendor/github.com/lrstanley/girc/.editorconfig
@@ -1,6 +1,9 @@
# THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform.
#
-# editorconfig.org
+# editorconfig: https://editorconfig.org/
+# actual source: https://github.com/lrstanley/.github/blob/master/terraform/github-common-files/templates/.editorconfig
+#
+
root = true
[*]
diff --git a/vendor/github.com/lrstanley/girc/.golangci.yml b/vendor/github.com/lrstanley/girc/.golangci.yml
index 1a8320c9..1b4e7221 100644
--- a/vendor/github.com/lrstanley/girc/.golangci.yml
+++ b/vendor/github.com/lrstanley/girc/.golangci.yml
@@ -1,11 +1,33 @@
# THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform.
+#
+# golangci-lint: https://golangci-lint.run/
+# false-positives: https://golangci-lint.run/usage/false-positives/
+# actual source: https://github.com/lrstanley/.github/blob/master/terraform/github-common-files/templates/.golangci.yml
+# modified variant of: https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322
+#
+
run:
- tests: False
timeout: 3m
issues:
max-per-linter: 0
- max-same-issues: 0
+ # max-same-issues: 0
+ max-same-issues: 50
+
+ exclude-rules:
+ - source: "(noinspection|TODO)"
+ linters: [godot]
+ - source: "//noinspection"
+ linters: [gocritic]
+ - path: "_test\\.go"
+ linters:
+ - bodyclose
+ - dupl
+ - funlen
+ - goconst
+ - gosec
+ - noctx
+ - wrapcheck
severity:
default-severity: error
@@ -16,17 +38,102 @@ severity:
severity: warning
linters:
+ disable-all: true
enable:
- - asciicheck
- - exportloopref
- - gci
- - gocritic
- - gofmt
- - misspell
+ - asasalint # checks for pass []any as any in variadic func(...any)
+ - asciicheck # checks that your code does not contain non-ASCII identifiers
+ - bidichk # checks for dangerous unicode character sequences
+ - bodyclose # checks whether HTTP response body is closed successfully
+ - cyclop # checks function and package cyclomatic complexity
+ - dupl # tool for code clone detection
+ - durationcheck # checks for two durations multiplied together
+ - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases
+ - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13
+ - execinquery # checks query string in Query function which reads your Go src files and warning it finds
+ - exportloopref # checks for pointers to enclosing loop variables
+ - forbidigo # forbids identifiers
+ - funlen # tool for detection of long functions
+ - gci # controls golang package import order and makes it always deterministic
+ - gocheckcompilerdirectives # validates go compiler directive comments (//go:)
+ - gochecknoinits # checks that no init functions are present in Go code
+ - goconst # finds repeated strings that could be replaced by a constant
+ - gocritic # provides diagnostics that check for bugs, performance and style issues
+ - gocyclo # computes and checks the cyclomatic complexity of functions
+ - godot # checks if comments end in a period
+ - godox # detects FIXME, TODO and other comment keywords
+ - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt
+ - gomnd # detects magic numbers
+ - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod
+ - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations
+ - goprintffuncname # checks that printf-like functions are named with f at the end
+ - gosec # inspects source code for security problems
+ - gosimple # specializes in simplifying a code
+ - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
+ - ineffassign # detects when assignments to existing variables are not used
+ - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap)
+ - makezero # finds slice declarations with non-zero initial length
+ - misspell # finds commonly misspelled words
+ - musttag # enforces field tags in (un)marshaled structs
+ - nakedret # finds naked returns in functions greater than a specified function length
+ - nilerr # finds the code that returns nil even if it checks that the error is not nil
+ - nilnil # checks that there is no simultaneous return of nil error and an invalid value
+ - noctx # finds sending http request without context.Context
+ - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL
+ - predeclared # finds code that shadows one of Go's predeclared identifiers
+ - promlinter # checks Prometheus metrics naming via promlint
+ - reassign # checks that package variables are not reassigned
+ - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint
+ - rowserrcheck # checks whether Err of rows is checked successfully
+ - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed
+ - staticcheck # is a go vet on steroids, applying a ton of static analysis checks
+ - stylecheck # is a replacement for golint
+ - tenv # detects using os.Setenv instead of t.Setenv since Go1.17
+ - testableexamples # checks if examples are testable (have an expected output)
+ - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes
+ - typecheck # like the front-end of a Go compiler, parses and type-checks Go code
+ - unconvert # removes unnecessary type conversions
+ - unparam # reports unused function parameters
+ - unused # checks for unused constants, variables, functions and types
+ - usestdlibvars # detects the possibility to use variables/constants from the Go standard library
+ - wastedassign # finds wasted assignment statements
+ - whitespace # detects leading and trailing whitespace
+
+ # disabled for now:
+ # - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error
+ # - gochecknoglobals # checks that no global variables exist
+ # - gocognit # computes and checks the cognitive complexity of functions
+ # - nestif # reports deeply nested if statements
+ # - nonamedreturns # reports all named returns
+ # - testpackage # makes you use a separate _test package
linters-settings:
+ cyclop:
+ # The maximal code complexity to report.
+ max-complexity: 30
+ # The maximal average package complexity.
+ # If it's higher than 0.0 (float) the check is enabled
+ package-average: 10.0
+
+ errcheck:
+ # Report about not checking of errors in type assertions: `a := b.(MyStruct)`.
+ # Such cases aren't reported by default.
+ check-type-assertions: true
+
+ funlen:
+ # Checks the number of lines in a function.
+ # If lower than 0, disable the check.
+ lines: 150
+ # Checks the number of statements in a function.
+ # If lower than 0, disable the check.
+ statements: 75
+
+ # gocognit:
+ # # Minimal code complexity to report.
+ # min-complexity: 25
+
gocritic:
disabled-checks:
+ - whyNoLint
- hugeParam
- ifElseChain
enabled-tags:
@@ -34,5 +141,71 @@ linters-settings:
- opinionated
- performance
- style
+ # https://go-critic.github.io/overview.
+ settings:
+ captLocal:
+ # Whether to restrict checker to params only.
+ paramsOnly: false
+ underef:
+ # Whether to skip (*x).method() calls where x is a pointer receiver.
+ skipRecvDeref: false
+
+ gomnd:
+ # Values always ignored: `time.Date`,
+ # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`,
+ # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`.
+ ignored-functions:
+ - os.Chmod
+ - os.Mkdir
+ - os.MkdirAll
+ - os.OpenFile
+ - os.WriteFile
+ - prometheus.ExponentialBuckets
+ - prometheus.ExponentialBucketsRange
+ - prometheus.LinearBuckets
+
+ gomodguard:
+ blocked:
+ # List of blocked modules.
+ modules:
+ - github.com/golang/protobuf:
+ recommendations:
+ - google.golang.org/protobuf
+ reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules"
+ - github.com/satori/go.uuid:
+ recommendations:
+ - github.com/google/uuid
+ reason: "satori's package is not maintained"
+ - github.com/gofrs/uuid:
+ recommendations:
+ - github.com/google/uuid
+ reason: "gofrs' package is not go module"
+
govet:
- check-shadowing: true
+ enable-all: true
+ # Run `go tool vet help` to see all analyzers.
+ disable:
+ - fieldalignment # too strict
+ settings:
+ shadow:
+ # Whether to be strict about shadowing; can be noisy.
+ strict: true
+
+ nakedret:
+ # Make an issue if func has more lines of code than this setting, and it has naked returns.
+ max-func-lines: 0
+
+ rowserrcheck:
+ # database/sql is always checked
+ packages:
+ - github.com/jmoiron/sqlx
+
+ stylecheck:
+ checks:
+ - all
+ - -ST1008 # handled by revive already.
+
+ tenv:
+ # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures.
+ # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked.
+ all: true
diff --git a/vendor/github.com/lrstanley/girc/builtin.go b/vendor/github.com/lrstanley/girc/builtin.go
index e60c577a..345452e5 100644
--- a/vendor/github.com/lrstanley/girc/builtin.go
+++ b/vendor/github.com/lrstanley/girc/builtin.go
@@ -408,6 +408,48 @@ func handleISUPPORT(c *Client, e Event) {
c.state.serverOptions[name] = val
}
c.state.Unlock()
+
+ // Check for max line/nick/user/host lengths here.
+ c.state.RLock()
+ maxLineLength := c.state.maxLineLength
+ c.state.RUnlock()
+ maxNickLength := defaultNickLength
+ maxUserLength := defaultUserLength
+ maxHostLength := defaultHostLength
+
+ var ok bool
+ var tmp int
+
+ if tmp, ok = c.GetServerOptionInt("LINELEN"); ok {
+ maxLineLength = tmp
+ c.state.Lock()
+ c.state.maxLineLength = maxTagLength - 2 // -2 for CR-LF.
+ c.state.Unlock()
+ }
+
+ if tmp, ok = c.GetServerOptionInt("NICKLEN"); ok {
+ maxNickLength = tmp
+ }
+ if tmp, ok = c.GetServerOptionInt("MAXNICKLEN"); ok && tmp > maxNickLength {
+ maxNickLength = tmp
+ }
+ if tmp, ok = c.GetServerOptionInt("USERLEN"); ok && tmp > maxUserLength {
+ maxUserLength = tmp
+ }
+ if tmp, ok = c.GetServerOptionInt("HOSTLEN"); ok && tmp > maxHostLength {
+ maxHostLength = tmp
+ }
+
+ prefixLen := defaultPrefixPadding + maxNickLength + maxUserLength + maxHostLength
+ if prefixLen >= maxLineLength {
+ // Give up and go with defaults.
+ c.state.notify(c, UPDATE_GENERAL)
+ return
+ }
+ c.state.Lock()
+ c.state.maxPrefixLength = prefixLen
+ c.state.Unlock()
+
c.state.notify(c, UPDATE_GENERAL)
}
diff --git a/vendor/github.com/lrstanley/girc/cap.go b/vendor/github.com/lrstanley/girc/cap.go
index 631b925b..f35f2ec4 100644
--- a/vendor/github.com/lrstanley/girc/cap.go
+++ b/vendor/github.com/lrstanley/girc/cap.go
@@ -267,9 +267,9 @@ func handleCAP(c *Client, e Event) {
}
if isError {
- c.rx <- &Event{Command: ERROR, Params: []string{
+ c.receive(&Event{Command: ERROR, Params: []string{
fmt.Sprintf("closing connection: strict transport policy provided by server is invalid; possible MITM? config: %#v", sts),
- }}
+ }})
return
}
diff --git a/vendor/github.com/lrstanley/girc/cap_sasl.go b/vendor/github.com/lrstanley/girc/cap_sasl.go
index d880316b..2a1e8417 100644
--- a/vendor/github.com/lrstanley/girc/cap_sasl.go
+++ b/vendor/github.com/lrstanley/girc/cap_sasl.go
@@ -95,9 +95,9 @@ func handleSASL(c *Client, e Event) {
// some reason. The SASL spec and IRCv3 spec do not define a clear
// way to abort a SASL exchange, other than to disconnect, or proceed
// with CAP END.
- c.rx <- &Event{Command: ERROR, Params: []string{
+ c.receive(&Event{Command: ERROR, Params: []string{
fmt.Sprintf("closing connection: SASL %s failed: %s", c.Config.SASL.Method(), e.Last()),
- }}
+ }})
return
}
@@ -131,5 +131,5 @@ func handleSASLError(c *Client, e Event) {
// Authentication failed. The SASL spec and IRCv3 spec do not define a
// clear way to abort a SASL exchange, other than to disconnect, or
// proceed with CAP END.
- c.rx <- &Event{Command: ERROR, Params: []string{"closing connection: " + e.Last()}}
+ c.receive(&Event{Command: ERROR, Params: []string{"closing connection: " + e.Last()}})
}
diff --git a/vendor/github.com/lrstanley/girc/cap_tags.go b/vendor/github.com/lrstanley/girc/cap_tags.go
index 42599f3a..3cc8887f 100644
--- a/vendor/github.com/lrstanley/girc/cap_tags.go
+++ b/vendor/github.com/lrstanley/girc/cap_tags.go
@@ -52,9 +52,12 @@ type Tags map[string]string
// ParseTags parses out the key-value map of tags. raw should only be the tag
// data, not a full message. For example:
-// @aaa=bbb;ccc;example.com/ddd=eee
+//
+// @aaa=bbb;ccc;example.com/ddd=eee
+//
// NOT:
-// @aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello
+//
+// @aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello
//
// Technically, there is a length limit of 4096, but the server should reject
// tag messages longer than this.
diff --git a/vendor/github.com/lrstanley/girc/client.go b/vendor/github.com/lrstanley/girc/client.go
index db6ec080..fa2d7f69 100644
--- a/vendor/github.com/lrstanley/girc/client.go
+++ b/vendor/github.com/lrstanley/girc/client.go
@@ -155,6 +155,10 @@ type Config struct {
// and the client. If this is set to -1, the client will not attempt to
// send client -> server PING requests.
PingDelay time.Duration
+ // PingTimeout specifies the duration at which girc will assume
+ // that the connection to the server has been lost if no PONG
+ // message has been received in reply to an outstanding PING.
+ PingTimeout time.Duration
// disableTracking disables all channel and user-level tracking. Useful
// for highly embedded scripts with single purposes. This has an exported
@@ -179,13 +183,13 @@ type Config struct {
// server.
//
// Client expectations:
-// - Perform any proxy resolution.
-// - Check the reverse DNS and forward DNS match.
-// - Check the IP against suitable access controls (ipaccess, dnsbl, etc).
+// - Perform any proxy resolution.
+// - Check the reverse DNS and forward DNS match.
+// - Check the IP against suitable access controls (ipaccess, dnsbl, etc).
//
// More information:
-// - https://ircv3.net/specs/extensions/webirc.html
-// - https://kiwiirc.com/docs/webirc
+// - https://ircv3.net/specs/extensions/webirc.html
+// - https://kiwiirc.com/docs/webirc
type WebIRC struct {
// Password that authenticates the WEBIRC command from this client.
Password string
@@ -262,6 +266,10 @@ func New(config Config) *Client {
c.Config.PingDelay = 600 * time.Second
}
+ if c.Config.PingTimeout == 0 {
+ c.Config.PingTimeout = 60 * time.Second
+ }
+
envDebug, _ := strconv.ParseBool(os.Getenv("GIRC_DEBUG"))
if c.Config.Debug == nil {
if envDebug {
@@ -300,6 +308,23 @@ func New(config Config) *Client {
return c
}
+// receive is a wrapper for sending to the Client.rx channel. It will timeout if
+// the event can't be sent within 30s.
+func (c *Client) receive(e *Event) {
+ t := time.NewTimer(30 * time.Second)
+ defer func() {
+ if !t.Stop() {
+ <-t.C
+ }
+ }()
+
+ select {
+ case c.rx <- e:
+ case <-t.C:
+ c.debugLogEvent(e, true)
+ }
+}
+
// String returns a brief description of the current client state.
func (c *Client) String() string {
connected := c.IsConnected()
@@ -380,7 +405,7 @@ func (e *ErrEvent) Error() string {
return e.Event.Last()
}
-func (c *Client) execLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) {
+func (c *Client) execLoop(ctx context.Context) error {
c.debug.Print("starting execLoop")
defer c.debug.Print("closing execLoop")
@@ -403,9 +428,10 @@ func (c *Client) execLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
}
done:
- wg.Done()
- return
+ return nil
case event = <-c.rx:
+ c.RunHandlers(event)
+
if event != nil && event.Command == ERROR {
// Handles incoming ERROR responses. These are only ever sent
// by the server (with the exception that this library may use
@@ -415,13 +441,9 @@ func (c *Client) execLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
// some reason the server doesn't disconnect the client, or
// if this library is the source of the error, this should
// signal back up to the main connect loop, to disconnect.
- errs <- &ErrEvent{Event: event}
- // Make sure to not actually exit, so we can let any handlers
- // actually handle the ERROR event.
+ return &ErrEvent{Event: event}
}
-
- c.RunHandlers(event)
}
}
}
@@ -669,8 +691,7 @@ func (c *Client) IsInChannel(channel string) (in bool) {
// during client connection. This is also known as ISUPPORT (or RPL_PROTOCTL).
// Will panic if used when tracking has been disabled. Examples of usage:
//
-// nickLen, success := GetServerOption("MAXNICKLEN")
-//
+// nickLen, success := GetServerOption("MAXNICKLEN")
func (c *Client) GetServerOption(key string) (result string, ok bool) {
c.panicIfNotTracking()
@@ -680,6 +701,42 @@ func (c *Client) GetServerOption(key string) (result string, ok bool) {
return result, ok
}
+// GetServerOptionInt retrieves a server capability setting (as an integer) that was
+// retrieved during client connection. This is also known as ISUPPORT (or RPL_PROTOCTL).
+// Will panic if used when tracking has been disabled. Examples of usage:
+//
+// nickLen, success := GetServerOption("MAXNICKLEN")
+func (c *Client) GetServerOptionInt(key string) (result int, ok bool) {
+ var data string
+ var err error
+
+ data, ok = c.GetServerOption(key)
+ if !ok {
+ return result, ok
+ }
+ result, err = strconv.Atoi(data)
+ if err != nil {
+ ok = false
+ }
+
+ return result, ok
+}
+
+// MaxEventLength returns the maximum supported server length of an event. This is the
+// maximum length of the command and arguments, excluding the source/prefix supported
+// by the protocol. If state tracking is enabled, this will utilize ISUPPORT/IRCv3
+// information to more accurately calculate the maximum supported length (i.e. extended
+// length events).
+func (c *Client) MaxEventLength() (max int) {
+ if !c.Config.disableTracking {
+ c.state.RLock()
+ max = c.state.maxLineLength - c.state.maxPrefixLength
+ c.state.RUnlock()
+ return max
+ }
+ return DefaultMaxLineLength - DefaultMaxPrefixLength
+}
+
// NetworkName returns the network identifier. E.g. "EsperNet", "ByteIRC".
// May be empty if the server does not support RPL_ISUPPORT (or RPL_PROTOCTL).
// Will panic if used when tracking has been disabled.
@@ -773,7 +830,7 @@ func (c *Client) debugLogEvent(e *Event, dropped bool) {
var prefix string
if dropped {
- prefix = "dropping event (disconnected):"
+ prefix = "dropping event (disconnected or timeout):"
} else {
prefix = ">"
}
diff --git a/vendor/github.com/lrstanley/girc/commands.go b/vendor/github.com/lrstanley/girc/commands.go
index 91a8b96a..a3bec879 100644
--- a/vendor/github.com/lrstanley/girc/commands.go
+++ b/vendor/github.com/lrstanley/girc/commands.go
@@ -25,8 +25,8 @@ func (cmd *Commands) Nick(name string) {
// prevent sending extensive JOIN commands.
func (cmd *Commands) Join(channels ...string) {
// 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
+ // we are not exceeding the line length (see Client.MaxEventLength()).
+ max := cmd.c.MaxEventLength() - len(JOIN) - 1
var buffer string
@@ -329,8 +329,8 @@ func (cmd *Commands) List(channels ...string) {
}
// 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
+ // we are not exceeding the line length (see Client.MaxEventLength()).
+ max := cmd.c.MaxEventLength() - len(JOIN) - 1
var buffer string
diff --git a/vendor/github.com/lrstanley/girc/conn.go b/vendor/github.com/lrstanley/girc/conn.go
index 626a6dca..c32eca69 100644
--- a/vendor/github.com/lrstanley/girc/conn.go
+++ b/vendor/github.com/lrstanley/girc/conn.go
@@ -12,6 +12,8 @@ import (
"net"
"sync"
"time"
+
+ "github.com/lrstanley/girc/internal/ctxgroup"
)
// Messages are delimited with CR and LF line endings, we're using the last
@@ -142,17 +144,44 @@ type ErrParseEvent struct {
func (e ErrParseEvent) Error() string { return "unable to parse event: " + e.Line }
-func (c *ircConn) decode() (event *Event, err error) {
- line, err := c.io.ReadString(delim)
- if err != nil {
- return nil, err
- }
+type decodedEvent struct {
+ event *Event
+ err error
+}
- if event = ParseEvent(line); event == nil {
- return nil, ErrParseEvent{line}
- }
+func (c *ircConn) decode() <-chan decodedEvent {
+ ch := make(chan decodedEvent)
+
+ go func() {
+ defer close(ch)
+
+ line, err := c.io.ReadString(delim)
+ if err != nil {
+ select {
+ case ch <- decodedEvent{err: err}:
+ default:
+ }
+
+ return
+ }
+
+ event := ParseEvent(line)
+ if event == nil {
+ select {
+ case ch <- decodedEvent{err: ErrParseEvent{Line: line}}:
+ default:
+ }
+
+ return
+ }
- return event, nil
+ select {
+ case ch <- decodedEvent{event: event}:
+ default:
+ }
+ }()
+
+ return ch
}
func (c *ircConn) encode(event *Event) error {
@@ -291,20 +320,17 @@ startConn:
} else {
c.conn = newMockConn(mock)
}
+ c.mu.Unlock()
var ctx context.Context
ctx, c.stop = context.WithCancel(context.Background())
- c.mu.Unlock()
- errs := make(chan error, 4)
- var wg sync.WaitGroup
- // 4 being the number of goroutines we need to finish when this function
- // returns.
- wg.Add(4)
- go c.execLoop(ctx, errs, &wg)
- go c.readLoop(ctx, errs, &wg)
- go c.sendLoop(ctx, errs, &wg)
- go c.pingLoop(ctx, errs, &wg)
+ group := ctxgroup.New(ctx)
+
+ group.Go(c.execLoop)
+ group.Go(c.readLoop)
+ group.Go(c.sendLoop)
+ group.Go(c.pingLoop)
// Passwords first.
@@ -338,16 +364,15 @@ startConn:
c.RunHandlers(&Event{Command: INITIALIZED, Params: []string{addr}})
// Wait for the first error.
- var result error
- select {
- case <-ctx.Done():
+ err := group.Wait()
+ if err != nil {
+ c.debug.Printf("received error, beginning cleanup: %v", err)
+ } else {
if !c.state.sts.beginUpgrade {
c.debug.Print("received request to close, beginning clean up")
}
+
c.RunHandlers(&Event{Command: CLOSED, Params: []string{addr}})
- case err := <-errs:
- c.debug.Printf("received error, beginning cleanup: %v", err)
- result = err
}
// Make sure that the connection is closed if not already.
@@ -363,20 +388,13 @@ startConn:
c.RunHandlers(&Event{Command: DISCONNECTED, Params: []string{addr}})
- // Once we have our error/result, let all other functions know we're done.
- c.debug.Print("waiting for all routines to finish")
-
- // Wait for all goroutines to finish.
- wg.Wait()
- close(errs)
-
// 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 err == nil {
if c.state.sts.beginUpgrade {
c.state.sts.beginUpgrade = false
c.mu.Unlock()
@@ -389,76 +407,85 @@ startConn:
}
c.mu.Unlock()
- return result
+ return err
}
// 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(ctx context.Context, errs chan error, wg *sync.WaitGroup) {
+func (c *Client) readLoop(ctx context.Context) error {
c.debug.Print("starting readLoop")
defer c.debug.Print("closing readLoop")
- var event *Event
- var err error
+ var de decodedEvent
for {
select {
case <-ctx.Done():
- wg.Done()
- return
+ return nil
default:
_ = c.conn.sock.SetReadDeadline(time.Now().Add(300 * time.Second))
- event, err = c.conn.decode()
- if err != nil {
- errs <- err
- wg.Done()
- return
+
+ select {
+ case <-ctx.Done():
+ return nil
+ case de = <-c.conn.decode():
+ }
+
+ if de.err != nil {
+ return de.err
}
// Check if it's an echo-message.
if !c.Config.disableTracking {
- event.Echo = (event.Command == PRIVMSG || event.Command == NOTICE) &&
- event.Source != nil && event.Source.ID() == c.GetID()
+ de.event.Echo = (de.event.Command == PRIVMSG || de.event.Command == NOTICE) &&
+ de.event.Source != nil && de.event.Source.ID() == c.GetID()
}
- c.rx <- event
+ c.receive(de.event)
}
}
}
-// Send sends an event to the server. Use Client.RunHandlers() if you are
-// simply looking to trigger handlers with an event.
+// Send sends an event to the server. Send will split events if the event is longer
+// than what the server supports, and is an event that supports splitting. Use
+// Client.RunHandlers() if you are simply looking to trigger handlers with an event.
func (c *Client) Send(event *Event) {
var delay time.Duration
- 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] != "" &&
(event.Command == PRIVMSG || event.Command == TOPIC || event.Command == NOTICE) {
event.Params[len(event.Params)-1] = Fmt(event.Params[len(event.Params)-1])
}
- <-time.After(delay)
- c.write(event)
+ var events []*Event
+ events = event.split(c.MaxEventLength())
+
+ for _, e := range events {
+ 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(e, true)
+ c.mu.RUnlock()
+ return
+ }
+
+ c.conn.mu.Lock()
+ delay = c.conn.rate(e.Len())
+ c.conn.mu.Unlock()
+ c.mu.RUnlock()
+ }
+
+ <-time.After(delay)
+ c.write(e)
+ }
}
// write is the lower level function to write an event. It does not have a
-// write-delay when sending events.
+// write-delay when sending events. write will timeout after 30s if the event
+// can't be sent.
func (c *Client) write(event *Event) {
c.mu.RLock()
defer c.mu.RUnlock()
@@ -468,7 +495,19 @@ func (c *Client) write(event *Event) {
c.debugLogEvent(event, true)
return
}
- c.tx <- event
+
+ t := time.NewTimer(30 * time.Second)
+ defer func() {
+ if !t.Stop() {
+ <-t.C
+ }
+ }()
+
+ select {
+ case c.tx <- event:
+ case <-t.C:
+ c.debugLogEvent(event, true)
+ }
}
// rate allows limiting events based on how frequent the event is being sent,
@@ -487,7 +526,7 @@ func (c *ircConn) rate(chars int) time.Duration {
return 0
}
-func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) {
+func (c *Client) sendLoop(ctx context.Context) error {
c.debug.Print("starting sendLoop")
defer c.debug.Print("closing sendLoop")
@@ -537,18 +576,14 @@ func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
if event.Command == QUIT {
c.Close()
- wg.Done()
- return
+ return nil
}
if err != nil {
- errs <- err
- wg.Done()
- return
+ return err
}
case <-ctx.Done():
- wg.Done()
- return
+ return nil
}
}
}
@@ -568,11 +603,10 @@ type ErrTimedOut struct {
func (ErrTimedOut) Error() string { return "timed out waiting for a requested PING response" }
-func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) {
+func (c *Client) pingLoop(ctx context.Context) error {
// Don't run the pingLoop if they want to disable it.
if c.Config.PingDelay <= 0 {
- wg.Done()
- return
+ return nil
}
c.debug.Print("starting pingLoop")
@@ -604,9 +638,8 @@ func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
}
c.conn.mu.RLock()
- if pingSent && time.Since(c.conn.lastPong) > c.Config.PingDelay+(60*time.Second) {
- // It's 60 seconds over what out ping delay is, connection
- // has probably dropped.
+ if pingSent && time.Since(c.conn.lastPong) > c.Config.PingDelay+c.Config.PingTimeout {
+ // PingTimeout exceeded, connection has probably dropped.
err := ErrTimedOut{
TimeSinceSuccess: time.Since(c.conn.lastPong),
LastPong: c.conn.lastPong,
@@ -615,9 +648,7 @@ func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
}
c.conn.mu.RUnlock()
- errs <- err
- wg.Done()
- return
+ return err
}
c.conn.mu.RUnlock()
@@ -628,8 +659,7 @@ func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
c.Cmd.Ping(fmt.Sprintf("%d", time.Now().UnixNano()))
pingSent = true
case <-ctx.Done():
- wg.Done()
- return
+ return nil
}
}
}
diff --git a/vendor/github.com/lrstanley/girc/event.go b/vendor/github.com/lrstanley/girc/event.go
index 7801615d..2622f89e 100644
--- a/vendor/github.com/lrstanley/girc/event.go
+++ b/vendor/github.com/lrstanley/girc/event.go
@@ -13,7 +13,41 @@ import (
const (
eventSpace byte = ' ' // Separator.
- maxLength int = 510 // Maximum length is 510 (2 for line endings).
+
+ // TODO: if state tracking is enabled, we SHOULD be able to use it's known length.
+
+ // Can be overridden by the NICKLEN (or MAXNICKLEN) ISUPPORT parameter. 30 or 31
+ // are typical values for this parameter advertised by servers today.
+ defaultNickLength = 30
+ // The maximum length of <username> may be specified by the USERLEN RPL_ISUPPORT
+ // parameter. If this length is advertised, the username MUST be silently truncated
+ // to the given length before being used.
+ defaultUserLength = 18
+ // If a looked-up domain name is longer than this length (or overridden by the
+ // HOSTLEN ISUPPORT parameter), the server SHOULD opt to use the IP address instead,
+ // so that the hostname is underneath this length.
+ defaultHostLength = 63
+
+ // defaultPrefixPadding defaults the estimated prefix padding length of a given
+ // event. See also:
+ // [ ":" ( servername / ( nickname [ [ "!" user ] "@" host ] ) ) SPACE ]
+ defaultPrefixPadding = 4
+)
+
+var (
+ // DefaultMaxLineLength is the default maximum length for an event. 510 (+2 for line endings)
+ // is used as a default as this is used by many older implementations.
+ //
+ // See also: RFC 2812
+ // IRC messages are always lines of characters terminated with a CR-LF
+ // (Carriage Return - Line Feed) pair, and these messages SHALL NOT
+ // exceed 512 characters in length, counting all characters including
+ // the trailing CR-LF.
+ DefaultMaxLineLength = 510
+
+ // DefaultMaxPrefixLength defines the default max ":nickname!user@host " length
+ // that's used to calculate line splitting.
+ DefaultMaxPrefixLength = defaultPrefixPadding + defaultNickLength + defaultUserLength + defaultHostLength
)
// cutCRFunc is used to trim CR characters from prefixes/messages.
@@ -125,16 +159,16 @@ func ParseEvent(raw string) (e *Event) {
// Event represents an IRC protocol message, see RFC1459 section 2.3.1
//
-// <message> :: [':' <prefix> <SPACE>] <command> <params> <crlf>
-// <prefix> :: <servername> | <nick> ['!' <user>] ['@' <host>]
-// <command> :: <letter>{<letter>} | <number> <number> <number>
-// <SPACE> :: ' '{' '}
-// <params> :: <SPACE> [':' <trailing> | <middle> <params>]
-// <middle> :: <Any *non-empty* sequence of octets not including SPACE or NUL
-// or CR or LF, the first of which may not be ':'>
-// <trailing> :: <Any, possibly empty, sequence of octets not including NUL or
-// CR or LF>
-// <crlf> :: CR LF
+// <message> :: [':' <prefix> <SPACE>] <command> <params> <crlf>
+// <prefix> :: <servername> | <nick> ['!' <user>] ['@' <host>]
+// <command> :: <letter>{<letter>} | <number> <number> <number>
+// <SPACE> :: ' '{' '}
+// <params> :: <SPACE> [':' <trailing> | <middle> <params>]
+// <middle> :: <Any *non-empty* sequence of octets not including SPACE or NUL
+// or CR or LF, the first of which may not be ':'>
+// <trailing> :: <Any, possibly empty, sequence of octets not including NUL or
+// CR or LF>
+// <crlf> :: CR LF
type Event struct {
// Source is the origin of the event.
Source *Source `json:"source"`
@@ -223,11 +257,80 @@ func (e *Event) Equals(ev *Event) bool {
return true
}
-// Len calculates the length of the string representation of event. Note that
-// this will return the true length (even if longer than what IRC supports),
-// which may be useful if you are trying to check and see if a message is
-// too long, to trim it down yourself.
+// split will split a potentially large event that is larger than what the server
+// supports, into multiple events. split will ignore events that cannot be split, and
+// if the event isn't longer than what the server supports, it will just return an array
+// with 1 entry, the original event.
+func (e *Event) split(maxLength int) []*Event {
+ if len(e.Params) < 1 || (e.Command != PRIVMSG && e.Command != NOTICE) {
+ return []*Event{e}
+ }
+
+ // Exclude source, even if it does exist, because the server will likely ignore the
+ // sent source anyway.
+ event := e.Copy()
+ event.Source = nil
+
+ if event.LenOpts(false) < maxLength {
+ return []*Event{e}
+ }
+
+ results := []*Event{}
+
+ // Will force the length check to include " :". This will allow us to get the length
+ // of the commands and necessary prefixes.
+ text := event.Last()
+ event.Params[len(event.Params)-1] = ""
+ cmdLen := event.LenOpts(false)
+
+ var ok bool
+ var ctcp *CTCPEvent
+ if ok, ctcp = e.IsCTCP(); ok {
+ if text == "" {
+ return []*Event{e}
+ }
+
+ text = ctcp.Text
+
+ // ctcpDelim's at start and end, and space between command and trailing text.
+ maxLength -= len(ctcp.Command) + 4
+ }
+
+ // If the command itself is longer than the limit, there is a problem. PRIVMSG should
+ // be 1->1 per RFC. Just return the original message and let it be the user of the
+ // libraries problem.
+ if cmdLen > maxLength {
+ return []*Event{e}
+ }
+
+ // Split the text into correctly size segments, and make the necessary number of
+ // events that duplicate the original event.
+ for _, split := range splitMessage(text, maxLength-cmdLen) {
+ if ctcp != nil {
+ split = string(ctcpDelim) + ctcp.Command + string(eventSpace) + split + string(ctcpDelim)
+ }
+ clonedEvent := event.Copy()
+ clonedEvent.Source = e.Source
+ clonedEvent.Params[len(e.Params)-1] = split
+ results = append(results, clonedEvent)
+ }
+
+ return results
+}
+
+// Len calculates the length of the string representation of event (including tags).
+// Note that this will return the true length (even if longer than what IRC supports),
+// which may be useful if you are trying to check and see if a message is too long, to
+// trim it down yourself.
func (e *Event) Len() (length int) {
+ return e.LenOpts(true)
+}
+
+// LenOpts calculates the length of the string representation of event (with a toggle
+// for tags). Note that this will return the true length (even if longer than what IRC
+// supports), which may be useful if you are trying to check and see if a message is
+// too long, to trim it down yourself.
+func (e *Event) LenOpts(includeTags bool) (length int) {
if e.Tags != nil {
// Include tags and trailing space.
length = e.Tags.Len() + 1
@@ -248,7 +351,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] == "" || strings.HasPrefix(e.Params[i], ":")) {
length++
}
}
@@ -259,10 +362,6 @@ func (e *Event) Len() (length int) {
// Bytes returns a []byte representation of event. Strips all newlines and
// carriage returns.
-//
-// Per RFC2812 section 2.3, messages should not exceed 512 characters in
-// length. This method forces that limit by discarding any characters
-// exceeding the length limit.
func (e *Event) Bytes() []byte {
buffer := new(bytes.Buffer)
@@ -284,7 +383,7 @@ func (e *Event) Bytes() []byte {
// Space separated list of arguments.
if len(e.Params) > 0 {
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] == "" || strings.HasPrefix(e.Params[i], ":")) {
buffer.WriteString(string(eventSpace) + string(messagePrefix) + e.Params[i])
continue
}
@@ -292,11 +391,6 @@ func (e *Event) Bytes() []byte {
}
}
- // We need the limit the buffer length.
- if buffer.Len() > (maxLength) {
- buffer.Truncate(maxLength)
- }
-
// If we truncated in the middle of a utf8 character, we need to remove
// the other (now invalid) bytes.
out := bytes.ToValidUTF8(buffer.Bytes(), nil)
diff --git a/vendor/github.com/lrstanley/girc/format.go b/vendor/github.com/lrstanley/girc/format.go
index 85e3e387..3b9d60af 100644
--- a/vendor/github.com/lrstanley/girc/format.go
+++ b/vendor/github.com/lrstanley/girc/format.go
@@ -7,13 +7,21 @@ package girc
import (
"bytes"
"fmt"
+ "net/url"
"regexp"
"strings"
+ "unicode/utf8"
)
const (
- fmtOpenChar = '{'
- fmtCloseChar = '}'
+ fmtOpenChar = '{'
+ fmtCloseChar = '}'
+ maxWordSplitLength = 30
+)
+
+var (
+ reCode = regexp.MustCompile(`(\x02|\x1d|\x0f|\x03|\x16|\x1f|\x01)`)
+ reColor = regexp.MustCompile(`\x03([019]?\d(,[019]?\d)?)`)
)
var fmtColors = map[string]int{
@@ -66,9 +74,9 @@ var fmtCodes = map[string]string{
//
// For example:
//
-// client.Message("#channel", Fmt("{red}{b}Hello {red,blue}World{c}"))
+// client.Message("#channel", Fmt("{red}{b}Hello {red,blue}World{c}"))
func Fmt(text string) string {
- var last = -1
+ last := -1
for i := 0; i < len(text); i++ {
if text[i] == fmtOpenChar {
last = i
@@ -136,16 +144,12 @@ func TrimFmt(text string) string {
return text
}
-// This is really the only fastest way of doing this (marginally better than
-// actually trying to parse it manually.)
-var reStripColor = regexp.MustCompile(`\x03([019]?\d(,[019]?\d)?)?`)
-
// StripRaw tries to strip all ASCII format codes that are used for IRC.
// Primarily, foreground/background colors, and other control bytes like
// reset, bold, italic, reverse, etc. This also is done in a specific way
// in order to ensure no truncation of other non-irc formatting.
func StripRaw(text string) string {
- text = reStripColor.ReplaceAllString(text, "")
+ text = reColor.ReplaceAllString(text, "")
for _, code := range fmtCodes {
text = strings.ReplaceAll(text, code, "")
@@ -164,12 +168,12 @@ func StripRaw(text string) string {
// all ASCII printable chars. This function will NOT do that for
// compatibility reasons.
//
-// 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 )
+// 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 )
func IsValidChannel(channel string) bool {
if len(channel) <= 1 || len(channel) > 50 {
return false
@@ -214,10 +218,10 @@ func IsValidChannel(channel string) bool {
// IsValidNick validates an IRC nickname. Note that this does not validate
// IRC nickname length.
//
-// nickname = ( letter / special ) *8( letter / digit / special / "-" )
-// letter = 0x41-0x5A / 0x61-0x7A
-// digit = 0x30-0x39
-// special = 0x5B-0x60 / 0x7B-0x7D
+// nickname = ( letter / special ) *8( letter / digit / special / "-" )
+// letter = 0x41-0x5A / 0x61-0x7A
+// digit = 0x30-0x39
+// special = 0x5B-0x60 / 0x7B-0x7D
func IsValidNick(nick string) bool {
if nick == "" {
return false
@@ -253,8 +257,9 @@ func IsValidNick(nick string) bool {
// not be supported on all networks. Some limit this to only a single period.
//
// Per RFC:
-// user = 1*( %x01-09 / %x0B-0C / %x0E-1F / %x21-3F / %x41-FF )
-// ; any octet except NUL, CR, LF, " " and "@"
+//
+// user = 1*( %x01-09 / %x0B-0C / %x0E-1F / %x21-3F / %x41-FF )
+// ; any octet except NUL, CR, LF, " " and "@"
func IsValidUser(name string) bool {
if name == "" {
return false
@@ -350,3 +355,172 @@ func Glob(input, match string) bool {
// Check suffix last.
return trailingGlob || strings.HasSuffix(input, parts[last])
}
+
+// sliceInsert inserts a string into a slice at a specific index, while trying
+// to avoid as many allocations as possible.
+func sliceInsert(input []string, i int, v ...string) []string {
+ total := len(input) + len(v)
+ if total <= cap(input) {
+ output := input[:total]
+ copy(output[i+len(v):], input[i:])
+ copy(output[i:], v)
+ return output
+ }
+ output := make([]string, total)
+ copy(output, input[:i])
+ copy(output[i:], v)
+ copy(output[i+len(v):], input[i:])
+ return output
+}
+
+// splitMessage is a text splitter that takes into consideration a few things:
+// - Ensuring the returned text is no longer than maxWidth.
+// - Attempting to split at the closest word boundary, while still staying inside
+// of the specific maxWidth.
+// - if there is no good word boundary for longer words (or e.g. links, raw data, etc)
+// that are above maxWordSplitLength characters, split the word into chunks to fit the
+//
+// maximum width.
+func splitMessage(input string, maxWidth int) (output []string) {
+ input = strings.ToValidUTF8(input, "?")
+
+ words := strings.FieldsFunc(strings.TrimSpace(input), func(r rune) bool {
+ switch r { // Same as unicode.IsSpace, but without ctrl/lf.
+ case '\t', '\v', '\f', ' ', 0x85, 0xA0:
+ return true
+ }
+ return false
+ })
+
+ output = []string{""}
+ codes := []string{}
+
+ var lastColor string
+ var match []string
+
+ for i := 0; i < len(words); i++ {
+ j := strings.IndexAny(words[i], "\n\r")
+ if j == -1 {
+ continue
+ }
+
+ word := words[i]
+ words[i] = word[:j]
+
+ words = sliceInsert(words, i+1, "", strings.TrimLeft(word[j:], "\n\r"))
+ }
+
+ for _, word := range words {
+ // Used in place of a single newline.
+ if word == "" {
+ // Last line was already empty or already only had control characters.
+ if output[len(output)-1] == "" || output[len(output)-1] == lastColor+word {
+ continue
+ }
+
+ output = append(output, strings.Join(codes, "")+lastColor+word)
+ continue
+ }
+
+ // Keep track of the last used color codes.
+ match = reColor.FindAllString(word, -1)
+ if len(match) > 0 {
+ lastColor = match[len(match)-1]
+ }
+
+ // Find all sequence codes -- this approach isn't perfect (ideally, a lexer
+ // should be used to track each exact type of code), but it's good enough for
+ // most cases.
+ match = reCode.FindAllString(word, -1)
+ if len(match) > 0 {
+ for _, m := range match {
+ // Reset was used, so clear all codes.
+ if m == fmtCodes["reset"] {
+ lastColor = ""
+ codes = []string{}
+ continue
+ }
+
+ // Check if we already have the code, and if so, remove it (closing).
+ contains := false
+ for i := 0; i < len(codes); i++ {
+ if m == codes[i] {
+ contains = true
+ codes = append(codes[:i], codes[i+1:]...)
+
+ // If it's a closing color code, reset the last used color
+ // as well.
+ if m == fmtCodes["clear"] {
+ lastColor = ""
+ }
+
+ break
+ }
+ }
+
+ // Track the new code, unless it's a color clear but we aren't
+ // tracking a color right now.
+ if !contains && (lastColor == "" || m != fmtCodes["clear"]) {
+ codes = append(codes, m)
+ }
+ }
+ }
+
+ checkappend:
+
+ // Check if we can append, otherwise we must split.
+ if 1+utf8.RuneCountInString(word)+utf8.RuneCountInString(output[len(output)-1]) < maxWidth {
+ if output[len(output)-1] != "" {
+ output[len(output)-1] += " "
+ }
+ output[len(output)-1] += word
+ continue
+ }
+
+ // If the word can fit on a line by itself, check if it's a url. If it is,
+ // put it on it's own line.
+ if utf8.RuneCountInString(word+strings.Join(codes, "")+lastColor) < maxWidth {
+ if _, err := url.Parse(word); err == nil {
+ output = append(output, strings.Join(codes, "")+lastColor+word)
+ continue
+ }
+ }
+
+ // Check to see if we can split by misc symbols, but must be at least a few
+ // characters long to be split by it.
+ if j := strings.IndexAny(word, "-+_=|/~:;,."); j > 3 && 1+utf8.RuneCountInString(word[0:j])+utf8.RuneCountInString(output[len(output)-1]) < maxWidth {
+ if output[len(output)-1] != "" {
+ output[len(output)-1] += " "
+ }
+ output[len(output)-1] += word[0:j]
+ word = word[j+1:]
+ goto checkappend
+ }
+
+ // If the word is longer than is acceptable to just put on the next line,
+ // split it into chunks. Also don't split the word if only a few characters
+ // left of the word would be on the next line.
+ if 1+utf8.RuneCountInString(word) > maxWordSplitLength && maxWidth-utf8.RuneCountInString(output[len(output)-1]) > 5 {
+ left := maxWidth - utf8.RuneCountInString(output[len(output)-1]) - 1 // -1 for the space
+
+ if output[len(output)-1] != "" {
+ output[len(output)-1] += " "
+ }
+ output[len(output)-1] += word[0:left]
+ word = word[left:]
+ goto checkappend
+ }
+
+ left := maxWidth - utf8.RuneCountInString(output[len(output)-1])
+ output[len(output)-1] += word[0:left]
+
+ output = append(output, strings.Join(codes, "")+lastColor)
+ word = word[left:]
+ goto checkappend
+ }
+
+ for i := 0; i < len(output); i++ {
+ output[i] = strings.ToValidUTF8(output[i], "?")
+ }
+ return output
+}
diff --git a/vendor/github.com/lrstanley/girc/internal/ctxgroup/ctxgroup.go b/vendor/github.com/lrstanley/girc/internal/ctxgroup/ctxgroup.go
new file mode 100644
index 00000000..e66ed2b5
--- /dev/null
+++ b/vendor/github.com/lrstanley/girc/internal/ctxgroup/ctxgroup.go
@@ -0,0 +1,67 @@
+// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
+// of this source code is governed by the MIT license that can be found in
+// the LICENSE file.
+
+package ctxgroup
+
+import (
+ "context"
+ "sync"
+)
+
+// A Group is a collection of goroutines working on subtasks that are part of
+// the same overall task.
+type Group struct {
+ ctx context.Context
+ cancel func()
+
+ wg sync.WaitGroup
+
+ errOnce sync.Once
+ err error
+}
+
+// New returns a new Group and an associated context derived from ctx.
+// Obtain the derived context from calling Group.Context().
+//
+// The derived context is canceled the first time a function passed to Go
+// returns a non-nil error or the first time Wait returns, whichever occurs
+// first.
+func New(ctx context.Context) *Group {
+ nctx, cancel := context.WithCancel(ctx)
+ return &Group{ctx: nctx, cancel: cancel}
+}
+
+// Context returns the context for this group. It may be canceled by the first
+// function to return a non-nil error.
+func (g *Group) Context() context.Context {
+ return g.ctx
+}
+
+// Wait blocks until all function calls from the Go method have returned, then
+// returns the first non-nil error (if any) from them.
+func (g *Group) Wait() error {
+ g.wg.Wait()
+ if g.cancel != nil {
+ g.cancel()
+ }
+ return g.err
+}
+
+// Go calls the given function in a new goroutine. The first call to return a
+// non-nil error cancels the group; its error will be returned by Wait.
+func (g *Group) Go(f func(ctx context.Context) error) {
+ g.wg.Add(1)
+ go func() {
+ defer g.wg.Done()
+
+ if err := f(g.ctx); err != nil {
+ g.errOnce.Do(func() {
+ g.err = err
+ if g.cancel != nil {
+ g.cancel()
+ }
+ })
+ }
+ }()
+}
diff --git a/vendor/github.com/lrstanley/girc/modes.go b/vendor/github.com/lrstanley/girc/modes.go
index 35ff103a..127b0a79 100644
--- a/vendor/github.com/lrstanley/girc/modes.go
+++ b/vendor/github.com/lrstanley/girc/modes.go
@@ -118,13 +118,14 @@ func (c *CModes) Get(mode string) (args string, ok bool) {
}
// hasArg checks to see if the mode supports arguments. What ones support this?:
-// A = Mode that adds or removes a nick or address to a list. Always has a parameter.
-// B = Mode that changes a setting and always has a parameter.
-// C = Mode that changes a setting and only has a parameter when set.
-// D = Mode that changes a setting and never has a parameter.
-// Note: Modes of type A return the list when there is no parameter present.
-// Note: Some clients assumes that any mode not listed is of type D.
-// Note: Modes in PREFIX are not listed but could be considered type B.
+//
+// A = Mode that adds or removes a nick or address to a list. Always has a parameter.
+// B = Mode that changes a setting and always has a parameter.
+// C = Mode that changes a setting and only has a parameter when set.
+// D = Mode that changes a setting and never has a parameter.
+// Note: Modes of type A return the list when there is no parameter present.
+// Note: Some clients assumes that any mode not listed is of type D.
+// Note: Modes in PREFIX are not listed but could be considered type B.
func (c *CModes) hasArg(set bool, mode byte) (hasArgs, isSetting bool) {
if len(c.raw) < 1 {
return false, true
diff --git a/vendor/github.com/lrstanley/girc/state.go b/vendor/github.com/lrstanley/girc/state.go
index d9e72981..96d2ef88 100644
--- a/vendor/github.com/lrstanley/girc/state.go
+++ b/vendor/github.com/lrstanley/girc/state.go
@@ -28,10 +28,21 @@ type state struct {
// last capability check. These will get sent once we have received the
// last capability list command from the server.
tmpCap map[string]map[string]string
+
// serverOptions are the standard capabilities and configurations
// supported by the server at connection time. This also includes
// RPL_ISUPPORT entries.
serverOptions map[string]string
+
+ // maxLineLength defines how long before we truncate (or split) messages.
+ // DefaultMaxLineLength is what is used by default, as this is going to be a common
+ // standard. However, protocols like IRCv3, or ISUPPORT can override this.
+ maxLineLength int
+
+ // maxPrefixLength defines the estimated prefix length (":nick!user@host ") that
+ // we can use to calculate line splits.
+ maxPrefixLength int
+
// motd is the servers message of the day.
motd string
@@ -51,9 +62,11 @@ func (s *state) reset(initial bool) {
s.host = ""
s.channels = make(map[string]*Channel)
s.users = make(map[string]*User)
- s.serverOptions = make(map[string]string)
s.enabledCap = make(map[string]map[string]string)
s.tmpCap = make(map[string]map[string]string)
+ s.serverOptions = make(map[string]string)
+ s.maxLineLength = DefaultMaxLineLength
+ s.maxPrefixLength = DefaultMaxPrefixLength
s.motd = ""
if initial {