diff options
-rw-r--r-- | vendor/github.com/lrstanley/girc/cap_sasl.go | 137 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/cap_tags.go | 318 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/constants.go | 338 |
3 files changed, 793 insertions, 0 deletions
diff --git a/vendor/github.com/lrstanley/girc/cap_sasl.go b/vendor/github.com/lrstanley/girc/cap_sasl.go new file mode 100644 index 00000000..6f86e101 --- /dev/null +++ b/vendor/github.com/lrstanley/girc/cap_sasl.go @@ -0,0 +1,137 @@ +// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use +// of this source code is governed by the MIT license that can be found in +// the LICENSE file. + +package girc + +import ( + "encoding/base64" + "fmt" +) + +// SASLMech is an representation of what a SASL mechanism should support. +// See SASLExternal and SASLPlain for implementations of this. +type SASLMech interface { + // Method returns the uppercase version of the SASL mechanism name. + Method() string + // Encode returns the response that the SASL mechanism wants to use. If + // the returned string is empty (e.g. the mechanism gives up), the handler + // will attempt to panic, as expectation is that if SASL authentication + // fails, the client will disconnect. + Encode(params []string) (output string) +} + +// SASLExternal implements the "EXTERNAL" SASL type. +type SASLExternal struct { + // Identity is an optional field which allows the client to specify + // pre-authentication identification. This means that EXTERNAL will + // supply this in the initial response. This usually isn't needed (e.g. + // CertFP). + Identity string `json:"identity"` +} + +// Method identifies what type of SASL this implements. +func (sasl *SASLExternal) Method() string { + return "EXTERNAL" +} + +// Encode for external SALS authentication should really only return a "+", +// unless the user has specified pre-authentication or identification data. +// See https://tools.ietf.org/html/rfc4422#appendix-A for more info. +func (sasl *SASLExternal) Encode(params []string) string { + if len(params) != 1 || params[0] != "+" { + return "" + } + + if sasl.Identity != "" { + return sasl.Identity + } + + return "+" +} + +// SASLPlain contains the user and password needed for PLAIN SASL authentication. +type SASLPlain struct { + User string `json:"user"` // User is the username for SASL. + Pass string `json:"pass"` // Pass is the password for SASL. +} + +// Method identifies what type of SASL this implements. +func (sasl *SASLPlain) Method() string { + return "PLAIN" +} + +// Encode encodes the plain user+password into a SASL PLAIN implementation. +// See https://tools.ietf.org/rfc/rfc4422.txt for more info. +func (sasl *SASLPlain) Encode(params []string) string { + if len(params) != 1 || params[0] != "+" { + return "" + } + + in := []byte(sasl.User) + + in = append(in, 0x0) + in = append(in, []byte(sasl.User)...) + in = append(in, 0x0) + in = append(in, []byte(sasl.Pass)...) + + return base64.StdEncoding.EncodeToString(in) +} + +const saslChunkSize = 400 + +func handleSASL(c *Client, e Event) { + if e.Command == RPL_SASLSUCCESS || e.Command == ERR_SASLALREADY { + // Let the server know that we're done. + c.write(&Event{Command: CAP, Params: []string{CAP_END}}) + return + } + + // Assume they want us to handle sending auth. + auth := c.Config.SASL.Encode(e.Params) + + if auth == "" { + // Assume the SASL authentication method doesn't want to respond for + // some reason. The SASL spec and IRCv3 spec do not define a clear + // way to abort a SASL exchange, other than to disconnect, or proceed + // with CAP END. + c.rx <- &Event{Command: ERROR, Trailing: fmt.Sprintf( + "closing connection: SASL %s failed: %s", + c.Config.SASL.Method(), e.Trailing, + )} + return + } + + // Send in "saslChunkSize"-length byte chunks. If the last chuck is + // exactly "saslChunkSize" bytes, send a "AUTHENTICATE +" 0-byte + // acknowledgement response to let the server know that we're done. + for { + if len(auth) > saslChunkSize { + c.write(&Event{Command: AUTHENTICATE, Params: []string{auth[0 : saslChunkSize-1]}, Sensitive: true}) + auth = auth[saslChunkSize:] + continue + } + + if len(auth) <= saslChunkSize { + c.write(&Event{Command: AUTHENTICATE, Params: []string{auth}, Sensitive: true}) + + if len(auth) == 400 { + c.write(&Event{Command: AUTHENTICATE, Params: []string{"+"}}) + } + break + } + } + return +} + +func handleSASLError(c *Client, e Event) { + if c.Config.SASL == nil { + c.write(&Event{Command: CAP, Params: []string{CAP_END}}) + return + } + + // Authentication failed. The SASL spec and IRCv3 spec do not define a + // clear way to abort a SASL exchange, other than to disconnect, or + // proceed with CAP END. + c.rx <- &Event{Command: ERROR, Trailing: "closing connection: " + e.Trailing} +} diff --git a/vendor/github.com/lrstanley/girc/cap_tags.go b/vendor/github.com/lrstanley/girc/cap_tags.go new file mode 100644 index 00000000..a4026a01 --- /dev/null +++ b/vendor/github.com/lrstanley/girc/cap_tags.go @@ -0,0 +1,318 @@ +// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use +// of this source code is governed by the MIT license that can be found in +// the LICENSE file. + +package girc + +import ( + "bytes" + "fmt" + "io" + "sort" + "strings" +) + +// handleTags handles any messages that have tags that will affect state. (e.g. +// 'account' tags.) +func handleTags(c *Client, e Event) { + if len(e.Tags) == 0 { + return + } + + account, ok := e.Tags.Get("account") + if !ok { + return + } + + c.state.Lock() + user := c.state.lookupUser(e.Source.Name) + if user != nil { + user.Extras.Account = account + } + c.state.Unlock() + c.state.notify(c, UPDATE_STATE) +} + +const ( + prefixTag byte = '@' + prefixTagValue byte = '=' + prefixUserTag byte = '+' + tagSeparator byte = ';' + maxTagLength int = 511 // 510 + @ and " " (space), though space usually not included. +) + +// Tags represents the key-value pairs in IRCv3 message tags. The map contains +// the encoded message-tag values. If the tag is present, it may still be +// empty. See Tags.Get() and Tags.Set() for use with getting/setting +// information within the tags. +// +// Note that retrieving and setting tags are not concurrent safe. If this is +// necessary, you will need to implement it yourself. +type Tags map[string]string + +// ParseTags parses out the key-value map of tags. raw should only be the tag +// data, not a full message. For example: +// @aaa=bbb;ccc;example.com/ddd=eee +// NOT: +// @aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello +func ParseTags(raw string) (t Tags) { + t = make(Tags) + + if len(raw) > 0 && raw[0] == prefixTag { + raw = raw[1:] + } + + parts := strings.Split(raw, string(tagSeparator)) + var hasValue int + + for i := 0; i < len(parts); i++ { + hasValue = strings.IndexByte(parts[i], prefixTagValue) + + // The tag doesn't contain a value or has a splitter with no value. + if hasValue < 1 || len(parts[i]) < hasValue+1 { + if !validTag(parts[i]) { + continue + } + + t[parts[i]] = "" + continue + } + + // Check if tag key or decoded value are invalid. + if !validTag(parts[i][:hasValue]) || !validTagValue(tagDecoder.Replace(parts[i][hasValue+1:])) { + continue + } + + t[parts[i][:hasValue]] = parts[i][hasValue+1:] + } + + return t +} + +// Len determines the length of the bytes representation of this tag map. This +// does not include the trailing space required when creating an event, but +// does include the tag prefix ("@"). +func (t Tags) Len() (length int) { + if t == nil { + return 0 + } + + return len(t.Bytes()) +} + +// Equals compares two Tags for equality. With the msgid IRCv3 spec +\ +// echo-message (amongst others), we may receive events that have msgid's, +// whereas our local events will not have the msgid. As such, don't compare +// all tags, only the necessary/important tags. +func (t Tags) Equals(tt Tags) bool { + // The only tag which is important at this time. + taccount, _ := t.Get("account") + ttaccount, _ := tt.Get("account") + return taccount == ttaccount +} + +// Keys returns a slice of (unsorted) tag keys. +func (t Tags) Keys() (keys []string) { + keys = make([]string, 0, t.Count()) + for key := range t { + keys = append(keys, key) + } + return keys +} + +// Count finds how many total tags that there are. +func (t Tags) Count() int { + if t == nil { + return 0 + } + + return len(t) +} + +// Bytes returns a []byte representation of this tag map, including the tag +// prefix ("@"). Note that this will return the tags sorted, regardless of +// the order of how they were originally parsed. +func (t Tags) Bytes() []byte { + if t == nil { + return []byte{} + } + + max := len(t) + if max == 0 { + return nil + } + + buffer := new(bytes.Buffer) + buffer.WriteByte(prefixTag) + + var current int + + // Sort the writing of tags so we can at least guarantee that they will + // be in order, and testable. + var names []string + for tagName := range t { + names = append(names, tagName) + } + sort.Strings(names) + + for i := 0; i < len(names); i++ { + // Trim at max allowed chars. + if (buffer.Len() + len(names[i]) + len(t[names[i]]) + 2) > maxTagLength { + return buffer.Bytes() + } + + buffer.WriteString(names[i]) + + // Write the value as necessary. + if len(t[names[i]]) > 0 { + buffer.WriteByte(prefixTagValue) + buffer.WriteString(t[names[i]]) + } + + // add the separator ";" between tags. + if current < max-1 { + buffer.WriteByte(tagSeparator) + } + + current++ + } + + return buffer.Bytes() +} + +// String returns a string representation of this tag map. +func (t Tags) String() string { + if t == nil { + return "" + } + + return string(t.Bytes()) +} + +// writeTo writes the necessary tag bytes to an io.Writer, including a trailing +// space-separator. +func (t Tags) writeTo(w io.Writer) (n int, err error) { + b := t.Bytes() + if len(b) == 0 { + return n, err + } + + n, err = w.Write(b) + if err != nil { + return n, err + } + + var j int + j, err = w.Write([]byte{eventSpace}) + n += j + + return n, err +} + +// tagDecode are encoded -> decoded pairs for replacement to decode. +var tagDecode = []string{ + "\\:", ";", + "\\s", " ", + "\\\\", "\\", + "\\r", "\r", + "\\n", "\n", +} +var tagDecoder = strings.NewReplacer(tagDecode...) + +// tagEncode are decoded -> encoded pairs for replacement to decode. +var tagEncode = []string{ + ";", "\\:", + " ", "\\s", + "\\", "\\\\", + "\r", "\\r", + "\n", "\\n", +} +var tagEncoder = strings.NewReplacer(tagEncode...) + +// Get returns the unescaped value of given tag key. Note that this is not +// concurrent safe. +func (t Tags) Get(key string) (tag string, success bool) { + if t == nil { + return "", false + } + + if _, ok := t[key]; ok { + tag = tagDecoder.Replace(t[key]) + success = true + } + + return tag, success +} + +// Set escapes given value and saves it as the value for given key. Note that +// this is not concurrent safe. +func (t Tags) Set(key, value string) error { + if t == nil { + t = make(Tags) + } + + if !validTag(key) { + return fmt.Errorf("tag key %q is invalid", key) + } + + value = tagEncoder.Replace(value) + + if len(value) > 0 && !validTagValue(value) { + return fmt.Errorf("tag value %q of key %q is invalid", value, key) + } + + // Check to make sure it's not too long here. + if (t.Len() + len(key) + len(value) + 2) > maxTagLength { + return fmt.Errorf("unable to set tag %q [value %q]: tags too long for message", key, value) + } + + t[key] = value + + return nil +} + +// Remove deletes the tag frwom the tag map. +func (t Tags) Remove(key string) (success bool) { + if t == nil { + return false + } + + if _, success = t[key]; success { + delete(t, key) + } + + return success +} + +// validTag validates an IRC tag. +func validTag(name string) bool { + if len(name) < 1 { + return false + } + + // Allow user tags to be passed to validTag. + if len(name) >= 2 && name[0] == prefixUserTag { + name = name[1:] + } + + for i := 0; i < len(name); i++ { + // A-Z, a-z, 0-9, -/._ + if (name[i] < 'A' || name[i] > 'Z') && (name[i] < 'a' || name[i] > 'z') && (name[i] < '-' || name[i] > '9') && name[i] != '_' { + return false + } + } + + return true +} + +// validTagValue valids a decoded IRC tag value. If the value is not decoded +// with tagDecoder first, it may be seen as invalid. +func validTagValue(value string) bool { + for i := 0; i < len(value); i++ { + // Don't allow any invisible chars within the tag, or semicolons. + if value[i] < '!' || value[i] > '~' || value[i] == ';' { + return false + } + } + return true +} diff --git a/vendor/github.com/lrstanley/girc/constants.go b/vendor/github.com/lrstanley/girc/constants.go new file mode 100644 index 00000000..4d3c65bc --- /dev/null +++ b/vendor/github.com/lrstanley/girc/constants.go @@ -0,0 +1,338 @@ +// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use +// of this source code is governed by the MIT license that can be found in +// the LICENSE file. + +package girc + +// Standard CTCP based constants. +const ( + CTCP_PING = "PING" + CTCP_PONG = "PONG" + CTCP_VERSION = "VERSION" + CTCP_USERINFO = "USERINFO" + CTCP_CLIENTINFO = "CLIENTINFO" + CTCP_SOURCE = "SOURCE" + CTCP_TIME = "TIME" + CTCP_FINGER = "FINGER" + CTCP_ERRMSG = "ERRMSG" +) + +// Emulated event commands used to allow easier hooks into the changing +// state of the client. +const ( + UPDATE_STATE = "CLIENT_STATE_UPDATED" // when channel/user state is updated. + UPDATE_GENERAL = "CLIENT_GENERAL_UPDATED" // when general state (client nick, server name, etc) is updated. + ALL_EVENTS = "*" // trigger on all events + CONNECTED = "CLIENT_CONNECTED" // when it's safe to send arbitrary commands (joins, list, who, etc), trailing is host:port + INITIALIZED = "CLIENT_INIT" // verifies successful socket connection, trailing is host:port + DISCONNECTED = "CLIENT_DISCONNECTED" // occurs when we're disconnected from the server (user-requested or not) + STOPPED = "CLIENT_STOPPED" // occurs when Client.Stop() has been called +) + +// User/channel prefixes :: RFC1459. +const ( + DefaultPrefixes = "(ov)@+" // the most common default prefixes + ModeAddPrefix = "+" // modes are being added + ModeDelPrefix = "-" // modes are being removed + + ChannelPrefix = "#" // regular channel + DistributedPrefix = "&" // distributed channel + OwnerPrefix = "~" // user owner +q (non-rfc) + AdminPrefix = "&" // user admin +a (non-rfc) + HalfOperatorPrefix = "%" // user half operator +h (non-rfc) + OperatorPrefix = "@" // user operator +o + VoicePrefix = "+" // user has voice +v +) + +// User modes :: RFC1459; section 4.2.3.2. +const ( + UserModeInvisible = "i" // invisible + UserModeOperator = "o" // server operator + UserModeServerNotices = "s" // user wants to receive server notices + UserModeWallops = "w" // user wants to receive wallops +) + +// Channel modes :: RFC1459; section 4.2.3.1. +const ( + ModeDefaults = "beI,k,l,imnpst" // the most common default modes + + ModeInviteOnly = "i" // only join with an invite + ModeKey = "k" // channel password + ModeLimit = "l" // user limit + ModeModerated = "m" // only voiced users and operators can talk + ModeOperator = "o" // operator + ModePrivate = "p" // private + ModeSecret = "s" // secret + ModeTopic = "t" // must be op to set topic + ModeVoice = "v" // speak during moderation mode + + ModeOwner = "q" // owner privileges (non-rfc) + ModeAdmin = "a" // admin privileges (non-rfc) + ModeHalfOperator = "h" // half-operator privileges (non-rfc) +) + +// IRC commands :: RFC2812; section 3 :: RFC2813; section 4. +const ( + ADMIN = "ADMIN" + AWAY = "AWAY" + CONNECT = "CONNECT" + DIE = "DIE" + ERROR = "ERROR" + INFO = "INFO" + INVITE = "INVITE" + ISON = "ISON" + JOIN = "JOIN" + KICK = "KICK" + KILL = "KILL" + LINKS = "LINKS" + LIST = "LIST" + LUSERS = "LUSERS" + MODE = "MODE" + MOTD = "MOTD" + NAMES = "NAMES" + NICK = "NICK" + NJOIN = "NJOIN" + NOTICE = "NOTICE" + OPER = "OPER" + PART = "PART" + PASS = "PASS" + PING = "PING" + PONG = "PONG" + PRIVMSG = "PRIVMSG" + QUIT = "QUIT" + REHASH = "REHASH" + RESTART = "RESTART" + SERVER = "SERVER" + SERVICE = "SERVICE" + SERVLIST = "SERVLIST" + SQUERY = "SQUERY" + SQUIT = "SQUIT" + STATS = "STATS" + SUMMON = "SUMMON" + TIME = "TIME" + TOPIC = "TOPIC" + TRACE = "TRACE" + USER = "USER" + USERHOST = "USERHOST" + USERS = "USERS" + VERSION = "VERSION" + WALLOPS = "WALLOPS" + WHO = "WHO" + WHOIS = "WHOIS" + WHOWAS = "WHOWAS" +) + +// Numeric IRC reply mapping :: RFC2812; section 5. +const ( + RPL_WELCOME = "001" + RPL_YOURHOST = "002" + RPL_CREATED = "003" + RPL_MYINFO = "004" + RPL_BOUNCE = "005" + RPL_ISUPPORT = "005" + RPL_USERHOST = "302" + RPL_ISON = "303" + RPL_AWAY = "301" + RPL_UNAWAY = "305" + RPL_NOWAWAY = "306" + RPL_WHOISUSER = "311" + RPL_WHOISSERVER = "312" + RPL_WHOISOPERATOR = "313" + RPL_WHOISIDLE = "317" + RPL_ENDOFWHOIS = "318" + RPL_WHOISCHANNELS = "319" + RPL_WHOWASUSER = "314" + RPL_ENDOFWHOWAS = "369" + RPL_LISTSTART = "321" + RPL_LIST = "322" + RPL_LISTEND = "323" + RPL_UNIQOPIS = "325" + RPL_CHANNELMODEIS = "324" + RPL_NOTOPIC = "331" + RPL_TOPIC = "332" + RPL_INVITING = "341" + RPL_SUMMONING = "342" + RPL_INVITELIST = "346" + RPL_ENDOFINVITELIST = "347" + RPL_EXCEPTLIST = "348" + RPL_ENDOFEXCEPTLIST = "349" + RPL_VERSION = "351" + RPL_WHOREPLY = "352" + RPL_ENDOFWHO = "315" + RPL_NAMREPLY = "353" + RPL_ENDOFNAMES = "366" + RPL_LINKS = "364" + RPL_ENDOFLINKS = "365" + RPL_BANLIST = "367" + RPL_ENDOFBANLIST = "368" + RPL_INFO = "371" + RPL_ENDOFINFO = "374" + RPL_MOTDSTART = "375" + RPL_MOTD = "372" + RPL_ENDOFMOTD = "376" + RPL_YOUREOPER = "381" + RPL_REHASHING = "382" + RPL_YOURESERVICE = "383" + RPL_TIME = "391" + RPL_USERSSTART = "392" + RPL_USERS = "393" + RPL_ENDOFUSERS = "394" + RPL_NOUSERS = "395" + RPL_TRACELINK = "200" + RPL_TRACECONNECTING = "201" + RPL_TRACEHANDSHAKE = "202" + RPL_TRACEUNKNOWN = "203" + RPL_TRACEOPERATOR = "204" + RPL_TRACEUSER = "205" + RPL_TRACESERVER = "206" + RPL_TRACESERVICE = "207" + RPL_TRACENEWTYPE = "208" + RPL_TRACECLASS = "209" + RPL_TRACERECONNECT = "210" + RPL_TRACELOG = "261" + RPL_TRACEEND = "262" + RPL_STATSLINKINFO = "211" + RPL_STATSCOMMANDS = "212" + RPL_ENDOFSTATS = "219" + RPL_STATSUPTIME = "242" + RPL_STATSOLINE = "243" + RPL_UMODEIS = "221" + RPL_SERVLIST = "234" + RPL_SERVLISTEND = "235" + RPL_LUSERCLIENT = "251" + RPL_LUSEROP = "252" + RPL_LUSERUNKNOWN = "253" + RPL_LUSERCHANNELS = "254" + RPL_LUSERME = "255" + RPL_ADMINME = "256" + RPL_ADMINLOC1 = "257" + RPL_ADMINLOC2 = "258" + RPL_ADMINEMAIL = "259" + RPL_TRYAGAIN = "263" + ERR_NOSUCHNICK = "401" + ERR_NOSUCHSERVER = "402" + ERR_NOSUCHCHANNEL = "403" + ERR_CANNOTSENDTOCHAN = "404" + ERR_TOOMANYCHANNELS = "405" + ERR_WASNOSUCHNICK = "406" + ERR_TOOMANYTARGETS = "407" + ERR_NOSUCHSERVICE = "408" + ERR_NOORIGIN = "409" + ERR_NORECIPIENT = "411" + ERR_NOTEXTTOSEND = "412" + ERR_NOTOPLEVEL = "413" + ERR_WILDTOPLEVEL = "414" + ERR_BADMASK = "415" + ERR_UNKNOWNCOMMAND = "421" + ERR_NOMOTD = "422" + ERR_NOADMININFO = "423" + ERR_FILEERROR = "424" + ERR_NONICKNAMEGIVEN = "431" + ERR_ERRONEUSNICKNAME = "432" + ERR_NICKNAMEINUSE = "433" + ERR_NICKCOLLISION = "436" + ERR_UNAVAILRESOURCE = "437" + ERR_USERNOTINCHANNEL = "441" + ERR_NOTONCHANNEL = "442" + ERR_USERONCHANNEL = "443" + ERR_NOLOGIN = "444" + ERR_SUMMONDISABLED = "445" + ERR_USERSDISABLED = "446" + ERR_NOTREGISTERED = "451" + ERR_NEEDMOREPARAMS = "461" + ERR_ALREADYREGISTRED = "462" + ERR_NOPERMFORHOST = "463" + ERR_PASSWDMISMATCH = "464" + ERR_YOUREBANNEDCREEP = "465" + ERR_YOUWILLBEBANNED = "466" + ERR_KEYSET = "467" + ERR_CHANNELISFULL = "471" + ERR_UNKNOWNMODE = "472" + ERR_INVITEONLYCHAN = "473" + ERR_BANNEDFROMCHAN = "474" + ERR_BADCHANNELKEY = "475" + ERR_BADCHANMASK = "476" + ERR_NOCHANMODES = "477" + ERR_BANLISTFULL = "478" + ERR_NOPRIVILEGES = "481" + ERR_CHANOPRIVSNEEDED = "482" + ERR_CANTKILLSERVER = "483" + ERR_RESTRICTED = "484" + ERR_UNIQOPPRIVSNEEDED = "485" + ERR_NOOPERHOST = "491" + ERR_UMODEUNKNOWNFLAG = "501" + ERR_USERSDONTMATCH = "502" +) + +// IRCv3 commands and extensions :: http://ircv3.net/irc/. +const ( + AUTHENTICATE = "AUTHENTICATE" + STARTTLS = "STARTTLS" + + CAP = "CAP" + CAP_ACK = "ACK" + CAP_CLEAR = "CLEAR" + CAP_END = "END" + CAP_LIST = "LIST" + CAP_LS = "LS" + CAP_NAK = "NAK" + CAP_REQ = "REQ" + CAP_NEW = "NEW" + CAP_DEL = "DEL" + + CAP_CHGHOST = "CHGHOST" + CAP_AWAY = "AWAY" + CAP_ACCOUNT = "ACCOUNT" +) + +// Numeric IRC reply mapping for ircv3 :: http://ircv3.net/irc/. +const ( + RPL_LOGGEDIN = "900" + RPL_LOGGEDOUT = "901" + RPL_NICKLOCKED = "902" + RPL_SASLSUCCESS = "903" + ERR_SASLFAIL = "904" + ERR_SASLTOOLONG = "905" + ERR_SASLABORTED = "906" + ERR_SASLALREADY = "907" + RPL_SASLMECHS = "908" + RPL_STARTTLS = "670" + ERR_STARTTLS = "691" +) + +// Numeric IRC event mapping :: RFC2812; section 5.3. +const ( + RPL_STATSCLINE = "213" + RPL_STATSNLINE = "214" + RPL_STATSILINE = "215" + RPL_STATSKLINE = "216" + RPL_STATSQLINE = "217" + RPL_STATSYLINE = "218" + RPL_SERVICEINFO = "231" + RPL_ENDOFSERVICES = "232" + RPL_SERVICE = "233" + RPL_STATSVLINE = "240" + RPL_STATSLLINE = "241" + RPL_STATSHLINE = "244" + RPL_STATSSLINE = "245" + RPL_STATSPING = "246" + RPL_STATSBLINE = "247" + RPL_STATSDLINE = "250" + RPL_NONE = "300" + RPL_WHOISCHANOP = "316" + RPL_KILLDONE = "361" + RPL_CLOSING = "362" + RPL_CLOSEEND = "363" + RPL_INFOSTART = "373" + RPL_MYPORTIS = "384" + ERR_NOSERVICEHOST = "492" +) + +// Misc. +const ( + ERR_TOOMANYMATCHES = "416" // IRCNet. + RPL_GLOBALUSERS = "266" // aircd/hybrid/bahamut, used on freenode. + RPL_LOCALUSERS = "265" // aircd/hybrid/bahamut, used on freenode. + RPL_TOPICWHOTIME = "333" // ircu, used on freenode. + RPL_WHOSPCRPL = "354" // ircu, used on networks with WHOX support. +) |