mirror of https://github.com/fluffle/goirc
Merge pull request #76 from jakebailey/master
Add IRCv3 tag parsing and CAP command. http://ircv3.net/specs/core/capability-negotiation-3.1.html http://ircv3.net/specs/core/message-tags-3.2.html
This commit is contained in:
commit
8be75dd9d4
|
@ -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,14 @@ 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 subcommand
|
||||||
|
// CAP subcommand :message
|
||||||
|
func (conn *Conn) Cap(subcommmand string, capabilities ...string) {
|
||||||
|
if len(capabilities) == 0 {
|
||||||
|
conn.Raw(CAP + " " + subcommmand)
|
||||||
|
} else {
|
||||||
|
conn.Raw(CAP + " " + subcommmand + " :" + strings.Join(capabilities, " "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,8 +105,40 @@ func (line *Line) Public() bool {
|
||||||
// CTCP command separated from any subsequent text. Then, CTCP ACTIONs are
|
// CTCP command separated from any subsequent text. Then, CTCP ACTIONs are
|
||||||
// rewritten such that Line.Cmd == ACTION. Other CTCP messages have Cmd
|
// rewritten such that Line.Cmd == ACTION. Other CTCP messages have Cmd
|
||||||
// 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.
|
||||||
|
//
|
||||||
|
// ParseLine also parses IRCv3 tags, if received. If a line does not have
|
||||||
|
// the tags section, Line.Tags will be nil. Tags are optional, and will
|
||||||
|
// only be included after the correct CAP command.
|
||||||
|
//
|
||||||
|
// http://ircv3.net/specs/core/capability-negotiation-3.1.html
|
||||||
|
// http://ircv3.net/specs/core/message-tags-3.2.html
|
||||||
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ; is represented as \: in a tag, so it's safe to split on ;
|
||||||
|
for _, tag := range strings.Split(rawTags, ";") {
|
||||||
|
if tag == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pair := strings.SplitN(tagsReplacer.Replace(tag), "=", 2)
|
||||||
|
if len(pair) < 2 {
|
||||||
|
line.Tags[tag] = ""
|
||||||
|
} else {
|
||||||
|
line.Tags[pair[0]] = pair[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 +201,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,82 @@ 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"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ // Skip empty tag
|
||||||
|
"@a=a; :nick!ident@host.com PRIVMSG me :Hello",
|
||||||
|
&Line{
|
||||||
|
Tags: map[string]string{"a": "a"},
|
||||||
|
Nick: "nick",
|
||||||
|
Ident: "ident",
|
||||||
|
Host: "host.com",
|
||||||
|
Src: "nick!ident@host.com",
|
||||||
|
Cmd: PRIVMSG,
|
||||||
|
Raw: "@a=a; :nick!ident@host.com PRIVMSG me :Hello",
|
||||||
|
Args: []string{"me", "Hello"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ // = in tag
|
||||||
|
"@a=a=a; :nick!ident@host.com PRIVMSG me :Hello",
|
||||||
|
&Line{
|
||||||
|
Tags: map[string]string{"a": "a=a"},
|
||||||
|
Nick: "nick",
|
||||||
|
Ident: "ident",
|
||||||
|
Host: "host.com",
|
||||||
|
Src: "nick!ident@host.com",
|
||||||
|
Cmd: PRIVMSG,
|
||||||
|
Raw: "@a=a=a; :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…
Reference in New Issue