Skip to content

Commit 074f1c8

Browse files
committed
commands: various tweaks and fixes
- Fixed being able to bypass cooldowns when receiving a lot of commands at the same time - Added guild scoped cooldowns (only applied to mentionrole for the moment) - Guild scoped cooldowns are respected in custom commands execCC and execAdmin - Fix possibility of clashing cooldowns with commands in different containers but same names
1 parent a7a94d2 commit 074f1c8

4 files changed

Lines changed: 159 additions & 41 deletions

File tree

commands/plugin_bot.go

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,29 @@ func YAGCommandMiddleware(inner dcmd.RunFunc) dcmd.RunFunc {
8484
data = data.WithContext(context.WithValue(data.Context(), CtxKeyMS, ms))
8585
}
8686

87+
// Lock the command for execution
88+
if !BlockingAddRunningCommand(data.Msg.GuildID, data.Msg.ChannelID, data.Msg.Author.ID, yc, time.Second*60) {
89+
if atomic.LoadInt32(shuttingDown) == 1 {
90+
return yc.Name + ": Bot is restarting, please try again in a couple seconds...", nil
91+
}
92+
93+
return yc.Name + ": Gave up trying to run command after 60 seconds waiting for your previous instance of this command to finish", nil
94+
}
95+
96+
defer removeRunningCommand(data.Msg.GuildID, data.Msg.ChannelID, data.Msg.Author.ID, yc)
97+
8798
// Check if the user can execute the command
8899
canExecute, resp, settings, err := yc.checkCanExecuteCommand(data, data.CS)
100+
if err != nil {
101+
yc.Logger(data).WithError(err).Error("An error occured while checking if we could run command")
102+
}
103+
89104
if resp != "" {
105+
if resp == ReasonCooldown {
106+
cdLeft, _ := yc.LongestCooldownLeft(data.ContainerChain, data.Msg.Author.ID, data.Msg.GuildID)
107+
return fmt.Sprintf("This command is on cooldown for another %d seconds", cdLeft), nil
108+
}
109+
90110
// yc.PostCommandExecuted(settings, data, "", errors.WithMessage(err, "checkCanExecuteCommand"))
91111
// m, err := common.BotSession.ChannelMessageSend(cState.ID(), resp)
92112
// go yc.deleteResponse([]*discordgo.Message{m})
@@ -103,17 +123,6 @@ func YAGCommandMiddleware(inner dcmd.RunFunc) dcmd.RunFunc {
103123

104124
data = data.WithContext(context.WithValue(data.Context(), CtxKeyCmdSettings, settings))
105125

106-
// Lock the command for execution
107-
if !BlockingAddRunningCommand(data.Msg.GuildID, data.Msg.ChannelID, data.Msg.Author.ID, yc, time.Second*60) {
108-
if atomic.LoadInt32(shuttingDown) == 1 {
109-
return yc.Name + ": Bot is shutting down or restarting, please try again in a couple seconds...", nil
110-
}
111-
112-
return yc.Name + ": Gave up trying to run command after 60 seconds waiting for your previous instance of this command to finish", nil
113-
}
114-
115-
defer removeRunningCommand(data.Msg.GuildID, data.Msg.ChannelID, data.Msg.Author.ID, yc)
116-
117126
err = dcmd.ParseCmdArgs(data)
118127
if err != nil {
119128
if dcmd.IsUserError(err) {

commands/tmplexec.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ type cmdExecFunc func(cmd string, args ...interface{}) (interface{}, error)
7878
func TmplExecCmdFuncs(ctx *templates.Context, maxExec int, dryRun bool) (userCtxCommandExec cmdExecFunc, botCtxCommandExec cmdExecFunc) {
7979
execUser := func(cmd string, args ...interface{}) (interface{}, error) {
8080
messageCopy := *ctx.Msg
81-
if ctx.CS != nil { //Check if CS is not a nil pointer
81+
if ctx.CS != nil { //Check if CS is not a nil pointer
8282
messageCopy.ChannelID = ctx.CS.ID
8383
}
8484
mc := &discordgo.MessageCreate{&messageCopy}
@@ -96,7 +96,7 @@ func TmplExecCmdFuncs(ctx *templates.Context, maxExec int, dryRun bool) (userCtx
9696

9797
messageCopy := *ctx.Msg
9898
messageCopy.Author = &botUserCopy
99-
if ctx.CS != nil { //Check if CS is not a nil pointer
99+
if ctx.CS != nil { //Check if CS is not a nil pointer
100100
messageCopy.ChannelID = ctx.CS.ID
101101
}
102102

@@ -219,13 +219,24 @@ func execCmd(tmplCtx *templates.Context, dryRun bool, m *discordgo.MessageCreate
219219
}
220220
runFunc = data.ContainerChain[len(data.ContainerChain)-1-i].BuildMiddlewareChain(runFunc, foundCmd)
221221
}
222-
// foundCmd.Trigger.
222+
223+
// Check guild scope cooldown
224+
cd, err := cast.GuildScopeCooldownLeft(data.ContainerChain, tmplCtx.GS.ID)
225+
if err != nil {
226+
return "", errors.WithStackIf(err)
227+
}
228+
229+
if cd > 0 {
230+
return "", errors.NewPlain("this command is on guild scope cooldown")
231+
}
223232

224233
resp, err := runFunc(data)
225234
if err != nil {
226235
return "", errors.WithMessage(err, "exec/execadmin, run")
227236
}
228237

238+
cast.SetCooldownGuild(data.ContainerChain, tmplCtx.GS.ID)
239+
229240
switch v := resp.(type) {
230241
case error:
231242
return "Error: " + v.Error(), nil

commands/yagcommmand.go

Lines changed: 123 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@ var (
6060
)
6161

6262
var (
63-
RKeyCommandCooldown = func(uID int64, cmd string) string { return "cmd_cd:" + discordgo.StrID(uID) + ":" + cmd }
64-
RKeyCommandLock = func(uID int64, cmd string) string { return "cmd_lock:" + discordgo.StrID(uID) + ":" + cmd }
63+
RKeyCommandCooldown = func(uID int64, cmd string) string { return "cmd_cd:" + discordgo.StrID(uID) + ":" + cmd }
64+
RKeyCommandCooldownGuild = func(gID int64, cmd string) string { return "cmd_guild_cd:" + discordgo.StrID(gID) + ":" + cmd }
65+
RKeyCommandLock = func(uID int64, cmd string) string { return "cmd_lock:" + discordgo.StrID(uID) + ":" + cmd }
6566

6667
CommandExecTimeout = time.Minute
6768

@@ -98,8 +99,9 @@ type YAGCommand struct {
9899
CustomEnabled bool // Set to true to handle the enable check itself
99100
Default bool // The default enabled state of this command
100101

101-
Cooldown int // Cooldown in seconds before user can use it again
102-
CmdCategory *dcmd.Category
102+
Cooldown int // Cooldown in seconds before user can use it again
103+
CmdCategory *dcmd.Category
104+
GuildScopeCooldown int
103105

104106
RunInDM bool // Set to enable this commmand in DM's
105107
HideFromHelp bool // Set to hide from help
@@ -190,7 +192,7 @@ func (yc *YAGCommand) Run(data *dcmd.Data) (interface{}, error) {
190192

191193
// Log errors
192194
if cmdErr == nil {
193-
err := yc.SetCooldown(data.Msg.Author.ID)
195+
err := yc.SetCooldowns(data.ContainerChain, data.Msg.Author.ID, data.Msg.GuildID)
194196
if err != nil {
195197
logger.WithError(err).Error("Failed setting cooldown")
196198
}
@@ -305,6 +307,15 @@ func (yc *YAGCommand) PostCommandExecuted(settings *CommandSettings, cmdData *dc
305307
return
306308
}
307309

310+
const (
311+
ReasonError = "An error occured"
312+
ReasonCommandDisabaledSettings = "Command is disabled in the settings"
313+
ReasonMissingRole = "Missing a required role for this command"
314+
ReasonIgnoredRole = "Has a ignored role for this command"
315+
ReasonUserMissingPerms = "User is missing one or more permissions to run this command"
316+
ReasonCooldown = "This command is on cooldown"
317+
)
318+
308319
// checks if the specified user can execute the command, and if so returns the settings for said command
309320
func (yc *YAGCommand) checkCanExecuteCommand(data *dcmd.Data, cState *dstate.ChannelState) (canExecute bool, resp string, settings *CommandSettings, err error) {
310321
// Check guild specific settings if not triggered from a DM
@@ -316,7 +327,8 @@ func (yc *YAGCommand) checkCanExecuteCommand(data *dcmd.Data, cState *dstate.Cha
316327
guild = cState.Guild
317328

318329
if guild == nil {
319-
resp = "You're not on a server?"
330+
err = errors.NewPlain("Not on a guild")
331+
resp = ReasonError
320332
return
321333
}
322334

@@ -329,12 +341,12 @@ func (yc *YAGCommand) checkCanExecuteCommand(data *dcmd.Data, cState *dstate.Cha
329341
settings, err = yc.GetSettings(data.ContainerChain, cState.ID, cop.ParentID, guild.ID)
330342
if err != nil {
331343
err = errors.WithMessage(err, "cs.GetSettings")
332-
resp = "Bot is having isssues, contact the bot owner."
344+
resp = ReasonError
333345
return
334346
}
335347

336348
if !settings.Enabled {
337-
resp = fmt.Sprintf("The %q command is currently disabled on this server or channel. *(Control panel to enable/disable <https://%s>)*", yc.Name, common.ConfHost.GetString())
349+
resp = ReasonCommandDisabaledSettings
338350
return
339351
}
340352

@@ -350,14 +362,14 @@ func (yc *YAGCommand) checkCanExecuteCommand(data *dcmd.Data, cState *dstate.Cha
350362
}
351363

352364
if !found {
353-
resp = "Missing a required role set up by the server admins for this command."
365+
resp = ReasonMissingRole
354366
return
355367
}
356368
}
357369

358370
for _, ignored := range settings.IgnoreRoles {
359371
if common.ContainsInt64Slice(member.Roles, ignored) {
360-
resp = "One of your roles is set up to be ignored by the server admins."
372+
resp = ReasonIgnoredRole
361373
return
362374
}
363375
}
@@ -367,7 +379,7 @@ func (yc *YAGCommand) checkCanExecuteCommand(data *dcmd.Data, cState *dstate.Cha
367379
var perms int
368380
perms, err = cState.Guild.MemberPermissionsMS(true, cState.ID, member)
369381
if err != nil {
370-
resp = "Unable to check permissions"
382+
resp = ReasonError
371383
return
372384
}
373385

@@ -380,7 +392,7 @@ func (yc *YAGCommand) checkCanExecuteCommand(data *dcmd.Data, cState *dstate.Cha
380392
}
381393

382394
if !foundMatch {
383-
resp = "Missing required permissions to use this command (" + yc.humanizedRequiredPerms() + ")"
395+
resp = ReasonUserMissingPerms
384396
return
385397
}
386398
}
@@ -391,14 +403,14 @@ func (yc *YAGCommand) checkCanExecuteCommand(data *dcmd.Data, cState *dstate.Cha
391403
}
392404

393405
// Check the command cooldown
394-
cdLeft, err := yc.CooldownLeft(data.Msg.Author.ID)
406+
cdLeft, err := yc.LongestCooldownLeft(data.ContainerChain, data.Msg.Author.ID, data.Msg.GuildID)
395407
if err != nil {
396408
// Just pretend the cooldown is off...
397-
logger.WithError(err).WithField("author", data.Msg.Author.ID).Error("Failed checking command cooldown")
409+
yc.Logger(data).Error("Failed checking command cooldown")
398410
}
399411

400412
if cdLeft > 0 {
401-
resp = fmt.Sprintf("**%q:** You need to wait %d seconds before you can use the %q command again", common.EscapeSpecialMentions(data.Msg.Author.Username), cdLeft, yc.Name)
413+
resp = ReasonCooldown
402414
return
403415
}
404416

@@ -595,34 +607,94 @@ OUTER:
595607
}
596608
}
597609

598-
// CooldownLeft returns the number of seconds before a command can be used again
599-
func (cs *YAGCommand) CooldownLeft(userID int64) (int, error) {
600-
if cs.Cooldown < 1 || common.Testing {
610+
// LongestCooldownLeft returns the longest cooldown for this command, either user scoped or guild scoped
611+
func (cs *YAGCommand) LongestCooldownLeft(cc []*dcmd.Container, userID int64, guildID int64) (int, error) {
612+
cdUser, err := cs.UserScopeCooldownLeft(cc, userID)
613+
if err != nil {
614+
return 0, err
615+
}
616+
617+
cdGuild, err := cs.GuildScopeCooldownLeft(cc, guildID)
618+
if err != nil {
619+
return 0, err
620+
}
621+
622+
if cdUser > cdGuild {
623+
return cdUser, nil
624+
}
625+
626+
return cdGuild, nil
627+
}
628+
629+
// UserScopeCooldownLeft returns the number of seconds before a command can be used again by this user
630+
func (cs *YAGCommand) UserScopeCooldownLeft(cc []*dcmd.Container, userID int64) (int, error) {
631+
if cs.Cooldown < 1 {
601632
return 0, nil
602633
}
603634

604635
var ttl int
605-
err := common.RedisPool.Do(retryableredis.Cmd(&ttl, "TTL", RKeyCommandCooldown(userID, cs.Name)))
606-
if ttl < 1 {
636+
err := common.RedisPool.Do(retryableredis.Cmd(&ttl, "TTL", RKeyCommandCooldown(userID, cs.FindNameFromContainerChain(cc))))
637+
if err != nil {
638+
return 0, errors.WithStackIf(err)
639+
}
640+
641+
return ttl, nil
642+
}
643+
644+
// GuildScopeCooldownLeft returns the number of seconds before a command can be used again on this server
645+
func (cs *YAGCommand) GuildScopeCooldownLeft(cc []*dcmd.Container, guildID int64) (int, error) {
646+
if cs.GuildScopeCooldown < 1 {
607647
return 0, nil
608648
}
609649

610-
return ttl, err
650+
var ttl int
651+
err := common.RedisPool.Do(retryableredis.Cmd(&ttl, "TTL", RKeyCommandCooldownGuild(guildID, cs.FindNameFromContainerChain(cc))))
652+
if err != nil {
653+
return 0, errors.WithStackIf(err)
654+
}
655+
656+
return ttl, nil
657+
}
658+
659+
// SetCooldowns is a helper that serts both User and Guild cooldown
660+
func (cs *YAGCommand) SetCooldowns(cc []*dcmd.Container, userID int64, guildID int64) error {
661+
err := cs.SetCooldownUser(cc, userID)
662+
if err != nil {
663+
return errors.WithStackIf(err)
664+
}
665+
666+
err = cs.SetCooldownGuild(cc, guildID)
667+
if err != nil {
668+
return errors.WithStackIf(err)
669+
}
670+
671+
return nil
611672
}
612673

613-
// SetCooldown sets the cooldown of the command as it's defined in the struct
614-
func (cs *YAGCommand) SetCooldown(userID int64) error {
674+
// SetCooldownUser sets the user scoped cooldown of the command as it's defined in the struct
675+
func (cs *YAGCommand) SetCooldownUser(cc []*dcmd.Container, userID int64) error {
615676
if cs.Cooldown < 1 {
616677
return nil
617678
}
618679
now := time.Now().Unix()
619680

620-
err := common.RedisPool.Do(retryableredis.FlatCmd(nil, "SET", RKeyCommandCooldown(userID, cs.Name), now, "EX", cs.Cooldown))
621-
return err
681+
err := common.RedisPool.Do(retryableredis.FlatCmd(nil, "SET", RKeyCommandCooldown(userID, cs.FindNameFromContainerChain(cc)), now, "EX", cs.Cooldown))
682+
return errors.WithStackIf(err)
683+
}
684+
685+
// SetCooldownGuild sets the guild scoped cooldown of the command as it's defined in the struct
686+
func (cs *YAGCommand) SetCooldownGuild(cc []*dcmd.Container, guildID int64) error {
687+
if cs.GuildScopeCooldown < 1 {
688+
return nil
689+
}
690+
691+
now := time.Now().Unix()
692+
err := common.RedisPool.Do(retryableredis.FlatCmd(nil, "SET", RKeyCommandCooldownGuild(guildID, cs.FindNameFromContainerChain(cc)), now, "EX", cs.GuildScopeCooldown))
693+
return errors.WithStackIf(err)
622694
}
623695

624696
func (yc *YAGCommand) Logger(data *dcmd.Data) *logrus.Entry {
625-
l := logger.WithField("cmd", yc.Name)
697+
l := logger.WithField("cmd", yc.FindNameFromContainerChain(data.ContainerChain))
626698
if data != nil {
627699
if data.Msg != nil {
628700
l = l.WithField("user_n", data.Msg.Author.Username)
@@ -632,6 +704,10 @@ func (yc *YAGCommand) Logger(data *dcmd.Data) *logrus.Entry {
632704
if data.CS != nil {
633705
l = l.WithField("channel", data.CS.ID)
634706
}
707+
708+
if data.GS != nil {
709+
l = l.WithField("guild", data.GS.ID)
710+
}
635711
}
636712

637713
return l
@@ -720,3 +796,24 @@ func removeRunningCommand(guildID, channelID, authorID int64, cmd *YAGCommand) {
720796

721797
return
722798
}
799+
800+
func (yc *YAGCommand) FindNameFromContainerChain(cc []*dcmd.Container) string {
801+
name := ""
802+
for _, v := range cc {
803+
if len(v.Names) < 1 {
804+
continue
805+
}
806+
807+
if name != "" {
808+
name += " "
809+
}
810+
811+
name += v.Names[0]
812+
}
813+
814+
if name != "" {
815+
name += " "
816+
}
817+
818+
return name + yc.Name
819+
}

stdcommands/mentionrole/mentionrole.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ var Command = &commands.YAGCommand{
7070
ArgSwitches: []*dcmd.ArgDef{
7171
&dcmd.ArgDef{Switch: "channel", Help: "Optional channel to send in", Type: dcmd.Channel},
7272
},
73-
RunFunc: cmdFuncMentionRole,
73+
RunFunc: cmdFuncMentionRole,
74+
GuildScopeCooldown: 10,
7475
}
7576

7677
func cmdFuncMentionRole(data *dcmd.Data) (interface{}, error) {

0 commit comments

Comments
 (0)