remove ircmap and gircclient, rename to ergochat/irc-go

This commit is contained in:
Shivaram Lingamneni 2021-06-17 16:51:48 -04:00
parent f5e0f875f7
commit 7b3bb1d1ea
28 changed files with 17 additions and 1794 deletions

View File

@ -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 .

View File

@ -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).

View File

@ -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)
}

View File

@ -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, " ")
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}
}
}

View File

@ -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)
}

View File

@ -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",
}

View File

@ -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)
}

View File

@ -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
View File

@ -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
View File

@ -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
)

View File

@ -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) {

View File

@ -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"
)
/*

View File

@ -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-"

View File

@ -8,7 +8,7 @@ import (
"strings"
"time"
"github.com/goshuirc/irc-go/ircmsg"
"github.com/ergochat/irc-go/ircmsg"
)
const (

View File

@ -5,7 +5,7 @@ import (
"strings"
"time"
"github.com/goshuirc/irc-go/ircmsg"
"github.com/ergochat/irc-go/ircmsg"
)
func eventRewriteCTCP(event *ircmsg.Message) {

View File

@ -7,7 +7,7 @@ import (
"fmt"
"testing"
"github.com/goshuirc/irc-go/ircmsg"
"github.com/ergochat/irc-go/ircmsg"
)
const (

View File

@ -5,7 +5,7 @@ import (
"errors"
"fmt"
"github.com/goshuirc/irc-go/ircmsg"
"github.com/ergochat/irc-go/ircmsg"
)
type saslResult struct {

View File

@ -6,7 +6,7 @@ import (
"os"
"testing"
"github.com/goshuirc/irc-go/ircmsg"
"github.com/ergochat/irc-go/ircmsg"
)
const (

View File

@ -13,7 +13,7 @@ import (
"sync/atomic"
"time"
"github.com/goshuirc/irc-go/ircmsg"
"github.com/ergochat/irc-go/ircmsg"
)
type empty struct{}

View File

@ -8,7 +8,7 @@ import (
"testing"
"time"
"github.com/goshuirc/irc-go/ircmsg"
"github.com/ergochat/irc-go/ircmsg"
)
const channel = "#go-eventirc-test"

View File

@ -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

View File

@ -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
}

View File

@ -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"},
{"3456", "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,
)
}
}
}