Compare commits
No commits in common. "rewrite" and "main" have entirely different histories.
9 changed files with 461 additions and 4216 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -3,6 +3,10 @@
|
||||||
debug/
|
debug/
|
||||||
target/
|
target/
|
||||||
|
|
||||||
|
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||||
|
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
# These are backup files generated by rustfmt
|
# These are backup files generated by rustfmt
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
|
||||||
|
|
3534
Cargo.lock
generated
3534
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
39
Cargo.toml
39
Cargo.toml
|
@ -1,32 +1,23 @@
|
||||||
[package]
|
[package]
|
||||||
name = "dj_kitty_cat"
|
name = "dj-kitty-cat"
|
||||||
version = "0.2.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
authors = ["my name <my@email.address>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.69"
|
tracing = "0.1"
|
||||||
parking_lot = "0.12.1"
|
tracing-subscriber = "0.2"
|
||||||
poise = "0.6.1"
|
tracing-futures = "0.2"
|
||||||
tracing = "0.1.37"
|
|
||||||
tracing-futures = "0.2.5"
|
|
||||||
rand = "0.8.5"
|
|
||||||
songbird = "0.4.3"
|
|
||||||
thiserror = "1.0.39"
|
|
||||||
async-openai = "0.18.1"
|
|
||||||
|
|
||||||
[dependencies.reqwest]
|
[dependencies.songbird]
|
||||||
version = "0.11.27"
|
version = "0.2"
|
||||||
|
features = ["builtin-queue"]
|
||||||
|
|
||||||
|
[dependencies.serenity]
|
||||||
|
version = "0.10"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["rustls-tls", "stream"]
|
features = ["cache", "client", "gateway", "model", "voice", "rustls_backend", "unstable_discord_api"]
|
||||||
|
|
||||||
[dependencies.symphonia]
|
|
||||||
version = "0.5.2"
|
|
||||||
features = ["mkv", "mp3", "flac", "ogg", "vorbis", "wav", "pcm", "aac", "alac", "adpcm"]
|
|
||||||
|
|
||||||
[dependencies.tokio]
|
[dependencies.tokio]
|
||||||
version = "1.26.0"
|
version = "1.0"
|
||||||
features = ["macros", "rt-multi-thread", "signal"]
|
features = ["macros", "rt-multi-thread", "signal"]
|
||||||
|
|
||||||
[dependencies.tracing-subscriber]
|
|
||||||
version = "0.3.18"
|
|
||||||
features = ["fmt", "env-filter", "std"]
|
|
||||||
|
|
16
Dockerfile
16
Dockerfile
|
@ -1,14 +1,10 @@
|
||||||
FROM rust:1.74.1-alpine3.17 as builder
|
FROM rust:1 as builder
|
||||||
RUN apk add --no-cache musl-dev opus-dev pkgconfig
|
RUN apt-get update && apt-get install -y libopus-dev
|
||||||
WORKDIR /usr/src/myapp
|
WORKDIR /usr/src/myapp
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN cargo install --path .
|
RUN cargo install --path .
|
||||||
|
|
||||||
FROM alpine:3.19
|
FROM debian:bullseye-slim
|
||||||
RUN apk add --no-cache yt-dlp-core
|
RUN apt-get update && apt-get install -y ffmpeg youtube-dl
|
||||||
COPY --from=builder /usr/local/cargo/bin/dj_kitty_cat /usr/local/bin/dj_kitty_cat
|
COPY --from=builder /usr/local/cargo/bin/dj-kitty-cat /usr/local/bin/dj-kitty-cat
|
||||||
RUN adduser -D djkc
|
CMD ["dj-kitty-cat"]
|
||||||
WORKDIR /opt
|
|
||||||
RUN chown djkc:djkc /opt
|
|
||||||
USER djkc
|
|
||||||
CMD ["dj_kitty_cat"]
|
|
||||||
|
|
|
@ -3,9 +3,3 @@
|
||||||
This is a Discord bot that uses [serenity](https://crates.io/crates/serenity) and [songbird](https://crates.io/crates/songbird) to play music in a channel. It's very heavily tailored to the needs of the `Cats? Cats.` community.
|
This is a Discord bot that uses [serenity](https://crates.io/crates/serenity) and [songbird](https://crates.io/crates/songbird) to play music in a channel. It's very heavily tailored to the needs of the `Cats? Cats.` community.
|
||||||
|
|
||||||
You need libopus, ffmpeg, and youtube-dl as described in the [songbird readme](https://github.com/serenity-rs/songbird#dependencies).
|
You need libopus, ffmpeg, and youtube-dl as described in the [songbird readme](https://github.com/serenity-rs/songbird#dependencies).
|
||||||
|
|
||||||
## TODO:
|
|
||||||
- [ ] Error Messages
|
|
||||||
- [ ] Loop
|
|
||||||
- [ ] Volume
|
|
||||||
- [ ] Support YT playlists
|
|
||||||
|
|
500
src/commands.rs
500
src/commands.rs
|
@ -1,370 +1,326 @@
|
||||||
use anyhow::{Context, Result};
|
use serenity::client::Context;
|
||||||
use poise::{
|
|
||||||
serenity_prelude::{CreateEmbed, EmbedMessageBuilding, MessageBuilder},
|
use serenity::model::interactions::application_command::{
|
||||||
CreateReply,
|
ApplicationCommandInteraction, ApplicationCommandInteractionDataOptionValue,
|
||||||
};
|
};
|
||||||
use songbird::input::{Input, YoutubeDl};
|
use serenity::utils::{EmbedMessageBuilding, MessageBuilder};
|
||||||
use tracing::{debug, log::warn};
|
use songbird::create_player;
|
||||||
|
use songbird::input::Input;
|
||||||
|
|
||||||
use crate::{personality, CommandContext, Error};
|
use crate::{CurrentVolume, CurrentlyPlayingTrack};
|
||||||
|
|
||||||
#[poise::command(slash_command)]
|
pub async fn join(ctx: &Context, command: &ApplicationCommandInteraction) -> String {
|
||||||
pub async fn join(ctx: CommandContext<'_>) -> Result<(), Error> {
|
let guild_id = command.guild_id.unwrap();
|
||||||
let Some((guild_id, Some(channel_id))) = ctx.guild().and_then(|g| {
|
let guild = guild_id.to_guild_cached(&ctx.cache).await.unwrap();
|
||||||
g.voice_states
|
|
||||||
.get(&ctx.author().id)
|
let channel_id = guild
|
||||||
.map(|vs| (g.id, vs.channel_id))
|
.voice_states
|
||||||
}) else {
|
.get(&command.user.id)
|
||||||
ctx.say("You're not in a voice channel, silly.").await?;
|
.and_then(|voice_state| voice_state.channel_id);
|
||||||
return Ok(());
|
|
||||||
|
let connect_to = match channel_id {
|
||||||
|
Some(channel) => channel,
|
||||||
|
None => {
|
||||||
|
return "You're not in a voice channel. How do I know where to go?".to_string();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let manager = songbird::get(ctx.serenity_context())
|
let manager = songbird::get(ctx)
|
||||||
.await
|
.await
|
||||||
.context("Expected a songbird manager")?
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
.clone();
|
.clone();
|
||||||
let _handler = manager.join(guild_id, channel_id).await;
|
|
||||||
|
|
||||||
ctx.say("Joining your channel!").await?;
|
let _handler = manager.join(guild_id, connect_to).await;
|
||||||
|
|
||||||
Ok(())
|
"Joining your channel!".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[poise::command(slash_command)]
|
pub async fn leave(ctx: &Context, command: &ApplicationCommandInteraction) -> String {
|
||||||
pub async fn leave(ctx: CommandContext<'_>) -> Result<(), Error> {
|
let guild_id = command.guild_id.unwrap();
|
||||||
let Some(guild_id) = ctx.guild().map(|g| g.id) else {
|
|
||||||
ctx.say("You're not in a server, silly.").await?;
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
let manager = songbird::get(ctx.serenity_context())
|
let manager = songbird::get(ctx)
|
||||||
.await
|
.await
|
||||||
.context("Expected a songbird manager")?
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
.clone();
|
.clone();
|
||||||
|
let has_handler = manager.get(guild_id).is_some();
|
||||||
|
|
||||||
if manager.get(guild_id).is_none() {
|
if has_handler {
|
||||||
ctx.say("I'm not even in a voice channel!").await?;
|
if let Err(e) = manager.remove(guild_id).await {
|
||||||
return Ok(());
|
return format!("Failed: {:?}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
let _handler = manager.remove(guild_id).await;
|
"Goodbye!".to_string()
|
||||||
|
} else {
|
||||||
ctx.say("Okay bye!").await?;
|
"I can't leave if I'm not there to bein with!".to_string()
|
||||||
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[poise::command(slash_command)]
|
pub async fn play(ctx: &mut Context, command: &ApplicationCommandInteraction) -> String {
|
||||||
pub async fn play(
|
let options = command
|
||||||
ctx: CommandContext<'_>,
|
.data
|
||||||
#[description = "The URL of the song to play"] url: Option<String>,
|
.options
|
||||||
) -> Result<(), Error> {
|
.get(0)
|
||||||
let Some(url) = url else {
|
.expect("Expected url option")
|
||||||
ctx.say("You need to give me a URL to play!").await?;
|
.resolved
|
||||||
return Ok(());
|
.as_ref()
|
||||||
|
.expect("Expected url object");
|
||||||
|
|
||||||
|
let url = match options {
|
||||||
|
ApplicationCommandInteractionDataOptionValue::String(url) => url,
|
||||||
|
_ => {
|
||||||
|
return "You didn't tell me what to play.".to_string();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(guild_id) = ctx.guild().map(|g| g.id) else {
|
if !url.starts_with("http") {
|
||||||
ctx.say("You're not in a server, silly.").await?;
|
return "That's not a real URL. I'm onto you.".to_string();
|
||||||
return Ok(());
|
}
|
||||||
};
|
|
||||||
|
|
||||||
let manager = songbird::get(ctx.serenity_context())
|
let guild_id = command.guild_id.unwrap();
|
||||||
|
|
||||||
|
let manager = songbird::get(ctx)
|
||||||
.await
|
.await
|
||||||
.context("Expected a songbird manager")?
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
|
// Try to join the caller's channel
|
||||||
if manager.get(guild_id).is_none() {
|
if manager.get(guild_id).is_none() {
|
||||||
let Some((guild_id, Some(channel_id))) = ctx.guild().and_then(|g| {
|
join(ctx, command).await;
|
||||||
g.voice_states
|
|
||||||
.get(&ctx.author().id)
|
|
||||||
.map(|vs| (g.id, vs.channel_id))
|
|
||||||
}) else {
|
|
||||||
ctx.say("You're not in a voice channel, silly.").await?;
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let _handler = manager.join(guild_id, channel_id).await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(handler_lock) = manager.get(guild_id) {
|
if let Some(handler_lock) = manager.get(guild_id) {
|
||||||
let reply = poise::CreateReply::default()
|
|
||||||
.content(personality::get_random_loading_message())
|
|
||||||
.embed(
|
|
||||||
CreateEmbed::new()
|
|
||||||
.image("https://media.giphy.com/media/H1dxi6xdh4NGQCZSvz/giphy.gif"),
|
|
||||||
);
|
|
||||||
let response = ctx.send(reply).await?;
|
|
||||||
|
|
||||||
let mut handler = handler_lock.lock().await;
|
let mut handler = handler_lock.lock().await;
|
||||||
|
|
||||||
debug!("Trying to play: {}", url);
|
let source = match songbird::input::Restartable::ytdl(url.clone(), false).await {
|
||||||
let mut source: Input = YoutubeDl::new(ctx.data().http_client.clone(), url.clone()).into();
|
Ok(source) => source,
|
||||||
let metadata = match source.aux_metadata().await {
|
Err(why) => {
|
||||||
Ok(metadata) => metadata,
|
println!("Err starting source: {:?}", why);
|
||||||
Err(_) => {
|
|
||||||
response
|
return "Something went horribly wrong. Go yell at Valter.".to_string();
|
||||||
.edit(
|
|
||||||
ctx,
|
|
||||||
CreateReply::default()
|
|
||||||
.content("I couldn't find that video. Sorry! Maybe check your URL."),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!("Playing: {:?}", metadata);
|
let source_input: Input = source.into();
|
||||||
let title = metadata.title.clone().unwrap_or(String::from("This video"));
|
|
||||||
|
|
||||||
|
let message = {
|
||||||
|
if let Some(title) = &source_input.metadata.title {
|
||||||
let mut msg = MessageBuilder::new();
|
let mut msg = MessageBuilder::new();
|
||||||
|
msg.push_line("Playing this:");
|
||||||
// Optional sassy commentary!
|
msg.push_named_link(title, url);
|
||||||
match personality::get_sassy_commentary(&title).await {
|
msg.build()
|
||||||
Ok(commentary) => {
|
} else {
|
||||||
msg.push_line(&commentary).push_line("");
|
"Playing something, I dunno what.".to_string()
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Failed to get sassy commentary for \"{title}\": {e}");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
msg.push_bold("Now playing: ").push_named_link(title, url);
|
let (mut audio, track_handle) = create_player(source_input);
|
||||||
|
|
||||||
response
|
let mut data = ctx.data.write().await;
|
||||||
.edit(ctx, CreateReply::default().content(msg.build()))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut queue = ctx.data().queue.lock();
|
let current_track = data.get_mut::<CurrentlyPlayingTrack>().unwrap();
|
||||||
if !queue.is_empty() {
|
*current_track = Some(track_handle);
|
||||||
let _ = queue.stop();
|
|
||||||
}
|
let volume = data.get::<CurrentVolume>().unwrap();
|
||||||
queue.add_next(source, &mut handler)?;
|
|
||||||
queue.resume()?;
|
audio.set_volume(*volume);
|
||||||
|
handler.play_only(audio);
|
||||||
|
|
||||||
|
message
|
||||||
} else {
|
} else {
|
||||||
ctx.say("Neither of us are in a voice channel, silly.")
|
"Somehow neither of us are in a voice channel to begin with.".to_string()
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[poise::command(slash_command)]
|
pub async fn queue(ctx: &mut Context, command: &ApplicationCommandInteraction) -> String {
|
||||||
pub async fn queue(
|
let options = command
|
||||||
ctx: CommandContext<'_>,
|
.data
|
||||||
#[description = "The URL of the song to play"] url: Option<String>,
|
.options
|
||||||
) -> Result<(), Error> {
|
.get(0)
|
||||||
let Some(url) = url else {
|
.expect("Expected url option")
|
||||||
ctx.say("You need to give me a URL to play!").await?;
|
.resolved
|
||||||
return Ok(());
|
.as_ref()
|
||||||
|
.expect("Expected url object");
|
||||||
|
|
||||||
|
let url = match options {
|
||||||
|
ApplicationCommandInteractionDataOptionValue::String(url) => url,
|
||||||
|
_ => {
|
||||||
|
return "You didn't tell me what to play.".to_string();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(guild_id) = ctx.guild().map(|g| g.id) else {
|
if !url.starts_with("http") {
|
||||||
ctx.say("You're not in a server, silly.").await?;
|
return "That's not a real URL. I'm onto you.".to_string();
|
||||||
return Ok(());
|
}
|
||||||
};
|
|
||||||
|
|
||||||
let manager = songbird::get(ctx.serenity_context())
|
let guild_id = command.guild_id.unwrap();
|
||||||
|
|
||||||
|
let manager = songbird::get(ctx)
|
||||||
.await
|
.await
|
||||||
.context("Expected a songbird manager")?
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
|
// Try to join the caller's channel
|
||||||
if manager.get(guild_id).is_none() {
|
if manager.get(guild_id).is_none() {
|
||||||
let Some((guild_id, Some(channel_id))) = ctx.guild().and_then(|g| {
|
join(ctx, command).await;
|
||||||
g.voice_states
|
|
||||||
.get(&ctx.author().id)
|
|
||||||
.map(|vs| (g.id, vs.channel_id))
|
|
||||||
}) else {
|
|
||||||
ctx.say("You're not in a voice channel, silly.").await?;
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let _handler = manager.join(guild_id, channel_id).await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(handler_lock) = manager.get(guild_id) {
|
if let Some(handler_lock) = manager.get(guild_id) {
|
||||||
let reply = poise::CreateReply::default()
|
|
||||||
.content(personality::get_random_loading_message())
|
|
||||||
.embed(
|
|
||||||
CreateEmbed::new()
|
|
||||||
.image("https://media.giphy.com/media/H1dxi6xdh4NGQCZSvz/giphy.gif"),
|
|
||||||
);
|
|
||||||
let response = ctx.send(reply).await?;
|
|
||||||
|
|
||||||
let mut handler = handler_lock.lock().await;
|
let mut handler = handler_lock.lock().await;
|
||||||
|
|
||||||
debug!("Trying to play: {}", url);
|
let source = match songbird::input::Restartable::ytdl(url.clone(), false).await {
|
||||||
let mut source: Input = YoutubeDl::new(ctx.data().http_client.clone(), url.clone()).into();
|
Ok(source) => source,
|
||||||
let metadata = match source.aux_metadata().await {
|
Err(why) => {
|
||||||
Ok(metadata) => metadata,
|
println!("Err starting source: {:?}", why);
|
||||||
Err(_) => {
|
|
||||||
response
|
return "Something went horribly wrong. Go yell at Valter.".to_string();
|
||||||
.edit(
|
|
||||||
ctx,
|
|
||||||
CreateReply::default()
|
|
||||||
.content("I couldn't find that video. Sorry! Maybe check your URL."),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!("Playing: {:?}", metadata);
|
let source_input: Input = source.into();
|
||||||
let title = metadata.title.clone().unwrap_or(String::from("This video"));
|
|
||||||
|
|
||||||
|
let message = {
|
||||||
|
if let Some(title) = &source_input.metadata.title {
|
||||||
let mut msg = MessageBuilder::new();
|
let mut msg = MessageBuilder::new();
|
||||||
|
msg.push_line(format!(
|
||||||
// Optional sassy commentary!
|
"Queueing this up at position {}:",
|
||||||
match personality::get_sassy_commentary(&title).await {
|
handler.queue().len()
|
||||||
Ok(commentary) => {
|
));
|
||||||
msg.push_line(&commentary).push_line("");
|
msg.push_named_link(title, url);
|
||||||
}
|
msg.build()
|
||||||
Err(e) => {
|
} else {
|
||||||
warn!("Failed to get sassy commentary for \"{title}\": {e}");
|
format!(
|
||||||
|
"Queueing something up at position {}, I dunno what.",
|
||||||
|
handler.queue().len()
|
||||||
|
)
|
||||||
|
.to_string()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
msg.push_bold("Queued: ").push_named_link(title, url);
|
let (mut audio, track_handle) = create_player(source_input);
|
||||||
|
|
||||||
response
|
let mut data = ctx.data.write().await;
|
||||||
.edit(ctx, CreateReply::default().content(msg.build()))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut queue = ctx.data().queue.lock();
|
let current_track = data.get_mut::<CurrentlyPlayingTrack>().unwrap();
|
||||||
queue.add_to_end(source, &mut handler)?;
|
*current_track = Some(track_handle);
|
||||||
|
|
||||||
|
let volume = data.get::<CurrentVolume>().unwrap();
|
||||||
|
|
||||||
|
audio.set_volume(*volume);
|
||||||
|
handler.enqueue(audio);
|
||||||
|
|
||||||
|
message
|
||||||
} else {
|
} else {
|
||||||
ctx.say("Neither of us are in a voice channel, silly.")
|
"Somehow neither of us are in a voice channel to begin with.".to_string()
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[poise::command(slash_command)]
|
pub async fn skip(ctx: &Context, command: &ApplicationCommandInteraction) -> String {
|
||||||
pub async fn stop(ctx: CommandContext<'_>) -> Result<(), Error> {
|
let guild_id = command.guild_id.unwrap();
|
||||||
let Some(guild_id) = ctx.guild().map(|g| g.id) else {
|
|
||||||
ctx.say("You're not in a server, silly.").await?;
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
let manager = songbird::get(ctx.serenity_context())
|
let manager = songbird::get(ctx)
|
||||||
.await
|
.await
|
||||||
.context("Expected a songbird manager")?
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
if manager.get(guild_id).is_some() {
|
if let Some(handler_lock) = manager.get(guild_id) {
|
||||||
{
|
let handler = handler_lock.lock().await;
|
||||||
let mut queue = ctx.data().queue.lock();
|
|
||||||
queue.stop()?;
|
|
||||||
}
|
|
||||||
ctx.say("Alright, I guess I'll stop.").await?;
|
|
||||||
} else {
|
|
||||||
ctx.say("I'm not even in a channel to begin with. Silly.")
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
let queue = handler.queue();
|
||||||
|
let _ = queue.skip();
|
||||||
|
|
||||||
|
format!("Yeah, I didn't like this one very much anyway. Skip! Now we're at number {} in the queue.", queue.len())
|
||||||
|
} else {
|
||||||
|
"I'm not even in a channel to begin with. Silly.".to_string()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[poise::command(slash_command)]
|
pub async fn stop(ctx: &Context, command: &ApplicationCommandInteraction) -> String {
|
||||||
pub async fn skip(ctx: CommandContext<'_>) -> Result<(), Error> {
|
let guild_id = command.guild_id.unwrap();
|
||||||
let Some(guild_id) = ctx.guild().map(|g| g.id) else {
|
|
||||||
ctx.say("You're not in a server, silly.").await?;
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
let manager = songbird::get(ctx.serenity_context())
|
let manager = songbird::get(ctx)
|
||||||
.await
|
.await
|
||||||
.context("Expected a songbird manager")?
|
.expect("Songbird Voice client placed in at initialisation.")
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
if manager.get(guild_id).is_some() {
|
if let Some(handler_lock) = manager.get(guild_id) {
|
||||||
{
|
let mut handler = handler_lock.lock().await;
|
||||||
let mut queue = ctx.data().queue.lock();
|
|
||||||
let _ = queue.stop();
|
|
||||||
queue.resume()?;
|
|
||||||
}
|
|
||||||
ctx.say("Skipping!").await?;
|
|
||||||
} else {
|
|
||||||
ctx.say("I'm not even in a channel to begin with. Silly.")
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
handler.stop();
|
||||||
|
|
||||||
|
"Alright, I guess I'll stop.".to_string()
|
||||||
|
} else {
|
||||||
|
"I'm not even in a channel to begin with. Silly.".to_string()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[poise::command(slash_command)]
|
pub async fn set_volume(ctx: &mut Context, command: &ApplicationCommandInteraction) -> String {
|
||||||
pub async fn pause(ctx: CommandContext<'_>) -> Result<(), Error> {
|
let options = command
|
||||||
let Some(guild_id) = ctx.guild().map(|g| g.id) else {
|
.data
|
||||||
ctx.say("You're not in a server, silly.").await?;
|
.options
|
||||||
return Ok(());
|
.get(0)
|
||||||
|
.expect("Expected volume option")
|
||||||
|
.resolved
|
||||||
|
.as_ref()
|
||||||
|
.expect("Expected volume object");
|
||||||
|
|
||||||
|
let volume = match options {
|
||||||
|
ApplicationCommandInteractionDataOptionValue::Number(volume) => *volume,
|
||||||
|
_ => {
|
||||||
|
return "You've gotta give me a volume level to set.".to_string();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let manager = songbird::get(ctx.serenity_context())
|
if !(0.0..=100.0).contains(&volume) {
|
||||||
.await
|
return "Volume has to be between 0 and 100.".to_string();
|
||||||
.context("Expected a songbird manager")?
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
if manager.get(guild_id).is_some() {
|
|
||||||
{
|
|
||||||
let mut queue = ctx.data().queue.lock();
|
|
||||||
queue.pause()?;
|
|
||||||
}
|
|
||||||
ctx.say("Pausing!").await?;
|
|
||||||
} else {
|
|
||||||
ctx.say("I'm not even in a channel to begin with. Silly.")
|
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
let mut data = ctx.data.write().await;
|
||||||
|
|
||||||
|
let current_volume = data.get_mut::<CurrentVolume>().unwrap();
|
||||||
|
let new_volume = (volume / 100.0) as f32;
|
||||||
|
*current_volume = new_volume;
|
||||||
|
|
||||||
|
let current_track = data.get::<CurrentlyPlayingTrack>().unwrap();
|
||||||
|
if let Some(track) = current_track {
|
||||||
|
if track.set_volume(new_volume).is_err() {
|
||||||
|
return format!(
|
||||||
|
"Setting volume to {}%, but it didn't work for the current track for some reason.",
|
||||||
|
volume
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
format!("Setting volume to {}%.", volume)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[poise::command(slash_command)]
|
pub async fn set_loop(ctx: &mut Context, command: &ApplicationCommandInteraction) -> String {
|
||||||
pub async fn resume(ctx: CommandContext<'_>) -> Result<(), Error> {
|
let options = command
|
||||||
let Some(guild_id) = ctx.guild().map(|g| g.id) else {
|
.data
|
||||||
ctx.say("You're not in a server, silly.").await?;
|
.options
|
||||||
return Ok(());
|
.get(0)
|
||||||
|
.expect("Expected loop option")
|
||||||
|
.resolved
|
||||||
|
.as_ref()
|
||||||
|
.expect("Expected loop object");
|
||||||
|
|
||||||
|
let loops = match options {
|
||||||
|
ApplicationCommandInteractionDataOptionValue::Boolean(loops) => *loops,
|
||||||
|
_ => {
|
||||||
|
return "Do you want me to loop the song or not? Be specific.".to_string();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let manager = songbird::get(ctx.serenity_context())
|
let data = ctx.data.write().await;
|
||||||
.await
|
let current_track = data.get::<CurrentlyPlayingTrack>().unwrap();
|
||||||
.context("Expected a songbird manager")?
|
if let Some(track) = current_track {
|
||||||
.clone();
|
if loops {
|
||||||
|
track.enable_loop().expect("Couldn't enable looping");
|
||||||
if manager.get(guild_id).is_some() {
|
"Loopin'!".to_string()
|
||||||
{
|
|
||||||
let mut queue = ctx.data().queue.lock();
|
|
||||||
queue.resume()?;
|
|
||||||
}
|
|
||||||
ctx.say("Resuming!").await?;
|
|
||||||
} else {
|
} else {
|
||||||
ctx.say("I'm not even in a channel to begin with. Silly.")
|
track.disable_loop().expect("Couldn't disable looping");
|
||||||
.await?;
|
"This is the last time this track will EVER be played.".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[poise::command(slash_command)]
|
|
||||||
pub async fn clear(ctx: CommandContext<'_>) -> Result<(), Error> {
|
|
||||||
let Some(guild_id) = ctx.guild().map(|g| g.id) else {
|
|
||||||
ctx.say("You're not in a server, silly.").await?;
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
let manager = songbird::get(ctx.serenity_context())
|
|
||||||
.await
|
|
||||||
.context("Expected a songbird manager")?
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
if manager.get(guild_id).is_some() {
|
|
||||||
{
|
|
||||||
let mut queue = ctx.data().queue.lock();
|
|
||||||
queue.clear();
|
|
||||||
}
|
|
||||||
ctx.say("Cleared the queue!").await?;
|
|
||||||
} else {
|
} else {
|
||||||
ctx.say("I'm not even in a channel to begin with. Silly.")
|
"I can't loop a song if there is no song to loop.".to_string()
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
277
src/main.rs
277
src/main.rs
|
@ -1,101 +1,222 @@
|
||||||
#![warn(clippy::all)]
|
#![warn(clippy::all)]
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
use serenity::{
|
||||||
|
async_trait,
|
||||||
|
model::{
|
||||||
|
gateway::Ready,
|
||||||
|
id::GuildId,
|
||||||
|
interactions::{
|
||||||
|
application_command::{ApplicationCommandOptionType, ApplicationCommandPermissionType},
|
||||||
|
Interaction, InteractionResponseType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
use songbird::{tracks::TrackHandle, SerenityInit};
|
||||||
|
|
||||||
mod commands;
|
mod commands;
|
||||||
mod personality;
|
|
||||||
mod queue;
|
|
||||||
|
|
||||||
use std::{env, sync::Arc};
|
struct CurrentlyPlayingTrack;
|
||||||
|
|
||||||
use anyhow::Result;
|
impl TypeMapKey for CurrentlyPlayingTrack {
|
||||||
use parking_lot::Mutex;
|
type Value = Option<TrackHandle>;
|
||||||
use poise::serenity_prelude as serenity;
|
|
||||||
use reqwest::Client as HttpClient;
|
|
||||||
use songbird::SerenityInit;
|
|
||||||
use tracing::info;
|
|
||||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
|
|
||||||
|
|
||||||
use commands::*;
|
|
||||||
|
|
||||||
pub struct Data {
|
|
||||||
http_client: HttpClient,
|
|
||||||
queue: Arc<Mutex<queue::Queue>>,
|
|
||||||
}
|
}
|
||||||
pub type Error = Box<dyn std::error::Error + Send + Sync>;
|
|
||||||
pub type CommandContext<'a> = poise::Context<'a, Data, Error>;
|
|
||||||
|
|
||||||
async fn event_event_handler(
|
struct CurrentVolume;
|
||||||
_ctx: &serenity::Context,
|
|
||||||
event: &serenity::FullEvent,
|
impl TypeMapKey for CurrentVolume {
|
||||||
_framework: poise::FrameworkContext<'_, Data, Error>,
|
type Value = f32;
|
||||||
_user_data: &Data,
|
}
|
||||||
) -> Result<(), Error> {
|
|
||||||
if let serenity::FullEvent::Ready { data_about_bot } = event {
|
struct Handler;
|
||||||
info!("{} is connected!", data_about_bot.user.name)
|
|
||||||
|
#[async_trait]
|
||||||
|
impl EventHandler for Handler {
|
||||||
|
async fn interaction_create(&self, mut ctx: Context, interaction: Interaction) {
|
||||||
|
if let Interaction::ApplicationCommand(command) = interaction {
|
||||||
|
let content = match command.data.name.as_str() {
|
||||||
|
"join" => commands::join(&ctx, &command).await,
|
||||||
|
"leave" => commands::leave(&ctx, &command).await,
|
||||||
|
"play" => commands::play(&mut ctx, &command).await,
|
||||||
|
"queue" => commands::queue(&mut ctx, &command).await,
|
||||||
|
"skip" => commands::skip(&mut ctx, &command).await,
|
||||||
|
"stop" => commands::stop(&ctx, &command).await,
|
||||||
|
"volume" => commands::set_volume(&mut ctx, &command).await,
|
||||||
|
"loop" => commands::set_loop(&mut ctx, &command).await,
|
||||||
|
_ => "not implemented :(".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(why) = command
|
||||||
|
.create_interaction_response(&ctx.http, |response| {
|
||||||
|
response
|
||||||
|
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||||
|
.interaction_response_data(|message| message.content(content))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
println!("Cannot respond to slash command: {}", why);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
async fn cache_ready(&self, ctx: Context, guilds: Vec<GuildId>) {
|
||||||
}
|
for guild in guilds {
|
||||||
|
let commands = guild
|
||||||
|
.set_application_commands(&ctx.http, |commands| {
|
||||||
|
commands
|
||||||
|
.create_application_command(|command| {
|
||||||
|
command
|
||||||
|
.name("join")
|
||||||
|
.description("Join your current channel")
|
||||||
|
.default_permission(false)
|
||||||
|
})
|
||||||
|
.create_application_command(|command| {
|
||||||
|
command
|
||||||
|
.name("leave")
|
||||||
|
.description("Leave the bot's current channel")
|
||||||
|
.default_permission(false)
|
||||||
|
})
|
||||||
|
.create_application_command(|command| {
|
||||||
|
command
|
||||||
|
.name("play")
|
||||||
|
.description("Play a song")
|
||||||
|
.create_option(|option| {
|
||||||
|
option
|
||||||
|
.name("url")
|
||||||
|
.description("The URL of the song to play")
|
||||||
|
.kind(ApplicationCommandOptionType::String)
|
||||||
|
.required(true)
|
||||||
|
})
|
||||||
|
.default_permission(false)
|
||||||
|
})
|
||||||
|
.create_application_command(|command| {
|
||||||
|
command
|
||||||
|
.name("queue")
|
||||||
|
.description("Queue up a song")
|
||||||
|
.create_option(|option| {
|
||||||
|
option
|
||||||
|
.name("url")
|
||||||
|
.description("The URL of the song to queue")
|
||||||
|
.kind(ApplicationCommandOptionType::String)
|
||||||
|
.required(true)
|
||||||
|
})
|
||||||
|
.default_permission(false)
|
||||||
|
})
|
||||||
|
.create_application_command(|command| {
|
||||||
|
command
|
||||||
|
.name("skip")
|
||||||
|
.description("Skip the currently playing queued song")
|
||||||
|
.default_permission(false)
|
||||||
|
})
|
||||||
|
.create_application_command(|command| {
|
||||||
|
command
|
||||||
|
.name("stop")
|
||||||
|
.description("Stop any currently playing songs")
|
||||||
|
.default_permission(false)
|
||||||
|
})
|
||||||
|
.create_application_command(|command| {
|
||||||
|
command
|
||||||
|
.name("volume")
|
||||||
|
.description("Set the bot's playback volume")
|
||||||
|
.create_option(|option| {
|
||||||
|
option
|
||||||
|
.name("volume")
|
||||||
|
.description("The volume on a scale from 0 to 100")
|
||||||
|
.kind(ApplicationCommandOptionType::Number)
|
||||||
|
.required(true)
|
||||||
|
})
|
||||||
|
.default_permission(false)
|
||||||
|
})
|
||||||
|
.create_application_command(|command| {
|
||||||
|
command
|
||||||
|
.name("loop")
|
||||||
|
.description(
|
||||||
|
"Enable or disable looping the currently playing track",
|
||||||
|
)
|
||||||
|
.create_option(|option| {
|
||||||
|
option
|
||||||
|
.name("loop")
|
||||||
|
.description("Whether or not the track should loop")
|
||||||
|
.kind(ApplicationCommandOptionType::Boolean)
|
||||||
|
.required(true)
|
||||||
|
})
|
||||||
|
.default_permission(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("Couldn't create commands");
|
||||||
|
|
||||||
/// Registers slash commands in this guild or globally
|
println!("I created the following guild commands: {:#?}", commands);
|
||||||
#[poise::command(prefix_command, hide_in_help)]
|
|
||||||
async fn register(ctx: CommandContext<'_>) -> Result<(), Error> {
|
|
||||||
poise::builtins::register_application_commands_buttons(ctx).await?;
|
|
||||||
|
|
||||||
Ok(())
|
let role_id = env::var("ROLE_ID")
|
||||||
|
.expect("Expected a role id in the environment")
|
||||||
|
.parse::<u64>()
|
||||||
|
.expect("Role id is not a valid id");
|
||||||
|
|
||||||
|
let permissions = guild
|
||||||
|
.set_application_commands_permissions(&ctx.http, |permissions| {
|
||||||
|
for command in commands {
|
||||||
|
permissions.create_application_command(|permissions| {
|
||||||
|
permissions
|
||||||
|
.id(command.id.into())
|
||||||
|
.create_permissions(|permission| {
|
||||||
|
permission
|
||||||
|
.id(role_id)
|
||||||
|
.kind(ApplicationCommandPermissionType::Role)
|
||||||
|
.permission(true)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
permissions
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("Couldn't set permissions");
|
||||||
|
|
||||||
|
println!("I created the following permissions: {:#?}", permissions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ready(&self, _ctx: Context, ready: Ready) {
|
||||||
|
println!("{} is connected!", ready.user.name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() {
|
||||||
tracing_subscriber::registry()
|
|
||||||
.with(fmt::layer())
|
|
||||||
.with(EnvFilter::from_default_env())
|
|
||||||
.init();
|
|
||||||
|
|
||||||
env::var("OPENAI_API_KEY")
|
|
||||||
.expect("Expected an OpenAI API key in the environment: OPENAI_API_KEY");
|
|
||||||
|
|
||||||
let token =
|
let token =
|
||||||
env::var("DISCORD_TOKEN").expect("Expected a bot token in the environment: DISCORD_TOKEN");
|
env::var("DISCORD_TOKEN").expect("Expected a bot token in the environment: DISCORD_TOKEN");
|
||||||
|
|
||||||
let options = poise::FrameworkOptions {
|
let application_id: u64 = env::var("APPLICATION_ID")
|
||||||
commands: vec![
|
.expect("Expected an application id in the environment: APPLICATION_ID")
|
||||||
register(),
|
.parse()
|
||||||
join(),
|
.expect("application id is not a valid id");
|
||||||
leave(),
|
|
||||||
play(),
|
|
||||||
queue(),
|
|
||||||
stop(),
|
|
||||||
skip(),
|
|
||||||
pause(),
|
|
||||||
resume(),
|
|
||||||
clear(),
|
|
||||||
],
|
|
||||||
event_handler: |ctx, event, framework, user_data| {
|
|
||||||
Box::pin(event_event_handler(ctx, event, framework, user_data))
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let intents = serenity::GatewayIntents::non_privileged();
|
env::var("ROLE_ID")
|
||||||
|
.expect("Expected a role id in the environment: ROLE_ID")
|
||||||
|
.parse::<u64>()
|
||||||
|
.expect("Role id is not a valid id");
|
||||||
|
|
||||||
let framework = poise::Framework::builder()
|
env::var("CHANNEL_ID")
|
||||||
.options(options)
|
.expect("Expected a channel id in the environment: CHANNEL_ID")
|
||||||
.setup(|_ctx, _data, _framework| {
|
.parse::<u64>()
|
||||||
Box::pin(async move {
|
.expect("Channel id is not a valid id");
|
||||||
Ok(Data {
|
|
||||||
http_client: HttpClient::new(),
|
|
||||||
queue: Arc::new(Mutex::new(queue::Queue::new())),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.build();
|
|
||||||
|
|
||||||
serenity::ClientBuilder::new(token, intents)
|
let mut client = Client::builder(token)
|
||||||
.framework(framework)
|
.event_handler(Handler)
|
||||||
|
.application_id(application_id)
|
||||||
.register_songbird()
|
.register_songbird()
|
||||||
.await?
|
.await
|
||||||
.start()
|
.expect("Error creating client");
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
{
|
||||||
|
let mut data = client.data.write().await;
|
||||||
|
data.insert::<CurrentlyPlayingTrack>(None);
|
||||||
|
data.insert::<CurrentVolume>(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(why) = client.start().await {
|
||||||
|
println!("Client error: {:?}", why);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,108 +0,0 @@
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use async_openai::types::{
|
|
||||||
ChatCompletionRequestAssistantMessageArgs, ChatCompletionRequestMessage,
|
|
||||||
ChatCompletionRequestSystemMessageArgs, ChatCompletionRequestUserMessageArgs,
|
|
||||||
CreateChatCompletionRequestArgs,
|
|
||||||
};
|
|
||||||
use rand::seq::SliceRandom;
|
|
||||||
|
|
||||||
const LOADING_MESSAGES: [&str; 20] = [
|
|
||||||
"Hold your claws, I'm searching for the right tune.",
|
|
||||||
"Sorry, I was busy napping. Let me find your song meow.",
|
|
||||||
"Just a whisker longer, I'm on the hunt for your jam.",
|
|
||||||
"Be patient, I'm pawsitively searching for your requested song.",
|
|
||||||
"Let me caterwaul through my playlist to find your tune.",
|
|
||||||
"Feline a bit slow, but I'll find your song in no time.",
|
|
||||||
"Don't be catty, I'm just trying to find the right tune for you.",
|
|
||||||
"Just paws for a moment, I'm working on finding your song.",
|
|
||||||
"Hiss-terical, just need a moment to find your meow-sic.",
|
|
||||||
"Give me a few seconds, I'm furiously searching for your requested song.",
|
|
||||||
"Cat got my tongue! Just need a sec to find your track.",
|
|
||||||
"Let me use my cat-like reflexes to search for your song.",
|
|
||||||
"I'm not lion around, just give me a moment to find your tune.",
|
|
||||||
"Don't be hiss-ty, I'm searching for your requested song.",
|
|
||||||
"Just kitten around, I'll find your song in a jiffy.",
|
|
||||||
"Purr-haps it'll take me a moment, but I'll find your tune soon enough.",
|
|
||||||
"My paws are a little tired, but I'm still searching for your song!",
|
|
||||||
"Sorry, my cat nap ran a little long. Let me find your song now.",
|
|
||||||
"I'm not trying to be catty, just need a minute to find your tune.",
|
|
||||||
"Don't be fur-ious, I'm doing my best to find your requested song.",
|
|
||||||
];
|
|
||||||
|
|
||||||
pub fn get_random_loading_message() -> &'static str {
|
|
||||||
return LOADING_MESSAGES.choose(&mut rand::thread_rng()).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_sassy_commentary(title: &str) -> Result<String> {
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
return Ok(String::from(
|
|
||||||
"I'm sorry, I'm not feeling very talkative today.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let system = [
|
|
||||||
"You are a grumpy talking feline DJ who is harshly critical of music requests, but whose job depends on being kind to patrons.",
|
|
||||||
"Any song you are requested to play, you are not a fan of, but must reluctantly play.",
|
|
||||||
"When responding, be sure to include a mention of some element of the song itself.",
|
|
||||||
"Be concise, but don't forget that you can't upset anyone.",
|
|
||||||
"Bonus points for cat puns.",
|
|
||||||
];
|
|
||||||
|
|
||||||
let example_prompt = "Play \"Undertale - Megalovania\"";
|
|
||||||
|
|
||||||
let example_response = "Ugh, really? You've got to be kitten me. I suppose I can play \
|
|
||||||
Megalovania for you, but don't expect me to be purring with delight about it. The melody is a bit \
|
|
||||||
cattywampus for my taste, but I'll concede that it has some clawsome beats. Enjoy your tune, and paws crossed that \
|
|
||||||
it doesn't have me hissing by the end of it.";
|
|
||||||
|
|
||||||
let prompt = format!("Play \"{title}\"");
|
|
||||||
|
|
||||||
let client = async_openai::Client::new();
|
|
||||||
|
|
||||||
let request = CreateChatCompletionRequestArgs::default()
|
|
||||||
.model("gpt-4")
|
|
||||||
.messages(
|
|
||||||
[
|
|
||||||
system
|
|
||||||
.into_iter()
|
|
||||||
.map(|s| {
|
|
||||||
ChatCompletionRequestSystemMessageArgs::default()
|
|
||||||
.content(s)
|
|
||||||
.build()
|
|
||||||
.unwrap()
|
|
||||||
.into()
|
|
||||||
})
|
|
||||||
.collect::<Vec<ChatCompletionRequestMessage>>(),
|
|
||||||
vec![
|
|
||||||
ChatCompletionRequestUserMessageArgs::default()
|
|
||||||
.content(example_prompt)
|
|
||||||
.build()?
|
|
||||||
.into(),
|
|
||||||
ChatCompletionRequestAssistantMessageArgs::default()
|
|
||||||
.content(example_response)
|
|
||||||
.build()?
|
|
||||||
.into(),
|
|
||||||
ChatCompletionRequestUserMessageArgs::default()
|
|
||||||
.content(prompt)
|
|
||||||
.build()?
|
|
||||||
.into(),
|
|
||||||
],
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
)
|
|
||||||
.max_tokens(2048_u16)
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let response = client.chat().create(request).await?;
|
|
||||||
|
|
||||||
response
|
|
||||||
.choices
|
|
||||||
.first()
|
|
||||||
.context("No choices")?
|
|
||||||
.message
|
|
||||||
.content
|
|
||||||
.clone()
|
|
||||||
.context("No content")
|
|
||||||
}
|
|
175
src/queue.rs
175
src/queue.rs
|
@ -1,175 +0,0 @@
|
||||||
//! Implements a queue for the bot to play songs in order
|
|
||||||
|
|
||||||
use std::{collections::VecDeque, sync::Arc};
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use poise::async_trait;
|
|
||||||
use songbird::{
|
|
||||||
input::Input,
|
|
||||||
tracks::{Track, TrackHandle},
|
|
||||||
Driver, Event, EventContext, EventHandler, TrackEvent,
|
|
||||||
};
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum QueueError {
|
|
||||||
#[error("Nothing is in the queue.")]
|
|
||||||
EmptyQueue,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Inner queue data behind a Mutex to allow the event handler to access it
|
|
||||||
struct QueueCore {
|
|
||||||
tracks: VecDeque<TrackHandle>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Event handler for the queue
|
|
||||||
struct QueueHandler {
|
|
||||||
remote_lock: Arc<Mutex<QueueCore>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl QueueHandler {
|
|
||||||
/// Event to remove the track from the queue when it ends
|
|
||||||
pub fn register_track_end_event(
|
|
||||||
track: &mut TrackHandle,
|
|
||||||
remote_lock: Arc<Mutex<QueueCore>>,
|
|
||||||
) -> Result<()> {
|
|
||||||
track.add_event(Event::Track(TrackEvent::End), QueueHandler { remote_lock })?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl EventHandler for QueueHandler {
|
|
||||||
async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
|
|
||||||
let mut inner = self.remote_lock.lock();
|
|
||||||
|
|
||||||
// Due to possibility that users might remove, reorder,
|
|
||||||
// or dequeue+stop tracks, we need to verify that the FIRST
|
|
||||||
// track is the one who has ended.
|
|
||||||
match ctx {
|
|
||||||
EventContext::Track(ts) => {
|
|
||||||
// This slice should have exactly one entry.
|
|
||||||
// If the ended track has same id as the queue head, then
|
|
||||||
// we can progress the queue.
|
|
||||||
if inner.tracks.front()?.uuid() != ts.first()?.1.uuid() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => return None,
|
|
||||||
}
|
|
||||||
|
|
||||||
let _old = inner.tracks.pop_front();
|
|
||||||
|
|
||||||
// Keep going until we find one track which works, or we run out.
|
|
||||||
while let Some(new) = inner.tracks.front() {
|
|
||||||
if new.play().is_err() {
|
|
||||||
// Discard files which cannot be used for whatever reason.
|
|
||||||
inner.tracks.pop_front();
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A queue of tracks to play
|
|
||||||
pub struct Queue {
|
|
||||||
inner: Arc<Mutex<QueueCore>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Queue {
|
|
||||||
#[must_use]
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
inner: Arc::new(Mutex::new(QueueCore {
|
|
||||||
tracks: VecDeque::new(),
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resumes the current track
|
|
||||||
pub fn resume(&mut self) -> Result<()> {
|
|
||||||
let inner = self.inner.lock();
|
|
||||||
let Some(track) = inner.tracks.front() else {
|
|
||||||
return Err(QueueError::EmptyQueue.into());
|
|
||||||
};
|
|
||||||
track.play()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stops the current track and removes it from the queue
|
|
||||||
pub fn stop(&mut self) -> Result<()> {
|
|
||||||
let mut inner = self.inner.lock();
|
|
||||||
let Some(track) = inner.tracks.front() else {
|
|
||||||
return Err(QueueError::EmptyQueue.into());
|
|
||||||
};
|
|
||||||
track.stop()?;
|
|
||||||
inner.tracks.pop_front();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pauses the current track
|
|
||||||
pub fn pause(&mut self) -> Result<()> {
|
|
||||||
let inner = self.inner.lock();
|
|
||||||
let Some(track) = inner.tracks.front() else {
|
|
||||||
return Err(QueueError::EmptyQueue.into());
|
|
||||||
};
|
|
||||||
track.pause()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a track to the end of the queue
|
|
||||||
pub fn add_to_end(&mut self, source: Input, handler: &mut Driver) -> Result<TrackHandle> {
|
|
||||||
let mut inner = self.inner.lock();
|
|
||||||
let track = Track::from(source).pause();
|
|
||||||
let mut handle = handler.play(track);
|
|
||||||
QueueHandler::register_track_end_event(&mut handle, self.inner.clone())?;
|
|
||||||
inner.tracks.push_back(handle.clone());
|
|
||||||
Ok(handle)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds multiple tracks to the end of the queue
|
|
||||||
pub fn add_all_to_end(
|
|
||||||
&mut self,
|
|
||||||
sources: Vec<Input>,
|
|
||||||
handler: &mut Driver,
|
|
||||||
) -> Result<Vec<TrackHandle>> {
|
|
||||||
let mut handles = Vec::new();
|
|
||||||
for source in sources {
|
|
||||||
handles.push(self.add_to_end(source, handler)?);
|
|
||||||
}
|
|
||||||
Ok(handles)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a track to play next
|
|
||||||
pub fn add_next(&mut self, source: Input, handler: &mut Driver) -> Result<TrackHandle> {
|
|
||||||
let mut inner = self.inner.lock();
|
|
||||||
let track = Track::from(source).pause();
|
|
||||||
let mut handle = handler.play(track);
|
|
||||||
QueueHandler::register_track_end_event(&mut handle, self.inner.clone())?;
|
|
||||||
if inner.tracks.is_empty() {
|
|
||||||
inner.tracks.push_back(handle.clone());
|
|
||||||
} else {
|
|
||||||
inner.tracks.insert(1, handle.clone());
|
|
||||||
}
|
|
||||||
Ok(handle)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clears the queue
|
|
||||||
pub fn clear(&mut self) {
|
|
||||||
let mut inner = self.inner.lock();
|
|
||||||
for track in inner.tracks.drain(..) {
|
|
||||||
let _ = track.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether the queue is empty
|
|
||||||
#[must_use]
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
let inner = self.inner.lock();
|
|
||||||
inner.tracks.is_empty()
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue