diff options
| author | hachem <im@hachem.wtf> | 2026-08-29 10:44:22 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-08-29 10:44:22 +0200 |
| commit | af4a6f4c5e9a3e3a501deac71d92bbbcd12ef81f (patch) | |
| tree | 4882c0dd5e7e252e6216e00cc5cba0e6c2bfd533 /internal/setup/setup.go | |
| parent | 25bdd381d6dcf8581b1391c1c1fa22c108913ccb (diff) | |
add: reimplement discord bot
Diffstat (limited to 'internal/setup/setup.go')
| -rw-r--r-- | internal/setup/setup.go | 224 |
1 files changed, 224 insertions, 0 deletions
diff --git a/internal/setup/setup.go b/internal/setup/setup.go new file mode 100644 index 0000000..eafaaa0 --- /dev/null +++ b/internal/setup/setup.go @@ -0,0 +1,224 @@ +package setup + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "strconv" + "strings" + + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" + + "github.com/hachem-wtf/benson/internal/config" +) + +// Run is the server-manager wizard. It configures Minecraft/Fabric versions, +// paths, merge hooks and mods — everything except the Discord bot. +func Run() error { + cfg := loadConfig() + + var ( + mcVersion = cfg.MinecraftVersion + fabricLoader = cfg.FabricLoaderVersion + loaderMeta = cfg.LoaderMetaVersion + javaPath = cfg.JavaPath + serverLoc = cfg.ServerLocation + backupLoc = cfg.BackupLocation + preCmd = cfg.MergePreCommand + postCmd = cfg.MergePostCommand + modsText = strings.Join(cfg.ModrinthMods, "\n") + confirm = true + ) + + form := huh.NewForm( + huh.NewGroup( + huh.NewNote(). + Title("Benson setup"). + Description("Configure the server manager.\nThe Discord bot is optional — run `benson setup bot` for that."), + ), + huh.NewGroup( + huh.NewInput().Title("Minecraft version").Placeholder("1.21.10").Value(&mcVersion).Validate(required("Minecraft version")), + huh.NewInput().Title("Fabric loader version").Placeholder("0.17.3").Value(&fabricLoader).Validate(required("Fabric loader version")), + huh.NewInput().Title("Loader / installer version").Placeholder("1.1.0").Value(&loaderMeta).Validate(required("loader version")), + huh.NewInput().Title("Java path").Description("Leave blank to use 'java' from PATH").Value(&javaPath), + ).Title("Minecraft / Fabric"), + huh.NewGroup( + huh.NewInput().Title("Server location").Description("Live server directory, merged into by 'merge'").Value(&serverLoc), + huh.NewInput().Title("Backup location").Description("Where server backups are written").Value(&backupLoc), + huh.NewInput().Title("Pre-merge command").Description("Optional shell command run before merging").Value(&preCmd), + huh.NewInput().Title("Post-merge command").Description("Optional shell command run after merging").Value(&postCmd), + ).Title("Paths & hooks (optional)"), + huh.NewGroup( + huh.NewText().Title("Modrinth mods").Description("One mod slug per line").Lines(8).Value(&modsText), + ).Title("Mods"), + huh.NewGroup( + huh.NewConfirm().Title("Write config/config.json?").Affirmative("Write").Negative("Cancel").Value(&confirm), + ), + ) + + if done, err := runForm(form); err != nil || !done { + return err + } + + cfg.MinecraftVersion = strings.TrimSpace(mcVersion) + cfg.FabricLoaderVersion = strings.TrimSpace(fabricLoader) + cfg.LoaderMetaVersion = strings.TrimSpace(loaderMeta) + cfg.JavaPath = strings.TrimSpace(javaPath) + cfg.ServerLocation = strings.TrimSpace(serverLoc) + cfg.BackupLocation = strings.TrimSpace(backupLoc) + cfg.MergePreCommand = strings.TrimSpace(preCmd) + cfg.MergePostCommand = strings.TrimSpace(postCmd) + cfg.ModrinthMods = parseMods(modsText) + + if err := writeConfigFile(cfg); err != nil { + return err + } + + fmt.Printf("\n %s Wrote %s\n\n Run `benson` for the CLI, or `benson setup bot` to set up the Discord bot.\n\n", + check(), config.Path()) + return nil +} + +// RunBot is the Discord-bot wizard. It configures the bot token, channel and +// check interval, leaving the server-manager settings untouched. +func RunBot() error { + cfg := loadConfig() + api := loadAPI() + + interval := cfg.CheckIntervalMinutes + if interval <= 0 { + interval = 60 + } + + var ( + token = api.Token + channelID = cfg.ChannelID + intervalStr = strconv.Itoa(interval) + confirm = true + ) + + form := huh.NewForm( + huh.NewGroup( + huh.NewNote(). + Title("Benson bot setup"). + Description("Configure the Discord bot that posts Modrinth compatibility reports."), + ), + huh.NewGroup( + huh.NewInput().Title("Discord bot token").Description("Developer Portal → Bot → Reset/Copy Token").Password(true).Value(&token).Validate(required("token")), + huh.NewInput().Title("Channel ID").Description("Channel where reports are posted").Value(&channelID).Validate(required("channel ID")), + huh.NewInput().Title("Check interval (minutes)").Value(&intervalStr).Validate(positiveInt), + ).Title("Discord"), + huh.NewGroup( + huh.NewConfirm().Title("Write config/config.json and config/api.json?").Affirmative("Write").Negative("Cancel").Value(&confirm), + ), + ) + + if done, err := runForm(form); err != nil || !done { + return err + } + + minutes, _ := strconv.Atoi(strings.TrimSpace(intervalStr)) + cfg.ChannelID = strings.TrimSpace(channelID) + cfg.CheckIntervalMinutes = minutes + api.Token = strings.TrimSpace(token) + + if err := writeConfigFile(cfg); err != nil { + return err + } + if err := writeAPIFile(api); err != nil { + return err + } + + fmt.Printf("\n %s Wrote %s\n %s Wrote %s\n\n Run `benson bot` to start the bot.\n\n", + check(), config.Path(), check(), config.APIPath()) + return nil +} + +func loadConfig() *config.Config { + if cfg, err := config.Load(); err == nil { + return cfg + } + return &config.Config{} +} + +func loadAPI() *config.API { + if api, err := config.LoadAPI(); err == nil { + return api + } + return &config.API{} +} + +// runForm runs a themed form, reporting whether the user confirmed the write. +func runForm(form *huh.Form) (bool, error) { + if err := form.WithTheme(emberTheme()).Run(); err != nil { + if errors.Is(err, huh.ErrUserAborted) { + fmt.Println("Setup cancelled.") + return false, nil + } + return false, err + } + return true, nil +} + +func required(label string) func(string) error { + return func(s string) error { + if strings.TrimSpace(s) == "" { + return fmt.Errorf("%s is required", label) + } + return nil + } +} + +func positiveInt(s string) error { + n, err := strconv.Atoi(strings.TrimSpace(s)) + if err != nil || n <= 0 { + return errors.New("must be a positive number") + } + return nil +} + +func parseMods(text string) []string { + mods := []string{} + for _, line := range strings.Split(text, "\n") { + if slug := strings.TrimSpace(line); slug != "" { + mods = append(mods, slug) + } + } + return mods +} + +func check() string { + return lipgloss.NewStyle().Foreground(lipgloss.Color("#22C55E")).Render("✓") +} + +func emberTheme() *huh.Theme { + theme := huh.ThemeCharm() + ember := lipgloss.Color("#F97316") + theme.Focused.Title = theme.Focused.Title.Foreground(ember).Bold(true) + theme.Focused.Base = theme.Focused.Base.BorderForeground(ember) + return theme +} + +func writeConfigFile(cfg *config.Config) error { + if err := os.MkdirAll(config.Dir, 0o755); err != nil { + return err + } + data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return err + } + return os.WriteFile(config.Path(), append(data, '\n'), 0o644) +} + +func writeAPIFile(api *config.API) error { + if err := os.MkdirAll(config.Dir, 0o755); err != nil { + return err + } + data, err := json.MarshalIndent(api, "", " ") + if err != nil { + return err + } + return os.WriteFile(config.APIPath(), append(data, '\n'), 0o600) +} |
