3 Commits

Author SHA1 Message Date
5ae4baa2a6 Bump version 2025-09-16 21:09:25 +01:00
6884adc5b2 Add some docs 2025-09-16 21:06:51 +01:00
6ade91e11b Add cron parser for start time of a reminder 2025-09-16 21:00:58 +01:00
6 changed files with 50 additions and 5 deletions

12
Cargo.lock generated
View File

@ -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.39" 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",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "reminder-rs" name = "reminder-rs"
version = "1.7.39" 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"

View File

@ -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;

View File

@ -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) => {

View File

@ -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)
}

View File

@ -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>