Prepare for reminding
This commit is contained in:
parent
c1c0ced266
commit
aebbcd7815
5 changed files with 124 additions and 5 deletions
|
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
96
src/reminder.rs
Normal 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)?)
|
||||||
|
}
|
||||||
|
|
@ -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(())
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue