44 "fmt"
55 "regexp"
66 "strings"
7+ "sync"
8+ "time"
79
810 "github.com/bwmarrin/discordgo"
911 "github.com/google/uuid"
3941 alumniRole string = config .GetString ("commands.member.alumni_role_id" )
4042)
4143
42- // Member is the member command
44+ // notReceivedCooldown tracks the last time a verification email was sent in
45+ // the /member flow, per user ID. Once the email goes out, a user must wait
46+ // ten minutes before declaring they did not recieve the code; the "I recieved
47+ // the code" path is never gated.
48+ var notReceivedCooldown = struct {
49+ sync.Mutex
50+ m map [string ]time.Time // user ID -> time the verification email was sent
51+ }{m : make (map [string ]time.Time )}
52+
53+ // Member is the handler for the /member command
4354func Member () (* discordgo.ApplicationCommand , func (s * discordgo.Session , i * discordgo.InteractionCreate )) {
4455 return & discordgo.ApplicationCommand {
4556 Name : "member" ,
@@ -473,6 +484,13 @@ func recievedEmail(s *discordgo.Session, i *discordgo.InteractionCreate, userEma
473484 )
474485 defer span .Finish ()
475486
487+ // Anchor the cooldown now, at the moment the code email was sent: from
488+ // here the user must wait ten minutes before declaring the code never
489+ // arrived, and every new email send restarts that window.
490+ notReceivedCooldown .Lock ()
491+ notReceivedCooldown .m [i .Member .User .ID ] = time .Now ()
492+ notReceivedCooldown .Unlock ()
493+
476494 interactionCreateChan := make (chan * discordgo.InteractionCreate )
477495 defer close (interactionCreateChan )
478496
@@ -489,6 +507,33 @@ func recievedEmail(s *discordgo.Session, i *discordgo.InteractionCreate, userEma
489507 defer delete (* ComponentHandlers , recievedSlug )
490508
491509 (* ComponentHandlers )[unrecievedSlug ] = func (s * discordgo.Session , i * discordgo.InteractionCreate ) {
510+ // The cooldown was anchored when the verification email was sent; refuse
511+ // until the ten-minute delivery window has passed.
512+ notReceivedCooldown .Lock ()
513+ last , ok := notReceivedCooldown .m [i .Member .User .ID ]
514+ blocked := ok && time .Now ().Before (last .Add (10 * time .Minute ))
515+ notReceivedCooldown .Unlock ()
516+ if blocked {
517+ remaining := int (time .Until (last .Add (10 * time .Minute )).Seconds ())/ 60 + 1
518+ units := "minutes"
519+ if remaining == 1 {
520+ units = "minute"
521+ }
522+ err := s .InteractionRespond (i .Interaction , & discordgo.InteractionResponse {
523+ Type : discordgo .InteractionResponseChannelMessageWithSource ,
524+ Data : & discordgo.InteractionResponseData {
525+ Flags : discordgo .MessageFlagsEphemeral ,
526+ Content : fmt .Sprintf ("You can only request manual verification after 10 minutes. Please wait **%d** %s and try again." , remaining , units ),
527+ },
528+ })
529+ if err != nil {
530+ logging .Error (s , err .Error (), i .Member .User , span , logrus.Fields {"error" : err })
531+ return
532+ }
533+ // Do not push to the channels; the prompt above stays open and
534+ // the function continues waiting for the user to click a button.
535+ return
536+ }
492537 recievedChan <- false
493538 interactionCreateChan <- i
494539 }
0 commit comments