diff options
Diffstat (limited to 'vendor/github.com/lrstanley/girc')
-rw-r--r-- | vendor/github.com/lrstanley/girc/.editorconfig | 5 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/.golangci.yml | 191 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/builtin.go | 42 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/cap.go | 4 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/cap_sasl.go | 6 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/cap_tags.go | 7 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/client.go | 89 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/commands.go | 8 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/conn.go | 208 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/event.go | 146 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/format.go | 216 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/internal/ctxgroup/ctxgroup.go | 67 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/modes.go | 15 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/state.go | 15 |
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 { |