first commit, yay

This commit is contained in:
2024-02-14 15:40:37 +01:00
commit c53b2bba49
4 changed files with 3474 additions and 0 deletions

409
src/main.rs Normal file
View File

@@ -0,0 +1,409 @@
use dotenv::dotenv;
use futures::StreamExt;
use songbird::{
input::{Compose, YoutubeDl},
shards::TwilightMap,
tracks::{PlayMode, TrackHandle},
Songbird,
};
use std::{collections::HashMap, env, error::Error, future::Future, num::NonZeroU64, sync::Arc};
use tokio::{process::Command, sync::RwLock};
use tracing::{debug, info};
use twilight_cache_inmemory::InMemoryCache;
use twilight_gateway::{
stream::{self, ShardEventStream},
Event, Intents, Shard,
};
use twilight_http::Client as HttpClient;
use twilight_model::{
channel::Message,
gateway::payload::incoming::MessageCreate,
id::{marker::GuildMarker, Id},
};
use twilight_standby::Standby;
type State = Arc<StateRef>;
#[derive(Debug)]
struct StateRef {
http: HttpClient,
cache: InMemoryCache,
trackdata: RwLock<HashMap<Id<GuildMarker>, TrackHandle>>,
songbird: Songbird,
standby: Standby,
}
fn spawn(
fut: impl Future<Output = Result<(), Box<dyn Error + Send + Sync + 'static>>> + Send + 'static,
) {
tokio::spawn(async move {
if let Err(why) = fut.await {
tracing::debug!("handler error: {:?}", why);
}
});
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
dotenv().ok();
// Initialize the tracing subscriber.
tracing_subscriber::fmt::init();
let (mut shards, state) = {
let token = env::var("DISCORD_TOKEN")?;
let http = HttpClient::new(token.clone());
let user_id = http.current_user().await?.model().await?.id;
let intents = Intents::GUILDS
| Intents::GUILD_MESSAGES
| Intents::GUILD_VOICE_STATES
| Intents::MESSAGE_CONTENT;
let config = twilight_gateway::Config::new(token.clone(), intents);
let shards: Vec<Shard> =
stream::create_recommended(&http, config, |_, builder| builder.build())
.await?
.collect();
let senders = TwilightMap::new(
shards
.iter()
.map(|s| (s.id().number(), s.sender()))
.collect(),
);
let songbird = Songbird::twilight(Arc::new(senders), user_id);
let cache = InMemoryCache::new();
(
shards,
Arc::new(StateRef {
http,
cache,
trackdata: Default::default(),
songbird,
standby: Standby::new(),
}),
)
};
let mut stream = ShardEventStream::new(shards.iter_mut());
loop {
let event = match stream.next().await {
Some((_, Ok(event))) => event,
Some((_, Err(source))) => {
tracing::warn!(?source, "error receiving event");
if source.is_fatal() {
break;
}
continue;
}
None => break,
};
debug!("{:?}", &event);
state.cache.update(&event);
state.standby.process(&event);
state.songbird.process(&event).await;
if let Event::MessageCreate(msg) = event {
if msg.guild_id.is_none() || !msg.content.starts_with('!') {
continue;
}
match msg.content.split(' ').next() {
Some("!join") => spawn(join(msg.0, Arc::clone(&state))),
Some("!leave") => spawn(leave(msg.0, Arc::clone(&state))),
Some("!pause") => spawn(pause(msg.0, Arc::clone(&state))),
Some("!play") => spawn(play(msg.0, Arc::clone(&state))),
Some("!seek") => spawn(seek(msg.0, Arc::clone(&state))),
Some("!stop") => spawn(stop(msg.0, Arc::clone(&state))),
Some("!volume") => spawn(volume(msg.0, Arc::clone(&state))),
_ => continue,
}
}
}
Ok(())
}
async fn join(msg: Message, state: State) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
let user_id = msg.author.id;
let guild_id = msg.guild_id.ok_or("No guild id attached to the message.")?;
let channel_id = state
.cache
.voice_state(user_id, guild_id)
.ok_or("Cannot get voice state for user")?
.channel_id();
let channel_id =
NonZeroU64::new(channel_id.into()).ok_or("Joined voice channel must have nonzero ID.")?;
let content = match state.songbird.join(guild_id, channel_id).await {
Ok(_handle) => format!("Joined <#{}>!", channel_id),
Err(e) => format!("Failed to join <#{}>! Why: {:?}", channel_id, e),
};
state
.http
.create_message(msg.channel_id)
.content(&content)?
.await?;
Ok(())
}
async fn leave(msg: Message, state: State) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
tracing::debug!(
"leave command in channel {} by {}",
msg.channel_id,
msg.author.name
);
let guild_id = msg.guild_id.unwrap();
state.songbird.leave(guild_id).await?;
state
.http
.create_message(msg.channel_id)
.content("Left the channel")?
.await?;
Ok(())
}
async fn play(msg: Message, state: State) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
tracing::debug!(
"play command in channel {} by {}",
msg.channel_id,
msg.author.name
);
let author_id = msg.author.id;
let guild_id = msg.guild_id.unwrap();
let url = msg.content.replace("!play", "").trim().to_string();
let mut src = YoutubeDl::new(reqwest::Client::new(), url);
if let Ok(metadata) = src.aux_metadata().await {
info!("metadata: {:?}", metadata);
let content = format!(
"Playing **{:?}** by **{:?}**",
metadata.title.as_ref().unwrap_or(&"<UNKNOWN>".to_string()),
metadata.artist.as_ref().unwrap_or(&"<UNKNOWN>".to_string()),
);
state
.http
.create_message(msg.channel_id)
.content(&content)?
.await?;
if let Some(call_lock) = state.songbird.get(guild_id) {
let mut call = call_lock.lock().await;
let handle = call.play_input(src.into());
let mut store = state.trackdata.write().await;
store.insert(guild_id, handle);
}
} else {
state
.http
.create_message(msg.channel_id)
.content("Didn't find any results")?
.await?;
}
// let ytdl = "yt-dlp";
// let ytdl_args = [
// "-j", // print JSON information for video for metadata
// "-q", // don't print progress logs (this messes with -o -)
// "--no-simulate", // ensure video is downloaded regardless of printing
// "-f",
// "webm[abr>0]/bestaudio/best", // select best quality audio-only
// "-R",
// "infinite", // infinite number of download retries
// "--no-playlist", // only download the video if URL also has playlist info
// "--ignore-config", // disable all configuration files for a yt-dlp run
// "--no-warnings", // don't print out warnings
// &msg.content, // URL to download
// "-o",
// "-", // stream data to stdout
// ];
// let output = Command::new("yt-dlp")
// .args(ytdl_args)
// .output()
// .await?;
// let stdout = output.stdout;
// info!("metadata: {:?}", output.stderr);
// if let Some(call_lock) = state.songbird.get(guild_id) {
// let mut call = call_lock.lock().await;
// let handle = call.play(stdout.into());
// let mut store = state.trackdata.write().await;
// store.insert(guild_id, handle);
// }
Ok(())
}
async fn pause(msg: Message, state: State) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
tracing::debug!(
"pause command in channel {} by {}",
msg.channel_id,
msg.author.name
);
let guild_id = msg.guild_id.unwrap();
let store = state.trackdata.read().await;
let content = if let Some(handle) = store.get(&guild_id) {
let info = handle.get_info().await?;
let paused = match info.playing {
PlayMode::Play => {
let _success = handle.pause();
false
}
_ => {
let _success = handle.play();
true
}
};
let action = if paused { "Unpaused" } else { "Paused" };
format!("{} the track", action)
} else {
format!("No track to (un)pause!")
};
state
.http
.create_message(msg.channel_id)
.content(&content)?
.await?;
Ok(())
}
async fn seek(msg: Message, state: State) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
tracing::debug!(
"seek command in channel {} by {}",
msg.channel_id,
msg.author.name
);
state
.http
.create_message(msg.channel_id)
.content("Where in the track do you want to seek to (in seconds)?")?
.await?;
let author_id = msg.author.id;
let msg = state
.standby
.wait_for_message(msg.channel_id, move |new_msg: &MessageCreate| {
new_msg.author.id == author_id
})
.await?;
let guild_id = msg.guild_id.unwrap();
let position = msg.content.parse::<u64>()?;
let store = state.trackdata.read().await;
let content = if let Some(handle) = store.get(&guild_id) {
let _success = handle.seek(std::time::Duration::from_secs(position));
format!("Seeking to {}s", position)
} else {
format!("No track to seek over!")
};
state
.http
.create_message(msg.channel_id)
.content(&content)?
.await?;
Ok(())
}
async fn stop(msg: Message, state: State) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
tracing::debug!(
"stop command in channel {} by {}",
msg.channel_id,
msg.author.name
);
let guild_id = msg.guild_id.unwrap();
if let Some(call_lock) = state.songbird.get(guild_id) {
let mut call = call_lock.lock().await;
let _ = call.stop();
}
state
.http
.create_message(msg.channel_id)
.content("Stopped the track")?
.await?;
Ok(())
}
async fn volume(msg: Message, state: State) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
tracing::debug!(
"volume command in channel {} by {}",
msg.channel_id,
msg.author.name
);
state
.http
.create_message(msg.channel_id)
.content("What's the volume you want to set (0.0-10.0, 1.0 being the default)?")?
.await?;
let author_id = msg.author.id;
let msg = state
.standby
.wait_for_message(msg.channel_id, move |new_msg: &MessageCreate| {
new_msg.author.id == author_id
})
.await?;
let guild_id = msg.guild_id.unwrap();
let volume = msg.content.parse::<f64>()?;
if !volume.is_finite() || volume > 10.0 || volume < 0.0 {
state
.http
.create_message(msg.channel_id)
.content("Invalid volume!")?
.await?;
return Ok(());
}
let store = state.trackdata.read().await;
let content = if let Some(handle) = store.get(&guild_id) {
let _success = handle.set_volume(volume as f32);
format!("Set the volume to {}", volume)
} else {
format!("No track to change volume!")
};
state
.http
.create_message(msg.channel_id)
.content(&content)?
.await?;
Ok(())
}