first commit, yay
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/target
|
||||||
|
.env
|
||||||
|
.vscode
|
||||||
3040
Cargo.lock
generated
Normal file
3040
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
Normal file
22
Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "djheuel"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Johannes Heuel <johannes.heuel@rwth-aachen.de>"]
|
||||||
|
edition = "2021"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
symphonia = { version = "0.5.2", features=["all"] }
|
||||||
|
songbird = { version = "0.4.0", features = ["driver", "gateway", "twilight", "rustls"] }
|
||||||
|
tokio = { features = ["macros", "rt-multi-thread", "signal", "sync"], version = "1" }
|
||||||
|
regex = { version = "1", features = ["unicode-case"] }
|
||||||
|
futures = "0.3"
|
||||||
|
reqwest = "0.11"
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = "0.2"
|
||||||
|
twilight-gateway = "0.15"
|
||||||
|
twilight-http = "0.15"
|
||||||
|
twilight-model = "0.15"
|
||||||
|
twilight-standby = "0.15"
|
||||||
|
twilight-cache-inmemory = "0.15"
|
||||||
|
dotenv = "0.15.0"
|
||||||
409
src/main.rs
Normal file
409
src/main.rs
Normal 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(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user