mastermind-bot/main.go

247 lines
6.8 KiB
Go

package main
import (
"encoding/json"
"log"
"os"
"os/signal"
"slices"
"strconv"
"strings"
dg "github.com/bwmarrin/discordgo"
)
type Settings struct {
Token string `json:"token"`
Guild string `json:"guild"`
StudentRole string `json:"student_role"`
Leaderboard [][]string `json:"leaderboard"`
// Hex code for embeds color
Color int `json:"color"`
students map[string]int
rankings map[int]map[string]struct{}
}
var stg Settings
func init() {
file, err := os.ReadFile("settings.json")
if err != nil {
log.Fatalf("Error while reading settings.json : %s", err)
}
if err := json.Unmarshal(file, &stg); err != nil {
log.Fatalf("Error while reading settings.json : %s", err.Error())
}
stg.students = make(map[string]int, len(stg.Leaderboard))
stg.rankings = make(map[int]map[string]struct{}, len(stg.Leaderboard))
i := 1
for _, r := range stg.Leaderboard {
stg.rankings[i] = make(map[string]struct{}, len(r))
for _, s := range r {
stg.rankings[i][s] = struct{}{}
stg.students[s] = i
}
i += len(r)
}
}
var descs = map[string]*dg.ApplicationCommand{
"ping": &dg.ApplicationCommand{
Name: "ping",
Description: "Replies with pong",
DescriptionLocalizations: &map[dg.Locale]string{
dg.French: "Répond pong",
},
},
"sub": &dg.ApplicationCommand{
Name: "sub",
Description: "Submit a guess",
DescriptionLocalizations: &map[dg.Locale]string{
dg.French: "Envoie un essais pour deviner",
},
// /!\ Make sure to update handler if other options are added
// Guess options are added in init function
// It is assumed in handler that there aren't other options
Options: []*dg.ApplicationCommandOption{},
},
}
// Just in case...
var duplicateNames = []string{
"bis", "ter", "quater", "quinquies", "sexies", "septies", "octies", "nonies", "decies", "undecies", "duodecies", "terdecies", "quaterdecies", "quindecies", "sexdecies", "septdecies", "octodecies", "novodecies", "vicies",
}
func init() {
for rank, students := range stg.rankings {
rankName := strconv.Itoa(rank)
for i := range len(students) {
opt := dg.ApplicationCommandOption{
Type: dg.ApplicationCommandOptionUser,
Description: "Guess for student " + rankName,
DescriptionLocalizations: map[dg.Locale]string{
dg.French: "Supposition pour l'élève " + rankName,
},
Required: true,
}
if i == 0 {
opt.Name = rankName
} else {
opt.Name = rankName + "_" + duplicateNames[i-1]
}
descs["sub"].Options = append(descs["sub"].Options, &opt)
}
}
}
func send_error(s *dg.Session, i *dg.Interaction, reason string) {
log.Printf("Sending error (\"%s\")", reason)
s.InteractionResponseEdit(i, &dg.WebhookEdit{
Embeds: &[]*dg.MessageEmbed{
{
Title: "⚠️ Error",
Description: reason,
Color: 0xFF0000,
},
},
})
}
// Handlers are separated because we need to be able to access descs
var handlers = map[string]func(*dg.Session, *dg.InteractionCreate){
"ping": func(s *dg.Session, i *dg.InteractionCreate) {
s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
Type: dg.InteractionResponseChannelMessageWithSource,
Data: &dg.InteractionResponseData{
Content: ":ping_pong: Pong !",
},
})
},
"sub": func(s *dg.Session, i *dg.InteractionCreate) {
s.InteractionRespond(i.Interaction, &dg.InteractionResponse{
Type: dg.InteractionResponseDeferredChannelMessageWithSource,
})
red := 0
green := 0
guesses := []string{}
ids := make(map[string]struct{}, len(descs["sub"].Options))
for _, opt := range descs["sub"].Options {
val := i.ApplicationCommandData().GetOption(opt.Name)
if val == nil {
send_error(s, i.Interaction, "Missing option")
return
}
if val.Type != dg.ApplicationCommandOptionUser {
send_error(s, i.Interaction, "Invalid option type")
return
}
guess := val.UserValue(s)
if _, ok := ids[guess.ID]; ok {
if i.Locale == dg.French {
send_error(s, i.Interaction, "Les doublons ne sont pas autorisés !")
} else {
send_error(s, i.Interaction, "Duplicates are not allowed!")
}
return
} else {
ids[guess.ID] = struct{}{}
}
if mem, err := s.GuildMember(stg.Guild, guess.ID); err != nil || !slices.Contains(mem.Roles, stg.StudentRole) {
if i.Locale == dg.French {
send_error(s, i.Interaction, "<@"+guess.ID+"> n'est pas un élève !")
} else {
send_error(s, i.Interaction, "<@"+guess.ID+"> is not a student!")
}
return
}
grstr, _, _ := strings.Cut(opt.Name, "_")
guessRank, err := strconv.Atoi(grstr)
if err != nil || stg.rankings[guessRank] == nil {
send_error(s, i.Interaction, "Invalid option name")
return
}
if num, ok := stg.students[guess.ID]; ok {
if num == guessRank {
green += 1
} else {
red += 1
}
}
guesses = append(guesses, strconv.Itoa(guessRank)+": <@"+guess.ID+">")
}
var cmdAuthor string
if i.Member != nil {
cmdAuthor = i.Member.DisplayName()
} else {
cmdAuthor = i.User.DisplayName()
}
var title string
if i.Locale == dg.French {
title = "Tentative par " + cmdAuthor
} else {
title = "Guess by " + cmdAuthor
}
s.InteractionResponseEdit(i.Interaction, &dg.WebhookEdit{
Embeds: &[]*dg.MessageEmbed{
{
Title: title,
Description: strings.Join(guesses, "\n") + "\n\n**" + strconv.Itoa(green) + " 🟩**\n**" + strconv.Itoa(red) + " 🟥**",
Color: stg.Color,
},
},
})
if green == len(stg.students) {
var winTitle string
if i.Locale == dg.French {
winTitle = "🎉 " + cmdAuthor + " a gagné !"
} else {
winTitle = "🎉 " + cmdAuthor + " has won!"
}
s.FollowupMessageCreate(i.Interaction, true, &dg.WebhookParams{
Embeds: []*dg.MessageEmbed{
&dg.MessageEmbed{
Color: 0x00FF00,
Title: winTitle,
},
},
})
}
},
}
func main() {
s, _ := dg.New("Bot " + stg.Token)
s.AddHandler(func(s *dg.Session, r *dg.Ready) {
log.Printf("Ready!")
})
err := s.Open()
if err != nil {
log.Panicf("Failed to create session : %s", err)
}
s.AddHandler(func(s *dg.Session, i *dg.InteractionCreate) {
if i.Type != dg.InteractionApplicationCommand {
log.Printf("Warning : Received unknown interaction type (%s)", i.Type.String())
}
if h, ok := handlers[i.ApplicationCommandData().Name]; ok {
h(s, i)
log.Printf("Reveived command \"%s\"", i.ApplicationCommandData().Name)
} else {
log.Printf("Warning : Received unknown command \"%s\" (id:%s)", i.ApplicationCommandData().Name, i.ApplicationCommandData().ID)
}
})
// Create commands
for _, d := range descs {
if _, err := s.ApplicationCommandCreate(s.State.User.ID, stg.Guild, d); err != nil {
log.Panicf("Failed to create command : %s", err)
}
}
defer s.Close()
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
log.Println("Press Ctrl+C to exit")
<-stop
}