From 6f469d088bc025eabc2f94e4840862e90b508ae6 Mon Sep 17 00:00:00 2001 From: Evan Burkey Date: Mon, 20 Jan 2025 22:39:01 -0800 Subject: [PATCH] init --- .gitignore | 5 ++ README.md | 11 +++ db.go | 193 +++++++++++++++++++++++++++++++++++++++++++++++++++++ discord.go | 72 ++++++++++++++++++++ go.mod | 18 +++++ go.sum | 25 +++++++ main.go | 79 ++++++++++++++++++++++ 7 files changed, 403 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 db.go create mode 100644 discord.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2fe8a48 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea +token +run.sh +*.sqlite +*.lua diff --git a/README.md b/README.md new file mode 100644 index 0000000..782ecf0 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Dammaz Korn + +A book of grudges for your discord server + +## Usage + +Requires the following flags to run: + +- `-t` Discord bot token +- `-g` Guild ID (server ID) +- `-r` The one role that is allowed to add and remove to the pug banlist. I advise making a special role just for this diff --git a/db.go b/db.go new file mode 100644 index 0000000..a108108 --- /dev/null +++ b/db.go @@ -0,0 +1,193 @@ +package main + +import ( + "database/sql" + "errors" + "fmt" + "github.com/yuin/gluamapper" + "github.com/yuin/gopher-lua" + "golang.org/x/text/language" + "golang.org/x/text/message" + "io" + "os" + "sort" + "strings" +) + +var db *sql.DB + +const ( + dbName = "gambot.sqlite" +) + +func initDB() (err error) { + var newDb bool + _, err = os.Stat(dbName) + if errors.Is(err, os.ErrNotExist) { + newDb = true + } + + db, err = sql.Open("sqlite3", dbName) + if err != nil { + return + } + + if newDb { + commands := []string{ + `CREATE TABLE 'players' ( + 'name' VARCHAR(18) NOT NULL PRIMARY KEY, + 'val' integer SIGNED NOT NULL);`, + } + for _, command := range commands { + _, err = db.Exec(command) + } + } + + return +} + +type Player struct { + Name string + Val int +} + +func (p *Player) FormatVal() string { + pr := message.NewPrinter(language.English) + return pr.Sprintf("%d", p.Val) +} + +func getPlayers() ([]Player, error) { + players := make([]Player, 0) + tx, err := db.Begin() + if err != nil { + return nil, fmt.Errorf("failed to begin transaction: %s", err) + } + + stmt, err := tx.Prepare("SELECT * FROM players") + if err != nil { + return nil, fmt.Errorf("failed to prepare statement: %s", err) + } + defer stmt.Close() + + rows, err := stmt.Query() + if err != nil { + return nil, fmt.Errorf("failed to query rows: %s", err) + } + defer rows.Close() + + for rows.Next() { + var name string + var val int + err = rows.Scan(&name, &val) + if err != nil { + return nil, fmt.Errorf("failed to scan row: %s", err) + } + players = append(players, Player{name, val}) + } + + sort.Slice(players, func(i, j int) bool { + return players[i].Val > players[j].Val + }) + + return players, nil +} + +type WoWGoldGamblerDB struct { + ProfileKeys map[string]string + Global struct { + Game struct { + ChatChannel string + Wager int + RealmFilter bool + } + Stats struct { + Records struct { + Overall map[string]any + Classic map[string]any + } + Player map[string]int + Aliases map[string][]string + } + } +} + +func readTable(path string) error { + L := lua.NewState() + defer L.Close() + L.OpenLibs() + + file, err := os.Open(path) + if err != nil { + return fmt.Errorf("failed to open lua file: %w", err) + } + defer file.Close() + + fData, err := io.ReadAll(file) + if err != nil { + return fmt.Errorf("failed to read from file: %w", err) + } + fStr := cleanup(fData) + + if err = L.DoString(fStr); err != nil { + return fmt.Errorf("failed to load %s: %s", path, err) + } + + var gamboDB WoWGoldGamblerDB + table := L.GetGlobal("WoWGoldGamblerDB").(*lua.LTable) + err = gluamapper.Map(table, &gamboDB) + if err != nil { + return fmt.Errorf("failed to parse table: %s", err) + } + + var playerMap = make(map[string]int) + for name, val := range gamboDB.Global.Stats.Player { + playerMap[name] = val + } + + for mainChar, alts := range gamboDB.Global.Stats.Aliases { + for _, alt := range alts { + playerMap[mainChar] += playerMap[alt] + playerMap[alt] = 0 + } + } + + tx, err := db.Begin() + if err != nil { + return fmt.Errorf("failed to begin transaction: %s", err) + } + + stmt, err := tx.Prepare("INSERT OR REPLACE INTO players(name, val) VALUES(?, ?)") + if err != nil { + tx.Rollback() + return fmt.Errorf("failed to prepare statement: %s", err) + } + defer stmt.Close() + + for k, v := range playerMap { + if v == 0 { + continue + } + _, err = stmt.Exec(k, v) + if err != nil { + tx.Rollback() + return fmt.Errorf("failed to execute statement: %s", err) + } + } + + if err = tx.Commit(); err != nil { + return fmt.Errorf("failed to commit transaction: %s", err) + } + + return nil +} + +func cleanup(data []byte) string { + s := string(data) + strings.ReplaceAll(s, "Games Played", "GamesPlayed") + strings.Replace(s, "Gold Traded", "GoldTraded", 1) + strings.Replace(s, "Biggest Win", "BiggestWin", 1) + strings.Replace(s, "Biggest Wager", "BiggestWager", 1) + strings.Replace(s, "Luckiest Roll", "LuckiestRoll", 1) + strings.Replace(s, "Unluckiest Roll", "UnluckiestRoll", 1) + return s +} diff --git a/discord.go b/discord.go new file mode 100644 index 0000000..11a611a --- /dev/null +++ b/discord.go @@ -0,0 +1,72 @@ +package main + +import ( + "fmt" + "github.com/bwmarrin/discordgo" + "log" + "strings" +) + +var commands = []*discordgo.ApplicationCommand{ + { + Name: "gambo", + Description: "print gambo data", + }, +} + +var token, appID, guildID string + +func handleMessage(s *discordgo.Session, i *discordgo.InteractionCreate) { + log.Println("Received gambo command") + c, err := content() + if err != nil { + log.Println(err) + return + } + + embed := &discordgo.MessageEmbed{ + Title: "== Degen Hall of Fame ==", + Description: c, + } + + err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{embed}, + }, + }) + + if err != nil { + log.Printf("could not respond to interaction: %s", err) + } +} + +func content() (string, error) { + players, err := getPlayers() + if err != nil { + return "", err + } + var b strings.Builder + b.WriteString("**All Hail Gambolord " + players[0].Name + "!**\n") + b.WriteString("## Current Rankings\n") + + m := 0 + for _, player := range players { + if len(player.Name) > m { + m = len(player.Name) + } + } + + b.WriteString("```\n") + for _, player := range players { + formattedVal := player.FormatVal() + if player.Val > 0 { + b.WriteString(fmt.Sprintf("%-*s +%*s\n", m+2, player.Name, len(formattedVal), formattedVal)) + } else { + b.WriteString(fmt.Sprintf("%-*s %*s\n", m+2, player.Name, len(formattedVal), formattedVal)) + } + } + b.WriteString("```\n") + + return b.String(), nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b414ff6 --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module gambot + +go 1.23 + +require ( + github.com/bwmarrin/discordgo v0.28.1 + github.com/mattn/go-sqlite3 v1.14.24 + github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7 + github.com/yuin/gopher-lua v1.1.1 + golang.org/x/text v0.11.0 +) + +require ( + github.com/gorilla/websocket v1.5.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + golang.org/x/crypto v0.11.0 // indirect + golang.org/x/sys v0.10.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..abf61a6 --- /dev/null +++ b/go.sum @@ -0,0 +1,25 @@ +github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4= +github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7 h1:noHsffKZsNfU38DwcXWEPldrTjIZ8FPNKx8mYMGnqjs= +github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7/go.mod h1:bbMEM6aU1WDF1ErA5YJ0p91652pGv140gGw4Ww3RGp8= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..45677ec --- /dev/null +++ b/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "os/signal" + "syscall" + + "github.com/bwmarrin/discordgo" + + _ "github.com/mattn/go-sqlite3" +) + +func main() { + if err := initDB(); err != nil { + panic(err) + } + defer db.Close() + + var tablePath string + + flag.StringVar(&token, "t", "", "Bot Token") + flag.StringVar(&appID, "a", "", "App ID") + flag.StringVar(&guildID, "g", "", "Guild ID") + flag.StringVar(&tablePath, "p", "", "Path to table") + flag.Parse() + + if err := readTable(tablePath); err != nil { + panic(err) + } + + if err := runBot(token); err != nil { + panic(err) + } +} + +func runBot(token string) error { + bot, err := discordgo.New("Bot " + token) + if err != nil { + return fmt.Errorf("failed to create Discord session: %w\n", err) + } + + bot.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { + if i.Type != discordgo.InteractionApplicationCommand { + return + } + + data := i.ApplicationCommandData() + if data.Name != "gambo" { + return + } + + handleMessage(s, i) + }) + + bot.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) { + log.Printf("Logged in as %s", r.User.String()) + }) + + _, err = bot.ApplicationCommandBulkOverwrite(appID, guildID, commands) + if err != nil { + log.Fatalf("could not register commands: %s", err) + } + + err = bot.Open() + if err != nil { + return fmt.Errorf("failed to open bot session: %w\n", err) + } + defer bot.Close() + + fmt.Println("Starting bot...") + + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) + <-sc + return nil +}