Initial commit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
|||||||
|
# discord-lobby-mute
|
||||||
|
|
||||||
|
Runs in the system tray as a console app. Press a configurable global hotkey to toggle server-mute for everyone in a Discord voice channel.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Create a bot at https://discord.com/developers/applications
|
||||||
|
- Enable **Server Members Intent** under Privileged Gateway Intents
|
||||||
|
- Grant the **Mute Members** permission when inviting it to your server
|
||||||
|
2. Place `config.json` next to the `.exe` and fill in your values (see below)
|
||||||
|
3. Run `discord-lobby-mute.exe`
|
||||||
|
|
||||||
|
## config.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "YOUR_BOT_TOKEN",
|
||||||
|
"guild_id": "YOUR_SERVER_ID",
|
||||||
|
"channel_id": "YOUR_VOICE_CHANNEL_ID",
|
||||||
|
"keybind": "ctrl+shift+m"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To get IDs: enable **Developer Mode** in Discord settings, then right-click a server or channel and choose **Copy ID**.
|
||||||
|
|
||||||
|
## Keybind format
|
||||||
|
|
||||||
|
`modifier+modifier+key` — modifiers come first, key last. Case-insensitive.
|
||||||
|
|
||||||
|
Examples: `ctrl+shift+m`, `alt+f9`, `ctrl+alt+delete`
|
||||||
|
|
||||||
|
### Modifiers
|
||||||
|
|
||||||
|
| Value | Key |
|
||||||
|
|---------|--------------|
|
||||||
|
| `ctrl` | Control |
|
||||||
|
| `shift` | Shift |
|
||||||
|
| `alt` | Alt |
|
||||||
|
| `win` | Windows key |
|
||||||
|
| `super` | Windows key |
|
||||||
|
|
||||||
|
### Keys
|
||||||
|
|
||||||
|
| Value | Key |
|
||||||
|
|-------------------|----------------------|
|
||||||
|
| `a` – `z` | Letter keys |
|
||||||
|
| `0` – `9` | Number row |
|
||||||
|
| `f1` – `f20` | Function keys |
|
||||||
|
| `space` | Space |
|
||||||
|
| `enter` / `return`| Enter / Return |
|
||||||
|
| `escape` / `esc` | Escape |
|
||||||
|
| `tab` | Tab |
|
||||||
|
| `delete` | Delete |
|
||||||
|
| `left` | Left arrow |
|
||||||
|
| `right` | Right arrow |
|
||||||
|
| `up` | Up arrow |
|
||||||
|
| `down` | Down arrow |
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Cross-compile for Windows from Linux/macOS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o discord-lobby-mute.exe .
|
||||||
|
```
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"token": "YOUR_BOT_TOKEN_HERE",
|
||||||
|
"guild_id": "YOUR_SERVER_ID",
|
||||||
|
"channel_id": "YOUR_VOICE_CHANNEL_ID",
|
||||||
|
"keybind": "ctrl+shift+m"
|
||||||
|
}
|
||||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,15 @@
|
|||||||
|
module discord-lobby-mute
|
||||||
|
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bwmarrin/discordgo v0.29.0
|
||||||
|
golang.design/x/hotkey v0.4.1
|
||||||
|
golang.design/x/mainthread v0.3.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gorilla/websocket v1.4.2 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
|
||||||
|
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||||
|
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
golang.design/x/hotkey v0.4.1 h1:zLP/2Pztl4WjyxURdW84GoZ5LUrr6hr69CzJFJ5U1go=
|
||||||
|
golang.design/x/hotkey v0.4.1/go.mod h1:M8SGcwFYHnKRa83FpTFQoZvPO5vVT+kWPztFqTQKmXA=
|
||||||
|
golang.design/x/mainthread v0.3.0 h1:UwFus0lcPodNpMOGoQMe87jSFwbSsEY//CA7yVmu4j8=
|
||||||
|
golang.design/x/mainthread v0.3.0/go.mod h1:vYX7cF2b3pTJMGM/hc13NmN6kblKnf4/IyvHeu259L0=
|
||||||
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
|
||||||
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/sys v0.0.0-20201022201747-fb209a7c41cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
count := 0
|
||||||
|
for _, vs := range guild.VoiceStates {
|
||||||
|
if vs.ChannelID == cfg.ChannelID {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user