From f224c77baca7cdeafa9d9e1124d2dd3a878bbb35 Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 12 Sep 2018 18:07:49 +0800 Subject: [PATCH] start irc module --- Gopkg.lock | 9 + model/line_message_log.go | 13 + model/line_user.go | 11 + model/youtube_channel.go | 13 + module/background/background.go | 1 + module/background/opay.go | 21 ++ module/twitch-irc/twitch-irc.go | 33 ++ vendor/gopkg.in/irc.v2/.gitignore | 3 + vendor/gopkg.in/irc.v2/.gitmodules | 3 + vendor/gopkg.in/irc.v2/.travis.yml | 21 ++ vendor/gopkg.in/irc.v2/LICENSE | 18 + vendor/gopkg.in/irc.v2/README.md | 104 ++++++ vendor/gopkg.in/irc.v2/client.go | 331 ++++++++++++++++++ vendor/gopkg.in/irc.v2/client_handlers.go | 151 +++++++++ vendor/gopkg.in/irc.v2/conn.go | 103 ++++++ vendor/gopkg.in/irc.v2/go.mod | 1 + vendor/gopkg.in/irc.v2/handler.go | 16 + vendor/gopkg.in/irc.v2/parser.go | 394 ++++++++++++++++++++++ vendor/gopkg.in/irc.v2/utils.go | 50 +++ 19 files changed, 1296 insertions(+) create mode 100644 model/line_message_log.go create mode 100644 model/line_user.go create mode 100644 model/youtube_channel.go create mode 100644 module/background/opay.go create mode 100644 module/twitch-irc/twitch-irc.go create mode 100644 vendor/gopkg.in/irc.v2/.gitignore create mode 100644 vendor/gopkg.in/irc.v2/.gitmodules create mode 100644 vendor/gopkg.in/irc.v2/.travis.yml create mode 100644 vendor/gopkg.in/irc.v2/LICENSE create mode 100644 vendor/gopkg.in/irc.v2/README.md create mode 100644 vendor/gopkg.in/irc.v2/client.go create mode 100644 vendor/gopkg.in/irc.v2/client_handlers.go create mode 100644 vendor/gopkg.in/irc.v2/conn.go create mode 100644 vendor/gopkg.in/irc.v2/go.mod create mode 100644 vendor/gopkg.in/irc.v2/handler.go create mode 100644 vendor/gopkg.in/irc.v2/parser.go create mode 100644 vendor/gopkg.in/irc.v2/utils.go diff --git a/Gopkg.lock b/Gopkg.lock index 849488c..2a9dfaf 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -188,6 +188,14 @@ revision = "5f1438d3fca68893a817e4a66806cea46a9e4ebf" version = "v8.18.2" +[[projects]] + digest = "1:54c3b474a0e4a8e65c74e0dbb272bcf30a4bab48883d34f958cbb9d8e8ef387a" + name = "gopkg.in/irc.v2" + packages = ["."] + pruneopts = "UT" + revision = "4901bf6be124ba1558d3657e91286393c97fb47f" + version = "v2.1.2" + [[projects]] digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202" name = "gopkg.in/yaml.v2" @@ -209,6 +217,7 @@ "github.com/lib/pq", "github.com/robfig/cron", "golang.org/x/crypto/bcrypt", + "gopkg.in/irc.v2", "gopkg.in/yaml.v2", ] solver-name = "gps-cdcl" diff --git a/model/line_message_log.go b/model/line_message_log.go new file mode 100644 index 0000000..15c12f1 --- /dev/null +++ b/model/line_message_log.go @@ -0,0 +1,13 @@ +package model + +import "time" + +// LineMessageLog - +type LineMessageLog struct { + ID string `db:"id" cc:"id"` + Group string `db:"group" cc:"group"` + User string `db:"user" cc:"user"` + Message string `db:"message" cc:"message"` + Ctime time.Time `db:"ctime" cc:"ctime"` + Mtime time.Time `db:"mtime" cc:"mtime"` +} diff --git a/model/line_user.go b/model/line_user.go new file mode 100644 index 0000000..4399f57 --- /dev/null +++ b/model/line_user.go @@ -0,0 +1,11 @@ +package model + +import "time" + +// LineUser - +type LineUser struct { + ID string `db:"id" cc:"id"` + Name string `db:"name" cc:"name"` + Ctime time.Time `db:"ctime" cc:"ctime"` + Mtime time.Time `db:"mtime" cc:"mtime"` +} diff --git a/model/youtube_channel.go b/model/youtube_channel.go new file mode 100644 index 0000000..3559411 --- /dev/null +++ b/model/youtube_channel.go @@ -0,0 +1,13 @@ +package model + +import "time" + +// YoutubeChannel - +type YoutubeChannel struct { + ID string `db:"id" cc:"id"` + Name string `db:"name" cc:"name"` + LastVideo string `db:"lastvideo" cc:"lastvideo"` + Expire int32 `db:"expire" cc:"expire"` + Ctime time.Time `db:"ctime" cc:"ctime"` + Mtime time.Time `db:"mtime" cc:"mtime"` +} diff --git a/module/background/background.go b/module/background/background.go index 542601f..6c20684 100644 --- a/module/background/background.go +++ b/module/background/background.go @@ -11,5 +11,6 @@ func SetBackground() { c = cron.New() c.AddFunc("0 * * * * *", readFacebookPage) c.AddFunc("*/20 * * * * *", getStreamStatus) + c.AddFunc("*/5 * * * * *", checkOpay) c.Start() } diff --git a/module/background/opay.go b/module/background/opay.go new file mode 100644 index 0000000..ff77987 --- /dev/null +++ b/module/background/opay.go @@ -0,0 +1,21 @@ +package background + +import ( + "git.trj.tw/golang/mtfosbot/model" +) + +func checkOpay() { + channels, err := model.GetAllTwitchChannel() + if err != nil { + return + } + for _, v := range channels { + if len(v.OpayID) > 0 && v.Join { + go getOpayData(v) + } + } +} + +func getOpayData(ch *model.TwitchChannel) { + +} diff --git a/module/twitch-irc/twitch-irc.go b/module/twitch-irc/twitch-irc.go new file mode 100644 index 0000000..6445980 --- /dev/null +++ b/module/twitch-irc/twitch-irc.go @@ -0,0 +1,33 @@ +package twitchirc + +import ( + "fmt" + "net" + + "gopkg.in/irc.v2" + + "git.trj.tw/golang/mtfosbot/module/config" +) + +var client *irc.Client + +// InitIRC - +func InitIRC() (err error) { + conf := config.GetConf() + conn, err := net.Dial("tcp", conf.Twitch.ChatHost) + if err != nil { + return + } + config := irc.ClientConfig{ + Handler: irc.HandlerFunc(ircHandle), + } + + client = irc.NewClient(conn, config) + + err = client.Run() + return +} + +func ircHandle(c *irc.Client, m *irc.Message) { + fmt.Println(m.String()) +} diff --git a/vendor/gopkg.in/irc.v2/.gitignore b/vendor/gopkg.in/irc.v2/.gitignore new file mode 100644 index 0000000..b64fa18 --- /dev/null +++ b/vendor/gopkg.in/irc.v2/.gitignore @@ -0,0 +1,3 @@ +*.cover +*.test +*.out diff --git a/vendor/gopkg.in/irc.v2/.gitmodules b/vendor/gopkg.in/irc.v2/.gitmodules new file mode 100644 index 0000000..8ed3899 --- /dev/null +++ b/vendor/gopkg.in/irc.v2/.gitmodules @@ -0,0 +1,3 @@ +[submodule "testcases"] + path = testcases + url = https://github.com/go-irc/irc-parser-tests/ diff --git a/vendor/gopkg.in/irc.v2/.travis.yml b/vendor/gopkg.in/irc.v2/.travis.yml new file mode 100644 index 0000000..494bd20 --- /dev/null +++ b/vendor/gopkg.in/irc.v2/.travis.yml @@ -0,0 +1,21 @@ +language: go + +before_install: + - go get github.com/axw/gocov/gocov + - go get github.com/mattn/goveralls + - go get golang.org/x/tools/cmd/cover + # Grab all deps (should just be test deps) + - go get -v -t ./... + # Linting deps + - go get github.com/alecthomas/gometalinter + - gometalinter --install + # Remove the go file from the test cases dir as it fails linting + - rm ./testcases/*.go + +script: + - gometalinter --fast ./... -D gas + - go test -race -v ./... + - go test -covermode=count -coverprofile=profile.cov + +after_script: + - $HOME/gopath/bin/goveralls -coverprofile=profile.cov -service=travis-ci diff --git a/vendor/gopkg.in/irc.v2/LICENSE b/vendor/gopkg.in/irc.v2/LICENSE new file mode 100644 index 0000000..7e5e1ab --- /dev/null +++ b/vendor/gopkg.in/irc.v2/LICENSE @@ -0,0 +1,18 @@ +Copyright 2016 Kaleb Elwert + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/gopkg.in/irc.v2/README.md b/vendor/gopkg.in/irc.v2/README.md new file mode 100644 index 0000000..f2d755b --- /dev/null +++ b/vendor/gopkg.in/irc.v2/README.md @@ -0,0 +1,104 @@ +# go-irc + +[![GoDoc](https://img.shields.io/badge/doc-GoDoc-blue.svg)](https://godoc.org/github.com/go-irc/irc) +[![Build Status](https://img.shields.io/travis/go-irc/irc.svg)](https://travis-ci.org/go-irc/irc) +[![Coverage Status](https://img.shields.io/coveralls/go-irc/irc.svg)](https://coveralls.io/github/go-irc/irc?branch=master) + +This package was originally created to only handle message parsing, +but has since been expanded to include a small abstraction around a +connection and a simple client. + +This library is not designed to hide any of the IRC elements from +you. If you just want to build a simple chat bot and don't want to +deal with IRC in particular, there are a number of other libraries +which provide a more full featured client if that's what you're +looking for. + +This library is meant to stay as simple as possible so it can be a +building block for other packages. + +This library aims for API compatibility whenever possible. New +functions and other additions will most likely not result in a major +version increase unless they break the API. This library aims to +follow the semver recommendations mentioned on gopkg.in. + +Due to complications in how to support x/net/context vs the built-in context +package, only go 1.7+ is officially supported. + +## Import Paths + +All development happens on the `master` branch and when features are +considered stable enough, a new release will be tagged. + +As a result of this, there are multiple import locations. + +* `gopkg.in/irc.v2` should be used to develop against the commits + tagged as stable +* `github.com/go-irc/irc` should be used to develop against the master branch + +## Development + +In order to run the tests, make sure all submodules are up to date. If you are +just using this library, these are not needed. + +## Example + +```go +package main + +import ( + "log" + "net" + + "github.com/belak/irc" +) + +func main() { + conn, err := net.Dial("tcp", "chat.freenode.net:6667") + if err != nil { + log.Fatalln(err) + } + + config := irc.ClientConfig{ + Nick: "i_have_a_nick", + Pass: "password", + User: "username", + Name: "Full Name", + Handler: irc.HandlerFunc(func(c *irc.Client, m *irc.Message) { + if m.Command == "001" { + // 001 is a welcome event, so we join channels there + c.Write("JOIN #bot-test-chan") + } else if m.Command == "PRIVMSG" && m.FromChannel() { + // Create a handler on all messages. + c.WriteMessage(&irc.Message{ + Command: "PRIVMSG", + Params: []string{ + m.Params[0], + m.Trailing(), + }, + }) + } + }), + } + + // Create the client + client := irc.NewClient(conn, config) + err = client.Run() + if err != nil { + log.Fatalln(err) + } +} +``` + +## Major Version Changes + +### v1 + +Initial release + +### v2 + +- CTCP messages will no longer be rewritten. The decision was made that this + library should pass through all messages without mangling them. +- Remove Message.FromChannel as this is not always accurate, while + Client.FromChannel should always be accurate. diff --git a/vendor/gopkg.in/irc.v2/client.go b/vendor/gopkg.in/irc.v2/client.go new file mode 100644 index 0000000..999ebff --- /dev/null +++ b/vendor/gopkg.in/irc.v2/client.go @@ -0,0 +1,331 @@ +package irc + +import ( + "context" + "errors" + "fmt" + "io" + "sync" + "time" +) + +// ClientConfig is a structure used to configure a Client. +type ClientConfig struct { + // General connection information. + Nick string + Pass string + User string + Name string + + // Connection settings + PingFrequency time.Duration + PingTimeout time.Duration + + // SendLimit is how frequent messages can be sent. If this is zero, + // there will be no limit. + SendLimit time.Duration + + // SendBurst is the number of messages which can be sent in a burst. + SendBurst int + + // Handler is used for message dispatching. + Handler Handler +} + +type cap struct { + // Requested means that this cap was requested by the user + Requested bool + + // Required will be true if this cap is non-optional + Required bool + + // Enabled means that this cap was accepted by the server + Enabled bool + + // Available means that the server supports this cap + Available bool +} + +// Client is a wrapper around Conn which is designed to make common operations +// much simpler. +type Client struct { + *Conn + config ClientConfig + + // Internal state + currentNick string + limiter chan struct{} + incomingPongChan chan string + errChan chan error + caps map[string]cap + remainingCapResponses int + connected bool +} + +// NewClient creates a client given an io stream and a client config. +func NewClient(rw io.ReadWriter, config ClientConfig) *Client { + c := &Client{ + Conn: NewConn(rw), + config: config, + errChan: make(chan error, 1), + caps: make(map[string]cap), + } + + // Replace the writer writeCallback with one of our own + c.Conn.Writer.writeCallback = c.writeCallback + + return c +} + +func (c *Client) writeCallback(w *Writer, line string) error { + if c.limiter != nil { + <-c.limiter + } + + _, err := w.writer.Write([]byte(line + "\r\n")) + if err != nil { + c.sendError(err) + } + return err +} + +// maybeStartLimiter will start a ticker which will limit how quickly messages +// can be written to the connection if the SendLimit is set in the config. +func (c *Client) maybeStartLimiter(wg *sync.WaitGroup, exiting chan struct{}) { + if c.config.SendLimit == 0 { + return + } + + wg.Add(1) + + // If SendBurst is 0, this will be unbuffered, so keep that in mind. + c.limiter = make(chan struct{}, c.config.SendBurst) + limitTick := time.NewTicker(c.config.SendLimit) + + go func() { + defer wg.Done() + + var done bool + for !done { + select { + case <-limitTick.C: + select { + case c.limiter <- struct{}{}: + default: + } + case <-exiting: + done = true + } + } + + limitTick.Stop() + close(c.limiter) + c.limiter = nil + }() +} + +// maybeStartPingLoop will start a goroutine to send out PING messages at the +// PingFrequency in the config if the frequency is not 0. +func (c *Client) maybeStartPingLoop(wg *sync.WaitGroup, exiting chan struct{}) { + if c.config.PingFrequency <= 0 { + return + } + + wg.Add(1) + + c.incomingPongChan = make(chan string, 5) + + go func() { + defer wg.Done() + + pingHandlers := make(map[string]chan struct{}) + ticker := time.NewTicker(c.config.PingFrequency) + + defer ticker.Stop() + + for { + select { + case <-ticker.C: + // Each time we get a tick, we send off a ping and start a + // goroutine to handle the pong. + timestamp := time.Now().Unix() + pongChan := make(chan struct{}, 1) + pingHandlers[fmt.Sprintf("%d", timestamp)] = pongChan + wg.Add(1) + go c.handlePing(timestamp, pongChan, wg, exiting) + case data := <-c.incomingPongChan: + // Make sure the pong gets routed to the correct + // goroutine. + + c := pingHandlers[data] + delete(pingHandlers, data) + + if c != nil { + c <- struct{}{} + } + case <-exiting: + return + } + } + }() +} + +func (c *Client) handlePing(timestamp int64, pongChan chan struct{}, wg *sync.WaitGroup, exiting chan struct{}) { + defer wg.Done() + + c.Writef("PING :%d", timestamp) + + timer := time.NewTimer(c.config.PingTimeout) + defer timer.Stop() + + select { + case <-timer.C: + c.sendError(errors.New("Ping Timeout")) + case <-pongChan: + return + case <-exiting: + return + } +} + +// maybeStartCapHandshake will run a CAP LS and all the relevant CAP REQ +// commands if there are any CAPs requested. +func (c *Client) maybeStartCapHandshake() { + if len(c.caps) <= 0 { + return + } + + c.Write("CAP LS") + c.remainingCapResponses = 1 // We count the CAP LS response as a normal response + for key, cap := range c.caps { + if cap.Requested { + c.Writef("CAP REQ :%s", key) + c.remainingCapResponses++ + } + } +} + +// CapRequest allows you to request IRCv3 capabilities from the server during +// the handshake. The behavior is undefined if this is called before the +// handshake completes so it is recommended that this be called before Run. If +// the CAP is marked as required, the client will exit if that CAP could not be +// negotiated during the handshake. +func (c *Client) CapRequest(capName string, required bool) { + cap := c.caps[capName] + cap.Requested = true + cap.Required = cap.Required || required + c.caps[capName] = cap +} + +// CapEnabled allows you to check if a CAP is enabled for this connection. Note +// that it will not be populated until after the CAP handshake is done, so it is +// recommended to wait to check this until after a message like 001. +func (c *Client) CapEnabled(capName string) bool { + return c.caps[capName].Enabled +} + +// CapAvailable allows you to check if a CAP is available on this server. Note +// that it will not be populated until after the CAP handshake is done, so it is +// recommended to wait to check this until after a message like 001. +func (c *Client) CapAvailable(capName string) bool { + return c.caps[capName].Available +} + +func (c *Client) sendError(err error) { + select { + case c.errChan <- err: + default: + } +} + +func (c *Client) startReadLoop(wg *sync.WaitGroup) { + wg.Add(1) + + go func() { + defer wg.Done() + + for { + m, err := c.ReadMessage() + if err != nil { + c.sendError(err) + break + } + + if f, ok := clientFilters[m.Command]; ok { + f(c, m) + } + + if c.config.Handler != nil { + c.config.Handler.Handle(c, m) + } + } + + }() +} + +// Run starts the main loop for this IRC connection. Note that it may break in +// strange and unexpected ways if it is called again before the first connection +// exits. +func (c *Client) Run() error { + return c.RunContext(context.TODO()) +} + +// RunContext is the same as Run but a context.Context can be passed in for +// cancelation. +func (c *Client) RunContext(ctx context.Context) error { + // exiting is used by the main goroutine here to ensure any sub-goroutines + // get closed when exiting. + exiting := make(chan struct{}) + var wg sync.WaitGroup + + c.maybeStartLimiter(&wg, exiting) + c.maybeStartPingLoop(&wg, exiting) + + c.currentNick = c.config.Nick + + if c.config.Pass != "" { + c.Writef("PASS :%s", c.config.Pass) + } + + c.maybeStartCapHandshake() + + // This feels wrong because it results in CAP LS, CAP REQ, NICK, USER, CAP + // END, but it works and lets us keep the code a bit simpler. + c.Writef("NICK :%s", c.config.Nick) + c.Writef("USER %s 0.0.0.0 0.0.0.0 :%s", c.config.User, c.config.Name) + + // Now that the handshake is pretty much done, we can start listening for + // messages. + c.startReadLoop(&wg) + + // Wait for an error from any goroutine or for the context to time out, then + // signal we're exiting and wait for the goroutines to exit. + var err error + select { + case err = <-c.errChan: + case <-ctx.Done(): + } + + close(exiting) + wg.Wait() + + return err +} + +// CurrentNick returns what the nick of the client is known to be at this point +// in time. +func (c *Client) CurrentNick() string { + return c.currentNick +} + +// FromChannel takes a Message representing a PRIVMSG and returns if that +// message came from a channel or directly from a user. +func (c *Client) FromChannel(m *Message) bool { + if len(m.Params) < 1 { + return false + } + + // The first param is the target, so if this doesn't match the current nick, + // the message came from a channel. + return m.Params[0] != c.currentNick +} diff --git a/vendor/gopkg.in/irc.v2/client_handlers.go b/vendor/gopkg.in/irc.v2/client_handlers.go new file mode 100644 index 0000000..3b5cd8d --- /dev/null +++ b/vendor/gopkg.in/irc.v2/client_handlers.go @@ -0,0 +1,151 @@ +package irc + +import ( + "fmt" + "strings" +) + +type clientFilter func(*Client, *Message) + +// clientFilters are pre-processing which happens for certain message +// types. These were moved from below to keep the complexity of each +// component down. +var clientFilters = map[string]clientFilter{ + "001": handle001, + "433": handle433, + "437": handle437, + "PING": handlePing, + "PONG": handlePong, + "NICK": handleNick, + "CAP": handleCap, +} + +// From rfc2812 section 5.1 (Command responses) +// +// 001 RPL_WELCOME +// "Welcome to the Internet Relay Network +// !@" +func handle001(c *Client, m *Message) { + c.currentNick = m.Params[0] + c.connected = true +} + +// From rfc2812 section 5.2 (Error Replies) +// +// 433 ERR_NICKNAMEINUSE +// " :Nickname is already in use" +// +// - Returned when a NICK message is processed that results +// in an attempt to change to a currently existing +// nickname. +func handle433(c *Client, m *Message) { + // We only want to try and handle nick collisions during the initial + // handshake. + if c.connected { + return + } + c.currentNick = c.currentNick + "_" + c.Writef("NICK :%s", c.currentNick) +} + +// From rfc2812 section 5.2 (Error Replies) +// +// 437 ERR_UNAVAILRESOURCE +// " :Nick/channel is temporarily unavailable" +// +// - Returned by a server to a user trying to join a channel +// currently blocked by the channel delay mechanism. +// +// - Returned by a server to a user trying to change nickname +// when the desired nickname is blocked by the nick delay +// mechanism. +func handle437(c *Client, m *Message) { + // We only want to try and handle nick collisions during the initial + // handshake. + if c.connected { + return + } + c.currentNick = c.currentNick + "_" + c.Writef("NICK :%s", c.currentNick) +} + +func handlePing(c *Client, m *Message) { + reply := m.Copy() + reply.Command = "PONG" + c.WriteMessage(reply) +} + +func handlePong(c *Client, m *Message) { + if c.incomingPongChan != nil { + select { + case c.incomingPongChan <- m.Trailing(): + default: + // Note that this return isn't really needed, but it helps some code + // coverage tools actually see this line. + return + } + } +} + +func handleNick(c *Client, m *Message) { + if m.Prefix.Name == c.currentNick && len(m.Params) > 0 { + c.currentNick = m.Params[0] + } +} + +var capFilters = map[string]clientFilter{ + "LS": handleCapLs, + "ACK": handleCapAck, + "NAK": handleCapNak, +} + +func handleCap(c *Client, m *Message) { + if c.remainingCapResponses <= 0 || len(m.Params) <= 2 { + return + } + + if filter, ok := capFilters[m.Params[1]]; ok { + filter(c, m) + } + + if c.remainingCapResponses <= 0 { + for key, cap := range c.caps { + if cap.Required && !cap.Enabled { + c.sendError(fmt.Errorf("CAP %s requested but not accepted", key)) + return + } + } + + c.Write("CAP END") + } +} + +func handleCapLs(c *Client, m *Message) { + for _, key := range strings.Split(m.Trailing(), " ") { + cap := c.caps[key] + cap.Available = true + c.caps[key] = cap + } + c.remainingCapResponses-- +} + +func handleCapAck(c *Client, m *Message) { + for _, key := range strings.Split(m.Trailing(), " ") { + cap := c.caps[key] + cap.Enabled = true + c.caps[key] = cap + } + c.remainingCapResponses-- +} + +func handleCapNak(c *Client, m *Message) { + // If we got a NAK and this REQ was required, we need to bail + // with an error. + for _, key := range strings.Split(m.Trailing(), " ") { + if c.caps[key].Required { + c.sendError(fmt.Errorf("CAP %s requested but was rejected", key)) + return + } + } + c.remainingCapResponses-- +} diff --git a/vendor/gopkg.in/irc.v2/conn.go b/vendor/gopkg.in/irc.v2/conn.go new file mode 100644 index 0000000..7f9b68e --- /dev/null +++ b/vendor/gopkg.in/irc.v2/conn.go @@ -0,0 +1,103 @@ +package irc + +import ( + "bufio" + "fmt" + "io" +) + +// Conn represents a simple IRC client. It embeds an irc.Reader and an +// irc.Writer. +type Conn struct { + *Reader + *Writer +} + +// NewConn creates a new Conn +func NewConn(rw io.ReadWriter) *Conn { + return &Conn{ + NewReader(rw), + NewWriter(rw), + } +} + +// Writer is the outgoing side of a connection. +type Writer struct { + // DebugCallback is called for each outgoing message. The name of this may + // not be stable. + DebugCallback func(line string) + + // Internal fields + writer io.Writer + writeCallback func(w *Writer, line string) error +} + +func defaultWriteCallback(w *Writer, line string) error { + _, err := w.writer.Write([]byte(line + "\r\n")) + return err +} + +// NewWriter creates an irc.Writer from an io.Writer. +func NewWriter(w io.Writer) *Writer { + return &Writer{nil, w, defaultWriteCallback} +} + +// Write is a simple function which will write the given line to the +// underlying connection. +func (w *Writer) Write(line string) error { + if w.DebugCallback != nil { + w.DebugCallback(line) + } + + return w.writeCallback(w, line) +} + +// Writef is a wrapper around the connection's Write method and +// fmt.Sprintf. Simply use it to send a message as you would normally +// use fmt.Printf. +func (w *Writer) Writef(format string, args ...interface{}) error { + return w.Write(fmt.Sprintf(format, args...)) +} + +// WriteMessage writes the given message to the stream +func (w *Writer) WriteMessage(m *Message) error { + return w.Write(m.String()) +} + +// Reader is the incoming side of a connection. The data will be +// buffered, so do not re-use the io.Reader used to create the +// irc.Reader. +type Reader struct { + // DebugCallback is called for each incoming message. The name of this may + // not be stable. + DebugCallback func(string) + + // Internal fields + reader *bufio.Reader +} + +// NewReader creates an irc.Reader from an io.Reader. Note that once a reader is +// passed into this function, you should no longer use it as it is being used +// inside a bufio.Reader so you cannot rely on only the amount of data for a +// Message being read when you call ReadMessage. +func NewReader(r io.Reader) *Reader { + return &Reader{ + nil, + bufio.NewReader(r), + } +} + +// ReadMessage returns the next message from the stream or an error. +func (r *Reader) ReadMessage() (*Message, error) { + line, err := r.reader.ReadString('\n') + if err != nil { + return nil, err + } + + if r.DebugCallback != nil { + r.DebugCallback(line) + } + + // Parse the message from our line + return ParseMessage(line) +} diff --git a/vendor/gopkg.in/irc.v2/go.mod b/vendor/gopkg.in/irc.v2/go.mod new file mode 100644 index 0000000..b80d8be --- /dev/null +++ b/vendor/gopkg.in/irc.v2/go.mod @@ -0,0 +1 @@ +module "github.com/go-irc/irc/v2" diff --git a/vendor/gopkg.in/irc.v2/handler.go b/vendor/gopkg.in/irc.v2/handler.go new file mode 100644 index 0000000..6a9fca7 --- /dev/null +++ b/vendor/gopkg.in/irc.v2/handler.go @@ -0,0 +1,16 @@ +package irc + +// Handler is a simple interface meant for dispatching a message from +// a Client connection. +type Handler interface { + Handle(*Client, *Message) +} + +// HandlerFunc is a simple wrapper around a function which allows it +// to be used as a Handler. +type HandlerFunc func(*Client, *Message) + +// Handle calls f(c, m) +func (f HandlerFunc) Handle(c *Client, m *Message) { + f(c, m) +} diff --git a/vendor/gopkg.in/irc.v2/parser.go b/vendor/gopkg.in/irc.v2/parser.go new file mode 100644 index 0000000..819de4d --- /dev/null +++ b/vendor/gopkg.in/irc.v2/parser.go @@ -0,0 +1,394 @@ +package irc + +import ( + "bytes" + "errors" + "strings" +) + +var tagDecodeSlashMap = map[rune]rune{ + ':': ';', + 's': ' ', + '\\': '\\', + 'r': '\r', + 'n': '\n', +} + +var tagEncodeMap = map[rune]string{ + ';': "\\:", + ' ': "\\s", + '\\': "\\\\", + '\r': "\\r", + '\n': "\\n", +} + +var ( + // ErrZeroLengthMessage is returned when parsing if the input is + // zero-length. + ErrZeroLengthMessage = errors.New("irc: Cannot parse zero-length message") + + // ErrMissingDataAfterPrefix is returned when parsing if there is + // no message data after the prefix. + ErrMissingDataAfterPrefix = errors.New("irc: No message data after prefix") + + // ErrMissingDataAfterTags is returned when parsing if there is no + // message data after the tags. + ErrMissingDataAfterTags = errors.New("irc: No message data after tags") + + // ErrMissingCommand is returned when parsing if there is no + // command in the parsed message. + ErrMissingCommand = errors.New("irc: Missing message command") +) + +// TagValue represents the value of a tag. +type TagValue string + +// ParseTagValue parses a TagValue from the connection. If you need to +// set a TagValue, you probably want to just set the string itself, so +// it will be encoded properly. +func ParseTagValue(v string) TagValue { + ret := &bytes.Buffer{} + + input := bytes.NewBufferString(v) + + for { + c, _, err := input.ReadRune() + if err != nil { + break + } + + if c == '\\' { + c2, _, err := input.ReadRune() + + // If we got a backslash then the end of the tag value, we should + // just ignore the backslash. + if err != nil { + break + } + + if replacement, ok := tagDecodeSlashMap[c2]; ok { + ret.WriteRune(replacement) + } else { + ret.WriteRune(c2) + } + } else { + ret.WriteRune(c) + } + } + + return TagValue(ret.String()) +} + +// Encode converts a TagValue to the format in the connection. +func (v TagValue) Encode() string { + ret := &bytes.Buffer{} + + for _, c := range v { + if replacement, ok := tagEncodeMap[c]; ok { + ret.WriteString(replacement) + } else { + ret.WriteRune(c) + } + } + + return ret.String() +} + +// Tags represents the IRCv3 message tags. +type Tags map[string]TagValue + +// ParseTags takes a tag string and parses it into a tag map. It will +// always return a tag map, even if there are no valid tags. +func ParseTags(line string) Tags { + ret := Tags{} + + tags := strings.Split(line, ";") + for _, tag := range tags { + parts := strings.SplitN(tag, "=", 2) + if len(parts) < 2 { + ret[parts[0]] = "" + continue + } + + ret[parts[0]] = ParseTagValue(parts[1]) + } + + return ret +} + +// GetTag is a convenience method to look up a tag in the map. +func (t Tags) GetTag(key string) (string, bool) { + ret, ok := t[key] + return string(ret), ok +} + +// Copy will create a new copy of all IRC tags attached to this +// message. +func (t Tags) Copy() Tags { + ret := Tags{} + + for k, v := range t { + ret[k] = v + } + + return ret +} + +// String ensures this is stringable +func (t Tags) String() string { + buf := &bytes.Buffer{} + + for k, v := range t { + buf.WriteByte(';') + buf.WriteString(k) + if v != "" { + buf.WriteByte('=') + buf.WriteString(v.Encode()) + } + } + + // We don't need the first byte because that's an extra ';' + // character. + buf.ReadByte() + + return buf.String() +} + +// Prefix represents the prefix of a message, generally the user who sent it +type Prefix struct { + // Name will contain the nick of who sent the message, the + // server who sent the message, or a blank string + Name string + + // User will either contain the user who sent the message or a blank string + User string + + // Host will either contain the host of who sent the message or a blank string + Host string +} + +// ParsePrefix takes an identity string and parses it into an +// identity struct. It will always return an Prefix struct and never +// nil. +func ParsePrefix(line string) *Prefix { + // Start by creating an Prefix with nothing but the host + id := &Prefix{ + Name: line, + } + + uh := strings.SplitN(id.Name, "@", 2) + if len(uh) == 2 { + id.Name, id.Host = uh[0], uh[1] + } + + nu := strings.SplitN(id.Name, "!", 2) + if len(nu) == 2 { + id.Name, id.User = nu[0], nu[1] + } + + return id +} + +// Copy will create a new copy of an Prefix +func (p *Prefix) Copy() *Prefix { + if p == nil { + return nil + } + + newPrefix := &Prefix{} + + *newPrefix = *p + + return newPrefix +} + +// String ensures this is stringable +func (p *Prefix) String() string { + buf := &bytes.Buffer{} + buf.WriteString(p.Name) + + if p.User != "" { + buf.WriteString("!") + buf.WriteString(p.User) + } + + if p.Host != "" { + buf.WriteString("@") + buf.WriteString(p.Host) + } + + return buf.String() +} + +// Message represents a line parsed from the server +type Message struct { + // Each message can have IRCv3 tags + Tags + + // Each message can have a Prefix + *Prefix + + // Command is which command is being called. + Command string + + // Params are all the arguments for the command. + Params []string +} + +// MustParseMessage calls ParseMessage and either returns the message +// or panics if an error is returned. +func MustParseMessage(line string) *Message { + m, err := ParseMessage(line) + if err != nil { + panic(err.Error()) + } + return m +} + +// ParseMessage takes a message string (usually a whole line) and +// parses it into a Message struct. This will return nil in the case +// of invalid messages. +func ParseMessage(line string) (*Message, error) { + // Trim the line and make sure we have data + line = strings.TrimRight(line, "\r\n") + if len(line) == 0 { + return nil, ErrZeroLengthMessage + } + + c := &Message{ + Tags: Tags{}, + Prefix: &Prefix{}, + } + + if line[0] == '@' { + split := strings.SplitN(line, " ", 2) + if len(split) < 2 { + return nil, ErrMissingDataAfterTags + } + + c.Tags = ParseTags(split[0][1:]) + line = split[1] + } + + if line[0] == ':' { + split := strings.SplitN(line, " ", 2) + if len(split) < 2 { + return nil, ErrMissingDataAfterPrefix + } + + // Parse the identity, if there was one + c.Prefix = ParsePrefix(split[0][1:]) + line = split[1] + } + + // Split out the trailing then the rest of the args. Because + // we expect there to be at least one result as an arg (the + // command) we don't need to special case the trailing arg and + // can just attempt a split on " :" + split := strings.SplitN(line, " :", 2) + c.Params = strings.FieldsFunc(split[0], func(r rune) bool { + return r == ' ' + }) + + // If there are no args, we need to bail because we need at + // least the command. + if len(c.Params) == 0 { + return nil, ErrMissingCommand + } + + // If we had a trailing arg, append it to the other args + if len(split) == 2 { + c.Params = append(c.Params, split[1]) + } + + // Because of how it's parsed, the Command will show up as the + // first arg. + c.Command = strings.ToUpper(c.Params[0]) + c.Params = c.Params[1:] + + // If there are no params, set it to nil, to make writing tests and other + // things simpler. + if len(c.Params) == 0 { + c.Params = nil + } + + return c, nil +} + +// Trailing returns the last argument in the Message or an empty string +// if there are no args +func (m *Message) Trailing() string { + if len(m.Params) < 1 { + return "" + } + + return m.Params[len(m.Params)-1] +} + +// Copy will create a new copy of an message +func (m *Message) Copy() *Message { + // Create a new message + newMessage := &Message{} + + // Copy stuff from the old message + *newMessage = *m + + // Copy any IRcv3 tags + newMessage.Tags = m.Tags.Copy() + + // Copy the Prefix + newMessage.Prefix = m.Prefix.Copy() + + // Copy the Params slice + newMessage.Params = append(make([]string, 0, len(m.Params)), m.Params...) + + // Similar to parsing, if Params is empty, set it to nil + if len(newMessage.Params) == 0 { + newMessage.Params = nil + } + + return newMessage +} + +// String ensures this is stringable +func (m *Message) String() string { + buf := &bytes.Buffer{} + + // Write any IRCv3 tags if they exist in the message + if len(m.Tags) > 0 { + buf.WriteByte('@') + buf.WriteString(m.Tags.String()) + buf.WriteByte(' ') + } + + // Add the prefix if we have one + if m.Prefix != nil && m.Prefix.Name != "" { + buf.WriteByte(':') + buf.WriteString(m.Prefix.String()) + buf.WriteByte(' ') + } + + // Add the command since we know we'll always have one + buf.WriteString(m.Command) + + if len(m.Params) > 0 { + args := m.Params[:len(m.Params)-1] + trailing := m.Params[len(m.Params)-1] + + if len(args) > 0 { + buf.WriteByte(' ') + buf.WriteString(strings.Join(args, " ")) + } + + // If trailing is zero-length, contains a space or starts with + // a : we need to actually specify that it's trailing. + if len(trailing) == 0 || strings.ContainsRune(trailing, ' ') || trailing[0] == ':' { + buf.WriteString(" :") + } else { + buf.WriteString(" ") + } + buf.WriteString(trailing) + } + + return buf.String() +} diff --git a/vendor/gopkg.in/irc.v2/utils.go b/vendor/gopkg.in/irc.v2/utils.go new file mode 100644 index 0000000..4633314 --- /dev/null +++ b/vendor/gopkg.in/irc.v2/utils.go @@ -0,0 +1,50 @@ +package irc + +import ( + "bytes" + "regexp" +) + +var maskTranslations = map[byte]string{ + '?': ".", + '*': ".*", +} + +// MaskToRegex converts an irc mask to a go Regexp for more convenient +// use. This should never return an error, but we have this here just +// in case. +func MaskToRegex(rawMask string) (*regexp.Regexp, error) { + input := bytes.NewBufferString(rawMask) + + output := &bytes.Buffer{} + output.WriteByte('^') + + for { + c, err := input.ReadByte() + if err != nil { + break + } + + if c == '\\' { + c, err = input.ReadByte() + if err != nil { + output.WriteString(regexp.QuoteMeta("\\")) + break + } + + if c == '?' || c == '*' || c == '\\' { + output.WriteString(regexp.QuoteMeta(string(c))) + } else { + output.WriteString(regexp.QuoteMeta("\\" + string(c))) + } + } else if trans, ok := maskTranslations[c]; ok { + output.WriteString(trans) + } else { + output.WriteString(regexp.QuoteMeta(string(c))) + } + } + + output.WriteByte('$') + + return regexp.Compile(output.String()) +}