diff --git a/.gitignore b/.gitignore index 6151927..149a5d0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ play.opus output.opus .env *.wav -*.opus \ No newline at end of file +*.opus +*.webm \ No newline at end of file diff --git a/README.md b/README.md index efb41fb..7965f61 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ Currently in development. +## System requirements +- `ffmpeg` make sure libopus is included +- `yt-dlp` + ## Current testing steps Copy .env.example to .env diff --git a/main.go b/main.go index 2c3f395..51811b5 100644 --- a/main.go +++ b/main.go @@ -5,10 +5,8 @@ import ( "io" "log" "os" - "os/signal" "os/exec" - "runtime" - "strings" + "os/signal" "syscall" "time" @@ -16,13 +14,21 @@ import ( "github.com/javif89/dotenv" ) -var commandPrefix string = "!" +// var commandPrefix string = "!" + +var commandHandlers = map[string]func(s *dg.Session, i *dg.InteractionCreate){ + "play": playCommand, +} func main() { env := dotenv.Load(".env") botToken := env.Get("DISCORD_BOT_TOKEN") log.Println("Starting bot") - discord, err := dg.New(fmt.Sprintf("Bot %s", botToken)) + + var discord *dg.Session + var err error + + discord, err = dg.New(fmt.Sprintf("Bot %s", botToken)) if err != nil { log.Println("Error starting the bot", err) @@ -30,7 +36,7 @@ func main() { } discord.AddHandler(ready) - discord.AddHandler(handleCommand) // This will receive an event when a message is sent + // discord.AddHandler(handleCommand) // This will receive an event when a message is sent discord.Identify.Intents = dg.IntentsGuilds | dg.IntentsGuildMessages | dg.IntentsGuildVoiceStates @@ -39,45 +45,50 @@ func main() { log.Println("Error opening Discord session: ", err) } + // Register slash commands + discord.AddHandler(handleSlashCommand) + log.Println("Bot is running. Press CTRL+C to exit.") sc := make(chan os.Signal, 1) signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) <-sc log.Println("Bot is exiting...") + log.Println("Removing commands") + + appId := env.Get("DISCORD_APP_ID") + cmds, _ := discord.ApplicationCommands(appId, "338782945110392832") + for _, cmd := range cmds { + discord.ApplicationCommandDelete(appId, "338782945110392832", cmd.ID) + } + discord.Close() log.Println("Goodbye!") } -func ready(s *dg.Session, event *dg.Ready) { - log.Println("Bot is ready") -} +func playCommand(s *dg.Session, i *dg.InteractionCreate) { + log.Println("Handling play command") + options := i.ApplicationCommandData().Options + url := options[0].StringValue() -func handleCommand(s *dg.Session, msg *dg.MessageCreate) { - // Ignore any message sent by the bot itself - if msg.Author.ID == s.State.User.ID { - return - } + s.InteractionRespond(i.Interaction, &dg.InteractionResponse{ + Type: dg.InteractionResponseChannelMessageWithSource, + Data: &dg.InteractionResponseData{ + Content: fmt.Sprintf("Playing dat music baybeee"), + }, + }) - if !strings.HasPrefix(msg.Content, commandPrefix) { - return - } + downloadVideo(url) // Get the channel - msgChannel, err := s.State.Channel(msg.ChannelID) + msgServer, err := s.State.Guild(i.GuildID) if err != nil { - log.Println("Could not find the channel the message came from") - return - } - - msgServer, err := s.State.Guild(msgChannel.GuildID) - if err != nil { - log.Println("Failed to get server for message") + log.Println("Failed to get server for action") } var voiceChannelId string for _, vs := range msgServer.VoiceStates { - if vs.UserID == msg.Author.ID { + if vs.UserID == i.Member.User.ID { voiceChannelId = vs.ChannelID } } @@ -87,26 +98,35 @@ func handleCommand(s *dg.Session, msg *dg.MessageCreate) { log.Println("Failed to join voice channel") } - log.Println("Joined channel. Playing sound") - voiceChannel.Speaking(true) - time.Sleep(time.Second * 1) + log.Println("Joined channel") + playOnVoiceChannel(voiceChannel) + + voiceChannel.Speaking(false) + log.Println("Disconnecting from voice channel") + time.Sleep(time.Second * 2) + + voiceChannel.Disconnect() + log.Println("Disconnected") +} + +func playOnVoiceChannel(voiceChannel *dg.VoiceConnection) { log.Println("Starting ffmpeg stream") - // I got this implementation from: + // I got the original implementation from: // https://github.com/nhooyr/botatouille/blob/7e1cd9d5a8d517fd43fd11599b2a62bf832a5c96/cmd/botatouille/music/music.go#L62-L104 // after hours of searching. ffmpeg := exec.Command( - "ffmpeg", - "-i", "testfile.wav", - "-hide_banner", - "-loglevel", "quiet", + "ffmpeg", + "-i", "vid.webm", + "-hide_banner", + "-loglevel", "quiet", "-i", "testfile.wav", - "-f", "data", - "-map", "0:a", - "-ar", "48k", + "-f", "data", + "-map", "0:a", + "-ar", "48k", "-ac", "2", - "-acodec", "libopus", - "-b:a", "128k", + "-acodec", "libopus", + "-b:a", "128k", "pipe:1") ffmpegOut, err := ffmpeg.StdoutPipe() @@ -114,14 +134,6 @@ func handleCommand(s *dg.Session, msg *dg.MessageCreate) { log.Fatal(err) } - framesChan := make(chan []byte, 100000) - go func() { - for { - voiceChannel.OpusSend <- <-framesChan - } - }() - - runtime.LockOSThread() err = ffmpeg.Start() if err != nil { log.Fatal(err) @@ -142,25 +154,170 @@ func handleCommand(s *dg.Session, msg *dg.MessageCreate) { log.Fatal(err) } - log.Printf("Sending opus frame: %d bytes", n) + // log.Printf("Read packet: %d bytes", n) packets = append(packets, p[:n]) } - log.Println("Iterating through packets") - - for _, p := range packets { - log.Printf("%d bytes", len(p)) - } + voiceChannel.Speaking(true) + time.Sleep(time.Second * 2) + log.Println("Playing sound") for _, p := range packets { log.Printf("Sending packet: %d bytes", len(p)) - voiceChannel.OpusSend <- p + voiceChannel.OpusSend <- p + } + log.Println("Ended stream") +} + +func handleSlashCommand(s *dg.Session, i *dg.InteractionCreate) { + log.Println("Command received?") + if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok { + h(s, i) + } +} + +func ready(s *dg.Session, event *dg.Ready) { + commands := []*dg.ApplicationCommand{ + { + Name: "play", + Description: "Play a song from youtube, spotify, apple music, etc", + Options: []*dg.ApplicationCommandOption{ + { + Type: dg.ApplicationCommandOptionString, + Name: "url", + Description: "URL to the song", + Required: true, + }, + }, + }, } - voiceChannel.Speaking(false) - log.Println("Disconnecting from voice channel") - time.Sleep(time.Second * 2) + env := dotenv.Load(".env") + appId := env.Get("DISCORD_APP_ID") - voiceChannel.Disconnect() - log.Println("Disconnected") -} \ No newline at end of file + log.Println("Bot is ready") + log.Println("Bulk registering commands") + _, err := s.ApplicationCommandBulkOverwrite(appId, "338782945110392832", commands) + if err != nil { + log.Fatal(err) + } + log.Println("Done") +} + +func downloadVideo(url string) error { + log.Printf("Downloading: %s", url) + os.Remove("vid.webm") + cmd := exec.Command( + "yt-dlp", + "-i", + "-q", + "-o", + "vid", + url) + + err := cmd.Run() + log.Println("Downloaded") + + return err +} + +// func handleCommand(s *dg.Session, msg *dg.MessageCreate) { +// // Ignore any message sent by the bot itself +// if msg.Author.ID == s.State.User.ID { +// return +// } + +// if !strings.HasPrefix(msg.Content, commandPrefix) { +// return +// } + +// // Get the channel +// msgChannel, err := s.State.Channel(msg.ChannelID) +// if err != nil { +// log.Println("Could not find the channel the message came from") +// return +// } + +// msgServer, err := s.State.Guild(msgChannel.GuildID) +// if err != nil { +// log.Println("Failed to get server for message") +// } + +// var voiceChannelId string +// for _, vs := range msgServer.VoiceStates { +// if vs.UserID == msg.Author.ID { +// voiceChannelId = vs.ChannelID +// } +// } + +// voiceChannel, err := s.ChannelVoiceJoin(msgServer.ID, voiceChannelId, false, true) +// if err != nil { +// log.Println("Failed to join voice channel") +// } + +// log.Println("Joined channel") + +// log.Println("Starting ffmpeg stream") +// // I got the original implementation from: +// // https://github.com/nhooyr/botatouille/blob/7e1cd9d5a8d517fd43fd11599b2a62bf832a5c96/cmd/botatouille/music/music.go#L62-L104 +// // after hours of searching. +// ffmpeg := exec.Command( +// "ffmpeg", +// "-i", "testfile.wav", +// "-hide_banner", +// "-loglevel", "quiet", +// "-i", "testfile.wav", +// "-f", "data", +// "-map", "0:a", +// "-ar", "48k", +// "-ac", "2", +// "-acodec", "libopus", +// "-b:a", "128k", +// "pipe:1") + +// ffmpegOut, err := ffmpeg.StdoutPipe() +// if err != nil { +// log.Fatal(err) +// } + +// // runtime.LockOSThread() +// err = ffmpeg.Start() +// if err != nil { +// log.Fatal(err) +// } + +// packets := [][]byte{} + +// for { +// // I read in the RFC that frames will not be bigger than this size +// p := make([]byte, 960) +// n, err := ffmpegOut.Read(p) +// if err != nil { +// log.Printf("Bytes: %d", n) +// if err == io.EOF { +// log.Println("Done streaming") +// break +// } +// log.Fatal(err) +// } + +// // log.Printf("Read packet: %d bytes", n) +// packets = append(packets, p[:n]) +// } + +// voiceChannel.Speaking(true) +// time.Sleep(time.Second * 2) +// log.Println("Playing sound") + +// for _, p := range packets { +// log.Printf("Sending packet: %d bytes", len(p)) +// voiceChannel.OpusSend <- p +// } + +// voiceChannel.Speaking(false) +// log.Println("Disconnecting from voice channel") +// time.Sleep(time.Second * 2) + +// voiceChannel.Disconnect() +// log.Println("Disconnected") +// }