implement basic whitelist add and remove functionality

This commit is contained in:
Arthur Lu 2024-10-21 19:58:19 +00:00
commit 04d4cb630b
9 changed files with 352 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
**/go.sum
**/config.json
**/db.json
dist/*

10
Makefile Normal file
View File

@ -0,0 +1,10 @@
.PHONY: build clean
build: clean
@echo "======================== Building Binary ======================="
CGO_ENABLED=0 go build -ldflags="-s -w" -v -o dist/ .
clean:
@echo "======================== Cleaning Project ======================"
go clean
rm -f dist/*

81
app/bot.go Normal file
View File

@ -0,0 +1,81 @@
package app
import (
"flag"
"log"
"os"
"os/signal"
"github.com/bwmarrin/discordgo"
"github.com/gorcon/rcon"
)
var s *discordgo.Session
var config Config
var dbPath string
var db MemberUsernameMap
var conn *rcon.Conn
func Run() {
// parse CLI options and get config
configPath := *(flag.String("config", "config.json", "path to config.json file"))
dbPath = *(flag.String("db", "db.json", "path to db.json file"))
flag.Parse()
// load config
config, err := GetConfig(configPath)
if err != nil {
log.Fatalf("Error when reading config file: %s", err)
}
// load db or create new empty db
db, err = LoadDB(dbPath)
if err != nil {
log.Fatalf("Error when reading config file: %s", err)
}
// open rconn connection
conn, err = rcon.Dial(config.RCON, config.Password)
if err != nil {
log.Fatalf("Failed to open rcon connection: %s", err.Error())
}
// create new session
s, err := discordgo.New("Bot " + config.Token)
if err != nil {
log.Fatalf("Invalid bot parameters: %v", err)
}
// attach slash command listener
s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) {
if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok {
h(s, i)
}
})
// attach session connected listener
s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) {
log.Printf("Logged in as: %v#%v", s.State.User.Username, s.State.User.Discriminator)
})
// overwrite registered commands
log.Println("Bulk overwriting commands")
_, err = s.ApplicationCommandBulkOverwrite(config.AppID, config.GuildID, commands)
if err != nil {
log.Fatalf("Failed to bulk overwrite commands: %s", err.Error())
}
// open session
err = s.Open()
if err != nil {
log.Fatalf("Cannot open the session: %v", err)
}
defer s.Close()
defer conn.Close()
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
log.Println("Press Ctrl+C to exit")
<-stop
}

128
app/commands.go Normal file
View File

@ -0,0 +1,128 @@
package app
import (
"fmt"
"strings"
"github.com/bwmarrin/discordgo"
)
var (
commands = []*discordgo.ApplicationCommand{
{
Name: "whitelist-add",
Description: "Add yourself to the whitelist",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "minecraft-username",
Description: "Minecraft Username",
Required: true,
},
},
},
{
Name: "whitelist-remove",
Description: "Remove yourself from the whitelist",
Options: []*discordgo.ApplicationCommandOption{},
},
{
Name: "whitelist-show",
Description: "Display your whitelist link",
Options: []*discordgo.ApplicationCommandOption{},
},
}
commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
"whitelist-add": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
// Access options in the order provided by the user.
options := i.ApplicationCommandData().Options
// Convert the slice into a map
optionMap := make(map[string]*discordgo.ApplicationCommandInteractionDataOption, len(options))
for _, opt := range options {
optionMap[opt.Name] = opt
}
requestedUsername := (optionMap["minecraft-username"]).StringValue()
invokingUserName := i.Member.User.Username
invokingUserID := i.Member.User.ID
// check requestedUsername validity
valid := checkMcUsernameValid(requestedUsername)
if !valid {
simpleResponse(s, i, fmt.Sprintf("%s is not a valid minecraft username", requestedUsername))
return
}
existingMCUsername, ok := db[invokingUserID]
if !ok { // invoking user does not currently have an mc username associated
// execute RCON
response, err := conn.Execute(fmt.Sprintf("whitelist add %s", requestedUsername))
if err != nil {
simpleResponse(s, i, fmt.Sprintf("Failed to add %s to whitelist: %s", requestedUsername, err.Error()))
return
}
// this can happen if the username is not a real player, OR if the user is already on the whitelist
// In both cases we want to exit early to avoid adding an invalid username to the db
if !strings.EqualFold(response, fmt.Sprintf("Added %s to the whitelist", requestedUsername)) {
simpleResponse(s, i, fmt.Sprintf("Failed to add %s to whitelist: %s", requestedUsername, response))
return
}
// save state to db
db[invokingUserID] = requestedUsername
SaveDB(dbPath, db)
// send response
simpleResponse(s, i, fmt.Sprintf("%s linked minecraft username %s", invokingUserName, requestedUsername))
} else {
simpleResponse(s, i, fmt.Sprintf("%s already has a linked minecraft username %s", invokingUserName, existingMCUsername))
}
},
"whitelist-remove": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
invokingUserName := i.Member.User.Username
invokingUserID := i.Member.User.ID
existingMCUsername, ok := db[invokingUserID]
if !ok { // invoking user does not currently have an mc username associated
simpleResponse(s, i, fmt.Sprintf("%s does not have a linked minecraft username", invokingUserName))
} else {
// execute RCON
response, err := conn.Execute(fmt.Sprintf("whitelist remove %s", existingMCUsername))
if err != nil {
simpleResponse(s, i, fmt.Sprintf("Failed to remove %s from whitelist: %s", existingMCUsername, err.Error()))
return
}
// this can happen if the username is not a real player, OR if the user is already not on the whitelist
if !strings.EqualFold(response, fmt.Sprintf("Removed %s from the whitelist", existingMCUsername)) {
simpleResponse(s, i, fmt.Sprintf("Failed to remove %s from whitelist: %s", existingMCUsername, response))
return
}
// save state to db
delete(db, invokingUserID)
SaveDB(dbPath, db)
// send response
simpleResponse(s, i, fmt.Sprintf("%s unlinked minecraft username %s", invokingUserName, existingMCUsername))
}
},
"whitelist-show": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
invokingUserName := i.Member.User.Username
invokingUserID := i.Member.User.ID
existingMCUsername, ok := db[invokingUserID]
if ok {
simpleResponse(s, i, fmt.Sprintf("%s is %s", invokingUserName, existingMCUsername))
} else {
simpleResponse(s, i, fmt.Sprintf("%s does not have a linked minecraft username", invokingUserName))
}
},
}
)

88
app/utils.go Normal file
View File

@ -0,0 +1,88 @@
package app
import (
"encoding/json"
"errors"
"log"
"os"
"regexp"
"github.com/bwmarrin/discordgo"
)
type Config struct {
AppID string `json:"app-id"`
GuildID string `json:"guild-id"`
Token string `json:"token"`
RCON string `json:"mc-rcon"`
Password string `json:"mc-rcon-password"`
}
func GetConfig(configPath string) (Config, error) {
content, err := os.ReadFile(configPath)
if err != nil {
return Config{}, err
}
var config Config
err = json.Unmarshal(content, &config)
if err != nil {
return Config{}, err
}
return config, nil
}
type MemberUsernameMap map[string]string
func LoadDB(dbPath string) (MemberUsernameMap, error) {
var db MemberUsernameMap
if _, err := os.Stat(dbPath); errors.Is(err, os.ErrNotExist) {
log.Printf("Did not find db.json file, making new one at %s", dbPath)
_, err = os.Create(dbPath)
if err != nil {
log.Fatalf("Failed to create empty db.json file: %s", err.Error())
}
SaveDB(dbPath, db)
}
content, err := os.ReadFile(dbPath)
if err != nil {
return MemberUsernameMap{}, err
}
err = json.Unmarshal(content, &db)
if err != nil {
return MemberUsernameMap{}, err
}
return db, nil
}
func SaveDB(dbPath string, db MemberUsernameMap) error {
content, err := json.Marshal(db)
if err != nil {
log.Fatalf("Failed to marshal db as json: %s", err.Error())
return err
}
err = os.WriteFile(dbPath, []byte(content), 0644)
if err != nil {
log.Fatalf("Failed to write to db.json file: %s", err.Error())
return err
}
return nil
}
func simpleResponse(s *discordgo.Session, i *discordgo.InteractionCreate, message string) {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: message,
},
})
}
func checkMcUsernameValid(username string) bool {
res, _ := regexp.MatchString("^[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_]{3,16}$", username) // ^ and $ ensure that the whole string must match
return res
}

View File

@ -0,0 +1,7 @@
{
"app-id": "",
"guild-id": "",
"token": "",
"mc-rcon": "1.2.3.4:25575",
"mc-rcon-password": ""
}

View File

@ -0,0 +1,9 @@
package main
import (
app "discord-minecraft-whitelist-bot/app"
)
func main() {
app.Run()
}

14
go.mod Normal file
View File

@ -0,0 +1,14 @@
module discord-minecraft-whitelist-bot
go 1.23.2
require (
github.com/bwmarrin/discordgo v0.28.1
github.com/gorcon/rcon v1.3.5
)
require (
github.com/gorilla/websocket v1.5.3 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/sys v0.26.0 // indirect
)

View File

@ -0,0 +1,11 @@
[Unit]
Description=discord-minecraft-whitelist-bot
After=network.target
[Service]
WorkingDirectory=/<path to dir>
ExecStart=/<path to dir>/discord-minecraft-whitelist-bot
Restart=always
RestartSec=10
Type=simple
[Install]
WantedBy=default.target