diff --git a/vendor/github.com/lrstanley/girc/builtin.go b/vendor/github.com/lrstanley/girc/builtin.go index e7ccc199..12257ca3 100644 --- a/vendor/github.com/lrstanley/girc/builtin.go +++ b/vendor/github.com/lrstanley/girc/builtin.go @@ -113,7 +113,9 @@ func handlePING(c *Client, e Event) { } func handlePONG(c *Client, e Event) { + c.conn.mu.Lock() c.conn.lastPong = time.Now() + c.conn.mu.Unlock() } // handleJOIN ensures that the state has updated users and channels. diff --git a/vendor/github.com/lrstanley/girc/cap.go b/vendor/github.com/lrstanley/girc/cap.go index 1d50460f..e7037f9f 100644 --- a/vendor/github.com/lrstanley/girc/cap.go +++ b/vendor/github.com/lrstanley/girc/cap.go @@ -5,14 +5,11 @@ package girc import ( - "bytes" - "encoding/base64" - "fmt" - "io" - "sort" "strings" ) +// Something not in the list? Depending on the type of capability, you can +// enable it using Config.SupportedCaps. var possibleCap = map[string][]string{ "account-notify": nil, "account-tag": nil, @@ -22,11 +19,25 @@ var possibleCap = map[string][]string{ "chghost": nil, "extended-join": nil, "invite-notify": nil, - "message-tags": nil, "multi-prefix": nil, + "server-time": nil, "userhost-in-names": nil, + + "draft/message-tags-0.2": nil, + "draft/msgid": nil, + + // "echo-message" is supported, but it's not enabled by default. This is + // to prevent unwanted confusion and utilize less traffic if it's not needed. + // echo messages aren't sent to girc.PRIVMSG and girc.NOTICE handlers, + // rather they are only sent to girc.ALL_EVENTS handlers (this is to prevent + // each handler to have to check these types of things for each message). + // You can compare events using Event.Equals() to see if they are the same. } +// https://ircv3.net/specs/extensions/server-time-3.2.html +// ::= YYYY-MM-DDThh:mm:ss.sssZ +const capServerTimeFormat = "2006-01-02T15:04:05.999Z" + func (c *Client) listCAP() { if !c.Config.disableTracking { c.write(&Event{Command: CAP, Params: []string{CAP_LS, "302"}}) @@ -74,8 +85,8 @@ func parseCap(raw string) map[string][]string { } // handleCAP attempts to find out what IRCv3 capabilities the server supports. -// This will lock further registration until we have acknowledged the -// capabilities. +// This will lock further registration until we have acknowledged (or denied) +// the capabilities. func handleCAP(c *Client, e Event) { if len(e.Params) >= 2 && (e.Params[1] == CAP_NEW || e.Params[1] == CAP_DEL) { c.listCAP() @@ -172,133 +183,6 @@ func handleCAP(c *Client, e Event) { } } -// 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: invalid %s SASL configuration provided: %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} -} - // handleCHGHOST handles incoming IRCv3 hostname change events. CHGHOST is // what occurs (when enabled) when a servers services change the hostname of // a user. Traditionally, this was simply resolved with a quick QUIT and JOIN, @@ -352,288 +236,3 @@ func handleACCOUNT(c *Client, e Event) { c.state.Unlock() c.state.notify(c, UPDATE_STATE) } - -// 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()) -} - -// 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/client.go b/vendor/github.com/lrstanley/girc/client.go index 501554b9..4f823e16 100644 --- a/vendor/github.com/lrstanley/girc/client.go +++ b/vendor/github.com/lrstanley/girc/client.go @@ -14,6 +14,7 @@ import ( "log" "runtime" "sort" + "strings" "sync" "time" ) @@ -173,8 +174,8 @@ func (conf *Config) isValid() error { conf.Port = 6667 } - if conf.Port < 21 || conf.Port > 65535 { - return &ErrInvalidConfig{Conf: *conf, err: errors.New("port outside valid range (21-65535)")} + if conf.Port < 1 || conf.Port > 65535 { + return &ErrInvalidConfig{Conf: *conf, err: errors.New("port outside valid range (1-65535)")} } if !IsValidNick(conf.Nick) { @@ -432,7 +433,6 @@ func (c *Client) GetNick() string { if c.state.nick == "" { return c.Config.Nick } - return c.state.nick } @@ -448,140 +448,124 @@ func (c *Client) GetIdent() string { if c.state.ident == "" { return c.Config.User } - return c.state.ident } // GetHost returns the current host of the active connection. Panics if // tracking is disabled. May be empty, as this is obtained from when we join // a channel, as there is no other more efficient method to return this info. -func (c *Client) GetHost() string { +func (c *Client) GetHost() (host string) { c.panicIfNotTracking() c.state.RLock() - defer c.state.RUnlock() - - return c.state.host + host = c.state.host + c.state.RUnlock() + return host } -// ChannelList returns the active list of channel names that the client is in. -// Panics if tracking is disabled. +// ChannelList returns the (sorted) active list of channel names that the client +// is in. Panics if tracking is disabled. func (c *Client) ChannelList() []string { c.panicIfNotTracking() c.state.RLock() - channels := make([]string, len(c.state.channels)) - var i int + channels := make([]string, 0, len(c.state.channels)) for channel := range c.state.channels { - channels[i] = c.state.channels[channel].Name - i++ + channels = append(channels, c.state.channels[channel].Name) } c.state.RUnlock() sort.Strings(channels) - return channels } -// Channels returns the active channels that the client is in. Panics if -// tracking is disabled. +// Channels returns the (sorted) active channels that the client is in. Panics +// if tracking is disabled. func (c *Client) Channels() []*Channel { c.panicIfNotTracking() c.state.RLock() - channels := make([]*Channel, len(c.state.channels)) - var i int + channels := make([]*Channel, 0, len(c.state.channels)) for channel := range c.state.channels { - channels[i] = c.state.channels[channel].Copy() - i++ + channels = append(channels, c.state.channels[channel].Copy()) } c.state.RUnlock() + sort.Slice(channels, func(i, j int) bool { + return channels[i].Name < channels[j].Name + }) return channels } -// UserList returns the active list of nicknames that the client is tracking -// across all networks. Panics if tracking is disabled. +// UserList returns the (sorted) active list of nicknames that the client is +// tracking across all channels. Panics if tracking is disabled. func (c *Client) UserList() []string { c.panicIfNotTracking() c.state.RLock() - users := make([]string, len(c.state.users)) - var i int + users := make([]string, 0, len(c.state.users)) for user := range c.state.users { - users[i] = c.state.users[user].Nick - i++ + users = append(users, c.state.users[user].Nick) } c.state.RUnlock() sort.Strings(users) - return users } -// Users returns the active users that the client is tracking across all -// networks. Panics if tracking is disabled. +// Users returns the (sorted) active users that the client is tracking across +// all channels. Panics if tracking is disabled. func (c *Client) Users() []*User { c.panicIfNotTracking() c.state.RLock() - users := make([]*User, len(c.state.users)) - var i int + users := make([]*User, 0, len(c.state.users)) for user := range c.state.users { - users[i] = c.state.users[user].Copy() - i++ + users = append(users, c.state.users[user].Copy()) } c.state.RUnlock() + sort.Slice(users, func(i, j int) bool { + return users[i].Nick < users[j].Nick + }) return users } // LookupChannel looks up a given channel in state. If the channel doesn't // exist, nil is returned. Panics if tracking is disabled. -func (c *Client) LookupChannel(name string) *Channel { +func (c *Client) LookupChannel(name string) (channel *Channel) { c.panicIfNotTracking() if name == "" { return nil } c.state.RLock() - defer c.state.RUnlock() - - channel := c.state.lookupChannel(name) - if channel == nil { - return nil - } - - return channel.Copy() + channel = c.state.lookupChannel(name).Copy() + c.state.RUnlock() + return channel } // LookupUser looks up a given user in state. If the user doesn't exist, nil // is returned. Panics if tracking is disabled. -func (c *Client) LookupUser(nick string) *User { +func (c *Client) LookupUser(nick string) (user *User) { c.panicIfNotTracking() if nick == "" { return nil } c.state.RLock() - defer c.state.RUnlock() - - user := c.state.lookupUser(nick) - if user == nil { - return nil - } - - return user.Copy() + user = c.state.lookupUser(nick).Copy() + c.state.RUnlock() + return user } // IsInChannel returns true if the client is in channel. Panics if tracking // is disabled. -func (c *Client) IsInChannel(channel string) bool { +func (c *Client) IsInChannel(channel string) (in bool) { c.panicIfNotTracking() c.state.RLock() - _, inChannel := c.state.channels[ToRFC1459(channel)] + _, in = c.state.channels[ToRFC1459(channel)] c.state.RUnlock() - - return inChannel + return in } // GetServerOption retrieves a server capability setting that was retrieved @@ -596,7 +580,6 @@ func (c *Client) GetServerOption(key string) (result string, ok bool) { c.state.RLock() result, ok = c.state.serverOptions[key] c.state.RUnlock() - return result, ok } @@ -607,7 +590,6 @@ func (c *Client) NetworkName() (name string) { c.panicIfNotTracking() name, _ = c.GetServerOption("NETWORK") - return name } @@ -615,33 +597,31 @@ func (c *Client) NetworkName() (name string) { // supplied this information during connection. May be empty if the server // does not support RPL_MYINFO. Will panic if used when tracking has been // disabled. -func (c *Client) ServerVersion() string { +func (c *Client) ServerVersion() (version string) { c.panicIfNotTracking() - version, _ := c.GetServerOption("VERSION") - + version, _ = c.GetServerOption("VERSION") return version } // ServerMOTD returns the servers message of the day, if the server has sent // it upon connect. Will panic if used when tracking has been disabled. -func (c *Client) ServerMOTD() string { +func (c *Client) ServerMOTD() (motd string) { c.panicIfNotTracking() c.state.RLock() - motd := c.state.motd + motd = c.state.motd c.state.RUnlock() - return motd } // Latency is the latency between the server and the client. This is measured // by determining the difference in time between when we ping the server, and // when we receive a pong. -func (c *Client) Latency() time.Duration { +func (c *Client) Latency() (delta time.Duration) { c.mu.RLock() c.conn.mu.RLock() - delta := c.conn.lastPong.Sub(c.conn.lastPing) + delta = c.conn.lastPong.Sub(c.conn.lastPing) c.conn.mu.RUnlock() c.mu.RUnlock() @@ -652,6 +632,30 @@ func (c *Client) Latency() time.Duration { return delta } +// HasCapability checks if the client connection has the given capability. If +// you want the full list of capabilities, listen for the girc.CAP_ACK event. +// Will panic if used when tracking has been disabled. +func (c *Client) HasCapability(name string) (has bool) { + c.panicIfNotTracking() + + if !c.IsConnected() { + return false + } + + name = strings.ToLower(name) + + c.state.RLock() + for i := 0; i < len(c.state.enabledCap); i++ { + if strings.ToLower(c.state.enabledCap[i]) == name { + has = true + break + } + } + c.state.RUnlock() + + return has +} + // panicIfNotTracking will throw a panic when it's called, and tracking is // disabled. Adds useful info like what function specifically, and where it // was called from. diff --git a/vendor/github.com/lrstanley/girc/conn.go b/vendor/github.com/lrstanley/girc/conn.go index a46a5dd7..77d87988 100644 --- a/vendor/github.com/lrstanley/girc/conn.go +++ b/vendor/github.com/lrstanley/girc/conn.go @@ -371,6 +371,12 @@ func (c *Client) readLoop(ctx context.Context, errs chan error, wg *sync.WaitGro return } + // Check if it's an echo-message. + if !c.Config.disableTracking { + event.Echo = (event.Command == PRIVMSG || event.Command == NOTICE) && + event.Source != nil && event.Source.Name == c.GetNick() + } + c.rx <- event } } @@ -500,7 +506,7 @@ type ErrTimedOut struct { Delay time.Duration } -func (ErrTimedOut) Error() string { return "timed out during ping to server" } +func (ErrTimedOut) Error() string { return "timed out waiting for a requested PING response" } func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) { // Don't run the pingLoop if they want to disable it. diff --git a/vendor/github.com/lrstanley/girc/contants.go b/vendor/github.com/lrstanley/girc/contants.go deleted file mode 100644 index 4d3c65bc..00000000 --- a/vendor/github.com/lrstanley/girc/contants.go +++ /dev/null @@ -1,338 +0,0 @@ -// Copyright (c) Liam Stanley . 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. -) diff --git a/vendor/github.com/lrstanley/girc/event.go b/vendor/github.com/lrstanley/girc/event.go index ef7209ed..20182340 100644 --- a/vendor/github.com/lrstanley/girc/event.go +++ b/vendor/github.com/lrstanley/girc/event.go @@ -8,6 +8,7 @@ import ( "bytes" "fmt" "strings" + "time" ) const ( @@ -33,18 +34,35 @@ func cutCRFunc(r rune) bool { // CR or LF> // :: CR LF type Event struct { - Source *Source `json:"source"` // The source of the event. - Tags Tags `json:"tags"` // IRCv3 style message tags. Only use if network supported. - Command string `json:"command"` // the IRC command, e.g. JOIN, PRIVMSG, KILL. - Params []string `json:"params"` // parameters to the command. Commonly nickname, channel, etc. - Trailing string `json:"trailing"` // any trailing data. e.g. with a PRIVMSG, this is the message text. - EmptyTrailing bool `json:"empty_trailing"` // if true, trailing prefix (:) will be added even if Event.Trailing is empty. - Sensitive bool `json:"sensitive"` // if the message is sensitive (e.g. and should not be logged). + // Source is the origin of the event. + Source *Source `json:"source"` + // Tags are the IRCv3 style message tags for the given event. Only use + // if network supported. + Tags Tags `json:"tags"` + // Timestamp is the time the event was received. This could optionally be + // used for client-stored sent messages too. If the server supports the + // "server-time" capability, this is synced to the UTC time that the server + // specifies. + Timestamp time.Time `json:"timestamp"` + // Command that represents the event, e.g. JOIN, PRIVMSG, KILL. + Command string `json:"command"` + // Params (parameters/args) to the command. Commonly nickname, channel, etc. + Params []string `json:"params"` + // Trailing text. e.g. with a PRIVMSG, this is the message text (part + // after the colon.) + Trailing string `json:"trailing"` + // EmptyTrailign, if true, the text prefix (:) will be added even if + // Event.Trailing is empty. + EmptyTrailing bool `json:"empty_trailing"` + // Sensitive should be true if the message is sensitive (e.g. and should + // not be logged/shown in debugging output). + Sensitive bool `json:"sensitive"` + // If the event is an echo-message response. + Echo bool `json:"echo"` } -// ParseEvent takes a string and attempts to create a Event struct. -// -// Returns nil if the Event is invalid. +// ParseEvent takes a string and attempts to create a Event struct. Returns +// nil if the Event is invalid. func ParseEvent(raw string) (e *Event) { // Ignore empty events. if raw = strings.TrimFunc(raw, cutCRFunc); len(raw) < 2 { @@ -52,7 +70,7 @@ func ParseEvent(raw string) (e *Event) { } var i, j int - e = &Event{} + e = &Event{Timestamp: time.Now()} if raw[0] == prefixTag { // Tags end with a space. @@ -63,6 +81,13 @@ func ParseEvent(raw string) (e *Event) { } e.Tags = ParseTags(raw[1:i]) + if rawServerTime, ok := e.Tags.Get("time"); ok { + // Attempt to parse server-time. If we can't parse it, we just + // fall back to the time we received the message (locally.) + if stime, err := time.Parse(capServerTimeFormat, rawServerTime); err == nil { + e.Timestamp = stime.Local() + } + } raw = raw[i+1:] } @@ -151,10 +176,12 @@ func (e *Event) Copy() *Event { } newEvent := &Event{ + Timestamp: e.Timestamp, Command: e.Command, Trailing: e.Trailing, EmptyTrailing: e.EmptyTrailing, Sensitive: e.Sensitive, + Echo: e.Echo, } // Copy Source field, as it's a pointer and needs to be dereferenced. @@ -179,6 +206,25 @@ func (e *Event) Copy() *Event { return newEvent } +// Equals compares two Events for equality. +func (e *Event) Equals(ev *Event) bool { + if e.Command != ev.Command || e.Trailing != ev.Trailing || len(e.Params) != len(ev.Params) { + return false + } + + for i := 0; i < len(e.Params); i++ { + if e.Params[i] != ev.Params[i] { + return false + } + } + + if !e.Source.Equals(ev.Source) || !e.Tags.Equals(ev.Tags) { + return false + } + + return true +} + // Len calculates the length of the string representation of event. Note that // this will return the true length (even if longer than what IRC supports), // which may be useful if you are trying to check and see if a message is @@ -276,7 +322,7 @@ func (e *Event) String() string { // an event prettier, but also to filter out events that most don't visually // see in normal IRC clients. e.g. most clients don't show WHO queries. func (e *Event) Pretty() (out string, ok bool) { - if e.Sensitive { + if e.Sensitive || e.Echo { return "", false } @@ -377,6 +423,10 @@ func (e *Event) Pretty() (out string, ok bool) { return fmt.Sprintf("[*] topic for %s is: %s", e.Params[len(e.Params)-1], e.Trailing), true } + if e.Command == CAP && len(e.Params) == 2 && len(e.Trailing) > 1 && e.Params[1] == CAP_ACK { + return "[*] enabling capabilities: " + e.Trailing, true + } + return "", false } @@ -449,6 +499,20 @@ type Source struct { Host string `json:"host"` } +// Equals compares two Sources for equality. +func (s *Source) Equals(ss *Source) bool { + if s == nil && ss == nil { + return true + } + if s != nil && ss == nil || s == nil && ss != nil { + return false + } + if s.Name != ss.Name || s.Ident != ss.Ident || s.Host != ss.Host { + return false + } + return true +} + // Copy returns a deep copy of Source. func (s *Source) Copy() *Source { if s == nil { diff --git a/vendor/github.com/lrstanley/girc/format.go b/vendor/github.com/lrstanley/girc/format.go index 78a1f7bb..b974c3bd 100644 --- a/vendor/github.com/lrstanley/girc/format.go +++ b/vendor/github.com/lrstanley/girc/format.go @@ -136,7 +136,7 @@ func TrimFmt(text string) string { return text } -// This is really the only fastest way of doing this (marginably better than +// This is really the only fastest way of doing this (marginally better than // actually trying to parse it manually.) var reStripColor = regexp.MustCompile(`\x03([019]?[0-9](,[019]?[0-9])?)?`) @@ -154,7 +154,7 @@ func StripRaw(text string) string { return text } -// IsValidChannel validates if channel is an RFC complaint channel or not. +// IsValidChannel validates if channel is an RFC compliant channel or not. // // NOTE: If you are using this to validate a channel that contains a channel // ID, (!NAME), this only supports the standard 5 character length. @@ -271,7 +271,7 @@ func IsValidUser(name string) bool { } // Check to see if the first index is alphanumeric. - if (name[0] < 'A' || name[0] > 'J') && (name[0] < 'a' || name[0] > 'z') && (name[0] < '0' || name[0] > '9') { + if (name[0] < 'A' || name[0] > 'Z') && (name[0] < 'a' || name[0] > 'z') && (name[0] < '0' || name[0] > '9') { return false } diff --git a/vendor/github.com/lrstanley/girc/handler.go b/vendor/github.com/lrstanley/girc/handler.go index 6c082708..bde08976 100644 --- a/vendor/github.com/lrstanley/girc/handler.go +++ b/vendor/github.com/lrstanley/girc/handler.go @@ -22,19 +22,28 @@ func (c *Client) RunHandlers(event *Event) { } // Log the event. - c.debug.Print("< " + StripRaw(event.String())) + prefix := "< " + if event.Echo { + prefix += "[echo-message] " + } + c.debug.Print(prefix + StripRaw(event.String())) if c.Config.Out != nil { if pretty, ok := event.Pretty(); ok { fmt.Fprintln(c.Config.Out, StripRaw(pretty)) } } - // Background handlers first. + // Background handlers first. If the event is an echo-message, then only + // send the echo version to ALL_EVENTS. c.Handlers.exec(ALL_EVENTS, true, c, event.Copy()) - c.Handlers.exec(event.Command, true, c, event.Copy()) + if !event.Echo { + c.Handlers.exec(event.Command, true, c, event.Copy()) + } c.Handlers.exec(ALL_EVENTS, false, c, event.Copy()) - c.Handlers.exec(event.Command, false, c, event.Copy()) + if !event.Echo { + c.Handlers.exec(event.Command, false, c, event.Copy()) + } // Check if it's a CTCP. if ctcp := decodeCTCP(event.Copy()); ctcp != nil { diff --git a/vendor/github.com/lrstanley/girc/state.go b/vendor/github.com/lrstanley/girc/state.go index 7c537028..36dcc82b 100644 --- a/vendor/github.com/lrstanley/girc/state.go +++ b/vendor/github.com/lrstanley/girc/state.go @@ -132,6 +132,10 @@ func (u User) Channels(c *Client) []*Channel { // Copy returns a deep copy of the user which can be modified without making // changes to the actual state. func (u *User) Copy() *User { + if u == nil { + return nil + } + nu := &User{} *nu = *u @@ -148,7 +152,7 @@ func (u *User) addChannel(name string) { } u.ChannelList = append(u.ChannelList, ToRFC1459(name)) - sort.StringsAreSorted(u.ChannelList) + sort.Strings(u.ChannelList) u.Perms.set(name, Perms{}) } @@ -321,6 +325,10 @@ func (ch *Channel) deleteUser(nick string) { // Copy returns a deep copy of a given channel. func (ch *Channel) Copy() *Channel { + if ch == nil { + return nil + } + nc := &Channel{} *nc = *ch @@ -483,6 +491,9 @@ func (s *state) renameUser(from, to string) { for j := 0; j < len(s.channels[user.ChannelList[i]].UserList); j++ { if s.channels[user.ChannelList[i]].UserList[j] == from { s.channels[user.ChannelList[i]].UserList[j] = ToRFC1459(to) + + sort.Strings(s.channels[user.ChannelList[i]].UserList) + break } } } diff --git a/vendor/manifest b/vendor/manifest index e5c6d7a5..500f8b12 100644 --- a/vendor/manifest +++ b/vendor/manifest @@ -369,7 +369,7 @@ "importpath": "github.com/lrstanley/girc", "repository": "https://github.com/lrstanley/girc", "vcs": "git", - "revision": "5dff93b5453c1b2ac8382c9a38881635f47bba0e", + "revision": "102f17f86306c2152a8c6188f9bb8b0e7288de31", "branch": "master", "notests": true },