diff --git a/config.go b/config.go new file mode 100644 index 0000000..ce6993e --- /dev/null +++ b/config.go @@ -0,0 +1,57 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "strconv" + + "layeh.com/gumble/gumble" +) + +type YammerConfig struct { + Config *gumble.Config + MumbleAddr string + MumbleInsecure bool + Die chan bool + ActiveConns map[string]chan bool +} + +func lookupEnvOrString(key string, defaultVal string) string { + if val, ok := os.LookupEnv(key); ok { + return val + } + return defaultVal +} + +func lookupEnvOrInt(key string, defaultVal int) int { + if val, ok := os.LookupEnv(key); ok { + v, err := strconv.Atoi(val) + if err != nil { + log.Fatalf("LookupEnvOrInt[%s]: %v", key, err) + } + return v + } + return defaultVal +} + +func lookupEnvOrBool(key string, defaultVal bool) bool { + if val, ok := os.LookupEnv(key); ok { + v, err := strconv.ParseBool(val) + if err != nil { + log.Fatalf("LookupEnvOrInt[%s]: %v", key, err) + } + return v + } + return defaultVal +} + +func getConfig(fs *flag.FlagSet) []string { + cfg := make([]string, 0, 10) + fs.VisitAll(func(f *flag.Flag) { + cfg = append(cfg, fmt.Sprintf("%s:%q", f.Name, f.Value.String())) + }) + + return cfg +} diff --git a/handlers.go b/handlers.go new file mode 100644 index 0000000..a8efb51 --- /dev/null +++ b/handlers.go @@ -0,0 +1,91 @@ +package main + +import ( + "log" + "strings" + + "github.com/bwmarrin/discordgo" +) + +func ready(s *discordgo.Session, event *discordgo.Ready) { + log.Println("READY event registered") +} + +func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { + + // Ignore all messages created by the bot itself + // This isn't required in this specific example but it's a good practice. + log.Println("Checking message") + if m.Author.ID == s.State.User.ID { + return + } + + if strings.HasPrefix(m.Content, "!yammer link") { + + // Find the channel that the message came from. + c, err := s.State.Channel(m.ChannelID) + if err != nil { + // Could not find channel. + return + } + + // Find the guild for that channel. + g, err := s.State.Guild(c.GuildID) + if err != nil { + // Could not find guild. + return + } + + // Look for the message sender in that guild's current voice states. + for _, vs := range g.VoiceStates { + if vs.UserID == m.Author.ID { + log.Printf("Trying to join GID %v and VID %v\n", g.ID, vs.ChannelID) + die := make(chan bool) + YBConfig.ActiveConns[vs.ChannelID] = die + go startBridge(s, g.ID, vs.ChannelID, YBConfig.Config, YBConfig.MumbleAddr, YBConfig.MumbleInsecure, die) + return + } + } + } + + if strings.HasPrefix(m.Content, "!yammer unlink") { + + // Find the channel that the message came from. + c, err := s.State.Channel(m.ChannelID) + if err != nil { + // Could not find channel. + return + } + + // Find the guild for that channel. + g, err := s.State.Guild(c.GuildID) + if err != nil { + // Could not find guild. + return + } + + // Look for the message sender in that guild's current voice states. + for _, vs := range g.VoiceStates { + if vs.UserID == m.Author.ID { + log.Printf("Trying to leave GID %v and VID %v\n", g.ID, vs.ChannelID) + YBConfig.ActiveConns[vs.ChannelID] <- true + YBConfig.ActiveConns[vs.ChannelID] = nil + return + } + } + } +} + +func guildCreate(s *discordgo.Session, event *discordgo.GuildCreate) { + + if event.Guild.Unavailable { + return + } + + for _, channel := range event.Guild.Channels { + if channel.ID == event.Guild.ID { + _, _ = s.ChannelMessageSend(channel.ID, "Mumble-Discord bridge is active") + return + } + } +} diff --git a/main.go b/main.go index 0c2c690..8ae56d0 100644 --- a/main.go +++ b/main.go @@ -1,14 +1,12 @@ package main import ( - "crypto/tls" "flag" - "fmt" "log" - "net" "os" "os/signal" "strconv" + "syscall" "time" "github.com/bwmarrin/discordgo" @@ -17,43 +15,7 @@ import ( _ "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 -} +var YBConfig *YammerConfig func main() { godotenv.Load() @@ -98,6 +60,12 @@ func main() { // Open Websocket discord.LogLevel = 2 + discord.StateEnabled = true + discord.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsGuilds | discordgo.IntentsGuildMessages | discordgo.IntentsGuildVoiceStates) + // register handlers + discord.AddHandler(ready) + discord.AddHandler(messageCreate) + discord.AddHandler(guildCreate) err = discord.Open() if err != nil { log.Println(err) @@ -106,75 +74,22 @@ func main() { defer discord.Close() log.Println("Discord Bot Connected") - - 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 + YBConfig = &YammerConfig{ + Config: config, + MumbleAddr: *mumbleAddr + ":" + strconv.Itoa(*mumblePort), + MumbleInsecure: *mumbleInsecure, + ActiveConns: make(map[string]chan bool), } - mumble, err := gumble.DialWithDialer(new(net.Dialer),*mumbleAddr+":"+strconv.Itoa(*mumblePort),config, &tlsConfig) + //go startBridge(discord, *discordGID, *discordCID, config, *mumbleAddr+":"+strconv.Itoa(*mumblePort), *mumbleInsecure, die) - 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") - } + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) + <-sc + log.Println("Bot shutting down") } diff --git a/yammer.go b/yammer.go new file mode 100644 index 0000000..7186a67 --- /dev/null +++ b/yammer.go @@ -0,0 +1,119 @@ +package main + +import ( + "crypto/tls" + "fmt" + "log" + "net" + "os" + "os/signal" + "strconv" + "time" + + "github.com/bwmarrin/discordgo" + "layeh.com/gumble/gumble" +) + +func startBridge(discord *discordgo.Session, discordGID string, discordCID string, config *gumble.Config, mumbleAddr string, mumbleInsecure bool, 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{} + + var tlsConfig tls.Config + if mumbleInsecure { + tlsConfig.InsecureSkipVerify = true + } + + mumble, err := gumble.DialWithDialer(new(net.Dialer), mumbleAddr, 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) + + 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) + 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") + dgv.Disconnect() + } +} + +func pingMumble(host, port string, c chan int) { + m, _ := time.ParseDuration("30s") + curr := 0 + log.Println("Started mumble ping loop") + for { + time.Sleep(3 * time.Second) + resp, err := gumble.Ping(host+":"+port, -1, m) + if err != nil { + panic(err) + } + if resp.ConnectedUsers-1 != curr { + curr = resp.ConnectedUsers - 1 + log.Printf("Now %v users in mumble\n", curr) + if curr > 0 { + c <- curr + } + } + } + log.Println("Mumble ping loop broken") +} + +func discordStatusUpdate(dg *discordgo.Session, c chan int) { + status := "" + curr := 0 + log.Println("Started discord control loop") + for { + curr = <-c + log.Println("Updating discord status") + if curr == 0 { + status = "" + } else { + status = fmt.Sprintf("%v users in Mumble\n", curr) + } + dg.UpdateListeningStatus(status) + } + log.Println("Discord control loop broken") +}