revamped natural to use a regex to match commands. natural now supports until parameter

This commit is contained in:
jellywx 2021-01-14 17:56:57 +00:00
parent 702743c108
commit 04232162f2
3 changed files with 178 additions and 208 deletions

View File

@ -1,11 +1,13 @@
use regex_command_attr::command; use regex_command_attr::command;
use serenity::{ use serenity::{
cache::Cache,
client::Context, client::Context,
http::CacheHttp, http::CacheHttp,
model::{ model::{
channel::GuildChannel, channel::GuildChannel,
channel::Message, channel::Message,
guild::Guild,
id::{ChannelId, GuildId, UserId}, id::{ChannelId, GuildId, UserId},
misc::Mentionable, misc::Mentionable,
webhook::Webhook, webhook::Webhook,
@ -16,14 +18,13 @@ use serenity::{
use crate::{ use crate::{
check_subscription_on_message, command_help, check_subscription_on_message, command_help,
consts::{ consts::{
CHARACTERS, DAY, HOUR, LOCAL_TIMEZONE, MAX_TIME, MINUTE, MIN_INTERVAL, PYTHON_LOCATION, CHARACTERS, DAY, HOUR, MAX_TIME, MINUTE, MIN_INTERVAL, REGEX_CHANNEL, REGEX_CHANNEL_USER,
REGEX_CHANNEL, REGEX_CHANNEL_USER, REGEX_CONTENT_SUBSTITUTION, REGEX_REMIND_COMMAND, REGEX_CONTENT_SUBSTITUTION, REGEX_NATURAL_COMMAND, REGEX_REMIND_COMMAND, THEME_COLOR,
THEME_COLOR,
}, },
framework::SendIterator, framework::SendIterator,
get_ctx_data, get_ctx_data,
models::{ChannelData, GuildData, Timer, UserData}, models::{ChannelData, GuildData, Timer, UserData},
time_parser::TimeParser, time_parser::{natural_parser, TimeParser},
}; };
use chrono::{offset::TimeZone, NaiveDateTime}; use chrono::{offset::TimeZone, NaiveDateTime};
@ -32,8 +33,6 @@ use rand::{rngs::OsRng, seq::IteratorRandom};
use sqlx::MySqlPool; use sqlx::MySqlPool;
use std::str::from_utf8;
use num_integer::Integer; use num_integer::Integer;
use std::{ use std::{
@ -45,10 +44,7 @@ use std::{
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
use regex::{Captures, RegexBuilder}; use regex::Captures;
use serenity::cache::Cache;
use serenity::model::guild::Guild;
use tokio::process::Command;
fn shorthand_displacement(seconds: u64) -> String { fn shorthand_displacement(seconds: u64) -> String {
let (days, seconds) = seconds.div_rem(&DAY); let (days, seconds) = seconds.div_rem(&DAY);
@ -1224,229 +1220,171 @@ async fn natural(ctx: &Context, msg: &Message, args: String) {
let user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap(); let user_data = UserData::from_user(&msg.author, &ctx, &pool).await.unwrap();
let send_str = lm.get(&user_data.language, "natural/send"); match REGEX_NATURAL_COMMAND.captures(&args) {
let to_str = lm.get(&user_data.language, "natural/to"); Some(captures) => {
let every_str = lm.get(&user_data.language, "natural/every"); let location_ids = if let Some(mentions) = captures.name("mentions").map(|m| m.as_str())
{
parse_mention_list(mentions)
} else {
vec![ReminderScope::Channel(msg.channel_id.into())]
};
let mut args_iter = args.splitn(2, &send_str); let expires = if let Some(expires_crop) = captures.name("expires") {
natural_parser(expires_crop.as_str(), &user_data.timezone).await
} else {
None
};
let (time_crop_opt, msg_crop_opt) = (args_iter.next(), args_iter.next().map(|m| m.trim())); let interval = if let Some(interval_crop) = captures.name("interval") {
natural_parser(interval_crop.as_str(), &user_data.timezone)
.await
.map(|i| i - since_epoch.as_secs() as i64)
} else {
None
};
if let (Some(time_crop), Some(msg_crop)) = (time_crop_opt, msg_crop_opt) { if let Some(timestamp) =
let python_call = Command::new(&*PYTHON_LOCATION) natural_parser(captures.name("time").unwrap().as_str(), &user_data.timezone).await
.arg("-c") {
.arg(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/dp.py"))) let content_res = Content::build(captures.name("msg").unwrap().as_str(), msg).await;
.arg(time_crop)
.arg(&user_data.timezone)
.arg(&*LOCAL_TIMEZONE)
.output()
.await;
if let Some(timestamp) = python_call match content_res {
.ok() Ok(mut content) => {
.map(|inner| { let offset = timestamp as u64 - since_epoch.as_secs();
if inner.status.success() {
Some(from_utf8(&*inner.stdout).unwrap().parse::<i64>().unwrap())
} else {
None
}
})
.flatten()
{
let mut location_ids = vec![ReminderScope::Channel(msg.channel_id.as_u64().to_owned())];
let mut content = msg_crop;
let mut interval = None;
if msg.guild_id.is_some() { let mut ok_locations = vec![];
let re_match = RegexBuilder::new(&format!(r#"(?:\s*)(?P<msg>.*) {} (?P<mentions>((?:<@\d+>)|(?:<@!\d+>)|(?:<#\d+>)|(?:\s+))+)$"#, to_str)) let mut err_locations = vec![];
.dot_matches_new_line(true) let mut err_types = HashSet::new();
.build()
.unwrap()
.captures(msg_crop);
if let Some(captures) = re_match { for scope in location_ids {
content = captures.name("msg").unwrap().as_str(); let res = create_reminder(
&ctx,
let mentions = captures.name("mentions").unwrap().as_str(); &pool,
msg.author.id,
location_ids = parse_mention_list(mentions); msg.guild_id,
} &scope,
} timestamp,
expires,
if check_subscription_on_message(&ctx, &msg).await { interval.clone(),
let re_match = &mut content,
RegexBuilder::new(&format!(r#"(?P<msg>.*) {} (?P<interval>.*)$"#, every_str))
.dot_matches_new_line(true)
.build()
.unwrap()
.captures(content);
if let Some(captures) = re_match {
content = captures.name("msg").unwrap().as_str();
let interval_str = captures.name("interval").unwrap().as_str();
let python_call = Command::new(&*PYTHON_LOCATION)
.arg("-c")
.arg(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/dp.py")))
.arg(&format!("1 {}", interval_str))
.arg(&*LOCAL_TIMEZONE)
.arg(&*LOCAL_TIMEZONE)
.output()
.await;
interval = python_call
.ok()
.map(|inner| {
if inner.status.success() {
Some(
from_utf8(&*inner.stdout).unwrap().parse::<i64>().unwrap()
- since_epoch.as_secs() as i64,
)
} else {
None
}
})
.flatten();
}
}
let content_res = Content::build(&content, msg).await;
match content_res {
Ok(mut content) => {
let offset = timestamp as u64 - since_epoch.as_secs();
let mut ok_locations = vec![];
let mut err_locations = vec![];
let mut err_types = HashSet::new();
for scope in location_ids {
let res = create_reminder(
&ctx,
&pool,
msg.author.id,
msg.guild_id,
&scope,
timestamp,
None,
interval,
&mut content,
)
.await;
if let Err(e) = res {
err_locations.push(scope);
err_types.insert(e);
} else {
ok_locations.push(scope);
}
}
let success_part = match ok_locations.len() {
0 => "".to_string(),
1 => lm
.get(&user_data.language, "remind/success")
.replace("{location}", &ok_locations[0].mention())
.replace("{offset}", &shorthand_displacement(offset)),
n => lm
.get(&user_data.language, "remind/success_bulk")
.replace("{number}", &n.to_string())
.replace(
"{location}",
&ok_locations
.iter()
.map(|l| l.mention())
.collect::<Vec<String>>()
.join(", "),
) )
.replace("{offset}", &shorthand_displacement(offset)), .await;
};
let error_part = format!( if let Err(e) = res {
"{}\n{}", err_locations.push(scope);
match err_locations.len() { err_types.insert(e);
} else {
ok_locations.push(scope);
}
}
let success_part = match ok_locations.len() {
0 => "".to_string(), 0 => "".to_string(),
1 => lm 1 => lm
.get(&user_data.language, "remind/issue") .get(&user_data.language, "remind/success")
.replace("{location}", &err_locations[0].mention()), .replace("{location}", &ok_locations[0].mention())
.replace("{offset}", &shorthand_displacement(offset)),
n => lm n => lm
.get(&user_data.language, "remind/issue_bulk") .get(&user_data.language, "remind/success_bulk")
.replace("{number}", &n.to_string()) .replace("{number}", &n.to_string())
.replace( .replace(
"{location}", "{location}",
&err_locations &ok_locations
.iter() .iter()
.map(|l| l.mention()) .map(|l| l.mention())
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(", "), .join(", "),
),
},
err_types
.iter()
.map(|err| match err {
ReminderError::DiscordError(s) => lm
.get(&user_data.language, err.to_response_natural())
.replace("{error}", &s),
_ => lm
.get(&user_data.language, err.to_response_natural())
.to_string(),
})
.collect::<Vec<String>>()
.join("\n")
);
let _ = msg
.channel_id
.send_message(&ctx, |m| {
m.embed(|e| {
e.title(
lm.get(&user_data.language, "remind/title")
.replace("{number}", &ok_locations.len().to_string()),
) )
.description(format!("{}\n\n{}", success_part, error_part)) .replace("{offset}", &shorthand_displacement(offset)),
.color(*THEME_COLOR) };
let error_part = format!(
"{}\n{}",
match err_locations.len() {
0 => "".to_string(),
1 => lm
.get(&user_data.language, "remind/issue")
.replace("{location}", &err_locations[0].mention()),
n => lm
.get(&user_data.language, "remind/issue_bulk")
.replace("{number}", &n.to_string())
.replace(
"{location}",
&err_locations
.iter()
.map(|l| l.mention())
.collect::<Vec<String>>()
.join(", "),
),
},
err_types
.iter()
.map(|err| match err {
ReminderError::DiscordError(s) => lm
.get(&user_data.language, err.to_response_natural())
.replace("{error}", &s),
_ => lm
.get(&user_data.language, err.to_response_natural())
.to_string(),
})
.collect::<Vec<String>>()
.join("\n")
);
let _ = msg
.channel_id
.send_message(&ctx, |m| {
m.embed(|e| {
e.title(
lm.get(&user_data.language, "remind/title")
.replace("{number}", &ok_locations.len().to_string()),
)
.description(format!("{}\n\n{}", success_part, error_part))
.color(*THEME_COLOR)
})
}) })
}) .await;
.await; }
}
Err(content_error) => {
Err(content_error) => { let _ = msg
let _ = msg .channel_id
.channel_id .send_message(ctx, |m| {
.send_message(ctx, |m| { m.embed(move |e| {
m.embed(move |e| { e.title(
e.title( lm.get(&user_data.language, "remind/title")
lm.get(&user_data.language, "remind/title") .replace("{number}", "0"),
.replace("{number}", "0"), )
) .description(
.description( lm.get(&user_data.language, content_error.to_response()),
lm.get(&user_data.language, content_error.to_response()), )
) .color(*THEME_COLOR)
.color(*THEME_COLOR) })
}) })
}) .await;
.await; }
} }
} else {
let _ = msg
.channel_id
.say(&ctx, "DEV ERROR: Failed to invoke Python")
.await;
} }
} else { }
None => {
let prefix = GuildData::prefix_from_id(msg.guild_id, &pool).await;
let resp = lm
.get(&user_data.language, "natural/no_argument")
.replace("{prefix}", &prefix);
let _ = msg let _ = msg
.channel_id .channel_id
.say(&ctx, "DEV ERROR: Failed to invoke Python") .send_message(&ctx, |m| m.embed(|e| e.description(resp)))
.await; .await;
} }
} else {
let prefix = GuildData::prefix_from_id(msg.guild_id, &pool).await;
let resp = lm
.get(&user_data.language, "natural/no_argument")
.replace("{prefix}", &prefix);
let _ = msg
.channel_id
.send_message(&ctx, |m| m.embed(|e| e.description(resp)))
.await;
} }
} }

View File

@ -50,7 +50,13 @@ lazy_static! {
pub static ref REGEX_CHANNEL_USER: Regex = Regex::new(r#"\s*<(#|@)(?:!)?(\d+)>\s*"#).unwrap(); pub static ref REGEX_CHANNEL_USER: Regex = Regex::new(r#"\s*<(#|@)(?:!)?(\d+)>\s*"#).unwrap();
pub static ref REGEX_REMIND_COMMAND: Regex = Regex::new( pub static ref REGEX_REMIND_COMMAND: Regex = Regex::new(
r#"(?P<mentions>(?:<@\d+>\s|<@!\d+>\s|<#\d+>\s)*)(?P<time>(?:(?:\d+)(?:s|m|h|d|:|/|-|))+)(?:\s+(?P<interval>(?:(?:\d+)(?:s|m|h|d|))+))?(?:\s+(?P<expires>(?:(?:\d+)(?:s|m|h|d|:|/|-|))+))?\s+(?P<content>.*)"#) r#"(?P<mentions>(?:<@\d+>\s|<@!\d+>\s|<#\d+>\s)*)(?P<time>(?:(?:\d+)(?:s|m|h|d|:|/|-|))+)(?:\s+(?P<interval>(?:(?:\d+)(?:s|m|h|d|))+))?(?:\s+(?P<expires>(?:(?:\d+)(?:s|m|h|d|:|/|-|))+))?\s+(?P<content>.*)"#
)
.unwrap();
pub static ref REGEX_NATURAL_COMMAND: Regex = Regex::new(
r#"(?P<time>.*?) send (?P<msg>.*?)(?: every (?P<interval>.*?)(?: until (?P<expires>.*?))?)?(?: to (?P<mentions>((?:<@\d+>)|(?:<@!\d+>)|(?:<#\d+>)|(?:\s+))+))?$"#
)
.unwrap(); .unwrap();
pub static ref SUBSCRIPTION_ROLES: HashSet<u64> = HashSet::from_iter( pub static ref SUBSCRIPTION_ROLES: HashSet<u64> = HashSet::from_iter(

View File

@ -2,9 +2,13 @@ use std::time::{SystemTime, UNIX_EPOCH};
use std::fmt::{Display, Formatter, Result as FmtResult}; use std::fmt::{Display, Formatter, Result as FmtResult};
use crate::consts::{LOCAL_TIMEZONE, PYTHON_LOCATION};
use chrono::TimeZone; use chrono::TimeZone;
use chrono_tz::Tz; use chrono_tz::Tz;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::str::from_utf8;
use tokio::process::Command;
#[derive(Debug)] #[derive(Debug)]
pub enum InvalidTime { pub enum InvalidTime {
@ -172,3 +176,25 @@ impl TimeParser {
Ok(full) Ok(full)
} }
} }
pub(crate) async fn natural_parser(time: &str, timezone: &str) -> Option<i64> {
Command::new(&*PYTHON_LOCATION)
.arg("-c")
.arg(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/dp.py")))
.arg(time)
.arg(timezone)
.arg(&*LOCAL_TIMEZONE)
.output()
.await
.ok()
.map(|inner| {
if inner.status.success() {
Some(from_utf8(&*inner.stdout).unwrap().parse::<i64>().unwrap())
} else {
None
}
})
.flatten()
.map(|inner| if inner < 0 { None } else { Some(inner) })
.flatten()
}