package main import ( "encoding/json" "fmt" "log" "os" "path/filepath" "strings" "github.com/bwmarrin/discordgo" "golang.design/x/hotkey" "golang.design/x/mainthread" ) type Config struct { Token string `json:"token"` GuildID string `json:"guild_id"` ChannelID string `json:"channel_id"` Keybind string `json:"keybind"` ExemptUsers []string `json:"exempt_users"` } func loadConfig() (*Config, error) { exe, _ := os.Executable() for _, dir := range []string{filepath.Dir(exe), "."} { data, err := os.ReadFile(filepath.Join(dir, "config.json")) if err != nil { continue } var cfg Config if err := json.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("parsing config.json: %w", err) } return &cfg, nil } return nil, fmt.Errorf("config.json not found next to executable or in working directory") } var modMap = map[string]hotkey.Modifier{ "ctrl": hotkey.ModCtrl, "shift": hotkey.ModShift, "alt": hotkey.ModAlt, "win": hotkey.ModWin, "super": hotkey.ModWin, } var keyMap = map[string]hotkey.Key{ "a": hotkey.KeyA, "b": hotkey.KeyB, "c": hotkey.KeyC, "d": hotkey.KeyD, "e": hotkey.KeyE, "f": hotkey.KeyF, "g": hotkey.KeyG, "h": hotkey.KeyH, "i": hotkey.KeyI, "j": hotkey.KeyJ, "k": hotkey.KeyK, "l": hotkey.KeyL, "m": hotkey.KeyM, "n": hotkey.KeyN, "o": hotkey.KeyO, "p": hotkey.KeyP, "q": hotkey.KeyQ, "r": hotkey.KeyR, "s": hotkey.KeyS, "t": hotkey.KeyT, "u": hotkey.KeyU, "v": hotkey.KeyV, "w": hotkey.KeyW, "x": hotkey.KeyX, "y": hotkey.KeyY, "z": hotkey.KeyZ, "0": hotkey.Key0, "1": hotkey.Key1, "2": hotkey.Key2, "3": hotkey.Key3, "4": hotkey.Key4, "5": hotkey.Key5, "6": hotkey.Key6, "7": hotkey.Key7, "8": hotkey.Key8, "9": hotkey.Key9, "f1": hotkey.KeyF1, "f2": hotkey.KeyF2, "f3": hotkey.KeyF3, "f4": hotkey.KeyF4, "f5": hotkey.KeyF5, "f6": hotkey.KeyF6, "f7": hotkey.KeyF7, "f8": hotkey.KeyF8, "f9": hotkey.KeyF9, "f10": hotkey.KeyF10, "f11": hotkey.KeyF11, "f12": hotkey.KeyF12, "f13": hotkey.KeyF13, "f14": hotkey.KeyF14, "f15": hotkey.KeyF15, "f16": hotkey.KeyF16, "f17": hotkey.KeyF17, "f18": hotkey.KeyF18, "f19": hotkey.KeyF19, "f20": hotkey.KeyF20, "space": hotkey.KeySpace, "return": hotkey.KeyReturn, "enter": hotkey.KeyReturn, "escape": hotkey.KeyEscape, "esc": hotkey.KeyEscape, "tab": hotkey.KeyTab, "delete": hotkey.KeyDelete, "left": hotkey.KeyLeft, "right": hotkey.KeyRight, "up": hotkey.KeyUp, "down": hotkey.KeyDown, } func parseKeybind(s string) ([]hotkey.Modifier, hotkey.Key, error) { parts := strings.Split(strings.ToLower(strings.TrimSpace(s)), "+") if len(parts) == 0 { return nil, 0, fmt.Errorf("empty keybind") } var mods []hotkey.Modifier for _, p := range parts[:len(parts)-1] { p = strings.TrimSpace(p) m, ok := modMap[p] if !ok { return nil, 0, fmt.Errorf("unknown modifier %q — valid: ctrl, shift, alt, win", p) } mods = append(mods, m) } keyStr := strings.TrimSpace(parts[len(parts)-1]) k, ok := keyMap[keyStr] if !ok { return nil, 0, fmt.Errorf("unknown key %q — valid: a-z, 0-9, f1-f20, space, enter, escape, tab, delete, left, right, up, down", keyStr) } return mods, k, nil } func toggleMute(dg *discordgo.Session, cfg *Config, muted bool) { guild, err := dg.State.Guild(cfg.GuildID) if err != nil { log.Printf("guild not in state cache (is the bot in the server?): %v", err) return } exempt := make(map[string]bool, len(cfg.ExemptUsers)) for _, id := range cfg.ExemptUsers { exempt[id] = true } count := 0 for _, vs := range guild.VoiceStates { if vs.ChannelID == cfg.ChannelID && !exempt[vs.UserID] { if err := dg.GuildMemberMute(cfg.GuildID, vs.UserID, muted); err != nil { log.Printf(" warning: user %s: %v", vs.UserID, err) } count++ } } if count == 0 { fmt.Println(" (no users found in channel)") } else { fmt.Printf(" %d user(s) affected\n", count) } } func main() { mainthread.Init(run) } func run() { cfg, err := loadConfig() if err != nil { log.Fatal(err) } dg, err := discordgo.New("Bot " + cfg.Token) if err != nil { log.Fatal(err) } dg.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsGuildVoiceStates ready := make(chan struct{}) dg.AddHandler(func(_ *discordgo.Session, _ *discordgo.Ready) { close(ready) }) if err := dg.Open(); err != nil { log.Fatalf("connect to Discord: %v", err) } defer dg.Close() <-ready fmt.Println("Connected to Discord.") mods, key, err := parseKeybind(cfg.Keybind) if err != nil { log.Fatalf("keybind: %v", err) } hk := hotkey.New(mods, key) if err := hk.Register(); err != nil { log.Fatalf("register hotkey %q: %v", cfg.Keybind, err) } defer hk.Unregister() fmt.Printf("Listening on %s — toggles server mute for channel %s\n", cfg.Keybind, cfg.ChannelID) muted := false for range hk.Keydown() { muted = !muted if muted { fmt.Print("Muting all users... ") } else { fmt.Print("Unmuting all users... ") } toggleMute(dg, cfg, muted) } }