From 3ef9fc40bf9218ae76a1f7f7ae8b9b2f3ff16858 Mon Sep 17 00:00:00 2001 From: Tyler Stiene Date: Tue, 19 Jan 2021 01:06:08 -0500 Subject: [PATCH] refactor discord dm disable text messages for discord and mumble with option list users connected to discord on connection to mumble --- Makefile | 2 +- README.md | 46 +++++---- bridge.go | 247 ++++++++++++++++++++++++++------------------ config.go | 16 +-- discord-handlers.go | 227 ++++++++++++++++++++++++++++++++++++++++ discord.go | 108 +++++++++---------- handlers.go | 236 ------------------------------------------ main.go | 146 ++++++++++++++------------ mumble-handlers.go | 69 +++++++++++++ mumble.go | 8 +- 10 files changed, 621 insertions(+), 484 deletions(-) create mode 100644 discord-handlers.go delete mode 100644 handlers.go create mode 100644 mumble-handlers.go diff --git a/Makefile b/Makefile index 84e785d..221c93c 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -GOFILES=main.go mumble.go discord.go bridge.go config.go handlers.go +GOFILES=main.go mumble.go discord.go bridge.go config.go mumble-handlers.go discord-handlers.go mumble-discord-bridge: $(GOFILES) goreleaser build --skip-validate --rm-dist diff --git a/README.md b/README.md index a4c4d8d..2979a60 100644 --- a/README.md +++ b/README.md @@ -11,29 +11,35 @@ All variables can be set using flags or in the environment. The binary will also attempt to load .env file located in the working directory. ```bash -Usage of mumble-discord-bridge: +Usage of ./mumble-discord-bridge: -discord-cid string - DISCORD_CID, discord channel ID - -discord-gid string - DISCORD_GID, discord guild ID - -discord-token string - DISCORD_TOKEN, discord bot token + DISCORD_CID, discord cid, required -discord-command string - DISCORD_COMMAND, the string to look for when manually entering commands in Discord (in the form of !DISCORD_COMMAND) - -mumble-address string - MUMBLE_ADDRESS, mumble server address, example example.com - -mumble-password string - MUMBLE_PASSWORD, mumble password, optional - -mumble-port int - MUMBLE_PORT mumble port (default 64738) - -mumble-username string - MUMBLE_USERNAME, mumble username (default "discord-bridge") - -mumble-insecure bool - MUMBLE_INSECURE, allow connection to insecure (invalid TLS cert) mumble server - -mumble-channel string - MUMBLE_CHANNEL, pick what channel the bridge joins in Mumble. Must be a direct child of Root. + DISCORD_COMMAND, Discord command string, env alt DISCORD_COMMAND, optional, (defaults mumble-discord) (default "mumble-discord") + -discord-disable-text + DISCORD_DISABLE_TEXT, disable sending direct messages to discord, (default false) + -discord-gid string + DISCORD_GID, discord gid, required + -discord-token string + DISCORD_TOKEN, discord bot token, required -mode string - MODE, determines what mode the bridge starts in + MODE, [constant, manual, auto] determine which mode the bridge starts in, (default constant) (default "constant") + -mumble-address string + MUMBLE_ADDRESS, mumble server address, example example.com, required + -mumble-channel string + MUMBLE_CHANNEL, mumble channel to start in, optional + -mumble-disable-text + MUMBLE_DISABLE_TEXT, disable sending text to mumble, (default false) + -mumble-insecure + MUMBLE_INSECURE, mumble insecure, optional + -mumble-password string + MUMBLE_PASSWORD, mumble password, optional + -mumble-port int + MUMBLE_PORT, mumble port, (default 64738) (default 64738) + -mumble-username string + MUMBLE_USERNAME, mumble username, (default: discord) (default "Discord") + -nice + NICE, whether the bridge should automatically try to 'nice' itself, (default false) ``` The bridge can be run with the follow modes: diff --git a/bridge.go b/bridge.go index b1a771f..16d96fb 100644 --- a/bridge.go +++ b/bridge.go @@ -5,183 +5,234 @@ import ( "fmt" "log" "net" - "os" - "os/signal" "strconv" + "sync" "time" "github.com/bwmarrin/discordgo" "layeh.com/gumble/gumble" ) -//BridgeState manages dynamic information about the bridge during runtime -type BridgeState struct { - ActiveConn chan bool - Connected bool - Mode bridgeMode - Client *gumble.Client - DiscordUsers map[string]bool - MumbleUsers map[string]bool - MumbleUserCount int - DiscordUserCount int - AutoChan chan bool +type discordUser struct { + username string + seen bool + dm *discordgo.Channel } -func startBridge(discord *discordgo.Session, discordGID string, discordCID string, l *Listener, die chan bool) { - dgv, err := discord.ChannelVoiceJoin(discordGID, discordCID, false, false) +//BridgeState manages dynamic information about the bridge during runtime +type BridgeState struct { + // The configuration data for this bridge + BridgeConfig *BridgeConfig + + // TODO + BridgeDie chan bool + + // Bridge connection + Connected bool + + // The bridge mode constant, auto, manual. Default is constant. + Mode bridgeMode + + // Discord session. This is created and outside the bridge state + DiscordSession *discordgo.Session + + // Discord voice connection. Empty if not connected. + DiscordVoice *discordgo.VoiceConnection + + // Mumble client. Empty if not connected. + MumbleClient *gumble.Client + + // Map of Discord users tracked by this bridge. + DiscordUsers map[string]discordUser + DiscordUsersMutex sync.Mutex + + // Map of Mumble users tracked by this bridge + MumbleUsers map[string]bool + MumbleUsersMutex sync.Mutex + + // Kill the auto connect routine + AutoChanDie chan bool + + // Discord Duplex and Event Listener + DiscordStream *DiscordDuplex + DiscordListener *DiscordListener + + // Mumble Duplex and Event Listener + MumbleStream *MumbleDuplex + MumbleListener *MumbleListener +} + +// startBridge established the voice connection +func (b *BridgeState) startBridge() { + + b.BridgeDie = make(chan bool) + + var err error + + // DISCORD Connect Voice + + b.DiscordVoice, err = b.DiscordSession.ChannelVoiceJoin(b.BridgeConfig.GID, b.BridgeConfig.CID, false, false) if err != nil { log.Println(err) return } - defer dgv.Speaking(false) - defer dgv.Close() + defer b.DiscordVoice.Speaking(false) + defer b.DiscordVoice.Close() - // MUMBLE Setup + // MUMBLE Connect - m := MumbleDuplex{ - Close: make(chan bool), + b.MumbleStream = &MumbleDuplex{ + die: b.BridgeDie, } + det := b.BridgeConfig.MumbleConfig.AudioListeners.Attach(b.MumbleStream) var tlsConfig tls.Config - if l.BridgeConf.MumbleInsecure { + if b.BridgeConfig.MumbleInsecure { tlsConfig.InsecureSkipVerify = true } - mumble, err := gumble.DialWithDialer(new(net.Dialer), l.BridgeConf.MumbleAddr, l.BridgeConf.Config, &tlsConfig) + b.MumbleClient, err = gumble.DialWithDialer(new(net.Dialer), b.BridgeConfig.MumbleAddr, b.BridgeConfig.MumbleConfig, &tlsConfig) if err != nil { log.Println(err) return } - defer mumble.Disconnect() - l.Bridge.Client = mumble + defer b.MumbleClient.Disconnect() + // Shared Channels // Shared channels pass PCM information in 10ms chunks [480]int16 - var toMumble = mumble.AudioOutgoing() + // These channels are internal and are not added to the bridge state. + var toMumble = b.MumbleClient.AudioOutgoing() var toDiscord = make(chan []int16, 100) log.Println("Mumble Connected") // Start Passing Between - // Mumble - go m.fromMumbleMixer(toDiscord, die) - det := l.BridgeConf.Config.AudioListeners.Attach(m) - //Discord - go discordReceivePCM(dgv, die) - go fromDiscordMixer(toMumble, die) - go discordSendPCM(dgv, toDiscord, die) - c := make(chan os.Signal) - signal.Notify(c, os.Interrupt) + // From Mumble + go b.MumbleStream.fromMumbleMixer(toDiscord, b.BridgeDie) + + // From Discord + b.DiscordStream = &DiscordDuplex{ + Bridge: b, + fromDiscordMap: make(map[uint32]fromDiscord), + die: b.BridgeDie, + } + + go b.DiscordStream.discordReceivePCM() + go b.DiscordStream.fromDiscordMixer(toMumble) + + // To Discord + go b.DiscordStream.discordSendPCM(toDiscord) go func() { ticker := time.NewTicker(500 * time.Millisecond) for { <-ticker.C - if mumble.State() != 2 { - log.Println("Lost mumble connection " + strconv.Itoa(int(mumble.State()))) + if b.MumbleClient.State() != 2 { + log.Println("Lost mumble connection " + strconv.Itoa(int(b.MumbleClient.State()))) select { default: - close(die) - case <-die: + close(b.BridgeDie) + case <-b.BridgeDie: //die is already closed } - - select { - default: - close(m.Close) - case <-m.Close: - //m.Close is already closed - } - return } } }() - l.ConnectedLock.Lock() - l.Bridge.Connected = true - l.ConnectedLock.Unlock() + + b.Connected = true select { - case sig := <-c: - log.Printf("\nGot %s signal. Terminating Mumble-Bridge\n", sig) - case <-die: + case <-b.BridgeDie: log.Println("\nGot internal die request. Terminating Mumble-Bridge") - dgv.Disconnect() + b.DiscordVoice.Disconnect() det.Detach() - close(die) - close(m.Close) + close(toDiscord) close(toMumble) - l.Bridge.Connected = false - l.Bridge.Client = nil - l.Bridge.MumbleUserCount = 0 - l.Bridge.MumbleUsers = make(map[string]bool) - l.Bridge.DiscordUserCount = 0 - l.Bridge.DiscordUsers = make(map[string]bool) + b.Connected = false + b.DiscordVoice = nil + b.MumbleClient = nil + b.MumbleUsers = make(map[string]bool) + b.DiscordUsers = make(map[string]discordUser) } } -func discordStatusUpdate(dg *discordgo.Session, host, port string, l *Listener) { - status := "" - curr := 0 +func (b *BridgeState) discordStatusUpdate() { m, _ := time.ParseDuration("30s") for { time.Sleep(3 * time.Second) - resp, err := gumble.Ping(host+":"+port, -1, m) + resp, err := gumble.Ping(b.BridgeConfig.MumbleAddr, -1, m) + status := "" if err != nil { log.Printf("error pinging mumble server %v\n", err) - dg.UpdateListeningStatus("an error pinging mumble") + b.DiscordSession.UpdateListeningStatus("an error pinging mumble") } else { - l.UserCountLock.Lock() - l.ConnectedLock.Lock() - curr = resp.ConnectedUsers - if l.Bridge.Connected { - curr = curr - 1 + b.MumbleUsersMutex.Lock() + userCount := resp.ConnectedUsers + if b.Connected { + userCount = userCount - 1 } - if curr != l.Bridge.MumbleUserCount { - l.Bridge.MumbleUserCount = curr - } - if curr == 0 { - status = "" + if userCount == 0 { + status = "No users in Mumble" } else { - if len(l.Bridge.MumbleUsers) > 0 { - status = fmt.Sprintf("%v/%v users in Mumble\n", len(l.Bridge.MumbleUsers), curr) + if len(b.MumbleUsers) > 0 { + status = fmt.Sprintf("%v/%v users in Mumble\n", len(b.MumbleUsers), userCount) } else { - status = fmt.Sprintf("%v users in Mumble\n", curr) + status = fmt.Sprintf("%v users in Mumble\n", userCount) } } - l.ConnectedLock.Unlock() - l.UserCountLock.Unlock() - dg.UpdateListeningStatus(status) + b.MumbleUsersMutex.Unlock() + b.DiscordSession.UpdateListeningStatus(status) } } } -//AutoBridge starts a goroutine to check the number of users in discord and mumble -//when there is at least one user on both, starts up the bridge -//when there are no users on either side, kills the bridge -func AutoBridge(s *discordgo.Session, l *Listener) { +// AutoBridge starts a goroutine to check the number of users in discord and mumble +// when there is at least one user on both, starts up the bridge +// when there are no users on either side, kills the bridge +func (b *BridgeState) AutoBridge() { log.Println("beginning auto mode") + ticker := time.NewTicker(3 * time.Second) + for { select { - default: - case <-l.Bridge.AutoChan: + case <-ticker.C: + case <-b.AutoChanDie: log.Println("ending automode") return } - time.Sleep(3 * time.Second) - l.UserCountLock.Lock() - if !l.Bridge.Connected && l.Bridge.MumbleUserCount > 0 && l.Bridge.DiscordUserCount > 0 { + + b.MumbleUsersMutex.Lock() + b.DiscordUsersMutex.Lock() + + if !b.Connected && len(b.MumbleUsers) > 0 && len(b.DiscordUsers) > 0 { log.Println("users detected in mumble and discord, bridging") - die := make(chan bool) - l.Bridge.ActiveConn = die - go startBridge(s, l.BridgeConf.GID, l.BridgeConf.CID, l, die) + go b.startBridge() } - if l.Bridge.Connected && l.Bridge.MumbleUserCount == 0 && l.Bridge.DiscordUserCount <= 1 { + if b.Connected && len(b.MumbleUsers) == 0 && len(b.DiscordUsers) <= 1 { log.Println("no one online, killing bridge") - l.Bridge.ActiveConn <- true - l.Bridge.ActiveConn = nil + b.BridgeDie <- true + b.BridgeDie = nil } - l.UserCountLock.Unlock() + + b.MumbleUsersMutex.Unlock() + b.DiscordUsersMutex.Unlock() } } + +func (b *BridgeState) discordSendMessageAll(msg string) { + if b.BridgeConfig.DiscordDisableText { + return + } + + b.DiscordUsersMutex.Lock() + for id := range b.DiscordUsers { + du := b.DiscordUsers[id] + if du.dm != nil { + b.DiscordSession.ChannelMessageSend(du.dm.ID, msg) + } + } + b.DiscordUsersMutex.Unlock() +} diff --git a/config.go b/config.go index 4522085..71148ab 100644 --- a/config.go +++ b/config.go @@ -21,13 +21,15 @@ const ( //BridgeConfig holds configuration information set at startup //It should not change during runtime type BridgeConfig struct { - Config *gumble.Config - MumbleAddr string - MumbleInsecure bool - MumbleChannel string - Command string - GID string - CID string + MumbleConfig *gumble.Config + MumbleAddr string + MumbleInsecure bool + MumbleChannel string + MumbleDisableText bool + Command string + GID string + CID string + DiscordDisableText bool } func lookupEnvOrString(key string, defaultVal string) string { diff --git a/discord-handlers.go b/discord-handlers.go new file mode 100644 index 0000000..f56fe75 --- /dev/null +++ b/discord-handlers.go @@ -0,0 +1,227 @@ +package main + +import ( + "fmt" + "log" + "strings" + "time" + + "github.com/bwmarrin/discordgo" +) + +// DiscordListener holds references to the current BridgeConf +// and BridgeState for use by the event handlers +type DiscordListener struct { + Bridge *BridgeState +} + +func (l *DiscordListener) ready(s *discordgo.Session, event *discordgo.Ready) { + log.Println("READY event registered") + + //Setup initial discord state + var g *discordgo.Guild + g = nil + + for _, i := range event.Guilds { + if i.ID == l.Bridge.BridgeConfig.GID { + g = i + } + } + + if g == nil { + log.Println("bad guild on READY") + return + } + + for _, vs := range g.VoiceStates { + if vs.ChannelID == l.Bridge.BridgeConfig.CID { + + u, err := s.User(vs.UserID) + if err != nil { + log.Println("error looking up username") + } + + dm, err := s.UserChannelCreate(u.ID) + if err != nil { + log.Println("Error creating private channel for", u.Username) + } + + l.Bridge.DiscordUsersMutex.Lock() + l.Bridge.DiscordUsers[vs.UserID] = discordUser{ + username: u.Username, + seen: true, + dm: dm, + } + l.Bridge.DiscordUsersMutex.Unlock() + + // If connected to mumble inform users of Discord users + if l.Bridge.Connected && !l.Bridge.BridgeConfig.MumbleDisableText { + l.Bridge.MumbleClient.Do(func() { + l.Bridge.MumbleClient.Self.Channel.Send(fmt.Sprintf("%v has joined Discord\n", u.Username), false) + }) + } + + } + } +} + +func (l *DiscordListener) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { + + if l.Bridge.Mode == bridgeModeConstant { + return + } + + // Ignore all messages created by the bot itself + if m.Author.ID == s.State.User.ID { + return + } + // Find the channel that the message came from. + c, err := s.State.Channel(m.ChannelID) + if err != nil { + // Could not find channel. + return + } + + // Find the guild for that channel. + g, err := s.State.Guild(c.GuildID) + if err != nil { + // Could not find guild. + return + } + prefix := "!" + l.Bridge.BridgeConfig.Command + if strings.HasPrefix(m.Content, prefix+" link") { + // Look for the message sender in that guild's current voice states. + for _, vs := range g.VoiceStates { + if vs.UserID == m.Author.ID { + log.Printf("Trying to join GID %v and VID %v\n", g.ID, vs.ChannelID) + go l.Bridge.startBridge() + return + } + } + } + + if strings.HasPrefix(m.Content, prefix+" unlink") { + // Look for the message sender in that guild's current voice states. + for _, vs := range g.VoiceStates { + if vs.UserID == m.Author.ID { + log.Printf("Trying to leave GID %v and VID %v\n", g.ID, vs.ChannelID) + l.Bridge.BridgeDie <- true + l.Bridge.BridgeDie = nil + return + } + } + } + + if strings.HasPrefix(m.Content, prefix+" refresh") { + // Look for the message sender in that guild's current voice states. + for _, vs := range g.VoiceStates { + if vs.UserID == m.Author.ID { + log.Printf("Trying to refresh GID %v and VID %v\n", g.ID, vs.ChannelID) + l.Bridge.BridgeDie <- true + + time.Sleep(5 * time.Second) + + go l.Bridge.startBridge() + return + } + } + } + + if strings.HasPrefix(m.Content, prefix+" auto") { + if l.Bridge.Mode != bridgeModeAuto { + l.Bridge.Mode = bridgeModeAuto + l.Bridge.AutoChanDie = make(chan bool) + go l.Bridge.AutoBridge() + } else { + l.Bridge.AutoChanDie <- true + l.Bridge.Mode = bridgeModeManual + } + } +} + +func (l *DiscordListener) guildCreate(s *discordgo.Session, event *discordgo.GuildCreate) { + + if event.Guild.Unavailable { + return + } + + for _, channel := range event.Guild.Channels { + if channel.ID == event.Guild.ID { + log.Println("Mumble-Discord bridge is active in new guild") + return + } + } +} + +func (l *DiscordListener) voiceUpdate(s *discordgo.Session, event *discordgo.VoiceStateUpdate) { + l.Bridge.DiscordUsersMutex.Lock() + defer l.Bridge.DiscordUsersMutex.Unlock() + + if event.GuildID == l.Bridge.BridgeConfig.GID { + + g, err := s.State.Guild(l.Bridge.BridgeConfig.GID) + if err != nil { + log.Println("error finding guild") + panic(err) + } + + for u := range l.Bridge.DiscordUsers { + du := l.Bridge.DiscordUsers[u] + du.seen = false + l.Bridge.DiscordUsers[u] = du + } + + // Sync the channel voice states to the local discordUsersMap + for _, vs := range g.VoiceStates { + if vs.ChannelID == l.Bridge.BridgeConfig.CID { + if s.State.User.ID == vs.UserID { + // Ignore bot + continue + } + + if _, ok := l.Bridge.DiscordUsers[vs.UserID]; !ok { + + u, err := s.User(vs.UserID) + if err != nil { + log.Println("error looking up username") + continue + } + + println("User joined Discord " + u.Username) + dm, err := s.UserChannelCreate(u.ID) + if err != nil { + log.Println("Error creating private channel for", u.Username) + } + l.Bridge.DiscordUsers[vs.UserID] = discordUser{ + username: u.Username, + seen: true, + dm: dm, + } + if l.Bridge.Connected && !l.Bridge.BridgeConfig.MumbleDisableText { + l.Bridge.MumbleClient.Do(func() { + l.Bridge.MumbleClient.Self.Channel.Send(fmt.Sprintf("%v has joined Discord\n", u.Username), false) + }) + } + } else { + du := l.Bridge.DiscordUsers[vs.UserID] + du.seen = true + l.Bridge.DiscordUsers[vs.UserID] = du + } + + } + } + + // Remove users that are no longer connected + for id := range l.Bridge.DiscordUsers { + if l.Bridge.DiscordUsers[id].seen == false { + println("User left Discord channel " + l.Bridge.DiscordUsers[id].username) + if l.Bridge.Connected && !l.Bridge.BridgeConfig.MumbleDisableText { + l.Bridge.MumbleClient.Do(func() { + l.Bridge.MumbleClient.Self.Channel.Send(fmt.Sprintf("%v has left Discord channel\n", l.Bridge.DiscordUsers[id].username), false) + }) + } + delete(l.Bridge.DiscordUsers, id) + } + } + } +} diff --git a/discord.go b/discord.go index 98cbcd8..4224d5a 100644 --- a/discord.go +++ b/discord.go @@ -18,9 +18,16 @@ type fromDiscord struct { streaming bool } -var discordMutex sync.Mutex -var discordMixerMutex sync.Mutex -var fromDiscordMap = make(map[uint32]fromDiscord) +// DiscordDuplex Handle discord voice stream +type DiscordDuplex struct { + Bridge *BridgeState + + discordMutex sync.Mutex + discordMixerMutex sync.Mutex + fromDiscordMap map[uint32]fromDiscord + + die chan bool +} // OnError gets called by dgvoice when an error is encountered. // By default logs to STDERR @@ -36,7 +43,7 @@ var OnError = func(str string, err error) { // SendPCM will receive on the provied channel encode // received PCM data into Opus then send that to Discordgo -func discordSendPCM(v *discordgo.VoiceConnection, pcm <-chan []int16, die chan bool) { +func (dd *DiscordDuplex) discordSendPCM(pcm <-chan []int16) { const channels int = 1 const frameRate int = 48000 // audio sampling rate const frameSize int = 960 // uint16 size of each audio frame @@ -57,7 +64,7 @@ func discordSendPCM(v *discordgo.VoiceConnection, pcm <-chan []int16, die chan b for { select { - case <-die: + case <-dd.die: log.Println("Killing discordSendPCM") return default: @@ -65,7 +72,7 @@ func discordSendPCM(v *discordgo.VoiceConnection, pcm <-chan []int16, die chan b <-ticker.C if len(pcm) > 1 { if !streaming { - v.Speaking(true) + dd.Bridge.DiscordVoice.Speaking(true) streaming = true } @@ -79,11 +86,11 @@ func discordSendPCM(v *discordgo.VoiceConnection, pcm <-chan []int16, die chan b continue } - if v.Ready == false || v.OpusSend == nil { + if dd.Bridge.DiscordVoice.Ready == false || dd.Bridge.DiscordVoice.OpusSend == nil { if lastReady == true { - OnError(fmt.Sprintf("Discordgo not ready for opus packets. %+v : %+v", v.Ready, v.OpusSend), nil) + OnError(fmt.Sprintf("Discordgo not ready for opus packets. %+v : %+v", dd.Bridge.DiscordVoice.Ready, dd.Bridge.DiscordVoice.OpusSend), nil) readyTimeout = time.AfterFunc(30*time.Second, func() { - die <- true + dd.die <- true }) lastReady = false } @@ -93,10 +100,10 @@ func discordSendPCM(v *discordgo.VoiceConnection, pcm <-chan []int16, die chan b lastReady = true readyTimeout.Stop() } - v.OpusSend <- opus + dd.Bridge.DiscordVoice.OpusSend <- opus } else { if streaming { - v.Speaking(false) + dd.Bridge.DiscordVoice.Speaking(false) streaming = false } } @@ -105,25 +112,19 @@ func discordSendPCM(v *discordgo.VoiceConnection, pcm <-chan []int16, die chan b // ReceivePCM will receive on the the Discordgo OpusRecv channel and decode // the opus audio into PCM then send it on the provided channel. -func discordReceivePCM(v *discordgo.VoiceConnection, die chan bool) { +func (dd *DiscordDuplex) discordReceivePCM() { var err error lastReady := true var readyTimeout *time.Timer for { - select { - case <-die: - log.Println("killing discord ReceivePCM") - return - default: - } - if v.Ready == false || v.OpusRecv == nil { + if dd.Bridge.DiscordVoice.Ready == false || dd.Bridge.DiscordVoice.OpusRecv == nil { if lastReady == true { - OnError(fmt.Sprintf("Discordgo not to receive opus packets. %+v : %+v", v.Ready, v.OpusSend), nil) + OnError(fmt.Sprintf("Discordgo not to receive opus packets. %+v : %+v", dd.Bridge.DiscordVoice.Ready, dd.Bridge.DiscordVoice.OpusSend), nil) readyTimeout = time.AfterFunc(30*time.Second, func() { log.Println("set ready timeout") - die <- true + dd.die <- true }) lastReady = false } @@ -133,22 +134,25 @@ func discordReceivePCM(v *discordgo.VoiceConnection, die chan bool) { lastReady = true readyTimeout.Stop() } + var ok bool var p *discordgo.Packet + select { - case p, ok = <-v.OpusRecv: - case <-die: + case p, ok = <-dd.Bridge.DiscordVoice.OpusRecv: + case <-dd.die: log.Println("killing discord ReceivePCM") return } + if !ok { log.Println("Opus not ok") continue } - discordMutex.Lock() - _, ok = fromDiscordMap[p.SSRC] - discordMutex.Unlock() + dd.discordMutex.Lock() + _, ok = dd.fromDiscordMap[p.SSRC] + dd.discordMutex.Unlock() if !ok { newStream := fromDiscord{} newStream.pcm = make(chan []int16, 100) @@ -158,14 +162,14 @@ func discordReceivePCM(v *discordgo.VoiceConnection, die chan bool) { OnError("error creating opus decoder", err) continue } - discordMutex.Lock() - fromDiscordMap[p.SSRC] = newStream - discordMutex.Unlock() + dd.discordMutex.Lock() + dd.fromDiscordMap[p.SSRC] = newStream + dd.discordMutex.Unlock() } - discordMutex.Lock() - p.PCM, err = fromDiscordMap[p.SSRC].decoder.Decode(p.Opus, 960, false) - discordMutex.Unlock() + dd.discordMutex.Lock() + p.PCM, err = dd.fromDiscordMap[p.SSRC].decoder.Decode(p.Opus, 960, false) + dd.discordMutex.Unlock() if err != nil { OnError("Error decoding opus data", err) continue @@ -175,62 +179,62 @@ func discordReceivePCM(v *discordgo.VoiceConnection, die chan bool) { continue } - discordMutex.Lock() + dd.discordMutex.Lock() select { - case fromDiscordMap[p.SSRC].pcm <- p.PCM[0:480]: + case dd.fromDiscordMap[p.SSRC].pcm <- p.PCM[0:480]: default: log.Println("fromDiscordMap buffer full. Dropping packet") - discordMutex.Unlock() + dd.discordMutex.Unlock() continue } select { - case fromDiscordMap[p.SSRC].pcm <- p.PCM[480:960]: + case dd.fromDiscordMap[p.SSRC].pcm <- p.PCM[480:960]: default: log.Println("fromDiscordMap buffer full. Dropping packet") } - discordMutex.Unlock() + dd.discordMutex.Unlock() } } -func fromDiscordMixer(toMumble chan<- gumble.AudioBuffer, die chan bool) { +func (dd *DiscordDuplex) fromDiscordMixer(toMumble chan<- gumble.AudioBuffer) { ticker := time.NewTicker(10 * time.Millisecond) sendAudio := false for { select { - case <-die: + case <-dd.die: log.Println("killing fromDiscordMixer") return - default: + case <-ticker.C: } - <-ticker.C - discordMutex.Lock() + + dd.discordMutex.Lock() sendAudio = false internalMixerArr := make([][]int16, 0) // Work through each channel - for i := range fromDiscordMap { - if len(fromDiscordMap[i].pcm) > 0 { + for i := range dd.fromDiscordMap { + if len(dd.fromDiscordMap[i].pcm) > 0 { sendAudio = true - if fromDiscordMap[i].streaming == false { - x := fromDiscordMap[i] + if dd.fromDiscordMap[i].streaming == false { + x := dd.fromDiscordMap[i] x.streaming = true - fromDiscordMap[i] = x + dd.fromDiscordMap[i] = x } - x1 := (<-fromDiscordMap[i].pcm) + x1 := (<-dd.fromDiscordMap[i].pcm) internalMixerArr = append(internalMixerArr, x1) } else { - if fromDiscordMap[i].streaming == true { - x := fromDiscordMap[i] + if dd.fromDiscordMap[i].streaming == true { + x := dd.fromDiscordMap[i] x.streaming = false - fromDiscordMap[i] = x + dd.fromDiscordMap[i] = x } } } - discordMutex.Unlock() + dd.discordMutex.Unlock() outBuf := make([]int16, 480) diff --git a/handlers.go b/handlers.go deleted file mode 100644 index 64a6db2..0000000 --- a/handlers.go +++ /dev/null @@ -1,236 +0,0 @@ -package main - -import ( - "fmt" - "log" - "strings" - "sync" - "time" - - "github.com/bwmarrin/discordgo" - "layeh.com/gumble/gumble" -) - -//Listener holds references to the current BridgeConf -//and BridgeState for use by the event handlers -type Listener struct { - BridgeConf *BridgeConfig - Bridge *BridgeState - UserCountLock *sync.Mutex - ConnectedLock *sync.Mutex -} - -func (l *Listener) ready(s *discordgo.Session, event *discordgo.Ready) { - log.Println("READY event registered") - //Setup initial discord state - var g *discordgo.Guild - g = nil - for _, i := range event.Guilds { - if i.ID == l.BridgeConf.GID { - g = i - } - } - if g == nil { - log.Println("bad guild on READY") - return - } - for _, vs := range g.VoiceStates { - if vs.ChannelID == l.BridgeConf.CID { - l.UserCountLock.Lock() - l.Bridge.DiscordUserCount = l.Bridge.DiscordUserCount + 1 - u, err := s.User(vs.UserID) - if err != nil { - log.Println("error looking up username") - } - l.Bridge.DiscordUsers[u.Username] = true - if l.Bridge.Connected { - l.Bridge.Client.Do(func() { - l.Bridge.Client.Self.Channel.Send(fmt.Sprintf("%v has joined Discord channel\n", u.Username), false) - }) - } - l.UserCountLock.Unlock() - } - } -} - -func (l *Listener) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { - - if l.Bridge.Mode == bridgeModeConstant { - return - } - - // Ignore all messages created by the bot itself - if m.Author.ID == s.State.User.ID { - return - } - // Find the channel that the message came from. - c, err := s.State.Channel(m.ChannelID) - if err != nil { - // Could not find channel. - return - } - - // Find the guild for that channel. - g, err := s.State.Guild(c.GuildID) - if err != nil { - // Could not find guild. - return - } - prefix := "!" + l.BridgeConf.Command - if strings.HasPrefix(m.Content, prefix+" link") { - // Look for the message sender in that guild's current voice states. - for _, vs := range g.VoiceStates { - if vs.UserID == m.Author.ID { - log.Printf("Trying to join GID %v and VID %v\n", g.ID, vs.ChannelID) - die := make(chan bool) - l.Bridge.ActiveConn = die - go startBridge(s, g.ID, vs.ChannelID, l, die) - return - } - } - } - - if strings.HasPrefix(m.Content, prefix+" unlink") { - // Look for the message sender in that guild's current voice states. - for _, vs := range g.VoiceStates { - if vs.UserID == m.Author.ID { - log.Printf("Trying to leave GID %v and VID %v\n", g.ID, vs.ChannelID) - l.Bridge.ActiveConn <- true - l.Bridge.ActiveConn = nil - return - } - } - } - - if strings.HasPrefix(m.Content, prefix+" refresh") { - // Look for the message sender in that guild's current voice states. - for _, vs := range g.VoiceStates { - if vs.UserID == m.Author.ID { - log.Printf("Trying to refresh GID %v and VID %v\n", g.ID, vs.ChannelID) - l.Bridge.ActiveConn <- true - time.Sleep(5 * time.Second) - l.Bridge.ActiveConn = make(chan bool) - go startBridge(s, g.ID, vs.ChannelID, l, l.Bridge.ActiveConn) - return - } - } - } - - if strings.HasPrefix(m.Content, prefix+" auto") { - if l.Bridge.Mode != bridgeModeAuto { - l.Bridge.Mode = bridgeModeAuto - l.Bridge.AutoChan = make(chan bool) - go AutoBridge(s, l) - } else { - l.Bridge.AutoChan <- true - l.Bridge.Mode = bridgeModeManual - } - } -} - -func (l *Listener) guildCreate(s *discordgo.Session, event *discordgo.GuildCreate) { - - if event.Guild.Unavailable { - return - } - - for _, channel := range event.Guild.Channels { - if channel.ID == event.Guild.ID { - log.Println("Mumble-Discord bridge is active in new guild") - return - } - } -} - -func (l *Listener) voiceUpdate(s *discordgo.Session, event *discordgo.VoiceStateUpdate) { - l.UserCountLock.Lock() - if event.GuildID == l.BridgeConf.GID { - if event.ChannelID == l.BridgeConf.CID { - //get user - u, err := s.User(event.UserID) - if err != nil { - log.Printf("error looking up user for uid %v", event.UserID) - } - //check to see if actually new user - if l.Bridge.DiscordUsers[u.Username] { - //not actually new user - l.UserCountLock.Unlock() - return - } - log.Println("user joined watched discord channel") - l.ConnectedLock.Lock() - if l.Bridge.Connected { - l.Bridge.Client.Do(func() { - l.Bridge.Client.Self.Channel.Send(fmt.Sprintf("%v has joined Discord channel\n", u.Username), false) - }) - } - l.ConnectedLock.Unlock() - l.Bridge.DiscordUsers[u.Username] = true - l.Bridge.DiscordUserCount = l.Bridge.DiscordUserCount + 1 - l.UserCountLock.Unlock() - } - if event.ChannelID == "" { - //leave event, trigger recount of active users - //TODO when next version of discordgo comes out, switch to PreviousState - g, err := s.State.Guild(event.GuildID) - if err != nil { - // Could not find guild. - l.UserCountLock.Unlock() - return - } - - // Look for current voice states in watched channel - count := 0 - for _, vs := range g.VoiceStates { - if vs.ChannelID == l.BridgeConf.CID { - count = count + 1 - } - } - if l.Bridge.DiscordUserCount > count { - u, err := s.User(event.UserID) - if err != nil { - log.Printf("error looking up user for uid %v", event.UserID) - } - delete(l.Bridge.DiscordUsers, u.Username) - log.Println("user left watched discord channel") - l.ConnectedLock.Lock() - if l.Bridge.Connected { - l.Bridge.Client.Do(func() { - l.Bridge.Client.Self.Channel.Send(fmt.Sprintf("%v has left Discord channel\n", u.Username), false) - }) - } - l.ConnectedLock.Unlock() - l.Bridge.DiscordUserCount = count - } - l.UserCountLock.Unlock() - } - - } - return -} - -func (l *Listener) mumbleConnect(e *gumble.ConnectEvent) { - if l.BridgeConf.MumbleChannel != "" { - //join specified channel - startingChannel := e.Client.Channels.Find(l.BridgeConf.MumbleChannel) - if startingChannel != nil { - e.Client.Self.Move(startingChannel) - } - } -} - -func (l *Listener) mumbleUserChange(e *gumble.UserChangeEvent) { - l.UserCountLock.Lock() - if e.Type.Has(gumble.UserChangeConnected) || e.Type.Has(gumble.UserChangeChannel) || e.Type.Has(gumble.UserChangeDisconnected) { - l.Bridge.MumbleUsers = make(map[string]bool) - for _, user := range l.Bridge.Client.Self.Channel.Users { - //note, this might be too slow for really really big channels? - //event listeners block while processing - //also probably bad to rebuild the set every user change. - if user.Name != l.Bridge.Client.Self.Name { - l.Bridge.MumbleUsers[user.Name] = true - } - } - } - l.UserCountLock.Unlock() -} diff --git a/main.go b/main.go index a9c23c7..655943e 100644 --- a/main.go +++ b/main.go @@ -2,12 +2,12 @@ package main import ( "flag" + "fmt" "log" "os" "os/signal" "runtime/pprof" "strconv" - "sync" "syscall" "time" @@ -26,23 +26,27 @@ var ( ) func main() { - log.Println("Mumble-Discord-Bridge") - log.Println("v" + version + " " + commit + " " + date) + var err error + + fmt.Println("Mumble-Discord-Bridge") + fmt.Println("v" + version + " " + commit + " " + date) godotenv.Load() - mumbleAddr := flag.String("mumble-address", lookupEnvOrString("MUMBLE_ADDRESS", ""), "MUMBLE_ADDRESS, mumble server address, example example.com") - mumblePort := flag.Int("mumble-port", lookupEnvOrInt("MUMBLE_PORT", 64738), "MUMBLE_PORT mumble port") - mumbleUsername := flag.String("mumble-username", lookupEnvOrString("MUMBLE_USERNAME", "Discord"), "MUMBLE_USERNAME, mumble username") + mumbleAddr := flag.String("mumble-address", lookupEnvOrString("MUMBLE_ADDRESS", ""), "MUMBLE_ADDRESS, mumble server address, example example.com, required") + mumblePort := flag.Int("mumble-port", lookupEnvOrInt("MUMBLE_PORT", 64738), "MUMBLE_PORT, mumble port, (default 64738)") + mumbleUsername := flag.String("mumble-username", lookupEnvOrString("MUMBLE_USERNAME", "Discord"), "MUMBLE_USERNAME, mumble username, (default: discord)") mumblePassword := flag.String("mumble-password", lookupEnvOrString("MUMBLE_PASSWORD", ""), "MUMBLE_PASSWORD, mumble password, optional") - mumbleInsecure := flag.Bool("mumble-insecure", lookupEnvOrBool("MUMBLE_INSECURE", false), "mumble insecure, env alt MUMBLE_INSECURE") - mumbleChannel := flag.String("mumble-channel", lookupEnvOrString("MUMBLE_CHANNEL", ""), "mumble channel to start in") - discordToken := flag.String("discord-token", lookupEnvOrString("DISCORD_TOKEN", ""), "DISCORD_TOKEN, discord bot token") - discordGID := flag.String("discord-gid", lookupEnvOrString("DISCORD_GID", ""), "DISCORD_GID, discord gid") - discordCID := flag.String("discord-cid", lookupEnvOrString("DISCORD_CID", ""), "DISCORD_CID, discord cid") - discordCommand := flag.String("discord-command", lookupEnvOrString("DISCORD_COMMAND", "mumble-discord"), "DISCORD_COMMAND,Discord command string, env alt DISCORD_COMMAND, optional, defaults to mumble-discord") - mode := flag.String("mode", lookupEnvOrString("MODE", "constant"), "MODE,determine which mode the bridge starts in") - nice := flag.Bool("nice", lookupEnvOrBool("NICE", false), "NICE,whether the bridge should automatically try to 'nice' itself") + mumbleInsecure := flag.Bool("mumble-insecure", lookupEnvOrBool("MUMBLE_INSECURE", false), " MUMBLE_INSECURE, mumble insecure, optional") + mumbleChannel := flag.String("mumble-channel", lookupEnvOrString("MUMBLE_CHANNEL", ""), "MUMBLE_CHANNEL, mumble channel to start in, optional") + mumbleDisableText := flag.Bool("mumble-disable-text", lookupEnvOrBool("MUMBLE_DISABLE_TEXT", false), "MUMBLE_DISABLE_TEXT, disable sending text to mumble, (default false)") + discordToken := flag.String("discord-token", lookupEnvOrString("DISCORD_TOKEN", ""), "DISCORD_TOKEN, discord bot token, required") + discordGID := flag.String("discord-gid", lookupEnvOrString("DISCORD_GID", ""), "DISCORD_GID, discord gid, required") + discordCID := flag.String("discord-cid", lookupEnvOrString("DISCORD_CID", ""), "DISCORD_CID, discord cid, required") + discordCommand := flag.String("discord-command", lookupEnvOrString("DISCORD_COMMAND", "mumble-discord"), "DISCORD_COMMAND, Discord command string, env alt DISCORD_COMMAND, optional, (defaults mumble-discord)") + discordDisableText := flag.Bool("discord-disable-text", lookupEnvOrBool("DISCORD_DISABLE_TEXT", false), "DISCORD_DISABLE_TEXT, disable sending direct messages to discord, (default false)") + mode := flag.String("mode", lookupEnvOrString("MODE", "constant"), "MODE, [constant, manual, auto] determine which mode the bridge starts in, (default constant)") + nice := flag.Bool("nice", lookupEnvOrBool("NICE", false), "NICE, whether the bridge should automatically try to 'nice' itself, (default false)") cpuprofile := flag.String("cpuprofile", "", "write cpu profile to `file`") @@ -75,6 +79,7 @@ func main() { } } + // Optional CPU Profiling if *cpuprofile != "" { f, err := os.Create(*cpuprofile) if err != nil { @@ -87,62 +92,70 @@ func main() { defer pprof.StopCPUProfile() } - //Connect to discord - discord, err := discordgo.New("Bot " + *discordToken) - if err != nil { - log.Println(err) - return - } + // BRIDGE SETUP - // Mumble setup - config := gumble.NewConfig() - config.Username = *mumbleUsername - config.Password = *mumblePassword - config.AudioInterval = time.Millisecond * 10 - - // Bridge setup - BridgeConf := &BridgeConfig{ - Config: config, - MumbleAddr: *mumbleAddr + ":" + strconv.Itoa(*mumblePort), - MumbleInsecure: *mumbleInsecure, - MumbleChannel: *mumbleChannel, - Command: *discordCommand, - GID: *discordGID, - CID: *discordCID, - } Bridge := &BridgeState{ - ActiveConn: make(chan bool), - Connected: false, - MumbleUserCount: 0, - DiscordUserCount: 0, - DiscordUsers: make(map[string]bool), - MumbleUsers: make(map[string]bool), + BridgeConfig: &BridgeConfig{ + // MumbleConfig: config, + MumbleAddr: *mumbleAddr + ":" + strconv.Itoa(*mumblePort), + MumbleInsecure: *mumbleInsecure, + MumbleChannel: *mumbleChannel, + MumbleDisableText: *mumbleDisableText, + Command: *discordCommand, + GID: *discordGID, + CID: *discordCID, + DiscordDisableText: *discordDisableText, + }, + Connected: false, + DiscordUsers: make(map[string]discordUser), + MumbleUsers: make(map[string]bool), } - ul := &sync.Mutex{} - cl := &sync.Mutex{} - l := &Listener{BridgeConf, Bridge, ul, cl} - // Discord setup - // Open Websocket - discord.LogLevel = 1 - discord.StateEnabled = true - discord.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged) - discord.ShouldReconnectOnError = true - // register handlers - discord.AddHandler(l.ready) - discord.AddHandler(l.messageCreate) - discord.AddHandler(l.guildCreate) - discord.AddHandler(l.voiceUpdate) - err = discord.Open() - l.BridgeConf.Config.Attach(gumbleutil.Listener{ - Connect: l.mumbleConnect, - UserChange: l.mumbleUserChange, + // MUMBLE SETUP + MumbleConfig := gumble.NewConfig() + Bridge.BridgeConfig.MumbleConfig = MumbleConfig + MumbleConfig.Username = *mumbleUsername + MumbleConfig.Password = *mumblePassword + MumbleConfig.AudioInterval = time.Millisecond * 10 + + Bridge.MumbleListener = &MumbleListener{ + Bridge: Bridge, + } + + MumbleConfig.Attach(gumbleutil.Listener{ + Connect: Bridge.MumbleListener.mumbleConnect, + UserChange: Bridge.MumbleListener.mumbleUserChange, }) + + // DISCORD SETUP + + //Connect to discord + Bridge.DiscordSession, err = discordgo.New("Bot " + *discordToken) if err != nil { log.Println(err) return } - defer discord.Close() + + Bridge.DiscordSession.LogLevel = 1 + Bridge.DiscordSession.StateEnabled = true + Bridge.DiscordSession.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged) + Bridge.DiscordSession.ShouldReconnectOnError = true + // register handlers + Bridge.DiscordListener = &DiscordListener{ + Bridge: Bridge, + } + Bridge.DiscordSession.AddHandler(Bridge.DiscordListener.ready) + Bridge.DiscordSession.AddHandler(Bridge.DiscordListener.messageCreate) + Bridge.DiscordSession.AddHandler(Bridge.DiscordListener.guildCreate) + Bridge.DiscordSession.AddHandler(Bridge.DiscordListener.voiceUpdate) + + // Open Discord websocket + err = Bridge.DiscordSession.Open() + if err != nil { + log.Println(err) + return + } + defer Bridge.DiscordSession.Close() log.Println("Discord Bot Connected") log.Printf("Discord bot looking for command !%v", *discordCommand) @@ -150,22 +163,23 @@ func main() { switch *mode { case "auto": log.Println("bridge starting in automatic mode") - Bridge.AutoChan = make(chan bool) + Bridge.AutoChanDie = make(chan bool) Bridge.Mode = bridgeModeAuto - go AutoBridge(discord, l) + go Bridge.AutoBridge() case "manual": log.Println("bridge starting in manual mode") Bridge.Mode = bridgeModeManual case "constant": log.Println("bridge starting in constant mode") Bridge.Mode = bridgeModeConstant - go startBridge(discord, *discordGID, *discordCID, l, make(chan bool)) + go Bridge.startBridge() default: - discord.Close() + Bridge.DiscordSession.Close() log.Fatalln("invalid bridge mode set") } - go discordStatusUpdate(discord, *mumbleAddr, strconv.Itoa(*mumblePort), l) + go Bridge.discordStatusUpdate() + sc := make(chan os.Signal, 1) signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) <-sc diff --git a/mumble-handlers.go b/mumble-handlers.go new file mode 100644 index 0000000..cb4ead0 --- /dev/null +++ b/mumble-handlers.go @@ -0,0 +1,69 @@ +package main + +import ( + "strings" + + "layeh.com/gumble/gumble" +) + +// MumbleListener Handle mumble events +type MumbleListener struct { + Bridge *BridgeState +} + +func (l *MumbleListener) mumbleConnect(e *gumble.ConnectEvent) { + if l.Bridge.BridgeConfig.MumbleChannel != "" { + //join specified channel + startingChannel := e.Client.Channels.Find(l.Bridge.BridgeConfig.MumbleChannel) + if startingChannel != nil { + e.Client.Self.Move(startingChannel) + } + } +} + +func (l *MumbleListener) mumbleUserChange(e *gumble.UserChangeEvent) { + l.Bridge.MumbleUsersMutex.Lock() + if e.Type.Has(gumble.UserChangeConnected) || e.Type.Has(gumble.UserChangeChannel) || e.Type.Has(gumble.UserChangeDisconnected) { + l.Bridge.MumbleUsers = make(map[string]bool) + for _, user := range l.Bridge.MumbleClient.Self.Channel.Users { + //note, this might be too slow for really really big channels? + //event listeners block while processing + //also probably bad to rebuild the set every user change. + if user.Name != l.Bridge.MumbleClient.Self.Name { + l.Bridge.MumbleUsers[user.Name] = true + } + } + } + l.Bridge.MumbleUsersMutex.Unlock() + + if e.Type.Has(gumble.UserChangeConnected) { + + if !l.Bridge.BridgeConfig.MumbleDisableText { + e.User.Send("Mumble-Discord-Bridge v" + version) + + // Tell the user who is connected to discord + if len(l.Bridge.DiscordUsers) == 0 { + e.User.Send("No users connected to Discord") + } else { + s := "Connected to Discord: " + + arr := []string{} + l.Bridge.DiscordUsersMutex.Lock() + for u := range l.Bridge.DiscordUsers { + arr = append(arr, l.Bridge.DiscordUsers[u].username) + } + + s = s + strings.Join(arr[:], ",") + + l.Bridge.DiscordUsersMutex.Unlock() + e.User.Send(s) + } + } + + // Send discord a notice + l.Bridge.discordSendMessageAll(e.User.Name + " has joined mumble") + } + if e.Type.Has(gumble.UserChangeDisconnected) { + l.Bridge.discordSendMessageAll(e.User.Name + " has left mumble") + } +} diff --git a/mumble.go b/mumble.go index c3d143b..8912989 100644 --- a/mumble.go +++ b/mumble.go @@ -15,7 +15,7 @@ var mumbleStreamingArr []bool // MumbleDuplex - listenera and outgoing type MumbleDuplex struct { - Close chan bool + die chan bool } // OnAudioStream - Spawn routines to handle incoming packets @@ -29,11 +29,11 @@ func (m MumbleDuplex) OnAudioStream(e *gumble.AudioStreamEvent) { mumbleStreamingArr = append(mumbleStreamingArr, false) mutex.Unlock() - go func(die chan bool) { + go func() { log.Println("new mumble audio stream", e.User.Name) for { select { - case <-die: + case <-m.die: log.Println("Removing mumble audio stream") return case p := <-e.C: @@ -45,7 +45,7 @@ func (m MumbleDuplex) OnAudioStream(e *gumble.AudioStreamEvent) { } } } - }(m.Close) + }() return }