Pull IRC client out
This commit is contained in:
parent
78b6a0859b
commit
adcf12e1fa
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
|
||||
}
|
62
server/conn.go
Normal file
62
server/conn.go
Normal file
@ -0,0 +1,62 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/khlieng/name_pending/Godeps/_workspace/src/github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type conn struct {
|
||||
conn *websocket.Conn
|
||||
in chan WSRequest
|
||||
out chan []byte
|
||||
}
|
||||
|
||||
func newConn(ws *websocket.Conn) *conn {
|
||||
return &conn{
|
||||
conn: ws,
|
||||
in: make(chan WSRequest, 32),
|
||||
out: make(chan []byte, 32),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) send() {
|
||||
var err error
|
||||
ping := time.Tick(20 * time.Second)
|
||||
|
||||
for {
|
||||
select {
|
||||
case msg, ok := <-c.out:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
err = c.conn.WriteMessage(websocket.TextMessage, msg)
|
||||
|
||||
case <-ping:
|
||||
err = c.conn.WriteJSON(WSResponse{Type: "ping"})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) recv() {
|
||||
var req WSRequest
|
||||
|
||||
for {
|
||||
err := c.conn.ReadJSON(&req)
|
||||
if err != nil {
|
||||
close(c.in)
|
||||
return
|
||||
}
|
||||
|
||||
c.in <- req
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) close() {
|
||||
close(c.out)
|
||||
}
|
379
server/irc.go
379
server/irc.go
@ -1,379 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
PING = "PING"
|
||||
NICK = "NICK"
|
||||
JOIN = "JOIN"
|
||||
PART = "PART"
|
||||
MODE = "MODE"
|
||||
PRIVMSG = "PRIVMSG"
|
||||
NOTICE = "NOTICE"
|
||||
TOPIC = "TOPIC"
|
||||
QUIT = "QUIT"
|
||||
|
||||
RPL_WELCOME = "001"
|
||||
RPL_YOURHOST = "002"
|
||||
RPL_CREATED = "003"
|
||||
RPL_LUSERCLIENT = "251"
|
||||
RPL_LUSEROP = "252"
|
||||
RPL_LUSERUNKNOWN = "253"
|
||||
RPL_LUSERCHANNELS = "254"
|
||||
RPL_LUSERME = "255"
|
||||
|
||||
RPL_AWAY = "301"
|
||||
|
||||
RPL_WHOISUSER = "311"
|
||||
RPL_WHOISSERVER = "312"
|
||||
RPL_WHOISOPERATOR = "313"
|
||||
RPL_WHOISIDLE = "317"
|
||||
RPL_ENDOFWHOIS = "318"
|
||||
RPL_WHOISCHANNELS = "319"
|
||||
|
||||
RPL_TOPIC = "332"
|
||||
|
||||
RPL_NAMREPLY = "353"
|
||||
RPL_ENDOFNAMES = "366"
|
||||
|
||||
RPL_MOTD = "372"
|
||||
RPL_MOTDSTART = "375"
|
||||
RPL_ENDOFMOTD = "376"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Prefix string
|
||||
Nick string
|
||||
Command string
|
||||
Params []string
|
||||
Trailing string
|
||||
}
|
||||
|
||||
type IRC struct {
|
||||
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
|
||||
|
||||
nick string
|
||||
|
||||
Messages chan *Message
|
||||
Server string
|
||||
Host string
|
||||
TLS bool
|
||||
TLSConfig *tls.Config
|
||||
Password string
|
||||
Username string
|
||||
Realname string
|
||||
}
|
||||
|
||||
func NewIRC(nick, username string) *IRC {
|
||||
return &IRC{
|
||||
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 (i *IRC) Connect(address string) {
|
||||
if idx := strings.Index(address, ":"); idx < 0 {
|
||||
i.Host = address
|
||||
|
||||
if i.TLS {
|
||||
address += ":6697"
|
||||
} else {
|
||||
address += ":6667"
|
||||
}
|
||||
} else {
|
||||
i.Host = address[:idx]
|
||||
}
|
||||
i.Server = address
|
||||
i.dialer = &net.Dialer{Timeout: 10 * time.Second}
|
||||
|
||||
go i.run()
|
||||
}
|
||||
|
||||
func (i *IRC) Connected() bool {
|
||||
i.lock.Lock()
|
||||
defer i.lock.Unlock()
|
||||
|
||||
return i.connected
|
||||
}
|
||||
|
||||
func (i *IRC) Pass(password string) {
|
||||
i.write("PASS " + password)
|
||||
}
|
||||
|
||||
func (i *IRC) Nick(nick string) {
|
||||
i.Write("NICK " + nick)
|
||||
|
||||
i.lock.Lock()
|
||||
i.nick = nick
|
||||
i.lock.Unlock()
|
||||
}
|
||||
|
||||
func (i *IRC) User(username, realname string) {
|
||||
i.writef("USER %s 0 * :%s", username, realname)
|
||||
}
|
||||
|
||||
func (i *IRC) Oper(name, password string) {
|
||||
i.Write("OPER " + name + " " + password)
|
||||
}
|
||||
|
||||
func (i *IRC) Mode(target, modes, params string) {
|
||||
i.Write(strings.TrimRight("MODE "+target+" "+modes+" "+params, " "))
|
||||
}
|
||||
|
||||
func (i *IRC) Quit() {
|
||||
go func() {
|
||||
if i.Connected() {
|
||||
i.write("QUIT")
|
||||
}
|
||||
close(i.quit)
|
||||
}()
|
||||
}
|
||||
|
||||
func (i *IRC) Join(channels ...string) {
|
||||
i.Write("JOIN " + strings.Join(channels, ","))
|
||||
}
|
||||
|
||||
func (i *IRC) Part(channels ...string) {
|
||||
i.Write("PART " + strings.Join(channels, ","))
|
||||
}
|
||||
|
||||
func (i *IRC) Topic(channel string) {
|
||||
i.Write("TOPIC " + channel)
|
||||
}
|
||||
|
||||
func (i *IRC) Invite(nick, channel string) {
|
||||
i.Write("INVITE " + nick + " " + channel)
|
||||
}
|
||||
|
||||
func (i *IRC) Kick(channel string, users ...string) {
|
||||
i.Write("KICK " + channel + " " + strings.Join(users, ","))
|
||||
}
|
||||
|
||||
func (i *IRC) Privmsg(target, msg string) {
|
||||
i.Writef("PRIVMSG %s :%s", target, msg)
|
||||
}
|
||||
|
||||
func (i *IRC) Notice(target, msg string) {
|
||||
i.Writef("NOTICE %s :%s", target, msg)
|
||||
}
|
||||
|
||||
func (i *IRC) Whois(nick string) {
|
||||
i.Write("WHOIS " + nick)
|
||||
}
|
||||
|
||||
func (i *IRC) Away(message string) {
|
||||
i.Write("AWAY :" + message)
|
||||
}
|
||||
|
||||
func (i *IRC) GetNick() string {
|
||||
i.lock.Lock()
|
||||
defer i.lock.Unlock()
|
||||
|
||||
return i.nick
|
||||
}
|
||||
|
||||
func (i *IRC) Write(data string) {
|
||||
i.out <- data + "\r\n"
|
||||
}
|
||||
|
||||
func (i *IRC) Writef(format string, a ...interface{}) {
|
||||
i.out <- fmt.Sprintf(format+"\r\n", a...)
|
||||
}
|
||||
|
||||
func (i *IRC) write(data string) {
|
||||
i.conn.Write([]byte(data + "\r\n"))
|
||||
}
|
||||
|
||||
func (i *IRC) writef(format string, a ...interface{}) {
|
||||
fmt.Fprintf(i.conn, format+"\r\n", a...)
|
||||
}
|
||||
|
||||
func (i *IRC) run() {
|
||||
i.tryConnect()
|
||||
for {
|
||||
select {
|
||||
case <-i.quit:
|
||||
i.close()
|
||||
i.lock.Lock()
|
||||
i.connected = false
|
||||
i.lock.Unlock()
|
||||
return
|
||||
|
||||
case <-i.reconnect:
|
||||
i.reconnect = make(chan struct{})
|
||||
i.once = sync.Once{}
|
||||
i.tryConnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *IRC) tryConnect() {
|
||||
// TODO: backoff
|
||||
for {
|
||||
select {
|
||||
case <-i.quit:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
err := i.connect()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *IRC) connect() error {
|
||||
if i.TLS {
|
||||
if i.TLSConfig == nil {
|
||||
i.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
|
||||
if conn, err := tls.DialWithDialer(i.dialer, "tcp", i.Server, i.TLSConfig); err != nil {
|
||||
return err
|
||||
} else {
|
||||
i.conn = conn
|
||||
}
|
||||
} else {
|
||||
if conn, err := i.dialer.Dial("tcp", i.Server); err != nil {
|
||||
return err
|
||||
} else {
|
||||
i.conn = conn
|
||||
}
|
||||
}
|
||||
|
||||
i.lock.Lock()
|
||||
i.connected = true
|
||||
i.lock.Unlock()
|
||||
|
||||
i.reader = bufio.NewReader(i.conn)
|
||||
|
||||
if i.Password != "" {
|
||||
i.Pass(i.Password)
|
||||
}
|
||||
i.write("NICK " + i.nick)
|
||||
i.User(i.Username, i.Realname)
|
||||
|
||||
i.ready.Add(1)
|
||||
go i.send()
|
||||
go i.recv()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *IRC) send() {
|
||||
i.ready.Wait()
|
||||
for {
|
||||
select {
|
||||
case <-i.quit:
|
||||
return
|
||||
|
||||
case <-i.reconnect:
|
||||
return
|
||||
|
||||
case msg := <-i.out:
|
||||
_, err := i.conn.Write([]byte(msg))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *IRC) recv() {
|
||||
defer i.conn.Close()
|
||||
for {
|
||||
line, err := i.reader.ReadString('\n')
|
||||
if err != nil {
|
||||
i.lock.Lock()
|
||||
i.connected = false
|
||||
i.lock.Unlock()
|
||||
i.once.Do(i.ready.Done)
|
||||
close(i.reconnect)
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-i.quit:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
msg := parseMessage(line)
|
||||
i.Messages <- msg
|
||||
|
||||
switch msg.Command {
|
||||
case PING:
|
||||
go i.write("PONG :" + msg.Trailing)
|
||||
|
||||
case RPL_WELCOME:
|
||||
i.once.Do(i.ready.Done)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *IRC) close() {
|
||||
if i.Connected() {
|
||||
i.once.Do(i.ready.Done)
|
||||
}
|
||||
close(i.out)
|
||||
close(i.Messages)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
@ -4,86 +4,87 @@ import (
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/khlieng/name_pending/irc"
|
||||
"github.com/khlieng/name_pending/storage"
|
||||
)
|
||||
|
||||
func handleMessages(irc *IRC, session *Session) {
|
||||
func handleIRC(client *irc.Client, session *Session) {
|
||||
var whois WhoisReply
|
||||
userBuffers := make(map[string][]string)
|
||||
var motd MOTD
|
||||
|
||||
for {
|
||||
msg, ok := <-irc.Messages
|
||||
msg, ok := <-client.Messages
|
||||
if !ok {
|
||||
session.deleteIRC(irc.Host)
|
||||
session.deleteIRC(client.Host)
|
||||
return
|
||||
}
|
||||
|
||||
switch msg.Command {
|
||||
case NICK:
|
||||
case irc.Nick:
|
||||
session.sendJSON("nick", Nick{
|
||||
Server: irc.Host,
|
||||
Server: client.Host,
|
||||
Old: msg.Nick,
|
||||
New: msg.Trailing,
|
||||
})
|
||||
|
||||
channelStore.RenameUser(msg.Nick, msg.Trailing, irc.Host)
|
||||
channelStore.RenameUser(msg.Nick, msg.Trailing, client.Host)
|
||||
|
||||
case JOIN:
|
||||
case irc.Join:
|
||||
session.sendJSON("join", Join{
|
||||
Server: irc.Host,
|
||||
Server: client.Host,
|
||||
User: msg.Nick,
|
||||
Channels: msg.Params,
|
||||
})
|
||||
|
||||
channelStore.AddUser(msg.Nick, irc.Host, msg.Params[0])
|
||||
channelStore.AddUser(msg.Nick, client.Host, msg.Params[0])
|
||||
|
||||
if msg.Nick == irc.GetNick() {
|
||||
if msg.Nick == client.GetNick() {
|
||||
session.user.AddChannel(storage.Channel{
|
||||
Server: irc.Host,
|
||||
Server: client.Host,
|
||||
Name: msg.Params[0],
|
||||
})
|
||||
}
|
||||
|
||||
case PART:
|
||||
case irc.Part:
|
||||
session.sendJSON("part", Part{
|
||||
Join: Join{
|
||||
Server: irc.Host,
|
||||
Server: client.Host,
|
||||
User: msg.Nick,
|
||||
Channels: msg.Params,
|
||||
},
|
||||
Reason: msg.Trailing,
|
||||
})
|
||||
|
||||
channelStore.RemoveUser(msg.Nick, irc.Host, msg.Params[0])
|
||||
channelStore.RemoveUser(msg.Nick, client.Host, msg.Params[0])
|
||||
|
||||
if msg.Nick == irc.GetNick() {
|
||||
session.user.RemoveChannel(irc.Host, msg.Params[0])
|
||||
if msg.Nick == client.GetNick() {
|
||||
session.user.RemoveChannel(client.Host, msg.Params[0])
|
||||
}
|
||||
|
||||
case MODE:
|
||||
case irc.Mode:
|
||||
target := msg.Params[0]
|
||||
if len(msg.Params) > 2 && isChannel(target) {
|
||||
mode := parseMode(msg.Params[1])
|
||||
mode.Server = irc.Host
|
||||
mode.Server = client.Host
|
||||
mode.Channel = target
|
||||
mode.User = msg.Params[2]
|
||||
|
||||
session.sendJSON("mode", mode)
|
||||
|
||||
channelStore.SetMode(irc.Host, target, msg.Params[2], mode.Add, mode.Remove)
|
||||
channelStore.SetMode(client.Host, target, msg.Params[2], mode.Add, mode.Remove)
|
||||
}
|
||||
|
||||
case PRIVMSG, NOTICE:
|
||||
if msg.Params[0] == irc.GetNick() {
|
||||
case irc.Privmsg, irc.Notice:
|
||||
if msg.Params[0] == client.GetNick() {
|
||||
session.sendJSON("pm", Chat{
|
||||
Server: irc.Host,
|
||||
Server: client.Host,
|
||||
From: msg.Nick,
|
||||
Message: msg.Trailing,
|
||||
})
|
||||
} else {
|
||||
session.sendJSON("message", Chat{
|
||||
Server: irc.Host,
|
||||
Server: client.Host,
|
||||
From: msg.Nick,
|
||||
To: msg.Params[0],
|
||||
Message: msg.Trailing,
|
||||
@ -91,90 +92,90 @@ func handleMessages(irc *IRC, session *Session) {
|
||||
}
|
||||
|
||||
if msg.Params[0] != "*" {
|
||||
session.user.LogMessage(irc.Host, msg.Nick, msg.Params[0], msg.Trailing)
|
||||
session.user.LogMessage(client.Host, msg.Nick, msg.Params[0], msg.Trailing)
|
||||
}
|
||||
|
||||
case QUIT:
|
||||
case irc.Quit:
|
||||
session.sendJSON("quit", Quit{
|
||||
Server: irc.Host,
|
||||
Server: client.Host,
|
||||
User: msg.Nick,
|
||||
Reason: msg.Trailing,
|
||||
})
|
||||
|
||||
channelStore.RemoveUserAll(msg.Nick, irc.Host)
|
||||
channelStore.RemoveUserAll(msg.Nick, client.Host)
|
||||
|
||||
case RPL_WELCOME,
|
||||
RPL_YOURHOST,
|
||||
RPL_CREATED,
|
||||
RPL_LUSERCLIENT,
|
||||
RPL_LUSEROP,
|
||||
RPL_LUSERUNKNOWN,
|
||||
RPL_LUSERCHANNELS,
|
||||
RPL_LUSERME:
|
||||
case irc.ReplyWelcome,
|
||||
irc.ReplyYourHost,
|
||||
irc.ReplyCreated,
|
||||
irc.ReplyLUserClient,
|
||||
irc.ReplyLUserOp,
|
||||
irc.ReplyLUserUnknown,
|
||||
irc.ReplyLUserChannels,
|
||||
irc.ReplyLUserMe:
|
||||
session.sendJSON("pm", Chat{
|
||||
Server: irc.Host,
|
||||
Server: client.Host,
|
||||
From: msg.Nick,
|
||||
Message: strings.Join(msg.Params[1:], " "),
|
||||
})
|
||||
|
||||
case RPL_WHOISUSER:
|
||||
case irc.ReplyWhoisUser:
|
||||
whois.Nick = msg.Params[1]
|
||||
whois.Username = msg.Params[2]
|
||||
whois.Host = msg.Params[3]
|
||||
whois.Realname = msg.Params[5]
|
||||
|
||||
case RPL_WHOISSERVER:
|
||||
case irc.ReplyWhoisServer:
|
||||
whois.Server = msg.Params[2]
|
||||
|
||||
case RPL_WHOISCHANNELS:
|
||||
case irc.ReplyWhoisChannels:
|
||||
whois.Channels = append(whois.Channels, strings.Split(strings.TrimRight(msg.Trailing, " "), " ")...)
|
||||
|
||||
case RPL_ENDOFWHOIS:
|
||||
case irc.ReplyEndOfWhois:
|
||||
session.sendJSON("whois", whois)
|
||||
|
||||
whois = WhoisReply{}
|
||||
|
||||
case RPL_TOPIC:
|
||||
case irc.ReplyTopic:
|
||||
session.sendJSON("topic", Topic{
|
||||
Server: irc.Host,
|
||||
Server: client.Host,
|
||||
Channel: msg.Params[1],
|
||||
Topic: msg.Trailing,
|
||||
})
|
||||
|
||||
channelStore.SetTopic(msg.Trailing, irc.Host, msg.Params[1])
|
||||
channelStore.SetTopic(msg.Trailing, client.Host, msg.Params[1])
|
||||
|
||||
case RPL_NAMREPLY:
|
||||
case irc.ReplyNamReply:
|
||||
users := strings.Split(msg.Trailing, " ")
|
||||
userBuffer := userBuffers[msg.Params[2]]
|
||||
userBuffers[msg.Params[2]] = append(userBuffer, users...)
|
||||
|
||||
case RPL_ENDOFNAMES:
|
||||
case irc.ReplyEndOfNames:
|
||||
channel := msg.Params[1]
|
||||
users := userBuffers[channel]
|
||||
|
||||
session.sendJSON("users", Userlist{
|
||||
Server: irc.Host,
|
||||
Server: client.Host,
|
||||
Channel: channel,
|
||||
Users: users,
|
||||
})
|
||||
|
||||
channelStore.SetUsers(users, irc.Host, channel)
|
||||
channelStore.SetUsers(users, client.Host, channel)
|
||||
delete(userBuffers, channel)
|
||||
|
||||
case RPL_MOTDSTART:
|
||||
motd.Server = irc.Host
|
||||
case irc.ReplyMotdStart:
|
||||
motd.Server = client.Host
|
||||
motd.Title = msg.Trailing
|
||||
|
||||
case RPL_MOTD:
|
||||
case irc.ReplyMotd:
|
||||
motd.Content = append(motd.Content, msg.Trailing)
|
||||
|
||||
case RPL_ENDOFMOTD:
|
||||
case irc.ReplyEndOfMotd:
|
||||
session.sendJSON("motd", motd)
|
||||
|
||||
motd = MOTD{}
|
||||
|
||||
default:
|
||||
printMessage(msg, irc)
|
||||
printMessage(msg, client)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -202,6 +203,6 @@ func isChannel(s string) bool {
|
||||
return strings.IndexAny(s, "&#+!") == 0
|
||||
}
|
||||
|
||||
func printMessage(msg *Message, irc *IRC) {
|
||||
log.Println(irc.GetNick()+":", msg.Prefix, msg.Command, msg.Params, msg.Trailing)
|
||||
func printMessage(msg *irc.Message, i *irc.Client) {
|
||||
log.Println(i.GetNick()+":", msg.Prefix, msg.Command, msg.Params, msg.Trailing)
|
||||
}
|
@ -9,6 +9,7 @@ import (
|
||||
"github.com/khlieng/name_pending/Godeps/_workspace/src/github.com/gorilla/websocket"
|
||||
"github.com/khlieng/name_pending/Godeps/_workspace/src/github.com/julienschmidt/httprouter"
|
||||
|
||||
"github.com/khlieng/name_pending/irc"
|
||||
"github.com/khlieng/name_pending/storage"
|
||||
)
|
||||
|
||||
@ -63,16 +64,16 @@ func reconnect() {
|
||||
channels := user.GetChannels()
|
||||
|
||||
for _, server := range user.GetServers() {
|
||||
irc := NewIRC(server.Nick, server.Username)
|
||||
irc.TLS = server.TLS
|
||||
irc.Password = server.Password
|
||||
irc.Realname = server.Realname
|
||||
i := irc.NewClient(server.Nick, server.Username)
|
||||
i.TLS = server.TLS
|
||||
i.Password = server.Password
|
||||
i.Realname = server.Realname
|
||||
|
||||
go func(server storage.Server) {
|
||||
irc.Connect(server.Address)
|
||||
session.setIRC(irc.Host, irc)
|
||||
i.Connect(server.Address)
|
||||
session.setIRC(i.Host, i)
|
||||
|
||||
go handleMessages(irc, session)
|
||||
go handleIRC(i, session)
|
||||
|
||||
var joining []string
|
||||
for _, channel := range channels {
|
||||
@ -80,7 +81,7 @@ func reconnect() {
|
||||
joining = append(joining, channel.Name)
|
||||
}
|
||||
}
|
||||
irc.Join(joining...)
|
||||
i.Join(joining...)
|
||||
}(server)
|
||||
}
|
||||
}
|
||||
|
@ -4,14 +4,15 @@ import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
|
||||
"github.com/khlieng/name_pending/irc"
|
||||
"github.com/khlieng/name_pending/storage"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
irc map[string]*IRC
|
||||
irc map[string]*irc.Client
|
||||
ircLock sync.Mutex
|
||||
|
||||
ws map[string]*WebSocket
|
||||
ws map[string]*conn
|
||||
wsLock sync.Mutex
|
||||
out chan []byte
|
||||
|
||||
@ -20,23 +21,23 @@ type Session struct {
|
||||
|
||||
func NewSession() *Session {
|
||||
return &Session{
|
||||
irc: make(map[string]*IRC),
|
||||
ws: make(map[string]*WebSocket),
|
||||
irc: make(map[string]*irc.Client),
|
||||
ws: make(map[string]*conn),
|
||||
out: make(chan []byte, 32),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) getIRC(server string) (*IRC, bool) {
|
||||
func (s *Session) getIRC(server string) (*irc.Client, bool) {
|
||||
s.ircLock.Lock()
|
||||
irc, ok := s.irc[server]
|
||||
i, ok := s.irc[server]
|
||||
s.ircLock.Unlock()
|
||||
|
||||
return irc, ok
|
||||
return i, ok
|
||||
}
|
||||
|
||||
func (s *Session) setIRC(server string, irc *IRC) {
|
||||
func (s *Session) setIRC(server string, i *irc.Client) {
|
||||
s.ircLock.Lock()
|
||||
s.irc[server] = irc
|
||||
s.irc[server] = i
|
||||
s.ircLock.Unlock()
|
||||
}
|
||||
|
||||
@ -54,7 +55,7 @@ func (s *Session) numIRC() int {
|
||||
return n
|
||||
}
|
||||
|
||||
func (s *Session) setWS(addr string, w *WebSocket) {
|
||||
func (s *Session) setWS(addr string, w *conn) {
|
||||
s.wsLock.Lock()
|
||||
s.ws[addr] = w
|
||||
s.wsLock.Unlock()
|
||||
@ -85,7 +86,7 @@ func (s *Session) write() {
|
||||
for res := range s.out {
|
||||
s.wsLock.Lock()
|
||||
for _, ws := range s.ws {
|
||||
ws.Out <- res
|
||||
ws.out <- res
|
||||
}
|
||||
s.wsLock.Unlock()
|
||||
}
|
||||
|
@ -1,47 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/khlieng/name_pending/Godeps/_workspace/src/github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type WebSocket struct {
|
||||
conn *websocket.Conn
|
||||
|
||||
Out chan []byte
|
||||
}
|
||||
|
||||
func NewWebSocket(ws *websocket.Conn) *WebSocket {
|
||||
return &WebSocket{
|
||||
conn: ws,
|
||||
Out: make(chan []byte, 32),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WebSocket) write() {
|
||||
var err error
|
||||
ping := time.Tick(20 * time.Second)
|
||||
|
||||
for {
|
||||
select {
|
||||
case msg, ok := <-w.Out:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
err = w.conn.WriteMessage(websocket.TextMessage, msg)
|
||||
|
||||
case <-ping:
|
||||
err = w.conn.WriteJSON(WSResponse{Type: "ping"})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WebSocket) close() {
|
||||
close(w.Out)
|
||||
}
|
@ -7,31 +7,32 @@ import (
|
||||
|
||||
"github.com/khlieng/name_pending/Godeps/_workspace/src/github.com/gorilla/websocket"
|
||||
|
||||
"github.com/khlieng/name_pending/irc"
|
||||
"github.com/khlieng/name_pending/storage"
|
||||
)
|
||||
|
||||
func handleWS(ws *websocket.Conn) {
|
||||
defer ws.Close()
|
||||
func handleWS(conn *websocket.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
var session *Session
|
||||
var UUID string
|
||||
var req WSRequest
|
||||
|
||||
addr := ws.RemoteAddr().String()
|
||||
w := NewWebSocket(ws)
|
||||
go w.write()
|
||||
addr := conn.RemoteAddr().String()
|
||||
|
||||
ws := newConn(conn)
|
||||
defer ws.close()
|
||||
go ws.send()
|
||||
go ws.recv()
|
||||
|
||||
log.Println(addr, "connected")
|
||||
|
||||
for {
|
||||
err := ws.ReadJSON(&req)
|
||||
if err != nil {
|
||||
req, ok := <-ws.in
|
||||
if !ok {
|
||||
if session != nil {
|
||||
session.deleteWS(addr)
|
||||
}
|
||||
|
||||
w.close()
|
||||
|
||||
log.Println(addr, "disconnected")
|
||||
return
|
||||
}
|
||||
@ -77,7 +78,7 @@ func handleWS(ws *websocket.Conn) {
|
||||
go session.write()
|
||||
}
|
||||
|
||||
session.setWS(addr, w)
|
||||
session.setWS(addr, ws)
|
||||
|
||||
case "connect":
|
||||
var data Connect
|
||||
@ -87,24 +88,24 @@ func handleWS(ws *websocket.Conn) {
|
||||
if _, ok := session.getIRC(data.Server); !ok {
|
||||
log.Println(addr, "connecting to", data.Server)
|
||||
|
||||
irc := NewIRC(data.Nick, data.Username)
|
||||
irc.TLS = data.TLS
|
||||
irc.Password = data.Password
|
||||
irc.Realname = data.Realname
|
||||
i := irc.NewClient(data.Nick, data.Username)
|
||||
i.TLS = data.TLS
|
||||
i.Password = data.Password
|
||||
i.Realname = data.Realname
|
||||
|
||||
if idx := strings.Index(data.Server, ":"); idx < 0 {
|
||||
session.setIRC(data.Server, irc)
|
||||
session.setIRC(data.Server, i)
|
||||
} else {
|
||||
session.setIRC(data.Server[:idx], irc)
|
||||
session.setIRC(data.Server[:idx], i)
|
||||
}
|
||||
|
||||
go func() {
|
||||
irc.Connect(data.Server)
|
||||
go handleMessages(irc, session)
|
||||
i.Connect(data.Server)
|
||||
go handleIRC(i, session)
|
||||
|
||||
session.user.AddServer(storage.Server{
|
||||
Name: data.Name,
|
||||
Address: irc.Host,
|
||||
Address: i.Host,
|
||||
TLS: data.TLS,
|
||||
Password: data.Password,
|
||||
Nick: data.Nick,
|
||||
@ -121,8 +122,8 @@ func handleWS(ws *websocket.Conn) {
|
||||
|
||||
json.Unmarshal(req.Request, &data)
|
||||
|
||||
if irc, ok := session.getIRC(data.Server); ok {
|
||||
irc.Join(data.Channels...)
|
||||
if i, ok := session.getIRC(data.Server); ok {
|
||||
i.Join(data.Channels...)
|
||||
}
|
||||
|
||||
case "part":
|
||||
@ -130,8 +131,8 @@ func handleWS(ws *websocket.Conn) {
|
||||
|
||||
json.Unmarshal(req.Request, &data)
|
||||
|
||||
if irc, ok := session.getIRC(data.Server); ok {
|
||||
irc.Part(data.Channels...)
|
||||
if i, ok := session.getIRC(data.Server); ok {
|
||||
i.Part(data.Channels...)
|
||||
}
|
||||
|
||||
case "quit":
|
||||
@ -139,10 +140,10 @@ func handleWS(ws *websocket.Conn) {
|
||||
|
||||
json.Unmarshal(req.Request, &data)
|
||||
|
||||
if irc, ok := session.getIRC(data.Server); ok {
|
||||
irc.Quit()
|
||||
if i, ok := session.getIRC(data.Server); ok {
|
||||
i.Quit()
|
||||
session.deleteIRC(data.Server)
|
||||
channelStore.RemoveUserAll(irc.GetNick(), data.Server)
|
||||
channelStore.RemoveUserAll(i.GetNick(), data.Server)
|
||||
session.user.RemoveServer(data.Server)
|
||||
}
|
||||
|
||||
@ -151,8 +152,8 @@ func handleWS(ws *websocket.Conn) {
|
||||
|
||||
json.Unmarshal(req.Request, &data)
|
||||
|
||||
if irc, ok := session.getIRC(data.Server); ok {
|
||||
irc.Privmsg(data.To, data.Message)
|
||||
if i, ok := session.getIRC(data.Server); ok {
|
||||
i.Privmsg(data.To, data.Message)
|
||||
}
|
||||
|
||||
case "nick":
|
||||
@ -160,8 +161,8 @@ func handleWS(ws *websocket.Conn) {
|
||||
|
||||
json.Unmarshal(req.Request, &data)
|
||||
|
||||
if irc, ok := session.getIRC(data.Server); ok {
|
||||
irc.Nick(data.New)
|
||||
if i, ok := session.getIRC(data.Server); ok {
|
||||
i.Nick(data.New)
|
||||
session.user.SetNick(data.New, data.Server)
|
||||
}
|
||||
|
||||
@ -170,8 +171,8 @@ func handleWS(ws *websocket.Conn) {
|
||||
|
||||
json.Unmarshal(req.Request, &data)
|
||||
|
||||
if irc, ok := session.getIRC(data.Server); ok {
|
||||
irc.Invite(data.User, data.Channel)
|
||||
if i, ok := session.getIRC(data.Server); ok {
|
||||
i.Invite(data.User, data.Channel)
|
||||
}
|
||||
|
||||
case "kick":
|
||||
@ -179,8 +180,8 @@ func handleWS(ws *websocket.Conn) {
|
||||
|
||||
json.Unmarshal(req.Request, &data)
|
||||
|
||||
if irc, ok := session.getIRC(data.Server); ok {
|
||||
irc.Kick(data.Channel, data.User)
|
||||
if i, ok := session.getIRC(data.Server); ok {
|
||||
i.Kick(data.Channel, data.User)
|
||||
}
|
||||
|
||||
case "whois":
|
||||
@ -188,8 +189,8 @@ func handleWS(ws *websocket.Conn) {
|
||||
|
||||
json.Unmarshal(req.Request, &data)
|
||||
|
||||
if irc, ok := session.getIRC(data.Server); ok {
|
||||
irc.Whois(data.User)
|
||||
if i, ok := session.getIRC(data.Server); ok {
|
||||
i.Whois(data.User)
|
||||
}
|
||||
|
||||
case "away":
|
||||
@ -197,8 +198,8 @@ func handleWS(ws *websocket.Conn) {
|
||||
|
||||
json.Unmarshal(req.Request, &data)
|
||||
|
||||
if irc, ok := session.getIRC(data.Server); ok {
|
||||
irc.Away(data.Message)
|
||||
if i, ok := session.getIRC(data.Server); ok {
|
||||
i.Away(data.Message)
|
||||
}
|
||||
|
||||
case "search":
|
||||
|
Loading…
Reference in New Issue
Block a user