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