commit 04d4cb630bbb70ef73cdc1c36ed6cb6cc75ea311 Author: Arthur Lu Date: Mon Oct 21 19:58:19 2024 +0000 implement basic whitelist add and remove functionality diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..56c7468 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +**/go.sum +**/config.json +**/db.json +dist/* \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..68d04a8 --- /dev/null +++ b/Makefile @@ -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/* \ No newline at end of file diff --git a/app/bot.go b/app/bot.go new file mode 100644 index 0000000..3437e22 --- /dev/null +++ b/app/bot.go @@ -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 +} diff --git a/app/commands.go b/app/commands.go new file mode 100644 index 0000000..eb5abb2 --- /dev/null +++ b/app/commands.go @@ -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)) + } + }, + } +) diff --git a/app/utils.go b/app/utils.go new file mode 100644 index 0000000..9612e53 --- /dev/null +++ b/app/utils.go @@ -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 +} diff --git a/configs/template.config.json b/configs/template.config.json new file mode 100644 index 0000000..8269088 --- /dev/null +++ b/configs/template.config.json @@ -0,0 +1,7 @@ +{ + "app-id": "", + "guild-id": "", + "token": "", + "mc-rcon": "1.2.3.4:25575", + "mc-rcon-password": "" +} \ No newline at end of file diff --git a/discord-minecraft-whitelist-bot.go b/discord-minecraft-whitelist-bot.go new file mode 100644 index 0000000..7af7f37 --- /dev/null +++ b/discord-minecraft-whitelist-bot.go @@ -0,0 +1,9 @@ +package main + +import ( + app "discord-minecraft-whitelist-bot/app" +) + +func main() { + app.Run() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..afcbe46 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/init/discord-minecraft-whitelist-bot.service b/init/discord-minecraft-whitelist-bot.service new file mode 100644 index 0000000..de3f499 --- /dev/null +++ b/init/discord-minecraft-whitelist-bot.service @@ -0,0 +1,11 @@ +[Unit] +Description=discord-minecraft-whitelist-bot +After=network.target +[Service] +WorkingDirectory=/ +ExecStart=//discord-minecraft-whitelist-bot +Restart=always +RestartSec=10 +Type=simple +[Install] +WantedBy=default.target \ No newline at end of file