implement basic whitelist add and remove functionality
This commit is contained in:
commit
04d4cb630b
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
**/go.sum
|
||||
**/config.json
|
||||
**/db.json
|
||||
dist/*
|
10
Makefile
Normal file
10
Makefile
Normal 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
81
app/bot.go
Normal 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
128
app/commands.go
Normal 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
88
app/utils.go
Normal 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
|
||||
}
|
7
configs/template.config.json
Normal file
7
configs/template.config.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"app-id": "",
|
||||
"guild-id": "",
|
||||
"token": "",
|
||||
"mc-rcon": "1.2.3.4:25575",
|
||||
"mc-rcon-password": ""
|
||||
}
|
9
discord-minecraft-whitelist-bot.go
Normal file
9
discord-minecraft-whitelist-bot.go
Normal file
@ -0,0 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
app "discord-minecraft-whitelist-bot/app"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app.Run()
|
||||
}
|
14
go.mod
Normal file
14
go.mod
Normal 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
|
||||
)
|
11
init/discord-minecraft-whitelist-bot.service
Normal file
11
init/discord-minecraft-whitelist-bot.service
Normal 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
|
Loading…
Reference in New Issue
Block a user