diff --git a/README.md b/README.md index c25fc88..038665d 100644 --- a/README.md +++ b/README.md @@ -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) \ No newline at end of file +- [discordgo](https://github.com/bwmarrin/discordgo) diff --git a/bridge.go b/bridge.go new file mode 100644 index 0000000..bc2cc69 --- /dev/null +++ b/bridge.go @@ -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() + } +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..4522085 --- /dev/null +++ b/config.go @@ -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 +} diff --git a/discord.go b/discord.go index 8988850..98cbcd8 100644 --- a/discord.go +++ b/discord.go @@ -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() diff --git a/handlers.go b/handlers.go new file mode 100644 index 0000000..64a6db2 --- /dev/null +++ b/handlers.go @@ -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() +} diff --git a/main.go b/main.go index ac36b1c..c8c5611 100644 --- a/main.go +++ b/main.go @@ -1,61 +1,22 @@ package main import ( - "crypto/tls" "flag" - "fmt" "log" - "net" "os" "os/signal" "strconv" + "sync" "syscall" "time" "github.com/bwmarrin/discordgo" "github.com/joho/godotenv" "layeh.com/gumble/gumble" + "layeh.com/gumble/gumbleutil" _ "layeh.com/gumble/opus" ) -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 -} - func main() { godotenv.Load() @@ -64,11 +25,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)) @@ -88,24 +51,66 @@ func main() { if *discordCID == "" { log.Fatalln("missing discord cid") } - - // Attempt to set the nice value of the process - err := syscall.Setpriority(syscall.PRIO_PROCESS, os.Getpid(), -5) - if err != nil { - log.Println("Unable to set 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 - + //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 = 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 @@ -113,75 +118,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") } diff --git a/mumble.go b/mumble.go index 2909c36..24d2d4e 100644 --- a/mumble.go +++ b/mumble.go @@ -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,14 @@ 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 { + default: + 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 +46,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 +100,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") }