aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock211
-rw-r--r--Cargo.toml5
-rw-r--r--src/audio/mod.rs47
-rw-r--r--src/audio/play_queue.rs260
-rw-r--r--src/audio/timeutil.rs202
-rw-r--r--src/audio/ytdl.rs62
-rw-r--r--src/bot.rs48
-rw-r--r--src/commands/help.rs23
-rw-r--r--src/commands/meme/create.rs84
-rw-r--r--src/commands/meme/history.rs216
-rw-r--r--src/commands/meme/invoke.rs18
-rw-r--r--src/commands/meme/mod.rs61
-rw-r--r--src/commands/mod.rs32
-rw-r--r--src/commands/playback.rs215
-rw-r--r--src/commands/roll.rs6
-rw-r--r--src/commands/sound_levels.rs166
-rw-r--r--src/commands/today/mod.rs56
-rw-r--r--src/config.rs16
-rw-r--r--src/db/mod.rs315
-rw-r--r--src/db/models.rs132
-rw-r--r--src/game.rs80
-rw-r--r--src/main.rs19
-rw-r--r--src/util.rs111
23 files changed, 853 insertions, 1532 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 4e4cf4e..bca786f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 34a33a1..bb6da5c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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()),
- }
-}
diff --git a/src/bot.rs b/src/bot.rs
index b3db4e9..2c9f73b 100644
--- a/src/bot.rs
+++ b/src/bot.rs
@@ -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)
+}