summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/lrstanley/girc/ctcp.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/lrstanley/girc/ctcp.go')
-rw-r--r--vendor/github.com/lrstanley/girc/ctcp.go288
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)))
+}