You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

196 lines
5.2 KiB

// Package mux provides a simple Discord message route multiplexer that
// parses messages and then executes a matching registered handler, if found.
// mux can be used with both Disgord and the DiscordGo library.
package mux
import (
"fmt"
"log"
"regexp"
"strings"
"github.com/bwmarrin/discordgo"
)
// Route holds information about a specific message route handler
type Route struct {
Pattern string // match pattern that should trigger this route handler
Description string // short description of this route
Help string // detailed help string for this route
Run HandlerFunc // route handler function to call
}
// Context holds a bit of extra data we pass along to route handlers
// This way processing some of this only needs to happen once.
type Context struct {
Fields []string
Content string
IsDirected bool
IsPrivate bool
HasPrefix bool
HasMention bool
HasMentionFirst bool
}
// HandlerFunc is the function signature required for a message route handler.
type HandlerFunc func(*discordgo.Session, *discordgo.Message, *Context)
// Mux is the main struct for all mux methods.
type Mux struct {
Routes []*Route
Default *Route
Prefix string
}
// New returns a new Discord message route mux
func New() *Mux {
m := &Mux{}
m.Prefix = "!"
return m
}
// Route allows you to register a route
func (m *Mux) Route(pattern, desc string, cb HandlerFunc) (*Route, error) {
r := Route{}
r.Pattern = pattern
r.Description = desc
r.Run = cb
m.Routes = append(m.Routes, &r)
return &r, nil
}
// FuzzyMatch attempts to find the best route match for a given message.
func (m *Mux) FuzzyMatch(msg string) (*Route, []string) {
// Tokenize the msg string into a slice of words
fields := strings.Fields(msg)
// no point to continue if there's no fields
if len(fields) == 0 {
return nil, nil
}
// Search though the command list for a match
var r *Route
var rank int
var fk int
for fk, fv := range fields {
for _, rv := range m.Routes {
// If we find an exact match, return that immediately.
if rv.Pattern == strings.ToLower(fv) {
return rv, fields[fk:]
}
// Some "Fuzzy" searching...
if strings.HasPrefix(rv.Pattern, strings.ToLower(fv)) {
if len(fv) > rank {
r = rv
rank = len(fv)
}
}
}
}
return r, fields[fk:]
}
// OnMessageCreate is a DiscordGo Event Handler function. This must be
// registered using the DiscordGo.Session.AddHandler function. This function
// will receive all Discord messages and parse them for matches to registered
// routes.
func (m *Mux) OnMessageCreate(ds *discordgo.Session, mc *discordgo.MessageCreate) {
var err error
// Ignore all messages created by the Bot account itself
if mc.Author.ID == ds.State.User.ID {
return
}
// Create Context struct that we can put various infos into
ctx := &Context{
Content: strings.TrimSpace(mc.Content),
}
// Fetch the channel for this Message
var c *discordgo.Channel
c, err = ds.State.Channel(mc.ChannelID)
if err != nil {
// Try fetching via REST API
c, err = ds.Channel(mc.ChannelID)
if err != nil {
log.Printf("unable to fetch Channel for Message, %s", err)
} else {
// Attempt to add this channel into our State
err = ds.State.ChannelAdd(c)
if err != nil {
log.Printf("error updating State with Channel, %s", err)
}
}
}
// Add Channel info into Context (if we successfully got the channel)
if c != nil {
if c.Type == discordgo.ChannelTypeDM {
ctx.IsPrivate, ctx.IsDirected = true, true
}
}
// Detect @name or @nick mentions
if !ctx.IsDirected {
// Detect if Bot was @mentioned
for _, v := range mc.Mentions {
if v.ID == ds.State.User.ID {
ctx.IsDirected, ctx.HasMention = true, true
reg := regexp.MustCompile(fmt.Sprintf("<@!?(%s)>", ds.State.User.ID))
// Was the @mention the first part of the string?
if len(reg.FindStringIndex(ctx.Content)) > 0 && reg.FindStringIndex(ctx.Content)[0] == 0 {
ctx.HasMentionFirst = true
}
// strip bot mention tags from content string
ctx.Content = reg.ReplaceAllString(ctx.Content, "")
break
}
}
}
// Detect prefix mention
if !ctx.IsDirected && len(m.Prefix) > 0 {
// TODO : Must be changed to support a per-guild user defined prefix
if strings.HasPrefix(ctx.Content, m.Prefix) {
ctx.IsDirected, ctx.HasPrefix, ctx.HasMentionFirst = true, true, true
ctx.Content = strings.TrimPrefix(ctx.Content, m.Prefix)
}
}
// For now, if we're not specifically mentioned we do nothing.
// later I might add an option for global non-mentioned command words
if !ctx.IsDirected {
return
}
// Try to find the "best match" command out of the message.
r, fl := m.FuzzyMatch(ctx.Content)
if r != nil {
ctx.Fields = fl
r.Run(ds, mc.Message, ctx)
return
}
// If no command match was found, call the default.
// Ignore if only @mentioned in the middle of a message
if m.Default != nil && (ctx.HasMentionFirst) {
// TODO: This could use a ratelimit
// or should the ratelimit be inside the cmd handler?..
// In the case of "talking" to another bot, this can create an endless
// loop. Probably most common in private messages.
m.Default.Run(ds, mc.Message, ctx)
}
}