瀏覽代碼

Added LICENSE.

Steve Thielemann 1 年之前
父節點
當前提交
5680ea03da
共有 10 個文件被更改,包括 460 次插入136 次删除
  1. 21 0
      LICENSE
  2. 1 1
      client_test.go
  3. 29 2
      color.go
  4. 101 10
      color_test.go
  5. 129 90
      irc-client.go
  6. 43 0
      irc-client_test.go
  7. 30 11
      ircd_test.go
  8. 23 12
      throttle.go
  9. 14 10
      throttle_test.go
  10. 69 0
      version.go

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Steve Thielemann
+
+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.

+ 1 - 1
client_test.go

@@ -399,7 +399,7 @@ func TestConnectAutojoin(t *testing.T) {
 		"PING 12345",
 	}
 	var noticeExpect []string = []string{"Testing",
-		fmt.Sprintf("VERSION %s red-green.com/irc-client", config.Version),
+		fmt.Sprintf("VERSION %s %s", config.Version, VERSION),
 		"TIME ",
 		"PING 12345",
 	}

+ 29 - 2
color.go

@@ -6,11 +6,11 @@ import (
 	"regexp"
 )
 
+// Stripper removes IRC color and formatting codes.
 func Stripper(input string) string {
-	// \b, \_, \cNN
 	var rx *regexp.Regexp
 	var err error
-	rx, err = regexp.Compile("\x02|\x16|\x0f|\x03[0-9]{2}")
+	rx, err = regexp.Compile("\x02|\x1d|\x16|\x1e|\x11|\x16|\x0f|\x03[0-9]{2}")
 	if err != nil {
 		log.Panic("regexp:", err)
 	}
@@ -18,22 +18,49 @@ func Stripper(input string) string {
 	return input
 }
 
+// Set IRC bold
 func Bold(input string) string {
 	return "\x02" + input + "\x02"
 }
 
+// Set IRC Italics
+func Italics(input string) string {
+	return "\x1d" + input + "\x1d"
+}
+
+// Set IRC underline
 func Underline(input string) string {
 	return "\x16" + input + "\x16"
 }
 
+// Set IRC strike through
+func Strike(input string) string {
+	return "\x1e" + input + "\x1e"
+}
+
+// Set IRC monospace
+func Monospace(input string) string {
+	return "\x11" + input + "\x11"
+}
+
+// Set IRC reverse
+func Reverse(input string) string {
+	return "\x16" + input + "\x16"
+}
+
+// Reset IRC reset color and formatting
 func Reset() string {
 	return "\x0f"
 }
 
+// Color set IRC color
+//
+//	We always output the color code as 2 digits.
 func Color(color int) string {
 	return fmt.Sprintf("\x03%02d", color)
 }
 
+// Format a string with the given color code, and reset.
 func ColorString(color int, input string) string {
 	return fmt.Sprintf("\x03%02d%s\x0f", color, input)
 }

+ 101 - 10
color_test.go

@@ -19,26 +19,79 @@ func TestColor(t *testing.T) {
 		{"Three", 3}: "\x0303Three\x0f"} {
 		got = ColorString(sent.Color, sent.Text)
 		if got != expect {
-			t.Errorf("Got %s, expected %s", got, expect)
+			t.Errorf("Got %#v, expected %#v", got, expect)
 		}
 		plain = Stripper(got)
 		if plain != sent.Text {
-			t.Errorf("Got %s, expected %s", plain, sent.Text)
+			t.Errorf("Got %#v, expected %#v", plain, sent.Text)
 		}
 	}
 }
 
-func TestBold(t *testing.T) {
+// DRY - Don't repeat yourself
+
+type Attr struct {
+	Name string
+	Func func(string) string
+	Code string
+}
+
+func DryAttributeTest(t *testing.T, attr Attr) {
+	var sent, expect, got, plain string
+	var text []string = []string{"One", "Two", "Three"}
+	for _, sent = range text {
+		expect = attr.Code + sent + attr.Code
+		got = attr.Func(sent)
+		if got != expect {
+			t.Errorf("%s: Got %#v, expected %#v", attr.Name, got, expect)
+		}
+		plain = Stripper(got)
+		if plain != sent {
+			t.Errorf("%s: Got %#v, expected %#v", attr.Name, got, expect)
+		}
+	}
+}
+
+func TestAttributes(t *testing.T) {
+	Attrs := []Attr{
+		{"Bold", Bold, "\x02"},
+		{"Ttalics", Italics, "\x1d"},
+	}
+
+	for _, attr := range Attrs {
+		DryAttributeTest(t, attr)
+	}
+	/*
+		var sent string
+		var expect string
+		var got string
+		var plain string
+		for sent, expect = range map[string]string{"One": "\x02One\x02",
+			"Two":   "\x02Two\x02",
+			"Three": "\x02Three\x02"} {
+			got = Bold(sent)
+			if got != expect {
+				t.Errorf("Got %#v, expected %#v", got, expect)
+			}
+			plain = Stripper(got)
+			if plain != sent {
+				t.Errorf("Got %#v, expected %#v", plain, sent)
+			}
+		}
+	*/
+}
+
+func TestItalics(t *testing.T) {
 	var sent string
 	var expect string
 	var got string
 	var plain string
-	for sent, expect = range map[string]string{"One": "\x02One\x02",
-		"Two":   "\x02Two\x02",
-		"Three": "\x02Three\x02"} {
-		got = Bold(sent)
+	for sent, expect = range map[string]string{"One": "\x1dOne\x1d",
+		"Two":   "\x1dTwo\x1d",
+		"Three": "\x1dThree\x1d"} {
+		got = Italics(sent)
 		if got != expect {
-			t.Errorf("Got %s, expected %s", got, expect)
+			t.Errorf("Got %#v, expected %#v", got, expect)
 		}
 		plain = Stripper(got)
 		if plain != sent {
@@ -57,11 +110,49 @@ func TestUnderline(t *testing.T) {
 		"Three": "\x16Three\x16"} {
 		got = Underline(sent)
 		if got != expect {
-			t.Errorf("Got %s, expected %s", got, expect)
+			t.Errorf("Got %#v, expected %#v", got, expect)
 		}
 		plain = Stripper(got)
 		if plain != sent {
-			t.Errorf("Got %s, expected %s", plain, sent)
+			t.Errorf("Got %#v, expected %#v", plain, sent)
+		}
+	}
+}
+
+func TestStrike(t *testing.T) {
+	var sent string
+	var expect string
+	var got string
+	var plain string
+	for sent, expect = range map[string]string{"One": "\x1eOne\x1e",
+		"Two":   "\x1eTwo\x1e",
+		"Three": "\x1eThree\x1e"} {
+		got = Strike(sent)
+		if got != expect {
+			t.Errorf("Got %#v, expected %#v", got, expect)
+		}
+		plain = Stripper(got)
+		if plain != sent {
+			t.Errorf("Got %#v, expected %#v", plain, sent)
+		}
+	}
+}
+
+func TestMono(t *testing.T) {
+	var sent string
+	var expect string
+	var got string
+	var plain string
+	for sent, expect = range map[string]string{"One": "\x11One\x11",
+		"Two":   "\x11Two\x11",
+		"Three": "\x11Three\x11"} {
+		got = Monospace(sent)
+		if got != expect {
+			t.Errorf("Got %#v, expected %#v", got, expect)
+		}
+		plain = Stripper(got)
+		if plain != sent {
+			t.Errorf("Got %#v, expected %#v", plain, sent)
 		}
 	}
 }

+ 129 - 90
irc-client.go

@@ -1,3 +1,4 @@
+// An IRC-Client library.
 package ircclient
 
 import (
@@ -19,8 +20,19 @@ import (
 	"time"
 )
 
-const VERSION string = "red-green.com/irc-client 0.1.0"
+// get client version information from runtime package.
+var VERSION string = GetModuleVersion()
 
+func GetModuleVersion() string {
+	modules := GetModules()
+	version, has := modules["git.red-green.com/RedGreen/irc-client"]
+	if has {
+		return version
+	}
+	return "git.red-green.com/irc-client"
+}
+
+// Is string in slice?
 func StrInArray(strings []string, str string) bool {
 	for _, s := range strings {
 		if s == str {
@@ -30,14 +42,16 @@ func StrInArray(strings []string, str string) bool {
 	return false
 }
 
+// IRC Message
 type IRCMsg struct {
-	MsgParts []string
-	From     string
-	To       string
-	Cmd      string
-	Msg      string
+	MsgParts []string // Message parts
+	From     string   // Message From
+	To       string   // Message To
+	Cmd      string   // Command
+	Msg      string   // Message
 }
 
+// Convert nick to lowercase following IRC capitalization rules.
 func NameLower(name string) string {
 	// uppercase:  []\-
 	// lowercase:  {}|^
@@ -57,6 +71,8 @@ func Match(name1 string, name2 string) bool {
 
 // Strip out the NICK part of the From message.
 // :[email protected] => NickServ
+
+// IRCNick return just the NICK from :Nick!name@host
 func IRCNick(from string) string {
 	if from[0] == ':' {
 		from = from[1:]
@@ -74,30 +90,33 @@ IRCParse - split line into IRCMsg
 Everything after " :" is the Msg.
 Everything before " :" is split into MsgParts[].
 
-If >= 3 MsgParts {
-	To = MsgParts[2]
-}
-if >= 2 MsgParts {
-	From = IrcNic(MsgParts[0])
-	Cmd = MsgParts[1]
-} else {
-	Cmd = MsgParts[0]
-}
+	If >= 3 MsgParts {
+		To = MsgParts[2]
+	}
+
+	if >= 2 MsgParts {
+		From = IrcNic(MsgParts[0])
+		Cmd = MsgParts[1]
+	} else {
+
+		Cmd = MsgParts[0]
+	}
 
 Example Messages:
 
 :irc.red-green.com 001 test :Welcome to IRC
 ^From              ^Cmd     ^Msg
-                       ^To
+
+	^To
+
 ^0                 ^1  ^2   MsgParts[]
 
 PING :1234567890
 ^Cmd ^Msg
 ^0 MsgParts[]
-
-
-
 */
+
+// IRCParse Parse an IRC line into the IRCMsg struct.
 func IRCParse(line string) IRCMsg {
 	var pos int = strings.Index(line, " :")
 	var results IRCMsg
@@ -119,6 +138,19 @@ func IRCParse(line string) IRCMsg {
 	if len(results.MsgParts) >= 3 {
 		results.To = results.MsgParts[2]
 	}
+
+	// Translate CTCP and ACTION
+	if results.Cmd == "PRIVMSG" {
+		if (results.Msg[0] == '\x01') && (results.Msg[len(results.Msg)-1] == '\x01') {
+			results.Cmd = "CTCP"
+			results.Msg = results.Msg[1 : len(results.Msg)-1]
+			if strings.HasPrefix(results.Msg, "ACTION ") {
+				results.Cmd = "ACTION"
+				results.Msg = results.Msg[7:]
+			}
+		}
+	}
+
 	return results
 }
 
@@ -140,20 +172,25 @@ func oldIRCParse(line string) []string {
 }
 */
 
+// Sent to writer
+//
+//	The To part allows the writer to throttle messages to a specific target.
+//	When throttled, it delays and alternates between the targets.
 type IRCWrite struct {
-	To     string
-	Output string
+	To     string // Write To
+	Output string // Output
 }
 
+// Configuration
 type IRCConfig struct {
-	Port           int      `json:"Port"`
-	Hostname       string   `json:"Hostname"`
-	UseTLS         bool     `json:"UseTLS"`   // Use TLS Secure connection
-	UseSASL        bool     `json:"UseSASL"`  // Authenticate via SASL
-	Insecure       bool     `json:"Insecure"` // Allow self-signed certificates
-	Nick           string   `json:"Nick"`
-	Username       string   `json:"Username"`
-	Realname       string   `json:"Realname"`
+	Port           int      `json:"Port"`           // IRC Connection port
+	Hostname       string   `json:"Hostname"`       // Hostname for connection
+	UseTLS         bool     `json:"UseTLS"`         // Use TLS Secure connection
+	UseSASL        bool     `json:"UseSASL"`        // Authenticate via SASL
+	Insecure       bool     `json:"Insecure"`       // Allow self-signed certificates
+	Nick           string   `json:"Nick"`           // Client's nick
+	Username       string   `json:"Username"`       // Client's username
+	Realname       string   `json:"Realname"`       // Client's realname
 	Password       string   `json:"Password"`       // Password for nickserv
 	ServerPassword string   `json:"ServerPassword"` // Password for server
 	AutoJoin       []string `json:"AutoJoin"`       // Channels to auto-join
@@ -162,45 +199,57 @@ type IRCConfig struct {
 	Flood_Num      int      `json:"FloodNum"`       // Number of lines sent before considered a flood
 	Flood_Time     int      `json:"FloodTime"`      // Number of Seconds to track previous messages
 	Flood_Delay    int      `json:"FloodDelay"`     // Delay between sending when flood protection on (Milliseconds)
-	Debug_Output   bool     `json:"Debug"`
+	Debug_Output   bool     `json:"Debug"`          // Enable debug output
 }
 
 type IRCClient struct {
 	IRCConfig
-	MyNick       string // Client's current nick
-	OnExit       func() // Called on exit
-	Socket       net.Conn
-	Reader       *bufio.Reader
-	ReadChannel  chan IRCMsg
-	ReadEvents   []string
-	WriteChannel chan IRCWrite
-	DelChannel   chan string // For deleting channel or nicks that are missing.
-	Registered   bool
-	ISupport     map[string]string // 005
-	wg           sync.WaitGroup
-	Mutex        sync.Mutex
+	MyNick       string            // Client's current nick
+	OnExit       func()            // Called on exit
+	Socket       net.Conn          // Connection to IRCD
+	Reader       *bufio.Reader     // For reading a line at a time from the IRCD
+	ReadChannel  chan IRCMsg       // Channel of parsed IRC Messages
+	ReadEvents   []string          //
+	WriteChannel chan IRCWrite     // Channel for writing messages to IRCD
+	DelChannel   chan string       // For deleting channel or nicks that are missing.
+	Registered   bool              // Registered with services?
+	ISupport     map[string]string // IRCD capabilities (005)
+	wg           sync.WaitGroup    // Safe shutdown of goroutines
+	Mutex        sync.Mutex        // Guards MyNick
 }
 
+// Get the current nick of the client
 func (Config *IRCClient) GetNick() string {
 	Config.Mutex.Lock()
 	defer Config.Mutex.Unlock()
 	return Config.MyNick
 }
 
+// Sets the current nick of the client
 func (Config *IRCClient) SetNick(nick string) {
 	Config.Mutex.Lock()
 	defer Config.Mutex.Unlock()
 	Config.MyNick = nick
 }
 
-func (Config *IRCClient) IsAuto(ch string) bool {
-	return StrInArray(Config.AutoJoin, ch)
+// Is channel in the AutoJoin list?
+func (Config *IRCClient) IsAuto(channel string) bool {
+	return StrInArray(Config.AutoJoin, channel)
 }
 
+// Connect to IRCD and authenticate.
+//
+// This starts the ReaderRoutine to handle processing the lines from the IRCD.
+// You must setup ReadChannel if you want to process any messages.
 func (Config *IRCClient) Connect() bool {
 	var err error
 
 	// Set sensible defaults (if not provided),
+
+	if Config.Version == "" {
+		Config.Version = VERSION
+	}
+
 	if Config.Flood_Num == 0 {
 		Config.Flood_Num = 5
 	}
@@ -277,7 +326,7 @@ func (Config *IRCClient) Connect() bool {
 	return true
 }
 
-// Low level write to [TLS]Socket routine.
+// Write to IRCD
 func (Config *IRCClient) write(output string) error {
 	var err error
 	if Config.Debug_Output {
@@ -289,6 +338,7 @@ func (Config *IRCClient) write(output string) error {
 	return err
 }
 
+// WriterRoutine a goroutine that handles flood control
 func (Config *IRCClient) WriterRoutine() {
 	var err error
 	var throttle ThrottleBuffer
@@ -297,7 +347,7 @@ func (Config *IRCClient) WriterRoutine() {
 	Flood.Init(Config.Flood_Num, Config.Flood_Time)
 
 	defer Config.wg.Done()
-	throttle.init()
+	throttle.Init()
 
 	// signal handler
 	var sigChannel chan os.Signal = make(chan os.Signal, 1)
@@ -386,6 +436,7 @@ func (Config *IRCClient) WriterRoutine() {
 	}
 }
 
+// Shutdown the client.
 func (Config *IRCClient) Close() {
 	Config.Socket.Close()
 	Config.PriorityWrite("")
@@ -395,6 +446,7 @@ func (Config *IRCClient) Close() {
 	Config.wg.Wait()
 }
 
+// Generate random nick to change to (upon collision/433)
 func RandomNick(nick string) string {
 	var result string = nick + "-"
 	result += strconv.Itoa(rand.Intn(1000))
@@ -402,6 +454,8 @@ func RandomNick(nick string) string {
 }
 
 // PriorityWrite: Send output command to server immediately.
+//
+// This is never throttled.
 func (Config *IRCClient) PriorityWrite(output string) {
 	Config.WriteChannel <- IRCWrite{To: "", Output: output}
 }
@@ -426,6 +480,14 @@ func (Config *IRCClient) Action(to string, message string) {
 	Config.WriteTo(to, fmt.Sprintf("PRIVMSG %s :\x01ACTION %s\x01", to, message))
 }
 
+// Goroutine reader routine
+//
+//	This reads a line at a time, delimited by '\n'.
+//	Auto-replies to server PING.
+//	Converts CTCP & ACTION messages to type CTCP/ACTION.
+//	Automatically answers /VERSION and /TIME.
+//	Handles SASL authentication.
+//	Rejoins AutoJoin channels kicked from.
 func (Config *IRCClient) ReaderRoutine() {
 	defer Config.wg.Done()
 	var registering bool
@@ -465,52 +527,29 @@ func (Config *IRCClient) ReaderRoutine() {
 					Config.MyNick = newNick
 					Config.PriorityWrite("NICK " + newNick)
 				}
-			case "PRIVMSG":
-				// Convert Cmd "PRIVMSG" to "CTCP" or "ACTION".
-				if (results.Msg[0] == '\x01') && (results.Msg[len(results.Msg)-1] == '\x01') {
-					// ACTION
-					results.Cmd = "CTCP"
-					results.Msg = results.Msg[1 : len(results.Msg)-1]
-					if Config.Debug_Output {
-						log.Println("CTCP:", results.Msg)
-					}
-
-					// Process CTCP commands
-					if strings.HasPrefix(results.Msg, "ACTION ") {
-						results.Cmd = "ACTION"
-						results.Msg = results.Msg[7:]
-					}
-
-					if strings.HasPrefix(results.Msg, "PING ") {
-						Config.WriteTo(IRCNick(results.From),
-							fmt.Sprintf("NOTICE %s :\x01PING %s\x01",
-								IRCNick(results.From),
-								results.Msg[5:]))
-					}
-
-					if results.Msg == "VERSION" {
-						// Send version reply
-						var version string
-						if Config.Version != "" {
-							version = Config.Version + " " + VERSION
-						} else {
-							version = VERSION
-						}
-						Config.WriteTo(IRCNick(results.From),
-							fmt.Sprintf("NOTICE %s :\x01VERSION %s\x01",
-								IRCNick(results.From), version))
-					}
-
-					if results.Msg == "TIME" {
-						// Send time reply
-						var now time.Time = time.Now()
-						Config.WriteTo(IRCNick(results.From),
-							fmt.Sprintf("NOTICE %s :\x01TIME %s\x01",
-								IRCNick(results.From),
-								now.Format(time.ANSIC)))
+			case "CTCP":
+				if strings.HasPrefix(results.Msg, "PING ") {
+					Config.WriteTo(IRCNick(results.From),
+						fmt.Sprintf("NOTICE %s :\x01%s\x01",
+							IRCNick(results.From),
+							results.Msg))
+				} else if strings.HasPrefix(results.Msg, "VERSION") {
+					var version string
+					if Config.Version != "" {
+						version = Config.Version + " " + VERSION
+					} else {
+						version = VERSION
 					}
+					Config.WriteTo(IRCNick(results.From),
+						fmt.Sprintf("NOTICE %s :\x01VERSION %s\x01",
+							IRCNick(results.From), version))
+				} else if strings.HasPrefix(results.Msg, "TIME") {
+					var now time.Time = time.Now()
+					Config.WriteTo(IRCNick(results.From),
+						fmt.Sprintf("NOTICE %s :\x01TIME %s\x01",
+							IRCNick(results.From),
+							now.Format(time.ANSIC)))
 				}
-
 			}
 		} else {
 			// This is likely, 2022/04/05 10:11:41 ReadString: EOF

+ 43 - 0
irc-client_test.go

@@ -0,0 +1,43 @@
+package ircclient_test
+
+import (
+	"fmt"
+
+	ircclient "git.red-green.com/RedGreen/irc-client"
+)
+
+func ExampleIRCClient_Connect() {
+	var FromIRC chan ircclient.IRCMsg = make(chan ircclient.IRCMsg)
+	var config ircclient.IRCConfig = ircclient.IRCConfig{Port: 6667,
+		Hostname: "irc.libera.chat",
+		Nick:     "go-bot-test",
+		Username: "gobot",
+		Realname: "Bot Testing",
+		AutoJoin: []string{"##bugz"},
+	}
+	var irc ircclient.IRCClient = ircclient.IRCClient{IRCConfig: config,
+		ReadChannel: FromIRC}
+
+	irc.Connect()
+	defer irc.Close()
+
+	var Msg ircclient.IRCMsg
+	for Msg = range irc.ReadChannel {
+		// View all the commands:
+		// fmt.Printf("%#v\n", Msg)
+		switch Msg.Cmd {
+		case "EndMOTD":
+			// Ok! we're connected.
+			fmt.Println("Connected")
+			break
+		case "333":
+			// Joined channel, say something and quit.
+			irc.Msg("##bugz", "Hello!")
+			irc.WriteTo("", "QUIT")
+			break
+		case "PRIVMSG":
+			break
+		}
+	}
+	// Uncomment to test connection // Output: Connected
+}

+ 30 - 11
ircd_test.go

@@ -24,6 +24,13 @@ import (
 var KeyPair tls.Certificate
 var HasKeyPair bool
 
+// Display debug output when running tests.
+const TEST_DEBUG_OUTPUT bool = false
+
+// Give Key Pair for server TLS connection
+//
+// This reuses the KeyPair if already generated in this run.
+// Otherwise, it call generateKeyPair to do the actual certificate generation.
 func GenerateKeyPair() (keypair tls.Certificate) {
 	if !HasKeyPair {
 		KeyPair = generateKeyPair()
@@ -32,6 +39,7 @@ func GenerateKeyPair() (keypair tls.Certificate) {
 	return KeyPair
 }
 
+// Generate certificate for server TLS connection
 func generateKeyPair() (keypair tls.Certificate) {
 	// generate test certificate
 	priv, _ := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
@@ -67,6 +75,7 @@ func generateKeyPair() (keypair tls.Certificate) {
 	return listenerKeyPair
 }
 
+// Setup TLS Socket for IRCD connection testing.
 func setupTLSSocket() (listen net.Listener, addr string) {
 	// establish network socket connection to set Comm_handle
 	var err error
@@ -89,6 +98,7 @@ func setupTLSSocket() (listen net.Listener, addr string) {
 	return listener, address
 }
 
+// Setup Socket for IRCD connection testing.
 func setupSocket() (listen net.Listener, addr string) {
 	// establish network socket connection to set Comm_handle
 	var err error
@@ -106,8 +116,11 @@ func setupSocket() (listen net.Listener, addr string) {
 	return listener, address
 }
 
+// Write to IRCD server (testing)
 func ircWrite(server net.Conn, output string, t *testing.T) {
-	t.Logf(">> %s\n", output)
+	if TEST_DEBUG_OUTPUT {
+		t.Logf(">> %s\n", output)
+	}
 	_, err := server.Write([]byte(output + "\r\n"))
 	if err != nil {
 		t.Error("server.Write:", err)
@@ -117,19 +130,20 @@ func ircWrite(server net.Conn, output string, t *testing.T) {
 var abortAfter int = 150 // Milliseconds to abort part 3
 
 /*
-	mock up an irc server
+Mock up a test IRCD server
+
 	part 1 : nick, user
 	part 2 : identify to services (if not SASL/SASL failed)
 	part 3 : quit after abortAfter ms
 
 	Features:
-	if nick == "bad", return 443 Nick already in use.
-	JOIN #kick, kicks once from the channel.
-	SASL authentication testing.
-	TLS support, we make our own certificates. See generateKeyPair()
-	Any messages sent to "echo" are reflected back to the client.
-	Any messages to missing/#missing are returned 404/401.
-	After abortAfter milliseconds, we close the connection.
+	  if nick == "bad", return 443 Nick already in use.
+	  JOIN #kick, kicks once from the channel.
+	  SASL authentication testing.
+	  TLS support, we make our own certificates. See generateKeyPair()
+	  Any messages sent to "echo" are reflected back to the client.
+	  Any messages to missing/#missing are returned 404/401.
+	  After abortAfter milliseconds, we close the connection.
 */
 func ircServer(listener net.Listener, t *testing.T, config *IRCClient) {
 	var server net.Conn
@@ -164,7 +178,9 @@ func ircServer(listener net.Listener, t *testing.T, config *IRCClient) {
 			line = strings.Trim(line, "\r\n")
 			// process the received line here
 			parts = strings.Split(line, " ")
-			t.Logf("<< %s", line)
+			if TEST_DEBUG_OUTPUT {
+				t.Logf("<< %s", line)
+			}
 
 			switch parts[0] {
 			case "CAP":
@@ -347,7 +363,10 @@ func ircServer(listener net.Listener, t *testing.T, config *IRCClient) {
 			line = strings.Trim(line, "\r\n")
 			// process the received line here
 			parts = strings.Split(line, " ")
-			t.Logf("<< %s", line)
+			if TEST_DEBUG_OUTPUT {
+				t.Logf("<< %s", line)
+			}
+
 			switch parts[0] {
 			case "JOIN":
 				for _, channel := range strings.Split(parts[1], ",") {

+ 23 - 12
throttle.go

@@ -5,19 +5,24 @@ import (
 	"time"
 )
 
+// Track when the client last sent something
 type FloodTrack struct {
-	Pos     int
-	Size    int
-	Timeout int
+	Pos     int // Index to store next item
+	Size    int // Max number of Track items
+	Timeout int // Timeout in seconds
 	Track   []time.Time
 }
 
+// Initialize flood tracking
 func (F *FloodTrack) Init(size int, timeout int) {
 	F.Size = size
 	F.Timeout = timeout
 	F.Track = make([]time.Time, size)
 }
 
+// Are we full?
+//
+//	Expire records before checking.
 func (F *FloodTrack) Full() bool {
 	if F.Pos > 0 {
 		F.Expire()
@@ -25,6 +30,7 @@ func (F *FloodTrack) Full() bool {
 	return F.Pos == F.Size
 }
 
+// Expire Track records older then timeout
 func (F *FloodTrack) Expire() {
 	var idx int
 
@@ -42,28 +48,28 @@ ReCheck:
 	}
 }
 
+// Save Now() into the tracker.
 func (F *FloodTrack) Save() {
 	F.Track[F.Pos] = time.Now()
 	F.Pos++
 }
 
 type ThrottleBuffer struct {
-	buffer     map[string][]string
-	targets    []string
-	last       int
-	Life_sucks bool
+	buffer     map[string][]string // Map of targets and strings to send
+	targets    []string            // List of targets
+	last       int                 // Last index set
+	Life_sucks bool                // Is throttle active?
 }
 
-func (T *ThrottleBuffer) init() {
+func (T *ThrottleBuffer) Init() {
 	T.buffer = make(map[string][]string, 0)
 	T.targets = make([]string, 0)
 	T.last = 0
 }
 
-/*
-Pop next available from buffer.  If empty, remove from map and targets
-list.  Adjust last if needed.
-*/
+// Pop next available from buffer.
+//
+//	If empty, remove from map and targets list.  Adjust last if needed.
 func (T *ThrottleBuffer) pop() string {
 	// Get next target
 	var t string = T.targets[T.last]
@@ -106,6 +112,7 @@ func (T *ThrottleBuffer) pop() string {
 	return msg
 }
 
+// Push item into the buffer, begin throttling.
 func (T *ThrottleBuffer) push(To string, Output string) {
 	if len(T.targets) == 0 {
 		T.Life_sucks = true
@@ -121,6 +128,10 @@ func (T *ThrottleBuffer) push(To string, Output string) {
 	T.buffer[To] = append(T.buffer[To], Output)
 }
 
+// Delete target from buffer, if exists.
+//
+//	If we're kicked from the channel we're sending to, or if the
+//	nick we're sending to quits -- remove those from write queue.
 func (T *ThrottleBuffer) delete(To string) {
 	_, has := T.buffer[To]
 	if has {

+ 14 - 10
throttle_test.go

@@ -14,30 +14,30 @@ func TestFloodTrack(t *testing.T) {
 	Flood.Init(3, 1)
 
 	if Flood.Full() {
-		t.Error("Expected Track to be empty")
+		t.Error("Expected Flood to be empty")
 	}
 
 	for x := 1; x < 3; x++ {
 		Flood.Save()
 		if Flood.Pos != x {
-			t.Errorf("Expected Track Pos to be %d", x)
+			t.Errorf("Expected Flood Pos to be %d", x)
 		}
 		if Flood.Full() {
-			t.Error("Expected Track to be empty")
+			t.Error("Expected Flood to be empty")
 		}
 	}
 
 	Flood.Save()
 	if Flood.Pos != 3 {
-		t.Error("Expected Track Pos to be 3")
+		t.Error("Expected Flood Pos to be 3")
 	}
 	if !Flood.Full() {
-		t.Error("Expected Track to be full")
+		t.Error("Expected Flood to be full")
 	}
 	time.Sleep(time.Second)
 	Flood.Expire()
 	if Flood.Pos != 0 {
-		t.Error("Expected Track Pos to be 0")
+		t.Error("Expected Flood Pos to be 0")
 	}
 }
 
@@ -50,7 +50,7 @@ func TestThrottleOnly(t *testing.T) {
 	}()
 
 	var buff ThrottleBuffer
-	buff.init()
+	buff.Init()
 	buff.push("#chat", "msg1")
 	if !buff.Life_sucks {
 		t.Error("Flood control should be enabled here.")
@@ -76,7 +76,7 @@ func TestThrottleDelete(t *testing.T) {
 
 	var expect, line string
 	var buff ThrottleBuffer
-	buff.init()
+	buff.Init()
 	buff.push("#chat", "msg1")
 	buff.push("#chat", "msg2")
 
@@ -140,7 +140,7 @@ func TestThrottlePopDelete(t *testing.T) {
 	}()
 
 	var buff ThrottleBuffer
-	buff.init()
+	buff.Init()
 	buff.push("#chat", "m1")
 	buff.push("#chat", "m2")
 	buff.push("#chat", "m3")
@@ -150,6 +150,7 @@ func TestThrottlePopDelete(t *testing.T) {
 	buff.push("#other", "o3")
 
 	// When we pop from #test, it needs to be deleted.
+	// Because it has just one item.
 
 	var line, expect string
 
@@ -204,7 +205,7 @@ func TestThrottleBuffer(t *testing.T) {
 	}()
 
 	var buff ThrottleBuffer
-	buff.init()
+	buff.Init()
 	buff.push("#chat", "msg1")
 	if !buff.Life_sucks {
 		t.Error("Flood control should be enabled here.")
@@ -213,6 +214,9 @@ func TestThrottleBuffer(t *testing.T) {
 	buff.push("user", "msg3")
 
 	// verify output order
+	// #chat [0] "msg1"
+	// user  [0] "msg3"
+	// #chat [1] "msg2"
 
 	var str string
 	var expect string

+ 69 - 0
version.go

@@ -0,0 +1,69 @@
+package ircclient
+
+import (
+	"fmt"
+	"runtime/debug"
+)
+
+// Get build version from BuildInfo
+func ShowVersion() {
+	var buildinfo *debug.BuildInfo
+	var ok bool
+	buildinfo, ok = debug.ReadBuildInfo()
+	if ok {
+		// We have build info, display it.
+		fmt.Print("Version: ")
+		var bs debug.BuildSetting
+		for _, bs = range buildinfo.Settings {
+			if bs.Key == "vcs.revision" || bs.Key == "vcs.time" {
+				fmt.Print(bs.Value + " ")
+			}
+		}
+		fmt.Println()
+	}
+}
+
+// Get Go version, arch, and OS.  Get git commit hash.
+func GetVersion() (goversion string, gitcommit string, arch string, goos string) {
+	var buildinfo *debug.BuildInfo
+	var ok bool
+	var results string
+	var garch, gos string
+	buildinfo, ok = debug.ReadBuildInfo()
+	if ok {
+		// We have build info, display it.
+		goversion := buildinfo.GoVersion
+
+		var bs debug.BuildSetting
+		for _, bs = range buildinfo.Settings {
+			if bs.Key == "vcs.revision" || bs.Key == "vcs.time" {
+				results += bs.Value + " "
+			}
+			if bs.Key == "GOARCH" {
+				garch = bs.Value
+			}
+			if bs.Key == "GOOS" {
+				gos = bs.Value
+			}
+		}
+		return goversion, results, garch, gos
+	}
+	return "", "", "", ""
+}
+
+// Get modules information
+//
+// Return map of module name and version.
+func GetModules() map[string]string {
+	var buildinfo *debug.BuildInfo
+	var ok bool
+	var results map[string]string = make(map[string]string)
+	buildinfo, ok = debug.ReadBuildInfo()
+	if ok {
+		// We have Module info, return it.
+		for _, mod := range buildinfo.Deps {
+			results[mod.Path] = mod.Version
+		}
+	}
+	return results
+}