Compare commits
6 Commits
a9edcec43c
...
current
Author | SHA1 | Date | |
---|---|---|---|
5ae4baa2a6 | |||
6884adc5b2 | |||
6ade91e11b | |||
20f0fb1c20 | |||
4d14365f2b | |||
0f4df703eb |
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -524,6 +524,15 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cron-parser"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baa5650eabdaa360e2c240c2a5f544f10185b439cd76d748e44e3f28128a016b"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-channel"
|
name = "crossbeam-channel"
|
||||||
version = "0.5.13"
|
version = "0.5.13"
|
||||||
@ -2614,11 +2623,12 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reminder-rs"
|
name = "reminder-rs"
|
||||||
version = "1.7.38"
|
version = "1.7.40"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
|
"cron-parser",
|
||||||
"csv",
|
"csv",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "reminder-rs"
|
name = "reminder-rs"
|
||||||
version = "1.7.38"
|
version = "1.7.40"
|
||||||
authors = ["Jude Southworth <judesouthworth@pm.me>"]
|
authors = ["Jude Southworth <judesouthworth@pm.me>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0 only"
|
license = "AGPL-3.0 only"
|
||||||
@ -35,6 +35,7 @@ serenity = { version = "0.12", default-features = false, features = ["builder",
|
|||||||
oauth2 = "4"
|
oauth2 = "4"
|
||||||
csv = "1.2"
|
csv = "1.2"
|
||||||
sd-notify = "0.4.1"
|
sd-notify = "0.4.1"
|
||||||
|
cron-parser = "0.10"
|
||||||
|
|
||||||
[dependencies.extract_derive]
|
[dependencies.extract_derive]
|
||||||
path = "extract_derive"
|
path = "extract_derive"
|
||||||
|
@ -3,6 +3,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
|||||||
use chrono_tz::TZ_VARIANTS;
|
use chrono_tz::TZ_VARIANTS;
|
||||||
use poise::serenity_prelude::AutocompleteChoice;
|
use poise::serenity_prelude::AutocompleteChoice;
|
||||||
|
|
||||||
|
use crate::time_parser::cron_next_timestamp;
|
||||||
use crate::{models::CtxData, time_parser::natural_parser, Context};
|
use crate::{models::CtxData, time_parser::natural_parser, Context};
|
||||||
|
|
||||||
pub async fn timezone_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<String> {
|
pub async fn timezone_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<String> {
|
||||||
@ -42,7 +43,13 @@ pub async fn time_hint_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<Auto
|
|||||||
if partial.is_empty() {
|
if partial.is_empty() {
|
||||||
vec![AutocompleteChoice::new("Start typing a time...".to_string(), "now".to_string())]
|
vec![AutocompleteChoice::new("Start typing a time...".to_string(), "now".to_string())]
|
||||||
} else {
|
} else {
|
||||||
match natural_parser(partial, &ctx.timezone().await.to_string()).await {
|
let timezone = ctx.timezone().await;
|
||||||
|
let timestamp = match cron_next_timestamp(partial, timezone) {
|
||||||
|
Some(ts) => Some(ts),
|
||||||
|
None => natural_parser(partial, &timezone.to_string()).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
match timestamp {
|
||||||
Some(timestamp) => match SystemTime::now().duration_since(UNIX_EPOCH) {
|
Some(timestamp) => match SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||||
Ok(now) => {
|
Ok(now) => {
|
||||||
let diff = timestamp - now.as_secs() as i64;
|
let diff = timestamp - now.as_secs() as i64;
|
||||||
|
@ -16,7 +16,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
CtxData,
|
CtxData,
|
||||||
},
|
},
|
||||||
time_parser::natural_parser,
|
time_parser::{cron_next_timestamp, natural_parser},
|
||||||
utils::{check_guild_subscription, check_subscription},
|
utils::{check_guild_subscription, check_subscription},
|
||||||
Context, Database, Error,
|
Context, Database, Error,
|
||||||
};
|
};
|
||||||
@ -486,7 +486,10 @@ pub async fn create_reminder(
|
|||||||
let user_data = ctx.author_data().await.unwrap();
|
let user_data = ctx.author_data().await.unwrap();
|
||||||
let timezone = timezone.unwrap_or(ctx.timezone().await);
|
let timezone = timezone.unwrap_or(ctx.timezone().await);
|
||||||
|
|
||||||
let time = natural_parser(&time, &timezone.to_string()).await;
|
let time = match cron_next_timestamp(&time, timezone) {
|
||||||
|
Some(ts) => Some(ts),
|
||||||
|
None => natural_parser(&time, &timezone.to_string()).await,
|
||||||
|
};
|
||||||
|
|
||||||
match time {
|
match time {
|
||||||
Some(time) => {
|
Some(time) => {
|
||||||
|
@ -34,8 +34,10 @@ use crate::{
|
|||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref TIMEFROM_REGEX: Regex =
|
pub static ref TIMEFROM_REGEX: Regex =
|
||||||
Regex::new(r#"<<timefrom:(?P<time>\d+):(?P<format>.+)?>>"#).unwrap();
|
Regex::new(r#"<<timefrom:(?P<time>\d+):(?P<format>.+)?>>"#).unwrap();
|
||||||
pub static ref TIMENOW_REGEX: Regex =
|
pub static ref TIMENOW_REGEX: Regex = Regex::new(
|
||||||
Regex::new(r#"<<timenow:(?P<timezone>(?:\w|/|_)+):(?P<format>.+)?>>"#).unwrap();
|
r#"<<timenow(?:(?P<sign>[+-])(?P<offset>\d+))?:(?P<timezone>(?:\w|/|_)+?):(?P<format>.+?)?>>"#
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
pub static ref LOG_TO_DATABASE: bool = env::var("LOG_TO_DATABASE").map_or(true, |v| v == "1");
|
pub static ref LOG_TO_DATABASE: bool = env::var("LOG_TO_DATABASE").map_or(true, |v| v == "1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +66,7 @@ fn fmt_displacement(format: &str, seconds: u64) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn substitute(string: &str) -> String {
|
pub fn substitute(string: &str) -> String {
|
||||||
let new = TIMEFROM_REGEX.replace(string, |caps: &Captures| {
|
let new = TIMEFROM_REGEX.replace_all(string, |caps: &Captures| {
|
||||||
let final_time = caps.name("time").map(|m| m.as_str().parse::<i64>().ok()).flatten();
|
let final_time = caps.name("time").map(|m| m.as_str().parse::<i64>().ok()).flatten();
|
||||||
let format = caps.name("format").map(|m| m.as_str());
|
let format = caps.name("format").map(|m| m.as_str());
|
||||||
|
|
||||||
@ -92,12 +94,26 @@ pub fn substitute(string: &str) -> String {
|
|||||||
});
|
});
|
||||||
|
|
||||||
TIMENOW_REGEX
|
TIMENOW_REGEX
|
||||||
.replace(&new, |caps: &Captures| {
|
.replace_all(&new, |caps: &Captures| {
|
||||||
let timezone = caps.name("timezone").map(|m| m.as_str().parse::<Tz>().ok()).flatten();
|
let timezone = caps.name("timezone").map(|m| m.as_str().parse::<Tz>().ok()).flatten();
|
||||||
let format = caps.name("format").map(|m| m.as_str());
|
let format = caps.name("format").map(|m| m.as_str());
|
||||||
|
let sign = caps.name("sign").map(|m| m.as_str());
|
||||||
|
let offset = caps.name("offset").map(|m| m.as_str().parse::<i64>().ok()).flatten();
|
||||||
|
|
||||||
if let (Some(timezone), Some(format)) = (timezone, format) {
|
if let (Some(timezone), Some(format)) = (timezone, format) {
|
||||||
let now = Utc::now().with_timezone(&timezone);
|
let mut now = Utc::now().with_timezone(&timezone);
|
||||||
|
if let (Some(sign), Some(offset)) = (sign, offset) {
|
||||||
|
now = now
|
||||||
|
.checked_add_signed(TimeDelta::seconds(
|
||||||
|
offset * {
|
||||||
|
match sign {
|
||||||
|
"-" => -1,
|
||||||
|
_ => 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.unwrap_or(now)
|
||||||
|
}
|
||||||
|
|
||||||
now.format(format).to_string()
|
now.format(format).to_string()
|
||||||
} else {
|
} else {
|
||||||
|
@ -6,6 +6,8 @@ use std::{
|
|||||||
|
|
||||||
use chrono::{DateTime, Datelike, Timelike, Utc};
|
use chrono::{DateTime, Datelike, Timelike, Utc};
|
||||||
use chrono_tz::Tz;
|
use chrono_tz::Tz;
|
||||||
|
use cron_parser::parse;
|
||||||
|
use std::str::FromStr;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
use crate::consts::{LOCAL_TIMEZONE, PYTHON_LOCATION};
|
use crate::consts::{LOCAL_TIMEZONE, PYTHON_LOCATION};
|
||||||
@ -219,3 +221,7 @@ pub async fn natural_parser(time: &str, timezone: &str) -> Option<i64> {
|
|||||||
})
|
})
|
||||||
.and_then(|inner| if inner < 0 { None } else { Some(inner) })
|
.and_then(|inner| if inner < 0 { None } else { Some(inner) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn cron_next_timestamp(expr: &str, timezone: Tz) -> Option<i64> {
|
||||||
|
parse(expr, &Utc::now().with_timezone(&timezone)).ok().map(|next| next.timestamp() as i64)
|
||||||
|
}
|
||||||
|
@ -19,6 +19,24 @@
|
|||||||
Fill out the "time" and "content" fields. If you wish, press on "Optional" to view other options
|
Fill out the "time" and "content" fields. If you wish, press on "Optional" to view other options
|
||||||
for the reminder.
|
for the reminder.
|
||||||
</p>
|
</p>
|
||||||
|
<p class="subtitle">Time</p>
|
||||||
|
<p class="content">
|
||||||
|
The bot will take a "best-guess" at what time you entered. It will favour UK date formats
|
||||||
|
over US date formats (MM/DD/YY) where possible.
|
||||||
|
<br>
|
||||||
|
You can also use <code>cron</code>-like syntax to specify the time. For example, using
|
||||||
|
<code>0 0 1 * *</code> will send the reminder at midnight on the first of the next month.
|
||||||
|
For more information on cron syntax, see <a href="https://crontab.guru/">crontab.guru</a>.
|
||||||
|
<br>
|
||||||
|
<strong>Cron syntax is not repeating</strong>. Please use the optional "interval" field to specify a repetition interval.
|
||||||
|
</p>
|
||||||
|
<p class="subtitle">Pings</p>
|
||||||
|
<p class="content">
|
||||||
|
Roles and users can be pinged by including their @ mention in the "content" field.
|
||||||
|
To ping a role, the role must be set as mentionable, and the bot must have permissions to mention the role.
|
||||||
|
<br>
|
||||||
|
Please note that when using the dashboard, roles can only be pinged in the "Content..." field and not the embed fields.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -37,4 +55,40 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="hero is-small">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<p class="title">Custom formatting rules</p>
|
||||||
|
<p class="content">
|
||||||
|
Reminder content can be customized using formatting rules.
|
||||||
|
</p>
|
||||||
|
<p class="subtitle">timefrom</p>
|
||||||
|
<p class="content">
|
||||||
|
The <code>timefrom</code> formatting rule will display a formatted difference
|
||||||
|
between the time the reminder sends and a specified time.
|
||||||
|
<br>
|
||||||
|
For example, if the current time is 1755800000 (UNIX time), the format string
|
||||||
|
<code><<timefrom:1755803600>></code> would display "1 hour"
|
||||||
|
</p>
|
||||||
|
<p class="subtitle">timenow</p>
|
||||||
|
<p class="content">
|
||||||
|
The <code>timenow</code> formatting rule displays the current time or an offset
|
||||||
|
from the current time in a given timezone in a custom format.
|
||||||
|
<br>
|
||||||
|
For example, if the current time is 1755800000 (UNIX time), the format string
|
||||||
|
<code><<timenow:UTC:%H:%M:%S>></code> would display "18:13:20"
|
||||||
|
<br>
|
||||||
|
Optionally, an offset can be provided to display a time from your current time.
|
||||||
|
For example, if the current time is 1755800000 (UNIX time), the format string
|
||||||
|
<code><<timenow+120:UTC:%H:%M:%S>></code> would display "18:15:20",
|
||||||
|
or <code><<timenow-120:UTC:%H:%M:%S>></code> would display "18:11:20"
|
||||||
|
<br>
|
||||||
|
You can use this feature alongside Discord's timestamp formatting. The following
|
||||||
|
will show the text "in 2 minutes" for all users as a Discord timestamp:
|
||||||
|
<code><t:<<timenow+120:UTC:%s>>:R></code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Reference in New Issue
Block a user