girc-atomic/ctcp.go

295 lines
8.0 KiB
Go
Raw Normal View History

2017-02-06 07:45:31 +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.
2016-12-24 05:15:41 +00:00
package girc
import (
"fmt"
"runtime"
2016-12-24 05:15:41 +00:00
"strings"
"sync"
"time"
2016-12-24 05:15:41 +00:00
)
// ctcpDelim if the delimiter used for CTCP formatted events/messages.
2016-12-24 05:15:41 +00:00
const ctcpDelim byte = 0x01 // Prefix and suffix for CTCP messages.
// CTCPEvent is the necessary information from an IRC message.
2016-12-24 05:15:41 +00:00
type CTCPEvent struct {
// Origin is the original event that the CTCP event was decoded from.
2017-08-08 09:45:45 +00:00
Origin *Event `json:"origin"`
// Source is the author of the CTCP event.
2017-08-08 09:45:45 +00:00
Source *Source `json:"source"`
// Command is the type of CTCP event. E.g. PING, TIME, VERSION.
2017-08-08 09:45:45 +00:00
Command string `json:"command"`
// Text is the raw arguments following the command.
2017-08-08 09:45:45 +00:00
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).
2017-08-08 09:45:45 +00:00
Reply bool `json:"reply"`
2016-12-24 05:15:41 +00:00
}
// DecodeCTCP decodes an incoming CTCP event, if it is CTCP. nil is returned
// if the incoming event does not have valid CTCP encoding.
func DecodeCTCP(e *Event) *CTCPEvent {
// http://www.irchelp.org/protocol/ctcpspec.html
if e == nil {
return nil
}
2016-12-30 13:54:59 +00:00
// Must be targeting a user/channel, AND trailing must have
// DELIM+TAG+DELIM minimum (at least 3 chars).
if len(e.Params) != 2 || len(e.Params[1]) < 3 {
2016-12-24 05:15:41 +00:00
return nil
}
if e.Command != PRIVMSG && e.Command != NOTICE {
2016-12-24 05:15:41 +00:00
return nil
}
if e.Params[1][0] != ctcpDelim || e.Params[1][len(e.Params[1])-1] != ctcpDelim {
2016-12-24 05:15:41 +00:00
return nil
}
// Strip delimiters.
text := e.Params[1][1 : len(e.Params[1])-1]
2016-12-24 05:15:41 +00:00
2017-01-03 16:02:24 +00:00
s := strings.IndexByte(text, eventSpace)
2016-12-24 05:15:41 +00:00
// 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-11-30 15:23:52 +00:00
if (text[i] < 'A' || text[i] > 'Z') && (text[i] < '0' || text[i] > '9') {
2016-12-24 05:15:41 +00:00
return nil
}
}
return &CTCPEvent{
Origin: e,
Source: e.Source,
Command: text,
2017-07-12 05:06:20 +00:00
Reply: e.Command == NOTICE,
}
2016-12-24 05:15:41 +00:00
}
// Loop through checking the tag first.
for i := 0; i < s; i++ {
// Check for A-Z, 0-9.
2017-11-30 15:23:52 +00:00
if (text[i] < 'A' || text[i] > 'Z') && (text[i] < '0' || text[i] > '9') {
2016-12-24 05:15:41 +00:00
return nil
}
}
return &CTCPEvent{
Origin: e,
Source: e.Source,
Command: text[0:s],
2016-12-30 13:54:19 +00:00
Text: text[s+1:],
2017-07-12 05:06:20 +00:00
Reply: e.Command == NOTICE,
}
2016-12-24 05:15:41 +00:00
}
// EncodeCTCP encodes a CTCP event into a string, including delimiters.
func EncodeCTCP(ctcp *CTCPEvent) (out string) {
2016-12-24 05:15:41 +00:00
if ctcp == nil {
return ""
}
return EncodeCTCPRaw(ctcp.Command, ctcp.Text)
2016-12-24 05:15:41 +00:00
}
// EncodeCTCPRaw is much like EncodeCTCP, however accepts a raw command and
// string as input.
func EncodeCTCPRaw(cmd, text string) (out string) {
2016-12-24 05:15:41 +00:00
if len(cmd) <= 0 {
return ""
}
out = string(ctcpDelim) + cmd
if len(text) > 0 {
2017-01-03 16:02:24 +00:00
out += string(eventSpace) + text
2016-12-24 05:15:41 +00:00
}
return out + string(ctcpDelim)
}
// CTCP handles the storage and execution of CTCP handlers against incoming
// CTCP events.
2016-12-24 05:15:41 +00:00
type CTCP struct {
2017-02-12 03:51:05 +00:00
// mu is the mutex that should be used when accessing any ctcp handlers.
2016-12-24 05:15:41 +00:00
mu sync.RWMutex
// handlers is a map of CTCP message -> functions.
handlers map[string]CTCPHandler
}
// newCTCP returns a new clean CTCP handler.
2016-12-24 05:15:41 +00:00
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) {
2016-12-24 05:15:41 +00:00
c.mu.RLock()
2016-12-26 09:36:09 +00:00
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)
}
2016-12-26 09:36:09 +00:00
// Support wildcard CTCP event handling. Gets executed first before
// regular event handlers.
if _, ok := c.handlers["*"]; ok {
c.handlers["*"](client, *event)
2016-12-26 09:36:09 +00:00
}
2016-12-26 09:36:09 +00:00
if _, ok := c.handlers[event.Command]; !ok {
// If ACTION, don't do anything.
if event.Command == CTCP_ACTION {
return
}
// Send a ERRMSG reply, if we know who sent it.
2018-12-10 00:23:41 +00:00
if event.Source != nil && IsValidNick(event.Source.ID()) {
client.Cmd.SendCTCPReply(event.Source.ID(), CTCP_ERRMSG, "that is an unknown CTCP query")
}
2016-12-24 05:15:41 +00:00
return
}
c.handlers[event.Command](client, *event)
2016-12-24 05:15:41 +00:00
}
// parseCMD parses a CTCP command/tag, ensuring it's valid. If not, an empty
// string is returned.
2016-12-24 05:15:41 +00:00
func (c *CTCP) parseCMD(cmd string) string {
2016-12-26 11:06:36 +00:00
// TODO: Needs proper testing.
// Check if wildcard.
if cmd == "*" {
return "*"
}
2016-12-24 05:15:41 +00:00
cmd = strings.ToUpper(cmd)
for i := 0; i < len(cmd); i++ {
// Check for A-Z, 0-9.
2017-11-30 15:23:52 +00:00
if (cmd[i] < 'A' || cmd[i] > 'Z') && (cmd[i] < '0' || cmd[i] > '9') {
2016-12-24 05:15:41 +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.
2016-12-26 09:36:09 +00:00
// 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)) {
2016-12-24 05:15:41 +00:00
if cmd = c.parseCMD(cmd); cmd == "" {
return
}
c.mu.Lock()
2021-10-01 13:15:34 +00:00
defer c.mu.Unlock()
2021-10-09 15:50:48 +00:00
c.handlers[cmd] = handler
2016-12-24 05:15:41 +00:00
}
// SetBg is much like Set, however the handler is executed in the background,
2016-12-26 09:36:09 +00:00
// 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.
2016-12-24 05:15:41 +00:00
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.
2016-12-24 05:15:41 +00:00
func (c *CTCP) ClearAll() {
c.mu.Lock()
c.handlers = map[string]CTCPHandler{}
c.mu.Unlock()
// Register necessary handlers.
c.addDefaultHandlers()
2016-12-24 05:15:41 +00:00
}
// CTCPHandler is a type that represents the function necessary to
// implement a CTCP handler.
type CTCPHandler func(client *Client, ctcp CTCPEvent)
2016-12-24 05:15:41 +00:00
// 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)
}
2016-12-24 05:15:41 +00:00
// handleCTCPPing replies with a ping and whatever was originally requested.
func handleCTCPPing(client *Client, ctcp CTCPEvent) {
if ctcp.Reply {
return
}
2018-12-10 00:23:41 +00:00
client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_PING, ctcp.Text)
2016-12-24 05:15:41 +00:00
}
// handleCTCPPong replies with a pong.
func handleCTCPPong(client *Client, ctcp CTCPEvent) {
if ctcp.Reply {
return
}
2018-12-10 00:23:41 +00:00
client.Cmd.SendCTCPReply(ctcp.Source.ID(), 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 != "" {
2018-12-10 00:23:41 +00:00
client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_VERSION, client.Config.Version)
return
}
2017-06-15 08:46:10 +00:00
client.Cmd.SendCTCPReplyf(
2018-12-10 00:23:41 +00:00
ctcp.Source.ID(), CTCP_VERSION,
2021-10-09 13:25:17 +00:00
"girc (github.com/yunginnanet/girc-atomic) 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) {
2021-10-09 13:25:17 +00:00
client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_SOURCE, "https://github.com/yunginnanet/girc-atomic")
}
// handleCTCPTime replies with a RFC 1123 (Z) formatted version of Go's
// local time.
func handleCTCPTime(client *Client, ctcp CTCPEvent) {
2018-12-10 00:23:41 +00:00
client.Cmd.SendCTCPReply(ctcp.Source.ID(), 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) {
2021-10-01 13:15:34 +00:00
active := client.conn.lastActive.Load().(time.Time)
2018-12-10 00:23:41 +00:00
client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_FINGER, fmt.Sprintf("%s -- idle %s", client.Config.Name, time.Since(active)))
}