diff options
Diffstat (limited to 'src/util/mod.rs')
| -rw-r--r-- | src/util/mod.rs | 232 |
1 files changed, 232 insertions, 0 deletions
diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..a0105ac --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,232 @@ +use std::process::Stdio; + +use chrono::Duration; +use lazy_static::lazy_static; +use log::debug; +use poise::CreateReply; +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, + Result, + CONFIG, +}; + +mod rest_vec; + +pub use rest_vec::RestVec; + +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) -> Result<bool> { + 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: PoiseContext<'_>) -> Option<&Message> { + match ctx { + PoiseContext::Prefix(poise::PrefixContext { + msg, + .. + }) => Some(msg), + _ => None, + } +} + +#[inline] +pub fn tts(ctx: PoiseContext<'_>) -> Option<bool> { + msg(ctx).map(|msg| msg.tts) +} + +#[inline] +pub fn unwrap_tts(ctx: PoiseContext<'_>) -> bool { + tts(ctx).unwrap_or(false) +} + +#[inline] +pub async fn send( + ctx: &Context, + channel: ChannelId, + text: impl AsRef<str>, + tts: bool, +) -> Result<()> { + send_result(ctx, channel, text, tts).await.map(|_| ()) +} + +#[inline] +pub async fn reply(ctx: PoiseContext<'_>, text: impl AsRef<str>) -> Result<poise::ReplyHandle> { + 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) -> Result<Reaction> { + 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<str>, + tts: bool, +) -> Result<MessageId> { + 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<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()); + debug!("got ytdl: {}", result); + + result + }; + } + + 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(&*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<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) +} |
