Merge branch 'main' of github.com:Stieneee/mumble-discord-bridge into main

This commit is contained in:
Tyler Stiene 2021-01-16 13:57:18 -05:00
commit 07f87eecdd
8 changed files with 665 additions and 132 deletions

4
.gitignore vendored
View File

@ -1,4 +1,6 @@
.env
main
mumble-discord-bridge
dist
dist
bridge
.prof

View File

@ -13,11 +13,13 @@ The binary will also attempt to load .env file located in the working directory.
```bash
Usage of mumble-discord-bridge:
-discord-cid string
DISCORD_CID, discord cid
DISCORD_CID, discord channel ID
-discord-gid string
DISCORD_GID, discord gid
DISCORD_GID, discord guild ID
-discord-token string
DISCORD_TOKEN, discord bot token
-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
@ -28,8 +30,35 @@ Usage of mumble-discord-bridge:
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.
-mode string
MODE, determines what mode the bridge starts in
```
The bridge can be run with the follow modes:
```bash
auto
The bridge starts up but does not connect immediately. It can be either manually linked (see below) or will join the voice channels when there's at least one person on each side.
The bridge will leave both voice channels once there is no one on either end
manual
The bridge starts up but does not connect immediately. It will join the voice channels when issued the link command and will leave with the unlink command
constant
The bridge starts up and immediately connects to both Discord and Mumble voice channels. It can not be controlled in this mode and quits when the program is stopped
```
In "auto" or "manual" modes, the bridge can be controlled in Discord with the following commands:
```bash
!DISCORD_COMMAND link
Commands the bridge to join the Discord channel the user is in and the Mumble server
!DISCORD_COMMAND unlink
Commands the bridge to leave the Discord channel the user is in and the Mumble server
!DISCORD_COMMAND refresh
Commands the bridge to unlink, then link again.
!DISCORD_COMMAND auto
Toggle between manual and auto mode
```
## Setup
### Creating a Discord Bot
@ -110,4 +139,4 @@ Please consider opening an issue to discuss features and ideas.
The project would not have been possible without:
- [gumble](https://github.com/layeh/gumble)
- [discordgo](https://github.com/bwmarrin/discordgo)
- [discordgo](https://github.com/bwmarrin/discordgo)

188
bridge.go Normal file
View File

@ -0,0 +1,188 @@
package main
import (
"crypto/tls"
"fmt"
"log"
"net"
"os"
"os/signal"
"strconv"
"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
}
func startBridge(discord *discordgo.Session, discordGID string, discordCID string, l *Listener, die chan bool) {
dgv, err := discord.ChannelVoiceJoin(discordGID, discordCID, false, false)
if err != nil {
log.Println(err)
return
}
defer dgv.Speaking(false)
defer dgv.Close()
discord.ShouldReconnectOnError = true
// MUMBLE Setup
m := MumbleDuplex{
Close: make(chan bool),
}
var tlsConfig tls.Config
if l.BridgeConf.MumbleInsecure {
tlsConfig.InsecureSkipVerify = true
}
mumble, err := gumble.DialWithDialer(new(net.Dialer), l.BridgeConf.MumbleAddr, l.BridgeConf.Config, &tlsConfig)
if err != nil {
log.Println(err)
return
}
defer mumble.Disconnect()
l.Bridge.Client = mumble
// Shared Channels
// Shared channels pass PCM information in 10ms chunks [480]int16
var toMumble = mumble.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)
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())))
select {
default:
close(die)
case <-die:
//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()
select {
case sig := <-c:
log.Printf("\nGot %s signal. Terminating Mumble-Bridge\n", sig)
case <-die:
log.Println("\nGot internal die request. Terminating Mumble-Bridge")
dgv.Disconnect()
det.Detach()
close(die)
close(m.Close)
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)
}
}
func discordStatusUpdate(dg *discordgo.Session, host, port string, l *Listener) {
status := ""
curr := 0
m, _ := time.ParseDuration("30s")
for {
time.Sleep(3 * time.Second)
resp, err := gumble.Ping(host+":"+port, -1, m)
if err != nil {
log.Printf("error pinging mumble server %v\n", err)
dg.UpdateListeningStatus("an error pinging mumble")
} else {
l.UserCountLock.Lock()
l.ConnectedLock.Lock()
curr = resp.ConnectedUsers
if l.Bridge.Connected {
curr = curr - 1
}
if curr != l.Bridge.MumbleUserCount {
l.Bridge.MumbleUserCount = curr
}
if curr == 0 {
status = ""
} else {
if len(l.Bridge.MumbleUsers) > 0 {
status = fmt.Sprintf("%v/%v users in Mumble\n", len(l.Bridge.MumbleUsers), curr)
} else {
status = fmt.Sprintf("%v users in Mumble\n", curr)
}
}
l.ConnectedLock.Unlock()
l.UserCountLock.Unlock()
dg.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) {
log.Println("beginning auto mode")
for {
select {
default:
case <-l.Bridge.AutoChan:
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 {
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)
}
if l.Bridge.Connected && l.Bridge.MumbleUserCount == 0 && l.Bridge.DiscordUserCount <= 1 {
log.Println("no one online, killing bridge")
l.Bridge.ActiveConn <- true
l.Bridge.ActiveConn = nil
}
l.UserCountLock.Unlock()
}
}

69
config.go Normal file
View File

@ -0,0 +1,69 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"strconv"
"layeh.com/gumble/gumble"
)
type bridgeMode int
const (
bridgeModeAuto bridgeMode = iota
bridgeModeManual
bridgeModeConstant
)
//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
}
func lookupEnvOrString(key string, defaultVal string) string {
if val, ok := os.LookupEnv(key); ok {
return val
}
return defaultVal
}
func lookupEnvOrInt(key string, defaultVal int) int {
if val, ok := os.LookupEnv(key); ok {
v, err := strconv.Atoi(val)
if err != nil {
log.Fatalf("LookupEnvOrInt[%s]: %v", key, err)
}
return v
}
return defaultVal
}
func lookupEnvOrBool(key string, defaultVal bool) bool {
if val, ok := os.LookupEnv(key); ok {
v, err := strconv.ParseBool(val)
if err != nil {
log.Fatalf("LookupEnvOrInt[%s]: %v", key, err)
}
return v
}
return defaultVal
}
func getConfig(fs *flag.FlagSet) []string {
cfg := make([]string, 0, 10)
fs.VisitAll(func(f *flag.Flag) {
cfg = append(cfg, fmt.Sprintf("%s:%q", f.Name, f.Value.String()))
})
return cfg
}

View File

@ -56,8 +56,13 @@ func discordSendPCM(v *discordgo.VoiceConnection, pcm <-chan []int16, die chan b
var readyTimeout *time.Timer
for {
select {
case <-die:
log.Println("Killing discordSendPCM")
return
default:
}
<-ticker.C
if len(pcm) > 1 {
if !streaming {
v.Speaking(true)
@ -88,7 +93,6 @@ func discordSendPCM(v *discordgo.VoiceConnection, pcm <-chan []int16, die chan b
lastReady = true
readyTimeout.Stop()
}
v.OpusSend <- opus
} else {
if streaming {
@ -108,10 +112,17 @@ func discordReceivePCM(v *discordgo.VoiceConnection, die chan bool) {
var readyTimeout *time.Timer
for {
select {
case <-die:
log.Println("killing discord ReceivePCM")
return
default:
}
if v.Ready == false || v.OpusRecv == nil {
if lastReady == true {
OnError(fmt.Sprintf("Discordgo not to receive opus packets. %+v : %+v", v.Ready, v.OpusSend), nil)
readyTimeout = time.AfterFunc(30*time.Second, func() {
log.Println("set ready timeout")
die <- true
})
lastReady = false
@ -122,8 +133,14 @@ func discordReceivePCM(v *discordgo.VoiceConnection, die chan bool) {
lastReady = true
readyTimeout.Stop()
}
p, ok := <-v.OpusRecv
var ok bool
var p *discordgo.Packet
select {
case p, ok = <-v.OpusRecv:
case <-die:
log.Println("killing discord ReceivePCM")
return
}
if !ok {
log.Println("Opus not ok")
continue
@ -175,11 +192,17 @@ func discordReceivePCM(v *discordgo.VoiceConnection, die chan bool) {
}
}
func fromDiscordMixer(toMumble chan<- gumble.AudioBuffer) {
func fromDiscordMixer(toMumble chan<- gumble.AudioBuffer, die chan bool) {
ticker := time.NewTicker(10 * time.Millisecond)
sendAudio := false
for {
select {
case <-die:
log.Println("killing fromDiscordMixer")
return
default:
}
<-ticker.C
discordMutex.Lock()

236
handlers.go Normal file
View File

@ -0,0 +1,236 @@
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()
}

210
main.go
View File

@ -1,20 +1,20 @@
package main
import (
"crypto/tls"
"flag"
"fmt"
"log"
"net"
"os"
"os/signal"
"runtime/pprof"
"strconv"
"sync"
"syscall"
"time"
"github.com/bwmarrin/discordgo"
"github.com/joho/godotenv"
"layeh.com/gumble/gumble"
"layeh.com/gumble/gumbleutil"
_ "layeh.com/gumble/opus"
)
@ -25,43 +25,7 @@ var (
date string
)
func lookupEnvOrString(key string, defaultVal string) string {
if val, ok := os.LookupEnv(key); ok {
return val
}
return defaultVal
}
func lookupEnvOrInt(key string, defaultVal int) int {
if val, ok := os.LookupEnv(key); ok {
v, err := strconv.Atoi(val)
if err != nil {
log.Fatalf("LookupEnvOrInt[%s]: %v", key, err)
}
return v
}
return defaultVal
}
func lookupEnvOrBool(key string, defaultVal bool) bool {
if val, ok := os.LookupEnv(key); ok {
v, err := strconv.ParseBool(val)
if err != nil {
log.Fatalf("LookupEnvOrInt[%s]: %v", key, err)
}
return v
}
return defaultVal
}
func getConfig(fs *flag.FlagSet) []string {
cfg := make([]string, 0, 10)
fs.VisitAll(func(f *flag.Flag) {
cfg = append(cfg, fmt.Sprintf("%s:%q", f.Name, f.Value.String()))
})
return cfg
}
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
func main() {
log.Println("Mumble-Discord-Bridge")
@ -74,11 +38,13 @@ func main() {
mumbleUsername := flag.String("mumble-username", lookupEnvOrString("MUMBLE_USERNAME", "discord-bridge"), "MUMBLE_USERNAME, mumble username")
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", "manual"), "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")
flag.Parse()
log.Printf("app.config %v\n", getConfig(flag.CommandLine))
@ -98,26 +64,78 @@ func main() {
if *discordCID == "" {
log.Fatalln("missing discord cid")
}
// Attempt to set Process Priority
// This is allowed to fail
err := syscall.Setpriority(syscall.PRIO_PROCESS, os.Getpid(), -5)
if err != nil {
log.Println("Unable to set process priority. ", err)
if *mode == "" {
log.Fatalln("missing mode set")
}
if *nice {
err := syscall.Setpriority(syscall.PRIO_PROCESS, os.Getpid(), -5)
if err != nil {
log.Println("Unable to set priority. ", err)
}
}
// DISCORD Setup
if *cpuprofile != "" {
f, err := os.Create(*cpuprofile)
if err != nil {
log.Fatal("could not create CPU profile: ", err)
}
defer f.Close() // error handling omitted for example
if err := pprof.StartCPUProfile(f); err != nil {
log.Fatal("could not start CPU profile: ", err)
}
defer pprof.StopCPUProfile()
}
//Connect to discord
discord, err := discordgo.New("Bot " + *discordToken)
if err != nil {
log.Println(err)
return
}
// 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),
}
ul := &sync.Mutex{}
cl := &sync.Mutex{}
l := &Listener{BridgeConf, Bridge, ul, cl}
// Discord setup
// Open Websocket
discord.LogLevel = 1
discord.LogLevel = 2
discord.StateEnabled = true
discord.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged)
// 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,
})
if err != nil {
log.Println(err)
return
@ -125,75 +143,29 @@ func main() {
defer discord.Close()
log.Println("Discord Bot Connected")
log.Printf("Discord bot looking for command !%v", *discordCommand)
dgv, err := discord.ChannelVoiceJoin(*discordGID, *discordCID, false, false)
if err != nil {
log.Println(err)
return
}
defer dgv.Speaking(false)
defer dgv.Close()
discord.ShouldReconnectOnError = true
// MUMBLE Setup
config := gumble.NewConfig()
config.Username = *mumbleUsername
config.Password = *mumblePassword
config.AudioInterval = time.Millisecond * 10
m := MumbleDuplex{}
var tlsConfig tls.Config
if *mumbleInsecure {
tlsConfig.InsecureSkipVerify = true
switch *mode {
case "auto":
log.Println("bridge starting in automatic mode")
Bridge.AutoChan = make(chan bool)
Bridge.Mode = bridgeModeAuto
go AutoBridge(discord, l)
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))
default:
discord.Close()
log.Fatalln("invalid bridge mode set")
}
mumble, err := gumble.DialWithDialer(new(net.Dialer), *mumbleAddr+":"+strconv.Itoa(*mumblePort), config, &tlsConfig)
if err != nil {
log.Println(err)
return
}
defer mumble.Disconnect()
// Shared Channels
// Shared channels pass PCM information in 10ms chunks [480]int16
var toMumble = mumble.AudioOutgoing()
var toDiscord = make(chan []int16, 100)
var die = make(chan bool)
log.Println("Mumble Connected")
// Start Passing Between
// Mumble
go m.fromMumbleMixer(toDiscord)
config.AudioListeners.Attach(m)
//Discord
go discordReceivePCM(dgv, die)
go fromDiscordMixer(toMumble)
go discordSendPCM(dgv, toDiscord, die)
// Wait for Exit Signal
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt)
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())))
die <- true
}
}
}()
select {
case sig := <-c:
log.Printf("\nGot %s signal. Terminating Mumble-Bridge\n", sig)
case <-die:
log.Println("\nGot internal die request. Terminating Mumble-Bridge")
}
go discordStatusUpdate(discord, *mumbleAddr, strconv.Itoa(*mumblePort), l)
sc := make(chan os.Signal, 1)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
<-sc
log.Println("Bot shutting down")
}

View File

@ -14,7 +14,9 @@ var fromMumbleArr []chan gumble.AudioBuffer
var mumbleStreamingArr []bool
// MumbleDuplex - listenera and outgoing
type MumbleDuplex struct{}
type MumbleDuplex struct {
Close chan bool
}
// OnAudioStream - Spawn routines to handle incoming packets
func (m MumbleDuplex) OnAudioStream(e *gumble.AudioStreamEvent) {
@ -27,10 +29,13 @@ func (m MumbleDuplex) OnAudioStream(e *gumble.AudioStreamEvent) {
mumbleStreamingArr = append(mumbleStreamingArr, false)
mutex.Unlock()
go func() {
go func(die chan bool) {
log.Println("new mumble audio stream", e.User.Name)
for {
select {
case <-die:
log.Println("Removing mumble audio stream")
return
case p := <-e.C:
// log.Println("audio packet", p.Sender.Name, len(p.AudioBuffer))
@ -40,15 +45,21 @@ func (m MumbleDuplex) OnAudioStream(e *gumble.AudioStreamEvent) {
}
}
}
}()
}(m.Close)
return
}
func (m MumbleDuplex) fromMumbleMixer(toDiscord chan []int16) {
func (m MumbleDuplex) fromMumbleMixer(toDiscord chan []int16, die chan bool) {
ticker := time.NewTicker(10 * time.Millisecond)
sendAudio := false
for {
select {
case <-die:
log.Println("Killing fromMumbleMixer")
return
default:
}
<-ticker.C
mutex.Lock()
@ -88,6 +99,9 @@ func (m MumbleDuplex) fromMumbleMixer(toDiscord chan []int16) {
if sendAudio {
select {
case toDiscord <- outBuf:
case <-die:
log.Println("Killing fromMumbleMixer")
return
default:
log.Println("toDiscord buffer full. Dropping packet")
}