351 lines
8.9 KiB
Go
351 lines
8.9 KiB
Go
|
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||
|
// of this source code is governed by the MIT license that can be found in
|
||
|
// the LICENSE file.
|
||
|
|
||
|
package girc
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"regexp"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
fmtOpenChar = 0x7B // {
|
||
|
fmtCloseChar = 0x7D // }
|
||
|
)
|
||
|
|
||
|
var fmtColors = map[string]int{
|
||
|
"white": 0,
|
||
|
"black": 1,
|
||
|
"blue": 2,
|
||
|
"navy": 2,
|
||
|
"green": 3,
|
||
|
"red": 4,
|
||
|
"brown": 5,
|
||
|
"maroon": 5,
|
||
|
"purple": 6,
|
||
|
"gold": 7,
|
||
|
"olive": 7,
|
||
|
"orange": 7,
|
||
|
"yellow": 8,
|
||
|
"lightgreen": 9,
|
||
|
"lime": 9,
|
||
|
"teal": 10,
|
||
|
"cyan": 11,
|
||
|
"lightblue": 12,
|
||
|
"royal": 12,
|
||
|
"fuchsia": 13,
|
||
|
"lightpurple": 13,
|
||
|
"pink": 13,
|
||
|
"gray": 14,
|
||
|
"grey": 14,
|
||
|
"lightgrey": 15,
|
||
|
"silver": 15,
|
||
|
}
|
||
|
|
||
|
var fmtCodes = map[string]string{
|
||
|
"bold": "\x02",
|
||
|
"b": "\x02",
|
||
|
"italic": "\x1d",
|
||
|
"i": "\x1d",
|
||
|
"reset": "\x0f",
|
||
|
"r": "\x0f",
|
||
|
"clear": "\x03",
|
||
|
"c": "\x03", // Clears formatting.
|
||
|
"reverse": "\x16",
|
||
|
"underline": "\x1f",
|
||
|
"ul": "\x1f",
|
||
|
"ctcp": "\x01", // CTCP/ACTION delimiter.
|
||
|
}
|
||
|
|
||
|
// Fmt takes format strings like "{red}" or "{red,blue}" (for background
|
||
|
// colors) and turns them into the resulting ASCII format/color codes for IRC.
|
||
|
// See format.go for the list of supported format codes allowed.
|
||
|
//
|
||
|
// For example:
|
||
|
//
|
||
|
// client.Message("#channel", Fmt("{red}{b}Hello {red,blue}World{c}"))
|
||
|
func Fmt(text string) string {
|
||
|
var last = -1
|
||
|
for i := 0; i < len(text); i++ {
|
||
|
if text[i] == fmtOpenChar {
|
||
|
last = i
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if text[i] == fmtCloseChar && last > -1 {
|
||
|
code := strings.ToLower(text[last+1 : i])
|
||
|
|
||
|
// Check to see if they're passing in a second (background) color
|
||
|
// as {fgcolor,bgcolor}.
|
||
|
var secondary string
|
||
|
if com := strings.Index(code, ","); com > -1 {
|
||
|
secondary = code[com+1:]
|
||
|
code = code[:com]
|
||
|
}
|
||
|
|
||
|
var repl string
|
||
|
|
||
|
if color, ok := fmtColors[code]; ok {
|
||
|
repl = fmt.Sprintf("\x03%02d", color)
|
||
|
}
|
||
|
|
||
|
if repl != "" && secondary != "" {
|
||
|
if color, ok := fmtColors[secondary]; ok {
|
||
|
repl += fmt.Sprintf(",%02d", color)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if repl == "" {
|
||
|
if fmtCode, ok := fmtCodes[code]; ok {
|
||
|
repl = fmtCode
|
||
|
}
|
||
|
}
|
||
|
|
||
|
next := len(text[:last]+repl) - 1
|
||
|
text = text[:last] + repl + text[i+1:]
|
||
|
last = -1
|
||
|
i = next
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if last > -1 {
|
||
|
// A-Z, a-z, and ","
|
||
|
if text[i] != 0x2c && (text[i] <= 0x41 || text[i] >= 0x5a) && (text[i] <= 0x61 || text[i] >= 0x7a) {
|
||
|
last = -1
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return text
|
||
|
}
|
||
|
|
||
|
// TrimFmt strips all "{fmt}" formatting strings from the input text.
|
||
|
// See Fmt() for more information.
|
||
|
func TrimFmt(text string) string {
|
||
|
for color := range fmtColors {
|
||
|
text = strings.Replace(text, "{"+color+"}", "", -1)
|
||
|
}
|
||
|
for code := range fmtCodes {
|
||
|
text = strings.Replace(text, "{"+code+"}", "", -1)
|
||
|
}
|
||
|
|
||
|
return text
|
||
|
}
|
||
|
|
||
|
// This is really the only fastest way of doing this (marginably better than
|
||
|
// actually trying to parse it manually.)
|
||
|
var reStripColor = regexp.MustCompile(`\x03([019]?[0-9](,[019]?[0-9])?)?`)
|
||
|
|
||
|
// StripRaw tries to strip all ASCII format codes that are used for IRC.
|
||
|
// Primarily, foreground/background colors, and other control bytes like
|
||
|
// reset, bold, italic, reverse, etc. This also is done in a specific way
|
||
|
// in order to ensure no truncation of other non-irc formatting.
|
||
|
func StripRaw(text string) string {
|
||
|
text = reStripColor.ReplaceAllString(text, "")
|
||
|
|
||
|
for _, code := range fmtCodes {
|
||
|
text = strings.Replace(text, code, "", -1)
|
||
|
}
|
||
|
|
||
|
return text
|
||
|
}
|
||
|
|
||
|
// IsValidChannel validates if channel is an RFC complaint channel or not.
|
||
|
//
|
||
|
// NOTE: If you are using this to validate a channel that contains a channel
|
||
|
// ID, (!<channelid>NAME), this only supports the standard 5 character length.
|
||
|
//
|
||
|
// NOTE: If you do not need to validate against servers that support unicode,
|
||
|
// you may want to ensure that all channel chars are within the range of
|
||
|
// all ASCII printable chars. This function will NOT do that for
|
||
|
// compatibility reasons.
|
||
|
//
|
||
|
// channel = ( "#" / "+" / ( "!" channelid ) / "&" ) chanstring
|
||
|
// [ ":" chanstring ]
|
||
|
// chanstring = 0x01-0x07 / 0x08-0x09 / 0x0B-0x0C / 0x0E-0x1F / 0x21-0x2B
|
||
|
// chanstring = / 0x2D-0x39 / 0x3B-0xFF
|
||
|
// ; any octet except NUL, BELL, CR, LF, " ", "," and ":"
|
||
|
// channelid = 5( 0x41-0x5A / digit ) ; 5( A-Z / 0-9 )
|
||
|
func IsValidChannel(channel string) bool {
|
||
|
if len(channel) <= 1 || len(channel) > 50 {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// #, +, !<channelid>, or &
|
||
|
// Including "*" in the prefix list, as this is commonly used (e.g. ZNC)
|
||
|
if bytes.IndexByte([]byte{0x21, 0x23, 0x26, 0x2A, 0x2B}, channel[0]) == -1 {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// !<channelid> -- not very commonly supported, but we'll check it anyway.
|
||
|
// The ID must be 5 chars. This means min-channel size should be:
|
||
|
// 1 (prefix) + 5 (id) + 1 (+, channel name)
|
||
|
// On some networks, this may be extended with ISUPPORT capabilities,
|
||
|
// however this is extremely uncommon.
|
||
|
if channel[0] == 0x21 {
|
||
|
if len(channel) < 7 {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// check for valid ID
|
||
|
for i := 1; i < 6; i++ {
|
||
|
if (channel[i] < 0x30 || channel[i] > 0x39) && (channel[i] < 0x41 || channel[i] > 0x5A) {
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Check for invalid octets here.
|
||
|
bad := []byte{0x00, 0x07, 0x0D, 0x0A, 0x20, 0x2C, 0x3A}
|
||
|
for i := 1; i < len(channel); i++ {
|
||
|
if bytes.IndexByte(bad, channel[i]) != -1 {
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// IsValidNick validates an IRC nickame. Note that this does not validate
|
||
|
// IRC nickname length.
|
||
|
//
|
||
|
// nickname = ( letter / special ) *8( letter / digit / special / "-" )
|
||
|
// letter = 0x41-0x5A / 0x61-0x7A
|
||
|
// digit = 0x30-0x39
|
||
|
// special = 0x5B-0x60 / 0x7B-0x7D
|
||
|
func IsValidNick(nick string) bool {
|
||
|
if len(nick) <= 0 {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
nick = ToRFC1459(nick)
|
||
|
|
||
|
// Check the first index. Some characters aren't allowed for the first
|
||
|
// index of an IRC nickname.
|
||
|
if nick[0] < 0x41 || nick[0] > 0x7D {
|
||
|
// a-z, A-Z, and _\[]{}^|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
for i := 1; i < len(nick); i++ {
|
||
|
if (nick[i] < 0x41 || nick[i] > 0x7D) && (nick[i] < 0x30 || nick[i] > 0x39) && nick[i] != 0x2D {
|
||
|
// a-z, A-Z, 0-9, -, and _\[]{}^|
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// IsValidUser validates an IRC ident/username. Note that this does not
|
||
|
// validate IRC ident length.
|
||
|
//
|
||
|
// The validation checks are much like what characters are allowed with an
|
||
|
// IRC nickname (see IsValidNick()), however an ident/username can:
|
||
|
//
|
||
|
// 1. Must either start with alphanumberic char, or "~" then alphanumberic
|
||
|
// char.
|
||
|
//
|
||
|
// 2. Contain a "." (period), for use with "first.last". Though, this may
|
||
|
// not be supported on all networks. Some limit this to only a single period.
|
||
|
//
|
||
|
// Per RFC:
|
||
|
// user = 1*( %x01-09 / %x0B-0C / %x0E-1F / %x21-3F / %x41-FF )
|
||
|
// ; any octet except NUL, CR, LF, " " and "@"
|
||
|
func IsValidUser(name string) bool {
|
||
|
if len(name) <= 0 {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
name = ToRFC1459(name)
|
||
|
|
||
|
// "~" is prepended (commonly) if there was no ident server response.
|
||
|
if name[0] == 0x7E {
|
||
|
// Means name only contained "~".
|
||
|
if len(name) < 2 {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
name = name[1:]
|
||
|
}
|
||
|
|
||
|
// Check to see if the first index is alphanumeric.
|
||
|
if (name[0] < 0x41 || name[0] > 0x4A) && (name[0] < 0x61 || name[0] > 0x7A) && (name[0] < 0x30 || name[0] > 0x39) {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
for i := 1; i < len(name); i++ {
|
||
|
if (name[i] < 0x41 || name[i] > 0x7D) && (name[i] < 0x30 || name[i] > 0x39) && name[i] != 0x2D && name[i] != 0x2E {
|
||
|
// a-z, A-Z, 0-9, -, and _\[]{}^|
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// ToRFC1459 converts a string to the stripped down conversion within RFC
|
||
|
// 1459. This will do things like replace an "A" with an "a", "[]" with "{}",
|
||
|
// and so forth. Useful to compare two nicknames or channels.
|
||
|
func ToRFC1459(input string) (out string) {
|
||
|
for i := 0; i < len(input); i++ {
|
||
|
if input[i] >= 65 && input[i] <= 94 {
|
||
|
out += string(rune(input[i]) + 32)
|
||
|
} else {
|
||
|
out += string(input[i])
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return out
|
||
|
}
|
||
|
|
||
|
const globChar = "*"
|
||
|
|
||
|
// Glob will test a string pattern, potentially containing globs, against a
|
||
|
// string. The glob character is *.
|
||
|
func Glob(input, match string) bool {
|
||
|
// Empty pattern.
|
||
|
if match == "" {
|
||
|
return input == match
|
||
|
}
|
||
|
|
||
|
// If a glob, match all.
|
||
|
if match == globChar {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
parts := strings.Split(match, globChar)
|
||
|
|
||
|
if len(parts) == 1 {
|
||
|
// No globs, test for equality.
|
||
|
return input == match
|
||
|
}
|
||
|
|
||
|
leadingGlob, trailingGlob := strings.HasPrefix(match, globChar), strings.HasSuffix(match, globChar)
|
||
|
last := len(parts) - 1
|
||
|
|
||
|
// Check prefix first.
|
||
|
if !leadingGlob && !strings.HasPrefix(input, parts[0]) {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// Check middle section.
|
||
|
for i := 1; i < last; i++ {
|
||
|
if !strings.Contains(input, parts[i]) {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// Trim already-evaluated text from input during loop over match
|
||
|
// text.
|
||
|
idx := strings.Index(input, parts[i]) + len(parts[i])
|
||
|
input = input[idx:]
|
||
|
}
|
||
|
|
||
|
// Check suffix last.
|
||
|
return trailingGlob || strings.HasSuffix(input, parts[last])
|
||
|
}
|