diff --git a/gencapdefs.py b/gencapdefs.py index 0dab5b3e..a05a28c4 100644 --- a/gencapdefs.py +++ b/gencapdefs.py @@ -180,6 +180,7 @@ CAPDEFS = [ ] def validate_defs(): + CAPDEFS.sort(key=lambda d: d.name) numCaps = len(CAPDEFS) numNames = len(set(capdef.name for capdef in CAPDEFS)) if numCaps != numNames: diff --git a/irc/caps/defs.go b/irc/caps/defs.go index 4092bb2f..2ae423b5 100644 --- a/irc/caps/defs.go +++ b/irc/caps/defs.go @@ -13,10 +13,6 @@ const ( ) const ( - // Acc is the proposed IRCv3 capability named "draft/acc": - // https://github.com/ircv3/ircv3-specifications/pull/276 - Acc Capability = iota - // AccountNotify is the IRCv3 capability named "account-notify": // https://ircv3.net/specs/extensions/account-notify-3.1.html AccountNotify Capability = iota @@ -41,6 +37,34 @@ const ( // https://ircv3.net/specs/extensions/chghost-3.2.html ChgHost Capability = iota + // Acc is the proposed IRCv3 capability named "draft/acc": + // https://github.com/ircv3/ircv3-specifications/pull/276 + Acc Capability = iota + + // EventPlayback is the Proposed IRCv3 capability named "draft/event-playback": + // https://github.com/ircv3/ircv3-specifications/pull/362 + EventPlayback Capability = iota + + // LabeledResponse is the draft IRCv3 capability named "draft/labeled-response-0.2": + // https://ircv3.net/specs/extensions/labeled-response.html + LabeledResponse Capability = iota + + // Languages is the proposed IRCv3 capability named "draft/languages": + // https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6 + Languages Capability = iota + + // Rename is the proposed IRCv3 capability named "draft/rename": + // https://github.com/SaberUK/ircv3-specifications/blob/rename/extensions/rename.md + Rename Capability = iota + + // Resume is the proposed IRCv3 capability named "draft/resume-0.5": + // https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md + Resume Capability = iota + + // SetName is the proposed IRCv3 capability named "draft/setname": + // https://github.com/ircv3/ircv3-specifications/pull/361 + SetName Capability = iota + // EchoMessage is the IRCv3 capability named "echo-message": // https://ircv3.net/specs/extensions/echo-message-3.2.html EchoMessage Capability = iota @@ -53,18 +77,6 @@ const ( // https://ircv3.net/specs/extensions/invite-notify-3.2.html InviteNotify Capability = iota - // LabeledResponse is the draft IRCv3 capability named "draft/labeled-response-0.2": - // https://ircv3.net/specs/extensions/labeled-response.html - LabeledResponse Capability = iota - - // Languages is the proposed IRCv3 capability named "draft/languages": - // https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6 - Languages Capability = iota - - // MaxLine is the Oragono-specific capability named "oragono.io/maxline-2": - // https://oragono.io/maxline-2 - MaxLine Capability = iota - // MessageTags is the IRCv3 capability named "message-tags": // https://ircv3.net/specs/extensions/message-tags.html MessageTags Capability = iota @@ -73,13 +85,17 @@ const ( // https://ircv3.net/specs/extensions/multi-prefix-3.1.html MultiPrefix Capability = iota - // Rename is the proposed IRCv3 capability named "draft/rename": - // https://github.com/SaberUK/ircv3-specifications/blob/rename/extensions/rename.md - Rename Capability = iota + // Bouncer is the Oragono-specific capability named "oragono.io/bnc": + // https://oragono.io/bnc + Bouncer Capability = iota - // Resume is the proposed IRCv3 capability named "draft/resume-0.5": - // https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md - Resume Capability = iota + // MaxLine is the Oragono-specific capability named "oragono.io/maxline-2": + // https://oragono.io/maxline-2 + MaxLine Capability = iota + + // Nope is the Oragono vendor capability named "oragono.io/nope": + // https://oragono.io/nope + Nope Capability = iota // SASL is the IRCv3 capability named "sasl": // https://ircv3.net/specs/extensions/sasl-3.2.html @@ -89,10 +105,6 @@ const ( // https://ircv3.net/specs/extensions/server-time-3.2.html ServerTime Capability = iota - // SetName is the proposed IRCv3 capability named "draft/setname": - // https://github.com/ircv3/ircv3-specifications/pull/361 - SetName Capability = iota - // STS is the IRCv3 capability named "sts": // https://ircv3.net/specs/extensions/sts.html STS Capability = iota @@ -101,56 +113,44 @@ const ( // https://ircv3.net/specs/extensions/userhost-in-names-3.2.html UserhostInNames Capability = iota - // Bouncer is the Oragono-specific capability named "oragono.io/bnc": - // https://oragono.io/bnc - Bouncer Capability = iota - - // ZNCSelfMessage is the ZNC vendor capability named "znc.in/self-message": - // https://wiki.znc.in/Query_buffers - ZNCSelfMessage Capability = iota - - // EventPlayback is the Proposed IRCv3 capability named "draft/event-playback": - // https://github.com/ircv3/ircv3-specifications/pull/362 - EventPlayback Capability = iota - // ZNCPlayback is the ZNC vendor capability named "znc.in/playback": // https://wiki.znc.in/Playback ZNCPlayback Capability = iota - // Nope is the Oragono vendor capability named "oragono.io/nope": - // https://oragono.io/nope - Nope Capability = iota + // ZNCSelfMessage is the ZNC vendor capability named "znc.in/self-message": + // https://wiki.znc.in/Query_buffers + ZNCSelfMessage Capability = iota ) // `capabilityNames[capab]` is the string name of the capability `capab` var ( capabilityNames = [numCapabs]string{ - "draft/acc", "account-notify", "account-tag", "away-notify", "batch", "cap-notify", "chghost", + "draft/acc", + "draft/event-playback", + "draft/labeled-response-0.2", + "draft/languages", + "draft/rename", + "draft/resume-0.5", + "draft/setname", "echo-message", "extended-join", "invite-notify", - "draft/labeled-response-0.2", - "draft/languages", - "oragono.io/maxline-2", "message-tags", "multi-prefix", - "draft/rename", - "draft/resume-0.5", + "oragono.io/bnc", + "oragono.io/maxline-2", + "oragono.io/nope", "sasl", "server-time", - "draft/setname", "sts", "userhost-in-names", - "oragono.io/bnc", - "znc.in/self-message", - "draft/event-playback", "znc.in/playback", - "oragono.io/nope", + "znc.in/self-message", } ) diff --git a/irc/caps/set.go b/irc/caps/set.go index c5373b01..31d4c98d 100644 --- a/irc/caps/set.go +++ b/irc/caps/set.go @@ -4,9 +4,7 @@ package caps import ( - "bytes" - "sort" - + "fmt" "github.com/oragono/oragono/irc/utils" ) @@ -95,7 +93,8 @@ const maxPayloadLength = 440 // Strings returns all of our enabled capabilities as a slice of strings. func (s *Set) Strings(version Version, values Values) (result []string) { - var strs sort.StringSlice + var t utils.TokenLineBuilder + t.Initialize(maxPayloadLength, " ") var capab Capability asSlice := s[:] @@ -108,37 +107,15 @@ func (s *Set) Strings(version Version, values Values) (result []string) { if version >= Cap302 { val, exists := values[capab] if exists { - capString += "=" + val + capString = fmt.Sprintf("%s=%s", capString, val) } } - strs = append(strs, capString) + t.Add(capString) } - if len(strs) == 0 { - return []string{""} + result = t.Lines() + if result == nil { + result = []string{""} } - - // sort the cap string before we send it out - sort.Sort(strs) - - var buf bytes.Buffer - for _, str := range strs { - tokenLen := len(str) - if buf.Len() != 0 { - tokenLen += 1 - } - if maxPayloadLength < buf.Len()+tokenLen { - result = append(result, buf.String()) - buf.Reset() - } - if buf.Len() != 0 { - buf.WriteByte(' ') - } - buf.WriteString(str) - } - if buf.Len() != 0 { - result = append(result, buf.String()) - } - return } diff --git a/irc/utils/text.go b/irc/utils/text.go index 92b9af38..0623e255 100644 --- a/irc/utils/text.go +++ b/irc/utils/text.go @@ -83,3 +83,44 @@ func MakeSplitMessage(original string, origIs512 bool) (result SplitMessage) { return } + +// TokenLineBuilder is a helper for building IRC lines composed of delimited tokens, +// with a maximum line length. +type TokenLineBuilder struct { + lineLen int + delim string + buf bytes.Buffer + result []string +} + +func (t *TokenLineBuilder) Initialize(lineLen int, delim string) { + t.lineLen = lineLen + t.delim = delim +} + +// Add adds a token to the line, creating a new line if necessary. +func (t *TokenLineBuilder) Add(token string) { + tokenLen := len(token) + if t.buf.Len() != 0 { + tokenLen += len(t.delim) + } + if t.lineLen < t.buf.Len()+tokenLen { + t.result = append(t.result, t.buf.String()) + t.buf.Reset() + } + if t.buf.Len() != 0 { + t.buf.WriteString(t.delim) + } + t.buf.WriteString(token) +} + +// Lines terminates the line-building and returns all the lines. +func (t *TokenLineBuilder) Lines() (result []string) { + result = t.result + t.result = nil + if t.buf.Len() != 0 { + result = append(result, t.buf.String()) + t.buf.Reset() + } + return +} diff --git a/irc/utils/text_test.go b/irc/utils/text_test.go index 6defbe98..fac9f97a 100644 --- a/irc/utils/text_test.go +++ b/irc/utils/text_test.go @@ -58,3 +58,27 @@ func BenchmarkWordWrap(b *testing.B) { WordWrap(monteCristo, 60) } } + +func TestTokenLineBuilder(t *testing.T) { + lineLen := 400 + var tl TokenLineBuilder + tl.Initialize(lineLen, " ") + for _, token := range strings.Fields(monteCristo) { + tl.Add(token) + } + + lines := tl.Lines() + if len(lines) != 4 { + t.Errorf("expected 4 lines, got %d", len(lines)) + } + for _, line := range lines { + if len(line) > lineLen { + t.Errorf("line length %d exceeds maximum of %d", len(line), lineLen) + } + } + + joined := strings.Join(lines, " ") + if joined != monteCristo { + t.Errorf("text incorrectly split into lines: %s instead of %s", joined, monteCristo) + } +}