diff --git a/.gitignore b/.gitignore index 0984308..87b6e38 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ mumble-discord-bridge dist bridge .prof +cert.pem \ No newline at end of file diff --git a/Makefile b/Makefile index 221c93c..5d9d6cd 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,9 @@ mumble-discord-bridge: $(GOFILES) dev: $(GOFILES) goreleaser build --skip-validate --rm-dist && sudo ./dist/mumble-discord-bridge_linux_amd64/mumble-discord-bridge +dev-race: $(GOFILES) + go run -race *.go + dev-profile: $(GOFILES) goreleaser build --skip-validate --rm-dist && sudo ./dist/mumble-discord-bridge_linux_amd64/mumble-discord-bridge -cpuprofile cpu.prof diff --git a/README.md b/README.md index 465ffd5..23a072b 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,13 @@ Usage of ./mumble-discord-bridge: -mumble-address string MUMBLE_ADDRESS, mumble server address, example example.com, required -mumble-channel string - MUMBLE_CHANNEL, mumble channel to start in, optional + MUMBLE_CHANNEL, mumble channel to start in, using '/' to separate nested channels, optional -mumble-disable-text MUMBLE_DISABLE_TEXT, disable sending text to mumble, (default false) -mumble-insecure MUMBLE_INSECURE, mumble insecure, optional + -mumble-certificate + MUMBLE_CERTIFICATE, mumble client certificate, optional -mumble-password string MUMBLE_PASSWORD, mumble password, optional -mumble-port int @@ -40,6 +42,8 @@ Usage of ./mumble-discord-bridge: MUMBLE_USERNAME, mumble username, (default: discord) (default "Discord") -nice NICE, whether the bridge should automatically try to 'nice' itself, (default false) + -debug + DEBUG_LEVEL, DISCORD debug level, optional (default: 1) ``` The bridge can be run with the follow modes: @@ -74,7 +78,31 @@ The guide below provides information on how to setup a Discord bot. [Create a Discord Bot](https://discordpy.readthedocs.io/en/latest/discord.html) -Individual Discord servers need to invite the bot before it can connect. +Individual Discord servers need to invite the bot before it can connect. +The bot requires the following permissions: +* View Channels +* See Messages +* Read Message History +* Voice Channel Connect +* Voice Channel Speak +* Voice Channel Use Voice Activity + +### Finding Discord CID and GID + +Discord GID is a unique ID linked to one Discord Server, also called Guild. CID is similarly a unique ID for a Discord Channel. To find these you need to set Discord into developer Mode. + +[Instructions to enable Discord Developer Mode](https://discordia.me/en/developer-mode) + +Then you can get the GID by right-clicking your server and selecting Copy-ID. Similarly the CID can be found right clicking the voice channel and selecting Copy ID. + +### Generating Mumble Client (Optional) + +Optionally you can specify a client certificate for mumble [Mumble Certificates](https://wiki.mumble.info/wiki/Mumble_Certificates) +If you don't have a client certificate, you can generate one with this command: + +``` bash +openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout cert.pem -out cert.pem -subj "/CN=mumble-discord-bridge" +``` ### Binary @@ -141,6 +169,9 @@ Audio leveling from Discord needs to be improved. Delays in connecting to Mumble (such as from external authentication plugins) may result in extra error messages on initial connection. +There is an issue seen with Mumble-Server (murmur) 1.3.0 in which the bridge will loose the ability to send messages client after prolonged periods of connectivity. +This issue has been appears to be resolved by murmur 1.3.4. + ## License Distributed under the MIT License. See LICENSE for more information. diff --git a/bridge.go b/bridge.go index f620fff..b6f7c86 100644 --- a/bridge.go +++ b/bridge.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "net" + "os" "strconv" "sync" "time" @@ -34,6 +35,9 @@ type BridgeState struct { // Wait for bridge to exit cleanly WaitExit *sync.WaitGroup + // Bridge State Mutex + BridgeMutex sync.Mutex + // Bridge connection Connected bool @@ -70,6 +74,9 @@ type BridgeState struct { // Mumble Duplex and Event Listener MumbleStream *MumbleDuplex MumbleListener *MumbleListener + + // Discord Voice channel to join + DiscordChannelID string } // startBridge established the voice connection @@ -90,7 +97,12 @@ func (b *BridgeState) startBridge() { // DISCORD Connect Voice log.Println("Attempting to join Discord voice channel") - b.DiscordVoice, err = b.DiscordSession.ChannelVoiceJoin(b.BridgeConfig.GID, b.BridgeConfig.CID, false, false) + if b.DiscordChannelID == "" { + log.Println("Tried to start bridge but no Discord channel specified") + return + } + b.DiscordVoice, err = b.DiscordSession.ChannelVoiceJoin(b.BridgeConfig.GID, b.DiscordChannelID, false, false) + if err != nil { log.Println(err) b.DiscordVoice.Disconnect() @@ -111,6 +123,16 @@ func (b *BridgeState) startBridge() { tlsConfig.InsecureSkipVerify = true } + if b.BridgeConfig.MumbleCertificate != "" { + keyFile := b.BridgeConfig.MumbleCertificate + if certificate, err := tls.LoadX509KeyPair(keyFile, keyFile); err != nil { + fmt.Fprintf(os.Stderr, "%s: %s\n", os.Args[0], err) + os.Exit(1) + } else { + tlsConfig.Certificates = append(tlsConfig.Certificates, certificate) + } + } + log.Println("Attempting to join Mumble") b.MumbleClient, err = gumble.DialWithDialer(new(net.Dialer), b.BridgeConfig.MumbleAddr, b.BridgeConfig.MumbleConfig, &tlsConfig) @@ -149,6 +171,7 @@ func (b *BridgeState) startBridge() { go b.DiscordStream.discordSendPCM(ctx, &wg, cancel, toDiscord) // Monitor Mumble + wg.Add(1) go func() { wg.Add(1) ticker := time.NewTicker(500 * time.Millisecond) @@ -170,7 +193,9 @@ func (b *BridgeState) startBridge() { } }() + b.BridgeMutex.Lock() b.Connected = true + b.BridgeMutex.Unlock() // Hold until cancelled or external die request select { @@ -181,7 +206,10 @@ func (b *BridgeState) startBridge() { cancel() } + b.BridgeMutex.Lock() b.Connected = false + b.BridgeMutex.Unlock() + wg.Wait() log.Println("Terminating Bridge") b.MumbleUsersMutex.Lock() @@ -202,6 +230,7 @@ func (b *BridgeState) discordStatusUpdate() { b.DiscordSession.UpdateListeningStatus("an error pinging mumble") } else { b.MumbleUsersMutex.Lock() + b.BridgeMutex.Lock() b.MumbleUserCount = resp.ConnectedUsers if b.Connected { b.MumbleUserCount = b.MumbleUserCount - 1 @@ -215,6 +244,7 @@ func (b *BridgeState) discordStatusUpdate() { status = fmt.Sprintf("%v users in Mumble\n", b.MumbleUserCount) } } + b.BridgeMutex.Unlock() b.MumbleUsersMutex.Unlock() b.DiscordSession.UpdateListeningStatus(status) } @@ -238,6 +268,7 @@ func (b *BridgeState) AutoBridge() { b.MumbleUsersMutex.Lock() b.DiscordUsersMutex.Lock() + b.BridgeMutex.Lock() if !b.Connected && b.MumbleUserCount > 0 && len(b.DiscordUsers) > 0 { log.Println("users detected in mumble and discord, bridging") @@ -248,6 +279,7 @@ func (b *BridgeState) AutoBridge() { b.BridgeDie <- true } + b.BridgeMutex.Unlock() b.MumbleUsersMutex.Unlock() b.DiscordUsersMutex.Unlock() } diff --git a/config.go b/config.go index 71148ab..84b7558 100644 --- a/config.go +++ b/config.go @@ -24,7 +24,8 @@ type BridgeConfig struct { MumbleConfig *gumble.Config MumbleAddr string MumbleInsecure bool - MumbleChannel string + MumbleCertificate string + MumbleChannel []string MumbleDisableText bool Command string GID string diff --git a/discord-handlers.go b/discord-handlers.go index ac46684..e0e3083 100644 --- a/discord-handlers.go +++ b/discord-handlers.go @@ -24,7 +24,7 @@ func (l *DiscordListener) guildCreate(s *discordgo.Session, event *discordgo.Gui } for _, vs := range event.VoiceStates { - if vs.ChannelID == l.Bridge.BridgeConfig.CID { + if vs.ChannelID == l.Bridge.DiscordChannelID { if s.State.User.ID == vs.UserID { // Ignore bot continue @@ -49,11 +49,13 @@ func (l *DiscordListener) guildCreate(s *discordgo.Session, event *discordgo.Gui l.Bridge.DiscordUsersMutex.Unlock() // If connected to mumble inform users of Discord users + l.Bridge.BridgeMutex.Lock() 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) }) } + l.Bridge.BridgeMutex.Unlock() } } @@ -61,10 +63,6 @@ func (l *DiscordListener) guildCreate(s *discordgo.Session, event *discordgo.Gui 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 @@ -83,11 +81,26 @@ func (l *DiscordListener) messageCreate(s *discordgo.Session, m *discordgo.Messa return } prefix := "!" + l.Bridge.BridgeConfig.Command + + if l.Bridge.Mode == bridgeModeConstant && strings.HasPrefix(m.Content, prefix) { + l.Bridge.DiscordSession.ChannelMessageSend(m.ChannelID, "Constant mode enabled, manual commands can not be entered") + return + } + + l.Bridge.BridgeMutex.Lock() + bridgeConnected := l.Bridge.Connected + l.Bridge.BridgeMutex.Unlock() + 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 bridgeConnected { + l.Bridge.DiscordSession.ChannelMessageSend(m.ChannelID, "Bridge already running, unlink first") + return + } if vs.UserID == m.Author.ID { log.Printf("Trying to join GID %v and VID %v\n", g.ID, vs.ChannelID) + l.Bridge.DiscordChannelID = vs.ChannelID go l.Bridge.startBridge() return } @@ -96,8 +109,12 @@ func (l *DiscordListener) messageCreate(s *discordgo.Session, m *discordgo.Messa if strings.HasPrefix(m.Content, prefix+" unlink") { // Look for the message sender in that guild's current voice states. + if !bridgeConnected { + l.Bridge.DiscordSession.ChannelMessageSend(m.ChannelID, "Bridge is not currently running") + return + } for _, vs := range g.VoiceStates { - if vs.UserID == m.Author.ID { + if vs.UserID == m.Author.ID && vs.ChannelID == l.Bridge.DiscordChannelID { log.Printf("Trying to leave GID %v and VID %v\n", g.ID, vs.ChannelID) l.Bridge.BridgeDie <- true return @@ -107,6 +124,10 @@ func (l *DiscordListener) messageCreate(s *discordgo.Session, m *discordgo.Messa if strings.HasPrefix(m.Content, prefix+" refresh") { // Look for the message sender in that guild's current voice states. + if !bridgeConnected { + l.Bridge.DiscordSession.ChannelMessageSend(m.ChannelID, "Bridge is not currently running") + return + } 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) @@ -122,10 +143,14 @@ func (l *DiscordListener) messageCreate(s *discordgo.Session, m *discordgo.Messa if strings.HasPrefix(m.Content, prefix+" auto") { if l.Bridge.Mode != bridgeModeAuto { + l.Bridge.DiscordSession.ChannelMessageSend(m.ChannelID, "Auto mode enabled") l.Bridge.Mode = bridgeModeAuto + l.Bridge.DiscordChannelID = l.Bridge.BridgeConfig.CID l.Bridge.AutoChanDie = make(chan bool) go l.Bridge.AutoBridge() } else { + l.Bridge.DiscordSession.ChannelMessageSend(m.ChannelID, "Auto mode disabled") + l.Bridge.DiscordChannelID = "" l.Bridge.AutoChanDie <- true l.Bridge.Mode = bridgeModeManual } @@ -152,7 +177,7 @@ func (l *DiscordListener) voiceUpdate(s *discordgo.Session, event *discordgo.Voi // Sync the channel voice states to the local discordUsersMap for _, vs := range g.VoiceStates { - if vs.ChannelID == l.Bridge.BridgeConfig.CID { + if vs.ChannelID == l.Bridge.DiscordChannelID { if s.State.User.ID == vs.UserID { // Ignore bot continue @@ -176,11 +201,13 @@ func (l *DiscordListener) voiceUpdate(s *discordgo.Session, event *discordgo.Voi seen: true, dm: dm, } + l.Bridge.BridgeMutex.Lock() 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) }) } + l.Bridge.BridgeMutex.Unlock() } else { du := l.Bridge.DiscordUsers[vs.UserID] du.seen = true @@ -192,13 +219,15 @@ func (l *DiscordListener) voiceUpdate(s *discordgo.Session, event *discordgo.Voi // Remove users that are no longer connected for id := range l.Bridge.DiscordUsers { - if l.Bridge.DiscordUsers[id].seen == false { + if !l.Bridge.DiscordUsers[id].seen { log.Println("User left Discord channel " + l.Bridge.DiscordUsers[id].username) + l.Bridge.BridgeMutex.Lock() 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) }) } + l.Bridge.BridgeMutex.Unlock() delete(l.Bridge.DiscordUsers, id) } } diff --git a/discord.go b/discord.go index 44befca..4268780 100644 --- a/discord.go +++ b/discord.go @@ -23,9 +23,8 @@ type fromDiscord struct { type DiscordDuplex struct { Bridge *BridgeState - discordMutex sync.Mutex - discordMixerMutex sync.Mutex - fromDiscordMap map[uint32]fromDiscord + discordMutex sync.Mutex + fromDiscordMap map[uint32]fromDiscord } // OnError gets called by dgvoice when an error is encountered. @@ -87,8 +86,9 @@ func (dd *DiscordDuplex) discordSendPCM(ctx context.Context, wg *sync.WaitGroup, continue } - if dd.Bridge.DiscordVoice.Ready == false || dd.Bridge.DiscordVoice.OpusSend == nil { - if lastReady == true { + dd.Bridge.DiscordVoice.RWMutex.RLock() + if !dd.Bridge.DiscordVoice.Ready || dd.Bridge.DiscordVoice.OpusSend == nil { + if lastReady { 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() { log.Println("set ready timeout") @@ -96,13 +96,15 @@ func (dd *DiscordDuplex) discordSendPCM(ctx context.Context, wg *sync.WaitGroup, }) lastReady = false } - continue - } else if lastReady == false { + } else if !lastReady { fmt.Println("Discordgo ready to send opus packets") lastReady = true readyTimeout.Stop() + } else { + dd.Bridge.DiscordVoice.OpusSend <- opus } - dd.Bridge.DiscordVoice.OpusSend <- opus + dd.Bridge.DiscordVoice.RWMutex.RUnlock() + } else { if streaming { dd.Bridge.DiscordVoice.Speaking(false) @@ -123,8 +125,9 @@ func (dd *DiscordDuplex) discordReceivePCM(ctx context.Context, wg *sync.WaitGro wg.Add(1) for { - if dd.Bridge.DiscordVoice.Ready == false || dd.Bridge.DiscordVoice.OpusRecv == nil { - if lastReady == true { + dd.Bridge.DiscordVoice.RWMutex.RLock() + if !dd.Bridge.DiscordVoice.Ready || dd.Bridge.DiscordVoice.OpusRecv == nil { + if lastReady { 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") @@ -133,11 +136,12 @@ func (dd *DiscordDuplex) discordReceivePCM(ctx context.Context, wg *sync.WaitGro lastReady = false } continue - } else if lastReady == false { + } else if !lastReady { fmt.Println("Discordgo ready to receive packets") lastReady = true readyTimeout.Stop() } + dd.Bridge.DiscordVoice.RWMutex.RUnlock() var ok bool var p *discordgo.Packet @@ -222,7 +226,7 @@ func (dd *DiscordDuplex) fromDiscordMixer(ctx context.Context, wg *sync.WaitGrou for i := range dd.fromDiscordMap { if len(dd.fromDiscordMap[i].pcm) > 0 { sendAudio = true - if dd.fromDiscordMap[i].streaming == false { + if !dd.fromDiscordMap[i].streaming { x := dd.fromDiscordMap[i] x.streaming = true dd.fromDiscordMap[i] = x @@ -231,7 +235,7 @@ func (dd *DiscordDuplex) fromDiscordMixer(ctx context.Context, wg *sync.WaitGrou x1 := (<-dd.fromDiscordMap[i].pcm) internalMixerArr = append(internalMixerArr, x1) } else { - if dd.fromDiscordMap[i].streaming == true { + if dd.fromDiscordMap[i].streaming { x := dd.fromDiscordMap[i] x.streaming = false dd.fromDiscordMap[i] = x diff --git a/go.mod b/go.mod index 4279918..34d6833 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.saintnet.tech/stryan/yammerbot go 1.15 require ( - github.com/bwmarrin/discordgo v0.22.0 + github.com/bwmarrin/discordgo v0.23.2 github.com/golang/protobuf v1.4.3 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/joho/godotenv v1.3.0 diff --git a/go.sum b/go.sum index 3b3298d..f20af15 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,23 @@ +cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/bwmarrin/discordgo v0.22.0 h1:uBxY1HmlVCsW1IuaPjpCGT6A2DBwRn0nvOguQIxDdFM= github.com/bwmarrin/discordgo v0.22.0/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M= +github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4= +github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M= +github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/dchote/go-openal v0.0.0-20171116030048-f4a9a141d372/go.mod h1:74z+CYu2/mx4N+mcIS/rsvfAxBPBV9uv8zRAnwyFkdI= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473 h1:4cmBvAEBNJaGARUEs3/suWRyfyBfhf7I60WBZq+bv2w= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -30,42 +40,54 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210108172913-0df2131ae363/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 h1:5Beo0mZN8dRzgrMMkDp0jc8YXQKx9DiJ2k1dkvGsn5A= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -78,6 +100,7 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= layeh.com/gopus v0.0.0-20161224163843-0ebf989153aa h1:WNU4LYsgD2UHxgKgB36mL6iMAMOvr127alafSlgBbiA= layeh.com/gopus v0.0.0-20161224163843-0ebf989153aa/go.mod h1:AOef7vHz0+v4sWwJnr0jSyHiX/1NgsMoaxl+rEPz/I0= diff --git a/main.go b/main.go index 3474313..00e720b 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "os/signal" "runtime/pprof" "strconv" + "strings" "syscall" "time" @@ -38,7 +39,8 @@ func main() { 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, mumble insecure, optional") - mumbleChannel := flag.String("mumble-channel", lookupEnvOrString("MUMBLE_CHANNEL", ""), "MUMBLE_CHANNEL, mumble channel to start in, optional") + mumbleCertificate := flag.String("mumble-certificate", lookupEnvOrString("MUMBLE_CERTIFICATE", ""), "MUMBLE_CERTIFICATE, client certificate to use when connecting to the Mumble server") + mumbleChannel := flag.String("mumble-channel", lookupEnvOrString("MUMBLE_CHANNEL", ""), "MUMBLE_CHANNEL, mumble channel to start in, using '/' to separate nested channels, 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") @@ -47,6 +49,7 @@ func main() { 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)") + debug := flag.Int("debug-level", lookupEnvOrInt("DEBUG", 1), "DEBUG_LEVEL, Discord debug level, optional, (default 1)") cpuprofile := flag.String("cpuprofile", "", "write cpu profile to `file`") @@ -99,7 +102,8 @@ func main() { // MumbleConfig: config, MumbleAddr: *mumbleAddr + ":" + strconv.Itoa(*mumblePort), MumbleInsecure: *mumbleInsecure, - MumbleChannel: *mumbleChannel, + MumbleCertificate: *mumbleCertificate, + MumbleChannel: strings.Split(*mumbleChannel, "/"), MumbleDisableText: *mumbleDisableText, Command: *discordCommand, GID: *discordGID, @@ -135,7 +139,7 @@ func main() { return } - Bridge.DiscordSession.LogLevel = 1 + Bridge.DiscordSession.LogLevel = *debug Bridge.DiscordSession.StateEnabled = true Bridge.DiscordSession.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged) Bridge.DiscordSession.ShouldReconnectOnError = true @@ -163,6 +167,7 @@ func main() { log.Println("bridge starting in automatic mode") Bridge.AutoChanDie = make(chan bool) Bridge.Mode = bridgeModeAuto + Bridge.DiscordChannelID = Bridge.BridgeConfig.CID go Bridge.AutoBridge() case "manual": log.Println("bridge starting in manual mode") @@ -170,6 +175,7 @@ func main() { case "constant": log.Println("bridge starting in constant mode") Bridge.Mode = bridgeModeConstant + Bridge.DiscordChannelID = Bridge.BridgeConfig.CID go func() { for { Bridge.startBridge() @@ -188,13 +194,14 @@ func main() { signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) <-sc - // Signal the bridge to exit cleanly - Bridge.BridgeDie <- true - log.Println("OS Signal. Bot shutting down") // Wait or the bridge to exit cleanly + Bridge.BridgeMutex.Lock() if Bridge.Connected { + //TODO BridgeDie occasionally panics on send to closed channel + Bridge.BridgeDie <- true Bridge.WaitExit.Wait() } + Bridge.BridgeMutex.Unlock() } diff --git a/mumble-handlers.go b/mumble-handlers.go index 860d5da..3dc58df 100644 --- a/mumble-handlers.go +++ b/mumble-handlers.go @@ -13,12 +13,10 @@ type MumbleListener struct { } 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) - } + //join specified channel + startingChannel := e.Client.Channels.Find(l.Bridge.BridgeConfig.MumbleChannel...) + if startingChannel != nil { + e.Client.Self.Move(startingChannel) } } @@ -45,22 +43,23 @@ func (l *MumbleListener) mumbleUserChange(e *gumble.UserChangeEvent) { e.User.Send("Mumble-Discord-Bridge v" + version) // Tell the user who is connected to discord + l.Bridge.DiscordUsersMutex.Lock() 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) } + l.Bridge.DiscordUsersMutex.Unlock() + } // Send discord a notice diff --git a/mumble.go b/mumble.go index 4f99c70..66a6a4a 100644 --- a/mumble.go +++ b/mumble.go @@ -29,21 +29,18 @@ func (m MumbleDuplex) OnAudioStream(e *gumble.AudioStreamEvent) { mutex.Unlock() go func() { - // TODO kill go routine on cleanup - log.Println("new mumble audio stream", e.User.Name) - for { - select { - case p := <-e.C: - // log.Println("audio packet", p.Sender.Name, len(p.AudioBuffer)) + name := e.User.Name + log.Println("new mumble audio stream", name) + for p := range e.C { + // log.Println("audio packet", p.Sender.Name, len(p.AudioBuffer)) - // 480 per 10ms - for i := 0; i < len(p.AudioBuffer)/480; i++ { - localMumbleArray <- p.AudioBuffer[480*i : 480*(i+1)] - } + // 480 per 10ms + for i := 0; i < len(p.AudioBuffer)/480; i++ { + localMumbleArray <- p.AudioBuffer[480*i : 480*(i+1)] } } + log.Println("mumble audio stream ended", name) }() - return } func (m MumbleDuplex) fromMumbleMixer(ctx context.Context, wg *sync.WaitGroup, toDiscord chan []int16) { @@ -70,7 +67,7 @@ func (m MumbleDuplex) fromMumbleMixer(ctx context.Context, wg *sync.WaitGroup, t for i := 0; i < len(fromMumbleArr); i++ { if len(fromMumbleArr[i]) > 0 { sendAudio = true - if mumbleStreamingArr[i] == false { + if !mumbleStreamingArr[i] { mumbleStreamingArr[i] = true // log.Println("mumble starting", i) } @@ -78,7 +75,7 @@ func (m MumbleDuplex) fromMumbleMixer(ctx context.Context, wg *sync.WaitGroup, t x1 := (<-fromMumbleArr[i]) internalMixerArr = append(internalMixerArr, x1) } else { - if mumbleStreamingArr[i] == true { + if mumbleStreamingArr[i] { mumbleStreamingArr[i] = false // log.Println("mumble stopping", i) }