aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPřemysl Eric Janouch <p@janouch.name>2024-04-02 16:44:01 +0200
committerPřemysl Eric Janouch <p@janouch.name>2024-04-03 15:56:33 +0200
commit53ba996ec9c5c8fc64f66934d8c98509bd7ed06d (patch)
treefe396b25631e5a325c0e7d470dbb3e7e045d66b4
parentd450c6cc5fe4caa2f9a1db3f0ec76c84b6a110af (diff)
downloadxK-53ba996ec9c5c8fc64f66934d8c98509bd7ed06d.tar.gz
xK-53ba996ec9c5c8fc64f66934d8c98509bd7ed06d.tar.xz
xK-53ba996ec9c5c8fc64f66934d8c98509bd7ed06d.zip
Add a simple IRC notifier utility
-rw-r--r--LICENSE2
-rw-r--r--NEWS2
-rw-r--r--README.adoc4
-rw-r--r--xN/.gitignore3
-rw-r--r--xN/Makefile19
-rw-r--r--xN/go.mod3
-rw-r--r--xN/xN.adoc86
-rw-r--r--xN/xN.go279
-rw-r--r--xN/xN_test.go98
-rw-r--r--xS/irc.go132
-rw-r--r--xS/xS.go126
11 files changed, 627 insertions, 127 deletions
diff --git a/LICENSE b/LICENSE
index 0610bbd..d58be36 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2014 - 2023, Přemysl Eric Janouch <p@janouch.name>
+Copyright (c) 2014 - 2024, Přemysl Eric Janouch <p@janouch.name>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
diff --git a/NEWS b/NEWS
index 70f3849..7ee97ec 100644
--- a/NEWS
+++ b/NEWS
@@ -37,6 +37,8 @@
* Added a Go port of xD called xS
+ * Added a simple notifier called xN
+
1.5.0 (2021-12-21) "The Show Must Go On"
diff --git a/README.adoc b/README.adoc
index 6784e84..ed915dc 100644
--- a/README.adoc
+++ b/README.adoc
@@ -56,6 +56,10 @@ https://github.com/kiwiirc/webircgateway[].
Any further development, such as P10 or TS6 linking for IRC services,
or plugin support for arbitrary bridges, will happen here.
+xN
+--
+The IRC notifier, should you ever need to send automated messages from a script.
+
xB
--
The IRC bot. While originally intended to be a simple rewrite of my old GNU AWK
diff --git a/xN/.gitignore b/xN/.gitignore
new file mode 100644
index 0000000..86ad66d
--- /dev/null
+++ b/xN/.gitignore
@@ -0,0 +1,3 @@
+/xN
+/xN.1
+/irc.go
diff --git a/xN/Makefile b/xN/Makefile
new file mode 100644
index 0000000..5340b55
--- /dev/null
+++ b/xN/Makefile
@@ -0,0 +1,19 @@
+.POSIX:
+.SUFFIXES:
+AWK = env LC_ALL=C awk
+
+outputs = irc.go xN xN.1
+all: $(outputs)
+
+# If we want to keep module dependencies separate, we don't have many options.
+# Symlinking seems to work good enough.
+irc.go: ../xS/irc.go
+ ln -sf ../xS/irc.go $@
+
+xN: xN.go ../xK-version irc.go
+ go build -ldflags "-X 'main.projectVersion=$$(cat ../xK-version)'" -o $@
+xN.1: ../xK-version ../liberty/tools/asciiman.awk xN.adoc
+ env "asciidoc-release-version=$$(cat ../xK-version)" \
+ $(AWK) -f ../liberty/tools/asciiman.awk xN.adoc > $@
+clean:
+ rm -f $(outputs)
diff --git a/xN/go.mod b/xN/go.mod
new file mode 100644
index 0000000..50ff293
--- /dev/null
+++ b/xN/go.mod
@@ -0,0 +1,3 @@
+module janouch.name/xK/xN
+
+go 1.19
diff --git a/xN/xN.adoc b/xN/xN.adoc
new file mode 100644
index 0000000..1f5404f
--- /dev/null
+++ b/xN/xN.adoc
@@ -0,0 +1,86 @@
+xN(1)
+=====
+:doctype: manpage
+:manmanual: xK Manual
+:mansource: xK {release-version}
+
+Name
+----
+xN - IRC notifier
+
+Synopsis
+--------
+*xN* [_OPTION_]... IRC-URL...
+
+Description
+-----------
+*xN* is a simple IRC notifier, sending the text it receives on its standard
+input to all IRC targets specified by its command line arguments.
+
+The input text is forced to validate as UTF-8, and it is _not_ split
+automatically to comply with the maximum IRC message length.
+Thus, make sure to make the lines short, or they will be trimmed by the servers.
+
+*xN* does not attempt to appease flood detectors.
+
+Options
+-------
+*-debug*::
+ Print incoming IRC traffic, which may help in debugging any issues.
+
+*-version*::
+ Output version information and exit.
+
+URL format
+----------
+*xN* accepts URLs describing IRC users and channels, roughly as specified by
+the Internet Draft _draft-butcher-irc-url-04.txt_. Note, however, that close
+to no validation is done on these, and you should not pass URLs from untrusted
+sources, so as to avoid command or parameter injection.
+
+Any provided username will be propagated to the nickname, username,
+and realname. The default value for these is the name of your system user.
+
+As an extension, you may use the following options:
+
+*skipjoin*::
+ Do not JOIN channels before sending messages to them.
+ This requires channels to allow external messages
+ (which are disabled by channel mode *+n*).
+*usenotice*::
+ Send a NOTICE rather than a PRIVMSG, which distinguishes automated messages,
+ and is more appropriate for bots.
+
+Examples
+--------
+ $ uptime | xN 'irc://uptime@localhost/%23watch?skipjoin&usenotice'
+
+Send *uptime*(1) information as an external notice to channel *#watch*
+on the local server, using the standard port *6667*.
+
+ $ fortune -s | xN ircs://ohayou@irc.libera.chat/john,isuser
+
+Greet user *john* with a fortune for this day. In compliance with _RFC 7194_,
+the default TLS port is assumed to be *6697*.
+
+ $ xN 'ircs://agent:Password123@irc.cia.gov:1337/#hq?key=123456' <<EOF
+ The red fox trots quietly at midnight.
+ EOF
+
+Connect over TLS to *irc.cia.gov* on port *1337*, use *Password123*
+as the server password, register as user *agent*,
+join channel *#hq* using the channel key *123456*,
+and send a very secret message.
+
+Reporting bugs
+--------------
+Use https://git.janouch.name/p/xK to report bugs, request features,
+or submit pull requests.
+
+See also
+--------
+_Uniform Resource Locator Schemes for Internet Relay Chat Entities_,
+https://datatracker.ietf.org/doc/html/draft-butcher-irc-url-04[].
+
+_Default Port for Internet Relay Chat (IRC) via TLS/SSL_,
+https://datatracker.ietf.org/doc/html/rfc7194[].
diff --git a/xN/xN.go b/xN/xN.go
new file mode 100644
index 0000000..807de69
--- /dev/null
+++ b/xN/xN.go
@@ -0,0 +1,279 @@
+//
+// Copyright (c) 2024, Přemysl Eric Janouch <p@janouch.name>
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+
+// xN is a simple IRC notifier.
+package main
+
+import (
+ "bufio"
+ "bytes"
+ "crypto/tls"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "net"
+ "net/url"
+ "os"
+ "os/user"
+ "strconv"
+ "strings"
+)
+
+const projectName = "xN"
+
+var projectVersion = "?"
+
+var debugMode = false
+
+type parameters struct {
+ conn net.Conn // underlying network connection
+ username string // nickname + username + realname
+ password string // server password
+ target string // where to send text
+ isuser bool // the target is a user rather than a channel
+ chankey string // channel key
+ skipjoin bool // whether to send external messages to channels
+ usenotice bool // whether to use NOTICE rather than PRIVMSG
+ message []string // lines of the message to send
+}
+
+func (p *parameters) send(command string, args ...string) error {
+ var buf bytes.Buffer
+ buf.WriteString(command)
+ for i, arg := range args {
+ buf.WriteRune(' ')
+ if i+1 == len(args) {
+ buf.WriteRune(':')
+ }
+ buf.WriteString(arg)
+ }
+ buf.WriteString("\r\n")
+ _, err := p.conn.Write(buf.Bytes())
+ return err
+}
+
+const (
+ rplWELCOME = "001"
+ errNICKNAMEINUSE = "433"
+)
+
+func notify(p *parameters) error {
+ // The intro should comfortably fit in the TCP send buffer whole.
+ if p.password != "" {
+ p.send("PASS", p.password)
+ }
+ p.send("USER", p.username, "0", "*", p.username)
+ p.send("NICK", p.username)
+
+ scanner, nickCounter, issue := bufio.NewScanner(p.conn), 1, ""
+ for scanner.Scan() {
+ if debugMode {
+ log.Println(scanner.Text())
+ }
+
+ m := ircParseMessage(scanner.Text())
+ switch m.command {
+ case "PING":
+ p.send("PONG", m.params...)
+ case rplWELCOME:
+ if !p.isuser && !p.skipjoin {
+ if p.chankey != "" {
+ p.send("JOIN", p.target, p.chankey)
+ } else {
+ p.send("JOIN", p.target)
+ }
+ }
+ for _, line := range p.message {
+ if p.usenotice {
+ p.send("NOTICE", p.target, line)
+ } else {
+ p.send("PRIVMSG", p.target, line)
+ }
+ }
+ p.send("QUIT")
+ case errNICKNAMEINUSE:
+ p.send("NICK", fmt.Sprintf("%s%d", p.username, nickCounter))
+ nickCounter++
+ default:
+ // Prevent hanging on unsuccessful registrations.
+ numeric, _ := strconv.Atoi(m.command)
+ if numeric >= 400 && numeric <= 599 {
+ if len(m.params) > 1 {
+ issue = strings.Join(m.params[1:], " ")
+ } else {
+ issue = strings.Join(m.params, " ")
+ }
+ p.send("QUIT")
+ }
+ }
+ }
+ if err := scanner.Err(); err != nil {
+ return err
+ }
+ if issue != "" {
+ return errors.New(issue)
+ }
+ return nil
+}
+
+func parse(rawURL string, text []byte) (
+ p parameters, connect func() (net.Conn, error), err error) {
+ u, err := url.Parse(rawURL)
+ if err != nil {
+ return p, nil, err
+ } else if !u.IsAbs() || u.Opaque != "" {
+ return p, nil, errors.New("need an absolute URL")
+ } else if u.Path == "/" && u.Fragment != "" {
+ // Try to handle the common but degenerate case.
+ fragment := "%23" + u.Fragment
+ u.Fragment, u.RawFragment = "", ""
+ if u, err = url.Parse(u.String() + fragment); err != nil {
+ return p, nil, err
+ }
+ }
+
+ // Figure out registration details.
+ p.username = projectName
+ if u, _ := user.Current(); u != nil {
+ p.username = u.Username
+ }
+ if u.User.Username() != "" {
+ p.username = u.User.Username()
+ }
+
+ p.password, _ = u.User.Password()
+
+ // Figure out the target, which for our intents must accept messages.
+ path, _ := strings.CutPrefix(u.Path, "/")
+ elements := strings.Split(path, ",")
+ if path == "" || elements[0] == "" {
+ return p, nil, errors.New("unspecified entity")
+ }
+
+ // The last entity type wins.
+ p.target, p.isuser = elements[0], false
+ for _, typ := range elements[1:] {
+ switch typ {
+ case "isuser":
+ p.isuser = true
+ case "ischannel":
+ p.isuser = false
+ case "isserver":
+ // We do not support network names, and this is the default.
+ default:
+ return p, nil, errors.New("unsupported type: " + typ)
+ }
+ }
+ if p.isuser {
+ if i := strings.IndexAny(p.target, "!@"); i != -1 {
+ p.target = p.target[:i]
+ }
+ } else if !strings.HasPrefix(p.target, "#") {
+ // TODO(p): We should consult RPL_ISUPPORT rather than guess,
+ // though other prefixes are rare.
+ p.target = "#" + p.target
+ }
+
+ // Note that the draft RFC wants these to be case-insensitive.
+ p.chankey = u.Query().Get("key")
+ // Being able to skip channel join is our own requirement and invention,
+ // as are notices (names taken from Travis CI configuration).
+ p.skipjoin = u.Query().Has("skipjoin")
+ p.usenotice = u.Query().Has("usenotice")
+
+ // Ensure valid LF-separated UTF-8, and split it at lines.
+ sanitized := strings.ReplaceAll(string([]rune(string(text))), "\r", "\n")
+ for _, line := range strings.Split(sanitized, "\n") {
+ if line != "" {
+ p.message = append(p.message, line)
+ }
+ }
+
+ hostname, port := u.Hostname(), u.Port()
+ switch u.Scheme {
+ case "irc":
+ if port == "" {
+ port = "6667"
+ }
+ connect = func() (net.Conn, error) {
+ return net.Dial("tcp", net.JoinHostPort(hostname, port))
+ }
+ case "ircs":
+ if port == "" {
+ port = "6697"
+ }
+ connect = func() (net.Conn, error) {
+ return tls.Dial("tcp", net.JoinHostPort(hostname, port), nil)
+ }
+ default:
+ err = errors.New("unsupported scheme: " + u.Scheme)
+ }
+ return
+}
+
+// notifyByURL sends the given text to the IRC server specified by a URL.
+// See draft-butcher-irc-url-04.txt for the URL scheme specification
+// this function loosely follows.
+func notifyByURL(rawURL string, text []byte) error {
+ p, connect, err := parse(rawURL, text)
+ if p.conn, err = connect(); err != nil {
+ return err
+ }
+ defer p.conn.Close()
+ return notify(&p)
+}
+
+func main() {
+ flag.BoolVar(&debugMode, "debug", false, "run in verbose debug mode")
+ version := flag.Bool("version", false, "show version and exit")
+
+ flag.Usage = func() {
+ f := flag.CommandLine.Output()
+ fmt.Fprintf(f, "Usage: %s URL...\n", os.Args[0])
+ flag.PrintDefaults()
+ }
+ flag.Parse()
+ if flag.NArg() < 1 {
+ flag.Usage()
+ os.Exit(2)
+ }
+
+ if *version {
+ fmt.Printf("%s %s\n", projectName, projectVersion)
+ return
+ }
+
+ text, err := io.ReadAll(os.Stdin)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ status := 0
+ for _, rawURL := range flag.Args() {
+ if err := notifyByURL(rawURL, text); err != nil {
+ status = 1
+
+ var ue *url.Error
+ if errors.As(err, &ue) {
+ log.Println(err)
+ } else {
+ log.Printf("notify %q: %s\n", rawURL, err)
+ }
+ }
+ }
+ os.Exit(status)
+}
diff --git a/xN/xN_test.go b/xN/xN_test.go
new file mode 100644
index 0000000..4755b39
--- /dev/null
+++ b/xN/xN_test.go
@@ -0,0 +1,98 @@
+//
+// Copyright (c) 2024, Přemysl Eric Janouch <p@janouch.name>
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+
+package main
+
+import "testing"
+
+func TestParseURL(t *testing.T) {
+ for _, rawURL := range []string{
+ `irc:server/channel`,
+ `irc://server/channel,isnetwork`,
+ `ircs://ssl.ircnet.io`,
+ `ircs://ssl.ircnet.io/`,
+ `http://server/path`,
+ } {
+ if _, _, err := parse(rawURL, nil); err == nil {
+ t.Errorf("%q should not parse\n", rawURL)
+ }
+ }
+
+ for _, test := range []struct {
+ rawURL string
+ p parameters
+ }{
+ {
+ rawURL: `irc://uptime@localhost/%23watch?skipjoin&usenotice`,
+ p: parameters{
+ username: "uptime",
+ target: "#watch",
+ skipjoin: true,
+ usenotice: true,
+ },
+ },
+ {
+ rawURL: `ircs://ohayou@irc.libera.chat/john,isuser,isserver`,
+ p: parameters{
+ username: "ohayou",
+ target: "john",
+ isuser: true,
+ },
+ },
+ {
+ rawURL: `ircs://agent:Password123@irc.cia.gov:1337/#hq?key=123456`,
+ p: parameters{
+ username: "agent",
+ password: "Password123",
+ target: "#hq",
+ chankey: "123456",
+ },
+ },
+ } {
+ p, _, err := parse(test.rawURL, nil)
+ if err != nil {
+ t.Errorf("%q should parse, got: %s\n", test.rawURL, err)
+ continue
+ }
+ if p.username != test.p.username {
+ t.Errorf("%q: username: %v ≠ %v\n",
+ test.rawURL, p.username, test.p.username)
+ }
+ if p.password != test.p.password {
+ t.Errorf("%q: password: %v ≠ %v\n",
+ test.rawURL, p.password, test.p.password)
+ }
+ if p.target != test.p.target {
+ t.Errorf("%q: target: %v ≠ %v\n",
+ test.rawURL, p.target, test.p.target)
+ }
+ if p.isuser != test.p.isuser {
+ t.Errorf("%q: isuser: %v ≠ %v\n",
+ test.rawURL, p.isuser, test.p.isuser)
+ }
+ if p.chankey != test.p.chankey {
+ t.Errorf("%q: chankey: %v ≠ %v\n",
+ test.rawURL, p.chankey, test.p.chankey)
+ }
+ if p.skipjoin != test.p.skipjoin {
+ t.Errorf("%q: skipjoin: %v ≠ %v\n",
+ test.rawURL, p.skipjoin, test.p.skipjoin)
+ }
+ if p.usenotice != test.p.usenotice {
+ t.Errorf("%q: usenotice: %v ≠ %v\n",
+ test.rawURL, p.usenotice, test.p.usenotice)
+ }
+ }
+}
diff --git a/xS/irc.go b/xS/irc.go
new file mode 100644
index 0000000..71bf961
--- /dev/null
+++ b/xS/irc.go
@@ -0,0 +1,132 @@
+package main
+
+import (
+ "path/filepath"
+ "regexp"
+ "strings"
+)
+
+func ircToLower(c byte) byte {
+ switch c {
+ case '[':
+ return '{'
+ case ']':
+ return '}'
+ case '\\':
+ return '|'
+ case '~':
+ return '^'
+ }
+ if c >= 'A' && c <= 'Z' {
+ return c + ('a' - 'A')
+ }
+ return c
+}
+
+func ircToUpper(c byte) byte {
+ switch c {
+ case '{':
+ return '['
+ case '}':
+ return ']'
+ case '|':
+ return '\\'
+ case '^':
+ return '~'
+ }
+ if c >= 'a' && c <= 'z' {
+ return c - ('a' - 'A')
+ }
+ return c
+}
+
+// Convert identifier to a canonical form for case-insensitive comparisons.
+// ircToUpper is used so that statically initialized maps can be in uppercase.
+func ircToCanon(ident string) string {
+ var canon []byte
+ for _, c := range []byte(ident) {
+ canon = append(canon, ircToUpper(c))
+ }
+ return string(canon)
+}
+
+func ircEqual(s1, s2 string) bool {
+ return ircToCanon(s1) == ircToCanon(s2)
+}
+
+func ircFnmatch(pattern string, s string) bool {
+ pattern, s = ircToCanon(pattern), ircToCanon(s)
+ // FIXME: This should not support [] ranges and handle '/' specially.
+ // We could translate the pattern to a regular expression.
+ matched, _ := filepath.Match(pattern, s)
+ return matched
+}
+
+var reMsg = regexp.MustCompile(
+ `^(?:@([^ ]*) +)?(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(.*)?$`)
+var reArgs = regexp.MustCompile(`:.*| [^: ][^ ]*`)
+
+type message struct {
+ tags map[string]string // IRC 3.2 message tags
+ nick string // optional nickname
+ user string // optional username
+ host string // optional hostname or IP address
+ command string // command name
+ params []string // arguments
+}
+
+func ircUnescapeMessageTag(value string) string {
+ var buf []byte
+ escape := false
+ for i := 0; i < len(value); i++ {
+ if escape {
+ switch value[i] {
+ case ':':
+ buf = append(buf, ';')
+ case 's':
+ buf = append(buf, ' ')
+ case 'r':
+ buf = append(buf, '\r')
+ case 'n':
+ buf = append(buf, '\n')
+ default:
+ buf = append(buf, value[i])
+ }
+ escape = false
+ } else if value[i] == '\\' {
+ escape = true
+ } else {
+ buf = append(buf, value[i])
+ }
+ }
+ return string(buf)
+}
+
+func ircParseMessageTags(tags string, out map[string]string) {
+ for _, tag := range strings.Split(tags, ";") {
+ if tag == "" {
+ // Ignore empty.
+ } else if equal := strings.IndexByte(tag, '='); equal < 0 {
+ out[tag] = ""
+ } else {
+ out[tag[:equal]] = ircUnescapeMessageTag(tag[equal+1:])
+ }
+ }
+}
+
+func ircParseMessage(line string) *message {
+ m := reMsg.FindStringSubmatch(line)
+ if m == nil {
+ return nil
+ }
+
+ msg := message{nil, m[2], m[3], m[4], m[5], nil}
+ if m[1] != "" {
+ msg.tags = make(map[string]string)
+ ircParseMessageTags(m[1], msg.tags)
+ }
+ for _, x := range reArgs.FindAllString(m[6], -1) {
+ msg.params = append(msg.params, x[1:])
+ }
+ return &msg
+}
diff --git a/xS/xS.go b/xS/xS.go
index 25db005..69a9b36 100644
--- a/xS/xS.go
+++ b/xS/xS.go
@@ -456,132 +456,6 @@ func (fd *floodDetector) check() bool {
return count <= fd.limit
}
-// --- IRC protocol ------------------------------------------------------------
-
-func ircToLower(c byte) byte {
- switch c {
- case '[':
- return '{'
- case ']':
- return '}'
- case '\\':
- return '|'
- case '~':
- return '^'
- }
- if c >= 'A' && c <= 'Z' {
- return c + ('a' - 'A')
- }
- return c
-}
-
-func ircToUpper(c byte) byte {
- switch c {
- case '{':
- return '['
- case '}':
- return ']'
- case '|':
- return '\\'
- case '^':
- return '~'
- }
- if c >= 'a' && c <= 'z' {
- return c - ('a' - 'A')
- }
- return c
-}
-
-// Convert identifier to a canonical form for case-insensitive comparisons.
-// ircToUpper is used so that statically initialized maps can be in uppercase.
-func ircToCanon(ident string) string {
- var canon []byte
- for _, c := range []byte(ident) {
- canon = append(canon, ircToUpper(c))
- }
- return string(canon)
-}
-
-func ircEqual(s1, s2 string) bool {
- return ircToCanon(s1) == ircToCanon(s2)
-}
-
-func ircFnmatch(pattern string, s string) bool {
- pattern, s = ircToCanon(pattern), ircToCanon(s)
- // FIXME: This should not support [] ranges and handle '/' specially.
- // We could translate the pattern to a regular expression.
- matched, _ := filepath.Match(pattern, s)
- return matched
-}
-
-var reMsg = regexp.MustCompile(
- `^(?:@([^ ]*) +)?(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(.*)?$`)
-var reArgs = regexp.MustCompile(`:.*| [^: ][^ ]*`)
-
-type message struct {
- tags map[string]string // IRC 3.2 message tags
- nick string // optional nickname
- user string // optional username
- host string // optional hostname or IP address
- command string // command name
- params []string // arguments
-}
-
-func ircUnescapeMessageTag(value string) string {
- var buf []byte
- escape := false
- for i := 0; i < len(value); i++ {
- if escape {
- switch value[i] {
- case ':':
- buf = append(buf, ';')
- case 's':
- buf = append(buf, ' ')
- case 'r':
- buf = append(buf, '\r')
- case 'n':
- buf = append(buf, '\n')
- default:
- buf = append(buf, value[i])
- }
- escape = false
- } else if value[i] == '\\' {
- escape = true
- } else {
- buf = append(buf, value[i])
- }
- }
- return string(buf)
-}
-
-func ircParseMessageTags(tags string, out map[string]string) {
- for _, tag := range splitString(tags, ";", true /* ignoreEmpty */) {
- if equal := strings.IndexByte(tag, '='); equal < 0 {
- out[tag] = ""
- } else {
- out[tag[:equal]] = ircUnescapeMessageTag(tag[equal+1:])
- }
- }
-}
-
-func ircParseMessage(line string) *message {
- m := reMsg.FindStringSubmatch(line)
- if m == nil {
- return nil
- }
-
- msg := message{nil, m[2], m[3], m[4], m[5], nil}
- if m[1] != "" {
- msg.tags = make(map[string]string)
- ircParseMessageTags(m[1], msg.tags)
- }
- for _, x := range reArgs.FindAllString(m[6], -1) {
- msg.params = append(msg.params, x[1:])
- }
- return &msg
-
-}
-
// --- IRC token validation ----------------------------------------------------
// Everything as per RFC 2812