diff options
-rw-r--r-- | vendor/github.com/lrstanley/girc/LICENSE | 21 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/builtin.go | 518 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/cap.go | 639 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/client.go | 615 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/cmdhandler/cmd.go | 197 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/commands.go | 398 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/conn.go | 566 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/contants.go | 338 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/ctcp.go | 288 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/doc.go | 12 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/event.go | 550 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/format.go | 350 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/handler.go | 484 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/modes.go | 550 | ||||
-rw-r--r-- | vendor/github.com/lrstanley/girc/state.go | 489 | ||||
-rw-r--r-- | vendor/manifest | 8 |
16 files changed, 6023 insertions, 0 deletions
diff --git a/vendor/github.com/lrstanley/girc/LICENSE b/vendor/github.com/lrstanley/girc/LICENSE new file mode 100644 index 00000000..073d8c00 --- /dev/null +++ b/vendor/github.com/lrstanley/girc/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Liam Stanley <me@liamstanley.io> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/lrstanley/girc/builtin.go b/vendor/github.com/lrstanley/girc/builtin.go new file mode 100644 index 00000000..aecb1e11 --- /dev/null +++ b/vendor/github.com/lrstanley/girc/builtin.go @@ -0,0 +1,518 @@ +// 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 ( + "strings" + "time" +) + +// registerBuiltin sets up built-in handlers, based on client +// configuration. +func (c *Client) registerBuiltins() { + c.debug.Print("registering built-in handlers") + c.Handlers.mu.Lock() + + // Built-in things that should always be supported. + c.Handlers.register(true, RPL_WELCOME, HandlerFunc(func(c *Client, e Event) { + go handleConnect(c, e) + })) + c.Handlers.register(true, PING, HandlerFunc(handlePING)) + c.Handlers.register(true, PONG, HandlerFunc(handlePONG)) + + if !c.Config.disableTracking { + // Joins/parts/anything that may add/remove/rename users. + c.Handlers.register(true, JOIN, HandlerFunc(handleJOIN)) + c.Handlers.register(true, PART, HandlerFunc(handlePART)) + c.Handlers.register(true, KICK, HandlerFunc(handleKICK)) + c.Handlers.register(true, QUIT, HandlerFunc(handleQUIT)) + c.Handlers.register(true, NICK, HandlerFunc(handleNICK)) + c.Handlers.register(true, RPL_NAMREPLY, HandlerFunc(handleNAMES)) + + // Modes. + c.Handlers.register(true, MODE, HandlerFunc(handleMODE)) + c.Handlers.register(true, RPL_CHANNELMODEIS, HandlerFunc(handleMODE)) + + // WHO/WHOX responses. + c.Handlers.register(true, RPL_WHOREPLY, HandlerFunc(handleWHO)) + c.Handlers.register(true, RPL_WHOSPCRPL, HandlerFunc(handleWHO)) + + // Other misc. useful stuff. + c.Handlers.register(true, TOPIC, HandlerFunc(handleTOPIC)) + c.Handlers.register(true, RPL_TOPIC, HandlerFunc(handleTOPIC)) + c.Handlers.register(true, RPL_MYINFO, HandlerFunc(handleMYINFO)) + c.Handlers.register(true, RPL_ISUPPORT, HandlerFunc(handleISUPPORT)) + c.Handlers.register(true, RPL_MOTDSTART, HandlerFunc(handleMOTD)) + c.Handlers.register(true, RPL_MOTD, HandlerFunc(handleMOTD)) + + // Keep users lastactive times up to date. + c.Handlers.register(true, PRIVMSG, HandlerFunc(updateLastActive)) + c.Handlers.register(true, NOTICE, HandlerFunc(updateLastActive)) + c.Handlers.register(true, TOPIC, HandlerFunc(updateLastActive)) + c.Handlers.register(true, KICK, HandlerFunc(updateLastActive)) + + // CAP IRCv3-specific tracking and functionality. + c.Handlers.register(true, CAP, HandlerFunc(handleCAP)) + c.Handlers.register(true, CAP_CHGHOST, HandlerFunc(handleCHGHOST)) + c.Handlers.register(true, CAP_AWAY, HandlerFunc(handleAWAY)) + c.Handlers.register(true, CAP_ACCOUNT, HandlerFunc(handleACCOUNT)) + c.Handlers.register(true, ALL_EVENTS, HandlerFunc(handleTags)) + + // SASL IRCv3 support. + c.Handlers.register(true, AUTHENTICATE, HandlerFunc(handleSASL)) + c.Handlers.register(true, RPL_SASLSUCCESS, HandlerFunc(handleSASL)) + c.Handlers.register(true, RPL_NICKLOCKED, HandlerFunc(handleSASLError)) + c.Handlers.register(true, ERR_SASLFAIL, HandlerFunc(handleSASLError)) + c.Handlers.register(true, ERR_SASLTOOLONG, HandlerFunc(handleSASLError)) + c.Handlers.register(true, ERR_SASLABORTED, HandlerFunc(handleSASLError)) + c.Handlers.register(true, RPL_SASLMECHS, HandlerFunc(handleSASLError)) + } + + // Nickname collisions. + c.Handlers.register(true, ERR_NICKNAMEINUSE, HandlerFunc(nickCollisionHandler)) + c.Handlers.register(true, ERR_NICKCOLLISION, HandlerFunc(nickCollisionHandler)) + c.Handlers.register(true, ERR_UNAVAILRESOURCE, HandlerFunc(nickCollisionHandler)) + + c.Handlers.mu.Unlock() +} + +// handleConnect is a helper function which lets the client know that enough +// time has passed and now they can send commands. +// +// Should always run in separate thread due to blocking delay. +func handleConnect(c *Client, e Event) { + // This should be the nick that the server gives us. 99% of the time, it's + // the one we supplied during connection, but some networks will rename + // users on connect. + if len(e.Params) > 0 { + c.state.Lock() + c.state.nick = e.Params[0] + c.state.Unlock() + + c.state.notify(c, UPDATE_GENERAL) + } + + time.Sleep(2 * time.Second) + c.RunHandlers(&Event{Command: CONNECTED, Trailing: c.Server()}) +} + +// nickCollisionHandler helps prevent the client from having conflicting +// nicknames with another bot, user, etc. +func nickCollisionHandler(c *Client, e Event) { + if c.Config.HandleNickCollide == nil { + c.Cmd.Nick(c.GetNick() + "_") + return + } + + c.Cmd.Nick(c.Config.HandleNickCollide(c.GetNick())) +} + +// handlePING helps respond to ping requests from the server. +func handlePING(c *Client, e Event) { + c.Cmd.Pong(e.Trailing) +} + +func handlePONG(c *Client, e Event) { + c.conn.lastPong = time.Now() +} + +// handleJOIN ensures that the state has updated users and channels. +func handleJOIN(c *Client, e Event) { + if e.Source == nil { + return + } + + var channelName string + if len(e.Params) > 0 { + channelName = e.Params[0] + } else { + channelName = e.Trailing + } + + c.state.Lock() + + channel := c.state.lookupChannel(channelName) + if channel == nil { + if ok := c.state.createChannel(channelName); !ok { + c.state.Unlock() + return + } + + channel = c.state.lookupChannel(channelName) + } + + user := c.state.lookupUser(e.Source.Name) + if user == nil { + if ok := c.state.createUser(e.Source.Name); !ok { + c.state.Unlock() + return + } + user = c.state.lookupUser(e.Source.Name) + } + + defer c.state.notify(c, UPDATE_STATE) + + channel.addUser(user.Nick) + user.addChannel(channel.Name) + + // Assume extended-join (ircv3). + if len(e.Params) == 2 { + if e.Params[1] != "*" { + user.Extras.Account = e.Params[1] + } + + if len(e.Trailing) > 0 { + user.Extras.Name = e.Trailing + } + } + c.state.Unlock() + + if e.Source.Name == c.GetNick() { + // If it's us, don't just add our user to the list. Run a WHO which + // will tell us who exactly is in the entire channel. + c.Send(&Event{Command: WHO, Params: []string{channelName, "%tacuhnr,1"}}) + + // Also send a MODE to obtain the list of channel modes. + c.Send(&Event{Command: MODE, Params: []string{channelName}}) + + // Update our ident and host too, in state -- since there is no + // cleaner method to do this. + c.state.Lock() + c.state.ident = e.Source.Ident + c.state.host = e.Source.Host + c.state.Unlock() + return + } + + // Only WHO the user, which is more efficient. + c.Send(&Event{Command: WHO, Params: []string{e.Source.Name, "%tacuhnr,1"}}) +} + +// handlePART ensures that the state is clean of old user and channel entries. +func handlePART(c *Client, e Event) { + if e.Source == nil { + return + } + + var channel string + if len(e.Params) > 0 { + channel = e.Params[0] + } else { + channel = e.Trailing + } + + if channel == "" { + return + } + + defer c.state.notify(c, UPDATE_STATE) + + if e.Source.Name == c.GetNick() { + c.state.Lock() + c.state.deleteChannel(channel) + c.state.Unlock() + return + } + + c.state.Lock() + c.state.deleteUser(channel, e.Source.Name) + c.state.Unlock() +} + +// handleTOPIC handles incoming TOPIC events and keeps channel tracking info +// updated with the latest channel topic. +func handleTOPIC(c *Client, e Event) { + var name string + switch len(e.Params) { + case 0: + return + case 1: + name = e.Params[0] + default: + name = e.Params[len(e.Params)-1] + } + + c.state.Lock() + channel := c.state.lookupChannel(name) + if channel == nil { + c.state.Unlock() + return + } + + channel.Topic = e.Trailing + c.state.Unlock() + c.state.notify(c, UPDATE_STATE) +} + +// handlWHO updates our internal tracking of users/channels with WHO/WHOX +// information. +func handleWHO(c *Client, e Event) { + var ident, host, nick, account, realname string + + // Assume WHOX related. + if e.Command == RPL_WHOSPCRPL { + if len(e.Params) != 7 { + // Assume there was some form of error or invalid WHOX response. + return + } + + if e.Params[1] != "1" { + // We should always be sending 1, and we should receive 1. If this + // is anything but, then we didn't send the request and we can + // ignore it. + return + } + + ident, host, nick, account = e.Params[3], e.Params[4], e.Params[5], e.Params[6] + realname = e.Trailing + } else { + // Assume RPL_WHOREPLY. + ident, host, nick = e.Params[2], e.Params[3], e.Params[5] + if len(e.Trailing) > 2 { + realname = e.Trailing[2:] + } + } + + c.state.Lock() + user := c.state.lookupUser(nick) + if user == nil { + c.state.Unlock() + return + } + + user.Host = host + user.Ident = ident + user.Extras.Name = realname + + if account != "0" { + user.Extras.Account = account + } + + c.state.Unlock() + c.state.notify(c, UPDATE_STATE) +} + +// handleKICK ensures that users are cleaned up after being kicked from the +// channel +func handleKICK(c *Client, e Event) { + if len(e.Params) < 2 { + // Needs at least channel and user. + return + } + + defer c.state.notify(c, UPDATE_STATE) + + if e.Params[1] == c.GetNick() { + c.state.Lock() + c.state.deleteChannel(e.Params[0]) + c.state.Unlock() + return + } + + // Assume it's just another user. + c.state.Lock() + c.state.deleteUser(e.Params[0], e.Params[1]) + c.state.Unlock() +} + +// handleNICK ensures that users are renamed in state, or the client name is +// up to date. +func handleNICK(c *Client, e Event) { + if e.Source == nil { + return + } + + c.state.Lock() + // renameUser updates the LastActive time automatically. + if len(e.Params) == 1 { + c.state.renameUser(e.Source.Name, e.Params[0]) + } else if len(e.Trailing) > 0 { + c.state.renameUser(e.Source.Name, e.Trailing) + } + c.state.Unlock() + c.state.notify(c, UPDATE_STATE) +} + +// handleQUIT handles users that are quitting from the network. +func handleQUIT(c *Client, e Event) { + if e.Source == nil { + return + } + + if e.Source.Name == c.GetNick() { + return + } + + c.state.Lock() + c.state.deleteUser("", e.Source.Name) + c.state.Unlock() + c.state.notify(c, UPDATE_STATE) +} + +// handleMYINFO handles incoming MYINFO events -- these are commonly used +// to tell us what the server name is, what version of software is being used +// as well as what channel and user modes are being used on the server. +func handleMYINFO(c *Client, e Event) { + // Malformed or odd output. As this can differ strongly between networks, + // just skip it. + if len(e.Params) < 3 { + return + } + + c.state.Lock() + c.state.serverOptions["SERVER"] = e.Params[1] + c.state.serverOptions["VERSION"] = e.Params[2] + c.state.Unlock() + c.state.notify(c, UPDATE_GENERAL) +} + +// handleISUPPORT handles incoming RPL_ISUPPORT (also known as RPL_PROTOCTL) +// events. These commonly contain the server capabilities and limitations. +// For example, things like max channel name length, or nickname length. +func handleISUPPORT(c *Client, e Event) { + // Must be a ISUPPORT-based message. 005 is also used for server bounce + // related things, so this handler may be triggered during other + // situations. + + // Also known as RPL_PROTOCTL. + if !strings.HasSuffix(e.Trailing, "this server") { + return + } + + // Must have at least one configuration. + if len(e.Params) < 2 { + return + } + + c.state.Lock() + // Skip the first parameter, as it's our nickname. + for i := 1; i < len(e.Params); i++ { + j := strings.IndexByte(e.Params[i], 0x3D) // = + + if j < 1 || (j+1) == len(e.Params[i]) { + c.state.serverOptions[e.Params[i]] = "" + continue + } + + name := e.Params[i][0:j] + val := e.Params[i][j+1:] + c.state.serverOptions[name] = val + } + c.state.Unlock() + c.state.notify(c, UPDATE_GENERAL) +} + +// handleMOTD handles incoming MOTD messages and buffers them up for use with +// Client.ServerMOTD(). +func handleMOTD(c *Client, e Event) { + c.state.Lock() + + defer c.state.notify(c, UPDATE_GENERAL) + + // Beginning of the MOTD. + if e.Command == RPL_MOTDSTART { + c.state.motd = "" + + c.state.Unlock() + return + } + + // Otherwise, assume we're getting sent the MOTD line-by-line. + if len(c.state.motd) != 0 { + e.Trailing = "\n" + e.Trailing + } + + c.state.motd += e.Trailing + c.state.Unlock() +} + +// handleNAMES handles incoming NAMES queries, of which lists all users in +// a given channel. Optionally also obtains ident/host values, as well as +// permissions for each user, depending on what capabilities are enabled. +func handleNAMES(c *Client, e Event) { + if len(e.Params) < 1 { + return + } + + channel := c.state.lookupChannel(e.Params[len(e.Params)-1]) + if channel == nil { + return + } + + parts := strings.Split(e.Trailing, " ") + + var host, ident, modes, nick string + var ok bool + + c.state.Lock() + for i := 0; i < len(parts); i++ { + modes, nick, ok = parseUserPrefix(parts[i]) + if !ok { + continue + } + + // If userhost-in-names. + if strings.Contains(nick, "@") { + s := ParseSource(nick) + if s == nil { + continue + } + + host = s.Host + nick = s.Name + ident = s.Ident + } + + if !IsValidNick(nick) { + continue + } + + c.state.createUser(nick) + user := c.state.lookupUser(nick) + if user == nil { + continue + } + + user.addChannel(channel.Name) + channel.addUser(nick) + + // Add necessary userhost-in-names data into the user. + if host != "" { + user.Host = host + } + if ident != "" { + user.Ident = ident + } + + // Don't append modes, overwrite them. + perms, _ := user.Perms.Lookup(channel.Name) + perms.set(modes, false) + user.Perms.set(channel.Name, perms) + } + c.state.Unlock() + c.state.notify(c, UPDATE_STATE) +} + +// updateLastActive is a wrapper for any event which the source author +// should have it's LastActive time updated. This is useful for things like +// a KICK where we know they are active, as they just kicked another user, +// even though they may not be talking. +func updateLastActive(c *Client, e Event) { + if e.Source == nil { + return + } + + c.state.Lock() + + // Update the users last active time, if they exist. + user := c.state.lookupUser(e.Source.Name) + if user == nil { + c.state.Unlock() + return + } + + user.LastActive = time.Now() + c.state.Unlock() +} diff --git a/vendor/github.com/lrstanley/girc/cap.go b/vendor/github.com/lrstanley/girc/cap.go new file mode 100644 index 00000000..751135b3 --- /dev/null +++ b/vendor/github.com/lrstanley/girc/cap.go @@ -0,0 +1,639 @@ +// 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" + "encoding/base64" + "fmt" + "io" + "sort" + "strings" +) + +var possibleCap = map[string][]string{ + "account-notify": nil, + "account-tag": nil, + "away-notify": nil, + "batch": nil, + "cap-notify": nil, + "chghost": nil, + "extended-join": nil, + "invite-notify": nil, + "message-tags": nil, + "multi-prefix": nil, + "userhost-in-names": nil, +} + +func (c *Client) listCAP() { + if !c.Config.disableTracking { + c.write(&Event{Command: CAP, Params: []string{CAP_LS, "302"}}) + } +} + +func possibleCapList(c *Client) map[string][]string { + out := make(map[string][]string) + + if c.Config.SASL != nil { + out["sasl"] = nil + } + + for k := range c.Config.SupportedCaps { + out[k] = c.Config.SupportedCaps[k] + } + + for k := range possibleCap { + out[k] = possibleCap[k] + } + + return out +} + +func parseCap(raw string) map[string][]string { + out := make(map[string][]string) + parts := strings.Split(raw, " ") + + var val int + + for i := 0; i < len(parts); i++ { + val = strings.IndexByte(parts[i], prefixTagValue) // = + + // No value splitter, or has splitter but no trailing value. + if val < 1 || len(parts[i]) < val+1 { + // The capability doesn't contain a value. + out[parts[i]] = nil + continue + } + + out[parts[i][:val]] = strings.Split(parts[i][val+1:], ",") + } + + return out +} + +// handleCAP attempts to find out what IRCv3 capabilities the server supports. +// This will lock further registration until we have acknowledged 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() + return + } + + // We can assume there was a failure attempting to enable a capability. + if len(e.Params) == 2 && e.Params[1] == CAP_NAK { + // Let the server know that we're done. + c.write(&Event{Command: CAP, Params: []string{CAP_END}}) + return + } + + possible := possibleCapList(c) + + if len(e.Params) >= 2 && len(e.Trailing) > 1 && e.Params[1] == CAP_LS { + c.state.Lock() + + caps := parseCap(e.Trailing) + + for k := range caps { + if _, ok := possible[k]; !ok { + continue + } + + if len(possible[k]) == 0 || len(caps[k]) == 0 { + c.state.tmpCap = append(c.state.tmpCap, k) + continue + } + + var contains bool + for i := 0; i < len(caps[k]); i++ { + for j := 0; j < len(possible[k]); j++ { + if caps[k][i] == possible[k][j] { + // Assume we have a matching split value. + contains = true + goto checkcontains + } + } + } + + checkcontains: + if !contains { + continue + } + + c.state.tmpCap = append(c.state.tmpCap, k) + } + c.state.Unlock() + + // Indicates if this is a multi-line LS. (2 args means it's the + // last LS). + if len(e.Params) == 2 { + // If we support no caps, just ack the CAP message and END. + if len(c.state.tmpCap) == 0 { + c.write(&Event{Command: CAP, Params: []string{CAP_END}}) + return + } + + // Let them know which ones we'd like to enable. + c.write(&Event{Command: CAP, Params: []string{CAP_REQ}, Trailing: strings.Join(c.state.tmpCap, " ")}) + + // Re-initialize the tmpCap, so if we get multiple 'CAP LS' requests + // due to cap-notify, we can re-evaluate what we can support. + c.state.Lock() + c.state.tmpCap = []string{} + c.state.Unlock() + } + } + + if len(e.Params) == 2 && len(e.Trailing) > 1 && e.Params[1] == CAP_ACK { + c.state.Lock() + c.state.enabledCap = strings.Split(e.Trailing, " ") + + // Do we need to do sasl auth? + wantsSASL := false + for i := 0; i < len(c.state.enabledCap); i++ { + if c.state.enabledCap[i] == "sasl" { + wantsSASL = true + break + } + } + c.state.Unlock() + + if wantsSASL { + c.write(&Event{Command: AUTHENTICATE, Params: []string{c.Config.SASL.Method()}}) + // Don't "CAP END", since we want to authenticate. + return + } + + // Let the server know that we're done. + c.write(&Event{Command: CAP, Params: []string{CAP_END}}) + return + } +} + +// 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, +// however CHGHOST resolves this in a much cleaner fashion. +func handleCHGHOST(c *Client, e Event) { + if len(e.Params) != 2 { + return + } + + c.state.Lock() + user := c.state.lookupUser(e.Source.Name) + if user != nil { + user.Ident = e.Params[0] + user.Host = e.Params[1] + } + c.state.Unlock() + c.state.notify(c, UPDATE_STATE) +} + +// handleAWAY handles incoming IRCv3 AWAY events, for which are sent both +// when users are no longer away, or when they are away. +func handleAWAY(c *Client, e Event) { + c.state.Lock() + user := c.state.lookupUser(e.Source.Name) + if user != nil { + user.Extras.Away = e.Trailing + } + c.state.Unlock() + c.state.notify(c, UPDATE_STATE) +} + +// handleACCOUNT handles incoming IRCv3 ACCOUNT events. ACCOUNT is sent when +// a user logs into an account, logs out of their account, or logs into a +// different account. The account backend is handled server-side, so this +// could be NickServ, X (undernet?), etc. +func handleACCOUNT(c *Client, e Event) { + if len(e.Params) != 1 { + return + } + + account := e.Params[0] + if account == "*" { + account = "" + } + + 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) +} + +// 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 = 0x40 // @ + prefixTagValue byte = 0x3D // = + prefixUserTag byte = 0x2B // + + tagSeparator byte = 0x3B // ; + 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] < 0x41 || name[i] > 0x5A) && (name[i] < 0x61 || name[i] > 0x7A) && (name[i] < 0x2D || name[i] > 0x39) && name[i] != 0x5F { + 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] < 0x21 || value[i] > 0x7E || value[i] == 0x3B { + return false + } + } + return true +} diff --git a/vendor/github.com/lrstanley/girc/client.go b/vendor/github.com/lrstanley/girc/client.go new file mode 100644 index 00000000..1a4d4ac2 --- /dev/null +++ b/vendor/github.com/lrstanley/girc/client.go @@ -0,0 +1,615 @@ +// 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 ( + "context" + "crypto/tls" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "runtime" + "sort" + "sync" + "time" +) + +// Client contains all of the information necessary to run a single IRC +// client. +type Client struct { + // Config represents the configuration. Please take extra caution in that + // entries in this are not edited while the client is connected, to prevent + // data races. This is NOT concurrent safe to update. + Config Config + // rx is a buffer of events waiting to be processed. + rx chan *Event + // tx is a buffer of events waiting to be sent. + tx chan *Event + // state represents the throw-away state for the irc session. + state *state + // initTime represents the creation time of the client. + initTime time.Time + // Handlers is a handler which manages internal and external handlers. + Handlers *Caller + // CTCP is a handler which manages internal and external CTCP handlers. + CTCP *CTCP + // Cmd contains various helper methods to interact with the server. + Cmd *Commands + // mu is the mux used for connections/disconnections from the server, + // so multiple threads aren't trying to connect at the same time, and + // vice versa. + mu sync.RWMutex + // stop is used to communicate with Connect(), letting it know that the + // client wishes to cancel/close. + stop context.CancelFunc + // conn is a net.Conn reference to the IRC server. If this is nil, it is + // safe to assume that we're not connected. If this is not nil, this + // means we're either connected, connecting, or cleaning up. This should + // be guarded with Client.mu. + conn *ircConn + // debug is used if a writer is supplied for Client.Config.Debugger. + debug *log.Logger +} + +// Config contains configuration options for an IRC client +type Config struct { + // Server is a host/ip of the server you want to connect to. This only + // has an affect during the dial process + Server string + // ServerPass is the server password used to authenticate. This only has + // an affect during the dial process. + ServerPass string + // Port is the port that will be used during server connection. This only + // has an affect during the dial process. + Port int + // Nick is an rfc-valid nickname used during connection. This only has an + // affect during the dial process. + Nick string + // User is the username/ident to use on connect. Ignored if an identd + // server is used. This only has an affect during the dial process. + User string + // Name is the "realname" that's used during connection. This only has an + // affect during the dial process. + Name string + // SASL contains the necessary authentication data to authenticate + // with SASL. See the documentation for SASLMech for what is currently + // supported. Capability tracking must be enabled for this to work, as + // this requires IRCv3 CAP handling. + SASL SASLMech + // Bind is used to bind to a specific host or ip during the dial process + // when connecting to the server. This can be a hostname, however it must + // resolve to an IPv4/IPv6 address bindable on your system. Otherwise, + // you can simply use a IPv4/IPv6 address directly. This only has an + // affect during the dial process and will not work with DialerConnect(). + Bind string + // SSL allows dialing via TLS. See TLSConfig to set your own TLS + // configuration (e.g. to not force hostname checking). This only has an + // affect during the dial process. + SSL bool + // TLSConfig is an optional user-supplied tls configuration, used during + // socket creation to the server. SSL must be enabled for this to be used. + // This only has an affect during the dial process. + TLSConfig *tls.Config + // AllowFlood allows the client to bypass the rate limit of outbound + // messages. + AllowFlood bool + // GlobalFormat enables passing through all events which have trailing + // text through the color Fmt() function, so you don't have to wrap + // every response in the Fmt() method. + // + // Note that this only actually applies to PRIVMSG, NOTICE and TOPIC + // events, to ensure it doesn't clobber unwanted events. + GlobalFormat bool + // Debug is an optional, user supplied location to log the raw lines + // sent from the server, or other useful debug logs. Defaults to + // ioutil.Discard. For quick debugging, this could be set to os.Stdout. + Debug io.Writer + // Out is used to write out a prettified version of incoming events. For + // example, channel JOIN/PART, PRIVMSG/NOTICE, KICk, etc. Useful to get + // a brief output of the activity of the client. If you are looking to + // log raw messages, look at a handler and girc.ALLEVENTS and the relevant + // Event.Bytes() or Event.String() methods. + Out io.Writer + // RecoverFunc is called when a handler throws a panic. If RecoverFunc is + // set, the panic will be considered recovered, otherwise the client will + // panic. Set this to DefaultRecoverHandler if you don't want the client + // to panic, however you don't want to handle the panic yourself. + // DefaultRecoverHandler will log the panic to Debug or os.Stdout if + // Debug is unset. + RecoverFunc func(c *Client, e *HandlerError) + // SupportedCaps are the IRCv3 capabilities you would like the client to + // support on top of the ones which the client already supports (see + // cap.go for which ones the client enables by default). Only use this + // if you have not called DisableTracking(). The keys value gets passed + // to the server if supported. + SupportedCaps map[string][]string + // Version is the application version information that will be used in + // response to a CTCP VERSION, if default CTCP replies have not been + // overwritten or a VERSION handler was already supplied. + Version string + // PingDelay is the frequency between when the client sends a keep-alive + // PING to the server, and awaits a response (and times out if the server + // doesn't respond in time). This should be between 20-600 seconds. See + // Client.Lag() if you want to determine the delay between the server + // and the client. If this is set to -1, the client will not attempt to + // send client -> server PING requests. + PingDelay time.Duration + + // disableTracking disables all channel and user-level tracking. Useful + // for highly embedded scripts with single purposes. This has an exported + // method which enables this and ensures prop cleanup, see + // Client.DisableTracking(). + disableTracking bool + // HandleNickCollide when set, allows the client to handle nick collisions + // in a custom way. If unset, the client will attempt to append a + // underscore to the end of the nickname, in order to bypass using + // an invalid nickname. For example, if "test" is already in use, or is + // blocked by the network/a service, the client will try and use "test_", + // then it will attempt "test__", "test___", and so on. + HandleNickCollide func(oldNick string) (newNick string) +} + +// ErrInvalidConfig is returned when the configuration passed to the client +// is invalid. +type ErrInvalidConfig struct { + Conf Config // Conf is the configuration that was not valid. + err error +} + +func (e ErrInvalidConfig) Error() string { return "invalid configuration: " + e.err.Error() } + +// isValid checks some basic settings to ensure the config is valid. +func (conf *Config) isValid() error { + if conf.Server == "" { + return &ErrInvalidConfig{Conf: *conf, err: errors.New("empty server")} + } + + // Default port to 6667 (the standard IRC port). + if conf.Port == 0 { + conf.Port = 6667 + } + + if conf.Port < 21 || conf.Port > 65535 { + return &ErrInvalidConfig{Conf: *conf, err: errors.New("port outside valid range (21-65535)")} + } + + if !IsValidNick(conf.Nick) { + return &ErrInvalidConfig{Conf: *conf, err: errors.New("bad nickname specified")} + } + if !IsValidUser(conf.User) { + return &ErrInvalidConfig{Conf: *conf, err: errors.New("bad user/ident specified")} + } + + return nil +} + +// ErrNotConnected is returned if a method is used when the client isn't +// connected. +var ErrNotConnected = errors.New("client is not connected to server") + +// ErrDisconnected is called when Config.Retries is less than 1, and we +// non-intentionally disconnected from the server. +var ErrDisconnected = errors.New("unexpectedly disconnected") + +// ErrInvalidTarget should be returned if the target which you are +// attempting to send an event to is invalid or doesn't match RFC spec. +type ErrInvalidTarget struct { + Target string +} + +func (e *ErrInvalidTarget) Error() string { return "invalid target: " + e.Target } + +// New creates a new IRC client with the specified server, name and config. +func New(config Config) *Client { + c := &Client{ + Config: config, + rx: make(chan *Event, 25), + tx: make(chan *Event, 25), + CTCP: newCTCP(), + initTime: time.Now(), + } + + c.Cmd = &Commands{c: c} + + if c.Config.PingDelay >= 0 && c.Config.PingDelay < (20*time.Second) { + c.Config.PingDelay = 20 * time.Second + } else if c.Config.PingDelay > (600 * time.Second) { + c.Config.PingDelay = 600 * time.Second + } + + if c.Config.Debug == nil { + c.debug = log.New(ioutil.Discard, "", 0) + } else { + c.debug = log.New(c.Config.Debug, "debug:", log.Ltime|log.Lshortfile) + c.debug.Print("initializing debugging") + } + + // Setup the caller. + c.Handlers = newCaller(c.debug) + + // Give ourselves a new state. + c.state = &state{} + c.state.reset() + + // Register builtin handlers. + c.registerBuiltins() + + // Register default CTCP responses. + c.CTCP.addDefaultHandlers() + + return c +} + +// String returns a brief description of the current client state. +func (c *Client) String() string { + connected := c.IsConnected() + + return fmt.Sprintf( + "<Client init:%q handlers:%d connected:%t>", c.initTime.String(), c.Handlers.Len(), connected, + ) +} + +// Close closes the network connection to the server, and sends a STOPPED +// event. This should cause Connect() to return with nil. This should be +// safe to call multiple times. See Connect()'s documentation on how +// handlers and goroutines are handled when disconnected from the server. +func (c *Client) Close() { + c.mu.RLock() + if c.stop != nil { + c.debug.Print("requesting client to stop") + c.stop() + } + c.mu.RUnlock() +} + +// ErrEvent is an error returned when the server (or library) sends an ERROR +// message response. The string returned contains the trailing text from the +// message. +type ErrEvent struct { + Event *Event +} + +func (e *ErrEvent) Error() string { + if e.Event == nil { + return "unknown error occurred" + } + + return e.Event.Trailing +} + +func (c *Client) execLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) { + c.debug.Print("starting execLoop") + defer c.debug.Print("closing execLoop") + + var event *Event + + for { + select { + case <-ctx.Done(): + // We've been told to exit, however we shouldn't bail on the + // current events in the queue that should be processed, as one + // may want to handle an ERROR, QUIT, etc. + c.debug.Printf("received signal to close, flushing %d events and executing", len(c.rx)) + for { + select { + case event = <-c.rx: + c.RunHandlers(event) + default: + goto done + } + } + + done: + wg.Done() + return + case event = <-c.rx: + if event != nil && event.Command == ERROR { + // Handles incoming ERROR responses. These are only ever sent + // by the server (with the exception that this library may use + // them as a lower level way of signalling to disconnect due + // to some other client-choosen error), and should always be + // followed up by the server disconnecting the client. If for + // some reason the server doesn't disconnect the client, or + // if this library is the source of the error, this should + // signal back up to the main connect loop, to disconnect. + errs <- &ErrEvent{Event: event} + + // Make sure to not actually exit, so we can let any handlers + // actually handle the ERROR event. + } + + c.RunHandlers(event) + } + } +} + +// DisableTracking disables all channel/user-level/CAP tracking, and clears +// all internal handlers. Useful for highly embedded scripts with single +// purposes. This cannot be un-done on a client. +func (c *Client) DisableTracking() { + c.debug.Print("disabling tracking") + c.Config.disableTracking = true + c.Handlers.clearInternal() + + c.state.Lock() + c.state.channels = nil + c.state.Unlock() + c.state.notify(c, UPDATE_STATE) + + c.registerBuiltins() +} + +// Server returns the string representation of host+port pair for net.Conn. +func (c *Client) Server() string { + return fmt.Sprintf("%s:%d", c.Config.Server, c.Config.Port) +} + +// Lifetime returns the amount of time that has passed since the client was +// created. +func (c *Client) Lifetime() time.Duration { + return time.Since(c.initTime) +} + +// Uptime is the time at which the client successfully connected to the +// server. +func (c *Client) Uptime() (up *time.Time, err error) { + if !c.IsConnected() { + return nil, ErrNotConnected + } + + c.mu.RLock() + c.conn.mu.RLock() + up = c.conn.connTime + c.conn.mu.RUnlock() + c.mu.RUnlock() + + return up, nil +} + +// ConnSince is the duration that has past since the client successfully +// connected to the server. +func (c *Client) ConnSince() (since *time.Duration, err error) { + if !c.IsConnected() { + return nil, ErrNotConnected + } + + c.mu.RLock() + c.conn.mu.RLock() + timeSince := time.Since(*c.conn.connTime) + c.conn.mu.RUnlock() + c.mu.RUnlock() + + return &timeSince, nil +} + +// IsConnected returns true if the client is connected to the server. +func (c *Client) IsConnected() (connected bool) { + c.mu.RLock() + if c.conn == nil { + c.mu.RUnlock() + return false + } + + c.conn.mu.RLock() + connected = c.conn.connected + c.conn.mu.RUnlock() + c.mu.RUnlock() + + return connected +} + +// GetNick returns the current nickname of the active connection. Panics if +// tracking is disabled. +func (c *Client) GetNick() string { + c.panicIfNotTracking() + + c.state.RLock() + defer c.state.RUnlock() + + if c.state.nick == "" { + return c.Config.Nick + } + + return c.state.nick +} + +// GetIdent returns the current ident 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) GetIdent() string { + c.panicIfNotTracking() + + c.state.RLock() + defer c.state.RUnlock() + + 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 { + c.panicIfNotTracking() + + c.state.RLock() + defer c.state.RUnlock() + + return c.state.host +} + +// Channels returns the active list of channels that the client is in. +// Panics if tracking is disabled. +func (c *Client) Channels() []string { + c.panicIfNotTracking() + + c.state.RLock() + channels := make([]string, len(c.state.channels)) + var i int + for channel := range c.state.channels { + channels[i] = c.state.channels[channel].Name + i++ + } + c.state.RUnlock() + sort.Strings(channels) + + return channels +} + +// Users returns the active list of users that the client is tracking across +// all files. Panics if tracking is disabled. +func (c *Client) Users() []string { + c.panicIfNotTracking() + + c.state.RLock() + users := make([]string, len(c.state.users)) + var i int + for user := range c.state.users { + users[i] = c.state.users[user].Nick + i++ + } + c.state.RUnlock() + sort.Strings(users) + + 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 { + 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() +} + +// 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 { + 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() +} + +// IsInChannel returns true if the client is in channel. Panics if tracking +// is disabled. +func (c *Client) IsInChannel(channel string) bool { + c.panicIfNotTracking() + + c.state.RLock() + _, inChannel := c.state.channels[ToRFC1459(channel)] + c.state.RUnlock() + + return inChannel +} + +// GetServerOption retrieves a server capability setting that was retrieved +// during client connection. This is also known as ISUPPORT (or RPL_PROTOCTL). +// Will panic if used when tracking has been disabled. Examples of usage: +// +// nickLen, success := GetServerOption("MAXNICKLEN") +// +func (c *Client) GetServerOption(key string) (result string, ok bool) { + c.panicIfNotTracking() + + c.state.RLock() + result, ok = c.state.serverOptions[key] + c.state.RUnlock() + + return result, ok +} + +// NetworkName returns the network identifier. E.g. "EsperNet", "ByteIRC". +// May be empty if the server does not support RPL_ISUPPORT (or RPL_PROTOCTL). +// Will panic if used when tracking has been disabled. +func (c *Client) NetworkName() (name string) { + c.panicIfNotTracking() + + name, _ = c.GetServerOption("NETWORK") + + return name +} + +// ServerVersion returns the server software version, if the server has +// 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() (version string) { + c.panicIfNotTracking() + + 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() (motd string) { + c.panicIfNotTracking() + + c.state.RLock() + motd = c.state.motd + c.state.RUnlock() + + return motd +} + +// Lag 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) Lag() time.Duration { + c.mu.RLock() + c.conn.mu.RLock() + delta := c.conn.lastPong.Sub(c.conn.lastPing) + c.conn.mu.RUnlock() + c.mu.RUnlock() + + if delta < 0 { + return 0 + } + + return delta +} + +// 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. +func (c *Client) panicIfNotTracking() { + if !c.Config.disableTracking { + return + } + + pc, _, _, _ := runtime.Caller(1) + fn := runtime.FuncForPC(pc) + _, file, line, _ := runtime.Caller(2) + + panic(fmt.Sprintf("%s used when tracking is disabled (caller %s:%d)", fn.Name(), file, line)) +} diff --git a/vendor/github.com/lrstanley/girc/cmdhandler/cmd.go b/vendor/github.com/lrstanley/girc/cmdhandler/cmd.go new file mode 100644 index 00000000..71bb9f89 --- /dev/null +++ b/vendor/github.com/lrstanley/girc/cmdhandler/cmd.go @@ -0,0 +1,197 @@ +package cmdhandler + +import ( + "errors" + "fmt" + "regexp" + "strings" + "sync" + + "github.com/lrstanley/girc" +) + +// Input is a wrapper for events, based around private messages. +type Input struct { + Origin *girc.Event + Args []string +} + +// Command is an IRC command, supporting aliases, help documentation and easy +// wrapping for message inputs. +type Command struct { + // Name of command, e.g. "search" or "ping". + Name string + // Aliases for the above command, e.g. "s" for search, or "p" for "ping". + Aliases []string + // Help documentation. Should be in the format "<arg> <arg> [arg] -- + // something useful here" + Help string + // MinArgs is the minimum required arguments for the command. Defaults to + // 0, which means multiple, or no arguments can be supplied. If set + // above 0, this means that the command handler will throw an error asking + // the person to check "<prefix>help <command>" for more info. + MinArgs int + // Fn is the function which is executed when the command is ran from a + // private message, or channel. + Fn func(*girc.Client, *Input) +} + +func (c *Command) genHelp(prefix string) string { + out := "{b}" + prefix + c.Name + "{b}" + + if c.Aliases != nil && len(c.Aliases) > 0 { + out += " ({b}" + prefix + strings.Join(c.Aliases, "{b}, {b}"+prefix) + "{b})" + } + + out += " :: " + c.Help + + return out +} + +// CmdHandler is an irc command parser and execution format which you could +// use as an example for building your own version/bot. +// +// An example of how you would register this with girc: +// +// ch, err := cmdhandler.New("!") +// if err != nil { +// panic(err) +// } +// +// ch.Add(&cmdhandler.Command{ +// Name: "ping", +// Help: "Sends a pong reply back to the original user.", +// Fn: func(c *girc.Client, input *cmdhandler.Input) { +// c.Commands.ReplyTo(*input.Origin, "pong!") +// }, +// }) +// +// client.Handlers.AddHandler(girc.PRIVMSG, ch) +type CmdHandler struct { + prefix string + re *regexp.Regexp + + mu sync.Mutex + cmds map[string]*Command +} + +var cmdMatch = `^%s([a-z0-9-_]{1,20})(?: (.*))?$` + +// New returns a new CmdHandler based on the specified command prefix. A good +// prefix is a single character, and easy to remember/use. E.g. "!", or ".". +func New(prefix string) (*CmdHandler, error) { + re, err := regexp.Compile(fmt.Sprintf(cmdMatch, regexp.QuoteMeta(prefix))) + if err != nil { + return nil, err + } + + return &CmdHandler{prefix: prefix, re: re, cmds: make(map[string]*Command)}, nil +} + +var validName = regexp.MustCompile(`^[a-z0-9-_]{1,20}$`) + +// Add registers a new command to the handler. Note that you cannot remove +// commands once added, unless you add another CmdHandler to the client. +func (ch *CmdHandler) Add(cmd *Command) error { + if cmd == nil { + return errors.New("nil command provided to CmdHandler") + } + + cmd.Name = strings.ToLower(cmd.Name) + if !validName.MatchString(cmd.Name) { + return fmt.Errorf("invalid command name: %q (req: %q)", cmd.Name, validName.String()) + } + + if cmd.Aliases != nil { + for i := 0; i < len(cmd.Aliases); i++ { + cmd.Aliases[i] = strings.ToLower(cmd.Aliases[i]) + if !validName.MatchString(cmd.Aliases[i]) { + return fmt.Errorf("invalid command name: %q (req: %q)", cmd.Aliases[i], validName.String()) + } + } + } + + if cmd.MinArgs < 0 { + cmd.MinArgs = 0 + } + + ch.mu.Lock() + defer ch.mu.Unlock() + + if _, ok := ch.cmds[cmd.Name]; ok { + return fmt.Errorf("command already registered: %s", cmd.Name) + } + + ch.cmds[cmd.Name] = cmd + + // Since we'd be storing pointers, duplicates do not matter. + for i := 0; i < len(cmd.Aliases); i++ { + if _, ok := ch.cmds[cmd.Aliases[i]]; ok { + return fmt.Errorf("alias already registered: %s", cmd.Aliases[i]) + } + + ch.cmds[cmd.Aliases[i]] = cmd + } + + return nil +} + +// Execute satisfies the girc.Handler interface. +func (ch *CmdHandler) Execute(client *girc.Client, event girc.Event) { + if event.Source == nil || event.Command != girc.PRIVMSG { + return + } + + parsed := ch.re.FindStringSubmatch(event.Trailing) + if len(parsed) != 3 { + return + } + + invCmd := strings.ToLower(parsed[1]) + args := strings.Split(parsed[2], " ") + if len(args) == 1 && args[0] == "" { + args = []string{} + } + + ch.mu.Lock() + defer ch.mu.Unlock() + + if invCmd == "help" { + if len(args) == 0 { + client.Cmd.ReplyTo(event, girc.Fmt("type '{b}!help {blue}<command>{c}{b}' to optionally get more info about a specific command.")) + return + } + + args[0] = strings.ToLower(args[0]) + + if _, ok := ch.cmds[args[0]]; !ok { + client.Cmd.ReplyTof(event, girc.Fmt("unknown command {b}%q{b}."), args[0]) + return + } + + if ch.cmds[args[0]].Help == "" { + client.Cmd.ReplyTof(event, girc.Fmt("there is no help documentation for {b}%q{b}"), args[0]) + return + } + + client.Cmd.ReplyTo(event, girc.Fmt(ch.cmds[args[0]].genHelp(ch.prefix))) + return + } + + cmd, ok := ch.cmds[invCmd] + if !ok { + return + } + + if len(args) < cmd.MinArgs { + client.Cmd.ReplyTof(event, girc.Fmt("not enough arguments supplied for {b}%q{b}. try '{b}%shelp %s{b}'?"), invCmd, ch.prefix, invCmd) + return + } + + in := &Input{ + Origin: &event, + Args: args, + } + + go cmd.Fn(client, in) +} diff --git a/vendor/github.com/lrstanley/girc/commands.go b/vendor/github.com/lrstanley/girc/commands.go new file mode 100644 index 00000000..bce25322 --- /dev/null +++ b/vendor/github.com/lrstanley/girc/commands.go @@ -0,0 +1,398 @@ +// 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 ( + "errors" + "fmt" +) + +// Commands holds a large list of useful methods to interact with the server, +// and wrappers for common events. +type Commands struct { + c *Client +} + +// Nick changes the client nickname. +func (cmd *Commands) Nick(name string) error { + if !IsValidNick(name) { + return &ErrInvalidTarget{Target: name} + } + + cmd.c.Send(&Event{Command: NICK, Params: []string{name}}) + return nil +} + +// Join attempts to enter a list of IRC channels, at bulk if possible to +// prevent sending extensive JOIN commands. +func (cmd *Commands) Join(channels ...string) error { + // We can join multiple channels at once, however we need to ensure that + // we are not exceeding the line length. (see maxLength) + max := maxLength - len(JOIN) - 1 + + var buffer string + + for i := 0; i < len(channels); i++ { + if !IsValidChannel(channels[i]) { + return &ErrInvalidTarget{Target: channels[i]} + } + + if len(buffer+","+channels[i]) > max { + cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}}) + buffer = "" + continue + } + + if len(buffer) == 0 { + buffer = channels[i] + } else { + buffer += "," + channels[i] + } + + if i == len(channels)-1 { + cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}}) + return nil + } + } + + return nil +} + +// JoinKey attempts to enter an IRC channel with a password. +func (cmd *Commands) JoinKey(channel, password string) error { + if !IsValidChannel(channel) { + return &ErrInvalidTarget{Target: channel} + } + + cmd.c.Send(&Event{Command: JOIN, Params: []string{channel, password}}) + return nil +} + +// Part leaves an IRC channel. +func (cmd *Commands) Part(channel, message string) error { + if !IsValidChannel(channel) { + return &ErrInvalidTarget{Target: channel} + } + + cmd.c.Send(&Event{Command: JOIN, Params: []string{channel}}) + return nil +} + +// PartMessage leaves an IRC channel with a specified leave message. +func (cmd *Commands) PartMessage(channel, message string) error { + if !IsValidChannel(channel) { + return &ErrInvalidTarget{Target: channel} + } + + cmd.c.Send(&Event{Command: JOIN, Params: []string{channel}, Trailing: message}) + return nil +} + +// SendCTCP sends a CTCP request to target. Note that this method uses +// PRIVMSG specifically. +func (cmd *Commands) SendCTCP(target, ctcpType, message string) error { + out := encodeCTCPRaw(ctcpType, message) + if out == "" { + return errors.New("invalid CTCP") + } + + return cmd.Message(target, out) +} + +// SendCTCPf sends a CTCP request to target using a specific format. Note that +// this method uses PRIVMSG specifically. +func (cmd *Commands) SendCTCPf(target, ctcpType, format string, a ...interface{}) error { + return cmd.SendCTCP(target, ctcpType, fmt.Sprintf(format, a...)) +} + +// SendCTCPReplyf sends a CTCP response to target using a specific format. +// Note that this method uses NOTICE specifically. +func (cmd *Commands) SendCTCPReplyf(target, ctcpType, format string, a ...interface{}) error { + return cmd.SendCTCPReply(target, ctcpType, fmt.Sprintf(format, a...)) +} + +// SendCTCPReply sends a CTCP response to target. Note that this method uses +// NOTICE specifically. +func (cmd *Commands) SendCTCPReply(target, ctcpType, message string) error { + out := encodeCTCPRaw(ctcpType, message) + if out == "" { + return errors.New("invalid CTCP") + } + + return cmd.Notice(target, out) +} + +// Message sends a PRIVMSG to target (either channel, service, or user). +func (cmd *Commands) Message(target, message string) error { + if !IsValidNick(target) && !IsValidChannel(target) { + return &ErrInvalidTarget{Target: target} + } + + cmd.c.Send(&Event{Command: PRIVMSG, Params: []string{target}, Trailing: message}) + return nil +} + +// Messagef sends a formated PRIVMSG to target (either channel, service, or +// user). +func (cmd *Commands) Messagef(target, format string, a ...interface{}) error { + return cmd.Message(target, fmt.Sprintf(format, a...)) +} + +// ErrInvalidSource is returned when a method needs to know the origin of an +// event, however Event.Source is unknown (e.g. sent by the user, not the +// server.) +var ErrInvalidSource = errors.New("event has nil or invalid source address") + +// Reply sends a reply to channel or user, based on where the supplied event +// originated from. See also ReplyTo(). +func (cmd *Commands) Reply(event Event, message string) error { + if event.Source == nil { + return ErrInvalidSource + } + + if len(event.Params) > 0 && IsValidChannel(event.Params[0]) { + return cmd.Message(event.Params[0], message) + } + + return cmd.Message(event.Source.Name, message) +} + +// Replyf sends a reply to channel or user with a format string, based on +// where the supplied event originated from. See also ReplyTof(). +func (cmd *Commands) Replyf(event Event, format string, a ...interface{}) error { + return cmd.Reply(event, fmt.Sprintf(format, a...)) +} + +// ReplyTo sends a reply to a channel or user, based on where the supplied +// event originated from. ReplyTo(), when originating from a channel will +// default to replying with "<user>, <message>". See also Reply(). +func (cmd *Commands) ReplyTo(event Event, message string) error { + if event.Source == nil { + return ErrInvalidSource + } + + if len(event.Params) > 0 && IsValidChannel(event.Params[0]) { + return cmd.Message(event.Params[0], event.Source.Name+", "+message) + } + + return cmd.Message(event.Source.Name, message) +} + +// ReplyTof sends a reply to a channel or user with a format string, based +// on where the supplied event originated from. ReplyTo(), when originating +// from a channel will default to replying with "<user>, <message>". See +// also Replyf(). +func (cmd *Commands) ReplyTof(event Event, format string, a ...interface{}) error { + return cmd.ReplyTo(event, fmt.Sprintf(format, a...)) +} + +// Action sends a PRIVMSG ACTION (/me) to target (either channel, service, +// or user). +func (cmd *Commands) Action(target, message string) error { + if !IsValidNick(target) && !IsValidChannel(target) { + return &ErrInvalidTarget{Target: target} + } + + cmd.c.Send(&Event{ + Command: PRIVMSG, + Params: []string{target}, + Trailing: fmt.Sprintf("\001ACTION %s\001", message), + }) + return nil +} + +// Actionf sends a formated PRIVMSG ACTION (/me) to target (either channel, +// service, or user). +func (cmd *Commands) Actionf(target, format string, a ...interface{}) error { + return cmd.Action(target, fmt.Sprintf(format, a...)) +} + +// Notice sends a NOTICE to target (either channel, service, or user). +func (cmd *Commands) Notice(target, message string) error { + if !IsValidNick(target) && !IsValidChannel(target) { + return &ErrInvalidTarget{Target: target} + } + + cmd.c.Send(&Event{Command: NOTICE, Params: []string{target}, Trailing: message}) + return nil +} + +// Noticef sends a formated NOTICE to target (either channel, service, or +// user). +func (cmd *Commands) Noticef(target, format string, a ...interface{}) error { + return cmd.Notice(target, fmt.Sprintf(format, a...)) +} + +// SendRaw sends a raw string back to the server, without carriage returns +// or newlines. +func (cmd *Commands) SendRaw(raw string) error { + e := ParseEvent(raw) + if e == nil { + return errors.New("invalid event: " + raw) + } + + cmd.c.Send(e) + return nil +} + +// SendRawf sends a formated string back to the server, without carriage +// returns or newlines. +func (cmd *Commands) SendRawf(format string, a ...interface{}) error { + return cmd.SendRaw(fmt.Sprintf(format, a...)) +} + +// Topic sets the topic of channel to message. Does not verify the length +// of the topic. +func (cmd *Commands) Topic(channel, message string) { + cmd.c.Send(&Event{Command: TOPIC, Params: []string{channel}, Trailing: message}) +} + +// Who sends a WHO query to the server, which will attempt WHOX by default. +// See http://faerion.sourceforge.net/doc/irc/whox.var for more details. This +// sends "%tcuhnr,2" per default. Do not use "1" as this will conflict with +// girc's builtin tracking functionality. +func (cmd *Commands) Who(target string) error { + if !IsValidNick(target) && !IsValidChannel(target) && !IsValidUser(target) { + return &ErrInvalidTarget{Target: target} + } + + cmd.c.Send(&Event{Command: WHO, Params: []string{target, "%tcuhnr,2"}}) + return nil +} + +// Whois sends a WHOIS query to the server, targeted at a specific user. +// as WHOIS is a bit slower, you may want to use WHO for brief user info. +func (cmd *Commands) Whois(nick string) error { + if !IsValidNick(nick) { + return &ErrInvalidTarget{Target: nick} + } + + cmd.c.Send(&Event{Command: WHOIS, Params: []string{nick}}) + return nil +} + +// Ping sends a PING query to the server, with a specific identifier that +// the server should respond with. +func (cmd *Commands) Ping(id string) { + cmd.c.write(&Event{Command: PING, Params: []string{id}}) +} + +// Pong sends a PONG query to the server, with an identifier which was +// received from a previous PING query received by the client. +func (cmd *Commands) Pong(id string) { + cmd.c.write(&Event{Command: PONG, Params: []string{id}}) +} + +// Oper sends a OPER authentication query to the server, with a username +// and password. +func (cmd *Commands) Oper(user, pass string) { + cmd.c.Send(&Event{Command: OPER, Params: []string{user, pass}, Sensitive: true}) +} + +// Kick sends a KICK query to the server, attempting to kick nick from +// channel, with reason. If reason is blank, one will not be sent to the +// server. +func (cmd *Commands) Kick(channel, nick, reason string) error { + if !IsValidChannel(channel) { + return &ErrInvalidTarget{Target: channel} + } + + if !IsValidNick(nick) { + return &ErrInvalidTarget{Target: nick} + } + + if reason != "" { + cmd.c.Send(&Event{Command: KICK, Params: []string{channel, nick}, Trailing: reason}) + return nil + } + + cmd.c.Send(&Event{Command: KICK, Params: []string{channel, nick}}) + return nil +} + +// Invite sends a INVITE query to the server, to invite nick to channel. +func (cmd *Commands) Invite(channel, nick string) error { + if !IsValidChannel(channel) { + return &ErrInvalidTarget{Target: channel} + } + + if !IsValidNick(nick) { + return &ErrInvalidTarget{Target: nick} + } + + cmd.c.Send(&Event{Command: INVITE, Params: []string{nick, channel}}) + return nil +} + +// Away sends a AWAY query to the server, suggesting that the client is no +// longer active. If reason is blank, Client.Back() is called. Also see +// Client.Back(). +func (cmd *Commands) Away(reason string) { + if reason == "" { + cmd.Back() + return + } + + cmd.c.Send(&Event{Command: AWAY, Params: []string{reason}}) +} + +// Back sends a AWAY query to the server, however the query is blank, +// suggesting that the client is active once again. Also see Client.Away(). +func (cmd *Commands) Back() { + cmd.c.Send(&Event{Command: AWAY}) +} + +// List sends a LIST query to the server, which will list channels and topics. +// Supports multiple channels at once, in hopes it will reduce extensive +// LIST queries to the server. Supply no channels to run a list against the +// entire server (warning, that may mean LOTS of channels!) +func (cmd *Commands) List(channels ...string) error { + if len(channels) == 0 { + cmd.c.Send(&Event{Command: LIST}) + return nil + } + + // We can LIST multiple channels at once, however we need to ensure that + // we are not exceeding the line length. (see maxLength) + max := maxLength - len(JOIN) - 1 + + var buffer string + + for i := 0; i < len(channels); i++ { + if !IsValidChannel(channels[i]) { + return &ErrInvalidTarget{Target: channels[i]} + } + + if len(buffer+","+channels[i]) > max { + cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}}) + buffer = "" + continue + } + + if len(buffer) == 0 { + buffer = channels[i] + } else { + buffer += "," + channels[i] + } + + if i == len(channels)-1 { + cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}}) + return nil + } + } + + return nil +} + +// Whowas sends a WHOWAS query to the server. amount is the amount of results +// you want back. +func (cmd *Commands) Whowas(nick string, amount int) error { + if !IsValidNick(nick) { + return &ErrInvalidTarget{Target: nick} + } + + cmd.c.Send(&Event{Command: WHOWAS, Params: []string{nick, string(amount)}}) + return nil +} diff --git a/vendor/github.com/lrstanley/girc/conn.go b/vendor/github.com/lrstanley/girc/conn.go new file mode 100644 index 00000000..a46a5dd7 --- /dev/null +++ b/vendor/github.com/lrstanley/girc/conn.go @@ -0,0 +1,566 @@ +// 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 ( + "bufio" + "context" + "crypto/tls" + "fmt" + "net" + "sync" + "time" +) + +// Messages are delimited with CR and LF line endings, we're using the last +// one to split the stream. Both are removed during parsing of the message. +const delim byte = '\n' + +var endline = []byte("\r\n") + +// ircConn represents an IRC network protocol connection, it consists of an +// Encoder and Decoder to manage i/o. +type ircConn struct { + io *bufio.ReadWriter + sock net.Conn + + mu sync.RWMutex + // lastWrite is used to keep track of when we last wrote to the server. + lastWrite time.Time + // lastActive is the last time the client was interacting with the server, + // excluding a few background commands (PING, PONG, WHO, etc). + lastActive time.Time + // writeDelay is used to keep track of rate limiting of events sent to + // the server. + writeDelay time.Duration + // connected is true if we're actively connected to a server. + connected bool + // connTime is the time at which the client has connected to a server. + connTime *time.Time + // lastPing is the last time that we pinged the server. + lastPing time.Time + // lastPong is the last successful time that we pinged the server and + // received a successful pong back. + lastPong time.Time + pingDelay time.Duration +} + +// Dialer is an interface implementation of net.Dialer. Use this if you would +// like to implement your own dialer which the client will use when connecting. +type Dialer interface { + // Dial takes two arguments. Network, which should be similar to "tcp", + // "tdp6", "udp", etc -- as well as address, which is the hostname or ip + // of the network. Note that network can be ignored if your transport + // doesn't take advantage of network types. + Dial(network, address string) (net.Conn, error) +} + +// newConn sets up and returns a new connection to the server. +func newConn(conf Config, dialer Dialer, addr string) (*ircConn, error) { + if err := conf.isValid(); err != nil { + return nil, err + } + + var conn net.Conn + var err error + + if dialer == nil { + netDialer := &net.Dialer{Timeout: 5 * time.Second} + + if conf.Bind != "" { + var local *net.TCPAddr + local, err = net.ResolveTCPAddr("tcp", conf.Bind+":0") + if err != nil { + return nil, err + } + + netDialer.LocalAddr = local + } + + dialer = netDialer + } + + if conn, err = dialer.Dial("tcp", addr); err != nil { + return nil, err + } + + if conf.SSL { + var tlsConn net.Conn + tlsConn, err = tlsHandshake(conn, conf.TLSConfig, conf.Server, true) + if err != nil { + return nil, err + } + + conn = tlsConn + } + + ctime := time.Now() + + c := &ircConn{ + sock: conn, + connTime: &ctime, + connected: true, + } + c.newReadWriter() + + return c, nil +} + +func newMockConn(conn net.Conn) *ircConn { + ctime := time.Now() + c := &ircConn{ + sock: conn, + connTime: &ctime, + connected: true, + } + c.newReadWriter() + + return c +} + +// ErrParseEvent is returned when an event cannot be parsed with ParseEvent(). +type ErrParseEvent struct { + Line string +} + +func (e ErrParseEvent) Error() string { return "unable to parse event: " + e.Line } + +func (c *ircConn) decode() (event *Event, err error) { + line, err := c.io.ReadString(delim) + if err != nil { + return nil, err + } + + if event = ParseEvent(line); event == nil { + return nil, ErrParseEvent{line} + } + + return event, nil +} + +func (c *ircConn) encode(event *Event) error { + if _, err := c.io.Write(event.Bytes()); err != nil { + return err + } + if _, err := c.io.Write(endline); err != nil { + return err + } + + return c.io.Flush() +} + +func (c *ircConn) newReadWriter() { + c.io = bufio.NewReadWriter(bufio.NewReader(c.sock), bufio.NewWriter(c.sock)) +} + +func tlsHandshake(conn net.Conn, conf *tls.Config, server string, validate bool) (net.Conn, error) { + if conf == nil { + conf = &tls.Config{ServerName: server, InsecureSkipVerify: !validate} + } + + tlsConn := tls.Client(conn, conf) + return net.Conn(tlsConn), nil +} + +// Close closes the underlying socket. +func (c *ircConn) Close() error { + return c.sock.Close() +} + +// Connect attempts to connect to the given IRC server. Returns only when +// an error has occurred, or a disconnect was requested with Close(). Connect +// will only return once all client-based goroutines have been closed to +// ensure there are no long-running routines becoming backed up. +// +// Connect will wait for all non-goroutine handlers to complete on error/quit, +// however it will not wait for goroutine-based handlers. +// +// If this returns nil, this means that the client requested to be closed +// (e.g. Client.Close()). Connect will panic if called when the last call has +// not completed. +func (c *Client) Connect() error { + return c.internalConnect(nil, nil) +} + +// DialerConnect allows you to specify your own custom dialer which implements +// the Dialer interface. +// +// An example of using this library would be to take advantage of the +// golang.org/x/net/proxy library: +// +// proxyUrl, _ := proxyURI, err = url.Parse("socks5://1.2.3.4:8888") +// dialer, _ := proxy.FromURL(proxyURI, &net.Dialer{Timeout: 5 * time.Second}) +// _ := girc.DialerConnect(dialer) +func (c *Client) DialerConnect(dialer Dialer) error { + return c.internalConnect(nil, dialer) +} + +// MockConnect is used to implement mocking with an IRC server. Supply a net.Conn +// that will be used to spoof the server. A useful way to do this is to so +// net.Pipe(), pass one end into MockConnect(), and the other end into +// bufio.NewReader(). +// +// For example: +// +// client := girc.New(girc.Config{ +// Server: "dummy.int", +// Port: 6667, +// Nick: "test", +// User: "test", +// Name: "Testing123", +// }) +// +// in, out := net.Pipe() +// defer in.Close() +// defer out.Close() +// b := bufio.NewReader(in) +// +// go func() { +// if err := client.MockConnect(out); err != nil { +// panic(err) +// } +// }() +// +// defer client.Close(false) +// +// for { +// in.SetReadDeadline(time.Now().Add(300 * time.Second)) +// line, err := b.ReadString(byte('\n')) +// if err != nil { +// panic(err) +// } +// +// event := girc.ParseEvent(line) +// +// if event == nil { +// continue +// } +// +// // Do stuff with event here. +// } +func (c *Client) MockConnect(conn net.Conn) error { + return c.internalConnect(conn, nil) +} + +func (c *Client) internalConnect(mock net.Conn, dialer Dialer) error { + // We want to be the only one handling connects/disconnects right now. + c.mu.Lock() + + if c.conn != nil { + panic("use of connect more than once") + } + + // Reset the state. + c.state.reset() + + if mock == nil { + // Validate info, and actually make the connection. + c.debug.Printf("connecting to %s...", c.Server()) + conn, err := newConn(c.Config, dialer, c.Server()) + if err != nil { + c.mu.Unlock() + return err + } + + c.conn = conn + } else { + c.conn = newMockConn(mock) + } + + var ctx context.Context + ctx, c.stop = context.WithCancel(context.Background()) + c.mu.Unlock() + + errs := make(chan error, 4) + var wg sync.WaitGroup + // 4 being the number of goroutines we need to finish when this function + // returns. + wg.Add(4) + go c.execLoop(ctx, errs, &wg) + go c.readLoop(ctx, errs, &wg) + go c.sendLoop(ctx, errs, &wg) + go c.pingLoop(ctx, errs, &wg) + + // Passwords first. + if c.Config.ServerPass != "" { + c.write(&Event{Command: PASS, Params: []string{c.Config.ServerPass}, Sensitive: true}) + } + + // List the IRCv3 capabilities, specifically with the max protocol we + // support. The IRCv3 specification doesn't directly state if this should + // be called directly before registration, or if it should be called + // after NICK/USER requests. It looks like non-supporting networks + // should ignore this, and some IRCv3 capable networks require this to + // occur before NICK/USER registration. + c.listCAP() + + // Then nickname. + c.write(&Event{Command: NICK, Params: []string{c.Config.Nick}}) + + // Then username and realname. + if c.Config.Name == "" { + c.Config.Name = c.Config.User + } + + c.write(&Event{Command: USER, Params: []string{c.Config.User, "*", "*"}, Trailing: c.Config.Name}) + + // Send a virtual event allowing hooks for successful socket connection. + c.RunHandlers(&Event{Command: INITIALIZED, Trailing: c.Server()}) + + // Wait for the first error. + var result error + select { + case <-ctx.Done(): + c.debug.Print("received request to close, beginning clean up") + c.RunHandlers(&Event{Command: STOPPED, Trailing: c.Server()}) + case err := <-errs: + c.debug.Print("received error, beginning clean up") + result = err + } + + // Make sure that the connection is closed if not already. + c.mu.RLock() + if c.stop != nil { + c.stop() + } + c.conn.mu.Lock() + c.conn.connected = false + _ = c.conn.Close() + c.conn.mu.Unlock() + c.mu.RUnlock() + + // Once we have our error/result, let all other functions know we're done. + c.debug.Print("waiting for all routines to finish") + + // Wait for all goroutines to finish. + wg.Wait() + close(errs) + + // This helps ensure that the end user isn't improperly using the client + // more than once. If they want to do this, they should be using multiple + // clients, not multiple instances of Connect(). + c.mu.Lock() + c.conn = nil + c.mu.Unlock() + + return result +} + +// readLoop sets a timeout of 300 seconds, and then attempts to read from the +// IRC server. If there is an error, it calls Reconnect. +func (c *Client) readLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) { + c.debug.Print("starting readLoop") + defer c.debug.Print("closing readLoop") + + var event *Event + var err error + + for { + select { + case <-ctx.Done(): + wg.Done() + return + default: + _ = c.conn.sock.SetReadDeadline(time.Now().Add(300 * time.Second)) + event, err = c.conn.decode() + if err != nil { + errs <- err + wg.Done() + return + } + + c.rx <- event + } + } +} + +// Send sends an event to the server. Use Client.RunHandlers() if you are +// simply looking to trigger handlers with an event. +func (c *Client) Send(event *Event) { + if !c.Config.AllowFlood { + <-time.After(c.conn.rate(event.Len())) + } + + if c.Config.GlobalFormat && event.Trailing != "" && + (event.Command == PRIVMSG || event.Command == TOPIC || event.Command == NOTICE) { + event.Trailing = Fmt(event.Trailing) + } + + c.write(event) +} + +// write is the lower level function to write an event. It does not have a +// write-delay when sending events. +func (c *Client) write(event *Event) { + c.tx <- event +} + +// rate allows limiting events based on how frequent the event is being sent, +// as well as how many characters each event has. +func (c *ircConn) rate(chars int) time.Duration { + _time := time.Second + ((time.Duration(chars) * time.Second) / 100) + + c.mu.Lock() + if c.writeDelay += _time - time.Now().Sub(c.lastWrite); c.writeDelay < 0 { + c.writeDelay = 0 + } + c.mu.Unlock() + + c.mu.RLock() + defer c.mu.RUnlock() + if c.writeDelay > (8 * time.Second) { + return _time + } + + return 0 +} + +func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) { + c.debug.Print("starting sendLoop") + defer c.debug.Print("closing sendLoop") + + var err error + + for { + select { + case event := <-c.tx: + // Check if tags exist on the event. If they do, and message-tags + // isn't a supported capability, remove them from the event. + if event.Tags != nil { + c.state.RLock() + var in bool + for i := 0; i < len(c.state.enabledCap); i++ { + if c.state.enabledCap[i] == "message-tags" { + in = true + break + } + } + c.state.RUnlock() + + if !in { + event.Tags = Tags{} + } + } + + // Log the event. + if event.Sensitive { + c.debug.Printf("> %s ***redacted***", event.Command) + } else { + c.debug.Print("> ", StripRaw(event.String())) + } + if c.Config.Out != nil { + if pretty, ok := event.Pretty(); ok { + fmt.Fprintln(c.Config.Out, StripRaw(pretty)) + } + } + + c.conn.mu.Lock() + c.conn.lastWrite = time.Now() + + if event.Command != PING && event.Command != PONG && event.Command != WHO { + c.conn.lastActive = c.conn.lastWrite + } + c.conn.mu.Unlock() + + // Write the raw line. + _, err = c.conn.io.Write(event.Bytes()) + if err == nil { + // And the \r\n. + _, err = c.conn.io.Write(endline) + if err == nil { + // Lastly, flush everything to the socket. + err = c.conn.io.Flush() + } + } + + if err != nil { + errs <- err + wg.Done() + return + } + case <-ctx.Done(): + wg.Done() + return + } + } +} + +// ErrTimedOut is returned when we attempt to ping the server, and timed out +// before receiving a PONG back. +type ErrTimedOut struct { + // TimeSinceSuccess is how long ago we received a successful pong. + TimeSinceSuccess time.Duration + // LastPong is the time we received our last successful pong. + LastPong time.Time + // LastPong is the last time we sent a pong request. + LastPing time.Time + // Delay is the configured delay between how often we send a ping request. + Delay time.Duration +} + +func (ErrTimedOut) Error() string { return "timed out during ping to server" } + +func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) { + // Don't run the pingLoop if they want to disable it. + if c.Config.PingDelay <= 0 { + wg.Done() + return + } + + c.debug.Print("starting pingLoop") + defer c.debug.Print("closing pingLoop") + + c.conn.mu.Lock() + c.conn.lastPing = time.Now() + c.conn.lastPong = time.Now() + c.conn.mu.Unlock() + + tick := time.NewTicker(c.Config.PingDelay) + defer tick.Stop() + + started := time.Now() + past := false + + for { + select { + case <-tick.C: + // Delay during connect to wait for the client to register, otherwise + // some ircd's will not respond (e.g. during SASL negotiation). + if !past { + if time.Since(started) < 30*time.Second { + continue + } + + past = true + } + + c.conn.mu.RLock() + if time.Since(c.conn.lastPong) > c.Config.PingDelay+(60*time.Second) { + // It's 60 seconds over what out ping delay is, connection + // has probably dropped. + errs <- ErrTimedOut{ + TimeSinceSuccess: time.Since(c.conn.lastPong), + LastPong: c.conn.lastPong, + LastPing: c.conn.lastPing, + Delay: c.Config.PingDelay, + } + + wg.Done() + c.conn.mu.RUnlock() + return + } + c.conn.mu.RUnlock() + + c.conn.mu.Lock() + c.conn.lastPing = time.Now() + c.conn.mu.Unlock() + + c.Cmd.Ping(fmt.Sprintf("%d", time.Now().UnixNano())) + case <-ctx.Done(): + wg.Done() + return + } + } +} diff --git a/vendor/github.com/lrstanley/girc/contants.go b/vendor/github.com/lrstanley/girc/contants.go new file mode 100644 index 00000000..4d3c65bc --- /dev/null +++ b/vendor/github.com/lrstanley/girc/contants.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. +) diff --git a/vendor/github.com/lrstanley/girc/ctcp.go b/vendor/github.com/lrstanley/girc/ctcp.go new file mode 100644 index 00000000..c6c0bace --- /dev/null +++ b/vendor/github.com/lrstanley/girc/ctcp.go @@ -0,0 +1,288 @@ +// 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 ( + "fmt" + "runtime" + "strings" + "sync" + "time" +) + +// ctcpDelim if the delimiter used for CTCP formatted events/messages. +const ctcpDelim byte = 0x01 // Prefix and suffix for CTCP messages. + +// CTCPEvent is the necessary information from an IRC message. +type CTCPEvent struct { + // Origin is the original event that the CTCP event was decoded from. + Origin *Event `json:"origin"` + // Source is the author of the CTCP event. + Source *Source `json:"source"` + // Command is the type of CTCP event. E.g. PING, TIME, VERSION. + Command string `json:"command"` + // Text is the raw arguments following the command. + Text string `json:"text"` + // Reply is true if the CTCP event is intended to be a reply to a + // previous CTCP (e.g, if we sent one). + Reply bool `json:"reply"` +} + +// decodeCTCP decodes an incoming CTCP event, if it is CTCP. nil is returned +// if the incoming event does not match a valid CTCP. +func decodeCTCP(e *Event) *CTCPEvent { + // http://www.irchelp.org/protocol/ctcpspec.html + + // Must be targeting a user/channel, AND trailing must have + // DELIM+TAG+DELIM minimum (at least 3 chars). + if len(e.Params) != 1 || len(e.Trailing) < 3 { + return nil + } + + if (e.Command != PRIVMSG && e.Command != NOTICE) || !IsValidNick(e.Params[0]) { + return nil + } + + if e.Trailing[0] != ctcpDelim || e.Trailing[len(e.Trailing)-1] != ctcpDelim { + return nil + } + + // Strip delimiters. + text := e.Trailing[1 : len(e.Trailing)-1] + + s := strings.IndexByte(text, eventSpace) + + // Check to see if it only contains a tag. + if s < 0 { + for i := 0; i < len(text); i++ { + // Check for A-Z, 0-9. + if (text[i] < 0x41 || text[i] > 0x5A) && (text[i] < 0x30 || text[i] > 0x39) { + return nil + } + } + + return &CTCPEvent{ + Origin: e, + Source: e.Source, + Command: text, + Reply: e.Command == NOTICE, + } + } + + // Loop through checking the tag first. + for i := 0; i < s; i++ { + // Check for A-Z, 0-9. + if (text[i] < 0x41 || text[i] > 0x5A) && (text[i] < 0x30 || text[i] > 0x39) { + return nil + } + } + + return &CTCPEvent{ + Origin: e, + Source: e.Source, + Command: text[0:s], + Text: text[s+1:], + Reply: e.Command == NOTICE, + } +} + +// encodeCTCP encodes a CTCP event into a string, including delimiters. +func encodeCTCP(ctcp *CTCPEvent) (out string) { + if ctcp == nil { + return "" + } + + return encodeCTCPRaw(ctcp.Command, ctcp.Text) +} + +// encodeCTCPRaw is much like encodeCTCP, however accepts a raw command and +// string as input. +func encodeCTCPRaw(cmd, text string) (out string) { + if len(cmd) <= 0 { + return "" + } + + out = string(ctcpDelim) + cmd + + if len(text) > 0 { + out += string(eventSpace) + text + } + + return out + string(ctcpDelim) +} + +// CTCP handles the storage and execution of CTCP handlers against incoming +// CTCP events. +type CTCP struct { + // mu is the mutex that should be used when accessing any ctcp handlers. + mu sync.RWMutex + // handlers is a map of CTCP message -> functions. + handlers map[string]CTCPHandler +} + +// newCTCP returns a new clean CTCP handler. +func newCTCP() *CTCP { + return &CTCP{handlers: map[string]CTCPHandler{}} +} + +// call executes the necessary CTCP handler for the incoming event/CTCP +// command. +func (c *CTCP) call(client *Client, event *CTCPEvent) { + c.mu.RLock() + defer c.mu.RUnlock() + + // If they want to catch any panics, add to defer stack. + if client.Config.RecoverFunc != nil && event.Origin != nil { + defer recoverHandlerPanic(client, event.Origin, "ctcp-"+strings.ToLower(event.Command), 3) + } + + // Support wildcard CTCP event handling. Gets executed first before + // regular event handlers. + if _, ok := c.handlers["*"]; ok { + c.handlers["*"](client, *event) + } + + if _, ok := c.handlers[event.Command]; !ok { + // Send a ERRMSG reply, if we know who sent it. + if event.Source != nil && IsValidNick(event.Source.Name) { + client.Cmd.SendCTCPReply(event.Source.Name, CTCP_ERRMSG, "that is an unknown CTCP query") + } + return + } + + c.handlers[event.Command](client, *event) +} + +// parseCMD parses a CTCP command/tag, ensuring it's valid. If not, an empty +// string is returned. +func (c *CTCP) parseCMD(cmd string) string { + // TODO: Needs proper testing. + // Check if wildcard. + if cmd == "*" { + return "*" + } + + cmd = strings.ToUpper(cmd) + + for i := 0; i < len(cmd); i++ { + // Check for A-Z, 0-9. + if (cmd[i] < 0x41 || cmd[i] > 0x5A) && (cmd[i] < 0x30 || cmd[i] > 0x39) { + return "" + } + } + + return cmd +} + +// Set saves handler for execution upon a matching incoming CTCP event. +// Use SetBg if the handler may take an extended period of time to execute. +// If you would like to have a handler which will catch ALL CTCP requests, +// simply use "*" in place of the command. +func (c *CTCP) Set(cmd string, handler func(client *Client, ctcp CTCPEvent)) { + if cmd = c.parseCMD(cmd); cmd == "" { + return + } + + c.mu.Lock() + c.handlers[cmd] = CTCPHandler(handler) + c.mu.Unlock() +} + +// SetBg is much like Set, however the handler is executed in the background, +// ensuring that event handling isn't hung during long running tasks. See Set +// for more information. +func (c *CTCP) SetBg(cmd string, handler func(client *Client, ctcp CTCPEvent)) { + c.Set(cmd, func(client *Client, ctcp CTCPEvent) { + go handler(client, ctcp) + }) +} + +// Clear removes currently setup handler for cmd, if one is set. +func (c *CTCP) Clear(cmd string) { + if cmd = c.parseCMD(cmd); cmd == "" { + return + } + + c.mu.Lock() + delete(c.handlers, cmd) + c.mu.Unlock() +} + +// ClearAll removes all currently setup and re-sets the default handlers. +func (c *CTCP) ClearAll() { + c.mu.Lock() + c.handlers = map[string]CTCPHandler{} + c.mu.Unlock() + + // Register necessary handlers. + c.addDefaultHandlers() +} + +// CTCPHandler is a type that represents the function necessary to +// implement a CTCP handler. +type CTCPHandler func(client *Client, ctcp CTCPEvent) + +// addDefaultHandlers adds some useful default CTCP response handlers. +func (c *CTCP) addDefaultHandlers() { + c.SetBg(CTCP_PING, handleCTCPPing) + c.SetBg(CTCP_PONG, handleCTCPPong) + c.SetBg(CTCP_VERSION, handleCTCPVersion) + c.SetBg(CTCP_SOURCE, handleCTCPSource) + c.SetBg(CTCP_TIME, handleCTCPTime) + c.SetBg(CTCP_FINGER, handleCTCPFinger) +} + +// handleCTCPPing replies with a ping and whatever was originally requested. +func handleCTCPPing(client *Client, ctcp CTCPEvent) { + if ctcp.Reply { + return + } + client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_PING, ctcp.Text) +} + +// handleCTCPPong replies with a pong. +func handleCTCPPong(client *Client, ctcp CTCPEvent) { + if ctcp.Reply { + return + } + client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_PONG, "") +} + +// handleCTCPVersion replies with the name of the client, Go version, as well +// as the os type (darwin, linux, windows, etc) and architecture type (x86, +// arm, etc). +func handleCTCPVersion(client *Client, ctcp CTCPEvent) { + if client.Config.Version != "" { + client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_VERSION, client.Config.Version) + return + } + + client.Cmd.SendCTCPReplyf( + ctcp.Source.Name, CTCP_VERSION, + "girc (github.com/lrstanley/girc) using %s (%s, %s)", + runtime.Version(), runtime.GOOS, runtime.GOARCH, + ) +} + +// handleCTCPSource replies with the public git location of this library. +func handleCTCPSource(client *Client, ctcp CTCPEvent) { + client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_SOURCE, "https://github.com/lrstanley/girc") +} + +// handleCTCPTime replies with a RFC 1123 (Z) formatted version of Go's +// local time. +func handleCTCPTime(client *Client, ctcp CTCPEvent) { + client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_TIME, ":"+time.Now().Format(time.RFC1123Z)) +} + +// handleCTCPFinger replies with the realname and idle time of the user. This +// is obsoleted by improvements to the IRC protocol, however still supported. +func handleCTCPFinger(client *Client, ctcp CTCPEvent) { + client.conn.mu.RLock() + active := client.conn.lastActive + client.conn.mu.RUnlock() + + client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_FINGER, fmt.Sprintf("%s -- idle %s", client.Config.Name, time.Since(active))) +} diff --git a/vendor/github.com/lrstanley/girc/doc.go b/vendor/github.com/lrstanley/girc/doc.go new file mode 100644 index 00000000..e46a41aa --- /dev/null +++ b/vendor/github.com/lrstanley/girc/doc.go @@ -0,0 +1,12 @@ +// Package girc provides a high level, yet flexible IRC library for use with +// interacting with IRC servers. girc has support for user/channel tracking, +// as well as a few other neat features (like auto-reconnect). +// +// Much of what girc can do, can also be disabled. The goal is to provide a +// solid API that you don't necessarily have to work with out of the box if +// you don't want to. +// +// See the examples below for a few brief and useful snippets taking +// advantage of girc, which should give you a general idea of how the API +// works. +package girc diff --git a/vendor/github.com/lrstanley/girc/event.go b/vendor/github.com/lrstanley/girc/event.go new file mode 100644 index 00000000..77d9b45e --- /dev/null +++ b/vendor/github.com/lrstanley/girc/event.go @@ -0,0 +1,550 @@ +// 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" + "strings" +) + +const ( + eventSpace byte = 0x20 // Separator. + maxLength = 510 // Maximum length is 510 (2 for line endings). +) + +// cutCRFunc is used to trim CR characters from prefixes/messages. +func cutCRFunc(r rune) bool { + return r == '\r' || r == '\n' +} + +// Event represents an IRC protocol message, see RFC1459 section 2.3.1 +// +// <message> :: [':' <prefix> <SPACE>] <command> <params> <crlf> +// <prefix> :: <servername> | <nick> ['!' <user>] ['@' <host>] +// <command> :: <letter>{<letter>} | <number> <number> <number> +// <SPACE> :: ' '{' '} +// <params> :: <SPACE> [':' <trailing> | <middle> <params>] +// <middle> :: <Any *non-empty* sequence of octets not including SPACE or NUL +// or CR or LF, the first of which may not be ':'> +// <trailing> :: <Any, possibly empty, sequence of octets not including NUL or +// CR or LF> +// <crlf> :: CR LF +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). +} + +// 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 { + return nil + } + + var i, j int + e = &Event{} + + if raw[0] == prefixTag { + // Tags end with a space. + i = strings.IndexByte(raw, eventSpace) + + if i < 2 { + return nil + } + + e.Tags = ParseTags(raw[1:i]) + raw = raw[i+1:] + } + + if raw[0] == messagePrefix { + // Prefix ends with a space. + i = strings.IndexByte(raw, eventSpace) + + // Prefix string must not be empty if the indicator is present. + if i < 2 { + return nil + } + + e.Source = ParseSource(raw[1:i]) + + // Skip space at the end of the prefix. + i++ + } + + // Find end of command. + j = i + strings.IndexByte(raw[i:], eventSpace) + + if j < i { + // If there are no proceeding spaces, it's the only thing specified. + e.Command = strings.ToUpper(raw[i:]) + return e + } + + e.Command = strings.ToUpper(raw[i:j]) + + // Skip the space after the command. + j++ + + // Check if and where the trailing text is within the incoming line. + var lastIndex, trailerIndex int + for { + // We must loop through, as it's possible that the first message + // prefix is not actually what we want. (e.g, colons are commonly + // used within ISUPPORT to delegate things like CHANLIMIT or TARGMAX.) + lastIndex = trailerIndex + trailerIndex = strings.IndexByte(raw[j+lastIndex:], messagePrefix) + + if trailerIndex == -1 { + // No trailing argument found, assume the rest is just params. + e.Params = strings.Split(raw[j:], string(eventSpace)) + return e + } + + // This means we found a prefix that was proceeded by a space, and + // it's good to assume this is the start of trailing text to the line. + if raw[j+lastIndex+trailerIndex-1] == eventSpace { + i = lastIndex + trailerIndex + break + } + + // Keep looping through until we either can't find any more prefixes, + // or we find the one we want. + trailerIndex += lastIndex + 1 + } + + // Set i to that of the substring we were using before, and where the + // trailing prefix is. + i = j + i + + // Check if we need to parse arguments. If so, take everything after the + // command, and right before the trailing prefix, and cut it up. + if i > j { + e.Params = strings.Split(raw[j:i-1], string(eventSpace)) + } + + e.Trailing = raw[i+1:] + + // We need to re-encode the trailing argument even if it was empty. + if len(e.Trailing) <= 0 { + e.EmptyTrailing = true + } + + return e +} + +// Copy makes a deep copy of a given event, for use with allowing untrusted +// functions/handlers edit the event without causing potential issues with +// other handlers. +func (e *Event) Copy() *Event { + if e == nil { + return nil + } + + newEvent := &Event{ + Command: e.Command, + Trailing: e.Trailing, + EmptyTrailing: e.EmptyTrailing, + Sensitive: e.Sensitive, + } + + // Copy Source field, as it's a pointer and needs to be dereferenced. + if e.Source != nil { + newEvent.Source = e.Source.Copy() + } + + // Copy Params in order to dereference as well. + if e.Params != nil { + newEvent.Params = make([]string, len(e.Params)) + copy(newEvent.Params, e.Params) + } + + // Copy tags as necessary. + if e.Tags != nil { + newEvent.Tags = Tags{} + for k, v := range e.Tags { + newEvent.Tags[k] = v + } + } + + return newEvent +} + +// Len calculates the length of the string representation of event. Note that +// this will return the true length (even if longer than what IRC supports), +// which may be useful if you are trying to check and see if a message is +// too long, to trim it down yourself. +func (e *Event) Len() (length int) { + if e.Tags != nil { + // Include tags and trailing space. + length = e.Tags.Len() + 1 + } + if e.Source != nil { + // Include prefix and trailing space. + length += e.Source.Len() + 2 + } + + length += len(e.Command) + + if len(e.Params) > 0 { + length += len(e.Params) + + for i := 0; i < len(e.Params); i++ { + length += len(e.Params[i]) + } + } + + if len(e.Trailing) > 0 || e.EmptyTrailing { + // Include prefix and space. + length += len(e.Trailing) + 2 + } + + return +} + +// Bytes returns a []byte representation of event. Strips all newlines and +// carriage returns. +// +// Per RFC2812 section 2.3, messages should not exceed 512 characters in +// length. This method forces that limit by discarding any characters +// exceeding the length limit. +func (e *Event) Bytes() []byte { + buffer := new(bytes.Buffer) + + // Tags. + if e.Tags != nil { + e.Tags.writeTo(buffer) + } + + // Event prefix. + if e.Source != nil { + buffer.WriteByte(messagePrefix) + e.Source.writeTo(buffer) + buffer.WriteByte(eventSpace) + } + + // Command is required. + buffer.WriteString(e.Command) + + // Space separated list of arguments. + if len(e.Params) > 0 { + buffer.WriteByte(eventSpace) + buffer.WriteString(strings.Join(e.Params, string(eventSpace))) + } + + if len(e.Trailing) > 0 || e.EmptyTrailing { + buffer.WriteByte(eventSpace) + buffer.WriteByte(messagePrefix) + buffer.WriteString(e.Trailing) + } + + // We need the limit the buffer length. + if buffer.Len() > (maxLength) { + buffer.Truncate(maxLength) + } + + out := buffer.Bytes() + + // Strip newlines and carriage returns. + for i := 0; i < len(out); i++ { + if out[i] == 0x0A || out[i] == 0x0D { + out = append(out[:i], out[i+1:]...) + i-- // Decrease the index so we can pick up where we left off. + } + } + + return out +} + +// String returns a string representation of this event. Strips all newlines +// and carriage returns. +func (e *Event) String() string { + return string(e.Bytes()) +} + +// Pretty returns a prettified string of the event. If the event doesn't +// support prettification, ok is false. Pretty is not just useful to make +// 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 { + return "", false + } + + if e.Command == ERROR { + return fmt.Sprintf("[*] an error occurred: %s", e.Trailing), true + } + + if e.Source == nil { + if e.Command != PRIVMSG && e.Command != NOTICE { + return "", false + } + + if len(e.Params) > 0 && len(e.Trailing) > 0 { + return fmt.Sprintf("[>] writing %s [%s]: %s", strings.ToLower(e.Command), strings.Join(e.Params, ", "), e.Trailing), true + } else if len(e.Params) > 0 { + return fmt.Sprintf("[>] writing %s [%s]", strings.ToLower(e.Command), strings.Join(e.Params, ", ")), true + } else if len(e.Trailing) > 0 { + return fmt.Sprintf("[>] writing %s: %s", strings.ToLower(e.Command), e.Trailing), true + } + + return "", false + } + + if e.Command == INITIALIZED { + return fmt.Sprintf("[*] connection to %s initialized", e.Trailing), true + } + + if e.Command == CONNECTED { + return fmt.Sprintf("[*] successfully connected to %s", e.Trailing), true + } + + if (e.Command == PRIVMSG || e.Command == NOTICE) && len(e.Params) > 0 { + if ctcp := decodeCTCP(e); ctcp != nil { + if ctcp.Reply { + return + } + + return fmt.Sprintf("[*] CTCP query from %s: %s%s", ctcp.Source.Name, ctcp.Command, " "+ctcp.Text), true + } + return fmt.Sprintf("[%s] (%s) %s", strings.Join(e.Params, ","), e.Source.Name, e.Trailing), true + } + + if e.Command == RPL_MOTD || e.Command == RPL_MOTDSTART || + e.Command == RPL_WELCOME || e.Command == RPL_YOURHOST || + e.Command == RPL_CREATED || e.Command == RPL_LUSERCLIENT { + return "[*] " + e.Trailing, true + } + + if e.Command == JOIN && len(e.Params) > 0 { + return fmt.Sprintf("[*] %s (%s) has joined %s", e.Source.Name, e.Source.Host, e.Params[0]), true + } + + if e.Command == PART && len(e.Params) > 0 { + return fmt.Sprintf("[*] %s (%s) has left %s (%s)", e.Source.Name, e.Source.Host, e.Params[0], e.Trailing), true + } + + if e.Command == QUIT { + return fmt.Sprintf("[*] %s has quit (%s)", e.Source.Name, e.Trailing), true + } + + if e.Command == KICK && len(e.Params) == 2 { + return fmt.Sprintf("[%s] *** %s has kicked %s: %s", e.Params[0], e.Source.Name, e.Params[1], e.Trailing), true + } + + if e.Command == NICK && len(e.Params) == 1 { + return fmt.Sprintf("[*] %s is now known as %s", e.Source.Name, e.Params[0]), true + } + + if e.Command == TOPIC && len(e.Params) > 0 { + return fmt.Sprintf("[%s] *** %s has set the topic to: %s", e.Params[len(e.Params)-1], e.Source.Name, e.Trailing), true + } + + if e.Command == MODE && len(e.Params) > 2 { + return fmt.Sprintf("[%s] *** %s set modes: %s", e.Params[0], e.Source.Name, strings.Join(e.Params[1:], " ")), true + } + + if e.Command == CAP_AWAY { + if len(e.Trailing) > 0 { + return fmt.Sprintf("[*] %s is now away: %s", e.Source.Name, e.Trailing), true + } + + return fmt.Sprintf("[*] %s is no longer away", e.Source.Name), true + } + + if e.Command == CAP_CHGHOST && len(e.Params) == 2 { + return fmt.Sprintf("[*] %s has changed their host to %s (was %s)", e.Source.Name, e.Params[1], e.Source.Host), true + } + + if e.Command == CAP_ACCOUNT && len(e.Params) == 1 { + if e.Params[0] == "*" { + return fmt.Sprintf("[*] %s has become un-authenticated", e.Source.Name), true + } + + return fmt.Sprintf("[*] %s has authenticated for account: %s", e.Source.Name, e.Params[0]), true + } + + if e.Command == RPL_TOPIC && len(e.Params) > 0 && len(e.Trailing) > 0 { + return fmt.Sprintf("[*] topic for %s is: %s", e.Params[len(e.Params)-1], e.Trailing), true + } + + return "", false +} + +// IsAction checks to see if the event is a PRIVMSG, and is an ACTION (/me). +func (e *Event) IsAction() bool { + if e.Source == nil || e.Command != PRIVMSG || len(e.Trailing) < 9 { + return false + } + + if !strings.HasPrefix(e.Trailing, "\001ACTION") || e.Trailing[len(e.Trailing)-1] != ctcpDelim { + return false + } + + return true +} + +// IsFromChannel checks to see if a message was from a channel (rather than +// a private message). +func (e *Event) IsFromChannel() bool { + if e.Source == nil || e.Command != PRIVMSG || len(e.Params) < 1 { + return false + } + + if !IsValidChannel(e.Params[0]) { + return false + } + + return true +} + +// IsFromUser checks to see if a message was from a user (rather than a +// channel). +func (e *Event) IsFromUser() bool { + if e.Source == nil || e.Command != PRIVMSG || len(e.Params) < 1 { + return false + } + + if !IsValidNick(e.Params[0]) { + return false + } + + return true +} + +// StripAction returns the stripped version of the action encoding from a +// PRIVMSG ACTION (/me). +func (e *Event) StripAction() string { + if !e.IsAction() { + return e.Trailing + } + + return e.Trailing[8 : len(e.Trailing)-1] +} + +const ( + messagePrefix byte = 0x3A // ":" -- prefix or last argument + prefixIdent byte = 0x21 // "!" -- username + prefixHost byte = 0x40 // "@" -- hostname +) + +// Source represents the sender of an IRC event, see RFC1459 section 2.3.1. +// <servername> | <nick> [ '!' <user> ] [ '@' <host> ] +type Source struct { + // Name is the nickname, server name, or service name. + Name string `json:"name"` + // Ident is commonly known as the "user". + Ident string `json:"ident"` + // Host is the hostname or IP address of the user/service. Is not accurate + // due to how IRC servers can spoof hostnames. + Host string `json:"host"` +} + +// Copy returns a deep copy of Source. +func (s *Source) Copy() *Source { + if s == nil { + return nil + } + + newSource := &Source{ + Name: s.Name, + Ident: s.Ident, + Host: s.Host, + } + + return newSource +} + +// ParseSource takes a string and attempts to create a Source struct. +func ParseSource(raw string) (src *Source) { + src = new(Source) + + user := strings.IndexByte(raw, prefixIdent) + host := strings.IndexByte(raw, prefixHost) + + switch { + case user > 0 && host > user: + src.Name = raw[:user] + src.Ident = raw[user+1 : host] + src.Host = raw[host+1:] + case user > 0: + src.Name = raw[:user] + src.Ident = raw[user+1:] + case host > 0: + src.Name = raw[:host] + src.Host = raw[host+1:] + default: + src.Name = raw + } + + return src +} + +// Len calculates the length of the string representation of prefix +func (s *Source) Len() (length int) { + length = len(s.Name) + if len(s.Ident) > 0 { + length = 1 + length + len(s.Ident) + } + if len(s.Host) > 0 { + length = 1 + length + len(s.Host) + } + + return +} + +// Bytes returns a []byte representation of source. +func (s *Source) Bytes() []byte { + buffer := new(bytes.Buffer) + s.writeTo(buffer) + + return buffer.Bytes() +} + +// String returns a string representation of source. +func (s *Source) String() (out string) { + out = s.Name + if len(s.Ident) > 0 { + out = out + string(prefixIdent) + s.Ident + } + if len(s.Host) > 0 { + out = out + string(prefixHost) + s.Host + } + + return +} + +// IsHostmask returns true if source looks like a user hostmask. +func (s *Source) IsHostmask() bool { + return len(s.Ident) > 0 && len(s.Host) > 0 +} + +// IsServer returns true if this source looks like a server name. +func (s *Source) IsServer() bool { + return len(s.Ident) <= 0 && len(s.Host) <= 0 +} + +// writeTo is an utility function to write the source to the bytes.Buffer +// in Event.String(). +func (s *Source) writeTo(buffer *bytes.Buffer) { + buffer.WriteString(s.Name) + if len(s.Ident) > 0 { + buffer.WriteByte(prefixIdent) + buffer.WriteString(s.Ident) + } + if len(s.Host) > 0 { + buffer.WriteByte(prefixHost) + buffer.WriteString(s.Host) + } + + return +} diff --git a/vendor/github.com/lrstanley/girc/format.go b/vendor/github.com/lrstanley/girc/format.go new file mode 100644 index 00000000..5d32f8ad --- /dev/null +++ b/vendor/github.com/lrstanley/girc/format.go @@ -0,0 +1,350 @@ +// 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" + "regexp" + "strings" +) + +const ( + fmtOpenChar = 0x7B // { + fmtCloseChar = 0x7D // } +) + +var fmtColors = map[string]int{ + "white": 0, + "black": 1, + "blue": 2, + "navy": 2, + "green": 3, + "red": 4, + "brown": 5, + "maroon": 5, + "purple": 6, + "gold": 7, + "olive": 7, + "orange": 7, + "yellow": 8, + "lightgreen": 9, + "lime": 9, + "teal": 10, + "cyan": 11, + "lightblue": 12, + "royal": 12, + "fuchsia": 13, + "lightpurple": 13, + "pink": 13, + "gray": 14, + "grey": 14, + "lightgrey": 15, + "silver": 15, +} + +var fmtCodes = map[string]string{ + "bold": "\x02", + "b": "\x02", + "italic": "\x1d", + "i": "\x1d", + "reset": "\x0f", + "r": "\x0f", + "clear": "\x03", + "c": "\x03", // Clears formatting. + "reverse": "\x16", + "underline": "\x1f", + "ul": "\x1f", + "ctcp": "\x01", // CTCP/ACTION delimiter. +} + +// Fmt takes format strings like "{red}" or "{red,blue}" (for background +// colors) and turns them into the resulting ASCII format/color codes for IRC. +// See format.go for the list of supported format codes allowed. +// +// For example: +// +// client.Message("#channel", Fmt("{red}{b}Hello {red,blue}World{c}")) +func Fmt(text string) string { + var last = -1 + for i := 0; i < len(text); i++ { + if text[i] == fmtOpenChar { + last = i + continue + } + + if text[i] == fmtCloseChar && last > -1 { + code := strings.ToLower(text[last+1 : i]) + + // Check to see if they're passing in a second (background) color + // as {fgcolor,bgcolor}. + var secondary string + if com := strings.Index(code, ","); com > -1 { + secondary = code[com+1:] + code = code[:com] + } + + var repl string + + if color, ok := fmtColors[code]; ok { + repl = fmt.Sprintf("\x03%02d", color) + } + + if repl != "" && secondary != "" { + if color, ok := fmtColors[secondary]; ok { + repl += fmt.Sprintf(",%02d", color) + } + } + + if repl == "" { + if fmtCode, ok := fmtCodes[code]; ok { + repl = fmtCode + } + } + + next := len(text[:last]+repl) - 1 + text = text[:last] + repl + text[i+1:] + last = -1 + i = next + continue + } + + if last > -1 { + // A-Z, a-z, and "," + if text[i] != 0x2c && (text[i] <= 0x41 || text[i] >= 0x5a) && (text[i] <= 0x61 || text[i] >= 0x7a) { + last = -1 + continue + } + } + } + + return text +} + +// TrimFmt strips all "{fmt}" formatting strings from the input text. +// See Fmt() for more information. +func TrimFmt(text string) string { + for color := range fmtColors { + text = strings.Replace(text, "{"+color+"}", "", -1) + } + for code := range fmtCodes { + text = strings.Replace(text, "{"+code+"}", "", -1) + } + + return text +} + +// This is really the only fastest way of doing this (marginably better than +// actually trying to parse it manually.) +var reStripColor = regexp.MustCompile(`\x03([019]?[0-9](,[019]?[0-9])?)?`) + +// StripRaw tries to strip all ASCII format codes that are used for IRC. +// Primarily, foreground/background colors, and other control bytes like +// reset, bold, italic, reverse, etc. This also is done in a specific way +// in order to ensure no truncation of other non-irc formatting. +func StripRaw(text string) string { + text = reStripColor.ReplaceAllString(text, "") + + for _, code := range fmtCodes { + text = strings.Replace(text, code, "", -1) + } + + return text +} + +// IsValidChannel validates if channel is an RFC complaint channel or not. +// +// NOTE: If you are using this to validate a channel that contains a channel +// ID, (!<channelid>NAME), this only supports the standard 5 character length. +// +// NOTE: If you do not need to validate against servers that support unicode, +// you may want to ensure that all channel chars are within the range of +// all ASCII printable chars. This function will NOT do that for +// compatibility reasons. +// +// channel = ( "#" / "+" / ( "!" channelid ) / "&" ) chanstring +// [ ":" chanstring ] +// chanstring = 0x01-0x07 / 0x08-0x09 / 0x0B-0x0C / 0x0E-0x1F / 0x21-0x2B +// chanstring = / 0x2D-0x39 / 0x3B-0xFF +// ; any octet except NUL, BELL, CR, LF, " ", "," and ":" +// channelid = 5( 0x41-0x5A / digit ) ; 5( A-Z / 0-9 ) +func IsValidChannel(channel string) bool { + if len(channel) <= 1 || len(channel) > 50 { + return false + } + + // #, +, !<channelid>, or & + // Including "*" in the prefix list, as this is commonly used (e.g. ZNC) + if bytes.IndexByte([]byte{0x21, 0x23, 0x26, 0x2A, 0x2B}, channel[0]) == -1 { + return false + } + + // !<channelid> -- not very commonly supported, but we'll check it anyway. + // The ID must be 5 chars. This means min-channel size should be: + // 1 (prefix) + 5 (id) + 1 (+, channel name) + // On some networks, this may be extended with ISUPPORT capabilities, + // however this is extremely uncommon. + if channel[0] == 0x21 { + if len(channel) < 7 { + return false + } + + // check for valid ID + for i := 1; i < 6; i++ { + if (channel[i] < 0x30 || channel[i] > 0x39) && (channel[i] < 0x41 || channel[i] > 0x5A) { + return false + } + } + } + + // Check for invalid octets here. + bad := []byte{0x00, 0x07, 0x0D, 0x0A, 0x20, 0x2C, 0x3A} + for i := 1; i < len(channel); i++ { + if bytes.IndexByte(bad, channel[i]) != -1 { + return false + } + } + + return true +} + +// IsValidNick validates an IRC nickame. Note that this does not validate +// IRC nickname length. +// +// nickname = ( letter / special ) *8( letter / digit / special / "-" ) +// letter = 0x41-0x5A / 0x61-0x7A +// digit = 0x30-0x39 +// special = 0x5B-0x60 / 0x7B-0x7D +func IsValidNick(nick string) bool { + if len(nick) <= 0 { + return false + } + + nick = ToRFC1459(nick) + + // Check the first index. Some characters aren't allowed for the first + // index of an IRC nickname. + if nick[0] < 0x41 || nick[0] > 0x7D { + // a-z, A-Z, and _\[]{}^| + return false + } + + for i := 1; i < len(nick); i++ { + if (nick[i] < 0x41 || nick[i] > 0x7D) && (nick[i] < 0x30 || nick[i] > 0x39) && nick[i] != 0x2D { + // a-z, A-Z, 0-9, -, and _\[]{}^| + return false + } + } + + return true +} + +// IsValidUser validates an IRC ident/username. Note that this does not +// validate IRC ident length. +// +// The validation checks are much like what characters are allowed with an +// IRC nickname (see IsValidNick()), however an ident/username can: +// +// 1. Must either start with alphanumberic char, or "~" then alphanumberic +// char. +// +// 2. Contain a "." (period), for use with "first.last". Though, this may +// not be supported on all networks. Some limit this to only a single period. +// +// Per RFC: +// user = 1*( %x01-09 / %x0B-0C / %x0E-1F / %x21-3F / %x41-FF ) +// ; any octet except NUL, CR, LF, " " and "@" +func IsValidUser(name string) bool { + if len(name) <= 0 { + return false + } + + name = ToRFC1459(name) + + // "~" is prepended (commonly) if there was no ident server response. + if name[0] == 0x7E { + // Means name only contained "~". + if len(name) < 2 { + return false + } + + name = name[1:] + } + + // Check to see if the first index is alphanumeric. + if (name[0] < 0x41 || name[0] > 0x4A) && (name[0] < 0x61 || name[0] > 0x7A) && (name[0] < 0x30 || name[0] > 0x39) { + return false + } + + for i := 1; i < len(name); i++ { + if (name[i] < 0x41 || name[i] > 0x7D) && (name[i] < 0x30 || name[i] > 0x39) && name[i] != 0x2D && name[i] != 0x2E { + // a-z, A-Z, 0-9, -, and _\[]{}^| + return false + } + } + + return true +} + +// ToRFC1459 converts a string to the stripped down conversion within RFC +// 1459. This will do things like replace an "A" with an "a", "[]" with "{}", +// and so forth. Useful to compare two nicknames or channels. +func ToRFC1459(input string) (out string) { + for i := 0; i < len(input); i++ { + if input[i] >= 65 && input[i] <= 94 { + out += string(rune(input[i]) + 32) + } else { + out += string(input[i]) + } + } + + return out +} + +const globChar = "*" + +// Glob will test a string pattern, potentially containing globs, against a +// string. The glob character is *. +func Glob(input, match string) bool { + // Empty pattern. + if match == "" { + return input == match + } + + // If a glob, match all. + if match == globChar { + return true + } + + parts := strings.Split(match, globChar) + + if len(parts) == 1 { + // No globs, test for equality. + return input == match + } + + leadingGlob, trailingGlob := strings.HasPrefix(match, globChar), strings.HasSuffix(match, globChar) + last := len(parts) - 1 + + // Check prefix first. + if !leadingGlob && !strings.HasPrefix(input, parts[0]) { + return false + } + + // Check middle section. + for i := 1; i < last; i++ { + if !strings.Contains(input, parts[i]) { + return false + } + + // Trim already-evaluated text from input during loop over match + // text. + idx := strings.Index(input, parts[i]) + len(parts[i]) + input = input[idx:] + } + + // Check suffix last. + return trailingGlob || strings.HasSuffix(input, parts[last]) +} diff --git a/vendor/github.com/lrstanley/girc/handler.go b/vendor/github.com/lrstanley/girc/handler.go new file mode 100644 index 00000000..f0c737f2 --- /dev/null +++ b/vendor/github.com/lrstanley/girc/handler.go @@ -0,0 +1,484 @@ +// 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 ( + "fmt" + "log" + "math/rand" + "runtime" + "runtime/debug" + "strings" + "sync" + "time" +) + +// RunHandlers manually runs handlers for a given event. +func (c *Client) RunHandlers(event *Event) { + if event == nil { + return + } + + // Log the event. + c.debug.Print("< " + StripRaw(event.String())) + if c.Config.Out != nil { + if pretty, ok := event.Pretty(); ok { + fmt.Fprintln(c.Config.Out, StripRaw(pretty)) + } + } + + // Regular wildcard handlers. + c.Handlers.exec(ALL_EVENTS, c, event.Copy()) + + // Then regular handlers. + c.Handlers.exec(event.Command, c, event.Copy()) + + // Check if it's a CTCP. + if ctcp := decodeCTCP(event.Copy()); ctcp != nil { + // Execute it. + c.CTCP.call(c, ctcp) + } +} + +// Handler is lower level implementation of a handler. See +// Caller.AddHandler() +type Handler interface { + Execute(*Client, Event) +} + +// HandlerFunc is a type that represents the function necessary to +// implement Handler. +type HandlerFunc func(client *Client, event Event) + +// Execute calls the HandlerFunc with the sender and irc message. +func (f HandlerFunc) Execute(client *Client, event Event) { + f(client, event) +} + +// Caller manages internal and external (user facing) handlers. +type Caller struct { + // mu is the mutex that should be used when accessing handlers. + mu sync.RWMutex + + // external/internal keys are of structure: + // map[COMMAND][CUID]Handler + // Also of note: "COMMAND" should always be uppercase for normalization. + + // external is a map of user facing handlers. + external map[string]map[string]Handler + // internal is a map of internally used handlers for the client. + internal map[string]map[string]Handler + // debug is the clients logger used for debugging. + debug *log.Logger +} + +// newCaller creates and initializes a new handler. +func newCaller(debugOut *log.Logger) *Caller { + c := &Caller{ + external: map[string]map[string]Handler{}, + internal: map[string]map[string]Handler{}, + debug: debugOut, + } + + return c +} + +// Len returns the total amount of user-entered registered handlers. +func (c *Caller) Len() int { + var total int + + c.mu.RLock() + for command := range c.external { + total += len(c.external[command]) + } + c.mu.RUnlock() + + return total +} + +// Count is much like Caller.Len(), however it counts the number of +// registered handlers for a given command. +func (c *Caller) Count(cmd string) int { + var total int + + cmd = strings.ToUpper(cmd) + + c.mu.RLock() + for command := range c.external { + if command == cmd { + total += len(c.external[command]) + } + } + c.mu.RUnlock() + + return total +} + +func (c *Caller) String() string { + var total int + + c.mu.RLock() + for cmd := range c.internal { + total += len(c.internal[cmd]) + } + c.mu.RUnlock() + + return fmt.Sprintf("<Caller external:%d internal:%d>", c.Len(), total) +} + +const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +// cuid generates a unique UID string for each handler for ease of removal. +func (c *Caller) cuid(cmd string, n int) (cuid, uid string) { + b := make([]byte, n) + + for i := range b { + b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))] + } + + return cmd + ":" + string(b), string(b) +} + +// cuidToID allows easy mapping between a generated cuid and the caller +// external/internal handler maps. +func (c *Caller) cuidToID(input string) (cmd, uid string) { + i := strings.IndexByte(input, 0x3A) + if i < 0 { + return "", "" + } + + return input[:i], input[i+1:] +} + +type execStack struct { + Handler + cuid string +} + +// exec executes all handlers pertaining to specified event. Internal first, +// then external. +// +// Please note that there is no specific order/priority for which the +// handler types themselves or the handlers are executed. +func (c *Caller) exec(command string, client *Client, event *Event) { + // Build a stack of handlers which can be executed concurrently. + var stack []execStack + + c.mu.RLock() + // Get internal handlers first. + if _, ok := c.internal[command]; ok { + for cuid := range c.internal[command] { + stack = append(stack, execStack{c.internal[command][cuid], cuid}) + } + } + + // Aaand then external handlers. + if _, ok := c.external[command]; ok { + for cuid := range c.external[command] { + stack = append(stack, execStack{c.external[command][cuid], cuid}) + } + } + c.mu.RUnlock() + + // Run all handlers concurrently across the same event. This should + // still help prevent mis-ordered events, while speeding up the + // execution speed. + var wg sync.WaitGroup + wg.Add(len(stack)) + for i := 0; i < len(stack); i++ { + go func(index int) { + c.debug.Printf("executing handler %s for event %s (%d of %d)", stack[index].cuid, command, index+1, len(stack)) + start := time.Now() + + // If they want to catch any panics, add to defer stack. + if client.Config.RecoverFunc != nil { + defer recoverHandlerPanic(client, event, stack[index].cuid, 3) + } + + stack[index].Execute(client, *event) + + c.debug.Printf("execution of %s took %s (%d of %d)", stack[index].cuid, time.Since(start), index+1, len(stack)) + wg.Done() + }(i) + } + + // Wait for all of the handlers to complete. Not doing this may cause + // new events from becoming ahead of older handlers. + wg.Wait() +} + +// ClearAll clears all external handlers currently setup within the client. +// This ignores internal handlers. +func (c *Caller) ClearAll() { + c.mu.Lock() + c.external = map[string]map[string]Handler{} + c.mu.Unlock() + + c.debug.Print("cleared all external handlers") +} + +// clearInternal clears all internal handlers currently setup within the +// client. +func (c *Caller) clearInternal() { + c.mu.Lock() + c.internal = map[string]map[string]Handler{} + c.mu.Unlock() + + c.debug.Print("cleared all internal handlers") +} + +// Clear clears all of the handlers for the given event. +// This ignores internal handlers. +func (c *Caller) Clear(cmd string) { + cmd = strings.ToUpper(cmd) + + c.mu.Lock() + if _, ok := c.external[cmd]; ok { + delete(c.external, cmd) + } + c.mu.Unlock() + + c.debug.Printf("cleared external handlers for %s", cmd) +} + +// Remove removes the handler with cuid from the handler stack. success +// indicates that it existed, and has been removed. If not success, it +// wasn't a registered handler. +func (c *Caller) Remove(cuid string) (success bool) { + c.mu.Lock() + success = c.remove(cuid) + c.mu.Unlock() + + return success +} + +// remove is much like Remove, however is NOT concurrency safe. Lock Caller.mu +// on your own. +func (c *Caller) remove(cuid string) (success bool) { + cmd, uid := c.cuidToID(cuid) + if len(cmd) == 0 || len(uid) == 0 { + return false + } + + // Check if the irc command/event has any handlers on it. + if _, ok := c.external[cmd]; !ok { + return false + } + + // Check to see if it's actually a registered handler. + if _, ok := c.external[cmd][uid]; !ok { + return false + } + + delete(c.external[cmd], uid) + c.debug.Printf("removed handler %s", cuid) + + // Assume success. + return true +} + +// sregister is much like Caller.register(), except that it safely locks +// the Caller mutex. +func (c *Caller) sregister(internal bool, cmd string, handler Handler) (cuid string) { + c.mu.Lock() + cuid = c.register(internal, cmd, handler) + c.mu.Unlock() + + return cuid +} + +// register will register a handler in the internal tracker. Unsafe (you +// must lock c.mu yourself!) +func (c *Caller) register(internal bool, cmd string, handler Handler) (cuid string) { + var uid string + + cmd = strings.ToUpper(cmd) + + if internal { + if _, ok := c.internal[cmd]; !ok { + c.internal[cmd] = map[string]Handler{} + } + + cuid, uid = c.cuid(cmd, 20) + c.internal[cmd][uid] = handler + } else { + if _, ok := c.external[cmd]; !ok { + c.external[cmd] = map[string]Handler{} + } + + cuid, uid = c.cuid(cmd, 20) + c.external[cmd][uid] = handler + } + + _, file, line, _ := runtime.Caller(3) + + c.debug.Printf("registering handler for %q with cuid %q (internal: %t) from: %s:%d", cmd, cuid, internal, file, line) + + return cuid +} + +// AddHandler registers a handler (matching the handler interface) for the +// given event. cuid is the handler uid which can be used to remove the +// handler with Caller.Remove(). +func (c *Caller) AddHandler(cmd string, handler Handler) (cuid string) { + return c.sregister(false, cmd, handler) +} + +// Add registers the handler function for the given event. cuid is the +// handler uid which can be used to remove the handler with Caller.Remove(). +func (c *Caller) Add(cmd string, handler func(client *Client, event Event)) (cuid string) { + return c.sregister(false, cmd, HandlerFunc(handler)) +} + +// AddBg registers the handler function for the given event and executes it +// in a go-routine. cuid is the handler uid which can be used to remove the +// handler with Caller.Remove(). +func (c *Caller) AddBg(cmd string, handler func(client *Client, event Event)) (cuid string) { + return c.sregister(false, cmd, HandlerFunc(func(client *Client, event Event) { + // Setting up background-based handlers this way allows us to get + // clean call stacks for use with panic recovery. + go func() { + // If they want to catch any panics, add to defer stack. + if client.Config.RecoverFunc != nil { + defer recoverHandlerPanic(client, &event, "goroutine", 3) + } + + handler(client, event) + }() + })) +} + +// AddTmp adds a "temporary" handler, which is good for one-time or few-time +// uses. This supports a deadline and/or manual removal, as this differs +// much from how normal handlers work. An example of a good use for this +// would be to capture the entire output of a multi-response query to the +// server. (e.g. LIST, WHOIS, etc) +// +// The supplied handler is able to return a boolean, which if true, will +// remove the handler from the handler stack. +// +// Additionally, AddTmp has a useful option, deadline. When set to greater +// than 0, deadline will be the amount of time that passes before the handler +// is removed from the stack, regardless if the handler returns true or not. +// This is useful in that it ensures that the handler is cleaned up if the +// server does not respond appropriately, or takes too long to respond. +// +// Note that handlers supplied with AddTmp are executed in a goroutine to +// ensure that they are not blocking other handlers. Additionally, use cuid +// with Caller.Remove() to prematurely remove the handler from the stack, +// bypassing the timeout or waiting for the handler to return that it wants +// to be removed from the stack. +func (c *Caller) AddTmp(cmd string, deadline time.Duration, handler func(client *Client, event Event) bool) (cuid string, done chan struct{}) { + var uid string + cuid, uid = c.cuid(cmd, 20) + + done = make(chan struct{}) + + c.mu.Lock() + if _, ok := c.external[cmd]; !ok { + c.external[cmd] = map[string]Handler{} + } + c.external[cmd][uid] = HandlerFunc(func(client *Client, event Event) { + // Setting up background-based handlers this way allows us to get + // clean call stacks for use with panic recovery. + go func() { + // If they want to catch any panics, add to defer stack. + if client.Config.RecoverFunc != nil { + defer recoverHandlerPanic(client, &event, "tmp-goroutine", 3) + } + + remove := handler(client, event) + if remove { + if ok := c.Remove(cuid); ok { + close(done) + } + } + }() + }) + c.mu.Unlock() + + if deadline > 0 { + go func() { + <-time.After(deadline) + if ok := c.Remove(cuid); ok { + close(done) + } + }() + } + + return cuid, done +} + +// recoverHandlerPanic is used to catch all handler panics, and re-route +// them if necessary. +func recoverHandlerPanic(client *Client, event *Event, id string, skip int) { + perr := recover() + if perr == nil { + return + } + + var file string + var line int + var ok bool + + _, file, line, ok = runtime.Caller(skip) + + err := &HandlerError{ + Event: *event, + ID: id, + File: file, + Line: line, + Panic: perr, + Stack: debug.Stack(), + callOk: ok, + } + + client.Config.RecoverFunc(client, err) + return +} + +// HandlerError is the error returned when a panic is intentionally recovered +// from. It contains useful information like the handler identifier (if +// applicable), filename, line in file where panic occurred, the call +// trace, and original event. +type HandlerError struct { + Event Event // Event is the event that caused the error. + ID string // ID is the CUID of the handler. + File string // File is the file from where the panic originated. + Line int // Line number where panic originated. + Panic interface{} // Panic is the error that was passed to panic(). + Stack []byte // Stack is the call stack. Note you may have to skip 1 or 2 due to debug functions. + callOk bool +} + +// Error returns a prettified version of HandlerError, containing ID, file, +// line, and basic error string. +func (e *HandlerError) Error() string { + if e.callOk { + return fmt.Sprintf("panic during handler [%s] execution in %s:%d: %s", e.ID, e.File, e.Line, e.Panic) + } + + return fmt.Sprintf("panic during handler [%s] execution in unknown: %s", e.ID, e.Panic) +} + +// String returns the error that panic returned, as well as the entire call +// trace of where it originated. +func (e *HandlerError) String() string { + return fmt.Sprintf("panic: %s\n\n%s", e.Panic, string(e.Stack)) +} + +// DefaultRecoverHandler can be used with Config.RecoverFunc as a default +// catch-all for panics. This will log the error, and the call trace to the +// debug log (see Config.Debug), or os.Stdout if Config.Debug is unset. +func DefaultRecoverHandler(client *Client, err *HandlerError) { + if client.Config.Debug == nil { + fmt.Println(err.Error()) + fmt.Println(err.String()) + return + } + + client.debug.Println(err.Error()) + client.debug.Println(err.String()) +} diff --git a/vendor/github.com/lrstanley/girc/modes.go b/vendor/github.com/lrstanley/girc/modes.go new file mode 100644 index 00000000..c0ad7d11 --- /dev/null +++ b/vendor/github.com/lrstanley/girc/modes.go @@ -0,0 +1,550 @@ +// 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/json" + "strings" + "sync" +) + +// CMode represents a single step of a given mode change. +type CMode struct { + add bool // if it's a +, or -. + name byte // character representation of the given mode. + setting bool // if it's a setting (should be stored) or temporary (op/voice/etc). + args string // arguments to the mode, if arguments are supported. +} + +// Short returns a short representation of a mode without arguments. E.g. "+a", +// or "-b". +func (c *CMode) Short() string { + var status string + if c.add { + status = "+" + } else { + status = "-" + } + + return status + string(c.name) +} + +// String returns a string representation of a mode, including optional +// arguments. E.g. "+b user*!ident@host.*.com" +func (c *CMode) String() string { + if len(c.args) == 0 { + return c.Short() + } + + return c.Short() + " " + c.args +} + +// CModes is a representation of a set of modes. This may be the given state +// of a channel/user, or the given state changes to a given channel/user. +type CModes struct { + raw string // raw supported modes. + modesListArgs string // modes that add/remove users from lists and support args. + modesArgs string // modes that support args. + modesSetArgs string // modes that support args ONLY when set. + modesNoArgs string // modes that do not support args. + + prefixes string // user permission prefixes. these aren't a CMode.setting. + modes []CMode // the list of modes for this given state. +} + +// Copy returns a deep copy of CModes. +func (c *CModes) Copy() (nc CModes) { + nc = CModes{} + nc = *c + + nc.modes = make([]CMode, len(c.modes)) + + // Copy modes. + for i := 0; i < len(c.modes); i++ { + nc.modes[i] = c.modes[i] + } + + return nc +} + +// String returns a complete set of modes for this given state (change?). For +// example, "+a-b+cde some-arg". +func (c *CModes) String() string { + var out string + var args string + + if len(c.modes) > 0 { + out += "+" + } + + for i := 0; i < len(c.modes); i++ { + out += string(c.modes[i].name) + + if len(c.modes[i].args) > 0 { + args += " " + c.modes[i].args + } + } + + return out + args +} + +// HasMode checks if the CModes state has a given mode. E.g. "m", or "I". +func (c *CModes) HasMode(mode string) bool { + for i := 0; i < len(c.modes); i++ { + if string(c.modes[i].name) == mode { + return true + } + } + + return false +} + +// Get returns the arguments for a given mode within this session, if it +// supports args. +func (c *CModes) Get(mode string) (args string, ok bool) { + for i := 0; i < len(c.modes); i++ { + if string(c.modes[i].name) == mode { + if len(c.modes[i].args) == 0 { + return "", false + } + + return c.modes[i].args, true + } + } + + return "", false +} + +// hasArg checks to see if the mode supports arguments. What ones support this?: +// A = Mode that adds or removes a nick or address to a list. Always has a parameter. +// B = Mode that changes a setting and always has a parameter. +// C = Mode that changes a setting and only has a parameter when set. +// D = Mode that changes a setting and never has a parameter. +// Note: Modes of type A return the list when there is no parameter present. +// Note: Some clients assumes that any mode not listed is of type D. +// Note: Modes in PREFIX are not listed but could be considered type B. +func (c *CModes) hasArg(set bool, mode byte) (hasArgs, isSetting bool) { + if len(c.raw) < 1 { + return false, true + } + + if strings.IndexByte(c.modesListArgs, mode) > -1 { + return true, false + } + + if strings.IndexByte(c.modesArgs, mode) > -1 { + return true, true + } + + if strings.IndexByte(c.modesSetArgs, mode) > -1 { + if set { + return true, true + } + + return false, true + } + + if strings.IndexByte(c.prefixes, mode) > -1 { + return true, false + } + + return false, true +} + +// Apply merges two state changes, or one state change into a state of modes. +// For example, the latter would mean applying an incoming MODE with the modes +// stored for a channel. +func (c *CModes) Apply(modes []CMode) { + var new []CMode + + for j := 0; j < len(c.modes); j++ { + isin := false + for i := 0; i < len(modes); i++ { + if !modes[i].setting { + continue + } + if c.modes[j].name == modes[i].name && modes[i].add { + new = append(new, modes[i]) + isin = true + break + } + } + + if !isin { + new = append(new, c.modes[j]) + } + } + + for i := 0; i < len(modes); i++ { + if !modes[i].setting || !modes[i].add { + continue + } + + isin := false + for j := 0; j < len(new); j++ { + if modes[i].name == new[j].name { + isin = true + break + } + } + + if !isin { + new = append(new, modes[i]) + } + } + + c.modes = new +} + +// Parse parses a set of flags and args, returning the necessary list of +// mappings for the mode flags. +func (c *CModes) Parse(flags string, args []string) (out []CMode) { + // add is the mode state we're currently in. Adding, or removing modes. + add := true + var argCount int + + for i := 0; i < len(flags); i++ { + if flags[i] == 0x2B { + add = true + continue + } + if flags[i] == 0x2D { + add = false + continue + } + + mode := CMode{ + name: flags[i], + add: add, + } + + hasArgs, isSetting := c.hasArg(add, flags[i]) + if hasArgs && len(args) >= argCount+1 { + mode.args = args[argCount] + argCount++ + } + mode.setting = isSetting + + out = append(out, mode) + } + + return out +} + +// NewCModes returns a new CModes reference. channelModes and userPrefixes +// would be something you see from the server's "CHANMODES" and "PREFIX" +// ISUPPORT capability messages (alternatively, fall back to the standard) +// DefaultPrefixes and ModeDefaults. +func NewCModes(channelModes, userPrefixes string) CModes { + split := strings.SplitN(channelModes, ",", 4) + if len(split) != 4 { + for i := len(split); i < 4; i++ { + split = append(split, "") + } + } + + return CModes{ + raw: channelModes, + modesListArgs: split[0], + modesArgs: split[1], + modesSetArgs: split[2], + modesNoArgs: split[3], + + prefixes: userPrefixes, + modes: []CMode{}, + } +} + +// IsValidChannelMode validates a channel mode (CHANMODES). +func IsValidChannelMode(raw string) bool { + if len(raw) < 1 { + return false + } + + for i := 0; i < len(raw); i++ { + // Allowed are: ",", A-Z and a-z. + if raw[i] != 0x2C && (raw[i] < 0x41 || raw[i] > 0x5A) && (raw[i] < 0x61 || raw[i] > 0x7A) { + return false + } + } + + return true +} + +// isValidUserPrefix validates a list of ISUPPORT-style user prefixes (PREFIX). +func isValidUserPrefix(raw string) bool { + if len(raw) < 1 { + return false + } + + if raw[0] != 0x28 { // (. + return false + } + + var keys, rep int + var passedKeys bool + + // Skip the first one as we know it's (. + for i := 1; i < len(raw); i++ { + if raw[i] == 0x29 { // ). + passedKeys = true + continue + } + + if passedKeys { + rep++ + } else { + keys++ + } + } + + return keys == rep +} + +// parsePrefixes parses the mode character mappings from the symbols of a +// ISUPPORT-style user prefixes list (PREFIX). +func parsePrefixes(raw string) (modes, prefixes string) { + if !isValidUserPrefix(raw) { + return modes, prefixes + } + + i := strings.Index(raw, ")") + if i < 1 { + return modes, prefixes + } + + return raw[1:i], raw[i+1:] +} + +// handleMODE handles incoming MODE messages, and updates the tracking +// information for each channel, as well as if any of the modes affect user +// permissions. +func handleMODE(c *Client, e Event) { + // Check if it's a RPL_CHANNELMODEIS. + if e.Command == RPL_CHANNELMODEIS && len(e.Params) > 2 { + // RPL_CHANNELMODEIS sends the user as the first param, skip it. + e.Params = e.Params[1:] + } + // Should be at least MODE <target> <flags>, to be useful. As well, only + // tracking channel modes at the moment. + if len(e.Params) < 2 || !IsValidChannel(e.Params[0]) { + return + } + + c.state.RLock() + channel := c.state.lookupChannel(e.Params[0]) + if channel == nil { + c.state.RUnlock() + return + } + + flags := e.Params[1] + var args []string + if len(e.Params) > 2 { + args = append(args, e.Params[2:]...) + } + + modes := channel.Modes.Parse(flags, args) + channel.Modes.Apply(modes) + + // Loop through and update users modes as necessary. + for i := 0; i < len(modes); i++ { + if modes[i].setting || len(modes[i].args) == 0 { + continue + } + + user := c.state.lookupUser(modes[i].args) + if user != nil { + perms, _ := user.Perms.Lookup(channel.Name) + perms.setFromMode(modes[i]) + user.Perms.set(channel.Name, perms) + } + } + + c.state.RUnlock() + c.state.notify(c, UPDATE_STATE) +} + +// chanModes returns the ISUPPORT list of server-supported channel modes, +// alternatively falling back to ModeDefaults. +func (s *state) chanModes() string { + if modes, ok := s.serverOptions["CHANMODES"]; ok && IsValidChannelMode(modes) { + return modes + } + + return ModeDefaults +} + +// userPrefixes returns the ISUPPORT list of server-supported user prefixes. +// This includes mode characters, as well as user prefix symbols. Falls back +// to DefaultPrefixes if not server-supported. +func (s *state) userPrefixes() string { + if prefix, ok := s.serverOptions["PREFIX"]; ok && isValidUserPrefix(prefix) { + return prefix + } + + return DefaultPrefixes +} + +// UserPerms contains all of the permissions for each channel the user is +// in. +type UserPerms struct { + mu sync.RWMutex + channels map[string]Perms +} + +// Copy returns a deep copy of the channel permissions. +func (p *UserPerms) Copy() (perms *UserPerms) { + np := &UserPerms{ + channels: make(map[string]Perms), + } + + p.mu.RLock() + for key := range p.channels { + np.channels[key] = p.channels[key] + } + p.mu.RUnlock() + + return np +} + +// MarshalJSON implements json.Marshaler. +func (p *UserPerms) MarshalJSON() ([]byte, error) { + p.mu.Lock() + out, err := json.Marshal(&p.channels) + p.mu.Unlock() + + return out, err +} + +// Lookup looks up the users permissions for a given channel. ok is false +// if the user is not in the given channel. +func (p *UserPerms) Lookup(channel string) (perms Perms, ok bool) { + p.mu.RLock() + perms, ok = p.channels[ToRFC1459(channel)] + p.mu.RUnlock() + + return perms, ok +} + +func (p *UserPerms) set(channel string, perms Perms) { + p.mu.Lock() + p.channels[ToRFC1459(channel)] = perms + p.mu.Unlock() +} + +func (p *UserPerms) remove(channel string) { + p.mu.Lock() + delete(p.channels, ToRFC1459(channel)) + p.mu.Unlock() +} + +// Perms contains all channel-based user permissions. The minimum op, and +// voice should be supported on all networks. This also supports non-rfc +// Owner, Admin, and HalfOp, if the network has support for it. +type Perms struct { + // Owner (non-rfc) indicates that the user has full permissions to the + // channel. More than one user can have owner permission. + Owner bool `json:"owner"` + // Admin (non-rfc) is commonly given to users that are trusted enough + // to manage channel permissions, as well as higher level service settings. + Admin bool `json:"admin"` + // Op is commonly given to trusted users who can manage a given channel + // by kicking, and banning users. + Op bool `json:"op"` + // HalfOp (non-rfc) is commonly used to give users permissions like the + // ability to kick, without giving them greater abilities to ban all users. + HalfOp bool `json:"half_op"` + // Voice indicates the user has voice permissions, commonly given to known + // users, with very light trust, or to indicate a user is active. + Voice bool `json:"voice"` +} + +// IsAdmin indicates that the user has banning abilities, and are likely a +// very trustable user (e.g. op+). +func (m Perms) IsAdmin() bool { + if m.Owner || m.Admin || m.Op { + return true + } + + return false +} + +// IsTrusted indicates that the user at least has modes set upon them, higher +// than a regular joining user. +func (m Perms) IsTrusted() bool { + if m.IsAdmin() || m.HalfOp || m.Voice { + return true + } + + return false +} + +// reset resets the modes of a user. +func (m *Perms) reset() { + m.Owner = false + m.Admin = false + m.Op = false + m.HalfOp = false + m.Voice = false +} + +// set translates raw prefix characters into proper permissions. Only +// use this function when you have a session lock. +func (m *Perms) set(prefix string, append bool) { + if !append { + m.reset() + } + + for i := 0; i < len(prefix); i++ { + switch string(prefix[i]) { + case OwnerPrefix: + m.Owner = true + case AdminPrefix: + m.Admin = true + case OperatorPrefix: + m.Op = true + case HalfOperatorPrefix: + m.HalfOp = true + case VoicePrefix: + m.Voice = true + } + } +} + +// setFromMode sets user-permissions based on channel user mode chars. E.g. +// "o" being oper, "v" being voice, etc. +func (m *Perms) setFromMode(mode CMode) { + switch string(mode.name) { + case ModeOwner: + m.Owner = mode.add + case ModeAdmin: + m.Admin = mode.add + case ModeOperator: + m.Op = mode.add + case ModeHalfOperator: + m.HalfOp = mode.add + case ModeVoice: + m.Voice = mode.add + } +} + +// parseUserPrefix parses a raw mode line, like "@user" or "@+user". +func parseUserPrefix(raw string) (modes, nick string, success bool) { + for i := 0; i < len(raw); i++ { + char := string(raw[i]) + + if char == OwnerPrefix || char == AdminPrefix || char == HalfOperatorPrefix || + char == OperatorPrefix || char == VoicePrefix { + modes += char + continue + } + + // Assume we've gotten to the nickname part. + return modes, raw[i:], true + } + + return +} diff --git a/vendor/github.com/lrstanley/girc/state.go b/vendor/github.com/lrstanley/girc/state.go new file mode 100644 index 00000000..7c537028 --- /dev/null +++ b/vendor/github.com/lrstanley/girc/state.go @@ -0,0 +1,489 @@ +// 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 ( + "sort" + "sync" + "time" +) + +// state represents the actively-changing variables within the client +// runtime. Note that everything within the state should be guarded by the +// embedded sync.RWMutex. +type state struct { + sync.RWMutex + // nick, ident, and host are the internal trackers for our user. + nick, ident, host string + // channels represents all channels we're active in. + channels map[string]*Channel + // users represents all of users that we're tracking. + users map[string]*User + // enabledCap are the capabilities which are enabled for this connection. + enabledCap []string + // tmpCap are the capabilties which we share with the server during the + // last capability check. These will get sent once we have received the + // last capability list command from the server. + tmpCap []string + // serverOptions are the standard capabilities and configurations + // supported by the server at connection time. This also includes + // RPL_ISUPPORT entries. + serverOptions map[string]string + // motd is the servers message of the day. + motd string +} + +// notify sends state change notifications so users can update their refs +// when state changes. +func (s *state) notify(c *Client, ntype string) { + c.RunHandlers(&Event{Command: ntype}) +} + +// reset resets the state back to it's original form. +func (s *state) reset() { + s.Lock() + s.nick = "" + s.ident = "" + s.host = "" + s.channels = make(map[string]*Channel) + s.users = make(map[string]*User) + s.serverOptions = make(map[string]string) + s.enabledCap = []string{} + s.motd = "" + s.Unlock() +} + +// User represents an IRC user and the state attached to them. +type User struct { + // Nick is the users current nickname. rfc1459 compliant. + Nick string `json:"nick"` + // Ident is the users username/ident. Ident is commonly prefixed with a + // "~", which indicates that they do not have a identd server setup for + // authentication. + Ident string `json:"ident"` + // Host is the visible host of the users connection that the server has + // provided to us for their connection. May not always be accurate due to + // many networks spoofing/hiding parts of the hostname for privacy + // reasons. + Host string `json:"host"` + + // ChannelList is a sorted list of all channels that we are currently + // tracking the user in. Each channel name is rfc1459 compliant. See + // User.Channels() for a shorthand if you're looking for the *Channel + // version of the channel list. + ChannelList []string `json:"channels"` + + // FirstSeen represents the first time that the user was seen by the + // client for the given channel. Only usable if from state, not in past. + FirstSeen time.Time `json:"first_seen"` + // LastActive represents the last time that we saw the user active, + // which could be during nickname change, message, channel join, etc. + // Only usable if from state, not in past. + LastActive time.Time `json:"last_active"` + + // Perms are the user permissions applied to this user that affect the given + // channel. This supports non-rfc style modes like Admin, Owner, and HalfOp. + Perms *UserPerms `json:"perms"` + + // Extras are things added on by additional tracking methods, which may + // or may not work on the IRC server in mention. + Extras struct { + // Name is the users "realname" or full name. Commonly contains links + // to the IRC client being used, or something of non-importance. May + // also be empty if unsupported by the server/tracking is disabled. + Name string `json:"name"` + // Account refers to the account which the user is authenticated as. + // This differs between each network (e.g. usually Nickserv, but + // could also be something like Undernet). May also be empty if + // unsupported by the server/tracking is disabled. + Account string `json:"account"` + // Away refers to the away status of the user. An empty string + // indicates that they are active, otherwise the string is what they + // set as their away message. May also be empty if unsupported by the + // server/tracking is disabled. + Away string `json:"away"` + } `json:"extras"` +} + +// Channels returns a reference of *Channels that the client knows the user +// is in. If you're just looking for the namme of the channels, use +// User.ChannelList. +func (u User) Channels(c *Client) []*Channel { + if c == nil { + panic("nil Client provided") + } + + channels := []*Channel{} + + c.state.RLock() + for i := 0; i < len(u.ChannelList); i++ { + ch := c.state.lookupChannel(u.ChannelList[i]) + if ch != nil { + channels = append(channels, ch) + } + } + c.state.RUnlock() + + return channels +} + +// Copy returns a deep copy of the user which can be modified without making +// changes to the actual state. +func (u *User) Copy() *User { + nu := &User{} + *nu = *u + + nu.Perms = u.Perms.Copy() + _ = copy(nu.ChannelList, u.ChannelList) + + return nu +} + +// addChannel adds the channel to the users channel list. +func (u *User) addChannel(name string) { + if u.InChannel(name) { + return + } + + u.ChannelList = append(u.ChannelList, ToRFC1459(name)) + sort.StringsAreSorted(u.ChannelList) + + u.Perms.set(name, Perms{}) +} + +// deleteChannel removes an existing channel from the users channel list. +func (u *User) deleteChannel(name string) { + name = ToRFC1459(name) + + j := -1 + for i := 0; i < len(u.ChannelList); i++ { + if u.ChannelList[i] == name { + j = i + break + } + } + + if j != -1 { + u.ChannelList = append(u.ChannelList[:j], u.ChannelList[j+1:]...) + } + + u.Perms.remove(name) +} + +// InChannel checks to see if a user is in the given channel. +func (u *User) InChannel(name string) bool { + name = ToRFC1459(name) + + for i := 0; i < len(u.ChannelList); i++ { + if u.ChannelList[i] == name { + return true + } + } + + return false +} + +// Lifetime represents the amount of time that has passed since we have first +// seen the user. +func (u *User) Lifetime() time.Duration { + return time.Since(u.FirstSeen) +} + +// Active represents the the amount of time that has passed since we have +// last seen the user. +func (u *User) Active() time.Duration { + return time.Since(u.LastActive) +} + +// IsActive returns true if they were active within the last 30 minutes. +func (u *User) IsActive() bool { + return u.Active() < (time.Minute * 30) +} + +// Channel represents an IRC channel and the state attached to it. +type Channel struct { + // Name of the channel. Must be rfc1459 compliant. + Name string `json:"name"` + // Topic of the channel. + Topic string `json:"topic"` + + // UserList is a sorted list of all users we are currently tracking within + // the channel. Each is the nickname, and is rfc1459 compliant. + UserList []string `json:"user_list"` + // Joined represents the first time that the client joined the channel. + Joined time.Time `json:"joined"` + // Modes are the known channel modes that the bot has captured. + Modes CModes `json:"modes"` +} + +// Users returns a reference of *Users that the client knows the channel has +// If you're just looking for just the name of the users, use Channnel.UserList. +func (ch Channel) Users(c *Client) []*User { + if c == nil { + panic("nil Client provided") + } + + users := []*User{} + + c.state.RLock() + for i := 0; i < len(ch.UserList); i++ { + user := c.state.lookupUser(ch.UserList[i]) + if user != nil { + users = append(users, user) + } + } + c.state.RUnlock() + + return users +} + +// Trusted returns a list of users which have voice or greater in the given +// channel. See Perms.IsTrusted() for more information. +func (ch Channel) Trusted(c *Client) []*User { + if c == nil { + panic("nil Client provided") + } + + users := []*User{} + + c.state.RLock() + for i := 0; i < len(ch.UserList); i++ { + user := c.state.lookupUser(ch.UserList[i]) + if user == nil { + continue + } + + perms, ok := user.Perms.Lookup(ch.Name) + if ok && perms.IsTrusted() { + users = append(users, user) + } + } + c.state.RUnlock() + + return users +} + +// Admins returns a list of users which have half-op (if supported), or +// greater permissions (op, admin, owner, etc) in the given channel. See +// Perms.IsAdmin() for more information. +func (ch Channel) Admins(c *Client) []*User { + if c == nil { + panic("nil Client provided") + } + + users := []*User{} + + c.state.RLock() + for i := 0; i < len(ch.UserList); i++ { + user := c.state.lookupUser(ch.UserList[i]) + if user == nil { + continue + } + + perms, ok := user.Perms.Lookup(ch.Name) + if ok && perms.IsAdmin() { + users = append(users, user) + } + } + c.state.RUnlock() + + return users +} + +// addUser adds a user to the users list. +func (ch *Channel) addUser(nick string) { + if ch.UserIn(nick) { + return + } + + ch.UserList = append(ch.UserList, ToRFC1459(nick)) + sort.Strings(ch.UserList) +} + +// deleteUser removes an existing user from the users list. +func (ch *Channel) deleteUser(nick string) { + nick = ToRFC1459(nick) + + j := -1 + for i := 0; i < len(ch.UserList); i++ { + if ch.UserList[i] == nick { + j = i + break + } + } + + if j != -1 { + ch.UserList = append(ch.UserList[:j], ch.UserList[j+1:]...) + } +} + +// Copy returns a deep copy of a given channel. +func (ch *Channel) Copy() *Channel { + nc := &Channel{} + *nc = *ch + + _ = copy(nc.UserList, ch.UserList) + + // And modes. + nc.Modes = ch.Modes.Copy() + + return nc +} + +// Len returns the count of users in a given channel. +func (ch *Channel) Len() int { + return len(ch.UserList) +} + +// UserIn checks to see if a given user is in a channel. +func (ch *Channel) UserIn(name string) bool { + name = ToRFC1459(name) + + for i := 0; i < len(ch.UserList); i++ { + if ch.UserList[i] == name { + return true + } + } + + return false +} + +// Lifetime represents the amount of time that has passed since we have first +// joined the channel. +func (ch *Channel) Lifetime() time.Duration { + return time.Since(ch.Joined) +} + +// createChannel creates the channel in state, if not already done. +func (s *state) createChannel(name string) (ok bool) { + supported := s.chanModes() + prefixes, _ := parsePrefixes(s.userPrefixes()) + + if _, ok := s.channels[ToRFC1459(name)]; ok { + return false + } + + s.channels[ToRFC1459(name)] = &Channel{ + Name: name, + UserList: []string{}, + Joined: time.Now(), + Modes: NewCModes(supported, prefixes), + } + + return true +} + +// deleteChannel removes the channel from state, if not already done. +func (s *state) deleteChannel(name string) { + name = ToRFC1459(name) + + _, ok := s.channels[name] + if !ok { + return + } + + for _, user := range s.channels[name].UserList { + s.users[user].deleteChannel(name) + + if len(s.users[user].ChannelList) == 0 { + // Assume we were only tracking them in this channel, and they + // should be removed from state. + + delete(s.users, user) + } + } + + delete(s.channels, name) +} + +// lookupChannel returns a reference to a channel, nil returned if no results +// found. +func (s *state) lookupChannel(name string) *Channel { + return s.channels[ToRFC1459(name)] +} + +// lookupUser returns a reference to a user, nil returned if no results +// found. +func (s *state) lookupUser(name string) *User { + return s.users[ToRFC1459(name)] +} + +// createUser creates the user in state, if not already done. +func (s *state) createUser(nick string) (ok bool) { + if _, ok := s.users[ToRFC1459(nick)]; ok { + // User already exists. + return false + } + + s.users[ToRFC1459(nick)] = &User{ + Nick: nick, + FirstSeen: time.Now(), + LastActive: time.Now(), + Perms: &UserPerms{channels: make(map[string]Perms)}, + } + + return true +} + +// deleteUser removes the user from channel state. +func (s *state) deleteUser(channelName, nick string) { + user := s.lookupUser(nick) + if user == nil { + return + } + + if channelName == "" { + for i := 0; i < len(user.ChannelList); i++ { + s.channels[user.ChannelList[i]].deleteUser(nick) + } + + delete(s.users, ToRFC1459(nick)) + return + } + + channel := s.lookupChannel(channelName) + if channel == nil { + return + } + + user.deleteChannel(channelName) + channel.deleteUser(nick) + + if len(user.ChannelList) == 0 { + // This means they are no longer in any channels we track, delete + // them from state. + + delete(s.users, ToRFC1459(nick)) + } +} + +// renameUser renames the user in state, in all locations where relevant. +func (s *state) renameUser(from, to string) { + from = ToRFC1459(from) + + // Update our nickname. + if from == ToRFC1459(s.nick) { + s.nick = to + } + + user := s.lookupUser(from) + if user == nil { + return + } + + delete(s.users, from) + + user.Nick = to + user.LastActive = time.Now() + s.users[ToRFC1459(to)] = user + + for i := 0; i < len(user.ChannelList); i++ { + 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) + } + } + } +} diff --git a/vendor/manifest b/vendor/manifest index 4e4c238e..c6981eb2 100644 --- a/vendor/manifest +++ b/vendor/manifest @@ -279,6 +279,14 @@ "notests": true }, { + "importpath": "github.com/lrstanley/girc", + "repository": "https://github.com/lrstanley/girc", + "vcs": "git", + "revision": "055075db54ebd311be5946efb3f62502846089ff", + "branch": "master", + "notests": true + }, + { "importpath": "github.com/matrix-org/gomatrix", "repository": "https://github.com/matrix-org/gomatrix", "vcs": "git", |