start irc module
This commit is contained in:
parent
f29f982f7f
commit
f224c77bac
9
Gopkg.lock
generated
9
Gopkg.lock
generated
@ -188,6 +188,14 @@
|
|||||||
revision = "5f1438d3fca68893a817e4a66806cea46a9e4ebf"
|
revision = "5f1438d3fca68893a817e4a66806cea46a9e4ebf"
|
||||||
version = "v8.18.2"
|
version = "v8.18.2"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:54c3b474a0e4a8e65c74e0dbb272bcf30a4bab48883d34f958cbb9d8e8ef387a"
|
||||||
|
name = "gopkg.in/irc.v2"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = "UT"
|
||||||
|
revision = "4901bf6be124ba1558d3657e91286393c97fb47f"
|
||||||
|
version = "v2.1.2"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202"
|
digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202"
|
||||||
name = "gopkg.in/yaml.v2"
|
name = "gopkg.in/yaml.v2"
|
||||||
@ -209,6 +217,7 @@
|
|||||||
"github.com/lib/pq",
|
"github.com/lib/pq",
|
||||||
"github.com/robfig/cron",
|
"github.com/robfig/cron",
|
||||||
"golang.org/x/crypto/bcrypt",
|
"golang.org/x/crypto/bcrypt",
|
||||||
|
"gopkg.in/irc.v2",
|
||||||
"gopkg.in/yaml.v2",
|
"gopkg.in/yaml.v2",
|
||||||
]
|
]
|
||||||
solver-name = "gps-cdcl"
|
solver-name = "gps-cdcl"
|
||||||
|
13
model/line_message_log.go
Normal file
13
model/line_message_log.go
Normal file
@ -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"`
|
||||||
|
}
|
11
model/line_user.go
Normal file
11
model/line_user.go
Normal file
@ -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"`
|
||||||
|
}
|
13
model/youtube_channel.go
Normal file
13
model/youtube_channel.go
Normal file
@ -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"`
|
||||||
|
}
|
@ -11,5 +11,6 @@ func SetBackground() {
|
|||||||
c = cron.New()
|
c = cron.New()
|
||||||
c.AddFunc("0 * * * * *", readFacebookPage)
|
c.AddFunc("0 * * * * *", readFacebookPage)
|
||||||
c.AddFunc("*/20 * * * * *", getStreamStatus)
|
c.AddFunc("*/20 * * * * *", getStreamStatus)
|
||||||
|
c.AddFunc("*/5 * * * * *", checkOpay)
|
||||||
c.Start()
|
c.Start()
|
||||||
}
|
}
|
||||||
|
21
module/background/opay.go
Normal file
21
module/background/opay.go
Normal file
@ -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) {
|
||||||
|
|
||||||
|
}
|
33
module/twitch-irc/twitch-irc.go
Normal file
33
module/twitch-irc/twitch-irc.go
Normal file
@ -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())
|
||||||
|
}
|
3
vendor/gopkg.in/irc.v2/.gitignore
generated
vendored
Normal file
3
vendor/gopkg.in/irc.v2/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
*.cover
|
||||||
|
*.test
|
||||||
|
*.out
|
3
vendor/gopkg.in/irc.v2/.gitmodules
generated
vendored
Normal file
3
vendor/gopkg.in/irc.v2/.gitmodules
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[submodule "testcases"]
|
||||||
|
path = testcases
|
||||||
|
url = https://github.com/go-irc/irc-parser-tests/
|
21
vendor/gopkg.in/irc.v2/.travis.yml
generated
vendored
Normal file
21
vendor/gopkg.in/irc.v2/.travis.yml
generated
vendored
Normal file
@ -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
|
18
vendor/gopkg.in/irc.v2/LICENSE
generated
vendored
Normal file
18
vendor/gopkg.in/irc.v2/LICENSE
generated
vendored
Normal file
@ -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.
|
104
vendor/gopkg.in/irc.v2/README.md
generated
vendored
Normal file
104
vendor/gopkg.in/irc.v2/README.md
generated
vendored
Normal file
@ -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.
|
331
vendor/gopkg.in/irc.v2/client.go
generated
vendored
Normal file
331
vendor/gopkg.in/irc.v2/client.go
generated
vendored
Normal file
@ -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
|
||||||
|
}
|
151
vendor/gopkg.in/irc.v2/client_handlers.go
generated
vendored
Normal file
151
vendor/gopkg.in/irc.v2/client_handlers.go
generated
vendored
Normal file
@ -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
|
||||||
|
// <nick>!<user>@<host>"
|
||||||
|
func handle001(c *Client, m *Message) {
|
||||||
|
c.currentNick = m.Params[0]
|
||||||
|
c.connected = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// From rfc2812 section 5.2 (Error Replies)
|
||||||
|
//
|
||||||
|
// 433 ERR_NICKNAMEINUSE
|
||||||
|
// "<nick> :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> :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--
|
||||||
|
}
|
103
vendor/gopkg.in/irc.v2/conn.go
generated
vendored
Normal file
103
vendor/gopkg.in/irc.v2/conn.go
generated
vendored
Normal file
@ -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)
|
||||||
|
}
|
1
vendor/gopkg.in/irc.v2/go.mod
generated
vendored
Normal file
1
vendor/gopkg.in/irc.v2/go.mod
generated
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
module "github.com/go-irc/irc/v2"
|
16
vendor/gopkg.in/irc.v2/handler.go
generated
vendored
Normal file
16
vendor/gopkg.in/irc.v2/handler.go
generated
vendored
Normal file
@ -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)
|
||||||
|
}
|
394
vendor/gopkg.in/irc.v2/parser.go
generated
vendored
Normal file
394
vendor/gopkg.in/irc.v2/parser.go
generated
vendored
Normal file
@ -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()
|
||||||
|
}
|
50
vendor/gopkg.in/irc.v2/utils.go
generated
vendored
Normal file
50
vendor/gopkg.in/irc.v2/utils.go
generated
vendored
Normal file
@ -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())
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user