1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
|
package bot
import (
"errors"
"log"
"strings"
"sync"
"time"
"golang.org/x/time/rate"
"github.com/bwmarrin/discordgo"
)
var (
ErrRateLimitExceeded = errors.New("bot: per-command rate limit exceeded")
)
type command struct {
aliases []string
verb string
helptext string
}
func (c *command) Verb() string {
return c.verb
}
func (c *command) Helptext() string {
return c.helptext
}
// Handler is the type that bot command functions need to implement. Errors
// should be returned.
type Handler func(*discordgo.Session, *discordgo.Message, []string) error
// CommandHandler is a generic interface for types that implement a bot
// command. It is akin to http.Handler, but more comprehensive.
type CommandHandler interface {
Verb() string
Helptext() string
Handler(*discordgo.Session, *discordgo.Message, []string) error
Permissions(*discordgo.Session, *discordgo.Message, []string) error
}
type basicCommand struct {
*command
handler Handler
permissions Handler
limiter *rate.Limiter
}
func (bc *basicCommand) Handler(s *discordgo.Session, m *discordgo.Message, parv []string) error {
return bc.handler(s, m, parv)
}
func (bc *basicCommand) Permissions(s *discordgo.Session, m *discordgo.Message, parv []string) error {
if !bc.limiter.Allow() {
return ErrRateLimitExceeded
}
return bc.permissions(s, m, parv)
}
// The "default" command set, useful for simple bot projects.
var (
DefaultCommandSet = NewCommandSet()
)
// Command handling errors.
var (
ErrAlreadyExists = errors.New("bot: command already exists")
ErrNoSuchCommand = errors.New("bot: no such command exists")
ErrNoPermissions = errors.New("bot: you do not have permissions for this command")
ErrParvCountMismatch = errors.New("bot: parameter count mismatch")
)
// The default command prefix. Command `foo` becomes `.foo` in chat, etc.
const (
DefaultPrefix = "."
)
// NewCommand creates an anonymous command and adds it to the default CommandSet.
func NewCommand(verb, helptext string, handler, permissions Handler) error {
return DefaultCommandSet.Add(NewBasicCommand(verb, helptext, handler, permissions))
}
// NewBasicCommand creates a CommandHandler instance using the implementation
// functions supplied as arguments.
func NewBasicCommand(verb, helptext string, permissions, handler Handler) CommandHandler {
return &basicCommand{
command: &command{
verb: verb,
helptext: helptext,
},
handler: handler,
permissions: permissions,
limiter: rate.NewLimiter(rate.Every(5*time.Second), 1),
}
}
// CommandSet is a group of bot commands similar to an http.ServeMux.
type CommandSet struct {
sync.Mutex
cmds map[string]CommandHandler
Prefix string
}
// NewCommandSet creates a new command set with the `help` command pre-loaded.
func NewCommandSet() *CommandSet {
cs := &CommandSet{
cmds: map[string]CommandHandler{},
Prefix: DefaultPrefix,
}
cs.AddCmd("help", "Shows help for the bot", NoPermissions, cs.help)
return cs
}
// NoPermissions is a simple middelware function that allows all command invocations
// to pass the permissions check.
func NoPermissions(s *discordgo.Session, m *discordgo.Message, parv []string) error {
return nil
}
// AddCmd is syntactic sugar for cs.Add(NewBasicCommand(args...))
func (cs *CommandSet) AddCmd(verb, helptext string, permissions, handler Handler) error {
return cs.Add(NewBasicCommand(verb, helptext, permissions, handler))
}
// Add adds a single command handler to the CommandSet. This can be done at runtime
// but it is suggested to only add commands on application boot.
func (cs *CommandSet) Add(h CommandHandler) error {
cs.Lock()
defer cs.Unlock()
v := strings.ToLower(h.Verb())
if _, ok := cs.cmds[v]; ok {
return ErrAlreadyExists
}
cs.cmds[v] = h
return nil
}
// Run makes a CommandSet compatible with discordgo event dispatching.
func (cs *CommandSet) Run(s *discordgo.Session, msg *discordgo.Message) error {
cs.Lock()
defer cs.Unlock()
if strings.HasPrefix(msg.Content, cs.Prefix) {
params := strings.Fields(msg.Content)
verb := strings.ToLower(params[0][1:])
cmd, ok := cs.cmds[verb]
if !ok {
return ErrNoSuchCommand
}
err := cmd.Permissions(s, msg, params)
if err != nil {
log.Printf("Permissions error: %s: %v", msg.Author.Username, err)
s.ChannelMessageSend(msg.ChannelID, "You don't have permissions for that, sorry.")
return ErrNoPermissions
}
err = cmd.Handler(s, msg, params)
if err != nil {
log.Printf("command handler error: %v", err)
s.ChannelMessageSend(msg.ChannelID, "error when running that command: "+err.Error())
return err
}
}
return nil
}
|