use std::process::Stdio; use chrono::Duration; use lazy_static::lazy_static; use log::debug; use poise::{ CreateReply, FrameworkError, }; use regex::{ Match, Regex, }; use serenity::{ all::{ CreateMessage, Message, Reaction, ReactionType, }, client::Context, model::{ id::{ ChannelId, MessageId, }, permissions::Permissions, }, }; use url::Url; use crate::{ commands::playback::songbird, PoiseContext, CONFIG, }; mod rest_vec; pub use rest_vec::*; pub async fn currently_playing(ctx: PoiseContext<'_>) -> bool { let (_sb, call) = songbird(ctx).await.expect("no songbird"); let call = call.lock().await; call.queue().current().is_some() } pub async fn users_listening(ctx: &Context) -> anyhow::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 .iter() .any(|(_, state)| state.channel_id == Some(CONFIG.discord.voice_channel())) }) .unwrap_or(false); Ok(res) } #[inline] pub fn msg(ctx: poise::Context<'_, U, E>) -> Option<&Message> { match ctx { poise::Context::Prefix(poise::PrefixContext { msg, .. }) => Some(msg), _ => None, } } #[inline] pub fn err_msg<'a, U, E>(err: &'a FrameworkError) -> Option<&'a Message> { use FrameworkError::*; if let Some(ctx) = err.ctx() { return msg(ctx); } match *err { UnknownCommand { msg, .. } | NonCommandMessage { msg, .. } | DynamicPrefix { msg, .. } => Some(msg), _ => None, } } #[inline] pub fn tts(ctx: poise::Context<'_, U, E>) -> Option { msg(ctx).map(|msg| msg.tts) } #[inline] pub fn unwrap_tts(ctx: poise::Context<'_, U, E>) -> bool { tts(ctx).unwrap_or(false) } #[inline] pub async fn send( ctx: &Context, channel: ChannelId, text: impl AsRef, tts: bool, ) -> anyhow::Result<()> { send_result(ctx, channel, text, tts).await.map(|_| ()) } #[inline] pub async fn reply( ctx: poise::Context<'_, U, E>, text: impl AsRef, ) -> Result { let handle = poise::send_reply(ctx, CreateReply::default().tts(unwrap_tts(ctx)).content(text.as_ref())) .await?; Ok(handle) } #[inline] pub async fn react(ctx: PoiseContext<'_>, react: ReactionType) -> anyhow::Result { let react = msg(ctx).ok_or_else(|| anyhow::anyhow!("elp"))?.react(ctx, react).await?; Ok(react) } pub async fn send_result( ctx: &Context, channel: ChannelId, text: impl AsRef, tts: bool, ) -> anyhow::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) -> anyhow::Result { use serde_json::Value; use tokio::process::Command; let args = [ "-f", "webm[abr>0]/bestaudio/best", "--no-playlist", "--print-json", "--skip-download", uri, ]; debug!("downloading info for uri: {uri}"); let mut command = Command::new(&*crate::config::YTDL_COMMAND); command.args(args).stdin(Stdio::null()); debug!("running command: {command:?}"); let out = command.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) }