girc-atomic/ctcp.go

299 rindas
8.6 KiB
Go

// 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 (
"runtime"
"strings"
"sync"
"time"
cmap "github.com/orcaman/concurrent-map/v2"
)
// 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 have valid CTCP encoding.
func DecodeCTCP(e *Event) *CTCPEvent {
// http://www.irchelp.org/protocol/ctcpspec.html
if e == nil {
return nil
}
// 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 {
return nil
}
if e.Command != PRIVMSG && e.Command != NOTICE {
return nil
}
if e.Params[1][0] != ctcpDelim || e.Params[1][len(e.Params[1])-1] != ctcpDelim {
return nil
}
// Strip delimiters.
text := e.Params[1][1 : len(e.Params[1])-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.
if (text[i] < 'A' || text[i] > 'Z') && (text[i] < '0' || text[i] > '9') {
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.
if (text[i] < 'A' || text[i] > 'Z') && (text[i] < '0' || text[i] > '9') {
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 cmd == "" {
return cmd
}
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 cmap.ConcurrentMap[string, CTCPHandler]
}
// newCTCP returns a new clean CTCP handler.
func newCTCP() *CTCP {
return &CTCP{handlers: cmap.New[CTCPHandler]()}
}
// call executes the necessary CTCP handler for the incoming event/CTCP
// command.
func (c *CTCP) call(client *Client, event *CTCPEvent) {
// 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 val, ok := c.handlers.Get("*"); ok && val != nil {
val(client, *event)
}
val, ok := c.handlers.Get(event.Command)
if !ok || val == nil || event.Command == CTCP_ACTION {
return
}
val(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.
if (cmd[i] < 'A' || cmd[i] > 'Z') && (cmd[i] < '0' || cmd[i] > '9') {
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.handlers.Set(cmd, handler)
}
// 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.handlers.Remove(cmd)
}
// ClearAll removes all currently setup and re-sets the default handlers.
func (c *CTCP) ClearAll() {
c.handlers = cmap.New[CTCPHandler]()
// 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_USERINFO, handleCTCPUserInfo)
c.SetBg(CTCP_CLIENTINFO, handleCTCPClientInfo)
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.ID(), CTCP_PING, ctcp.Text)
}
// handleCTCPPong replies with a pong.
func handleCTCPPong(client *Client, ctcp CTCPEvent) {
if ctcp.Reply {
return
}
client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_PONG, "")
}
// handleCTCPVersion replies with the name of the client, Go version, as well
// as the os type if not overridden by client configuration.
func handleCTCPVersion(client *Client, ctcp CTCPEvent) {
if client.Config.Version != "" {
client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_VERSION, client.Config.Version)
return
}
client.Cmd.SendCTCPReplyf(
ctcp.Source.ID(), CTCP_VERSION,
"girc-atomic %s (%s, %s)",
Version, runtime.GOOS, runtime.GOARCH,
)
}
// handleCTCPUserInfo replies with the configured user information if available, otherwise it does not reply.
func handleCTCPUserInfo(client *Client, ctcp CTCPEvent) {
if client.Config.UserInfo == "" {
return
}
client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_USERINFO, client.Config.UserInfo)
}
// handleCTCPClientInfo replies with the configured client information if available, otherwise it does not reply.
func handleCTCPClientInfo(client *Client, ctcp CTCPEvent) {
if client.Config.UserInfo == "" {
return
}
client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_USERINFO, client.Config.UserInfo)
}
// handleCTCPUserInfo replies with the public git location of this library.
func handleCTCPSource(client *Client, ctcp CTCPEvent) {
if client.Config.Source != "" {
client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_SOURCE, client.Config.Source)
return
}
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) {
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) {
if client.Config.Finger != "" {
client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_FINGER, client.Config.Finger)
return
}
// irssi doesn't appear to do this on a stock install so gonna just go ahead and nix it.
// active := client.conn.lastActive.Load().(time.Time)
// client.Cmd.SendCTCPReply(ctcp.Source.ID(), CTCP_FINGER, fmt.Sprintf("%s -- idle %s", client.Config.Name, time.Since(active)))
}