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 }