diff options
Diffstat (limited to 'vendor/github.com/lrstanley/girc/ctcp.go')
-rw-r--r-- | vendor/github.com/lrstanley/girc/ctcp.go | 288 |
1 files changed, 288 insertions, 0 deletions
diff --git a/vendor/github.com/lrstanley/girc/ctcp.go b/vendor/github.com/lrstanley/girc/ctcp.go new file mode 100644 index 00000000..c6c0bace --- /dev/null +++ b/vendor/github.com/lrstanley/girc/ctcp.go @@ -0,0 +1,288 @@ +// 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. + if (text[i] < 0x41 || text[i] > 0x5A) && (text[i] < 0x30 || text[i] > 0x39) { + 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] < 0x41 || text[i] > 0x5A) && (text[i] < 0x30 || text[i] > 0x39) { + 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. + if (cmd[i] < 0x41 || cmd[i] > 0x5A) && (cmd[i] < 0x30 || cmd[i] > 0x39) { + 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))) +} |