Merge pull request #7 from stryan/main

Runtime modes, status announcements, discord control
This commit is contained in:
Tyler Stiene 2021-01-16 13:48:45 -05:00 committed by GitHub
commit f92b97391a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 649 additions and 130 deletions

View File

@ -13,11 +13,13 @@ The binary will also attempt to load .env file located in the working directory.
```bash ```bash
Usage of mumble-discord-bridge: Usage of mumble-discord-bridge:
-discord-cid string -discord-cid string
DISCORD_CID, discord cid DISCORD_CID, discord channel ID
-discord-gid string -discord-gid string
DISCORD_GID, discord gid DISCORD_GID, discord guild ID
-discord-token string -discord-token string
DISCORD_TOKEN, discord bot token 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 string
MUMBLE_ADDRESS, mumble server address, example example.com MUMBLE_ADDRESS, mumble server address, example example.com
-mumble-password string -mumble-password string
@ -28,8 +30,35 @@ Usage of mumble-discord-bridge:
MUMBLE_USERNAME, mumble username (default "discord-bridge") MUMBLE_USERNAME, mumble username (default "discord-bridge")
-mumble-insecure bool -mumble-insecure bool
MUMBLE_INSECURE, allow connection to insecure (invalid TLS cert) mumble server 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 ## Setup
### Creating a Discord Bot ### Creating a Discord Bot

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

189
main.go
View File

@ -1,61 +1,22 @@
package main package main
import ( import (
"crypto/tls"
"flag" "flag"
"fmt"
"log" "log"
"net"
"os" "os"
"os/signal" "os/signal"
"strconv" "strconv"
"sync"
"syscall" "syscall"
"time" "time"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"layeh.com/gumble/gumble" "layeh.com/gumble/gumble"
"layeh.com/gumble/gumbleutil"
_ "layeh.com/gumble/opus" _ "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() { func main() {
godotenv.Load() godotenv.Load()
@ -64,11 +25,13 @@ func main() {
mumbleUsername := flag.String("mumble-username", lookupEnvOrString("MUMBLE_USERNAME", "discord-bridge"), "MUMBLE_USERNAME, mumble username") 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") 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") 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") 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") discordGID := flag.String("discord-gid", lookupEnvOrString("DISCORD_GID", ""), "DISCORD_GID, discord gid")
discordCID := flag.String("discord-cid", lookupEnvOrString("DISCORD_CID", ""), "DISCORD_CID, discord cid") 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() flag.Parse()
log.Printf("app.config %v\n", getConfig(flag.CommandLine)) log.Printf("app.config %v\n", getConfig(flag.CommandLine))
@ -88,24 +51,66 @@ func main() {
if *discordCID == "" { if *discordCID == "" {
log.Fatalln("missing discord cid") log.Fatalln("missing discord cid")
} }
if *mode == "" {
// Attempt to set the nice value of the process log.Fatalln("missing mode set")
}
if *nice {
err := syscall.Setpriority(syscall.PRIO_PROCESS, os.Getpid(), -5) err := syscall.Setpriority(syscall.PRIO_PROCESS, os.Getpid(), -5)
if err != nil { if err != nil {
log.Println("Unable to set priority. ", err) log.Println("Unable to set priority. ", err)
} }
}
// DISCORD Setup //Connect to discord
discord, err := discordgo.New("Bot " + *discordToken) discord, err := discordgo.New("Bot " + *discordToken)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return 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 // Open Websocket
discord.LogLevel = 2 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() err = discord.Open()
l.BridgeConf.Config.Attach(gumbleutil.Listener{
Connect: l.mumbleConnect,
UserChange: l.mumbleUserChange,
})
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return return
@ -113,75 +118,29 @@ func main() {
defer discord.Close() defer discord.Close()
log.Println("Discord Bot Connected") log.Println("Discord Bot Connected")
log.Printf("Discord bot looking for command !%v", *discordCommand)
dgv, err := discord.ChannelVoiceJoin(*discordGID, *discordCID, false, false) switch *mode {
if err != nil { case "auto":
log.Println(err) log.Println("bridge starting in automatic mode")
return Bridge.AutoChan = make(chan bool)
} Bridge.Mode = bridgeModeAuto
defer dgv.Speaking(false) go AutoBridge(discord, l)
defer dgv.Close() case "manual":
log.Println("bridge starting in manual mode")
discord.ShouldReconnectOnError = true Bridge.Mode = bridgeModeManual
case "constant":
// MUMBLE Setup log.Println("bridge starting in constant mode")
Bridge.Mode = bridgeModeConstant
config := gumble.NewConfig() go startBridge(discord, *discordGID, *discordCID, l, make(chan bool))
config.Username = *mumbleUsername default:
config.Password = *mumblePassword discord.Close()
config.AudioInterval = time.Millisecond * 10 log.Fatalln("invalid bridge mode set")
m := MumbleDuplex{}
var tlsConfig tls.Config
if *mumbleInsecure {
tlsConfig.InsecureSkipVerify = true
} }
mumble, err := gumble.DialWithDialer(new(net.Dialer), *mumbleAddr+":"+strconv.Itoa(*mumblePort), config, &tlsConfig) go discordStatusUpdate(discord, *mumbleAddr, strconv.Itoa(*mumblePort), l)
sc := make(chan os.Signal, 1)
if err != nil { signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
log.Println(err) <-sc
return log.Println("Bot shutting down")
}
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")
}
} }

View File

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