2017-11-08 21:47:18 +00:00
|
|
|
// Copyright (c) Liam Stanley <me@liamstanley.io>. 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 (
|
|
|
|
"fmt"
|
|
|
|
"runtime"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
// ctcpDelim if the delimiter used for CTCP formatted events/messages.
|
|
|
|
const ctcpDelim byte = 0x01 // Prefix and suffix for CTCP messages.
|
|
|
|
|
|
|
|
// CTCPEvent is the necessary information from an IRC message.
|
|
|
|
type CTCPEvent struct {
|
|
|
|
// Origin is the original event that the CTCP event was decoded from.
|
|
|
|
Origin *Event `json:"origin"`
|
|
|
|
// Source is the author of the CTCP event.
|
|
|
|
Source *Source `json:"source"`
|
|
|
|
// Command is the type of CTCP event. E.g. PING, TIME, VERSION.
|
|
|
|
Command string `json:"command"`
|
|
|
|
// Text is the raw arguments following the command.
|
|
|
|
Text string `json:"text"`
|
|
|
|
// Reply is true if the CTCP event is intended to be a reply to a
|
|
|
|
// previous CTCP (e.g, if we sent one).
|
|
|
|
Reply bool `json:"reply"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// decodeCTCP decodes an incoming CTCP event, if it is CTCP. nil is returned
|
|
|
|
// if the incoming event does not match a valid CTCP.
|
|
|
|
func decodeCTCP(e *Event) *CTCPEvent {
|
|
|
|
// http://www.irchelp.org/protocol/ctcpspec.html
|
|
|
|
|
|
|
|
// Must be targeting a user/channel, AND trailing must have
|
|
|
|
// DELIM+TAG+DELIM minimum (at least 3 chars).
|
|
|
|
if len(e.Params) != 1 || len(e.Trailing) < 3 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if (e.Command != PRIVMSG && e.Command != NOTICE) || !IsValidNick(e.Params[0]) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if e.Trailing[0] != ctcpDelim || e.Trailing[len(e.Trailing)-1] != ctcpDelim {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Strip delimiters.
|
|
|
|
text := e.Trailing[1 : len(e.Trailing)-1]
|
|
|
|
|
|
|
|
s := strings.IndexByte(text, eventSpace)
|
|
|
|
|
|
|
|
// 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.
|
2017-12-02 22:58:02 +00:00
|
|
|
if (text[i] < 'A' || text[i] > 'Z') && (text[i] < '0' || text[i] > '9') {
|
2017-11-08 21:47:18 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return &CTCPEvent{
|
|
|
|
Origin: e,
|
|
|
|
Source: e.Source,
|
|
|
|
Command: text,
|
|
|
|
Reply: e.Command == NOTICE,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Loop through checking the tag first.
|
|
|
|
for i := 0; i < s; i++ {
|
|
|
|
// Check for A-Z, 0-9.
|
2017-12-02 22:58:02 +00:00
|
|
|
if (text[i] < 'A' || text[i] > 'Z') && (text[i] < '0' || text[i] > '9') {
|
2017-11-08 21:47:18 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return &CTCPEvent{
|
|
|
|
Origin: e,
|
|
|
|
Source: e.Source,
|
|
|
|
Command: text[0:s],
|
|
|
|
Text: text[s+1:],
|
|
|
|
Reply: e.Command == NOTICE,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// encodeCTCP encodes a CTCP event into a string, including delimiters.
|
|
|
|
func encodeCTCP(ctcp *CTCPEvent) (out string) {
|
|
|
|
if ctcp == nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
return encodeCTCPRaw(ctcp.Command, ctcp.Text)
|
|
|
|
}
|
|
|
|
|
|
|
|
// encodeCTCPRaw is much like encodeCTCP, however accepts a raw command and
|
|
|
|
// string as input.
|
|
|
|
func encodeCTCPRaw(cmd, text string) (out string) {
|
|
|
|
if len(cmd) <= 0 {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
out = string(ctcpDelim) + cmd
|
|
|
|
|
|
|
|
if len(text) > 0 {
|
|
|
|
out += string(eventSpace) + text
|
|
|
|
}
|
|
|
|
|
|
|
|
return out + string(ctcpDelim)
|
|
|
|
}
|
|
|
|
|
|
|
|
// CTCP handles the storage and execution of CTCP handlers against incoming
|
|
|
|
// CTCP events.
|
|
|
|
type CTCP struct {
|
|
|
|
// mu is the mutex that should be used when accessing any ctcp handlers.
|
|
|
|
mu sync.RWMutex
|
|
|
|
// handlers is a map of CTCP message -> functions.
|
|
|
|
handlers map[string]CTCPHandler
|
|
|
|
}
|
|
|
|
|
|
|
|
// newCTCP returns a new clean CTCP handler.
|
|
|
|
func newCTCP() *CTCP {
|
|
|
|
return &CTCP{handlers: map[string]CTCPHandler{}}
|
|
|
|
}
|
|
|
|
|
|
|
|
// call executes the necessary CTCP handler for the incoming event/CTCP
|
|
|
|
// command.
|
|
|
|
func (c *CTCP) call(client *Client, event *CTCPEvent) {
|
|
|
|
c.mu.RLock()
|
|
|
|
defer c.mu.RUnlock()
|
|
|
|
|
|
|
|
// If they want to catch any panics, add to defer stack.
|
|
|
|
if client.Config.RecoverFunc != nil && event.Origin != nil {
|
|
|
|
defer recoverHandlerPanic(client, event.Origin, "ctcp-"+strings.ToLower(event.Command), 3)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Support wildcard CTCP event handling. Gets executed first before
|
|
|
|
// regular event handlers.
|
|
|
|
if _, ok := c.handlers["*"]; ok {
|
|
|
|
c.handlers["*"](client, *event)
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, ok := c.handlers[event.Command]; !ok {
|
|
|
|
// Send a ERRMSG reply, if we know who sent it.
|
|
|
|
if event.Source != nil && IsValidNick(event.Source.Name) {
|
|
|
|
client.Cmd.SendCTCPReply(event.Source.Name, CTCP_ERRMSG, "that is an unknown CTCP query")
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c.handlers[event.Command](client, *event)
|
|
|
|
}
|
|
|
|
|
|
|
|
// parseCMD parses a CTCP command/tag, ensuring it's valid. If not, an empty
|
|
|
|
// string is returned.
|
|
|
|
func (c *CTCP) parseCMD(cmd string) string {
|
|
|
|
// TODO: Needs proper testing.
|
|
|
|
// Check if wildcard.
|
|
|
|
if cmd == "*" {
|
|
|
|
return "*"
|
|
|
|
}
|
|
|
|
|
|
|
|
cmd = strings.ToUpper(cmd)
|
|
|
|
|
|
|
|
for i := 0; i < len(cmd); i++ {
|
|
|
|
// Check for A-Z, 0-9.
|
2017-12-02 22:58:02 +00:00
|
|
|
if (cmd[i] < 'A' || cmd[i] > 'Z') && (cmd[i] < '0' || cmd[i] > '9') {
|
2017-11-08 21:47:18 +00:00
|
|
|
return ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return cmd
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set saves handler for execution upon a matching incoming CTCP event.
|
|
|
|
// Use SetBg if the handler may take an extended period of time to execute.
|
|
|
|
// If you would like to have a handler which will catch ALL CTCP requests,
|
|
|
|
// simply use "*" in place of the command.
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetBg is much like Set, however the handler is executed in the background,
|
|
|
|
// ensuring that event handling isn't hung during long running tasks. See Set
|
|
|
|
// for more information.
|
|
|
|
func (c *CTCP) SetBg(cmd string, handler func(client *Client, ctcp CTCPEvent)) {
|
|
|
|
c.Set(cmd, func(client *Client, ctcp CTCPEvent) {
|
|
|
|
go handler(client, ctcp)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clear removes currently setup handler for cmd, if one is set.
|
|
|
|
func (c *CTCP) Clear(cmd string) {
|
|
|
|
if cmd = c.parseCMD(cmd); cmd == "" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c.mu.Lock()
|
|
|
|
delete(c.handlers, cmd)
|
|
|
|
c.mu.Unlock()
|
|
|
|
}
|
|
|
|
|
|
|
|
// ClearAll removes all currently setup and re-sets the default handlers.
|
|
|
|
func (c *CTCP) ClearAll() {
|
|
|
|
c.mu.Lock()
|
|
|
|
c.handlers = map[string]CTCPHandler{}
|
|
|
|
c.mu.Unlock()
|
|
|
|
|
|
|
|
// Register necessary handlers.
|
|
|
|
c.addDefaultHandlers()
|
|
|
|
}
|
|
|
|
|
|
|
|
// CTCPHandler is a type that represents the function necessary to
|
|
|
|
// implement a CTCP handler.
|
|
|
|
type CTCPHandler func(client *Client, ctcp CTCPEvent)
|
|
|
|
|
|
|
|
// addDefaultHandlers adds some useful default CTCP response handlers.
|
|
|
|
func (c *CTCP) addDefaultHandlers() {
|
|
|
|
c.SetBg(CTCP_PING, handleCTCPPing)
|
|
|
|
c.SetBg(CTCP_PONG, handleCTCPPong)
|
|
|
|
c.SetBg(CTCP_VERSION, handleCTCPVersion)
|
|
|
|
c.SetBg(CTCP_SOURCE, handleCTCPSource)
|
|
|
|
c.SetBg(CTCP_TIME, handleCTCPTime)
|
|
|
|
c.SetBg(CTCP_FINGER, handleCTCPFinger)
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleCTCPPing replies with a ping and whatever was originally requested.
|
|
|
|
func handleCTCPPing(client *Client, ctcp CTCPEvent) {
|
|
|
|
if ctcp.Reply {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_PING, ctcp.Text)
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleCTCPPong replies with a pong.
|
|
|
|
func handleCTCPPong(client *Client, ctcp CTCPEvent) {
|
|
|
|
if ctcp.Reply {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_PONG, "")
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleCTCPVersion replies with the name of the client, Go version, as well
|
|
|
|
// as the os type (darwin, linux, windows, etc) and architecture type (x86,
|
|
|
|
// arm, etc).
|
|
|
|
func handleCTCPVersion(client *Client, ctcp CTCPEvent) {
|
|
|
|
if client.Config.Version != "" {
|
|
|
|
client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_VERSION, client.Config.Version)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
client.Cmd.SendCTCPReplyf(
|
|
|
|
ctcp.Source.Name, CTCP_VERSION,
|
|
|
|
"girc (github.com/lrstanley/girc) using %s (%s, %s)",
|
|
|
|
runtime.Version(), runtime.GOOS, runtime.GOARCH,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleCTCPSource replies with the public git location of this library.
|
|
|
|
func handleCTCPSource(client *Client, ctcp CTCPEvent) {
|
|
|
|
client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_SOURCE, "https://github.com/lrstanley/girc")
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleCTCPTime replies with a RFC 1123 (Z) formatted version of Go's
|
|
|
|
// local time.
|
|
|
|
func handleCTCPTime(client *Client, ctcp CTCPEvent) {
|
|
|
|
client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_TIME, ":"+time.Now().Format(time.RFC1123Z))
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleCTCPFinger replies with the realname and idle time of the user. This
|
|
|
|
// is obsoleted by improvements to the IRC protocol, however still supported.
|
|
|
|
func handleCTCPFinger(client *Client, ctcp CTCPEvent) {
|
|
|
|
client.conn.mu.RLock()
|
|
|
|
active := client.conn.lastActive
|
|
|
|
client.conn.mu.RUnlock()
|
|
|
|
|
|
|
|
client.Cmd.SendCTCPReply(ctcp.Source.Name, CTCP_FINGER, fmt.Sprintf("%s -- idle %s", client.Config.Name, time.Since(active)))
|
|
|
|
}
|