mirror of https://github.com/fluffle/goirc
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
35
client.go
35
client.go
|
@ -5,18 +5,23 @@ import (
|
|||
"flag"
|
||||
"fmt"
|
||||
irc "github.com/fluffle/goirc/client"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var host *string = flag.String("host", "irc.freenode.net", "IRC server")
|
||||
var host *string = flag.String("host", "irc.synirc.net", "IRC server")
|
||||
var channel *string = flag.String("channel", "#go-nuts", "IRC channel")
|
||||
var nick *string = flag.String("nick", "Septapus", "Nick")
|
||||
var ident *string = flag.String("ident", "Septapus", "Ident")
|
||||
var name *string = flag.String("name", "Septapus v9", "Name")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
// create new IRC connection
|
||||
c := irc.Client("GoTest", "gotest")
|
||||
c := irc.Client(*nick, *ident, *name)
|
||||
c.EnableStateTracking()
|
||||
c.HandleFunc(irc.CONNECTED,
|
||||
func(conn *irc.Conn, line *irc.Line) { conn.Join(*channel) })
|
||||
|
@ -26,8 +31,27 @@ func main() {
|
|||
c.HandleFunc(irc.DISCONNECTED,
|
||||
func(conn *irc.Conn, line *irc.Line) { quit <- true })
|
||||
|
||||
c.HandleFunc(irc.PRIVMSG, YouTubeFunc)
|
||||
c.HandleFunc(irc.PRIVMSG, UrlFunc)
|
||||
// Set up some simple commands, !bark and !roll.
|
||||
c.SimpleCommandFunc("bark", func(conn *irc.Conn, line *irc.Line) { conn.Privmsg(line.Target(), "Woof Woof") })
|
||||
c.SimpleCommandHelpFunc("roll", `Rolls a d6, "roll <n>" to roll n dice at once.`, func(conn *irc.Conn, line *irc.Line) {
|
||||
count := 1
|
||||
fields := strings.Fields(line.Message())
|
||||
if len(fields) > 1 {
|
||||
var err error
|
||||
if count, err = strconv.Atoi(fields[len(fields)-1]); err != nil {
|
||||
count = 1
|
||||
}
|
||||
}
|
||||
total := 0
|
||||
for i := 0; i < count; i++ {
|
||||
total += rand.Intn(6) + 1
|
||||
}
|
||||
conn.Privmsg(line.Target(), fmt.Sprintf("%d", total))
|
||||
})
|
||||
|
||||
// Set up some commands that are triggered by a regex in a message.
|
||||
c.CommandFunc(irc.YouTubeRegex, irc.YouTubeFunc, 10)
|
||||
c.CommandFunc(irc.UrlRegex, irc.UrlFunc, 0)
|
||||
|
||||
// set up a goroutine to read commands from stdin
|
||||
in := make(chan string, 4)
|
||||
|
@ -87,10 +111,9 @@ func main() {
|
|||
for !reallyquit {
|
||||
// connect to server
|
||||
if err := c.Connect(*host); err != nil {
|
||||
fmt.Printf("Connection error: %s\n", err)
|
||||
fmt.Printf("Error %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// wait on quit channel
|
||||
<-quit
|
||||
}
|
||||
|
|
|
@ -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…
Reference in New Issue