Prepare for reminding

This commit is contained in:
Rahix 2026-03-02 08:46:52 +01:00
parent c1c0ced266
commit aebbcd7815
5 changed files with 124 additions and 5 deletions

View file

@ -13,6 +13,9 @@ pub struct Context {
pub repo_url: url::Url, pub repo_url: url::Url,
/// URL of the repository with authentication information attached /// URL of the repository with authentication information attached
pub repo_auth_url: url::Url, pub repo_auth_url: url::Url,
/// Timestamp "now" to be used in comparisons
pub timestamp: jiff::Zoned,
} }
impl Context { impl Context {
@ -67,12 +70,15 @@ impl Context {
let forgejo = forgejo_api::Forgejo::new(forgejo_api::Auth::Token(token), server_url) let forgejo = forgejo_api::Forgejo::new(forgejo_api::Auth::Token(token), server_url)
.context("Could not create Forgejo API access object")?; .context("Could not create Forgejo API access object")?;
let timestamp = jiff::Zoned::now();
Ok(Self { Ok(Self {
forgejo, forgejo,
owner: owner.to_owned(), owner: owner.to_owned(),
repo: repo.to_owned(), repo: repo.to_owned(),
repo_url, repo_url,
repo_auth_url, repo_auth_url,
timestamp,
}) })
} }
} }

View file

@ -1,10 +1,11 @@
#![allow(unused)] #![allow(unused)]
mod task;
mod collect;
mod ci_meta; mod ci_meta;
mod collect;
mod context; mod context;
mod reminder;
mod scheduler; mod scheduler;
mod task;
mod util; mod util;
use context::Context; use context::Context;
@ -16,6 +17,8 @@ async fn run() -> anyhow::Result<()> {
scheduler::reschedule_recurring_tasks(&ctx, &tasks).await?; scheduler::reschedule_recurring_tasks(&ctx, &tasks).await?;
reminder::remind_due_tasks(&ctx, &tasks).await?;
Ok(()) Ok(())
} }

96
src/reminder.rs Normal file
View file

@ -0,0 +1,96 @@
use crate::task;
use crate::util;
use anyhow::Context as _;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BatchingInterval {
Monthly,
Weekly,
OnTheDay,
}
pub async fn remind_due_tasks(ctx: &crate::Context, tasks: &[task::Task]) -> anyhow::Result<()> {
for task in tasks {
let Some(due_date) = task.state.due_date_if_open() else {
continue;
};
let batching_interval = find_batching_interval(ctx, due_date, task);
log::debug!("Reminding {task} with interval {batching_interval:?}.");
if !is_time_to_remind(ctx, due_date, batching_interval) {
log::debug!("Not yet time, skipping.");
continue;
}
if is_overdue(ctx, due_date) {
log::info!("Task {task} is already overdue!");
}
log::warn!("TODO: Remind {task}");
}
Ok(())
}
fn find_batching_interval(
ctx: &crate::Context,
due_date: &jiff::Zoned,
task: &task::Task,
) -> BatchingInterval {
let time_until_due = due_date - &ctx.timestamp;
if let Some(recurring) = &task.recurring {
match &recurring.interval {
task::RecurringInterval::Months(_) => BatchingInterval::Monthly,
task::RecurringInterval::Weeks(_) => BatchingInterval::Weekly,
task::RecurringInterval::Days(_) => BatchingInterval::OnTheDay,
}
} else {
// For tasks that are not recurring, the batching interval is determined based on how
// far in the future the task is due.
let weeks_until_due = time_until_due
.total((jiff::Unit::Week, jiff::SpanRelativeTo::days_are_24_hours()))
.unwrap();
if weeks_until_due >= 3. {
BatchingInterval::Monthly
} else if weeks_until_due >= 1. {
BatchingInterval::Weekly
} else {
BatchingInterval::OnTheDay
}
}
}
fn is_time_to_remind(
ctx: &crate::Context,
due_date: &jiff::Zoned,
batching_interval: BatchingInterval,
) -> bool {
let batch_time = match batching_interval {
BatchingInterval::Monthly => due_date.first_of_month().unwrap(),
BatchingInterval::Weekly => start_of_week(due_date).unwrap(),
BatchingInterval::OnTheDay => due_date.clone(),
};
let batch_time = batch_time
.round(
jiff::ZonedRound::new()
.smallest(jiff::Unit::Day)
.mode(jiff::RoundMode::Floor),
)
.unwrap();
ctx.timestamp >= batch_time
}
fn is_overdue(ctx: &crate::Context, due_date: &jiff::Zoned) -> bool {
&ctx.timestamp >= due_date
}
fn start_of_week(t: &jiff::Zoned) -> anyhow::Result<jiff::Zoned> {
Ok(t.tomorrow()?
.nth_weekday(-1, jiff::civil::Weekday::Monday)?)
}

View file

@ -15,12 +15,15 @@ pub async fn reschedule_recurring_tasks(
let completed_date = match &task.state { let completed_date = match &task.state {
// Already scheduled // Already scheduled
task::State::Open { due: Some(_) } => continue, task::State::Open { due: Some(_) } => {
log::debug!("Task {task} is already scheduled. No action.");
continue
},
// Invalid state, will warn about this // Invalid state, will warn about this
task::State::Open { due: None } => { task::State::Open { due: None } => {
log::warn!("Task {task} is recurring but has no due date. Updating from today."); log::warn!("Task {task} is recurring but has no due date. Scheduling from today.");
&jiff::Zoned::now() &ctx.timestamp
} }
// Need scheduling // Need scheduling
@ -36,6 +39,8 @@ pub async fn reschedule_recurring_tasks(
if rescheduled == 0 { if rescheduled == 0 {
log::info!("No tasks need rescheduling."); log::info!("No tasks need rescheduling.");
} else {
log::debug!("Rescheduled {rescheduled} tasks.");
} }
Ok(()) Ok(())

View file

@ -24,6 +24,15 @@ pub enum State {
Completed { date: jiff::Zoned }, Completed { date: jiff::Zoned },
} }
impl State {
pub fn due_date_if_open(&self) -> Option<&jiff::Zoned> {
match self {
State::Open { due } => due.as_ref(),
_ => None,
}
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Recurring { pub struct Recurring {
pub interval: RecurringInterval, pub interval: RecurringInterval,