summaryrefslogtreecommitdiffstats
path: root/vendor
diff options
context:
space:
mode:
authorWim <wim@42.be>2017-11-08 22:47:18 +0100
committerWim <wim@42.be>2017-11-08 22:47:18 +0100
commite3131541346e49f7f1f789f30c3262421d462458 (patch)
tree8001cd0766f60ad935da8e18de7f7e97aabed1e6 /vendor
parent27e94c438d370c77aa3b2634b615ffafe1828310 (diff)
downloadmatterbridge-msglm-e3131541346e49f7f1f789f30c3262421d462458.tar.gz
matterbridge-msglm-e3131541346e49f7f1f789f30c3262421d462458.tar.bz2
matterbridge-msglm-e3131541346e49f7f1f789f30c3262421d462458.zip
Vendor github.com/lrstanley/girc
Diffstat (limited to 'vendor')
-rw-r--r--vendor/github.com/lrstanley/girc/LICENSE21
-rw-r--r--vendor/github.com/lrstanley/girc/builtin.go518
-rw-r--r--vendor/github.com/lrstanley/girc/cap.go639
-rw-r--r--vendor/github.com/lrstanley/girc/client.go615
-rw-r--r--vendor/github.com/lrstanley/girc/cmdhandler/cmd.go197
-rw-r--r--vendor/github.com/lrstanley/girc/commands.go398
-rw-r--r--vendor/github.com/lrstanley/girc/conn.go566
-rw-r--r--vendor/github.com/lrstanley/girc/contants.go338
-rw-r--r--vendor/github.com/lrstanley/girc/ctcp.go288
-rw-r--r--vendor/github.com/lrstanley/girc/doc.go12
-rw-r--r--vendor/github.com/lrstanley/girc/event.go550
-rw-r--r--vendor/github.com/lrstanley/girc/format.go350
-rw-r--r--vendor/github.com/lrstanley/girc/handler.go484
-rw-r--r--vendor/github.com/lrstanley/girc/modes.go550
-rw-r--r--vendor/github.com/lrstanley/girc/state.go489
-rw-r--r--vendor/manifest8
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",