dispatch/pkg/irc/client.go

327 lines
6.6 KiB
Go
Raw Normal View History

2015-06-05 22:34:13 +00:00
package irc
import (
"bufio"
"crypto/tls"
"net"
"strings"
"sync"
2020-05-04 23:35:05 +00:00
"time"
2016-03-01 00:51:26 +00:00
"github.com/jpillora/backoff"
2015-06-05 22:34:13 +00:00
)
2020-05-23 07:42:20 +00:00
type Config struct {
2020-06-04 00:28:41 +00:00
Host string
Port string
TLS bool
TLSConfig *tls.Config
ServerPassword string
Nick string
Username string
Realname string
SASLMechanisms []string
Account string
Password string
2020-05-20 02:19:40 +00:00
// Version is the reply to VERSION and FINGER CTCP messages
Version string
// Source is the reply to SOURCE CTCP messages
Source string
2020-05-23 07:42:20 +00:00
HandleNickInUse func(string) string
}
type Client struct {
Config *Config
2020-05-23 07:42:20 +00:00
2016-01-13 17:53:54 +00:00
Messages chan *Message
ConnectionChanged chan ConnectionState
2019-01-27 07:53:07 +00:00
Features *Features
state *state
nick string
channels []string
2015-06-05 22:34:13 +00:00
2020-05-23 06:05:37 +00:00
wantedCapabilities []string
requestedCapabilities map[string][]string
enabledCapabilities map[string][]string
2020-06-04 00:28:41 +00:00
negotiating bool
saslMechanisms []SASL
currentSASL SASL
2020-05-23 06:05:37 +00:00
conn net.Conn
connected bool
registered bool
dialer *net.Dialer
recvBuf []byte
scan *bufio.Scanner
backoff *backoff.Backoff
out chan string
2015-06-05 22:34:13 +00:00
quit chan struct{}
reconnect chan struct{}
2016-01-12 23:12:51 +00:00
sendRecv sync.WaitGroup
2015-06-05 22:34:13 +00:00
lock sync.Mutex
}
func NewClient(config *Config) *Client {
2020-05-23 07:42:20 +00:00
if config.Port == "" {
if config.TLS {
config.Port = "6697"
} else {
config.Port = "6667"
}
}
if config.Username == "" {
config.Username = config.Nick
}
if config.Realname == "" {
config.Realname = config.Nick
}
2020-06-04 00:28:41 +00:00
if config.SASLMechanisms == nil {
config.SASLMechanisms = DefaultSASLMechanisms
}
client := &Client{
2020-05-23 07:42:20 +00:00
Config: config,
2020-05-23 06:05:37 +00:00
Messages: make(chan *Message, 32),
ConnectionChanged: make(chan ConnectionState, 4),
Features: NewFeatures(),
nick: config.Nick,
2020-05-23 07:42:20 +00:00
requestedCapabilities: map[string][]string{},
enabledCapabilities: map[string][]string{},
2020-05-23 06:05:37 +00:00
dialer: &net.Dialer{Timeout: 10 * time.Second},
recvBuf: make([]byte, 0, 4096),
2016-01-13 00:00:57 +00:00
backoff: &backoff.Backoff{
2020-05-04 23:35:05 +00:00
Min: 500 * time.Millisecond,
Max: 30 * time.Second,
2016-01-13 00:00:57 +00:00
Jitter: true,
},
out: make(chan string, 32),
quit: make(chan struct{}),
reconnect: make(chan struct{}),
2015-06-05 22:34:13 +00:00
}
client.state = newState(client)
2020-06-04 00:28:41 +00:00
client.initSASL()
return client
2015-06-05 22:34:13 +00:00
}
2020-06-04 00:28:41 +00:00
func (c *Client) initSASL() {
saslMechanisms := []SASL{}
for _, mech := range c.Config.SASLMechanisms {
if mech == "EXTERNAL" {
if c.Config.TLSConfig != nil && len(c.Config.TLSConfig.Certificates) > 0 {
saslMechanisms = append(saslMechanisms, &SASLExternal{})
}
} else if c.Config.Account != "" && c.Config.Password != "" {
if mech == "PLAIN" {
saslMechanisms = append(saslMechanisms, &SASLPlain{
Username: c.Config.Account,
Password: c.Config.Password,
})
} else if strings.HasPrefix(mech, "SCRAM-") {
saslMechanisms = append(saslMechanisms, &SASLScram{
Username: c.Config.Account,
Password: c.Config.Password,
Hash: mech[6:],
})
}
}
}
c.wantedCapabilities = append([]string{}, clientWantedCaps...)
c.negotiating = false
c.currentSASL = nil
2020-06-04 00:28:41 +00:00
if len(saslMechanisms) > 0 {
c.wantedCapabilities = append(c.wantedCapabilities, "sasl")
c.saslMechanisms = saslMechanisms
}
}
2015-06-05 22:34:13 +00:00
func (c *Client) GetNick() string {
c.lock.Lock()
2018-04-29 00:13:22 +00:00
nick := c.nick
c.lock.Unlock()
return nick
2015-06-05 22:34:13 +00:00
}
func (c *Client) Is(nick string) bool {
return c.EqualFold(nick, c.GetNick())
}
2017-04-11 04:04:59 +00:00
func (c *Client) setNick(nick string) {
c.lock.Lock()
c.nick = nick
c.lock.Unlock()
}
2015-06-05 22:34:13 +00:00
func (c *Client) Connected() bool {
c.lock.Lock()
2018-04-29 00:13:22 +00:00
connected := c.connected
c.lock.Unlock()
return connected
2015-06-05 22:34:13 +00:00
}
func (c *Client) Registered() bool {
c.lock.Lock()
reg := c.registered
c.lock.Unlock()
return reg
}
func (c *Client) setRegistered(reg bool) {
c.lock.Lock()
c.registered = reg
c.lock.Unlock()
}
2020-05-23 07:42:20 +00:00
func (c *Client) Host() string {
return c.Config.Host
}
func (c *Client) MOTD() []string {
return c.state.getMOTD()
}
func (c *Client) ChannelUsers(channel string) []string {
return c.state.getUsers(channel)
}
func (c *Client) ChannelTopic(channel string) string {
return c.state.getTopic(channel)
}
2015-06-05 22:34:13 +00:00
func (c *Client) Nick(nick string) {
c.Write("NICK " + nick)
}
func (c *Client) Oper(name, password string) {
c.Write("OPER " + name + " " + password)
}
func (c *Client) Mode(target, modes, params string) {
c.Write(strings.TrimRight("MODE "+target+" "+modes+" "+params, " "))
}
func (c *Client) Quit() {
go func() {
if c.Connected() {
c.write("QUIT")
}
close(c.quit)
}()
}
func (c *Client) Join(channels ...string) {
c.Write("JOIN " + strings.Join(channels, ","))
}
func (c *Client) Part(channels ...string) {
c.Write("PART " + strings.Join(channels, ","))
c.removeChannels(channels...)
2015-06-05 22:34:13 +00:00
}
func (c *Client) Topic(channel string, topic ...string) {
msg := "TOPIC " + channel
if len(topic) > 0 {
msg += " :" + topic[0]
}
c.Write(msg)
2015-06-05 22:34:13 +00:00
}
func (c *Client) Invite(nick, channel string) {
c.Write("INVITE " + nick + " " + channel)
}
func (c *Client) Kick(channel string, users ...string) {
c.Write("KICK " + channel + " " + strings.Join(users, ","))
}
func (c *Client) Privmsg(target, msg string) {
c.Writef("PRIVMSG %s :%s", target, msg)
}
func (c *Client) Notice(target, msg string) {
c.Writef("NOTICE %s :%s", target, msg)
}
2020-05-20 02:19:40 +00:00
func (c *Client) ReplyCTCP(target, command, params string) {
c.Notice(target, EncodeCTCP(&CTCP{
Command: command,
Params: params,
}))
}
2015-06-05 22:34:13 +00:00
func (c *Client) Whois(nick string) {
c.Write("WHOIS " + nick)
}
func (c *Client) Away(message string) {
c.Write("AWAY :" + message)
}
2015-06-11 02:57:52 +00:00
2019-01-11 01:46:46 +00:00
func (c *Client) List() {
c.Write("LIST")
}
2015-06-11 02:57:52 +00:00
func (c *Client) writePass(password string) {
c.write("PASS " + password)
}
func (c *Client) writeNick(nick string) {
c.write("NICK " + nick)
}
func (c *Client) writeUser(username, realname string) {
c.writef("USER %s 0 * :%s", username, realname)
}
2020-06-04 00:28:41 +00:00
func (c *Client) authenticate(response string) {
c.write("AUTHENTICATE " + response)
}
2015-06-11 02:57:52 +00:00
func (c *Client) register() {
2020-06-04 00:28:41 +00:00
c.beginCAP()
if c.Config.ServerPassword != "" {
c.writePass(c.Config.ServerPassword)
2015-06-11 02:57:52 +00:00
}
2020-05-23 07:42:20 +00:00
c.writeNick(c.Config.Nick)
c.writeUser(c.Config.Username, c.Config.Realname)
2015-06-11 02:57:52 +00:00
}
2016-02-03 20:12:32 +00:00
func (c *Client) addChannel(channel string) {
c.lock.Lock()
c.channels = append(c.channels, channel)
c.lock.Unlock()
}
func (c *Client) removeChannels(channels ...string) {
c.lock.Lock()
for _, removeCh := range channels {
for i, ch := range c.channels {
if c.EqualFold(removeCh, ch) {
c.channels = append(c.channels[:i], c.channels[i+1:]...)
2020-06-15 08:58:51 +00:00
break
}
}
}
c.lock.Unlock()
}
2016-02-03 20:12:32 +00:00
func (c *Client) flushChannels() {
c.lock.Lock()
if len(c.channels) > 0 {
c.Join(c.channels...)
c.channels = []string{}
}
c.lock.Unlock()
}