remove ircmap and gircclient, rename to ergochat/irc-go
This commit is contained in:
parent
f5e0f875f7
commit
7b3bb1d1ea
1
Makefile
1
Makefile
|
@ -2,7 +2,6 @@
|
|||
|
||||
test:
|
||||
cd ircfmt && go test . && go vet .
|
||||
cd ircmap && go test . && go vet .
|
||||
cd ircmsg && go test . && go vet .
|
||||
cd ircreader && go test . && go vet .
|
||||
cd ircutils && go test . && go vet .
|
||||
|
|
|
@ -18,7 +18,5 @@ Packages:
|
|||
* [**ircevent**](https://godoc.org/github.com/goshuirc/irc-go/ircevent): IRC client library (fork of [thoj/go-ircevent](https://github.com/thoj/go-ircevent)).
|
||||
* [**ircfmt**](https://godoc.org/github.com/goshuirc/irc-go/ircfmt): IRC format codes handling, escaping and unescaping.
|
||||
* [**ircutils**](https://godoc.org/github.com/goshuirc/irc-go/ircutils): Useful utility functions and classes that don't fit into their own packages.
|
||||
* [**ircmap**](https://godoc.org/github.com/goshuirc/irc-go/ircmap): IRC string casefolding.
|
||||
* [**gircclient**](https://godoc.org/github.com/goshuirc/irc-go/client): Alternative, work-in-progress client library.
|
||||
|
||||
For a relatively complete example of the library's use, see [slingamn/titlebot](https://github.com/slingamn/titlebot).
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
// written by Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the ISC license
|
||||
|
||||
package gircclient
|
||||
|
||||
import (
|
||||
"github.com/goshuirc/irc-go/ircfmt"
|
||||
)
|
||||
|
||||
// Msg sends a message to the given target.
|
||||
func (sc *ServerConnection) Msg(tags map[string]string, target string, message string, escaped bool) {
|
||||
if escaped {
|
||||
message = ircfmt.Unescape(message)
|
||||
}
|
||||
sc.Send(tags, "", "PRIVMSG", target, message)
|
||||
}
|
||||
|
||||
// Notice sends a notice to the given target.
|
||||
func (sc *ServerConnection) Notice(tags map[string]string, target string, message string, escaped bool) {
|
||||
if escaped {
|
||||
message = ircfmt.Unescape(message)
|
||||
}
|
||||
sc.Send(tags, "", "NOTICE", target, message)
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
// written by Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the ISC license
|
||||
|
||||
package gircclient
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ClientCapabilities holds the capabilities that can and have been enabled on
|
||||
// a ServerConnection.
|
||||
type ClientCapabilities struct {
|
||||
Available map[string]*string
|
||||
Enabled map[string]bool
|
||||
Wanted []string
|
||||
}
|
||||
|
||||
// NewClientCapabilities returns a newly-initialised ClientCapabilities.
|
||||
func NewClientCapabilities() ClientCapabilities {
|
||||
var cc ClientCapabilities
|
||||
|
||||
cc.Available = make(map[string]*string, 0)
|
||||
cc.Enabled = make(map[string]bool, 0)
|
||||
cc.Wanted = make([]string, 0)
|
||||
|
||||
return cc
|
||||
}
|
||||
|
||||
// AddWantedCaps adds the given capabilities to our list of capabilities that
|
||||
// we want from the server.
|
||||
func (cc *ClientCapabilities) AddWantedCaps(caps ...string) {
|
||||
for _, name := range caps {
|
||||
// I'm not sure how fast this is, but speed isn't too much of a concern
|
||||
// here. Adding 'wanted capabilities' is something that generally only
|
||||
// happens at startup anyway.
|
||||
i := sort.Search(len(cc.Wanted), func(i int) bool { return cc.Wanted[i] >= name })
|
||||
|
||||
if i >= len(cc.Wanted) || cc.Wanted[i] != name {
|
||||
cc.Wanted = append(cc.Wanted, name)
|
||||
sort.Strings(cc.Wanted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddCaps adds capabilities from LS lists to our Available map.
|
||||
func (cc *ClientCapabilities) AddCaps(tags ...string) {
|
||||
var name string
|
||||
var value *string
|
||||
|
||||
for _, tag := range tags {
|
||||
if len(tag) == 0 {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(tag, "=") {
|
||||
vals := strings.SplitN(tag, "=", 2)
|
||||
name = vals[0]
|
||||
value = &vals[1]
|
||||
} else {
|
||||
name = tag
|
||||
value = nil
|
||||
}
|
||||
|
||||
cc.Available[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
// EnableCaps enables the given capabilities.
|
||||
func (cc *ClientCapabilities) EnableCaps(caps ...string) {
|
||||
for _, name := range caps {
|
||||
if strings.HasPrefix(name, "-") {
|
||||
name = strings.TrimPrefix(name, "-")
|
||||
delete(cc.Enabled, name)
|
||||
} else {
|
||||
cc.Enabled[name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DelCaps removes the given capabilities.
|
||||
func (cc *ClientCapabilities) DelCaps(caps ...string) {
|
||||
for _, name := range caps {
|
||||
delete(cc.Available, name)
|
||||
delete(cc.Enabled, name)
|
||||
}
|
||||
}
|
||||
|
||||
// ToRequestLine returns a line of capabilities to request, to be used in a
|
||||
// CAP REQ line.
|
||||
func (cc *ClientCapabilities) ToRequestLine() string {
|
||||
var caps []string
|
||||
caps = make([]string, 0)
|
||||
|
||||
for _, name := range cc.Wanted {
|
||||
_, capIsAvailable := cc.Available[name]
|
||||
_, capIsEnabled := cc.Enabled[name]
|
||||
|
||||
if capIsAvailable && !capIsEnabled {
|
||||
caps = append(caps, name)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(caps, " ")
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
// written by Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the ISC license
|
||||
|
||||
package gircclient
|
||||
|
||||
type channel struct {
|
||||
Name string
|
||||
Key string
|
||||
UseKey bool
|
||||
}
|
338
client/client.go
338
client/client.go
|
@ -1,338 +0,0 @@
|
|||
// written by Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the ISC license
|
||||
|
||||
package gircclient
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goshuirc/eventmgr"
|
||||
"github.com/goshuirc/irc-go/ircmap"
|
||||
"github.com/goshuirc/irc-go/ircmsg"
|
||||
)
|
||||
|
||||
// ServerConnection is a connection to a single server.
|
||||
type ServerConnection struct {
|
||||
Name string
|
||||
Connected bool
|
||||
Registered bool
|
||||
Casemapping ircmap.MappingType
|
||||
CommandPrefixes []string
|
||||
|
||||
// internal stuff
|
||||
RawConnection net.Conn
|
||||
eventsIn eventmgr.EventManager
|
||||
eventsOut eventmgr.EventManager
|
||||
channelsToJoin []channel
|
||||
|
||||
// data we keep track of
|
||||
Features ServerFeatures
|
||||
Caps ClientCapabilities
|
||||
|
||||
// details users must supply before connection
|
||||
Nick string
|
||||
InitialNick string
|
||||
FallbackNicks []string
|
||||
fallbackNickIndex int
|
||||
InitialUser string
|
||||
InitialRealName string
|
||||
ConnectionPass string
|
||||
|
||||
// options
|
||||
SimplifyEvents bool
|
||||
}
|
||||
|
||||
// newServerConnection returns an initialised ServerConnection, for internal
|
||||
// use.
|
||||
func newServerConnection(name string) *ServerConnection {
|
||||
var sc ServerConnection
|
||||
|
||||
sc.Name = name
|
||||
sc.Caps = NewClientCapabilities()
|
||||
sc.Features = make(ServerFeatures)
|
||||
|
||||
sc.Caps.AddWantedCaps("account-notify", "away-notify", "extended-join", "multi-prefix", "sasl")
|
||||
sc.Caps.AddWantedCaps("account-tag", "cap-notify", "chghost", "invite-notify", "server-time", "userhost-in-names")
|
||||
|
||||
sc.Features.Parse("CHANTYPES=#", "LINELEN=512", "PREFIX=(ov)@+")
|
||||
|
||||
sc.SimplifyEvents = true
|
||||
|
||||
return &sc
|
||||
}
|
||||
|
||||
// Connect connects to the given address.
|
||||
func (sc *ServerConnection) Connect(address string, ssl bool, tlsconfig *tls.Config) error {
|
||||
// check the required attributes
|
||||
if sc.InitialNick == "" || sc.InitialUser == "" {
|
||||
return errors.New("InitialNick and InitialUser must be set before connecting")
|
||||
}
|
||||
|
||||
// connect
|
||||
var conn net.Conn
|
||||
var err error
|
||||
|
||||
if ssl {
|
||||
conn, err = tls.Dial("tcp", address, tlsconfig)
|
||||
} else {
|
||||
conn, err = net.Dial("tcp", address)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sc.RawConnection = conn
|
||||
sc.Connected = true
|
||||
|
||||
sc.Send(nil, "", "CAP", "LS", "302")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// JoinChannel joins a channel, or marks the channel as to be joined after registration.
|
||||
func (sc *ServerConnection) JoinChannel(name string, key string, useKey bool) {
|
||||
if sc.Registered {
|
||||
params := []string{name}
|
||||
if useKey {
|
||||
params = []string{name, key}
|
||||
}
|
||||
sc.Send(nil, "", "JOIN", params...)
|
||||
} else {
|
||||
sc.channelsToJoin = append(sc.channelsToJoin, channel{
|
||||
Name: name,
|
||||
Key: key,
|
||||
UseKey: useKey,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// WaitForConnection waits for the serverConnection to become available.
|
||||
// This is used when writing a custom event loop.
|
||||
func (sc *ServerConnection) WaitForConnection() {
|
||||
waitTime := 10 * time.Millisecond
|
||||
for sc.RawConnection == nil {
|
||||
time.Sleep(waitTime)
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessIncomingLine processes the incoming IRC line.
|
||||
// This is used when writing a custom event loop.
|
||||
func (sc *ServerConnection) ProcessIncomingLine(line string) {
|
||||
line = strings.Trim(line, "\r\n")
|
||||
|
||||
// ignore empty lines
|
||||
if len(line) < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
// dispatch raw
|
||||
rawInfo := eventmgr.NewInfoMap()
|
||||
rawInfo["server"] = sc
|
||||
rawInfo["direction"] = "in"
|
||||
rawInfo["data"] = line
|
||||
|
||||
sc.dispatchRawIn(rawInfo)
|
||||
|
||||
// dispatch events
|
||||
message, err := ircmsg.ParseLine(line)
|
||||
|
||||
// convert numerics to names
|
||||
cmd := message.Command
|
||||
num, err := strconv.Atoi(cmd)
|
||||
if err == nil {
|
||||
name, exists := Numerics[num]
|
||||
if exists {
|
||||
cmd = name
|
||||
}
|
||||
}
|
||||
|
||||
info := eventmgr.NewInfoMap()
|
||||
info["server"] = sc
|
||||
info["direction"] = "in"
|
||||
info["tags"] = message.AllTags()
|
||||
info["prefix"] = message.Prefix
|
||||
info["command"] = cmd
|
||||
info["params"] = message.Params
|
||||
|
||||
// simplify event
|
||||
if sc.SimplifyEvents {
|
||||
err = SimplifyEvent(info)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Could not simplify incoming IRC message, skipping line.")
|
||||
fmt.Println("line:", line)
|
||||
fmt.Println("error:", err)
|
||||
fmt.Println("info:", info)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// IRC commands are case-insensitive
|
||||
sc.dispatchIn(strings.ToUpper(cmd), info)
|
||||
if strings.ToUpper(cmd) == "PRIVMSG" {
|
||||
sc.dispatchCommand(info)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Disconnect closes the IRC socket.
|
||||
// It is used when writing your own event loop.
|
||||
func (sc *ServerConnection) Disconnect() {
|
||||
sc.Connected = false
|
||||
sc.RawConnection.Close()
|
||||
info := eventmgr.NewInfoMap()
|
||||
info["server"] = sc
|
||||
sc.dispatchOut("server disconnected", info)
|
||||
}
|
||||
|
||||
// ReceiveLoop runs a loop of receiving and dispatching new messages.
|
||||
func (sc *ServerConnection) ReceiveLoop() {
|
||||
// wait for the connection to become available
|
||||
sc.WaitForConnection()
|
||||
|
||||
reader := bufio.NewReader(sc.RawConnection)
|
||||
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
sc.ProcessIncomingLine(line)
|
||||
}
|
||||
|
||||
sc.Disconnect()
|
||||
}
|
||||
|
||||
// RegisterEvent registers a new handler for the given event.
|
||||
//
|
||||
// The standard directions are "in" and "out".
|
||||
//
|
||||
// 'name' can either be the name of an event, "all", or "raw". Note that "all"
|
||||
// will not catch "raw" events, but will catch all others.
|
||||
func (sc *ServerConnection) RegisterEvent(direction string, name string, handler eventmgr.HandlerFn, priority int) {
|
||||
if direction == "in" || direction == "both" {
|
||||
sc.eventsIn.Attach(name, handler, priority)
|
||||
}
|
||||
if direction == "out" || direction == "both" {
|
||||
sc.eventsOut.Attach(name, handler, priority)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterCommand registers a command to be called via the configured prefix or the client's nickname (e.g !help, "GoshuBot: help")
|
||||
func (sc *ServerConnection) RegisterCommand(name string, handler eventmgr.HandlerFn, priority int) {
|
||||
sc.eventsIn.Attach("cmd_"+name, handler, priority)
|
||||
}
|
||||
|
||||
// Shutdown closes the connection to the server.
|
||||
func (sc *ServerConnection) Shutdown(message string) {
|
||||
sc.Send(nil, "", "QUIT", message)
|
||||
sc.Connected = false
|
||||
sc.RawConnection.Close()
|
||||
}
|
||||
|
||||
// Casefold folds the given string using the server's casemapping.
|
||||
func (sc *ServerConnection) Casefold(message string) (string, error) {
|
||||
return ircmap.Casefold(sc.Casemapping, message)
|
||||
}
|
||||
|
||||
// Send sends an IRC message to the server. If the message cannot be converted
|
||||
// to a raw IRC line, an error is returned.
|
||||
func (sc *ServerConnection) Send(tags map[string]string, prefix string, command string, params ...string) error {
|
||||
msg := ircmsg.MakeMessage(tags, prefix, command, params...)
|
||||
line, err := msg.Line()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprint(sc.RawConnection, line)
|
||||
|
||||
// dispatch raw event
|
||||
info := eventmgr.NewInfoMap()
|
||||
info["server"] = sc
|
||||
info["direction"] = "out"
|
||||
info["data"] = line
|
||||
sc.dispatchRawOut(info)
|
||||
|
||||
var outTags map[string]string
|
||||
if tags == nil {
|
||||
outTags = map[string]string{}
|
||||
} else {
|
||||
outTags = tags
|
||||
}
|
||||
|
||||
// dispatch real event
|
||||
info = eventmgr.NewInfoMap()
|
||||
info["server"] = sc
|
||||
info["direction"] = "out"
|
||||
info["tags"] = outTags
|
||||
info["prefix"] = prefix
|
||||
info["command"] = command
|
||||
info["params"] = params
|
||||
|
||||
// IRC commands are case-insensitive
|
||||
sc.dispatchOut(strings.ToUpper(command), info)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// dispatchCommand dispatches an event based on simple commands (e.g !help)
|
||||
func (sc *ServerConnection) dispatchCommand(info eventmgr.InfoMap) {
|
||||
params := strings.Fields(info["params"].([]string)[1])
|
||||
|
||||
for _, p := range sc.CommandPrefixes {
|
||||
if strings.HasPrefix(params[0], p) {
|
||||
if len(params) > 1 {
|
||||
info["cmdparams"] = params[1:]
|
||||
} else {
|
||||
info["cmdparams"] = []string{}
|
||||
}
|
||||
sc.eventsIn.Dispatch("cmd_"+params[0][1:], info)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (params[0] == sc.Nick || params[0] == sc.Nick+":") && len(params) > 1 {
|
||||
if len(params) > 2 {
|
||||
info["cmdparams"] = params[2:]
|
||||
} else {
|
||||
info["cmdparams"] = []string{}
|
||||
}
|
||||
sc.eventsIn.Dispatch("cmd_"+params[1], info)
|
||||
}
|
||||
}
|
||||
|
||||
// dispatchRawIn dispatches raw inbound messages.
|
||||
func (sc *ServerConnection) dispatchRawIn(info eventmgr.InfoMap) {
|
||||
sc.eventsIn.Dispatch("raw", info)
|
||||
}
|
||||
|
||||
// dispatchIn dispatches inbound messages.
|
||||
func (sc *ServerConnection) dispatchIn(name string, info eventmgr.InfoMap) {
|
||||
sc.eventsIn.Dispatch(name, info)
|
||||
sc.eventsIn.Dispatch("all", info)
|
||||
}
|
||||
|
||||
// dispatchRawOut dispatches raw outbound messages.
|
||||
func (sc *ServerConnection) dispatchRawOut(info eventmgr.InfoMap) {
|
||||
sc.eventsOut.Dispatch("raw", info)
|
||||
}
|
||||
|
||||
// dispatchOut dispatches outbound messages.
|
||||
func (sc *ServerConnection) dispatchOut(name string, info eventmgr.InfoMap) {
|
||||
sc.eventsOut.Dispatch(name, info)
|
||||
sc.eventsOut.Dispatch("all", info)
|
||||
}
|
||||
|
||||
// IsChannel returns true if the given target is a channel.
|
||||
func (sc *ServerConnection) IsChannel(target string) bool {
|
||||
channelChars := sc.Features["CHANTYPES"].(string)
|
||||
return strings.ContainsAny(string(target[0]), channelChars)
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
// written by Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the ISC license
|
||||
|
||||
/*
|
||||
Package gircclient is an IRC client library.
|
||||
|
||||
It uses the other various gIRC-Go libraries to provide a clean, consistent
|
||||
interface for connecting to and interacting with IRC servers.
|
||||
|
||||
The Reactor is the primary handler of all new clients.
|
||||
|
||||
This package is in planning/pre-alpha and the API will change substantially.
|
||||
*/
|
||||
package gircclient
|
|
@ -1,70 +0,0 @@
|
|||
// written by Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the ISC license
|
||||
|
||||
package gircclient
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/goshuirc/eventmgr"
|
||||
)
|
||||
|
||||
// EventTransforms holds the set of event transformations we apply when
|
||||
// simplifying given events.
|
||||
var EventTransforms = map[string]EventTransform{
|
||||
"RPL_WELCOME": {
|
||||
StringParams: map[int]string{
|
||||
1: "message",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// EventTransform holds a set of event transformations that should take place
|
||||
// when simplifying the given event.
|
||||
type EventTransform struct {
|
||||
// StringParams maps the given parameter (int) to the given key in the
|
||||
// InfoMap as a string.
|
||||
StringParams map[int]string
|
||||
// IntParams maps the given parameter (int) to the given key in the InfoMap
|
||||
// as an integer.
|
||||
IntParams map[int]string
|
||||
}
|
||||
|
||||
// SimplifyEvent simplifies the given event in-place. This includes better
|
||||
// argument names, convenience attributes, and native objects instead of
|
||||
// strings where appropriate.
|
||||
func SimplifyEvent(e eventmgr.InfoMap) error {
|
||||
transforms, exists := EventTransforms[e["command"].(string)]
|
||||
|
||||
// no transforms found
|
||||
if exists == false {
|
||||
return nil
|
||||
}
|
||||
|
||||
// apply transformations
|
||||
if len(transforms.StringParams) > 0 {
|
||||
for i, param := range e["params"].([]string) {
|
||||
name, exists := transforms.StringParams[i]
|
||||
if exists {
|
||||
e[name] = param
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(transforms.IntParams) > 0 {
|
||||
for i, param := range e["params"].([]string) {
|
||||
name, exists := transforms.IntParams[i]
|
||||
if exists {
|
||||
num, err := strconv.Atoi(param)
|
||||
if err == nil {
|
||||
e[name] = num
|
||||
} else {
|
||||
return errors.New("Param " + param + " was not an integer in " + e["command"].(string) + " event")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we were successful!
|
||||
return nil
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
// written by Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the ISC license
|
||||
|
||||
package gircclient
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ServerFeatures holds a map of server features (RPL_ISUPPORT).
|
||||
type ServerFeatures map[string]interface{}
|
||||
|
||||
// parseFeatureValue changes a raw RPL_ISUPPORT value into a better one.
|
||||
func parseFeatureValue(name string, value string) interface{} {
|
||||
var val interface{}
|
||||
|
||||
if name == "LINELEN" {
|
||||
num, err := strconv.Atoi(value)
|
||||
|
||||
if err != nil || num < 0 {
|
||||
val = 512
|
||||
} else {
|
||||
val = num
|
||||
}
|
||||
} else if name == "NICKLEN" || name == "CHANNELLEN" || name == "TOPICLEN" || name == "USERLEN" {
|
||||
num, err := strconv.Atoi(value)
|
||||
|
||||
if err != nil || num < 0 {
|
||||
val = nil
|
||||
} else {
|
||||
val = num
|
||||
}
|
||||
} else {
|
||||
val = value
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
// Parse the given RPL_ISUPPORT-type tokens and add them to our support list.
|
||||
func (sf *ServerFeatures) Parse(tokens ...string) {
|
||||
for _, token := range tokens {
|
||||
if strings.Contains(token, "=") {
|
||||
vals := strings.SplitN(token, "=", 2)
|
||||
name := strings.ToUpper(vals[0])
|
||||
value := vals[1]
|
||||
|
||||
(*sf)[name] = parseFeatureValue(name, value)
|
||||
} else {
|
||||
(*sf)[strings.ToUpper(token)] = true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
// written by Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the ISC license
|
||||
|
||||
package gircclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/goshuirc/eventmgr"
|
||||
"github.com/goshuirc/irc-go/ircmap"
|
||||
)
|
||||
|
||||
// welcomeHandler sets the nick to the first parameter of the 001 message.
|
||||
// This ensures that when we connect to IRCds that silently truncate the
|
||||
// nickname, we keep the correct one.
|
||||
func welcomeHandler(event string, info eventmgr.InfoMap) {
|
||||
sc := info["server"].(*ServerConnection)
|
||||
sc.Nick = info["params"].([]string)[0]
|
||||
|
||||
sc.Registered = true
|
||||
|
||||
// join channels if we have any to join
|
||||
for _, channel := range sc.channelsToJoin {
|
||||
params := []string{channel.Name}
|
||||
if channel.UseKey {
|
||||
params = []string{channel.Name, channel.Key}
|
||||
}
|
||||
sc.Send(nil, "", "JOIN", params...)
|
||||
}
|
||||
sc.channelsToJoin = []channel{} // empty array
|
||||
}
|
||||
|
||||
func featuresHandler(event string, info eventmgr.InfoMap) {
|
||||
sc := info["server"].(*ServerConnection)
|
||||
|
||||
// parse features into our internal list
|
||||
tags := info["params"].([]string)
|
||||
tags = tags[1 : len(tags)-1] // remove first and last params
|
||||
sc.Features.Parse(tags...)
|
||||
|
||||
if sc.Casemapping == ircmap.NONE {
|
||||
name, exists := sc.Features["CASEMAPPING"]
|
||||
if exists {
|
||||
sc.Casemapping = ircmap.Mappings[name.(string)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func capHandler(event string, info eventmgr.InfoMap) {
|
||||
sc := info["server"].(*ServerConnection)
|
||||
params := info["params"].([]string)
|
||||
subcommand := strings.ToUpper(params[1])
|
||||
|
||||
if subcommand == "ACK" {
|
||||
sc.Caps.EnableCaps(strings.Split(params[2], " ")...)
|
||||
} else if subcommand == "LS" {
|
||||
if len(params) > 3 {
|
||||
sc.Caps.AddCaps(strings.Split(params[3], " ")...)
|
||||
} else {
|
||||
sc.Caps.AddCaps(strings.Split(params[2], " ")...)
|
||||
capsToRequest := sc.Caps.ToRequestLine()
|
||||
|
||||
if len(capsToRequest) > 0 {
|
||||
sc.Send(nil, "", "CAP", "REQ", capsToRequest)
|
||||
}
|
||||
|
||||
if !sc.Registered {
|
||||
sc.Send(nil, "", "CAP", "END")
|
||||
}
|
||||
}
|
||||
} else if subcommand == "NEW" {
|
||||
sc.Caps.AddCaps(strings.Split(params[2], " ")...)
|
||||
capsToRequest := sc.Caps.ToRequestLine()
|
||||
|
||||
if len(capsToRequest) > 0 {
|
||||
sc.Send(nil, "", "CAP", "REQ", capsToRequest)
|
||||
}
|
||||
} else if subcommand == "DEL" {
|
||||
sc.Caps.DelCaps(strings.Split(params[2], " ")...)
|
||||
}
|
||||
|
||||
if !sc.Registered && (subcommand == "ACK" || subcommand == "NAK") {
|
||||
sendRegistration(sc)
|
||||
}
|
||||
}
|
||||
|
||||
func pingHandler(event string, info eventmgr.InfoMap) {
|
||||
sc := info["server"].(*ServerConnection)
|
||||
sc.Send(nil, "", "PONG", info["params"].([]string)...)
|
||||
}
|
||||
|
||||
func nicknameInUseHandler(event string, info eventmgr.InfoMap) {
|
||||
sc := info["server"].(*ServerConnection)
|
||||
if sc.Registered {
|
||||
return
|
||||
}
|
||||
|
||||
// set new nickname
|
||||
if len(sc.FallbackNicks) <= sc.fallbackNickIndex {
|
||||
sc.Nick = fmt.Sprintf("%s_", sc.Nick)
|
||||
} else {
|
||||
sc.Nick = sc.FallbackNicks[sc.fallbackNickIndex]
|
||||
sc.fallbackNickIndex++
|
||||
}
|
||||
|
||||
sc.Send(nil, "", "NICK", sc.Nick)
|
||||
}
|
||||
|
||||
func sendRegistration(sc *ServerConnection) {
|
||||
sc.Nick = sc.InitialNick
|
||||
if sc.ConnectionPass != "" {
|
||||
sc.Send(nil, "", "PASS", sc.ConnectionPass)
|
||||
}
|
||||
sc.Send(nil, "", "NICK", sc.InitialNick)
|
||||
sc.Send(nil, "", "USER", sc.InitialUser, "0", "*", sc.InitialRealName)
|
||||
}
|
|
@ -1,297 +0,0 @@
|
|||
// written by Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the ISC license
|
||||
|
||||
package gircclient
|
||||
|
||||
// Numerics is a map of IRC numerics to names.
|
||||
// Taken from http://defs.ircdocs.horse/defs/ircnumerics.html
|
||||
var Numerics = map[int]string{
|
||||
1: "RPL_WELCOME",
|
||||
2: "RPL_YOURHOST",
|
||||
3: "RPL_CREATED",
|
||||
4: "RPL_MYINFO",
|
||||
5: "RPL_ISUPPORT",
|
||||
8: "RPL_SNOMASK",
|
||||
9: "RPL_STATMEMTOT",
|
||||
10: "RPL_BOUNCE",
|
||||
14: "RPL_YOURCOOKIE",
|
||||
42: "RPL_YOURID",
|
||||
43: "RPL_SAVENICK",
|
||||
50: "RPL_ATTEMPTINGJUNC",
|
||||
51: "RPL_ATTEMPTINGREROUTE",
|
||||
105: "RPL_REMOTEISUPPORT",
|
||||
200: "RPL_TRACELINK",
|
||||
201: "RPL_TRACECONNECTING",
|
||||
202: "RPL_TRACEHANDSHAKE",
|
||||
203: "RPL_TRACEUNKNOWN",
|
||||
204: "RPL_TRACEOPERATOR",
|
||||
205: "RPL_TRACEUSER",
|
||||
206: "RPL_TRACESERVER",
|
||||
207: "RPL_TRACESERVICE",
|
||||
208: "RPL_TRACENEWTYPE",
|
||||
209: "RPL_TRACECLASS",
|
||||
210: "RPL_STATS",
|
||||
211: "RPL_STATSLINKINFO",
|
||||
212: "RPL_STATSCOMMANDS",
|
||||
213: "RPL_STATSCLINE",
|
||||
215: "RPL_STATSILINE",
|
||||
216: "RPL_STATSKLINE",
|
||||
218: "RPL_STATSYLINE",
|
||||
219: "RPL_ENDOFSTATS",
|
||||
221: "RPL_UMODEIS",
|
||||
234: "RPL_SERVLIST",
|
||||
235: "RPL_SERVLISTEND",
|
||||
236: "RPL_STATSVERBOSE",
|
||||
237: "RPL_STATSENGINE",
|
||||
239: "RPL_STATSIAUTH",
|
||||
241: "RPL_STATSLLINE",
|
||||
242: "RPL_STATSUPTIME",
|
||||
243: "RPL_STATSOLINE",
|
||||
244: "RPL_STATSHLINE",
|
||||
245: "RPL_STATSSLINE",
|
||||
250: "RPL_STATSCONN",
|
||||
251: "RPL_LUSERCLIENT",
|
||||
252: "RPL_LUSEROP",
|
||||
253: "RPL_LUSERUNKNOWN",
|
||||
254: "RPL_LUSERCHANNELS",
|
||||
255: "RPL_LUSERME",
|
||||
256: "RPL_ADMINME",
|
||||
257: "RPL_ADMINLOC1",
|
||||
258: "RPL_ADMINLOC2",
|
||||
259: "RPL_ADMINEMAIL",
|
||||
261: "RPL_TRACELOG",
|
||||
263: "RPL_TRYAGAIN",
|
||||
265: "RPL_LOCALUSERS",
|
||||
266: "RPL_GLOBALUSERS",
|
||||
267: "RPL_START_NETSTAT",
|
||||
268: "RPL_NETSTAT",
|
||||
269: "RPL_END_NETSTAT",
|
||||
271: "RPL_SILELIST",
|
||||
272: "RPL_ENDOFSILELIST",
|
||||
273: "RPL_NOTIFY",
|
||||
276: "RPL_VCHANEXIST",
|
||||
277: "RPL_VCHANLIST",
|
||||
278: "RPL_VCHANHELP",
|
||||
280: "RPL_GLIST",
|
||||
296: "RPL_CHANINFO_KICKS",
|
||||
299: "RPL_END_CHANINFO",
|
||||
300: "RPL_NONE",
|
||||
301: "RPL_AWAY",
|
||||
302: "RPL_USERHOST",
|
||||
303: "RPL_ISON",
|
||||
305: "RPL_UNAWAY",
|
||||
306: "RPL_NOWAWAY",
|
||||
311: "RPL_WHOISUSER",
|
||||
312: "RPL_WHOISSERVER",
|
||||
313: "RPL_WHOISOPERATOR",
|
||||
314: "RPL_WHOWASUSER",
|
||||
315: "RPL_ENDOFWHO",
|
||||
317: "RPL_WHOISIDLE",
|
||||
318: "RPL_ENDOFWHOIS",
|
||||
319: "RPL_WHOISCHANNELS",
|
||||
322: "RPL_LIST",
|
||||
323: "RPL_LISTEND",
|
||||
324: "RPL_CHANNELMODEIS",
|
||||
326: "RPL_NOCHANPASS",
|
||||
327: "RPL_CHPASSUNKNOWN",
|
||||
328: "RPL_CHANNEL_URL",
|
||||
329: "RPL_CREATIONTIME",
|
||||
331: "RPL_NOTOPIC",
|
||||
332: "RPL_TOPIC",
|
||||
333: "RPL_TOPICWHOTIME",
|
||||
336: "RPL_INVITELIST",
|
||||
337: "RPL_ENDOFINVITELIST",
|
||||
339: "RPL_BADCHANPASS",
|
||||
340: "RPL_USERIP",
|
||||
341: "RPL_INVITING",
|
||||
345: "RPL_INVITED",
|
||||
346: "RPL_INVITELIST",
|
||||
347: "RPL_ENDOFINVITELIST",
|
||||
348: "RPL_EXCEPTLIST",
|
||||
349: "RPL_ENDOFEXCEPTLIST",
|
||||
351: "RPL_VERSION",
|
||||
352: "RPL_WHOREPLY",
|
||||
353: "RPL_NAMREPLY",
|
||||
354: "RPL_WHOSPCRPL",
|
||||
355: "RPL_NAMREPLY_",
|
||||
364: "RPL_LINKS",
|
||||
365: "RPL_ENDOFLINKS",
|
||||
366: "RPL_ENDOFNAMES",
|
||||
367: "RPL_BANLIST",
|
||||
368: "RPL_ENDOFBANLIST",
|
||||
369: "RPL_ENDOFWHOWAS",
|
||||
371: "RPL_INFO",
|
||||
372: "RPL_MOTD",
|
||||
374: "RPL_ENDOFINFO",
|
||||
375: "RPL_MOTDSTART",
|
||||
376: "RPL_ENDOFMOTD",
|
||||
381: "RPL_YOUREOPER",
|
||||
382: "RPL_REHASHING",
|
||||
383: "RPL_YOURESERVICE",
|
||||
385: "RPL_NOTOPERANYMORE",
|
||||
388: "RPL_ALIST",
|
||||
389: "RPL_ENDOFALIST",
|
||||
391: "RPL_TIME",
|
||||
392: "RPL_USERSSTART",
|
||||
393: "RPL_USERS",
|
||||
394: "RPL_ENDOFUSERS",
|
||||
395: "RPL_NOUSERS",
|
||||
400: "ERR_UNKNOWNERROR",
|
||||
401: "ERR_NOSUCHNICK",
|
||||
402: "ERR_NOSUCHSERVER",
|
||||
403: "ERR_NOSUCHCHANNEL",
|
||||
404: "ERR_CANNOTSENDTOCHAN",
|
||||
405: "ERR_TOOMANYCHANNELS",
|
||||
406: "ERR_WASNOSUCHNICK",
|
||||
407: "ERR_TOOMANYTARGETS",
|
||||
408: "ERR_NOSUCHSERVICE",
|
||||
409: "ERR_NOORIGIN",
|
||||
410: "ERR_INVALIDCAPCMD",
|
||||
411: "ERR_NORECIPIENT",
|
||||
412: "ERR_NOTEXTTOSEND",
|
||||
413: "ERR_NOTOPLEVEL",
|
||||
414: "ERR_WILDTOPLEVEL",
|
||||
415: "ERR_BADMASK",
|
||||
416: "ERR_TOOMANYMATCHES",
|
||||
419: "ERR_LENGTHTRUNCATED",
|
||||
421: "ERR_UNKNOWNCOMMAND",
|
||||
422: "ERR_NOMOTD",
|
||||
423: "ERR_NOADMININFO",
|
||||
424: "ERR_FILEERROR",
|
||||
425: "ERR_NOOPERMOTD",
|
||||
429: "ERR_TOOMANYAWAY",
|
||||
430: "ERR_EVENTNICKCHANGE",
|
||||
431: "ERR_NONICKNAMEGIVEN",
|
||||
432: "ERR_ERRONEUSNICKNAME",
|
||||
433: "ERR_NICKNAMEINUSE",
|
||||
436: "ERR_NICKCOLLISION",
|
||||
439: "ERR_TARGETTOOFAST",
|
||||
440: "ERR_SERVICESDOWN",
|
||||
441: "ERR_USERNOTINCHANNEL",
|
||||
442: "ERR_NOTONCHANNEL",
|
||||
443: "ERR_USERONCHANNEL",
|
||||
444: "ERR_NOLOGIN",
|
||||
445: "ERR_SUMMONDISABLED",
|
||||
446: "ERR_USERSDISABLED",
|
||||
447: "ERR_NONICKCHANGE",
|
||||
449: "ERR_NOTIMPLEMENTED",
|
||||
451: "ERR_NOTREGISTERED",
|
||||
452: "ERR_IDCOLLISION",
|
||||
453: "ERR_NICKLOST",
|
||||
455: "ERR_HOSTILENAME",
|
||||
456: "ERR_ACCEPTFULL",
|
||||
457: "ERR_ACCEPTEXIST",
|
||||
458: "ERR_ACCEPTNOT",
|
||||
459: "ERR_NOHIDING",
|
||||
460: "ERR_NOTFORHALFOPS",
|
||||
461: "ERR_NEEDMOREPARAMS",
|
||||
462: "ERR_ALREADYREGISTERED",
|
||||
463: "ERR_NOPERMFORHOST",
|
||||
464: "ERR_PASSWDMISMATCH",
|
||||
465: "ERR_YOUREBANNEDCREEP",
|
||||
467: "ERR_KEYSET",
|
||||
469: "ERR_LINKSET",
|
||||
471: "ERR_CHANNELISFULL",
|
||||
472: "ERR_UNKNOWNMODE",
|
||||
473: "ERR_INVITEONLYCHAN",
|
||||
474: "ERR_BANNEDFROMCHAN",
|
||||
475: "ERR_BADCHANNELKEY",
|
||||
476: "ERR_BADCHANMASK",
|
||||
478: "ERR_BANLISTFULL",
|
||||
481: "ERR_NOPRIVILEGES",
|
||||
482: "ERR_CHANOPRIVSNEEDED",
|
||||
483: "ERR_CANTKILLSERVER",
|
||||
485: "ERR_UNIQOPRIVSNEEDED",
|
||||
491: "ERR_NOOPERHOST",
|
||||
492: "ERR_NOCTCP",
|
||||
493: "ERR_NOFEATURE",
|
||||
494: "ERR_BADFEATURE",
|
||||
496: "ERR_BADLOGSYS",
|
||||
497: "ERR_BADLOGVALUE",
|
||||
498: "ERR_ISOPERLCHAN",
|
||||
499: "ERR_CHANOWNPRIVNEEDED",
|
||||
500: "ERR_TOOMANYJOINS",
|
||||
501: "ERR_UMODEUNKNOWNFLAG",
|
||||
502: "ERR_USERSDONTMATCH",
|
||||
504: "ERR_USERNOTONSERV",
|
||||
511: "ERR_SILELISTFULL",
|
||||
512: "ERR_TOOMANYWATCH",
|
||||
513: "ERR_BADPING",
|
||||
515: "ERR_BADEXPIRE",
|
||||
516: "ERR_DONTCHEAT",
|
||||
517: "ERR_DISABLED",
|
||||
522: "ERR_WHOSYNTAX",
|
||||
523: "ERR_WHOLIMEXCEED",
|
||||
525: "ERR_REMOTEPFX",
|
||||
526: "ERR_PFXUNROUTABLE",
|
||||
531: "ERR_CANTSENDTOUSER",
|
||||
550: "ERR_BADHOSTMASK",
|
||||
551: "ERR_HOSTUNAVAIL",
|
||||
552: "ERR_USINGSLINE",
|
||||
600: "RPL_LOGON",
|
||||
601: "RPL_LOGOFF",
|
||||
602: "RPL_WATCHOFF",
|
||||
603: "RPL_WATCHSTAT",
|
||||
604: "RPL_NOWON",
|
||||
605: "RPL_NOWOFF",
|
||||
606: "RPL_WATCHLIST",
|
||||
607: "RPL_ENDOFWATCHLIST",
|
||||
608: "RPL_WATCHCLEAR",
|
||||
611: "RPL_ISLOCOP",
|
||||
612: "RPL_ISNOTOPER",
|
||||
613: "RPL_ENDOFISOPER",
|
||||
618: "RPL_DCCLIST",
|
||||
624: "RPL_OMOTDSTART",
|
||||
625: "RPL_OMOTD",
|
||||
626: "RPL_ENDOFO",
|
||||
630: "RPL_SETTINGS",
|
||||
631: "RPL_ENDOFSETTINGS",
|
||||
672: "RPL_UNKNOWNMODES",
|
||||
673: "RPL_CANNOTSETMODES",
|
||||
704: "RPL_HELPSTART",
|
||||
705: "RPL_HELPTXT",
|
||||
706: "RPL_ENDOFHELP",
|
||||
708: "RPL_ETRACEFULL",
|
||||
709: "RPL_ETRACE",
|
||||
710: "RPL_KNOCK",
|
||||
711: "RPL_KNOCKDLVR",
|
||||
712: "ERR_TOOMANYKNOCK",
|
||||
713: "ERR_CHANOPEN",
|
||||
714: "ERR_KNOCKONCHAN",
|
||||
716: "RPL_TARGUMODEG",
|
||||
717: "RPL_TARGNOTIFY",
|
||||
718: "RPL_UMODEGMSG",
|
||||
720: "RPL_OMOTDSTART",
|
||||
721: "RPL_OMOTD",
|
||||
722: "RPL_ENDOFOMOTD",
|
||||
723: "ERR_NOPRIVS",
|
||||
724: "RPL_TESTMARK",
|
||||
725: "RPL_TESTLINE",
|
||||
726: "RPL_NOTESTLINE",
|
||||
730: "RPL_MONONLINE",
|
||||
731: "RPL_MONOFFLINE",
|
||||
732: "RPL_MONLIST",
|
||||
733: "RPL_ENDOFMONLIST",
|
||||
734: "ERR_MONLISTFULL",
|
||||
760: "RPL_WHOISKEYVALUE",
|
||||
761: "RPL_KEYVALUE",
|
||||
762: "RPL_METADATAEND",
|
||||
764: "ERR_METADATALIMIT",
|
||||
765: "ERR_TARGETINVALID",
|
||||
766: "ERR_NOMATCHINGKEY",
|
||||
767: "ERR_KEYINVALID",
|
||||
768: "ERR_KEYNOTSET",
|
||||
769: "ERR_KEYNOPERMISSION",
|
||||
771: "RPL_XINFO",
|
||||
773: "RPL_XINFOSTART",
|
||||
774: "RPL_XINFOEND",
|
||||
900: "RPL_LOGGEDIN",
|
||||
901: "RPL_LOGGEDOUT",
|
||||
902: "ERR_NICKLOCKED",
|
||||
903: "RPL_SASLSUCCESS",
|
||||
904: "ERR_SASLFAIL",
|
||||
905: "ERR_SASLTOOLONG",
|
||||
906: "ERR_SASLABORTED",
|
||||
908: "RPL_SASLMECHS",
|
||||
999: "ERR_NUMERIC_ERR",
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
// written by Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the ISC license
|
||||
|
||||
package gircclient
|
||||
|
||||
import "github.com/goshuirc/eventmgr"
|
||||
|
||||
// eventRegistration holds events that have not yet been registered.
|
||||
type eventRegistration struct {
|
||||
Direction string
|
||||
Name string
|
||||
Handler eventmgr.HandlerFn
|
||||
Priority int
|
||||
}
|
||||
|
||||
// Reactor is the start-point for gircclient. It creates and manages
|
||||
// ServerConnections.
|
||||
type Reactor struct {
|
||||
ServerConnections map[string]*ServerConnection
|
||||
eventsToRegister []eventRegistration
|
||||
}
|
||||
|
||||
// NewReactor returns a new, empty Reactor.
|
||||
func NewReactor() Reactor {
|
||||
var newReactor Reactor
|
||||
|
||||
newReactor.ServerConnections = make(map[string]*ServerConnection, 0)
|
||||
newReactor.eventsToRegister = make([]eventRegistration, 0)
|
||||
|
||||
// add the default handlers
|
||||
newReactor.RegisterEvent("in", "CAP", capHandler, -10)
|
||||
newReactor.RegisterEvent("in", "RPL_WELCOME", welcomeHandler, -10)
|
||||
newReactor.RegisterEvent("in", "RPL_ISUPPORT", featuresHandler, -10)
|
||||
newReactor.RegisterEvent("in", "PING", pingHandler, -10)
|
||||
newReactor.RegisterEvent("in", "ERR_NICKNAMEINUSE", nicknameInUseHandler, -10)
|
||||
|
||||
return newReactor
|
||||
}
|
||||
|
||||
// CreateServer creates a ServerConnection and returns it.
|
||||
func (r *Reactor) CreateServer(name string) *ServerConnection {
|
||||
sc := newServerConnection(name)
|
||||
|
||||
r.ServerConnections[name] = sc
|
||||
|
||||
for _, e := range r.eventsToRegister {
|
||||
sc.RegisterEvent(e.Direction, e.Name, e.Handler, e.Priority)
|
||||
}
|
||||
|
||||
return sc
|
||||
}
|
||||
|
||||
// Shutdown shuts down all ServerConnections.
|
||||
func (r *Reactor) Shutdown(message string) {
|
||||
for _, sc := range r.ServerConnections {
|
||||
sc.Shutdown(message)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterEvent registers an event with all current and new ServerConnections.
|
||||
func (r *Reactor) RegisterEvent(direction string, name string, handler eventmgr.HandlerFn, priority int) {
|
||||
for _, sc := range r.ServerConnections {
|
||||
sc.RegisterEvent(direction, name, handler, priority)
|
||||
}
|
||||
|
||||
// for future servers
|
||||
var newEvent eventRegistration
|
||||
newEvent.Direction = direction
|
||||
newEvent.Name = name
|
||||
newEvent.Handler = handler
|
||||
newEvent.Priority = priority
|
||||
r.eventsToRegister = append(r.eventsToRegister, newEvent)
|
||||
}
|
|
@ -1,400 +0,0 @@
|
|||
package gircclient
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircmap"
|
||||
"github.com/goshuirc/irc-go/ircmsg"
|
||||
)
|
||||
|
||||
func TestPlainConnection(t *testing.T) {
|
||||
reactor := NewReactor()
|
||||
client := reactor.CreateServer("local")
|
||||
|
||||
initialiseServerConnection(client)
|
||||
|
||||
// we mock up a server connection to test the client
|
||||
listener, _ := net.Listen("tcp", ":0")
|
||||
|
||||
client.Connect(listener.Addr().String(), false, nil)
|
||||
go client.ReceiveLoop()
|
||||
|
||||
testServerConnection(t, reactor, client, listener)
|
||||
}
|
||||
|
||||
func TestFailingConnection(t *testing.T) {
|
||||
reactor := NewReactor()
|
||||
client := reactor.CreateServer("local")
|
||||
|
||||
// we mock up a server connection to test the client
|
||||
listener, _ := net.Listen("tcp", ":0")
|
||||
|
||||
// Try to connect before setting InitialNick and InitialUser
|
||||
err := client.Connect(listener.Addr().String(), false, nil)
|
||||
|
||||
if err == nil {
|
||||
t.Error(
|
||||
"ServerConnection allowed connection before InitialNick and InitialUser were set",
|
||||
)
|
||||
}
|
||||
|
||||
// Actually set attributes and fail properly this time
|
||||
client.InitialNick = "test"
|
||||
client.InitialUser = "t"
|
||||
client.Connect("here is a malformed address:6667", false, nil)
|
||||
|
||||
if err == nil {
|
||||
t.Error(
|
||||
"ServerConnection allowed connection with a blatently malformed address",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSConnection(t *testing.T) {
|
||||
reactor := NewReactor()
|
||||
client := reactor.CreateServer("local")
|
||||
|
||||
initialiseServerConnection(client)
|
||||
|
||||
// generate a test certificate to use
|
||||
priv, _ := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
|
||||
|
||||
notBefore := time.Now().Add(-1 * time.Hour * 30) // valid 30 hours ago
|
||||
notAfter := notBefore.Add(time.Hour * 90) // for 90 hours
|
||||
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, _ := rand.Int(rand.Reader, serialNumberLimit)
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"gIRC-Go Co"},
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
|
||||
template.IPAddresses = append(template.IPAddresses, net.ParseIP("127.0.0.1"))
|
||||
template.IPAddresses = append(template.IPAddresses, net.ParseIP("::"))
|
||||
template.DNSNames = append(template.DNSNames, "localhost")
|
||||
|
||||
derBytes, _ := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||
|
||||
c := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||
b, _ := x509.MarshalECPrivateKey(priv)
|
||||
k := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b})
|
||||
|
||||
// we mock up a server connection to test the client
|
||||
listenerKeyPair, _ := tls.X509KeyPair(c, k)
|
||||
|
||||
var listenerTLSConfig tls.Config
|
||||
listenerTLSConfig.Certificates = make([]tls.Certificate, 0)
|
||||
listenerTLSConfig.Certificates = append(listenerTLSConfig.Certificates, listenerKeyPair)
|
||||
listener, _ := tls.Listen("tcp", ":0", &listenerTLSConfig)
|
||||
|
||||
// mock up the client side too
|
||||
clientTLSCertPool := x509.NewCertPool()
|
||||
clientTLSCertPool.AppendCertsFromPEM(c)
|
||||
|
||||
var clientTLSConfig tls.Config
|
||||
clientTLSConfig.RootCAs = clientTLSCertPool
|
||||
clientTLSConfig.ServerName = "localhost"
|
||||
go client.Connect(listener.Addr().String(), true, &clientTLSConfig)
|
||||
go client.ReceiveLoop()
|
||||
|
||||
testServerConnection(t, reactor, client, listener)
|
||||
}
|
||||
|
||||
func sendMessage(conn net.Conn, tags map[string]string, prefix string, command string, params ...string) {
|
||||
ircmsg := ircmsg.MakeMessage(tags, prefix, command, params...)
|
||||
line, err := ircmsg.Line()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(conn, line)
|
||||
|
||||
// need to wait for a quick moment here for TLS to process any changes this
|
||||
// message has caused
|
||||
runtime.Gosched()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
func initialiseServerConnection(client *ServerConnection) {
|
||||
client.InitialNick = "coolguy"
|
||||
client.InitialUser = "c"
|
||||
client.InitialRealName = "girc-go Test Client "
|
||||
}
|
||||
|
||||
func testServerConnection(t *testing.T, reactor Reactor, client *ServerConnection, listener net.Listener) {
|
||||
// start our reader
|
||||
conn, _ := listener.Accept()
|
||||
reader := bufio.NewReader(conn)
|
||||
|
||||
var message string
|
||||
|
||||
// CAP
|
||||
message, _ = reader.ReadString('\n')
|
||||
if message != "CAP LS 302\r\n" {
|
||||
t.Error(
|
||||
"Did not receive CAP LS message, received: [",
|
||||
message,
|
||||
"]",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
sendMessage(conn, nil, "example.com", "CAP", "*", "LS", "*", "multi-prefix userhost-in-names")
|
||||
sendMessage(conn, nil, "example.com", "CAP", "*", "LS", "chghost")
|
||||
|
||||
message, _ = reader.ReadString('\n')
|
||||
if message != "CAP REQ :chghost multi-prefix userhost-in-names\r\n" {
|
||||
t.Error(
|
||||
"Did not receive CAP REQ message, received: [",
|
||||
message,
|
||||
"]",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// these should be silently ignored
|
||||
fmt.Fprintf(conn, "\r\n\r\n\r\n")
|
||||
|
||||
sendMessage(conn, nil, "example.com", "CAP", "*", "ACK", "chghost multi-prefix userhost-in-names")
|
||||
|
||||
message, _ = reader.ReadString('\n')
|
||||
if message != "CAP END\r\n" {
|
||||
t.Error(
|
||||
"Did not receive CAP END message, received: [",
|
||||
message,
|
||||
"]",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// NICK/USER
|
||||
message, _ = reader.ReadString('\n')
|
||||
if message != "NICK coolguy\r\n" {
|
||||
t.Error(
|
||||
"Did not receive NICK message, received: [",
|
||||
message,
|
||||
"]",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
message, _ = reader.ReadString('\n')
|
||||
if message != "USER c 0 * :girc-go Test Client \r\n" {
|
||||
t.Error(
|
||||
"Did not receive USER message, received: [",
|
||||
message,
|
||||
"]",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// make sure nick changes properly
|
||||
sendMessage(conn, nil, "example.com", "001", "dan", "Welcome to the gIRC-Go Test Network!")
|
||||
|
||||
if client.Nick != "dan" {
|
||||
t.Error(
|
||||
"Nick was not set with 001, expected",
|
||||
"dan",
|
||||
"got",
|
||||
client.Nick,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// send 002/003/004
|
||||
sendMessage(conn, nil, "example.com", "002", "dan", "Your host is example.com, running version latest")
|
||||
sendMessage(conn, nil, "example.com", "003", "dan", "This server was created almost no time ago!")
|
||||
sendMessage(conn, nil, "example.com", "004", "dan", "example.com", "latest", "r", "b", "b")
|
||||
|
||||
// make sure LINELEN gets set correctly
|
||||
sendMessage(conn, nil, "example.com", "005", "dan", "LINELEN=", "are available on this server")
|
||||
|
||||
if client.Features["LINELEN"].(int) != 512 {
|
||||
t.Error(
|
||||
"LINELEN default was not set with 005, expected",
|
||||
512,
|
||||
"got",
|
||||
client.Features["LINELEN"],
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// make sure casemapping and other ISUPPORT values are set properly
|
||||
sendMessage(conn, nil, "example.com", "005", "dan", "CASEMAPPING=rfc3454", "NICKLEN=27", "USERLEN=", "SAFELIST", "are available on this server")
|
||||
|
||||
if client.Casemapping != ircmap.RFC3454 {
|
||||
t.Error(
|
||||
"Casemapping was not set with 005, expected",
|
||||
ircmap.RFC3454,
|
||||
"got",
|
||||
client.Casemapping,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if client.Features["NICKLEN"].(int) != 27 {
|
||||
t.Error(
|
||||
"NICKLEN was not set with 005, expected",
|
||||
27,
|
||||
"got",
|
||||
client.Features["NICKLEN"],
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if client.Features["USERLEN"] != nil {
|
||||
t.Error(
|
||||
"USERLEN was not set with 005, expected",
|
||||
nil,
|
||||
"got",
|
||||
client.Features["USERLEN"],
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if client.Features["SAFELIST"].(bool) != true {
|
||||
t.Error(
|
||||
"SAFELIST was not set with 005, expected",
|
||||
true,
|
||||
"got",
|
||||
client.Features["SAFELIST"],
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// test PING
|
||||
sendMessage(conn, nil, "example.com", "PING", "3847362")
|
||||
|
||||
message, _ = reader.ReadString('\n')
|
||||
if message != "PONG 3847362\r\n" {
|
||||
t.Error(
|
||||
"Did not receive PONG message, received: [",
|
||||
message,
|
||||
"]",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// test CAP NEW
|
||||
sendMessage(conn, nil, "example.com", "CAP", client.Nick, "NEW", "sasl=plain")
|
||||
|
||||
message, _ = reader.ReadString('\n')
|
||||
if message != "CAP REQ sasl\r\n" {
|
||||
t.Error(
|
||||
"Did not receive CAP REQ sasl message, received: [",
|
||||
message,
|
||||
"]",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
sendMessage(conn, nil, "example.com", "CAP", client.Nick, "ACK", "sasl")
|
||||
|
||||
sendMessage(conn, nil, "example.com", "CAP", client.Nick, "DEL", "sasl")
|
||||
|
||||
_, exists := client.Caps.Available["sasl"]
|
||||
if exists {
|
||||
t.Error(
|
||||
"SASL cap is still available on client after CAP DEL sasl",
|
||||
)
|
||||
}
|
||||
|
||||
_, exists = client.Caps.Enabled["sasl"]
|
||||
if exists {
|
||||
t.Error(
|
||||
"SASL cap still enabled on client after CAP DEL sasl",
|
||||
)
|
||||
}
|
||||
|
||||
sendMessage(conn, nil, "example.com", "CAP", client.Nick, "ACK", "-chghost")
|
||||
|
||||
_, exists = client.Caps.Enabled["chghost"]
|
||||
if exists {
|
||||
t.Error(
|
||||
"chghost cap still enabled on client after ACK -chghost",
|
||||
)
|
||||
}
|
||||
|
||||
// test actions
|
||||
client.Msg(nil, "coalguys", "Isn't this such an $bamazing$r day?!", true)
|
||||
|
||||
message, _ = reader.ReadString('\n')
|
||||
if message != "PRIVMSG coalguys :Isn't this such an \x02amazing\x0f day?!\r\n" {
|
||||
t.Error(
|
||||
"Did not receive PRIVMSG message, received: [",
|
||||
message,
|
||||
"]",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
client.Notice(nil, "coalguys", "Isn't this such a $c[red]great$c day?", true)
|
||||
|
||||
message, _ = reader.ReadString('\n')
|
||||
if message != "NOTICE coalguys :Isn't this such a \x034great\x03 day?\r\n" {
|
||||
t.Error(
|
||||
"Did not receive NOTICE message, received: [",
|
||||
message,
|
||||
"]",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// test casefolding
|
||||
target, _ := client.Casefold("#beßtchannEL")
|
||||
if target != "#besstchannel" {
|
||||
t.Error(
|
||||
"Channel name was not casefolded correctly, expected",
|
||||
"#besstchannel",
|
||||
"got",
|
||||
target,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// shutdown client
|
||||
reactor.Shutdown(" Get mad! ")
|
||||
|
||||
message, _ = reader.ReadString('\n')
|
||||
if message != "QUIT : Get mad! \r\n" {
|
||||
t.Error(
|
||||
"Did not receive QUIT message, received: [",
|
||||
message,
|
||||
"]",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// test malformed Send
|
||||
err := client.Send(nil, "", "PRIVMSG", "MyFriend", "", "param with spaces", "Hey man!")
|
||||
if err == nil {
|
||||
t.Error(
|
||||
"ServerConnection allowed a Send with empty and params with spaces before the last param",
|
||||
)
|
||||
}
|
||||
|
||||
// close connection and listener
|
||||
conn.Close()
|
||||
listener.Close()
|
||||
}
|
7
doc.go
7
doc.go
|
@ -2,15 +2,12 @@
|
|||
// released under the ISC license
|
||||
|
||||
/*
|
||||
Package goshuirc has useful, self-contained packages that help with IRC
|
||||
ergochat/irc-go has useful, self-contained packages that help with IRC
|
||||
development. These packages are split up so you can easily choose which ones to
|
||||
use while ignoring the others, handling things like simplifying formatting
|
||||
codes, parsing and creating raw IRC lines, and event management.
|
||||
|
||||
An example bot that uses these packages can be found here:
|
||||
https://gist.github.com/DanielOaks/cbbc957e8dba39f59d9e
|
||||
|
||||
These packages are in their early stages. For the status of each package, see
|
||||
the documentation for that package.
|
||||
*/
|
||||
package goshuirc
|
||||
package ircgo
|
||||
|
|
8
go.mod
8
go.mod
|
@ -1,9 +1,3 @@
|
|||
module github.com/goshuirc/irc-go
|
||||
module github.com/ergochat/irc-go
|
||||
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/DanielOaks/go-idn v0.0.0-20160120021903-76db0e10dc65
|
||||
github.com/goshuirc/eventmgr v0.0.0-20170615162049-060479027c93
|
||||
golang.org/x/text v0.3.2
|
||||
)
|
||||
|
|
|
@ -7,8 +7,8 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircevent"
|
||||
"github.com/goshuirc/irc-go/ircmsg"
|
||||
"github.com/ergochat/irc-go/ircevent"
|
||||
"github.com/ergochat/irc-go/ircmsg"
|
||||
)
|
||||
|
||||
func getenv(key, defaultValue string) (value string) {
|
||||
|
|
|
@ -8,8 +8,8 @@ import (
|
|||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircevent"
|
||||
"github.com/goshuirc/irc-go/ircmsg"
|
||||
"github.com/ergochat/irc-go/ircevent"
|
||||
"github.com/ergochat/irc-go/ircmsg"
|
||||
)
|
||||
|
||||
/*
|
||||
|
|
|
@ -30,12 +30,12 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircmsg"
|
||||
"github.com/goshuirc/irc-go/ircreader"
|
||||
"github.com/ergochat/irc-go/ircmsg"
|
||||
"github.com/ergochat/irc-go/ircreader"
|
||||
)
|
||||
|
||||
const (
|
||||
Version = "goshuirc/irc-go"
|
||||
Version = "ergochat/irc-go"
|
||||
|
||||
// prefix for keepalive ping parameters
|
||||
keepalivePrefix = "KeepAlive-"
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircmsg"
|
||||
"github.com/ergochat/irc-go/ircmsg"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircmsg"
|
||||
"github.com/ergochat/irc-go/ircmsg"
|
||||
)
|
||||
|
||||
func eventRewriteCTCP(event *ircmsg.Message) {
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircmsg"
|
||||
"github.com/ergochat/irc-go/ircmsg"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircmsg"
|
||||
"github.com/ergochat/irc-go/ircmsg"
|
||||
)
|
||||
|
||||
type saslResult struct {
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircmsg"
|
||||
"github.com/ergochat/irc-go/ircmsg"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -13,7 +13,7 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircmsg"
|
||||
"github.com/ergochat/irc-go/ircmsg"
|
||||
)
|
||||
|
||||
type empty struct{}
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircmsg"
|
||||
"github.com/ergochat/irc-go/ircmsg"
|
||||
)
|
||||
|
||||
const channel = "#go-eventirc-test"
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
// written by Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the ISC license
|
||||
|
||||
/*
|
||||
Package ircmap handles IRC casefolding, ie ascii/rfc1459 casefolding.
|
||||
*/
|
||||
package ircmap
|
|
@ -1,141 +0,0 @@
|
|||
// written by Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the ISC license
|
||||
|
||||
package ircmap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/secure/precis"
|
||||
|
||||
"github.com/DanielOaks/go-idn/idna2003/stringprep"
|
||||
)
|
||||
|
||||
// MappingType values represent the types of IRC casemapping we support.
|
||||
type MappingType int
|
||||
|
||||
const (
|
||||
// NONE represents no casemapping.
|
||||
NONE MappingType = 0 + iota
|
||||
|
||||
// ASCII represents the traditional "ascii" casemapping.
|
||||
ASCII
|
||||
|
||||
// RFC1459 represents the casemapping defined by "rfc1459"
|
||||
RFC1459
|
||||
|
||||
// RFC3454 represents the UTF-8 nameprep casefolding as used by mammon-ircd.
|
||||
RFC3454
|
||||
|
||||
// RFC7613 represents the UTF-8 casefolding currently being drafted by me
|
||||
// with the IRCv3 WG.
|
||||
RFC7613
|
||||
)
|
||||
|
||||
var (
|
||||
// Mappings is a mapping of ISUPPORT CASEMAP strings to our MappingTypes.
|
||||
Mappings = map[string]MappingType{
|
||||
"ascii": ASCII,
|
||||
"rfc1459": RFC1459,
|
||||
"rfc3454": RFC3454,
|
||||
"rfc7613": RFC7613,
|
||||
}
|
||||
)
|
||||
|
||||
// ChannelPrefixes are the allowed prefixes for channels, used in casefolding.
|
||||
var ChannelPrefixes = map[byte]bool{
|
||||
// standard, well-used
|
||||
'#': true,
|
||||
'&': true,
|
||||
// standard, not well-used
|
||||
'!': true,
|
||||
'+': true,
|
||||
// znc uses for partylines
|
||||
'~': true,
|
||||
}
|
||||
|
||||
// rfc1459Fold casefolds only the special chars defined by RFC1459 -- the
|
||||
// others are handled by the strings.ToLower earlier.
|
||||
func rfc1459Fold(r rune) rune {
|
||||
if '[' <= r && r <= ']' {
|
||||
r += '{' - '['
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrCouldNotStabilize indicates that we could not stabilize the input string.
|
||||
ErrCouldNotStabilize = errors.New("Could not stabilize string while casefolding")
|
||||
)
|
||||
|
||||
// Each pass of PRECIS casefolding is a composition of idempotent operations,
|
||||
// but not idempotent itself. Therefore, the spec says "do it four times and hope
|
||||
// it converges" (lolwtf). Golang's PRECIS implementation has a "repeat" option,
|
||||
// which provides this functionality, but unfortunately it's not exposed publicly.
|
||||
func iterateFolding(profile *precis.Profile, oldStr string) (str string, err error) {
|
||||
str = oldStr
|
||||
// follow the stabilizing rules laid out here:
|
||||
// https://tools.ietf.org/html/draft-ietf-precis-7564bis-10.html#section-7
|
||||
for i := 0; i < 4; i++ {
|
||||
str, err = profile.CompareKey(str)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if oldStr == str {
|
||||
break
|
||||
}
|
||||
oldStr = str
|
||||
}
|
||||
if oldStr != str {
|
||||
return "", ErrCouldNotStabilize
|
||||
}
|
||||
return str, nil
|
||||
}
|
||||
|
||||
// PrecisCasefold returns a casefolded string, without doing any name or channel character checks.
|
||||
func PrecisCasefold(str string) (string, error) {
|
||||
return iterateFolding(precis.UsernameCaseMapped, str)
|
||||
}
|
||||
|
||||
// Casefold returns a string, lowercased/casefolded according to the given
|
||||
// mapping as defined by this package (or an error if the given string is not
|
||||
// valid in the chosen mapping).
|
||||
func Casefold(mapping MappingType, input string) (string, error) {
|
||||
return CasefoldCustomChannelPrefixes(mapping, input, ChannelPrefixes)
|
||||
}
|
||||
|
||||
// CasefoldCustomChannelPrefixes returns a string, lowercased/casefolded
|
||||
// according to the given mapping as defined by this package (or an error if
|
||||
// the given string is not valid in the chosen mapping), using a custom
|
||||
// channel prefix map.
|
||||
func CasefoldCustomChannelPrefixes(mapping MappingType, input string, channelPrefixes map[byte]bool) (string, error) {
|
||||
var out string
|
||||
var err error
|
||||
|
||||
if mapping == ASCII || mapping == RFC1459 {
|
||||
// strings.ToLower ONLY replaces a-z, no unicode stuff so we're safe
|
||||
// to use that here without any issues.
|
||||
out = strings.ToLower(input)
|
||||
|
||||
if mapping == RFC1459 {
|
||||
out = strings.Map(rfc1459Fold, out)
|
||||
}
|
||||
} else if mapping == RFC3454 {
|
||||
out, err = stringprep.Nameprep(input)
|
||||
} else if mapping == RFC7613 {
|
||||
// skip channel prefixes to avoid bidi rule (as per spec)
|
||||
var start int
|
||||
for start = 0; start < len(input) && channelPrefixes[input[start]]; start++ {
|
||||
}
|
||||
|
||||
lowered, err := PrecisCasefold(input[start:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return input[:start] + lowered, err
|
||||
}
|
||||
|
||||
return out, err
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
package ircmap
|
||||
|
||||
import "testing"
|
||||
|
||||
type testcase struct {
|
||||
raw string
|
||||
folded string
|
||||
}
|
||||
|
||||
var equalASCIITests = []testcase{
|
||||
{"Tes4tstsASFd", "tes4tstsasfd"},
|
||||
{"ONsot{[}]sadf", "onsot{[}]sadf"},
|
||||
{"#K03jmn0r-4GD", "#k03jmn0r-4gd"},
|
||||
}
|
||||
|
||||
var equalRFC1459Tests = []testcase{
|
||||
{"rTes4tstsASFd", "rtes4tstsasfd"},
|
||||
{"rONsot{[}]sadf", "ronsot{{}}sadf"},
|
||||
{"#rK03j\\mn0r-4GD", "#rk03j|mn0r-4gd"},
|
||||
}
|
||||
|
||||
var equalRFC3454Tests = []testcase{
|
||||
{"#TeStChAn", "#testchan"},
|
||||
{"#beßtchannEL", "#besstchannel"},
|
||||
{"34563456", "34563456"},
|
||||
}
|
||||
|
||||
var equalRFC7613Tests = []testcase{
|
||||
{"##愛でる", "##愛でる"},
|
||||
{"#ß", "#ß"},
|
||||
{"БЙЮЯ", "бйюя"},
|
||||
}
|
||||
|
||||
func TestASCII(t *testing.T) {
|
||||
for _, pair := range equalASCIITests {
|
||||
val, err := Casefold(ASCII, pair.raw)
|
||||
|
||||
if err != nil {
|
||||
t.Error(
|
||||
"For", pair.raw,
|
||||
"expected", pair.folded,
|
||||
"but we got an error:", err.Error(),
|
||||
)
|
||||
}
|
||||
if val != pair.folded {
|
||||
t.Error(
|
||||
"For", pair.raw,
|
||||
"expected", pair.folded,
|
||||
"got", val,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRFC1459(t *testing.T) {
|
||||
for _, pair := range equalRFC1459Tests {
|
||||
val, err := Casefold(RFC1459, pair.raw)
|
||||
|
||||
if err != nil {
|
||||
t.Error(
|
||||
"For", pair.raw,
|
||||
"expected", pair.folded,
|
||||
"but we got an error:", err.Error(),
|
||||
)
|
||||
}
|
||||
if val != pair.folded {
|
||||
t.Error(
|
||||
"For", pair.raw,
|
||||
"expected", pair.folded,
|
||||
"got", val,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRFC3454(t *testing.T) {
|
||||
for _, pair := range equalRFC3454Tests {
|
||||
val, err := Casefold(RFC3454, pair.raw)
|
||||
|
||||
if err != nil {
|
||||
t.Error(
|
||||
"For", pair.raw,
|
||||
"expected", pair.folded,
|
||||
"but we got an error:", err.Error(),
|
||||
)
|
||||
}
|
||||
if val != pair.folded {
|
||||
t.Error(
|
||||
"For", pair.raw,
|
||||
"expected", pair.folded,
|
||||
"got", val,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRFC7613(t *testing.T) {
|
||||
for _, pair := range equalRFC7613Tests {
|
||||
val, err := Casefold(RFC7613, pair.raw)
|
||||
|
||||
if err != nil {
|
||||
t.Error(
|
||||
"For", pair.raw,
|
||||
"expected", pair.folded,
|
||||
"but we got an error:", err.Error(),
|
||||
)
|
||||
}
|
||||
if val != pair.folded {
|
||||
t.Error(
|
||||
"For", pair.raw,
|
||||
"expected", pair.folded,
|
||||
"got", val,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue