discord dm
disable text messages for discord and mumble with option
list users connected to discord on connection to mumble
This commit is contained in:
Tyler Stiene 2021-01-19 01:06:08 -05:00
parent f5a230d57d
commit 3ef9fc40bf
10 changed files with 621 additions and 484 deletions

View File

@ -1,4 +1,4 @@
GOFILES=main.go mumble.go discord.go bridge.go config.go handlers.go GOFILES=main.go mumble.go discord.go bridge.go config.go mumble-handlers.go discord-handlers.go
mumble-discord-bridge: $(GOFILES) mumble-discord-bridge: $(GOFILES)
goreleaser build --skip-validate --rm-dist goreleaser build --skip-validate --rm-dist

View File

@ -11,29 +11,35 @@ All variables can be set using flags or in the environment.
The binary will also attempt to load .env file located in the working directory. 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 channel ID DISCORD_CID, discord cid, required
-discord-gid string
DISCORD_GID, discord guild ID
-discord-token string
DISCORD_TOKEN, discord bot token
-discord-command string -discord-command string
DISCORD_COMMAND, the string to look for when manually entering commands in Discord (in the form of !DISCORD_COMMAND) DISCORD_COMMAND, Discord command string, env alt DISCORD_COMMAND, optional, (defaults mumble-discord) (default "mumble-discord")
-mumble-address string -discord-disable-text
MUMBLE_ADDRESS, mumble server address, example example.com DISCORD_DISABLE_TEXT, disable sending direct messages to discord, (default false)
-mumble-password string -discord-gid string
MUMBLE_PASSWORD, mumble password, optional DISCORD_GID, discord gid, required
-mumble-port int -discord-token string
MUMBLE_PORT mumble port (default 64738) DISCORD_TOKEN, discord bot token, required
-mumble-username string
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 string
MODE, determines what mode the bridge starts in MODE, [constant, manual, auto] determine which mode the bridge starts in, (default constant) (default "constant")
-mumble-address string
MUMBLE_ADDRESS, mumble server address, example example.com, required
-mumble-channel string
MUMBLE_CHANNEL, mumble channel to start in, optional
-mumble-disable-text
MUMBLE_DISABLE_TEXT, disable sending text to mumble, (default false)
-mumble-insecure
MUMBLE_INSECURE, mumble insecure, optional
-mumble-password string
MUMBLE_PASSWORD, mumble password, optional
-mumble-port int
MUMBLE_PORT, mumble port, (default 64738) (default 64738)
-mumble-username string
MUMBLE_USERNAME, mumble username, (default: discord) (default "Discord")
-nice
NICE, whether the bridge should automatically try to 'nice' itself, (default false)
``` ```
The bridge can be run with the follow modes: The bridge can be run with the follow modes:

247
bridge.go
View File

@ -5,183 +5,234 @@ import (
"fmt" "fmt"
"log" "log"
"net" "net"
"os"
"os/signal"
"strconv" "strconv"
"sync"
"time" "time"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"layeh.com/gumble/gumble" "layeh.com/gumble/gumble"
) )
//BridgeState manages dynamic information about the bridge during runtime type discordUser struct {
type BridgeState struct { username string
ActiveConn chan bool seen bool
Connected bool dm *discordgo.Channel
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) { //BridgeState manages dynamic information about the bridge during runtime
dgv, err := discord.ChannelVoiceJoin(discordGID, discordCID, false, false) type BridgeState struct {
// The configuration data for this bridge
BridgeConfig *BridgeConfig
// TODO
BridgeDie chan bool
// Bridge connection
Connected bool
// The bridge mode constant, auto, manual. Default is constant.
Mode bridgeMode
// Discord session. This is created and outside the bridge state
DiscordSession *discordgo.Session
// Discord voice connection. Empty if not connected.
DiscordVoice *discordgo.VoiceConnection
// Mumble client. Empty if not connected.
MumbleClient *gumble.Client
// Map of Discord users tracked by this bridge.
DiscordUsers map[string]discordUser
DiscordUsersMutex sync.Mutex
// Map of Mumble users tracked by this bridge
MumbleUsers map[string]bool
MumbleUsersMutex sync.Mutex
// Kill the auto connect routine
AutoChanDie chan bool
// Discord Duplex and Event Listener
DiscordStream *DiscordDuplex
DiscordListener *DiscordListener
// Mumble Duplex and Event Listener
MumbleStream *MumbleDuplex
MumbleListener *MumbleListener
}
// startBridge established the voice connection
func (b *BridgeState) startBridge() {
b.BridgeDie = make(chan bool)
var err error
// DISCORD Connect Voice
b.DiscordVoice, err = b.DiscordSession.ChannelVoiceJoin(b.BridgeConfig.GID, b.BridgeConfig.CID, false, false)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return return
} }
defer dgv.Speaking(false) defer b.DiscordVoice.Speaking(false)
defer dgv.Close() defer b.DiscordVoice.Close()
// MUMBLE Setup // MUMBLE Connect
m := MumbleDuplex{ b.MumbleStream = &MumbleDuplex{
Close: make(chan bool), die: b.BridgeDie,
} }
det := b.BridgeConfig.MumbleConfig.AudioListeners.Attach(b.MumbleStream)
var tlsConfig tls.Config var tlsConfig tls.Config
if l.BridgeConf.MumbleInsecure { if b.BridgeConfig.MumbleInsecure {
tlsConfig.InsecureSkipVerify = true tlsConfig.InsecureSkipVerify = true
} }
mumble, err := gumble.DialWithDialer(new(net.Dialer), l.BridgeConf.MumbleAddr, l.BridgeConf.Config, &tlsConfig) b.MumbleClient, err = gumble.DialWithDialer(new(net.Dialer), b.BridgeConfig.MumbleAddr, b.BridgeConfig.MumbleConfig, &tlsConfig)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return return
} }
defer mumble.Disconnect() defer b.MumbleClient.Disconnect()
l.Bridge.Client = mumble
// Shared Channels // Shared Channels
// Shared channels pass PCM information in 10ms chunks [480]int16 // Shared channels pass PCM information in 10ms chunks [480]int16
var toMumble = mumble.AudioOutgoing() // These channels are internal and are not added to the bridge state.
var toMumble = b.MumbleClient.AudioOutgoing()
var toDiscord = make(chan []int16, 100) var toDiscord = make(chan []int16, 100)
log.Println("Mumble Connected") log.Println("Mumble Connected")
// Start Passing Between // Start Passing Between
// Mumble
go m.fromMumbleMixer(toDiscord, die)
det := l.BridgeConf.Config.AudioListeners.Attach(m)
//Discord // From Mumble
go discordReceivePCM(dgv, die) go b.MumbleStream.fromMumbleMixer(toDiscord, b.BridgeDie)
go fromDiscordMixer(toMumble, die)
go discordSendPCM(dgv, toDiscord, die) // From Discord
c := make(chan os.Signal) b.DiscordStream = &DiscordDuplex{
signal.Notify(c, os.Interrupt) Bridge: b,
fromDiscordMap: make(map[uint32]fromDiscord),
die: b.BridgeDie,
}
go b.DiscordStream.discordReceivePCM()
go b.DiscordStream.fromDiscordMixer(toMumble)
// To Discord
go b.DiscordStream.discordSendPCM(toDiscord)
go func() { go func() {
ticker := time.NewTicker(500 * time.Millisecond) ticker := time.NewTicker(500 * time.Millisecond)
for { for {
<-ticker.C <-ticker.C
if mumble.State() != 2 { if b.MumbleClient.State() != 2 {
log.Println("Lost mumble connection " + strconv.Itoa(int(mumble.State()))) log.Println("Lost mumble connection " + strconv.Itoa(int(b.MumbleClient.State())))
select { select {
default: default:
close(die) close(b.BridgeDie)
case <-die: case <-b.BridgeDie:
//die is already closed //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 b.Connected = true
l.ConnectedLock.Unlock()
select { select {
case sig := <-c: case <-b.BridgeDie:
log.Printf("\nGot %s signal. Terminating Mumble-Bridge\n", sig)
case <-die:
log.Println("\nGot internal die request. Terminating Mumble-Bridge") log.Println("\nGot internal die request. Terminating Mumble-Bridge")
dgv.Disconnect() b.DiscordVoice.Disconnect()
det.Detach() det.Detach()
close(die) close(toDiscord)
close(m.Close)
close(toMumble) close(toMumble)
l.Bridge.Connected = false b.Connected = false
l.Bridge.Client = nil b.DiscordVoice = nil
l.Bridge.MumbleUserCount = 0 b.MumbleClient = nil
l.Bridge.MumbleUsers = make(map[string]bool) b.MumbleUsers = make(map[string]bool)
l.Bridge.DiscordUserCount = 0 b.DiscordUsers = make(map[string]discordUser)
l.Bridge.DiscordUsers = make(map[string]bool)
} }
} }
func discordStatusUpdate(dg *discordgo.Session, host, port string, l *Listener) { func (b *BridgeState) discordStatusUpdate() {
status := ""
curr := 0
m, _ := time.ParseDuration("30s") m, _ := time.ParseDuration("30s")
for { for {
time.Sleep(3 * time.Second) time.Sleep(3 * time.Second)
resp, err := gumble.Ping(host+":"+port, -1, m) resp, err := gumble.Ping(b.BridgeConfig.MumbleAddr, -1, m)
status := ""
if err != nil { if err != nil {
log.Printf("error pinging mumble server %v\n", err) log.Printf("error pinging mumble server %v\n", err)
dg.UpdateListeningStatus("an error pinging mumble") b.DiscordSession.UpdateListeningStatus("an error pinging mumble")
} else { } else {
l.UserCountLock.Lock() b.MumbleUsersMutex.Lock()
l.ConnectedLock.Lock() userCount := resp.ConnectedUsers
curr = resp.ConnectedUsers if b.Connected {
if l.Bridge.Connected { userCount = userCount - 1
curr = curr - 1
} }
if curr != l.Bridge.MumbleUserCount { if userCount == 0 {
l.Bridge.MumbleUserCount = curr status = "No users in Mumble"
}
if curr == 0 {
status = ""
} else { } else {
if len(l.Bridge.MumbleUsers) > 0 { if len(b.MumbleUsers) > 0 {
status = fmt.Sprintf("%v/%v users in Mumble\n", len(l.Bridge.MumbleUsers), curr) status = fmt.Sprintf("%v/%v users in Mumble\n", len(b.MumbleUsers), userCount)
} else { } else {
status = fmt.Sprintf("%v users in Mumble\n", curr) status = fmt.Sprintf("%v users in Mumble\n", userCount)
} }
} }
l.ConnectedLock.Unlock() b.MumbleUsersMutex.Unlock()
l.UserCountLock.Unlock() b.DiscordSession.UpdateListeningStatus(status)
dg.UpdateListeningStatus(status)
} }
} }
} }
//AutoBridge starts a goroutine to check the number of users in discord and mumble // 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 is at least one user on both, starts up the bridge
//when there are no users on either side, kills the bridge // when there are no users on either side, kills the bridge
func AutoBridge(s *discordgo.Session, l *Listener) { func (b *BridgeState) AutoBridge() {
log.Println("beginning auto mode") log.Println("beginning auto mode")
ticker := time.NewTicker(3 * time.Second)
for { for {
select { select {
default: case <-ticker.C:
case <-l.Bridge.AutoChan: case <-b.AutoChanDie:
log.Println("ending automode") log.Println("ending automode")
return return
} }
time.Sleep(3 * time.Second)
l.UserCountLock.Lock() b.MumbleUsersMutex.Lock()
if !l.Bridge.Connected && l.Bridge.MumbleUserCount > 0 && l.Bridge.DiscordUserCount > 0 { b.DiscordUsersMutex.Lock()
if !b.Connected && len(b.MumbleUsers) > 0 && len(b.DiscordUsers) > 0 {
log.Println("users detected in mumble and discord, bridging") log.Println("users detected in mumble and discord, bridging")
die := make(chan bool) go b.startBridge()
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 { if b.Connected && len(b.MumbleUsers) == 0 && len(b.DiscordUsers) <= 1 {
log.Println("no one online, killing bridge") log.Println("no one online, killing bridge")
l.Bridge.ActiveConn <- true b.BridgeDie <- true
l.Bridge.ActiveConn = nil b.BridgeDie = nil
} }
l.UserCountLock.Unlock()
b.MumbleUsersMutex.Unlock()
b.DiscordUsersMutex.Unlock()
} }
} }
func (b *BridgeState) discordSendMessageAll(msg string) {
if b.BridgeConfig.DiscordDisableText {
return
}
b.DiscordUsersMutex.Lock()
for id := range b.DiscordUsers {
du := b.DiscordUsers[id]
if du.dm != nil {
b.DiscordSession.ChannelMessageSend(du.dm.ID, msg)
}
}
b.DiscordUsersMutex.Unlock()
}

View File

@ -21,13 +21,15 @@ const (
//BridgeConfig holds configuration information set at startup //BridgeConfig holds configuration information set at startup
//It should not change during runtime //It should not change during runtime
type BridgeConfig struct { type BridgeConfig struct {
Config *gumble.Config MumbleConfig *gumble.Config
MumbleAddr string MumbleAddr string
MumbleInsecure bool MumbleInsecure bool
MumbleChannel string MumbleChannel string
Command string MumbleDisableText bool
GID string Command string
CID string GID string
CID string
DiscordDisableText bool
} }
func lookupEnvOrString(key string, defaultVal string) string { func lookupEnvOrString(key string, defaultVal string) string {

227
discord-handlers.go Normal file
View File

@ -0,0 +1,227 @@
package main
import (
"fmt"
"log"
"strings"
"time"
"github.com/bwmarrin/discordgo"
)
// DiscordListener holds references to the current BridgeConf
// and BridgeState for use by the event handlers
type DiscordListener struct {
Bridge *BridgeState
}
func (l *DiscordListener) 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.Bridge.BridgeConfig.GID {
g = i
}
}
if g == nil {
log.Println("bad guild on READY")
return
}
for _, vs := range g.VoiceStates {
if vs.ChannelID == l.Bridge.BridgeConfig.CID {
u, err := s.User(vs.UserID)
if err != nil {
log.Println("error looking up username")
}
dm, err := s.UserChannelCreate(u.ID)
if err != nil {
log.Println("Error creating private channel for", u.Username)
}
l.Bridge.DiscordUsersMutex.Lock()
l.Bridge.DiscordUsers[vs.UserID] = discordUser{
username: u.Username,
seen: true,
dm: dm,
}
l.Bridge.DiscordUsersMutex.Unlock()
// If connected to mumble inform users of Discord users
if l.Bridge.Connected && !l.Bridge.BridgeConfig.MumbleDisableText {
l.Bridge.MumbleClient.Do(func() {
l.Bridge.MumbleClient.Self.Channel.Send(fmt.Sprintf("%v has joined Discord\n", u.Username), false)
})
}
}
}
}
func (l *DiscordListener) 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.Bridge.BridgeConfig.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)
go l.Bridge.startBridge()
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.BridgeDie <- true
l.Bridge.BridgeDie = 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.BridgeDie <- true
time.Sleep(5 * time.Second)
go l.Bridge.startBridge()
return
}
}
}
if strings.HasPrefix(m.Content, prefix+" auto") {
if l.Bridge.Mode != bridgeModeAuto {
l.Bridge.Mode = bridgeModeAuto
l.Bridge.AutoChanDie = make(chan bool)
go l.Bridge.AutoBridge()
} else {
l.Bridge.AutoChanDie <- true
l.Bridge.Mode = bridgeModeManual
}
}
}
func (l *DiscordListener) 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 *DiscordListener) voiceUpdate(s *discordgo.Session, event *discordgo.VoiceStateUpdate) {
l.Bridge.DiscordUsersMutex.Lock()
defer l.Bridge.DiscordUsersMutex.Unlock()
if event.GuildID == l.Bridge.BridgeConfig.GID {
g, err := s.State.Guild(l.Bridge.BridgeConfig.GID)
if err != nil {
log.Println("error finding guild")
panic(err)
}
for u := range l.Bridge.DiscordUsers {
du := l.Bridge.DiscordUsers[u]
du.seen = false
l.Bridge.DiscordUsers[u] = du
}
// Sync the channel voice states to the local discordUsersMap
for _, vs := range g.VoiceStates {
if vs.ChannelID == l.Bridge.BridgeConfig.CID {
if s.State.User.ID == vs.UserID {
// Ignore bot
continue
}
if _, ok := l.Bridge.DiscordUsers[vs.UserID]; !ok {
u, err := s.User(vs.UserID)
if err != nil {
log.Println("error looking up username")
continue
}
println("User joined Discord " + u.Username)
dm, err := s.UserChannelCreate(u.ID)
if err != nil {
log.Println("Error creating private channel for", u.Username)
}
l.Bridge.DiscordUsers[vs.UserID] = discordUser{
username: u.Username,
seen: true,
dm: dm,
}
if l.Bridge.Connected && !l.Bridge.BridgeConfig.MumbleDisableText {
l.Bridge.MumbleClient.Do(func() {
l.Bridge.MumbleClient.Self.Channel.Send(fmt.Sprintf("%v has joined Discord\n", u.Username), false)
})
}
} else {
du := l.Bridge.DiscordUsers[vs.UserID]
du.seen = true
l.Bridge.DiscordUsers[vs.UserID] = du
}
}
}
// Remove users that are no longer connected
for id := range l.Bridge.DiscordUsers {
if l.Bridge.DiscordUsers[id].seen == false {
println("User left Discord channel " + l.Bridge.DiscordUsers[id].username)
if l.Bridge.Connected && !l.Bridge.BridgeConfig.MumbleDisableText {
l.Bridge.MumbleClient.Do(func() {
l.Bridge.MumbleClient.Self.Channel.Send(fmt.Sprintf("%v has left Discord channel\n", l.Bridge.DiscordUsers[id].username), false)
})
}
delete(l.Bridge.DiscordUsers, id)
}
}
}
}

View File

@ -18,9 +18,16 @@ type fromDiscord struct {
streaming bool streaming bool
} }
var discordMutex sync.Mutex // DiscordDuplex Handle discord voice stream
var discordMixerMutex sync.Mutex type DiscordDuplex struct {
var fromDiscordMap = make(map[uint32]fromDiscord) Bridge *BridgeState
discordMutex sync.Mutex
discordMixerMutex sync.Mutex
fromDiscordMap map[uint32]fromDiscord
die chan bool
}
// OnError gets called by dgvoice when an error is encountered. // OnError gets called by dgvoice when an error is encountered.
// By default logs to STDERR // By default logs to STDERR
@ -36,7 +43,7 @@ var OnError = func(str string, err error) {
// SendPCM will receive on the provied channel encode // SendPCM will receive on the provied channel encode
// received PCM data into Opus then send that to Discordgo // received PCM data into Opus then send that to Discordgo
func discordSendPCM(v *discordgo.VoiceConnection, pcm <-chan []int16, die chan bool) { func (dd *DiscordDuplex) discordSendPCM(pcm <-chan []int16) {
const channels int = 1 const channels int = 1
const frameRate int = 48000 // audio sampling rate const frameRate int = 48000 // audio sampling rate
const frameSize int = 960 // uint16 size of each audio frame const frameSize int = 960 // uint16 size of each audio frame
@ -57,7 +64,7 @@ func discordSendPCM(v *discordgo.VoiceConnection, pcm <-chan []int16, die chan b
for { for {
select { select {
case <-die: case <-dd.die:
log.Println("Killing discordSendPCM") log.Println("Killing discordSendPCM")
return return
default: default:
@ -65,7 +72,7 @@ func discordSendPCM(v *discordgo.VoiceConnection, pcm <-chan []int16, die chan b
<-ticker.C <-ticker.C
if len(pcm) > 1 { if len(pcm) > 1 {
if !streaming { if !streaming {
v.Speaking(true) dd.Bridge.DiscordVoice.Speaking(true)
streaming = true streaming = true
} }
@ -79,11 +86,11 @@ func discordSendPCM(v *discordgo.VoiceConnection, pcm <-chan []int16, die chan b
continue continue
} }
if v.Ready == false || v.OpusSend == nil { if dd.Bridge.DiscordVoice.Ready == false || dd.Bridge.DiscordVoice.OpusSend == nil {
if lastReady == true { if lastReady == true {
OnError(fmt.Sprintf("Discordgo not ready for opus packets. %+v : %+v", v.Ready, v.OpusSend), nil) OnError(fmt.Sprintf("Discordgo not ready for opus packets. %+v : %+v", dd.Bridge.DiscordVoice.Ready, dd.Bridge.DiscordVoice.OpusSend), nil)
readyTimeout = time.AfterFunc(30*time.Second, func() { readyTimeout = time.AfterFunc(30*time.Second, func() {
die <- true dd.die <- true
}) })
lastReady = false lastReady = false
} }
@ -93,10 +100,10 @@ func discordSendPCM(v *discordgo.VoiceConnection, pcm <-chan []int16, die chan b
lastReady = true lastReady = true
readyTimeout.Stop() readyTimeout.Stop()
} }
v.OpusSend <- opus dd.Bridge.DiscordVoice.OpusSend <- opus
} else { } else {
if streaming { if streaming {
v.Speaking(false) dd.Bridge.DiscordVoice.Speaking(false)
streaming = false streaming = false
} }
} }
@ -105,25 +112,19 @@ func discordSendPCM(v *discordgo.VoiceConnection, pcm <-chan []int16, die chan b
// ReceivePCM will receive on the the Discordgo OpusRecv channel and decode // ReceivePCM will receive on the the Discordgo OpusRecv channel and decode
// the opus audio into PCM then send it on the provided channel. // the opus audio into PCM then send it on the provided channel.
func discordReceivePCM(v *discordgo.VoiceConnection, die chan bool) { func (dd *DiscordDuplex) discordReceivePCM() {
var err error var err error
lastReady := true lastReady := true
var readyTimeout *time.Timer var readyTimeout *time.Timer
for { for {
select { if dd.Bridge.DiscordVoice.Ready == false || dd.Bridge.DiscordVoice.OpusRecv == nil {
case <-die:
log.Println("killing discord ReceivePCM")
return
default:
}
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", dd.Bridge.DiscordVoice.Ready, dd.Bridge.DiscordVoice.OpusSend), nil)
readyTimeout = time.AfterFunc(30*time.Second, func() { readyTimeout = time.AfterFunc(30*time.Second, func() {
log.Println("set ready timeout") log.Println("set ready timeout")
die <- true dd.die <- true
}) })
lastReady = false lastReady = false
} }
@ -133,22 +134,25 @@ func discordReceivePCM(v *discordgo.VoiceConnection, die chan bool) {
lastReady = true lastReady = true
readyTimeout.Stop() readyTimeout.Stop()
} }
var ok bool var ok bool
var p *discordgo.Packet var p *discordgo.Packet
select { select {
case p, ok = <-v.OpusRecv: case p, ok = <-dd.Bridge.DiscordVoice.OpusRecv:
case <-die: case <-dd.die:
log.Println("killing discord ReceivePCM") log.Println("killing discord ReceivePCM")
return return
} }
if !ok { if !ok {
log.Println("Opus not ok") log.Println("Opus not ok")
continue continue
} }
discordMutex.Lock() dd.discordMutex.Lock()
_, ok = fromDiscordMap[p.SSRC] _, ok = dd.fromDiscordMap[p.SSRC]
discordMutex.Unlock() dd.discordMutex.Unlock()
if !ok { if !ok {
newStream := fromDiscord{} newStream := fromDiscord{}
newStream.pcm = make(chan []int16, 100) newStream.pcm = make(chan []int16, 100)
@ -158,14 +162,14 @@ func discordReceivePCM(v *discordgo.VoiceConnection, die chan bool) {
OnError("error creating opus decoder", err) OnError("error creating opus decoder", err)
continue continue
} }
discordMutex.Lock() dd.discordMutex.Lock()
fromDiscordMap[p.SSRC] = newStream dd.fromDiscordMap[p.SSRC] = newStream
discordMutex.Unlock() dd.discordMutex.Unlock()
} }
discordMutex.Lock() dd.discordMutex.Lock()
p.PCM, err = fromDiscordMap[p.SSRC].decoder.Decode(p.Opus, 960, false) p.PCM, err = dd.fromDiscordMap[p.SSRC].decoder.Decode(p.Opus, 960, false)
discordMutex.Unlock() dd.discordMutex.Unlock()
if err != nil { if err != nil {
OnError("Error decoding opus data", err) OnError("Error decoding opus data", err)
continue continue
@ -175,62 +179,62 @@ func discordReceivePCM(v *discordgo.VoiceConnection, die chan bool) {
continue continue
} }
discordMutex.Lock() dd.discordMutex.Lock()
select { select {
case fromDiscordMap[p.SSRC].pcm <- p.PCM[0:480]: case dd.fromDiscordMap[p.SSRC].pcm <- p.PCM[0:480]:
default: default:
log.Println("fromDiscordMap buffer full. Dropping packet") log.Println("fromDiscordMap buffer full. Dropping packet")
discordMutex.Unlock() dd.discordMutex.Unlock()
continue continue
} }
select { select {
case fromDiscordMap[p.SSRC].pcm <- p.PCM[480:960]: case dd.fromDiscordMap[p.SSRC].pcm <- p.PCM[480:960]:
default: default:
log.Println("fromDiscordMap buffer full. Dropping packet") log.Println("fromDiscordMap buffer full. Dropping packet")
} }
discordMutex.Unlock() dd.discordMutex.Unlock()
} }
} }
func fromDiscordMixer(toMumble chan<- gumble.AudioBuffer, die chan bool) { func (dd *DiscordDuplex) fromDiscordMixer(toMumble chan<- gumble.AudioBuffer) {
ticker := time.NewTicker(10 * time.Millisecond) ticker := time.NewTicker(10 * time.Millisecond)
sendAudio := false sendAudio := false
for { for {
select { select {
case <-die: case <-dd.die:
log.Println("killing fromDiscordMixer") log.Println("killing fromDiscordMixer")
return return
default: case <-ticker.C:
} }
<-ticker.C
discordMutex.Lock() dd.discordMutex.Lock()
sendAudio = false sendAudio = false
internalMixerArr := make([][]int16, 0) internalMixerArr := make([][]int16, 0)
// Work through each channel // Work through each channel
for i := range fromDiscordMap { for i := range dd.fromDiscordMap {
if len(fromDiscordMap[i].pcm) > 0 { if len(dd.fromDiscordMap[i].pcm) > 0 {
sendAudio = true sendAudio = true
if fromDiscordMap[i].streaming == false { if dd.fromDiscordMap[i].streaming == false {
x := fromDiscordMap[i] x := dd.fromDiscordMap[i]
x.streaming = true x.streaming = true
fromDiscordMap[i] = x dd.fromDiscordMap[i] = x
} }
x1 := (<-fromDiscordMap[i].pcm) x1 := (<-dd.fromDiscordMap[i].pcm)
internalMixerArr = append(internalMixerArr, x1) internalMixerArr = append(internalMixerArr, x1)
} else { } else {
if fromDiscordMap[i].streaming == true { if dd.fromDiscordMap[i].streaming == true {
x := fromDiscordMap[i] x := dd.fromDiscordMap[i]
x.streaming = false x.streaming = false
fromDiscordMap[i] = x dd.fromDiscordMap[i] = x
} }
} }
} }
discordMutex.Unlock() dd.discordMutex.Unlock()
outBuf := make([]int16, 480) outBuf := make([]int16, 480)

View File

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

146
main.go
View File

@ -2,12 +2,12 @@ package main
import ( import (
"flag" "flag"
"fmt"
"log" "log"
"os" "os"
"os/signal" "os/signal"
"runtime/pprof" "runtime/pprof"
"strconv" "strconv"
"sync"
"syscall" "syscall"
"time" "time"
@ -26,23 +26,27 @@ var (
) )
func main() { func main() {
log.Println("Mumble-Discord-Bridge") var err error
log.Println("v" + version + " " + commit + " " + date)
fmt.Println("Mumble-Discord-Bridge")
fmt.Println("v" + version + " " + commit + " " + date)
godotenv.Load() godotenv.Load()
mumbleAddr := flag.String("mumble-address", lookupEnvOrString("MUMBLE_ADDRESS", ""), "MUMBLE_ADDRESS, mumble server address, example example.com") mumbleAddr := flag.String("mumble-address", lookupEnvOrString("MUMBLE_ADDRESS", ""), "MUMBLE_ADDRESS, mumble server address, example example.com, required")
mumblePort := flag.Int("mumble-port", lookupEnvOrInt("MUMBLE_PORT", 64738), "MUMBLE_PORT mumble port") mumblePort := flag.Int("mumble-port", lookupEnvOrInt("MUMBLE_PORT", 64738), "MUMBLE_PORT, mumble port, (default 64738)")
mumbleUsername := flag.String("mumble-username", lookupEnvOrString("MUMBLE_USERNAME", "Discord"), "MUMBLE_USERNAME, mumble username") mumbleUsername := flag.String("mumble-username", lookupEnvOrString("MUMBLE_USERNAME", "Discord"), "MUMBLE_USERNAME, mumble username, (default: discord)")
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, mumble insecure, optional")
mumbleChannel := flag.String("mumble-channel", lookupEnvOrString("MUMBLE_CHANNEL", ""), "mumble channel to start in") mumbleChannel := flag.String("mumble-channel", lookupEnvOrString("MUMBLE_CHANNEL", ""), "MUMBLE_CHANNEL, mumble channel to start in, optional")
discordToken := flag.String("discord-token", lookupEnvOrString("DISCORD_TOKEN", ""), "DISCORD_TOKEN, discord bot token") mumbleDisableText := flag.Bool("mumble-disable-text", lookupEnvOrBool("MUMBLE_DISABLE_TEXT", false), "MUMBLE_DISABLE_TEXT, disable sending text to mumble, (default false)")
discordGID := flag.String("discord-gid", lookupEnvOrString("DISCORD_GID", ""), "DISCORD_GID, discord gid") discordToken := flag.String("discord-token", lookupEnvOrString("DISCORD_TOKEN", ""), "DISCORD_TOKEN, discord bot token, required")
discordCID := flag.String("discord-cid", lookupEnvOrString("DISCORD_CID", ""), "DISCORD_CID, discord cid") discordGID := flag.String("discord-gid", lookupEnvOrString("DISCORD_GID", ""), "DISCORD_GID, discord gid, required")
discordCommand := flag.String("discord-command", lookupEnvOrString("DISCORD_COMMAND", "mumble-discord"), "DISCORD_COMMAND,Discord command string, env alt DISCORD_COMMAND, optional, defaults to mumble-discord") discordCID := flag.String("discord-cid", lookupEnvOrString("DISCORD_CID", ""), "DISCORD_CID, discord cid, required")
mode := flag.String("mode", lookupEnvOrString("MODE", "constant"), "MODE,determine which mode the bridge starts in") discordCommand := flag.String("discord-command", lookupEnvOrString("DISCORD_COMMAND", "mumble-discord"), "DISCORD_COMMAND, Discord command string, env alt DISCORD_COMMAND, optional, (defaults mumble-discord)")
nice := flag.Bool("nice", lookupEnvOrBool("NICE", false), "NICE,whether the bridge should automatically try to 'nice' itself") discordDisableText := flag.Bool("discord-disable-text", lookupEnvOrBool("DISCORD_DISABLE_TEXT", false), "DISCORD_DISABLE_TEXT, disable sending direct messages to discord, (default false)")
mode := flag.String("mode", lookupEnvOrString("MODE", "constant"), "MODE, [constant, manual, auto] determine which mode the bridge starts in, (default constant)")
nice := flag.Bool("nice", lookupEnvOrBool("NICE", false), "NICE, whether the bridge should automatically try to 'nice' itself, (default false)")
cpuprofile := flag.String("cpuprofile", "", "write cpu profile to `file`") cpuprofile := flag.String("cpuprofile", "", "write cpu profile to `file`")
@ -75,6 +79,7 @@ func main() {
} }
} }
// Optional CPU Profiling
if *cpuprofile != "" { if *cpuprofile != "" {
f, err := os.Create(*cpuprofile) f, err := os.Create(*cpuprofile)
if err != nil { if err != nil {
@ -87,62 +92,70 @@ func main() {
defer pprof.StopCPUProfile() defer pprof.StopCPUProfile()
} }
//Connect to discord // BRIDGE SETUP
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{ Bridge := &BridgeState{
ActiveConn: make(chan bool), BridgeConfig: &BridgeConfig{
Connected: false, // MumbleConfig: config,
MumbleUserCount: 0, MumbleAddr: *mumbleAddr + ":" + strconv.Itoa(*mumblePort),
DiscordUserCount: 0, MumbleInsecure: *mumbleInsecure,
DiscordUsers: make(map[string]bool), MumbleChannel: *mumbleChannel,
MumbleUsers: make(map[string]bool), MumbleDisableText: *mumbleDisableText,
Command: *discordCommand,
GID: *discordGID,
CID: *discordCID,
DiscordDisableText: *discordDisableText,
},
Connected: false,
DiscordUsers: make(map[string]discordUser),
MumbleUsers: make(map[string]bool),
} }
ul := &sync.Mutex{}
cl := &sync.Mutex{}
l := &Listener{BridgeConf, Bridge, ul, cl}
// Discord setup // MUMBLE SETUP
// Open Websocket MumbleConfig := gumble.NewConfig()
discord.LogLevel = 1 Bridge.BridgeConfig.MumbleConfig = MumbleConfig
discord.StateEnabled = true MumbleConfig.Username = *mumbleUsername
discord.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged) MumbleConfig.Password = *mumblePassword
discord.ShouldReconnectOnError = true MumbleConfig.AudioInterval = time.Millisecond * 10
// register handlers
discord.AddHandler(l.ready) Bridge.MumbleListener = &MumbleListener{
discord.AddHandler(l.messageCreate) Bridge: Bridge,
discord.AddHandler(l.guildCreate) }
discord.AddHandler(l.voiceUpdate)
err = discord.Open() MumbleConfig.Attach(gumbleutil.Listener{
l.BridgeConf.Config.Attach(gumbleutil.Listener{ Connect: Bridge.MumbleListener.mumbleConnect,
Connect: l.mumbleConnect, UserChange: Bridge.MumbleListener.mumbleUserChange,
UserChange: l.mumbleUserChange,
}) })
// DISCORD SETUP
//Connect to discord
Bridge.DiscordSession, err = discordgo.New("Bot " + *discordToken)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return return
} }
defer discord.Close()
Bridge.DiscordSession.LogLevel = 1
Bridge.DiscordSession.StateEnabled = true
Bridge.DiscordSession.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged)
Bridge.DiscordSession.ShouldReconnectOnError = true
// register handlers
Bridge.DiscordListener = &DiscordListener{
Bridge: Bridge,
}
Bridge.DiscordSession.AddHandler(Bridge.DiscordListener.ready)
Bridge.DiscordSession.AddHandler(Bridge.DiscordListener.messageCreate)
Bridge.DiscordSession.AddHandler(Bridge.DiscordListener.guildCreate)
Bridge.DiscordSession.AddHandler(Bridge.DiscordListener.voiceUpdate)
// Open Discord websocket
err = Bridge.DiscordSession.Open()
if err != nil {
log.Println(err)
return
}
defer Bridge.DiscordSession.Close()
log.Println("Discord Bot Connected") log.Println("Discord Bot Connected")
log.Printf("Discord bot looking for command !%v", *discordCommand) log.Printf("Discord bot looking for command !%v", *discordCommand)
@ -150,22 +163,23 @@ func main() {
switch *mode { switch *mode {
case "auto": case "auto":
log.Println("bridge starting in automatic mode") log.Println("bridge starting in automatic mode")
Bridge.AutoChan = make(chan bool) Bridge.AutoChanDie = make(chan bool)
Bridge.Mode = bridgeModeAuto Bridge.Mode = bridgeModeAuto
go AutoBridge(discord, l) go Bridge.AutoBridge()
case "manual": case "manual":
log.Println("bridge starting in manual mode") log.Println("bridge starting in manual mode")
Bridge.Mode = bridgeModeManual Bridge.Mode = bridgeModeManual
case "constant": case "constant":
log.Println("bridge starting in constant mode") log.Println("bridge starting in constant mode")
Bridge.Mode = bridgeModeConstant Bridge.Mode = bridgeModeConstant
go startBridge(discord, *discordGID, *discordCID, l, make(chan bool)) go Bridge.startBridge()
default: default:
discord.Close() Bridge.DiscordSession.Close()
log.Fatalln("invalid bridge mode set") log.Fatalln("invalid bridge mode set")
} }
go discordStatusUpdate(discord, *mumbleAddr, strconv.Itoa(*mumblePort), l) go Bridge.discordStatusUpdate()
sc := make(chan os.Signal, 1) sc := make(chan os.Signal, 1)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
<-sc <-sc

69
mumble-handlers.go Normal file
View File

@ -0,0 +1,69 @@
package main
import (
"strings"
"layeh.com/gumble/gumble"
)
// MumbleListener Handle mumble events
type MumbleListener struct {
Bridge *BridgeState
}
func (l *MumbleListener) mumbleConnect(e *gumble.ConnectEvent) {
if l.Bridge.BridgeConfig.MumbleChannel != "" {
//join specified channel
startingChannel := e.Client.Channels.Find(l.Bridge.BridgeConfig.MumbleChannel)
if startingChannel != nil {
e.Client.Self.Move(startingChannel)
}
}
}
func (l *MumbleListener) mumbleUserChange(e *gumble.UserChangeEvent) {
l.Bridge.MumbleUsersMutex.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.MumbleClient.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.MumbleClient.Self.Name {
l.Bridge.MumbleUsers[user.Name] = true
}
}
}
l.Bridge.MumbleUsersMutex.Unlock()
if e.Type.Has(gumble.UserChangeConnected) {
if !l.Bridge.BridgeConfig.MumbleDisableText {
e.User.Send("Mumble-Discord-Bridge v" + version)
// Tell the user who is connected to discord
if len(l.Bridge.DiscordUsers) == 0 {
e.User.Send("No users connected to Discord")
} else {
s := "Connected to Discord: "
arr := []string{}
l.Bridge.DiscordUsersMutex.Lock()
for u := range l.Bridge.DiscordUsers {
arr = append(arr, l.Bridge.DiscordUsers[u].username)
}
s = s + strings.Join(arr[:], ",")
l.Bridge.DiscordUsersMutex.Unlock()
e.User.Send(s)
}
}
// Send discord a notice
l.Bridge.discordSendMessageAll(e.User.Name + " has joined mumble")
}
if e.Type.Has(gumble.UserChangeDisconnected) {
l.Bridge.discordSendMessageAll(e.User.Name + " has left mumble")
}
}

View File

@ -15,7 +15,7 @@ var mumbleStreamingArr []bool
// MumbleDuplex - listenera and outgoing // MumbleDuplex - listenera and outgoing
type MumbleDuplex struct { type MumbleDuplex struct {
Close chan bool die chan bool
} }
// OnAudioStream - Spawn routines to handle incoming packets // OnAudioStream - Spawn routines to handle incoming packets
@ -29,11 +29,11 @@ func (m MumbleDuplex) OnAudioStream(e *gumble.AudioStreamEvent) {
mumbleStreamingArr = append(mumbleStreamingArr, false) mumbleStreamingArr = append(mumbleStreamingArr, false)
mutex.Unlock() mutex.Unlock()
go func(die chan bool) { go func() {
log.Println("new mumble audio stream", e.User.Name) log.Println("new mumble audio stream", e.User.Name)
for { for {
select { select {
case <-die: case <-m.die:
log.Println("Removing mumble audio stream") log.Println("Removing mumble audio stream")
return return
case p := <-e.C: case p := <-e.C:
@ -45,7 +45,7 @@ func (m MumbleDuplex) OnAudioStream(e *gumble.AudioStreamEvent) {
} }
} }
} }
}(m.Close) }()
return return
} }