├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── go.mod ├── client_handler_test.go ├── client_handler.go ├── .golangci.yml ├── LICENSE ├── utils.go ├── utils_test.go ├── go.sum ├── utils └── gen-numerics.py ├── README.md ├── conn.go ├── client_handlers.go ├── isupport.go ├── conn_test.go ├── stream_test.go ├── parser_test.go ├── tracker.go ├── parser.go ├── client.go ├── client_test.go └── numerics.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [belak] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.cover 2 | *.test 3 | *.out 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "testcases"] 2 | path = _testcases 3 | url = https://github.com/go-irc/irc-parser-tests.git 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gopkg.in/irc.v4 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/stretchr/testify v1.8.0 7 | golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 8 | gopkg.in/yaml.v2 v2.4.0 9 | ) 10 | -------------------------------------------------------------------------------- /client_handler_test.go: -------------------------------------------------------------------------------- 1 | package irc_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "gopkg.in/irc.v4" 9 | ) 10 | 11 | func TestHandlerFunc(t *testing.T) { 12 | t.Parallel() 13 | 14 | hit := false 15 | var f irc.HandlerFunc = func(c *irc.Client, m *irc.Message) { 16 | hit = true 17 | } 18 | 19 | f.Handle(nil, nil) 20 | assert.True(t, hit, "HandlerFunc doesn't work correctly as Handler") 21 | } 22 | -------------------------------------------------------------------------------- /client_handler.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | // Handler is a simple interface meant for dispatching a message from 4 | // a Client connection. 5 | type Handler interface { 6 | Handle(*Client, *Message) 7 | } 8 | 9 | // HandlerFunc is a simple wrapper around a function which allows it 10 | // to be used as a Handler. 11 | type HandlerFunc func(*Client, *Message) 12 | 13 | // Handle calls f(c, m). 14 | func (f HandlerFunc) Handle(c *Client, m *Message) { 15 | f(c, m) 16 | } 17 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable-all: true 3 | disable: 4 | - gochecknoglobals 5 | - gomnd 6 | - nlreturn 7 | - wsl 8 | # TODO: maybe re-enable these 9 | - exhaustruct 10 | - godox 11 | - varnamelen 12 | # TODO: fix error handling 13 | - goerr113 14 | - wrapcheck 15 | - errorlint 16 | - errname 17 | 18 | # Deprecated linters 19 | - deadcode 20 | - exhaustivestruct 21 | - golint 22 | - ifshort 23 | - interfacer 24 | - maligned 25 | - nosnakecase 26 | - scopelint 27 | - structcheck 28 | - varcheck 29 | 30 | linters-settings: 31 | cyclop: 32 | max-complexity: 15 33 | govet: 34 | check-shadowing: true 35 | gci: 36 | local-prefixes: gopkg.in/irc.v4 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Kaleb Elwert 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "bytes" 5 | "regexp" 6 | ) 7 | 8 | var maskTranslations = map[byte]string{ 9 | '?': ".", 10 | '*': ".*", 11 | } 12 | 13 | // MaskToRegex converts an irc mask to a go Regexp for more convenient 14 | // use. This should never return an error, but we have this here just 15 | // in case. 16 | func MaskToRegex(rawMask string) (*regexp.Regexp, error) { 17 | input := bytes.NewBufferString(rawMask) 18 | 19 | output := &bytes.Buffer{} 20 | output.WriteByte('^') 21 | 22 | for { 23 | c, err := input.ReadByte() 24 | if err != nil { 25 | break 26 | } 27 | 28 | if c == '\\' { //nolint:nestif 29 | c, err = input.ReadByte() 30 | if err != nil { 31 | output.WriteString(regexp.QuoteMeta("\\")) 32 | break 33 | } 34 | 35 | if c == '?' || c == '*' || c == '\\' { 36 | output.WriteString(regexp.QuoteMeta(string(c))) 37 | } else { 38 | output.WriteString(regexp.QuoteMeta("\\" + string(c))) 39 | } 40 | } else if trans, ok := maskTranslations[c]; ok { 41 | output.WriteString(trans) 42 | } else { 43 | output.WriteString(regexp.QuoteMeta(string(c))) 44 | } 45 | } 46 | 47 | output.WriteByte('$') 48 | 49 | return regexp.Compile(output.String()) 50 | } 51 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package irc_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "gopkg.in/irc.v4" 9 | ) 10 | 11 | func TestMaskToRegex(t *testing.T) { 12 | t.Parallel() 13 | 14 | var testCases = []struct { //nolint:gofumpt 15 | Input string 16 | Expect string 17 | }{ 18 | { // Empty should be fine 19 | Input: "", 20 | Expect: "^$", 21 | }, 22 | { // EVERYONE! 23 | Input: "*!*@*", 24 | Expect: "^.*!.*@.*$", 25 | }, 26 | { 27 | Input: "", 28 | Expect: "^$", 29 | }, 30 | { 31 | Input: "", 32 | Expect: "^$", 33 | }, 34 | { // Escape the slash 35 | Input: "a\\\\b", 36 | Expect: "^a\\\\b$", 37 | }, 38 | { // Escape a * 39 | Input: "a\\*b", 40 | Expect: "^a\\*b$", 41 | }, 42 | { // Escape a ? 43 | Input: "a\\?b", 44 | Expect: "^a\\?b$", 45 | }, 46 | { // Single slash in the middle of a string should be a slash 47 | Input: "a\\b", 48 | Expect: "^a\\\\b$", 49 | }, 50 | { // Single slash should just match a single slash 51 | Input: "\\", 52 | Expect: "^\\\\$", 53 | }, 54 | { 55 | Input: "\\a?", 56 | Expect: "^\\\\a.$", 57 | }, 58 | } 59 | 60 | for _, testCase := range testCases { 61 | ret, err := irc.MaskToRegex(testCase.Input) 62 | assert.NoError(t, err) 63 | assert.Equal(t, testCase.Expect, ret.String()) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 8 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 9 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 10 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 11 | golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ= 12 | golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 16 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 17 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 18 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 19 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Because "push" also shows up on PRs, we don't need to do both. 4 | on: [push] 5 | 6 | jobs: 7 | build-1_13: 8 | name: Build (Go 1.13) 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Set up Go 13 | uses: actions/setup-go@v2 14 | with: 15 | go-version: '~1.13.5' 16 | 17 | - name: Check out code 18 | uses: actions/checkout@v2 19 | with: 20 | submodules: true 21 | 22 | - name: Clean up extra files 23 | run: rm ./_testcases/*.go 24 | 25 | - name: Download deps 26 | run: go mod download 27 | 28 | - name: Run tests 29 | run: go test -race -v ./... 30 | 31 | build-latest: 32 | name: Build (Latest) 33 | runs-on: ubuntu-latest 34 | 35 | steps: 36 | - name: Set up Go 37 | uses: actions/setup-go@v2 38 | with: 39 | go-version: '~1.19' 40 | 41 | - name: Check out code 42 | uses: actions/checkout@v2 43 | with: 44 | submodules: true 45 | 46 | - name: Clean up extra files 47 | run: rm ./_testcases/*.go 48 | 49 | - name: Download deps 50 | run: go mod download 51 | 52 | - name: Run tests 53 | run: go test -covermode=atomic -coverprofile=profile.cov -race -v ./... 54 | 55 | - name: Submit coverage report 56 | env: 57 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | run: | 59 | GO111MODULE=off go get github.com/mattn/goveralls 60 | $(go env GOPATH)/bin/goveralls -coverprofile=profile.cov -service=github 61 | 62 | lint: 63 | name: golangci-lint 64 | runs-on: ubuntu-latest 65 | 66 | steps: 67 | - name: Check out code 68 | uses: actions/checkout@v2 69 | 70 | - name: Run golangci-lint 71 | uses: golangci/golangci-lint-action@v2 72 | with: 73 | version: 'v1.49.0' 74 | -------------------------------------------------------------------------------- /utils/gen-numerics.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | 3 | data = yaml.safe_load(open('./numerics.yml', 'r')) 4 | vals = data['values'] 5 | 6 | used = set() 7 | 8 | print('//nolint') 9 | print('package irc') 10 | print() 11 | print('const (') 12 | 13 | 14 | def print_item(idx, item, ircv3=False, obsolete=None, tablevel=1, **kwargs): 15 | if idx in used: 16 | return 17 | 18 | origin = item.get('origin', '') 19 | origin_name = kwargs.pop('origin', '') 20 | 21 | if ircv3: 22 | if ('ircv3.net' not in item.get('contact', '') 23 | and 'ircv3.net' not in item.get('information', '')): 24 | return 25 | elif origin_name and not origin or origin_name not in origin: 26 | return 27 | 28 | kwargs['obsolete'] = obsolete 29 | for k, v in kwargs.items(): 30 | if item.get(k) != v: 31 | return 32 | 33 | # Mark seen 34 | used.add(idx) 35 | 36 | print('\t' * tablevel, end='') 37 | 38 | print('{} = "{}"'.format(item['name'], item['numeric']), end='') 39 | 40 | if origin and origin != origin_name: 41 | print(' // {}'.format(origin), end='') 42 | 43 | print() 44 | 45 | 46 | def print_specific(**kwargs): 47 | for index, item in enumerate(vals): 48 | print_item(index, item, **kwargs) 49 | 50 | 51 | print('\t// RFC1459') 52 | print_specific(origin='RFC1459') 53 | print() 54 | print('\t// RFC1459 (Obsolete)') 55 | print_specific(origin='RFC1459', obsolete=True) 56 | print() 57 | print('\t// RFC2812') 58 | print_specific(origin='RFC2812') 59 | print() 60 | print('\t// RFC2812 (Obsolete)') 61 | print_specific(origin='RFC2812', obsolete=True) 62 | print() 63 | print('\t// IRCv3') 64 | print_specific(origin='IRCv3', ircv3=True) 65 | print() 66 | #print('\t// IRCv3 (obsolete)') 67 | #print_specific(origin='IRCv3', ircv3=True, obsolete=True) 68 | #print() 69 | print('\t// Other') 70 | print_specific(name='RPL_ISUPPORT') 71 | print() 72 | print('\t// Ignored') 73 | print('\t//') 74 | print('\t// Anything not in an RFC has not been included because') 75 | print('\t// there are way too many conflicts to deal with.') 76 | print('\t/*') 77 | print_specific(tablevel=2) 78 | print('\t//*/') 79 | print() 80 | print('\t// Obsolete') 81 | print('\t/*') 82 | print_specific(obsolete=True, tablevel=2) 83 | print('\t//*/') 84 | 85 | print(')') 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-irc 2 | 3 | [![GoDoc](https://img.shields.io/badge/doc-GoDoc-blue.svg)](https://pkg.go.dev/gopkg.in/irc.v4) 4 | [![Build Status](https://img.shields.io/github/actions/workflow/status/go-irc/irc/ci.yml?branch=master)](https://github.com/go-irc/irc/actions) 5 | [![Coverage Status](https://img.shields.io/coveralls/github/go-irc/irc.svg)](https://coveralls.io/github/go-irc/irc?branch=master) 6 | 7 | This package was originally created to only handle message parsing, but has since been expanded to include small abstractions around connections and a very general client type with some small conveniences. 8 | 9 | This library is not designed to hide any of the IRC elements from you. If you just want to build a simple chat bot and don't want to deal with IRC in particular, there are a number of other libraries which provide a more full featured client if that's what you're looking for. 10 | 11 | This library is meant to stay as simple as possible so it can be a building block for other packages. 12 | 13 | This library aims for API compatibility whenever possible. New functions and other additions will not result in a major version increase unless they break the API. This library aims to follow the semver recommendations mentioned on gopkg.in. 14 | 15 | This packages uses newer error handling APIs so, only go 1.13+ is officially supported. 16 | 17 | ## Import Paths 18 | 19 | All development happens on the `master` branch and when features are considered stable enough, a new release will be tagged. 20 | 21 | * `gopkg.in/irc.v4` should be used to develop against the commits tagged as stable 22 | 23 | ## Development 24 | 25 | In order to run the tests, make sure all submodules are up to date. If you are just using this library, these are not needed. 26 | 27 | ## Notes on Unstable APIs 28 | 29 | Currently the ISupport and Tracker APIs are considered unstable - these may be broken or removed with minor version changes, so use them at your own risk. 30 | 31 | ## Major Version Changes 32 | 33 | ### v4 34 | 35 | - Added initial ISupport and Tracker support as unstable APIs 36 | - Drop the separate TagValue type 37 | - Drop Tags.GetTag 38 | 39 | ### v3 40 | 41 | - Import path changed back to `gopkg.in/irc.v3` without the version suffix. 42 | 43 | ### v2 44 | 45 | - CTCP messages will no longer be rewritten. The decision was made that this library should pass through all messages without mangling them. 46 | - Remove Message.FromChannel as this is not always accurate, while Client.FromChannel should always be accurate. 47 | 48 | ### v1 49 | 50 | Initial release 51 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "io" 8 | ) 9 | 10 | // Conn represents a simple IRC client. It embeds an irc.Reader and an 11 | // irc.Writer. 12 | type Conn struct { 13 | *Reader 14 | *Writer 15 | } 16 | 17 | // NewConn creates a new Conn. 18 | func NewConn(rw io.ReadWriter) *Conn { 19 | return &Conn{ 20 | NewReader(rw), 21 | NewWriter(rw), 22 | } 23 | } 24 | 25 | // Writer is the outgoing side of a connection. 26 | type Writer struct { 27 | // DebugCallback is called for each outgoing message. The name of this may 28 | // not be stable. 29 | DebugCallback func(line string) 30 | 31 | // WriteCallback is called for each outgoing message. It needs to write the 32 | // message to the connection. Note that this API is not a part of the semver 33 | // stability guarantee. 34 | WriteCallback func(w *Writer, line string) error 35 | 36 | // Internal fields 37 | writer io.Writer 38 | } 39 | 40 | func defaultWriteCallback(w *Writer, line string) error { 41 | _, err := w.RawWrite([]byte(line + "\r\n")) 42 | return err 43 | } 44 | 45 | // NewWriter creates an irc.Writer from an io.Writer. 46 | func NewWriter(w io.Writer) *Writer { 47 | return &Writer{nil, defaultWriteCallback, w} 48 | } 49 | 50 | // RawWrite will write the given data to the underlying connection, skipping the 51 | // WriteCallback. This is meant to be used by implementations of the 52 | // WriteCallback to write data directly to the stream. Otherwise, it is 53 | // recommended to avoid this function and use one of the other helpers. Also 54 | // note that it will not append \r\n to the end of the line. 55 | func (w *Writer) RawWrite(data []byte) (int, error) { 56 | return w.writer.Write(data) 57 | } 58 | 59 | // Write is a simple function which will write the given line to the 60 | // underlying connection. 61 | func (w *Writer) Write(line string) error { 62 | if w.DebugCallback != nil { 63 | w.DebugCallback(line) 64 | } 65 | 66 | return w.WriteCallback(w, line) 67 | } 68 | 69 | // Writef is a wrapper around the connection's Write method and 70 | // fmt.Sprintf. Simply use it to send a message as you would normally 71 | // use fmt.Printf. 72 | func (w *Writer) Writef(format string, args ...interface{}) error { 73 | return w.Write(fmt.Sprintf(format, args...)) 74 | } 75 | 76 | // WriteMessage writes the given message to the stream. 77 | func (w *Writer) WriteMessage(m *Message) error { 78 | return w.Write(m.String()) 79 | } 80 | 81 | // Reader is the incoming side of a connection. The data will be 82 | // buffered, so do not re-use the io.Reader used to create the 83 | // irc.Reader. 84 | type Reader struct { 85 | // DebugCallback is called for each incoming message. The name of this may 86 | // not be stable. 87 | DebugCallback func(string) 88 | 89 | // Internal fields 90 | reader *bufio.Reader 91 | } 92 | 93 | // NewReader creates an irc.Reader from an io.Reader. Note that once a reader is 94 | // passed into this function, you should no longer use it as it is being used 95 | // inside a bufio.Reader so you cannot rely on only the amount of data for a 96 | // Message being read when you call ReadMessage. 97 | func NewReader(r io.Reader) *Reader { 98 | return &Reader{ 99 | nil, 100 | bufio.NewReader(r), 101 | } 102 | } 103 | 104 | // ReadMessage returns the next message from the stream or an error. 105 | // It ignores empty messages. 106 | func (r *Reader) ReadMessage() (*Message, error) { 107 | var msg *Message 108 | 109 | // It's valid for a message to be empty. Clients should ignore these, 110 | // so we do to be good citizens. 111 | err := ErrZeroLengthMessage 112 | for errors.Is(err, ErrZeroLengthMessage) { 113 | var line string 114 | line, err = r.reader.ReadString('\n') 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | if r.DebugCallback != nil { 120 | r.DebugCallback(line) 121 | } 122 | 123 | // Parse the message from our line 124 | msg, err = ParseMessage(line) 125 | } 126 | return msg, err 127 | } 128 | -------------------------------------------------------------------------------- /client_handlers.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type clientFilter func(*Client, *Message) 9 | 10 | // clientFilters are pre-processing which happens for certain message 11 | // types. These were moved from below to keep the complexity of each 12 | // component down. 13 | var clientFilters = map[string]clientFilter{ 14 | "001": handle001, 15 | "433": handle433, 16 | "437": handle437, 17 | "PING": handlePing, 18 | "PONG": handlePong, 19 | "NICK": handleNick, 20 | "CAP": handleCap, 21 | } 22 | 23 | // From rfc2812 section 5.1 (Command responses) 24 | // 25 | // 001 RPL_WELCOME 26 | // "Welcome to the Internet Relay Network 27 | // !@" 28 | func handle001(c *Client, m *Message) { 29 | c.currentNick = m.Params[0] 30 | c.connected = true 31 | } 32 | 33 | // From rfc2812 section 5.2 (Error Replies) 34 | // 35 | // 433 ERR_NICKNAMEINUSE 36 | // " :Nickname is already in use" 37 | // 38 | // - Returned when a NICK message is processed that results 39 | // in an attempt to change to a currently existing 40 | // nickname. 41 | func handle433(c *Client, m *Message) { 42 | // We only want to try and handle nick collisions during the initial 43 | // handshake. 44 | if c.connected { 45 | return 46 | } 47 | c.currentNick += "_" 48 | _ = c.Writef("NICK :%s", c.currentNick) 49 | } 50 | 51 | // From rfc2812 section 5.2 (Error Replies) 52 | // 53 | // 437 ERR_UNAVAILRESOURCE 54 | // " :Nick/channel is temporarily unavailable" 55 | // 56 | // - Returned by a server to a user trying to join a channel 57 | // currently blocked by the channel delay mechanism. 58 | // 59 | // - Returned by a server to a user trying to change nickname 60 | // when the desired nickname is blocked by the nick delay 61 | // mechanism. 62 | func handle437(c *Client, m *Message) { 63 | // We only want to try and handle nick collisions during the initial 64 | // handshake. 65 | if c.connected { 66 | return 67 | } 68 | c.currentNick += "_" 69 | _ = c.Writef("NICK :%s", c.currentNick) 70 | } 71 | 72 | func handlePing(c *Client, m *Message) { 73 | reply := m.Copy() 74 | reply.Command = "PONG" 75 | _ = c.WriteMessage(reply) 76 | } 77 | 78 | func handlePong(c *Client, m *Message) { 79 | if c.incomingPongChan != nil { 80 | select { 81 | case c.incomingPongChan <- m.Trailing(): 82 | default: 83 | // Note that this return isn't really needed, but it helps some code 84 | // coverage tools actually see this line. 85 | return 86 | } 87 | } 88 | } 89 | 90 | func handleNick(c *Client, m *Message) { 91 | if m.Prefix.Name == c.currentNick && len(m.Params) > 0 { 92 | c.currentNick = m.Params[0] 93 | } 94 | } 95 | 96 | var capFilters = map[string]clientFilter{ 97 | "LS": handleCapLs, 98 | "ACK": handleCapAck, 99 | "NAK": handleCapNak, 100 | } 101 | 102 | func handleCap(c *Client, m *Message) { 103 | if c.remainingCapResponses <= 0 || len(m.Params) <= 2 { 104 | return 105 | } 106 | 107 | if filter, ok := capFilters[m.Params[1]]; ok { 108 | filter(c, m) 109 | } 110 | 111 | if c.remainingCapResponses <= 0 { 112 | for key, capStatus := range c.caps { 113 | if capStatus.Required && !capStatus.Enabled { 114 | c.sendError(fmt.Errorf("CAP %s requested but not accepted", key)) 115 | return 116 | } 117 | } 118 | 119 | _ = c.Write("CAP END") 120 | } 121 | } 122 | 123 | func handleCapLs(c *Client, m *Message) { 124 | for _, key := range strings.Split(m.Trailing(), " ") { 125 | capStatus := c.caps[key] 126 | capStatus.Available = true 127 | c.caps[key] = capStatus 128 | } 129 | c.remainingCapResponses-- 130 | } 131 | 132 | func handleCapAck(c *Client, m *Message) { 133 | for _, key := range strings.Split(m.Trailing(), " ") { 134 | capStatus := c.caps[key] 135 | capStatus.Enabled = true 136 | c.caps[key] = capStatus 137 | } 138 | c.remainingCapResponses-- 139 | } 140 | 141 | func handleCapNak(c *Client, m *Message) { 142 | // If we got a NAK and this REQ was required, we need to bail 143 | // with an error. 144 | for _, key := range strings.Split(m.Trailing(), " ") { 145 | if c.caps[key].Required { 146 | c.sendError(fmt.Errorf("CAP %s requested but was rejected", key)) 147 | return 148 | } 149 | } 150 | c.remainingCapResponses-- 151 | } 152 | -------------------------------------------------------------------------------- /isupport.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "sync" 7 | ) 8 | 9 | // ISupportTracker tracks the ISUPPORT values returned by servers and provides a 10 | // convenient way to access them. 11 | // 12 | // From http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt 13 | // 14 | // 005 RPL_ISUPPORT. 15 | type ISupportTracker struct { 16 | sync.RWMutex 17 | 18 | data map[string]string 19 | } 20 | 21 | // NewISupportTracker creates a new tracker instance with a set of sane defaults 22 | // if the server is missing them. 23 | func NewISupportTracker() *ISupportTracker { 24 | return &ISupportTracker{ 25 | data: map[string]string{ 26 | "PREFIX": "(ov)@+", 27 | }, 28 | } 29 | } 30 | 31 | // Handle needs to be called for all 005 IRC messages. All other messages will 32 | // be ignored. 33 | func (t *ISupportTracker) Handle(msg *Message) error { 34 | // Ensure only ISupport messages go through here 35 | if msg.Command != "005" { 36 | return nil 37 | } 38 | 39 | if len(msg.Params) < 2 { 40 | return errors.New("malformed RPL_ISUPPORT message") 41 | } 42 | 43 | // Check for really old servers (or servers which based 005 off of rfc2812). 44 | if !strings.HasSuffix(msg.Trailing(), "server") { 45 | return errors.New("received invalid RPL_ISUPPORT message") 46 | } 47 | 48 | t.Lock() 49 | defer t.Unlock() 50 | 51 | for _, param := range msg.Params[1 : len(msg.Params)-1] { 52 | data := strings.SplitN(param, "=", 2) 53 | if len(data) < 2 { 54 | t.data[data[0]] = "" 55 | continue 56 | } 57 | 58 | // TODO: this should properly handle decoding values containing \xHH 59 | t.data[data[0]] = data[1] 60 | } 61 | 62 | return nil 63 | } 64 | 65 | // IsEnabled will check for boolean ISupport values. Note that for ISupport 66 | // boolean true simply means the value exists. 67 | func (t *ISupportTracker) IsEnabled(key string) bool { 68 | t.RLock() 69 | defer t.RUnlock() 70 | 71 | _, ok := t.data[key] 72 | return ok 73 | } 74 | 75 | // GetList will check for list ISupport values. 76 | func (t *ISupportTracker) GetList(key string) ([]string, bool) { 77 | t.RLock() 78 | defer t.RUnlock() 79 | 80 | data, ok := t.data[key] 81 | if !ok { 82 | return nil, false 83 | } 84 | 85 | return strings.Split(data, ","), true 86 | } 87 | 88 | // GetMap will check for map ISupport values. 89 | func (t *ISupportTracker) GetMap(key string) (map[string]string, bool) { 90 | t.RLock() 91 | defer t.RUnlock() 92 | 93 | data, ok := t.data[key] 94 | if !ok { 95 | return nil, false 96 | } 97 | 98 | ret := make(map[string]string) 99 | 100 | for _, v := range strings.Split(data, ",") { 101 | innerData := strings.SplitN(v, ":", 2) 102 | if len(innerData) != 2 { 103 | return nil, false 104 | } 105 | 106 | ret[innerData[0]] = innerData[1] 107 | } 108 | 109 | return ret, true 110 | } 111 | 112 | // GetRaw will get the raw ISupport values. 113 | func (t *ISupportTracker) GetRaw(key string) (string, bool) { 114 | t.RLock() 115 | defer t.RUnlock() 116 | 117 | ret, ok := t.data[key] 118 | return ret, ok 119 | } 120 | 121 | // GetPrefixMap gets the mapping of mode to symbol for the PREFIX value. 122 | // Unfortunately, this is fairly specific, so it can only be used with PREFIX. 123 | func (t *ISupportTracker) GetPrefixMap() (map[rune]rune, bool) { 124 | // Sample: (qaohv)~&@%+ 125 | prefix, _ := t.GetRaw("PREFIX") 126 | 127 | // We only care about the symbols 128 | i := strings.IndexByte(prefix, ')') 129 | if len(prefix) == 0 || prefix[0] != '(' || i < 0 { 130 | // "Invalid prefix format" 131 | return nil, false 132 | } 133 | 134 | // We loop through the string using range so we get bytes, then we throw the 135 | // two results together in the map. 136 | symbols := make([]rune, 0, len(prefix)/2-1) // ~&@%+ 137 | for _, r := range prefix[i+1:] { 138 | symbols = append(symbols, r) 139 | } 140 | 141 | modes := make([]rune, 0, len(symbols)) // qaohv 142 | for _, r := range prefix[1:i] { 143 | modes = append(modes, r) 144 | } 145 | 146 | if len(modes) != len(symbols) { 147 | // "Mismatched modes and symbols" 148 | return nil, false 149 | } 150 | 151 | prefixes := make(map[rune]rune) 152 | for k := range symbols { 153 | prefixes[symbols[k]] = modes[k] 154 | } 155 | 156 | return prefixes, true 157 | } 158 | -------------------------------------------------------------------------------- /conn_test.go: -------------------------------------------------------------------------------- 1 | package irc_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | 12 | "gopkg.in/irc.v4" 13 | ) 14 | 15 | var errorWriterErr = errors.New("errorWriter: error") 16 | 17 | type errorWriter struct{} 18 | 19 | func (ew *errorWriter) Write([]byte) (int, error) { 20 | return 0, errorWriterErr 21 | } 22 | 23 | type nopCloser struct { 24 | io.Reader 25 | io.Writer 26 | } 27 | 28 | func newNopCloser(inner io.ReadWriter) *nopCloser { 29 | return &nopCloser{ 30 | Reader: inner, 31 | Writer: inner, 32 | } 33 | } 34 | 35 | func (nc *nopCloser) Close() error { 36 | return nil 37 | } 38 | 39 | var _ io.ReadWriteCloser = (*nopCloser)(nil) 40 | 41 | type readWriteCloser struct { 42 | io.Reader 43 | io.Writer 44 | io.Closer 45 | } 46 | 47 | type testReadWriteCloser struct { 48 | client *bytes.Buffer 49 | server *bytes.Buffer 50 | } 51 | 52 | func newTestReadWriteCloser() *testReadWriteCloser { 53 | return &testReadWriteCloser{ 54 | client: &bytes.Buffer{}, 55 | server: &bytes.Buffer{}, 56 | } 57 | } 58 | 59 | func (t *testReadWriteCloser) Read(p []byte) (int, error) { 60 | return t.server.Read(p) 61 | } 62 | 63 | func (t *testReadWriteCloser) Write(p []byte) (int, error) { 64 | return t.client.Write(p) 65 | } 66 | 67 | func testReadMessage(t *testing.T, c *irc.Conn) *irc.Message { 68 | t.Helper() 69 | 70 | m, err := c.ReadMessage() 71 | assert.NoError(t, err) 72 | return m 73 | } 74 | 75 | func testLines(t *testing.T, rwc *testReadWriteCloser, expected []string) { 76 | t.Helper() 77 | 78 | lines := strings.Split(rwc.client.String(), "\r\n") 79 | var line, clientLine string 80 | for len(expected) > 0 { 81 | line, expected = expected[0], expected[1:] 82 | clientLine, lines = lines[0], lines[1:] 83 | 84 | assert.Equal(t, line, clientLine) 85 | } 86 | 87 | for _, line := range lines { 88 | assert.Equal(t, "", strings.TrimSpace(line), "Extra non-empty lines") 89 | } 90 | 91 | // Reset the contents 92 | rwc.client.Reset() 93 | rwc.server.Reset() 94 | } 95 | 96 | func TestWriteMessageError(t *testing.T) { 97 | t.Parallel() 98 | 99 | rw := &readWriteCloser{ 100 | &bytes.Buffer{}, 101 | &errorWriter{}, 102 | nil, 103 | } 104 | 105 | c := irc.NewConn(rw) 106 | 107 | err := c.WriteMessage(irc.MustParseMessage("PING :hello world")) 108 | assert.Error(t, err) 109 | 110 | err = c.Writef("PING :hello world") 111 | assert.Error(t, err) 112 | 113 | err = c.Write("PING :hello world") 114 | assert.Error(t, err) 115 | } 116 | 117 | func TestConn(t *testing.T) { 118 | t.Parallel() 119 | 120 | rwc := newTestReadWriteCloser() 121 | c := irc.NewConn(rwc) 122 | 123 | // Test writing a message 124 | m := &irc.Message{Prefix: &irc.Prefix{}, Command: "PING", Params: []string{"Hello World"}} 125 | err := c.WriteMessage(m) 126 | assert.NoError(t, err) 127 | testLines(t, rwc, []string{ 128 | "PING :Hello World", 129 | }) 130 | 131 | // Test with Writef 132 | err = c.Writef("PING :%s", "Hello World") 133 | assert.NoError(t, err) 134 | testLines(t, rwc, []string{ 135 | "PING :Hello World", 136 | }) 137 | 138 | m = irc.MustParseMessage("PONG :Hello World") 139 | rwc.server.WriteString(m.String() + "\r\n") 140 | m2 := testReadMessage(t, c) 141 | 142 | assert.EqualValues(t, m, m2, "Message returned by client did not match input") 143 | 144 | // Test welcome message 145 | m = irc.MustParseMessage("001 test_nick") 146 | rwc.server.WriteString(m.String() + "\r\n") 147 | m2 = testReadMessage(t, c) 148 | assert.EqualValues(t, m, m2, "Message returned by client did not match input") 149 | 150 | rwc.server.WriteString(":invalid_message\r\n") 151 | _, err = c.ReadMessage() 152 | assert.Equal(t, irc.ErrMissingDataAfterPrefix, err) 153 | 154 | // Ensure empty messages are ignored 155 | m = irc.MustParseMessage("001 test_nick") 156 | rwc.server.WriteString("\r\n" + m.String() + "\r\n") 157 | m2 = testReadMessage(t, c) 158 | assert.EqualValues(t, m, m2, "Message returned by client did not match input") 159 | 160 | // This is an odd one... if there wasn't any output, it'll hit 161 | // EOF, so we expect an error here so we can test an error 162 | // condition. 163 | _, err = c.ReadMessage() 164 | assert.Equal(t, io.EOF, err, "Didn't get expected EOF") 165 | } 166 | 167 | func TestDebugCallback(t *testing.T) { 168 | t.Parallel() 169 | 170 | var readerHit, writerHit bool 171 | rwc := newTestReadWriteCloser() 172 | c := irc.NewConn(rwc) 173 | c.Writer.DebugCallback = func(string) { 174 | writerHit = true 175 | } 176 | c.Reader.DebugCallback = func(string) { 177 | readerHit = true 178 | } 179 | 180 | m := &irc.Message{Prefix: &irc.Prefix{}, Command: "PING", Params: []string{"Hello World"}} 181 | err := c.WriteMessage(m) 182 | assert.NoError(t, err) 183 | testLines(t, rwc, []string{ 184 | "PING :Hello World", 185 | }) 186 | m = irc.MustParseMessage("PONG :Hello World") 187 | rwc.server.WriteString(m.String() + "\r\n") 188 | testReadMessage(t, c) 189 | 190 | assert.True(t, readerHit) 191 | assert.True(t, writerHit) 192 | } 193 | -------------------------------------------------------------------------------- /stream_test.go: -------------------------------------------------------------------------------- 1 | package irc_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | 12 | "gopkg.in/irc.v4" 13 | ) 14 | 15 | // TestAction is used to execute an action during a stream test. If a 16 | // non-nil error is returned the test will be failed. 17 | type TestAction func(t *testing.T, rw *testReadWriter) 18 | 19 | func SendLine(output string) TestAction { 20 | return SendLineWithTimeout(output, 1*time.Second) 21 | } 22 | 23 | func AssertClosed() TestAction { 24 | return func(t *testing.T, rw *testReadWriter) { 25 | t.Helper() 26 | 27 | if !rw.closed { 28 | assert.Fail(t, "Expected conn to be closed") 29 | } 30 | } 31 | } 32 | 33 | func SendLineWithTimeout(output string, timeout time.Duration) TestAction { 34 | return func(t *testing.T, rw *testReadWriter) { 35 | t.Helper() 36 | 37 | waitChan := time.After(timeout) 38 | 39 | // First we send the message 40 | select { 41 | case rw.readChan <- output: 42 | case <-waitChan: 43 | assert.Fail(t, "SendLine send timeout on %s", output) 44 | return 45 | case <-rw.exiting: 46 | assert.Fail(t, "Failed to send") 47 | return 48 | } 49 | 50 | // Now we wait for the buffer to be emptied 51 | select { 52 | case <-rw.readEmptyChan: 53 | case <-waitChan: 54 | assert.Fail(t, "SendLine timeout on %s", output) 55 | case <-rw.exiting: 56 | assert.Fail(t, "Failed to send whole message") 57 | } 58 | } 59 | } 60 | 61 | func SendFunc(cb func() string) TestAction { 62 | return func(t *testing.T, rw *testReadWriter) { 63 | t.Helper() 64 | 65 | SendLine(cb())(t, rw) 66 | } 67 | } 68 | 69 | func LineFunc(cb func(m *irc.Message)) TestAction { 70 | return func(t *testing.T, rw *testReadWriter) { 71 | t.Helper() 72 | 73 | select { 74 | case line := <-rw.writeChan: 75 | cb(irc.MustParseMessage(line)) 76 | case <-time.After(1 * time.Second): 77 | assert.Fail(t, "LineFunc timeout") 78 | case <-rw.exiting: 79 | } 80 | } 81 | } 82 | 83 | func ExpectLine(input string) TestAction { 84 | return ExpectLineWithTimeout(input, 1*time.Second) 85 | } 86 | 87 | func ExpectLineWithTimeout(input string, timeout time.Duration) TestAction { 88 | return func(t *testing.T, rw *testReadWriter) { 89 | t.Helper() 90 | 91 | select { 92 | case line := <-rw.writeChan: 93 | assert.Equal(t, input, line) 94 | case <-time.After(timeout): 95 | assert.Fail(t, "ExpectLine timeout on %s", input) 96 | case <-rw.exiting: 97 | } 98 | } 99 | } 100 | 101 | func Delay(delay time.Duration) TestAction { 102 | return func(t *testing.T, rw *testReadWriter) { 103 | t.Helper() 104 | 105 | select { 106 | case <-time.After(delay): 107 | case <-rw.exiting: 108 | } 109 | } 110 | } 111 | 112 | /* 113 | func QueueReadError(err error) TestAction { 114 | return func(t *testing.T, rw *testReadWriter) { 115 | select { 116 | case rw.readErrorChan <- err: 117 | default: 118 | assert.Fail(t, "Tried to queue a second read error") 119 | } 120 | } 121 | } 122 | */ 123 | 124 | func QueueWriteError(err error) TestAction { 125 | return func(t *testing.T, rw *testReadWriter) { 126 | t.Helper() 127 | 128 | select { 129 | case rw.writeErrorChan <- err: 130 | default: 131 | assert.Fail(t, "Tried to queue a second write error") 132 | } 133 | } 134 | } 135 | 136 | type testReadWriter struct { 137 | writeErrorChan chan error 138 | writeChan chan string 139 | readErrorChan chan error 140 | readChan chan string 141 | readEmptyChan chan struct{} 142 | exiting chan struct{} 143 | clientDone chan struct{} 144 | closed bool 145 | serverBuffer bytes.Buffer 146 | } 147 | 148 | func (rw *testReadWriter) maybeBroadcastEmpty() { 149 | if rw.serverBuffer.Len() == 0 { 150 | select { 151 | case rw.readEmptyChan <- struct{}{}: 152 | default: 153 | } 154 | } 155 | } 156 | 157 | func (rw *testReadWriter) Read(buf []byte) (int, error) { 158 | // Check for a read error first 159 | select { 160 | case err := <-rw.readErrorChan: 161 | return 0, err 162 | default: 163 | } 164 | 165 | // If there's data left in the buffer, we want to use that first. 166 | if rw.serverBuffer.Len() > 0 { 167 | s, err := rw.serverBuffer.Read(buf) 168 | if errors.Is(err, io.EOF) { 169 | err = nil 170 | } 171 | rw.maybeBroadcastEmpty() 172 | return s, err 173 | } 174 | 175 | // Read from server. We're waiting for this whole test to finish, data to 176 | // come in from the server buffer, or for an error. We expect only one read 177 | // to be happening at once. 178 | select { 179 | case err := <-rw.readErrorChan: 180 | return 0, err 181 | case data := <-rw.readChan: 182 | rw.serverBuffer.WriteString(data) 183 | s, err := rw.serverBuffer.Read(buf) 184 | if errors.Is(err, io.EOF) { 185 | err = nil 186 | } 187 | rw.maybeBroadcastEmpty() 188 | return s, err 189 | case <-rw.exiting: 190 | return 0, io.EOF 191 | } 192 | } 193 | 194 | func (rw *testReadWriter) Write(buf []byte) (int, error) { 195 | select { 196 | case err := <-rw.writeErrorChan: 197 | return 0, err 198 | default: 199 | } 200 | 201 | // Write to server. We can cheat with this because we know things 202 | // will be written a line at a time. 203 | select { 204 | case rw.writeChan <- string(buf): 205 | return len(buf), nil 206 | case <-rw.exiting: 207 | return 0, errors.New("Connection closed") 208 | } 209 | } 210 | 211 | func (rw *testReadWriter) Close() error { 212 | select { 213 | case <-rw.exiting: 214 | return errors.New("Connection closed") 215 | default: 216 | // Ensure no double close 217 | if !rw.closed { 218 | rw.closed = true 219 | close(rw.exiting) 220 | } 221 | return nil 222 | } 223 | } 224 | 225 | func newTestReadWriter() *testReadWriter { 226 | return &testReadWriter{ 227 | writeErrorChan: make(chan error, 1), 228 | writeChan: make(chan string), 229 | readErrorChan: make(chan error, 1), 230 | readChan: make(chan string), 231 | readEmptyChan: make(chan struct{}, 1), 232 | exiting: make(chan struct{}), 233 | clientDone: make(chan struct{}), 234 | } 235 | } 236 | 237 | func runClientTest( 238 | t *testing.T, 239 | cc irc.ClientConfig, 240 | expectedErr error, 241 | setup func(c *irc.Client), 242 | actions []TestAction, 243 | ) *irc.Client { 244 | t.Helper() 245 | 246 | rw := newTestReadWriter() 247 | c := irc.NewClient(rw, cc) 248 | 249 | if setup != nil { 250 | setup(c) 251 | } 252 | 253 | go func() { 254 | err := c.Run() 255 | assert.Equal(t, expectedErr, err) 256 | close(rw.clientDone) 257 | }() 258 | 259 | runTest(t, rw, actions) 260 | 261 | return c 262 | } 263 | 264 | func runTest(t *testing.T, rw *testReadWriter, actions []TestAction) { 265 | t.Helper() 266 | 267 | // Perform each of the actions 268 | for _, action := range actions { 269 | action(t, rw) 270 | } 271 | 272 | // TODO: Make sure there are no more incoming messages 273 | 274 | // Ask everything to shut down 275 | rw.Close() 276 | 277 | // Wait for the client to stop 278 | select { 279 | case <-rw.clientDone: 280 | case <-time.After(1 * time.Second): 281 | assert.Fail(t, "Timeout in client shutdown") 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package irc_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | yaml "gopkg.in/yaml.v2" 11 | 12 | "gopkg.in/irc.v4" 13 | ) 14 | 15 | func BenchmarkParseMessage(b *testing.B) { 16 | for i := 0; i < b.N; i++ { 17 | irc.MustParseMessage("@tag1=something :nick!user@host PRIVMSG #channel :some message") 18 | } 19 | } 20 | 21 | func TestParseMessage(t *testing.T) { 22 | t.Parallel() 23 | 24 | var messageTests = []struct { //nolint:gofumpt 25 | Input string 26 | Err error 27 | }{ 28 | { 29 | Input: "", 30 | Err: irc.ErrZeroLengthMessage, 31 | }, 32 | { 33 | Input: "@asdf", 34 | Err: irc.ErrMissingDataAfterTags, 35 | }, 36 | { 37 | Input: ":asdf", 38 | Err: irc.ErrMissingDataAfterPrefix, 39 | }, 40 | { 41 | Input: " :", 42 | Err: irc.ErrMissingCommand, 43 | }, 44 | { 45 | Input: "PING :asdf", 46 | }, 47 | } 48 | 49 | for i, test := range messageTests { 50 | m, err := irc.ParseMessage(test.Input) 51 | assert.Equal(t, test.Err, err, "%d. Error didn't match expected", i) 52 | 53 | if test.Err != nil { 54 | assert.Nil(t, m, "%d. Didn't get nil message", i) 55 | } else { 56 | assert.NotNil(t, m, "%d. Got nil message", i) 57 | } 58 | } 59 | } 60 | 61 | func TestMustParseMessage(t *testing.T) { 62 | t.Parallel() 63 | 64 | assert.Panics(t, func() { 65 | irc.MustParseMessage("") 66 | }, "Didn't get expected panic") 67 | 68 | assert.NotPanics(t, func() { 69 | irc.MustParseMessage("PING :asdf") 70 | }, "Got unexpected panic") 71 | } 72 | 73 | func TestMessageParam(t *testing.T) { 74 | t.Parallel() 75 | 76 | m := irc.MustParseMessage("PING :test") 77 | assert.Equal(t, m.Param(0), "test") 78 | assert.Equal(t, m.Param(-1), "") 79 | assert.Equal(t, m.Param(2), "") 80 | } 81 | 82 | func TestMessageTrailing(t *testing.T) { 83 | t.Parallel() 84 | 85 | m := irc.MustParseMessage("PING :helloworld") 86 | assert.Equal(t, "helloworld", m.Trailing()) 87 | 88 | m = irc.MustParseMessage("PING") 89 | assert.Equal(t, "", m.Trailing()) 90 | } 91 | 92 | func TestMessageCopy(t *testing.T) { 93 | t.Parallel() 94 | 95 | m := irc.MustParseMessage("@tag=val :user@host PING :helloworld") 96 | 97 | // Ensure copied messages are equal 98 | c := m.Copy() 99 | assert.EqualValues(t, m, c, "Copied values are not equal") 100 | 101 | // Ensure messages with modified tags don't match 102 | c = m.Copy() 103 | for k := range c.Tags { 104 | c.Tags[k] += "junk" 105 | } 106 | assert.False(t, assert.ObjectsAreEqualValues(m, c), "Copied with modified tags should not match") 107 | 108 | // Ensure messages with modified prefix don't match 109 | c = m.Copy() 110 | c.Prefix.Name += "junk" 111 | assert.False(t, assert.ObjectsAreEqualValues(m, c), "Copied with modified identity should not match") 112 | 113 | // Ensure messages with modified params don't match 114 | c = m.Copy() 115 | c.Params = append(c.Params, "junk") 116 | assert.False(t, assert.ObjectsAreEqualValues(m, c), "Copied with additional params should not match") 117 | 118 | // The message itself doesn't matter, we just need to make sure we 119 | // don't error if the user does something crazy and makes Params 120 | // nil. 121 | m = irc.MustParseMessage("PING :hello world") 122 | m.Prefix = nil 123 | c = m.Copy() 124 | assert.EqualValues(t, m, c, "nil prefix copy failed") 125 | 126 | // Ensure an empty Params is copied as nil 127 | m = irc.MustParseMessage("PING") 128 | m.Params = []string{} 129 | c = m.Copy() 130 | assert.Nil(t, c.Params, "Expected nil for empty params") 131 | } 132 | 133 | // Everything beyond here comes from the testcases repo 134 | 135 | type MsgSplitTests struct { 136 | Tests []struct { 137 | Desc string 138 | Input string 139 | Atoms struct { 140 | Source *string 141 | Verb string 142 | Params []string 143 | Tags map[string]interface{} 144 | } 145 | } 146 | } 147 | 148 | func TestMsgSplit(t *testing.T) { 149 | t.Parallel() 150 | 151 | data, err := ioutil.ReadFile("./_testcases/tests/msg-split.yaml") 152 | require.NoError(t, err) 153 | 154 | var splitTests MsgSplitTests 155 | err = yaml.Unmarshal(data, &splitTests) 156 | require.NoError(t, err) 157 | 158 | for _, test := range splitTests.Tests { 159 | msg, err := irc.ParseMessage(test.Input) 160 | assert.NoError(t, err, "%s: Failed to parse: %s (%s)", test.Desc, test.Input, err) 161 | 162 | assert.Equal(t, 163 | strings.ToUpper(test.Atoms.Verb), msg.Command, 164 | "%s: Wrong command for input: %s", test.Desc, test.Input, 165 | ) 166 | assert.Equal(t, 167 | test.Atoms.Params, msg.Params, 168 | "%s: Wrong params for input: %s", test.Desc, test.Input, 169 | ) 170 | 171 | if test.Atoms.Source != nil { 172 | assert.Equal(t, *test.Atoms.Source, msg.Prefix.String()) 173 | } 174 | 175 | assert.Equal(t, 176 | len(test.Atoms.Tags), len(msg.Tags), 177 | "%s: Wrong number of tags", 178 | test.Desc, 179 | ) 180 | 181 | for k, v := range test.Atoms.Tags { 182 | tag, ok := msg.Tags[k] 183 | assert.True(t, ok, "Missing tag") 184 | if v == nil { 185 | assert.EqualValues(t, "", tag, "%s: Tag %q differs: %s != \"\"", test.Desc, k, tag) 186 | } else { 187 | assert.EqualValues(t, v, tag, "%s: Tag %q differs: %s != %s", test.Desc, k, v, tag) 188 | } 189 | } 190 | } 191 | } 192 | 193 | type MsgJoinTests struct { 194 | Tests []struct { 195 | Desc string 196 | Atoms struct { 197 | Source string 198 | Verb string 199 | Params []string 200 | Tags map[string]interface{} 201 | } 202 | Matches []string 203 | } 204 | } 205 | 206 | func TestMsgJoin(t *testing.T) { 207 | var ok bool 208 | 209 | t.Parallel() 210 | 211 | data, err := ioutil.ReadFile("./_testcases/tests/msg-join.yaml") 212 | require.NoError(t, err) 213 | 214 | var splitTests MsgJoinTests 215 | err = yaml.Unmarshal(data, &splitTests) 216 | require.NoError(t, err) 217 | 218 | for _, test := range splitTests.Tests { 219 | msg := &irc.Message{ 220 | Prefix: irc.ParsePrefix(test.Atoms.Source), 221 | Command: test.Atoms.Verb, 222 | Params: test.Atoms.Params, 223 | Tags: make(map[string]string), 224 | } 225 | 226 | for k, v := range test.Atoms.Tags { 227 | if v == nil { 228 | msg.Tags[k] = "" 229 | } else { 230 | msg.Tags[k], ok = v.(string) 231 | assert.True(t, ok) 232 | } 233 | } 234 | 235 | assert.Contains(t, test.Matches, msg.String()) 236 | } 237 | } 238 | 239 | type UserhostSplitTests struct { 240 | Tests []struct { 241 | Desc string 242 | Source string 243 | Atoms struct { 244 | Nick string 245 | User string 246 | Host string 247 | } 248 | } 249 | } 250 | 251 | func TestUserhostSplit(t *testing.T) { 252 | t.Parallel() 253 | 254 | data, err := ioutil.ReadFile("./_testcases/tests/userhost-split.yaml") 255 | require.NoError(t, err) 256 | 257 | var userhostTests UserhostSplitTests 258 | err = yaml.Unmarshal(data, &userhostTests) 259 | require.NoError(t, err) 260 | 261 | for _, test := range userhostTests.Tests { 262 | prefix := irc.ParsePrefix(test.Source) 263 | 264 | assert.Equal(t, 265 | test.Atoms.Nick, prefix.Name, 266 | "%s: Name did not match for input: %q", test.Desc, test.Source, 267 | ) 268 | assert.Equal(t, 269 | test.Atoms.User, prefix.User, 270 | "%s: User did not match for input: %q", test.Desc, test.Source, 271 | ) 272 | assert.Equal(t, 273 | test.Atoms.Host, prefix.Host, 274 | "%s: Host did not match for input: %q", test.Desc, test.Source, 275 | ) 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /tracker.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | // TODO: store all nicks by uuid and map them in outgoing seabird events rather 4 | // than passing the nicks around directly 5 | 6 | // TODO: properly handle figuring out the mode when it changes for a user. 7 | 8 | import ( 9 | "errors" 10 | "strings" 11 | "sync" 12 | ) 13 | 14 | // Tracker provides a convenient interface to track users, the channels they are 15 | // in, and what modes they have in those channels. 16 | type Tracker struct { 17 | sync.RWMutex 18 | 19 | channels map[string]*ChannelState 20 | isupport *ISupportTracker 21 | currentNick string 22 | } 23 | 24 | // NewTracker creates a new tracker instance. 25 | func NewTracker(isupport *ISupportTracker) *Tracker { 26 | return &Tracker{ 27 | channels: make(map[string]*ChannelState), 28 | isupport: isupport, 29 | } 30 | } 31 | 32 | // ChannelState represents the current state of a channel, including the name, 33 | // topic, and all users in it. 34 | type ChannelState struct { 35 | Name string 36 | Topic string 37 | Users map[string]struct{} 38 | } 39 | 40 | // ListChannels will list the names of all known channels. 41 | func (t *Tracker) ListChannels() []string { 42 | t.RLock() 43 | defer t.RUnlock() 44 | 45 | ret := make([]string, 0, len(t.channels)) 46 | for channel := range t.channels { 47 | ret = append(ret, channel) 48 | } 49 | 50 | return ret 51 | } 52 | 53 | // GetChannel will look up the ChannelState for a given channel name. It will 54 | // return nil if the channel is unknown. 55 | func (t *Tracker) GetChannel(name string) *ChannelState { 56 | t.RLock() 57 | defer t.RUnlock() 58 | 59 | return t.channels[name] 60 | } 61 | 62 | // Handle needs to be called for all 001, 332, 353, JOIN, TOPIC, PART, KICK, 63 | // QUIT, and NICK messages. All other messages will be ignored. Note that this 64 | // will not handle calling the underlying ISupportTracker's Handle method. 65 | func (t *Tracker) Handle(msg *Message) error { 66 | switch msg.Command { 67 | case "001": 68 | return t.handle001(msg) 69 | case "332": 70 | return t.handleRplTopic(msg) 71 | case "353": 72 | return t.handleRplNamReply(msg) 73 | case "JOIN": 74 | return t.handleJoin(msg) 75 | case "TOPIC": 76 | return t.handleTopic(msg) 77 | case "PART": 78 | return t.handlePart(msg) 79 | case "KICK": 80 | return t.handleKick(msg) 81 | case "QUIT": 82 | return t.handleQuit(msg) 83 | case "NICK": 84 | return t.handleNick(msg) 85 | } 86 | 87 | return nil 88 | } 89 | 90 | func (t *Tracker) handle001(msg *Message) error { 91 | if len(msg.Params) != 2 { 92 | return errors.New("malformed RPL_WELCOME message") 93 | } 94 | 95 | t.Lock() 96 | defer t.Unlock() 97 | 98 | t.currentNick = msg.Params[0] 99 | 100 | return nil 101 | } 102 | 103 | func (t *Tracker) handleTopic(msg *Message) error { 104 | if len(msg.Params) != 2 { 105 | return errors.New("malformed TOPIC message") 106 | } 107 | 108 | channel := msg.Params[0] 109 | topic := msg.Trailing() 110 | 111 | t.Lock() 112 | defer t.Unlock() 113 | 114 | if _, ok := t.channels[channel]; !ok { 115 | return errors.New("received TOPIC message for unknown channel") 116 | } 117 | 118 | t.channels[channel].Topic = topic 119 | 120 | return nil 121 | } 122 | 123 | func (t *Tracker) handleRplTopic(msg *Message) error { 124 | if len(msg.Params) != 3 { 125 | return errors.New("malformed RPL_TOPIC message") 126 | } 127 | 128 | // client set channel topic to topic 129 | 130 | // client := msg.Params[0] 131 | channel := msg.Params[1] 132 | topic := msg.Trailing() 133 | 134 | t.Lock() 135 | defer t.Unlock() 136 | 137 | if _, ok := t.channels[channel]; !ok { 138 | return errors.New("received RPL_TOPIC for unknown channel") 139 | } 140 | 141 | t.channels[channel].Topic = topic 142 | 143 | return nil 144 | } 145 | 146 | func (t *Tracker) handleJoin(msg *Message) error { 147 | if len(msg.Params) != 1 { 148 | return errors.New("malformed JOIN message") 149 | } 150 | 151 | // user joined channel 152 | user := msg.Prefix.Name 153 | channel := msg.Trailing() 154 | 155 | t.Lock() 156 | defer t.Unlock() 157 | 158 | _, ok := t.channels[channel] 159 | 160 | if !ok { 161 | if user != t.currentNick { 162 | return errors.New("received JOIN message for unknown channel") 163 | } 164 | 165 | t.channels[channel] = &ChannelState{Name: channel, Users: make(map[string]struct{})} 166 | } 167 | 168 | state := t.channels[channel] 169 | state.Users[user] = struct{}{} 170 | 171 | return nil 172 | } 173 | 174 | func (t *Tracker) handlePart(msg *Message) error { 175 | if len(msg.Params) < 1 { 176 | return errors.New("malformed PART message") 177 | } 178 | 179 | // user joined channel 180 | 181 | user := msg.Prefix.Name 182 | channel := msg.Params[0] 183 | 184 | t.Lock() 185 | defer t.Unlock() 186 | 187 | if _, ok := t.channels[channel]; !ok { 188 | return errors.New("received PART message for unknown channel") 189 | } 190 | 191 | // If we left the channel, we can drop the whole thing, otherwise just drop 192 | // this user from the channel. 193 | if user == t.currentNick { 194 | delete(t.channels, channel) 195 | } else { 196 | state := t.channels[channel] 197 | delete(state.Users, user) 198 | } 199 | 200 | return nil 201 | } 202 | 203 | func (t *Tracker) handleKick(msg *Message) error { 204 | if len(msg.Params) != 3 { 205 | return errors.New("malformed KICK message") 206 | } 207 | 208 | // user was kicked from channel by actor 209 | 210 | // actor := msg.Prefix.Name 211 | user := msg.Params[1] 212 | channel := msg.Params[0] 213 | 214 | t.Lock() 215 | defer t.Unlock() 216 | 217 | if _, ok := t.channels[channel]; !ok { 218 | return errors.New("received KICK message for unknown channel") 219 | } 220 | 221 | // If we left the channel, we can drop the whole thing, otherwise just drop 222 | // this user from the channel. 223 | if user == t.currentNick { 224 | delete(t.channels, channel) 225 | } else { 226 | state := t.channels[channel] 227 | delete(state.Users, user) 228 | } 229 | 230 | return nil 231 | } 232 | 233 | func (t *Tracker) handleQuit(msg *Message) error { 234 | if len(msg.Params) != 1 { 235 | return errors.New("malformed QUIT message") 236 | } 237 | 238 | // user quit 239 | 240 | user := msg.Prefix.Name 241 | 242 | t.Lock() 243 | defer t.Unlock() 244 | 245 | for _, state := range t.channels { 246 | delete(state.Users, user) 247 | } 248 | 249 | return nil 250 | } 251 | 252 | func (t *Tracker) handleNick(msg *Message) error { 253 | if len(msg.Params) != 1 { 254 | return errors.New("malformed NICK message") 255 | } 256 | 257 | // oldUser renamed to newUser 258 | 259 | oldUser := msg.Prefix.Name 260 | newUser := msg.Params[0] 261 | 262 | t.Lock() 263 | defer t.Unlock() 264 | 265 | if t.currentNick == oldUser { 266 | t.currentNick = newUser 267 | } 268 | 269 | for _, state := range t.channels { 270 | if _, ok := state.Users[oldUser]; ok { 271 | delete(state.Users, oldUser) 272 | state.Users[newUser] = struct{}{} 273 | } 274 | } 275 | 276 | return nil 277 | } 278 | 279 | func (t *Tracker) handleRplNamReply(msg *Message) error { 280 | if len(msg.Params) != 4 { 281 | return errors.New("malformed RPL_NAMREPLY message") 282 | } 283 | 284 | channel := msg.Params[2] 285 | users := strings.Split(strings.TrimSpace(msg.Trailing()), " ") 286 | 287 | prefixes, ok := t.isupport.GetPrefixMap() 288 | if !ok { 289 | return errors.New("ISupport missing prefix map") 290 | } 291 | 292 | t.Lock() 293 | defer t.Unlock() 294 | 295 | if _, ok := t.channels[channel]; !ok { 296 | return errors.New("received RPL_NAMREPLY message for untracked channel") 297 | } 298 | 299 | for _, user := range users { 300 | i := strings.IndexFunc(user, func(r rune) bool { 301 | _, ok := prefixes[r] 302 | return !ok 303 | }) 304 | 305 | if i != -1 { 306 | user = user[i:] 307 | } 308 | 309 | // The bot user should be added via JOIN 310 | if user == t.currentNick { 311 | continue 312 | } 313 | 314 | state := t.channels[channel] 315 | state.Users[user] = struct{}{} 316 | } 317 | 318 | return nil 319 | } 320 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "strings" 7 | ) 8 | 9 | var tagDecodeSlashMap = map[rune]rune{ 10 | ':': ';', 11 | 's': ' ', 12 | '\\': '\\', 13 | 'r': '\r', 14 | 'n': '\n', 15 | } 16 | 17 | var tagEncodeMap = map[rune]string{ 18 | ';': "\\:", 19 | ' ': "\\s", 20 | '\\': "\\\\", 21 | '\r': "\\r", 22 | '\n': "\\n", 23 | } 24 | 25 | var ( 26 | // ErrZeroLengthMessage is returned when parsing if the input is 27 | // zero-length. 28 | ErrZeroLengthMessage = errors.New("irc: cannot parse zero-length message") 29 | 30 | // ErrMissingDataAfterPrefix is returned when parsing if there is 31 | // no message data after the prefix. 32 | ErrMissingDataAfterPrefix = errors.New("irc: no message data after prefix") 33 | 34 | // ErrMissingDataAfterTags is returned when parsing if there is no 35 | // message data after the tags. 36 | ErrMissingDataAfterTags = errors.New("irc: no message data after tags") 37 | 38 | // ErrMissingCommand is returned when parsing if there is no 39 | // command in the parsed message. 40 | ErrMissingCommand = errors.New("irc: missing message command") 41 | ) 42 | 43 | // ParseTagValue parses an encoded tag value as a string. If you need to set a 44 | // tag, you probably want to just set the string itself, so it will be encoded 45 | // properly. 46 | func ParseTagValue(v string) string { 47 | ret := &bytes.Buffer{} 48 | 49 | input := bytes.NewBufferString(v) 50 | 51 | for { 52 | c, _, err := input.ReadRune() 53 | if err != nil { 54 | break 55 | } 56 | 57 | if c == '\\' { 58 | // If we got a backslash followed by the end of the tag value, we 59 | // should just ignore the backslash. 60 | c2, _, err := input.ReadRune() 61 | if err != nil { 62 | break 63 | } 64 | 65 | if replacement, ok := tagDecodeSlashMap[c2]; ok { 66 | ret.WriteRune(replacement) 67 | } else { 68 | ret.WriteRune(c2) 69 | } 70 | } else { 71 | ret.WriteRune(c) 72 | } 73 | } 74 | 75 | return ret.String() 76 | } 77 | 78 | // EncodeTagValue converts a raw string to the format in the connection. 79 | func EncodeTagValue(v string) string { 80 | ret := &bytes.Buffer{} 81 | 82 | for _, c := range v { 83 | if replacement, ok := tagEncodeMap[c]; ok { 84 | ret.WriteString(replacement) 85 | } else { 86 | ret.WriteRune(c) 87 | } 88 | } 89 | 90 | return ret.String() 91 | } 92 | 93 | // Tags represents the IRCv3 message tags. 94 | type Tags map[string]string 95 | 96 | // ParseTags takes a tag string and parses it into a tag map. It will 97 | // always return a tag map, even if there are no valid tags. 98 | func ParseTags(line string) Tags { 99 | ret := Tags{} 100 | 101 | tags := strings.Split(line, ";") 102 | for _, tag := range tags { 103 | parts := strings.SplitN(tag, "=", 2) 104 | if len(parts) < 2 { 105 | ret[parts[0]] = "" 106 | continue 107 | } 108 | 109 | ret[parts[0]] = ParseTagValue(parts[1]) 110 | } 111 | 112 | return ret 113 | } 114 | 115 | // Copy will create a new copy of all IRC tags attached to this 116 | // message. 117 | func (t Tags) Copy() Tags { 118 | ret := Tags{} 119 | 120 | for k, v := range t { 121 | ret[k] = v 122 | } 123 | 124 | return ret 125 | } 126 | 127 | // String ensures this is stringable. 128 | func (t Tags) String() string { 129 | buf := &bytes.Buffer{} 130 | 131 | for k, v := range t { 132 | buf.WriteByte(';') 133 | buf.WriteString(k) 134 | if v != "" { 135 | buf.WriteByte('=') 136 | buf.WriteString(EncodeTagValue(v)) 137 | } 138 | } 139 | 140 | // We don't need the first byte because that's an extra ';' 141 | // character. 142 | _, _ = buf.ReadByte() 143 | 144 | return buf.String() 145 | } 146 | 147 | // Prefix represents the prefix of a message, generally the user who sent it. 148 | type Prefix struct { 149 | // Name will contain the nick of who sent the message, the 150 | // server who sent the message, or a blank string 151 | Name string 152 | 153 | // User will either contain the user who sent the message or a blank string 154 | User string 155 | 156 | // Host will either contain the host of who sent the message or a blank string 157 | Host string 158 | } 159 | 160 | // ParsePrefix takes an identity string and parses it into an 161 | // identity struct. It will always return an Prefix struct and never 162 | // nil. 163 | func ParsePrefix(line string) *Prefix { 164 | // Start by creating an Prefix with nothing but the host 165 | id := &Prefix{ 166 | Name: line, 167 | } 168 | 169 | uh := strings.SplitN(id.Name, "@", 2) 170 | if len(uh) == 2 { 171 | id.Name, id.Host = uh[0], uh[1] 172 | } 173 | 174 | nu := strings.SplitN(id.Name, "!", 2) 175 | if len(nu) == 2 { 176 | id.Name, id.User = nu[0], nu[1] 177 | } 178 | 179 | return id 180 | } 181 | 182 | // Copy will create a new copy of an Prefix. 183 | func (p *Prefix) Copy() *Prefix { 184 | if p == nil { 185 | return nil 186 | } 187 | 188 | newPrefix := &Prefix{} 189 | 190 | *newPrefix = *p 191 | 192 | return newPrefix 193 | } 194 | 195 | // String ensures this is stringable. 196 | func (p *Prefix) String() string { 197 | buf := &bytes.Buffer{} 198 | buf.WriteString(p.Name) 199 | 200 | if p.User != "" { 201 | buf.WriteString("!") 202 | buf.WriteString(p.User) 203 | } 204 | 205 | if p.Host != "" { 206 | buf.WriteString("@") 207 | buf.WriteString(p.Host) 208 | } 209 | 210 | return buf.String() 211 | } 212 | 213 | // Message represents a line parsed from the server. 214 | type Message struct { 215 | // Each message can have IRCv3 tags 216 | Tags 217 | 218 | // Each message can have a Prefix 219 | *Prefix 220 | 221 | // Command is which command is being called. 222 | Command string 223 | 224 | // Params are all the arguments for the command. 225 | Params []string 226 | } 227 | 228 | // MustParseMessage calls ParseMessage and either returns the message 229 | // or panics if an error is returned. 230 | func MustParseMessage(line string) *Message { 231 | m, err := ParseMessage(line) 232 | if err != nil { 233 | panic(err.Error()) 234 | } 235 | return m 236 | } 237 | 238 | // ParseMessage takes a message string (usually a whole line) and 239 | // parses it into a Message struct. This will return nil in the case 240 | // of invalid messages. 241 | func ParseMessage(line string) (*Message, error) { //nolint:funlen 242 | // Trim the line and make sure we have data 243 | line = strings.TrimRight(line, "\r\n") 244 | if len(line) == 0 { 245 | return nil, ErrZeroLengthMessage 246 | } 247 | 248 | c := &Message{ 249 | Tags: Tags{}, 250 | Prefix: &Prefix{}, 251 | } 252 | 253 | if line[0] == '@' { 254 | loc := strings.Index(line, " ") 255 | if loc == -1 { 256 | return nil, ErrMissingDataAfterTags 257 | } 258 | 259 | c.Tags = ParseTags(line[1:loc]) 260 | line = line[loc+1:] 261 | } 262 | 263 | if line[0] == ':' { 264 | loc := strings.Index(line, " ") 265 | if loc == -1 { 266 | return nil, ErrMissingDataAfterPrefix 267 | } 268 | 269 | // Parse the identity, if there was one 270 | c.Prefix = ParsePrefix(line[1:loc]) 271 | line = line[loc+1:] 272 | } 273 | 274 | // Split out the trailing then the rest of the args. Because 275 | // we expect there to be at least one result as an arg (the 276 | // command) we don't need to special case the trailing arg and 277 | // can just attempt a split on " :" 278 | split := strings.SplitN(line, " :", 2) 279 | c.Params = strings.FieldsFunc(split[0], func(r rune) bool { 280 | return r == ' ' 281 | }) 282 | 283 | // If there are no args, we need to bail because we need at 284 | // least the command. 285 | if len(c.Params) == 0 { 286 | return nil, ErrMissingCommand 287 | } 288 | 289 | // If we had a trailing arg, append it to the other args 290 | if len(split) == 2 { 291 | c.Params = append(c.Params, split[1]) 292 | } 293 | 294 | // Because of how it's parsed, the Command will show up as the 295 | // first arg. 296 | c.Command = strings.ToUpper(c.Params[0]) 297 | c.Params = c.Params[1:] 298 | 299 | // If there are no params, set it to nil, to make writing tests and other 300 | // things simpler. 301 | if len(c.Params) == 0 { 302 | c.Params = nil 303 | } 304 | 305 | return c, nil 306 | } 307 | 308 | // Param returns the i'th argument in the Message or an empty string 309 | // if the requested arg does not exist. 310 | func (m *Message) Param(i int) string { 311 | if i < 0 || i >= len(m.Params) { 312 | return "" 313 | } 314 | return m.Params[i] 315 | } 316 | 317 | // Trailing returns the last argument in the Message or an empty string 318 | // if there are no args. 319 | func (m *Message) Trailing() string { 320 | if len(m.Params) < 1 { 321 | return "" 322 | } 323 | 324 | return m.Params[len(m.Params)-1] 325 | } 326 | 327 | // Copy will create a new copy of an message. 328 | func (m *Message) Copy() *Message { 329 | // Create a new message 330 | newMessage := &Message{} 331 | 332 | // Copy stuff from the old message 333 | *newMessage = *m 334 | 335 | // Copy any IRcv3 tags 336 | newMessage.Tags = m.Tags.Copy() 337 | 338 | // Copy the Prefix 339 | newMessage.Prefix = m.Prefix.Copy() 340 | 341 | // Copy the Params slice 342 | newMessage.Params = append(make([]string, 0, len(m.Params)), m.Params...) 343 | 344 | // Similar to parsing, if Params is empty, set it to nil 345 | if len(newMessage.Params) == 0 { 346 | newMessage.Params = nil 347 | } 348 | 349 | return newMessage 350 | } 351 | 352 | // String ensures this is stringable. 353 | func (m *Message) String() string { 354 | buf := &bytes.Buffer{} 355 | 356 | // Write any IRCv3 tags if they exist in the message 357 | if len(m.Tags) > 0 { 358 | buf.WriteByte('@') 359 | buf.WriteString(m.Tags.String()) 360 | buf.WriteByte(' ') 361 | } 362 | 363 | // Add the prefix if we have one 364 | if m.Prefix != nil && m.Prefix.Name != "" { 365 | buf.WriteByte(':') 366 | buf.WriteString(m.Prefix.String()) 367 | buf.WriteByte(' ') 368 | } 369 | 370 | // Add the command since we know we'll always have one 371 | buf.WriteString(m.Command) 372 | 373 | if len(m.Params) > 0 { 374 | args := m.Params[:len(m.Params)-1] 375 | trailing := m.Params[len(m.Params)-1] 376 | 377 | if len(args) > 0 { 378 | buf.WriteByte(' ') 379 | buf.WriteString(strings.Join(args, " ")) 380 | } 381 | 382 | // If trailing is zero-length, contains a space or starts with 383 | // a : we need to actually specify that it's trailing. 384 | if len(trailing) == 0 || strings.ContainsRune(trailing, ' ') || trailing[0] == ':' { 385 | buf.WriteString(" :") 386 | } else { 387 | buf.WriteString(" ") 388 | } 389 | buf.WriteString(trailing) 390 | } 391 | 392 | return buf.String() 393 | } 394 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package irc 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "sync" 9 | "time" 10 | 11 | "golang.org/x/time/rate" 12 | ) 13 | 14 | // ClientConfig is a structure used to configure a Client. 15 | type ClientConfig struct { 16 | // General connection information. 17 | Nick string 18 | Pass string 19 | User string 20 | Name string 21 | 22 | // If this is set to true, the ISupport value on the client struct will be 23 | // non-nil. 24 | EnableISupport bool 25 | 26 | // If this is set to true, the Tracker value on the client struct will be 27 | // non-nil. 28 | EnableTracker bool 29 | 30 | // Connection settings 31 | PingFrequency time.Duration 32 | PingTimeout time.Duration 33 | 34 | // SendLimit is how frequent messages can be sent. If this is zero, 35 | // there will be no limit. 36 | SendLimit time.Duration 37 | 38 | // SendBurst is the number of messages which can be sent in a burst. 39 | SendBurst int 40 | 41 | // Handler is used for message dispatching. 42 | Handler Handler 43 | } 44 | 45 | type capStatus struct { 46 | // Requested means that this cap was requested by the user 47 | Requested bool 48 | 49 | // Required will be true if this cap is non-optional 50 | Required bool 51 | 52 | // Enabled means that this cap was accepted by the server 53 | Enabled bool 54 | 55 | // Available means that the server supports this cap 56 | Available bool 57 | } 58 | 59 | // Client is a wrapper around irc.Conn which is designed to make common 60 | // operations much simpler. It is safe for concurrent use. 61 | type Client struct { 62 | *Conn 63 | closer io.Closer 64 | ISupport *ISupportTracker 65 | Tracker *Tracker 66 | 67 | config ClientConfig 68 | 69 | // Internal state 70 | currentNick string 71 | limiter *rate.Limiter 72 | incomingPongChan chan string 73 | errChan chan error 74 | caps map[string]capStatus 75 | remainingCapResponses int 76 | connected bool 77 | } 78 | 79 | // NewClient creates a client given an io stream and a client config. 80 | func NewClient(rwc io.ReadWriteCloser, config ClientConfig) *Client { 81 | c := &Client{ //nolint:exhaustruct 82 | Conn: NewConn(rwc), 83 | closer: rwc, 84 | config: config, 85 | currentNick: config.Nick, 86 | errChan: make(chan error, 1), 87 | caps: make(map[string]capStatus), 88 | } 89 | 90 | if config.SendLimit != 0 { 91 | if config.SendBurst == 0 { 92 | config.SendBurst = 1 93 | } 94 | 95 | c.limiter = rate.NewLimiter(rate.Every(config.SendLimit), config.SendBurst) 96 | } 97 | 98 | if config.EnableISupport || config.EnableTracker { 99 | c.ISupport = NewISupportTracker() 100 | } 101 | 102 | if config.EnableTracker { 103 | c.Tracker = NewTracker(c.ISupport) 104 | } 105 | 106 | // Replace the writer writeCallback with one of our own 107 | c.Conn.Writer.WriteCallback = c.writeCallback 108 | 109 | return c 110 | } 111 | 112 | func (c *Client) writeCallback(w *Writer, line string) error { 113 | if c.limiter != nil { 114 | // Note that context.Background imitates the previous implementation, 115 | // but it may be worth looking for a way to use this with a passed in 116 | // context in the future. 117 | err := c.limiter.Wait(context.Background()) 118 | if err != nil { 119 | return err 120 | } 121 | } 122 | 123 | _, err := w.RawWrite([]byte(line + "\r\n")) 124 | if err != nil { 125 | c.sendError(err) 126 | } 127 | return err 128 | } 129 | 130 | // maybeStartPingLoop will start a goroutine to send out PING messages at the 131 | // PingFrequency in the config if the frequency is not 0. 132 | func (c *Client) maybeStartPingLoop(wg *sync.WaitGroup, exiting chan struct{}) { 133 | if c.config.PingFrequency <= 0 { 134 | return 135 | } 136 | 137 | wg.Add(1) 138 | 139 | c.incomingPongChan = make(chan string, 5) 140 | 141 | go func() { 142 | defer wg.Done() 143 | 144 | pingHandlers := make(map[string]chan struct{}) 145 | ticker := time.NewTicker(c.config.PingFrequency) 146 | 147 | defer ticker.Stop() 148 | 149 | for { 150 | select { 151 | case <-ticker.C: 152 | // Each time we get a tick, we send off a ping and start a 153 | // goroutine to handle the pong. 154 | timestamp := time.Now().Unix() 155 | pongChan := make(chan struct{}, 1) 156 | pingHandlers[fmt.Sprintf("%d", timestamp)] = pongChan 157 | wg.Add(1) 158 | go c.handlePing(timestamp, pongChan, wg, exiting) 159 | case data := <-c.incomingPongChan: 160 | // Make sure the pong gets routed to the correct 161 | // goroutine. 162 | 163 | c := pingHandlers[data] 164 | delete(pingHandlers, data) 165 | 166 | if c != nil { 167 | c <- struct{}{} 168 | } 169 | case <-exiting: 170 | return 171 | } 172 | } 173 | }() 174 | } 175 | 176 | func (c *Client) handlePing(timestamp int64, pongChan chan struct{}, wg *sync.WaitGroup, exiting chan struct{}) { 177 | defer wg.Done() 178 | 179 | err := c.Writef("PING :%d", timestamp) 180 | if err != nil { 181 | c.sendError(err) 182 | return 183 | } 184 | 185 | timer := time.NewTimer(c.config.PingTimeout) 186 | defer timer.Stop() 187 | 188 | select { 189 | case <-timer.C: 190 | c.sendError(errors.New("ping timeout")) 191 | case <-pongChan: 192 | return 193 | case <-exiting: 194 | return 195 | } 196 | } 197 | 198 | // maybeStartCapHandshake will run a CAP LS and all the relevant CAP REQ 199 | // commands if there are any CAPs requested. 200 | func (c *Client) maybeStartCapHandshake() error { 201 | if len(c.caps) == 0 { 202 | return nil 203 | } 204 | 205 | err := c.Write("CAP LS") 206 | if err != nil { 207 | return err 208 | } 209 | 210 | c.remainingCapResponses = 1 // We count the CAP LS response as a normal response 211 | for key, cap := range c.caps { 212 | if cap.Requested { 213 | err = c.Writef("CAP REQ :%s", key) 214 | if err != nil { 215 | return err 216 | } 217 | c.remainingCapResponses++ 218 | } 219 | } 220 | 221 | return nil 222 | } 223 | 224 | // CapRequest allows you to request IRCv3 capabilities from the server during 225 | // the handshake. The behavior is undefined if this is called before the 226 | // handshake completes so it is recommended that this be called before Run. If 227 | // the CAP is marked as required, the client will exit if that CAP could not be 228 | // negotiated during the handshake. 229 | func (c *Client) CapRequest(capName string, required bool) { 230 | capStatus := c.caps[capName] 231 | capStatus.Requested = true 232 | capStatus.Required = capStatus.Required || required 233 | c.caps[capName] = capStatus 234 | } 235 | 236 | // CapEnabled allows you to check if a CAP is enabled for this connection. Note 237 | // that it will not be populated until after the CAP handshake is done, so it is 238 | // recommended to wait to check this until after a message like 001. 239 | func (c *Client) CapEnabled(capName string) bool { 240 | return c.caps[capName].Enabled 241 | } 242 | 243 | // CapAvailable allows you to check if a CAP is available on this server. Note 244 | // that it will not be populated until after the CAP handshake is done, so it is 245 | // recommended to wait to check this until after a message like 001. 246 | func (c *Client) CapAvailable(capName string) bool { 247 | return c.caps[capName].Available 248 | } 249 | 250 | func (c *Client) sendError(err error) { 251 | select { 252 | case c.errChan <- err: 253 | default: 254 | } 255 | } 256 | 257 | func (c *Client) startReadLoop(wg *sync.WaitGroup, exiting chan struct{}) { 258 | wg.Add(1) 259 | 260 | go func() { 261 | defer wg.Done() 262 | 263 | for { 264 | select { 265 | case <-exiting: 266 | return 267 | default: 268 | m, err := c.ReadMessage() 269 | if err != nil { 270 | c.sendError(err) 271 | break 272 | } 273 | 274 | if f, ok := clientFilters[m.Command]; ok { 275 | f(c, m) 276 | } 277 | 278 | if c.ISupport != nil { 279 | _ = c.ISupport.Handle(m) 280 | } 281 | 282 | if c.Tracker != nil { 283 | _ = c.Tracker.Handle(m) 284 | } 285 | 286 | if c.config.Handler != nil { 287 | c.config.Handler.Handle(c, m) 288 | } 289 | } 290 | } 291 | }() 292 | } 293 | 294 | // Run starts the main loop for this IRC connection. Note that it may break in 295 | // strange and unexpected ways if it is called again before the first connection 296 | // exits. 297 | func (c *Client) Run() error { 298 | return c.RunContext(context.Background()) 299 | } 300 | 301 | // RunContext is the same as Run but a context.Context can be passed in for 302 | // cancelation. 303 | func (c *Client) RunContext(ctx context.Context) error { 304 | // exiting is used by the main goroutine here to ensure any sub-goroutines 305 | // get closed when exiting. 306 | exiting := make(chan struct{}) 307 | var wg sync.WaitGroup 308 | 309 | c.maybeStartPingLoop(&wg, exiting) 310 | 311 | if c.config.Pass != "" { 312 | err := c.Writef("PASS :%s", c.config.Pass) 313 | if err != nil { 314 | return err 315 | } 316 | } 317 | 318 | err := c.maybeStartCapHandshake() 319 | if err != nil { 320 | return err 321 | } 322 | 323 | if c.config.Nick == "" { 324 | return errors.New("ClientConfig.Nick must be specified") 325 | } 326 | 327 | user := c.config.User 328 | if user == "" { 329 | user = c.config.Nick 330 | } 331 | 332 | name := c.config.Name 333 | if name == "" { 334 | name = c.config.Nick 335 | } 336 | 337 | // This feels wrong because it results in CAP LS, CAP REQ, NICK, USER, CAP 338 | // END, but it works and lets us keep the code a bit simpler. 339 | err = c.Writef("NICK :%s", c.config.Nick) 340 | if err != nil { 341 | return err 342 | } 343 | err = c.Writef("USER %s 0 * :%s", user, name) 344 | if err != nil { 345 | return err 346 | } 347 | 348 | // Now that the handshake is pretty much done, we can start listening for 349 | // messages. 350 | c.startReadLoop(&wg, exiting) 351 | 352 | // Wait for an error from any goroutine or for the context to time out, then 353 | // signal we're exiting and wait for the goroutines to exit. 354 | select { 355 | case err = <-c.errChan: 356 | case <-ctx.Done(): 357 | err = ctx.Err() 358 | } 359 | 360 | close(exiting) 361 | c.closer.Close() 362 | wg.Wait() 363 | 364 | return err 365 | } 366 | 367 | // CurrentNick returns what the nick of the client is known to be at this point 368 | // in time. 369 | func (c *Client) CurrentNick() string { 370 | return c.currentNick 371 | } 372 | 373 | // FromChannel takes a Message representing a PRIVMSG and returns if that 374 | // message came from a channel or directly from a user. 375 | func (c *Client) FromChannel(m *Message) bool { 376 | if len(m.Params) < 1 { 377 | return false 378 | } 379 | 380 | // The first param is the target, so if this doesn't match the current nick, 381 | // the message came from a channel. 382 | return m.Params[0] != c.currentNick 383 | } 384 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | //nolint:funlen 2 | package irc_test 3 | 4 | import ( 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/assert" 13 | 14 | "gopkg.in/irc.v4" 15 | ) 16 | 17 | type TestHandler struct { 18 | messages []*irc.Message 19 | delay time.Duration 20 | } 21 | 22 | func (th *TestHandler) Handle(c *irc.Client, m *irc.Message) { 23 | th.messages = append(th.messages, m) 24 | if th.delay > 0 { 25 | time.Sleep(th.delay) 26 | } 27 | } 28 | 29 | func (th *TestHandler) Messages() []*irc.Message { 30 | ret := th.messages 31 | th.messages = nil 32 | return ret 33 | } 34 | 35 | func TestCapReq(t *testing.T) { 36 | t.Parallel() 37 | 38 | config := irc.ClientConfig{ 39 | Nick: "test_nick", 40 | Pass: "test_pass", 41 | User: "test_user", 42 | Name: "test_name", 43 | } 44 | 45 | // Happy path 46 | c := runClientTest(t, config, io.EOF, func(c *irc.Client) { 47 | assert.False(t, c.CapAvailable("random-thing")) 48 | assert.False(t, c.CapAvailable("multi-prefix")) 49 | c.CapRequest("multi-prefix", true) 50 | }, []TestAction{ 51 | ExpectLine("PASS :test_pass\r\n"), 52 | ExpectLine("CAP LS\r\n"), 53 | ExpectLine("CAP REQ :multi-prefix\r\n"), 54 | ExpectLine("NICK :test_nick\r\n"), 55 | ExpectLine("USER test_user 0 * :test_name\r\n"), 56 | SendLine("CAP * LS :multi-prefix\r\n"), 57 | SendLine("CAP * ACK :multi-prefix\r\n"), 58 | ExpectLine("CAP END\r\n"), 59 | }) 60 | assert.False(t, c.CapEnabled("random-thing")) 61 | assert.True(t, c.CapEnabled("multi-prefix")) 62 | assert.False(t, c.CapAvailable("random-thing")) 63 | assert.True(t, c.CapAvailable("multi-prefix")) 64 | 65 | // Malformed CAP responses should be ignored 66 | c = runClientTest(t, config, io.EOF, func(c *irc.Client) { 67 | assert.False(t, c.CapAvailable("random-thing")) 68 | assert.False(t, c.CapAvailable("multi-prefix")) 69 | c.CapRequest("multi-prefix", true) 70 | }, []TestAction{ 71 | ExpectLine("PASS :test_pass\r\n"), 72 | ExpectLine("CAP LS\r\n"), 73 | ExpectLine("CAP REQ :multi-prefix\r\n"), 74 | ExpectLine("NICK :test_nick\r\n"), 75 | ExpectLine("USER test_user 0 * :test_name\r\n"), 76 | SendLine("CAP * LS :multi-prefix\r\n"), 77 | 78 | // TODO: There's currently a bug somewhere preventing this from working 79 | // as expected without this delay. My current guess is that there's a 80 | // bug in flushing the output buffer in tests, but it's odd that it only 81 | // shows up here. 82 | Delay(10 * time.Millisecond), 83 | 84 | SendLine("CAP * ACK\r\n"), // Malformed CAP response 85 | SendLine("CAP * ACK :multi-prefix\r\n"), 86 | ExpectLine("CAP END\r\n"), 87 | }) 88 | assert.False(t, c.CapEnabled("random-thing")) 89 | assert.True(t, c.CapEnabled("multi-prefix")) 90 | assert.False(t, c.CapAvailable("random-thing")) 91 | assert.True(t, c.CapAvailable("multi-prefix")) 92 | 93 | // Additional CAP messages after the start are ignored. 94 | c = runClientTest(t, config, io.EOF, func(c *irc.Client) { 95 | assert.False(t, c.CapAvailable("random-thing")) 96 | assert.False(t, c.CapAvailable("multi-prefix")) 97 | c.CapRequest("multi-prefix", true) 98 | }, []TestAction{ 99 | ExpectLine("PASS :test_pass\r\n"), 100 | ExpectLine("CAP LS\r\n"), 101 | ExpectLine("CAP REQ :multi-prefix\r\n"), 102 | ExpectLine("NICK :test_nick\r\n"), 103 | ExpectLine("USER test_user 0 * :test_name\r\n"), 104 | SendLine("CAP * LS :multi-prefix\r\n"), 105 | SendLine("CAP * ACK :multi-prefix\r\n"), 106 | ExpectLine("CAP END\r\n"), 107 | SendLine("CAP * NAK :multi-prefix\r\n"), 108 | }) 109 | assert.False(t, c.CapEnabled("random-thing")) 110 | assert.True(t, c.CapEnabled("multi-prefix")) 111 | assert.False(t, c.CapAvailable("random-thing")) 112 | assert.True(t, c.CapAvailable("multi-prefix")) 113 | 114 | c = runClientTest(t, config, io.EOF, func(c *irc.Client) { 115 | assert.False(t, c.CapAvailable("random-thing")) 116 | assert.False(t, c.CapAvailable("multi-prefix")) 117 | c.CapRequest("multi-prefix", false) 118 | }, []TestAction{ 119 | ExpectLine("PASS :test_pass\r\n"), 120 | ExpectLine("CAP LS\r\n"), 121 | ExpectLine("CAP REQ :multi-prefix\r\n"), 122 | ExpectLine("NICK :test_nick\r\n"), 123 | ExpectLine("USER test_user 0 * :test_name\r\n"), 124 | SendLine("CAP * LS :multi-prefix\r\n"), 125 | SendLine("CAP * NAK :multi-prefix\r\n"), 126 | ExpectLine("CAP END\r\n"), 127 | }) 128 | assert.False(t, c.CapEnabled("random-thing")) 129 | assert.False(t, c.CapEnabled("multi-prefix")) 130 | assert.False(t, c.CapAvailable("random-thing")) 131 | assert.True(t, c.CapAvailable("multi-prefix")) 132 | 133 | c = runClientTest(t, config, errors.New("CAP multi-prefix requested but was rejected"), func(c *irc.Client) { 134 | assert.False(t, c.CapAvailable("random-thing")) 135 | assert.False(t, c.CapAvailable("multi-prefix")) 136 | c.CapRequest("multi-prefix", true) 137 | }, []TestAction{ 138 | ExpectLine("PASS :test_pass\r\n"), 139 | ExpectLine("CAP LS\r\n"), 140 | ExpectLine("CAP REQ :multi-prefix\r\n"), 141 | ExpectLine("NICK :test_nick\r\n"), 142 | ExpectLine("USER test_user 0 * :test_name\r\n"), 143 | SendLine("CAP * LS :multi-prefix\r\n"), 144 | SendLine("CAP * NAK :multi-prefix\r\n"), 145 | }) 146 | assert.False(t, c.CapEnabled("random-thing")) 147 | assert.False(t, c.CapEnabled("multi-prefix")) 148 | assert.False(t, c.CapAvailable("random-thing")) 149 | assert.True(t, c.CapAvailable("multi-prefix")) 150 | 151 | c = runClientTest(t, config, errors.New("CAP multi-prefix requested but not accepted"), func(c *irc.Client) { 152 | assert.False(t, c.CapAvailable("random-thing")) 153 | assert.False(t, c.CapAvailable("multi-prefix")) 154 | c.CapRequest("multi-prefix", true) 155 | }, []TestAction{ 156 | ExpectLine("PASS :test_pass\r\n"), 157 | ExpectLine("CAP LS\r\n"), 158 | ExpectLine("CAP REQ :multi-prefix\r\n"), 159 | ExpectLine("NICK :test_nick\r\n"), 160 | ExpectLine("USER test_user 0 * :test_name\r\n"), 161 | SendLine("CAP * LS :multi-prefix\r\n"), 162 | SendLine("CAP * ACK :\r\n"), 163 | }) 164 | assert.False(t, c.CapEnabled("random-thing")) 165 | assert.False(t, c.CapEnabled("multi-prefix")) 166 | assert.False(t, c.CapAvailable("random-thing")) 167 | assert.True(t, c.CapAvailable("multi-prefix")) 168 | } 169 | 170 | func TestClient(t *testing.T) { 171 | t.Parallel() 172 | 173 | config := irc.ClientConfig{ 174 | Nick: "test_nick", 175 | Pass: "test_pass", 176 | User: "test_user", 177 | Name: "test_name", 178 | } 179 | 180 | runClientTest(t, config, io.EOF, nil, []TestAction{ 181 | ExpectLine("PASS :test_pass\r\n"), 182 | ExpectLine("NICK :test_nick\r\n"), 183 | ExpectLine("USER test_user 0 * :test_name\r\n"), 184 | }) 185 | 186 | runClientTest(t, config, io.EOF, nil, []TestAction{ 187 | ExpectLine("PASS :test_pass\r\n"), 188 | ExpectLine("NICK :test_nick\r\n"), 189 | ExpectLine("USER test_user 0 * :test_name\r\n"), 190 | SendLine("PING :hello world\r\n"), 191 | ExpectLine("PONG :hello world\r\n"), 192 | }) 193 | 194 | c := runClientTest(t, config, io.EOF, nil, []TestAction{ 195 | ExpectLine("PASS :test_pass\r\n"), 196 | ExpectLine("NICK :test_nick\r\n"), 197 | ExpectLine("USER test_user 0 * :test_name\r\n"), 198 | SendLine(":test_nick NICK :new_test_nick\r\n"), 199 | }) 200 | assert.Equal(t, "new_test_nick", c.CurrentNick()) 201 | 202 | c = runClientTest(t, config, io.EOF, nil, []TestAction{ 203 | ExpectLine("PASS :test_pass\r\n"), 204 | ExpectLine("NICK :test_nick\r\n"), 205 | ExpectLine("USER test_user 0 * :test_name\r\n"), 206 | SendLine("001 :new_test_nick\r\n"), 207 | }) 208 | assert.Equal(t, "new_test_nick", c.CurrentNick()) 209 | 210 | c = runClientTest(t, config, io.EOF, nil, []TestAction{ 211 | ExpectLine("PASS :test_pass\r\n"), 212 | ExpectLine("NICK :test_nick\r\n"), 213 | ExpectLine("USER test_user 0 * :test_name\r\n"), 214 | SendLine("433\r\n"), 215 | ExpectLine("NICK :test_nick_\r\n"), 216 | }) 217 | assert.Equal(t, "test_nick_", c.CurrentNick()) 218 | 219 | c = runClientTest(t, config, io.EOF, nil, []TestAction{ 220 | ExpectLine("PASS :test_pass\r\n"), 221 | ExpectLine("NICK :test_nick\r\n"), 222 | ExpectLine("USER test_user 0 * :test_name\r\n"), 223 | SendLine("437\r\n"), 224 | ExpectLine("NICK :test_nick_\r\n"), 225 | }) 226 | 227 | assert.Equal(t, "test_nick_", c.CurrentNick()) 228 | c = runClientTest(t, config, io.EOF, nil, []TestAction{ 229 | ExpectLine("PASS :test_pass\r\n"), 230 | ExpectLine("NICK :test_nick\r\n"), 231 | ExpectLine("USER test_user 0 * :test_name\r\n"), 232 | SendLine("433\r\n"), 233 | ExpectLine("NICK :test_nick_\r\n"), 234 | SendLine("001 :test_nick_\r\n"), 235 | SendLine("433\r\n"), 236 | }) 237 | assert.Equal(t, "test_nick_", c.CurrentNick()) 238 | 239 | c = runClientTest(t, config, io.EOF, nil, []TestAction{ 240 | ExpectLine("PASS :test_pass\r\n"), 241 | ExpectLine("NICK :test_nick\r\n"), 242 | ExpectLine("USER test_user 0 * :test_name\r\n"), 243 | SendLine("437\r\n"), 244 | ExpectLine("NICK :test_nick_\r\n"), 245 | SendLine("001 :test_nick_\r\n"), 246 | SendLine("437\r\n"), 247 | }) 248 | assert.Equal(t, "test_nick_", c.CurrentNick()) 249 | } 250 | 251 | func TestSendLimit(t *testing.T) { 252 | t.Parallel() 253 | 254 | handler := &TestHandler{} 255 | 256 | config := irc.ClientConfig{ 257 | Nick: "test_nick", 258 | Pass: "test_pass", 259 | User: "test_user", 260 | Name: "test_name", 261 | 262 | Handler: handler, 263 | 264 | SendLimit: 10 * time.Millisecond, 265 | SendBurst: 2, 266 | } 267 | 268 | before := time.Now() 269 | runClientTest(t, config, io.EOF, nil, []TestAction{ 270 | ExpectLine("PASS :test_pass\r\n"), 271 | ExpectLine("NICK :test_nick\r\n"), 272 | ExpectLine("USER test_user 0 * :test_name\r\n"), 273 | SendLine("001 :hello_world\r\n"), 274 | }) 275 | assert.WithinDuration(t, before, time.Now(), 50*time.Millisecond) 276 | 277 | // This last test isn't really a test. It's being used to make sure we 278 | // hit the branch which handles dropping ticks if the buffered channel is 279 | // full. 280 | handler.delay = 20 * time.Millisecond // Sleep for 20ms when we get the 001 message 281 | config.SendLimit = 10 * time.Millisecond 282 | config.SendBurst = 0 283 | 284 | before = time.Now() 285 | runClientTest(t, config, io.EOF, nil, []TestAction{ 286 | ExpectLine("PASS :test_pass\r\n"), 287 | ExpectLine("NICK :test_nick\r\n"), 288 | ExpectLine("USER test_user 0 * :test_name\r\n"), 289 | SendLine("001 :hello_world\r\n"), 290 | }) 291 | assert.WithinDuration(t, before, time.Now(), 60*time.Millisecond) 292 | } 293 | 294 | func TestClientHandler(t *testing.T) { 295 | t.Parallel() 296 | 297 | handler := &TestHandler{} 298 | config := irc.ClientConfig{ 299 | Nick: "test_nick", 300 | Pass: "test_pass", 301 | User: "test_user", 302 | Name: "test_name", 303 | 304 | Handler: handler, 305 | } 306 | 307 | runClientTest(t, config, io.EOF, nil, []TestAction{ 308 | ExpectLine("PASS :test_pass\r\n"), 309 | ExpectLine("NICK :test_nick\r\n"), 310 | ExpectLine("USER test_user 0 * :test_name\r\n"), 311 | SendLine("001 :hello_world\r\n"), 312 | }) 313 | assert.EqualValues(t, []*irc.Message{ 314 | { 315 | Tags: irc.Tags{}, 316 | Prefix: &irc.Prefix{}, 317 | Command: "001", 318 | Params: []string{"hello_world"}, 319 | }, 320 | }, handler.Messages()) 321 | } 322 | 323 | func TestFromChannel(t *testing.T) { 324 | t.Parallel() 325 | 326 | c := irc.NewClient(newNopCloser(&bytes.Buffer{}), irc.ClientConfig{Nick: "test_nick"}) 327 | 328 | m := irc.MustParseMessage("PRIVMSG test_nick :hello world") 329 | assert.False(t, c.FromChannel(m)) 330 | 331 | m = irc.MustParseMessage("PRIVMSG #a_channel :hello world") 332 | assert.True(t, c.FromChannel(m)) 333 | 334 | m = irc.MustParseMessage("PING") 335 | assert.False(t, c.FromChannel(m)) 336 | } 337 | 338 | func TestPingLoop(t *testing.T) { 339 | t.Parallel() 340 | 341 | config := irc.ClientConfig{ 342 | Nick: "test_nick", 343 | Pass: "test_pass", 344 | User: "test_user", 345 | Name: "test_name", 346 | 347 | PingFrequency: 20 * time.Millisecond, 348 | PingTimeout: 5 * time.Millisecond, 349 | } 350 | 351 | var lastPing *irc.Message 352 | 353 | // Successful ping 354 | runClientTest(t, config, io.EOF, nil, []TestAction{ 355 | ExpectLine("PASS :test_pass\r\n"), 356 | ExpectLine("NICK :test_nick\r\n"), 357 | ExpectLine("USER test_user 0 * :test_name\r\n"), 358 | SendLine("001 :hello_world\r\n"), 359 | Delay(20 * time.Millisecond), 360 | LineFunc(func(m *irc.Message) { 361 | lastPing = m 362 | }), 363 | SendFunc(func() string { 364 | return fmt.Sprintf("PONG :%s\r\n", lastPing.Trailing()) 365 | }), 366 | Delay(10 * time.Millisecond), 367 | }) 368 | 369 | // Ping timeout 370 | runClientTest(t, config, errors.New("ping timeout"), nil, []TestAction{ 371 | ExpectLine("PASS :test_pass\r\n"), 372 | ExpectLine("NICK :test_nick\r\n"), 373 | ExpectLine("USER test_user 0 * :test_name\r\n"), 374 | SendLine("001 :hello_world\r\n"), 375 | Delay(20 * time.Millisecond), 376 | LineFunc(func(m *irc.Message) { 377 | lastPing = m 378 | }), 379 | Delay(20 * time.Millisecond), 380 | }) 381 | 382 | // Exit in the middle of handling a ping 383 | runClientTest(t, config, io.EOF, nil, []TestAction{ 384 | ExpectLine("PASS :test_pass\r\n"), 385 | ExpectLine("NICK :test_nick\r\n"), 386 | ExpectLine("USER test_user 0 * :test_name\r\n"), 387 | SendLine("001 :hello_world\r\n"), 388 | Delay(20 * time.Millisecond), 389 | LineFunc(func(m *irc.Message) { 390 | lastPing = m 391 | }), 392 | }) 393 | 394 | /* 395 | // This one is just for coverage, so we know we're hitting the 396 | // branch that drops extra pings. 397 | runClientTest(t, config, io.EOF, func(c *irc.Client) { 398 | c.incomingPongChan = make(chan string) 399 | handlePong(c, irc.MustParseMessage("PONG :hello 1")) 400 | }, []TestAction{ 401 | ExpectLine("PASS :test_pass\r\n"), 402 | ExpectLine("NICK :test_nick\r\n"), 403 | ExpectLine("USER test_user 0 * :test_name\r\n"), 404 | SendLine("001 :hello_world\r\n"), 405 | }) 406 | */ 407 | 408 | // Successful ping with write error 409 | runClientTest(t, config, errors.New("test error"), nil, []TestAction{ 410 | ExpectLine("PASS :test_pass\r\n"), 411 | ExpectLine("NICK :test_nick\r\n"), 412 | ExpectLine("USER test_user 0 * :test_name\r\n"), 413 | // We queue this up a line early because the next write will happen after the delay. 414 | QueueWriteError(errors.New("test error")), 415 | SendLine("001 :hello_world\r\n"), 416 | Delay(25 * time.Millisecond), 417 | }) 418 | 419 | // See if we can get the client to hang 420 | runClientTest(t, config, errors.New("test error"), nil, []TestAction{ 421 | ExpectLine("PASS :test_pass\r\n"), 422 | ExpectLine("NICK :test_nick\r\n"), 423 | ExpectLine("USER test_user 0 * :test_name\r\n"), 424 | // We queue this up a line early because the next write will happen after the delay. 425 | QueueWriteError(errors.New("test error")), 426 | SendLine("001 :hello_world\r\n"), 427 | Delay(2 * time.Second), 428 | AssertClosed(), 429 | }) 430 | } 431 | -------------------------------------------------------------------------------- /numerics.go: -------------------------------------------------------------------------------- 1 | //nolint 2 | package irc 3 | 4 | const ( 5 | // RFC1459 6 | RPL_TRACELINK = "200" 7 | RPL_TRACECONNECTING = "201" 8 | RPL_TRACEHANDSHAKE = "202" 9 | RPL_TRACEUNKNOWN = "203" 10 | RPL_TRACEOPERATOR = "204" 11 | RPL_TRACEUSER = "205" 12 | RPL_TRACESERVER = "206" 13 | RPL_TRACENEWTYPE = "208" 14 | RPL_STATSLINKINFO = "211" 15 | RPL_STATSCOMMANDS = "212" 16 | RPL_STATSCLINE = "213" 17 | RPL_STATSNLINE = "214" 18 | RPL_STATSILINE = "215" 19 | RPL_STATSKLINE = "216" 20 | RPL_STATSQLINE = "217" 21 | RPL_STATSYLINE = "218" 22 | RPL_ENDOFSTATS = "219" 23 | RPL_UMODEIS = "221" 24 | RPL_STATSLLINE = "241" 25 | RPL_STATSUPTIME = "242" 26 | RPL_STATSOLINE = "243" 27 | RPL_STATSHLINE = "244" 28 | RPL_LUSERCLIENT = "251" 29 | RPL_LUSEROP = "252" 30 | RPL_LUSERUNKNOWN = "253" 31 | RPL_LUSERCHANNELS = "254" 32 | RPL_LUSERME = "255" 33 | RPL_ADMINME = "256" 34 | RPL_ADMINLOC1 = "257" 35 | RPL_ADMINLOC2 = "258" 36 | RPL_ADMINEMAIL = "259" 37 | RPL_TRACELOG = "261" 38 | RPL_NONE = "300" 39 | RPL_AWAY = "301" 40 | RPL_USERHOST = "302" 41 | RPL_ISON = "303" 42 | RPL_UNAWAY = "305" 43 | RPL_NOWAWAY = "306" 44 | RPL_WHOISUSER = "311" 45 | RPL_WHOISSERVER = "312" 46 | RPL_WHOISOPERATOR = "313" 47 | RPL_WHOWASUSER = "314" 48 | RPL_ENDOFWHO = "315" 49 | RPL_WHOISIDLE = "317" 50 | RPL_ENDOFWHOIS = "318" 51 | RPL_WHOISCHANNELS = "319" 52 | RPL_LIST = "322" 53 | RPL_LISTEND = "323" 54 | RPL_CHANNELMODEIS = "324" 55 | RPL_NOTOPIC = "331" 56 | RPL_TOPIC = "332" 57 | RPL_INVITING = "341" 58 | RPL_VERSION = "351" 59 | RPL_WHOREPLY = "352" 60 | RPL_NAMREPLY = "353" 61 | RPL_LINKS = "364" 62 | RPL_ENDOFLINKS = "365" 63 | RPL_ENDOFNAMES = "366" 64 | RPL_BANLIST = "367" 65 | RPL_ENDOFBANLIST = "368" 66 | RPL_ENDOFWHOWAS = "369" 67 | RPL_INFO = "371" 68 | RPL_MOTD = "372" 69 | RPL_ENDOFINFO = "374" 70 | RPL_MOTDSTART = "375" 71 | RPL_ENDOFMOTD = "376" 72 | RPL_YOUREOPER = "381" 73 | RPL_REHASHING = "382" 74 | RPL_TIME = "391" 75 | RPL_USERSSTART = "392" 76 | RPL_USERS = "393" 77 | RPL_ENDOFUSERS = "394" 78 | RPL_NOUSERS = "395" 79 | ERR_NOSUCHNICK = "401" 80 | ERR_NOSUCHSERVER = "402" 81 | ERR_NOSUCHCHANNEL = "403" 82 | ERR_CANNOTSENDTOCHAN = "404" 83 | ERR_TOOMANYCHANNELS = "405" 84 | ERR_WASNOSUCHNICK = "406" 85 | ERR_TOOMANYTARGETS = "407" 86 | ERR_NOORIGIN = "409" 87 | ERR_NORECIPIENT = "411" 88 | ERR_NOTEXTTOSEND = "412" 89 | ERR_NOTOPLEVEL = "413" 90 | ERR_WILDTOPLEVEL = "414" 91 | ERR_UNKNOWNCOMMAND = "421" 92 | ERR_NOMOTD = "422" 93 | ERR_NOADMININFO = "423" 94 | ERR_FILEERROR = "424" 95 | ERR_NONICKNAMEGIVEN = "431" 96 | ERR_ERRONEUSNICKNAME = "432" 97 | ERR_NICKNAMEINUSE = "433" 98 | ERR_NICKCOLLISION = "436" 99 | ERR_USERNOTINCHANNEL = "441" 100 | ERR_NOTONCHANNEL = "442" 101 | ERR_USERONCHANNEL = "443" 102 | ERR_NOLOGIN = "444" 103 | ERR_SUMMONDISABLED = "445" 104 | ERR_USERSDISABLED = "446" 105 | ERR_NOTREGISTERED = "451" 106 | ERR_NEEDMOREPARAMS = "461" 107 | ERR_ALREADYREGISTERED = "462" 108 | ERR_NOPERMFORHOST = "463" 109 | ERR_PASSWDMISMATCH = "464" 110 | ERR_YOUREBANNEDCREEP = "465" 111 | ERR_KEYSET = "467" 112 | ERR_CHANNELISFULL = "471" 113 | ERR_UNKNOWNMODE = "472" 114 | ERR_INVITEONLYCHAN = "473" 115 | ERR_BANNEDFROMCHAN = "474" 116 | ERR_BADCHANNELKEY = "475" 117 | ERR_NOPRIVILEGES = "481" 118 | ERR_CHANOPRIVSNEEDED = "482" 119 | ERR_CANTKILLSERVER = "483" 120 | ERR_NOOPERHOST = "491" 121 | ERR_UMODEUNKNOWNFLAG = "501" 122 | ERR_USERSDONTMATCH = "502" 123 | 124 | // RFC1459 (Obsolete) 125 | RPL_SERVICEINFO = "231" 126 | RPL_ENDOFSERVICES = "232" 127 | RPL_SERVICE = "233" 128 | RPL_WHOISCHANOP = "316" 129 | RPL_LISTSTART = "321" 130 | RPL_SUMMONING = "342" 131 | RPL_KILLDONE = "361" 132 | RPL_CLOSING = "362" 133 | RPL_CLOSEEND = "363" 134 | RPL_INFOSTART = "373" 135 | RPL_MYPORTIS = "384" 136 | ERR_YOUWILLBEBANNED = "466" 137 | ERR_NOSERVICEHOST = "492" 138 | 139 | // RFC2812 140 | RPL_WELCOME = "001" 141 | RPL_YOURHOST = "002" 142 | RPL_CREATED = "003" 143 | RPL_MYINFO = "004" 144 | RPL_TRACESERVICE = "207" 145 | RPL_TRACECLASS = "209" 146 | RPL_SERVLIST = "234" 147 | RPL_SERVLISTEND = "235" 148 | RPL_STATSVLINE = "240" 149 | RPL_STATSBLINE = "247" 150 | RPL_STATSDLINE = "250" 151 | RPL_TRACEEND = "262" 152 | RPL_TRYAGAIN = "263" 153 | RPL_UNIQOPIS = "325" 154 | RPL_INVITELIST = "346" 155 | RPL_ENDOFINVITELIST = "347" 156 | RPL_EXCEPTLIST = "348" 157 | RPL_ENDOFEXCEPTLIST = "349" 158 | RPL_YOURESERVICE = "383" 159 | ERR_NOSUCHSERVICE = "408" 160 | ERR_BADMASK = "415" 161 | ERR_UNAVAILRESOURCE = "437" 162 | ERR_BADCHANMASK = "476" 163 | ERR_NOCHANMODES = "477" 164 | ERR_BANLISTFULL = "478" 165 | ERR_RESTRICTED = "484" 166 | ERR_UNIQOPRIVSNEEDED = "485" 167 | 168 | // RFC2812 (Obsolete) 169 | RPL_BOUNCE = "005" 170 | RPL_TRACERECONNECT = "210" 171 | RPL_STATSPING = "246" 172 | 173 | // IRCv3 174 | ERR_INVALIDCAPCMD = "410" // Undernet? 175 | RPL_STARTTLS = "670" 176 | ERR_STARTTLS = "691" 177 | RPL_MONONLINE = "730" // RatBox 178 | RPL_MONOFFLINE = "731" // RatBox 179 | RPL_MONLIST = "732" // RatBox 180 | RPL_ENDOFMONLIST = "733" // RatBox 181 | ERR_MONLISTFULL = "734" // RatBox 182 | RPL_WHOISKEYVALUE = "760" 183 | RPL_KEYVALUE = "761" 184 | RPL_METADATAEND = "762" 185 | ERR_METADATALIMIT = "764" 186 | ERR_TARGETINVALID = "765" 187 | ERR_NOMATCHINGKEY = "766" 188 | ERR_KEYINVALID = "767" 189 | ERR_KEYNOTSET = "768" 190 | ERR_KEYNOPERMISSION = "769" 191 | RPL_LOGGEDIN = "900" // Charybdis/Atheme, IRCv3 192 | RPL_LOGGEDOUT = "901" // Charybdis/Atheme, IRCv3 193 | ERR_NICKLOCKED = "902" // Charybdis/Atheme, IRCv3 194 | RPL_SASLSUCCESS = "903" // Charybdis/Atheme, IRCv3 195 | ERR_SASLFAIL = "904" // Charybdis/Atheme, IRCv3 196 | ERR_SASLTOOLONG = "905" // Charybdis/Atheme, IRCv3 197 | ERR_SASLABORTED = "906" // Charybdis/Atheme, IRCv3 198 | ERR_SASLALREADY = "907" // Charybdis/Atheme, IRCv3 199 | RPL_SASLMECHS = "908" // Charybdis/Atheme, IRCv3 200 | 201 | // Other 202 | RPL_ISUPPORT = "005" 203 | 204 | // Ignored 205 | // 206 | // Anything not in an RFC has not been included because 207 | // there are way too many conflicts to deal with. 208 | /* 209 | RPL_MAP = "006" // Unreal 210 | RPL_MAPEND = "007" // Unreal 211 | RPL_SNOMASK = "008" // ircu 212 | RPL_STATMEMTOT = "009" // ircu 213 | RPL_BOUNCE = "010" 214 | RPL_YOURCOOKIE = "014" // Hybrid? 215 | RPL_MAP = "015" // ircu 216 | RPL_MAPMORE = "016" // ircu 217 | RPL_MAPEND = "017" // ircu 218 | RPL_MAPUSERS = "018" // InspIRCd 219 | RPL_HELLO = "020" // rusnet-ircd 220 | RPL_APASSWARN_SET = "030" // ircu 221 | RPL_APASSWARN_SECRET = "031" // ircu 222 | RPL_APASSWARN_CLEAR = "032" // ircu 223 | RPL_YOURID = "042" // IRCnet 224 | RPL_SAVENICK = "043" // IRCnet 225 | RPL_ATTEMPTINGJUNC = "050" // aircd 226 | RPL_ATTEMPTINGREROUTE = "051" // aircd 227 | RPL_REMOTEISUPPORT = "105" // Unreal 228 | RPL_STATS = "210" // aircd 229 | RPL_STATSHELP = "210" // Unreal 230 | RPL_STATSPLINE = "217" // ircu 231 | RPL_STATSPLINE = "220" // Hybrid 232 | RPL_STATSBLINE = "220" // Bahamut, Unreal 233 | RPL_STATSWLINE = "220" // Nefarious 234 | RPL_MODLIST = "222" 235 | RPL_SQLINE_NICK = "222" // Unreal 236 | RPL_STATSBLINE = "222" // Bahamut 237 | RPL_STATSJLINE = "222" // ircu 238 | RPL_CODEPAGE = "222" // rusnet-ircd 239 | RPL_STATSELINE = "223" // Bahamut 240 | RPL_STATSGLINE = "223" // Unreal 241 | RPL_CHARSET = "223" // rusnet-ircd 242 | RPL_STATSFLINE = "224" // Hybrid, Bahamut 243 | RPL_STATSTLINE = "224" // Unreal 244 | RPL_STATSDLINE = "225" // Hybrid 245 | RPL_STATSCLONE = "225" // Bahamut 246 | RPL_STATSELINE = "225" // Unreal 247 | RPL_STATSCOUNT = "226" // Bahamut 248 | RPL_STATSALINE = "226" // Hybrid 249 | RPL_STATSNLINE = "226" // Unreal 250 | RPL_STATSGLINE = "227" // Bahamut 251 | RPL_STATSVLINE = "227" // Unreal 252 | RPL_STATSBLINE = "227" // Rizon 253 | RPL_STATSQLINE = "228" // ircu 254 | RPL_STATSBANVER = "228" // Unreal 255 | RPL_STATSSPAMF = "229" // Unreal 256 | RPL_STATSEXCEPTTKL = "230" // Unreal 257 | RPL_RULES = "232" // Unreal 258 | RPL_STATSVERBOSE = "236" // ircu 259 | RPL_STATSENGINE = "237" // ircu 260 | RPL_STATSFLINE = "238" // ircu 261 | RPL_STATSIAUTH = "239" // IRCnet 262 | RPL_STATSXLINE = "240" // AustHex 263 | RPL_STATSSLINE = "245" // Bahamut, IRCnet, Hybrid 264 | RPL_STATSTLINE = "245" // Hybrid? 265 | RPL_STATSSERVICE = "246" // Hybrid 266 | RPL_STATSTLINE = "246" // ircu 267 | RPL_STATSULINE = "246" // Hybrid 268 | RPL_STATSXLINE = "247" // Hybrid, PTlink, Unreal 269 | RPL_STATSGLINE = "247" // ircu 270 | RPL_STATSULINE = "248" // ircu 271 | RPL_STATSDEFINE = "248" // IRCnet 272 | RPL_STATSULINE = "249" 273 | RPL_STATSDEBUG = "249" // Hybrid 274 | RPL_STATSCONN = "250" // ircu, Unreal 275 | RPL_TRACEPING = "262" 276 | RPL_USINGSSL = "264" // rusnet-ircd 277 | RPL_LOCALUSERS = "265" // aircd, Hybrid, Bahamut 278 | RPL_GLOBALUSERS = "266" // aircd, Hybrid, Bahamut 279 | RPL_START_NETSTAT = "267" // aircd 280 | RPL_NETSTAT = "268" // aircd 281 | RPL_END_NETSTAT = "269" // aircd 282 | RPL_PRIVS = "270" // ircu 283 | RPL_SILELIST = "271" // ircu 284 | RPL_ENDOFSILELIST = "272" // ircu 285 | RPL_NOTIFY = "273" // aircd 286 | RPL_ENDNOTIFY = "274" // aircd 287 | RPL_STATSDELTA = "274" // IRCnet 288 | RPL_STATSDLINE = "275" // ircu, Ultimate 289 | RPL_USINGSSL = "275" // Bahamut 290 | RPL_WHOISCERTFP = "276" // oftc-hybrid 291 | RPL_STATSRLINE = "276" // ircu 292 | RPL_GLIST = "280" // ircu 293 | RPL_ENDOFGLIST = "281" // ircu 294 | RPL_ACCEPTLIST = "281" 295 | RPL_ENDOFACCEPT = "282" 296 | RPL_JUPELIST = "282" // ircu 297 | RPL_ALIST = "283" 298 | RPL_ENDOFJUPELIST = "283" // ircu 299 | RPL_ENDOFALIST = "284" 300 | RPL_FEATURE = "284" // ircu 301 | RPL_GLIST_HASH = "285" 302 | RPL_CHANINFO_HANDLE = "285" // aircd 303 | RPL_NEWHOSTIS = "285" // QuakeNet 304 | RPL_CHANINFO_USERS = "286" // aircd 305 | RPL_CHKHEAD = "286" // QuakeNet 306 | RPL_CHANINFO_CHOPS = "287" // aircd 307 | RPL_CHANUSER = "287" // QuakeNet 308 | RPL_CHANINFO_VOICES = "288" // aircd 309 | RPL_PATCHHEAD = "288" // QuakeNet 310 | RPL_CHANINFO_AWAY = "289" // aircd 311 | RPL_PATCHCON = "289" // QuakeNet 312 | RPL_CHANINFO_OPERS = "290" // aircd 313 | RPL_HELPHDR = "290" // Unreal 314 | RPL_DATASTR = "290" // QuakeNet 315 | RPL_CHANINFO_BANNED = "291" // aircd 316 | RPL_HELPOP = "291" // Unreal 317 | RPL_ENDOFCHECK = "291" // QuakeNet 318 | RPL_CHANINFO_BANS = "292" // aircd 319 | RPL_HELPTLR = "292" // Unreal 320 | ERR_SEARCHNOMATCH = "292" // Nefarious 321 | RPL_CHANINFO_INVITE = "293" // aircd 322 | RPL_HELPHLP = "293" // Unreal 323 | RPL_CHANINFO_INVITES = "294" // aircd 324 | RPL_HELPFWD = "294" // Unreal 325 | RPL_CHANINFO_KICK = "295" // aircd 326 | RPL_HELPIGN = "295" // Unreal 327 | RPL_CHANINFO_KICKS = "296" // aircd 328 | RPL_END_CHANINFO = "299" // aircd 329 | RPL_TEXT = "304" // irc2? 330 | RPL_USERIP = "307" 331 | RPL_WHOISREGNICK = "307" // Bahamut, Unreal 332 | RPL_SUSERHOST = "307" // AustHex 333 | RPL_NOTIFYACTION = "308" // aircd 334 | RPL_WHOISADMIN = "308" // Bahamut 335 | RPL_RULESSTART = "308" // Unreal 336 | RPL_NICKTRACE = "309" // aircd 337 | RPL_WHOISSADMIN = "309" // Bahamut 338 | RPL_ENDOFRULES = "309" // Unreal 339 | RPL_WHOISHELPER = "309" // AustHex 340 | RPL_WHOISSVCMSG = "310" // Bahamut 341 | RPL_WHOISHELPOP = "310" // Unreal 342 | RPL_WHOISSERVICE = "310" // AustHex 343 | RPL_WHOISPRIVDEAF = "316" // Nefarious 344 | RPL_WHOISVIRT = "320" // AustHex 345 | RPL_WHOIS_HIDDEN = "320" // Anothernet 346 | RPL_WHOISSPECIAL = "320" // Unreal 347 | RPL_CHANNELPASSIS = "325" 348 | RPL_WHOISWEBIRC = "325" // Nefarious 349 | RPL_NOCHANPASS = "326" 350 | RPL_CHPASSUNKNOWN = "327" 351 | RPL_WHOISHOST = "327" // rusnet-ircd 352 | RPL_CHANNEL_URL = "328" // Bahamut, AustHex 353 | RPL_CREATIONTIME = "329" // Bahamut 354 | RPL_WHOWAS_TIME = "330" 355 | RPL_WHOISACCOUNT = "330" // ircu 356 | RPL_TOPICWHOTIME = "333" // ircu 357 | RPL_LISTUSAGE = "334" // ircu 358 | RPL_COMMANDSYNTAX = "334" // Bahamut 359 | RPL_LISTSYNTAX = "334" // Unreal 360 | RPL_WHOISBOT = "335" // Unreal 361 | RPL_WHOISTEXT = "335" // Hybrid 362 | RPL_WHOISACCOUNTONLY = "335" // Nefarious 363 | RPL_INVITELIST = "336" // Hybrid 364 | RPL_WHOISBOT = "336" // Nefarious 365 | RPL_ENDOFINVITELIST = "337" // Hybrid 366 | RPL_WHOISTEXT = "337" // Hybrid? 367 | RPL_CHANPASSOK = "338" 368 | RPL_WHOISACTUALLY = "338" // ircu, Bahamut 369 | RPL_BADCHANPASS = "339" 370 | RPL_WHOISMARKS = "339" // Nefarious 371 | RPL_USERIP = "340" // ircu 372 | RPL_WHOISKILL = "343" // Nefarious 373 | RPL_WHOISCOUNTRY = "344" // InspIRCd 3.0 374 | RPL_INVITED = "345" // GameSurge 375 | RPL_WHOISGATEWAY = "350" // InspIRCd 3.0 376 | RPL_WHOSPCRPL = "354" // ircu 377 | RPL_NAMREPLY_ = "355" // QuakeNet 378 | RPL_MAP = "357" // AustHex 379 | RPL_MAPMORE = "358" // AustHex 380 | RPL_MAPEND = "359" // AustHex 381 | RPL_KICKEXPIRED = "377" // aircd 382 | RPL_BANEXPIRED = "378" // aircd 383 | RPL_WHOISHOST = "378" // Unreal 384 | RPL_KICKLINKED = "379" // aircd 385 | RPL_WHOISMODES = "379" // Unreal 386 | RPL_BANLINKED = "380" // aircd 387 | RPL_YOURHELPER = "380" // AustHex 388 | RPL_NOTOPERANYMORE = "385" // AustHex, Hybrid, Unreal 389 | RPL_QLIST = "386" // Unreal 390 | RPL_IRCOPS = "386" // Ultimate 391 | RPL_IRCOPSHEADER = "386" // Nefarious 392 | RPL_ENDOFQLIST = "387" // Unreal 393 | RPL_ENDOFIRCOPS = "387" // Ultimate 394 | RPL_IRCOPS = "387" // Nefarious 395 | RPL_ALIST = "388" // Unreal 396 | RPL_ENDOFIRCOPS = "388" // Nefarious 397 | RPL_ENDOFALIST = "389" // Unreal 398 | RPL_TIME = "391" // ircu 399 | RPL_TIME = "391" // bdq-ircd 400 | RPL_TIME = "391" 401 | RPL_VISIBLEHOST = "396" // Hybrid 402 | RPL_CLONES = "399" // InspIRCd 3.0 403 | ERR_UNKNOWNERROR = "400" 404 | ERR_NOCOLORSONCHAN = "408" // Bahamut 405 | ERR_NOCTRLSONCHAN = "408" // Hybrid 406 | ERR_TOOMANYMATCHES = "416" // IRCnet 407 | ERR_QUERYTOOLONG = "416" // ircu 408 | ERR_INPUTTOOLONG = "417" // ircu 409 | ERR_LENGTHTRUNCATED = "419" // aircd 410 | ERR_AMBIGUOUSCOMMAND = "420" // InspIRCd 411 | ERR_NOOPERMOTD = "425" // Unreal 412 | ERR_TOOMANYAWAY = "429" // Bahamut 413 | ERR_EVENTNICKCHANGE = "430" // AustHex 414 | ERR_SERVICENAMEINUSE = "434" // AustHex? 415 | ERR_NORULES = "434" // Unreal, Ultimate 416 | ERR_SERVICECONFUSED = "435" // Unreal 417 | ERR_BANONCHAN = "435" // Bahamut 418 | ERR_BANNICKCHANGE = "437" // ircu 419 | ERR_NICKTOOFAST = "438" // ircu 420 | ERR_DEAD = "438" // IRCnet 421 | ERR_TARGETTOOFAST = "439" // ircu 422 | ERR_SERVICESDOWN = "440" // Bahamut, Unreal 423 | ERR_NONICKCHANGE = "447" // Unreal 424 | ERR_FORBIDDENCHANNEL = "448" // Unreal 425 | ERR_NOTIMPLEMENTED = "449" // Undernet 426 | ERR_IDCOLLISION = "452" 427 | ERR_NICKLOST = "453" 428 | ERR_HOSTILENAME = "455" // Unreal 429 | ERR_ACCEPTFULL = "456" 430 | ERR_ACCEPTEXIST = "457" 431 | ERR_ACCEPTNOT = "458" 432 | ERR_NOHIDING = "459" // Unreal 433 | ERR_NOTFORHALFOPS = "460" // Unreal 434 | ERR_INVALIDUSERNAME = "468" // ircu 435 | ERR_ONLYSERVERSCANCHANGE = "468" // Bahamut, Unreal 436 | ERR_NOCODEPAGE = "468" // rusnet-ircd 437 | ERR_LINKSET = "469" // Unreal 438 | ERR_LINKCHANNEL = "470" // Unreal 439 | ERR_KICKEDFROMCHAN = "470" // aircd 440 | ERR_7BIT = "470" // rusnet-ircd 441 | ERR_NEEDREGGEDNICK = "477" // Bahamut, ircu, Unreal 442 | ERR_BADCHANNAME = "479" // Hybrid 443 | ERR_LINKFAIL = "479" // Unreal 444 | ERR_NOCOLOR = "479" // rusnet-ircd 445 | ERR_NOULINE = "480" // AustHex 446 | ERR_CANNOTKNOCK = "480" // Unreal 447 | ERR_THROTTLE = "480" // Ratbox 448 | ERR_NOWALLOP = "480" // rusnet-ircd 449 | ERR_ISCHANSERVICE = "484" // Undernet 450 | ERR_DESYNC = "484" // Bahamut, Hybrid, PTlink 451 | ERR_ATTACKDENY = "484" // Unreal 452 | ERR_KILLDENY = "485" // Unreal 453 | ERR_CANTKICKADMIN = "485" // PTlink 454 | ERR_ISREALSERVICE = "485" // QuakeNet 455 | ERR_CHANBANREASON = "485" // Hybrid 456 | ERR_NONONREG = "486" // Unreal? 457 | ERR_HTMDISABLED = "486" // Unreal 458 | ERR_ACCOUNTONLY = "486" // QuakeNet 459 | ERR_RLINED = "486" // rusnet-ircd 460 | ERR_CHANTOORECENT = "487" // IRCnet 461 | ERR_MSGSERVICES = "487" // Bahamut 462 | ERR_NOTFORUSERS = "487" // Unreal? 463 | ERR_NONONSSL = "487" // ChatIRCd 464 | ERR_TSLESSCHAN = "488" // IRCnet 465 | ERR_HTMDISABLED = "488" // Unreal? 466 | ERR_NOSSL = "488" // Bahamut 467 | ERR_SECUREONLYCHAN = "489" // Unreal 468 | ERR_VOICENEEDED = "489" // Undernet 469 | ERR_ALLMUSTSSL = "490" // InspIRCd 470 | ERR_NOSWEAR = "490" // Unreal 471 | ERR_NOCTCP = "492" // Hybrid / Unreal? 472 | ERR_CANNOTSENDTOUSER = "492" // Charybdis? 473 | ERR_NOSHAREDCHAN = "493" // Bahamut 474 | ERR_NOFEATURE = "493" // ircu 475 | ERR_BADFEATVALUE = "494" // ircu 476 | ERR_OWNMODE = "494" // Bahamut, charybdis? 477 | ERR_BADLOGTYPE = "495" // ircu 478 | ERR_BADLOGSYS = "496" // ircu 479 | ERR_BADLOGVALUE = "497" // ircu 480 | ERR_ISOPERLCHAN = "498" // ircu 481 | ERR_CHANOWNPRIVNEEDED = "499" // Unreal 482 | ERR_TOOMANYJOINS = "500" // Unreal? 483 | ERR_NOREHASHPARAM = "500" // rusnet-ircd 484 | ERR_CANNOTSETMODER = "500" // InspIRCd 485 | ERR_UNKNOWNSNOMASK = "501" // InspIRCd 486 | ERR_USERNOTONSERV = "504" 487 | ERR_SILELISTFULL = "511" // ircu 488 | ERR_TOOMANYWATCH = "512" // Bahamut 489 | ERR_NOSUCHGLINE = "512" // ircu 490 | ERR_BADPING = "513" // ircu 491 | ERR_TOOMANYDCC = "514" // Bahamut 492 | ERR_NOSUCHJUPE = "514" // irch 493 | ERR_BADEXPIRE = "515" // ircu 494 | ERR_DONTCHEAT = "516" // ircu 495 | ERR_DISABLED = "517" // ircu 496 | ERR_NOINVITE = "518" // Unreal 497 | ERR_LONGMASK = "518" // ircu 498 | ERR_ADMONLY = "519" // Unreal 499 | ERR_TOOMANYUSERS = "519" // ircu 500 | ERR_OPERONLY = "520" // Unreal 501 | ERR_MASKTOOWIDE = "520" // ircu 502 | ERR_LISTSYNTAX = "521" // Bahamut 503 | ERR_NOSUCHGLINE = "521" // Nefarious 504 | ERR_WHOSYNTAX = "522" // Bahamut 505 | ERR_WHOLIMEXCEED = "523" // Bahamut 506 | ERR_QUARANTINED = "524" // ircu 507 | ERR_OPERSPVERIFY = "524" // Unreal 508 | ERR_HELPNOTFOUND = "524" // Hybrid 509 | ERR_INVALIDKEY = "525" // ircu 510 | ERR_CANTSENDTOUSER = "531" // InspIRCd 511 | ERR_BADHOSTMASK = "550" // QuakeNet 512 | ERR_HOSTUNAVAIL = "551" // QuakeNet 513 | ERR_USINGSLINE = "552" // QuakeNet 514 | ERR_STATSSLINE = "553" // QuakeNet 515 | ERR_NOTLOWEROPLEVEL = "560" // ircu 516 | ERR_NOTMANAGER = "561" // ircu 517 | ERR_CHANSECURED = "562" // ircu 518 | ERR_UPASSSET = "563" // ircu 519 | ERR_UPASSNOTSET = "564" // ircu 520 | ERR_NOMANAGER = "566" // ircu 521 | ERR_UPASS_SAME_APASS = "567" // ircu 522 | ERR_LASTERROR = "568" // ircu 523 | RPL_NOOMOTD = "568" // Nefarious 524 | RPL_REAWAY = "597" // Unreal 525 | RPL_GONEAWAY = "598" // Unreal 526 | RPL_NOTAWAY = "599" // Unreal 527 | RPL_LOGON = "600" // Bahamut, Unreal 528 | RPL_LOGOFF = "601" // Bahamut, Unreal 529 | RPL_WATCHOFF = "602" // Bahamut, Unreal 530 | RPL_WATCHSTAT = "603" // Bahamut, Unreal 531 | RPL_NOWON = "604" // Bahamut, Unreal 532 | RPL_NOWOFF = "605" // Bahamut, Unreal 533 | RPL_WATCHLIST = "606" // Bahamut, Unreal 534 | RPL_ENDOFWATCHLIST = "607" // Bahamut, Unreal 535 | RPL_WATCHCLEAR = "608" // Ultimate 536 | RPL_NOWISAWAY = "609" // Unreal 537 | RPL_MAPMORE = "610" // Unreal 538 | RPL_ISOPER = "610" // Ultimate 539 | RPL_ISLOCOP = "611" // Ultimate 540 | RPL_ISNOTOPER = "612" // Ultimate 541 | RPL_ENDOFISOPER = "613" // Ultimate 542 | RPL_MAPMORE = "615" // PTlink 543 | RPL_WHOISMODES = "615" // Ultimate 544 | RPL_WHOISHOST = "616" // Ultimate 545 | RPL_WHOISSSLFP = "617" // Nefarious 546 | RPL_DCCSTATUS = "617" // Bahamut 547 | RPL_WHOISBOT = "617" // Ultimate 548 | RPL_DCCLIST = "618" // Bahamut 549 | RPL_ENDOFDCCLIST = "619" // Bahamut 550 | RPL_WHOWASHOST = "619" // Ultimate 551 | RPL_DCCINFO = "620" // Bahamut 552 | RPL_RULESSTART = "620" // Ultimate 553 | RPL_RULES = "621" // Ultimate 554 | RPL_ENDOFRULES = "622" // Ultimate 555 | RPL_MAPMORE = "623" // Ultimate 556 | RPL_OMOTDSTART = "624" // Ultimate 557 | RPL_OMOTD = "625" // Ultimate 558 | RPL_ENDOFOMOTD = "626" // Ultimate 559 | RPL_SETTINGS = "630" // Ultimate 560 | RPL_ENDOFSETTINGS = "631" // Ultimate 561 | RPL_SYNTAX = "650" // InspIRCd 3.0 562 | RPL_CHANNELSMSG = "651" // InspIRCd 3.0 563 | RPL_WHOWASIP = "652" // InspIRCd 3.0 564 | RPL_UNINVITED = "653" // InspIRCd 3.0 565 | RPL_SPAMCMDFWD = "659" // Unreal 566 | RPL_WHOISSECURE = "671" // Unreal 567 | RPL_UNKNOWNMODES = "672" // Ithildin 568 | RPL_WHOISREALIP = "672" // Rizon 569 | RPL_CANNOTSETMODES = "673" // Ithildin 570 | RPL_WHOISYOURID = "674" // ChatIRCd 571 | RPL_LANGUAGES = "690" // Unreal? 572 | ERR_INVALIDMODEPARAM = "696" // InspIRCd 3.0 573 | ERR_LISTMODEALREADYSET = "697" // InspIRCd 3.0 574 | ERR_LISTMODENOTSET = "698" // InspIRCd 3.0 575 | RPL_COMMANDS = "700" // InspIRCd 3.0 576 | RPL_COMMANDSEND = "701" // InspIRCd 3.0 577 | RPL_MODLIST = "702" // RatBox 578 | RPL_ENDOFMODLIST = "703" // RatBox 579 | RPL_HELPSTART = "704" // RatBox 580 | RPL_HELPTXT = "705" // RatBox 581 | RPL_ENDOFHELP = "706" // RatBox 582 | ERR_TARGCHANGE = "707" // RatBox 583 | RPL_ETRACEFULL = "708" // RatBox 584 | RPL_ETRACE = "709" // RatBox 585 | RPL_KNOCK = "710" // RatBox 586 | RPL_KNOCKDLVR = "711" // RatBox 587 | ERR_TOOMANYKNOCK = "712" // RatBox 588 | ERR_CHANOPEN = "713" // RatBox 589 | ERR_KNOCKONCHAN = "714" // RatBox 590 | ERR_KNOCKDISABLED = "715" // RatBox 591 | ERR_TOOMANYINVITE = "715" // Hybrid 592 | RPL_INVITETHROTTLE = "715" // Rizon 593 | RPL_TARGUMODEG = "716" // RatBox 594 | RPL_TARGNOTIFY = "717" // RatBox 595 | RPL_UMODEGMSG = "718" // RatBox 596 | RPL_OMOTDSTART = "720" // RatBox 597 | RPL_OMOTD = "721" // RatBox 598 | RPL_ENDOFOMOTD = "722" // RatBox 599 | ERR_NOPRIVS = "723" // RatBox 600 | RPL_TESTMASK = "724" // RatBox 601 | RPL_TESTLINE = "725" // RatBox 602 | RPL_NOTESTLINE = "726" // RatBox 603 | RPL_TESTMASKGECOS = "727" // RatBox 604 | RPL_QUIETLIST = "728" // Charybdis 605 | RPL_ENDOFQUIETLIST = "729" // Charybdis 606 | RPL_RSACHALLENGE2 = "740" // RatBox 607 | RPL_ENDOFRSACHALLENGE2 = "741" // RatBox 608 | ERR_MLOCKRESTRICTED = "742" // Charybdis 609 | ERR_INVALIDBAN = "743" // Charybdis 610 | ERR_TOPICLOCK = "744" // InspIRCd? 611 | RPL_SCANMATCHED = "750" // RatBox 612 | RPL_SCANUMODES = "751" // RatBox 613 | RPL_ETRACEEND = "759" // irc2.11 614 | RPL_XINFO = "771" // Ithildin 615 | RPL_XINFOSTART = "773" // Ithildin 616 | RPL_XINFOEND = "774" // Ithildin 617 | RPL_CHECK = "802" // InspIRCd 3.0 618 | RPL_OTHERUMODEIS = "803" // InspIRCd 3.0 619 | RPL_OTHERSNOMASKIS = "804" // InspIRCd 3.0 620 | ERR_BADCHANNEL = "926" // InspIRCd 621 | ERR_INVALIDWATCHNICK = "942" // InspIRCd 622 | RPL_IDLETIMESET = "944" // InspIRCd 623 | RPL_NICKLOCKOFF = "945" // InspIRCd 624 | ERR_NICKNOTLOCKED = "946" // InspIRCd 625 | RPL_NICKLOCKON = "947" // InspIRCd 626 | ERR_INVALIDIDLETIME = "948" // InspIRCd 627 | RPL_UNSILENCED = "950" // InspIRCd 628 | RPL_SILENCED = "951" // InspIRCd 629 | ERR_NOTSILENCED = "952" // InspIRCd 630 | RPL_ENDOFPROPLIST = "960" // InspIRCd 631 | RPL_PROPLIST = "961" // InspIRCd 632 | ERR_CANNOTDOCOMMAND = "972" // Unreal 633 | ERR_CANTUNLOADMODULE = "972" // InspIRCd 634 | RPL_UNLOADEDMODULE = "973" // InspIRCd 635 | ERR_CANNOTCHANGECHANMODE = "974" // Unreal 636 | ERR_CANTLOADMODULE = "974" // InspIRCd 637 | RPL_LOADEDMODULE = "975" // InspIRCd 638 | ERR_LASTERROR = "975" // Nefarious 639 | RPL_SERVLOCKON = "988" // InspIRCd 640 | RPL_SERVLOCKOFF = "989" // InspIRCd 641 | RPL_DCCALLOWSTART = "990" // InspIRCd 642 | RPL_DCCALLOWLIST = "991" // InspIRCd 643 | RPL_DCCALLOWEND = "992" // InspIRCd 644 | RPL_DCCALLOWTIMED = "993" // InspIRCd 645 | RPL_DCCALLOWPERMANENT = "994" // InspIRCd 646 | RPL_DCCALLOWREMOVED = "995" // InspIRCd 647 | ERR_DCCALLOWINVALID = "996" // InspIRCd 648 | RPL_DCCALLOWEXPIRED = "997" // InspIRCd 649 | ERR_UNKNOWNDCCALLOWCMD = "998" // InspIRCd 650 | ERR_NUMERIC_ERR = "999" // Bahamut 651 | //*/ 652 | 653 | // Obsolete 654 | /* 655 | RPL_STATMEM = "010" // ircu 656 | RPL_STATSZLINE = "225" // Bahamut 657 | RPL_MAPUSERS = "270" // InspIRCd 2.0 658 | RPL_VCHANEXIST = "276" // Hybrid 659 | RPL_VCHANLIST = "277" // Hybrid 660 | RPL_VCHANHELP = "278" // Hybrid 7.0? 661 | RPL_CHANNELMLOCKIS = "325" // sorircd 662 | RPL_WHOWASREAL = "360" // Charybdis 663 | RPL_SPAM = "377" // AustHex 664 | RPL_MOTD = "378" // AustHex 665 | RPL_WHOWASIP = "379" // InspIRCd 2.0 666 | RPL_RSACHALLENGE = "386" // Hybrid 667 | ERR_SSLONLYCHAN = "480" // Hybrid 668 | ERR_BANNEDNICK = "485" // Ratbox 669 | ERR_DELAYREJOIN = "495" // InspIRCd 2.0 670 | ERR_GHOSTEDCLIENT = "503" // Hybrid 671 | ERR_VWORLDWARN = "503" // AustHex 672 | ERR_INVALID_ERROR = "514" // ircu 673 | ERR_WHOTRUNC = "520" // AustHex 674 | ERR_REMOTEPFX = "525" // CAPAB USERCMDPFX 675 | ERR_PFXUNROUTABLE = "526" // CAPAB USERCMDPFX 676 | RPL_DUMPING = "640" // Unreal 677 | RPL_DUMPRPL = "641" // Unreal 678 | RPL_EODUMP = "642" // Unreal 679 | RPL_COMMANDS = "702" // InspIRCd 2.0 680 | RPL_COMMANDSEND = "703" // InspIRCd 2.0 681 | ERR_WORDFILTERED = "936" // InspIRCd 682 | ERR_ALREADYCHANFILTERED = "937" // InspIRCd 2.0 683 | ERR_NOSUCHCHANFILTER = "938" // InspIRCd 2.0 684 | ERR_CHANFILTERFULL = "939" // InspIRCd 2.0 685 | RPL_DCCALLOWHELP = "998" // InspIRCd 686 | RPL_ENDOFDCCALLOWHELP = "999" // InspIRCd 2.0 687 | //*/ 688 | ) 689 | --------------------------------------------------------------------------------