forked from External/ergo
use ergochat/irc-go instead of goshuirc/irc-go
This commit is contained in:
parent
66af8cd63c
commit
4910aefa37
32 changed files with 95 additions and 53 deletions
13
vendor/github.com/ergochat/irc-go/LICENSE
generated
vendored
Normal file
13
vendor/github.com/ergochat/irc-go/LICENSE
generated
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
Copyright (c) 2016-2017 Daniel Oaks
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
86
vendor/github.com/ergochat/irc-go/ircfmt/doc.go
generated
vendored
Normal file
86
vendor/github.com/ergochat/irc-go/ircfmt/doc.go
generated
vendored
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// written by Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the ISC license
|
||||
|
||||
/*
|
||||
Package ircfmt handles IRC formatting codes, escaping and unescaping.
|
||||
|
||||
This allows for a simpler representation of strings that contain colour codes,
|
||||
bold codes, and such, without having to write and handle raw bytes when
|
||||
assembling outgoing messages.
|
||||
|
||||
This lets you turn raw IRC messages into our escaped versions, and turn escaped
|
||||
versions back into raw messages suitable for sending on IRC connections. This
|
||||
is designed to be used on things like PRIVMSG / NOTICE commands, MOTD blocks,
|
||||
and such.
|
||||
|
||||
The escape character we use in this library is the dollar sign ("$"), along
|
||||
with the given escape characters:
|
||||
|
||||
--------------------------------
|
||||
Name | Escape | Raw
|
||||
--------------------------------
|
||||
Dollarsign | $$ | $
|
||||
Bold | $b | 0x02
|
||||
Colour | $c | 0x03
|
||||
Monospace | $m | 0x11
|
||||
Reverse Colour | $v | 0x16
|
||||
Italic | $i | 0x1d
|
||||
Strikethrough | $s | 0x1e
|
||||
Underscore | $u | 0x1f
|
||||
Reset | $r | 0x0f
|
||||
--------------------------------
|
||||
|
||||
Colours are escaped in a slightly different way, using the actual names of them
|
||||
rather than just the raw numbers.
|
||||
|
||||
In our escaped format, the colours for the fore and background are contained in
|
||||
square brackets after the colour ("$c") escape. For example:
|
||||
|
||||
Red foreground:
|
||||
Escaped: This is a $c[red]cool message!
|
||||
Raw: This is a 0x034cool message!
|
||||
|
||||
Blue foreground, green background:
|
||||
Escaped: This is a $c[blue,green]rad message!
|
||||
Raw: This is a 0x032,3rad message!
|
||||
|
||||
When assembling a raw message, we make sure to use the full colour code
|
||||
("02" vs just "2") when it could become confused due to numbers just after the
|
||||
colour escape code. For instance, lines like this will be unescaped correctly:
|
||||
|
||||
No number after colour escape:
|
||||
Escaped: This is a $c[red]cool message!
|
||||
Raw: This is a 0x034cool message!
|
||||
|
||||
Number after colour escape:
|
||||
Escaped: This is $c[blue]20% cooler!
|
||||
Raw: This is 0x030220% cooler
|
||||
|
||||
Here are the colour names and codes we recognise:
|
||||
|
||||
--------------------
|
||||
Code | Name
|
||||
--------------------
|
||||
00 | white
|
||||
01 | black
|
||||
02 | blue
|
||||
03 | green
|
||||
04 | red
|
||||
05 | brown
|
||||
06 | magenta
|
||||
07 | orange
|
||||
08 | yellow
|
||||
09 | light green
|
||||
10 | cyan
|
||||
11 | light cyan
|
||||
12 | light blue
|
||||
13 | pink
|
||||
14 | grey
|
||||
15 | light grey
|
||||
99 | default
|
||||
--------------------
|
||||
|
||||
These other colours aren't given names:
|
||||
https://modern.ircdocs.horse/formatting.html#colors-16-98
|
||||
*/
|
||||
package ircfmt
|
||||
317
vendor/github.com/ergochat/irc-go/ircfmt/ircfmt.go
generated
vendored
Normal file
317
vendor/github.com/ergochat/irc-go/ircfmt/ircfmt.go
generated
vendored
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
// written by Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the ISC license
|
||||
|
||||
package ircfmt
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// raw bytes and strings to do replacing with
|
||||
bold string = "\x02"
|
||||
colour string = "\x03"
|
||||
monospace string = "\x11"
|
||||
reverseColour string = "\x16"
|
||||
italic string = "\x1d"
|
||||
strikethrough string = "\x1e"
|
||||
underline string = "\x1f"
|
||||
reset string = "\x0f"
|
||||
|
||||
runecolour rune = '\x03'
|
||||
runebold rune = '\x02'
|
||||
runemonospace rune = '\x11'
|
||||
runereverseColour rune = '\x16'
|
||||
runeitalic rune = '\x1d'
|
||||
runestrikethrough rune = '\x1e'
|
||||
runereset rune = '\x0f'
|
||||
runeunderline rune = '\x1f'
|
||||
|
||||
// valid characters in a colour code character, for speed
|
||||
colours1 string = "0123456789"
|
||||
)
|
||||
|
||||
var (
|
||||
// valtoescape replaces most of IRC characters with our escapes.
|
||||
valtoescape = strings.NewReplacer("$", "$$", colour, "$c", reverseColour, "$v", bold, "$b", italic, "$i", strikethrough, "$s", underline, "$u", monospace, "$m", reset, "$r")
|
||||
// valToStrip replaces most of the IRC characters with nothing
|
||||
valToStrip = strings.NewReplacer(colour, "$c", reverseColour, "", bold, "", italic, "", strikethrough, "", underline, "", monospace, "", reset, "")
|
||||
|
||||
// escapetoval contains most of our escapes and how they map to real IRC characters.
|
||||
// intentionally skips colour, since that's handled elsewhere.
|
||||
escapetoval = map[rune]string{
|
||||
'$': "$",
|
||||
'b': bold,
|
||||
'i': italic,
|
||||
'v': reverseColour,
|
||||
's': strikethrough,
|
||||
'u': underline,
|
||||
'm': monospace,
|
||||
'r': reset,
|
||||
}
|
||||
|
||||
// valid colour codes
|
||||
numtocolour = map[string]string{
|
||||
"99": "default",
|
||||
"15": "light grey",
|
||||
"14": "grey",
|
||||
"13": "pink",
|
||||
"12": "light blue",
|
||||
"11": "light cyan",
|
||||
"10": "cyan",
|
||||
"09": "light green",
|
||||
"08": "yellow",
|
||||
"07": "orange",
|
||||
"06": "magenta",
|
||||
"05": "brown",
|
||||
"04": "red",
|
||||
"03": "green",
|
||||
"02": "blue",
|
||||
"01": "black",
|
||||
"00": "white",
|
||||
"9": "light green",
|
||||
"8": "yellow",
|
||||
"7": "orange",
|
||||
"6": "magenta",
|
||||
"5": "brown",
|
||||
"4": "red",
|
||||
"3": "green",
|
||||
"2": "blue",
|
||||
"1": "black",
|
||||
"0": "white",
|
||||
}
|
||||
|
||||
colourcodesTruncated = map[string]string{
|
||||
"white": "0",
|
||||
"black": "1",
|
||||
"blue": "2",
|
||||
"green": "3",
|
||||
"red": "4",
|
||||
"brown": "5",
|
||||
"magenta": "6",
|
||||
"orange": "7",
|
||||
"yellow": "8",
|
||||
"light green": "9",
|
||||
"cyan": "10",
|
||||
"light cyan": "11",
|
||||
"light blue": "12",
|
||||
"pink": "13",
|
||||
"grey": "14",
|
||||
"light grey": "15",
|
||||
"default": "99",
|
||||
}
|
||||
|
||||
bracketedExpr = regexp.MustCompile(`^\[.*?\]`)
|
||||
colourDigits = regexp.MustCompile(`^[0-9]{1,2}$`)
|
||||
)
|
||||
|
||||
// Escape takes a raw IRC string and returns it with our escapes.
|
||||
//
|
||||
// IE, it turns this: "This is a \x02cool\x02, \x034red\x0f message!"
|
||||
// into: "This is a $bcool$b, $c[red]red$r message!"
|
||||
func Escape(in string) string {
|
||||
// replace all our usual escapes
|
||||
in = valtoescape.Replace(in)
|
||||
|
||||
inRunes := []rune(in)
|
||||
//var out string
|
||||
out := strings.Builder{}
|
||||
for 0 < len(inRunes) {
|
||||
if 1 < len(inRunes) && inRunes[0] == '$' && inRunes[1] == 'c' {
|
||||
// handle colours
|
||||
out.WriteString("$c")
|
||||
inRunes = inRunes[2:] // strip colour code chars
|
||||
|
||||
if len(inRunes) < 1 || !strings.Contains(colours1, string(inRunes[0])) {
|
||||
out.WriteString("[]")
|
||||
continue
|
||||
}
|
||||
|
||||
var foreBuffer, backBuffer string
|
||||
foreBuffer += string(inRunes[0])
|
||||
inRunes = inRunes[1:]
|
||||
if 0 < len(inRunes) && strings.Contains(colours1, string(inRunes[0])) {
|
||||
foreBuffer += string(inRunes[0])
|
||||
inRunes = inRunes[1:]
|
||||
}
|
||||
if 1 < len(inRunes) && inRunes[0] == ',' && strings.Contains(colours1, string(inRunes[1])) {
|
||||
backBuffer += string(inRunes[1])
|
||||
inRunes = inRunes[2:]
|
||||
if 0 < len(inRunes) && strings.Contains(colours1, string(inRunes[0])) {
|
||||
backBuffer += string(inRunes[0])
|
||||
inRunes = inRunes[1:]
|
||||
}
|
||||
}
|
||||
|
||||
foreName, exists := numtocolour[foreBuffer]
|
||||
if !exists {
|
||||
foreName = foreBuffer
|
||||
}
|
||||
backName, exists := numtocolour[backBuffer]
|
||||
if !exists {
|
||||
backName = backBuffer
|
||||
}
|
||||
|
||||
out.WriteRune('[')
|
||||
out.WriteString(foreName)
|
||||
if backName != "" {
|
||||
out.WriteRune(',')
|
||||
out.WriteString(backName)
|
||||
}
|
||||
out.WriteRune(']')
|
||||
|
||||
} else {
|
||||
// special case for $$c
|
||||
if len(inRunes) > 2 && inRunes[0] == '$' && inRunes[1] == '$' && inRunes[2] == 'c' {
|
||||
out.WriteRune(inRunes[0])
|
||||
out.WriteRune(inRunes[1])
|
||||
out.WriteRune(inRunes[2])
|
||||
inRunes = inRunes[3:]
|
||||
} else {
|
||||
out.WriteRune(inRunes[0])
|
||||
inRunes = inRunes[1:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out.String()
|
||||
}
|
||||
|
||||
// Strip takes a raw IRC string and removes it with all formatting codes removed
|
||||
// IE, it turns this: "This is a \x02cool\x02, \x034red\x0f message!"
|
||||
// into: "This is a cool, red message!"
|
||||
func Strip(in string) string {
|
||||
out := strings.Builder{}
|
||||
runes := []rune(in)
|
||||
if out.Len() < len(runes) { // Reduce allocations where needed
|
||||
out.Grow(len(in) - out.Len())
|
||||
}
|
||||
for len(runes) > 0 {
|
||||
switch runes[0] {
|
||||
case runebold, runemonospace, runereverseColour, runeitalic, runestrikethrough, runeunderline, runereset:
|
||||
runes = runes[1:]
|
||||
case runecolour:
|
||||
runes = removeColour(runes)
|
||||
default:
|
||||
out.WriteRune(runes[0])
|
||||
runes = runes[1:]
|
||||
}
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func removeNumber(runes []rune) []rune {
|
||||
if len(runes) > 0 && runes[0] >= '0' && runes[0] <= '9' {
|
||||
runes = runes[1:]
|
||||
}
|
||||
return runes
|
||||
}
|
||||
|
||||
func removeColour(runes []rune) []rune {
|
||||
if runes[0] != runecolour {
|
||||
return runes
|
||||
}
|
||||
|
||||
runes = runes[1:]
|
||||
runes = removeNumber(runes)
|
||||
runes = removeNumber(runes)
|
||||
|
||||
if len(runes) > 1 && runes[0] == ',' && runes[1] >= '0' && runes[1] <= '9' {
|
||||
runes = runes[2:]
|
||||
} else {
|
||||
return runes // Nothing else because we dont have a comma
|
||||
}
|
||||
runes = removeNumber(runes)
|
||||
return runes
|
||||
}
|
||||
|
||||
// resolve "light blue" to "12", "12" to "12", "asdf" to "", etc.
|
||||
func resolveToColourCode(str string) (result string) {
|
||||
str = strings.ToLower(strings.TrimSpace(str))
|
||||
if colourDigits.MatchString(str) {
|
||||
return str
|
||||
}
|
||||
return colourcodesTruncated[str]
|
||||
}
|
||||
|
||||
// resolve "[light blue, black]" to ("13, "1")
|
||||
func resolveToColourCodes(namedColors string) (foreground, background string) {
|
||||
// cut off the brackets
|
||||
namedColors = strings.TrimPrefix(namedColors, "[")
|
||||
namedColors = strings.TrimSuffix(namedColors, "]")
|
||||
|
||||
var foregroundStr, backgroundStr string
|
||||
commaIdx := strings.IndexByte(namedColors, ',')
|
||||
if commaIdx != -1 {
|
||||
foregroundStr = namedColors[:commaIdx]
|
||||
backgroundStr = namedColors[commaIdx+1:]
|
||||
} else {
|
||||
foregroundStr = namedColors
|
||||
}
|
||||
|
||||
return resolveToColourCode(foregroundStr), resolveToColourCode(backgroundStr)
|
||||
}
|
||||
|
||||
// Unescape takes our escaped string and returns a raw IRC string.
|
||||
//
|
||||
// IE, it turns this: "This is a $bcool$b, $c[red]red$r message!"
|
||||
// into this: "This is a \x02cool\x02, \x034red\x0f message!"
|
||||
func Unescape(in string) string {
|
||||
var out strings.Builder
|
||||
|
||||
remaining := in
|
||||
for len(remaining) != 0 {
|
||||
char := remaining[0]
|
||||
remaining = remaining[1:]
|
||||
|
||||
if char != '$' || len(remaining) == 0 {
|
||||
// not an escape
|
||||
out.WriteByte(char)
|
||||
continue
|
||||
}
|
||||
|
||||
// ingest the next character of the escape
|
||||
char = remaining[0]
|
||||
remaining = remaining[1:]
|
||||
|
||||
if char == 'c' {
|
||||
out.WriteString(colour)
|
||||
|
||||
namedColors := bracketedExpr.FindString(remaining)
|
||||
if namedColors == "" {
|
||||
// for a non-bracketed color code, output the following characters directly,
|
||||
// e.g., `$c1,8` will become `\x031,8`
|
||||
continue
|
||||
}
|
||||
// process bracketed color codes:
|
||||
remaining = remaining[len(namedColors):]
|
||||
followedByDigit := len(remaining) != 0 && ('0' <= remaining[0] && remaining[0] <= '9')
|
||||
|
||||
foreground, background := resolveToColourCodes(namedColors)
|
||||
if foreground != "" {
|
||||
if len(foreground) == 1 && background == "" && followedByDigit {
|
||||
out.WriteByte('0')
|
||||
}
|
||||
out.WriteString(foreground)
|
||||
if background != "" {
|
||||
out.WriteByte(',')
|
||||
if len(background) == 1 && followedByDigit {
|
||||
out.WriteByte('0')
|
||||
}
|
||||
out.WriteString(background)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val, exists := escapetoval[rune(char)]
|
||||
if exists {
|
||||
out.WriteString(val)
|
||||
} else {
|
||||
// invalid escape, use the raw char
|
||||
out.WriteByte(char)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out.String()
|
||||
}
|
||||
7
vendor/github.com/ergochat/irc-go/ircmsg/doc.go
generated
vendored
Normal file
7
vendor/github.com/ergochat/irc-go/ircmsg/doc.go
generated
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// written by Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the ISC license
|
||||
|
||||
/*
|
||||
Package ircmsg helps parse and create lines for IRC connections.
|
||||
*/
|
||||
package ircmsg
|
||||
463
vendor/github.com/ergochat/irc-go/ircmsg/message.go
generated
vendored
Normal file
463
vendor/github.com/ergochat/irc-go/ircmsg/message.go
generated
vendored
Normal file
|
|
@ -0,0 +1,463 @@
|
|||
// Copyright (c) 2016-2019 Daniel Oaks <daniel@danieloaks.net>
|
||||
// Copyright (c) 2018-2019 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||
|
||||
// released under the ISC license
|
||||
|
||||
package ircmsg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const (
|
||||
// "The size limit for message tags is 8191 bytes, including the leading
|
||||
// '@' (0x40) and trailing space ' ' (0x20) characters."
|
||||
MaxlenTags = 8191
|
||||
|
||||
// MaxlenTags - ('@' + ' ')
|
||||
MaxlenTagData = MaxlenTags - 2
|
||||
|
||||
// "Clients MUST NOT send messages with tag data exceeding 4094 bytes,
|
||||
// this includes tags with or without the client-only prefix."
|
||||
MaxlenClientTagData = 4094
|
||||
|
||||
// "Servers MUST NOT add tag data exceeding 4094 bytes to messages."
|
||||
MaxlenServerTagData = 4094
|
||||
|
||||
// '@' + MaxlenClientTagData + ' '
|
||||
// this is the analogue of MaxlenTags when the source of the message is a client
|
||||
MaxlenTagsFromClient = MaxlenClientTagData + 2
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrorLineIsEmpty indicates that the given IRC line was empty.
|
||||
ErrorLineIsEmpty = errors.New("Line is empty")
|
||||
|
||||
// ErrorLineContainsBadChar indicates that the line contained invalid characters
|
||||
ErrorLineContainsBadChar = errors.New("Line contains invalid characters")
|
||||
|
||||
// ErrorBodyTooLong indicates that the message body exceeded the specified
|
||||
// length limit (typically 512 bytes). This error is non-fatal; if encountered
|
||||
// when parsing a message, the message is parsed up to the length limit, and
|
||||
// if encountered when serializing a message, the message is truncated to the limit.
|
||||
ErrorBodyTooLong = errors.New("Line body exceeded the specified length limit; outgoing messages will be truncated")
|
||||
|
||||
// ErrorTagsTooLong indicates that the message exceeded the maximum tag length
|
||||
// (the specified response on the server side is 417 ERR_INPUTTOOLONG).
|
||||
ErrorTagsTooLong = errors.New("Line could not be processed because its tag data exceeded the length limit")
|
||||
|
||||
// ErrorInvalidTagContent indicates that a tag name or value was invalid
|
||||
ErrorInvalidTagContent = errors.New("Line could not be processed because it contained an invalid tag name or value")
|
||||
|
||||
// ErrorCommandMissing indicates that an IRC message was invalid because it lacked a command.
|
||||
ErrorCommandMissing = errors.New("IRC messages MUST have a command")
|
||||
|
||||
// ErrorBadParam indicates that an IRC message could not be serialized because
|
||||
// its parameters violated the syntactic constraints on IRC parameters:
|
||||
// non-final parameters cannot be empty, contain a space, or start with `:`.
|
||||
ErrorBadParam = errors.New("Cannot have an empty param, a param with spaces, or a param that starts with ':' before the last parameter")
|
||||
)
|
||||
|
||||
// Message represents an IRC message, as defined by the RFCs and as
|
||||
// extended by the IRCv3 Message Tags specification with the introduction
|
||||
// of message tags.
|
||||
type Message struct {
|
||||
Prefix string
|
||||
Command string
|
||||
Params []string
|
||||
forceTrailing bool
|
||||
tags map[string]string
|
||||
clientOnlyTags map[string]string
|
||||
}
|
||||
|
||||
// ForceTrailing ensures that when the message is serialized, the final parameter
|
||||
// will be encoded as a "trailing parameter" (preceded by a colon). This is
|
||||
// almost never necessary and should not be used except when having to interact
|
||||
// with broken implementations that don't correctly interpret IRC messages.
|
||||
func (msg *Message) ForceTrailing() {
|
||||
msg.forceTrailing = true
|
||||
}
|
||||
|
||||
// GetTag returns whether a tag is present, and if so, what its value is.
|
||||
func (msg *Message) GetTag(tagName string) (present bool, value string) {
|
||||
if len(tagName) == 0 {
|
||||
return
|
||||
} else if tagName[0] == '+' {
|
||||
value, present = msg.clientOnlyTags[tagName]
|
||||
return
|
||||
} else {
|
||||
value, present = msg.tags[tagName]
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// HasTag returns whether a tag is present.
|
||||
func (msg *Message) HasTag(tagName string) (present bool) {
|
||||
present, _ = msg.GetTag(tagName)
|
||||
return
|
||||
}
|
||||
|
||||
// SetTag sets a tag.
|
||||
func (msg *Message) SetTag(tagName, tagValue string) {
|
||||
if len(tagName) == 0 {
|
||||
return
|
||||
} else if tagName[0] == '+' {
|
||||
if msg.clientOnlyTags == nil {
|
||||
msg.clientOnlyTags = make(map[string]string)
|
||||
}
|
||||
msg.clientOnlyTags[tagName] = tagValue
|
||||
} else {
|
||||
if msg.tags == nil {
|
||||
msg.tags = make(map[string]string)
|
||||
}
|
||||
msg.tags[tagName] = tagValue
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteTag deletes a tag.
|
||||
func (msg *Message) DeleteTag(tagName string) {
|
||||
if len(tagName) == 0 {
|
||||
return
|
||||
} else if tagName[0] == '+' {
|
||||
delete(msg.clientOnlyTags, tagName)
|
||||
} else {
|
||||
delete(msg.tags, tagName)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateTags is a convenience to set multiple tags at once.
|
||||
func (msg *Message) UpdateTags(tags map[string]string) {
|
||||
for name, value := range tags {
|
||||
msg.SetTag(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
// AllTags returns all tags as a single map.
|
||||
func (msg *Message) AllTags() (result map[string]string) {
|
||||
result = make(map[string]string, len(msg.tags)+len(msg.clientOnlyTags))
|
||||
for name, value := range msg.tags {
|
||||
result[name] = value
|
||||
}
|
||||
for name, value := range msg.clientOnlyTags {
|
||||
result[name] = value
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ClientOnlyTags returns the client-only tags (the tags with the + prefix).
|
||||
// The returned map may be internal storage of the Message object and
|
||||
// should not be modified.
|
||||
func (msg *Message) ClientOnlyTags() map[string]string {
|
||||
return msg.clientOnlyTags
|
||||
}
|
||||
|
||||
// ParseLine creates and returns a message from the given IRC line.
|
||||
func ParseLine(line string) (ircmsg Message, err error) {
|
||||
return parseLine(line, 0, 0)
|
||||
}
|
||||
|
||||
// ParseLineStrict creates and returns an Message from the given IRC line,
|
||||
// taking the maximum length into account and truncating the message as appropriate.
|
||||
// If fromClient is true, it enforces the client limit on tag data length (4094 bytes),
|
||||
// allowing the server to return ERR_INPUTTOOLONG as appropriate. If truncateLen is
|
||||
// nonzero, it is the length at which the non-tag portion of the message is truncated.
|
||||
func ParseLineStrict(line string, fromClient bool, truncateLen int) (ircmsg Message, err error) {
|
||||
maxTagDataLength := MaxlenTagData
|
||||
if fromClient {
|
||||
maxTagDataLength = MaxlenClientTagData
|
||||
}
|
||||
return parseLine(line, maxTagDataLength, truncateLen)
|
||||
}
|
||||
|
||||
// slice off any amount of ' ' from the front of the string
|
||||
func trimInitialSpaces(str string) string {
|
||||
var i int
|
||||
for i = 0; i < len(str) && str[i] == ' '; i++ {
|
||||
}
|
||||
return str[i:]
|
||||
}
|
||||
|
||||
func parseLine(line string, maxTagDataLength int, truncateLen int) (ircmsg Message, err error) {
|
||||
// remove either \n or \r\n from the end of the line:
|
||||
line = strings.TrimSuffix(line, "\n")
|
||||
line = strings.TrimSuffix(line, "\r")
|
||||
// whether we removed them ourselves, or whether they were removed previously,
|
||||
// they count against the line limit:
|
||||
if truncateLen != 0 {
|
||||
if truncateLen <= 2 {
|
||||
return ircmsg, ErrorLineIsEmpty
|
||||
}
|
||||
truncateLen -= 2
|
||||
}
|
||||
// now validate for the 3 forbidden bytes:
|
||||
if strings.IndexByte(line, '\x00') != -1 || strings.IndexByte(line, '\n') != -1 || strings.IndexByte(line, '\r') != -1 {
|
||||
return ircmsg, ErrorLineContainsBadChar
|
||||
}
|
||||
|
||||
if len(line) < 1 {
|
||||
return ircmsg, ErrorLineIsEmpty
|
||||
}
|
||||
|
||||
// tags
|
||||
if line[0] == '@' {
|
||||
tagEnd := strings.IndexByte(line, ' ')
|
||||
if tagEnd == -1 {
|
||||
return ircmsg, ErrorLineIsEmpty
|
||||
}
|
||||
tags := line[1:tagEnd]
|
||||
if 0 < maxTagDataLength && maxTagDataLength < len(tags) {
|
||||
return ircmsg, ErrorTagsTooLong
|
||||
}
|
||||
err = ircmsg.parseTags(tags)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// skip over the tags and the separating space
|
||||
line = line[tagEnd+1:]
|
||||
}
|
||||
|
||||
// truncate if desired
|
||||
if truncateLen != 0 && truncateLen < len(line) {
|
||||
err = ErrorBodyTooLong
|
||||
line = line[:truncateLen]
|
||||
}
|
||||
|
||||
// modern: "These message parts, and parameters themselves, are separated
|
||||
// by one or more ASCII SPACE characters"
|
||||
line = trimInitialSpaces(line)
|
||||
|
||||
// prefix
|
||||
if 0 < len(line) && line[0] == ':' {
|
||||
prefixEnd := strings.IndexByte(line, ' ')
|
||||
if prefixEnd == -1 {
|
||||
return ircmsg, ErrorLineIsEmpty
|
||||
}
|
||||
ircmsg.Prefix = line[1:prefixEnd]
|
||||
// skip over the prefix and the separating space
|
||||
line = line[prefixEnd+1:]
|
||||
}
|
||||
|
||||
line = trimInitialSpaces(line)
|
||||
|
||||
// command
|
||||
commandEnd := strings.IndexByte(line, ' ')
|
||||
paramStart := commandEnd + 1
|
||||
if commandEnd == -1 {
|
||||
commandEnd = len(line)
|
||||
paramStart = len(line)
|
||||
}
|
||||
// normalize command to uppercase:
|
||||
ircmsg.Command = strings.ToUpper(line[:commandEnd])
|
||||
if len(ircmsg.Command) == 0 {
|
||||
return ircmsg, ErrorLineIsEmpty
|
||||
}
|
||||
line = line[paramStart:]
|
||||
|
||||
for {
|
||||
line = trimInitialSpaces(line)
|
||||
if len(line) == 0 {
|
||||
break
|
||||
}
|
||||
// handle trailing
|
||||
if line[0] == ':' {
|
||||
ircmsg.Params = append(ircmsg.Params, line[1:])
|
||||
break
|
||||
}
|
||||
paramEnd := strings.IndexByte(line, ' ')
|
||||
if paramEnd == -1 {
|
||||
ircmsg.Params = append(ircmsg.Params, line)
|
||||
break
|
||||
}
|
||||
ircmsg.Params = append(ircmsg.Params, line[:paramEnd])
|
||||
line = line[paramEnd+1:]
|
||||
}
|
||||
|
||||
return ircmsg, err
|
||||
}
|
||||
|
||||
// helper to parse tags
|
||||
func (ircmsg *Message) parseTags(tags string) (err error) {
|
||||
for 0 < len(tags) {
|
||||
tagEnd := strings.IndexByte(tags, ';')
|
||||
endPos := tagEnd
|
||||
nextPos := tagEnd + 1
|
||||
if tagEnd == -1 {
|
||||
endPos = len(tags)
|
||||
nextPos = len(tags)
|
||||
}
|
||||
tagPair := tags[:endPos]
|
||||
equalsIndex := strings.IndexByte(tagPair, '=')
|
||||
var tagName, tagValue string
|
||||
if equalsIndex == -1 {
|
||||
// tag with no value
|
||||
tagName = tagPair
|
||||
} else {
|
||||
tagName, tagValue = tagPair[:equalsIndex], tagPair[equalsIndex+1:]
|
||||
}
|
||||
// "Implementations [...] MUST NOT perform any validation that would
|
||||
// reject the message if an invalid tag key name is used."
|
||||
if validateTagName(tagName) {
|
||||
if !validateTagValue(tagValue) {
|
||||
return ErrorInvalidTagContent
|
||||
}
|
||||
ircmsg.SetTag(tagName, UnescapeTagValue(tagValue))
|
||||
}
|
||||
// skip over the tag just processed, plus the delimiting ; if any
|
||||
tags = tags[nextPos:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MakeMessage provides a simple way to create a new Message.
|
||||
func MakeMessage(tags map[string]string, prefix string, command string, params ...string) (ircmsg Message) {
|
||||
ircmsg.Prefix = prefix
|
||||
ircmsg.Command = command
|
||||
ircmsg.Params = params
|
||||
ircmsg.UpdateTags(tags)
|
||||
return ircmsg
|
||||
}
|
||||
|
||||
// Line returns a sendable line created from an Message.
|
||||
func (ircmsg *Message) Line() (result string, err error) {
|
||||
bytes, err := ircmsg.line(0, 0, 0, 0)
|
||||
if err == nil {
|
||||
result = string(bytes)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LineBytes returns a sendable line created from an Message.
|
||||
func (ircmsg *Message) LineBytes() (result []byte, err error) {
|
||||
result, err = ircmsg.line(0, 0, 0, 0)
|
||||
return
|
||||
}
|
||||
|
||||
// LineBytesStrict returns a sendable line, as a []byte, created from an Message.
|
||||
// fromClient controls whether the server-side or client-side tag length limit
|
||||
// is enforced. If truncateLen is nonzero, it is the length at which the
|
||||
// non-tag portion of the message is truncated.
|
||||
func (ircmsg *Message) LineBytesStrict(fromClient bool, truncateLen int) ([]byte, error) {
|
||||
var tagLimit, clientOnlyTagDataLimit, serverAddedTagDataLimit int
|
||||
if fromClient {
|
||||
// enforce client max tags:
|
||||
// <client_max> (4096) :: '@' <tag_data 4094> ' '
|
||||
tagLimit = MaxlenTagsFromClient
|
||||
} else {
|
||||
// on the server side, enforce separate client-only and server-added tag budgets:
|
||||
// "Servers MUST NOT add tag data exceeding 4094 bytes to messages."
|
||||
// <combined_max> (8191) :: '@' <tag_data 4094> ';' <tag_data 4094> ' '
|
||||
clientOnlyTagDataLimit = MaxlenClientTagData
|
||||
serverAddedTagDataLimit = MaxlenServerTagData
|
||||
}
|
||||
return ircmsg.line(tagLimit, clientOnlyTagDataLimit, serverAddedTagDataLimit, truncateLen)
|
||||
}
|
||||
|
||||
func paramRequiresTrailing(param string) bool {
|
||||
return len(param) == 0 || strings.IndexByte(param, ' ') != -1 || param[0] == ':'
|
||||
}
|
||||
|
||||
// line returns a sendable line created from an Message.
|
||||
func (ircmsg *Message) line(tagLimit, clientOnlyTagDataLimit, serverAddedTagDataLimit, truncateLen int) (result []byte, err error) {
|
||||
if len(ircmsg.Command) == 0 {
|
||||
return nil, ErrorCommandMissing
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
// write the tags, computing the budgets for client-only tags and regular tags
|
||||
var lenRegularTags, lenClientOnlyTags, lenTags int
|
||||
if 0 < len(ircmsg.tags) || 0 < len(ircmsg.clientOnlyTags) {
|
||||
var tagError error
|
||||
buf.WriteByte('@')
|
||||
firstTag := true
|
||||
writeTags := func(tags map[string]string) {
|
||||
for tag, val := range tags {
|
||||
if !(validateTagName(tag) && validateTagValue(val)) {
|
||||
tagError = ErrorInvalidTagContent
|
||||
}
|
||||
if !firstTag {
|
||||
buf.WriteByte(';') // delimiter
|
||||
}
|
||||
buf.WriteString(tag)
|
||||
if val != "" {
|
||||
buf.WriteByte('=')
|
||||
buf.WriteString(EscapeTagValue(val))
|
||||
}
|
||||
firstTag = false
|
||||
}
|
||||
}
|
||||
writeTags(ircmsg.tags)
|
||||
lenRegularTags = buf.Len() - 1 // '@' is not counted
|
||||
writeTags(ircmsg.clientOnlyTags)
|
||||
lenClientOnlyTags = (buf.Len() - 1) - lenRegularTags // '@' is not counted
|
||||
if lenRegularTags != 0 {
|
||||
// semicolon between regular and client-only tags is not counted
|
||||
lenClientOnlyTags -= 1
|
||||
}
|
||||
buf.WriteByte(' ')
|
||||
if tagError != nil {
|
||||
return nil, tagError
|
||||
}
|
||||
}
|
||||
lenTags = buf.Len()
|
||||
|
||||
if 0 < tagLimit && tagLimit < buf.Len() {
|
||||
return nil, ErrorTagsTooLong
|
||||
}
|
||||
if (0 < clientOnlyTagDataLimit && clientOnlyTagDataLimit < lenClientOnlyTags) || (0 < serverAddedTagDataLimit && serverAddedTagDataLimit < lenRegularTags) {
|
||||
return nil, ErrorTagsTooLong
|
||||
}
|
||||
|
||||
if len(ircmsg.Prefix) > 0 {
|
||||
buf.WriteByte(':')
|
||||
buf.WriteString(ircmsg.Prefix)
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
|
||||
buf.WriteString(ircmsg.Command)
|
||||
|
||||
for i, param := range ircmsg.Params {
|
||||
buf.WriteByte(' ')
|
||||
requiresTrailing := paramRequiresTrailing(param)
|
||||
lastParam := i == len(ircmsg.Params)-1
|
||||
if (requiresTrailing || ircmsg.forceTrailing) && lastParam {
|
||||
buf.WriteByte(':')
|
||||
} else if requiresTrailing && !lastParam {
|
||||
return nil, ErrorBadParam
|
||||
}
|
||||
buf.WriteString(param)
|
||||
}
|
||||
|
||||
// truncate if desired; leave 2 bytes over for \r\n:
|
||||
if truncateLen != 0 && (truncateLen-2) < (buf.Len()-lenTags) {
|
||||
err = ErrorBodyTooLong
|
||||
newBufLen := lenTags + (truncateLen - 2)
|
||||
buf.Truncate(newBufLen)
|
||||
// XXX: we may have truncated in the middle of a UTF8-encoded codepoint;
|
||||
// if so, remove additional bytes, stopping when the sequence either
|
||||
// ends in a valid codepoint, or we have removed 3 bytes (the maximum
|
||||
// length of the remnant of a once-valid, truncated codepoint; we don't
|
||||
// want to truncate the entire message if it wasn't UTF8 in the first
|
||||
// place).
|
||||
for i := 0; i < (utf8.UTFMax - 1); i++ {
|
||||
r, n := utf8.DecodeLastRune(buf.Bytes())
|
||||
if r == utf8.RuneError && n <= 1 {
|
||||
newBufLen--
|
||||
buf.Truncate(newBufLen)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
buf.WriteString("\r\n")
|
||||
|
||||
result = buf.Bytes()
|
||||
toValidate := result[:len(result)-2]
|
||||
if bytes.IndexByte(toValidate, '\x00') != -1 || bytes.IndexByte(toValidate, '\r') != -1 || bytes.IndexByte(toValidate, '\n') != -1 {
|
||||
return nil, ErrorLineContainsBadChar
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
103
vendor/github.com/ergochat/irc-go/ircmsg/tags.go
generated
vendored
Normal file
103
vendor/github.com/ergochat/irc-go/ircmsg/tags.go
generated
vendored
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
// written by Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the ISC license
|
||||
|
||||
package ircmsg
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var (
|
||||
// valtoescape replaces real characters with message tag escapes.
|
||||
valtoescape = strings.NewReplacer("\\", "\\\\", ";", "\\:", " ", "\\s", "\r", "\\r", "\n", "\\n")
|
||||
|
||||
escapedCharLookupTable [256]byte
|
||||
)
|
||||
|
||||
func init() {
|
||||
// most chars escape to themselves
|
||||
for i := 0; i < 256; i += 1 {
|
||||
escapedCharLookupTable[i] = byte(i)
|
||||
}
|
||||
// these are the exceptions
|
||||
escapedCharLookupTable[':'] = ';'
|
||||
escapedCharLookupTable['s'] = ' '
|
||||
escapedCharLookupTable['r'] = '\r'
|
||||
escapedCharLookupTable['n'] = '\n'
|
||||
}
|
||||
|
||||
// EscapeTagValue takes a value, and returns an escaped message tag value.
|
||||
//
|
||||
// This function is automatically used when lines are created from an
|
||||
// Message, so you don't need to call it yourself before creating a line.
|
||||
func EscapeTagValue(inString string) string {
|
||||
return valtoescape.Replace(inString)
|
||||
}
|
||||
|
||||
// UnescapeTagValue takes an escaped message tag value, and returns the raw value.
|
||||
//
|
||||
// This function is automatically used when lines are interpreted by ParseLine,
|
||||
// so you don't need to call it yourself after parsing a line.
|
||||
func UnescapeTagValue(inString string) string {
|
||||
// buf.Len() == 0 is the fastpath where we have not needed to unescape any chars
|
||||
var buf strings.Builder
|
||||
remainder := inString
|
||||
for {
|
||||
backslashPos := strings.IndexByte(remainder, '\\')
|
||||
|
||||
if backslashPos == -1 {
|
||||
if buf.Len() == 0 {
|
||||
return inString
|
||||
} else {
|
||||
buf.WriteString(remainder)
|
||||
break
|
||||
}
|
||||
} else if backslashPos == len(remainder)-1 {
|
||||
// trailing backslash, which we strip
|
||||
if buf.Len() == 0 {
|
||||
return inString[:len(inString)-1]
|
||||
} else {
|
||||
buf.WriteString(remainder[:len(remainder)-1])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// non-trailing backslash detected; we're now on the slowpath
|
||||
// where we modify the string
|
||||
if buf.Len() == 0 {
|
||||
buf.Grow(len(inString)) // just an optimization
|
||||
}
|
||||
buf.WriteString(remainder[:backslashPos])
|
||||
buf.WriteByte(escapedCharLookupTable[remainder[backslashPos+1]])
|
||||
remainder = remainder[backslashPos+2:]
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// https://ircv3.net/specs/extensions/message-tags.html#rules-for-naming-message-tags
|
||||
func validateTagName(name string) bool {
|
||||
if len(name) == 0 {
|
||||
return false
|
||||
}
|
||||
if name[0] == '+' {
|
||||
name = name[1:]
|
||||
}
|
||||
if len(name) == 0 {
|
||||
return false
|
||||
}
|
||||
// let's err on the side of leniency here; allow -./ (45-47) in any position
|
||||
for i := 0; i < len(name); i++ {
|
||||
c := name[i]
|
||||
if !(('-' <= c && c <= '/') || ('0' <= c && c <= '9') || ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// "Tag values MUST be encoded as UTF8."
|
||||
func validateTagValue(value string) bool {
|
||||
return utf8.ValidString(value)
|
||||
}
|
||||
125
vendor/github.com/ergochat/irc-go/ircreader/ircreader.go
generated
vendored
Normal file
125
vendor/github.com/ergochat/irc-go/ircreader/ircreader.go
generated
vendored
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
// Copyright (c) 2020-2021 Shivaram Lingamneni
|
||||
// released under the MIT license
|
||||
|
||||
package ircreader
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
/*
|
||||
Reader is an optimized line reader for IRC lines containing tags;
|
||||
most IRC lines will not approach the maximum line length (8191 bytes
|
||||
of tag data, plus 512 bytes of message data), so we want a buffered
|
||||
reader that can start with a smaller buffer and expand if necessary,
|
||||
while also maintaining a hard upper limit on the size of the buffer.
|
||||
*/
|
||||
|
||||
var (
|
||||
ErrReadQ = errors.New("readQ exceeded (read too many bytes without terminating newline)")
|
||||
)
|
||||
|
||||
type Reader struct {
|
||||
conn io.Reader
|
||||
|
||||
initialSize int
|
||||
maxSize int
|
||||
|
||||
buf []byte
|
||||
start int // start of valid (i.e., read but not yet consumed) data in the buffer
|
||||
end int // end of valid data in the buffer
|
||||
searchFrom int // start of valid data in the buffer not yet searched for \n
|
||||
eof bool
|
||||
}
|
||||
|
||||
// Returns a new *Reader with sane buffer size limits.
|
||||
func NewIRCReader(conn io.Reader) *Reader {
|
||||
var reader Reader
|
||||
reader.Initialize(conn, 512, 8192+1024)
|
||||
return &reader
|
||||
}
|
||||
|
||||
// "Placement new" for a Reader; initializes it with custom buffer size
|
||||
// limits.
|
||||
func (cc *Reader) Initialize(conn io.Reader, initialSize, maxSize int) {
|
||||
*cc = Reader{}
|
||||
cc.conn = conn
|
||||
cc.initialSize = initialSize
|
||||
cc.maxSize = maxSize
|
||||
}
|
||||
|
||||
// Blocks until a full IRC line is read, then returns it. Accepts either \n
|
||||
// or \r\n as the line terminator (but not \r in isolation). Passes through
|
||||
// errors from the underlying connection. Returns ErrReadQ if the buffer limit
|
||||
// was exceeded without a terminating \n.
|
||||
func (cc *Reader) ReadLine() ([]byte, error) {
|
||||
for {
|
||||
// try to find a terminated line in the buffered data already read
|
||||
nlidx := bytes.IndexByte(cc.buf[cc.searchFrom:cc.end], '\n')
|
||||
if nlidx != -1 {
|
||||
// got a complete line
|
||||
line := cc.buf[cc.start : cc.searchFrom+nlidx]
|
||||
cc.start = cc.searchFrom + nlidx + 1
|
||||
cc.searchFrom = cc.start
|
||||
// treat \r\n as the line terminator if it was present
|
||||
if 0 < len(line) && line[len(line)-1] == '\r' {
|
||||
line = line[:len(line)-1]
|
||||
}
|
||||
return line, nil
|
||||
}
|
||||
|
||||
// are we out of space? we can read more if any of these are true:
|
||||
// 1. cc.start != 0, so we can slide the existing data back
|
||||
// 2. cc.end < len(cc.buf), so we can read data into the end of the buffer
|
||||
// 3. len(cc.buf) < cc.maxSize, so we can grow the buffer
|
||||
if cc.start == 0 && cc.end == len(cc.buf) && len(cc.buf) == cc.maxSize {
|
||||
return nil, ErrReadQ
|
||||
}
|
||||
|
||||
if cc.eof {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
if len(cc.buf) < cc.maxSize && (len(cc.buf)-(cc.end-cc.start) < cc.initialSize/2) {
|
||||
// allocate a new buffer, copy any remaining data
|
||||
newLen := roundUpToPowerOfTwo(len(cc.buf) + 1)
|
||||
if newLen > cc.maxSize {
|
||||
newLen = cc.maxSize
|
||||
} else if newLen < cc.initialSize {
|
||||
newLen = cc.initialSize
|
||||
}
|
||||
newBuf := make([]byte, newLen)
|
||||
copy(newBuf, cc.buf[cc.start:cc.end])
|
||||
cc.buf = newBuf
|
||||
} else if cc.start != 0 {
|
||||
// slide remaining data back to the front of the buffer
|
||||
copy(cc.buf, cc.buf[cc.start:cc.end])
|
||||
}
|
||||
cc.end = cc.end - cc.start
|
||||
cc.start = 0
|
||||
|
||||
cc.searchFrom = cc.end
|
||||
n, err := cc.conn.Read(cc.buf[cc.end:])
|
||||
cc.end += n
|
||||
if n != 0 && err == io.EOF {
|
||||
// we may have received new \n-terminated lines, try to parse them
|
||||
cc.eof = true
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return n such that v <= n and n == 2**i for some i
|
||||
func roundUpToPowerOfTwo(v int) int {
|
||||
// http://graphics.stanford.edu/~seander/bithacks.html
|
||||
v -= 1
|
||||
v |= v >> 1
|
||||
v |= v >> 2
|
||||
v |= v >> 4
|
||||
v |= v >> 8
|
||||
v |= v >> 16
|
||||
return v + 1
|
||||
}
|
||||
9
vendor/github.com/ergochat/irc-go/ircutils/doc.go
generated
vendored
Normal file
9
vendor/github.com/ergochat/irc-go/ircutils/doc.go
generated
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// written by Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the ISC license
|
||||
|
||||
/*
|
||||
Package ircutils provides small, useful utility functions and classes.
|
||||
|
||||
This package is in an alpha stage.
|
||||
*/
|
||||
package ircutils
|
||||
41
vendor/github.com/ergochat/irc-go/ircutils/hostnames.go
generated
vendored
Normal file
41
vendor/github.com/ergochat/irc-go/ircutils/hostnames.go
generated
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// written by Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the ISC license
|
||||
|
||||
package ircutils
|
||||
|
||||
import "strings"
|
||||
|
||||
var allowedHostnameChars = "abcdefghijklmnopqrstuvwxyz1234567890-."
|
||||
|
||||
// HostnameIsValid provides a way for servers to check whether a looked-up client
|
||||
// hostname is valid (see InspIRCd #1033 for why this is required).
|
||||
//
|
||||
// This function shouldn't be called by clients since they don't need to validate
|
||||
// hostnames for IRC use, just by servers that need to confirm hostnames of incoming
|
||||
// clients.
|
||||
//
|
||||
// In addition to this function, servers should impose their own limits on max
|
||||
// hostname length -- this function limits it to 200 but most servers will probably
|
||||
// want to make it smaller than that.
|
||||
func HostnameIsValid(hostname string) bool {
|
||||
// IRC hostnames specifically require a period, rough limit of 200 chars
|
||||
if !strings.Contains(hostname, ".") || len(hostname) < 1 || len(hostname) > 200 {
|
||||
return false
|
||||
}
|
||||
|
||||
// ensure each part of hostname is valid
|
||||
for _, part := range strings.Split(hostname, ".") {
|
||||
if len(part) < 1 || len(part) > 63 || strings.HasPrefix(part, "-") || strings.HasSuffix(part, "-") {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ensure all chars of hostname are valid
|
||||
for _, char := range strings.Split(strings.ToLower(hostname), "") {
|
||||
if !strings.Contains(allowedHostnameChars, char) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
62
vendor/github.com/ergochat/irc-go/ircutils/unicode.go
generated
vendored
Normal file
62
vendor/github.com/ergochat/irc-go/ircutils/unicode.go
generated
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// Copyright (c) 2021 Shivaram Lingamneni
|
||||
// Released under the MIT License
|
||||
|
||||
package ircutils
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// truncate a message, taking care not to make valid UTF8 into invalid UTF8
|
||||
func TruncateUTF8Safe(message string, byteLimit int) (result string) {
|
||||
if len(message) <= byteLimit {
|
||||
return message
|
||||
}
|
||||
message = message[:byteLimit]
|
||||
for i := 0; i < (utf8.UTFMax - 1); i++ {
|
||||
r, n := utf8.DecodeLastRuneInString(message)
|
||||
if r == utf8.RuneError && n <= 1 {
|
||||
message = message[:len(message)-1]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
// Sanitizes human-readable text to make it safe for IRC;
|
||||
// assumes UTF-8 and uses the replacement character where
|
||||
// applicable.
|
||||
func SanitizeText(message string, byteLimit int) (result string) {
|
||||
var buf strings.Builder
|
||||
|
||||
for _, r := range message {
|
||||
if r == '\x00' || r == '\r' {
|
||||
continue
|
||||
} else if r == '\n' {
|
||||
if buf.Len()+2 <= byteLimit {
|
||||
buf.WriteString(" ")
|
||||
continue
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else if unicode.IsSpace(r) {
|
||||
if buf.Len()+1 <= byteLimit {
|
||||
buf.WriteString(" ")
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
rLen := utf8.RuneLen(r)
|
||||
if buf.Len()+rLen <= byteLimit {
|
||||
buf.WriteRune(r)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
56
vendor/github.com/ergochat/irc-go/ircutils/userhost.go
generated
vendored
Normal file
56
vendor/github.com/ergochat/irc-go/ircutils/userhost.go
generated
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// written by Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the ISC license
|
||||
|
||||
package ircutils
|
||||
|
||||
import "strings"
|
||||
|
||||
// UserHost holds a username+host combination
|
||||
type UserHost struct {
|
||||
Nick string
|
||||
User string
|
||||
Host string
|
||||
}
|
||||
|
||||
// ParseUserhost takes a userhost string and returns a UserHost instance.
|
||||
func ParseUserhost(userhost string) UserHost {
|
||||
var uh UserHost
|
||||
|
||||
if len(userhost) == 0 {
|
||||
return uh
|
||||
}
|
||||
|
||||
if strings.Contains(userhost, "!") {
|
||||
usersplit := strings.SplitN(userhost, "!", 2)
|
||||
var rest string
|
||||
if len(usersplit) == 2 {
|
||||
uh.Nick = usersplit[0]
|
||||
rest = usersplit[1]
|
||||
} else {
|
||||
rest = usersplit[0]
|
||||
}
|
||||
|
||||
hostsplit := strings.SplitN(rest, "@", 2)
|
||||
if len(hostsplit) == 2 {
|
||||
uh.User = hostsplit[0]
|
||||
uh.Host = hostsplit[1]
|
||||
} else {
|
||||
uh.User = hostsplit[0]
|
||||
}
|
||||
} else {
|
||||
hostsplit := strings.SplitN(userhost, "@", 2)
|
||||
if len(hostsplit) == 2 {
|
||||
uh.Nick = hostsplit[0]
|
||||
uh.Host = hostsplit[1]
|
||||
} else {
|
||||
uh.User = hostsplit[0]
|
||||
}
|
||||
}
|
||||
|
||||
return uh
|
||||
}
|
||||
|
||||
// // Canonical returns the canonical string representation of the userhost.
|
||||
// func (uh *UserHost) Canonical() string {
|
||||
// return ""
|
||||
// }
|
||||
Loading…
Add table
Add a link
Reference in a new issue