revamped natural to use a regex to match commands. natural now supports until parameter
This commit is contained in:
parent
702743c108
commit
04232162f2
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user