diff --git a/client/commands.go b/client/commands.go index 1f471c9..20e3a66 100644 --- a/client/commands.go +++ b/client/commands.go @@ -38,6 +38,46 @@ func cutNewLines(s string) string { return r[0] } +// indexFragment looks for the last sentence split-point (defined as one of +// the punctuation characters .:;,!?"' followed by a space) in the string s +// and returns the index in the string after that split-point. If no split- +// point is found it returns the index after the last space in s, or -1. +func indexFragment(s string) int { + max := -1 + for _, sep := range []string{". ", ": ", "; ", ", ", "! ", "? ", "\" ", "' "} { + if idx := strings.LastIndex(s, sep); idx > max { + max = idx + } + } + if max > 0 { + return max + 2 + } + if idx := strings.LastIndex(s, " "); idx > 0 { + return idx + 1 + } + return -1 +} + +// splitMessage splits a message > splitLen chars at: +// 1. the end of the last sentence fragment before splitLen +// 2. the end of the last word before splitLen +// 3. splitLen itself +func splitMessage(msg string, splitLen int) (msgs []string) { + // This is quite short ;-) + if splitLen < 10 { + splitLen = 10 + } + for len(msg) > splitLen { + idx := indexFragment(msg[:splitLen]) + if idx < 0 { + idx = splitLen + } + msgs = append(msgs, msg[:idx] + "...") + msg = msg[idx:] + } + return append(msgs, msg) +} + // Raw() sends a raw line to the server, should really only be used for // debugging purposes but may well come in handy. func (conn *Conn) Raw(rawline string) { @@ -93,29 +133,42 @@ func (conn *Conn) Whois(nick string) { conn.Raw(WHOIS + " " + nick) } func (conn *Conn) Who(nick string) { conn.Raw(WHO + " " + nick) } // Privmsg() sends a PRIVMSG to the target t -func (conn *Conn) Privmsg(t, msg string) { conn.Raw(PRIVMSG + " " + t + " :" + msg) } +func (conn *Conn) Privmsg(t, msg string) { + for _, s := range splitMessage(msg, conn.cfg.SplitLen) { + conn.Raw(PRIVMSG + " " + t + " :" + s) + } +} // Notice() sends a NOTICE to the target t -func (conn *Conn) Notice(t, msg string) { conn.Raw(NOTICE + " " + t + " :" + msg) } +func (conn *Conn) Notice(t, msg string) { + for _, s := range splitMessage(msg, conn.cfg.SplitLen) { + conn.Raw(NOTICE + " " + t + " :" + s) + } +} // Ctcp() sends a (generic) CTCP message to the target t // with an optional argument func (conn *Conn) Ctcp(t, ctcp string, arg ...string) { - msg := strings.Join(arg, " ") - if msg != "" { - msg = " " + msg + // We need to split again here to ensure + for _, s := range splitMessage(strings.Join(arg, " "), conn.cfg.SplitLen) { + if s != "" { + s = " " + s + } + // Using Raw rather than PRIVMSG here to avoid double-split problems. + conn.Raw(PRIVMSG + " " + t + " :\001" + strings.ToUpper(ctcp) + s + "\001") } - conn.Privmsg(t, "\001"+strings.ToUpper(ctcp)+msg+"\001") } // CtcpReply() sends a generic CTCP reply to the target t // with an optional argument func (conn *Conn) CtcpReply(t, ctcp string, arg ...string) { - msg := strings.Join(arg, " ") - if msg != "" { - msg = " " + msg + for _, s := range splitMessage(strings.Join(arg, " "), conn.cfg.SplitLen) { + if s != "" { + s = " " + s + } + // Using Raw rather than NOTICE here to avoid double-split problems. + conn.Raw(NOTICE + " " + t + " :\001" + strings.ToUpper(ctcp) + s + "\001") } - conn.Notice(t, "\001"+strings.ToUpper(ctcp)+msg+"\001") } // Version() sends a CTCP "VERSION" to the target t diff --git a/client/commands_test.go b/client/commands_test.go index 4c47ee6..dabaf61 100644 --- a/client/commands_test.go +++ b/client/commands_test.go @@ -1,6 +1,9 @@ package client -import "testing" +import ( + "reflect" + "testing" +) func TestCutNewLines(t *testing.T) { tests := []struct{ in, out string }{ @@ -16,7 +19,58 @@ func TestCutNewLines(t *testing.T) { for i, test := range tests { out := cutNewLines(test.in) if test.out != out { - t.Errorf("test %d: expected '%s', got '%s'", i, test.out, out) + t.Errorf("test %d: expected %q, got %q", i, test.out, out) + } + } +} + +func TestIndexFragment(t *testing.T) { + tests := []struct { + in string + out int + }{ + {"", -1}, + {"foobarbaz", -1}, + {"foo bar baz", 8}, + {"foo. bar baz", 5}, + {"foo: bar baz", 5}, + {"foo; bar baz", 5}, + {"foo, bar baz", 5}, + {"foo! bar baz", 5}, + {"foo? bar baz", 5}, + {"foo\" bar baz", 5}, + {"foo' bar baz", 5}, + {"foo. bar. baz beep", 10}, + {"foo. bar, baz beep", 10}, + } + for i, test := range tests { + out := indexFragment(test.in) + if test.out != out { + t.Errorf("test %d: expected %d, got %d", i, test.out, out) + } + } +} + +func TestSplitMessage(t *testing.T) { + tests := []struct { + in string + sp int + out []string + }{ + {"", 0, []string{""}}, + {"foo", 0, []string{"foo"}}, + {"foo bar baz beep", 0, []string{"foo bar ...", "baz beep"}}, + {"foo bar baz beep", 13, []string{"foo bar baz ...", "beep"}}, + {"foo. bar baz beep", 0, []string{"foo. ...", "bar baz ...", "beep"}}, + {"foo bar, baz beep", 13, []string{"foo bar, ...", "baz beep"}}, + {"0123456789012345", 0, []string{"0123456789...", "012345"}}, + {"0123456789012345", 13, []string{"0123456789012...", "345"}}, + {"0123456789012345", 20, []string{"0123456789012345"}}, + } + for i, test := range tests { + out := splitMessage(test.in, test.sp) + if !reflect.DeepEqual(test.out, out) { + t.Errorf("test %d: expected %q, got %q", i, test.out, out) } } } @@ -25,6 +79,10 @@ func TestClientCommands(t *testing.T) { c, s := setUp(t) defer s.tearDown() + // Avoid having to type ridiculously long lines to test that + // messages longer than SplitLen are correctly sent to the server. + c.cfg.SplitLen = 20 + c.Pass("password") s.nc.Expect("PASS password") @@ -59,12 +117,30 @@ func TestClientCommands(t *testing.T) { c.Privmsg("#foo", "bar") s.nc.Expect("PRIVMSG #foo :bar") + // 0123456789012345678901234567890123 + c.Privmsg("#foo", "foo bar baz blorp. woo woobly woo.") + s.nc.Expect("PRIVMSG #foo :foo bar baz blorp. ...") + s.nc.Expect("PRIVMSG #foo :woo woobly woo.") + c.Notice("somebody", "something") s.nc.Expect("NOTICE somebody :something") + // 01234567890123456789012345678901234567 + c.Notice("somebody", "something much much longer that splits") + s.nc.Expect("NOTICE somebody :something much much ...") + s.nc.Expect("NOTICE somebody :longer that splits") + c.Ctcp("somebody", "ping", "123456789") s.nc.Expect("PRIVMSG somebody :\001PING 123456789\001") + c.Ctcp("somebody", "ping", "123456789012345678901234567890") + s.nc.Expect("PRIVMSG somebody :\001PING 12345678901234567890...\001") + s.nc.Expect("PRIVMSG somebody :\001PING 1234567890\001") + + c.CtcpReply("somebody", "pong", "123456789012345678901234567890") + s.nc.Expect("NOTICE somebody :\001PONG 12345678901234567890...\001") + s.nc.Expect("NOTICE somebody :\001PONG 1234567890\001") + c.CtcpReply("somebody", "pong", "123456789") s.nc.Expect("NOTICE somebody :\001PONG 123456789\001") diff --git a/client/connection.go b/client/connection.go index f093125..6e469e0 100644 --- a/client/connection.go +++ b/client/connection.go @@ -71,6 +71,10 @@ type Config struct { // Configurable panic recovery for all handlers. Recover func(*Conn, *Line) + + // Split PRIVMSGs, NOTICEs and CTCPs longer than + // SplitLen characters over multiple lines. + SplitLen int } func NewConfig(nick string, args ...string) *Config { @@ -79,6 +83,7 @@ func NewConfig(nick string, args ...string) *Config { PingFreq: 3 * time.Minute, NewNick: func(s string) string { return s + "_" }, Recover: (*Conn).LogPanic, // in dispatch.go + SplitLen: 450, } cfg.Me.Ident = "goirc" if len(args) > 0 && args[0] != "" {