223 lines
5.3 KiB
Go
223 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/bwmarrin/discordgo"
|
|
"github.com/docker/docker/api/types"
|
|
docker "github.com/docker/docker/client"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
var processorLock sync.Mutex
|
|
|
|
func handleApplicationCommand(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
cmdData := i.ApplicationCommandData()
|
|
if cmdData.Name != "register" {
|
|
return
|
|
}
|
|
|
|
rolesOk := false
|
|
for _, roleID := range i.Member.Roles {
|
|
if roleID == Config.RequiredRole {
|
|
rolesOk = true
|
|
}
|
|
}
|
|
|
|
if !rolesOk {
|
|
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
Data: &discordgo.InteractionResponseData{
|
|
Embeds: []*discordgo.MessageEmbed{
|
|
{Description: "Not verified - please verify first.", Color: 0xff0000},
|
|
},
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
username, rolesOk := cmdData.Options[0].Value.(string)
|
|
if !rolesOk {
|
|
log.Error().Interface("val", cmdData.Options[0].Value).Msg("discord api string not a string??")
|
|
return
|
|
}
|
|
|
|
if validationErr := validateUsername(username); validationErr != nil {
|
|
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
Data: &discordgo.InteractionResponseData{
|
|
Embeds: []*discordgo.MessageEmbed{
|
|
{Description: fmt.Sprintf("Invalid username: %s", validationErr.Error()), Color: 0xff0000},
|
|
},
|
|
},
|
|
})
|
|
return
|
|
}
|
|
|
|
var (
|
|
err error
|
|
existingUsername string
|
|
alreadyRegistered bool
|
|
response *discordgo.InteractionResponse
|
|
)
|
|
|
|
processorLock.Lock()
|
|
|
|
known, err := loadKnownUsers()
|
|
if err != nil {
|
|
processorLock.Unlock()
|
|
goto error
|
|
}
|
|
|
|
existingUsername, alreadyRegistered = known[i.Interaction.Member.User.ID]
|
|
|
|
if alreadyRegistered && strings.EqualFold(existingUsername, username) {
|
|
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
Data: &discordgo.InteractionResponseData{
|
|
Embeds: []*discordgo.MessageEmbed{
|
|
{Description: "You've already registered with this username!", Color: 0xff0000},
|
|
},
|
|
},
|
|
})
|
|
return
|
|
} else if alreadyRegistered {
|
|
err = removeFromWhitelist(existingUsername)
|
|
if err != nil {
|
|
processorLock.Unlock()
|
|
goto error
|
|
}
|
|
}
|
|
|
|
err = addToWhitelist(username)
|
|
if err != nil {
|
|
processorLock.Unlock()
|
|
goto error
|
|
}
|
|
|
|
known[i.Interaction.Member.User.ID] = username
|
|
|
|
err = saveKnownUsers(known)
|
|
|
|
processorLock.Unlock()
|
|
|
|
response = &discordgo.InteractionResponse{
|
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
Data: &discordgo.InteractionResponseData{
|
|
Embeds: []*discordgo.MessageEmbed{
|
|
{Description: fmt.Sprintf("Successfully registered `%s`", username), Color: 0x00ff00},
|
|
},
|
|
},
|
|
}
|
|
|
|
if alreadyRegistered {
|
|
response.Data.Embeds[0].Description += fmt.Sprintf(" and removed `%s`", existingUsername)
|
|
}
|
|
|
|
err = s.InteractionRespond(i.Interaction, response)
|
|
if err != nil {
|
|
goto error
|
|
}
|
|
|
|
return
|
|
error:
|
|
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
Data: &discordgo.InteractionResponseData{
|
|
Content: fmt.Sprintf("An internal error was encoutered and the bot was unable to process your command.\nError: %s", err.Error()),
|
|
},
|
|
})
|
|
log.Error().Err(err).Msg("in handler")
|
|
}
|
|
|
|
type knownUsers map[string]string
|
|
|
|
func loadKnownUsers() (knownUsers, error) {
|
|
res := make(knownUsers)
|
|
|
|
fcont, err := os.ReadFile("knownusers.json")
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return res, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if err := json.Unmarshal(fcont, &res); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func saveKnownUsers(res knownUsers) error {
|
|
jcont, err := json.Marshal(res)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.WriteFile("knownusers.json", jcont, 0644)
|
|
}
|
|
|
|
var usernameValidationRegexp = regexp.MustCompile(`^[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_]{3,16}$`)
|
|
|
|
func validateUsername(username string) error {
|
|
if !usernameValidationRegexp.MatchString(username) {
|
|
return errors.New("illegal")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func addToWhitelist(username string) error {
|
|
return runCommandInContainer([]string{"mc-send-to-console", "whitelist", "add", username})
|
|
}
|
|
|
|
func removeFromWhitelist(username string) error {
|
|
return runCommandInContainer([]string{"mc-send-to-console", "whitelist", "remove", username})
|
|
}
|
|
|
|
func runCommandInContainer(cmd []string) error {
|
|
client, err := docker.NewClientWithOpts(docker.WithHost(Config.DockerSocket))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer client.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
|
|
defer cancel()
|
|
|
|
id, err := client.ContainerExecCreate(ctx, Config.ContainerName, types.ExecConfig{
|
|
Cmd: cmd,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := client.ContainerExecStart(ctx, id.ID, types.ExecStartCheck{Detach: true}); err != nil {
|
|
return err
|
|
}
|
|
|
|
time.Sleep(time.Millisecond * 200)
|
|
|
|
ctx, cancel = context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
|
|
insp, err := client.ContainerExecInspect(ctx, id.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if insp.Running || insp.ExitCode != 0 {
|
|
return fmt.Errorf("failed to run %s: insp.Running=%v, insp.ExitCode=%d", cmd, insp.Running, insp.ExitCode)
|
|
}
|
|
|
|
return nil
|
|
}
|