Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c56192141 | |||
| 75c113909d |
@@ -9,7 +9,9 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
fyne.io/systray v1.12.1 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
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
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
fyne.io/systray v1.12.1 h1:ygBD6aZXwiOmZoY5N+ukbH9pih0Kq6fYgVeMYbr5skQ=
|
||||
fyne.io/systray v1.12.1/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||
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/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
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=
|
||||
@@ -12,6 +16,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
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/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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,24 @@
|
||||
//go:build !windows
|
||||
|
||||
package main
|
||||
|
||||
import "golang.design/x/hotkey"
|
||||
|
||||
func startHotkey(keybind string) (<-chan struct{}, error) {
|
||||
mods, key, err := parseKeybind(keybind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hk := hotkey.New(mods, key)
|
||||
if err := hk.Register(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make(chan struct{})
|
||||
go func() {
|
||||
for range hk.Keydown() {
|
||||
out <- struct{}{}
|
||||
}
|
||||
close(out)
|
||||
}()
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
)
|
||||
|
||||
func monkeyIcon() []byte {
|
||||
const size = 32
|
||||
img := image.NewRGBA(image.Rect(0, 0, size, size))
|
||||
|
||||
brown := color.RGBA{110, 72, 38, 255}
|
||||
tan := color.RGBA{210, 175, 130, 255}
|
||||
pink := color.RGBA{190, 130, 110, 255}
|
||||
dark := color.RGBA{15, 8, 3, 255}
|
||||
white := color.RGBA{255, 255, 255, 255}
|
||||
|
||||
// Ears drawn first so the head circle overlaps the inner edge naturally.
|
||||
fillCircle(img, 4, 13, 5, brown)
|
||||
fillCircle(img, 28, 13, 5, brown)
|
||||
fillCircle(img, 4, 13, 3, pink)
|
||||
fillCircle(img, 28, 13, 3, pink)
|
||||
|
||||
fillCircle(img, 16, 15, 13, brown) // head
|
||||
|
||||
fillEllipse(img, 16, 21, 7, 4, tan) // muzzle
|
||||
|
||||
// Eyes: light surround, dark iris, white highlight.
|
||||
fillCircle(img, 11, 12, 3, tan)
|
||||
fillCircle(img, 21, 12, 3, tan)
|
||||
fillCircle(img, 11, 12, 2, dark)
|
||||
fillCircle(img, 21, 12, 2, dark)
|
||||
fillCircle(img, 12, 11, 1, white)
|
||||
fillCircle(img, 22, 11, 1, white)
|
||||
|
||||
// Nostrils.
|
||||
fillCircle(img, 14, 21, 1, dark)
|
||||
fillCircle(img, 18, 21, 1, dark)
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = png.Encode(&buf, img)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func fillCircle(img *image.RGBA, cx, cy, r int, c color.RGBA) {
|
||||
for y := cy - r; y <= cy+r; y++ {
|
||||
for x := cx - r; x <= cx+r; x++ {
|
||||
dx, dy := x-cx, y-cy
|
||||
if dx*dx+dy*dy <= r*r {
|
||||
img.SetRGBA(x, y, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fillEllipse(img *image.RGBA, cx, cy, rx, ry int, c color.RGBA) {
|
||||
for y := cy - ry; y <= cy+ry; y++ {
|
||||
for x := cx - rx; x <= cx+rx; x++ {
|
||||
dx := float64(x-cx) / float64(rx)
|
||||
dy := float64(y-cy) / float64(ry)
|
||||
if dx*dx+dy*dy <= 1.0 {
|
||||
img.SetRGBA(x, y, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+76
-1
@@ -2,7 +2,14 @@
|
||||
|
||||
package main
|
||||
|
||||
import "golang.design/x/hotkey"
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.design/x/hotkey"
|
||||
)
|
||||
|
||||
var modMap = map[string]hotkey.Modifier{
|
||||
"ctrl": hotkey.ModCtrl,
|
||||
@@ -11,3 +18,71 @@ var modMap = map[string]hotkey.Modifier{
|
||||
"win": hotkey.ModWin,
|
||||
"super": hotkey.ModWin,
|
||||
}
|
||||
|
||||
var (
|
||||
user32 = syscall.NewLazyDLL("user32.dll")
|
||||
procRegisterHotKey = user32.NewProc("RegisterHotKey")
|
||||
procGetMessageW = user32.NewProc("GetMessageW")
|
||||
)
|
||||
|
||||
// winMsg mirrors the Win32 MSG struct.
|
||||
type winMsg struct {
|
||||
hwnd uintptr
|
||||
message uint32
|
||||
wParam uintptr
|
||||
lParam uintptr
|
||||
time uint32
|
||||
ptX int32
|
||||
ptY int32
|
||||
}
|
||||
|
||||
func startHotkey(keybind string) (<-chan struct{}, error) {
|
||||
mods, key, err := parseKeybind(keybind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// hotkey.Modifier and hotkey.Key values on Windows are the native Win32
|
||||
// MOD_* and VK_* constants, so they can be passed directly to RegisterHotKey.
|
||||
var modFlags uintptr
|
||||
for _, m := range mods {
|
||||
modFlags |= uintptr(m)
|
||||
}
|
||||
vk := uintptr(key)
|
||||
|
||||
ch := make(chan struct{}, 1)
|
||||
ready := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
runtime.LockOSThread()
|
||||
// Intentionally no defer UnlockOSThread — this goroutine owns its OS
|
||||
// thread for the lifetime of the app to keep the hotkey message queue alive.
|
||||
|
||||
ret, _, e := procRegisterHotKey.Call(0, 1, modFlags, vk)
|
||||
if ret == 0 {
|
||||
ready <- fmt.Errorf("RegisterHotKey: %w", e)
|
||||
return
|
||||
}
|
||||
close(ready)
|
||||
|
||||
const wmHotkey = 0x0312
|
||||
var msg winMsg
|
||||
for {
|
||||
r, _, _ := procGetMessageW.Call(
|
||||
uintptr(unsafe.Pointer(&msg)), 0, wmHotkey, wmHotkey,
|
||||
)
|
||||
if r == 0 || r == ^uintptr(0) { // WM_QUIT or error
|
||||
return
|
||||
}
|
||||
select {
|
||||
case ch <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := <-ready; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
@@ -3,14 +3,16 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fyne.io/systray"
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"golang.design/x/hotkey"
|
||||
"golang.design/x/mainthread"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -107,25 +109,46 @@ func toggleMute(dg *discordgo.Session, cfg *Config, muted bool) {
|
||||
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)
|
||||
log.Printf("warning: user %s: %v", vs.UserID, err)
|
||||
}
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count == 0 {
|
||||
fmt.Println(" (no users found in channel)")
|
||||
log.Println("(no users found in channel)")
|
||||
} else {
|
||||
fmt.Printf(" %d user(s) affected\n", count)
|
||||
log.Printf("%d user(s) affected", count)
|
||||
}
|
||||
}
|
||||
|
||||
func main() { mainthread.Init(run) }
|
||||
func setupLogging() {
|
||||
// Try exe directory first, fall back to temp dir (e.g. if installed in Program Files).
|
||||
var dirs []string
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
dirs = append(dirs, filepath.Dir(exe))
|
||||
}
|
||||
dirs = append(dirs, os.TempDir())
|
||||
|
||||
for _, dir := range dirs {
|
||||
logPath := filepath.Join(dir, "discord-lobby-mute.log")
|
||||
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
log.SetOutput(io.MultiWriter(os.Stderr, f))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func run() {
|
||||
setupLogging()
|
||||
log.Println("starting")
|
||||
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("config loaded")
|
||||
|
||||
dg, err := discordgo.New("Bot " + cfg.Token)
|
||||
if err != nil {
|
||||
@@ -138,35 +161,49 @@ func run() {
|
||||
close(ready)
|
||||
})
|
||||
|
||||
log.Println("connecting to Discord...")
|
||||
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)
|
||||
select {
|
||||
case <-ready:
|
||||
case <-time.After(30 * time.Second):
|
||||
log.Fatal("timed out waiting for Discord ready — check token and network connection")
|
||||
}
|
||||
log.Println("connected to Discord")
|
||||
|
||||
hk := hotkey.New(mods, key)
|
||||
if err := hk.Register(); err != nil {
|
||||
keydownCh, err := startHotkey(cfg.Keybind)
|
||||
if 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)
|
||||
log.Printf("Listening on %s — toggles server mute for channel %s", 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... ")
|
||||
go func() {
|
||||
muted := false
|
||||
for range keydownCh {
|
||||
muted = !muted
|
||||
if muted {
|
||||
log.Print("Muting all users...")
|
||||
} else {
|
||||
log.Print("Unmuting all users...")
|
||||
}
|
||||
toggleMute(dg, cfg, muted)
|
||||
}
|
||||
toggleMute(dg, cfg, muted)
|
||||
}
|
||||
}()
|
||||
|
||||
systray.Run(
|
||||
func() {
|
||||
systray.SetIcon(monkeyIcon())
|
||||
systray.SetTooltip("Discord Lobby Mute — " + cfg.Keybind)
|
||||
mQuit := systray.AddMenuItem("Quit", "Quit Discord Lobby Mute")
|
||||
go func() {
|
||||
<-mQuit.ClickedCh
|
||||
systray.Quit()
|
||||
}()
|
||||
},
|
||||
func() {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !windows
|
||||
|
||||
package main
|
||||
|
||||
import "golang.design/x/mainthread"
|
||||
|
||||
func main() { mainthread.Init(run) }
|
||||
@@ -0,0 +1,5 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
func main() { run() }
|
||||
Reference in New Issue
Block a user