start irc module

This commit is contained in:
Jay 2018-09-12 18:07:49 +08:00
parent f29f982f7f
commit f224c77bac
19 changed files with 1296 additions and 0 deletions

9
Gopkg.lock generated
View File

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

13
model/line_message_log.go Normal file
View 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
View 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
View 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"`
}

View File

@ -11,5 +11,6 @@ func SetBackground() {
c = cron.New()
c.AddFunc("0 * * * * *", readFacebookPage)
c.AddFunc("*/20 * * * * *", getStreamStatus)
c.AddFunc("*/5 * * * * *", checkOpay)
c.Start()
}

21
module/background/opay.go Normal file
View 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) {
}

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

@ -0,0 +1,3 @@
*.cover
*.test
*.out

3
vendor/gopkg.in/irc.v2/.gitmodules generated vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
module "github.com/go-irc/irc/v2"

16
vendor/gopkg.in/irc.v2/handler.go generated vendored Normal file
View 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
View 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
View 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())
}