mirror of
https://github.com/stryan/mumble-discord-bridge.git
synced 2024-11-14 11:45:40 -05:00
Merge pull request #7 from stryan/main
Runtime modes, status announcements, discord control
This commit is contained in:
commit
f92b97391a
35
README.md
35
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)
|
||||
- [discordgo](https://github.com/bwmarrin/discordgo)
|
||||
|
188
bridge.go
Normal file
188
bridge.go
Normal 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
69
config.go
Normal 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
|
||||
}
|
33
discord.go
33
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()
|
||||
|
||||
|
236
handlers.go
Normal file
236
handlers.go
Normal 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()
|
||||
}
|
195
main.go
195
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")
|
||||
}
|
||||
|
23
mumble.go
23
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")
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user