diff --git a/client.go b/client.go index 6919066..32ca9b2 100644 --- a/client.go +++ b/client.go @@ -524,6 +524,21 @@ func (c *Client) PartMessage(channel, message string) error { return c.Send(&Event{Command: JOIN, Params: []string{channel}, Trailing: message}) } +// SendCTCP sends a CTCP request to target. +func (c *Client) SendCTCP(target, ctcpType, message string) error { + out := encodeCTCPRaw(ctcpType, message) + if out == "" { + return errors.New("invalid CTCP") + } + + return c.Message(target, out) +} + +// SendCTCPf sends a CTCP request to target using a specific format. +func (c *Client) SendCTCPf(target, ctcpType, format string, a ...interface{}) error { + return c.SendCTCP(target, ctcpType, fmt.Sprintf(format, a...)) +} + // Message sends a PRIVMSG to target (either channel, service, or user). func (c *Client) Message(target, message string) error { if !IsValidNick(target) && !IsValidChannel(target) { diff --git a/contants.go b/contants.go index 3d53776..25fe201 100644 --- a/contants.go +++ b/contants.go @@ -4,6 +4,18 @@ package girc +// Standard CTCP based constants +const ( + CTCP_PING = "PING" + CTCP_PONG = "PONG" + CTCP_VERSION = "VERSION" + CTCP_USERINFO = "USERINFO" + CTCP_CLIENTINFO = "CLIENTINFO" + CTCP_FINGER = "FINGER" + CTCP_SOURCE = "SOURCE" + CTCP_TIME = "TIME" +) + // Misc constants for use with the client. const ( ALLEVENTS = "*" // trigger on all events diff --git a/ctcp.go b/ctcp.go new file mode 100644 index 0000000..f5b8a83 --- /dev/null +++ b/ctcp.go @@ -0,0 +1,151 @@ +// Copyright 2016 Liam Stanley . 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" + "sync" +) + +const ctcpDelim byte = 0x01 // Prefix and suffix for CTCP messages. + +type CTCPEvent struct { + Source *Source + Command string + Text string +} + +func decodeCTCP(e *Event) *CTCPEvent { + if len(e.Params) != 1 { + return nil + } + + if e.Command != "PRIVMSG" || !IsValidNick(e.Params[0]) { + return nil + } + + if e.Trailing[0] != ctcpDelim || e.Trailing[len(e.Trailing)-1] != ctcpDelim { + return nil + } + + text := e.Trailing[1 : len(e.Trailing)-1] + + s := strings.IndexByte(text, space) + + // 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{Source: e.Source, Command: text} + } + + // 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{Source: e.Source, Command: text[1:s], Text: text[s+1 : len(text)-1]} +} + +func encodeCTCP(ctcp *CTCPEvent) (out string) { + if ctcp == nil { + return "" + } + + return encodeCTCPRaw(ctcp.Command, ctcp.Text) +} + +func encodeCTCPRaw(cmd, text string) (out string) { + if len(cmd) <= 0 { + return "" + } + + out = string(ctcpDelim) + cmd + + if len(text) > 0 { + out += string(space) + text + } + + return out + string(ctcpDelim) +} + +type CTCP struct { + // mu is the mutex that should be used when accessing callbacks. + mu sync.RWMutex + // handlers is a map of CTCP message -> functions. + handlers map[string]CTCPHandler +} + +func newCTCP() *CTCP { + return &CTCP{handlers: map[string]CTCPHandler{}} +} + +func (c *CTCP) Call(event *CTCPEvent, client *Client) { + c.mu.RLock() + if _, ok := c.handlers[event.Command]; !ok { + c.mu.RUnlock() + return + } + + go c.handlers[event.Command](client, event) + c.mu.RUnlock() +} + +func (c *CTCP) parseCMD(cmd string) string { + 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 +} + +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() +} + +func (c *CTCP) Clear(cmd string) { + if cmd = c.parseCMD(cmd); cmd == "" { + return + } + + c.mu.Lock() + delete(c.handlers, cmd) + c.mu.Unlock() +} + +func (c *CTCP) ClearAll() { + c.mu.Lock() + c.handlers = map[string]CTCPHandler{} + c.mu.Unlock() +} + +// CTCPHandler is a type that represents the function necessary to +// implement a CTCP handler. +type CTCPHandler func(client *Client, ctcp *CTCPEvent) + +func (c *CTCP) addDefaultHandlers() {} + +func handleCTCPPing(client *Client, ctcp *CTCPEvent) { + client.SendCTCP(ctcp.Source.Name, CTCP_PING, "") +} diff --git a/event.go b/event.go index 1b6beb9..fc5cc58 100644 --- a/event.go +++ b/event.go @@ -34,7 +34,7 @@ func cutCRFunc(r rune) bool { // CR or LF> // :: CR LF type Event struct { - *Source // The source of the event. + Source *Source // The source of the event. Command string // the IRC command, e.g. JOIN, PRIVMSG, KILL. Params []string // parameters to the command. Commonly nickname, channel, etc. Trailing string // any trailing data. e.g. with a PRIVMSG, this is the message text. @@ -240,7 +240,7 @@ func (e *Event) IsAction() bool { return false } - if !strings.HasPrefix(e.Trailing, "\001ACTION") || !strings.HasSuffix(e.Trailing, "\001") { + if !strings.HasPrefix(e.Trailing, "\001ACTION") || e.Trailing[len(e.Trailing)-1] != ctcpDelim { return false }