mirror of
				https://github.com/fluffle/goirc
				synced 2025-11-04 03:58:03 +00:00 
			
		
		
		
	Add support for complex regex commands.
Refactor commands so they are just handlers. Add commands that listen for YouTube/Webpage mentions and print information (Video Info/Title). Add simple command examples in client.go TODO: Use a better data store for the commands, perhaps a linked list like handlers.
This commit is contained in:
		
							parent
							
								
									18a149335b
								
							
						
					
					
						commit
						89c23a7787
					
				
					 5 changed files with 143 additions and 112 deletions
				
			
		| 
						 | 
				
			
			@ -22,7 +22,7 @@ type Conn struct {
 | 
			
		|||
 | 
			
		||||
	// Handlers and Commands
 | 
			
		||||
	handlers *hSet
 | 
			
		||||
	commands *cSet
 | 
			
		||||
	commands *commandSet
 | 
			
		||||
 | 
			
		||||
	// State tracker for nicks and channels
 | 
			
		||||
	ST         state.StateTracker
 | 
			
		||||
| 
						 | 
				
			
			@ -54,9 +54,6 @@ type Conn struct {
 | 
			
		|||
	// Client->server ping frequency, in seconds. Defaults to 3m.
 | 
			
		||||
	PingFreq time.Duration
 | 
			
		||||
 | 
			
		||||
	// Controls what is stripped from line.Args[1] for Commands
 | 
			
		||||
	CommandStripNick, CommandStripPrefix bool
 | 
			
		||||
 | 
			
		||||
	// Set this to true to disable flood protection and false to re-enable
 | 
			
		||||
	Flood bool
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -85,7 +82,7 @@ func Client(nick string, args ...string) *Conn {
 | 
			
		|||
		cLoop:      make(chan bool),
 | 
			
		||||
		cPing:      make(chan bool),
 | 
			
		||||
		handlers:   handlerSet(),
 | 
			
		||||
		commands:   commandSet(),
 | 
			
		||||
		commands:   newCommandSet(),
 | 
			
		||||
		stRemovers: make([]Remover, 0, len(stHandlers)),
 | 
			
		||||
		PingFreq:   3 * time.Minute,
 | 
			
		||||
		NewNick:    func(s string) string { return s + "_" },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,10 @@
 | 
			
		|||
package client
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/fluffle/golog/logging"
 | 
			
		||||
	"math"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -16,6 +19,12 @@ type Remover interface {
 | 
			
		|||
	Remove()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type RemoverFunc func()
 | 
			
		||||
 | 
			
		||||
func (r RemoverFunc) Remove() {
 | 
			
		||||
	r()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type HandlerFunc func(*Conn, *Line)
 | 
			
		||||
 | 
			
		||||
func (hf HandlerFunc) Handle(conn *Conn, line *Line) {
 | 
			
		||||
| 
						 | 
				
			
			@ -113,87 +122,76 @@ func (hs *hSet) dispatch(conn *Conn, line *Line) {
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// An IRC command looks like this:
 | 
			
		||||
type Command interface {
 | 
			
		||||
	Execute(*Conn, *Line)
 | 
			
		||||
	Help() string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type command struct {
 | 
			
		||||
	fn   HandlerFunc
 | 
			
		||||
	help string
 | 
			
		||||
	handler  Handler
 | 
			
		||||
	set      *commandSet
 | 
			
		||||
	regex    string
 | 
			
		||||
	priority int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *command) Execute(conn *Conn, line *Line) {
 | 
			
		||||
	c.fn(conn, line)
 | 
			
		||||
func (c *command) Handle(conn *Conn, line *Line) {
 | 
			
		||||
	c.handler.Handle(conn, line)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *command) Help() string {
 | 
			
		||||
	return c.help
 | 
			
		||||
func (c *command) Remove() {
 | 
			
		||||
	c.set.remove(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type cNode struct {
 | 
			
		||||
	cmd    Command
 | 
			
		||||
	set    *cSet
 | 
			
		||||
	prefix string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cn *cNode) Execute(conn *Conn, line *Line) {
 | 
			
		||||
	cn.cmd.Execute(conn, line)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cn *cNode) Help() string {
 | 
			
		||||
	return cn.cmd.Help()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cn *cNode) Remove() {
 | 
			
		||||
	cn.set.remove(cn)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type cSet struct {
 | 
			
		||||
	set map[string]*cNode
 | 
			
		||||
type commandSet struct {
 | 
			
		||||
	set []*command
 | 
			
		||||
	sync.RWMutex
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func commandSet() *cSet {
 | 
			
		||||
	return &cSet{set: make(map[string]*cNode)}
 | 
			
		||||
func newCommandSet() *commandSet {
 | 
			
		||||
	return &commandSet{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cs *cSet) add(pf string, c Command) Remover {
 | 
			
		||||
func (cs *commandSet) add(regex string, handler Handler, priority int) Remover {
 | 
			
		||||
	cs.Lock()
 | 
			
		||||
	defer cs.Unlock()
 | 
			
		||||
	pf = strings.ToLower(pf)
 | 
			
		||||
	if _, ok := cs.set[pf]; ok {
 | 
			
		||||
		logging.Error("Command prefix '%s' already registered.", pf)
 | 
			
		||||
		return nil
 | 
			
		||||
	c := &command{
 | 
			
		||||
		handler:  handler,
 | 
			
		||||
		set:      cs,
 | 
			
		||||
		regex:    regex,
 | 
			
		||||
		priority: priority,
 | 
			
		||||
	}
 | 
			
		||||
	cn := &cNode{
 | 
			
		||||
		cmd:    c,
 | 
			
		||||
		set:    cs,
 | 
			
		||||
		prefix: pf,
 | 
			
		||||
	// Check for exact regex matches. This will filter out any repeated SimpleCommands.
 | 
			
		||||
	for _, c := range cs.set {
 | 
			
		||||
		if c.regex == regex {
 | 
			
		||||
			logging.Error("Command prefix '%s' already registered.", regex)
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	cs.set[pf] = cn
 | 
			
		||||
	return cn
 | 
			
		||||
	cs.set = append(cs.set, c)
 | 
			
		||||
	return c
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cs *cSet) remove(cn *cNode) {
 | 
			
		||||
func (cs *commandSet) remove(c *command) {
 | 
			
		||||
	cs.Lock()
 | 
			
		||||
	defer cs.Unlock()
 | 
			
		||||
	delete(cs.set, cn.prefix)
 | 
			
		||||
	cn.set = nil
 | 
			
		||||
	for index, value := range cs.set {
 | 
			
		||||
		if value == c {
 | 
			
		||||
			copy(cs.set[index:], cs.set[index+1:])
 | 
			
		||||
			cs.set = cs.set[:len(cs.set)-1]
 | 
			
		||||
			c.set = nil
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cs *cSet) match(txt string) (final Command, prefixlen int) {
 | 
			
		||||
// Matches the command with the highest priority.
 | 
			
		||||
func (cs *commandSet) match(txt string) (handler Handler) {
 | 
			
		||||
	cs.RLock()
 | 
			
		||||
	defer cs.RUnlock()
 | 
			
		||||
	txt = strings.ToLower(txt)
 | 
			
		||||
	for prefix, cmd := range cs.set {
 | 
			
		||||
		if !strings.HasPrefix(txt, prefix) {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if final == nil || len(prefix) > prefixlen {
 | 
			
		||||
			prefixlen = len(prefix)
 | 
			
		||||
			final = cmd
 | 
			
		||||
	maxPriority := math.MinInt32
 | 
			
		||||
	for _, c := range cs.set {
 | 
			
		||||
		if c.priority > maxPriority {
 | 
			
		||||
			if regex, error := regexp.Compile(c.regex); error == nil {
 | 
			
		||||
				if regex.MatchString(txt) {
 | 
			
		||||
					maxPriority = c.priority
 | 
			
		||||
					handler = c.handler
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
| 
						 | 
				
			
			@ -213,18 +211,55 @@ func (conn *Conn) HandleFunc(name string, hf HandlerFunc) Remover {
 | 
			
		|||
	return conn.Handle(name, hf)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (conn *Conn) Command(prefix string, c Command) Remover {
 | 
			
		||||
	return conn.commands.add(prefix, c)
 | 
			
		||||
func (conn *Conn) Command(regex string, handler Handler, priority int) Remover {
 | 
			
		||||
	return conn.commands.add(regex, handler, priority)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (conn *Conn) CommandFunc(prefix string, hf HandlerFunc, help string) Remover {
 | 
			
		||||
	return conn.Command(prefix, &command{hf, help})
 | 
			
		||||
func (conn *Conn) CommandFunc(regex string, handlerFunc HandlerFunc, priority int) Remover {
 | 
			
		||||
	return conn.Command(regex, handlerFunc, priority)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var SimpleCommandRegex string = `^!%v(\s|$)`
 | 
			
		||||
 | 
			
		||||
// Simple commands are commands that are triggered from a simple prefix
 | 
			
		||||
// SimpleCommand("roll" handler)
 | 
			
		||||
// !roll
 | 
			
		||||
// Because simple commands are simple, they get the highest priority.
 | 
			
		||||
func (conn *Conn) SimpleCommand(prefix string, handler Handler) Remover {
 | 
			
		||||
	return conn.Command(fmt.Sprintf(SimpleCommandRegex, strings.ToLower(prefix)), handler, math.MaxInt32)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (conn *Conn) SimpleCommandFunc(prefix string, handlerFunc HandlerFunc) Remover {
 | 
			
		||||
	return conn.SimpleCommand(prefix, handlerFunc)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// This will also register a help command to go along with the simple command itself.
 | 
			
		||||
// eg. SimpleCommandHelp("bark", "Bot will bark", handler) will make the following commands:
 | 
			
		||||
// !bark
 | 
			
		||||
// !help bark
 | 
			
		||||
func (conn *Conn) SimpleCommandHelp(prefix string, help string, handler Handler) Remover {
 | 
			
		||||
	commandCommand := conn.SimpleCommand(prefix, handler)
 | 
			
		||||
	helpCommand := conn.SimpleCommandFunc(fmt.Sprintf("help %v", prefix), HandlerFunc(func(conn *Conn, line *Line) {
 | 
			
		||||
		conn.Privmsg(line.Target(), help)
 | 
			
		||||
	}))
 | 
			
		||||
	return RemoverFunc(func() {
 | 
			
		||||
		commandCommand.Remove()
 | 
			
		||||
		helpCommand.Remove()
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (conn *Conn) SimpleCommandHelpFunc(prefix string, help string, handlerFunc HandlerFunc) Remover {
 | 
			
		||||
	return conn.SimpleCommandHelp(prefix, help, handlerFunc)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (conn *Conn) dispatch(line *Line) {
 | 
			
		||||
	conn.handlers.dispatch(conn, line)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (conn *Conn) cmdMatch(txt string) (Command, int) {
 | 
			
		||||
	return conn.commands.match(txt)
 | 
			
		||||
func (conn *Conn) command(line *Line) {
 | 
			
		||||
	command := conn.commands.match(strings.ToLower(line.Message()))
 | 
			
		||||
	if command != nil {
 | 
			
		||||
		command.Handle(conn, line)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,9 +29,11 @@ type youTubeVideo struct {
 | 
			
		|||
	} `json:entry`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var UrlRegex string = `(\s|^)(http://|https://)(.*?)(\s|$)`
 | 
			
		||||
 | 
			
		||||
func UrlFunc(conn *Conn, line *Line) {
 | 
			
		||||
	text := line.Message()
 | 
			
		||||
	if regex, err := regexp.Compile(`(\s|^)(http://|https://)(.*?)(\s|$)`); err == nil {
 | 
			
		||||
	if regex, err := regexp.Compile(UrlRegex); err == nil {
 | 
			
		||||
		url := strings.TrimSpace(regex.FindString(text))
 | 
			
		||||
		if url != "" {
 | 
			
		||||
			if resp, err := http.Get(url); err == nil {
 | 
			
		||||
| 
						 | 
				
			
			@ -50,9 +52,11 @@ func UrlFunc(conn *Conn, line *Line) {
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var YouTubeRegex string = `(\s|^)(http://|https://)?(www.)?(youtube.com/watch\?v=|youtu.be/)(.*?)(\s|$|\&|#)`
 | 
			
		||||
 | 
			
		||||
func YouTubeFunc(conn *Conn, line *Line) {
 | 
			
		||||
	text := line.Message()
 | 
			
		||||
	if regex, err := regexp.Compile(`(\s|^)(http://|https://)?(www.)?(youtube.com/watch\?v=|youtu.be/)(.*?)(\s|$|\&|#)`); err == nil {
 | 
			
		||||
	if regex, err := regexp.Compile(YouTubeRegex); err == nil {
 | 
			
		||||
		if regex.Match([]byte(text)) {
 | 
			
		||||
			matches := regex.FindStringSubmatch(text)
 | 
			
		||||
			id := matches[len(matches)-2]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,12 +9,13 @@ import (
 | 
			
		|||
 | 
			
		||||
// sets up the internal event handlers to do essential IRC protocol things
 | 
			
		||||
var intHandlers = map[string]HandlerFunc{
 | 
			
		||||
	INIT:  (*Conn).h_init,
 | 
			
		||||
	"001": (*Conn).h_001,
 | 
			
		||||
	"433": (*Conn).h_433,
 | 
			
		||||
	CTCP:  (*Conn).h_CTCP,
 | 
			
		||||
	NICK:  (*Conn).h_NICK,
 | 
			
		||||
	PING:  (*Conn).h_PING,
 | 
			
		||||
	INIT:    (*Conn).h_init,
 | 
			
		||||
	"001":   (*Conn).h_001,
 | 
			
		||||
	"433":   (*Conn).h_433,
 | 
			
		||||
	CTCP:    (*Conn).h_CTCP,
 | 
			
		||||
	NICK:    (*Conn).h_NICK,
 | 
			
		||||
	PING:    (*Conn).h_PING,
 | 
			
		||||
	PRIVMSG: (*Conn).h_PRIVMSG,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (conn *Conn) addIntHandlers() {
 | 
			
		||||
| 
						 | 
				
			
			@ -96,34 +97,5 @@ func (conn *Conn) h_NICK(line *Line) {
 | 
			
		|||
 | 
			
		||||
// Handle PRIVMSGs that trigger Commands
 | 
			
		||||
func (conn *Conn) h_PRIVMSG(line *Line) {
 | 
			
		||||
	txt := line.Args[1]
 | 
			
		||||
	if conn.CommandStripNick && strings.HasPrefix(txt, conn.Me.Nick) {
 | 
			
		||||
		// Look for '^${nick}[:;>,-]? '
 | 
			
		||||
		l := len(conn.Me.Nick)
 | 
			
		||||
		switch txt[l] {
 | 
			
		||||
		case ':', ';', '>', ',', '-':
 | 
			
		||||
			l++
 | 
			
		||||
		}
 | 
			
		||||
		if txt[l] == ' ' {
 | 
			
		||||
			txt = strings.TrimSpace(txt[l:])
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	cmd, l := conn.cmdMatch(txt)
 | 
			
		||||
	if cmd == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if conn.CommandStripPrefix {
 | 
			
		||||
		txt = strings.TrimSpace(txt[l:])
 | 
			
		||||
	}
 | 
			
		||||
	if txt != line.Args[1] {
 | 
			
		||||
		line = line.Copy()
 | 
			
		||||
		line.Args[1] = txt
 | 
			
		||||
	}
 | 
			
		||||
	cmd.Execute(conn, line)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (conn *Conn) c_HELP(line *Line) {
 | 
			
		||||
	if cmd, _ := conn.cmdMatch(line.Args[1]); cmd != nil {
 | 
			
		||||
		conn.Privmsg(line.Args[0], cmd.Help())
 | 
			
		||||
	}
 | 
			
		||||
	conn.command(line)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue