mirror of
				https://github.com/fluffle/goirc
				synced 2025-11-04 03:58:03 +00:00 
			
		
		
		
	add tags parsing and CAP command, with parsing tests
This commit is contained in:
		
							parent
							
								
									5cf08f7e9c
								
							
						
					
					
						commit
						f6a94cc3a3
					
				
					 3 changed files with 103 additions and 4 deletions
				
			
		| 
						 | 
					@ -11,6 +11,7 @@ const (
 | 
				
			||||||
	DISCONNECTED = "DISCONNECTED"
 | 
						DISCONNECTED = "DISCONNECTED"
 | 
				
			||||||
	ACTION       = "ACTION"
 | 
						ACTION       = "ACTION"
 | 
				
			||||||
	AWAY         = "AWAY"
 | 
						AWAY         = "AWAY"
 | 
				
			||||||
 | 
						CAP          = "CAP"
 | 
				
			||||||
	CTCP         = "CTCP"
 | 
						CTCP         = "CTCP"
 | 
				
			||||||
	CTCPREPLY    = "CTCPREPLY"
 | 
						CTCPREPLY    = "CTCPREPLY"
 | 
				
			||||||
	INVITE       = "INVITE"
 | 
						INVITE       = "INVITE"
 | 
				
			||||||
| 
						 | 
					@ -289,3 +290,13 @@ func (conn *Conn) Ping(message string) { conn.Raw(PING + " :" + message) }
 | 
				
			||||||
// Pong sends a PONG command to the server.
 | 
					// Pong sends a PONG command to the server.
 | 
				
			||||||
//     PONG :message
 | 
					//     PONG :message
 | 
				
			||||||
func (conn *Conn) Pong(message string) { conn.Raw(PONG + " :" + message) }
 | 
					func (conn *Conn) Pong(message string) { conn.Raw(PONG + " :" + message) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Cap sends a CAP command to the server.
 | 
				
			||||||
 | 
					//     CAP capability
 | 
				
			||||||
 | 
					func (conn *Conn) Cap(subcommmand string, messages ...string) {
 | 
				
			||||||
 | 
						if len(messages) == 0 {
 | 
				
			||||||
 | 
							conn.Raw(CAP + " " + subcommmand)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							conn.Raw(CAP + " " + subcommmand + " :" + strings.Join(messages, " "))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,12 +1,15 @@
 | 
				
			||||||
package client
 | 
					package client
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"runtime"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
	"runtime"
 | 
					
 | 
				
			||||||
	"github.com/fluffle/goirc/logging"
 | 
						"github.com/fluffle/goirc/logging"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var tagsReplacer = strings.NewReplacer("\\:", ";", "\\s", " ", "\\r", "\r", "\\n", "\n")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// We parse an incoming line into this struct. Line.Cmd is used as the trigger
 | 
					// We parse an incoming line into this struct. Line.Cmd is used as the trigger
 | 
				
			||||||
// name for incoming event handlers and is the IRC verb, the first sequence
 | 
					// name for incoming event handlers and is the IRC verb, the first sequence
 | 
				
			||||||
// of non-whitespace characters after ":nick!user@host", e.g. PRIVMSG.
 | 
					// of non-whitespace characters after ":nick!user@host", e.g. PRIVMSG.
 | 
				
			||||||
| 
						 | 
					@ -14,6 +17,7 @@ import (
 | 
				
			||||||
//   Src == "nick!user@host"
 | 
					//   Src == "nick!user@host"
 | 
				
			||||||
//   Cmd == e.g. PRIVMSG, 332
 | 
					//   Cmd == e.g. PRIVMSG, 332
 | 
				
			||||||
type Line struct {
 | 
					type Line struct {
 | 
				
			||||||
 | 
						Tags                   map[string]string
 | 
				
			||||||
	Nick, Ident, Host, Src string
 | 
						Nick, Ident, Host, Src string
 | 
				
			||||||
	Cmd, Raw               string
 | 
						Cmd, Raw               string
 | 
				
			||||||
	Args                   []string
 | 
						Args                   []string
 | 
				
			||||||
| 
						 | 
					@ -25,6 +29,12 @@ func (l *Line) Copy() *Line {
 | 
				
			||||||
	nl := *l
 | 
						nl := *l
 | 
				
			||||||
	nl.Args = make([]string, len(l.Args))
 | 
						nl.Args = make([]string, len(l.Args))
 | 
				
			||||||
	copy(nl.Args, l.Args)
 | 
						copy(nl.Args, l.Args)
 | 
				
			||||||
 | 
						if l.Tags != nil {
 | 
				
			||||||
 | 
							nl.Tags = make(map[string]string)
 | 
				
			||||||
 | 
							for k, v := range l.Tags {
 | 
				
			||||||
 | 
								nl.Tags[k] = v
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	return &nl
 | 
						return &nl
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -97,6 +107,27 @@ func (line *Line) Public() bool {
 | 
				
			||||||
// set to CTCP or CTCPREPLY, and the CTCP command prepended to line.Args.
 | 
					// set to CTCP or CTCPREPLY, and the CTCP command prepended to line.Args.
 | 
				
			||||||
func ParseLine(s string) *Line {
 | 
					func ParseLine(s string) *Line {
 | 
				
			||||||
	line := &Line{Raw: s}
 | 
						line := &Line{Raw: s}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if s[0] == '@' {
 | 
				
			||||||
 | 
							var rawTags string
 | 
				
			||||||
 | 
							line.Tags = make(map[string]string)
 | 
				
			||||||
 | 
							if idx := strings.Index(s, " "); idx != -1 {
 | 
				
			||||||
 | 
								rawTags, s = s[1:idx], s[idx+1:]
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// all tags are escaped, so splitting on ; is right by design
 | 
				
			||||||
 | 
							for _, tag := range strings.Split(rawTags, ";") {
 | 
				
			||||||
 | 
								pair := strings.Split(tagsReplacer.Replace(tag), "=")
 | 
				
			||||||
 | 
								if len(pair) > 1 {
 | 
				
			||||||
 | 
									line.Tags[pair[0]] = pair[1]
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									line.Tags[tag] = ""
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if s[0] == ':' {
 | 
						if s[0] == ':' {
 | 
				
			||||||
		// remove a source and parse it
 | 
							// remove a source and parse it
 | 
				
			||||||
		if idx := strings.Index(s, " "); idx != -1 {
 | 
							if idx := strings.Index(s, " "); idx != -1 {
 | 
				
			||||||
| 
						 | 
					@ -159,7 +190,6 @@ func ParseLine(s string) *Line {
 | 
				
			||||||
	return line
 | 
						return line
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
func (line *Line) argslen(minlen int) bool {
 | 
					func (line *Line) argslen(minlen int) bool {
 | 
				
			||||||
	pc, _, _, _ := runtime.Caller(1)
 | 
						pc, _, _, _ := runtime.Caller(1)
 | 
				
			||||||
	fn := runtime.FuncForPC(pc)
 | 
						fn := runtime.FuncForPC(pc)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,12 +1,14 @@
 | 
				
			||||||
package client
 | 
					package client
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"reflect"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestLineCopy(t *testing.T) {
 | 
					func TestLineCopy(t *testing.T) {
 | 
				
			||||||
	l1 := &Line{
 | 
						l1 := &Line{
 | 
				
			||||||
 | 
							Tags:  map[string]string{"foo": "bar", "fizz": "buzz"},
 | 
				
			||||||
		Nick:  "nick",
 | 
							Nick:  "nick",
 | 
				
			||||||
		Ident: "ident",
 | 
							Ident: "ident",
 | 
				
			||||||
		Host:  "host",
 | 
							Host:  "host",
 | 
				
			||||||
| 
						 | 
					@ -20,7 +22,8 @@ func TestLineCopy(t *testing.T) {
 | 
				
			||||||
	l2 := l1.Copy()
 | 
						l2 := l1.Copy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Ugly. Couldn't be bothered to bust out reflect and actually think.
 | 
						// Ugly. Couldn't be bothered to bust out reflect and actually think.
 | 
				
			||||||
	if l2.Nick != "nick" || l2.Ident != "ident" || l2.Host != "host" ||
 | 
						if l2.Tags == nil || l2.Tags["foo"] != "bar" || l2.Tags["fizz"] != "buzz" ||
 | 
				
			||||||
 | 
							l2.Nick != "nick" || l2.Ident != "ident" || l2.Host != "host" ||
 | 
				
			||||||
		l2.Src != "src" || l2.Cmd != "cmd" || l2.Raw != "raw" ||
 | 
							l2.Src != "src" || l2.Cmd != "cmd" || l2.Raw != "raw" ||
 | 
				
			||||||
		l2.Args[0] != "arg" || l2.Args[1] != "text" || l2.Time != l1.Time {
 | 
							l2.Args[0] != "arg" || l2.Args[1] != "text" || l2.Time != l1.Time {
 | 
				
			||||||
		t.Errorf("Line not copied correctly")
 | 
							t.Errorf("Line not copied correctly")
 | 
				
			||||||
| 
						 | 
					@ -28,6 +31,7 @@ func TestLineCopy(t *testing.T) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Now, modify l2 and verify l1 not changed
 | 
						// Now, modify l2 and verify l1 not changed
 | 
				
			||||||
 | 
						l2.Tags["foo"] = "baz"
 | 
				
			||||||
	l2.Nick = l2.Nick[1:]
 | 
						l2.Nick = l2.Nick[1:]
 | 
				
			||||||
	l2.Ident = "foo"
 | 
						l2.Ident = "foo"
 | 
				
			||||||
	l2.Host = ""
 | 
						l2.Host = ""
 | 
				
			||||||
| 
						 | 
					@ -35,7 +39,8 @@ func TestLineCopy(t *testing.T) {
 | 
				
			||||||
	l2.Args[1] = "bar"
 | 
						l2.Args[1] = "bar"
 | 
				
			||||||
	l2.Time = time.Now()
 | 
						l2.Time = time.Now()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if l1.Nick != "nick" || l1.Ident != "ident" || l1.Host != "host" ||
 | 
						if l2.Tags == nil || l2.Tags["foo"] != "baz" || l2.Tags["fizz"] != "buzz" ||
 | 
				
			||||||
 | 
							l1.Nick != "nick" || l1.Ident != "ident" || l1.Host != "host" ||
 | 
				
			||||||
		l1.Src != "src" || l1.Cmd != "cmd" || l1.Raw != "raw" ||
 | 
							l1.Src != "src" || l1.Cmd != "cmd" || l1.Raw != "raw" ||
 | 
				
			||||||
		l1.Args[0] != "arg" || l1.Args[1] != "text" || l1.Time == l2.Time {
 | 
							l1.Args[0] != "arg" || l1.Args[1] != "text" || l1.Time == l2.Time {
 | 
				
			||||||
		t.Errorf("Original modified when copy changed")
 | 
							t.Errorf("Original modified when copy changed")
 | 
				
			||||||
| 
						 | 
					@ -88,3 +93,56 @@ func TestLineTarget(t *testing.T) {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestLineTags(t *testing.T) {
 | 
				
			||||||
 | 
						tests := []struct {
 | 
				
			||||||
 | 
							in  string
 | 
				
			||||||
 | 
							out *Line
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{ // Make sure non-tagged lines work
 | 
				
			||||||
 | 
								":nick!ident@host.com PRIVMSG me :Hello",
 | 
				
			||||||
 | 
								&Line{
 | 
				
			||||||
 | 
									Nick:  "nick",
 | 
				
			||||||
 | 
									Ident: "ident",
 | 
				
			||||||
 | 
									Host:  "host.com",
 | 
				
			||||||
 | 
									Src:   "nick!ident@host.com",
 | 
				
			||||||
 | 
									Cmd:   PRIVMSG,
 | 
				
			||||||
 | 
									Raw:   ":nick!ident@host.com PRIVMSG me :Hello",
 | 
				
			||||||
 | 
									Args:  []string{"me", "Hello"},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{ // Tags example from the spec
 | 
				
			||||||
 | 
								"@aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello",
 | 
				
			||||||
 | 
								&Line{
 | 
				
			||||||
 | 
									Tags:  map[string]string{"aaa": "bbb", "ccc": "", "example.com/ddd": "eee"},
 | 
				
			||||||
 | 
									Nick:  "nick",
 | 
				
			||||||
 | 
									Ident: "ident",
 | 
				
			||||||
 | 
									Host:  "host.com",
 | 
				
			||||||
 | 
									Src:   "nick!ident@host.com",
 | 
				
			||||||
 | 
									Cmd:   PRIVMSG,
 | 
				
			||||||
 | 
									Raw:   "@aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello",
 | 
				
			||||||
 | 
									Args:  []string{"me", "Hello"},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{ // Test escaped characters
 | 
				
			||||||
 | 
								"@\\:=\\:;\\s=\\s;\\r=\\r;\\n=\\n :nick!ident@host.com PRIVMSG me :Hello",
 | 
				
			||||||
 | 
								&Line{
 | 
				
			||||||
 | 
									Tags:  map[string]string{";": ";", " ": " ", "\r": "\r", "\n": "\n"},
 | 
				
			||||||
 | 
									Nick:  "nick",
 | 
				
			||||||
 | 
									Ident: "ident",
 | 
				
			||||||
 | 
									Host:  "host.com",
 | 
				
			||||||
 | 
									Src:   "nick!ident@host.com",
 | 
				
			||||||
 | 
									Cmd:   PRIVMSG,
 | 
				
			||||||
 | 
									Raw:   "@\\:=\\:;\\s=\\s;\\r=\\r;\\n=\\n :nick!ident@host.com PRIVMSG me :Hello",
 | 
				
			||||||
 | 
									Args:  []string{"me", "Hello"},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for i, test := range tests {
 | 
				
			||||||
 | 
							got := ParseLine(test.in)
 | 
				
			||||||
 | 
							if !reflect.DeepEqual(got, test.out) {
 | 
				
			||||||
 | 
								t.Errorf("test %d:\nexpected %#v\ngot %#v", i, test.out, got)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue