Pull IRC client out
This commit is contained in:
parent
78b6a0859b
commit
adcf12e1fa
11 changed files with 567 additions and 538 deletions
129
irc/client.go
Normal file
129
irc/client.go
Normal file
|
@ -0,0 +1,129 @@
|
|||
package irc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Server string
|
||||
Host string
|
||||
TLS bool
|
||||
TLSConfig *tls.Config
|
||||
Password string
|
||||
Username string
|
||||
Realname string
|
||||
Messages chan *Message
|
||||
|
||||
nick string
|
||||
|
||||
conn net.Conn
|
||||
connected bool
|
||||
dialer *net.Dialer
|
||||
reader *bufio.Reader
|
||||
out chan string
|
||||
|
||||
quit chan struct{}
|
||||
reconnect chan struct{}
|
||||
ready sync.WaitGroup
|
||||
once sync.Once
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func NewClient(nick, username string) *Client {
|
||||
return &Client{
|
||||
nick: nick,
|
||||
Username: username,
|
||||
Realname: nick,
|
||||
Messages: make(chan *Message, 32),
|
||||
out: make(chan string, 32),
|
||||
quit: make(chan struct{}),
|
||||
reconnect: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) GetNick() string {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
return c.nick
|
||||
}
|
||||
|
||||
func (c *Client) Connected() bool {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
return c.connected
|
||||
}
|
||||
|
||||
func (c *Client) Pass(password string) {
|
||||
c.write("PASS " + password)
|
||||
}
|
||||
|
||||
func (c *Client) Nick(nick string) {
|
||||
c.Write("NICK " + nick)
|
||||
|
||||
c.lock.Lock()
|
||||
c.nick = nick
|
||||
c.lock.Unlock()
|
||||
}
|
||||
|
||||
func (c *Client) User(username, realname string) {
|
||||
c.writef("USER %s 0 * :%s", username, realname)
|
||||
}
|
||||
|
||||
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, ","))
|
||||
}
|
||||
|
||||
func (c *Client) Topic(channel string) {
|
||||
c.Write("TOPIC " + channel)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (c *Client) Whois(nick string) {
|
||||
c.Write("WHOIS " + nick)
|
||||
}
|
||||
|
||||
func (c *Client) Away(message string) {
|
||||
c.Write("AWAY :" + message)
|
||||
}
|
177
irc/conn.go
Normal file
177
irc/conn.go
Normal file
|
@ -0,0 +1,177 @@
|
|||
package irc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (c *Client) Connect(address string) {
|
||||
if idx := strings.Index(address, ":"); idx < 0 {
|
||||
c.Host = address
|
||||
|
||||
if c.TLS {
|
||||
address += ":6697"
|
||||
} else {
|
||||
address += ":6667"
|
||||
}
|
||||
} else {
|
||||
c.Host = address[:idx]
|
||||
}
|
||||
c.Server = address
|
||||
c.dialer = &net.Dialer{Timeout: 10 * time.Second}
|
||||
|
||||
go c.run()
|
||||
}
|
||||
|
||||
func (c *Client) Write(data string) {
|
||||
c.out <- data + "\r\n"
|
||||
}
|
||||
|
||||
func (c *Client) Writef(format string, a ...interface{}) {
|
||||
c.out <- fmt.Sprintf(format+"\r\n", a...)
|
||||
}
|
||||
|
||||
func (c *Client) write(data string) {
|
||||
c.conn.Write([]byte(data + "\r\n"))
|
||||
}
|
||||
|
||||
func (c *Client) writef(format string, a ...interface{}) {
|
||||
fmt.Fprintf(c.conn, format+"\r\n", a...)
|
||||
}
|
||||
|
||||
func (c *Client) connect() error {
|
||||
if c.TLS {
|
||||
if c.TLSConfig == nil {
|
||||
c.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
|
||||
if conn, err := tls.DialWithDialer(c.dialer, "tcp", c.Server, c.TLSConfig); err != nil {
|
||||
return err
|
||||
} else {
|
||||
c.conn = conn
|
||||
}
|
||||
} else {
|
||||
if conn, err := c.dialer.Dial("tcp", c.Server); err != nil {
|
||||
return err
|
||||
} else {
|
||||
c.conn = conn
|
||||
}
|
||||
}
|
||||
|
||||
c.lock.Lock()
|
||||
c.connected = true
|
||||
c.lock.Unlock()
|
||||
|
||||
c.reader = bufio.NewReader(c.conn)
|
||||
|
||||
if c.Password != "" {
|
||||
c.Pass(c.Password)
|
||||
}
|
||||
c.write("NICK " + c.nick)
|
||||
c.User(c.Username, c.Realname)
|
||||
|
||||
c.ready.Add(1)
|
||||
go c.send()
|
||||
go c.recv()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) tryConnect() {
|
||||
// TODO: backoff
|
||||
for {
|
||||
select {
|
||||
case <-c.quit:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
err := c.connect()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) run() {
|
||||
c.tryConnect()
|
||||
for {
|
||||
select {
|
||||
case <-c.quit:
|
||||
c.close()
|
||||
c.lock.Lock()
|
||||
c.connected = false
|
||||
c.lock.Unlock()
|
||||
return
|
||||
|
||||
case <-c.reconnect:
|
||||
c.reconnect = make(chan struct{})
|
||||
c.once = sync.Once{}
|
||||
c.tryConnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) send() {
|
||||
c.ready.Wait()
|
||||
for {
|
||||
select {
|
||||
case <-c.quit:
|
||||
return
|
||||
|
||||
case <-c.reconnect:
|
||||
return
|
||||
|
||||
case msg := <-c.out:
|
||||
_, err := c.conn.Write([]byte(msg))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) recv() {
|
||||
defer c.conn.Close()
|
||||
for {
|
||||
line, err := c.reader.ReadString('\n')
|
||||
if err != nil {
|
||||
c.lock.Lock()
|
||||
c.connected = false
|
||||
c.lock.Unlock()
|
||||
c.once.Do(c.ready.Done)
|
||||
close(c.reconnect)
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-c.quit:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
msg := parseMessage(line)
|
||||
c.Messages <- msg
|
||||
|
||||
switch msg.Command {
|
||||
case Ping:
|
||||
go c.write("PONG :" + msg.Trailing)
|
||||
|
||||
case ReplyWelcome:
|
||||
c.once.Do(c.ready.Done)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) close() {
|
||||
if c.Connected() {
|
||||
c.once.Do(c.ready.Done)
|
||||
}
|
||||
close(c.out)
|
||||
close(c.Messages)
|
||||
}
|
35
irc/const.go
Normal file
35
irc/const.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package irc
|
||||
|
||||
const (
|
||||
Ping = "PING"
|
||||
Nick = "NICK"
|
||||
Join = "JOIN"
|
||||
Part = "PART"
|
||||
Mode = "MODE"
|
||||
Privmsg = "PRIVMSG"
|
||||
Notice = "NOTICE"
|
||||
Topic = "TOPIC"
|
||||
Quit = "QUIT"
|
||||
|
||||
ReplyWelcome = "001"
|
||||
ReplyYourHost = "002"
|
||||
ReplyCreated = "003"
|
||||
ReplyLUserClient = "251"
|
||||
ReplyLUserOp = "252"
|
||||
ReplyLUserUnknown = "253"
|
||||
ReplyLUserChannels = "254"
|
||||
ReplyLUserMe = "255"
|
||||
ReplyAway = "301"
|
||||
ReplyWhoisUser = "311"
|
||||
ReplyWhoisServer = "312"
|
||||
ReplyWhoisOperator = "313"
|
||||
ReplyWhoisIdle = "317"
|
||||
ReplyEndOfWhois = "318"
|
||||
ReplyWhoisChannels = "319"
|
||||
ReplyTopic = "332"
|
||||
ReplyNamReply = "353"
|
||||
ReplyEndOfNames = "366"
|
||||
ReplyMotd = "372"
|
||||
ReplyMotdStart = "375"
|
||||
ReplyEndOfMotd = "376"
|
||||
)
|
48
irc/message.go
Normal file
48
irc/message.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package irc
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Prefix string
|
||||
Nick string
|
||||
Command string
|
||||
Params []string
|
||||
Trailing string
|
||||
}
|
||||
|
||||
func parseMessage(line string) *Message {
|
||||
line = strings.Trim(line, "\r\n")
|
||||
msg := Message{}
|
||||
cmdStart := 0
|
||||
cmdEnd := len(line)
|
||||
|
||||
if strings.HasPrefix(line, ":") {
|
||||
cmdStart = strings.Index(line, " ") + 1
|
||||
msg.Prefix = line[1 : cmdStart-1]
|
||||
|
||||
if i := strings.Index(msg.Prefix, "!"); i > 0 {
|
||||
msg.Nick = msg.Prefix[:i]
|
||||
} else {
|
||||
msg.Nick = msg.Prefix
|
||||
}
|
||||
}
|
||||
|
||||
if i := strings.Index(line, " :"); i > 0 {
|
||||
cmdEnd = i
|
||||
msg.Trailing = line[i+2:]
|
||||
}
|
||||
|
||||
cmd := strings.Split(line[cmdStart:cmdEnd], " ")
|
||||
msg.Command = cmd[0]
|
||||
if len(cmd) > 1 {
|
||||
msg.Params = cmd[1:]
|
||||
}
|
||||
|
||||
if msg.Trailing != "" {
|
||||
msg.Params = append(msg.Params, msg.Trailing)
|
||||
}
|
||||
|
||||
return &msg
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue