diff --git a/.github/workflows/cachix.yml b/.github/workflows/cachix.yml index a149045..2989d1d 100644 --- a/.github/workflows/cachix.yml +++ b/.github/workflows/cachix.yml @@ -17,7 +17,7 @@ jobs: authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' - run: nix build .#coggiebot-stable - run: cachix push coggiebot result - - run: nix build .#deploy + - run: nix build .#cache-target - run: cachix push coggiebot result # update-self: diff --git a/Cargo.lock b/Cargo.lock index 063cccd..4b8d708 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,6 +36,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "arrayvec" version = "0.7.4" @@ -182,6 +197,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits 0.2.16", + "time 0.1.45", + "wasm-bindgen", + "winapi", +] + [[package]] name = "cipher" version = "0.3.0" @@ -213,7 +243,7 @@ dependencies = [ [[package]] name = "coggiebot" -version = "1.4.13" +version = "1.4.14" dependencies = [ "mockingbird", "serenity", @@ -235,6 +265,34 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time 0.3.28", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d606d0fba62e13cf04db20536c05cb7f13673c161cb47a47a82b9b9e7d3f1daa" +dependencies = [ + "cookie", + "idna 0.2.3", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time 0.3.28", + "url", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -561,7 +619,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -702,6 +760,50 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.4.0" @@ -807,6 +909,12 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + [[package]] name = "memchr" version = "2.6.0" @@ -845,15 +953,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys", ] [[package]] name = "mockingbird" -version = "0.0.3" +version = "0.0.4" dependencies = [ "cc", + "chrono", "reqwest", "serde", "serde_json", @@ -1152,6 +1261,22 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" +dependencies = [ + "idna 0.3.0", + "psl-types", +] + [[package]] name = "quote" version = "1.0.33" @@ -1252,6 +1377,8 @@ checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ "base64 0.21.3", "bytes", + "cookie", + "cookie_store", "encoding_rs", "futures-core", "futures-util", @@ -1532,7 +1659,7 @@ dependencies = [ "serde-value", "serde_json", "static_assertions", - "time", + "time 0.3.28", "tokio", "tracing", "typemap_rev", @@ -1799,6 +1926,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "time" version = "0.3.28" @@ -2099,7 +2237,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", - "idna", + "idna 0.4.0", "percent-encoding", "serde", ] @@ -2152,6 +2290,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/crates/coggiebot/Cargo.toml b/crates/coggiebot/Cargo.toml index e5cb67e..f134453 100644 --- a/crates/coggiebot/Cargo.toml +++ b/crates/coggiebot/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "coggiebot" -version = "1.4.13" +version = "1.4.14" edition = "2021" [dependencies] @@ -35,6 +35,9 @@ mockingbird-mp3 = ["mockingbird?/wget-mp3"] # mockingbird compatiable with deep-mixer mockingbird-deemix = ["mockingbird?/deemix"] +################ +mockingbird-deemix-check = ["mockingbird?/check"] + ################ # mockingbird compatiable with spotify # mockingbird-spotify = ["mockingbird-deemix", "dep:serde", "dep:serde_derive", "dep:serde_json"] diff --git a/crates/mockingbird/Cargo.toml b/crates/mockingbird/Cargo.toml index 2868af2..2332ee3 100644 --- a/crates/mockingbird/Cargo.toml +++ b/crates/mockingbird/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mockingbird" -version = "0.0.3" +version = "0.0.4" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -14,13 +14,15 @@ tokio = {version = "1.0", default-features=false, features = ["time", "rt"]} #### serde = { version = "1.0", optional=true } serde_json = { version = "1.0", optional=true } -reqwest = { version = "0.11", optional = true } +reqwest = { version = "0.11", optional = true, features = ["cookies"]} +chrono = {version = "^0.4.26", optional = true } [build-dependencies] cc = "1" [features] -default = ["ytdl", "deemix"] +default = ["ytdl", "deemix", "check"] +check = ["dep:chrono", "dep:reqwest", "dep:serde", "dep:serde_json"] ytdl = ["songbird/yt-dlp"] deemix = ["dep:serde", "dep:serde_json"] wget-mp3 = ["dep:reqwest"] diff --git a/crates/mockingbird/src/check.rs b/crates/mockingbird/src/check.rs new file mode 100644 index 0000000..d2b61d8 --- /dev/null +++ b/crates/mockingbird/src/check.rs @@ -0,0 +1,185 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use reqwest::{cookie::Jar, Url}; +use tokio::process::Command; +use tokio::io::AsyncWriteExt; + +const USER_AGENT: &str = "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/112.0"; + +#[derive(Debug)] +pub enum ARLError { + ParseError(serde_json::Error), + HTTP(reqwest::Error), +} + +impl From for ARLError { + fn from(e: serde_json::Error) -> Self { + Self::ParseError(e) + } +} + +impl From for ARLError { + fn from(e: reqwest::Error) -> Self { + Self::HTTP(e) + } +} + +impl std::fmt::Display for ARLError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::ParseError(e) => write!(f, "Parse error: {}", e), + Self::HTTP(e) => write!(f, "HTTP error: {}", e), + } + } +} + +impl std::error::Error for ARLError {} + +#[derive(Debug)] +pub struct ExtractChecks { + pub name: String, + pub email: String, + pub explicit: String, + pub offer_name: String, + pub offer_id: i64, + pub country: String, + pub expiration: i64, + pub inscription: String, + pub default_sound_quality: String, + pub lossless: bool, + pub mobile_sq: SoundQuality, + pub tablet_sq: SoundQuality, + pub web_sq: SoundQuality, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct SoundQuality { + pub high: bool, + pub lossless: bool, + pub low: bool, + pub reality: bool, + pub standard: bool, +} + +impl ExtractChecks { + #[inline(always)] + pub fn premium(&self) -> bool { + self.offer_id > 100_000 + } + + #[inline(always)] + pub fn explicit(&self) -> bool { + self.explicit.to_lowercase() == "explicit_display" + } + + #[inline(always)] + pub fn lossless(&self) -> bool { + self.lossless + // self.mobile_sq.lossless || self.tablet_sq.lossless || self.web_sq.lossless + } + + pub async fn tabulize(&self) -> Result> { + let mut child = Command::new("column") + .arg("-t") + .arg("-s") + .arg(",") + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .spawn() + .expect("failed to spawn command"); + + fn f(x: bool) -> char { + x.then(|| '✓').unwrap_or('✗') + } + + child.stdin + .as_mut() + .expect("failed to get stdin") + .write_all( + format!( + "Quality,Mobile,Tablet,Web\nReality,{},{},{}\nLossless,{},{},{}\nHigh,{},{},{}\nStandard,{},{},{}\nLow,{},{},{}\n", + + f(self.mobile_sq.reality), + f(self.tablet_sq.reality), + f(self.web_sq.reality), + + f(self.mobile_sq.lossless), + f(self.tablet_sq.lossless), + f(self.web_sq.lossless), + + f(self.mobile_sq.high), + f(self.tablet_sq.high), + f(self.web_sq.high), + + f(self.mobile_sq.standard), + f(self.tablet_sq.standard), + f(self.web_sq.standard), + + f(self.mobile_sq.low), + f(self.tablet_sq.low), + f(self.web_sq.low), + ).as_bytes() + ) + .await + .expect("failed to write to stdin"); + return Ok(String::from_utf8(child.wait_with_output().await?.stdout)?); + } + + #[inline(always)] + pub fn dank(&self) -> bool { + self.premium() + && self.explicit() + && self.lossless() + } +} + +pub async fn get_arl_data(arl: &str) -> Result { + let url = "https://www.deezer.com".parse::().unwrap(); + let jar = Jar::default(); + jar.add_cookie_str(&format!("arl={}; Domain=deezer.com", arl), &url); + let resp = reqwest::Client::builder() + .cookie_store(true) + .cookie_provider(std::sync::Arc::new(jar)) + .build()? + .post("https://www.deezer.com/ajax/gw-light.php?method=deezer.getUserData&input=3&api_version=1.0&api_token=&cid=433085605") + .header("Origin", "https://www.deezer.com") + .header("Referer", "https://www.deezer.com/us/") + .header("User-Agent", USER_AGENT) + .body("{}") + .send() + .await?; + + Ok(resp.json().await?) +} + + +pub async fn check_arl(arl: &str) -> Result { + let data = get_arl_data(arl).await?; + + let mut name: String = String::from("Anonymous"); + let firstname = data["results"]["USER"]["FIRSTNAME"].to_string().trim().to_string(); + let blogname = data["results"]["USER"]["BLOG_NAME"].to_string().trim().to_string(); + + if !firstname.is_empty() { + name = firstname.to_string(); + } + else if !blogname.is_empty() { + name = blogname.to_string(); + } + + Ok(ExtractChecks { + name, + explicit: data["results"]["USER"]["EXPLICIT_CONTENT_LEVEL"].as_str().unwrap().to_string(), + offer_name: data["results"]["OFFER_NAME"].to_string(), + offer_id: data["results"]["OFFER_ID"].as_i64().unwrap(), + email: data["results"]["USER"]["EMAIL"].to_string(), + country: data["results"]["USER"]["OPTIONS"]["license_country"].as_str().unwrap().to_string(), + default_sound_quality: data["results"]["USER"]["OPTIONS"]["audio_quality_default_preset"].to_string(), + expiration: data["results"]["USER"]["OPTIONS"]["expiration_timestamp"].as_i64().unwrap(), + inscription: data["results"]["USER"]["INSCRIPTION_DATE"].to_string(), + lossless: data["results"]["USER"]["OPTIONS"]["mobile_lossless"].as_bool().unwrap(), + mobile_sq: dbg!(serde_json::from_value::(dbg!(data["results"]["USER"]["OPTIONS"]["mobile_sound_quality"].clone())))?, + tablet_sq: serde_json::from_value::(data["results"]["USER"]["OPTIONS"]["tablet_sound_quality"].clone())?, + web_sq: dbg!(serde_json::from_value::(dbg!(data["results"]["USER"]["OPTIONS"]["web_sound_quality"].clone())))?, + }) +} diff --git a/crates/mockingbird/src/controller.rs b/crates/mockingbird/src/controller.rs index 79871fa..09d3a8b 100644 --- a/crates/mockingbird/src/controller.rs +++ b/crates/mockingbird/src/controller.rs @@ -1,14 +1,19 @@ -use serenity::framework::standard::{ +use tokio::fs::File; +use serenity::{framework::standard::{ macros::{command, group}, CommandResult, Args, -}; +}, http::Http}; use serenity::model::channel::Message; use serenity::prelude::*; +use songbird::Call; +use songbird::input::{Input, error::Error as SongbirdError}; +use songbird::create_player; + #[group] #[commands( - deafen, join, leave, mute, skip, stop, undeafen, unmute, queue + deafen, join, leave, mute, skip, stop, undeafen, unmute, queue, arl_check, arl_raw )] struct Commands; @@ -16,14 +21,57 @@ enum Players { Ytdl, Deemix, } -use songbird::Call; -use songbird::input::error::Error as SongbirdError; -use songbird::create_player; + +fn warn_unimplemented() -> &'static str { + "This feature is not implemented." +} + +#[allow(unused_variables)] +enum HandlerError { + Songbird(SongbirdError), + NotImplemented, +} + +impl From for HandlerError { + fn from(err: SongbirdError) -> Self { + HandlerError::Songbird(err) + } +} + +impl std::fmt::Display for HandlerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Songbird(err) => write!(f, "Songbird error: {}", err), + Self::NotImplemented => write!(f, "This feature is not implemented."), + } + } +} + +/* + * Some ugly place holders for + * feature generated code. +*/ +#[cfg(feature = "deemix")] +async fn ph_deemix_player(uri: &str) -> Result +{ crate::deemix::deemix(uri).await.map_err(HandlerError::from) } + +#[cfg(not(feature = "deemix"))] +async fn ph_deemix_player(uri: &str) -> Result +{ return Err(HandlerError::NotImplemented) } + +#[cfg(feature = "ytdl")] +async fn ph_ytdl_player(uri: &str) -> Result +{ return songbird::ytdl(uri).await.map_err(HandlerError::from) } + +#[cfg(not(feature = "ytdl"))] +async fn ph_ytdl_player(uri: &str) -> Result +{ return Err(HandlerError::NotImplemented) } + impl Players { fn from_str(data : &str) -> Option { - const DEEMIX: [&'static str; 2] = ["deezer.link", "open.spotify"]; + const DEEMIX: [&'static str; 3] = ["deezer.page.link", "deezer.com", "open.spotify"]; const YTDL: [&'static str; 4] = ["youtube.com", "youtu.be", "music.youtube.com", "soundcloud.com"]; if DEEMIX.iter().any(|x|data.contains(x)) { return Some(Self::Deemix) } @@ -31,17 +79,25 @@ impl Players { else { return None } } - async fn play(&self, handler: &mut Call, uri: &str) -> Result<(), SongbirdError> + async fn play(&self, ctx: &Http, msg: &Message, handler: &mut Call, uri: &str) -> Result<(), SongbirdError> { - match self { - Self::Deemix => { - let (track, _track_handle) = create_player(crate::deemix::deemix(uri).await?); + let input = match self { + Self::Deemix => ph_deemix_player(uri).await, + Self::Ytdl => ph_ytdl_player(uri).await + }; + + match input { + Ok(input) => { + let (track, _track_handle) = create_player(input); handler.enqueue(track); } - Self::Ytdl => { - let (track, _track_handle) = create_player(songbird::ytdl(uri).await?); - handler.enqueue(track); + + Err(HandlerError::NotImplemented) => { + msg.channel_id.say(&ctx, warn_unimplemented()).await; + return Ok(()) } + + Err(HandlerError::Songbird(err)) => return Err(err), } Ok(()) @@ -77,6 +133,7 @@ async fn queue(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { let guild = msg.guild(&ctx.cache).unwrap(); let guild_id = guild.id; + let http = ctx.http.clone(); let manager = songbird::get(ctx) .await @@ -91,7 +148,7 @@ async fn queue(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { .ok_or_else(|| String::from("Failed to select extractor for URL")); match player { - Ok(player) => player.play(&mut handler, &url).await?, + Ok(player) => player.play(&http, msg, &mut handler, &url).await?, Err(e) => { check_msg( msg.channel_id @@ -401,6 +458,140 @@ async fn unmute(ctx: &Context, msg: &Message) -> CommandResult { Ok(()) } +fn santitize_arl(arl: &str) -> Result<(), ()> +{ + if arl.trim().len() == 192 && arl.chars().all(|c| c.is_ascii_hexdigit()) + { return Ok(()) } + Err(()) +} + +#[command("arl-raw")] +#[cfg(feature = "check")] +async fn arl_raw( + ctx: &Context, + msg: &Message, + mut args: Args +) -> CommandResult +{ + let mut iargs = args.iter::(); + while let Some(Ok(arl)) = iargs.next() { + if let Err(()) = santitize_arl(&arl) { + check_msg(msg.channel_id.say(&ctx.http, "Invalid ARL").await); + continue; + } + + let check = crate::check::get_arl_data(&arl).await?; + + check_msg(msg.channel_id.send_files( + &ctx.http, + vec![ + (serde_json::to_string_pretty(&check)?.as_bytes(), format!("{}.json", arl).as_str()) + ], + |m| m + ).await); + } + Ok(()) +} + +#[command("arl")] +#[cfg(feature = "check")] +async fn arl_check( + ctx: &Context, + msg: &Message, + mut args: Args +) -> CommandResult +{ + use chrono::prelude::*; + + const BLANKSPACE: &'static str = "\x20"; // 0x20 + const RED: u32 = 0x00FF0000; + const GREEN: u32 = 0x0000FF00; + const YELLOW: u32 = 0x00FFFF00; + + let mut iargs = args.iter::(); + + while let Some(Ok(arl)) = dbg!(iargs.next()) + { + if let Err(()) = santitize_arl(&arl) { + check_msg(msg.channel_id.say(&ctx.http, "Invalid ARL").await); + continue; + } + + let arl = arl.trim(); + if dbg!(arl.len()) != 192 { + check_msg( + msg.channel_id + .say(&ctx.http, "Must provide an ARL") + .await, + ); + continue + } + + let check = match crate::check::check_arl(&arl).await { + Ok(check) => check, + Err(e) => { + check_msg( + msg.channel_id + .say(&ctx.http, format!("Error: {}", e)) + .await, + ); + continue + } + }; + + let is_usa = check.country.to_ascii_lowercase() == "us"; + + let color = if check.dank() + { GREEN } + else if check.lossless() && check.explicit() + { YELLOW } + else + { RED }; + + fn checkmark(check: bool) -> &'static str { + if check { ":white_check_mark:" } + else { ":x:" } + } + + let explicit = checkmark(check.explicit()); + let lossless = checkmark(check.lossless()); + let country_checkmark = checkmark(is_usa); + let sound_quality_table = check.tabulize().await.expect("Failed to collect column -t"); + + + let naive = NaiveDateTime::from_timestamp_opt(check.expiration, 0).unwrap(); + let datetime: DateTime = DateTime::from_utc(naive, Utc); + let expiredate = datetime.format("%Y-%m-%d %H:%M:%S"); + let expire_checkmark = checkmark(datetime > Utc::now()); + + check_msg(msg.channel_id + .send_message(&ctx.http, |m| + { + let m = + m.add_embed(|e| + e.title("ARL Check") + .color(color) + .description(&arl) + .fields(vec![ + (format!("Allows Explicit: {explicit}").as_str(), BLANKSPACE, false), + (format!("Allows Lossless: {lossless}").as_str(), BLANKSPACE, false), + (format!("Country: {} {}", check.country, country_checkmark).as_str(), BLANKSPACE, false), + (format!("Inscription date: {}", check.inscription).as_str(), BLANKSPACE, false), + (format!("Expiration: {expiredate} {expire_checkmark}").as_str(), BLANKSPACE, false), + (format!("Email: {}", check.email).as_str(), BLANKSPACE, false), + (format!("Offer {} ({})", check.offer_name, check.offer_id).as_str(), BLANKSPACE, false), + (BLANKSPACE, &format!("```\n{}\n```", sound_quality_table), false), + ]) + .footer(|f| f.text(format!("**Deezer uses Mobile API.**"))) + ); + m + }).await); + + } + + Ok(()) +} + use serenity::Result as SerenityResult; /// Checks that a message successfully sent; if not, then logs why to stdout. fn check_msg(result: SerenityResult) { diff --git a/crates/mockingbird/src/deemix.rs b/crates/mockingbird/src/deemix.rs index 3e9c3e6..fcfe7d0 100644 --- a/crates/mockingbird/src/deemix.rs +++ b/crates/mockingbird/src/deemix.rs @@ -1,3 +1,6 @@ +use std::io::{BufReader, BufRead, Read}; +use std::process::ChildStderr; +use serenity::json::JsonError; use songbird::{ constants::SAMPLE_RATE_RAW, input::{ @@ -74,6 +77,27 @@ pub async fn deemix_metadata(uri: &str) -> Result, error: JsonError) -> SongbirdError { + let fault = String::from_utf8_lossy(&writebuf); + tracing::error!("TRIED PARSING: \n {}", fault); + tracing::error!("... [start] flushing buffer to logs..."); + o_vec.clear(); + + // Potentially hangs thread if EOF is never encountered + serde_read.read_to_end(&mut o_vec).unwrap(); + tracing::error!("{}", String::from_utf8_lossy(&o_vec)); + tracing::error!("... [ end ] flushed buffer to logs..."); + SongbirdError::Json { error, parsed_text: fault } +} + +#[cfg(not(feature = "debug"))] +fn handle_deemix_stream_error(writebuf: Vec, error: JsonError) -> SongbirdError { + let fault = String::from_utf8_lossy(&writebuf); + tracing::error!("TRIED PARSING: \n {}", String::from_utf8_lossy(&writebuf)); + SongbirdError::Json { error, parsed_text: fault.to_string() } +} + pub async fn deemix( uri: &str, ) -> SongbirdResult{ @@ -111,26 +135,15 @@ pub async fn _deemix( unsafe { bigpipe(deemix_out, pipesize); } let stderr = deemix.stderr.take(); + let (returned_stderr, value) = tokio::task::spawn_blocking(move || { - use std::io::{BufReader, BufRead, Read}; let mut s = stderr.unwrap(); let out: SongbirdResult = { let mut o_vec = vec![]; let mut serde_read = BufReader::new(s.by_ref()); // Newline... if let Ok(len) = serde_read.read_until(0xA, &mut o_vec) { - serde_json::from_slice(&o_vec[..len]).map_err(|err| SongbirdError::Json { - error: { - tracing::error!("TRIED PARSING: \n {}", String::from_utf8_lossy(&o_vec)); - tracing::error!("... [start] flushing buffer to logs..."); - o_vec.clear(); - serde_read.read_to_end(&mut o_vec).unwrap(); - tracing::error!("{}", String::from_utf8_lossy(&o_vec)); - tracing::error!("... [ end ] flushed buffer to logs..."); - err - }, - parsed_text: std::str::from_utf8(&o_vec).unwrap_or_default().to_string(), - }) + serde_json::from_slice(&o_vec[..len]).map_err(|e| handle_deemix_stream_error(o_vec, e)) } else { SongbirdResult::Err(SongbirdError::Metadata) } @@ -243,4 +256,4 @@ fn metadata_from_deemix_output(val: &serde_json::Value) -> Metadata sample_rate: Some(SAMPLE_RATE_RAW as u32), ..Default::default() } -} \ No newline at end of file +} diff --git a/crates/mockingbird/src/lib.rs b/crates/mockingbird/src/lib.rs index b0eb067..5e38555 100644 --- a/crates/mockingbird/src/lib.rs +++ b/crates/mockingbird/src/lib.rs @@ -1,12 +1,17 @@ - mod controller; + +#[cfg(feature = "deemix")] mod deemix; +#[cfg(feature = "check")] +mod check; + #[cfg(test)] mod testsuite; pub use controller::COMMANDS_GROUP as COMMANDS; + use serenity::client::ClientBuilder; pub async fn init(cfg: ClientBuilder) -> ClientBuilder { tracing::info!("Mockingbird initializing..."); diff --git a/flake.nix b/flake.nix index dde58d7..745f792 100644 --- a/flake.nix +++ b/flake.nix @@ -36,12 +36,19 @@ mockingbird mockingbird-ytdl mockingbird-deemix + mockingbird-deemix-check ]); coggiebot-stable = cogpkgs.mkCoggiebot { features-list = stable-features; }; + coggiebot-next = cogpkgs.mkCoggiebot { + features-list = stable-features ++ [ + cogpkgs.features.mockingbird-deemix-check + ]; + }; + non-nixos = (pkgs.callPackage ./iac/linux) { features=cogpkgs.features; }; vanilla-linux = non-nixos { @@ -80,10 +87,13 @@ packages.coggiebot-deploy = vanilla-linux; packages.default = coggiebot-stable; packages.coggiebot = coggiebot-stable; + packages.coggiebot-stable = coggiebot-stable; packages.coggiebot-stable-docker = pkgs.callPackage ./iac/coggiebot/docker.nix { coggiebot = coggiebot-stable; }; + + packages.cache-target = coggiebot-stable; })) packages; nixosModules.coggiebot = {pkgs, lib, config, ...}: diff --git a/iac/coggiebot/default.nix b/iac/coggiebot/default.nix index 0e9dcde..03fc27f 100644 --- a/iac/coggiebot/default.nix +++ b/iac/coggiebot/default.nix @@ -67,6 +67,12 @@ let }); dependencies = [ "mockingbird" ]; } + { name = "mockingbird-deemix-check"; + pkg-override = (prev: { + buildInputs = prev.buildInputs ++ [ pkgs.pkg-config pkgs.openssl ]; + }); + dependencies = [ "mockingbird" ]; + } { name = "mockingbird-ytdl"; dependencies = [ "mockingbird" ]; pkg-override = (prev: {