diff --git a/pkg/irc/client.go b/pkg/irc/client.go index 83dd40b0..a64a82d9 100644 --- a/pkg/irc/client.go +++ b/pkg/irc/client.go @@ -21,6 +21,11 @@ type Client struct { Realname string HandleNickInUse func(string) string + // Version is the reply to VERSION and FINGER CTCP messages + Version string + // Source is the reply to SOURCE CTCP messages + Source string + DownloadFolder string Autoget bool @@ -53,8 +58,8 @@ func NewClient(nick, username string) *Client { Username: username, Realname: nick, Messages: make(chan *Message, 32), - ConnectionChanged: make(chan ConnectionState, 16), - Progress: make(chan DownloadProgress, 16), + ConnectionChanged: make(chan ConnectionState, 4), + Progress: make(chan DownloadProgress, 4), out: make(chan string, 32), quit: make(chan struct{}), reconnect: make(chan struct{}), @@ -154,6 +159,13 @@ func (c *Client) Notice(target, msg string) { c.Writef("NOTICE %s :%s", target, msg) } +func (c *Client) ReplyCTCP(target, command, params string) { + c.Notice(target, EncodeCTCP(&CTCP{ + Command: command, + Params: params, + })) +} + func (c *Client) Whois(nick string) { c.Write("WHOIS " + nick) } diff --git a/pkg/irc/client_test.go b/pkg/irc/client_test.go index 3e724656..985f57f3 100644 --- a/pkg/irc/client_test.go +++ b/pkg/irc/client_test.go @@ -128,6 +128,12 @@ func TestNotice(t *testing.T) { assert.Equal(t, "NOTICE user :the message\r\n", <-out) } +func TestReplyCTCP(t *testing.T) { + c, out := testClientSend() + c.ReplyCTCP("user", "PING", "PONG") + assert.Equal(t, "NOTICE user :\x01PING PONG\x01\r\n", <-out) +} + func TestWhois(t *testing.T) { c, out := testClientSend() c.Whois("user") diff --git a/pkg/irc/conn.go b/pkg/irc/conn.go index fc8b698a..f4ada4a8 100644 --- a/pkg/irc/conn.go +++ b/pkg/irc/conn.go @@ -225,7 +225,7 @@ func (c *Client) recv() { case Privmsg: if ctcp := msg.ToCTCP(); ctcp != nil { - c.handleCTCP(ctcp) + c.handleCTCP(ctcp, msg) } case ReplyWelcome: @@ -250,14 +250,3 @@ func (c *Client) recv() { c.Messages <- msg } } - -func (c *Client) handleCTCP(ctcp *CTCP) { - switch ctcp.Command { - case "DCC": - if strings.HasPrefix(ctcp.Params, "SEND") { - if dccSend := ParseDCCSend(ctcp); dccSend != nil { - go c.Download(dccSend) - } - } - } -} diff --git a/pkg/irc/ctcp.go b/pkg/irc/ctcp.go new file mode 100644 index 00000000..3b20768a --- /dev/null +++ b/pkg/irc/ctcp.go @@ -0,0 +1,76 @@ +package irc + +import ( + "fmt" + "strings" + "time" +) + +// ClientInfo is the CTCP messages this client implements +const ClientInfo = "ACTION CLIENTINFO DCC FINGER PING SOURCE TIME VERSION USERINFO" + +type CTCP struct { + Command string + Params string +} + +func DecodeCTCP(str string) *CTCP { + if len(str) > 1 && str[0] == 0x01 { + parts := strings.SplitN(strings.Trim(str, "\x01"), " ", 2) + ctcp := CTCP{} + + if parts[0] != "" { + ctcp.Command = parts[0] + } else { + return nil + } + + if len(parts) == 2 { + ctcp.Params = parts[1] + } + + return &ctcp + } + + return nil +} + +func EncodeCTCP(ctcp *CTCP) string { + if ctcp == nil || ctcp.Command == "" { + return "" + } + return fmt.Sprintf("\x01%s %s\x01", ctcp.Command, ctcp.Params) +} + +func (c *Client) handleCTCP(ctcp *CTCP, msg *Message) { + switch ctcp.Command { + case "CLIENTINFO": + c.ReplyCTCP(msg.Nick, ctcp.Command, ClientInfo) + + case "DCC": + if strings.HasPrefix(ctcp.Params, "SEND") { + if dccSend := ParseDCCSend(ctcp); dccSend != nil { + go c.Download(dccSend) + } + } + + case "FINGER", "VERSION": + if c.Version != "" { + c.ReplyCTCP(msg.Nick, ctcp.Command, c.Version) + } + + case "PING": + c.ReplyCTCP(msg.Nick, ctcp.Command, ctcp.Params) + + case "SOURCE": + if c.Source != "" { + c.ReplyCTCP(msg.Nick, ctcp.Command, c.Source) + } + + case "TIME": + c.ReplyCTCP(msg.Nick, ctcp.Command, time.Now().UTC().Format(time.RFC3339)) + + case "USERINFO": + c.ReplyCTCP(msg.Nick, ctcp.Command, fmt.Sprintf("%s (%s)", c.GetNick(), c.Realname)) + } +} diff --git a/pkg/irc/message.go b/pkg/irc/message.go index 6327fec4..850f29df 100644 --- a/pkg/irc/message.go +++ b/pkg/irc/message.go @@ -19,32 +19,8 @@ func (m *Message) LastParam() string { return "" } -type CTCP struct { - Command string - Params string -} - func (m *Message) ToCTCP() *CTCP { - lp := m.LastParam() - - if len(lp) > 1 && lp[0] == 0x01 { - parts := strings.SplitN(strings.Trim(lp, "\x01"), " ", 2) - ctcp := CTCP{} - - if parts[0] != "" { - ctcp.Command = parts[0] - } else { - return nil - } - - if len(parts) == 2 { - ctcp.Params = parts[1] - } - - return &ctcp - } - - return nil + return DecodeCTCP(m.LastParam()) } func ParseMessage(line string) *Message { diff --git a/server/irc.go b/server/irc.go index 23336a64..20475dd2 100644 --- a/server/irc.go +++ b/server/irc.go @@ -8,6 +8,7 @@ import ( "github.com/khlieng/dispatch/pkg/irc" "github.com/khlieng/dispatch/storage" + "github.com/khlieng/dispatch/version" ) func createNickInUseHandler(i *irc.Client, state *State) func(string) string { @@ -33,6 +34,8 @@ func connectIRC(server *storage.Server, state *State, srcIP []byte) *irc.Client i := irc.NewClient(server.Nick, server.Username) i.TLS = server.TLS i.Realname = server.Realname + i.Version = fmt.Sprintf("Dispatch %s (git: %s)", version.Tag, version.Commit) + i.Source = "https://github.com/khlieng/dispatch" i.HandleNickInUse = createNickInUseHandler(i, state) address := server.Host diff --git a/server/irc_handler.go b/server/irc_handler.go index 1875f5cb..5c527b2a 100644 --- a/server/irc_handler.go +++ b/server/irc_handler.go @@ -176,6 +176,10 @@ func (i *ircHandler) mode(msg *irc.Message) { } func (i *ircHandler) message(msg *irc.Message) { + if ctcp := msg.ToCTCP(); ctcp != nil && ctcp.Command != "ACTION" { + return + } + message := Message{ ID: betterguid.New(), Server: i.client.Host,