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
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)
|
|
}
|
|
|
|
}
|