Compare commits
5 Commits
master
...
split-mess
Author | SHA1 | Date | |
---|---|---|---|
|
662a911d11 | ||
|
e2b3e11741 | ||
|
14813a795d | ||
|
b472e83947 | ||
|
6f29ca92da |
24
.github/workflows/test.yml
vendored
Normal file
24
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
name: test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push: {}
|
||||||
|
pull_request: { branches: [master] }
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v2
|
||||||
|
with: { go-version: '1.x' }
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: setup
|
||||||
|
run: |
|
||||||
|
go get -v golang.org/x/lint/golint
|
||||||
|
- name: lint
|
||||||
|
run: golint -min_confidence 0.9 -set_exit_status
|
||||||
|
- name: test
|
||||||
|
run: |
|
||||||
|
GORACE="exitcode=1 halt_on_error=1" go test -v -coverprofile=coverage.txt -race -timeout 3m -count 3 -cpu 1,4
|
||||||
|
bash <(curl -s https://codecov.io/bash)
|
||||||
|
- name: vet
|
||||||
|
run: go vet -v .
|
25
.travis.yml
25
.travis.yml
@ -1,25 +0,0 @@
|
|||||||
language: go
|
|
||||||
go:
|
|
||||||
- 1.11.x
|
|
||||||
- tip
|
|
||||||
before_install:
|
|
||||||
- go get -v golang.org/x/lint/golint
|
|
||||||
script:
|
|
||||||
- $HOME/gopath/bin/golint -min_confidence 0.9 -set_exit_status
|
|
||||||
- GORACE="exitcode=1 halt_on_error=1" go test -v -coverprofile=coverage.txt -race -timeout 3m -count 3 -cpu 1,4
|
|
||||||
- go vet -v .
|
|
||||||
after_success:
|
|
||||||
- bash <(curl -s https://codecov.io/bash)
|
|
||||||
branches:
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
notifications:
|
|
||||||
irc:
|
|
||||||
channels:
|
|
||||||
- irc.byteirc.org#/dev/null
|
|
||||||
template:
|
|
||||||
- "%{repository} #%{build_number} %{branch}/%{commit}: %{author} -- %{message}
|
|
||||||
%{build_url}"
|
|
||||||
on_success: change
|
|
||||||
on_failure: change
|
|
||||||
skip_join: false
|
|
@ -1,11 +1,11 @@
|
|||||||
<p align="center"><a href="https://godoc.org/github.com/lrstanley/girc"><img width="270" src="http://i.imgur.com/DEnyrdB.png"></a></p>
|
<p align="center"><a href="https://godoc.org/github.com/lrstanley/girc"><img width="270" src="http://i.imgur.com/DEnyrdB.png"></a></p>
|
||||||
<p align="center">girc, a flexible IRC library for Go</p>
|
<p align="center">girc, a flexible IRC library for Go</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://travis-ci.org/lrstanley/girc"><img src="https://travis-ci.org/lrstanley/girc.svg?branch=master" alt="Build Status"></a>
|
<a href="https://github.com/lrstanley/girc/actions"><img src="https://github.com/lrstanley/girc/workflows/test/badge.svg" alt="Test Status"></a>
|
||||||
<a href="https://codecov.io/gh/lrstanley/girc"><img src="https://codecov.io/gh/lrstanley/girc/branch/master/graph/badge.svg" alt="Coverage Status"></a>
|
<a href="https://codecov.io/gh/lrstanley/girc"><img src="https://codecov.io/gh/lrstanley/girc/branch/master/graph/badge.svg" alt="Coverage Status"></a>
|
||||||
<a href="https://godoc.org/github.com/lrstanley/girc"><img src="https://godoc.org/github.com/lrstanley/girc?status.png" alt="GoDoc"></a>
|
<a href="https://pkg.go.dev/github.com/lrstanley/girc"><img src="https://pkg.go.dev/badge/github.com/lrstanley/girc" alt="GoDoc"></a>
|
||||||
<a href="https://goreportcard.com/report/github.com/lrstanley/girc"><img src="https://goreportcard.com/badge/github.com/lrstanley/girc" alt="Go Report Card"></a>
|
<a href="https://goreportcard.com/report/github.com/lrstanley/girc"><img src="https://goreportcard.com/badge/github.com/lrstanley/girc" alt="Go Report Card"></a>
|
||||||
<a href="https://byteirc.org/channel/%23%2Fdev%2Fnull"><img src="https://img.shields.io/badge/ByteIRC-%23%2Fdev%2Fnull-blue.svg" alt="IRC Chat"></a>
|
<a href="https://liam.sh/chat"><img src="https://img.shields.io/badge/community-chat%20with%20us-green.svg" alt="Community Chat"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
42
builtin.go
42
builtin.go
@ -405,6 +405,48 @@ func handleISUPPORT(c *Client, e Event) {
|
|||||||
c.state.serverOptions[name] = val
|
c.state.serverOptions[name] = val
|
||||||
}
|
}
|
||||||
c.state.Unlock()
|
c.state.Unlock()
|
||||||
|
|
||||||
|
// Check for max line/nick/user/host lengths here.
|
||||||
|
c.state.RLock()
|
||||||
|
maxLineLength := c.state.maxLineLength
|
||||||
|
c.state.RUnlock()
|
||||||
|
maxNickLength := defaultNickLength
|
||||||
|
maxUserLength := defaultUserLength
|
||||||
|
maxHostLength := defaultHostLength
|
||||||
|
|
||||||
|
var ok bool
|
||||||
|
var tmp int
|
||||||
|
|
||||||
|
if tmp, ok = c.GetServerOptionInt("LINELEN"); ok {
|
||||||
|
maxLineLength = tmp
|
||||||
|
c.state.Lock()
|
||||||
|
c.state.maxLineLength = maxTagLength - 2 // -2 for CR-LF.
|
||||||
|
c.state.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
if tmp, ok = c.GetServerOptionInt("NICKLEN"); ok {
|
||||||
|
maxNickLength = tmp
|
||||||
|
}
|
||||||
|
if tmp, ok = c.GetServerOptionInt("MAXNICKLEN"); ok && tmp > maxNickLength {
|
||||||
|
maxNickLength = tmp
|
||||||
|
}
|
||||||
|
if tmp, ok = c.GetServerOptionInt("USERLEN"); ok && tmp > maxUserLength {
|
||||||
|
maxUserLength = tmp
|
||||||
|
}
|
||||||
|
if tmp, ok = c.GetServerOptionInt("HOSTLEN"); ok && tmp > maxHostLength {
|
||||||
|
maxHostLength = tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
prefixLen := defaultPrefixPadding + maxNickLength + maxUserLength + maxHostLength
|
||||||
|
if prefixLen >= maxLineLength {
|
||||||
|
// Give up and go with defaults.
|
||||||
|
c.state.notify(c, UPDATE_GENERAL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.state.Lock()
|
||||||
|
c.state.maxPrefixLength = prefixLen
|
||||||
|
c.state.Unlock()
|
||||||
|
|
||||||
c.state.notify(c, UPDATE_GENERAL)
|
c.state.notify(c, UPDATE_GENERAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ func TestTagGetSetCount(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add a hidden ascii value at the end to make it invalid.
|
// Add a hidden ascii value at the end to make it invalid.
|
||||||
if err := e.Tags.Set("key", "invalid-value"+string(0x08)); err == nil {
|
if err := e.Tags.Set("key", "invalid-value"+string(rune(0x08))); err == nil {
|
||||||
t.Fatal("tag set of invalid value should have returned error")
|
t.Fatal("tag set of invalid value should have returned error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
38
client.go
38
client.go
@ -678,6 +678,29 @@ func (c *Client) GetServerOption(key string) (result string, ok bool) {
|
|||||||
return result, ok
|
return result, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetServerOptionInt retrieves a server capability setting (as an integer) that was
|
||||||
|
// retrieved during client connection. This is also known as ISUPPORT (or RPL_PROTOCTL).
|
||||||
|
// Will panic if used when tracking has been disabled. Examples of usage:
|
||||||
|
//
|
||||||
|
// nickLen, success := GetServerOption("MAXNICKLEN")
|
||||||
|
//
|
||||||
|
func (c *Client) GetServerOptionInt(key string) (result int, ok bool) {
|
||||||
|
var data string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
data, ok = c.GetServerOption(key)
|
||||||
|
if !ok {
|
||||||
|
return result, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err = strconv.Atoi(data)
|
||||||
|
if err != nil {
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, ok
|
||||||
|
}
|
||||||
|
|
||||||
// NetworkName returns the network identifier. E.g. "EsperNet", "ByteIRC".
|
// NetworkName returns the network identifier. E.g. "EsperNet", "ByteIRC".
|
||||||
// May be empty if the server does not support RPL_ISUPPORT (or RPL_PROTOCTL).
|
// May be empty if the server does not support RPL_ISUPPORT (or RPL_PROTOCTL).
|
||||||
// Will panic if used when tracking has been disabled.
|
// Will panic if used when tracking has been disabled.
|
||||||
@ -752,6 +775,21 @@ func (c *Client) HasCapability(name string) (has bool) {
|
|||||||
return has
|
return has
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MaxEventLength return the maximum supported server length of an event. This is the
|
||||||
|
// maximum length of the command and arguments, excluding the source/prefix supported
|
||||||
|
// by the protocol. If state tracking is enabled, this will utilize ISUPPORT/IRCv3
|
||||||
|
// information to more accurately calculate the maximum supported length (i.e. extended
|
||||||
|
// length events).
|
||||||
|
func (c *Client) MaxEventLength() (max int) {
|
||||||
|
if !c.Config.disableTracking {
|
||||||
|
c.state.RLock()
|
||||||
|
max = c.state.maxLineLength - c.state.maxPrefixLength
|
||||||
|
c.state.RUnlock()
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
return DefaultMaxLineLength - DefaultMaxPrefixLength
|
||||||
|
}
|
||||||
|
|
||||||
// panicIfNotTracking will throw a panic when it's called, and tracking is
|
// panicIfNotTracking will throw a panic when it's called, and tracking is
|
||||||
// disabled. Adds useful info like what function specifically, and where it
|
// disabled. Adds useful info like what function specifically, and where it
|
||||||
// was called from.
|
// was called from.
|
||||||
|
@ -25,7 +25,7 @@ func (cmd *Commands) Nick(name string) {
|
|||||||
func (cmd *Commands) Join(channels ...string) {
|
func (cmd *Commands) Join(channels ...string) {
|
||||||
// We can join multiple channels at once, however we need to ensure that
|
// We can join multiple channels at once, however we need to ensure that
|
||||||
// we are not exceeding the line length. (see maxLength)
|
// we are not exceeding the line length. (see maxLength)
|
||||||
max := maxLength - len(JOIN) - 1
|
max := cmd.c.MaxEventLength() - len(JOIN) - 1
|
||||||
|
|
||||||
var buffer string
|
var buffer string
|
||||||
|
|
||||||
@ -329,7 +329,7 @@ func (cmd *Commands) List(channels ...string) {
|
|||||||
|
|
||||||
// We can LIST multiple channels at once, however we need to ensure that
|
// We can LIST multiple channels at once, however we need to ensure that
|
||||||
// we are not exceeding the line length. (see maxLength)
|
// we are not exceeding the line length. (see maxLength)
|
||||||
max := maxLength - len(JOIN) - 1
|
max := cmd.c.MaxEventLength() - len(JOIN) - 1
|
||||||
|
|
||||||
var buffer string
|
var buffer string
|
||||||
|
|
||||||
@ -356,7 +356,7 @@ func (cmd *Commands) List(channels ...string) {
|
|||||||
// Whowas sends a WHOWAS query to the server. amount is the amount of results
|
// Whowas sends a WHOWAS query to the server. amount is the amount of results
|
||||||
// you want back.
|
// you want back.
|
||||||
func (cmd *Commands) Whowas(user string, amount int) {
|
func (cmd *Commands) Whowas(user string, amount int) {
|
||||||
cmd.c.Send(&Event{Command: WHOWAS, Params: []string{user, string(amount)}})
|
cmd.c.Send(&Event{Command: WHOWAS, Params: []string{user, fmt.Sprint(amount)}})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Monitor sends a MONITOR query to the server. The results of the query
|
// Monitor sends a MONITOR query to the server. The results of the query
|
||||||
|
48
conn.go
48
conn.go
@ -427,35 +427,41 @@ func (c *Client) readLoop(ctx context.Context, errs chan error, wg *sync.WaitGro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send sends an event to the server. Use Client.RunHandlers() if you are
|
// Send sends an event to the server. Send will split events if the event is longer than
|
||||||
// simply looking to trigger handlers with an event.
|
// what the server supports, and is an event that supports splitting. Use
|
||||||
|
// Client.RunHandlers() if you are simply looking to trigger handlers with an event.
|
||||||
func (c *Client) Send(event *Event) {
|
func (c *Client) Send(event *Event) {
|
||||||
var delay time.Duration
|
var delay time.Duration
|
||||||
|
|
||||||
if !c.Config.AllowFlood {
|
|
||||||
c.mu.RLock()
|
|
||||||
|
|
||||||
// Drop the event early as we're disconnected, this way we don't have to wait
|
|
||||||
// the (potentially long) rate limit delay before dropping.
|
|
||||||
if c.conn == nil {
|
|
||||||
c.debugLogEvent(event, true)
|
|
||||||
c.mu.RUnlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.conn.mu.Lock()
|
|
||||||
delay = c.conn.rate(event.Len())
|
|
||||||
c.conn.mu.Unlock()
|
|
||||||
c.mu.RUnlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Config.GlobalFormat && len(event.Params) > 0 && event.Params[len(event.Params)-1] != "" &&
|
if c.Config.GlobalFormat && len(event.Params) > 0 && event.Params[len(event.Params)-1] != "" &&
|
||||||
(event.Command == PRIVMSG || event.Command == TOPIC || event.Command == NOTICE) {
|
(event.Command == PRIVMSG || event.Command == TOPIC || event.Command == NOTICE) {
|
||||||
event.Params[len(event.Params)-1] = Fmt(event.Params[len(event.Params)-1])
|
event.Params[len(event.Params)-1] = Fmt(event.Params[len(event.Params)-1])
|
||||||
}
|
}
|
||||||
|
|
||||||
<-time.After(delay)
|
var events []*Event
|
||||||
c.write(event)
|
events = event.split(c.MaxEventLength())
|
||||||
|
|
||||||
|
for _, e := range events {
|
||||||
|
if !c.Config.AllowFlood {
|
||||||
|
c.mu.RLock()
|
||||||
|
|
||||||
|
// Drop the event early as we're disconnected, this way we don't have to wait
|
||||||
|
// the (potentially long) rate limit delay before dropping.
|
||||||
|
if c.conn == nil {
|
||||||
|
c.debugLogEvent(e, true)
|
||||||
|
c.mu.RUnlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.conn.mu.Lock()
|
||||||
|
delay = c.conn.rate(e.Len())
|
||||||
|
c.conn.mu.Unlock()
|
||||||
|
c.mu.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
<-time.After(delay)
|
||||||
|
c.write(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// write is the lower level function to write an event. It does not have a
|
// write is the lower level function to write an event. It does not have a
|
||||||
|
130
event.go
130
event.go
@ -13,7 +13,41 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
eventSpace byte = ' ' // Separator.
|
eventSpace byte = ' ' // Separator.
|
||||||
maxLength = 510 // Maximum length is 510 (2 for line endings).
|
|
||||||
|
// TODO: if state tracking is enabled, we SHOULD be able to use it's known length.
|
||||||
|
|
||||||
|
// Can be overridden by the NICKLEN (or MAXNICKLEN) ISUPPORT parameter. 30 or 31
|
||||||
|
// are typical values for this parameter advertised by servers today.
|
||||||
|
defaultNickLength = 30
|
||||||
|
// The maximum length of <username> may be specified by the USERLEN RPL_ISUPPORT
|
||||||
|
// parameter. If this length is advertised, the username MUST be silently truncated
|
||||||
|
// to the given length before being used.
|
||||||
|
defaultUserLength = 18
|
||||||
|
// If a looked-up domain name is longer than this length (or overridden by the
|
||||||
|
// HOSTLEN ISUPPORT parameter), the server SHOULD opt to use the IP address instead,
|
||||||
|
// so that the hostname is underneath this length.
|
||||||
|
defaultHostLength = 63
|
||||||
|
|
||||||
|
// defaultPrefixPadding defaults the estimated prefix padding length of a given
|
||||||
|
// event. See also:
|
||||||
|
// [ ":" ( servername / ( nickname [ [ "!" user ] "@" host ] ) ) SPACE ]
|
||||||
|
defaultPrefixPadding = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DefaultMaxLineLength is the default maximum length for an event. 510 (+2 for line endings)
|
||||||
|
// is used as a default as this is used by many older implementations.
|
||||||
|
//
|
||||||
|
// See also: RFC 2812
|
||||||
|
// IRC messages are always lines of characters terminated with a CR-LF
|
||||||
|
// (Carriage Return - Line Feed) pair, and these messages SHALL NOT
|
||||||
|
// exceed 512 characters in length, counting all characters including
|
||||||
|
// the trailing CR-LF.
|
||||||
|
DefaultMaxLineLength = 510
|
||||||
|
|
||||||
|
// DefaultMaxPrefixLength defines the default max ":nickname!user@host " length
|
||||||
|
// that's used to calculate line splitting.
|
||||||
|
DefaultMaxPrefixLength = defaultPrefixPadding + defaultNickLength + defaultUserLength + defaultHostLength
|
||||||
)
|
)
|
||||||
|
|
||||||
// cutCRFunc is used to trim CR characters from prefixes/messages.
|
// cutCRFunc is used to trim CR characters from prefixes/messages.
|
||||||
@ -223,11 +257,82 @@ func (e *Event) Equals(ev *Event) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Len calculates the length of the string representation of event. Note that
|
// split will split a potentially large event that is larger than what the server
|
||||||
// this will return the true length (even if longer than what IRC supports),
|
// supports, into multiple events. split will ignore events that cannot be split, and
|
||||||
// which may be useful if you are trying to check and see if a message is
|
// if the event isn't longer than what the server supports, it will just return an array
|
||||||
// too long, to trim it down yourself.
|
// with 1 entry, the original event.
|
||||||
|
func (e *Event) split(maxLength int) []*Event {
|
||||||
|
if len(e.Params) < 1 || (e.Command != PRIVMSG && e.Command != NOTICE) {
|
||||||
|
return []*Event{e}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude source, even if it does exist, because the server will likely ignore the
|
||||||
|
// sent source anyway.
|
||||||
|
event := e.Copy()
|
||||||
|
event.Source = nil
|
||||||
|
|
||||||
|
if event.LenOpts(false) < maxLength {
|
||||||
|
return []*Event{e}
|
||||||
|
}
|
||||||
|
|
||||||
|
results := []*Event{}
|
||||||
|
|
||||||
|
// Will force the length check to include " :". This will allow us to get the length
|
||||||
|
// of the commands and necessary prefixes.
|
||||||
|
text := event.Last()
|
||||||
|
event.Params[len(event.Params)-1] = ""
|
||||||
|
cmdLen := event.LenOpts(false)
|
||||||
|
|
||||||
|
var ok bool
|
||||||
|
var ctcp *CTCPEvent
|
||||||
|
if ok, ctcp = e.IsCTCP(); ok {
|
||||||
|
if len(text) == 0 {
|
||||||
|
return []*Event{e}
|
||||||
|
}
|
||||||
|
|
||||||
|
text = ctcp.Text
|
||||||
|
|
||||||
|
// ctcpDelim's at start and end, and space between command and trailing text.
|
||||||
|
maxLength -= len(ctcp.Command) + 4
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: colors? use last color at start of split? make sure it's POST-color gen?
|
||||||
|
|
||||||
|
// If the command itself is longer than the limit, there is a problem. PRIVMSG should
|
||||||
|
// be 1->1 per RFC. Just return the original message and let it be the user of the
|
||||||
|
// libraries problem.
|
||||||
|
if cmdLen > maxLength {
|
||||||
|
return []*Event{e}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the text into correctly size segments, and make the necessary number of
|
||||||
|
// events that duplicate the original event.
|
||||||
|
for _, split := range splitAtWord(text, maxLength-cmdLen) {
|
||||||
|
if ctcp != nil {
|
||||||
|
split = string(ctcpDelim) + ctcp.Command + string(eventSpace) + split + string(ctcpDelim)
|
||||||
|
}
|
||||||
|
clonedEvent := event.Copy()
|
||||||
|
clonedEvent.Source = e.Source
|
||||||
|
clonedEvent.Params[len(e.Params)-1] = split
|
||||||
|
results = append(results, clonedEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len calculates the length of the string representation of event (including tags).
|
||||||
|
// Note that this will return the true length (even if longer than what IRC supports),
|
||||||
|
// which may be useful if you are trying to check and see if a message is too long, to
|
||||||
|
// trim it down yourself.
|
||||||
func (e *Event) Len() (length int) {
|
func (e *Event) Len() (length int) {
|
||||||
|
return e.LenOpts(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LenOpts calculates the length of the string representation of event (with a toggle
|
||||||
|
// for tags). Note that this will return the true length (even if longer than what IRC
|
||||||
|
// supports), which may be useful if you are trying to check and see if a message is
|
||||||
|
// too long, to trim it down yourself.
|
||||||
|
func (e *Event) LenOpts(includeTags bool) (length int) {
|
||||||
if e.Tags != nil {
|
if e.Tags != nil {
|
||||||
// Include tags and trailing space.
|
// Include tags and trailing space.
|
||||||
length = e.Tags.Len() + 1
|
length = e.Tags.Len() + 1
|
||||||
@ -248,7 +353,7 @@ func (e *Event) Len() (length int) {
|
|||||||
|
|
||||||
// If param contains a space or it's empty, it's trailing, so it should be
|
// If param contains a space or it's empty, it's trailing, so it should be
|
||||||
// prefixed with a colon (:).
|
// prefixed with a colon (:).
|
||||||
if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || e.Params[i] == "") {
|
if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || e.Params[i] == "" || strings.HasPrefix(e.Params[i], ":")) {
|
||||||
length++
|
length++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -259,10 +364,6 @@ func (e *Event) Len() (length int) {
|
|||||||
|
|
||||||
// Bytes returns a []byte representation of event. Strips all newlines and
|
// Bytes returns a []byte representation of event. Strips all newlines and
|
||||||
// carriage returns.
|
// carriage returns.
|
||||||
//
|
|
||||||
// Per RFC2812 section 2.3, messages should not exceed 512 characters in
|
|
||||||
// length. This method forces that limit by discarding any characters
|
|
||||||
// exceeding the length limit.
|
|
||||||
func (e *Event) Bytes() []byte {
|
func (e *Event) Bytes() []byte {
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
|
|
||||||
@ -283,10 +384,8 @@ func (e *Event) Bytes() []byte {
|
|||||||
|
|
||||||
// Space separated list of arguments.
|
// Space separated list of arguments.
|
||||||
if len(e.Params) > 0 {
|
if len(e.Params) > 0 {
|
||||||
// buffer.WriteByte(eventSpace)
|
|
||||||
|
|
||||||
for i := 0; i < len(e.Params); i++ {
|
for i := 0; i < len(e.Params); i++ {
|
||||||
if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || e.Params[i] == "") {
|
if i == len(e.Params)-1 && (strings.Contains(e.Params[i], " ") || e.Params[i] == "" || strings.HasPrefix(e.Params[i], ":")) {
|
||||||
buffer.WriteString(string(eventSpace) + string(messagePrefix) + e.Params[i])
|
buffer.WriteString(string(eventSpace) + string(messagePrefix) + e.Params[i])
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -294,11 +393,6 @@ func (e *Event) Bytes() []byte {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need the limit the buffer length.
|
|
||||||
if buffer.Len() > (maxLength) {
|
|
||||||
buffer.Truncate(maxLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
out := buffer.Bytes()
|
out := buffer.Bytes()
|
||||||
|
|
||||||
// Strip newlines and carriage returns.
|
// Strip newlines and carriage returns.
|
||||||
|
77
format.go
77
format.go
@ -350,3 +350,80 @@ func Glob(input, match string) bool {
|
|||||||
// Check suffix last.
|
// Check suffix last.
|
||||||
return trailingGlob || strings.HasSuffix(input, parts[last])
|
return trailingGlob || strings.HasSuffix(input, parts[last])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxWordSplitLength = 30
|
||||||
|
|
||||||
|
// splitAtWord is a text splitter that takes into consideration a few things:
|
||||||
|
// * Ensuring the returned text is no longer than maxWidth.
|
||||||
|
// * Attempting to split at the closest word boundary, while still staying inside
|
||||||
|
// of the specific maxWidth.
|
||||||
|
// * if there is no good word boundry for longer words (or e.g. links, raw data, etc)
|
||||||
|
// that are above maxWordSplitLength characters, split the word into chunks to fit the
|
||||||
|
// maximum width.
|
||||||
|
func splitAtWord(input string, maxWidth int) (output []string) {
|
||||||
|
// TODO: breaks multi-spaces.
|
||||||
|
words := strings.Fields(input)
|
||||||
|
|
||||||
|
// TODO: don't split a url if there isn't enough space, if it can safely fit within
|
||||||
|
// the next line.
|
||||||
|
// TODO: also split on newline, if splitting is enabled? makes it easier to just
|
||||||
|
// pipe text in.
|
||||||
|
// TODO: if word contains a dash, and adding the word to the line causes an overflow,
|
||||||
|
// try to split on the dash?
|
||||||
|
|
||||||
|
// Increment maxWidth for calculations, because we always prefix with a space (then
|
||||||
|
// strip it before we return).
|
||||||
|
maxWidth++
|
||||||
|
|
||||||
|
var i, spaceRemaining int
|
||||||
|
var split string
|
||||||
|
|
||||||
|
setupNextSplit:
|
||||||
|
spaceRemaining = maxWidth
|
||||||
|
split = ""
|
||||||
|
beginLoop:
|
||||||
|
for i < len(words) {
|
||||||
|
// Last line was the perfect length, add to output and keep looping.
|
||||||
|
if spaceRemaining == 0 {
|
||||||
|
output = append(output, split[1:])
|
||||||
|
goto setupNextSplit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Word makes the line too long.
|
||||||
|
if len(words[i])+1 > spaceRemaining {
|
||||||
|
// Is the word small enough to where we don't need to split it up?
|
||||||
|
if len(words[i]) < maxWordSplitLength && maxWidth >= maxWordSplitLength {
|
||||||
|
output = append(output, split[1:])
|
||||||
|
goto setupNextSplit
|
||||||
|
}
|
||||||
|
|
||||||
|
split += " " + words[i][0:spaceRemaining-1]
|
||||||
|
if len(words) == i {
|
||||||
|
words = append(words, words[i][spaceRemaining-1:])
|
||||||
|
} else {
|
||||||
|
words = append(words[:i+1], words[i:]...)
|
||||||
|
words[i+1] = words[i][spaceRemaining-1:]
|
||||||
|
words[i] = words[i][0 : spaceRemaining-1]
|
||||||
|
}
|
||||||
|
spaceRemaining -= 1 + len(words[i][0:spaceRemaining-1])
|
||||||
|
i++
|
||||||
|
goto beginLoop
|
||||||
|
}
|
||||||
|
|
||||||
|
split += " " + words[i]
|
||||||
|
spaceRemaining -= 1 + len(words[i])
|
||||||
|
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(split) > 0 {
|
||||||
|
output = append(output, split[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// At least return some kind of string, rather than nil.
|
||||||
|
if len(output) == 0 {
|
||||||
|
output = append(output, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
15
state.go
15
state.go
@ -28,11 +28,22 @@ type state struct {
|
|||||||
// last capability check. These will get sent once we have received the
|
// last capability check. These will get sent once we have received the
|
||||||
// last capability list command from the server.
|
// last capability list command from the server.
|
||||||
tmpCap map[string]map[string]string
|
tmpCap map[string]map[string]string
|
||||||
|
|
||||||
// serverOptions are the standard capabilities and configurations
|
// serverOptions are the standard capabilities and configurations
|
||||||
// supported by the server at connection time. This also includes
|
// supported by the server at connection time. This also includes
|
||||||
// RPL_ISUPPORT entries.
|
// RPL_ISUPPORT entries.
|
||||||
serverOptions map[string]string
|
serverOptions map[string]string
|
||||||
// motd is the servers message of the day.
|
// motd is the servers message of the day.
|
||||||
|
|
||||||
|
// maxLineLength defines how long before we truncate (or split) messages.
|
||||||
|
// DefaultMaxLineLength is what is used by default, as this is going to be a common
|
||||||
|
// standard. However, protocols like IRCv3, or ISUPPORT can override this.
|
||||||
|
maxLineLength int
|
||||||
|
|
||||||
|
// maxPrefixLength defines the estimated prefix length (":nick!user@host ") that
|
||||||
|
// we can use to calculate line splits.
|
||||||
|
maxPrefixLength int
|
||||||
|
|
||||||
motd string
|
motd string
|
||||||
|
|
||||||
// sts are strict transport security configurations, if specified by the
|
// sts are strict transport security configurations, if specified by the
|
||||||
@ -51,9 +62,11 @@ func (s *state) reset(initial bool) {
|
|||||||
s.host = ""
|
s.host = ""
|
||||||
s.channels = make(map[string]*Channel)
|
s.channels = make(map[string]*Channel)
|
||||||
s.users = make(map[string]*User)
|
s.users = make(map[string]*User)
|
||||||
s.serverOptions = make(map[string]string)
|
|
||||||
s.enabledCap = make(map[string]map[string]string)
|
s.enabledCap = make(map[string]map[string]string)
|
||||||
s.tmpCap = make(map[string]map[string]string)
|
s.tmpCap = make(map[string]map[string]string)
|
||||||
|
s.serverOptions = make(map[string]string)
|
||||||
|
s.maxLineLength = DefaultMaxLineLength
|
||||||
|
s.maxPrefixLength = DefaultMaxPrefixLength
|
||||||
s.motd = ""
|
s.motd = ""
|
||||||
|
|
||||||
if initial {
|
if initial {
|
||||||
|
Loading…
Reference in New Issue
Block a user