Split long messages at a configurable length. Fixes #29.

This commit is contained in:
Alex Bramley 2013-03-17 01:21:09 +00:00
parent 0b64613fe3
commit 5c56572b0d
3 changed files with 146 additions and 12 deletions

View File

@ -38,6 +38,46 @@ func cutNewLines(s string) string {
return r[0] 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 // Raw() sends a raw line to the server, should really only be used for
// debugging purposes but may well come in handy. // debugging purposes but may well come in handy.
func (conn *Conn) Raw(rawline string) { 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) } func (conn *Conn) Who(nick string) { conn.Raw(WHO + " " + nick) }
// Privmsg() sends a PRIVMSG to the target t // 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 // 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 // Ctcp() sends a (generic) CTCP message to the target t
// with an optional argument // with an optional argument
func (conn *Conn) Ctcp(t, ctcp string, arg ...string) { func (conn *Conn) Ctcp(t, ctcp string, arg ...string) {
msg := strings.Join(arg, " ") // We need to split again here to ensure
if msg != "" { for _, s := range splitMessage(strings.Join(arg, " "), conn.cfg.SplitLen) {
msg = " " + msg 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 // CtcpReply() sends a generic CTCP reply to the target t
// with an optional argument // with an optional argument
func (conn *Conn) CtcpReply(t, ctcp string, arg ...string) { func (conn *Conn) CtcpReply(t, ctcp string, arg ...string) {
msg := strings.Join(arg, " ") for _, s := range splitMessage(strings.Join(arg, " "), conn.cfg.SplitLen) {
if msg != "" { if s != "" {
msg = " " + msg 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 // Version() sends a CTCP "VERSION" to the target t

View File

@ -1,6 +1,9 @@
package client package client
import "testing" import (
"reflect"
"testing"
)
func TestCutNewLines(t *testing.T) { func TestCutNewLines(t *testing.T) {
tests := []struct{ in, out string }{ tests := []struct{ in, out string }{
@ -16,7 +19,58 @@ func TestCutNewLines(t *testing.T) {
for i, test := range tests { for i, test := range tests {
out := cutNewLines(test.in) out := cutNewLines(test.in)
if test.out != out { 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) c, s := setUp(t)
defer s.tearDown() 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") c.Pass("password")
s.nc.Expect("PASS password") s.nc.Expect("PASS password")
@ -59,12 +117,30 @@ func TestClientCommands(t *testing.T) {
c.Privmsg("#foo", "bar") c.Privmsg("#foo", "bar")
s.nc.Expect("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") c.Notice("somebody", "something")
s.nc.Expect("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") c.Ctcp("somebody", "ping", "123456789")
s.nc.Expect("PRIVMSG somebody :\001PING 123456789\001") 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") c.CtcpReply("somebody", "pong", "123456789")
s.nc.Expect("NOTICE somebody :\001PONG 123456789\001") s.nc.Expect("NOTICE somebody :\001PONG 123456789\001")

View File

@ -71,6 +71,10 @@ type Config struct {
// Configurable panic recovery for all handlers. // Configurable panic recovery for all handlers.
Recover func(*Conn, *Line) 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 { func NewConfig(nick string, args ...string) *Config {
@ -79,6 +83,7 @@ func NewConfig(nick string, args ...string) *Config {
PingFreq: 3 * time.Minute, PingFreq: 3 * time.Minute,
NewNick: func(s string) string { return s + "_" }, NewNick: func(s string) string { return s + "_" },
Recover: (*Conn).LogPanic, // in dispatch.go Recover: (*Conn).LogPanic, // in dispatch.go
SplitLen: 450,
} }
cfg.Me.Ident = "goirc" cfg.Me.Ident = "goirc"
if len(args) > 0 && args[0] != "" { if len(args) > 0 && args[0] != "" {