diff options
| author | Nathan Perry <np@nathanperry.dev> | 2024-05-08 12:55:35 -0400 |
|---|---|---|
| committer | Nathan Perry <np@nathanperry.dev> | 2024-05-08 14:16:01 -0400 |
| commit | ffba60b278162707bc4eb004c3bfb6b2e9595213 (patch) | |
| tree | edf8172ecad59d46a6056944fd9e79f7dfb327c2 | |
| parent | fe467f60d99efa54f2ef64606e7d39b9b06d7294 (diff) | |
rework to use songbird
| -rw-r--r-- | Cargo.lock | 211 | ||||
| -rw-r--r-- | Cargo.toml | 5 | ||||
| -rw-r--r-- | src/audio/mod.rs | 47 | ||||
| -rw-r--r-- | src/audio/play_queue.rs | 260 | ||||
| -rw-r--r-- | src/audio/timeutil.rs | 202 | ||||
| -rw-r--r-- | src/audio/ytdl.rs | 62 | ||||
| -rw-r--r-- | src/bot.rs | 48 | ||||
| -rw-r--r-- | src/commands/help.rs | 23 | ||||
| -rw-r--r-- | src/commands/meme/create.rs | 84 | ||||
| -rw-r--r-- | src/commands/meme/history.rs | 216 | ||||
| -rw-r--r-- | src/commands/meme/invoke.rs | 18 | ||||
| -rw-r--r-- | src/commands/meme/mod.rs | 61 | ||||
| -rw-r--r-- | src/commands/mod.rs | 32 | ||||
| -rw-r--r-- | src/commands/playback.rs | 215 | ||||
| -rw-r--r-- | src/commands/roll.rs | 6 | ||||
| -rw-r--r-- | src/commands/sound_levels.rs | 166 | ||||
| -rw-r--r-- | src/commands/today/mod.rs | 56 | ||||
| -rw-r--r-- | src/config.rs | 16 | ||||
| -rw-r--r-- | src/db/mod.rs | 315 | ||||
| -rw-r--r-- | src/db/models.rs | 132 | ||||
| -rw-r--r-- | src/game.rs | 80 | ||||
| -rw-r--r-- | src/main.rs | 19 | ||||
| -rw-r--r-- | src/util.rs | 111 |
23 files changed, 853 insertions, 1532 deletions
@@ -140,12 +140,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -756,25 +750,6 @@ dependencies = [ ] [[package]] -name = "h2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 1.1.0", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -829,29 +804,6 @@ dependencies = [ ] [[package]] -name = "http-body" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" -dependencies = [ - "bytes", - "http 1.1.0", -] - -[[package]] -name = "http-body-util" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" -dependencies = [ - "bytes", - "futures-core", - "http 1.1.0", - "http-body 1.0.0", - "pin-project-lite", -] - -[[package]] name = "httparse" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -873,9 +825,9 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.26", + "h2", "http 0.2.12", - "http-body 0.4.6", + "http-body", "httparse", "httpdate", "itoa", @@ -888,26 +840,6 @@ dependencies = [ ] [[package]] -name = "hyper" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2 0.4.4", - "http 1.1.0", - "http-body 1.0.0", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] name = "hyper-rustls" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -915,7 +847,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http 0.2.12", - "hyper 0.14.28", + "hyper", "rustls 0.21.12", "tokio", "tokio-rustls 0.24.1", @@ -923,38 +855,15 @@ dependencies = [ [[package]] name = "hyper-tls" -version = "0.6.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "http-body-util", - "hyper 1.3.1", - "hyper-util", + "hyper", "native-tls", "tokio", "tokio-native-tls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http 1.1.0", - "http-body 1.0.0", - "hyper 1.3.1", - "pin-project-lite", - "socket2", - "tokio", - "tower", - "tower-service", - "tracing", ] [[package]] @@ -1854,27 +1763,30 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2 0.3.26", + "h2", "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.28", + "http-body", + "hyper", "hyper-rustls", + "hyper-tls", "ipnet", "js-sys", "log", "mime", "mime_guess", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", "rustls 0.21.12", - "rustls-pemfile 1.0.4", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "system-configuration", "tokio", + "tokio-native-tls", "tokio-rustls 0.24.1", "tokio-util", "tower-service", @@ -1884,49 +1796,7 @@ dependencies = [ "wasm-streams", "web-sys", "webpki-roots 0.25.4", - "winreg 0.50.0", -] - -[[package]] -name = "reqwest" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" -dependencies = [ - "base64 0.22.1", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2 0.4.4", - "http 1.1.0", - "http-body 1.0.0", - "http-body-util", - "hyper 1.3.1", - "hyper-tls", - "hyper-util", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls-pemfile 2.1.2", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "system-configuration", - "tokio", - "tokio-native-tls", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg 0.52.0", + "winreg", ] [[package]] @@ -2059,7 +1929,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ "openssl-probe", - "rustls-pemfile 1.0.4", + "rustls-pemfile", "schannel", "security-framework", ] @@ -2074,16 +1944,6 @@ dependencies = [ ] [[package]] -name = "rustls-pemfile" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" -dependencies = [ - "base64 0.22.1", - "rustls-pki-types", -] - -[[package]] name = "rustls-pki-types" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2330,7 +2190,7 @@ dependencies = [ "mime_guess", "parking_lot", "percent-encoding", - "reqwest 0.11.27", + "reqwest", "secrecy", "serde", "serde_json", @@ -2461,7 +2321,7 @@ dependencies = [ "parking_lot", "pin-project", "rand", - "reqwest 0.11.27", + "reqwest", "ringbuf", "rubato", "rusty_pool", @@ -2710,7 +2570,7 @@ dependencies = [ "r2d2_postgres", "rand", "regex", - "reqwest 0.12.4", + "reqwest", "serde", "serde_json", "serenity", @@ -2721,6 +2581,7 @@ dependencies = [ "thiserror", "time", "timeago", + "tokio", "typemap", "url", ] @@ -2791,6 +2652,8 @@ dependencies = [ "bytes", "libc", "mio", + "num_cpus", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", @@ -2957,28 +2820,6 @@ dependencies = [ ] [[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-layer" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" - -[[package]] name = "tower-service" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3657,16 +3498,6 @@ dependencies = [ ] [[package]] -name = "winreg" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - -[[package]] name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -24,7 +24,7 @@ diesel = { version = "2.1", features = ["postgres", "chrono", "r2d2"], optional ctrlc = { version = "3.4", features = ["termination"] } rand = "0.8" either = "1.10" -reqwest = { version = "0.12", features = ["json"] } +reqwest = { version = "0.11", features = ["json"] } sha1 = { version = "0.10", features = ["std"] } regex = "1.10" itertools = "0.12" @@ -41,7 +41,8 @@ diesel_migrations = { version = "2.1", optional = true } envconfig = "0.10" envconfig_derive = "0.10" tap = "1.0" -songbird = "0.4" +songbird = { version = "0.4", features = ["builtin-queue"] } +tokio = { version = "1.37", features = ["full"]} [dependencies.serenity] version = "0.12" diff --git a/src/audio/mod.rs b/src/audio/mod.rs deleted file mode 100644 index 9affdb1..0000000 --- a/src/audio/mod.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::sync::Arc; - -use chrono::Duration; -use either::Either; -use serenity::{ - model::id::ChannelId, - prelude::*, -}; -use typemap::Key; - -pub use self::{ - play_queue::PlayQueue, - timeutil::parse_times, - ytdl::*, -}; - -mod play_queue; -mod timeutil; -mod ytdl; - -pub struct VoiceManager; - -impl Key for VoiceManager { - type Value = Arc<Mutex<ClientVoiceManager>>; -} - -impl VoiceManager { - pub fn register(c: &mut Client) { - let mut data = c.data.write(); - data.insert::<VoiceManager>(Arc::clone(&c.voice_manager)); - } -} - -#[derive(Clone, Debug)] -pub struct PlayArgs { - pub data: Either<String, Vec<u8>>, - pub initiator: String, - pub sender_channel: ChannelId, - pub start: Option<Duration>, - pub end: Option<Duration>, -} - -#[derive(Clone)] -pub struct CurrentItem { - pub init_args: PlayArgs, - pub audio: LockedAudio, -} diff --git a/src/audio/play_queue.rs b/src/audio/play_queue.rs deleted file mode 100644 index 34fc113..0000000 --- a/src/audio/play_queue.rs +++ /dev/null @@ -1,260 +0,0 @@ -use std::{ - collections::VecDeque, - io::{self, BufRead, BufReader, Cursor, Read}, - process, - sync::{Arc, RwLock}, - thread, - time::Duration, -}; - -use either::{Left, Right}; -use log::{ - debug, - error, - trace, -}; -use serenity::{ - CacheAndHttp, - client::bridge::voice::ClientVoiceManager, - prelude::*, - voice, -}; -use typemap::Key; - -use crate::{ - audio::{ - CurrentItem, - PlayArgs, - ytdl_url, - }, - commands::{ - sound_levels::DEFAULT_VOLUME, - }, - Result, - CONFIG, FFMPEG_COMMAND, -}; - -const SECONDS_LEAD_TIME: f32 = 0.75; -const SECONDS_TRAIL_TIME: f32 = 0.1; -const SAMPLE_RATE: usize = 48000; -const CHANNELS: usize = 2; -const BYTES_PER_SAMPLE: usize = 2; -const PRE_SILENCE_BYTES: usize = (SECONDS_LEAD_TIME * (SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE) as f32) as usize; -const POST_SILENCE_BYTES: usize = (SECONDS_TRAIL_TIME * (SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE) as f32) as usize; - -#[derive(Clone)] -pub struct PlayQueue { - pub general_queue: VecDeque<PlayArgs>, - pub meme_queue: VecDeque<PlayArgs>, - pub playing: Option<CurrentItem>, - pub volume: f32, -} - -impl Key for PlayQueue { - type Value = Arc<RwLock<PlayQueue>>; -} - -impl serenity::prelude::TypeMapKey for PlayQueue { - type Value = Arc<RwLock<PlayQueue>>; -} - -impl PlayQueue { - pub fn new() -> Self { - PlayQueue { - general_queue: VecDeque::new(), - meme_queue: VecDeque::new(), - playing: None, - volume: DEFAULT_VOLUME, - } - } - - pub fn register(c: &mut Client) { - let voice_manager = Arc::clone(&c.voice_manager); - - let queue = Arc::new(RwLock::new(PlayQueue::new())); - - { - let mut data = c.data.write(); - data.insert::<PlayQueue>(Arc::clone(&queue)); - } - - - let cache_http = c.cache_and_http.clone(); - thread::spawn(move || { - loop { - if let Err(e) = Self::update(&cache_http, &queue, &voice_manager) { - error!("updating playqueue: {}", e); - } - - thread::sleep(Duration::from_millis(250)); - } - }); - - } - - fn update(cache_http: &CacheAndHttp, queue_lck: &Arc<RwLock<Self>>, voice_manager: &Arc<Mutex<ClientVoiceManager>>) -> Result<()> { - let (queue_is_empty, queue_has_playing) = { - let queue = queue_lck.read().unwrap(); - - let allow_continue = queue.playing.clone().map_or(false, |x| !x.audio.lock().finished); - - if allow_continue { - return Ok(()); - } - - (queue.general_queue.is_empty() && queue.meme_queue.is_empty(), queue.playing.is_some()) - }; - - if queue_is_empty { - if queue_has_playing { - let mut queue = queue_lck.write().unwrap(); - - assert!({ - let audio_lck = queue.playing.clone().unwrap().audio; - let audio = audio_lck.lock(); - audio.finished - }); - - queue.playing = None; - - let mut manager = voice_manager.lock(); - manager.leave(CONFIG.discord.guild()); - debug!("disconnected because playback finished"); - } - - return Ok(()); - } - - let mut queue = queue_lck.write().unwrap(); - - let mut item = if !queue.meme_queue.is_empty() { - queue.meme_queue.pop_front().unwrap() - } else { - queue.general_queue.pop_front().unwrap() - }; - - let src = match &mut item.data { - Left(ref url) => { - let youtube_url = ytdl_url(url.as_str())?; - - let duration_opts = if let Some(e) = item.end { - vec! [ - "-ss".to_owned(), item.start.map_or_else( - || "00:00:00".to_owned(), - |s| format!("{:02}:{:02}:{:02}", s.num_hours(), s.num_minutes() % 60, s.num_seconds() % 60) - ), - - "-to".to_owned(), format!("{:02}:{:02}:{:02}", e.num_hours(), e.num_minutes() % 60, e.num_seconds() % 60), - ] - } else { - vec! [] - }; - - let ffmpeg_command = process::Command::new(&*FFMPEG_COMMAND) - .arg("-i") - .arg(youtube_url) - .args(duration_opts) - .args(&[ - "-ac", "2", - "-ar", "48000", - "-f", "s16le", - "-acodec", "pcm_s16le", - "-", - ]) - .stdout(process::Stdio::piped()) - .stderr(process::Stdio::null()) - .stdin(process::Stdio::null()) - .spawn()?; - - let audio_reader = ffmpeg_command.stdout.unwrap(); - - let pre_silence = vec![0u8; PRE_SILENCE_BYTES]; - let post_silence = vec![0u8; POST_SILENCE_BYTES]; - - let reader = Cursor::new(pre_silence).chain(audio_reader).chain(Cursor::new(post_silence)); - - voice::pcm(true, reader) - }, - Right(ref vec) => { - let transcoder = process::Command::new(&*FFMPEG_COMMAND) - .args(&[ - "-format", "opus", - "-i", "pipe:0", - "-acodec", "pcm_s16le", - "-f", "s16le", - "-" - ]) - .stdin(process::Stdio::piped()) - .stdout(process::Stdio::piped()) - .stderr(process::Stdio::piped()) - .spawn() - .expect("unable to call ffmpeg"); - - let process::Child { - stdin, - stderr, - stdout, - .. - } = transcoder; - - thread::spawn(move || { - let stderr = BufReader::new(stderr.unwrap()); - - for line in stderr.lines() { - let line = line.unwrap(); - - trace!("{}", line); - } - }); - - let v = vec.clone(); - thread::spawn(move || { - if let Err(e) = io::copy(&mut Cursor::new(v), &mut stdin.unwrap()) { - use std::io::ErrorKind; - if e.kind() == ErrorKind::BrokenPipe { - debug!("ffmpeg closed unexpectedly"); - } else { - error!("copying audio to ffmpeg {}", e); - } - } - }); - - let pre_silence = vec![0u8; PRE_SILENCE_BYTES]; - let post_silence = vec![0u8; POST_SILENCE_BYTES]; - - let reader = Cursor::new(pre_silence) - .chain(stdout.unwrap()) - .chain(Cursor::new(post_silence)); - - voice::pcm(true, reader) - } - }; - - let mut manager = voice_manager.lock(); - let handler = manager.join(CONFIG.discord.guild(), CONFIG.discord.voice_channel()); - - match handler { - Some(handler) => { - let audio = handler.play_only(src); - { - audio.lock().volume(queue.volume); - } - - queue.playing = Some(CurrentItem { - init_args: item, - audio, - }); - - debug!("playing new song"); - }, - None => { - error!("couldn't join channel"); - item.sender_channel.say(&cache_http.http, "something happened somewhere somehow.")?; - } - } - - Ok(()) - } - -} - diff --git a/src/audio/timeutil.rs b/src/audio/timeutil.rs deleted file mode 100644 index 238897f..0000000 --- a/src/audio/timeutil.rs +++ /dev/null @@ -1,202 +0,0 @@ -use chrono::Duration; -use regex::{ - Match, - Regex, -}; - -use lazy_static::lazy_static; - -lazy_static! { - static ref START_REGEX: Regex = - Regex::new(r"(?:start|begin(?:ning)?)\s*=?\s*(?:(?P<hours>\d+)h\s?)?(?:(?P<minutes>\d+)m\s?)?(?:(?P<seconds>\d+)s?)?").unwrap(); - - static ref DUR_REGEX: Regex = - Regex::new(r"dur(?:ation)?\s*=?\s*(?:(?P<hours>\d+)h\s?)?(?:(?P<minutes>\d+)m\s?)?(?:(?P<seconds>\d+)s?)?").unwrap(); - - static ref END_REGEX: Regex = - Regex::new(r"(?:end|term(?:inate|ination)?)\s*=?\s*(?:(?P<hours>\d+)h\s?)?(?:(?P<minutes>\d+)m\s?)?(?:(?P<seconds>\d+)s?)?").unwrap(); -} - -pub fn parse_times<A: AsRef<str>>(s: A) -> (Option<Duration>, Option<Duration>) { - fn parse_match(m: Option<Match>) -> u64 { - m.and_then(|s| s.as_str().parse::<u64>().ok()).unwrap_or(0) - } - - fn parse_captures<B: AsRef<str>>(r: &Regex, s: B) -> Option<Duration> { - r.captures(s.as_ref()) - .map(|capt| { - let hours = parse_match(capt.name("hours")); - let minutes = parse_match(capt.name("minutes")); - let seconds = parse_match(capt.name("seconds")); - - let result = Duration::hours(hours as i64) + - Duration::minutes(minutes as i64) + - Duration::seconds(seconds as i64); - - assert!(result >= Duration::zero()); - - result - }) - } - - let start_time = parse_captures(&START_REGEX, &s); - let dur = parse_captures(&DUR_REGEX, &s); - let end_time = parse_captures(&END_REGEX, s) - .or_else(|| start_time.and_then(|start| dur.map(|d| start + d))); - - (start_time, end_time) -} - -#[cfg(test)] -mod test { - use time::Duration; - use itertools::iproduct; - - use super::*; - - #[test] - fn test_start() { - let captures = START_REGEX.captures("start 1h2m3s").unwrap(); - - assert_eq!(captures.name("hours").unwrap().as_str(), "1"); - assert_eq!(captures.name("minutes").unwrap().as_str(), "2"); - assert_eq!(captures.name("seconds").unwrap().as_str(), "3"); - - assert!(START_REGEX.captures("").is_none()); - - let captures = START_REGEX.captures("start 1").unwrap(); - assert_eq!(captures.name("seconds").unwrap().as_str(), "1"); - } - - #[test] - fn test_dur() { - let captures = DUR_REGEX.captures("dur 1h2m3s").unwrap(); - - assert_eq!(captures.name("hours").unwrap().as_str(), "1"); - assert_eq!(captures.name("minutes").unwrap().as_str(), "2"); - assert_eq!(captures.name("seconds").unwrap().as_str(), "3"); - - assert!(DUR_REGEX.captures("").is_none()); - - let captures = DUR_REGEX.captures("dur 1").unwrap(); - assert_eq!(captures.name("seconds").unwrap().as_str(), "1"); - } - - #[test] - fn test_end() { - let captures = END_REGEX.captures("end 1h2m3s").unwrap(); - - assert_eq!(captures.name("hours").unwrap().as_str(), "1"); - assert_eq!(captures.name("minutes").unwrap().as_str(), "2"); - assert_eq!(captures.name("seconds").unwrap().as_str(), "3"); - - assert!(END_REGEX.captures("").is_none()); - - let captures = END_REGEX.captures("end 1").unwrap(); - assert_eq!(captures.name("seconds").unwrap().as_str(), "1"); - } - - #[test] - fn test_parse_matrix() { - fn format_time(d: &Duration) -> impl Iterator<Item=String> { - let seconds = d.num_seconds() % 60; - let minutes = d.num_minutes() % 60; - let hours = d.num_hours(); - - let elems = vec![true, false]; - - #[inline] - fn format_maybe_zero<S: AsRef<str>>(v: i64, unit: S, always: bool) -> String { - if always || v != 0 { - format!("{}{}", v, unit.as_ref()) - } else { - "".to_owned() - } - } - - iproduct!(elems.clone(), elems.clone(), elems) - .filter_map(move |(secs, mins, hr)| { - if !secs && !mins && !hr { - return None; - } - - let hr_string = format_maybe_zero(hours, "h", hr); - let mn_string = format_maybe_zero(minutes, "m", mins); - let sec_string = format_maybe_zero(seconds, "s", secs); - - Some(format!("{}{}{}", hr_string, mn_string, sec_string)) - }) - } - - let start_times = vec![None, Some(Duration::seconds(0)), Some(Duration::seconds(32))]; - let durs = vec![None, Some(Duration::seconds(0)), Some(Duration::seconds(123141))]; - let end_times = vec![None, Some(Duration::seconds(0)), Some(Duration::seconds(19851598))]; - - let start_names = vec!["start", "begin", "beginning"]; - let dur_names = vec!["dur", "duration"]; - let end_names = vec!["end", "term", "terminate", "termination"]; - - let pairs = vec! [ - (start_times, start_names), - (durs, dur_names), - (end_times, end_names), - ]; - - let elems = pairs.into_iter() - .map(|(times, names)| { - let result = times.into_iter() - .flat_map(move |d| { - let names_iter = names.clone().into_iter(); - - d.as_ref().map(move |dur| { - let dur = dur.clone(); - - Box::new(iproduct!(format_time(&dur), names_iter) - .map(move |(time, name)| Some((dur, format!("{} {}", name, time))))) as Box<dyn Iterator<Item=Option<(Duration, String)>>> - }).unwrap_or_else(|| Box::new(::std::iter::once(None))) - }); - - result.collect::<Vec<Option<(Duration, String)>>>() - }) - .collect::<Vec<Vec<Option<(Duration, String)>>>>(); - - let start_iters = &elems[0]; - let dur_iters = &elems[1]; - let end_iters = &elems[2]; - - iproduct!(start_iters, dur_iters, end_iters) - .for_each(|(start, dur, end)| { - let s = vec![start, dur, end] - .into_iter() - .filter_map(|o| { - o.as_ref().map(|(_, formatted)| formatted.to_owned()) - }) - .collect::<Vec<_>>() - .join(" "); - - println!("testing {}", s); - - let (parse_start, parse_end) = parse_times(s); - - match start { - Some((dur, _)) => assert_eq!(*dur, parse_start.unwrap()), - None => assert_eq!(None, parse_start), - } - - match end { - Some((d, _)) => assert_eq!(*d, parse_end.unwrap()), - None => { - match dur { - Some((d, _)) => { - match start { - Some((s, _)) => assert_eq!(parse_end.unwrap(), *s + *d), - None => assert_eq!(None, parse_end), - } - }, - None => assert_eq!(None, parse_end), - } - } - } - }); - } -} diff --git a/src/audio/ytdl.rs b/src/audio/ytdl.rs deleted file mode 100644 index 645f3f4..0000000 --- a/src/audio/ytdl.rs +++ /dev/null @@ -1,62 +0,0 @@ -/// This module is entirely adapted from the relevant code in Serenity. - -use std::{ - process::{ - Command, - Stdio, - }, -}; - -use serde_json::Value; -use serenity::{ - voice::{ - VoiceError, - } -}; -use lazy_static::lazy_static; - -use crate::{Result, CONFIG}; - -lazy_static! { - static ref YTDL_COMMAND: String = { - let result = CONFIG.ytdl.clone().unwrap_or("youtube-dl".to_owned()); - log::debug!("got ytdl: {}", result); - - result - }; -} - -pub fn ytdl_url(uri: &str) -> Result<String> { - let args = [ - "-f", - "webm[abr>0]/bestaudio/best", - "--no-playlist", - "--print-json", - "--skip-download", - uri, - ]; - - let out = Command::new(&*YTDL_COMMAND) - .args(&args) - .stdin(Stdio::null()) - .output()?; - - if !out.status.success() { - log::error!("running ytdl {:?}", out); - return Err(VoiceError::YouTubeDLRun(out).into()); - } - - let value = serde_json::from_reader(&out.stdout[..])?; - let mut obj = match value { - Value::Object(obj) => obj, - other => return Err(VoiceError::YouTubeDLProcessing(other).into()), - }; - - match obj.remove("url") { - Some(v) => match v { - Value::String(uri) => Ok(uri), - other => Err(VoiceError::YouTubeDLUrl(other).into()), - }, - None => Err(VoiceError::YouTubeDLUrl(Value::Object(obj)).into()), - } -} @@ -45,9 +45,9 @@ use serenity::{ }, prelude::*, }; +use songbird::SerenityInit; use crate::{ - audio, commands::register_commands, config::CONFIG, util, @@ -56,12 +56,18 @@ use crate::{ Result, }; +pub struct HttpKey; + +impl TypeMapKey for HttpKey { + type Value = reqwest::Client; +} + struct Handler; #[serenity::async_trait] impl EventHandler for Handler { async fn ready(&self, ctx: Context, r: Ready) { - let guild = r.guilds.iter().find(|g| g.id() == CONFIG.discord.guild()); + let guild = r.guilds.iter().find(|g| g.id == CONFIG.discord.guild()); if guild.is_none() { info!("bot isn't in configured guild. join here: {:?}", OAUTH_URL.as_str()); @@ -76,11 +82,14 @@ impl EventHandler for Handler { #[cfg(not(debug_assertions))] let botname = "thulani"; - guild.iter().for_each(|g| { - if let Err(e) = g.id().edit_nickname(&ctx, Some(botname)) { - error!("changing nickname: {:?}", e); - } - }); + use serenity::futures::StreamExt; + serenity::futures::stream::iter(guild.iter()) + .for_each(|g| async move { + if let Err(e) = g.id.edit_nickname(&ctx, Some(botname)).await { + error!("changing nickname: {:?}", e); + } + }) + .await; } async fn resume(&self, _ctx: Context, _resume: ResumedEvent) { @@ -233,21 +242,32 @@ fn after_handle<'fut>( }) } -pub fn run() -> Result<()> { +pub async fn run() -> Result<()> { let token = &CONFIG.discord.auth.token; - let mut client = Client::new(token, Handler)?; - audio::VoiceManager::register(&mut client); - audio::PlayQueue::register(&mut client); + let sb_config = songbird::Config::default(); - client.with_framework(framework()); + let mut client = + Client::builder(token, GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT) + .event_handler(Handler) + .register_songbird_from_config(sb_config) + .type_map_insert::<HttpKey>(reqwest::Client::new()) + .framework(framework().await) + .await?; let shard_manager = client.shard_manager.clone(); + ctrlc::set_handler(move || { info!("shutting down"); - shard_manager.lock().shutdown_all(); + + let shard_manager = shard_manager.clone(); + tokio::task::spawn(async move { + shard_manager.shutdown_all().await; + }); }) .expect("unable to create SIGINT/SIGTERM handlers"); - client.start() + client.start().await?; + + Ok(()) } diff --git a/src/commands/help.rs b/src/commands/help.rs index 588cdf7..0d84b2d 100644 --- a/src/commands/help.rs +++ b/src/commands/help.rs @@ -1,23 +1,20 @@ use std::collections::HashSet; use serenity::{ + framework::standard::{ + help_commands, + macros::help, + Args, + CommandGroup, + CommandResult, + HelpOptions, + }, model::{ channel::Message, id::UserId, }, - framework::{ - standard::{ - macros::help, - help_commands, - Args, - HelpOptions, - CommandGroup, - }, - }, prelude::*, }; -use serenity::framework::standard::CommandResult; - #[help] pub async fn help( @@ -28,5 +25,7 @@ pub async fn help( groups: &[&'static CommandGroup], owners: HashSet<UserId>, ) -> CommandResult { - help_commands::with_embeds(ctx, msg, args, opts, groups, owners) + help_commands::with_embeds(ctx, msg, args, opts, groups, owners).await?; + + Ok(()) } diff --git a/src/commands/meme/create.rs b/src/commands/meme/create.rs index 97c5276..1c12f2a 100644 --- a/src/commands/meme/create.rs +++ b/src/commands/meme/create.rs @@ -1,50 +1,41 @@ -use std::{ - io::Read, - process::{ - Command, - Stdio, - }, -}; +use std::process::Stdio; +use anyhow::anyhow; use diesel::result::Error as DieselError; +use lazy_static::lazy_static; use log::{ debug, error, warn, }; use serenity::{ + all::ReactionType, framework::standard::{ macros::command, Args, + CommandError, + CommandResult, Delimiter, }, + futures::TryFutureExt, model::channel::Message, prelude::*, }; -use url::Url; - -use anyhow::anyhow; -use lazy_static::lazy_static; -use serenity::{ - all::ReactionType, - framework::standard::{ - CommandError, - CommandResult, - }, - futures::TryFutureExt, +use tap::Pipe; +use tokio::{ + io::AsyncReadExt, + process::Command, }; +use url::Url; use crate::{ - audio::{ - parse_times, - ytdl_url, - }, db::{ connection, Audio, Image, NewMeme, }, + parse_times, util, FFMPEG_COMMAND, }; @@ -77,12 +68,12 @@ pub async fn addmeme(ctx: &Context, msg: &Message, args: Args) -> CommandResult .await; } - let image_id = image - .map(|att| { - let data = att.download()?; - Image::create(&mut conn, &att.filename, data, msg.author.id.get()) - }) - .transpose()?; + let mut image_id = None; + + if let Some(att) = image { + let data = att.download().await?; + image_id = Some(Image::create(&mut conn, &att.filename, data, msg.author.id.get())?); + }; let save_result = NewMeme { title, @@ -96,7 +87,9 @@ pub async fn addmeme(ctx: &Context, msg: &Message, args: Args) -> CommandResult use diesel::result::DatabaseErrorKind; match save_result { - Ok(_) => msg.react(&ctx, "👌"), + Ok(_) => { + msg.react(&ctx, ReactionType::Unicode("👌".to_string())).await?; + }, Err(e) => { if let Some(DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _)) = e.downcast_ref::<DieselError>() @@ -111,6 +104,8 @@ pub async fn addmeme(ctx: &Context, msg: &Message, args: Args) -> CommandResult return Err(e.into()); }, } + + Ok(()) } #[command] @@ -131,7 +126,7 @@ pub async fn addaudiomeme(ctx: &Context, msg: &Message, args: Args) -> CommandRe let opts = elems[1..].join(" "); let (start, end) = parse_times(opts); - let youtube_url = ytdl_url(audio_link.as_str())?; + let youtube_url = util::ytdl_url(audio_link.as_str()).await?; let duration_opts = if let Some(e) = end { vec![ @@ -178,18 +173,17 @@ pub async fn addaudiomeme(ctx: &Context, msg: &Message, args: Args) -> CommandRe let mut conn = connection()?; - let image = msg - .attachments - .first() - .ok_or(anyhow!("no attachment")) - .and_then(|att| { - let data = att.download()?; - Image::create(&mut conn, &att.filename, data, msg.author.id.get()) - }) - .ok(); + let image_att = msg.attachments.first().ok_or(anyhow!("no attachment")); + + let mut image_id = None; + + if let Ok(att) = image_att { + let data = att.download().await?; + image_id = Image::create(&mut conn, &att.filename, data, msg.author.id.get())?.pipe(Some); + } let mut audio_data = Vec::new(); - let bytes = audio_reader.read_to_end(&mut audio_data)?; + let bytes = audio_reader.read_to_end(&mut audio_data).await?; if bytes == 0 { debug!("read 0 bytes from audio reader"); @@ -203,7 +197,7 @@ pub async fn addaudiomeme(ctx: &Context, msg: &Message, args: Args) -> CommandRe let save_result = NewMeme { title, content: text, - image_id: image, + image_id, audio_id: Some(audio_id), metadata_id: 0, } @@ -212,7 +206,9 @@ pub async fn addaudiomeme(ctx: &Context, msg: &Message, args: Args) -> CommandRe use diesel::result::DatabaseErrorKind; match save_result { - Ok(_) => msg.react(&ctx, ReactionType::Unicode("👌".to_owned())), + Ok(_) => { + msg.react(&ctx, ReactionType::Unicode("👌".to_owned())).await?; + }, Err(e) => { if let Some(DieselError::DatabaseError(DatabaseErrorKind::UniqueViolation, _)) = e.downcast_ref::<DieselError>() @@ -224,7 +220,9 @@ pub async fn addaudiomeme(ctx: &Context, msg: &Message, args: Args) -> CommandRe .await; } - return Err(e); + return Err(e.into()); }, } + + Ok(()) } diff --git a/src/commands/meme/history.rs b/src/commands/meme/history.rs index 5e200b1..ed50e27 100644 --- a/src/commands/meme/history.rs +++ b/src/commands/meme/history.rs @@ -1,7 +1,11 @@ +use anyhow::anyhow; use diesel::{ result::Error as DieselError, NotFound, + PgConnection, }; +use itertools::Itertools; +use lazy_static::lazy_static; use log::{ debug, error, @@ -11,25 +15,23 @@ use serenity::{ framework::standard::{ macros::command, Args, + CommandError, + CommandResult, + }, + futures::{ + StreamExt, + TryFutureExt, + TryStreamExt, }, model::channel::Message, prelude::*, }; +use tap::Pipe; use timeago::{ Formatter, TimeUnit, }; -use anyhow::anyhow; -use lazy_static::lazy_static; -use serenity::{ - framework::standard::{ - CommandError, - CommandResult, - }, - futures::TryFutureExt, -}; - use crate::{ db::{ self, @@ -39,7 +41,6 @@ use crate::{ Metadata, }, util, - Result, CONFIG, }; @@ -53,10 +54,10 @@ lazy_static! { }; } -static CLEAN_DATE_FORMAT: &'static str = "%b %-e %Y"; +static CLEAN_DATE_FORMAT: &str = "%b %-e %Y"; #[command] -#[aliases("what")] +#[aliases("what", "hwaet", "hwæt")] pub async fn wat(ctx: &Context, msg: &Message, _: Args) -> CommandResult { let mut conn = connection()?; @@ -80,7 +81,7 @@ pub async fn wat(ctx: &Context, msg: &Message, _: Args) -> CommandResult { match meme { Ok(ref meme) => { let metadata = Metadata::find(&mut conn, meme.metadata_id)?; - let author = CONFIG.discord.guild().member(&ctx, metadata.created_by as u64)?; + let author = CONFIG.discord.guild().member(&ctx, metadata.created_by as u64).await?; util::send( ctx, @@ -98,22 +99,22 @@ pub async fn wat(ctx: &Context, msg: &Message, _: Args) -> CommandResult { Err(e) => { if let Some(NotFound) = e.downcast_ref::<DieselError>() { info!("last meme not found in database"); - return util::send(ctx, msg.channel_id, "heuueueeeeh?", msg.tts).await; + return util::send(ctx, msg.channel_id, "heuueueeeeh?", msg.tts) + .await + .map_err(CommandError::from); } util::send(ctx, msg.channel_id, "do i look like i know what a jpeg is", msg.tts) .await?; - return Err(e); + return Err(e.into()); }, }; - meme.map(|_| {}) + meme.map(|_| {}).map_err(CommandError::from) } #[command] pub async fn history(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - use itertools::Itertools; - let mut conn = connection()?; let n = args.single_quoted::<usize>().unwrap_or(CONFIG.default_hist); @@ -135,66 +136,76 @@ pub async fn history(ctx: &Context, msg: &Message, mut args: Args) -> CommandRes } info!("reporting meme history (len {})", n); - let resp = records - .into_iter() - .enumerate() - .rev() - .map(|(i, rec)| { - let dt = chrono::DateTime::from_utc(rec.time, chrono::Utc {}); - let ago = TIME_FORMATTER.convert((chrono::Utc::now() - dt).to_std().unwrap()); - let rand = if rec.random { - "R, " - } else { - "" - }; - Meme::find(&mut conn, rec.meme_id) - .and_then(|meme| { - Metadata::find(&mut conn, meme.metadata_id).map(|metadata| (metadata, meme)) - }) - .map(|(metadata, meme)| { - let author_name = CONFIG - .discord - .guild() - .member(&ctx, metadata.created_by as u64) - .map(|m| m.display_name().into_owned()) - .unwrap_or("???".to_owned()); - let invoker_name = CONFIG - .discord - .guild() - .member(&ctx, rec.user_id as u64) - .map(|m| m.display_name().into_owned()) - .unwrap_or("???".to_owned()); - format!( - "{}. [{}{}] \"{}\" by {} ({}). invoked by {}.", - i + 1, - rand, - ago, - meme.title, - author_name, - metadata.created.date().format(CLEAN_DATE_FORMAT), - invoker_name - ) - }) - .unwrap_or_else(|e| { - if let Some(variant) = e.downcast_ref::<DieselError>() { - if *variant != NotFound { - error!("error encountered loading meme history: {}", e); - } - } + let resp = serenity::futures::stream::iter(records.into_iter().enumerate().rev()) + .then(|(i, rec)| ir_info(ctx, i, rec, &mut conn)) + .try_collect::<Vec<String>>() + .await?; - let invoker_name = CONFIG - .discord - .guild() - .member(&ctx, rec.user_id as u64) - .map(|m| m.display_name().into_owned()) - .unwrap_or("???".to_owned()); - format!("{}. [{}{}] not found. invoked by {}.", i + 1, rand, ago, invoker_name) - }) - }) - .join("\n"); + let resp = resp.join("\n"); - util::send(ctx, msg.channel_id, &resp, false).await + util::send(ctx, msg.channel_id, &resp, false).await.map_err(CommandError::from) +} + +async fn ir_info( + ctx: &Context, + i: usize, + rec: InvocationRecord, + conn: &mut PgConnection, +) -> Result<String, CommandError> { + let dt = chrono::DateTime::from_utc(rec.time, chrono::Utc {}); + let ago = TIME_FORMATTER.convert((chrono::Utc::now() - dt).to_std().unwrap()); + + let rand = if rec.random { + "R, " + } else { + "" + }; + + let meme = Meme::find(conn, rec.meme_id) + .and_then(|meme| Metadata::find(conn, meme.metadata_id).map(|metadata| (metadata, meme))); + + let invoker_name = CONFIG + .discord + .guild() + .member(&ctx, rec.user_id as u64) + .await + .map(|m| m.display_name().to_owned()) + .unwrap_or("???".to_owned()); + + let result = match meme { + Ok((metadata, meme)) => { + let author_name = CONFIG + .discord + .guild() + .member(&ctx, metadata.created_by as u64) + .await + .map(|m| m.display_name().to_owned()) + .unwrap_or("???".to_owned()); + + format!( + "{}. [{}{}] \"{}\" by {} ({}). invoked by {}.", + i + 1, + rand, + ago, + meme.title, + author_name, + metadata.created.date().format(CLEAN_DATE_FORMAT), + invoker_name + ) + }, + Err(e) => { + if let Some(variant) = e.downcast_ref::<DieselError>() { + if *variant != NotFound { + error!("error encountered loading meme history: {}", e); + } + } + + format!("{}. [{}{}] not found. invoked by {}.", i + 1, rand, ago, invoker_name) + }, + }; + + Ok(result) } #[command] @@ -211,11 +222,12 @@ pub async fn stats(ctx: &Context, msg: &Message, _: Args) -> CommandResult { debug!("reporting stats"); - let rand_user: User = UserId::new(stats.most_random_meme_user).to_user(&ctx)?; - let direct_user: User = UserId::new(stats.most_directly_named_meme_user).to_user(&ctx)?; + let rand_user: User = UserId::new(stats.most_random_meme_user).to_user(&ctx).await?; + let direct_user: User = UserId::new(stats.most_directly_named_meme_user).to_user(&ctx).await?; - let rand_user = rand_user.nick_in(&ctx, CONFIG.discord.guild()).unwrap_or(rand_user.name); - let direct_user = direct_user.nick_in(&ctx, CONFIG.discord.guild()).unwrap_or(direct_user.name); + let rand_user = rand_user.nick_in(&ctx, CONFIG.discord.guild()).await.unwrap_or(rand_user.name); + let direct_user = + direct_user.nick_in(&ctx, CONFIG.discord.guild()).await.unwrap_or(direct_user.name); let s = format!( r#" @@ -270,15 +282,14 @@ and *{}* was the most-memed overall ({})"#, #[command] pub async fn memers(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { - use db; - use itertools::Itertools; use serenity::model::id::UserId; let s = db::memers()? .into_iter() - .map(|info| { - let user = UserId::new(info.user_id).to_user(&ctx)?; - let username = user.nick_in(&ctx, CONFIG.discord.guild()).unwrap_or(user.name); + .pipe(serenity::futures::stream::iter) + .then(|info| async move { + let user = UserId::new(info.user_id).to_user(&ctx).await?; + let username = user.nick_in(&ctx, CONFIG.discord.guild()).await.unwrap_or(user.name); let res = format!( "**{}**: {} total, {} random, {} specific. favorite meme: *{}* ({})", @@ -290,9 +301,10 @@ pub async fn memers(ctx: &Context, msg: &Message, _args: Args) -> CommandResult info.most_used_meme_count, ); - Ok(res) + Result::<_, CommandError>::Ok(res) }) - .collect::<Result<Vec<_>>>()? + .try_collect::<Vec<String>>() + .await? .into_iter() .join("\n"); @@ -301,11 +313,9 @@ pub async fn memers(ctx: &Context, msg: &Message, _args: Args) -> CommandResult #[command] pub async fn query(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - use std::borrow::Borrow; - - use itertools::Itertools; use regex::Regex; use serenity::model::id::UserId; + use std::borrow::Borrow; use crate::{ db, @@ -318,11 +328,10 @@ pub async fn query(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul static ref AGE_REGEX: Regex = Regex::new(r"(?i)(?:age|order)=(.*)").unwrap(); } - let guild = msg.channel_id.to_channel(&ctx)?.guild().ok_or(anyhow!("couldn't find guild"))?; - - let guild = guild.read().guild(&ctx).ok_or(anyhow!("couldn't find guild"))?; + let guild = + msg.channel_id.to_channel(&ctx).await?.guild().ok_or(anyhow!("couldn't find guild"))?; - let guild = guild.read(); + let guild = guild.guild(&ctx).ok_or(anyhow!("couldn't find guild"))?; let creator: Option<u64> = { let creator = args.quoted().current().map(|s| CREATOR_REGEX.is_match(s)).unwrap_or(false); @@ -354,11 +363,13 @@ pub async fn query(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul } }; - let result = db::query_meme(args.rest(), creator, order)? - .into_iter() - .map(|(meme, metadata)| { - let user = UserId::new(metadata.created_by as u64).to_user(&ctx)?; - let username = user.nick_in(&ctx, CONFIG.discord.guild()).unwrap_or(user.name); + let iter = db::query_meme(args.rest(), creator, order)?.into_iter(); + + let result = iter + .pipe(serenity::futures::stream::iter) + .then(|(meme, metadata)| async move { + let user = UserId::new(metadata.created_by as u64).to_user(&ctx).await?; + let username = user.nick_in(&ctx, CONFIG.discord.guild()).await.unwrap_or(user.name); Ok(format!( "*{}* by **{}** ({}). text length: **{}**, image: **{}**, audio: **{}**", @@ -368,9 +379,12 @@ pub async fn query(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul meme.content.map_or(0, |s| s.len()), meme.image_id.map_or("NO", |_s| "YES"), meme.audio_id.map_or("NO", |_s| "YES"), - )) + )) as Result<String, CommandError> }) - .collect::<Result<Vec<_>>>()? + .try_collect::<Vec<String>>() + .await; + + let result = result? .into_iter() .scan(0, |state, line| { *state = *state + line.len() + 1; diff --git a/src/commands/meme/invoke.rs b/src/commands/meme/invoke.rs index 03c6251..13996da 100644 --- a/src/commands/meme/invoke.rs +++ b/src/commands/meme/invoke.rs @@ -30,37 +30,37 @@ use crate::{ #[command] #[aliases("mem")] pub async fn meme(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - _meme(ctx, msg, args, AudioPlayback::Optional) + _meme(ctx, msg, args, AudioPlayback::Optional).await } #[command] pub async fn omen(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { let args = Args::new("", &[]); - _meme(ctx, msg, args, AudioPlayback::Optional) + _meme(ctx, msg, args, AudioPlayback::Optional).await } #[command] pub async fn silentomen(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { let args = Args::new("", &[]); - _meme(ctx, msg, args, AudioPlayback::Prohibited) + _meme(ctx, msg, args, AudioPlayback::Prohibited).await } #[command] pub async fn audioomen(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { let args = Args::new("", &[]); - _meme(ctx, msg, args, AudioPlayback::Required) + _meme(ctx, msg, args, AudioPlayback::Required).await } #[command] #[aliases("audiomeme", "audiomem")] pub async fn audio_meme(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - _meme(ctx, msg, args, AudioPlayback::Required) + _meme(ctx, msg, args, AudioPlayback::Required).await } #[command] #[aliases("silentmeme", "silentmem")] pub async fn silent_meme(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - _meme(ctx, msg, args, AudioPlayback::Prohibited) + _meme(ctx, msg, args, AudioPlayback::Prohibited).await } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -152,7 +152,7 @@ async fn rand_meme( #[command] #[aliases("rarememe", "raremem")] pub async fn rare_meme(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { - let should_audio = ctx.users_listening()?; + let should_audio = util::users_listening(ctx).await?; let mut conn = connection()?; @@ -160,7 +160,7 @@ pub async fn rare_meme(ctx: &Context, msg: &Message, _args: Args) -> CommandResu match meme { Ok(meme) => { InvocationRecord::create(&mut conn, msg.author.id.get(), msg.id.get(), meme.id, true)?; - send_meme(ctx, &meme, &mut conn, msg) + send_meme(ctx, &meme, &mut conn, msg).await }, Err(e) => { match e.downcast_ref::<DieselError>() { @@ -177,7 +177,7 @@ pub async fn rare_meme(ctx: &Context, msg: &Message, _args: Args) -> CommandResu .map_err(CommandError::from) .await?; - Err(e) + Err(e.into()) }, } } diff --git a/src/commands/meme/mod.rs b/src/commands/meme/mod.rs index 31d9b78..24fc50d 100644 --- a/src/commands/meme/mod.rs +++ b/src/commands/meme/mod.rs @@ -3,6 +3,7 @@ use log::debug; use rand::random; use serenity::{ all::ReactionType, + async_trait, builder::{ CreateAttachment, CreateMessage, @@ -14,13 +15,21 @@ use serenity::{ model::channel::Message, prelude::*, }; +use songbird::input::{ + core::io::MediaSource, + AudioStream, + AudioStreamError, + Compose, + Input, +}; use crate::{ - audio::{ - PlayArgs, - PlayQueue, + commands::songbird, + db::{ + Audio, + Meme, }, - db::Meme, + CONFIG, }; pub use self::{ @@ -94,27 +103,45 @@ async fn send_meme( }, }; - // note: slight edge-case race condition here: there could have been something queued since we - // checked whether anything was playing. not a significant negative impact and unlikely, so i'm - // not worrying about it if let Some(audio) = audio { let audio = audio?; - { - let queue_lock = ctx.data.write().await.get::<PlayQueue>().cloned().unwrap(); - let mut play_queue = queue_lock.write().unwrap(); + let (_sb, call) = songbird(ctx, msg).await?; + let mut call = call.lock().await; - play_queue.meme_queue.push_back(PlayArgs { - initiator: msg.author.name.clone(), - data: ::either::Right(audio.data.clone()), - sender_channel: msg.channel_id, - start: None, - end: None, - }); + if call.current_channel().is_none() { + call.join(CONFIG.discord.voice_channel()).await?; } + call.enqueue_input(Input::Lazy(Box::new(audio))).await; + msg.react(ctx, ReactionType::Unicode("📣".to_owned())).await?; } Ok(()) } + +#[async_trait] +impl Compose for Audio { + fn create(&mut self) -> Result<AudioStream<Box<dyn MediaSource>>, AudioStreamError> { + let ms = std::io::Cursor::new(self.data.clone()); + let ms: Box<dyn MediaSource> = Box::new(ms); + + Ok(AudioStream { + input: ms, + hint: None, + }) + } + + #[inline] + async fn create_async( + &mut self, + ) -> Result<AudioStream<Box<dyn MediaSource>>, AudioStreamError> { + self.create() + } + + #[inline] + fn should_create_async(&self) -> bool { + false + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 4893e73..c8a7014 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,3 +1,4 @@ +use crate::util; use log::info; use serenity::framework::{ standard::macros::group, @@ -9,10 +10,7 @@ pub use self::meme::*; pub use self::{ playback::*, roll::ROLL_COMMAND, - today::{ - today, - TODAY_COMMAND, - }, + today::TODAY_COMMAND, }; pub(crate) mod playback; @@ -38,16 +36,24 @@ pub fn register_commands(f: StandardFramework) -> StandardFramework { let result = result.group(&crate::game::GAME_GROUP); result.help(&help::HELP).unrecognised_command(|ctx, msg, unrec| { - let url = match msg.content.split_whitespace().skip(1).next() { - Some(x) if x.starts_with("http") => x, - _ => { - info!("bad command formatting: '{}'", unrec); - let _ = ctx.send(msg.channel_id, "format your commands right. fuck you.", msg.tts); - return; - }, - }; + Box::pin(async move { + let url = match msg.content.split_whitespace().skip(1).next() { + Some(x) if x.starts_with("http") => x, + _ => { + info!("bad command formatting: '{}'", unrec); + let _ = util::send( + ctx, + msg.channel_id, + "format your commands right. fuck you.", + msg.tts, + ) + .await; + return; + }, + }; - let _ = _play(ctx, msg, &url); + let _ = _play(ctx, msg, &url); + }) }) } diff --git a/src/commands/playback.rs b/src/commands/playback.rs index 21393a2..7ecef47 100644 --- a/src/commands/playback.rs +++ b/src/commands/playback.rs @@ -1,7 +1,3 @@ -use either::{ - Left, - Right, -}; use log::{ debug, error, @@ -18,32 +14,43 @@ use serenity::{ CommandError, CommandResult, }, - futures::TryFutureExt, model::channel::Message, prelude::*, }; -use tap::{ - Conv, - Pipe, +use songbird::{ + input::YoutubeDl, + Call, + Songbird, }; +use std::sync::Arc; +use tap::Conv; use crate::{ - audio::{ - parse_times, - PlayArgs, - PlayQueue, - VoiceManager, - }, + bot::HttpKey, commands::sound_levels::*, util, CONFIG, }; #[group] -#[commands(skip, pause, resume, list, die, mute, unmute, play, volume)] +#[commands(skip, pause, resume, list, die, mute, unmute, play)] #[only_in(guild)] struct Playback; +pub async fn songbird( + ctx: &Context, + msg: &Message, +) -> Result<(Arc<Songbird>, Arc<Mutex<Call>>), CommandError> { + let Some(gid) = msg.guild_id else { + return Err(anyhow::anyhow!("no guild id").into()); + }; + + let sb = songbird::get(ctx).await.expect("acquiring songbird handle"); + let call = sb.get_or_insert(gid); + + Ok((sb, call)) +} + pub async fn _play(ctx: &Context, msg: &Message, url: &str) -> CommandResult { use url::{ Host, @@ -83,18 +90,20 @@ pub async fn _play(ctx: &Context, msg: &Message, url: &str) -> CommandResult { return Ok(()); } - let (start, end) = parse_times(&msg.content); + let (_sb, call) = songbird(ctx, msg).await?; + let mut call = call.lock().await; - let queue_lock = ctx.data.write().await.get::<PlayQueue>().cloned().unwrap(); - let mut play_queue = queue_lock.write().unwrap(); + if call.current_channel().is_none() { + call.join(CONFIG.discord.voice_channel()).await?; + } - play_queue.general_queue.push_back(PlayArgs { - initiator: msg.author.name.clone(), - data: Left(url.conv::<String>()), - sender_channel: msg.channel_id, - start, - end, - }); + let client = { + let data = ctx.data.read().await; + data.get::<HttpKey>().unwrap().clone() + }; + + let input = YoutubeDl::new_ytdl_like("yt-dlp", client.clone(), url.conv::<String>()); + call.enqueue_input(input.into()).await; Ok(()) } @@ -115,37 +124,15 @@ pub async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult }, }; - _play(ctx, msg, &url) + _play(ctx, msg, &url).await } #[command] pub async fn pause(ctx: &Context, msg: &Message, _: Args) -> CommandResult { - let queue_lock = ctx.data.write().await.get::<PlayQueue>().cloned().unwrap(); + let (_sb, call) = songbird(ctx, msg).await?; - let done = || util::send(ctx, msg.channel_id, "r u srs", msg.tts).map_err(CommandError::from); - let playing = { - let play_queue = queue_lock.read().unwrap(); - - let current_item = match play_queue.playing { - Some(ref x) => x, - None => return done().await, - }; - - let audio = current_item.audio.lock(); - audio.playing - }; - - if !playing { - return done().await; - } - - { - let queue = queue_lock.write().unwrap(); - let ref audio = queue.playing.clone().unwrap().audio; - audio.lock().pause(); - - info!("paused playback"); - } + let call = call.lock().await; + call.queue().pause()?; Ok(()) } @@ -157,58 +144,21 @@ pub async fn resume(ctx: &Context, msg: &Message, _: Args) -> CommandResult { } async fn _resume(ctx: &Context, msg: &Message) -> CommandResult { - let queue_lock = ctx.data.write().await.get::<PlayQueue>().cloned().unwrap(); + let (_sb, call) = songbird(ctx, msg).await?; - let done = || util::send(ctx, msg.channel_id, "r u srs", msg.tts).map_err(CommandError::from); - let playing = { - let play_queue = queue_lock.read().unwrap(); - - let current_item = match play_queue.playing { - Some(ref x) => x, - None => { - done().await?; - return Ok(()); - }, - }; - - let audio = current_item.audio.lock(); - audio.playing - }; - - if playing { - done().await?; - debug!("attempted to resume playback while sound was already playing"); - return Ok(()); - } - - { - let queue = queue_lock.write().unwrap(); - let ref audio = queue.playing.clone().unwrap().audio; - audio.lock().play(); - info!("playback resumed"); - } + let call = call.lock().await; + call.queue().resume()?; Ok(()) } #[command] #[aliases("next")] -pub async fn skip(ctx: &Context, _msg: &Message, _args: Args) -> CommandResult { - let data = ctx.data.write().await; - - let mgr_lock = data.get::<VoiceManager>().cloned().unwrap(); - let mut manager = mgr_lock.lock(); +pub async fn skip(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { + let (_sb, call) = songbird(ctx, msg).await?; - let queue_lock = data.get::<PlayQueue>().cloned().unwrap(); - - if let Some(handler) = manager.get_mut(CONFIG.discord.guild()) { - handler.stop(); - let mut play_queue = queue_lock.write().unwrap(); - play_queue.playing = None; - info!("skipped currently-playing audio"); - } else { - debug!("got skip with no handler attached"); - } + let call = call.lock().await; + call.queue().skip()?; Ok(()) } @@ -216,29 +166,10 @@ pub async fn skip(ctx: &Context, _msg: &Message, _args: Args) -> CommandResult { #[command] #[aliases("sudoku", "fuckoff", "stop")] pub async fn die(ctx: &Context, msg: &Message, _: Args) -> CommandResult { - let data = ctx.data.write().await; - - let mgr_lock = data.get::<VoiceManager>().cloned().unwrap(); - let mut manager = mgr_lock.lock(); + let (_sb, call) = songbird(ctx, msg).await?; - let queue_lock = data.get::<PlayQueue>().cloned().unwrap(); - - { - let mut play_queue = queue_lock.write().unwrap(); - - play_queue.playing = None; - play_queue.general_queue.clear(); - play_queue.meme_queue.clear(); - } - - if let Some(handler) = manager.get_mut(CONFIG.discord.guild()) { - info!("killing playback"); - handler.stop(); - handler.leave(); - } else { - util::send(ctx, msg.channel_id, "YOU die", msg.tts).await?; - debug!("got die with no handler attached"); - } + let call = call.lock().await; + call.queue().stop(); Ok(()) } @@ -246,55 +177,19 @@ pub async fn die(ctx: &Context, msg: &Message, _: Args) -> CommandResult { #[command] #[aliases("queue")] pub async fn list(ctx: &Context, msg: &Message, _: Args) -> CommandResult { - let queue_lock = ctx.data.write().await.get::<PlayQueue>().cloned().unwrap(); - let play_queue = queue_lock.read().unwrap(); + let (_sb, call) = songbird(ctx, msg).await?; - let channel = msg.channel(&ctx).await.unwrap().guild().unwrap(); + let call = call.lock().await; + let queue = call.queue(); - info!("listing queue"); - match play_queue.playing { - Some(ref info) => { - let audio = info.audio.lock(); - let status = if audio.playing { - "playing" - } else { - "paused:" - }; + util::send(ctx, msg.channel_id, "(command fix work-in-progress)", msg.tts).await?; - let playing_info = match info.init_args.data { - Left(ref url) => format!(" `{}`", url), - Right(_) => "memeing".to_owned(), - }; + for track in queue.current_queue().into_iter() { + let info = track.get_info().await?; - util::send( - ctx, - msg.channel_id, - &format!("Currently {} {} ({})", status, playing_info, info.init_args.initiator), - msg.tts, - ) + util::send(ctx, msg.channel_id, format!("track playing for {:?}", info.play_time), msg.tts) .await?; - }, - None => { - debug!("`list` called with no items in queue"); - util::send(ctx, msg.channel_id, "Nothing is playing you meme", msg.tts).await?; - return Ok(()); - }, } - play_queue - .meme_queue - .iter() - .chain(play_queue.general_queue.iter()) - .pipe(serenity::futures::stream::iter) - .for_each(|info| async move { - let playing_info = match info.data { - Left(ref url) => format!("`{}`", url), - Right(_) => "meme".to_owned(), - }; - - let _ = channel.say(&ctx, &format!("{} ({})", playing_info, info.initiator)).await; - }) - .await; - Ok(()) } diff --git a/src/commands/roll.rs b/src/commands/roll.rs index 6aefe34..45e3ba8 100644 --- a/src/commands/roll.rs +++ b/src/commands/roll.rs @@ -56,7 +56,7 @@ impl Calc { use self::Rule::*; lazy_static! { - static ref CLIMBER: PrecClimber<self::Rule> = { + static ref CLIMBER: PrecClimber<Rule> = { use pest::prec_climber::{ Assoc::*, Operator, @@ -75,7 +75,7 @@ impl Calc { let result = Calc::parse(calc, s.as_ref()).map_err(|_| CalcError::Pest)?; - fn eval_single_pair(pair: Pair<self::Rule>) -> StdResult<f64, CalcError> { + fn eval_single_pair(pair: Pair<Rule>) -> StdResult<f64, CalcError> { let result = match pair.as_rule() { oct | hex | binary => { let base = match pair.as_rule() { @@ -159,7 +159,7 @@ impl Calc { Ok(result) } - fn eval_expr(p: Pairs<self::Rule>) -> StdResult<f64, CalcError> { + fn eval_expr(p: Pairs<Rule>) -> StdResult<f64, CalcError> { CLIMBER.climb(p, eval_single_pair, |lhs, op, rhs| { let lhs = lhs?; let rhs = rhs?; diff --git a/src/commands/sound_levels.rs b/src/commands/sound_levels.rs index db0b6a6..8c75b37 100644 --- a/src/commands/sound_levels.rs +++ b/src/commands/sound_levels.rs @@ -1,132 +1,88 @@ -use log::{ - error, - info, - trace, - warn, -}; use serenity::{ framework::standard::{ macros::command, Args, - CommandError, CommandResult, }, - futures::TryFutureExt, model::channel::Message, prelude::*, }; -use crate::{ - audio::{ - PlayQueue, - VoiceManager, - }, - util, - CONFIG, -}; +use crate::commands::songbird; pub const DEFAULT_VOLUME: f32 = 0.20; const MAX_VOLUME: f32 = 5.0; #[command] -pub async fn mute(ctx: &Context, _: &Message, _: Args) -> CommandResult { - let mgr_lock = ctx.data.write().await.get::<VoiceManager>().cloned().unwrap(); - let mut manager = mgr_lock.lock(); +pub async fn mute(ctx: &Context, msg: &Message, _: Args) -> CommandResult { + let (_sb, call) = songbird(ctx, msg).await?; - manager.get_mut(CONFIG.discord.guild()).map(|handler| { - if handler.self_mute { - trace!("Already muted.") - } else { - handler.mute(true); - trace!("Muted"); - } - }); + let mut call = call.lock().await; + call.mute(true).await?; Ok(()) } #[command] pub async fn unmute(ctx: &Context, msg: &Message, _: Args) -> CommandResult { - let mgr_lock = ctx.data.write().await.get::<VoiceManager>().cloned().unwrap(); - let mut manager = mgr_lock.lock(); + let (_sb, call) = songbird(ctx, msg).await?; - if let Some(handler) = manager.get_mut(CONFIG.discord.guild()) { - if !handler.self_mute { - trace!("Already unmuted.") - } else { - handler.mute(false); - trace!("Unmuted"); - let _ = util::send(ctx, msg.channel_id, "REEEEEEEEEEEEEE", msg.tts) - .map_err(CommandError::from) - .await; - } - } + let mut call = call.lock().await; + call.mute(true).await?; Ok(()) } -#[command] -pub async fn volume(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - if args.len() == 0 { - let vol = { - let queue_lock = ctx.data.write().await.get::<PlayQueue>().cloned().unwrap(); - let play_queue = queue_lock.read().unwrap(); - (play_queue.volume / DEFAULT_VOLUME * 100.0) as usize - }; - - trace!("reporting volume {}", vol); - - return util::send(ctx, msg.channel_id, &format!("volume: {}%", vol), msg.tts) - .map_err(CommandError::from) - .await; - } - - let vol: usize = match args.single::<f32>() { - Ok(vol) if vol.is_nan() => { - warn!("reporting NaN volume"); - return util::send(ctx, msg.channel_id, "you're a fuck", msg.tts) - .map_err(CommandError::from) - .await; - }, - Ok(vol) => vol as usize, - Err(e) => { - error!("parsing volume arg: {}", e); - return util::send(ctx, msg.channel_id, "???????", msg.tts) - .map_err(CommandError::from) - .await; - }, - }; - - let mut vol: f32 = (vol as f32) / 100.0; // force aliasing to reasonable values - let adjusted_text = if vol > MAX_VOLUME { - format!(" ({:.0}% max)", MAX_VOLUME * 100.0) - } else { - "".to_owned() - }; - - vol = vol.clamp(0.0, MAX_VOLUME); - - let queue_lock = ctx.data.write().await.get::<PlayQueue>().cloned().unwrap(); - - { - let mut play_queue = queue_lock.write().unwrap(); - play_queue.volume = vol * DEFAULT_VOLUME; - info!("volume updated to {}", vol); - } - - util::send(ctx, msg.channel_id, format!("volume adjusted{}", adjusted_text), msg.tts).await?; - - { - let play_queue = queue_lock.read().unwrap(); - - let current_item = match play_queue.playing { - Some(ref x) => x, - None => return Ok(()), - }; - - let mut audio = current_item.audio.lock(); - audio.volume(play_queue.volume); - } - - Ok(()) -} +// #[command] +// pub async fn volume(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { +// if args.len() == 0 { +// let vol = { +// let queue_lock = ctx.data.write().await.get::<PlayQueue>().cloned().unwrap(); +// let play_queue = queue_lock.read().unwrap(); +// (play_queue.volume / DEFAULT_VOLUME * 100.0) as usize +// }; +// +// trace!("reporting volume {}", vol); +// +// return util::send(ctx, msg.channel_id, &format!("volume: {}%", vol), msg.tts) +// .map_err(CommandError::from) +// .await; +// } +// +// let vol: usize = match args.single::<f32>() { +// Ok(vol) if vol.is_nan() => { +// warn!("reporting NaN volume"); +// return util::send(ctx, msg.channel_id, "you're a fuck", msg.tts) +// .map_err(CommandError::from) +// .await; +// }, +// Ok(vol) => vol as usize, +// Err(e) => { +// error!("parsing volume arg: {}", e); +// return util::send(ctx, msg.channel_id, "???????", msg.tts) +// .map_err(CommandError::from) +// .await; +// }, +// }; +// +// let mut vol: f32 = (vol as f32) / 100.0; // force aliasing to reasonable values +// let adjusted_text = if vol > MAX_VOLUME { +// format!(" ({:.0}% max)", MAX_VOLUME * 100.0) +// } else { +// "".to_owned() +// }; +// +// vol = vol.clamp(0.0, MAX_VOLUME); +// +// let queue_lock = ctx.data.write().await.get::<PlayQueue>().cloned().unwrap(); +// +// { +// let mut play_queue = queue_lock.write().unwrap(); +// play_queue.volume = vol * DEFAULT_VOLUME; +// info!("volume updated to {}", vol); +// } +// +// util::send(ctx, msg.channel_id, format!("volume adjusted{}", adjusted_text), msg.tts).await?; +// +// Ok(()) +// } diff --git a/src/commands/today/mod.rs b/src/commands/today/mod.rs index 0a0ba7b..7f1dca7 100644 --- a/src/commands/today/mod.rs +++ b/src/commands/today/mod.rs @@ -1,5 +1,4 @@ use chrono::Duration; -use either::Left; use lazy_static::lazy_static; use log::debug; use rand::{ @@ -15,13 +14,14 @@ use serenity::{ model::channel::Message, prelude::*, }; +use songbird::input::YoutubeDl; +use tap::Conv; use crate::{ - audio::{ - PlayArgs, - PlayQueue, - }, + bot::HttpKey, + commands::songbird, util, + CONFIG, }; mod prelude; @@ -50,19 +50,6 @@ pub struct TodayArgs { pub end: Option<Duration>, } -impl TodayArgs { - #[inline] - pub fn as_play_args(&self, msg: &Message) -> PlayArgs { - PlayArgs { - initiator: "you have done this to yourself :^)".to_string(), - data: Left(self.url.to_owned()), - sender_channel: msg.channel_id, - start: self.start, - end: self.end, - } - } -} - lazy_static! { static ref ALL: Vec<fn(chrono::NaiveDateTime) -> TodayIter> = vec![ sept_21::sept_21, @@ -112,17 +99,36 @@ pub async fn today(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { debug!("{} options for {}", options.len(), today); - let play_args = options.choose(&mut thread_rng()).map(|x| x.as_play_args(msg)); + let play_args = options.choose(&mut thread_rng()); if let Some(play_args) = play_args { - play_args.data.as_ref().left().iter().for_each(|url| { - debug!("today selected: {}", url); - }); + let (_sb, call) = songbird(ctx, msg).await?; + let mut call = call.lock().await; - let queue_lock = ctx.data.write().await.get::<PlayQueue>().cloned().unwrap(); - let mut play_queue = queue_lock.write().unwrap(); + if call.current_channel().is_none() { + call.join(CONFIG.discord.voice_channel()).await?; + } + + let client = { + let data = ctx.data.read().await; + data.get::<HttpKey>().unwrap().clone() + }; + + let input = + YoutubeDl::new_ytdl_like("yt-dlp", client.clone(), play_args.url.conv::<String>()); - play_queue.general_queue.push_front(play_args); + call.enqueue_input(input.into()).await; + + let q = call.queue(); + q.pause()?; + q.modify_queue(move |q| { + let last = q.pop_back(); + + if let Some(last) = last { + q.push_front(last); + } + }); + q.resume()?; } else { util::send(ctx, msg.channel_id, "no", false).await?; util::send(ctx, msg.channel_id, ":angry:", false).await?; diff --git a/src/config.rs b/src/config.rs index e03dbbe..59bb4c3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,16 +1,14 @@ use std::path::PathBuf; -use serenity::{ - model::id::{ - GuildId, - UserId, - ChannelId, - }, +use serenity::model::id::{ + ChannelId, + GuildId, + UserId, }; use dotenv::dotenv; -use lazy_static::lazy_static; use envconfig::Envconfig; +use lazy_static::lazy_static; lazy_static! { pub static ref CONFIG: Config = { @@ -18,7 +16,6 @@ lazy_static! { Config::init_from_env().unwrap() }; - pub static ref FFMPEG_COMMAND: String = { let result = CONFIG.ffmpeg.clone().unwrap_or("youtube-dl".to_owned()); log::debug!("got ffmpeg: {}", result); @@ -53,13 +50,16 @@ pub struct Config { #[envconfig(from = "RESTRICT")] pub restrict: Option<PathBuf>, + #[envconfig(nested = true)] pub discord: DiscordConfig, + #[envconfig(nested = true)] pub sheets: SheetsConfig, } #[derive(Envconfig)] pub struct DiscordConfig { + #[envconfig(nested = true)] pub auth: DiscordAuth, #[envconfig(from = "TARGET_GUILD")] diff --git a/src/db/mod.rs b/src/db/mod.rs index 1ac44e9..94953d5 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -10,46 +10,53 @@ use chrono::{ Utc, }; use diesel::{ - NotFound, prelude::*, - r2d2::{ConnectionManager, ManageConnection}, + r2d2::{ + ConnectionManager, + ManageConnection, + }, + NotFound, }; use postgres::Client as RawPgConn; use r2d2_postgres::{ - PostgresConnectionManager as RawPgConnMgr, postgres::{ - NoTls, Config, + NoTls, }, + PostgresConnectionManager as RawPgConnMgr, }; use anyhow::anyhow; -use lazy_static::lazy_static; use diesel_migrations::MigrationHarness; +use lazy_static::lazy_static; -use crate::{Error, Result}; +use crate::{ + Error, + Result, +}; pub use self::models::*; use self::schema::*; -mod schema; mod models; +mod schema; const MIGRATIONS: diesel_migrations::EmbeddedMigrations = diesel_migrations::embed_migrations!(); static MIGRATE: std::sync::Once = std::sync::Once::new(); lazy_static! { - static ref DB_URL: String = env::var("DATABASE_URL").expect("no database url in environment").into(); + static ref DB_URL: String = + env::var("DATABASE_URL").expect("no database url in environment").into(); static ref DB_CONFIG: Config = Config::from_str(&DB_URL).expect("parsing db url as config"); static ref CONN_MGR: ConnectionManager<PgConnection> = ConnectionManager::new(DB_URL.clone()); static ref RAW_CONN_MGR: RawPgConnMgr<NoTls> = RawPgConnMgr::new(DB_CONFIG.clone(), NoTls); } - #[inline] pub fn connection() -> Result<PgConnection> { - CONN_MGR.connect() + CONN_MGR + .connect() .map(|mut conn| { MIGRATE.call_once(|| { log::info!("running migrations"); @@ -69,17 +76,13 @@ fn raw_connection() -> Result<RawPgConn> { connection()?; } - RAW_CONN_MGR.connect() - .map_err(Error::from) + RAW_CONN_MGR.connect().map_err(Error::from) } pub fn find_meme<T: AsRef<str>>(conn: &mut PgConnection, search: T) -> Result<Meme> { let search = search.as_ref(); - let mut meme = memes::table - .filter(memes::title.eq(search)) - .limit(1) - .first::<Meme>(conn); + let mut meme = memes::table.filter(memes::title.eq(search)).limit(1).first::<Meme>(conn); if let Err(NotFound) = meme { let format_search = format!("%{}%", search); @@ -90,16 +93,21 @@ pub fn find_meme<T: AsRef<str>>(conn: &mut PgConnection, search: T) -> Result<Me .first::<Meme>(conn); } - meme - .map_err(Error::from) + meme.map_err(Error::from) } -pub fn query_meme<T: AsRef<str>>(search: T, user_id: Option<u64>, age_desc: bool) -> Result<Vec<(Meme, Metadata)>> { +pub fn query_meme<T: AsRef<str>>( + search: T, + user_id: Option<u64>, + age_desc: bool, +) -> Result<Vec<(Meme, Metadata)>> { let mut raw_conn = raw_connection()?; let search = format!("%{}%", search.as_ref()); - let rows = raw_conn.query(&format!(r#" + let rows = raw_conn.query( + &format!( + r#" SELECT memes.id, title, content, image_id, audio_id, metadata_id, created, created_by FROM memes INNER JOIN metadata ON memes.metadata_id = metadata.id @@ -108,27 +116,30 @@ pub fn query_meme<T: AsRef<str>>(search: T, user_id: Option<u64>, age_desc: bool ORDER BY metadata.created {} LIMIT 100 "#, - if age_desc { "DESC" } else { "ASC" }, - ), &[ - &search, - &(user_id.unwrap_or(0) as i64), - &user_id.is_none(), - ])?; + if age_desc { + "DESC" + } else { + "ASC" + }, + ), + &[&search, &(user_id.unwrap_or(0) as i64), &user_id.is_none()], + )?; - let result = rows.iter() + let result = rows + .iter() .map(|row| { let meme = Meme { - id: row.get(0), - title: row.get(1), - content: row.get(2), - image_id: row.get(3), - audio_id: row.get(4), + id: row.get(0), + title: row.get(1), + content: row.get(2), + image_id: row.get(3), + audio_id: row.get(4), metadata_id: row.get(5), }; let metadata = Metadata { - id: row.get(5), - created: row.get(6), + id: row.get(5), + created: row.get(6), created_by: row.get(7), }; @@ -139,26 +150,21 @@ pub fn query_meme<T: AsRef<str>>(search: T, user_id: Option<u64>, age_desc: bool Ok(result) } -pub fn delete_meme<T: AsRef<str>>(conn: &mut PgConnection, search: T, deleted_by: u64) -> Result<()> { +pub fn delete_meme<T: AsRef<str>>( + conn: &mut PgConnection, + search: T, + deleted_by: u64, +) -> Result<()> { conn.transaction::<(), Error, _>(|tx| { - let deleted = memes::table - .filter(memes::title.eq(search.as_ref())) - .first::<Meme>(tx)?; + let deleted = memes::table.filter(memes::title.eq(search.as_ref())).first::<Meme>(tx)?; - ::diesel::delete(memes::table) - .filter(memes::id.eq(deleted.id)) - .execute(tx)?; + diesel::delete(memes::table).filter(memes::id.eq(deleted.id)).execute(tx)?; if let Some(image_id) = deleted.image_id { - let count = memes::table - .filter(memes::image_id.eq(image_id)) - .count() - .execute(tx)?; + let count = memes::table.filter(memes::image_id.eq(image_id)).count().execute(tx)?; if count == 0 { - ::diesel::delete(images::table) - .filter(images::id.eq(image_id)) - .execute(tx)?; + diesel::delete(images::table).filter(images::id.eq(image_id)).execute(tx)?; } } @@ -169,21 +175,17 @@ pub fn delete_meme<T: AsRef<str>>(conn: &mut PgConnection, search: T, deleted_by .execute(tx)?; if count == 0 { - ::diesel::delete(audio::table) - .filter(audio::id.eq(audio_id)) - .execute(tx)?; + diesel::delete(audio::table).filter(audio::id.eq(audio_id)).execute(tx)?; } } let tombstone = NewTombstone { - deleted_by: deleted_by as i64, + deleted_by: deleted_by as i64, metadata_id: deleted.metadata_id, - meme_id: deleted.id, + meme_id: deleted.id, }; - let _ = ::diesel::insert_into(tombstones::table) - .values(&tombstone) - .execute(tx)?; + let _ = diesel::insert_into(tombstones::table).values(&tombstone).execute(tx)?; Ok(()) }) @@ -194,7 +196,8 @@ pub fn rare_meme(conn: &mut PgConnection, audio: bool) -> Result<Meme> { let mut raw_conn = raw_connection()?; - let rows = raw_conn.query(r#" + let rows = raw_conn.query( + r#" WITH meme_count AS ( SELECT @@ -224,9 +227,12 @@ pub fn rare_meme(conn: &mut PgConnection, audio: bool) -> Result<Meme> { sum(play_prop) OVER (ORDER BY play_prop DESC) as play_prop FROM least_used LIMIT 100; - "#, &[&!audio, &audio])?; + "#, + &[&!audio, &audio], + )?; - let elems = rows.iter() + let elems = rows + .iter() .map(|row| (row.get::<_, i32>(0), row.get::<_, f64>(1) as i64)) .collect::<Vec<_>>(); @@ -237,7 +243,8 @@ pub fn rare_meme(conn: &mut PgConnection, audio: bool) -> Result<Meme> { let mut rng = thread_rng(); let target_prob = rng.gen_range(0..elems.last().unwrap().1); - let meme_id = elems.into_iter() + let meme_id = elems + .into_iter() .find(|(_, x)| target_prob < *x) .ok_or_else(|| anyhow!("couldn't locate meme satisfying target probability"))? .0; @@ -246,36 +253,40 @@ pub fn rare_meme(conn: &mut PgConnection, audio: bool) -> Result<Meme> { } pub fn rand_meme(conn: &mut PgConnection, audio: bool) -> Result<Meme> { - use rand::{thread_rng, seq::SliceRandom}; + use rand::{ + seq::SliceRandom, + thread_rng, + }; let ids: Vec<i32> = if audio { memes::table .select(memes::id) - .filter(memes::content.is_not_null() - .or(memes::image_id.is_not_null()) - .or(memes::audio_id.is_not_null())) + .filter( + memes::content + .is_not_null() + .or(memes::image_id.is_not_null()) + .or(memes::audio_id.is_not_null()), + ) .load(conn) .map_err(Error::from)? } else { memes::table .select(memes::id) - .filter(memes::content.is_not_null() - .or(memes::image_id.is_not_null())) + .filter(memes::content.is_not_null().or(memes::image_id.is_not_null())) .load(conn) .map_err(Error::from)? }; - let id = ids.choose(&mut thread_rng()) - .ok_or_else(|| anyhow!("couldn't load meme"))?; + let id = ids.choose(&mut thread_rng()).ok_or_else(|| anyhow!("couldn't load meme"))?; - memes::table - .find(id) - .first::<Meme>(conn) - .map_err(Error::from) + memes::table.find(id).first::<Meme>(conn).map_err(Error::from) } pub fn rand_audio_meme(conn: &mut PgConnection) -> Result<Meme> { - use rand::{thread_rng, seq::SliceRandom}; + use rand::{ + seq::SliceRandom, + thread_rng, + }; let ids: Vec<i32> = memes::table .select(memes::id) @@ -283,17 +294,16 @@ pub fn rand_audio_meme(conn: &mut PgConnection) -> Result<Meme> { .load(conn) .map_err(Error::from)?; - let id = ids.choose(&mut thread_rng()) - .ok_or_else(|| anyhow!("couldn't load audio meme"))?; + let id = ids.choose(&mut thread_rng()).ok_or_else(|| anyhow!("couldn't load audio meme"))?; - memes::table - .find(id) - .first::<Meme>(conn) - .map_err(Error::from) + memes::table.find(id).first::<Meme>(conn).map_err(Error::from) } pub fn rand_silent_meme(conn: &mut PgConnection) -> Result<Meme> { - use rand::{thread_rng, seq::SliceRandom}; + use rand::{ + seq::SliceRandom, + thread_rng, + }; let ids: Vec<i32> = memes::table .select(memes::id) @@ -301,67 +311,63 @@ pub fn rand_silent_meme(conn: &mut PgConnection) -> Result<Meme> { .load(conn) .map_err(Error::from)?; - let id = ids.choose(&mut thread_rng()) - .ok_or_else(|| anyhow!("couldn't load audio meme"))?; + let id = ids.choose(&mut thread_rng()).ok_or_else(|| anyhow!("couldn't load audio meme"))?; - memes::table - .find(id) - .first::<Meme>(conn) - .map_err(Error::from) + memes::table.find(id).first::<Meme>(conn).map_err(Error::from) } #[derive(Debug, Clone)] pub struct Stats { - pub memes_overall: usize, - pub audio_memes: usize, - pub image_memes: usize, - pub started_recording: DateTime<Utc>, - pub total_meme_invocations: usize, - pub audio_meme_invocations: usize, + pub memes_overall: usize, + pub audio_memes: usize, + pub image_memes: usize, + pub started_recording: DateTime<Utc>, + pub total_meme_invocations: usize, + pub audio_meme_invocations: usize, pub random_meme_invocations: usize, - pub most_active_day: Date<Utc>, + pub most_active_day: Date<Utc>, pub most_active_day_count: usize, - pub most_audio_active_day: Date<Utc>, + pub most_audio_active_day: Date<Utc>, pub most_audio_active_count: usize, - pub most_random_meme_user: u64, - pub most_random_meme_user_count: usize, - pub most_directly_named_meme_user: u64, + pub most_random_meme_user: u64, + pub most_random_meme_user_count: usize, + pub most_directly_named_meme_user: u64, pub most_directly_named_meme_count: usize, - pub most_popular_named_meme: String, + pub most_popular_named_meme: String, pub most_popular_named_meme_count: usize, - pub most_popular_random_meme: String, + pub most_popular_random_meme: String, pub most_popular_random_meme_count: usize, - pub most_popular_meme_overall: String, + pub most_popular_meme_overall: String, pub most_popular_meme_overall_count: usize, } pub fn stats(conn: &mut PgConnection) -> Result<Stats> { - use diesel::dsl::{count_star, count}; use chrono::{ - NaiveDateTime, NaiveDate, + NaiveDateTime, + }; + use diesel::dsl::{ + count, + count_star, }; #[inline] fn to_utc(ndt: NaiveDateTime) -> DateTime<Utc> { - DateTime::from_utc(ndt, Utc{}) + DateTime::from_utc(ndt, Utc {}) } #[inline] fn to_utc_date(nd: NaiveDate) -> Date<Utc> { - Date::from_utc(nd, Utc{}) + Date::from_utc(nd, Utc {}) } - let total_count: i64 = memes::table - .select(count_star()) - .first(conn) - .map_err(Error::from)?; + let total_count: i64 = memes::table.select(count_star()).first(conn).map_err(Error::from)?; let image_count: i64 = memes::table .select(count(memes::image_id)) @@ -383,10 +389,8 @@ pub fn stats(conn: &mut PgConnection) -> Result<Stats> { let started_recording = to_utc(started_recording); - let total_meme_invocations: i64 = invocation_records::table - .select(count_star()) - .first(conn) - .map_err(Error::from)?; + let total_meme_invocations: i64 = + invocation_records::table.select(count_star()).first(conn).map_err(Error::from)?; let audio_meme_invocations: i64 = invocation_records::table .inner_join(memes::table) @@ -403,81 +407,102 @@ pub fn stats(conn: &mut PgConnection) -> Result<Stats> { let mut raw_conn = raw_connection()?; - let row = raw_conn.query_one(r#" + let row = raw_conn.query_one( + r#" SELECT DATE(time) as dt, COUNT(*) FROM invocation_records GROUP BY dt ORDER BY COUNT(*) DESC LIMIT 1; - "#, &[])?; + "#, + &[], + )?; let most_active_day = to_utc_date(row.get(0)); let most_active_day_count: i64 = row.get(1); - let row = raw_conn.query_one(r#" + let row = raw_conn.query_one( + r#" SELECT DATE(time) as dt, COUNT(*) FROM invocation_records INNER JOIN memes ON invocation_records.meme_id = memes.id WHERE memes.audio_id IS NOT NULL GROUP BY dt ORDER BY COUNT(*) DESC LIMIT 1; - "#, &[])?; + "#, + &[], + )?; let most_active_audio_day = to_utc_date(row.get(0)); let most_active_audio_day_count: i64 = row.get(1); - let row = raw_conn.query_one(r#" + let row = raw_conn.query_one( + r#" SELECT user_id, COUNT(*) FROM invocation_records WHERE random IS TRUE GROUP BY user_id ORDER BY COUNT(*) DESC LIMIT 1; - "#, &[])?; + "#, + &[], + )?; let most_random_invoker: i64 = row.get(0); let most_random_invoker_count: i64 = row.get(1); - let row = raw_conn.query_one(r#" + let row = raw_conn.query_one( + r#" SELECT user_id, COUNT(*) FROM invocation_records WHERE random IS FALSE GROUP BY user_id ORDER BY COUNT(*) DESC LIMIT 1; - "#, &[])?; + "#, + &[], + )?; let most_specific_invoker: i64 = row.get(0); let most_specific_invoker_count: i64 = row.get(1); - let row = raw_conn.query_one(r#" + let row = raw_conn.query_one( + r#" SELECT memes.title, COUNT(*) FROM invocation_records INNER JOIN memes ON meme_id = memes.id WHERE random IS FALSE GROUP BY memes.title ORDER BY COUNT(*) DESC LIMIT 1; - "#, &[])?; + "#, + &[], + )?; let most_requested_meme = row.get(0); let most_requested_meme_count: i64 = row.get(1); - let row = raw_conn.query_one(r#" + let row = raw_conn.query_one( + r#" SELECT memes.title, COUNT(*) FROM invocation_records INNER JOIN memes ON meme_id = memes.id WHERE random IS TRUE GROUP BY memes.title ORDER BY COUNT(*) DESC LIMIT 1; - "#, &[])?; + "#, + &[], + )?; let most_random_meme = row.get(0); let most_random_meme_count: i64 = row.get(1); - let row = raw_conn.query_one(r#" + let row = raw_conn.query_one( + r#" SELECT memes.title, COUNT(*) FROM invocation_records INNER JOIN memes ON meme_id = memes.id GROUP BY memes.title ORDER BY COUNT(*) DESC LIMIT 1; - "#, &[])?; + "#, + &[], + )?; let most_invoked_meme = row.get(0); let most_invoked_meme_count: i64 = row.get(1); @@ -514,10 +539,10 @@ pub fn stats(conn: &mut PgConnection) -> Result<Stats> { #[derive(Clone, Debug, Hash, PartialEq, Eq, Default)] pub struct MemerInfo { - pub user_id: u64, - pub random_memes: usize, - pub specific_memes: usize, - pub most_used_meme: String, + pub user_id: u64, + pub random_memes: usize, + pub specific_memes: usize, + pub most_used_meme: String, pub most_used_meme_count: usize, } @@ -563,21 +588,23 @@ pub fn memers() -> Result<Vec<MemerInfo>> { ORDER BY (random_count.count + specific_count.count) DESC "#, &[])?; - let result = rows.iter().map(|row| { - let user_id: i64 = row.get(0); - let random_count: i64 = row.get(1); - let specific_count: i64 = row.get(2); - let most_memed_meme: String = row.get(3); - let most_memed_count: i64 = row.get(4); + let result = rows + .iter() + .map(|row| { + let user_id: i64 = row.get(0); + let random_count: i64 = row.get(1); + let specific_count: i64 = row.get(2); + let most_memed_meme: String = row.get(3); + let most_memed_count: i64 = row.get(4); - MemerInfo { - user_id: user_id as u64, - random_memes: random_count as usize, - specific_memes: specific_count as usize, - most_used_meme: most_memed_meme, - most_used_meme_count: most_memed_count as usize, - } - }) + MemerInfo { + user_id: user_id as u64, + random_memes: random_count as usize, + specific_memes: specific_count as usize, + most_used_meme: most_memed_meme, + most_used_meme_count: most_memed_count as usize, + } + }) .collect(); Ok(result) diff --git a/src/db/models.rs b/src/db/models.rs index cdcdd99..bba25a2 100644 --- a/src/db/models.rs +++ b/src/db/models.rs @@ -1,8 +1,8 @@ use chrono::naive::NaiveDateTime; use diesel::{ + prelude::*, Identifiable, Insertable, - prelude::*, Queryable, }; use sha1::Digest; @@ -16,21 +16,23 @@ use crate::{ #[derive(Queryable, Identifiable, PartialEq, Debug, Clone)] #[diesel(table_name = memes)] pub struct Meme { - pub id: i32, - pub title: String, - pub content: Option<String>, - pub image_id: Option<i32>, - pub audio_id: Option<i32>, + pub id: i32, + pub title: String, + pub content: Option<String>, + pub image_id: Option<i32>, + pub audio_id: Option<i32>, pub metadata_id: i32, } impl Meme { pub fn image(&self, conn: &mut PgConnection) -> Option<Result<Image>> { - self.image_id.map(|x: i32| images::table.filter(images::id.eq(x)).first(conn).map_err(Error::from)) + self.image_id + .map(|x: i32| images::table.filter(images::id.eq(x)).first(conn).map_err(Error::from)) } pub fn audio(&self, conn: &mut PgConnection) -> Option<Result<Audio>> { - self.audio_id.map(|x: i32| audio::table.filter(audio::id.eq(x)).first(conn).map_err(Error::from)) + self.audio_id + .map(|x: i32| audio::table.filter(audio::id.eq(x)).first(conn).map_err(Error::from)) } pub fn find(conn: &mut PgConnection, id: i32) -> Result<Meme> { @@ -41,10 +43,10 @@ impl Meme { #[derive(Insertable, PartialEq, Debug, Clone)] #[diesel(table_name = memes)] pub struct NewMeme { - pub title: String, - pub content: Option<String>, - pub image_id: Option<i32>, - pub audio_id: Option<i32>, + pub title: String, + pub content: Option<String>, + pub image_id: Option<i32>, + pub audio_id: Option<i32>, pub metadata_id: i32, } @@ -54,28 +56,27 @@ impl NewMeme { self.metadata_id = metadata.id; - ::diesel::insert_into(memes::table) + diesel::insert_into(memes::table) .values(&self) .get_result::<Meme>(conn) .map_err(Error::from) } } - #[derive(Queryable, Identifiable, PartialEq, Debug)] #[diesel(table_name = audio)] pub struct Audio { - pub id: i32, - pub data: Vec<u8>, + pub id: i32, + pub data: Vec<u8>, pub metadata_id: i32, - pub data_hash: Vec<u8>, + pub data_hash: Vec<u8>, } impl Audio { pub fn create(conn: &mut PgConnection, data: Vec<u8>, by_user: u64) -> Result<i32> { let mut data_hash = ::sha1::Sha1::new(); data_hash.update(&data); - let data_hash = data_hash.digest().bytes().to_vec(); + let data_hash = data_hash.finalize().to_vec(); let id = audio::table .select(audio::id) @@ -94,7 +95,7 @@ impl Audio { metadata_id: metadata.id, }; - ::diesel::insert_into(audio::table) + diesel::insert_into(audio::table) .values(&new_audio) .returning(audio::id) .get_result(conn) @@ -105,27 +106,31 @@ impl Audio { #[derive(Insertable, PartialEq, Debug)] #[diesel(table_name = audio)] pub struct NewAudio { - pub data: Vec<u8>, + pub data: Vec<u8>, pub metadata_id: i32, - pub data_hash: Vec<u8>, + pub data_hash: Vec<u8>, } - #[derive(Queryable, Identifiable, PartialEq, Debug)] #[diesel(table_name = images)] pub struct Image { - pub id: i32, - pub data: Vec<u8>, + pub id: i32, + pub data: Vec<u8>, pub metadata_id: i32, - pub data_hash: Vec<u8>, - pub filename: String, + pub data_hash: Vec<u8>, + pub filename: String, } impl Image { - pub fn create(conn: &mut PgConnection, filename: &str, data: Vec<u8>, by_user: u64) -> Result<i32> { + pub fn create( + conn: &mut PgConnection, + filename: &str, + data: Vec<u8>, + by_user: u64, + ) -> Result<i32> { let mut data_hash = ::sha1::Sha1::new(); data_hash.update(&data); - let data_hash = data_hash.digest().bytes().to_vec(); + let data_hash = data_hash.finalize().to_vec(); let id = images::table .select(images::id) @@ -145,7 +150,7 @@ impl Image { metadata_id: metadata.id, }; - ::diesel::insert_into(images::table) + diesel::insert_into(images::table) .values(&new_image) .returning(images::id) .get_result(conn) @@ -156,24 +161,23 @@ impl Image { #[derive(Insertable, PartialEq, Debug)] #[diesel(table_name = images)] pub struct NewImage { - pub data: Vec<u8>, + pub data: Vec<u8>, pub metadata_id: i32, - pub data_hash: Vec<u8>, - pub filename: String, + pub data_hash: Vec<u8>, + pub filename: String, } - #[derive(Queryable, Identifiable, PartialEq, Debug, Clone)] #[diesel(table_name = metadata)] pub struct Metadata { - pub id: i32, - pub created: NaiveDateTime, + pub id: i32, + pub created: NaiveDateTime, pub created_by: i64, } impl Metadata { pub fn create(conn: &mut PgConnection, by_user: u64) -> Result<Metadata> { - ::diesel::insert_into(metadata::table) + diesel::insert_into(metadata::table) .values(&NewMetadata { created_by: by_user as i64, }) @@ -182,9 +186,7 @@ impl Metadata { } pub fn find(conn: &mut PgConnection, id: i32) -> Result<Metadata> { - metadata::table.find(id) - .get_result::<Metadata>(conn) - .map_err(Error::from) + metadata::table.find(id).get_result::<Metadata>(conn).map_err(Error::from) } } @@ -194,21 +196,20 @@ pub struct NewMetadata { pub created_by: i64, } - #[derive(Queryable, Identifiable, PartialEq, Debug)] #[diesel(table_name = audit_records)] pub struct AuditRecord { - pub id: i32, - pub updated: NaiveDateTime, - pub updated_by: i64, + pub id: i32, + pub updated: NaiveDateTime, + pub updated_by: i64, pub metadata_id: i32, } impl AuditRecord { pub fn create(conn: &mut PgConnection, metadata: i32, by_user: u64) -> Result<AuditRecord> { - ::diesel::insert_into(audit_records::table) + diesel::insert_into(audit_records::table) .values(&NewAuditRecord { - updated_by: by_user as i64, + updated_by: by_user as i64, metadata_id: metadata, }) .get_result::<AuditRecord>(conn) @@ -219,52 +220,57 @@ impl AuditRecord { #[derive(Insertable, PartialEq, Debug)] #[diesel(table_name = audit_records)] pub struct NewAuditRecord { - pub updated_by: i64, + pub updated_by: i64, pub metadata_id: i32, } #[derive(Queryable, Identifiable, PartialEq, Debug)] #[diesel(table_name = tombstones)] pub struct Tombstone { - pub id: i32, - pub deleted: NaiveDateTime, - pub deleted_by: i64, + pub id: i32, + pub deleted: NaiveDateTime, + pub deleted_by: i64, pub metadata_id: i32, - pub meme_id: i32, + pub meme_id: i32, } - #[derive(Insertable, PartialEq, Debug)] #[diesel(table_name = tombstones)] pub struct NewTombstone { - pub deleted_by: i64, + pub deleted_by: i64, pub metadata_id: i32, - pub meme_id: i32, + pub meme_id: i32, } #[derive(Queryable, Identifiable, PartialEq, Debug)] #[diesel(table_name = invocation_records)] pub struct InvocationRecord { - pub id: i32, - pub user_id: i64, + pub id: i32, + pub user_id: i64, pub message_id: i64, - pub meme_id: i32, - pub time: NaiveDateTime, - pub random: bool, + pub meme_id: i32, + pub time: NaiveDateTime, + pub random: bool, } #[derive(Insertable, PartialEq, Debug)] #[diesel(table_name = invocation_records)] pub struct NewInvocationRecord { - pub user_id: i64, + pub user_id: i64, pub message_id: i64, - pub meme_id: i32, - pub random: bool, + pub meme_id: i32, + pub random: bool, } impl InvocationRecord { - pub fn create(conn: &mut PgConnection, user_id: u64, message_id: u64, meme_id: i32, random: bool) -> Result<Self> { - ::diesel::insert_into(invocation_records::table) + pub fn create( + conn: &mut PgConnection, + user_id: u64, + message_id: u64, + meme_id: i32, + random: bool, + ) -> Result<Self> { + diesel::insert_into(invocation_records::table) .values(&NewInvocationRecord { user_id: user_id as i64, message_id: message_id as i64, diff --git a/src/game.rs b/src/game.rs index 362e304..47ae18c 100644 --- a/src/game.rs +++ b/src/game.rs @@ -10,11 +10,16 @@ use std::{ }, }; +use anyhow::{ + anyhow, + Error, +}; use fnv::{ FnvHashMap, FnvHashSet, }; use itertools::Itertools; +use lazy_static::lazy_static; use log::{ debug, error, @@ -29,7 +34,10 @@ use serenity::{ }, ArgError, Args, + CommandError, + CommandResult, }, + futures::TryFutureExt, model::{ channel::Message, guild::Guild, @@ -39,21 +47,13 @@ use serenity::{ }; use url::Url; -use anyhow::{ - anyhow, - Error, -}; -use lazy_static::lazy_static; -use serenity::framework::standard::CommandResult; - use crate::{ + bot::HttpKey, util, Result, CONFIG, }; -pub use self::Game as GROUP; - #[group] #[prefix = "game"] #[commands(game, installedgame, ownedgame, updategaem)] @@ -106,7 +106,7 @@ lazy_static! { }) .collect::<FnvHashMap<_, _>>(); - log::info!( + info!( "loaded user info for {} users ({:#?})", result.len(), result.keys().collect::<Vec<_>>() @@ -164,13 +164,13 @@ impl FromStr for GameStatus { #[command] #[aliases("installedgaem")] pub async fn installedgame(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - _game(ctx, msg, args, GameStatus::Installed) + _game(ctx, msg, args, GameStatus::Installed).await } #[command] #[aliases("ownedgaem")] pub async fn ownedgame(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - _game(ctx, msg, args, GameStatus::NotInstalled) + _game(ctx, msg, args, GameStatus::NotInstalled).await } #[derive(Copy, Clone, Debug, thiserror::Error, PartialEq, Eq, Hash)] @@ -315,7 +315,11 @@ async fn _game( return Ok(()); } - let data = load_spreadsheet().await?; + let client = { + let data = ctx.data.read().await; + data.get::<HttpKey>().unwrap().clone() + }; + let data = load_spreadsheet(&client).await?; let user_indexes = (0..data.len()) .filter_map(|i| { @@ -395,7 +399,7 @@ async fn _game( Ok(()) } -async fn load_spreadsheet() -> Result<Vec<Vec<String>>> { +async fn load_spreadsheet(client: &reqwest::Client) -> Result<Vec<Vec<String>>> { let mut u = SPREADSHEET_URL.clone(); u.query_pairs_mut() @@ -405,7 +409,6 @@ async fn load_spreadsheet() -> Result<Vec<Vec<String>>> { .append_pair("key", &CONFIG.sheets.api_key); let req = reqwest::Request::new(reqwest::Method::GET, u); - let client = reqwest::Client::new(); let resp = client.execute(req).await?; #[derive(Deserialize)] @@ -429,6 +432,11 @@ async fn load_spreadsheet() -> Result<Vec<Vec<String>>> { pub async fn updategaem(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { use regex::Regex; + let client = { + let data = ctx.data.read().await; + data.get::<HttpKey>().unwrap() + }; + let arg_user = args.single_quoted::<String>(); let user = if arg_user.is_err() { @@ -437,11 +445,9 @@ pub async fn updategaem(ctx: &Context, msg: &Message, mut args: Args) -> Command use std::borrow::Borrow; let guild = - msg.channel_id.to_channel(&ctx)?.guild().ok_or(anyhow!("couldn't find guild"))?; + msg.channel_id.to_channel(&ctx).await?.guild().ok_or(anyhow!("couldn't find guild"))?; - let guild = guild.read().guild(&ctx).ok_or(anyhow!("couldn't find guild"))?; - - let guild = guild.read(); + let guild = guild.guild(&ctx).ok_or(anyhow!("couldn't find guild"))?; get_user_id(guild.borrow(), arg_user.unwrap()).map_err(Error::from)? }; @@ -450,15 +456,23 @@ pub async fn updategaem(ctx: &Context, msg: &Message, mut args: Args) -> Command let username = match DISCORD_MAP.get(&user) { Some(s) => s, - None => return util::send(ctx, msg.channel_id, "WHO THE FUCK ARE YE", msg.tts).await, + None => { + return util::send(ctx, msg.channel_id, "WHO THE FUCK ARE YE", msg.tts) + .map_err(CommandError::from) + .await; + }, }; let steam_id = match STEAM_MAP.get(&user) { Some(u) => u, - None => return util::send(ctx, msg.channel_id, "WHO ARE YE ON STEAM", msg.tts).await, + None => { + return util::send(ctx, msg.channel_id, "WHO ARE YE ON STEAM", msg.tts) + .map_err(CommandError::from) + .await; + }, }; - let spreadsheet = load_spreadsheet().await?; + let spreadsheet = load_spreadsheet(client).await?; let user_column = (0..spreadsheet.len()) .find(|x| spreadsheet[*x][0].to_lowercase() == username.to_lowercase()); @@ -466,7 +480,9 @@ pub async fn updategaem(ctx: &Context, msg: &Message, mut args: Args) -> Command let user_column = match user_column { Some(c) => &spreadsheet[c][1..], None => { - return util::send(ctx, msg.channel_id, "YER NOT IN THE SPREADSHEET", msg.tts).await; + return util::send(ctx, msg.channel_id, "YER NOT IN THE SPREADSHEET", msg.tts) + .map_err(CommandError::from) + .await; }, }; @@ -480,7 +496,9 @@ pub async fn updategaem(ctx: &Context, msg: &Message, mut args: Args) -> Command Some(c) => &spreadsheet[c][1..], None => { error!("didn't find an appid column in the spreadsheet"); - return util::send(ctx, msg.channel_id, "SPREADSHEET BROKE", msg.tts).await; + return util::send(ctx, msg.channel_id, "SPREADSHEET BROKE", msg.tts) + .map_err(CommandError::from) + .await; }, }; @@ -517,7 +535,14 @@ pub async fn updategaem(ctx: &Context, msg: &Message, mut args: Args) -> Command play_time: u64, } - let games_owned = reqwest::get(u) + let client = { + let data = ctx.data.read().await; + data.get::<HttpKey>().unwrap() + }; + + let games_owned = client + .get(u) + .send() .await? .json::<SteamResp>() .await? @@ -548,7 +573,10 @@ pub async fn updategaem(ctx: &Context, msg: &Message, mut args: Args) -> Command ), msg.tts, ) + .await?; } else { - util::send(ctx, msg.channel_id, "up to date", msg.tts) + util::send(ctx, msg.channel_id, "up to date", msg.tts).await?; } + + Ok(()) } diff --git a/src/main.rs b/src/main.rs index ab5db5d..3b8c2af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,12 +5,9 @@ #![feature(box_patterns)] #![allow(deprecated)] -use std::{ - thread, - time::{ - Duration, - Instant, - }, +use std::time::{ + Duration, + Instant, }; use log::{ @@ -39,7 +36,6 @@ mod game { } } -mod audio; mod bot; mod commands; mod config; @@ -55,7 +51,8 @@ const BACKOFF_INIT: f64 = 100.0; const MIN_RUN_DURATION: Duration = Duration::from_secs(120); -fn main() { +#[tokio::main] +async fn main() { log_setup::init(false).expect("initializing logging"); let mut backoff_count: usize = 0; @@ -64,13 +61,13 @@ fn main() { let start = Instant::now(); info!("starting bot"); - match bot::run() { + match bot::run().await { Err(e) => { error!("error encountered running client: {:?}", e); }, _ => { // NOTE: we MUST have gotten here through SIGINT/SIGTERM handlers - ::std::process::exit(0); + std::process::exit(0); }, } @@ -87,6 +84,6 @@ fn main() { let backoff_millis = (BACKOFF_INIT * BACKOFF_FACTOR.powi(backoff_count as i32)) as u64; info!("bot died too quickly. backing off, retrying in {}ms.", backoff_millis); - thread::sleep(Duration::from_millis(backoff_millis)); + tokio::time::sleep(Duration::from_millis(backoff_millis)).await; } } diff --git a/src/util.rs b/src/util.rs index ed5fd54..d59c4da 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,3 +1,4 @@ +use chrono::Duration;
use serenity::{
client::Context,
model::{
@@ -8,32 +9,31 @@ use serenity::{ permissions::Permissions,
},
};
+use std::process::Stdio;
use lazy_static::lazy_static;
use log::debug;
-use serenity::{
- all::CreateMessage,
- futures::{
- AsyncReadExt,
- StreamExt,
- },
+use regex::{
+ Match,
+ Regex,
+};
+use serenity::all::{
+ CreateMessage,
+ Message,
};
use url::Url;
use crate::{
- audio::PlayQueue,
+ commands::songbird,
Result,
CONFIG,
};
-pub async fn currently_playing(ctx: &Context) -> bool {
- let queue_lock = {
- let data = ctx.data.read().await;
- data.get::<PlayQueue>().cloned().unwrap()
- };
+pub async fn currently_playing(ctx: &Context, msg: &Message) -> bool {
+ let (_sb, call) = songbird(ctx, msg).await.expect("no songbird");
- let play_queue = queue_lock.read().unwrap();
- play_queue.playing.is_some()
+ let call = call.lock().await;
+ call.queue().current().is_some()
}
pub async fn users_listening(ctx: &Context) -> Result<bool> {
@@ -77,7 +77,6 @@ pub async fn send_result( lazy_static! {
static ref REQUIRED_PERMS: Permissions = Permissions::EMBED_LINKS
- | Permissions::READ_MESSAGES
| Permissions::ADD_REACTIONS
| Permissions::SEND_MESSAGES
| Permissions::SEND_TTS_MESSAGES
@@ -98,3 +97,85 @@ lazy_static! { ))
.unwrap();
}
+
+pub async fn ytdl_url(uri: &str) -> Result<String> {
+ use serde_json::Value;
+ use tokio::process::Command;
+
+ lazy_static! {
+ static ref YTDL_COMMAND: String = {
+ let result = CONFIG.ytdl.clone().unwrap_or("youtube-dl".to_owned());
+ log::debug!("got ytdl: {}", result);
+
+ result
+ };
+ }
+
+ let args = [
+ "-f",
+ "webm[abr>0]/bestaudio/best",
+ "--no-playlist",
+ "--print-json",
+ "--skip-download",
+ uri,
+ ];
+
+ let out = Command::new(&*YTDL_COMMAND).args(&args).stdin(Stdio::null()).output().await?;
+
+ if !out.status.success() {
+ return Err(anyhow::anyhow!("running ytdl: {out:?}"));
+ }
+
+ let value = serde_json::from_reader(&out.stdout[..])?;
+ let mut obj = match value {
+ Value::Object(obj) => obj,
+ other => return Err(anyhow::anyhow!("ytdl output not object: {other:?}")),
+ };
+
+ match obj.remove("url") {
+ Some(v) => match v {
+ Value::String(uri) => Ok(uri),
+ other => Err(anyhow::anyhow!("url not string: {other:?}")),
+ },
+ None => Err(anyhow::anyhow!("no url")),
+ }
+}
+pub fn parse_times<A: AsRef<str>>(s: A) -> (Option<Duration>, Option<Duration>) {
+ lazy_static! {
+ static ref START_REGEX: Regex =
+ Regex::new(r"(?:start|begin(?:ning)?)\s*=?\s*(?:(?P<hours>\d+)h\s?)?(?:(?P<minutes>\d+)m\s?)?(?:(?P<seconds>\d+)s?)?").unwrap();
+
+ static ref DUR_REGEX: Regex =
+ Regex::new(r"dur(?:ation)?\s*=?\s*(?:(?P<hours>\d+)h\s?)?(?:(?P<minutes>\d+)m\s?)?(?:(?P<seconds>\d+)s?)?").unwrap();
+
+ static ref END_REGEX: Regex =
+ Regex::new(r"(?:end|term(?:inate|ination)?)\s*=?\s*(?:(?P<hours>\d+)h\s?)?(?:(?P<minutes>\d+)m\s?)?(?:(?P<seconds>\d+)s?)?").unwrap();
+ }
+
+ fn parse_match(m: Option<Match>) -> u64 {
+ m.and_then(|s| s.as_str().parse::<u64>().ok()).unwrap_or(0)
+ }
+
+ fn parse_captures<B: AsRef<str>>(r: &Regex, s: B) -> Option<Duration> {
+ r.captures(s.as_ref()).map(|capt| {
+ let hours = parse_match(capt.name("hours"));
+ let minutes = parse_match(capt.name("minutes"));
+ let seconds = parse_match(capt.name("seconds"));
+
+ let result = Duration::hours(hours as i64)
+ + Duration::minutes(minutes as i64)
+ + Duration::seconds(seconds as i64);
+
+ assert!(result >= Duration::zero());
+
+ result
+ })
+ }
+
+ let start_time = parse_captures(&START_REGEX, &s);
+ let dur = parse_captures(&DUR_REGEX, &s);
+ let end_time = parse_captures(&END_REGEX, s)
+ .or_else(|| start_time.and_then(|start| dur.map(|d| start + d)));
+
+ (start_time, end_time)
+}
|
