use chrono::Duration; use serenity::{ client::Context, model::{ id::{ ChannelId, MessageId, }, permissions::Permissions, }, }; use std::process::Stdio; use lazy_static::lazy_static; use log::debug; use regex::{ Match, Regex, }; use serenity::all::{ CreateMessage, Message, }; use url::Url; use crate::{ commands::songbird, Result, CONFIG, }; pub async fn currently_playing(ctx: &Context, msg: &Message) -> bool { let (_sb, call) = songbird(ctx, msg).await.expect("no songbird"); let call = call.lock().await; call.queue().current().is_some() } pub async fn users_listening(ctx: &Context) -> Result { let channel = CONFIG.discord.voice_channel().to_channel(&ctx).await?; let res = channel .guild() .and_then(|ch| ch.guild(&ctx)) .map(|g| { (&g.voice_states) .into_iter() .any(|(_, state)| state.channel_id == Some(CONFIG.discord.voice_channel())) }) .unwrap_or(false); Ok(res) } #[inline] pub async fn send( ctx: &Context, channel: ChannelId, text: impl AsRef, tts: bool, ) -> Result<()> { send_result(ctx, channel, text, tts).await.map(|_| ()) } pub async fn send_result( ctx: &Context, channel: ChannelId, text: impl AsRef, tts: bool, ) -> Result { let text = text.as_ref(); debug!("sending message {:?} to channel {:?} (tts: {})", text, channel, tts); let result = channel.send_message(ctx, CreateMessage::default().content(text).tts(tts)).await?; Ok(result.id) } lazy_static! { static ref REQUIRED_PERMS: Permissions = Permissions::EMBED_LINKS | Permissions::ADD_REACTIONS | Permissions::SEND_MESSAGES | Permissions::SEND_TTS_MESSAGES | Permissions::MENTION_EVERYONE | Permissions::USE_EXTERNAL_EMOJIS | Permissions::CONNECT | Permissions::SPEAK | Permissions::CHANGE_NICKNAME | Permissions::USE_VAD | Permissions::ATTACH_FILES; } lazy_static! { pub static ref OAUTH_URL: Url = Url::parse(&format!( "https://discordapp.com/api/oauth2/authorize?scope=bot&permissions={}&client_id={}", REQUIRED_PERMS.bits(), CONFIG.discord.auth.client_id, )) .unwrap(); } pub async fn ytdl_url(uri: &str) -> Result { 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>(s: A) -> (Option, Option) { lazy_static! { static ref START_REGEX: Regex = Regex::new(r"(?:start|begin(?:ning)?)\s*=?\s*(?:(?P\d+)h\s?)?(?:(?P\d+)m\s?)?(?:(?P\d+)s?)?").unwrap(); static ref DUR_REGEX: Regex = Regex::new(r"dur(?:ation)?\s*=?\s*(?:(?P\d+)h\s?)?(?:(?P\d+)m\s?)?(?:(?P\d+)s?)?").unwrap(); static ref END_REGEX: Regex = Regex::new(r"(?:end|term(?:inate|ination)?)\s*=?\s*(?:(?P\d+)h\s?)?(?:(?P\d+)m\s?)?(?:(?P\d+)s?)?").unwrap(); } fn parse_match(m: Option) -> u64 { m.and_then(|s| s.as_str().parse::().ok()).unwrap_or(0) } fn parse_captures>(r: &Regex, s: B) -> Option { 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) }