forked from lug/matterbridge
		
	
		
			
				
	
	
		
			353 lines
		
	
	
		
			9.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			353 lines
		
	
	
		
			9.1 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  = '{'
 | |
| 	fmtCloseChar = '}'
 | |
| )
 | |
| 
 | |
| 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] != ',' && (text[i] < 'A' || text[i] > 'Z') && (text[i] < 'a' || text[i] > 'z') {
 | |
| 				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, string(fmtOpenChar)+color+string(fmtCloseChar), "", -1)
 | |
| 	}
 | |
| 	for code := range fmtCodes {
 | |
| 		text = strings.Replace(text, string(fmtOpenChar)+code+string(fmtCloseChar), "", -1)
 | |
| 	}
 | |
| 
 | |
| 	return text
 | |
| }
 | |
| 
 | |
| // This is really the only fastest way of doing this (marginally 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 compliant 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 "*" and "~" in the prefix list, as these are commonly used
 | |
| 	// (e.g. ZNC.)
 | |
| 	if bytes.IndexByte([]byte{'!', '#', '&', '*', '~', '+'}, 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] == '!' {
 | |
| 		if len(channel) < 7 {
 | |
| 			return false
 | |
| 		}
 | |
| 
 | |
| 		// check for valid ID
 | |
| 		for i := 1; i < 6; i++ {
 | |
| 			if (channel[i] < '0' || channel[i] > '9') && (channel[i] < 'A' || channel[i] > 'Z') {
 | |
| 				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 nickname. 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
 | |
| 	}
 | |
| 
 | |
| 	// Check the first index. Some characters aren't allowed for the first
 | |
| 	// index of an IRC nickname.
 | |
| 	if (nick[0] < 'A' || nick[0] > '}') && nick[0] != '?' {
 | |
| 		// a-z, A-Z, '_\[]{}^|', and '?' in the case of znc.
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	for i := 1; i < len(nick); i++ {
 | |
| 		if (nick[i] < 'A' || nick[i] > '}') && (nick[i] < '0' || nick[i] > '9') && nick[i] != '-' {
 | |
| 			// 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
 | |
| 	}
 | |
| 
 | |
| 	// "~" is prepended (commonly) if there was no ident server response.
 | |
| 	if name[0] == '~' {
 | |
| 		// 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] < 'A' || name[0] > 'Z') && (name[0] < 'a' || name[0] > 'z') && (name[0] < '0' || name[0] > '9') {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	for i := 1; i < len(name); i++ {
 | |
| 		if (name[i] < 'A' || name[i] > '}') && (name[i] < '0' || name[i] > '9') && name[i] != '-' && name[i] != '.' {
 | |
| 			// 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. Note that this
 | |
| // should not be used to normalize nicknames or similar, as this may convert
 | |
| // valid input characters to non-rfc-valid characters. As such, it's main use
 | |
| // is for comparing two nicks.
 | |
| func ToRFC1459(input string) string {
 | |
| 	var 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])
 | |
| }
 | 
