diff --git a/bridge.go b/bridge.go index a38722a..bc2cc69 100644 --- a/bridge.go +++ b/bridge.go @@ -12,12 +12,13 @@ import ( "github.com/bwmarrin/discordgo" "layeh.com/gumble/gumble" - "layeh.com/gumble/gumbleutil" ) +//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 @@ -26,7 +27,7 @@ type BridgeState struct { AutoChan chan bool } -func startBridge(discord *discordgo.Session, discordGID string, discordCID string, config *gumble.Config, mumbleAddr string, mumbleInsecure bool, die 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) @@ -43,21 +44,18 @@ func startBridge(discord *discordgo.Session, discordGID string, discordCID strin } var tlsConfig tls.Config - if mumbleInsecure { + if l.BridgeConf.MumbleInsecure { tlsConfig.InsecureSkipVerify = true } - config.Attach(gumbleutil.Listener{ - Connect: mumbleConnect, - UserChange: mumbleUserChange, - }) - mumble, err := gumble.DialWithDialer(new(net.Dialer), mumbleAddr, config, &tlsConfig) + + mumble, err := gumble.DialWithDialer(new(net.Dialer), l.BridgeConf.MumbleAddr, l.BridgeConf.Config, &tlsConfig) if err != nil { log.Println(err) return } defer mumble.Disconnect() - Bridge.Client = mumble + l.Bridge.Client = mumble // Shared Channels // Shared channels pass PCM information in 10ms chunks [480]int16 var toMumble = mumble.AudioOutgoing() @@ -68,7 +66,7 @@ func startBridge(discord *discordgo.Session, discordGID string, discordCID strin // Start Passing Between // Mumble go m.fromMumbleMixer(toDiscord, die) - det := config.AudioListeners.Attach(m) + det := l.BridgeConf.Config.AudioListeners.Attach(m) //Discord go discordReceivePCM(dgv, die) @@ -100,28 +98,9 @@ func startBridge(discord *discordgo.Session, discordGID string, discordCID strin } } }() - - //Setup initial discord state - g, err := discord.State.Guild(discordGID) - if err != nil { - log.Println("error finding guild") - panic(err) - } - for _, vs := range g.VoiceStates { - if vs.ChannelID == discordCID { - Bridge.DiscordUserCount = Bridge.DiscordUserCount + 1 - u, err := discord.User(vs.UserID) - if err != nil { - log.Println("error looking up username") - continue - } - Bridge.DiscordUsers[u.Username] = true - Bridge.Client.Do(func() { - Bridge.Client.Self.Channel.Send(fmt.Sprintf("%v has joined Discord channel\n", u.Username), false) - }) - } - } - Bridge.Connected = true + l.ConnectedLock.Lock() + l.Bridge.Connected = true + l.ConnectedLock.Unlock() select { case sig := <-c: @@ -133,15 +112,16 @@ func startBridge(discord *discordgo.Session, discordGID string, discordCID strin close(die) close(m.Close) close(toMumble) - Bridge.Connected = false - Bridge.Client = nil - Bridge.MumbleUserCount = 0 - Bridge.DiscordUserCount = 0 - Bridge.DiscordUsers = make(map[string]bool) + 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) { +func discordStatusUpdate(dg *discordgo.Session, host, port string, l *Listener) { status := "" curr := 0 m, _ := time.ParseDuration("30s") @@ -153,48 +133,56 @@ func discordStatusUpdate(dg *discordgo.Session, host, port string) { 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 Bridge.Connected { + if l.Bridge.Connected { curr = curr - 1 } - if curr != Bridge.MumbleUserCount { - Bridge.MumbleUserCount = curr + if curr != l.Bridge.MumbleUserCount { + l.Bridge.MumbleUserCount = curr } if curr == 0 { status = "" } else { - if len(Bridge.MumbleUsers) > 0 { - status = fmt.Sprintf("%v/%v users in Mumble\n", len(Bridge.MumbleUsers), curr) + 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) } } } -func AutoBridge(s *discordgo.Session) { +//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 <-Bridge.AutoChan: + case <-l.Bridge.AutoChan: log.Println("ending automode") return } time.Sleep(3 * time.Second) - if !Bridge.Connected && Bridge.MumbleUserCount > 0 && Bridge.DiscordUserCount > 0 { + 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) - Bridge.ActiveConn = die - go startBridge(s, BridgeConf.GID, BridgeConf.CID, BridgeConf.Config, BridgeConf.MumbleAddr, BridgeConf.MumbleInsecure, die) + l.Bridge.ActiveConn = die + go startBridge(s, l.BridgeConf.GID, l.BridgeConf.CID, l, die) } - if Bridge.Connected && Bridge.MumbleUserCount == 0 && Bridge.DiscordUserCount <= 1 { + if l.Bridge.Connected && l.Bridge.MumbleUserCount == 0 && l.Bridge.DiscordUserCount <= 1 { log.Println("no one online, killing bridge") - Bridge.ActiveConn <- true - MumbleReset() - DiscordReset() + l.Bridge.ActiveConn <- true + l.Bridge.ActiveConn = nil } + l.UserCountLock.Unlock() } } diff --git a/config.go b/config.go index 5fd9885..4522085 100644 --- a/config.go +++ b/config.go @@ -10,20 +10,21 @@ import ( "layeh.com/gumble/gumble" ) -type BridgeMode int +type bridgeMode int const ( - BridgeModeAuto BridgeMode = iota - BridgeModeManual - BridgeModeConstant + 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 - Mode BridgeMode Command string GID string CID string diff --git a/discord.go b/discord.go index 462abb7..2f72daf 100644 --- a/discord.go +++ b/discord.go @@ -22,10 +22,6 @@ var discordMutex sync.Mutex var discordMixerMutex sync.Mutex var fromDiscordMap = make(map[uint32]fromDiscord) -func DiscordReset() { - fromDiscordMap = make(map[uint32]fromDiscord) -} - // OnError gets called by dgvoice when an error is encountered. // By default logs to STDERR var OnError = func(str string, err error) { diff --git a/handlers.go b/handlers.go index ab7186a..64a6db2 100644 --- a/handlers.go +++ b/handlers.go @@ -4,19 +4,58 @@ import ( "fmt" "log" "strings" + "sync" "time" "github.com/bwmarrin/discordgo" "layeh.com/gumble/gumble" ) -func ready(s *discordgo.Session, event *discordgo.Ready) { - log.Println("READY event registered") +//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 messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { +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() + } + } +} - if BridgeConf.Mode == BridgeModeConstant { +func (l *Listener) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { + + if l.Bridge.Mode == bridgeModeConstant { return } @@ -24,108 +63,72 @@ func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { if m.Author.ID == s.State.User.ID { return } - prefix := "!" + BridgeConf.Command + // 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") { - - // 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 - } - // 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) - Bridge.ActiveConn = die - go startBridge(s, g.ID, vs.ChannelID, BridgeConf.Config, BridgeConf.MumbleAddr, BridgeConf.MumbleInsecure, die) + l.Bridge.ActiveConn = die + go startBridge(s, g.ID, vs.ChannelID, l, die) return } } } if strings.HasPrefix(m.Content, prefix+" unlink") { - - // 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 - } - // 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) - Bridge.ActiveConn <- true - Bridge.ActiveConn = nil - MumbleReset() - DiscordReset() + l.Bridge.ActiveConn <- true + l.Bridge.ActiveConn = nil return } } } if strings.HasPrefix(m.Content, prefix+" refresh") { - - // 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 - } - // 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) - Bridge.ActiveConn <- true - MumbleReset() - DiscordReset() + l.Bridge.ActiveConn <- true time.Sleep(5 * time.Second) - Bridge.ActiveConn = make(chan bool) - go startBridge(s, g.ID, vs.ChannelID, BridgeConf.Config, BridgeConf.MumbleAddr, BridgeConf.MumbleInsecure, Bridge.ActiveConn) + 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 BridgeConf.Mode != BridgeModeAuto { - BridgeConf.Mode = BridgeModeAuto - Bridge.AutoChan = make(chan bool) - go AutoBridge(s) + if l.Bridge.Mode != bridgeModeAuto { + l.Bridge.Mode = bridgeModeAuto + l.Bridge.AutoChan = make(chan bool) + go AutoBridge(s, l) } else { - Bridge.AutoChan <- true - BridgeConf.Mode = BridgeModeManual + l.Bridge.AutoChan <- true + l.Bridge.Mode = bridgeModeManual } } } -func guildCreate(s *discordgo.Session, event *discordgo.GuildCreate) { +func (l *Listener) guildCreate(s *discordgo.Session, event *discordgo.GuildCreate) { if event.Guild.Unavailable { return @@ -139,27 +142,32 @@ func guildCreate(s *discordgo.Session, event *discordgo.GuildCreate) { } } -func voiceUpdate(s *discordgo.Session, event *discordgo.VoiceStateUpdate) { - if event.GuildID == BridgeConf.GID { - if event.ChannelID == BridgeConf.CID { +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 Bridge.DiscordUsers[u.Username] { + if l.Bridge.DiscordUsers[u.Username] { //not actually new user + l.UserCountLock.Unlock() return } log.Println("user joined watched discord channel") - if Bridge.Connected { - Bridge.Client.Do(func() { - Bridge.Client.Self.Channel.Send(fmt.Sprintf("%v has joined Discord channel\n", u.Username), false) + 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) }) } - Bridge.DiscordUsers[u.Username] = true - Bridge.DiscordUserCount = Bridge.DiscordUserCount + 1 + 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 @@ -167,56 +175,62 @@ func voiceUpdate(s *discordgo.Session, event *discordgo.VoiceStateUpdate) { 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 == BridgeConf.CID { + if vs.ChannelID == l.BridgeConf.CID { count = count + 1 } } - if Bridge.DiscordUserCount > count { + 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(Bridge.DiscordUsers, u.Username) + delete(l.Bridge.DiscordUsers, u.Username) log.Println("user left watched discord channel") - if Bridge.Connected { - Bridge.Client.Do(func() { - Bridge.Client.Self.Channel.Send(fmt.Sprintf("%v has left Discord channel\n", u.Username), false) + 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) }) } - Bridge.DiscordUserCount = count + l.ConnectedLock.Unlock() + l.Bridge.DiscordUserCount = count } + l.UserCountLock.Unlock() } } return } -func mumbleConnect(e *gumble.ConnectEvent) { - if BridgeConf.MumbleChannel != "" { +func (l *Listener) mumbleConnect(e *gumble.ConnectEvent) { + if l.BridgeConf.MumbleChannel != "" { //join specified channel - startingChannel := e.Client.Channels.Find(BridgeConf.MumbleChannel) + startingChannel := e.Client.Channels.Find(l.BridgeConf.MumbleChannel) if startingChannel != nil { e.Client.Self.Move(startingChannel) } } } -func mumbleUserChange(e *gumble.UserChangeEvent) { +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) { - Bridge.MumbleUsers = make(map[string]bool) - for _, user := range Bridge.Client.Self.Channel.Users { + 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 != Bridge.Client.Self.Name { - Bridge.MumbleUsers[user.Name] = true + 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 21e2666..e8414d1 100644 --- a/main.go +++ b/main.go @@ -6,18 +6,17 @@ import ( "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" ) -var BridgeConf *BridgeConfig -var Bridge *BridgeState - func main() { godotenv.Load() @@ -59,24 +58,56 @@ func main() { // 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(ready) - discord.AddHandler(messageCreate) - discord.AddHandler(guildCreate) - discord.AddHandler(voiceUpdate) + 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 @@ -85,49 +116,26 @@ func main() { log.Println("Discord Bot Connected") log.Printf("Discord bot looking for command !%v", *discordCommand) - // 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, - Mode: -1, - Command: *discordCommand, - GID: *discordGID, - CID: *discordCID, - } - Bridge = &BridgeState{ - ActiveConn: make(chan bool), - Connected: false, - MumbleUserCount: 0, - DiscordUserCount: 0, - DiscordUsers: make(map[string]bool), - } switch *mode { case "auto": log.Println("bridge starting in automatic mode") Bridge.AutoChan = make(chan bool) - BridgeConf.Mode = BridgeModeAuto - go AutoBridge(discord) + Bridge.Mode = bridgeModeAuto + go AutoBridge(discord, l) case "manual": log.Println("bridge starting in manual mode") - BridgeConf.Mode = BridgeModeManual + Bridge.Mode = bridgeModeManual case "constant": log.Println("bridge starting in constant mode") - BridgeConf.Mode = BridgeModeConstant - go startBridge(discord, *discordGID, *discordCID, config, BridgeConf.MumbleAddr, *mumbleInsecure, make(chan bool)) + Bridge.Mode = bridgeModeConstant + go startBridge(discord, *discordGID, *discordCID, l, make(chan bool)) default: discord.Close() log.Fatalln("invalid bridge mode set") } - go discordStatusUpdate(discord, *mumbleAddr, strconv.Itoa(*mumblePort)) + 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 diff --git a/mumble.go b/mumble.go index 09141bc..24d2d4e 100644 --- a/mumble.go +++ b/mumble.go @@ -18,11 +18,6 @@ type MumbleDuplex struct { Close chan bool } -func MumbleReset() { - fromMumbleArr = []chan gumble.AudioBuffer{} - mumbleStreamingArr = []bool{} -} - // OnAudioStream - Spawn routines to handle incoming packets func (m MumbleDuplex) OnAudioStream(e *gumble.AudioStreamEvent) {