mirror of
https://github.com/cwinfo/matterbridge.git
synced 2025-06-26 21:19:22 +00:00
Compare commits
43 Commits
Author | SHA1 | Date | |
---|---|---|---|
962062fe44 | |||
0578b21270 | |||
36a800c3f5 | |||
6d21f84187 | |||
f1e9833310 | |||
46f5acc4f9 | |||
95d4dcaeb3 | |||
64c542e614 | |||
13d081ea80 | |||
c0f9d86287 | |||
bcdecdaa73 | |||
daac3ebca2 | |||
639f9cf966 | |||
4fc48b5aa4 | |||
307ff77b42 | |||
9b500bc5f7 | |||
e313154134 | |||
27e94c438d | |||
58392876df | |||
115c4b1aa7 | |||
ba5649d259 | |||
1b30575510 | |||
7dbebd3ea7 | |||
6f18790352 | |||
d1e04a2ece | |||
bea0bbd0c2 | |||
0530503ef2 | |||
d1e8ff814b | |||
4f8ae761a2 | |||
b530e92834 | |||
b2a6777995 | |||
b461fc5e40 | |||
b7a8c6b60f | |||
41aa8ad799 | |||
7973baedd0 | |||
299b71d982 | |||
76aafe1fa8 | |||
95a0229aaf | |||
915a8fbad7 | |||
d4d7fef313 | |||
4e1dc9f885 | |||
155ae80d22 | |||
c7e336efd9 |
@ -37,7 +37,7 @@ Has a REST API.
|
|||||||
|
|
||||||
# Requirements
|
# Requirements
|
||||||
Accounts to one of the supported bridges
|
Accounts to one of the supported bridges
|
||||||
* [Mattermost](https://github.com/mattermost/platform/) 3.8.x - 3.10.x, 4.0.x - 4.2.x
|
* [Mattermost](https://github.com/mattermost/platform/) 3.8.x - 3.10.x, 4.x
|
||||||
* [IRC](http://www.mirc.com/servers.html)
|
* [IRC](http://www.mirc.com/servers.html)
|
||||||
* [XMPP](https://jabber.org)
|
* [XMPP](https://jabber.org)
|
||||||
* [Gitter](https://gitter.im)
|
* [Gitter](https://gitter.im)
|
||||||
@ -54,11 +54,11 @@ See https://github.com/42wim/matterbridge/wiki
|
|||||||
|
|
||||||
# Installing
|
# Installing
|
||||||
## Binaries
|
## Binaries
|
||||||
* Latest stable release [v1.3.0](https://github.com/42wim/matterbridge/releases/latest)
|
* Latest stable release [v1.4.1](https://github.com/42wim/matterbridge/releases/latest)
|
||||||
* Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/)
|
* Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/)
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
Go 1.7+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed, including setting up your [GOPATH] (https://golang.org/doc/code.html#GOPATH)
|
Go 1.8+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed, including setting up your [GOPATH] (https://golang.org/doc/code.html#GOPATH)
|
||||||
|
|
||||||
```
|
```
|
||||||
cd $GOPATH
|
cd $GOPATH
|
||||||
|
@ -33,8 +33,9 @@ type Message struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
Name string
|
Name string
|
||||||
Data *[]byte
|
Data *[]byte
|
||||||
|
Comment string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChannelInfo struct {
|
type ChannelInfo struct {
|
||||||
@ -80,6 +81,7 @@ type Protocol struct {
|
|||||||
ShowJoinPart bool // all protocols
|
ShowJoinPart bool // all protocols
|
||||||
ShowEmbeds bool // discord
|
ShowEmbeds bool // discord
|
||||||
SkipTLSVerify bool // IRC, mattermost
|
SkipTLSVerify bool // IRC, mattermost
|
||||||
|
StripNick bool // all protocols
|
||||||
Team string // mattermost
|
Team string // mattermost
|
||||||
Token string // gitter, slack, discord, api
|
Token string // gitter, slack, discord, api
|
||||||
URL string // mattermost, slack // DEPRECATED
|
URL string // mattermost, slack // DEPRECATED
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package bdiscord
|
package bdiscord
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"github.com/42wim/matterbridge/bridge/config"
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
log "github.com/Sirupsen/logrus"
|
log "github.com/Sirupsen/logrus"
|
||||||
"github.com/bwmarrin/discordgo"
|
"github.com/bwmarrin/discordgo"
|
||||||
@ -141,6 +142,24 @@ func (b *bdiscord) Send(msg config.Message) (string, error) {
|
|||||||
_, err := b.c.ChannelMessageEdit(channelID, msg.ID, msg.Username+msg.Text)
|
_, err := b.c.ChannelMessageEdit(channelID, msg.ID, msg.Username+msg.Text)
|
||||||
return msg.ID, err
|
return msg.ID, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if msg.Extra != nil {
|
||||||
|
// check if we have files to upload (from slack, telegram or mattermost)
|
||||||
|
if len(msg.Extra["file"]) > 0 {
|
||||||
|
var err error
|
||||||
|
for _, f := range msg.Extra["file"] {
|
||||||
|
fi := f.(config.FileInfo)
|
||||||
|
files := []*discordgo.File{}
|
||||||
|
files = append(files, &discordgo.File{fi.Name, "", bytes.NewReader(*fi.Data)})
|
||||||
|
_, err = b.c.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{Content: msg.Username + fi.Comment, Files: files})
|
||||||
|
if err != nil {
|
||||||
|
flog.Errorf("file upload failed: %#v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res, err := b.c.ChannelMessageSend(channelID, msg.Username+msg.Text)
|
res, err := b.c.ChannelMessageSend(channelID, msg.Username+msg.Text)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -95,7 +95,7 @@ func (b *Bgitter) JoinChannel(channel config.ChannelInfo) error {
|
|||||||
flog.Errorf("connection with gitter closed for room %s", room)
|
flog.Errorf("connection with gitter closed for room %s", room)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}(stream, room.Name)
|
}(stream, room.URI)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
28
bridge/helper/helper.go
Normal file
28
bridge/helper/helper.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DownloadFile(url string) (*[]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: time.Second * 5,
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
io.Copy(&buf, resp.Body)
|
||||||
|
data := buf.Bytes()
|
||||||
|
resp.Body.Close()
|
||||||
|
return &data, nil
|
||||||
|
}
|
@ -4,15 +4,15 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/42wim/go-ircevent"
|
|
||||||
"github.com/42wim/matterbridge/bridge/config"
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
log "github.com/Sirupsen/logrus"
|
log "github.com/Sirupsen/logrus"
|
||||||
|
"github.com/lrstanley/girc"
|
||||||
"github.com/paulrosania/go-charset/charset"
|
"github.com/paulrosania/go-charset/charset"
|
||||||
_ "github.com/paulrosania/go-charset/data"
|
_ "github.com/paulrosania/go-charset/data"
|
||||||
"github.com/saintfish/chardet"
|
"github.com/saintfish/chardet"
|
||||||
ircm "github.com/sorcix/irc"
|
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -21,7 +21,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Birc struct {
|
type Birc struct {
|
||||||
i *irc.Connection
|
i *girc.Client
|
||||||
Nick string
|
Nick string
|
||||||
names map[string][]string
|
names map[string][]string
|
||||||
Config *config.Protocol
|
Config *config.Protocol
|
||||||
@ -63,9 +63,9 @@ func New(cfg config.Protocol, account string, c chan config.Message) *Birc {
|
|||||||
func (b *Birc) Command(msg *config.Message) string {
|
func (b *Birc) Command(msg *config.Message) string {
|
||||||
switch msg.Text {
|
switch msg.Text {
|
||||||
case "!users":
|
case "!users":
|
||||||
b.i.AddCallback(ircm.RPL_NAMREPLY, b.storeNames)
|
b.i.Handlers.Add(girc.RPL_NAMREPLY, b.storeNames)
|
||||||
b.i.AddCallback(ircm.RPL_ENDOFNAMES, b.endNames)
|
b.i.Handlers.Add(girc.RPL_ENDOFNAMES, b.endNames)
|
||||||
b.i.SendRaw("NAMES " + msg.Channel)
|
b.i.Cmd.SendRaw("NAMES " + msg.Channel)
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@ -73,26 +73,60 @@ func (b *Birc) Command(msg *config.Message) string {
|
|||||||
func (b *Birc) Connect() error {
|
func (b *Birc) Connect() error {
|
||||||
b.Local = make(chan config.Message, b.Config.MessageQueue+10)
|
b.Local = make(chan config.Message, b.Config.MessageQueue+10)
|
||||||
flog.Infof("Connecting %s", b.Config.Server)
|
flog.Infof("Connecting %s", b.Config.Server)
|
||||||
i := irc.IRC(b.Config.Nick, b.Config.Nick)
|
server, portstr, err := net.SplitHostPort(b.Config.Server)
|
||||||
if log.GetLevel() == log.DebugLevel {
|
|
||||||
i.Debug = true
|
|
||||||
}
|
|
||||||
i.UseTLS = b.Config.UseTLS
|
|
||||||
i.UseSASL = b.Config.UseSASL
|
|
||||||
i.SASLLogin = b.Config.NickServNick
|
|
||||||
i.SASLPassword = b.Config.NickServPassword
|
|
||||||
i.TLSConfig = &tls.Config{InsecureSkipVerify: b.Config.SkipTLSVerify}
|
|
||||||
i.KeepAlive = time.Minute
|
|
||||||
i.PingFreq = time.Minute
|
|
||||||
if b.Config.Password != "" {
|
|
||||||
i.Password = b.Config.Password
|
|
||||||
}
|
|
||||||
i.AddCallback(ircm.RPL_WELCOME, b.handleNewConnection)
|
|
||||||
i.AddCallback(ircm.RPL_ENDOFMOTD, b.handleOtherAuth)
|
|
||||||
err := i.Connect(b.Config.Server)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
port, err := strconv.Atoi(portstr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// fix strict user handling of girc
|
||||||
|
user := b.Config.Nick
|
||||||
|
for !girc.IsValidUser(user) {
|
||||||
|
if len(user) == 1 {
|
||||||
|
user = "matterbridge"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
user = user[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
i := girc.New(girc.Config{
|
||||||
|
Server: server,
|
||||||
|
ServerPass: b.Config.Password,
|
||||||
|
Port: port,
|
||||||
|
Nick: b.Config.Nick,
|
||||||
|
User: user,
|
||||||
|
Name: b.Config.Nick,
|
||||||
|
SSL: b.Config.UseTLS,
|
||||||
|
TLSConfig: &tls.Config{InsecureSkipVerify: b.Config.SkipTLSVerify, ServerName: server},
|
||||||
|
PingDelay: time.Minute,
|
||||||
|
})
|
||||||
|
|
||||||
|
if b.Config.UseSASL {
|
||||||
|
i.Config.SASL = &girc.SASLPlain{b.Config.NickServNick, b.Config.NickServPassword}
|
||||||
|
}
|
||||||
|
|
||||||
|
i.Handlers.Add(girc.RPL_WELCOME, b.handleNewConnection)
|
||||||
|
i.Handlers.Add(girc.RPL_ENDOFMOTD, b.handleOtherAuth)
|
||||||
|
i.Handlers.Add("*", b.handleOther)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
if err := i.Connect(); err != nil {
|
||||||
|
flog.Errorf("error: %s", err)
|
||||||
|
flog.Info("reconnecting in 30 seconds...")
|
||||||
|
time.Sleep(30 * time.Second)
|
||||||
|
i.Handlers.Clear(girc.RPL_WELCOME)
|
||||||
|
i.Handlers.Add(girc.RPL_WELCOME, func(client *girc.Client, event girc.Event) {
|
||||||
|
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EVENT_REJOIN_CHANNELS}
|
||||||
|
// set our correct nick on reconnect if necessary
|
||||||
|
b.Nick = event.Source.Name
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
b.i = i
|
b.i = i
|
||||||
select {
|
select {
|
||||||
case <-b.connected:
|
case <-b.connected:
|
||||||
@ -100,15 +134,8 @@ func (b *Birc) Connect() error {
|
|||||||
case <-time.After(time.Second * 30):
|
case <-time.After(time.Second * 30):
|
||||||
return fmt.Errorf("connection timed out")
|
return fmt.Errorf("connection timed out")
|
||||||
}
|
}
|
||||||
i.Debug = false
|
//i.Debug = false
|
||||||
// clear on reconnects
|
i.Handlers.Clear("*")
|
||||||
i.ClearCallback(ircm.RPL_WELCOME)
|
|
||||||
i.AddCallback(ircm.RPL_WELCOME, func(event *irc.Event) {
|
|
||||||
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EVENT_REJOIN_CHANNELS}
|
|
||||||
// set our correct nick on reconnect if necessary
|
|
||||||
b.Nick = event.Nick
|
|
||||||
})
|
|
||||||
go i.Loop()
|
|
||||||
go b.doSend()
|
go b.doSend()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -122,9 +149,9 @@ func (b *Birc) Disconnect() error {
|
|||||||
func (b *Birc) JoinChannel(channel config.ChannelInfo) error {
|
func (b *Birc) JoinChannel(channel config.ChannelInfo) error {
|
||||||
if channel.Options.Key != "" {
|
if channel.Options.Key != "" {
|
||||||
flog.Debugf("using key %s for channel %s", channel.Options.Key, channel.Name)
|
flog.Debugf("using key %s for channel %s", channel.Options.Key, channel.Name)
|
||||||
b.i.Join(channel.Name + " " + channel.Options.Key)
|
b.i.Cmd.JoinKey(channel.Name, channel.Options.Key)
|
||||||
} else {
|
} else {
|
||||||
b.i.Join(channel.Name)
|
b.i.Cmd.Join(channel.Name)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -173,15 +200,15 @@ func (b *Birc) doSend() {
|
|||||||
for msg := range b.Local {
|
for msg := range b.Local {
|
||||||
<-throttle.C
|
<-throttle.C
|
||||||
if msg.Event == config.EVENT_USER_ACTION {
|
if msg.Event == config.EVENT_USER_ACTION {
|
||||||
b.i.Action(msg.Channel, msg.Username+msg.Text)
|
b.i.Cmd.Action(msg.Channel, msg.Username+msg.Text)
|
||||||
} else {
|
} else {
|
||||||
b.i.Privmsg(msg.Channel, msg.Username+msg.Text)
|
b.i.Cmd.Message(msg.Channel, msg.Username+msg.Text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Birc) endNames(event *irc.Event) {
|
func (b *Birc) endNames(client *girc.Client, event girc.Event) {
|
||||||
channel := event.Arguments[1]
|
channel := event.Params[1]
|
||||||
sort.Strings(b.names[channel])
|
sort.Strings(b.names[channel])
|
||||||
maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow()
|
maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow()
|
||||||
continued := false
|
continued := false
|
||||||
@ -194,101 +221,101 @@ func (b *Birc) endNames(event *irc.Event) {
|
|||||||
b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel], continued),
|
b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel], continued),
|
||||||
Channel: channel, Account: b.Account}
|
Channel: channel, Account: b.Account}
|
||||||
b.names[channel] = nil
|
b.names[channel] = nil
|
||||||
b.i.ClearCallback(ircm.RPL_NAMREPLY)
|
b.i.Handlers.Clear(girc.RPL_NAMREPLY)
|
||||||
b.i.ClearCallback(ircm.RPL_ENDOFNAMES)
|
b.i.Handlers.Clear(girc.RPL_ENDOFNAMES)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Birc) handleNewConnection(event *irc.Event) {
|
func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) {
|
||||||
flog.Debug("Registering callbacks")
|
flog.Debug("Registering callbacks")
|
||||||
i := b.i
|
i := b.i
|
||||||
b.Nick = event.Arguments[0]
|
b.Nick = event.Params[0]
|
||||||
i.AddCallback("PRIVMSG", b.handlePrivMsg)
|
|
||||||
i.AddCallback("CTCP_ACTION", b.handlePrivMsg)
|
i.Handlers.Add(girc.RPL_ENDOFMOTD, b.handleOtherAuth)
|
||||||
i.AddCallback(ircm.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
|
i.Handlers.Add("PRIVMSG", b.handlePrivMsg)
|
||||||
i.AddCallback(ircm.NOTICE, b.handleNotice)
|
i.Handlers.Add("CTCP_ACTION", b.handlePrivMsg)
|
||||||
//i.AddCallback(ircm.RPL_MYINFO, func(e *irc.Event) { flog.Infof("%s: %s", e.Code, strings.Join(e.Arguments[1:], " ")) })
|
i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
|
||||||
i.AddCallback("PING", func(e *irc.Event) {
|
i.Handlers.Add(girc.NOTICE, b.handleNotice)
|
||||||
i.SendRaw("PONG :" + e.Message())
|
i.Handlers.Add("JOIN", b.handleJoinPart)
|
||||||
flog.Debugf("PING/PONG")
|
i.Handlers.Add("PART", b.handleJoinPart)
|
||||||
})
|
i.Handlers.Add("QUIT", b.handleJoinPart)
|
||||||
i.AddCallback("JOIN", b.handleJoinPart)
|
i.Handlers.Add("KICK", b.handleJoinPart)
|
||||||
i.AddCallback("PART", b.handleJoinPart)
|
|
||||||
i.AddCallback("QUIT", b.handleJoinPart)
|
|
||||||
i.AddCallback("KICK", b.handleJoinPart)
|
|
||||||
i.AddCallback("*", b.handleOther)
|
|
||||||
// we are now fully connected
|
// we are now fully connected
|
||||||
b.connected <- struct{}{}
|
b.connected <- struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Birc) handleJoinPart(event *irc.Event) {
|
func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) {
|
||||||
channel := event.Arguments[0]
|
if len(event.Params) == 0 {
|
||||||
if event.Code == "KICK" {
|
flog.Debugf("handleJoinPart: empty Params? %#v", event)
|
||||||
flog.Infof("Got kicked from %s by %s", channel, event.Nick)
|
return
|
||||||
|
}
|
||||||
|
channel := event.Params[0]
|
||||||
|
if event.Command == "KICK" {
|
||||||
|
flog.Infof("Got kicked from %s by %s", channel, event.Source.Name)
|
||||||
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: channel, Account: b.Account, Event: config.EVENT_REJOIN_CHANNELS}
|
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: channel, Account: b.Account, Event: config.EVENT_REJOIN_CHANNELS}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if event.Code == "QUIT" {
|
if event.Command == "QUIT" {
|
||||||
if event.Nick == b.Nick && strings.Contains(event.Raw, "Ping timeout") {
|
if event.Source.Name == b.Nick && strings.Contains(event.Trailing, "Ping timeout") {
|
||||||
flog.Infof("%s reconnecting ..", b.Account)
|
flog.Infof("%s reconnecting ..", b.Account)
|
||||||
b.Remote <- config.Message{Username: "system", Text: "reconnect", Channel: channel, Account: b.Account, Event: config.EVENT_FAILURE}
|
b.Remote <- config.Message{Username: "system", Text: "reconnect", Channel: channel, Account: b.Account, Event: config.EVENT_FAILURE}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if event.Nick != b.Nick {
|
if event.Source.Name != b.Nick {
|
||||||
flog.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
|
flog.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
|
||||||
b.Remote <- config.Message{Username: "system", Text: event.Nick + " " + strings.ToLower(event.Code) + "s", Channel: channel, Account: b.Account, Event: config.EVENT_JOIN_LEAVE}
|
b.Remote <- config.Message{Username: "system", Text: event.Source.Name + " " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EVENT_JOIN_LEAVE}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
flog.Debugf("handle %#v", event)
|
flog.Debugf("handle %#v", event)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Birc) handleNotice(event *irc.Event) {
|
func (b *Birc) handleNotice(client *girc.Client, event girc.Event) {
|
||||||
if strings.Contains(event.Message(), "This nickname is registered") && event.Nick == b.Config.NickServNick {
|
if strings.Contains(event.String(), "This nickname is registered") && event.Source.Name == b.Config.NickServNick {
|
||||||
b.i.Privmsg(b.Config.NickServNick, "IDENTIFY "+b.Config.NickServPassword)
|
b.i.Cmd.Message(b.Config.NickServNick, "IDENTIFY "+b.Config.NickServPassword)
|
||||||
} else {
|
} else {
|
||||||
b.handlePrivMsg(event)
|
b.handlePrivMsg(client, event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Birc) handleOther(event *irc.Event) {
|
func (b *Birc) handleOther(client *girc.Client, event girc.Event) {
|
||||||
switch event.Code {
|
switch event.Command {
|
||||||
case "372", "375", "376", "250", "251", "252", "253", "254", "255", "265", "266", "002", "003", "004", "005":
|
case "372", "375", "376", "250", "251", "252", "253", "254", "255", "265", "266", "002", "003", "004", "005":
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
flog.Debugf("%#v", event.Raw)
|
flog.Debugf("%#v", event.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Birc) handleOtherAuth(event *irc.Event) {
|
func (b *Birc) handleOtherAuth(client *girc.Client, event girc.Event) {
|
||||||
if strings.EqualFold(b.Config.NickServNick, "Q@CServe.quakenet.org") {
|
if strings.EqualFold(b.Config.NickServNick, "Q@CServe.quakenet.org") {
|
||||||
flog.Debugf("Authenticating %s against %s", b.Config.NickServUsername, b.Config.NickServNick)
|
flog.Debugf("Authenticating %s against %s", b.Config.NickServUsername, b.Config.NickServNick)
|
||||||
b.i.Privmsg(b.Config.NickServNick, "AUTH "+b.Config.NickServUsername+" "+b.Config.NickServPassword)
|
b.i.Cmd.Message(b.Config.NickServNick, "AUTH "+b.Config.NickServUsername+" "+b.Config.NickServPassword)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Birc) handlePrivMsg(event *irc.Event) {
|
func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
|
||||||
b.Nick = b.i.GetNick()
|
b.Nick = b.i.GetNick()
|
||||||
// freenode doesn't send 001 as first reply
|
// freenode doesn't send 001 as first reply
|
||||||
if event.Code == "NOTICE" {
|
if event.Command == "NOTICE" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// don't forward queries to the bot
|
// don't forward queries to the bot
|
||||||
if event.Arguments[0] == b.Nick {
|
if event.Params[0] == b.Nick {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// don't forward message from ourself
|
// don't forward message from ourself
|
||||||
if event.Nick == b.Nick {
|
if event.Source.Name == b.Nick {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rmsg := config.Message{Username: event.Nick, Channel: event.Arguments[0], Account: b.Account, UserID: event.User + "@" + event.Host}
|
rmsg := config.Message{Username: event.Source.Name, Channel: event.Params[0], Account: b.Account, UserID: event.Source.Ident + "@" + event.Source.Host}
|
||||||
flog.Debugf("handlePrivMsg() %s %s %#v", event.Nick, event.Message(), event)
|
flog.Debugf("handlePrivMsg() %s %s %#v", event.Source.Name, event.Trailing, event)
|
||||||
msg := ""
|
msg := ""
|
||||||
if event.Code == "CTCP_ACTION" {
|
if event.Command == "CTCP_ACTION" {
|
||||||
// msg = event.Nick + " "
|
// msg = event.Source.Name + " "
|
||||||
rmsg.Event = config.EVENT_USER_ACTION
|
rmsg.Event = config.EVENT_USER_ACTION
|
||||||
}
|
}
|
||||||
msg += event.Message()
|
msg += event.Trailing
|
||||||
// strip IRC colors
|
// strip IRC colors
|
||||||
re := regexp.MustCompile(`[[:cntrl:]](\d+,|)\d+`)
|
re := regexp.MustCompile(`[[:cntrl:]](?:\d{1,2}(?:,\d{1,2})?)?`)
|
||||||
msg = re.ReplaceAllString(msg, "")
|
msg = re.ReplaceAllString(msg, "")
|
||||||
|
|
||||||
var r io.Reader
|
var r io.Reader
|
||||||
@ -317,49 +344,35 @@ func (b *Birc) handlePrivMsg(event *irc.Event) {
|
|||||||
output, _ := ioutil.ReadAll(r)
|
output, _ := ioutil.ReadAll(r)
|
||||||
msg = string(output)
|
msg = string(output)
|
||||||
|
|
||||||
flog.Debugf("Sending message from %s on %s to gateway", event.Arguments[0], b.Account)
|
flog.Debugf("Sending message from %s on %s to gateway", event.Params[0], b.Account)
|
||||||
rmsg.Text = msg
|
rmsg.Text = msg
|
||||||
b.Remote <- rmsg
|
b.Remote <- rmsg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Birc) handleTopicWhoTime(event *irc.Event) {
|
func (b *Birc) handleTopicWhoTime(client *girc.Client, event girc.Event) {
|
||||||
parts := strings.Split(event.Arguments[2], "!")
|
parts := strings.Split(event.Params[2], "!")
|
||||||
t, err := strconv.ParseInt(event.Arguments[3], 10, 64)
|
t, err := strconv.ParseInt(event.Params[3], 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
flog.Errorf("Invalid time stamp: %s", event.Arguments[3])
|
flog.Errorf("Invalid time stamp: %s", event.Params[3])
|
||||||
}
|
}
|
||||||
user := parts[0]
|
user := parts[0]
|
||||||
if len(parts) > 1 {
|
if len(parts) > 1 {
|
||||||
user += " [" + parts[1] + "]"
|
user += " [" + parts[1] + "]"
|
||||||
}
|
}
|
||||||
flog.Debugf("%s: Topic set by %s [%s]", event.Code, user, time.Unix(t, 0))
|
flog.Debugf("%s: Topic set by %s [%s]", event.Command, user, time.Unix(t, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Birc) nicksPerRow() int {
|
func (b *Birc) nicksPerRow() int {
|
||||||
return 4
|
return 4
|
||||||
/*
|
|
||||||
if b.Config.Mattermost.NicksPerRow < 1 {
|
|
||||||
return 4
|
|
||||||
}
|
|
||||||
return b.Config.Mattermost.NicksPerRow
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Birc) storeNames(event *irc.Event) {
|
func (b *Birc) storeNames(client *girc.Client, event girc.Event) {
|
||||||
channel := event.Arguments[2]
|
channel := event.Params[2]
|
||||||
b.names[channel] = append(
|
b.names[channel] = append(
|
||||||
b.names[channel],
|
b.names[channel],
|
||||||
strings.Split(strings.TrimSpace(event.Message()), " ")...)
|
strings.Split(strings.TrimSpace(event.Trailing), " ")...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Birc) formatnicks(nicks []string, continued bool) string {
|
func (b *Birc) formatnicks(nicks []string, continued bool) string {
|
||||||
return plainformatter(nicks, b.nicksPerRow())
|
return plainformatter(nicks, b.nicksPerRow())
|
||||||
/*
|
|
||||||
switch b.Config.Mattermost.NickFormatter {
|
|
||||||
case "table":
|
|
||||||
return tableformatter(nicks, b.nicksPerRow(), continued)
|
|
||||||
default:
|
|
||||||
return plainformatter(nicks, b.nicksPerRow())
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
@ -190,9 +190,9 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
|
|||||||
flog.Debugf("ERROR %#v", err)
|
flog.Debugf("ERROR %#v", err)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
message = "uploaded a file: " + fi.Name
|
message = fi.Comment
|
||||||
if b.Config.PrefixMessagesWithNick {
|
if b.Config.PrefixMessagesWithNick {
|
||||||
message = nick + "uploaded a file: " + fi.Name
|
message = nick + fi.Comment
|
||||||
}
|
}
|
||||||
res, err = b.mc.PostMessageWithFiles(b.mc.GetChannelId(channel, ""), message, []string{id})
|
res, err = b.mc.PostMessageWithFiles(b.mc.GetChannelId(channel, ""), message, []string{id})
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/42wim/matterbridge/bridge/config"
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
"github.com/42wim/matterbridge/matterhook"
|
"github.com/42wim/matterbridge/matterhook"
|
||||||
log "github.com/Sirupsen/logrus"
|
log "github.com/Sirupsen/logrus"
|
||||||
"github.com/nlopes/slack"
|
"github.com/matterbridge/slack"
|
||||||
"html"
|
"html"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -187,6 +187,26 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
|
|||||||
b.sc.UpdateMessage(schannel.ID, ts[1], message)
|
b.sc.UpdateMessage(schannel.ID, ts[1], message)
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if msg.Extra != nil {
|
||||||
|
// check if we have files to upload (from slack, telegram or mattermost)
|
||||||
|
if len(msg.Extra["file"]) > 0 {
|
||||||
|
var err error
|
||||||
|
for _, f := range msg.Extra["file"] {
|
||||||
|
fi := f.(config.FileInfo)
|
||||||
|
_, err = b.sc.UploadFile(slack.FileUploadParameters{
|
||||||
|
Reader: bytes.NewReader(*fi.Data),
|
||||||
|
Filename: fi.Name,
|
||||||
|
Channels: []string{schannel.ID},
|
||||||
|
InitialComment: fi.Comment,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
flog.Errorf("uploadfile %#v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_, id, err := b.sc.PostMessage(schannel.ID, message, np)
|
_, id, err := b.sc.PostMessage(schannel.ID, message, np)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@ -275,11 +295,16 @@ func (b *Bslack) handleSlack() {
|
|||||||
if message.Raw.File != nil {
|
if message.Raw.File != nil {
|
||||||
// limit to 1MB for now
|
// limit to 1MB for now
|
||||||
if message.Raw.File.Size <= 1000000 {
|
if message.Raw.File.Size <= 1000000 {
|
||||||
|
comment := ""
|
||||||
data, err := b.downloadFile(message.Raw.File.URLPrivateDownload)
|
data, err := b.downloadFile(message.Raw.File.URLPrivateDownload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
flog.Errorf("download %s failed %#v", message.Raw.File.URLPrivateDownload, err)
|
flog.Errorf("download %s failed %#v", message.Raw.File.URLPrivateDownload, err)
|
||||||
} else {
|
} else {
|
||||||
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: message.Raw.File.Name, Data: data})
|
results := regexp.MustCompile(`.*?commented: (.*)`).FindAllStringSubmatch(msg.Text, -1)
|
||||||
|
if len(results) > 0 {
|
||||||
|
comment = results[0][1]
|
||||||
|
}
|
||||||
|
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: message.Raw.File.Name, Data: data, Comment: comment})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -323,6 +348,9 @@ func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
|
|||||||
}
|
}
|
||||||
m.UserID = user.ID
|
m.UserID = user.ID
|
||||||
m.Username = user.Name
|
m.Username = user.Name
|
||||||
|
if user.Profile.DisplayName != "" {
|
||||||
|
m.Username = user.Profile.DisplayName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
m.Channel = channel.Name
|
m.Channel = channel.Name
|
||||||
m.Text = ev.Text
|
m.Text = ev.Text
|
||||||
@ -337,6 +365,8 @@ func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
|
|||||||
}
|
}
|
||||||
m.Raw = ev
|
m.Raw = ev
|
||||||
m.Text = b.replaceMention(m.Text)
|
m.Text = b.replaceMention(m.Text)
|
||||||
|
m.Text = b.replaceVariable(m.Text)
|
||||||
|
m.Text = b.replaceChannel(m.Text)
|
||||||
// when using webhookURL we can't check if it's our webhook or not for now
|
// when using webhookURL we can't check if it's our webhook or not for now
|
||||||
if ev.BotID != "" && b.Config.WebhookURL == "" {
|
if ev.BotID != "" && b.Config.WebhookURL == "" {
|
||||||
bot, err := b.rtm.GetBotInfo(ev.BotID)
|
bot, err := b.rtm.GetBotInfo(ev.BotID)
|
||||||
@ -345,6 +375,9 @@ func (b *Bslack) handleSlackClient(mchan chan *MMMessage) {
|
|||||||
}
|
}
|
||||||
if bot.Name != "" {
|
if bot.Name != "" {
|
||||||
m.Username = bot.Name
|
m.Username = bot.Name
|
||||||
|
if ev.Username != "" {
|
||||||
|
m.Username = ev.Username
|
||||||
|
}
|
||||||
m.UserID = bot.ID
|
m.UserID = bot.ID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -380,6 +413,8 @@ func (b *Bslack) handleMatterHook(mchan chan *MMMessage) {
|
|||||||
m.Username = message.UserName
|
m.Username = message.UserName
|
||||||
m.Text = message.Text
|
m.Text = message.Text
|
||||||
m.Text = b.replaceMention(m.Text)
|
m.Text = b.replaceMention(m.Text)
|
||||||
|
m.Text = b.replaceVariable(m.Text)
|
||||||
|
m.Text = b.replaceChannel(m.Text)
|
||||||
m.Channel = message.ChannelName
|
m.Channel = message.ChannelName
|
||||||
if m.Username == "slackbot" {
|
if m.Username == "slackbot" {
|
||||||
continue
|
continue
|
||||||
@ -391,23 +426,45 @@ func (b *Bslack) handleMatterHook(mchan chan *MMMessage) {
|
|||||||
func (b *Bslack) userName(id string) string {
|
func (b *Bslack) userName(id string) string {
|
||||||
for _, u := range b.Users {
|
for _, u := range b.Users {
|
||||||
if u.ID == id {
|
if u.ID == id {
|
||||||
|
if u.Profile.DisplayName != "" {
|
||||||
|
return u.Profile.DisplayName
|
||||||
|
}
|
||||||
return u.Name
|
return u.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users
|
||||||
func (b *Bslack) replaceMention(text string) string {
|
func (b *Bslack) replaceMention(text string) string {
|
||||||
results := regexp.MustCompile(`<@([a-zA-z0-9]+)>`).FindAllStringSubmatch(text, -1)
|
results := regexp.MustCompile(`<@([a-zA-z0-9]+)>`).FindAllStringSubmatch(text, -1)
|
||||||
for _, r := range results {
|
for _, r := range results {
|
||||||
text = strings.Replace(text, "<@"+r[1]+">", "@"+b.userName(r[1]), -1)
|
text = strings.Replace(text, "<@"+r[1]+">", "@"+b.userName(r[1]), -1)
|
||||||
|
|
||||||
}
|
}
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users
|
||||||
|
func (b *Bslack) replaceChannel(text string) string {
|
||||||
|
results := regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`).FindAllStringSubmatch(text, -1)
|
||||||
|
for _, r := range results {
|
||||||
|
text = strings.Replace(text, r[0], "#"+r[1], -1)
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
// @see https://api.slack.com/docs/message-formatting#variables
|
||||||
|
func (b *Bslack) replaceVariable(text string) string {
|
||||||
|
results := regexp.MustCompile(`<!([a-zA-Z0-9]+)(\|.+?)?>`).FindAllStringSubmatch(text, -1)
|
||||||
|
for _, r := range results {
|
||||||
|
text = strings.Replace(text, r[0], "@"+r[1], -1)
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
// @see https://api.slack.com/docs/message-formatting#linking_to_urls
|
||||||
func (b *Bslack) replaceURL(text string) string {
|
func (b *Bslack) replaceURL(text string) string {
|
||||||
results := regexp.MustCompile(`<(.*?)\|.*?>`).FindAllStringSubmatch(text, -1)
|
results := regexp.MustCompile(`<(.*?)(\|.*?)?>`).FindAllStringSubmatch(text, -1)
|
||||||
for _, r := range results {
|
for _, r := range results {
|
||||||
text = strings.Replace(text, r[0], r[1], -1)
|
text = strings.Replace(text, r[0], r[1], -1)
|
||||||
}
|
}
|
||||||
|
@ -105,8 +105,13 @@ func (b *Bsteam) handleEvents() {
|
|||||||
case *steam.ChatMsgEvent:
|
case *steam.ChatMsgEvent:
|
||||||
flog.Debugf("Receiving ChatMsgEvent: %#v", e)
|
flog.Debugf("Receiving ChatMsgEvent: %#v", e)
|
||||||
flog.Debugf("Sending message from %s on %s to gateway", b.getNick(e.ChatterId), b.Account)
|
flog.Debugf("Sending message from %s on %s to gateway", b.getNick(e.ChatterId), b.Account)
|
||||||
// for some reason we have to remove 0x18000000000000
|
var channel int64
|
||||||
channel := int64(e.ChatRoomId) - 0x18000000000000
|
if e.ChatRoomId == 0 {
|
||||||
|
channel = int64(e.ChatterId)
|
||||||
|
} else {
|
||||||
|
// for some reason we have to remove 0x18000000000000
|
||||||
|
channel = int64(e.ChatRoomId) - 0x18000000000000
|
||||||
|
}
|
||||||
msg := config.Message{Username: b.getNick(e.ChatterId), Text: e.Message, Channel: strconv.FormatInt(channel, 10), Account: b.Account, UserID: strconv.FormatInt(int64(e.ChatterId), 10)}
|
msg := config.Message{Username: b.getNick(e.ChatterId), Text: e.Message, Channel: strconv.FormatInt(channel, 10), Account: b.Account, UserID: strconv.FormatInt(int64(e.ChatterId), 10)}
|
||||||
b.Remote <- msg
|
b.Remote <- msg
|
||||||
case *steam.PersonaStateEvent:
|
case *steam.PersonaStateEvent:
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
package btelegram
|
package btelegram
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/42wim/matterbridge/bridge/config"
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
"github.com/42wim/matterbridge/bridge/helper"
|
||||||
log "github.com/Sirupsen/logrus"
|
log "github.com/Sirupsen/logrus"
|
||||||
"github.com/go-telegram-bot-api/telegram-bot-api"
|
"github.com/go-telegram-bot-api/telegram-bot-api"
|
||||||
)
|
)
|
||||||
@ -94,16 +97,31 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
m := tgbotapi.NewMessage(chatid, msg.Username+msg.Text)
|
if msg.Extra != nil {
|
||||||
if b.Config.MessageFormat == "HTML" {
|
// check if we have files to upload (from slack, telegram or mattermost)
|
||||||
m.ParseMode = tgbotapi.ModeHTML
|
if len(msg.Extra["file"]) > 0 {
|
||||||
|
var c tgbotapi.Chattable
|
||||||
|
for _, f := range msg.Extra["file"] {
|
||||||
|
fi := f.(config.FileInfo)
|
||||||
|
file := tgbotapi.FileBytes{fi.Name, *fi.Data}
|
||||||
|
re := regexp.MustCompile(".(jpg|png)$")
|
||||||
|
if re.MatchString(fi.Name) {
|
||||||
|
c = tgbotapi.NewPhotoUpload(chatid, file)
|
||||||
|
} else {
|
||||||
|
c = tgbotapi.NewDocumentUpload(chatid, file)
|
||||||
|
}
|
||||||
|
_, err := b.c.Send(c)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("file upload failed: %#v", err)
|
||||||
|
}
|
||||||
|
if fi.Comment != "" {
|
||||||
|
b.sendMessage(chatid, msg.Username+fi.Comment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
res, err := b.c.Send(m)
|
return b.sendMessage(chatid, msg.Username+msg.Text)
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return strconv.Itoa(res.MessageID), nil
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
|
func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
|
||||||
@ -113,6 +131,9 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
|
|||||||
username := ""
|
username := ""
|
||||||
channel := ""
|
channel := ""
|
||||||
text := ""
|
text := ""
|
||||||
|
|
||||||
|
fmsg := config.Message{Extra: make(map[string][]interface{})}
|
||||||
|
|
||||||
// handle channels
|
// handle channels
|
||||||
if update.ChannelPost != nil {
|
if update.ChannelPost != nil {
|
||||||
message = update.ChannelPost
|
message = update.ChannelPost
|
||||||
@ -146,19 +167,17 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
|
|||||||
if username == "" {
|
if username == "" {
|
||||||
username = "unknown"
|
username = "unknown"
|
||||||
}
|
}
|
||||||
if message.Sticker != nil && b.Config.UseInsecureURL {
|
if message.Sticker != nil {
|
||||||
text = text + " " + b.getFileDirectURL(message.Sticker.FileID)
|
b.handleDownload(message.Sticker, &fmsg)
|
||||||
}
|
}
|
||||||
if message.Video != nil && b.Config.UseInsecureURL {
|
if message.Video != nil {
|
||||||
text = text + " " + b.getFileDirectURL(message.Video.FileID)
|
b.handleDownload(message.Video, &fmsg)
|
||||||
}
|
}
|
||||||
if message.Photo != nil && b.Config.UseInsecureURL {
|
if message.Photo != nil {
|
||||||
photos := *message.Photo
|
b.handleDownload(message.Photo, &fmsg)
|
||||||
// last photo is the biggest
|
|
||||||
text = text + " " + b.getFileDirectURL(photos[len(photos)-1].FileID)
|
|
||||||
}
|
}
|
||||||
if message.Document != nil && b.Config.UseInsecureURL {
|
if message.Document != nil {
|
||||||
text = text + " " + message.Document.FileName + " : " + b.getFileDirectURL(message.Document.FileID)
|
b.handleDownload(message.Document, &fmsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// quote the previous message
|
// quote the previous message
|
||||||
@ -181,9 +200,9 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
|
|||||||
text = text + " (re @" + usernameReply + ":" + message.ReplyToMessage.Text + ")"
|
text = text + " (re @" + usernameReply + ":" + message.ReplyToMessage.Text + ")"
|
||||||
}
|
}
|
||||||
|
|
||||||
if text != "" {
|
if text != "" || len(fmsg.Extra) > 0 {
|
||||||
flog.Debugf("Sending message from %s on %s to gateway", username, b.Account)
|
flog.Debugf("Sending message from %s on %s to gateway", username, b.Account)
|
||||||
msg := config.Message{Username: username, Text: text, Channel: channel, Account: b.Account, UserID: strconv.Itoa(message.From.ID), ID: strconv.Itoa(message.MessageID)}
|
msg := config.Message{Username: username, Text: text, Channel: channel, Account: b.Account, UserID: strconv.Itoa(message.From.ID), ID: strconv.Itoa(message.MessageID), Extra: fmsg.Extra}
|
||||||
flog.Debugf("Message is %#v", msg)
|
flog.Debugf("Message is %#v", msg)
|
||||||
b.Remote <- msg
|
b.Remote <- msg
|
||||||
}
|
}
|
||||||
@ -197,3 +216,68 @@ func (b *Btelegram) getFileDirectURL(id string) string {
|
|||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Btelegram) handleDownload(file interface{}, msg *config.Message) {
|
||||||
|
size := 0
|
||||||
|
url := ""
|
||||||
|
name := ""
|
||||||
|
text := ""
|
||||||
|
fileid := ""
|
||||||
|
switch v := file.(type) {
|
||||||
|
case *tgbotapi.Sticker:
|
||||||
|
size = v.FileSize
|
||||||
|
url = b.getFileDirectURL(v.FileID)
|
||||||
|
urlPart := strings.Split(url, "/")
|
||||||
|
name = urlPart[len(urlPart)-1]
|
||||||
|
text = " " + url
|
||||||
|
fileid = v.FileID
|
||||||
|
case *tgbotapi.Video:
|
||||||
|
size = v.FileSize
|
||||||
|
url = b.getFileDirectURL(v.FileID)
|
||||||
|
urlPart := strings.Split(url, "/")
|
||||||
|
name = urlPart[len(urlPart)-1]
|
||||||
|
text = " " + url
|
||||||
|
fileid = v.FileID
|
||||||
|
case *[]tgbotapi.PhotoSize:
|
||||||
|
photos := *v
|
||||||
|
size = photos[len(photos)-1].FileSize
|
||||||
|
url = b.getFileDirectURL(photos[len(photos)-1].FileID)
|
||||||
|
urlPart := strings.Split(url, "/")
|
||||||
|
name = urlPart[len(urlPart)-1]
|
||||||
|
text = " " + url
|
||||||
|
case *tgbotapi.Document:
|
||||||
|
size = v.FileSize
|
||||||
|
url = b.getFileDirectURL(v.FileID)
|
||||||
|
name = v.FileName
|
||||||
|
text = " " + v.FileName + " : " + url
|
||||||
|
fileid = v.FileID
|
||||||
|
}
|
||||||
|
if b.Config.UseInsecureURL {
|
||||||
|
msg.Text = text
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra
|
||||||
|
// limit to 1MB for now
|
||||||
|
flog.Debugf("trying to download %#v fileid %#v with size %#v", name, fileid, size)
|
||||||
|
if size <= 1000000 {
|
||||||
|
data, err := helper.DownloadFile(url)
|
||||||
|
if err != nil {
|
||||||
|
flog.Errorf("download %s failed %#v", url, err)
|
||||||
|
} else {
|
||||||
|
flog.Debugf("download OK %#v %#v %#v", name, len(*data), len(url))
|
||||||
|
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: name, Data: data})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Btelegram) sendMessage(chatid int64, text string) (string, error) {
|
||||||
|
m := tgbotapi.NewMessage(chatid, text)
|
||||||
|
if b.Config.MessageFormat == "HTML" {
|
||||||
|
m.ParseMode = tgbotapi.ModeHTML
|
||||||
|
}
|
||||||
|
res, err := b.c.Send(m)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strconv.Itoa(res.MessageID), nil
|
||||||
|
}
|
||||||
|
34
changelog.md
34
changelog.md
@ -1,3 +1,37 @@
|
|||||||
|
# v1.4.1
|
||||||
|
## Bugfix
|
||||||
|
* telegram: fix issue with uploading for images/documents/stickers
|
||||||
|
* slack: remove double messages sent to other bridges when uploading files
|
||||||
|
* irc: Fix strict user handling of girc (irc). Closes #298
|
||||||
|
|
||||||
|
# v1.4.0
|
||||||
|
## Breaking changes
|
||||||
|
* general: `[general]` settings don't override the specific bridge settings
|
||||||
|
|
||||||
|
## New features
|
||||||
|
* irc: Replace sorcix/irc and go-ircevent with girc, this should be give better reconnects
|
||||||
|
* steam: Add support for bridging to individual steam chats. (steam) (#294)
|
||||||
|
* telegram: Download files from telegram and reupload to supported bridges (telegram). #278
|
||||||
|
* slack: Add support to upload files to slack, from bridges with private urls like slack/mattermost/telegram. (slack)
|
||||||
|
* discord: Add support to upload files to discord, from bridges with private urls like slack/mattermost/telegram. (discord)
|
||||||
|
* general: Add systemd service file (#291)
|
||||||
|
* general: Add support for DEBUG=1 envvar to enable debug. Closes #283
|
||||||
|
* general: Add StripNick option, only allow alphanumerical nicks. Closes #285
|
||||||
|
|
||||||
|
## Bugfix
|
||||||
|
* gitter: Use room.URI instead of room.Name. (gitter) (#293)
|
||||||
|
* slack: Allow slack messages with variables (eg. @here) to be formatted correctly. (slack) (#288)
|
||||||
|
* slack: Resolve slack channel to human-readable name. (slack) (#282)
|
||||||
|
* slack: Use DisplayName instead of deprecated username (slack). Closes #276
|
||||||
|
* slack: Allowed Slack bridge to extract simpler link format. (#287)
|
||||||
|
* irc: Strip irc colors correct, strip also ctrl chars (irc)
|
||||||
|
|
||||||
|
# v1.3.1
|
||||||
|
## New features
|
||||||
|
* Support mattermost 4.3.0 and every other 4.x as api4 should be stable (mattermost)
|
||||||
|
## Bugfix
|
||||||
|
* Use bot username if specified (slack). Closes #273
|
||||||
|
|
||||||
# v1.3.0
|
# v1.3.0
|
||||||
## New features
|
## New features
|
||||||
* Relay slack_attachments from mattermost to slack (slack). Closes #260
|
* Relay slack_attachments from mattermost to slack (slack). Closes #260
|
||||||
|
11
contrib/matterbridge.service
Normal file
11
contrib/matterbridge.service
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=matterbridge
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/matterbridge -conf /etc/matterbridge/bridge.toml
|
||||||
|
User=matterbridge
|
||||||
|
Group=matterbridge
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
@ -152,7 +152,10 @@ func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrM
|
|||||||
// only slack now, check will have to be done in the different bridges.
|
// only slack now, check will have to be done in the different bridges.
|
||||||
// we need to check if we can't use fallback or text in other bridges
|
// we need to check if we can't use fallback or text in other bridges
|
||||||
if msg.Extra != nil {
|
if msg.Extra != nil {
|
||||||
if dest.Protocol != "slack" {
|
if dest.Protocol != "discord" &&
|
||||||
|
dest.Protocol != "slack" &&
|
||||||
|
dest.Protocol != "mattermost" &&
|
||||||
|
dest.Protocol != "telegram" {
|
||||||
if msg.Text == "" {
|
if msg.Text == "" {
|
||||||
return brMsgIDs
|
return brMsgIDs
|
||||||
}
|
}
|
||||||
@ -210,8 +213,8 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if msg.Text == "" {
|
if msg.Text == "" {
|
||||||
// we have an attachment
|
// we have an attachment or actual bytes
|
||||||
if msg.Extra != nil && msg.Extra["attachments"] != nil {
|
if msg.Extra != nil && (msg.Extra["attachments"] != nil || len(msg.Extra["file"]) > 0) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
log.Debugf("ignoring empty message %#v from %s", msg, msg.Account)
|
log.Debugf("ignoring empty message %#v from %s", msg, msg.Account)
|
||||||
@ -243,9 +246,13 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
|
|||||||
func (gw *Gateway) modifyUsername(msg config.Message, dest *bridge.Bridge) string {
|
func (gw *Gateway) modifyUsername(msg config.Message, dest *bridge.Bridge) string {
|
||||||
br := gw.Bridges[msg.Account]
|
br := gw.Bridges[msg.Account]
|
||||||
msg.Protocol = br.Protocol
|
msg.Protocol = br.Protocol
|
||||||
nick := gw.Config.General.RemoteNickFormat
|
if gw.Config.General.StripNick || dest.Config.StripNick {
|
||||||
|
re := regexp.MustCompile("[^a-zA-Z0-9]+")
|
||||||
|
msg.Username = re.ReplaceAllString(msg.Username, "")
|
||||||
|
}
|
||||||
|
nick := dest.Config.RemoteNickFormat
|
||||||
if nick == "" {
|
if nick == "" {
|
||||||
nick = dest.Config.RemoteNickFormat
|
nick = gw.Config.General.RemoteNickFormat
|
||||||
}
|
}
|
||||||
if len(msg.Username) > 0 {
|
if len(msg.Username) > 0 {
|
||||||
// fix utf-8 issue #193
|
// fix utf-8 issue #193
|
||||||
|
@ -7,11 +7,12 @@ import (
|
|||||||
"github.com/42wim/matterbridge/gateway"
|
"github.com/42wim/matterbridge/gateway"
|
||||||
log "github.com/Sirupsen/logrus"
|
log "github.com/Sirupsen/logrus"
|
||||||
"github.com/google/gops/agent"
|
"github.com/google/gops/agent"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
version = "1.3.0"
|
version = "1.4.1"
|
||||||
githash string
|
githash string
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,7 +34,7 @@ func main() {
|
|||||||
fmt.Printf("version: %s %s\n", version, githash)
|
fmt.Printf("version: %s %s\n", version, githash)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if *flagDebug {
|
if *flagDebug || os.Getenv("DEBUG") == "1" {
|
||||||
log.Info("Enabling debug")
|
log.Info("Enabling debug")
|
||||||
log.SetLevel(log.DebugLevel)
|
log.SetLevel(log.DebugLevel)
|
||||||
}
|
}
|
||||||
|
@ -104,6 +104,11 @@ RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowJoinPart=false
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||||
|
#It will strip other characters from the nick
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
StripNick=false
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#XMPP section
|
#XMPP section
|
||||||
###################################################################
|
###################################################################
|
||||||
@ -161,6 +166,10 @@ RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowJoinPart=false
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||||
|
#It will strip other characters from the nick
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
StripNick=false
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#hipchat section
|
#hipchat section
|
||||||
@ -211,6 +220,10 @@ RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> "
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowJoinPart=false
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||||
|
#It will strip other characters from the nick
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
StripNick=false
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#mattermost section
|
#mattermost section
|
||||||
@ -321,6 +334,11 @@ RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowJoinPart=false
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||||
|
#It will strip other characters from the nick
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
StripNick=false
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#Gitter section
|
#Gitter section
|
||||||
#Best to make a dedicated gitter account for the bot.
|
#Best to make a dedicated gitter account for the bot.
|
||||||
@ -360,6 +378,11 @@ RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowJoinPart=false
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||||
|
#It will strip other characters from the nick
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
StripNick=false
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#slack section
|
#slack section
|
||||||
###################################################################
|
###################################################################
|
||||||
@ -446,6 +469,11 @@ RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowJoinPart=false
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||||
|
#It will strip other characters from the nick
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
StripNick=false
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#discord section
|
#discord section
|
||||||
###################################################################
|
###################################################################
|
||||||
@ -509,6 +537,11 @@ RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowJoinPart=false
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||||
|
#It will strip other characters from the nick
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
StripNick=false
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#telegram section
|
#telegram section
|
||||||
###################################################################
|
###################################################################
|
||||||
@ -571,6 +604,10 @@ RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowJoinPart=false
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||||
|
#It will strip other characters from the nick
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
StripNick=false
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#rocketchat section
|
#rocketchat section
|
||||||
@ -635,6 +672,11 @@ RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowJoinPart=false
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||||
|
#It will strip other characters from the nick
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
StripNick=false
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#matrix section
|
#matrix section
|
||||||
###################################################################
|
###################################################################
|
||||||
@ -690,6 +732,11 @@ RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowJoinPart=false
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||||
|
#It will strip other characters from the nick
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
StripNick=false
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#steam section
|
#steam section
|
||||||
###################################################################
|
###################################################################
|
||||||
@ -739,6 +786,10 @@ RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowJoinPart=false
|
ShowJoinPart=false
|
||||||
|
|
||||||
|
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||||
|
#It will strip other characters from the nick
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
StripNick=false
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#API
|
#API
|
||||||
@ -773,7 +824,7 @@ RemoteNickFormat="{NICK}"
|
|||||||
###################################################################
|
###################################################################
|
||||||
#General configuration
|
#General configuration
|
||||||
###################################################################
|
###################################################################
|
||||||
#Settings here override specific settings for each protocol
|
# Settings here are defaults that each protocol can override
|
||||||
[general]
|
[general]
|
||||||
#RemoteNickFormat defines how remote users appear on this bridge
|
#RemoteNickFormat defines how remote users appear on this bridge
|
||||||
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
|
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
|
||||||
@ -782,6 +833,11 @@ RemoteNickFormat="{NICK}"
|
|||||||
#OPTIONAL (default empty)
|
#OPTIONAL (default empty)
|
||||||
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
|
||||||
|
|
||||||
|
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
|
||||||
|
#It will strip other characters from the nick
|
||||||
|
#OPTIONAL (default false)
|
||||||
|
StripNick=false
|
||||||
|
|
||||||
###################################################################
|
###################################################################
|
||||||
#Gateway configuration
|
#Gateway configuration
|
||||||
###################################################################
|
###################################################################
|
||||||
@ -817,7 +873,7 @@ enable=true
|
|||||||
#mattermost - channel (the channel name as seen in the URL, not the displayname)
|
#mattermost - channel (the channel name as seen in the URL, not the displayname)
|
||||||
#gitter - username/room
|
#gitter - username/room
|
||||||
#xmpp - channel
|
#xmpp - channel
|
||||||
#slack - channel (the channel name as seen in the URL, not the displayname)
|
#slack - channel (without the #)
|
||||||
#discord - channel (without the #)
|
#discord - channel (without the #)
|
||||||
# - ID:123456789 (where 123456789 is the channel ID)
|
# - ID:123456789 (where 123456789 is the channel ID)
|
||||||
# (https://github.com/42wim/matterbridge/issues/57)
|
# (https://github.com/42wim/matterbridge/issues/57)
|
||||||
|
@ -895,9 +895,7 @@ func supportedVersion(version string) bool {
|
|||||||
if strings.HasPrefix(version, "3.8.0") ||
|
if strings.HasPrefix(version, "3.8.0") ||
|
||||||
strings.HasPrefix(version, "3.9.0") ||
|
strings.HasPrefix(version, "3.9.0") ||
|
||||||
strings.HasPrefix(version, "3.10.0") ||
|
strings.HasPrefix(version, "3.10.0") ||
|
||||||
strings.HasPrefix(version, "4.0") ||
|
strings.HasPrefix(version, "4.") {
|
||||||
strings.HasPrefix(version, "4.1") ||
|
|
||||||
strings.HasPrefix(version, "4.2") {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
21
vendor/github.com/lrstanley/girc/LICENSE
generated
vendored
Normal file
21
vendor/github.com/lrstanley/girc/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2016 Liam Stanley <me@liamstanley.io>
|
||||||
|
|
||||||
|
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.
|
518
vendor/github.com/lrstanley/girc/builtin.go
generated
vendored
Normal file
518
vendor/github.com/lrstanley/girc/builtin.go
generated
vendored
Normal file
@ -0,0 +1,518 @@
|
|||||||
|
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||||
|
// of this source code is governed by the MIT license that can be found in
|
||||||
|
// the LICENSE file.
|
||||||
|
|
||||||
|
package girc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// registerBuiltin sets up built-in handlers, based on client
|
||||||
|
// configuration.
|
||||||
|
func (c *Client) registerBuiltins() {
|
||||||
|
c.debug.Print("registering built-in handlers")
|
||||||
|
c.Handlers.mu.Lock()
|
||||||
|
|
||||||
|
// Built-in things that should always be supported.
|
||||||
|
c.Handlers.register(true, RPL_WELCOME, HandlerFunc(func(c *Client, e Event) {
|
||||||
|
go handleConnect(c, e)
|
||||||
|
}))
|
||||||
|
c.Handlers.register(true, PING, HandlerFunc(handlePING))
|
||||||
|
c.Handlers.register(true, PONG, HandlerFunc(handlePONG))
|
||||||
|
|
||||||
|
if !c.Config.disableTracking {
|
||||||
|
// Joins/parts/anything that may add/remove/rename users.
|
||||||
|
c.Handlers.register(true, JOIN, HandlerFunc(handleJOIN))
|
||||||
|
c.Handlers.register(true, PART, HandlerFunc(handlePART))
|
||||||
|
c.Handlers.register(true, KICK, HandlerFunc(handleKICK))
|
||||||
|
c.Handlers.register(true, QUIT, HandlerFunc(handleQUIT))
|
||||||
|
c.Handlers.register(true, NICK, HandlerFunc(handleNICK))
|
||||||
|
c.Handlers.register(true, RPL_NAMREPLY, HandlerFunc(handleNAMES))
|
||||||
|
|
||||||
|
// Modes.
|
||||||
|
c.Handlers.register(true, MODE, HandlerFunc(handleMODE))
|
||||||
|
c.Handlers.register(true, RPL_CHANNELMODEIS, HandlerFunc(handleMODE))
|
||||||
|
|
||||||
|
// WHO/WHOX responses.
|
||||||
|
c.Handlers.register(true, RPL_WHOREPLY, HandlerFunc(handleWHO))
|
||||||
|
c.Handlers.register(true, RPL_WHOSPCRPL, HandlerFunc(handleWHO))
|
||||||
|
|
||||||
|
// Other misc. useful stuff.
|
||||||
|
c.Handlers.register(true, TOPIC, HandlerFunc(handleTOPIC))
|
||||||
|
c.Handlers.register(true, RPL_TOPIC, HandlerFunc(handleTOPIC))
|
||||||
|
c.Handlers.register(true, RPL_MYINFO, HandlerFunc(handleMYINFO))
|
||||||
|
c.Handlers.register(true, RPL_ISUPPORT, HandlerFunc(handleISUPPORT))
|
||||||
|
c.Handlers.register(true, RPL_MOTDSTART, HandlerFunc(handleMOTD))
|
||||||
|
c.Handlers.register(true, RPL_MOTD, HandlerFunc(handleMOTD))
|
||||||
|
|
||||||
|
// Keep users lastactive times up to date.
|
||||||
|
c.Handlers.register(true, PRIVMSG, HandlerFunc(updateLastActive))
|
||||||
|
c.Handlers.register(true, NOTICE, HandlerFunc(updateLastActive))
|
||||||
|
c.Handlers.register(true, TOPIC, HandlerFunc(updateLastActive))
|
||||||
|
c.Handlers.register(true, KICK, HandlerFunc(updateLastActive))
|
||||||
|
|
||||||
|
// CAP IRCv3-specific tracking and functionality.
|
||||||
|
c.Handlers.register(true, CAP, HandlerFunc(handleCAP))
|
||||||
|
c.Handlers.register(true, CAP_CHGHOST, HandlerFunc(handleCHGHOST))
|
||||||
|
c.Handlers.register(true, CAP_AWAY, HandlerFunc(handleAWAY))
|
||||||
|
c.Handlers.register(true, CAP_ACCOUNT, HandlerFunc(handleACCOUNT))
|
||||||
|
c.Handlers.register(true, ALL_EVENTS, HandlerFunc(handleTags))
|
||||||
|
|
||||||
|
// SASL IRCv3 support.
|
||||||
|
c.Handlers.register(true, AUTHENTICATE, HandlerFunc(handleSASL))
|
||||||
|
c.Handlers.register(true, RPL_SASLSUCCESS, HandlerFunc(handleSASL))
|
||||||
|
c.Handlers.register(true, RPL_NICKLOCKED, HandlerFunc(handleSASLError))
|
||||||
|
c.Handlers.register(true, ERR_SASLFAIL, HandlerFunc(handleSASLError))
|
||||||
|
c.Handlers.register(true, ERR_SASLTOOLONG, HandlerFunc(handleSASLError))
|
||||||
|
c.Handlers.register(true, ERR_SASLABORTED, HandlerFunc(handleSASLError))
|
||||||
|
c.Handlers.register(true, RPL_SASLMECHS, HandlerFunc(handleSASLError))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nickname collisions.
|
||||||
|
c.Handlers.register(true, ERR_NICKNAMEINUSE, HandlerFunc(nickCollisionHandler))
|
||||||
|
c.Handlers.register(true, ERR_NICKCOLLISION, HandlerFunc(nickCollisionHandler))
|
||||||
|
c.Handlers.register(true, ERR_UNAVAILRESOURCE, HandlerFunc(nickCollisionHandler))
|
||||||
|
|
||||||
|
c.Handlers.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleConnect is a helper function which lets the client know that enough
|
||||||
|
// time has passed and now they can send commands.
|
||||||
|
//
|
||||||
|
// Should always run in separate thread due to blocking delay.
|
||||||
|
func handleConnect(c *Client, e Event) {
|
||||||
|
// This should be the nick that the server gives us. 99% of the time, it's
|
||||||
|
// the one we supplied during connection, but some networks will rename
|
||||||
|
// users on connect.
|
||||||
|
if len(e.Params) > 0 {
|
||||||
|
c.state.Lock()
|
||||||
|
c.state.nick = e.Params[0]
|
||||||
|
c.state.Unlock()
|
||||||
|
|
||||||
|
c.state.notify(c, UPDATE_GENERAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
c.RunHandlers(&Event{Command: CONNECTED, Trailing: c.Server()})
|
||||||
|
}
|
||||||
|
|
||||||
|
// nickCollisionHandler helps prevent the client from having conflicting
|
||||||
|
// nicknames with another bot, user, etc.
|
||||||
|
func nickCollisionHandler(c *Client, e Event) {
|
||||||
|
if c.Config.HandleNickCollide == nil {
|
||||||
|
c.Cmd.Nick(c.GetNick() + "_")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Cmd.Nick(c.Config.HandleNickCollide(c.GetNick()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePING helps respond to ping requests from the server.
|
||||||
|
func handlePING(c *Client, e Event) {
|
||||||
|
c.Cmd.Pong(e.Trailing)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePONG(c *Client, e Event) {
|
||||||
|
c.conn.lastPong = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleJOIN ensures that the state has updated users and channels.
|
||||||
|
func handleJOIN(c *Client, e Event) {
|
||||||
|
if e.Source == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var channelName string
|
||||||
|
if len(e.Params) > 0 {
|
||||||
|
channelName = e.Params[0]
|
||||||
|
} else {
|
||||||
|
channelName = e.Trailing
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.Lock()
|
||||||
|
|
||||||
|
channel := c.state.lookupChannel(channelName)
|
||||||
|
if channel == nil {
|
||||||
|
if ok := c.state.createChannel(channelName); !ok {
|
||||||
|
c.state.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
channel = c.state.lookupChannel(channelName)
|
||||||
|
}
|
||||||
|
|
||||||
|
user := c.state.lookupUser(e.Source.Name)
|
||||||
|
if user == nil {
|
||||||
|
if ok := c.state.createUser(e.Source.Name); !ok {
|
||||||
|
c.state.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user = c.state.lookupUser(e.Source.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer c.state.notify(c, UPDATE_STATE)
|
||||||
|
|
||||||
|
channel.addUser(user.Nick)
|
||||||
|
user.addChannel(channel.Name)
|
||||||
|
|
||||||
|
// Assume extended-join (ircv3).
|
||||||
|
if len(e.Params) == 2 {
|
||||||
|
if e.Params[1] != "*" {
|
||||||
|
user.Extras.Account = e.Params[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.Trailing) > 0 {
|
||||||
|
user.Extras.Name = e.Trailing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.state.Unlock()
|
||||||
|
|
||||||
|
if e.Source.Name == c.GetNick() {
|
||||||
|
// If it's us, don't just add our user to the list. Run a WHO which
|
||||||
|
// will tell us who exactly is in the entire channel.
|
||||||
|
c.Send(&Event{Command: WHO, Params: []string{channelName, "%tacuhnr,1"}})
|
||||||
|
|
||||||
|
// Also send a MODE to obtain the list of channel modes.
|
||||||
|
c.Send(&Event{Command: MODE, Params: []string{channelName}})
|
||||||
|
|
||||||
|
// Update our ident and host too, in state -- since there is no
|
||||||
|
// cleaner method to do this.
|
||||||
|
c.state.Lock()
|
||||||
|
c.state.ident = e.Source.Ident
|
||||||
|
c.state.host = e.Source.Host
|
||||||
|
c.state.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only WHO the user, which is more efficient.
|
||||||
|
c.Send(&Event{Command: WHO, Params: []string{e.Source.Name, "%tacuhnr,1"}})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePART ensures that the state is clean of old user and channel entries.
|
||||||
|
func handlePART(c *Client, e Event) {
|
||||||
|
if e.Source == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var channel string
|
||||||
|
if len(e.Params) > 0 {
|
||||||
|
channel = e.Params[0]
|
||||||
|
} else {
|
||||||
|
channel = e.Trailing
|
||||||
|
}
|
||||||
|
|
||||||
|
if channel == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer c.state.notify(c, UPDATE_STATE)
|
||||||
|
|
||||||
|
if e.Source.Name == c.GetNick() {
|
||||||
|
c.state.Lock()
|
||||||
|
c.state.deleteChannel(channel)
|
||||||
|
c.state.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.Lock()
|
||||||
|
c.state.deleteUser(channel, e.Source.Name)
|
||||||
|
c.state.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleTOPIC handles incoming TOPIC events and keeps channel tracking info
|
||||||
|
// updated with the latest channel topic.
|
||||||
|
func handleTOPIC(c *Client, e Event) {
|
||||||
|
var name string
|
||||||
|
switch len(e.Params) {
|
||||||
|
case 0:
|
||||||
|
return
|
||||||
|
case 1:
|
||||||
|
name = e.Params[0]
|
||||||
|
default:
|
||||||
|
name = e.Params[len(e.Params)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.Lock()
|
||||||
|
channel := c.state.lookupChannel(name)
|
||||||
|
if channel == nil {
|
||||||
|
c.state.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.Topic = e.Trailing
|
||||||
|
c.state.Unlock()
|
||||||
|
c.state.notify(c, UPDATE_STATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlWHO updates our internal tracking of users/channels with WHO/WHOX
|
||||||
|
// information.
|
||||||
|
func handleWHO(c *Client, e Event) {
|
||||||
|
var ident, host, nick, account, realname string
|
||||||
|
|
||||||
|
// Assume WHOX related.
|
||||||
|
if e.Command == RPL_WHOSPCRPL {
|
||||||
|
if len(e.Params) != 7 {
|
||||||
|
// Assume there was some form of error or invalid WHOX response.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Params[1] != "1" {
|
||||||
|
// We should always be sending 1, and we should receive 1. If this
|
||||||
|
// is anything but, then we didn't send the request and we can
|
||||||
|
// ignore it.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ident, host, nick, account = e.Params[3], e.Params[4], e.Params[5], e.Params[6]
|
||||||
|
realname = e.Trailing
|
||||||
|
} else {
|
||||||
|
// Assume RPL_WHOREPLY.
|
||||||
|
ident, host, nick = e.Params[2], e.Params[3], e.Params[5]
|
||||||
|
if len(e.Trailing) > 2 {
|
||||||
|
realname = e.Trailing[2:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.Lock()
|
||||||
|
user := c.state.lookupUser(nick)
|
||||||
|
if user == nil {
|
||||||
|
c.state.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Host = host
|
||||||
|
user.Ident = ident
|
||||||
|
user.Extras.Name = realname
|
||||||
|
|
||||||
|
if account != "0" {
|
||||||
|
user.Extras.Account = account
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.Unlock()
|
||||||
|
c.state.notify(c, UPDATE_STATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleKICK ensures that users are cleaned up after being kicked from the
|
||||||
|
// channel
|
||||||
|
func handleKICK(c *Client, e Event) {
|
||||||
|
if len(e.Params) < 2 {
|
||||||
|
// Needs at least channel and user.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer c.state.notify(c, UPDATE_STATE)
|
||||||
|
|
||||||
|
if e.Params[1] == c.GetNick() {
|
||||||
|
c.state.Lock()
|
||||||
|
c.state.deleteChannel(e.Params[0])
|
||||||
|
c.state.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assume it's just another user.
|
||||||
|
c.state.Lock()
|
||||||
|
c.state.deleteUser(e.Params[0], e.Params[1])
|
||||||
|
c.state.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleNICK ensures that users are renamed in state, or the client name is
|
||||||
|
// up to date.
|
||||||
|
func handleNICK(c *Client, e Event) {
|
||||||
|
if e.Source == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.Lock()
|
||||||
|
// renameUser updates the LastActive time automatically.
|
||||||
|
if len(e.Params) == 1 {
|
||||||
|
c.state.renameUser(e.Source.Name, e.Params[0])
|
||||||
|
} else if len(e.Trailing) > 0 {
|
||||||
|
c.state.renameUser(e.Source.Name, e.Trailing)
|
||||||
|
}
|
||||||
|
c.state.Unlock()
|
||||||
|
c.state.notify(c, UPDATE_STATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleQUIT handles users that are quitting from the network.
|
||||||
|
func handleQUIT(c *Client, e Event) {
|
||||||
|
if e.Source == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Source.Name == c.GetNick() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.Lock()
|
||||||
|
c.state.deleteUser("", e.Source.Name)
|
||||||
|
c.state.Unlock()
|
||||||
|
c.state.notify(c, UPDATE_STATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMYINFO handles incoming MYINFO events -- these are commonly used
|
||||||
|
// to tell us what the server name is, what version of software is being used
|
||||||
|
// as well as what channel and user modes are being used on the server.
|
||||||
|
func handleMYINFO(c *Client, e Event) {
|
||||||
|
// Malformed or odd output. As this can differ strongly between networks,
|
||||||
|
// just skip it.
|
||||||
|
if len(e.Params) < 3 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.Lock()
|
||||||
|
c.state.serverOptions["SERVER"] = e.Params[1]
|
||||||
|
c.state.serverOptions["VERSION"] = e.Params[2]
|
||||||
|
c.state.Unlock()
|
||||||
|
c.state.notify(c, UPDATE_GENERAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleISUPPORT handles incoming RPL_ISUPPORT (also known as RPL_PROTOCTL)
|
||||||
|
// events. These commonly contain the server capabilities and limitations.
|
||||||
|
// For example, things like max channel name length, or nickname length.
|
||||||
|
func handleISUPPORT(c *Client, e Event) {
|
||||||
|
// Must be a ISUPPORT-based message. 005 is also used for server bounce
|
||||||
|
// related things, so this handler may be triggered during other
|
||||||
|
// situations.
|
||||||
|
|
||||||
|
// Also known as RPL_PROTOCTL.
|
||||||
|
if !strings.HasSuffix(e.Trailing, "this server") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must have at least one configuration.
|
||||||
|
if len(e.Params) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.Lock()
|
||||||
|
// Skip the first parameter, as it's our nickname.
|
||||||
|
for i := 1; i < len(e.Params); i++ {
|
||||||
|
j := strings.IndexByte(e.Params[i], 0x3D) // =
|
||||||
|
|
||||||
|
if j < 1 || (j+1) == len(e.Params[i]) {
|
||||||
|
c.state.serverOptions[e.Params[i]] = ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name := e.Params[i][0:j]
|
||||||
|
val := e.Params[i][j+1:]
|
||||||
|
c.state.serverOptions[name] = val
|
||||||
|
}
|
||||||
|
c.state.Unlock()
|
||||||
|
c.state.notify(c, UPDATE_GENERAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMOTD handles incoming MOTD messages and buffers them up for use with
|
||||||
|
// Client.ServerMOTD().
|
||||||
|
func handleMOTD(c *Client, e Event) {
|
||||||
|
c.state.Lock()
|
||||||
|
|
||||||
|
defer c.state.notify(c, UPDATE_GENERAL)
|
||||||
|
|
||||||
|
// Beginning of the MOTD.
|
||||||
|
if e.Command == RPL_MOTDSTART {
|
||||||
|
c.state.motd = ""
|
||||||
|
|
||||||
|
c.state.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, assume we're getting sent the MOTD line-by-line.
|
||||||
|
if len(c.state.motd) != 0 {
|
||||||
|
e.Trailing = "\n" + e.Trailing
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.motd += e.Trailing
|
||||||
|
c.state.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleNAMES handles incoming NAMES queries, of which lists all users in
|
||||||
|
// a given channel. Optionally also obtains ident/host values, as well as
|
||||||
|
// permissions for each user, depending on what capabilities are enabled.
|
||||||
|
func handleNAMES(c *Client, e Event) {
|
||||||
|
if len(e.Params) < 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
channel := c.state.lookupChannel(e.Params[len(e.Params)-1])
|
||||||
|
if channel == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(e.Trailing, " ")
|
||||||
|
|
||||||
|
var host, ident, modes, nick string
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
c.state.Lock()
|
||||||
|
for i := 0; i < len(parts); i++ {
|
||||||
|
modes, nick, ok = parseUserPrefix(parts[i])
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If userhost-in-names.
|
||||||
|
if strings.Contains(nick, "@") {
|
||||||
|
s := ParseSource(nick)
|
||||||
|
if s == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
host = s.Host
|
||||||
|
nick = s.Name
|
||||||
|
ident = s.Ident
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsValidNick(nick) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.createUser(nick)
|
||||||
|
user := c.state.lookupUser(nick)
|
||||||
|
if user == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
user.addChannel(channel.Name)
|
||||||
|
channel.addUser(nick)
|
||||||
|
|
||||||
|
// Add necessary userhost-in-names data into the user.
|
||||||
|
if host != "" {
|
||||||
|
user.Host = host
|
||||||
|
}
|
||||||
|
if ident != "" {
|
||||||
|
user.Ident = ident
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't append modes, overwrite them.
|
||||||
|
perms, _ := user.Perms.Lookup(channel.Name)
|
||||||
|
perms.set(modes, false)
|
||||||
|
user.Perms.set(channel.Name, perms)
|
||||||
|
}
|
||||||
|
c.state.Unlock()
|
||||||
|
c.state.notify(c, UPDATE_STATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateLastActive is a wrapper for any event which the source author
|
||||||
|
// should have it's LastActive time updated. This is useful for things like
|
||||||
|
// a KICK where we know they are active, as they just kicked another user,
|
||||||
|
// even though they may not be talking.
|
||||||
|
func updateLastActive(c *Client, e Event) {
|
||||||
|
if e.Source == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.Lock()
|
||||||
|
|
||||||
|
// Update the users last active time, if they exist.
|
||||||
|
user := c.state.lookupUser(e.Source.Name)
|
||||||
|
if user == nil {
|
||||||
|
c.state.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user.LastActive = time.Now()
|
||||||
|
c.state.Unlock()
|
||||||
|
}
|
639
vendor/github.com/lrstanley/girc/cap.go
generated
vendored
Normal file
639
vendor/github.com/lrstanley/girc/cap.go
generated
vendored
Normal file
@ -0,0 +1,639 @@
|
|||||||
|
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||||
|
// of this source code is governed by the MIT license that can be found in
|
||||||
|
// the LICENSE file.
|
||||||
|
|
||||||
|
package girc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var possibleCap = map[string][]string{
|
||||||
|
"account-notify": nil,
|
||||||
|
"account-tag": nil,
|
||||||
|
"away-notify": nil,
|
||||||
|
"batch": nil,
|
||||||
|
"cap-notify": nil,
|
||||||
|
"chghost": nil,
|
||||||
|
"extended-join": nil,
|
||||||
|
"invite-notify": nil,
|
||||||
|
"message-tags": nil,
|
||||||
|
"multi-prefix": nil,
|
||||||
|
"userhost-in-names": nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) listCAP() {
|
||||||
|
if !c.Config.disableTracking {
|
||||||
|
c.write(&Event{Command: CAP, Params: []string{CAP_LS, "302"}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func possibleCapList(c *Client) map[string][]string {
|
||||||
|
out := make(map[string][]string)
|
||||||
|
|
||||||
|
if c.Config.SASL != nil {
|
||||||
|
out["sasl"] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range c.Config.SupportedCaps {
|
||||||
|
out[k] = c.Config.SupportedCaps[k]
|
||||||
|
}
|
||||||
|
|
||||||
|
for k := range possibleCap {
|
||||||
|
out[k] = possibleCap[k]
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCap(raw string) map[string][]string {
|
||||||
|
out := make(map[string][]string)
|
||||||
|
parts := strings.Split(raw, " ")
|
||||||
|
|
||||||
|
var val int
|
||||||
|
|
||||||
|
for i := 0; i < len(parts); i++ {
|
||||||
|
val = strings.IndexByte(parts[i], prefixTagValue) // =
|
||||||
|
|
||||||
|
// No value splitter, or has splitter but no trailing value.
|
||||||
|
if val < 1 || len(parts[i]) < val+1 {
|
||||||
|
// The capability doesn't contain a value.
|
||||||
|
out[parts[i]] = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
out[parts[i][:val]] = strings.Split(parts[i][val+1:], ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCAP attempts to find out what IRCv3 capabilities the server supports.
|
||||||
|
// This will lock further registration until we have acknowledged the
|
||||||
|
// capabilities.
|
||||||
|
func handleCAP(c *Client, e Event) {
|
||||||
|
if len(e.Params) >= 2 && (e.Params[1] == CAP_NEW || e.Params[1] == CAP_DEL) {
|
||||||
|
c.listCAP()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can assume there was a failure attempting to enable a capability.
|
||||||
|
if len(e.Params) == 2 && e.Params[1] == CAP_NAK {
|
||||||
|
// Let the server know that we're done.
|
||||||
|
c.write(&Event{Command: CAP, Params: []string{CAP_END}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
possible := possibleCapList(c)
|
||||||
|
|
||||||
|
if len(e.Params) >= 2 && len(e.Trailing) > 1 && e.Params[1] == CAP_LS {
|
||||||
|
c.state.Lock()
|
||||||
|
|
||||||
|
caps := parseCap(e.Trailing)
|
||||||
|
|
||||||
|
for k := range caps {
|
||||||
|
if _, ok := possible[k]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(possible[k]) == 0 || len(caps[k]) == 0 {
|
||||||
|
c.state.tmpCap = append(c.state.tmpCap, k)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var contains bool
|
||||||
|
for i := 0; i < len(caps[k]); i++ {
|
||||||
|
for j := 0; j < len(possible[k]); j++ {
|
||||||
|
if caps[k][i] == possible[k][j] {
|
||||||
|
// Assume we have a matching split value.
|
||||||
|
contains = true
|
||||||
|
goto checkcontains
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkcontains:
|
||||||
|
if !contains {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.tmpCap = append(c.state.tmpCap, k)
|
||||||
|
}
|
||||||
|
c.state.Unlock()
|
||||||
|
|
||||||
|
// Indicates if this is a multi-line LS. (2 args means it's the
|
||||||
|
// last LS).
|
||||||
|
if len(e.Params) == 2 {
|
||||||
|
// If we support no caps, just ack the CAP message and END.
|
||||||
|
if len(c.state.tmpCap) == 0 {
|
||||||
|
c.write(&Event{Command: CAP, Params: []string{CAP_END}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let them know which ones we'd like to enable.
|
||||||
|
c.write(&Event{Command: CAP, Params: []string{CAP_REQ}, Trailing: strings.Join(c.state.tmpCap, " ")})
|
||||||
|
|
||||||
|
// Re-initialize the tmpCap, so if we get multiple 'CAP LS' requests
|
||||||
|
// due to cap-notify, we can re-evaluate what we can support.
|
||||||
|
c.state.Lock()
|
||||||
|
c.state.tmpCap = []string{}
|
||||||
|
c.state.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.Params) == 2 && len(e.Trailing) > 1 && e.Params[1] == CAP_ACK {
|
||||||
|
c.state.Lock()
|
||||||
|
c.state.enabledCap = strings.Split(e.Trailing, " ")
|
||||||
|
|
||||||
|
// Do we need to do sasl auth?
|
||||||
|
wantsSASL := false
|
||||||
|
for i := 0; i < len(c.state.enabledCap); i++ {
|
||||||
|
if c.state.enabledCap[i] == "sasl" {
|
||||||
|
wantsSASL = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.state.Unlock()
|
||||||
|
|
||||||
|
if wantsSASL {
|
||||||
|
c.write(&Event{Command: AUTHENTICATE, Params: []string{c.Config.SASL.Method()}})
|
||||||
|
// Don't "CAP END", since we want to authenticate.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let the server know that we're done.
|
||||||
|
c.write(&Event{Command: CAP, Params: []string{CAP_END}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SASLMech is an representation of what a SASL mechanism should support.
|
||||||
|
// See SASLExternal and SASLPlain for implementations of this.
|
||||||
|
type SASLMech interface {
|
||||||
|
// Method returns the uppercase version of the SASL mechanism name.
|
||||||
|
Method() string
|
||||||
|
// Encode returns the response that the SASL mechanism wants to use. If
|
||||||
|
// the returned string is empty (e.g. the mechanism gives up), the handler
|
||||||
|
// will attempt to panic, as expectation is that if SASL authentication
|
||||||
|
// fails, the client will disconnect.
|
||||||
|
Encode(params []string) (output string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SASLExternal implements the "EXTERNAL" SASL type.
|
||||||
|
type SASLExternal struct {
|
||||||
|
// Identity is an optional field which allows the client to specify
|
||||||
|
// pre-authentication identification. This means that EXTERNAL will
|
||||||
|
// supply this in the initial response. This usually isn't needed (e.g.
|
||||||
|
// CertFP).
|
||||||
|
Identity string `json:"identity"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method identifies what type of SASL this implements.
|
||||||
|
func (sasl *SASLExternal) Method() string {
|
||||||
|
return "EXTERNAL"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode for external SALS authentication should really only return a "+",
|
||||||
|
// unless the user has specified pre-authentication or identification data.
|
||||||
|
// See https://tools.ietf.org/html/rfc4422#appendix-A for more info.
|
||||||
|
func (sasl *SASLExternal) Encode(params []string) string {
|
||||||
|
if len(params) != 1 || params[0] != "+" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if sasl.Identity != "" {
|
||||||
|
return sasl.Identity
|
||||||
|
}
|
||||||
|
|
||||||
|
return "+"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SASLPlain contains the user and password needed for PLAIN SASL authentication.
|
||||||
|
type SASLPlain struct {
|
||||||
|
User string `json:"user"` // User is the username for SASL.
|
||||||
|
Pass string `json:"pass"` // Pass is the password for SASL.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method identifies what type of SASL this implements.
|
||||||
|
func (sasl *SASLPlain) Method() string {
|
||||||
|
return "PLAIN"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode encodes the plain user+password into a SASL PLAIN implementation.
|
||||||
|
// See https://tools.ietf.org/rfc/rfc4422.txt for more info.
|
||||||
|
func (sasl *SASLPlain) Encode(params []string) string {
|
||||||
|
if len(params) != 1 || params[0] != "+" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
in := []byte(sasl.User)
|
||||||
|
|
||||||
|
in = append(in, 0x0)
|
||||||
|
in = append(in, []byte(sasl.User)...)
|
||||||
|
in = append(in, 0x0)
|
||||||
|
in = append(in, []byte(sasl.Pass)...)
|
||||||
|
|
||||||
|
return base64.StdEncoding.EncodeToString(in)
|
||||||
|
}
|
||||||
|
|
||||||
|
const saslChunkSize = 400
|
||||||
|
|
||||||
|
func handleSASL(c *Client, e Event) {
|
||||||
|
if e.Command == RPL_SASLSUCCESS || e.Command == ERR_SASLALREADY {
|
||||||
|
// Let the server know that we're done.
|
||||||
|
c.write(&Event{Command: CAP, Params: []string{CAP_END}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assume they want us to handle sending auth.
|
||||||
|
auth := c.Config.SASL.Encode(e.Params)
|
||||||
|
|
||||||
|
if auth == "" {
|
||||||
|
// Assume the SASL authentication method doesn't want to respond for
|
||||||
|
// some reason. The SASL spec and IRCv3 spec do not define a clear
|
||||||
|
// way to abort a SASL exchange, other than to disconnect, or proceed
|
||||||
|
// with CAP END.
|
||||||
|
c.rx <- &Event{Command: ERROR, Trailing: fmt.Sprintf(
|
||||||
|
"closing connection: invalid %s SASL configuration provided: %s",
|
||||||
|
c.Config.SASL.Method(), e.Trailing,
|
||||||
|
)}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send in "saslChunkSize"-length byte chunks. If the last chuck is
|
||||||
|
// exactly "saslChunkSize" bytes, send a "AUTHENTICATE +" 0-byte
|
||||||
|
// acknowledgement response to let the server know that we're done.
|
||||||
|
for {
|
||||||
|
if len(auth) > saslChunkSize {
|
||||||
|
c.write(&Event{Command: AUTHENTICATE, Params: []string{auth[0 : saslChunkSize-1]}, Sensitive: true})
|
||||||
|
auth = auth[saslChunkSize:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(auth) <= saslChunkSize {
|
||||||
|
c.write(&Event{Command: AUTHENTICATE, Params: []string{auth}, Sensitive: true})
|
||||||
|
|
||||||
|
if len(auth) == 400 {
|
||||||
|
c.write(&Event{Command: AUTHENTICATE, Params: []string{"+"}})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSASLError(c *Client, e Event) {
|
||||||
|
if c.Config.SASL == nil {
|
||||||
|
c.write(&Event{Command: CAP, Params: []string{CAP_END}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authentication failed. The SASL spec and IRCv3 spec do not define a
|
||||||
|
// clear way to abort a SASL exchange, other than to disconnect, or
|
||||||
|
// proceed with CAP END.
|
||||||
|
c.rx <- &Event{Command: ERROR, Trailing: "closing connection: " + e.Trailing}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCHGHOST handles incoming IRCv3 hostname change events. CHGHOST is
|
||||||
|
// what occurs (when enabled) when a servers services change the hostname of
|
||||||
|
// a user. Traditionally, this was simply resolved with a quick QUIT and JOIN,
|
||||||
|
// however CHGHOST resolves this in a much cleaner fashion.
|
||||||
|
func handleCHGHOST(c *Client, e Event) {
|
||||||
|
if len(e.Params) != 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.Lock()
|
||||||
|
user := c.state.lookupUser(e.Source.Name)
|
||||||
|
if user != nil {
|
||||||
|
user.Ident = e.Params[0]
|
||||||
|
user.Host = e.Params[1]
|
||||||
|
}
|
||||||
|
c.state.Unlock()
|
||||||
|
c.state.notify(c, UPDATE_STATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAWAY handles incoming IRCv3 AWAY events, for which are sent both
|
||||||
|
// when users are no longer away, or when they are away.
|
||||||
|
func handleAWAY(c *Client, e Event) {
|
||||||
|
c.state.Lock()
|
||||||
|
user := c.state.lookupUser(e.Source.Name)
|
||||||
|
if user != nil {
|
||||||
|
user.Extras.Away = e.Trailing
|
||||||
|
}
|
||||||
|
c.state.Unlock()
|
||||||
|
c.state.notify(c, UPDATE_STATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleACCOUNT handles incoming IRCv3 ACCOUNT events. ACCOUNT is sent when
|
||||||
|
// a user logs into an account, logs out of their account, or logs into a
|
||||||
|
// different account. The account backend is handled server-side, so this
|
||||||
|
// could be NickServ, X (undernet?), etc.
|
||||||
|
func handleACCOUNT(c *Client, e Event) {
|
||||||
|
if len(e.Params) != 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
account := e.Params[0]
|
||||||
|
if account == "*" {
|
||||||
|
account = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.Lock()
|
||||||
|
user := c.state.lookupUser(e.Source.Name)
|
||||||
|
if user != nil {
|
||||||
|
user.Extras.Account = account
|
||||||
|
}
|
||||||
|
c.state.Unlock()
|
||||||
|
c.state.notify(c, UPDATE_STATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleTags handles any messages that have tags that will affect state. (e.g.
|
||||||
|
// 'account' tags.)
|
||||||
|
func handleTags(c *Client, e Event) {
|
||||||
|
if len(e.Tags) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
account, ok := e.Tags.Get("account")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.Lock()
|
||||||
|
user := c.state.lookupUser(e.Source.Name)
|
||||||
|
if user != nil {
|
||||||
|
user.Extras.Account = account
|
||||||
|
}
|
||||||
|
c.state.Unlock()
|
||||||
|
c.state.notify(c, UPDATE_STATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
prefixTag byte = 0x40 // @
|
||||||
|
prefixTagValue byte = 0x3D // =
|
||||||
|
prefixUserTag byte = 0x2B // +
|
||||||
|
tagSeparator byte = 0x3B // ;
|
||||||
|
maxTagLength int = 511 // 510 + @ and " " (space), though space usually not included.
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tags represents the key-value pairs in IRCv3 message tags. The map contains
|
||||||
|
// the encoded message-tag values. If the tag is present, it may still be
|
||||||
|
// empty. See Tags.Get() and Tags.Set() for use with getting/setting
|
||||||
|
// information within the tags.
|
||||||
|
//
|
||||||
|
// Note that retrieving and setting tags are not concurrent safe. If this is
|
||||||
|
// necessary, you will need to implement it yourself.
|
||||||
|
type Tags map[string]string
|
||||||
|
|
||||||
|
// ParseTags parses out the key-value map of tags. raw should only be the tag
|
||||||
|
// data, not a full message. For example:
|
||||||
|
// @aaa=bbb;ccc;example.com/ddd=eee
|
||||||
|
// NOT:
|
||||||
|
// @aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me :Hello
|
||||||
|
func ParseTags(raw string) (t Tags) {
|
||||||
|
t = make(Tags)
|
||||||
|
|
||||||
|
if len(raw) > 0 && raw[0] == prefixTag {
|
||||||
|
raw = raw[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(raw, string(tagSeparator))
|
||||||
|
var hasValue int
|
||||||
|
|
||||||
|
for i := 0; i < len(parts); i++ {
|
||||||
|
hasValue = strings.IndexByte(parts[i], prefixTagValue)
|
||||||
|
|
||||||
|
// The tag doesn't contain a value or has a splitter with no value.
|
||||||
|
if hasValue < 1 || len(parts[i]) < hasValue+1 {
|
||||||
|
if !validTag(parts[i]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
t[parts[i]] = ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if tag key or decoded value are invalid.
|
||||||
|
if !validTag(parts[i][:hasValue]) || !validTagValue(tagDecoder.Replace(parts[i][hasValue+1:])) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
t[parts[i][:hasValue]] = parts[i][hasValue+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len determines the length of the bytes representation of this tag map. This
|
||||||
|
// does not include the trailing space required when creating an event, but
|
||||||
|
// does include the tag prefix ("@").
|
||||||
|
func (t Tags) Len() (length int) {
|
||||||
|
if t == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(t.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count finds how many total tags that there are.
|
||||||
|
func (t Tags) Count() int {
|
||||||
|
if t == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns a []byte representation of this tag map, including the tag
|
||||||
|
// prefix ("@"). Note that this will return the tags sorted, regardless of
|
||||||
|
// the order of how they were originally parsed.
|
||||||
|
func (t Tags) Bytes() []byte {
|
||||||
|
if t == nil {
|
||||||
|
return []byte{}
|
||||||
|
}
|
||||||
|
|
||||||
|
max := len(t)
|
||||||
|
if max == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
buffer.WriteByte(prefixTag)
|
||||||
|
|
||||||
|
var current int
|
||||||
|
|
||||||
|
// Sort the writing of tags so we can at least guarantee that they will
|
||||||
|
// be in order, and testable.
|
||||||
|
var names []string
|
||||||
|
for tagName := range t {
|
||||||
|
names = append(names, tagName)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
|
||||||
|
for i := 0; i < len(names); i++ {
|
||||||
|
// Trim at max allowed chars.
|
||||||
|
if (buffer.Len() + len(names[i]) + len(t[names[i]]) + 2) > maxTagLength {
|
||||||
|
return buffer.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(names[i])
|
||||||
|
|
||||||
|
// Write the value as necessary.
|
||||||
|
if len(t[names[i]]) > 0 {
|
||||||
|
buffer.WriteByte(prefixTagValue)
|
||||||
|
buffer.WriteString(t[names[i]])
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the separator ";" between tags.
|
||||||
|
if current < max-1 {
|
||||||
|
buffer.WriteByte(tagSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
current++
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of this tag map.
|
||||||
|
func (t Tags) String() string {
|
||||||
|
if t == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(t.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeTo writes the necessary tag bytes to an io.Writer, including a trailing
|
||||||
|
// space-separator.
|
||||||
|
func (t Tags) writeTo(w io.Writer) (n int, err error) {
|
||||||
|
b := t.Bytes()
|
||||||
|
if len(b) == 0 {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err = w.Write(b)
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var j int
|
||||||
|
j, err = w.Write([]byte{eventSpace})
|
||||||
|
n += j
|
||||||
|
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// tagDecode are encoded -> decoded pairs for replacement to decode.
|
||||||
|
var tagDecode = []string{
|
||||||
|
"\\:", ";",
|
||||||
|
"\\s", " ",
|
||||||
|
"\\\\", "\\",
|
||||||
|
"\\r", "\r",
|
||||||
|
"\\n", "\n",
|
||||||
|
}
|
||||||
|
var tagDecoder = strings.NewReplacer(tagDecode...)
|
||||||
|
|
||||||
|
// tagEncode are decoded -> encoded pairs for replacement to decode.
|
||||||
|
var tagEncode = []string{
|
||||||
|
";", "\\:",
|
||||||
|
" ", "\\s",
|
||||||
|
"\\", "\\\\",
|
||||||
|
"\r", "\\r",
|
||||||
|
"\n", "\\n",
|
||||||
|
}
|
||||||
|
var tagEncoder = strings.NewReplacer(tagEncode...)
|
||||||
|
|
||||||
|
// Get returns the unescaped value of given tag key. Note that this is not
|
||||||
|
// concurrent safe.
|
||||||
|
func (t Tags) Get(key string) (tag string, success bool) {
|
||||||
|
if t == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := t[key]; ok {
|
||||||
|
tag = tagDecoder.Replace(t[key])
|
||||||
|
success = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag, success
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set escapes given value and saves it as the value for given key. Note that
|
||||||
|
// this is not concurrent safe.
|
||||||
|
func (t Tags) Set(key, value string) error {
|
||||||
|
if t == nil {
|
||||||
|
t = make(Tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !validTag(key) {
|
||||||
|
return fmt.Errorf("tag key %q is invalid", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
value = tagEncoder.Replace(value)
|
||||||
|
|
||||||
|
if len(value) > 0 && !validTagValue(value) {
|
||||||
|
return fmt.Errorf("tag value %q of key %q is invalid", value, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check to make sure it's not too long here.
|
||||||
|
if (t.Len() + len(key) + len(value) + 2) > maxTagLength {
|
||||||
|
return fmt.Errorf("unable to set tag %q [value %q]: tags too long for message", key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
t[key] = value
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove deletes the tag frwom the tag map.
|
||||||
|
func (t Tags) Remove(key string) (success bool) {
|
||||||
|
if t == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, success = t[key]; success {
|
||||||
|
delete(t, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return success
|
||||||
|
}
|
||||||
|
|
||||||
|
// validTag validates an IRC tag.
|
||||||
|
func validTag(name string) bool {
|
||||||
|
if len(name) < 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow user tags to be passed to validTag.
|
||||||
|
if len(name) >= 2 && name[0] == prefixUserTag {
|
||||||
|
name = name[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(name); i++ {
|
||||||
|
// A-Z, a-z, 0-9, -/._
|
||||||
|
if (name[i] < 0x41 || name[i] > 0x5A) && (name[i] < 0x61 || name[i] > 0x7A) && (name[i] < 0x2D || name[i] > 0x39) && name[i] != 0x5F {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// validTagValue valids a decoded IRC tag value. If the value is not decoded
|
||||||
|
// with tagDecoder first, it may be seen as invalid.
|
||||||
|
func validTagValue(value string) bool {
|
||||||
|
for i := 0; i < len(value); i++ {
|
||||||
|
// Don't allow any invisible chars within the tag, or semicolons.
|
||||||
|
if value[i] < 0x21 || value[i] > 0x7E || value[i] == 0x3B {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
615
vendor/github.com/lrstanley/girc/client.go
generated
vendored
Normal file
615
vendor/github.com/lrstanley/girc/client.go
generated
vendored
Normal file
@ -0,0 +1,615 @@
|
|||||||
|
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||||
|
// of this source code is governed by the MIT license that can be found in
|
||||||
|
// the LICENSE file.
|
||||||
|
|
||||||
|
package girc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client contains all of the information necessary to run a single IRC
|
||||||
|
// client.
|
||||||
|
type Client struct {
|
||||||
|
// Config represents the configuration. Please take extra caution in that
|
||||||
|
// entries in this are not edited while the client is connected, to prevent
|
||||||
|
// data races. This is NOT concurrent safe to update.
|
||||||
|
Config Config
|
||||||
|
// rx is a buffer of events waiting to be processed.
|
||||||
|
rx chan *Event
|
||||||
|
// tx is a buffer of events waiting to be sent.
|
||||||
|
tx chan *Event
|
||||||
|
// state represents the throw-away state for the irc session.
|
||||||
|
state *state
|
||||||
|
// initTime represents the creation time of the client.
|
||||||
|
initTime time.Time
|
||||||
|
// Handlers is a handler which manages internal and external handlers.
|
||||||
|
Handlers *Caller
|
||||||
|
// CTCP is a handler which manages internal and external CTCP handlers.
|
||||||
|
CTCP *CTCP
|
||||||
|
// Cmd contains various helper methods to interact with the server.
|
||||||
|
Cmd *Commands
|
||||||
|
// mu is the mux used for connections/disconnections from the server,
|
||||||
|
// so multiple threads aren't trying to connect at the same time, and
|
||||||
|
// vice versa.
|
||||||
|
mu sync.RWMutex
|
||||||
|
// stop is used to communicate with Connect(), letting it know that the
|
||||||
|
// client wishes to cancel/close.
|
||||||
|
stop context.CancelFunc
|
||||||
|
// conn is a net.Conn reference to the IRC server. If this is nil, it is
|
||||||
|
// safe to assume that we're not connected. If this is not nil, this
|
||||||
|
// means we're either connected, connecting, or cleaning up. This should
|
||||||
|
// be guarded with Client.mu.
|
||||||
|
conn *ircConn
|
||||||
|
// debug is used if a writer is supplied for Client.Config.Debugger.
|
||||||
|
debug *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config contains configuration options for an IRC client
|
||||||
|
type Config struct {
|
||||||
|
// Server is a host/ip of the server you want to connect to. This only
|
||||||
|
// has an affect during the dial process
|
||||||
|
Server string
|
||||||
|
// ServerPass is the server password used to authenticate. This only has
|
||||||
|
// an affect during the dial process.
|
||||||
|
ServerPass string
|
||||||
|
// Port is the port that will be used during server connection. This only
|
||||||
|
// has an affect during the dial process.
|
||||||
|
Port int
|
||||||
|
// Nick is an rfc-valid nickname used during connection. This only has an
|
||||||
|
// affect during the dial process.
|
||||||
|
Nick string
|
||||||
|
// User is the username/ident to use on connect. Ignored if an identd
|
||||||
|
// server is used. This only has an affect during the dial process.
|
||||||
|
User string
|
||||||
|
// Name is the "realname" that's used during connection. This only has an
|
||||||
|
// affect during the dial process.
|
||||||
|
Name string
|
||||||
|
// SASL contains the necessary authentication data to authenticate
|
||||||
|
// with SASL. See the documentation for SASLMech for what is currently
|
||||||
|
// supported. Capability tracking must be enabled for this to work, as
|
||||||
|
// this requires IRCv3 CAP handling.
|
||||||
|
SASL SASLMech
|
||||||
|
// Bind is used to bind to a specific host or ip during the dial process
|
||||||
|
// when connecting to the server. This can be a hostname, however it must
|
||||||
|
// resolve to an IPv4/IPv6 address bindable on your system. Otherwise,
|
||||||
|
// you can simply use a IPv4/IPv6 address directly. This only has an
|
||||||
|
// affect during the dial process and will not work with DialerConnect().
|
||||||
|
Bind string
|
||||||
|
// SSL allows dialing via TLS. See TLSConfig to set your own TLS
|
||||||
|
// configuration (e.g. to not force hostname checking). This only has an
|
||||||
|
// affect during the dial process.
|
||||||
|
SSL bool
|
||||||
|
// TLSConfig is an optional user-supplied tls configuration, used during
|
||||||
|
// socket creation to the server. SSL must be enabled for this to be used.
|
||||||
|
// This only has an affect during the dial process.
|
||||||
|
TLSConfig *tls.Config
|
||||||
|
// AllowFlood allows the client to bypass the rate limit of outbound
|
||||||
|
// messages.
|
||||||
|
AllowFlood bool
|
||||||
|
// GlobalFormat enables passing through all events which have trailing
|
||||||
|
// text through the color Fmt() function, so you don't have to wrap
|
||||||
|
// every response in the Fmt() method.
|
||||||
|
//
|
||||||
|
// Note that this only actually applies to PRIVMSG, NOTICE and TOPIC
|
||||||
|
// events, to ensure it doesn't clobber unwanted events.
|
||||||
|
GlobalFormat bool
|
||||||
|
// Debug is an optional, user supplied location to log the raw lines
|
||||||
|
// sent from the server, or other useful debug logs. Defaults to
|
||||||
|
// ioutil.Discard. For quick debugging, this could be set to os.Stdout.
|
||||||
|
Debug io.Writer
|
||||||
|
// Out is used to write out a prettified version of incoming events. For
|
||||||
|
// example, channel JOIN/PART, PRIVMSG/NOTICE, KICk, etc. Useful to get
|
||||||
|
// a brief output of the activity of the client. If you are looking to
|
||||||
|
// log raw messages, look at a handler and girc.ALLEVENTS and the relevant
|
||||||
|
// Event.Bytes() or Event.String() methods.
|
||||||
|
Out io.Writer
|
||||||
|
// RecoverFunc is called when a handler throws a panic. If RecoverFunc is
|
||||||
|
// set, the panic will be considered recovered, otherwise the client will
|
||||||
|
// panic. Set this to DefaultRecoverHandler if you don't want the client
|
||||||
|
// to panic, however you don't want to handle the panic yourself.
|
||||||
|
// DefaultRecoverHandler will log the panic to Debug or os.Stdout if
|
||||||
|
// Debug is unset.
|
||||||
|
RecoverFunc func(c *Client, e *HandlerError)
|
||||||
|
// SupportedCaps are the IRCv3 capabilities you would like the client to
|
||||||
|
// support on top of the ones which the client already supports (see
|
||||||
|
// cap.go for which ones the client enables by default). Only use this
|
||||||
|
// if you have not called DisableTracking(). The keys value gets passed
|
||||||
|
// to the server if supported.
|
||||||
|
SupportedCaps map[string][]string
|
||||||
|
// Version is the application version information that will be used in
|
||||||
|
// response to a CTCP VERSION, if default CTCP replies have not been
|
||||||
|
// overwritten or a VERSION handler was already supplied.
|
||||||
|
Version string
|
||||||
|
// PingDelay is the frequency between when the client sends a keep-alive
|
||||||
|
// PING to the server, and awaits a response (and times out if the server
|
||||||
|
// doesn't respond in time). This should be between 20-600 seconds. See
|
||||||
|
// Client.Lag() if you want to determine the delay between the server
|
||||||
|
// and the client. If this is set to -1, the client will not attempt to
|
||||||
|
// send client -> server PING requests.
|
||||||
|
PingDelay time.Duration
|
||||||
|
|
||||||
|
// disableTracking disables all channel and user-level tracking. Useful
|
||||||
|
// for highly embedded scripts with single purposes. This has an exported
|
||||||
|
// method which enables this and ensures prop cleanup, see
|
||||||
|
// Client.DisableTracking().
|
||||||
|
disableTracking bool
|
||||||
|
// HandleNickCollide when set, allows the client to handle nick collisions
|
||||||
|
// in a custom way. If unset, the client will attempt to append a
|
||||||
|
// underscore to the end of the nickname, in order to bypass using
|
||||||
|
// an invalid nickname. For example, if "test" is already in use, or is
|
||||||
|
// blocked by the network/a service, the client will try and use "test_",
|
||||||
|
// then it will attempt "test__", "test___", and so on.
|
||||||
|
HandleNickCollide func(oldNick string) (newNick string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrInvalidConfig is returned when the configuration passed to the client
|
||||||
|
// is invalid.
|
||||||
|
type ErrInvalidConfig struct {
|
||||||
|
Conf Config // Conf is the configuration that was not valid.
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrInvalidConfig) Error() string { return "invalid configuration: " + e.err.Error() }
|
||||||
|
|
||||||
|
// isValid checks some basic settings to ensure the config is valid.
|
||||||
|
func (conf *Config) isValid() error {
|
||||||
|
if conf.Server == "" {
|
||||||
|
return &ErrInvalidConfig{Conf: *conf, err: errors.New("empty server")}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default port to 6667 (the standard IRC port).
|
||||||
|
if conf.Port == 0 {
|
||||||
|
conf.Port = 6667
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Port < 21 || conf.Port > 65535 {
|
||||||
|
return &ErrInvalidConfig{Conf: *conf, err: errors.New("port outside valid range (21-65535)")}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsValidNick(conf.Nick) {
|
||||||
|
return &ErrInvalidConfig{Conf: *conf, err: errors.New("bad nickname specified")}
|
||||||
|
}
|
||||||
|
if !IsValidUser(conf.User) {
|
||||||
|
return &ErrInvalidConfig{Conf: *conf, err: errors.New("bad user/ident specified")}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrNotConnected is returned if a method is used when the client isn't
|
||||||
|
// connected.
|
||||||
|
var ErrNotConnected = errors.New("client is not connected to server")
|
||||||
|
|
||||||
|
// ErrDisconnected is called when Config.Retries is less than 1, and we
|
||||||
|
// non-intentionally disconnected from the server.
|
||||||
|
var ErrDisconnected = errors.New("unexpectedly disconnected")
|
||||||
|
|
||||||
|
// ErrInvalidTarget should be returned if the target which you are
|
||||||
|
// attempting to send an event to is invalid or doesn't match RFC spec.
|
||||||
|
type ErrInvalidTarget struct {
|
||||||
|
Target string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrInvalidTarget) Error() string { return "invalid target: " + e.Target }
|
||||||
|
|
||||||
|
// New creates a new IRC client with the specified server, name and config.
|
||||||
|
func New(config Config) *Client {
|
||||||
|
c := &Client{
|
||||||
|
Config: config,
|
||||||
|
rx: make(chan *Event, 25),
|
||||||
|
tx: make(chan *Event, 25),
|
||||||
|
CTCP: newCTCP(),
|
||||||
|
initTime: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Cmd = &Commands{c: c}
|
||||||
|
|
||||||
|
if c.Config.PingDelay >= 0 && c.Config.PingDelay < (20*time.Second) {
|
||||||
|
c.Config.PingDelay = 20 * time.Second
|
||||||
|
} else if c.Config.PingDelay > (600 * time.Second) {
|
||||||
|
c.Config.PingDelay = 600 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Config.Debug == nil {
|
||||||
|
c.debug = log.New(ioutil.Discard, "", 0)
|
||||||
|
} else {
|
||||||
|
c.debug = log.New(c.Config.Debug, "debug:", log.Ltime|log.Lshortfile)
|
||||||
|
c.debug.Print("initializing debugging")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the caller.
|
||||||
|
c.Handlers = newCaller(c.debug)
|
||||||
|
|
||||||
|
// Give ourselves a new state.
|
||||||
|
c.state = &state{}
|
||||||
|
c.state.reset()
|
||||||
|
|
||||||
|
// Register builtin handlers.
|
||||||
|
c.registerBuiltins()
|
||||||
|
|
||||||
|
// Register default CTCP responses.
|
||||||
|
c.CTCP.addDefaultHandlers()
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a brief description of the current client state.
|
||||||
|
func (c *Client) String() string {
|
||||||
|
connected := c.IsConnected()
|
||||||
|
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"<Client init:%q handlers:%d connected:%t>", c.initTime.String(), c.Handlers.Len(), connected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the network connection to the server, and sends a STOPPED
|
||||||
|
// event. This should cause Connect() to return with nil. This should be
|
||||||
|
// safe to call multiple times. See Connect()'s documentation on how
|
||||||
|
// handlers and goroutines are handled when disconnected from the server.
|
||||||
|
func (c *Client) Close() {
|
||||||
|
c.mu.RLock()
|
||||||
|
if c.stop != nil {
|
||||||
|
c.debug.Print("requesting client to stop")
|
||||||
|
c.stop()
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrEvent is an error returned when the server (or library) sends an ERROR
|
||||||
|
// message response. The string returned contains the trailing text from the
|
||||||
|
// message.
|
||||||
|
type ErrEvent struct {
|
||||||
|
Event *Event
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrEvent) Error() string {
|
||||||
|
if e.Event == nil {
|
||||||
|
return "unknown error occurred"
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.Event.Trailing
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) execLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) {
|
||||||
|
c.debug.Print("starting execLoop")
|
||||||
|
defer c.debug.Print("closing execLoop")
|
||||||
|
|
||||||
|
var event *Event
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
// We've been told to exit, however we shouldn't bail on the
|
||||||
|
// current events in the queue that should be processed, as one
|
||||||
|
// may want to handle an ERROR, QUIT, etc.
|
||||||
|
c.debug.Printf("received signal to close, flushing %d events and executing", len(c.rx))
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event = <-c.rx:
|
||||||
|
c.RunHandlers(event)
|
||||||
|
default:
|
||||||
|
goto done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done:
|
||||||
|
wg.Done()
|
||||||
|
return
|
||||||
|
case event = <-c.rx:
|
||||||
|
if event != nil && event.Command == ERROR {
|
||||||
|
// Handles incoming ERROR responses. These are only ever sent
|
||||||
|
// by the server (with the exception that this library may use
|
||||||
|
// them as a lower level way of signalling to disconnect due
|
||||||
|
// to some other client-choosen error), and should always be
|
||||||
|
// followed up by the server disconnecting the client. If for
|
||||||
|
// some reason the server doesn't disconnect the client, or
|
||||||
|
// if this library is the source of the error, this should
|
||||||
|
// signal back up to the main connect loop, to disconnect.
|
||||||
|
errs <- &ErrEvent{Event: event}
|
||||||
|
|
||||||
|
// Make sure to not actually exit, so we can let any handlers
|
||||||
|
// actually handle the ERROR event.
|
||||||
|
}
|
||||||
|
|
||||||
|
c.RunHandlers(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableTracking disables all channel/user-level/CAP tracking, and clears
|
||||||
|
// all internal handlers. Useful for highly embedded scripts with single
|
||||||
|
// purposes. This cannot be un-done on a client.
|
||||||
|
func (c *Client) DisableTracking() {
|
||||||
|
c.debug.Print("disabling tracking")
|
||||||
|
c.Config.disableTracking = true
|
||||||
|
c.Handlers.clearInternal()
|
||||||
|
|
||||||
|
c.state.Lock()
|
||||||
|
c.state.channels = nil
|
||||||
|
c.state.Unlock()
|
||||||
|
c.state.notify(c, UPDATE_STATE)
|
||||||
|
|
||||||
|
c.registerBuiltins()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server returns the string representation of host+port pair for net.Conn.
|
||||||
|
func (c *Client) Server() string {
|
||||||
|
return fmt.Sprintf("%s:%d", c.Config.Server, c.Config.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifetime returns the amount of time that has passed since the client was
|
||||||
|
// created.
|
||||||
|
func (c *Client) Lifetime() time.Duration {
|
||||||
|
return time.Since(c.initTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uptime is the time at which the client successfully connected to the
|
||||||
|
// server.
|
||||||
|
func (c *Client) Uptime() (up *time.Time, err error) {
|
||||||
|
if !c.IsConnected() {
|
||||||
|
return nil, ErrNotConnected
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
c.conn.mu.RLock()
|
||||||
|
up = c.conn.connTime
|
||||||
|
c.conn.mu.RUnlock()
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
return up, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnSince is the duration that has past since the client successfully
|
||||||
|
// connected to the server.
|
||||||
|
func (c *Client) ConnSince() (since *time.Duration, err error) {
|
||||||
|
if !c.IsConnected() {
|
||||||
|
return nil, ErrNotConnected
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
c.conn.mu.RLock()
|
||||||
|
timeSince := time.Since(*c.conn.connTime)
|
||||||
|
c.conn.mu.RUnlock()
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
return &timeSince, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsConnected returns true if the client is connected to the server.
|
||||||
|
func (c *Client) IsConnected() (connected bool) {
|
||||||
|
c.mu.RLock()
|
||||||
|
if c.conn == nil {
|
||||||
|
c.mu.RUnlock()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
c.conn.mu.RLock()
|
||||||
|
connected = c.conn.connected
|
||||||
|
c.conn.mu.RUnlock()
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
return connected
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNick returns the current nickname of the active connection. Panics if
|
||||||
|
// tracking is disabled.
|
||||||
|
func (c *Client) GetNick() string {
|
||||||
|
c.panicIfNotTracking()
|
||||||
|
|
||||||
|
c.state.RLock()
|
||||||
|
defer c.state.RUnlock()
|
||||||
|
|
||||||
|
if c.state.nick == "" {
|
||||||
|
return c.Config.Nick
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.state.nick
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIdent returns the current ident of the active connection. Panics if
|
||||||
|
// tracking is disabled. May be empty, as this is obtained from when we join
|
||||||
|
// a channel, as there is no other more efficient method to return this info.
|
||||||
|
func (c *Client) GetIdent() string {
|
||||||
|
c.panicIfNotTracking()
|
||||||
|
|
||||||
|
c.state.RLock()
|
||||||
|
defer c.state.RUnlock()
|
||||||
|
|
||||||
|
if c.state.ident == "" {
|
||||||
|
return c.Config.User
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.state.ident
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHost returns the current host of the active connection. Panics if
|
||||||
|
// tracking is disabled. May be empty, as this is obtained from when we join
|
||||||
|
// a channel, as there is no other more efficient method to return this info.
|
||||||
|
func (c *Client) GetHost() string {
|
||||||
|
c.panicIfNotTracking()
|
||||||
|
|
||||||
|
c.state.RLock()
|
||||||
|
defer c.state.RUnlock()
|
||||||
|
|
||||||
|
return c.state.host
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channels returns the active list of channels that the client is in.
|
||||||
|
// Panics if tracking is disabled.
|
||||||
|
func (c *Client) Channels() []string {
|
||||||
|
c.panicIfNotTracking()
|
||||||
|
|
||||||
|
c.state.RLock()
|
||||||
|
channels := make([]string, len(c.state.channels))
|
||||||
|
var i int
|
||||||
|
for channel := range c.state.channels {
|
||||||
|
channels[i] = c.state.channels[channel].Name
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
c.state.RUnlock()
|
||||||
|
sort.Strings(channels)
|
||||||
|
|
||||||
|
return channels
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users returns the active list of users that the client is tracking across
|
||||||
|
// all files. Panics if tracking is disabled.
|
||||||
|
func (c *Client) Users() []string {
|
||||||
|
c.panicIfNotTracking()
|
||||||
|
|
||||||
|
c.state.RLock()
|
||||||
|
users := make([]string, len(c.state.users))
|
||||||
|
var i int
|
||||||
|
for user := range c.state.users {
|
||||||
|
users[i] = c.state.users[user].Nick
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
c.state.RUnlock()
|
||||||
|
sort.Strings(users)
|
||||||
|
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupChannel looks up a given channel in state. If the channel doesn't
|
||||||
|
// exist, nil is returned. Panics if tracking is disabled.
|
||||||
|
func (c *Client) LookupChannel(name string) *Channel {
|
||||||
|
c.panicIfNotTracking()
|
||||||
|
if name == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.RLock()
|
||||||
|
defer c.state.RUnlock()
|
||||||
|
|
||||||
|
channel := c.state.lookupChannel(name)
|
||||||
|
if channel == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return channel.Copy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupUser looks up a given user in state. If the user doesn't exist, nil
|
||||||
|
// is returned. Panics if tracking is disabled.
|
||||||
|
func (c *Client) LookupUser(nick string) *User {
|
||||||
|
c.panicIfNotTracking()
|
||||||
|
if nick == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.RLock()
|
||||||
|
defer c.state.RUnlock()
|
||||||
|
|
||||||
|
user := c.state.lookupUser(nick)
|
||||||
|
if user == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.Copy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsInChannel returns true if the client is in channel. Panics if tracking
|
||||||
|
// is disabled.
|
||||||
|
func (c *Client) IsInChannel(channel string) bool {
|
||||||
|
c.panicIfNotTracking()
|
||||||
|
|
||||||
|
c.state.RLock()
|
||||||
|
_, inChannel := c.state.channels[ToRFC1459(channel)]
|
||||||
|
c.state.RUnlock()
|
||||||
|
|
||||||
|
return inChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetServerOption retrieves a server capability setting that was retrieved
|
||||||
|
// during client connection. This is also known as ISUPPORT (or RPL_PROTOCTL).
|
||||||
|
// Will panic if used when tracking has been disabled. Examples of usage:
|
||||||
|
//
|
||||||
|
// nickLen, success := GetServerOption("MAXNICKLEN")
|
||||||
|
//
|
||||||
|
func (c *Client) GetServerOption(key string) (result string, ok bool) {
|
||||||
|
c.panicIfNotTracking()
|
||||||
|
|
||||||
|
c.state.RLock()
|
||||||
|
result, ok = c.state.serverOptions[key]
|
||||||
|
c.state.RUnlock()
|
||||||
|
|
||||||
|
return result, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkName returns the network identifier. E.g. "EsperNet", "ByteIRC".
|
||||||
|
// May be empty if the server does not support RPL_ISUPPORT (or RPL_PROTOCTL).
|
||||||
|
// Will panic if used when tracking has been disabled.
|
||||||
|
func (c *Client) NetworkName() (name string) {
|
||||||
|
c.panicIfNotTracking()
|
||||||
|
|
||||||
|
name, _ = c.GetServerOption("NETWORK")
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerVersion returns the server software version, if the server has
|
||||||
|
// supplied this information during connection. May be empty if the server
|
||||||
|
// does not support RPL_MYINFO. Will panic if used when tracking has been
|
||||||
|
// disabled.
|
||||||
|
func (c *Client) ServerVersion() (version string) {
|
||||||
|
c.panicIfNotTracking()
|
||||||
|
|
||||||
|
version, _ = c.GetServerOption("VERSION")
|
||||||
|
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerMOTD returns the servers message of the day, if the server has sent
|
||||||
|
// it upon connect. Will panic if used when tracking has been disabled.
|
||||||
|
func (c *Client) ServerMOTD() (motd string) {
|
||||||
|
c.panicIfNotTracking()
|
||||||
|
|
||||||
|
c.state.RLock()
|
||||||
|
motd = c.state.motd
|
||||||
|
c.state.RUnlock()
|
||||||
|
|
||||||
|
return motd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lag is the latency between the server and the client. This is measured by
|
||||||
|
// determining the difference in time between when we ping the server, and
|
||||||
|
// when we receive a pong.
|
||||||
|
func (c *Client) Lag() time.Duration {
|
||||||
|
c.mu.RLock()
|
||||||
|
c.conn.mu.RLock()
|
||||||
|
delta := c.conn.lastPong.Sub(c.conn.lastPing)
|
||||||
|
c.conn.mu.RUnlock()
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
if delta < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return delta
|
||||||
|
}
|
||||||
|
|
||||||
|
// panicIfNotTracking will throw a panic when it's called, and tracking is
|
||||||
|
// disabled. Adds useful info like what function specifically, and where it
|
||||||
|
// was called from.
|
||||||
|
func (c *Client) panicIfNotTracking() {
|
||||||
|
if !c.Config.disableTracking {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pc, _, _, _ := runtime.Caller(1)
|
||||||
|
fn := runtime.FuncForPC(pc)
|
||||||
|
_, file, line, _ := runtime.Caller(2)
|
||||||
|
|
||||||
|
panic(fmt.Sprintf("%s used when tracking is disabled (caller %s:%d)", fn.Name(), file, line))
|
||||||
|
}
|
197
vendor/github.com/lrstanley/girc/cmdhandler/cmd.go
generated
vendored
Normal file
197
vendor/github.com/lrstanley/girc/cmdhandler/cmd.go
generated
vendored
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
package cmdhandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/lrstanley/girc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Input is a wrapper for events, based around private messages.
|
||||||
|
type Input struct {
|
||||||
|
Origin *girc.Event
|
||||||
|
Args []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command is an IRC command, supporting aliases, help documentation and easy
|
||||||
|
// wrapping for message inputs.
|
||||||
|
type Command struct {
|
||||||
|
// Name of command, e.g. "search" or "ping".
|
||||||
|
Name string
|
||||||
|
// Aliases for the above command, e.g. "s" for search, or "p" for "ping".
|
||||||
|
Aliases []string
|
||||||
|
// Help documentation. Should be in the format "<arg> <arg> [arg] --
|
||||||
|
// something useful here"
|
||||||
|
Help string
|
||||||
|
// MinArgs is the minimum required arguments for the command. Defaults to
|
||||||
|
// 0, which means multiple, or no arguments can be supplied. If set
|
||||||
|
// above 0, this means that the command handler will throw an error asking
|
||||||
|
// the person to check "<prefix>help <command>" for more info.
|
||||||
|
MinArgs int
|
||||||
|
// Fn is the function which is executed when the command is ran from a
|
||||||
|
// private message, or channel.
|
||||||
|
Fn func(*girc.Client, *Input)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Command) genHelp(prefix string) string {
|
||||||
|
out := "{b}" + prefix + c.Name + "{b}"
|
||||||
|
|
||||||
|
if c.Aliases != nil && len(c.Aliases) > 0 {
|
||||||
|
out += " ({b}" + prefix + strings.Join(c.Aliases, "{b}, {b}"+prefix) + "{b})"
|
||||||
|
}
|
||||||
|
|
||||||
|
out += " :: " + c.Help
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// CmdHandler is an irc command parser and execution format which you could
|
||||||
|
// use as an example for building your own version/bot.
|
||||||
|
//
|
||||||
|
// An example of how you would register this with girc:
|
||||||
|
//
|
||||||
|
// ch, err := cmdhandler.New("!")
|
||||||
|
// if err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// ch.Add(&cmdhandler.Command{
|
||||||
|
// Name: "ping",
|
||||||
|
// Help: "Sends a pong reply back to the original user.",
|
||||||
|
// Fn: func(c *girc.Client, input *cmdhandler.Input) {
|
||||||
|
// c.Commands.ReplyTo(*input.Origin, "pong!")
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// client.Handlers.AddHandler(girc.PRIVMSG, ch)
|
||||||
|
type CmdHandler struct {
|
||||||
|
prefix string
|
||||||
|
re *regexp.Regexp
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
cmds map[string]*Command
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmdMatch = `^%s([a-z0-9-_]{1,20})(?: (.*))?$`
|
||||||
|
|
||||||
|
// New returns a new CmdHandler based on the specified command prefix. A good
|
||||||
|
// prefix is a single character, and easy to remember/use. E.g. "!", or ".".
|
||||||
|
func New(prefix string) (*CmdHandler, error) {
|
||||||
|
re, err := regexp.Compile(fmt.Sprintf(cmdMatch, regexp.QuoteMeta(prefix)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CmdHandler{prefix: prefix, re: re, cmds: make(map[string]*Command)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var validName = regexp.MustCompile(`^[a-z0-9-_]{1,20}$`)
|
||||||
|
|
||||||
|
// Add registers a new command to the handler. Note that you cannot remove
|
||||||
|
// commands once added, unless you add another CmdHandler to the client.
|
||||||
|
func (ch *CmdHandler) Add(cmd *Command) error {
|
||||||
|
if cmd == nil {
|
||||||
|
return errors.New("nil command provided to CmdHandler")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Name = strings.ToLower(cmd.Name)
|
||||||
|
if !validName.MatchString(cmd.Name) {
|
||||||
|
return fmt.Errorf("invalid command name: %q (req: %q)", cmd.Name, validName.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Aliases != nil {
|
||||||
|
for i := 0; i < len(cmd.Aliases); i++ {
|
||||||
|
cmd.Aliases[i] = strings.ToLower(cmd.Aliases[i])
|
||||||
|
if !validName.MatchString(cmd.Aliases[i]) {
|
||||||
|
return fmt.Errorf("invalid command name: %q (req: %q)", cmd.Aliases[i], validName.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.MinArgs < 0 {
|
||||||
|
cmd.MinArgs = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.mu.Lock()
|
||||||
|
defer ch.mu.Unlock()
|
||||||
|
|
||||||
|
if _, ok := ch.cmds[cmd.Name]; ok {
|
||||||
|
return fmt.Errorf("command already registered: %s", cmd.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.cmds[cmd.Name] = cmd
|
||||||
|
|
||||||
|
// Since we'd be storing pointers, duplicates do not matter.
|
||||||
|
for i := 0; i < len(cmd.Aliases); i++ {
|
||||||
|
if _, ok := ch.cmds[cmd.Aliases[i]]; ok {
|
||||||
|
return fmt.Errorf("alias already registered: %s", cmd.Aliases[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.cmds[cmd.Aliases[i]] = cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute satisfies the girc.Handler interface.
|
||||||
|
func (ch *CmdHandler) Execute(client *girc.Client, event girc.Event) {
|
||||||
|
if event.Source == nil || event.Command != girc.PRIVMSG {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed := ch.re.FindStringSubmatch(event.Trailing)
|
||||||
|
if len(parsed) != 3 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
invCmd := strings.ToLower(parsed[1])
|
||||||
|
args := strings.Split(parsed[2], " ")
|
||||||
|
if len(args) == 1 && args[0] == "" {
|
||||||
|
args = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.mu.Lock()
|
||||||
|
defer ch.mu.Unlock()
|
||||||
|
|
||||||
|
if invCmd == "help" {
|
||||||
|
if len(args) == 0 {
|
||||||
|
client.Cmd.ReplyTo(event, girc.Fmt("type '{b}!help {blue}<command>{c}{b}' to optionally get more info about a specific command."))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
args[0] = strings.ToLower(args[0])
|
||||||
|
|
||||||
|
if _, ok := ch.cmds[args[0]]; !ok {
|
||||||
|
client.Cmd.ReplyTof(event, girc.Fmt("unknown command {b}%q{b}."), args[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch.cmds[args[0]].Help == "" {
|
||||||
|
client.Cmd.ReplyTof(event, girc.Fmt("there is no help documentation for {b}%q{b}"), args[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Cmd.ReplyTo(event, girc.Fmt(ch.cmds[args[0]].genHelp(ch.prefix)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd, ok := ch.cmds[invCmd]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) < cmd.MinArgs {
|
||||||
|
client.Cmd.ReplyTof(event, girc.Fmt("not enough arguments supplied for {b}%q{b}. try '{b}%shelp %s{b}'?"), invCmd, ch.prefix, invCmd)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
in := &Input{
|
||||||
|
Origin: &event,
|
||||||
|
Args: args,
|
||||||
|
}
|
||||||
|
|
||||||
|
go cmd.Fn(client, in)
|
||||||
|
}
|
398
vendor/github.com/lrstanley/girc/commands.go
generated
vendored
Normal file
398
vendor/github.com/lrstanley/girc/commands.go
generated
vendored
Normal file
@ -0,0 +1,398 @@
|
|||||||
|
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||||
|
// of this source code is governed by the MIT license that can be found in
|
||||||
|
// the LICENSE file.
|
||||||
|
|
||||||
|
package girc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Commands holds a large list of useful methods to interact with the server,
|
||||||
|
// and wrappers for common events.
|
||||||
|
type Commands struct {
|
||||||
|
c *Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nick changes the client nickname.
|
||||||
|
func (cmd *Commands) Nick(name string) error {
|
||||||
|
if !IsValidNick(name) {
|
||||||
|
return &ErrInvalidTarget{Target: name}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: NICK, Params: []string{name}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join attempts to enter a list of IRC channels, at bulk if possible to
|
||||||
|
// prevent sending extensive JOIN commands.
|
||||||
|
func (cmd *Commands) Join(channels ...string) error {
|
||||||
|
// We can join multiple channels at once, however we need to ensure that
|
||||||
|
// we are not exceeding the line length. (see maxLength)
|
||||||
|
max := maxLength - len(JOIN) - 1
|
||||||
|
|
||||||
|
var buffer string
|
||||||
|
|
||||||
|
for i := 0; i < len(channels); i++ {
|
||||||
|
if !IsValidChannel(channels[i]) {
|
||||||
|
return &ErrInvalidTarget{Target: channels[i]}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buffer+","+channels[i]) > max {
|
||||||
|
cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}})
|
||||||
|
buffer = ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buffer) == 0 {
|
||||||
|
buffer = channels[i]
|
||||||
|
} else {
|
||||||
|
buffer += "," + channels[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == len(channels)-1 {
|
||||||
|
cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// JoinKey attempts to enter an IRC channel with a password.
|
||||||
|
func (cmd *Commands) JoinKey(channel, password string) error {
|
||||||
|
if !IsValidChannel(channel) {
|
||||||
|
return &ErrInvalidTarget{Target: channel}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: JOIN, Params: []string{channel, password}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part leaves an IRC channel.
|
||||||
|
func (cmd *Commands) Part(channel, message string) error {
|
||||||
|
if !IsValidChannel(channel) {
|
||||||
|
return &ErrInvalidTarget{Target: channel}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: JOIN, Params: []string{channel}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PartMessage leaves an IRC channel with a specified leave message.
|
||||||
|
func (cmd *Commands) PartMessage(channel, message string) error {
|
||||||
|
if !IsValidChannel(channel) {
|
||||||
|
return &ErrInvalidTarget{Target: channel}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: JOIN, Params: []string{channel}, Trailing: message})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendCTCP sends a CTCP request to target. Note that this method uses
|
||||||
|
// PRIVMSG specifically.
|
||||||
|
func (cmd *Commands) SendCTCP(target, ctcpType, message string) error {
|
||||||
|
out := encodeCTCPRaw(ctcpType, message)
|
||||||
|
if out == "" {
|
||||||
|
return errors.New("invalid CTCP")
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.Message(target, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendCTCPf sends a CTCP request to target using a specific format. Note that
|
||||||
|
// this method uses PRIVMSG specifically.
|
||||||
|
func (cmd *Commands) SendCTCPf(target, ctcpType, format string, a ...interface{}) error {
|
||||||
|
return cmd.SendCTCP(target, ctcpType, fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendCTCPReplyf sends a CTCP response to target using a specific format.
|
||||||
|
// Note that this method uses NOTICE specifically.
|
||||||
|
func (cmd *Commands) SendCTCPReplyf(target, ctcpType, format string, a ...interface{}) error {
|
||||||
|
return cmd.SendCTCPReply(target, ctcpType, fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendCTCPReply sends a CTCP response to target. Note that this method uses
|
||||||
|
// NOTICE specifically.
|
||||||
|
func (cmd *Commands) SendCTCPReply(target, ctcpType, message string) error {
|
||||||
|
out := encodeCTCPRaw(ctcpType, message)
|
||||||
|
if out == "" {
|
||||||
|
return errors.New("invalid CTCP")
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.Notice(target, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message sends a PRIVMSG to target (either channel, service, or user).
|
||||||
|
func (cmd *Commands) Message(target, message string) error {
|
||||||
|
if !IsValidNick(target) && !IsValidChannel(target) {
|
||||||
|
return &ErrInvalidTarget{Target: target}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: PRIVMSG, Params: []string{target}, Trailing: message})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Messagef sends a formated PRIVMSG to target (either channel, service, or
|
||||||
|
// user).
|
||||||
|
func (cmd *Commands) Messagef(target, format string, a ...interface{}) error {
|
||||||
|
return cmd.Message(target, fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrInvalidSource is returned when a method needs to know the origin of an
|
||||||
|
// event, however Event.Source is unknown (e.g. sent by the user, not the
|
||||||
|
// server.)
|
||||||
|
var ErrInvalidSource = errors.New("event has nil or invalid source address")
|
||||||
|
|
||||||
|
// Reply sends a reply to channel or user, based on where the supplied event
|
||||||
|
// originated from. See also ReplyTo().
|
||||||
|
func (cmd *Commands) Reply(event Event, message string) error {
|
||||||
|
if event.Source == nil {
|
||||||
|
return ErrInvalidSource
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
|
||||||
|
return cmd.Message(event.Params[0], message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.Message(event.Source.Name, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replyf sends a reply to channel or user with a format string, based on
|
||||||
|
// where the supplied event originated from. See also ReplyTof().
|
||||||
|
func (cmd *Commands) Replyf(event Event, format string, a ...interface{}) error {
|
||||||
|
return cmd.Reply(event, fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplyTo sends a reply to a channel or user, based on where the supplied
|
||||||
|
// event originated from. ReplyTo(), when originating from a channel will
|
||||||
|
// default to replying with "<user>, <message>". See also Reply().
|
||||||
|
func (cmd *Commands) ReplyTo(event Event, message string) error {
|
||||||
|
if event.Source == nil {
|
||||||
|
return ErrInvalidSource
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
|
||||||
|
return cmd.Message(event.Params[0], event.Source.Name+", "+message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.Message(event.Source.Name, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplyTof sends a reply to a channel or user with a format string, based
|
||||||
|
// on where the supplied event originated from. ReplyTo(), when originating
|
||||||
|
// from a channel will default to replying with "<user>, <message>". See
|
||||||
|
// also Replyf().
|
||||||
|
func (cmd *Commands) ReplyTof(event Event, format string, a ...interface{}) error {
|
||||||
|
return cmd.ReplyTo(event, fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action sends a PRIVMSG ACTION (/me) to target (either channel, service,
|
||||||
|
// or user).
|
||||||
|
func (cmd *Commands) Action(target, message string) error {
|
||||||
|
if !IsValidNick(target) && !IsValidChannel(target) {
|
||||||
|
return &ErrInvalidTarget{Target: target}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{
|
||||||
|
Command: PRIVMSG,
|
||||||
|
Params: []string{target},
|
||||||
|
Trailing: fmt.Sprintf("\001ACTION %s\001", message),
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actionf sends a formated PRIVMSG ACTION (/me) to target (either channel,
|
||||||
|
// service, or user).
|
||||||
|
func (cmd *Commands) Actionf(target, format string, a ...interface{}) error {
|
||||||
|
return cmd.Action(target, fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notice sends a NOTICE to target (either channel, service, or user).
|
||||||
|
func (cmd *Commands) Notice(target, message string) error {
|
||||||
|
if !IsValidNick(target) && !IsValidChannel(target) {
|
||||||
|
return &ErrInvalidTarget{Target: target}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: NOTICE, Params: []string{target}, Trailing: message})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Noticef sends a formated NOTICE to target (either channel, service, or
|
||||||
|
// user).
|
||||||
|
func (cmd *Commands) Noticef(target, format string, a ...interface{}) error {
|
||||||
|
return cmd.Notice(target, fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendRaw sends a raw string back to the server, without carriage returns
|
||||||
|
// or newlines.
|
||||||
|
func (cmd *Commands) SendRaw(raw string) error {
|
||||||
|
e := ParseEvent(raw)
|
||||||
|
if e == nil {
|
||||||
|
return errors.New("invalid event: " + raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(e)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendRawf sends a formated string back to the server, without carriage
|
||||||
|
// returns or newlines.
|
||||||
|
func (cmd *Commands) SendRawf(format string, a ...interface{}) error {
|
||||||
|
return cmd.SendRaw(fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Topic sets the topic of channel to message. Does not verify the length
|
||||||
|
// of the topic.
|
||||||
|
func (cmd *Commands) Topic(channel, message string) {
|
||||||
|
cmd.c.Send(&Event{Command: TOPIC, Params: []string{channel}, Trailing: message})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Who sends a WHO query to the server, which will attempt WHOX by default.
|
||||||
|
// See http://faerion.sourceforge.net/doc/irc/whox.var for more details. This
|
||||||
|
// sends "%tcuhnr,2" per default. Do not use "1" as this will conflict with
|
||||||
|
// girc's builtin tracking functionality.
|
||||||
|
func (cmd *Commands) Who(target string) error {
|
||||||
|
if !IsValidNick(target) && !IsValidChannel(target) && !IsValidUser(target) {
|
||||||
|
return &ErrInvalidTarget{Target: target}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: WHO, Params: []string{target, "%tcuhnr,2"}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whois sends a WHOIS query to the server, targeted at a specific user.
|
||||||
|
// as WHOIS is a bit slower, you may want to use WHO for brief user info.
|
||||||
|
func (cmd *Commands) Whois(nick string) error {
|
||||||
|
if !IsValidNick(nick) {
|
||||||
|
return &ErrInvalidTarget{Target: nick}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: WHOIS, Params: []string{nick}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping sends a PING query to the server, with a specific identifier that
|
||||||
|
// the server should respond with.
|
||||||
|
func (cmd *Commands) Ping(id string) {
|
||||||
|
cmd.c.write(&Event{Command: PING, Params: []string{id}})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pong sends a PONG query to the server, with an identifier which was
|
||||||
|
// received from a previous PING query received by the client.
|
||||||
|
func (cmd *Commands) Pong(id string) {
|
||||||
|
cmd.c.write(&Event{Command: PONG, Params: []string{id}})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Oper sends a OPER authentication query to the server, with a username
|
||||||
|
// and password.
|
||||||
|
func (cmd *Commands) Oper(user, pass string) {
|
||||||
|
cmd.c.Send(&Event{Command: OPER, Params: []string{user, pass}, Sensitive: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kick sends a KICK query to the server, attempting to kick nick from
|
||||||
|
// channel, with reason. If reason is blank, one will not be sent to the
|
||||||
|
// server.
|
||||||
|
func (cmd *Commands) Kick(channel, nick, reason string) error {
|
||||||
|
if !IsValidChannel(channel) {
|
||||||
|
return &ErrInvalidTarget{Target: channel}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsValidNick(nick) {
|
||||||
|
return &ErrInvalidTarget{Target: nick}
|
||||||
|
}
|
||||||
|
|
||||||
|
if reason != "" {
|
||||||
|
cmd.c.Send(&Event{Command: KICK, Params: []string{channel, nick}, Trailing: reason})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: KICK, Params: []string{channel, nick}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invite sends a INVITE query to the server, to invite nick to channel.
|
||||||
|
func (cmd *Commands) Invite(channel, nick string) error {
|
||||||
|
if !IsValidChannel(channel) {
|
||||||
|
return &ErrInvalidTarget{Target: channel}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsValidNick(nick) {
|
||||||
|
return &ErrInvalidTarget{Target: nick}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: INVITE, Params: []string{nick, channel}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Away sends a AWAY query to the server, suggesting that the client is no
|
||||||
|
// longer active. If reason is blank, Client.Back() is called. Also see
|
||||||
|
// Client.Back().
|
||||||
|
func (cmd *Commands) Away(reason string) {
|
||||||
|
if reason == "" {
|
||||||
|
cmd.Back()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: AWAY, Params: []string{reason}})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back sends a AWAY query to the server, however the query is blank,
|
||||||
|
// suggesting that the client is active once again. Also see Client.Away().
|
||||||
|
func (cmd *Commands) Back() {
|
||||||
|
cmd.c.Send(&Event{Command: AWAY})
|
||||||
|
}
|
||||||
|
|
||||||
|
// List sends a LIST query to the server, which will list channels and topics.
|
||||||
|
// Supports multiple channels at once, in hopes it will reduce extensive
|
||||||
|
// LIST queries to the server. Supply no channels to run a list against the
|
||||||
|
// entire server (warning, that may mean LOTS of channels!)
|
||||||
|
func (cmd *Commands) List(channels ...string) error {
|
||||||
|
if len(channels) == 0 {
|
||||||
|
cmd.c.Send(&Event{Command: LIST})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can LIST multiple channels at once, however we need to ensure that
|
||||||
|
// we are not exceeding the line length. (see maxLength)
|
||||||
|
max := maxLength - len(JOIN) - 1
|
||||||
|
|
||||||
|
var buffer string
|
||||||
|
|
||||||
|
for i := 0; i < len(channels); i++ {
|
||||||
|
if !IsValidChannel(channels[i]) {
|
||||||
|
return &ErrInvalidTarget{Target: channels[i]}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buffer+","+channels[i]) > max {
|
||||||
|
cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}})
|
||||||
|
buffer = ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buffer) == 0 {
|
||||||
|
buffer = channels[i]
|
||||||
|
} else {
|
||||||
|
buffer += "," + channels[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == len(channels)-1 {
|
||||||
|
cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whowas sends a WHOWAS query to the server. amount is the amount of results
|
||||||
|
// you want back.
|
||||||
|
func (cmd *Commands) Whowas(nick string, amount int) error {
|
||||||
|
if !IsValidNick(nick) {
|
||||||
|
return &ErrInvalidTarget{Target: nick}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.c.Send(&Event{Command: WHOWAS, Params: []string{nick, string(amount)}})
|
||||||
|
return nil
|
||||||
|
}
|
566
vendor/github.com/lrstanley/girc/conn.go
generated
vendored
Normal file
566
vendor/github.com/lrstanley/girc/conn.go
generated
vendored
Normal file
@ -0,0 +1,566 @@
|
|||||||
|
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||||
|
// of this source code is governed by the MIT license that can be found in
|
||||||
|
// the LICENSE file.
|
||||||
|
|
||||||
|
package girc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Messages are delimited with CR and LF line endings, we're using the last
|
||||||
|
// one to split the stream. Both are removed during parsing of the message.
|
||||||
|
const delim byte = '\n'
|
||||||
|
|
||||||
|
var endline = []byte("\r\n")
|
||||||
|
|
||||||
|
// ircConn represents an IRC network protocol connection, it consists of an
|
||||||
|
// Encoder and Decoder to manage i/o.
|
||||||
|
type ircConn struct {
|
||||||
|
io *bufio.ReadWriter
|
||||||
|
sock net.Conn
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
// lastWrite is used to keep track of when we last wrote to the server.
|
||||||
|
lastWrite time.Time
|
||||||
|
// lastActive is the last time the client was interacting with the server,
|
||||||
|
// excluding a few background commands (PING, PONG, WHO, etc).
|
||||||
|
lastActive time.Time
|
||||||
|
// writeDelay is used to keep track of rate limiting of events sent to
|
||||||
|
// the server.
|
||||||
|
writeDelay time.Duration
|
||||||
|
// connected is true if we're actively connected to a server.
|
||||||
|
connected bool
|
||||||
|
// connTime is the time at which the client has connected to a server.
|
||||||
|
connTime *time.Time
|
||||||
|
// lastPing is the last time that we pinged the server.
|
||||||
|
lastPing time.Time
|
||||||
|
// lastPong is the last successful time that we pinged the server and
|
||||||
|
// received a successful pong back.
|
||||||
|
lastPong time.Time
|
||||||
|
pingDelay time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dialer is an interface implementation of net.Dialer. Use this if you would
|
||||||
|
// like to implement your own dialer which the client will use when connecting.
|
||||||
|
type Dialer interface {
|
||||||
|
// Dial takes two arguments. Network, which should be similar to "tcp",
|
||||||
|
// "tdp6", "udp", etc -- as well as address, which is the hostname or ip
|
||||||
|
// of the network. Note that network can be ignored if your transport
|
||||||
|
// doesn't take advantage of network types.
|
||||||
|
Dial(network, address string) (net.Conn, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newConn sets up and returns a new connection to the server.
|
||||||
|
func newConn(conf Config, dialer Dialer, addr string) (*ircConn, error) {
|
||||||
|
if err := conf.isValid(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var conn net.Conn
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if dialer == nil {
|
||||||
|
netDialer := &net.Dialer{Timeout: 5 * time.Second}
|
||||||
|
|
||||||
|
if conf.Bind != "" {
|
||||||
|
var local *net.TCPAddr
|
||||||
|
local, err = net.ResolveTCPAddr("tcp", conf.Bind+":0")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
netDialer.LocalAddr = local
|
||||||
|
}
|
||||||
|
|
||||||
|
dialer = netDialer
|
||||||
|
}
|
||||||
|
|
||||||
|
if conn, err = dialer.Dial("tcp", addr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.SSL {
|
||||||
|
var tlsConn net.Conn
|
||||||
|
tlsConn, err = tlsHandshake(conn, conf.TLSConfig, conf.Server, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
conn = tlsConn
|
||||||
|
}
|
||||||
|
|
||||||
|
ctime := time.Now()
|
||||||
|
|
||||||
|
c := &ircConn{
|
||||||
|
sock: conn,
|
||||||
|
connTime: &ctime,
|
||||||
|
connected: true,
|
||||||
|
}
|
||||||
|
c.newReadWriter()
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockConn(conn net.Conn) *ircConn {
|
||||||
|
ctime := time.Now()
|
||||||
|
c := &ircConn{
|
||||||
|
sock: conn,
|
||||||
|
connTime: &ctime,
|
||||||
|
connected: true,
|
||||||
|
}
|
||||||
|
c.newReadWriter()
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrParseEvent is returned when an event cannot be parsed with ParseEvent().
|
||||||
|
type ErrParseEvent struct {
|
||||||
|
Line string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrParseEvent) Error() string { return "unable to parse event: " + e.Line }
|
||||||
|
|
||||||
|
func (c *ircConn) decode() (event *Event, err error) {
|
||||||
|
line, err := c.io.ReadString(delim)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if event = ParseEvent(line); event == nil {
|
||||||
|
return nil, ErrParseEvent{line}
|
||||||
|
}
|
||||||
|
|
||||||
|
return event, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ircConn) encode(event *Event) error {
|
||||||
|
if _, err := c.io.Write(event.Bytes()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := c.io.Write(endline); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.io.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ircConn) newReadWriter() {
|
||||||
|
c.io = bufio.NewReadWriter(bufio.NewReader(c.sock), bufio.NewWriter(c.sock))
|
||||||
|
}
|
||||||
|
|
||||||
|
func tlsHandshake(conn net.Conn, conf *tls.Config, server string, validate bool) (net.Conn, error) {
|
||||||
|
if conf == nil {
|
||||||
|
conf = &tls.Config{ServerName: server, InsecureSkipVerify: !validate}
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConn := tls.Client(conn, conf)
|
||||||
|
return net.Conn(tlsConn), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the underlying socket.
|
||||||
|
func (c *ircConn) Close() error {
|
||||||
|
return c.sock.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect attempts to connect to the given IRC server. Returns only when
|
||||||
|
// an error has occurred, or a disconnect was requested with Close(). Connect
|
||||||
|
// will only return once all client-based goroutines have been closed to
|
||||||
|
// ensure there are no long-running routines becoming backed up.
|
||||||
|
//
|
||||||
|
// Connect will wait for all non-goroutine handlers to complete on error/quit,
|
||||||
|
// however it will not wait for goroutine-based handlers.
|
||||||
|
//
|
||||||
|
// If this returns nil, this means that the client requested to be closed
|
||||||
|
// (e.g. Client.Close()). Connect will panic if called when the last call has
|
||||||
|
// not completed.
|
||||||
|
func (c *Client) Connect() error {
|
||||||
|
return c.internalConnect(nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialerConnect allows you to specify your own custom dialer which implements
|
||||||
|
// the Dialer interface.
|
||||||
|
//
|
||||||
|
// An example of using this library would be to take advantage of the
|
||||||
|
// golang.org/x/net/proxy library:
|
||||||
|
//
|
||||||
|
// proxyUrl, _ := proxyURI, err = url.Parse("socks5://1.2.3.4:8888")
|
||||||
|
// dialer, _ := proxy.FromURL(proxyURI, &net.Dialer{Timeout: 5 * time.Second})
|
||||||
|
// _ := girc.DialerConnect(dialer)
|
||||||
|
func (c *Client) DialerConnect(dialer Dialer) error {
|
||||||
|
return c.internalConnect(nil, dialer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockConnect is used to implement mocking with an IRC server. Supply a net.Conn
|
||||||
|
// that will be used to spoof the server. A useful way to do this is to so
|
||||||
|
// net.Pipe(), pass one end into MockConnect(), and the other end into
|
||||||
|
// bufio.NewReader().
|
||||||
|
//
|
||||||
|
// For example:
|
||||||
|
//
|
||||||
|
// client := girc.New(girc.Config{
|
||||||
|
// Server: "dummy.int",
|
||||||
|
// Port: 6667,
|
||||||
|
// Nick: "test",
|
||||||
|
// User: "test",
|
||||||
|
// Name: "Testing123",
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// in, out := net.Pipe()
|
||||||
|
// defer in.Close()
|
||||||
|
// defer out.Close()
|
||||||
|
// b := bufio.NewReader(in)
|
||||||
|
//
|
||||||
|
// go func() {
|
||||||
|
// if err := client.MockConnect(out); err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
// }()
|
||||||
|
//
|
||||||
|
// defer client.Close(false)
|
||||||
|
//
|
||||||
|
// for {
|
||||||
|
// in.SetReadDeadline(time.Now().Add(300 * time.Second))
|
||||||
|
// line, err := b.ReadString(byte('\n'))
|
||||||
|
// if err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// event := girc.ParseEvent(line)
|
||||||
|
//
|
||||||
|
// if event == nil {
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Do stuff with event here.
|
||||||
|
// }
|
||||||
|
func (c *Client) MockConnect(conn net.Conn) error {
|
||||||
|
return c.internalConnect(conn, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) internalConnect(mock net.Conn, dialer Dialer) error {
|
||||||
|
// We want to be the only one handling connects/disconnects right now.
|
||||||
|
c.mu.Lock()
|
||||||
|
|
||||||
|
if c.conn != nil {
|
||||||
|
panic("use of connect more than once")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the state.
|
||||||
|
c.state.reset()
|
||||||
|
|
||||||
|
if mock == nil {
|
||||||
|
// Validate info, and actually make the connection.
|
||||||
|
c.debug.Printf("connecting to %s...", c.Server())
|
||||||
|
conn, err := newConn(c.Config, dialer, c.Server())
|
||||||
|
if err != nil {
|
||||||
|
c.mu.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.conn = conn
|
||||||
|
} else {
|
||||||
|
c.conn = newMockConn(mock)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ctx context.Context
|
||||||
|
ctx, c.stop = context.WithCancel(context.Background())
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
errs := make(chan error, 4)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
// 4 being the number of goroutines we need to finish when this function
|
||||||
|
// returns.
|
||||||
|
wg.Add(4)
|
||||||
|
go c.execLoop(ctx, errs, &wg)
|
||||||
|
go c.readLoop(ctx, errs, &wg)
|
||||||
|
go c.sendLoop(ctx, errs, &wg)
|
||||||
|
go c.pingLoop(ctx, errs, &wg)
|
||||||
|
|
||||||
|
// Passwords first.
|
||||||
|
if c.Config.ServerPass != "" {
|
||||||
|
c.write(&Event{Command: PASS, Params: []string{c.Config.ServerPass}, Sensitive: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// List the IRCv3 capabilities, specifically with the max protocol we
|
||||||
|
// support. The IRCv3 specification doesn't directly state if this should
|
||||||
|
// be called directly before registration, or if it should be called
|
||||||
|
// after NICK/USER requests. It looks like non-supporting networks
|
||||||
|
// should ignore this, and some IRCv3 capable networks require this to
|
||||||
|
// occur before NICK/USER registration.
|
||||||
|
c.listCAP()
|
||||||
|
|
||||||
|
// Then nickname.
|
||||||
|
c.write(&Event{Command: NICK, Params: []string{c.Config.Nick}})
|
||||||
|
|
||||||
|
// Then username and realname.
|
||||||
|
if c.Config.Name == "" {
|
||||||
|
c.Config.Name = c.Config.User
|
||||||
|
}
|
||||||
|
|
||||||
|
c.write(&Event{Command: USER, Params: []string{c.Config.User, "*", "*"}, Trailing: c.Config.Name})
|
||||||
|
|
||||||
|
// Send a virtual event allowing hooks for successful socket connection.
|
||||||
|
c.RunHandlers(&Event{Command: INITIALIZED, Trailing: c.Server()})
|
||||||
|
|
||||||
|
// Wait for the first error.
|
||||||
|
var result error
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
c.debug.Print("received request to close, beginning clean up")
|
||||||
|
c.RunHandlers(&Event{Command: STOPPED, Trailing: c.Server()})
|
||||||
|
case err := <-errs:
|
||||||
|
c.debug.Print("received error, beginning clean up")
|
||||||
|
result = err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that the connection is closed if not already.
|
||||||
|
c.mu.RLock()
|
||||||
|
if c.stop != nil {
|
||||||
|
c.stop()
|
||||||
|
}
|
||||||
|
c.conn.mu.Lock()
|
||||||
|
c.conn.connected = false
|
||||||
|
_ = c.conn.Close()
|
||||||
|
c.conn.mu.Unlock()
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
// Once we have our error/result, let all other functions know we're done.
|
||||||
|
c.debug.Print("waiting for all routines to finish")
|
||||||
|
|
||||||
|
// Wait for all goroutines to finish.
|
||||||
|
wg.Wait()
|
||||||
|
close(errs)
|
||||||
|
|
||||||
|
// This helps ensure that the end user isn't improperly using the client
|
||||||
|
// more than once. If they want to do this, they should be using multiple
|
||||||
|
// clients, not multiple instances of Connect().
|
||||||
|
c.mu.Lock()
|
||||||
|
c.conn = nil
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// readLoop sets a timeout of 300 seconds, and then attempts to read from the
|
||||||
|
// IRC server. If there is an error, it calls Reconnect.
|
||||||
|
func (c *Client) readLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) {
|
||||||
|
c.debug.Print("starting readLoop")
|
||||||
|
defer c.debug.Print("closing readLoop")
|
||||||
|
|
||||||
|
var event *Event
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
wg.Done()
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
_ = c.conn.sock.SetReadDeadline(time.Now().Add(300 * time.Second))
|
||||||
|
event, err = c.conn.decode()
|
||||||
|
if err != nil {
|
||||||
|
errs <- err
|
||||||
|
wg.Done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.rx <- event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send sends an event to the server. Use Client.RunHandlers() if you are
|
||||||
|
// simply looking to trigger handlers with an event.
|
||||||
|
func (c *Client) Send(event *Event) {
|
||||||
|
if !c.Config.AllowFlood {
|
||||||
|
<-time.After(c.conn.rate(event.Len()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Config.GlobalFormat && event.Trailing != "" &&
|
||||||
|
(event.Command == PRIVMSG || event.Command == TOPIC || event.Command == NOTICE) {
|
||||||
|
event.Trailing = Fmt(event.Trailing)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.write(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write is the lower level function to write an event. It does not have a
|
||||||
|
// write-delay when sending events.
|
||||||
|
func (c *Client) write(event *Event) {
|
||||||
|
c.tx <- event
|
||||||
|
}
|
||||||
|
|
||||||
|
// rate allows limiting events based on how frequent the event is being sent,
|
||||||
|
// as well as how many characters each event has.
|
||||||
|
func (c *ircConn) rate(chars int) time.Duration {
|
||||||
|
_time := time.Second + ((time.Duration(chars) * time.Second) / 100)
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.writeDelay += _time - time.Now().Sub(c.lastWrite); c.writeDelay < 0 {
|
||||||
|
c.writeDelay = 0
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
if c.writeDelay > (8 * time.Second) {
|
||||||
|
return _time
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) sendLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) {
|
||||||
|
c.debug.Print("starting sendLoop")
|
||||||
|
defer c.debug.Print("closing sendLoop")
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event := <-c.tx:
|
||||||
|
// Check if tags exist on the event. If they do, and message-tags
|
||||||
|
// isn't a supported capability, remove them from the event.
|
||||||
|
if event.Tags != nil {
|
||||||
|
c.state.RLock()
|
||||||
|
var in bool
|
||||||
|
for i := 0; i < len(c.state.enabledCap); i++ {
|
||||||
|
if c.state.enabledCap[i] == "message-tags" {
|
||||||
|
in = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.state.RUnlock()
|
||||||
|
|
||||||
|
if !in {
|
||||||
|
event.Tags = Tags{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the event.
|
||||||
|
if event.Sensitive {
|
||||||
|
c.debug.Printf("> %s ***redacted***", event.Command)
|
||||||
|
} else {
|
||||||
|
c.debug.Print("> ", StripRaw(event.String()))
|
||||||
|
}
|
||||||
|
if c.Config.Out != nil {
|
||||||
|
if pretty, ok := event.Pretty(); ok {
|
||||||
|
fmt.Fprintln(c.Config.Out, StripRaw(pretty))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.conn.mu.Lock()
|
||||||
|
c.conn.lastWrite = time.Now()
|
||||||
|
|
||||||
|
if event.Command != PING && event.Command != PONG && event.Command != WHO {
|
||||||
|
c.conn.lastActive = c.conn.lastWrite
|
||||||
|
}
|
||||||
|
c.conn.mu.Unlock()
|
||||||
|
|
||||||
|
// Write the raw line.
|
||||||
|
_, err = c.conn.io.Write(event.Bytes())
|
||||||
|
if err == nil {
|
||||||
|
// And the \r\n.
|
||||||
|
_, err = c.conn.io.Write(endline)
|
||||||
|
if err == nil {
|
||||||
|
// Lastly, flush everything to the socket.
|
||||||
|
err = c.conn.io.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
errs <- err
|
||||||
|
wg.Done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
wg.Done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrTimedOut is returned when we attempt to ping the server, and timed out
|
||||||
|
// before receiving a PONG back.
|
||||||
|
type ErrTimedOut struct {
|
||||||
|
// TimeSinceSuccess is how long ago we received a successful pong.
|
||||||
|
TimeSinceSuccess time.Duration
|
||||||
|
// LastPong is the time we received our last successful pong.
|
||||||
|
LastPong time.Time
|
||||||
|
// LastPong is the last time we sent a pong request.
|
||||||
|
LastPing time.Time
|
||||||
|
// Delay is the configured delay between how often we send a ping request.
|
||||||
|
Delay time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ErrTimedOut) Error() string { return "timed out during ping to server" }
|
||||||
|
|
||||||
|
func (c *Client) pingLoop(ctx context.Context, errs chan error, wg *sync.WaitGroup) {
|
||||||
|
// Don't run the pingLoop if they want to disable it.
|
||||||
|
if c.Config.PingDelay <= 0 {
|
||||||
|
wg.Done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.debug.Print("starting pingLoop")
|
||||||
|
defer c.debug.Print("closing pingLoop")
|
||||||
|
|
||||||
|
c.conn.mu.Lock()
|
||||||
|
c.conn.lastPing = time.Now()
|
||||||
|
c.conn.lastPong = time.Now()
|
||||||
|
c.conn.mu.Unlock()
|
||||||
|
|
||||||
|
tick := time.NewTicker(c.Config.PingDelay)
|
||||||
|
defer tick.Stop()
|
||||||
|
|
||||||
|
started := time.Now()
|
||||||
|
past := false
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-tick.C:
|
||||||
|
// Delay during connect to wait for the client to register, otherwise
|
||||||
|
// some ircd's will not respond (e.g. during SASL negotiation).
|
||||||
|
if !past {
|
||||||
|
if time.Since(started) < 30*time.Second {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
past = true
|
||||||
|
}
|
||||||
|
|
||||||
|
c.conn.mu.RLock()
|
||||||
|
if time.Since(c.conn.lastPong) > c.Config.PingDelay+(60*time.Second) {
|
||||||
|
// It's 60 seconds over what out ping delay is, connection
|
||||||
|
// has probably dropped.
|
||||||
|
errs <- ErrTimedOut{
|
||||||
|
TimeSinceSuccess: time.Since(c.conn.lastPong),
|
||||||
|
LastPong: c.conn.lastPong,
|
||||||
|
LastPing: c.conn.lastPing,
|
||||||
|
Delay: c.Config.PingDelay,
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Done()
|
||||||
|
c.conn.mu.RUnlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.conn.mu.RUnlock()
|
||||||
|
|
||||||
|
c.conn.mu.Lock()
|
||||||
|
c.conn.lastPing = time.Now()
|
||||||
|
c.conn.mu.Unlock()
|
||||||
|
|
||||||
|
c.Cmd.Ping(fmt.Sprintf("%d", time.Now().UnixNano()))
|
||||||
|
case <-ctx.Done():
|
||||||
|
wg.Done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
338
vendor/github.com/lrstanley/girc/contants.go
generated
vendored
Normal file
338
vendor/github.com/lrstanley/girc/contants.go
generated
vendored
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||||
|
// of this source code is governed by the MIT license that can be found in
|
||||||
|
// the LICENSE file.
|
||||||
|
|
||||||
|
package girc
|
||||||
|
|
||||||
|
// Standard CTCP based constants.
|
||||||
|
const (
|
||||||
|
CTCP_PING = "PING"
|
||||||
|
CTCP_PONG = "PONG"
|
||||||
|
CTCP_VERSION = "VERSION"
|
||||||
|
CTCP_USERINFO = "USERINFO"
|
||||||
|
CTCP_CLIENTINFO = "CLIENTINFO"
|
||||||
|
CTCP_SOURCE = "SOURCE"
|
||||||
|
CTCP_TIME = "TIME"
|
||||||
|
CTCP_FINGER = "FINGER"
|
||||||
|
CTCP_ERRMSG = "ERRMSG"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Emulated event commands used to allow easier hooks into the changing
|
||||||
|
// state of the client.
|
||||||
|
const (
|
||||||
|
UPDATE_STATE = "CLIENT_STATE_UPDATED" // when channel/user state is updated.
|
||||||
|
UPDATE_GENERAL = "CLIENT_GENERAL_UPDATED" // when general state (client nick, server name, etc) is updated.
|
||||||
|
ALL_EVENTS = "*" // trigger on all events
|
||||||
|
CONNECTED = "CLIENT_CONNECTED" // when it's safe to send arbitrary commands (joins, list, who, etc), trailing is host:port
|
||||||
|
INITIALIZED = "CLIENT_INIT" // verifies successful socket connection, trailing is host:port
|
||||||
|
DISCONNECTED = "CLIENT_DISCONNECTED" // occurs when we're disconnected from the server (user-requested or not)
|
||||||
|
STOPPED = "CLIENT_STOPPED" // occurs when Client.Stop() has been called
|
||||||
|
)
|
||||||
|
|
||||||
|
// User/channel prefixes :: RFC1459.
|
||||||
|
const (
|
||||||
|
DefaultPrefixes = "(ov)@+" // the most common default prefixes
|
||||||
|
ModeAddPrefix = "+" // modes are being added
|
||||||
|
ModeDelPrefix = "-" // modes are being removed
|
||||||
|
|
||||||
|
ChannelPrefix = "#" // regular channel
|
||||||
|
DistributedPrefix = "&" // distributed channel
|
||||||
|
OwnerPrefix = "~" // user owner +q (non-rfc)
|
||||||
|
AdminPrefix = "&" // user admin +a (non-rfc)
|
||||||
|
HalfOperatorPrefix = "%" // user half operator +h (non-rfc)
|
||||||
|
OperatorPrefix = "@" // user operator +o
|
||||||
|
VoicePrefix = "+" // user has voice +v
|
||||||
|
)
|
||||||
|
|
||||||
|
// User modes :: RFC1459; section 4.2.3.2.
|
||||||
|
const (
|
||||||
|
UserModeInvisible = "i" // invisible
|
||||||
|
UserModeOperator = "o" // server operator
|
||||||
|
UserModeServerNotices = "s" // user wants to receive server notices
|
||||||
|
UserModeWallops = "w" // user wants to receive wallops
|
||||||
|
)
|
||||||
|
|
||||||
|
// Channel modes :: RFC1459; section 4.2.3.1.
|
||||||
|
const (
|
||||||
|
ModeDefaults = "beI,k,l,imnpst" // the most common default modes
|
||||||
|
|
||||||
|
ModeInviteOnly = "i" // only join with an invite
|
||||||
|
ModeKey = "k" // channel password
|
||||||
|
ModeLimit = "l" // user limit
|
||||||
|
ModeModerated = "m" // only voiced users and operators can talk
|
||||||
|
ModeOperator = "o" // operator
|
||||||
|
ModePrivate = "p" // private
|
||||||
|
ModeSecret = "s" // secret
|
||||||
|
ModeTopic = "t" // must be op to set topic
|
||||||
|
ModeVoice = "v" // speak during moderation mode
|
||||||
|
|
||||||
|
ModeOwner = "q" // owner privileges (non-rfc)
|
||||||
|
ModeAdmin = "a" // admin privileges (non-rfc)
|
||||||
|
ModeHalfOperator = "h" // half-operator privileges (non-rfc)
|
||||||
|
)
|
||||||
|
|
||||||
|
// IRC commands :: RFC2812; section 3 :: RFC2813; section 4.
|
||||||
|
const (
|
||||||
|
ADMIN = "ADMIN"
|
||||||
|
AWAY = "AWAY"
|
||||||
|
CONNECT = "CONNECT"
|
||||||
|
DIE = "DIE"
|
||||||
|
ERROR = "ERROR"
|
||||||
|
INFO = "INFO"
|
||||||
|
INVITE = "INVITE"
|
||||||
|
ISON = "ISON"
|
||||||
|
JOIN = "JOIN"
|
||||||
|
KICK = "KICK"
|
||||||
|
KILL = "KILL"
|
||||||
|
LINKS = "LINKS"
|
||||||
|
LIST = "LIST"
|
||||||
|
LUSERS = "LUSERS"
|
||||||
|
MODE = "MODE"
|
||||||
|
MOTD = "MOTD"
|
||||||
|
NAMES = "NAMES"
|
||||||
|
NICK = "NICK"
|
||||||
|
NJOIN = "NJOIN"
|
||||||
|
NOTICE = "NOTICE"
|
||||||
|
OPER = "OPER"
|
||||||
|
PART = "PART"
|
||||||
|
PASS = "PASS"
|
||||||
|
PING = "PING"
|
||||||
|
PONG = "PONG"
|
||||||
|
PRIVMSG = "PRIVMSG"
|
||||||
|
QUIT = "QUIT"
|
||||||
|
REHASH = "REHASH"
|
||||||
|
RESTART = "RESTART"
|
||||||
|
SERVER = "SERVER"
|
||||||
|
SERVICE = "SERVICE"
|
||||||
|
SERVLIST = "SERVLIST"
|
||||||
|
SQUERY = "SQUERY"
|
||||||
|
SQUIT = "SQUIT"
|
||||||
|
STATS = "STATS"
|
||||||
|
SUMMON = "SUMMON"
|
||||||
|
TIME = "TIME"
|
||||||
|
TOPIC = "TOPIC"
|
||||||
|
TRACE = "TRACE"
|
||||||
|
USER = "USER"
|
||||||
|
USERHOST = "USERHOST"
|
||||||
|
USERS = "USERS"
|
||||||
|
VERSION = "VERSION"
|
||||||
|
WALLOPS = "WALLOPS"
|
||||||
|
WHO = "WHO"
|
||||||
|
WHOIS = "WHOIS"
|
||||||
|
WHOWAS = "WHOWAS"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Numeric IRC reply mapping :: RFC2812; section 5.
|
||||||
|
const (
|
||||||
|
RPL_WELCOME = "001"
|
||||||
|
RPL_YOURHOST = "002"
|
||||||
|
RPL_CREATED = "003"
|
||||||
|
RPL_MYINFO = "004"
|
||||||
|
RPL_BOUNCE = "005"
|
||||||
|
RPL_ISUPPORT = "005"
|
||||||
|
RPL_USERHOST = "302"
|
||||||
|
RPL_ISON = "303"
|
||||||
|
RPL_AWAY = "301"
|
||||||
|
RPL_UNAWAY = "305"
|
||||||
|
RPL_NOWAWAY = "306"
|
||||||
|
RPL_WHOISUSER = "311"
|
||||||
|
RPL_WHOISSERVER = "312"
|
||||||
|
RPL_WHOISOPERATOR = "313"
|
||||||
|
RPL_WHOISIDLE = "317"
|
||||||
|
RPL_ENDOFWHOIS = "318"
|
||||||
|
RPL_WHOISCHANNELS = "319"
|
||||||
|
RPL_WHOWASUSER = "314"
|
||||||
|
RPL_ENDOFWHOWAS = "369"
|
||||||
|
RPL_LISTSTART = "321"
|
||||||
|
RPL_LIST = "322"
|
||||||
|
RPL_LISTEND = "323"
|
||||||
|
RPL_UNIQOPIS = "325"
|
||||||
|
RPL_CHANNELMODEIS = "324"
|
||||||
|
RPL_NOTOPIC = "331"
|
||||||
|
RPL_TOPIC = "332"
|
||||||
|
RPL_INVITING = "341"
|
||||||
|
RPL_SUMMONING = "342"
|
||||||
|
RPL_INVITELIST = "346"
|
||||||
|
RPL_ENDOFINVITELIST = "347"
|
||||||
|
RPL_EXCEPTLIST = "348"
|
||||||
|
RPL_ENDOFEXCEPTLIST = "349"
|
||||||
|
RPL_VERSION = "351"
|
||||||
|
RPL_WHOREPLY = "352"
|
||||||
|
RPL_ENDOFWHO = "315"
|
||||||
|
RPL_NAMREPLY = "353"
|
||||||
|
RPL_ENDOFNAMES = "366"
|
||||||
|
RPL_LINKS = "364"
|
||||||
|
RPL_ENDOFLINKS = "365"
|
||||||
|
RPL_BANLIST = "367"
|
||||||
|
RPL_ENDOFBANLIST = "368"
|
||||||
|
RPL_INFO = "371"
|
||||||
|
RPL_ENDOFINFO = "374"
|
||||||
|
RPL_MOTDSTART = "375"
|
||||||
|
RPL_MOTD = "372"
|
||||||
|
RPL_ENDOFMOTD = "376"
|
||||||
|
RPL_YOUREOPER = "381"
|
||||||
|
RPL_REHASHING = "382"
|
||||||
|
RPL_YOURESERVICE = "383"
|
||||||
|
RPL_TIME = "391"
|
||||||
|
RPL_USERSSTART = "392"
|
||||||
|
RPL_USERS = "393"
|
||||||
|
RPL_ENDOFUSERS = "394"
|
||||||
|
RPL_NOUSERS = "395"
|
||||||
|
RPL_TRACELINK = "200"
|
||||||
|
RPL_TRACECONNECTING = "201"
|
||||||
|
RPL_TRACEHANDSHAKE = "202"
|
||||||
|
RPL_TRACEUNKNOWN = "203"
|
||||||
|
RPL_TRACEOPERATOR = "204"
|
||||||
|
RPL_TRACEUSER = "205"
|
||||||
|
RPL_TRACESERVER = "206"
|
||||||
|
RPL_TRACESERVICE = "207"
|
||||||
|
RPL_TRACENEWTYPE = "208"
|
||||||
|
RPL_TRACECLASS = "209"
|
||||||
|
RPL_TRACERECONNECT = "210"
|
||||||
|
RPL_TRACELOG = "261"
|
||||||
|
RPL_TRACEEND = "262"
|
||||||
|
RPL_STATSLINKINFO = "211"
|
||||||
|
RPL_STATSCOMMANDS = "212"
|
||||||
|
RPL_ENDOFSTATS = "219"
|
||||||
|
RPL_STATSUPTIME = "242"
|
||||||
|
RPL_STATSOLINE = "243"
|
||||||
|
RPL_UMODEIS = "221"
|
||||||
|
RPL_SERVLIST = "234"
|
||||||
|
RPL_SERVLISTEND = "235"
|
||||||
|
RPL_LUSERCLIENT = "251"
|
||||||
|
RPL_LUSEROP = "252"
|
||||||
|
RPL_LUSERUNKNOWN = "253"
|
||||||
|
RPL_LUSERCHANNELS = "254"
|
||||||
|
RPL_LUSERME = "255"
|
||||||
|
RPL_ADMINME = "256"
|
||||||
|
RPL_ADMINLOC1 = "257"
|
||||||
|
RPL_ADMINLOC2 = "258"
|
||||||
|
RPL_ADMINEMAIL = "259"
|
||||||
|
RPL_TRYAGAIN = "263"
|
||||||
|
ERR_NOSUCHNICK = "401"
|
||||||
|
ERR_NOSUCHSERVER = "402"
|
||||||
|
ERR_NOSUCHCHANNEL = "403"
|
||||||
|
ERR_CANNOTSENDTOCHAN = "404"
|
||||||
|
ERR_TOOMANYCHANNELS = "405"
|
||||||
|
ERR_WASNOSUCHNICK = "406"
|
||||||
|
ERR_TOOMANYTARGETS = "407"
|
||||||
|
ERR_NOSUCHSERVICE = "408"
|
||||||
|
ERR_NOORIGIN = "409"
|
||||||
|
ERR_NORECIPIENT = "411"
|
||||||
|
ERR_NOTEXTTOSEND = "412"
|
||||||
|
ERR_NOTOPLEVEL = "413"
|
||||||
|
ERR_WILDTOPLEVEL = "414"
|
||||||
|
ERR_BADMASK = "415"
|
||||||
|
ERR_UNKNOWNCOMMAND = "421"
|
||||||
|
ERR_NOMOTD = "422"
|
||||||
|
ERR_NOADMININFO = "423"
|
||||||
|
ERR_FILEERROR = "424"
|
||||||
|
ERR_NONICKNAMEGIVEN = "431"
|
||||||
|
ERR_ERRONEUSNICKNAME = "432"
|
||||||
|
ERR_NICKNAMEINUSE = "433"
|
||||||
|
ERR_NICKCOLLISION = "436"
|
||||||
|
ERR_UNAVAILRESOURCE = "437"
|
||||||
|
ERR_USERNOTINCHANNEL = "441"
|
||||||
|
ERR_NOTONCHANNEL = "442"
|
||||||
|
ERR_USERONCHANNEL = "443"
|
||||||
|
ERR_NOLOGIN = "444"
|
||||||
|
ERR_SUMMONDISABLED = "445"
|
||||||
|
ERR_USERSDISABLED = "446"
|
||||||
|
ERR_NOTREGISTERED = "451"
|
||||||
|
ERR_NEEDMOREPARAMS = "461"
|
||||||
|
ERR_ALREADYREGISTRED = "462"
|
||||||
|
ERR_NOPERMFORHOST = "463"
|
||||||
|
ERR_PASSWDMISMATCH = "464"
|
||||||
|
ERR_YOUREBANNEDCREEP = "465"
|
||||||
|
ERR_YOUWILLBEBANNED = "466"
|
||||||
|
ERR_KEYSET = "467"
|
||||||
|
ERR_CHANNELISFULL = "471"
|
||||||
|
ERR_UNKNOWNMODE = "472"
|
||||||
|
ERR_INVITEONLYCHAN = "473"
|
||||||
|
ERR_BANNEDFROMCHAN = "474"
|
||||||
|
ERR_BADCHANNELKEY = "475"
|
||||||
|
ERR_BADCHANMASK = "476"
|
||||||
|
ERR_NOCHANMODES = "477"
|
||||||
|
ERR_BANLISTFULL = "478"
|
||||||
|
ERR_NOPRIVILEGES = "481"
|
||||||
|
ERR_CHANOPRIVSNEEDED = "482"
|
||||||
|
ERR_CANTKILLSERVER = "483"
|
||||||
|
ERR_RESTRICTED = "484"
|
||||||
|
ERR_UNIQOPPRIVSNEEDED = "485"
|
||||||
|
ERR_NOOPERHOST = "491"
|
||||||
|
ERR_UMODEUNKNOWNFLAG = "501"
|
||||||
|
ERR_USERSDONTMATCH = "502"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IRCv3 commands and extensions :: http://ircv3.net/irc/.
|
||||||
|
const (
|
||||||
|
AUTHENTICATE = "AUTHENTICATE"
|
||||||
|
STARTTLS = "STARTTLS"
|
||||||
|
|
||||||
|
CAP = "CAP"
|
||||||
|
CAP_ACK = "ACK"
|
||||||
|
CAP_CLEAR = "CLEAR"
|
||||||
|
CAP_END = "END"
|
||||||
|
CAP_LIST = "LIST"
|
||||||
|
CAP_LS = "LS"
|
||||||
|
CAP_NAK = "NAK"
|
||||||
|
CAP_REQ = "REQ"
|
||||||
|
CAP_NEW = "NEW"
|
||||||
|
CAP_DEL = "DEL"
|
||||||
|
|
||||||
|
CAP_CHGHOST = "CHGHOST"
|
||||||
|
CAP_AWAY = "AWAY"
|
||||||
|
CAP_ACCOUNT = "ACCOUNT"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Numeric IRC reply mapping for ircv3 :: http://ircv3.net/irc/.
|
||||||
|
const (
|
||||||
|
RPL_LOGGEDIN = "900"
|
||||||
|
RPL_LOGGEDOUT = "901"
|
||||||
|
RPL_NICKLOCKED = "902"
|
||||||
|
RPL_SASLSUCCESS = "903"
|
||||||
|
ERR_SASLFAIL = "904"
|
||||||
|
ERR_SASLTOOLONG = "905"
|
||||||
|
ERR_SASLABORTED = "906"
|
||||||
|
ERR_SASLALREADY = "907"
|
||||||
|
RPL_SASLMECHS = "908"
|
||||||
|
RPL_STARTTLS = "670"
|
||||||
|
ERR_STARTTLS = "691"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Numeric IRC event mapping :: RFC2812; section 5.3.
|
||||||
|
const (
|
||||||
|
RPL_STATSCLINE = "213"
|
||||||
|
RPL_STATSNLINE = "214"
|
||||||
|
RPL_STATSILINE = "215"
|
||||||
|
RPL_STATSKLINE = "216"
|
||||||
|
RPL_STATSQLINE = "217"
|
||||||
|
RPL_STATSYLINE = "218"
|
||||||
|
RPL_SERVICEINFO = "231"
|
||||||
|
RPL_ENDOFSERVICES = "232"
|
||||||
|
RPL_SERVICE = "233"
|
||||||
|
RPL_STATSVLINE = "240"
|
||||||
|
RPL_STATSLLINE = "241"
|
||||||
|
RPL_STATSHLINE = "244"
|
||||||
|
RPL_STATSSLINE = "245"
|
||||||
|
RPL_STATSPING = "246"
|
||||||
|
RPL_STATSBLINE = "247"
|
||||||
|
RPL_STATSDLINE = "250"
|
||||||
|
RPL_NONE = "300"
|
||||||
|
RPL_WHOISCHANOP = "316"
|
||||||
|
RPL_KILLDONE = "361"
|
||||||
|
RPL_CLOSING = "362"
|
||||||
|
RPL_CLOSEEND = "363"
|
||||||
|
RPL_INFOSTART = "373"
|
||||||
|
RPL_MYPORTIS = "384"
|
||||||
|
ERR_NOSERVICEHOST = "492"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Misc.
|
||||||
|
const (
|
||||||
|
ERR_TOOMANYMATCHES = "416" // IRCNet.
|
||||||
|
RPL_GLOBALUSERS = "266" // aircd/hybrid/bahamut, used on freenode.
|
||||||
|
RPL_LOCALUSERS = "265" // aircd/hybrid/bahamut, used on freenode.
|
||||||
|
RPL_TOPICWHOTIME = "333" // ircu, used on freenode.
|
||||||
|
RPL_WHOSPCRPL = "354" // ircu, used on networks with WHOX support.
|
||||||
|
)
|
288
vendor/github.com/lrstanley/girc/ctcp.go
generated
vendored
Normal file
288
vendor/github.com/lrstanley/girc/ctcp.go
generated
vendored
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||||
|
// of this source code is governed by the MIT license that can be found in
|
||||||
|
// the LICENSE file.
|
||||||
|
|
||||||
|
package girc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ctcpDelim if the delimiter used for CTCP formatted events/messages.
|
||||||
|
const ctcpDelim byte = 0x01 // Prefix and suffix for CTCP messages.
|
||||||
|
|
||||||
|
// CTCPEvent is the necessary information from an IRC message.
|
||||||
|
type CTCPEvent struct {
|
||||||
|
// Origin is the original event that the CTCP event was decoded from.
|
||||||
|
Origin *Event `json:"origin"`
|
||||||
|
// Source is the author of the CTCP event.
|
||||||
|
Source *Source `json:"source"`
|
||||||
|
// Command is the type of CTCP event. E.g. PING, TIME, VERSION.
|
||||||
|
Command string `json:"command"`
|
||||||
|
// Text is the raw arguments following the command.
|
||||||
|
Text string `json:"text"`
|
||||||
|
// Reply is true if the CTCP event is intended to be a reply to a
|
||||||
|
// previous CTCP (e.g, if we sent one).
|
||||||
|
Reply bool `json:"reply"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeCTCP decodes an incoming CTCP event, if it is CTCP. nil is returned
|
||||||
|
// if the incoming event does not match a valid CTCP.
|
||||||
|
func decodeCTCP(e *Event) *CTCPEvent {
|
||||||
|
// http://www.irchelp.org/protocol/ctcpspec.html
|
||||||
|
|
||||||
|
// Must be targeting a user/channel, AND trailing must have
|
||||||
|
// DELIM+TAG+DELIM minimum (at least 3 chars).
|
||||||
|
if len(e.Params) != 1 || len(e.Trailing) < 3 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.Command != PRIVMSG && e.Command != NOTICE) || !IsValidNick(e.Params[0]) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Trailing[0] != ctcpDelim || e.Trailing[len(e.Trailing)-1] != ctcpDelim {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip delimiters.
|
||||||
|
text := e.Trailing[1 : len(e.Trailing)-1]
|
||||||
|
|
||||||
|
s := strings.IndexByte(text, eventSpace)
|
||||||
|
|
||||||
|
// Check to see if it only contains a tag.
|
||||||
|
if s < 0 {
|
||||||
|
for i := 0; i < len(text); i++ {
|
||||||
|
// Check for A-Z, 0-9.
|
||||||
|
if (text[i] < 0x41 || text[i] > 0x5A) && (text[i] < 0x30 || text[i] > 0x39) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CTCPEvent{
|
||||||
|
Origin: e,
|
||||||
|
Source: e.Source,
|
||||||
|
Command: text,
|
||||||
|
Reply: e.Command == NOTICE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through checking the tag first.
|
||||||
|
for i := 0; i < s; i++ {
|
||||||
|
// Check for A-Z, 0-9.
|
||||||
|
if (text[i] < 0x41 || text[i] > 0x5A) && (text[i] < 0x30 || text[i] > 0x39) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CTCPEvent{
|
||||||
|
Origin: e,
|
||||||
|
Source: e.Source,
|
||||||
|
Command: text[0:s],
|
||||||
|
Text: text[s+1:],
|
||||||
|
Reply: e.Command == NOTICE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeCTCP encodes a CTCP event into a string, including delimiters.
|
||||||
|
func encodeCTCP(ctcp *CTCPEvent) (out string) {
|
||||||
|
if ctcp == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return encodeCTCPRaw(ctcp.Command, ctcp.Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeCTCPRaw is much like encodeCTCP, however accepts a raw command and
|
||||||
|
// string as input.
|
||||||
|
func encodeCTCPRaw(cmd, text string) (out string) {
|
||||||
|
if len(cmd) <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
out = string(ctcpDelim) + cmd
|
||||||
|
|
||||||
|
if len(text) > 0 {
|
||||||
|
out += string(eventSpace) + text
|
||||||
|
}
|
||||||
|
|
||||||
|
return out + string(ctcpDelim)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CTCP handles the storage and execution of CTCP handlers against incoming
|
||||||
|
// CTCP events.
|
||||||
|
type CTCP struct {
|
||||||
|
// mu is the mutex that should be used when accessing any ctcp handlers.
|
||||||
|
mu sync.RWMutex
|
||||||
|
// handlers is a map of CTCP message -> functions.
|
||||||
|
handlers map[string]CTCPHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// newCTCP returns a new clean CTCP handler.
|
||||||
|
func newCTCP() *CTCP {
|
||||||
|
return &CTCP{handlers: map[string]CTCPHandler{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// call executes the necessary CTCP handler for the incoming event/CTCP
|
||||||
|
// command.
|
||||||
|
func (c *CTCP) call(client *Client, event *CTCPEvent) {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
|
// If they want to catch any panics, add to defer stack.
|
||||||
|
if client.Config.RecoverFunc != nil && event.Origin != nil {
|
||||||
|
defer recoverHandlerPanic(client, event.Origin, "ctcp-"+strings.ToLower(event.Command), 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support wildcard CTCP event handling. Gets executed first before
|
||||||
|
// regular event handlers.
|
||||||
|
if _, ok := c.handlers["*"]; ok {
|
||||||
|
c.handlers["*"](client, *event)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := c.handlers[event.Command]; !ok {
|
||||||
|
// Send a ERRMSG reply, if we know who sent it.
|
||||||
|
if event.Source != nil && IsValidNick(event.Source.Name) {
|
||||||
|
client.Cmd.SendCTCPReply(event.Source.Name, CTCP_ERRMSG, "that is an unknown CTCP query")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.handlers[event.Command](client, *event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCMD parses a CTCP command/tag, ensuring it's valid. If not, an empty
|
||||||
|
// string is returned.
|
||||||
|
func (c *CTCP) parseCMD(cmd string) string {
|
||||||
|
// TODO: Needs proper testing.
|
||||||
|
// Check if wildcard.
|
||||||
|
if cmd == "*" {
|
||||||
|
return "*"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd = strings.ToUpper(cmd)
|
||||||
|
|
||||||
|
for i := 0; i < len(cmd); i++ {
|
||||||
|
// Check for A-Z, 0-9.
|
||||||
|
if (cmd[i] < 0x41 || cmd[i] > 0x5A) && (cmd[i] < 0x30 || cmd[i] > 0x39) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set saves handler for execution upon a matching incoming CTCP event.
|
||||||
|
// Use SetBg if the handler may take an extended period of time to execute.
|
||||||
|
// If you would like to have a handler which will catch ALL CTCP requests,
|
||||||
|
// simply use "*" in place of the command.
|
||||||
|
func (c *CTCP) Set(cmd string, handler func(client *Client, ctcp CTCPEvent)) {
|
||||||
|
if cmd = c.parseCMD(cmd); cmd == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
c.handlers[cmd] = CTCPHandler(handler)
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBg is much like Set, however the handler is executed in the background,
|
||||||
|
// ensuring that event handling isn't hung during long running tasks. See Set
|
||||||
|
// for more information.
|
||||||
|
func (c *CTCP) SetBg(cmd string, handler func(client *Client, ctcp CTCPEvent)) {
|
||||||
|
c.Set(cmd, func(client *Client, ctcp CTCPEvent) {
|
||||||
|
go handler(client, ctcp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear removes currently setup handler for cmd, if one is set.
|
||||||
|
func (c *CTCP) Clear(cmd string) {
|
||||||
|
if cmd = c.parseCMD(cmd); cmd == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
delete(c.handlers, cmd)
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearAll removes all currently setup and re-sets the default handlers.
|
||||||
|
func (c *CTCP) ClearAll() {
|
||||||
|
c.mu.Lock()
|
||||||
|
c.handlers = map[string]CTCPHandler{}
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
// Register necessary handlers.
|
||||||
|
c.addDefaultHandlers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CTCPHandler is a type that represents the function necessary to
|
||||||
|
// implement a CTCP handler.
|
||||||
|
type CTCPHandler func(client *Client, ctcp CTCPEvent)
|
||||||
|
|
||||||
|
// addDefaultHandlers adds some useful default CTCP response handlers.
|
||||||
|
func (c *CTCP) addDefaultHandlers() {
|
||||||
|
c.SetBg(CTCP_PING, handleCTCPPing)
|
||||||
|
c.SetBg(CTCP_PONG, handleCTCPPong)
|
||||||
|
c.SetBg(CTCP_VERSION, handleCTCPVersion)
|
||||||
|
c.SetBg(CTCP_SOURCE, handleCTCPSource)
|
||||||
|
c.SetBg(CTCP_TIME, handleCTCPTime)
|
||||||
|
c.SetBg(CTCP_FINGER, handleCTCPFinger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCTCPPing replies with a ping and whatever was originally requested.
|
||||||
|
func handleCTCPPing(client *Client, ctcp CTCPEvent) {
|
||||||
|
if ctcp.Reply {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_PING, ctcp.Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCTCPPong replies with a pong.
|
||||||
|
func handleCTCPPong(client *Client, ctcp CTCPEvent) {
|
||||||
|
if ctcp.Reply {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_PONG, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCTCPVersion replies with the name of the client, Go version, as well
|
||||||
|
// as the os type (darwin, linux, windows, etc) and architecture type (x86,
|
||||||
|
// arm, etc).
|
||||||
|
func handleCTCPVersion(client *Client, ctcp CTCPEvent) {
|
||||||
|
if client.Config.Version != "" {
|
||||||
|
client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_VERSION, client.Config.Version)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Cmd.SendCTCPReplyf(
|
||||||
|
ctcp.Source.Name, CTCP_VERSION,
|
||||||
|
"girc (github.com/lrstanley/girc) using %s (%s, %s)",
|
||||||
|
runtime.Version(), runtime.GOOS, runtime.GOARCH,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCTCPSource replies with the public git location of this library.
|
||||||
|
func handleCTCPSource(client *Client, ctcp CTCPEvent) {
|
||||||
|
client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_SOURCE, "https://github.com/lrstanley/girc")
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCTCPTime replies with a RFC 1123 (Z) formatted version of Go's
|
||||||
|
// local time.
|
||||||
|
func handleCTCPTime(client *Client, ctcp CTCPEvent) {
|
||||||
|
client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_TIME, ":"+time.Now().Format(time.RFC1123Z))
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCTCPFinger replies with the realname and idle time of the user. This
|
||||||
|
// is obsoleted by improvements to the IRC protocol, however still supported.
|
||||||
|
func handleCTCPFinger(client *Client, ctcp CTCPEvent) {
|
||||||
|
client.conn.mu.RLock()
|
||||||
|
active := client.conn.lastActive
|
||||||
|
client.conn.mu.RUnlock()
|
||||||
|
|
||||||
|
client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_FINGER, fmt.Sprintf("%s -- idle %s", client.Config.Name, time.Since(active)))
|
||||||
|
}
|
12
vendor/github.com/lrstanley/girc/doc.go
generated
vendored
Normal file
12
vendor/github.com/lrstanley/girc/doc.go
generated
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// Package girc provides a high level, yet flexible IRC library for use with
|
||||||
|
// interacting with IRC servers. girc has support for user/channel tracking,
|
||||||
|
// as well as a few other neat features (like auto-reconnect).
|
||||||
|
//
|
||||||
|
// Much of what girc can do, can also be disabled. The goal is to provide a
|
||||||
|
// solid API that you don't necessarily have to work with out of the box if
|
||||||
|
// you don't want to.
|
||||||
|
//
|
||||||
|
// See the examples below for a few brief and useful snippets taking
|
||||||
|
// advantage of girc, which should give you a general idea of how the API
|
||||||
|
// works.
|
||||||
|
package girc
|
550
vendor/github.com/lrstanley/girc/event.go
generated
vendored
Normal file
550
vendor/github.com/lrstanley/girc/event.go
generated
vendored
Normal file
@ -0,0 +1,550 @@
|
|||||||
|
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||||
|
// of this source code is governed by the MIT license that can be found in
|
||||||
|
// the LICENSE file.
|
||||||
|
|
||||||
|
package girc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
eventSpace byte = 0x20 // Separator.
|
||||||
|
maxLength = 510 // Maximum length is 510 (2 for line endings).
|
||||||
|
)
|
||||||
|
|
||||||
|
// cutCRFunc is used to trim CR characters from prefixes/messages.
|
||||||
|
func cutCRFunc(r rune) bool {
|
||||||
|
return r == '\r' || r == '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event represents an IRC protocol message, see RFC1459 section 2.3.1
|
||||||
|
//
|
||||||
|
// <message> :: [':' <prefix> <SPACE>] <command> <params> <crlf>
|
||||||
|
// <prefix> :: <servername> | <nick> ['!' <user>] ['@' <host>]
|
||||||
|
// <command> :: <letter>{<letter>} | <number> <number> <number>
|
||||||
|
// <SPACE> :: ' '{' '}
|
||||||
|
// <params> :: <SPACE> [':' <trailing> | <middle> <params>]
|
||||||
|
// <middle> :: <Any *non-empty* sequence of octets not including SPACE or NUL
|
||||||
|
// or CR or LF, the first of which may not be ':'>
|
||||||
|
// <trailing> :: <Any, possibly empty, sequence of octets not including NUL or
|
||||||
|
// CR or LF>
|
||||||
|
// <crlf> :: CR LF
|
||||||
|
type Event struct {
|
||||||
|
Source *Source `json:"source"` // The source of the event.
|
||||||
|
Tags Tags `json:"tags"` // IRCv3 style message tags. Only use if network supported.
|
||||||
|
Command string `json:"command"` // the IRC command, e.g. JOIN, PRIVMSG, KILL.
|
||||||
|
Params []string `json:"params"` // parameters to the command. Commonly nickname, channel, etc.
|
||||||
|
Trailing string `json:"trailing"` // any trailing data. e.g. with a PRIVMSG, this is the message text.
|
||||||
|
EmptyTrailing bool `json:"empty_trailing"` // if true, trailing prefix (:) will be added even if Event.Trailing is empty.
|
||||||
|
Sensitive bool `json:"sensitive"` // if the message is sensitive (e.g. and should not be logged).
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseEvent takes a string and attempts to create a Event struct.
|
||||||
|
//
|
||||||
|
// Returns nil if the Event is invalid.
|
||||||
|
func ParseEvent(raw string) (e *Event) {
|
||||||
|
// Ignore empty events.
|
||||||
|
if raw = strings.TrimFunc(raw, cutCRFunc); len(raw) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var i, j int
|
||||||
|
e = &Event{}
|
||||||
|
|
||||||
|
if raw[0] == prefixTag {
|
||||||
|
// Tags end with a space.
|
||||||
|
i = strings.IndexByte(raw, eventSpace)
|
||||||
|
|
||||||
|
if i < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Tags = ParseTags(raw[1:i])
|
||||||
|
raw = raw[i+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw[0] == messagePrefix {
|
||||||
|
// Prefix ends with a space.
|
||||||
|
i = strings.IndexByte(raw, eventSpace)
|
||||||
|
|
||||||
|
// Prefix string must not be empty if the indicator is present.
|
||||||
|
if i < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Source = ParseSource(raw[1:i])
|
||||||
|
|
||||||
|
// Skip space at the end of the prefix.
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find end of command.
|
||||||
|
j = i + strings.IndexByte(raw[i:], eventSpace)
|
||||||
|
|
||||||
|
if j < i {
|
||||||
|
// If there are no proceeding spaces, it's the only thing specified.
|
||||||
|
e.Command = strings.ToUpper(raw[i:])
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Command = strings.ToUpper(raw[i:j])
|
||||||
|
|
||||||
|
// Skip the space after the command.
|
||||||
|
j++
|
||||||
|
|
||||||
|
// Check if and where the trailing text is within the incoming line.
|
||||||
|
var lastIndex, trailerIndex int
|
||||||
|
for {
|
||||||
|
// We must loop through, as it's possible that the first message
|
||||||
|
// prefix is not actually what we want. (e.g, colons are commonly
|
||||||
|
// used within ISUPPORT to delegate things like CHANLIMIT or TARGMAX.)
|
||||||
|
lastIndex = trailerIndex
|
||||||
|
trailerIndex = strings.IndexByte(raw[j+lastIndex:], messagePrefix)
|
||||||
|
|
||||||
|
if trailerIndex == -1 {
|
||||||
|
// No trailing argument found, assume the rest is just params.
|
||||||
|
e.Params = strings.Split(raw[j:], string(eventSpace))
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// This means we found a prefix that was proceeded by a space, and
|
||||||
|
// it's good to assume this is the start of trailing text to the line.
|
||||||
|
if raw[j+lastIndex+trailerIndex-1] == eventSpace {
|
||||||
|
i = lastIndex + trailerIndex
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep looping through until we either can't find any more prefixes,
|
||||||
|
// or we find the one we want.
|
||||||
|
trailerIndex += lastIndex + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set i to that of the substring we were using before, and where the
|
||||||
|
// trailing prefix is.
|
||||||
|
i = j + i
|
||||||
|
|
||||||
|
// Check if we need to parse arguments. If so, take everything after the
|
||||||
|
// command, and right before the trailing prefix, and cut it up.
|
||||||
|
if i > j {
|
||||||
|
e.Params = strings.Split(raw[j:i-1], string(eventSpace))
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Trailing = raw[i+1:]
|
||||||
|
|
||||||
|
// We need to re-encode the trailing argument even if it was empty.
|
||||||
|
if len(e.Trailing) <= 0 {
|
||||||
|
e.EmptyTrailing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy makes a deep copy of a given event, for use with allowing untrusted
|
||||||
|
// functions/handlers edit the event without causing potential issues with
|
||||||
|
// other handlers.
|
||||||
|
func (e *Event) Copy() *Event {
|
||||||
|
if e == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
newEvent := &Event{
|
||||||
|
Command: e.Command,
|
||||||
|
Trailing: e.Trailing,
|
||||||
|
EmptyTrailing: e.EmptyTrailing,
|
||||||
|
Sensitive: e.Sensitive,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy Source field, as it's a pointer and needs to be dereferenced.
|
||||||
|
if e.Source != nil {
|
||||||
|
newEvent.Source = e.Source.Copy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy Params in order to dereference as well.
|
||||||
|
if e.Params != nil {
|
||||||
|
newEvent.Params = make([]string, len(e.Params))
|
||||||
|
copy(newEvent.Params, e.Params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy tags as necessary.
|
||||||
|
if e.Tags != nil {
|
||||||
|
newEvent.Tags = Tags{}
|
||||||
|
for k, v := range e.Tags {
|
||||||
|
newEvent.Tags[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len calculates the length of the string representation of event. Note that
|
||||||
|
// this will return the true length (even if longer than what IRC supports),
|
||||||
|
// which may be useful if you are trying to check and see if a message is
|
||||||
|
// too long, to trim it down yourself.
|
||||||
|
func (e *Event) Len() (length int) {
|
||||||
|
if e.Tags != nil {
|
||||||
|
// Include tags and trailing space.
|
||||||
|
length = e.Tags.Len() + 1
|
||||||
|
}
|
||||||
|
if e.Source != nil {
|
||||||
|
// Include prefix and trailing space.
|
||||||
|
length += e.Source.Len() + 2
|
||||||
|
}
|
||||||
|
|
||||||
|
length += len(e.Command)
|
||||||
|
|
||||||
|
if len(e.Params) > 0 {
|
||||||
|
length += len(e.Params)
|
||||||
|
|
||||||
|
for i := 0; i < len(e.Params); i++ {
|
||||||
|
length += len(e.Params[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.Trailing) > 0 || e.EmptyTrailing {
|
||||||
|
// Include prefix and space.
|
||||||
|
length += len(e.Trailing) + 2
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns a []byte representation of event. Strips all newlines and
|
||||||
|
// carriage returns.
|
||||||
|
//
|
||||||
|
// Per RFC2812 section 2.3, messages should not exceed 512 characters in
|
||||||
|
// length. This method forces that limit by discarding any characters
|
||||||
|
// exceeding the length limit.
|
||||||
|
func (e *Event) Bytes() []byte {
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
|
||||||
|
// Tags.
|
||||||
|
if e.Tags != nil {
|
||||||
|
e.Tags.writeTo(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event prefix.
|
||||||
|
if e.Source != nil {
|
||||||
|
buffer.WriteByte(messagePrefix)
|
||||||
|
e.Source.writeTo(buffer)
|
||||||
|
buffer.WriteByte(eventSpace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command is required.
|
||||||
|
buffer.WriteString(e.Command)
|
||||||
|
|
||||||
|
// Space separated list of arguments.
|
||||||
|
if len(e.Params) > 0 {
|
||||||
|
buffer.WriteByte(eventSpace)
|
||||||
|
buffer.WriteString(strings.Join(e.Params, string(eventSpace)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.Trailing) > 0 || e.EmptyTrailing {
|
||||||
|
buffer.WriteByte(eventSpace)
|
||||||
|
buffer.WriteByte(messagePrefix)
|
||||||
|
buffer.WriteString(e.Trailing)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need the limit the buffer length.
|
||||||
|
if buffer.Len() > (maxLength) {
|
||||||
|
buffer.Truncate(maxLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := buffer.Bytes()
|
||||||
|
|
||||||
|
// Strip newlines and carriage returns.
|
||||||
|
for i := 0; i < len(out); i++ {
|
||||||
|
if out[i] == 0x0A || out[i] == 0x0D {
|
||||||
|
out = append(out[:i], out[i+1:]...)
|
||||||
|
i-- // Decrease the index so we can pick up where we left off.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of this event. Strips all newlines
|
||||||
|
// and carriage returns.
|
||||||
|
func (e *Event) String() string {
|
||||||
|
return string(e.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pretty returns a prettified string of the event. If the event doesn't
|
||||||
|
// support prettification, ok is false. Pretty is not just useful to make
|
||||||
|
// an event prettier, but also to filter out events that most don't visually
|
||||||
|
// see in normal IRC clients. e.g. most clients don't show WHO queries.
|
||||||
|
func (e *Event) Pretty() (out string, ok bool) {
|
||||||
|
if e.Sensitive {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == ERROR {
|
||||||
|
return fmt.Sprintf("[*] an error occurred: %s", e.Trailing), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Source == nil {
|
||||||
|
if e.Command != PRIVMSG && e.Command != NOTICE {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(e.Params) > 0 && len(e.Trailing) > 0 {
|
||||||
|
return fmt.Sprintf("[>] writing %s [%s]: %s", strings.ToLower(e.Command), strings.Join(e.Params, ", "), e.Trailing), true
|
||||||
|
} else if len(e.Params) > 0 {
|
||||||
|
return fmt.Sprintf("[>] writing %s [%s]", strings.ToLower(e.Command), strings.Join(e.Params, ", ")), true
|
||||||
|
} else if len(e.Trailing) > 0 {
|
||||||
|
return fmt.Sprintf("[>] writing %s: %s", strings.ToLower(e.Command), e.Trailing), true
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == INITIALIZED {
|
||||||
|
return fmt.Sprintf("[*] connection to %s initialized", e.Trailing), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == CONNECTED {
|
||||||
|
return fmt.Sprintf("[*] successfully connected to %s", e.Trailing), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.Command == PRIVMSG || e.Command == NOTICE) && len(e.Params) > 0 {
|
||||||
|
if ctcp := decodeCTCP(e); ctcp != nil {
|
||||||
|
if ctcp.Reply {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("[*] CTCP query from %s: %s%s", ctcp.Source.Name, ctcp.Command, " "+ctcp.Text), true
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("[%s] (%s) %s", strings.Join(e.Params, ","), e.Source.Name, e.Trailing), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == RPL_MOTD || e.Command == RPL_MOTDSTART ||
|
||||||
|
e.Command == RPL_WELCOME || e.Command == RPL_YOURHOST ||
|
||||||
|
e.Command == RPL_CREATED || e.Command == RPL_LUSERCLIENT {
|
||||||
|
return "[*] " + e.Trailing, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == JOIN && len(e.Params) > 0 {
|
||||||
|
return fmt.Sprintf("[*] %s (%s) has joined %s", e.Source.Name, e.Source.Host, e.Params[0]), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == PART && len(e.Params) > 0 {
|
||||||
|
return fmt.Sprintf("[*] %s (%s) has left %s (%s)", e.Source.Name, e.Source.Host, e.Params[0], e.Trailing), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == QUIT {
|
||||||
|
return fmt.Sprintf("[*] %s has quit (%s)", e.Source.Name, e.Trailing), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == KICK && len(e.Params) == 2 {
|
||||||
|
return fmt.Sprintf("[%s] *** %s has kicked %s: %s", e.Params[0], e.Source.Name, e.Params[1], e.Trailing), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == NICK && len(e.Params) == 1 {
|
||||||
|
return fmt.Sprintf("[*] %s is now known as %s", e.Source.Name, e.Params[0]), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == TOPIC && len(e.Params) > 0 {
|
||||||
|
return fmt.Sprintf("[%s] *** %s has set the topic to: %s", e.Params[len(e.Params)-1], e.Source.Name, e.Trailing), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == MODE && len(e.Params) > 2 {
|
||||||
|
return fmt.Sprintf("[%s] *** %s set modes: %s", e.Params[0], e.Source.Name, strings.Join(e.Params[1:], " ")), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == CAP_AWAY {
|
||||||
|
if len(e.Trailing) > 0 {
|
||||||
|
return fmt.Sprintf("[*] %s is now away: %s", e.Source.Name, e.Trailing), true
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("[*] %s is no longer away", e.Source.Name), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == CAP_CHGHOST && len(e.Params) == 2 {
|
||||||
|
return fmt.Sprintf("[*] %s has changed their host to %s (was %s)", e.Source.Name, e.Params[1], e.Source.Host), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == CAP_ACCOUNT && len(e.Params) == 1 {
|
||||||
|
if e.Params[0] == "*" {
|
||||||
|
return fmt.Sprintf("[*] %s has become un-authenticated", e.Source.Name), true
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("[*] %s has authenticated for account: %s", e.Source.Name, e.Params[0]), true
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Command == RPL_TOPIC && len(e.Params) > 0 && len(e.Trailing) > 0 {
|
||||||
|
return fmt.Sprintf("[*] topic for %s is: %s", e.Params[len(e.Params)-1], e.Trailing), true
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAction checks to see if the event is a PRIVMSG, and is an ACTION (/me).
|
||||||
|
func (e *Event) IsAction() bool {
|
||||||
|
if e.Source == nil || e.Command != PRIVMSG || len(e.Trailing) < 9 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(e.Trailing, "\001ACTION") || e.Trailing[len(e.Trailing)-1] != ctcpDelim {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsFromChannel checks to see if a message was from a channel (rather than
|
||||||
|
// a private message).
|
||||||
|
func (e *Event) IsFromChannel() bool {
|
||||||
|
if e.Source == nil || e.Command != PRIVMSG || len(e.Params) < 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsValidChannel(e.Params[0]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsFromUser checks to see if a message was from a user (rather than a
|
||||||
|
// channel).
|
||||||
|
func (e *Event) IsFromUser() bool {
|
||||||
|
if e.Source == nil || e.Command != PRIVMSG || len(e.Params) < 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsValidNick(e.Params[0]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// StripAction returns the stripped version of the action encoding from a
|
||||||
|
// PRIVMSG ACTION (/me).
|
||||||
|
func (e *Event) StripAction() string {
|
||||||
|
if !e.IsAction() {
|
||||||
|
return e.Trailing
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.Trailing[8 : len(e.Trailing)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
messagePrefix byte = 0x3A // ":" -- prefix or last argument
|
||||||
|
prefixIdent byte = 0x21 // "!" -- username
|
||||||
|
prefixHost byte = 0x40 // "@" -- hostname
|
||||||
|
)
|
||||||
|
|
||||||
|
// Source represents the sender of an IRC event, see RFC1459 section 2.3.1.
|
||||||
|
// <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
|
||||||
|
type Source struct {
|
||||||
|
// Name is the nickname, server name, or service name.
|
||||||
|
Name string `json:"name"`
|
||||||
|
// Ident is commonly known as the "user".
|
||||||
|
Ident string `json:"ident"`
|
||||||
|
// Host is the hostname or IP address of the user/service. Is not accurate
|
||||||
|
// due to how IRC servers can spoof hostnames.
|
||||||
|
Host string `json:"host"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy returns a deep copy of Source.
|
||||||
|
func (s *Source) Copy() *Source {
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
newSource := &Source{
|
||||||
|
Name: s.Name,
|
||||||
|
Ident: s.Ident,
|
||||||
|
Host: s.Host,
|
||||||
|
}
|
||||||
|
|
||||||
|
return newSource
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseSource takes a string and attempts to create a Source struct.
|
||||||
|
func ParseSource(raw string) (src *Source) {
|
||||||
|
src = new(Source)
|
||||||
|
|
||||||
|
user := strings.IndexByte(raw, prefixIdent)
|
||||||
|
host := strings.IndexByte(raw, prefixHost)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case user > 0 && host > user:
|
||||||
|
src.Name = raw[:user]
|
||||||
|
src.Ident = raw[user+1 : host]
|
||||||
|
src.Host = raw[host+1:]
|
||||||
|
case user > 0:
|
||||||
|
src.Name = raw[:user]
|
||||||
|
src.Ident = raw[user+1:]
|
||||||
|
case host > 0:
|
||||||
|
src.Name = raw[:host]
|
||||||
|
src.Host = raw[host+1:]
|
||||||
|
default:
|
||||||
|
src.Name = raw
|
||||||
|
}
|
||||||
|
|
||||||
|
return src
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len calculates the length of the string representation of prefix
|
||||||
|
func (s *Source) Len() (length int) {
|
||||||
|
length = len(s.Name)
|
||||||
|
if len(s.Ident) > 0 {
|
||||||
|
length = 1 + length + len(s.Ident)
|
||||||
|
}
|
||||||
|
if len(s.Host) > 0 {
|
||||||
|
length = 1 + length + len(s.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns a []byte representation of source.
|
||||||
|
func (s *Source) Bytes() []byte {
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
s.writeTo(buffer)
|
||||||
|
|
||||||
|
return buffer.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of source.
|
||||||
|
func (s *Source) String() (out string) {
|
||||||
|
out = s.Name
|
||||||
|
if len(s.Ident) > 0 {
|
||||||
|
out = out + string(prefixIdent) + s.Ident
|
||||||
|
}
|
||||||
|
if len(s.Host) > 0 {
|
||||||
|
out = out + string(prefixHost) + s.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsHostmask returns true if source looks like a user hostmask.
|
||||||
|
func (s *Source) IsHostmask() bool {
|
||||||
|
return len(s.Ident) > 0 && len(s.Host) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsServer returns true if this source looks like a server name.
|
||||||
|
func (s *Source) IsServer() bool {
|
||||||
|
return len(s.Ident) <= 0 && len(s.Host) <= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeTo is an utility function to write the source to the bytes.Buffer
|
||||||
|
// in Event.String().
|
||||||
|
func (s *Source) writeTo(buffer *bytes.Buffer) {
|
||||||
|
buffer.WriteString(s.Name)
|
||||||
|
if len(s.Ident) > 0 {
|
||||||
|
buffer.WriteByte(prefixIdent)
|
||||||
|
buffer.WriteString(s.Ident)
|
||||||
|
}
|
||||||
|
if len(s.Host) > 0 {
|
||||||
|
buffer.WriteByte(prefixHost)
|
||||||
|
buffer.WriteString(s.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
350
vendor/github.com/lrstanley/girc/format.go
generated
vendored
Normal file
350
vendor/github.com/lrstanley/girc/format.go
generated
vendored
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||||
|
// of this source code is governed by the MIT license that can be found in
|
||||||
|
// the LICENSE file.
|
||||||
|
|
||||||
|
package girc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fmtOpenChar = 0x7B // {
|
||||||
|
fmtCloseChar = 0x7D // }
|
||||||
|
)
|
||||||
|
|
||||||
|
var fmtColors = map[string]int{
|
||||||
|
"white": 0,
|
||||||
|
"black": 1,
|
||||||
|
"blue": 2,
|
||||||
|
"navy": 2,
|
||||||
|
"green": 3,
|
||||||
|
"red": 4,
|
||||||
|
"brown": 5,
|
||||||
|
"maroon": 5,
|
||||||
|
"purple": 6,
|
||||||
|
"gold": 7,
|
||||||
|
"olive": 7,
|
||||||
|
"orange": 7,
|
||||||
|
"yellow": 8,
|
||||||
|
"lightgreen": 9,
|
||||||
|
"lime": 9,
|
||||||
|
"teal": 10,
|
||||||
|
"cyan": 11,
|
||||||
|
"lightblue": 12,
|
||||||
|
"royal": 12,
|
||||||
|
"fuchsia": 13,
|
||||||
|
"lightpurple": 13,
|
||||||
|
"pink": 13,
|
||||||
|
"gray": 14,
|
||||||
|
"grey": 14,
|
||||||
|
"lightgrey": 15,
|
||||||
|
"silver": 15,
|
||||||
|
}
|
||||||
|
|
||||||
|
var fmtCodes = map[string]string{
|
||||||
|
"bold": "\x02",
|
||||||
|
"b": "\x02",
|
||||||
|
"italic": "\x1d",
|
||||||
|
"i": "\x1d",
|
||||||
|
"reset": "\x0f",
|
||||||
|
"r": "\x0f",
|
||||||
|
"clear": "\x03",
|
||||||
|
"c": "\x03", // Clears formatting.
|
||||||
|
"reverse": "\x16",
|
||||||
|
"underline": "\x1f",
|
||||||
|
"ul": "\x1f",
|
||||||
|
"ctcp": "\x01", // CTCP/ACTION delimiter.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fmt takes format strings like "{red}" or "{red,blue}" (for background
|
||||||
|
// colors) and turns them into the resulting ASCII format/color codes for IRC.
|
||||||
|
// See format.go for the list of supported format codes allowed.
|
||||||
|
//
|
||||||
|
// For example:
|
||||||
|
//
|
||||||
|
// client.Message("#channel", Fmt("{red}{b}Hello {red,blue}World{c}"))
|
||||||
|
func Fmt(text string) string {
|
||||||
|
var last = -1
|
||||||
|
for i := 0; i < len(text); i++ {
|
||||||
|
if text[i] == fmtOpenChar {
|
||||||
|
last = i
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if text[i] == fmtCloseChar && last > -1 {
|
||||||
|
code := strings.ToLower(text[last+1 : i])
|
||||||
|
|
||||||
|
// Check to see if they're passing in a second (background) color
|
||||||
|
// as {fgcolor,bgcolor}.
|
||||||
|
var secondary string
|
||||||
|
if com := strings.Index(code, ","); com > -1 {
|
||||||
|
secondary = code[com+1:]
|
||||||
|
code = code[:com]
|
||||||
|
}
|
||||||
|
|
||||||
|
var repl string
|
||||||
|
|
||||||
|
if color, ok := fmtColors[code]; ok {
|
||||||
|
repl = fmt.Sprintf("\x03%02d", color)
|
||||||
|
}
|
||||||
|
|
||||||
|
if repl != "" && secondary != "" {
|
||||||
|
if color, ok := fmtColors[secondary]; ok {
|
||||||
|
repl += fmt.Sprintf(",%02d", color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if repl == "" {
|
||||||
|
if fmtCode, ok := fmtCodes[code]; ok {
|
||||||
|
repl = fmtCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next := len(text[:last]+repl) - 1
|
||||||
|
text = text[:last] + repl + text[i+1:]
|
||||||
|
last = -1
|
||||||
|
i = next
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if last > -1 {
|
||||||
|
// A-Z, a-z, and ","
|
||||||
|
if text[i] != 0x2c && (text[i] <= 0x41 || text[i] >= 0x5a) && (text[i] <= 0x61 || text[i] >= 0x7a) {
|
||||||
|
last = -1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimFmt strips all "{fmt}" formatting strings from the input text.
|
||||||
|
// See Fmt() for more information.
|
||||||
|
func TrimFmt(text string) string {
|
||||||
|
for color := range fmtColors {
|
||||||
|
text = strings.Replace(text, "{"+color+"}", "", -1)
|
||||||
|
}
|
||||||
|
for code := range fmtCodes {
|
||||||
|
text = strings.Replace(text, "{"+code+"}", "", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is really the only fastest way of doing this (marginably better than
|
||||||
|
// actually trying to parse it manually.)
|
||||||
|
var reStripColor = regexp.MustCompile(`\x03([019]?[0-9](,[019]?[0-9])?)?`)
|
||||||
|
|
||||||
|
// StripRaw tries to strip all ASCII format codes that are used for IRC.
|
||||||
|
// Primarily, foreground/background colors, and other control bytes like
|
||||||
|
// reset, bold, italic, reverse, etc. This also is done in a specific way
|
||||||
|
// in order to ensure no truncation of other non-irc formatting.
|
||||||
|
func StripRaw(text string) string {
|
||||||
|
text = reStripColor.ReplaceAllString(text, "")
|
||||||
|
|
||||||
|
for _, code := range fmtCodes {
|
||||||
|
text = strings.Replace(text, code, "", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidChannel validates if channel is an RFC complaint channel or not.
|
||||||
|
//
|
||||||
|
// NOTE: If you are using this to validate a channel that contains a channel
|
||||||
|
// ID, (!<channelid>NAME), this only supports the standard 5 character length.
|
||||||
|
//
|
||||||
|
// NOTE: If you do not need to validate against servers that support unicode,
|
||||||
|
// you may want to ensure that all channel chars are within the range of
|
||||||
|
// all ASCII printable chars. This function will NOT do that for
|
||||||
|
// compatibility reasons.
|
||||||
|
//
|
||||||
|
// channel = ( "#" / "+" / ( "!" channelid ) / "&" ) chanstring
|
||||||
|
// [ ":" chanstring ]
|
||||||
|
// chanstring = 0x01-0x07 / 0x08-0x09 / 0x0B-0x0C / 0x0E-0x1F / 0x21-0x2B
|
||||||
|
// chanstring = / 0x2D-0x39 / 0x3B-0xFF
|
||||||
|
// ; any octet except NUL, BELL, CR, LF, " ", "," and ":"
|
||||||
|
// channelid = 5( 0x41-0x5A / digit ) ; 5( A-Z / 0-9 )
|
||||||
|
func IsValidChannel(channel string) bool {
|
||||||
|
if len(channel) <= 1 || len(channel) > 50 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// #, +, !<channelid>, or &
|
||||||
|
// Including "*" in the prefix list, as this is commonly used (e.g. ZNC)
|
||||||
|
if bytes.IndexByte([]byte{0x21, 0x23, 0x26, 0x2A, 0x2B}, channel[0]) == -1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// !<channelid> -- not very commonly supported, but we'll check it anyway.
|
||||||
|
// The ID must be 5 chars. This means min-channel size should be:
|
||||||
|
// 1 (prefix) + 5 (id) + 1 (+, channel name)
|
||||||
|
// On some networks, this may be extended with ISUPPORT capabilities,
|
||||||
|
// however this is extremely uncommon.
|
||||||
|
if channel[0] == 0x21 {
|
||||||
|
if len(channel) < 7 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for valid ID
|
||||||
|
for i := 1; i < 6; i++ {
|
||||||
|
if (channel[i] < 0x30 || channel[i] > 0x39) && (channel[i] < 0x41 || channel[i] > 0x5A) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for invalid octets here.
|
||||||
|
bad := []byte{0x00, 0x07, 0x0D, 0x0A, 0x20, 0x2C, 0x3A}
|
||||||
|
for i := 1; i < len(channel); i++ {
|
||||||
|
if bytes.IndexByte(bad, channel[i]) != -1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidNick validates an IRC nickame. Note that this does not validate
|
||||||
|
// IRC nickname length.
|
||||||
|
//
|
||||||
|
// nickname = ( letter / special ) *8( letter / digit / special / "-" )
|
||||||
|
// letter = 0x41-0x5A / 0x61-0x7A
|
||||||
|
// digit = 0x30-0x39
|
||||||
|
// special = 0x5B-0x60 / 0x7B-0x7D
|
||||||
|
func IsValidNick(nick string) bool {
|
||||||
|
if len(nick) <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
nick = ToRFC1459(nick)
|
||||||
|
|
||||||
|
// Check the first index. Some characters aren't allowed for the first
|
||||||
|
// index of an IRC nickname.
|
||||||
|
if nick[0] < 0x41 || nick[0] > 0x7D {
|
||||||
|
// a-z, A-Z, and _\[]{}^|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i < len(nick); i++ {
|
||||||
|
if (nick[i] < 0x41 || nick[i] > 0x7D) && (nick[i] < 0x30 || nick[i] > 0x39) && nick[i] != 0x2D {
|
||||||
|
// a-z, A-Z, 0-9, -, and _\[]{}^|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidUser validates an IRC ident/username. Note that this does not
|
||||||
|
// validate IRC ident length.
|
||||||
|
//
|
||||||
|
// The validation checks are much like what characters are allowed with an
|
||||||
|
// IRC nickname (see IsValidNick()), however an ident/username can:
|
||||||
|
//
|
||||||
|
// 1. Must either start with alphanumberic char, or "~" then alphanumberic
|
||||||
|
// char.
|
||||||
|
//
|
||||||
|
// 2. Contain a "." (period), for use with "first.last". Though, this may
|
||||||
|
// not be supported on all networks. Some limit this to only a single period.
|
||||||
|
//
|
||||||
|
// Per RFC:
|
||||||
|
// user = 1*( %x01-09 / %x0B-0C / %x0E-1F / %x21-3F / %x41-FF )
|
||||||
|
// ; any octet except NUL, CR, LF, " " and "@"
|
||||||
|
func IsValidUser(name string) bool {
|
||||||
|
if len(name) <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
name = ToRFC1459(name)
|
||||||
|
|
||||||
|
// "~" is prepended (commonly) if there was no ident server response.
|
||||||
|
if name[0] == 0x7E {
|
||||||
|
// Means name only contained "~".
|
||||||
|
if len(name) < 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
name = name[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check to see if the first index is alphanumeric.
|
||||||
|
if (name[0] < 0x41 || name[0] > 0x4A) && (name[0] < 0x61 || name[0] > 0x7A) && (name[0] < 0x30 || name[0] > 0x39) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i < len(name); i++ {
|
||||||
|
if (name[i] < 0x41 || name[i] > 0x7D) && (name[i] < 0x30 || name[i] > 0x39) && name[i] != 0x2D && name[i] != 0x2E {
|
||||||
|
// a-z, A-Z, 0-9, -, and _\[]{}^|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToRFC1459 converts a string to the stripped down conversion within RFC
|
||||||
|
// 1459. This will do things like replace an "A" with an "a", "[]" with "{}",
|
||||||
|
// and so forth. Useful to compare two nicknames or channels.
|
||||||
|
func ToRFC1459(input string) (out string) {
|
||||||
|
for i := 0; i < len(input); i++ {
|
||||||
|
if input[i] >= 65 && input[i] <= 94 {
|
||||||
|
out += string(rune(input[i]) + 32)
|
||||||
|
} else {
|
||||||
|
out += string(input[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
const globChar = "*"
|
||||||
|
|
||||||
|
// Glob will test a string pattern, potentially containing globs, against a
|
||||||
|
// string. The glob character is *.
|
||||||
|
func Glob(input, match string) bool {
|
||||||
|
// Empty pattern.
|
||||||
|
if match == "" {
|
||||||
|
return input == match
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a glob, match all.
|
||||||
|
if match == globChar {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(match, globChar)
|
||||||
|
|
||||||
|
if len(parts) == 1 {
|
||||||
|
// No globs, test for equality.
|
||||||
|
return input == match
|
||||||
|
}
|
||||||
|
|
||||||
|
leadingGlob, trailingGlob := strings.HasPrefix(match, globChar), strings.HasSuffix(match, globChar)
|
||||||
|
last := len(parts) - 1
|
||||||
|
|
||||||
|
// Check prefix first.
|
||||||
|
if !leadingGlob && !strings.HasPrefix(input, parts[0]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check middle section.
|
||||||
|
for i := 1; i < last; i++ {
|
||||||
|
if !strings.Contains(input, parts[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim already-evaluated text from input during loop over match
|
||||||
|
// text.
|
||||||
|
idx := strings.Index(input, parts[i]) + len(parts[i])
|
||||||
|
input = input[idx:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check suffix last.
|
||||||
|
return trailingGlob || strings.HasSuffix(input, parts[last])
|
||||||
|
}
|
484
vendor/github.com/lrstanley/girc/handler.go
generated
vendored
Normal file
484
vendor/github.com/lrstanley/girc/handler.go
generated
vendored
Normal file
@ -0,0 +1,484 @@
|
|||||||
|
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||||
|
// of this source code is governed by the MIT license that can be found in
|
||||||
|
// the LICENSE file.
|
||||||
|
|
||||||
|
package girc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunHandlers manually runs handlers for a given event.
|
||||||
|
func (c *Client) RunHandlers(event *Event) {
|
||||||
|
if event == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the event.
|
||||||
|
c.debug.Print("< " + StripRaw(event.String()))
|
||||||
|
if c.Config.Out != nil {
|
||||||
|
if pretty, ok := event.Pretty(); ok {
|
||||||
|
fmt.Fprintln(c.Config.Out, StripRaw(pretty))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular wildcard handlers.
|
||||||
|
c.Handlers.exec(ALL_EVENTS, c, event.Copy())
|
||||||
|
|
||||||
|
// Then regular handlers.
|
||||||
|
c.Handlers.exec(event.Command, c, event.Copy())
|
||||||
|
|
||||||
|
// Check if it's a CTCP.
|
||||||
|
if ctcp := decodeCTCP(event.Copy()); ctcp != nil {
|
||||||
|
// Execute it.
|
||||||
|
c.CTCP.call(c, ctcp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler is lower level implementation of a handler. See
|
||||||
|
// Caller.AddHandler()
|
||||||
|
type Handler interface {
|
||||||
|
Execute(*Client, Event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlerFunc is a type that represents the function necessary to
|
||||||
|
// implement Handler.
|
||||||
|
type HandlerFunc func(client *Client, event Event)
|
||||||
|
|
||||||
|
// Execute calls the HandlerFunc with the sender and irc message.
|
||||||
|
func (f HandlerFunc) Execute(client *Client, event Event) {
|
||||||
|
f(client, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caller manages internal and external (user facing) handlers.
|
||||||
|
type Caller struct {
|
||||||
|
// mu is the mutex that should be used when accessing handlers.
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
// external/internal keys are of structure:
|
||||||
|
// map[COMMAND][CUID]Handler
|
||||||
|
// Also of note: "COMMAND" should always be uppercase for normalization.
|
||||||
|
|
||||||
|
// external is a map of user facing handlers.
|
||||||
|
external map[string]map[string]Handler
|
||||||
|
// internal is a map of internally used handlers for the client.
|
||||||
|
internal map[string]map[string]Handler
|
||||||
|
// debug is the clients logger used for debugging.
|
||||||
|
debug *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// newCaller creates and initializes a new handler.
|
||||||
|
func newCaller(debugOut *log.Logger) *Caller {
|
||||||
|
c := &Caller{
|
||||||
|
external: map[string]map[string]Handler{},
|
||||||
|
internal: map[string]map[string]Handler{},
|
||||||
|
debug: debugOut,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the total amount of user-entered registered handlers.
|
||||||
|
func (c *Caller) Len() int {
|
||||||
|
var total int
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
for command := range c.external {
|
||||||
|
total += len(c.external[command])
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count is much like Caller.Len(), however it counts the number of
|
||||||
|
// registered handlers for a given command.
|
||||||
|
func (c *Caller) Count(cmd string) int {
|
||||||
|
var total int
|
||||||
|
|
||||||
|
cmd = strings.ToUpper(cmd)
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
for command := range c.external {
|
||||||
|
if command == cmd {
|
||||||
|
total += len(c.external[command])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Caller) String() string {
|
||||||
|
var total int
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
for cmd := range c.internal {
|
||||||
|
total += len(c.internal[cmd])
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
return fmt.Sprintf("<Caller external:%d internal:%d>", c.Len(), total)
|
||||||
|
}
|
||||||
|
|
||||||
|
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
|
||||||
|
// cuid generates a unique UID string for each handler for ease of removal.
|
||||||
|
func (c *Caller) cuid(cmd string, n int) (cuid, uid string) {
|
||||||
|
b := make([]byte, n)
|
||||||
|
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd + ":" + string(b), string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cuidToID allows easy mapping between a generated cuid and the caller
|
||||||
|
// external/internal handler maps.
|
||||||
|
func (c *Caller) cuidToID(input string) (cmd, uid string) {
|
||||||
|
i := strings.IndexByte(input, 0x3A)
|
||||||
|
if i < 0 {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return input[:i], input[i+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
type execStack struct {
|
||||||
|
Handler
|
||||||
|
cuid string
|
||||||
|
}
|
||||||
|
|
||||||
|
// exec executes all handlers pertaining to specified event. Internal first,
|
||||||
|
// then external.
|
||||||
|
//
|
||||||
|
// Please note that there is no specific order/priority for which the
|
||||||
|
// handler types themselves or the handlers are executed.
|
||||||
|
func (c *Caller) exec(command string, client *Client, event *Event) {
|
||||||
|
// Build a stack of handlers which can be executed concurrently.
|
||||||
|
var stack []execStack
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
// Get internal handlers first.
|
||||||
|
if _, ok := c.internal[command]; ok {
|
||||||
|
for cuid := range c.internal[command] {
|
||||||
|
stack = append(stack, execStack{c.internal[command][cuid], cuid})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aaand then external handlers.
|
||||||
|
if _, ok := c.external[command]; ok {
|
||||||
|
for cuid := range c.external[command] {
|
||||||
|
stack = append(stack, execStack{c.external[command][cuid], cuid})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
// Run all handlers concurrently across the same event. This should
|
||||||
|
// still help prevent mis-ordered events, while speeding up the
|
||||||
|
// execution speed.
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(len(stack))
|
||||||
|
for i := 0; i < len(stack); i++ {
|
||||||
|
go func(index int) {
|
||||||
|
c.debug.Printf("executing handler %s for event %s (%d of %d)", stack[index].cuid, command, index+1, len(stack))
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
// If they want to catch any panics, add to defer stack.
|
||||||
|
if client.Config.RecoverFunc != nil {
|
||||||
|
defer recoverHandlerPanic(client, event, stack[index].cuid, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
stack[index].Execute(client, *event)
|
||||||
|
|
||||||
|
c.debug.Printf("execution of %s took %s (%d of %d)", stack[index].cuid, time.Since(start), index+1, len(stack))
|
||||||
|
wg.Done()
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all of the handlers to complete. Not doing this may cause
|
||||||
|
// new events from becoming ahead of older handlers.
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearAll clears all external handlers currently setup within the client.
|
||||||
|
// This ignores internal handlers.
|
||||||
|
func (c *Caller) ClearAll() {
|
||||||
|
c.mu.Lock()
|
||||||
|
c.external = map[string]map[string]Handler{}
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
c.debug.Print("cleared all external handlers")
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearInternal clears all internal handlers currently setup within the
|
||||||
|
// client.
|
||||||
|
func (c *Caller) clearInternal() {
|
||||||
|
c.mu.Lock()
|
||||||
|
c.internal = map[string]map[string]Handler{}
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
c.debug.Print("cleared all internal handlers")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear clears all of the handlers for the given event.
|
||||||
|
// This ignores internal handlers.
|
||||||
|
func (c *Caller) Clear(cmd string) {
|
||||||
|
cmd = strings.ToUpper(cmd)
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
if _, ok := c.external[cmd]; ok {
|
||||||
|
delete(c.external, cmd)
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
c.debug.Printf("cleared external handlers for %s", cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove removes the handler with cuid from the handler stack. success
|
||||||
|
// indicates that it existed, and has been removed. If not success, it
|
||||||
|
// wasn't a registered handler.
|
||||||
|
func (c *Caller) Remove(cuid string) (success bool) {
|
||||||
|
c.mu.Lock()
|
||||||
|
success = c.remove(cuid)
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
return success
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove is much like Remove, however is NOT concurrency safe. Lock Caller.mu
|
||||||
|
// on your own.
|
||||||
|
func (c *Caller) remove(cuid string) (success bool) {
|
||||||
|
cmd, uid := c.cuidToID(cuid)
|
||||||
|
if len(cmd) == 0 || len(uid) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the irc command/event has any handlers on it.
|
||||||
|
if _, ok := c.external[cmd]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check to see if it's actually a registered handler.
|
||||||
|
if _, ok := c.external[cmd][uid]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(c.external[cmd], uid)
|
||||||
|
c.debug.Printf("removed handler %s", cuid)
|
||||||
|
|
||||||
|
// Assume success.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// sregister is much like Caller.register(), except that it safely locks
|
||||||
|
// the Caller mutex.
|
||||||
|
func (c *Caller) sregister(internal bool, cmd string, handler Handler) (cuid string) {
|
||||||
|
c.mu.Lock()
|
||||||
|
cuid = c.register(internal, cmd, handler)
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
return cuid
|
||||||
|
}
|
||||||
|
|
||||||
|
// register will register a handler in the internal tracker. Unsafe (you
|
||||||
|
// must lock c.mu yourself!)
|
||||||
|
func (c *Caller) register(internal bool, cmd string, handler Handler) (cuid string) {
|
||||||
|
var uid string
|
||||||
|
|
||||||
|
cmd = strings.ToUpper(cmd)
|
||||||
|
|
||||||
|
if internal {
|
||||||
|
if _, ok := c.internal[cmd]; !ok {
|
||||||
|
c.internal[cmd] = map[string]Handler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
cuid, uid = c.cuid(cmd, 20)
|
||||||
|
c.internal[cmd][uid] = handler
|
||||||
|
} else {
|
||||||
|
if _, ok := c.external[cmd]; !ok {
|
||||||
|
c.external[cmd] = map[string]Handler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
cuid, uid = c.cuid(cmd, 20)
|
||||||
|
c.external[cmd][uid] = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
_, file, line, _ := runtime.Caller(3)
|
||||||
|
|
||||||
|
c.debug.Printf("registering handler for %q with cuid %q (internal: %t) from: %s:%d", cmd, cuid, internal, file, line)
|
||||||
|
|
||||||
|
return cuid
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddHandler registers a handler (matching the handler interface) for the
|
||||||
|
// given event. cuid is the handler uid which can be used to remove the
|
||||||
|
// handler with Caller.Remove().
|
||||||
|
func (c *Caller) AddHandler(cmd string, handler Handler) (cuid string) {
|
||||||
|
return c.sregister(false, cmd, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add registers the handler function for the given event. cuid is the
|
||||||
|
// handler uid which can be used to remove the handler with Caller.Remove().
|
||||||
|
func (c *Caller) Add(cmd string, handler func(client *Client, event Event)) (cuid string) {
|
||||||
|
return c.sregister(false, cmd, HandlerFunc(handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddBg registers the handler function for the given event and executes it
|
||||||
|
// in a go-routine. cuid is the handler uid which can be used to remove the
|
||||||
|
// handler with Caller.Remove().
|
||||||
|
func (c *Caller) AddBg(cmd string, handler func(client *Client, event Event)) (cuid string) {
|
||||||
|
return c.sregister(false, cmd, HandlerFunc(func(client *Client, event Event) {
|
||||||
|
// Setting up background-based handlers this way allows us to get
|
||||||
|
// clean call stacks for use with panic recovery.
|
||||||
|
go func() {
|
||||||
|
// If they want to catch any panics, add to defer stack.
|
||||||
|
if client.Config.RecoverFunc != nil {
|
||||||
|
defer recoverHandlerPanic(client, &event, "goroutine", 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler(client, event)
|
||||||
|
}()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTmp adds a "temporary" handler, which is good for one-time or few-time
|
||||||
|
// uses. This supports a deadline and/or manual removal, as this differs
|
||||||
|
// much from how normal handlers work. An example of a good use for this
|
||||||
|
// would be to capture the entire output of a multi-response query to the
|
||||||
|
// server. (e.g. LIST, WHOIS, etc)
|
||||||
|
//
|
||||||
|
// The supplied handler is able to return a boolean, which if true, will
|
||||||
|
// remove the handler from the handler stack.
|
||||||
|
//
|
||||||
|
// Additionally, AddTmp has a useful option, deadline. When set to greater
|
||||||
|
// than 0, deadline will be the amount of time that passes before the handler
|
||||||
|
// is removed from the stack, regardless if the handler returns true or not.
|
||||||
|
// This is useful in that it ensures that the handler is cleaned up if the
|
||||||
|
// server does not respond appropriately, or takes too long to respond.
|
||||||
|
//
|
||||||
|
// Note that handlers supplied with AddTmp are executed in a goroutine to
|
||||||
|
// ensure that they are not blocking other handlers. Additionally, use cuid
|
||||||
|
// with Caller.Remove() to prematurely remove the handler from the stack,
|
||||||
|
// bypassing the timeout or waiting for the handler to return that it wants
|
||||||
|
// to be removed from the stack.
|
||||||
|
func (c *Caller) AddTmp(cmd string, deadline time.Duration, handler func(client *Client, event Event) bool) (cuid string, done chan struct{}) {
|
||||||
|
var uid string
|
||||||
|
cuid, uid = c.cuid(cmd, 20)
|
||||||
|
|
||||||
|
done = make(chan struct{})
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
if _, ok := c.external[cmd]; !ok {
|
||||||
|
c.external[cmd] = map[string]Handler{}
|
||||||
|
}
|
||||||
|
c.external[cmd][uid] = HandlerFunc(func(client *Client, event Event) {
|
||||||
|
// Setting up background-based handlers this way allows us to get
|
||||||
|
// clean call stacks for use with panic recovery.
|
||||||
|
go func() {
|
||||||
|
// If they want to catch any panics, add to defer stack.
|
||||||
|
if client.Config.RecoverFunc != nil {
|
||||||
|
defer recoverHandlerPanic(client, &event, "tmp-goroutine", 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
remove := handler(client, event)
|
||||||
|
if remove {
|
||||||
|
if ok := c.Remove(cuid); ok {
|
||||||
|
close(done)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
if deadline > 0 {
|
||||||
|
go func() {
|
||||||
|
<-time.After(deadline)
|
||||||
|
if ok := c.Remove(cuid); ok {
|
||||||
|
close(done)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
return cuid, done
|
||||||
|
}
|
||||||
|
|
||||||
|
// recoverHandlerPanic is used to catch all handler panics, and re-route
|
||||||
|
// them if necessary.
|
||||||
|
func recoverHandlerPanic(client *Client, event *Event, id string, skip int) {
|
||||||
|
perr := recover()
|
||||||
|
if perr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var file string
|
||||||
|
var line int
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
_, file, line, ok = runtime.Caller(skip)
|
||||||
|
|
||||||
|
err := &HandlerError{
|
||||||
|
Event: *event,
|
||||||
|
ID: id,
|
||||||
|
File: file,
|
||||||
|
Line: line,
|
||||||
|
Panic: perr,
|
||||||
|
Stack: debug.Stack(),
|
||||||
|
callOk: ok,
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Config.RecoverFunc(client, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlerError is the error returned when a panic is intentionally recovered
|
||||||
|
// from. It contains useful information like the handler identifier (if
|
||||||
|
// applicable), filename, line in file where panic occurred, the call
|
||||||
|
// trace, and original event.
|
||||||
|
type HandlerError struct {
|
||||||
|
Event Event // Event is the event that caused the error.
|
||||||
|
ID string // ID is the CUID of the handler.
|
||||||
|
File string // File is the file from where the panic originated.
|
||||||
|
Line int // Line number where panic originated.
|
||||||
|
Panic interface{} // Panic is the error that was passed to panic().
|
||||||
|
Stack []byte // Stack is the call stack. Note you may have to skip 1 or 2 due to debug functions.
|
||||||
|
callOk bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns a prettified version of HandlerError, containing ID, file,
|
||||||
|
// line, and basic error string.
|
||||||
|
func (e *HandlerError) Error() string {
|
||||||
|
if e.callOk {
|
||||||
|
return fmt.Sprintf("panic during handler [%s] execution in %s:%d: %s", e.ID, e.File, e.Line, e.Panic)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("panic during handler [%s] execution in unknown: %s", e.ID, e.Panic)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the error that panic returned, as well as the entire call
|
||||||
|
// trace of where it originated.
|
||||||
|
func (e *HandlerError) String() string {
|
||||||
|
return fmt.Sprintf("panic: %s\n\n%s", e.Panic, string(e.Stack))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultRecoverHandler can be used with Config.RecoverFunc as a default
|
||||||
|
// catch-all for panics. This will log the error, and the call trace to the
|
||||||
|
// debug log (see Config.Debug), or os.Stdout if Config.Debug is unset.
|
||||||
|
func DefaultRecoverHandler(client *Client, err *HandlerError) {
|
||||||
|
if client.Config.Debug == nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
fmt.Println(err.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client.debug.Println(err.Error())
|
||||||
|
client.debug.Println(err.String())
|
||||||
|
}
|
550
vendor/github.com/lrstanley/girc/modes.go
generated
vendored
Normal file
550
vendor/github.com/lrstanley/girc/modes.go
generated
vendored
Normal file
@ -0,0 +1,550 @@
|
|||||||
|
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||||
|
// of this source code is governed by the MIT license that can be found in
|
||||||
|
// the LICENSE file.
|
||||||
|
|
||||||
|
package girc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CMode represents a single step of a given mode change.
|
||||||
|
type CMode struct {
|
||||||
|
add bool // if it's a +, or -.
|
||||||
|
name byte // character representation of the given mode.
|
||||||
|
setting bool // if it's a setting (should be stored) or temporary (op/voice/etc).
|
||||||
|
args string // arguments to the mode, if arguments are supported.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Short returns a short representation of a mode without arguments. E.g. "+a",
|
||||||
|
// or "-b".
|
||||||
|
func (c *CMode) Short() string {
|
||||||
|
var status string
|
||||||
|
if c.add {
|
||||||
|
status = "+"
|
||||||
|
} else {
|
||||||
|
status = "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
return status + string(c.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of a mode, including optional
|
||||||
|
// arguments. E.g. "+b user*!ident@host.*.com"
|
||||||
|
func (c *CMode) String() string {
|
||||||
|
if len(c.args) == 0 {
|
||||||
|
return c.Short()
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Short() + " " + c.args
|
||||||
|
}
|
||||||
|
|
||||||
|
// CModes is a representation of a set of modes. This may be the given state
|
||||||
|
// of a channel/user, or the given state changes to a given channel/user.
|
||||||
|
type CModes struct {
|
||||||
|
raw string // raw supported modes.
|
||||||
|
modesListArgs string // modes that add/remove users from lists and support args.
|
||||||
|
modesArgs string // modes that support args.
|
||||||
|
modesSetArgs string // modes that support args ONLY when set.
|
||||||
|
modesNoArgs string // modes that do not support args.
|
||||||
|
|
||||||
|
prefixes string // user permission prefixes. these aren't a CMode.setting.
|
||||||
|
modes []CMode // the list of modes for this given state.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy returns a deep copy of CModes.
|
||||||
|
func (c *CModes) Copy() (nc CModes) {
|
||||||
|
nc = CModes{}
|
||||||
|
nc = *c
|
||||||
|
|
||||||
|
nc.modes = make([]CMode, len(c.modes))
|
||||||
|
|
||||||
|
// Copy modes.
|
||||||
|
for i := 0; i < len(c.modes); i++ {
|
||||||
|
nc.modes[i] = c.modes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nc
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a complete set of modes for this given state (change?). For
|
||||||
|
// example, "+a-b+cde some-arg".
|
||||||
|
func (c *CModes) String() string {
|
||||||
|
var out string
|
||||||
|
var args string
|
||||||
|
|
||||||
|
if len(c.modes) > 0 {
|
||||||
|
out += "+"
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(c.modes); i++ {
|
||||||
|
out += string(c.modes[i].name)
|
||||||
|
|
||||||
|
if len(c.modes[i].args) > 0 {
|
||||||
|
args += " " + c.modes[i].args
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out + args
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasMode checks if the CModes state has a given mode. E.g. "m", or "I".
|
||||||
|
func (c *CModes) HasMode(mode string) bool {
|
||||||
|
for i := 0; i < len(c.modes); i++ {
|
||||||
|
if string(c.modes[i].name) == mode {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the arguments for a given mode within this session, if it
|
||||||
|
// supports args.
|
||||||
|
func (c *CModes) Get(mode string) (args string, ok bool) {
|
||||||
|
for i := 0; i < len(c.modes); i++ {
|
||||||
|
if string(c.modes[i].name) == mode {
|
||||||
|
if len(c.modes[i].args) == 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.modes[i].args, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasArg checks to see if the mode supports arguments. What ones support this?:
|
||||||
|
// A = Mode that adds or removes a nick or address to a list. Always has a parameter.
|
||||||
|
// B = Mode that changes a setting and always has a parameter.
|
||||||
|
// C = Mode that changes a setting and only has a parameter when set.
|
||||||
|
// D = Mode that changes a setting and never has a parameter.
|
||||||
|
// Note: Modes of type A return the list when there is no parameter present.
|
||||||
|
// Note: Some clients assumes that any mode not listed is of type D.
|
||||||
|
// Note: Modes in PREFIX are not listed but could be considered type B.
|
||||||
|
func (c *CModes) hasArg(set bool, mode byte) (hasArgs, isSetting bool) {
|
||||||
|
if len(c.raw) < 1 {
|
||||||
|
return false, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.IndexByte(c.modesListArgs, mode) > -1 {
|
||||||
|
return true, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.IndexByte(c.modesArgs, mode) > -1 {
|
||||||
|
return true, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.IndexByte(c.modesSetArgs, mode) > -1 {
|
||||||
|
if set {
|
||||||
|
return true, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.IndexByte(c.prefixes, mode) > -1 {
|
||||||
|
return true, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply merges two state changes, or one state change into a state of modes.
|
||||||
|
// For example, the latter would mean applying an incoming MODE with the modes
|
||||||
|
// stored for a channel.
|
||||||
|
func (c *CModes) Apply(modes []CMode) {
|
||||||
|
var new []CMode
|
||||||
|
|
||||||
|
for j := 0; j < len(c.modes); j++ {
|
||||||
|
isin := false
|
||||||
|
for i := 0; i < len(modes); i++ {
|
||||||
|
if !modes[i].setting {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c.modes[j].name == modes[i].name && modes[i].add {
|
||||||
|
new = append(new, modes[i])
|
||||||
|
isin = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isin {
|
||||||
|
new = append(new, c.modes[j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(modes); i++ {
|
||||||
|
if !modes[i].setting || !modes[i].add {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
isin := false
|
||||||
|
for j := 0; j < len(new); j++ {
|
||||||
|
if modes[i].name == new[j].name {
|
||||||
|
isin = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isin {
|
||||||
|
new = append(new, modes[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.modes = new
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parses a set of flags and args, returning the necessary list of
|
||||||
|
// mappings for the mode flags.
|
||||||
|
func (c *CModes) Parse(flags string, args []string) (out []CMode) {
|
||||||
|
// add is the mode state we're currently in. Adding, or removing modes.
|
||||||
|
add := true
|
||||||
|
var argCount int
|
||||||
|
|
||||||
|
for i := 0; i < len(flags); i++ {
|
||||||
|
if flags[i] == 0x2B {
|
||||||
|
add = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if flags[i] == 0x2D {
|
||||||
|
add = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := CMode{
|
||||||
|
name: flags[i],
|
||||||
|
add: add,
|
||||||
|
}
|
||||||
|
|
||||||
|
hasArgs, isSetting := c.hasArg(add, flags[i])
|
||||||
|
if hasArgs && len(args) >= argCount+1 {
|
||||||
|
mode.args = args[argCount]
|
||||||
|
argCount++
|
||||||
|
}
|
||||||
|
mode.setting = isSetting
|
||||||
|
|
||||||
|
out = append(out, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCModes returns a new CModes reference. channelModes and userPrefixes
|
||||||
|
// would be something you see from the server's "CHANMODES" and "PREFIX"
|
||||||
|
// ISUPPORT capability messages (alternatively, fall back to the standard)
|
||||||
|
// DefaultPrefixes and ModeDefaults.
|
||||||
|
func NewCModes(channelModes, userPrefixes string) CModes {
|
||||||
|
split := strings.SplitN(channelModes, ",", 4)
|
||||||
|
if len(split) != 4 {
|
||||||
|
for i := len(split); i < 4; i++ {
|
||||||
|
split = append(split, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CModes{
|
||||||
|
raw: channelModes,
|
||||||
|
modesListArgs: split[0],
|
||||||
|
modesArgs: split[1],
|
||||||
|
modesSetArgs: split[2],
|
||||||
|
modesNoArgs: split[3],
|
||||||
|
|
||||||
|
prefixes: userPrefixes,
|
||||||
|
modes: []CMode{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidChannelMode validates a channel mode (CHANMODES).
|
||||||
|
func IsValidChannelMode(raw string) bool {
|
||||||
|
if len(raw) < 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(raw); i++ {
|
||||||
|
// Allowed are: ",", A-Z and a-z.
|
||||||
|
if raw[i] != 0x2C && (raw[i] < 0x41 || raw[i] > 0x5A) && (raw[i] < 0x61 || raw[i] > 0x7A) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidUserPrefix validates a list of ISUPPORT-style user prefixes (PREFIX).
|
||||||
|
func isValidUserPrefix(raw string) bool {
|
||||||
|
if len(raw) < 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw[0] != 0x28 { // (.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys, rep int
|
||||||
|
var passedKeys bool
|
||||||
|
|
||||||
|
// Skip the first one as we know it's (.
|
||||||
|
for i := 1; i < len(raw); i++ {
|
||||||
|
if raw[i] == 0x29 { // ).
|
||||||
|
passedKeys = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if passedKeys {
|
||||||
|
rep++
|
||||||
|
} else {
|
||||||
|
keys++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys == rep
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePrefixes parses the mode character mappings from the symbols of a
|
||||||
|
// ISUPPORT-style user prefixes list (PREFIX).
|
||||||
|
func parsePrefixes(raw string) (modes, prefixes string) {
|
||||||
|
if !isValidUserPrefix(raw) {
|
||||||
|
return modes, prefixes
|
||||||
|
}
|
||||||
|
|
||||||
|
i := strings.Index(raw, ")")
|
||||||
|
if i < 1 {
|
||||||
|
return modes, prefixes
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw[1:i], raw[i+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMODE handles incoming MODE messages, and updates the tracking
|
||||||
|
// information for each channel, as well as if any of the modes affect user
|
||||||
|
// permissions.
|
||||||
|
func handleMODE(c *Client, e Event) {
|
||||||
|
// Check if it's a RPL_CHANNELMODEIS.
|
||||||
|
if e.Command == RPL_CHANNELMODEIS && len(e.Params) > 2 {
|
||||||
|
// RPL_CHANNELMODEIS sends the user as the first param, skip it.
|
||||||
|
e.Params = e.Params[1:]
|
||||||
|
}
|
||||||
|
// Should be at least MODE <target> <flags>, to be useful. As well, only
|
||||||
|
// tracking channel modes at the moment.
|
||||||
|
if len(e.Params) < 2 || !IsValidChannel(e.Params[0]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.RLock()
|
||||||
|
channel := c.state.lookupChannel(e.Params[0])
|
||||||
|
if channel == nil {
|
||||||
|
c.state.RUnlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := e.Params[1]
|
||||||
|
var args []string
|
||||||
|
if len(e.Params) > 2 {
|
||||||
|
args = append(args, e.Params[2:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
modes := channel.Modes.Parse(flags, args)
|
||||||
|
channel.Modes.Apply(modes)
|
||||||
|
|
||||||
|
// Loop through and update users modes as necessary.
|
||||||
|
for i := 0; i < len(modes); i++ {
|
||||||
|
if modes[i].setting || len(modes[i].args) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
user := c.state.lookupUser(modes[i].args)
|
||||||
|
if user != nil {
|
||||||
|
perms, _ := user.Perms.Lookup(channel.Name)
|
||||||
|
perms.setFromMode(modes[i])
|
||||||
|
user.Perms.set(channel.Name, perms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.state.RUnlock()
|
||||||
|
c.state.notify(c, UPDATE_STATE)
|
||||||
|
}
|
||||||
|
|
||||||
|
// chanModes returns the ISUPPORT list of server-supported channel modes,
|
||||||
|
// alternatively falling back to ModeDefaults.
|
||||||
|
func (s *state) chanModes() string {
|
||||||
|
if modes, ok := s.serverOptions["CHANMODES"]; ok && IsValidChannelMode(modes) {
|
||||||
|
return modes
|
||||||
|
}
|
||||||
|
|
||||||
|
return ModeDefaults
|
||||||
|
}
|
||||||
|
|
||||||
|
// userPrefixes returns the ISUPPORT list of server-supported user prefixes.
|
||||||
|
// This includes mode characters, as well as user prefix symbols. Falls back
|
||||||
|
// to DefaultPrefixes if not server-supported.
|
||||||
|
func (s *state) userPrefixes() string {
|
||||||
|
if prefix, ok := s.serverOptions["PREFIX"]; ok && isValidUserPrefix(prefix) {
|
||||||
|
return prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefaultPrefixes
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserPerms contains all of the permissions for each channel the user is
|
||||||
|
// in.
|
||||||
|
type UserPerms struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
channels map[string]Perms
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy returns a deep copy of the channel permissions.
|
||||||
|
func (p *UserPerms) Copy() (perms *UserPerms) {
|
||||||
|
np := &UserPerms{
|
||||||
|
channels: make(map[string]Perms),
|
||||||
|
}
|
||||||
|
|
||||||
|
p.mu.RLock()
|
||||||
|
for key := range p.channels {
|
||||||
|
np.channels[key] = p.channels[key]
|
||||||
|
}
|
||||||
|
p.mu.RUnlock()
|
||||||
|
|
||||||
|
return np
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements json.Marshaler.
|
||||||
|
func (p *UserPerms) MarshalJSON() ([]byte, error) {
|
||||||
|
p.mu.Lock()
|
||||||
|
out, err := json.Marshal(&p.channels)
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup looks up the users permissions for a given channel. ok is false
|
||||||
|
// if the user is not in the given channel.
|
||||||
|
func (p *UserPerms) Lookup(channel string) (perms Perms, ok bool) {
|
||||||
|
p.mu.RLock()
|
||||||
|
perms, ok = p.channels[ToRFC1459(channel)]
|
||||||
|
p.mu.RUnlock()
|
||||||
|
|
||||||
|
return perms, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UserPerms) set(channel string, perms Perms) {
|
||||||
|
p.mu.Lock()
|
||||||
|
p.channels[ToRFC1459(channel)] = perms
|
||||||
|
p.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *UserPerms) remove(channel string) {
|
||||||
|
p.mu.Lock()
|
||||||
|
delete(p.channels, ToRFC1459(channel))
|
||||||
|
p.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perms contains all channel-based user permissions. The minimum op, and
|
||||||
|
// voice should be supported on all networks. This also supports non-rfc
|
||||||
|
// Owner, Admin, and HalfOp, if the network has support for it.
|
||||||
|
type Perms struct {
|
||||||
|
// Owner (non-rfc) indicates that the user has full permissions to the
|
||||||
|
// channel. More than one user can have owner permission.
|
||||||
|
Owner bool `json:"owner"`
|
||||||
|
// Admin (non-rfc) is commonly given to users that are trusted enough
|
||||||
|
// to manage channel permissions, as well as higher level service settings.
|
||||||
|
Admin bool `json:"admin"`
|
||||||
|
// Op is commonly given to trusted users who can manage a given channel
|
||||||
|
// by kicking, and banning users.
|
||||||
|
Op bool `json:"op"`
|
||||||
|
// HalfOp (non-rfc) is commonly used to give users permissions like the
|
||||||
|
// ability to kick, without giving them greater abilities to ban all users.
|
||||||
|
HalfOp bool `json:"half_op"`
|
||||||
|
// Voice indicates the user has voice permissions, commonly given to known
|
||||||
|
// users, with very light trust, or to indicate a user is active.
|
||||||
|
Voice bool `json:"voice"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdmin indicates that the user has banning abilities, and are likely a
|
||||||
|
// very trustable user (e.g. op+).
|
||||||
|
func (m Perms) IsAdmin() bool {
|
||||||
|
if m.Owner || m.Admin || m.Op {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTrusted indicates that the user at least has modes set upon them, higher
|
||||||
|
// than a regular joining user.
|
||||||
|
func (m Perms) IsTrusted() bool {
|
||||||
|
if m.IsAdmin() || m.HalfOp || m.Voice {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset resets the modes of a user.
|
||||||
|
func (m *Perms) reset() {
|
||||||
|
m.Owner = false
|
||||||
|
m.Admin = false
|
||||||
|
m.Op = false
|
||||||
|
m.HalfOp = false
|
||||||
|
m.Voice = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// set translates raw prefix characters into proper permissions. Only
|
||||||
|
// use this function when you have a session lock.
|
||||||
|
func (m *Perms) set(prefix string, append bool) {
|
||||||
|
if !append {
|
||||||
|
m.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(prefix); i++ {
|
||||||
|
switch string(prefix[i]) {
|
||||||
|
case OwnerPrefix:
|
||||||
|
m.Owner = true
|
||||||
|
case AdminPrefix:
|
||||||
|
m.Admin = true
|
||||||
|
case OperatorPrefix:
|
||||||
|
m.Op = true
|
||||||
|
case HalfOperatorPrefix:
|
||||||
|
m.HalfOp = true
|
||||||
|
case VoicePrefix:
|
||||||
|
m.Voice = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setFromMode sets user-permissions based on channel user mode chars. E.g.
|
||||||
|
// "o" being oper, "v" being voice, etc.
|
||||||
|
func (m *Perms) setFromMode(mode CMode) {
|
||||||
|
switch string(mode.name) {
|
||||||
|
case ModeOwner:
|
||||||
|
m.Owner = mode.add
|
||||||
|
case ModeAdmin:
|
||||||
|
m.Admin = mode.add
|
||||||
|
case ModeOperator:
|
||||||
|
m.Op = mode.add
|
||||||
|
case ModeHalfOperator:
|
||||||
|
m.HalfOp = mode.add
|
||||||
|
case ModeVoice:
|
||||||
|
m.Voice = mode.add
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseUserPrefix parses a raw mode line, like "@user" or "@+user".
|
||||||
|
func parseUserPrefix(raw string) (modes, nick string, success bool) {
|
||||||
|
for i := 0; i < len(raw); i++ {
|
||||||
|
char := string(raw[i])
|
||||||
|
|
||||||
|
if char == OwnerPrefix || char == AdminPrefix || char == HalfOperatorPrefix ||
|
||||||
|
char == OperatorPrefix || char == VoicePrefix {
|
||||||
|
modes += char
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assume we've gotten to the nickname part.
|
||||||
|
return modes, raw[i:], true
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
489
vendor/github.com/lrstanley/girc/state.go
generated
vendored
Normal file
489
vendor/github.com/lrstanley/girc/state.go
generated
vendored
Normal file
@ -0,0 +1,489 @@
|
|||||||
|
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
|
||||||
|
// of this source code is governed by the MIT license that can be found in
|
||||||
|
// the LICENSE file.
|
||||||
|
|
||||||
|
package girc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// state represents the actively-changing variables within the client
|
||||||
|
// runtime. Note that everything within the state should be guarded by the
|
||||||
|
// embedded sync.RWMutex.
|
||||||
|
type state struct {
|
||||||
|
sync.RWMutex
|
||||||
|
// nick, ident, and host are the internal trackers for our user.
|
||||||
|
nick, ident, host string
|
||||||
|
// channels represents all channels we're active in.
|
||||||
|
channels map[string]*Channel
|
||||||
|
// users represents all of users that we're tracking.
|
||||||
|
users map[string]*User
|
||||||
|
// enabledCap are the capabilities which are enabled for this connection.
|
||||||
|
enabledCap []string
|
||||||
|
// tmpCap are the capabilties which we share with the server during the
|
||||||
|
// last capability check. These will get sent once we have received the
|
||||||
|
// last capability list command from the server.
|
||||||
|
tmpCap []string
|
||||||
|
// serverOptions are the standard capabilities and configurations
|
||||||
|
// supported by the server at connection time. This also includes
|
||||||
|
// RPL_ISUPPORT entries.
|
||||||
|
serverOptions map[string]string
|
||||||
|
// motd is the servers message of the day.
|
||||||
|
motd string
|
||||||
|
}
|
||||||
|
|
||||||
|
// notify sends state change notifications so users can update their refs
|
||||||
|
// when state changes.
|
||||||
|
func (s *state) notify(c *Client, ntype string) {
|
||||||
|
c.RunHandlers(&Event{Command: ntype})
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset resets the state back to it's original form.
|
||||||
|
func (s *state) reset() {
|
||||||
|
s.Lock()
|
||||||
|
s.nick = ""
|
||||||
|
s.ident = ""
|
||||||
|
s.host = ""
|
||||||
|
s.channels = make(map[string]*Channel)
|
||||||
|
s.users = make(map[string]*User)
|
||||||
|
s.serverOptions = make(map[string]string)
|
||||||
|
s.enabledCap = []string{}
|
||||||
|
s.motd = ""
|
||||||
|
s.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// User represents an IRC user and the state attached to them.
|
||||||
|
type User struct {
|
||||||
|
// Nick is the users current nickname. rfc1459 compliant.
|
||||||
|
Nick string `json:"nick"`
|
||||||
|
// Ident is the users username/ident. Ident is commonly prefixed with a
|
||||||
|
// "~", which indicates that they do not have a identd server setup for
|
||||||
|
// authentication.
|
||||||
|
Ident string `json:"ident"`
|
||||||
|
// Host is the visible host of the users connection that the server has
|
||||||
|
// provided to us for their connection. May not always be accurate due to
|
||||||
|
// many networks spoofing/hiding parts of the hostname for privacy
|
||||||
|
// reasons.
|
||||||
|
Host string `json:"host"`
|
||||||
|
|
||||||
|
// ChannelList is a sorted list of all channels that we are currently
|
||||||
|
// tracking the user in. Each channel name is rfc1459 compliant. See
|
||||||
|
// User.Channels() for a shorthand if you're looking for the *Channel
|
||||||
|
// version of the channel list.
|
||||||
|
ChannelList []string `json:"channels"`
|
||||||
|
|
||||||
|
// FirstSeen represents the first time that the user was seen by the
|
||||||
|
// client for the given channel. Only usable if from state, not in past.
|
||||||
|
FirstSeen time.Time `json:"first_seen"`
|
||||||
|
// LastActive represents the last time that we saw the user active,
|
||||||
|
// which could be during nickname change, message, channel join, etc.
|
||||||
|
// Only usable if from state, not in past.
|
||||||
|
LastActive time.Time `json:"last_active"`
|
||||||
|
|
||||||
|
// Perms are the user permissions applied to this user that affect the given
|
||||||
|
// channel. This supports non-rfc style modes like Admin, Owner, and HalfOp.
|
||||||
|
Perms *UserPerms `json:"perms"`
|
||||||
|
|
||||||
|
// Extras are things added on by additional tracking methods, which may
|
||||||
|
// or may not work on the IRC server in mention.
|
||||||
|
Extras struct {
|
||||||
|
// Name is the users "realname" or full name. Commonly contains links
|
||||||
|
// to the IRC client being used, or something of non-importance. May
|
||||||
|
// also be empty if unsupported by the server/tracking is disabled.
|
||||||
|
Name string `json:"name"`
|
||||||
|
// Account refers to the account which the user is authenticated as.
|
||||||
|
// This differs between each network (e.g. usually Nickserv, but
|
||||||
|
// could also be something like Undernet). May also be empty if
|
||||||
|
// unsupported by the server/tracking is disabled.
|
||||||
|
Account string `json:"account"`
|
||||||
|
// Away refers to the away status of the user. An empty string
|
||||||
|
// indicates that they are active, otherwise the string is what they
|
||||||
|
// set as their away message. May also be empty if unsupported by the
|
||||||
|
// server/tracking is disabled.
|
||||||
|
Away string `json:"away"`
|
||||||
|
} `json:"extras"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channels returns a reference of *Channels that the client knows the user
|
||||||
|
// is in. If you're just looking for the namme of the channels, use
|
||||||
|
// User.ChannelList.
|
||||||
|
func (u User) Channels(c *Client) []*Channel {
|
||||||
|
if c == nil {
|
||||||
|
panic("nil Client provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
channels := []*Channel{}
|
||||||
|
|
||||||
|
c.state.RLock()
|
||||||
|
for i := 0; i < len(u.ChannelList); i++ {
|
||||||
|
ch := c.state.lookupChannel(u.ChannelList[i])
|
||||||
|
if ch != nil {
|
||||||
|
channels = append(channels, ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.state.RUnlock()
|
||||||
|
|
||||||
|
return channels
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy returns a deep copy of the user which can be modified without making
|
||||||
|
// changes to the actual state.
|
||||||
|
func (u *User) Copy() *User {
|
||||||
|
nu := &User{}
|
||||||
|
*nu = *u
|
||||||
|
|
||||||
|
nu.Perms = u.Perms.Copy()
|
||||||
|
_ = copy(nu.ChannelList, u.ChannelList)
|
||||||
|
|
||||||
|
return nu
|
||||||
|
}
|
||||||
|
|
||||||
|
// addChannel adds the channel to the users channel list.
|
||||||
|
func (u *User) addChannel(name string) {
|
||||||
|
if u.InChannel(name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u.ChannelList = append(u.ChannelList, ToRFC1459(name))
|
||||||
|
sort.StringsAreSorted(u.ChannelList)
|
||||||
|
|
||||||
|
u.Perms.set(name, Perms{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteChannel removes an existing channel from the users channel list.
|
||||||
|
func (u *User) deleteChannel(name string) {
|
||||||
|
name = ToRFC1459(name)
|
||||||
|
|
||||||
|
j := -1
|
||||||
|
for i := 0; i < len(u.ChannelList); i++ {
|
||||||
|
if u.ChannelList[i] == name {
|
||||||
|
j = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if j != -1 {
|
||||||
|
u.ChannelList = append(u.ChannelList[:j], u.ChannelList[j+1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Perms.remove(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InChannel checks to see if a user is in the given channel.
|
||||||
|
func (u *User) InChannel(name string) bool {
|
||||||
|
name = ToRFC1459(name)
|
||||||
|
|
||||||
|
for i := 0; i < len(u.ChannelList); i++ {
|
||||||
|
if u.ChannelList[i] == name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifetime represents the amount of time that has passed since we have first
|
||||||
|
// seen the user.
|
||||||
|
func (u *User) Lifetime() time.Duration {
|
||||||
|
return time.Since(u.FirstSeen)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active represents the the amount of time that has passed since we have
|
||||||
|
// last seen the user.
|
||||||
|
func (u *User) Active() time.Duration {
|
||||||
|
return time.Since(u.LastActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsActive returns true if they were active within the last 30 minutes.
|
||||||
|
func (u *User) IsActive() bool {
|
||||||
|
return u.Active() < (time.Minute * 30)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel represents an IRC channel and the state attached to it.
|
||||||
|
type Channel struct {
|
||||||
|
// Name of the channel. Must be rfc1459 compliant.
|
||||||
|
Name string `json:"name"`
|
||||||
|
// Topic of the channel.
|
||||||
|
Topic string `json:"topic"`
|
||||||
|
|
||||||
|
// UserList is a sorted list of all users we are currently tracking within
|
||||||
|
// the channel. Each is the nickname, and is rfc1459 compliant.
|
||||||
|
UserList []string `json:"user_list"`
|
||||||
|
// Joined represents the first time that the client joined the channel.
|
||||||
|
Joined time.Time `json:"joined"`
|
||||||
|
// Modes are the known channel modes that the bot has captured.
|
||||||
|
Modes CModes `json:"modes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users returns a reference of *Users that the client knows the channel has
|
||||||
|
// If you're just looking for just the name of the users, use Channnel.UserList.
|
||||||
|
func (ch Channel) Users(c *Client) []*User {
|
||||||
|
if c == nil {
|
||||||
|
panic("nil Client provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
users := []*User{}
|
||||||
|
|
||||||
|
c.state.RLock()
|
||||||
|
for i := 0; i < len(ch.UserList); i++ {
|
||||||
|
user := c.state.lookupUser(ch.UserList[i])
|
||||||
|
if user != nil {
|
||||||
|
users = append(users, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.state.RUnlock()
|
||||||
|
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trusted returns a list of users which have voice or greater in the given
|
||||||
|
// channel. See Perms.IsTrusted() for more information.
|
||||||
|
func (ch Channel) Trusted(c *Client) []*User {
|
||||||
|
if c == nil {
|
||||||
|
panic("nil Client provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
users := []*User{}
|
||||||
|
|
||||||
|
c.state.RLock()
|
||||||
|
for i := 0; i < len(ch.UserList); i++ {
|
||||||
|
user := c.state.lookupUser(ch.UserList[i])
|
||||||
|
if user == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
perms, ok := user.Perms.Lookup(ch.Name)
|
||||||
|
if ok && perms.IsTrusted() {
|
||||||
|
users = append(users, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.state.RUnlock()
|
||||||
|
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admins returns a list of users which have half-op (if supported), or
|
||||||
|
// greater permissions (op, admin, owner, etc) in the given channel. See
|
||||||
|
// Perms.IsAdmin() for more information.
|
||||||
|
func (ch Channel) Admins(c *Client) []*User {
|
||||||
|
if c == nil {
|
||||||
|
panic("nil Client provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
users := []*User{}
|
||||||
|
|
||||||
|
c.state.RLock()
|
||||||
|
for i := 0; i < len(ch.UserList); i++ {
|
||||||
|
user := c.state.lookupUser(ch.UserList[i])
|
||||||
|
if user == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
perms, ok := user.Perms.Lookup(ch.Name)
|
||||||
|
if ok && perms.IsAdmin() {
|
||||||
|
users = append(users, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.state.RUnlock()
|
||||||
|
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
// addUser adds a user to the users list.
|
||||||
|
func (ch *Channel) addUser(nick string) {
|
||||||
|
if ch.UserIn(nick) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ch.UserList = append(ch.UserList, ToRFC1459(nick))
|
||||||
|
sort.Strings(ch.UserList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteUser removes an existing user from the users list.
|
||||||
|
func (ch *Channel) deleteUser(nick string) {
|
||||||
|
nick = ToRFC1459(nick)
|
||||||
|
|
||||||
|
j := -1
|
||||||
|
for i := 0; i < len(ch.UserList); i++ {
|
||||||
|
if ch.UserList[i] == nick {
|
||||||
|
j = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if j != -1 {
|
||||||
|
ch.UserList = append(ch.UserList[:j], ch.UserList[j+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy returns a deep copy of a given channel.
|
||||||
|
func (ch *Channel) Copy() *Channel {
|
||||||
|
nc := &Channel{}
|
||||||
|
*nc = *ch
|
||||||
|
|
||||||
|
_ = copy(nc.UserList, ch.UserList)
|
||||||
|
|
||||||
|
// And modes.
|
||||||
|
nc.Modes = ch.Modes.Copy()
|
||||||
|
|
||||||
|
return nc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the count of users in a given channel.
|
||||||
|
func (ch *Channel) Len() int {
|
||||||
|
return len(ch.UserList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserIn checks to see if a given user is in a channel.
|
||||||
|
func (ch *Channel) UserIn(name string) bool {
|
||||||
|
name = ToRFC1459(name)
|
||||||
|
|
||||||
|
for i := 0; i < len(ch.UserList); i++ {
|
||||||
|
if ch.UserList[i] == name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifetime represents the amount of time that has passed since we have first
|
||||||
|
// joined the channel.
|
||||||
|
func (ch *Channel) Lifetime() time.Duration {
|
||||||
|
return time.Since(ch.Joined)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createChannel creates the channel in state, if not already done.
|
||||||
|
func (s *state) createChannel(name string) (ok bool) {
|
||||||
|
supported := s.chanModes()
|
||||||
|
prefixes, _ := parsePrefixes(s.userPrefixes())
|
||||||
|
|
||||||
|
if _, ok := s.channels[ToRFC1459(name)]; ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
s.channels[ToRFC1459(name)] = &Channel{
|
||||||
|
Name: name,
|
||||||
|
UserList: []string{},
|
||||||
|
Joined: time.Now(),
|
||||||
|
Modes: NewCModes(supported, prefixes),
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteChannel removes the channel from state, if not already done.
|
||||||
|
func (s *state) deleteChannel(name string) {
|
||||||
|
name = ToRFC1459(name)
|
||||||
|
|
||||||
|
_, ok := s.channels[name]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, user := range s.channels[name].UserList {
|
||||||
|
s.users[user].deleteChannel(name)
|
||||||
|
|
||||||
|
if len(s.users[user].ChannelList) == 0 {
|
||||||
|
// Assume we were only tracking them in this channel, and they
|
||||||
|
// should be removed from state.
|
||||||
|
|
||||||
|
delete(s.users, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(s.channels, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookupChannel returns a reference to a channel, nil returned if no results
|
||||||
|
// found.
|
||||||
|
func (s *state) lookupChannel(name string) *Channel {
|
||||||
|
return s.channels[ToRFC1459(name)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookupUser returns a reference to a user, nil returned if no results
|
||||||
|
// found.
|
||||||
|
func (s *state) lookupUser(name string) *User {
|
||||||
|
return s.users[ToRFC1459(name)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// createUser creates the user in state, if not already done.
|
||||||
|
func (s *state) createUser(nick string) (ok bool) {
|
||||||
|
if _, ok := s.users[ToRFC1459(nick)]; ok {
|
||||||
|
// User already exists.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
s.users[ToRFC1459(nick)] = &User{
|
||||||
|
Nick: nick,
|
||||||
|
FirstSeen: time.Now(),
|
||||||
|
LastActive: time.Now(),
|
||||||
|
Perms: &UserPerms{channels: make(map[string]Perms)},
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteUser removes the user from channel state.
|
||||||
|
func (s *state) deleteUser(channelName, nick string) {
|
||||||
|
user := s.lookupUser(nick)
|
||||||
|
if user == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if channelName == "" {
|
||||||
|
for i := 0; i < len(user.ChannelList); i++ {
|
||||||
|
s.channels[user.ChannelList[i]].deleteUser(nick)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(s.users, ToRFC1459(nick))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
channel := s.lookupChannel(channelName)
|
||||||
|
if channel == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user.deleteChannel(channelName)
|
||||||
|
channel.deleteUser(nick)
|
||||||
|
|
||||||
|
if len(user.ChannelList) == 0 {
|
||||||
|
// This means they are no longer in any channels we track, delete
|
||||||
|
// them from state.
|
||||||
|
|
||||||
|
delete(s.users, ToRFC1459(nick))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// renameUser renames the user in state, in all locations where relevant.
|
||||||
|
func (s *state) renameUser(from, to string) {
|
||||||
|
from = ToRFC1459(from)
|
||||||
|
|
||||||
|
// Update our nickname.
|
||||||
|
if from == ToRFC1459(s.nick) {
|
||||||
|
s.nick = to
|
||||||
|
}
|
||||||
|
|
||||||
|
user := s.lookupUser(from)
|
||||||
|
if user == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(s.users, from)
|
||||||
|
|
||||||
|
user.Nick = to
|
||||||
|
user.LastActive = time.Now()
|
||||||
|
s.users[ToRFC1459(to)] = user
|
||||||
|
|
||||||
|
for i := 0; i < len(user.ChannelList); i++ {
|
||||||
|
for j := 0; j < len(s.channels[user.ChannelList[i]].UserList); j++ {
|
||||||
|
if s.channels[user.ChannelList[i]].UserList[j] == from {
|
||||||
|
s.channels[user.ChannelList[i]].UserList[j] = ToRFC1459(to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
0
vendor/github.com/nlopes/slack/dnd.go → vendor/github.com/matterbridge/slack/dnd.go
generated
vendored
0
vendor/github.com/nlopes/slack/dnd.go → vendor/github.com/matterbridge/slack/dnd.go
generated
vendored
0
vendor/github.com/nlopes/slack/im.go → vendor/github.com/matterbridge/slack/im.go
generated
vendored
0
vendor/github.com/nlopes/slack/im.go → vendor/github.com/matterbridge/slack/im.go
generated
vendored
0
vendor/github.com/nlopes/slack/rtm.go → vendor/github.com/matterbridge/slack/rtm.go
generated
vendored
0
vendor/github.com/nlopes/slack/rtm.go → vendor/github.com/matterbridge/slack/rtm.go
generated
vendored
38
vendor/github.com/nlopes/slack/users.go → vendor/github.com/matterbridge/slack/users.go
generated
vendored
38
vendor/github.com/nlopes/slack/users.go → vendor/github.com/matterbridge/slack/users.go
generated
vendored
@ -15,24 +15,26 @@ const (
|
|||||||
|
|
||||||
// UserProfile contains all the information details of a given user
|
// UserProfile contains all the information details of a given user
|
||||||
type UserProfile struct {
|
type UserProfile struct {
|
||||||
FirstName string `json:"first_name"`
|
FirstName string `json:"first_name"`
|
||||||
LastName string `json:"last_name"`
|
LastName string `json:"last_name"`
|
||||||
RealName string `json:"real_name"`
|
RealName string `json:"real_name"`
|
||||||
RealNameNormalized string `json:"real_name_normalized"`
|
RealNameNormalized string `json:"real_name_normalized"`
|
||||||
Email string `json:"email"`
|
DisplayName string `json:"display_name"`
|
||||||
Skype string `json:"skype"`
|
DisplayNameNormalized string `json:"display_name_normalized"`
|
||||||
Phone string `json:"phone"`
|
Email string `json:"email"`
|
||||||
Image24 string `json:"image_24"`
|
Skype string `json:"skype"`
|
||||||
Image32 string `json:"image_32"`
|
Phone string `json:"phone"`
|
||||||
Image48 string `json:"image_48"`
|
Image24 string `json:"image_24"`
|
||||||
Image72 string `json:"image_72"`
|
Image32 string `json:"image_32"`
|
||||||
Image192 string `json:"image_192"`
|
Image48 string `json:"image_48"`
|
||||||
ImageOriginal string `json:"image_original"`
|
Image72 string `json:"image_72"`
|
||||||
Title string `json:"title"`
|
Image192 string `json:"image_192"`
|
||||||
BotID string `json:"bot_id,omitempty"`
|
ImageOriginal string `json:"image_original"`
|
||||||
ApiAppID string `json:"api_app_id,omitempty"`
|
Title string `json:"title"`
|
||||||
StatusText string `json:"status_text,omitempty"`
|
BotID string `json:"bot_id,omitempty"`
|
||||||
StatusEmoji string `json:"status_emoji,omitempty"`
|
ApiAppID string `json:"api_app_id,omitempty"`
|
||||||
|
StatusText string `json:"status_text,omitempty"`
|
||||||
|
StatusEmoji string `json:"status_emoji,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// User contains all the information of a user
|
// User contains all the information of a user
|
8
vendor/github.com/mattn/go-xmpp/xmpp.go
generated
vendored
8
vendor/github.com/mattn/go-xmpp/xmpp.go
generated
vendored
@ -191,7 +191,9 @@ func (o Options) NewClient() (*Client, error) {
|
|||||||
tlsconn = tls.Client(c, o.TLSConfig)
|
tlsconn = tls.Client(c, o.TLSConfig)
|
||||||
} else {
|
} else {
|
||||||
DefaultConfig.ServerName = host
|
DefaultConfig.ServerName = host
|
||||||
tlsconn = tls.Client(c, &DefaultConfig)
|
newconfig := DefaultConfig
|
||||||
|
newconfig.ServerName = host
|
||||||
|
tlsconn = tls.Client(c, &newconfig)
|
||||||
}
|
}
|
||||||
if err = tlsconn.Handshake(); err != nil {
|
if err = tlsconn.Handshake(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -635,7 +637,7 @@ func (c *Client) SendPresence(presence Presence) (n int, err error) {
|
|||||||
|
|
||||||
// SendKeepAlive sends a "whitespace keepalive" as described in chapter 4.6.1 of RFC6120.
|
// SendKeepAlive sends a "whitespace keepalive" as described in chapter 4.6.1 of RFC6120.
|
||||||
func (c *Client) SendKeepAlive() (n int, err error) {
|
func (c *Client) SendKeepAlive() (n int, err error) {
|
||||||
return fmt.Fprintf(c.conn," ")
|
return fmt.Fprintf(c.conn, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendHtml sends the message as HTML as defined by XEP-0071
|
// SendHtml sends the message as HTML as defined by XEP-0071
|
||||||
@ -831,7 +833,7 @@ type rosterItem struct {
|
|||||||
func nextStart(p *xml.Decoder) (xml.StartElement, error) {
|
func nextStart(p *xml.Decoder) (xml.StartElement, error) {
|
||||||
for {
|
for {
|
||||||
t, err := p.Token()
|
t, err := p.Token()
|
||||||
if err != nil && err != io.EOF || t == nil {
|
if err != nil || t == nil {
|
||||||
return xml.StartElement{}, err
|
return xml.StartElement{}, err
|
||||||
}
|
}
|
||||||
switch t := t.(type) {
|
switch t := t.(type) {
|
||||||
|
3
vendor/github.com/mattn/go-xmpp/xmpp_muc.go
generated
vendored
3
vendor/github.com/mattn/go-xmpp/xmpp_muc.go
generated
vendored
@ -90,7 +90,8 @@ func (c *Client) JoinProtectedMUC(jid, nick string, password string, history_typ
|
|||||||
case NoHistory:
|
case NoHistory:
|
||||||
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
|
return fmt.Fprintf(c.conn, "<presence to='%s/%s'>\n" +
|
||||||
"<x xmlns='%s'>\n" +
|
"<x xmlns='%s'>\n" +
|
||||||
"<password>%s</password>\n"+
|
"<password>%s</password>" +
|
||||||
|
"</x>\n" +
|
||||||
"</presence>",
|
"</presence>",
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password))
|
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password))
|
||||||
case CharHistory:
|
case CharHistory:
|
||||||
|
21
vendor/github.com/nlopes/slack/examples/channels/channels.go
generated
vendored
21
vendor/github.com/nlopes/slack/examples/channels/channels.go
generated
vendored
@ -1,21 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/nlopes/slack"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
api := slack.New("YOUR_TOKEN_HERE")
|
|
||||||
channels, err := api.GetChannels(false)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("%s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, channel := range channels {
|
|
||||||
fmt.Println(channel.Name)
|
|
||||||
// channel is of type conversation & groupConversation
|
|
||||||
// see all available methods in `conversation.go`
|
|
||||||
}
|
|
||||||
}
|
|
30
vendor/github.com/nlopes/slack/examples/files/files.go
generated
vendored
30
vendor/github.com/nlopes/slack/examples/files/files.go
generated
vendored
@ -1,30 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/nlopes/slack"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
api := slack.New("YOUR_TOKEN_HERE")
|
|
||||||
params := slack.FileUploadParameters{
|
|
||||||
Title: "Batman Example",
|
|
||||||
//Filetype: "txt",
|
|
||||||
File: "example.txt",
|
|
||||||
//Content: "Nan Nan Nan Nan Nan Nan Nan Nan Batman",
|
|
||||||
}
|
|
||||||
file, err := api.UploadFile(params)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("%s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("Name: %s, URL: %s\n", file.Name, file.URL)
|
|
||||||
|
|
||||||
err = api.DeleteFile(file.ID)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("%s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("File %s deleted successfully.\n", file.Name)
|
|
||||||
}
|
|
22
vendor/github.com/nlopes/slack/examples/groups/groups.go
generated
vendored
22
vendor/github.com/nlopes/slack/examples/groups/groups.go
generated
vendored
@ -1,22 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/nlopes/slack"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
api := slack.New("YOUR_TOKEN_HERE")
|
|
||||||
// If you set debugging, it will log all requests to the console
|
|
||||||
// Useful when encountering issues
|
|
||||||
// api.SetDebug(true)
|
|
||||||
groups, err := api.GetGroups(false)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("%s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, group := range groups {
|
|
||||||
fmt.Printf("ID: %s, Name: %s\n", group.ID, group.Name)
|
|
||||||
}
|
|
||||||
}
|
|
32
vendor/github.com/nlopes/slack/examples/messages/messages.go
generated
vendored
32
vendor/github.com/nlopes/slack/examples/messages/messages.go
generated
vendored
@ -1,32 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/nlopes/slack"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
api := slack.New("YOUR_TOKEN_HERE")
|
|
||||||
params := slack.PostMessageParameters{}
|
|
||||||
attachment := slack.Attachment{
|
|
||||||
Pretext: "some pretext",
|
|
||||||
Text: "some text",
|
|
||||||
// Uncomment the following part to send a field too
|
|
||||||
/*
|
|
||||||
Fields: []slack.AttachmentField{
|
|
||||||
slack.AttachmentField{
|
|
||||||
Title: "a",
|
|
||||||
Value: "no",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
params.Attachments = []slack.Attachment{attachment}
|
|
||||||
channelID, timestamp, err := api.PostMessage("CHANNEL_ID", "Some text", params)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("%s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("Message successfully sent to channel %s at %s", channelID, timestamp)
|
|
||||||
}
|
|
123
vendor/github.com/nlopes/slack/examples/pins/pins.go
generated
vendored
123
vendor/github.com/nlopes/slack/examples/pins/pins.go
generated
vendored
@ -1,123 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/nlopes/slack"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
WARNING: This example is destructive in the sense that it create a channel called testpinning
|
|
||||||
*/
|
|
||||||
func main() {
|
|
||||||
var (
|
|
||||||
apiToken string
|
|
||||||
debug bool
|
|
||||||
)
|
|
||||||
|
|
||||||
flag.StringVar(&apiToken, "token", "YOUR_TOKEN_HERE", "Your Slack API Token")
|
|
||||||
flag.BoolVar(&debug, "debug", false, "Show JSON output")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
api := slack.New(apiToken)
|
|
||||||
if debug {
|
|
||||||
api.SetDebug(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
postAsUserName string
|
|
||||||
postAsUserID string
|
|
||||||
postToChannelID string
|
|
||||||
)
|
|
||||||
|
|
||||||
// Find the user to post as.
|
|
||||||
authTest, err := api.AuthTest()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error getting channels: %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
channelName := "testpinning"
|
|
||||||
|
|
||||||
// Post as the authenticated user.
|
|
||||||
postAsUserName = authTest.User
|
|
||||||
postAsUserID = authTest.UserID
|
|
||||||
|
|
||||||
// Create a temporary channel
|
|
||||||
channel, err := api.CreateChannel(channelName)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
// If the channel exists, that means we just need to unarchive it
|
|
||||||
if err.Error() == "name_taken" {
|
|
||||||
err = nil
|
|
||||||
channels, err := api.GetChannels(false)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Could not retrieve channels")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, archivedChannel := range channels {
|
|
||||||
if archivedChannel.Name == channelName {
|
|
||||||
if archivedChannel.IsArchived {
|
|
||||||
err = api.UnarchiveChannel(archivedChannel.ID)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Could not unarchive %s: %s\n", archivedChannel.ID, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
channel = &archivedChannel
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error setting test channel for pinning: %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
postToChannelID = channel.ID
|
|
||||||
|
|
||||||
fmt.Printf("Posting as %s (%s) in channel %s\n", postAsUserName, postAsUserID, postToChannelID)
|
|
||||||
|
|
||||||
// Post a message.
|
|
||||||
postParams := slack.PostMessageParameters{}
|
|
||||||
channelID, timestamp, err := api.PostMessage(postToChannelID, "Is this any good?", postParams)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error posting message: %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab a reference to the message.
|
|
||||||
msgRef := slack.NewRefToMessage(channelID, timestamp)
|
|
||||||
|
|
||||||
// Add message pin to channel
|
|
||||||
if err := api.AddPin(channelID, msgRef); err != nil {
|
|
||||||
fmt.Printf("Error adding pin: %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// List all of the users pins.
|
|
||||||
listPins, _, err := api.ListPins(channelID)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error listing pins: %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("\n")
|
|
||||||
fmt.Printf("All pins by %s...\n", authTest.User)
|
|
||||||
for _, item := range listPins {
|
|
||||||
fmt.Printf(" > Item type: %s\n", item.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the pin.
|
|
||||||
err = api.RemovePin(channelID, msgRef)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error remove pin: %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = api.ArchiveChannel(channelID); err != nil {
|
|
||||||
fmt.Printf("Error archiving channel: %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
126
vendor/github.com/nlopes/slack/examples/reactions/reactions.go
generated
vendored
126
vendor/github.com/nlopes/slack/examples/reactions/reactions.go
generated
vendored
@ -1,126 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/nlopes/slack"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var (
|
|
||||||
apiToken string
|
|
||||||
debug bool
|
|
||||||
)
|
|
||||||
|
|
||||||
flag.StringVar(&apiToken, "token", "YOUR_TOKEN_HERE", "Your Slack API Token")
|
|
||||||
flag.BoolVar(&debug, "debug", false, "Show JSON output")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
api := slack.New(apiToken)
|
|
||||||
if debug {
|
|
||||||
api.SetDebug(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
postAsUserName string
|
|
||||||
postAsUserID string
|
|
||||||
postToUserName string
|
|
||||||
postToUserID string
|
|
||||||
postToChannelID string
|
|
||||||
)
|
|
||||||
|
|
||||||
// Find the user to post as.
|
|
||||||
authTest, err := api.AuthTest()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error getting channels: %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post as the authenticated user.
|
|
||||||
postAsUserName = authTest.User
|
|
||||||
postAsUserID = authTest.UserID
|
|
||||||
|
|
||||||
// Posting to DM with self causes a conversation with slackbot.
|
|
||||||
postToUserName = authTest.User
|
|
||||||
postToUserID = authTest.UserID
|
|
||||||
|
|
||||||
// Find the channel.
|
|
||||||
_, _, chanID, err := api.OpenIMChannel(postToUserID)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error opening IM: %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
postToChannelID = chanID
|
|
||||||
|
|
||||||
fmt.Printf("Posting as %s (%s) in DM with %s (%s), channel %s\n", postAsUserName, postAsUserID, postToUserName, postToUserID, postToChannelID)
|
|
||||||
|
|
||||||
// Post a message.
|
|
||||||
postParams := slack.PostMessageParameters{}
|
|
||||||
channelID, timestamp, err := api.PostMessage(postToChannelID, "Is this any good?", postParams)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error posting message: %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab a reference to the message.
|
|
||||||
msgRef := slack.NewRefToMessage(channelID, timestamp)
|
|
||||||
|
|
||||||
// React with :+1:
|
|
||||||
if err := api.AddReaction("+1", msgRef); err != nil {
|
|
||||||
fmt.Printf("Error adding reaction: %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// React with :-1:
|
|
||||||
if err := api.AddReaction("cry", msgRef); err != nil {
|
|
||||||
fmt.Printf("Error adding reaction: %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all reactions on the message.
|
|
||||||
msgReactions, err := api.GetReactions(msgRef, slack.NewGetReactionsParameters())
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error getting reactions: %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("\n")
|
|
||||||
fmt.Printf("%d reactions to message...\n", len(msgReactions))
|
|
||||||
for _, r := range msgReactions {
|
|
||||||
fmt.Printf(" %d users say %s\n", r.Count, r.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// List all of the users reactions.
|
|
||||||
listReactions, _, err := api.ListReactions(slack.NewListReactionsParameters())
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error listing reactions: %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("\n")
|
|
||||||
fmt.Printf("All reactions by %s...\n", authTest.User)
|
|
||||||
for _, item := range listReactions {
|
|
||||||
fmt.Printf("%d on a %s...\n", len(item.Reactions), item.Type)
|
|
||||||
for _, r := range item.Reactions {
|
|
||||||
fmt.Printf(" %s (along with %d others)\n", r.Name, r.Count-1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the :cry: reaction.
|
|
||||||
err = api.RemoveReaction("cry", msgRef)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error remove reaction: %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all reactions on the message.
|
|
||||||
msgReactions, err = api.GetReactions(msgRef, slack.NewGetReactionsParameters())
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error getting reactions: %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("\n")
|
|
||||||
fmt.Printf("%d reactions to message after removing cry...\n", len(msgReactions))
|
|
||||||
for _, r := range msgReactions {
|
|
||||||
fmt.Printf(" %d users say %s\n", r.Count, r.Name)
|
|
||||||
}
|
|
||||||
}
|
|
46
vendor/github.com/nlopes/slack/examples/stars/stars.go
generated
vendored
46
vendor/github.com/nlopes/slack/examples/stars/stars.go
generated
vendored
@ -1,46 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/nlopes/slack"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var (
|
|
||||||
apiToken string
|
|
||||||
debug bool
|
|
||||||
)
|
|
||||||
|
|
||||||
flag.StringVar(&apiToken, "token", "YOUR_TOKEN_HERE", "Your Slack API Token")
|
|
||||||
flag.BoolVar(&debug, "debug", false, "Show JSON output")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
api := slack.New(apiToken)
|
|
||||||
if debug {
|
|
||||||
api.SetDebug(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all stars for the usr.
|
|
||||||
params := slack.NewStarsParameters()
|
|
||||||
starredItems, _, err := api.GetStarred(params)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error getting stars: %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, s := range starredItems {
|
|
||||||
var desc string
|
|
||||||
switch s.Type {
|
|
||||||
case slack.TYPE_MESSAGE:
|
|
||||||
desc = s.Message.Text
|
|
||||||
case slack.TYPE_FILE:
|
|
||||||
desc = s.File.Name
|
|
||||||
case slack.TYPE_FILE_COMMENT:
|
|
||||||
desc = s.File.Name + " - " + s.Comment.Comment
|
|
||||||
case slack.TYPE_CHANNEL, slack.TYPE_IM, slack.TYPE_GROUP:
|
|
||||||
desc = s.Channel
|
|
||||||
}
|
|
||||||
fmt.Printf("Starred %s: %s\n", s.Type, desc)
|
|
||||||
}
|
|
||||||
}
|
|
25
vendor/github.com/nlopes/slack/examples/team/team.go
generated
vendored
25
vendor/github.com/nlopes/slack/examples/team/team.go
generated
vendored
@ -1,25 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/nlopes/slack"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
api := slack.New("YOUR_TOKEN_HERE")
|
|
||||||
//Example for single user
|
|
||||||
billingActive, err := api.GetBillableInfo("U023BECGF")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("%s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("ID: U023BECGF, BillingActive: %v\n\n\n", billingActive["U023BECGF"])
|
|
||||||
|
|
||||||
//Example for team
|
|
||||||
billingActiveForTeam, err := api.GetBillableInfoForTeam()
|
|
||||||
for id, value := range billingActiveForTeam {
|
|
||||||
fmt.Printf("ID: %v, BillingActive: %v\n", id, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
17
vendor/github.com/nlopes/slack/examples/users/users.go
generated
vendored
17
vendor/github.com/nlopes/slack/examples/users/users.go
generated
vendored
@ -1,17 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/nlopes/slack"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
api := slack.New("YOUR_TOKEN_HERE")
|
|
||||||
user, err := api.GetUserInfo("U023BECGF")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("%s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("ID: %s, Fullname: %s, Email: %s\n", user.ID, user.Profile.RealName, user.Profile.Email)
|
|
||||||
}
|
|
54
vendor/github.com/nlopes/slack/examples/websocket/websocket.go
generated
vendored
54
vendor/github.com/nlopes/slack/examples/websocket/websocket.go
generated
vendored
@ -1,54 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/nlopes/slack"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
api := slack.New("YOUR TOKEN HERE")
|
|
||||||
logger := log.New(os.Stdout, "slack-bot: ", log.Lshortfile|log.LstdFlags)
|
|
||||||
slack.SetLogger(logger)
|
|
||||||
api.SetDebug(true)
|
|
||||||
|
|
||||||
rtm := api.NewRTM()
|
|
||||||
go rtm.ManageConnection()
|
|
||||||
|
|
||||||
for msg := range rtm.IncomingEvents {
|
|
||||||
fmt.Print("Event Received: ")
|
|
||||||
switch ev := msg.Data.(type) {
|
|
||||||
case *slack.HelloEvent:
|
|
||||||
// Ignore hello
|
|
||||||
|
|
||||||
case *slack.ConnectedEvent:
|
|
||||||
fmt.Println("Infos:", ev.Info)
|
|
||||||
fmt.Println("Connection counter:", ev.ConnectionCount)
|
|
||||||
// Replace #general with your Channel ID
|
|
||||||
rtm.SendMessage(rtm.NewOutgoingMessage("Hello world", "#general"))
|
|
||||||
|
|
||||||
case *slack.MessageEvent:
|
|
||||||
fmt.Printf("Message: %v\n", ev)
|
|
||||||
|
|
||||||
case *slack.PresenceChangeEvent:
|
|
||||||
fmt.Printf("Presence Change: %v\n", ev)
|
|
||||||
|
|
||||||
case *slack.LatencyReport:
|
|
||||||
fmt.Printf("Current latency: %v\n", ev.Value)
|
|
||||||
|
|
||||||
case *slack.RTMError:
|
|
||||||
fmt.Printf("Error: %s\n", ev.Error())
|
|
||||||
|
|
||||||
case *slack.InvalidAuthEvent:
|
|
||||||
fmt.Printf("Invalid credentials")
|
|
||||||
return
|
|
||||||
|
|
||||||
default:
|
|
||||||
|
|
||||||
// Ignore other events..
|
|
||||||
// fmt.Printf("Unexpected: %v\n", msg.Data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
26
vendor/manifest
vendored
26
vendor/manifest
vendored
@ -278,6 +278,14 @@
|
|||||||
"path": "/random",
|
"path": "/random",
|
||||||
"notests": true
|
"notests": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"importpath": "github.com/lrstanley/girc",
|
||||||
|
"repository": "https://github.com/lrstanley/girc",
|
||||||
|
"vcs": "git",
|
||||||
|
"revision": "055075db54ebd311be5946efb3f62502846089ff",
|
||||||
|
"branch": "master",
|
||||||
|
"notests": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"importpath": "github.com/matrix-org/gomatrix",
|
"importpath": "github.com/matrix-org/gomatrix",
|
||||||
"repository": "https://github.com/matrix-org/gomatrix",
|
"repository": "https://github.com/matrix-org/gomatrix",
|
||||||
@ -286,6 +294,14 @@
|
|||||||
"branch": "master",
|
"branch": "master",
|
||||||
"notests": true
|
"notests": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"importpath": "github.com/matterbridge/slack",
|
||||||
|
"repository": "https://github.com/matterbridge/slack",
|
||||||
|
"vcs": "git",
|
||||||
|
"revision": "1c6e6305bf9c07fc603c9cf28f09ab0517a03120",
|
||||||
|
"branch": "matterbridge",
|
||||||
|
"notests": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"importpath": "github.com/mattermost/platform/einterfaces",
|
"importpath": "github.com/mattermost/platform/einterfaces",
|
||||||
"repository": "https://github.com/mattermost/platform",
|
"repository": "https://github.com/mattermost/platform",
|
||||||
@ -396,7 +412,7 @@
|
|||||||
"importpath": "github.com/mattn/go-xmpp",
|
"importpath": "github.com/mattn/go-xmpp",
|
||||||
"repository": "https://github.com/mattn/go-xmpp",
|
"repository": "https://github.com/mattn/go-xmpp",
|
||||||
"vcs": "git",
|
"vcs": "git",
|
||||||
"revision": "16b6a7bdba1ca30969b5db9362b6605e43302daa",
|
"revision": "d0cdb99fae16437f69616ccc40662b6fe8ac6d47",
|
||||||
"branch": "master",
|
"branch": "master",
|
||||||
"notests": true
|
"notests": true
|
||||||
},
|
},
|
||||||
@ -425,14 +441,6 @@
|
|||||||
"path": "/i18n",
|
"path": "/i18n",
|
||||||
"notests": true
|
"notests": true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"importpath": "github.com/nlopes/slack",
|
|
||||||
"repository": "https://github.com/nlopes/slack",
|
|
||||||
"vcs": "git",
|
|
||||||
"revision": "5cde21b8b96a43fc3435a1f514123d14fd7eabdc",
|
|
||||||
"branch": "master",
|
|
||||||
"notests": true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"importpath": "github.com/paulrosania/go-charset",
|
"importpath": "github.com/paulrosania/go-charset",
|
||||||
"repository": "https://github.com/paulrosania/go-charset",
|
"repository": "https://github.com/paulrosania/go-charset",
|
||||||
|
Reference in New Issue
Block a user