This commit is contained in:
Evan Burkey 2025-01-20 22:39:01 -08:00
commit 6f469d088b
7 changed files with 403 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.idea
token
run.sh
*.sqlite
*.lua

11
README.md Normal file
View File

@ -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

193
db.go Normal file
View File

@ -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
}

72
discord.go Normal file
View File

@ -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
}

18
go.mod Normal file
View File

@ -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
)

25
go.sum Normal file
View File

@ -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=

79
main.go Normal file
View File

@ -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
}