package main import ( "context" "log" "os" "os/signal" "sync" "syscall" "time" "github.com/bwmarrin/discordgo" ) func main() { token := os.Getenv("DISCORD_TOKEN") channelID := os.Getenv("CHANNEL_ID") if token == "" || channelID == "" { log.Fatal("DISCORD_TOKEN and CHANNEL_ID are required") } session, err := discordgo.New("Bot " + token) if err != nil { log.Fatal(err) } session.Identify.Intents = discordgo.IntentsGuilds | discordgo.IntentsGuildVoiceStates if err := session.Open(); err != nil { log.Fatal(err) } defer session.Close() channel, err := session.Channel(channelID) if err != nil { log.Fatal(err) } if channel.Type != discordgo.ChannelTypeGuildVoice { log.Fatal("CHANNEL_ID must identify a guild voice channel") } var joinMu sync.Mutex ctx, cancel := context.WithCancel(context.Background()) defer cancel() join := func() { joinMu.Lock() defer joinMu.Unlock() for { select { case <-ctx.Done(): return default: } if _, err := session.ChannelVoiceJoin(ctx, channel.GuildID, channelID, false, true); err == nil { log.Printf("joined voice channel %s", channelID) return } else { log.Printf("voice join failed: %v; retrying in 5s", err) } select { case <-ctx.Done(): return case <-time.After(5 * time.Second): } } } session.AddHandler(func(s *discordgo.Session, update *discordgo.VoiceStateUpdate) { if s.State.User == nil || update.UserID != s.State.User.ID || update.ChannelID == channelID { return } log.Print("bot left target voice channel; reconnecting") go join() }) join() signals := make(chan os.Signal, 1) signal.Notify(signals, os.Interrupt, syscall.SIGTERM) <-signals cancel() session.RLock() voice := session.VoiceConnections[channel.GuildID] session.RUnlock() if voice != nil { _ = voice.Disconnect(context.Background()) } log.Print("bot stopped") }