Compare commits
5 commits
7dc6eb305d
...
e0b0ae4bc8
| Author | SHA1 | Date | |
|---|---|---|---|
| e0b0ae4bc8 | |||
| 9d8d77a356 | |||
| 6c58cd1774 | |||
| ceff3608c8 | |||
| 64d28bb868 |
9 changed files with 184 additions and 128 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -217,6 +217,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"forgejo-api",
|
"forgejo-api",
|
||||||
|
"futures",
|
||||||
"jiff",
|
"jiff",
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
||||||
|
|
@ -17,3 +17,4 @@ serde_json = "1.0.140"
|
||||||
tokio = { version = "1.45.0", features = ["full"] }
|
tokio = { version = "1.45.0", features = ["full"] }
|
||||||
time = "0.3.41"
|
time = "0.3.41"
|
||||||
jiff = "0.2.22"
|
jiff = "0.2.22"
|
||||||
|
futures = "0.3.32"
|
||||||
|
|
|
||||||
138
src/collect.rs
138
src/collect.rs
|
|
@ -3,51 +3,149 @@ use crate::util;
|
||||||
|
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use forgejo_api::structs::Issue;
|
use forgejo_api::structs::Issue;
|
||||||
|
use futures::stream::StreamExt as _;
|
||||||
|
use futures::stream::TryStreamExt as _;
|
||||||
|
|
||||||
pub async fn collect_tasks(ctx: &crate::Context) -> anyhow::Result<Vec<task::Task>> {
|
pub async fn collect_tasks(ctx: &crate::Context) -> anyhow::Result<Vec<task::Task>> {
|
||||||
let issues = list_all_issues(ctx).await?;
|
let issues = list_all_issues(ctx).await?;
|
||||||
|
|
||||||
let tasks = issues
|
let mut tasks = futures::stream::iter(issues)
|
||||||
.into_iter()
|
.map(|issue| async move {
|
||||||
.map(|issue| {
|
task_from_issue(ctx, &issue).await.with_context(|| {
|
||||||
task_from_issue(&issue).with_context(|| {
|
|
||||||
format!(
|
format!(
|
||||||
"Error while converting issue #{} to task",
|
"Error while converting issue #{} to task",
|
||||||
issue.number.unwrap_or(-1)
|
issue.number.unwrap_or(-1)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
.buffer_unordered(8)
|
||||||
|
.try_collect::<Vec<task::Task>>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tasks.sort_by_key(|t| t.issue_number);
|
||||||
|
|
||||||
Ok(tasks)
|
Ok(tasks)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn task_from_issue(issue: &Issue) -> anyhow::Result<task::Task> {
|
async fn task_from_issue(ctx: &crate::Context, issue: &Issue) -> anyhow::Result<task::Task> {
|
||||||
Ok(task::Task {
|
let task = task::Task {
|
||||||
issue_number: issue.get_number()?,
|
issue_number: issue.get_number()?,
|
||||||
title: issue.get_title()?,
|
title: issue.get_title()?,
|
||||||
state: task_state_from_issue(issue)?,
|
state: task_state_from_issue(ctx, issue).await?,
|
||||||
recurring: task_recurring_from_issue_labels(issue)?,
|
recurring: task_recurring_from_issue_labels(issue)?,
|
||||||
|
};
|
||||||
|
log::debug!(
|
||||||
|
"\
|
||||||
|
Collected Task #{} — {:?}
|
||||||
|
- State: {:?}
|
||||||
|
- Recurring: {:?}",
|
||||||
|
task.issue_number,
|
||||||
|
task.title,
|
||||||
|
task.state,
|
||||||
|
task.recurring,
|
||||||
|
);
|
||||||
|
Ok(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn task_state_from_issue(ctx: &crate::Context, issue: &Issue) -> anyhow::Result<task::State> {
|
||||||
|
match issue.get_state()? {
|
||||||
|
forgejo_api::structs::StateType::Open => {
|
||||||
|
if issue.due_date.is_some() {
|
||||||
|
Ok(task::State::Scheduled(
|
||||||
|
issue_get_schedule_info(ctx, issue).await?,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(task::State::Open)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
forgejo_api::structs::StateType::Closed => Ok(task::State::Completed {
|
||||||
|
date: util::time_to_jiff(
|
||||||
|
issue
|
||||||
|
.closed_at
|
||||||
|
.context("Closed issue without a closed_at date")?,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn issue_get_schedule_info(
|
||||||
|
ctx: &crate::Context,
|
||||||
|
issue: &Issue,
|
||||||
|
) -> anyhow::Result<task::ScheduledInfo> {
|
||||||
|
let due_date = util::time_to_jiff(
|
||||||
|
issue
|
||||||
|
.due_date
|
||||||
|
.expect("schedule info for task without due_date"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let issue_number = issue.number.context("issue without number")?;
|
||||||
|
|
||||||
|
let mut timeline = ctx
|
||||||
|
.forgejo
|
||||||
|
.issue_get_comments_and_timeline(
|
||||||
|
&ctx.owner,
|
||||||
|
&ctx.repo,
|
||||||
|
issue_number,
|
||||||
|
forgejo_api::structs::IssueGetCommentsAndTimelineQuery {
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.all()
|
||||||
|
.await
|
||||||
|
.context("Failed to fetch timeline")?;
|
||||||
|
|
||||||
|
// Should not be necessary, but let's be safe.
|
||||||
|
timeline.sort_by_key(|event| event.created_at);
|
||||||
|
|
||||||
|
let mut start_date =
|
||||||
|
util::time_to_jiff(issue.created_at.context("no created_at date for issue")?);
|
||||||
|
|
||||||
|
let mut has_reminder = None;
|
||||||
|
let mut has_urgent_reminder = None;
|
||||||
|
for event in timeline.iter().rev() {
|
||||||
|
match (event.r#type.as_deref(), &event.body) {
|
||||||
|
(Some("reopen"), _) => {
|
||||||
|
// When we find the event where the issue was last reopened, we stop.
|
||||||
|
start_date = created_at_time(event)?;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
(Some("comment"), Some(body)) => {
|
||||||
|
if body.contains(crate::reminder::SIGNATURE_URGENT) {
|
||||||
|
has_urgent_reminder = Some(created_at_time(event)?);
|
||||||
|
} else if body.contains(crate::reminder::SIGNATURE_BASIC) {
|
||||||
|
has_reminder = Some(created_at_time(event)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ignore all other events.
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let reminded = if let Some(date) = has_urgent_reminder {
|
||||||
|
task::ReminderState::RemindedUrgently { date }
|
||||||
|
} else if let Some(date) = has_reminder {
|
||||||
|
task::ReminderState::Reminded { date }
|
||||||
|
} else {
|
||||||
|
task::ReminderState::NotReminded
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(task::ScheduledInfo {
|
||||||
|
due_date,
|
||||||
|
start_date,
|
||||||
|
reminded,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn task_state_from_issue(issue: &Issue) -> anyhow::Result<task::State> {
|
fn created_at_time(ev: &forgejo_api::structs::TimelineComment) -> anyhow::Result<jiff::Zoned> {
|
||||||
match issue.get_state()? {
|
Ok(util::time_to_jiff(ev.created_at.with_context(|| {
|
||||||
forgejo_api::structs::StateType::Open => Ok(task::State::Open {
|
format!("Timeline event {:?} without created_at time", ev.r#type,)
|
||||||
due: issue.due_date.map(util::time_to_jiff),
|
})?))
|
||||||
}),
|
|
||||||
forgejo_api::structs::StateType::Closed => Ok(task::State::Completed {
|
|
||||||
date: util::time_to_jiff(issue
|
|
||||||
.closed_at
|
|
||||||
.context("Closed issue without a closed_at date")?),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn task_recurring_from_issue_labels(issue: &Issue) -> anyhow::Result<Option<task::Recurring>> {
|
fn task_recurring_from_issue_labels(issue: &Issue) -> anyhow::Result<Option<task::Recurring>> {
|
||||||
let labels = issue.get_label_names()?;
|
let labels = issue.get_label_names()?;
|
||||||
let Some(recurring_label) = get_recurring_label(&labels)? else {
|
let Some(recurring_label) = get_recurring_label(&labels)? else {
|
||||||
return Ok(None)
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
|
||||||
let interval = match recurring_label {
|
let interval = match recurring_label {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ pub struct Context {
|
||||||
/// URL of the repository page
|
/// URL of the repository page
|
||||||
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
|
||||||
|
#[expect(unused)]
|
||||||
pub repo_auth_url: url::Url,
|
pub repo_auth_url: url::Url,
|
||||||
|
|
||||||
/// Timestamp "now" to be used in comparisons
|
/// Timestamp "now" to be used in comparisons
|
||||||
|
|
@ -37,7 +38,7 @@ impl Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_from_dev_fallback() -> anyhow::Result<Self> {
|
pub fn new_from_dev_fallback() -> anyhow::Result<Self> {
|
||||||
let mut f = std::fs::File::open(&dev_environment_path()?)
|
let f = std::fs::File::open(&dev_environment_path()?)
|
||||||
.context("Failed to open dev environment config")?;
|
.context("Failed to open dev environment config")?;
|
||||||
|
|
||||||
let config: DevEnvironment =
|
let config: DevEnvironment =
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
#![allow(unused)]
|
|
||||||
|
|
||||||
mod collect;
|
mod collect;
|
||||||
mod context;
|
mod context;
|
||||||
mod reminder;
|
mod reminder;
|
||||||
|
|
|
||||||
128
src/reminder.rs
128
src/reminder.rs
|
|
@ -1,5 +1,4 @@
|
||||||
use crate::task;
|
use crate::task;
|
||||||
use crate::util;
|
|
||||||
|
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
|
|
||||||
|
|
@ -19,37 +18,43 @@ enum ReminderType {
|
||||||
pub async fn remind_all_tasks(ctx: &crate::Context, tasks: &[task::Task]) -> anyhow::Result<()> {
|
pub async fn remind_all_tasks(ctx: &crate::Context, tasks: &[task::Task]) -> anyhow::Result<()> {
|
||||||
let mut reminded = 0;
|
let mut reminded = 0;
|
||||||
for task in tasks {
|
for task in tasks {
|
||||||
let Some(due_date) = task.state.due_date_if_open() else {
|
let task::State::Scheduled(info) = &task.state else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let batching_interval = find_batching_interval(ctx, due_date, task);
|
let batching_interval = find_batching_interval(task, info);
|
||||||
log::debug!("Reminding {task} with interval {batching_interval:?}.");
|
log::debug!("Reminder interval for {task} is {batching_interval:?}.");
|
||||||
|
|
||||||
if !is_time_to_remind(ctx, due_date, batching_interval) {
|
if !is_time_to_remind(ctx, &info.due_date, batching_interval) {
|
||||||
log::debug!("Not yet time, skipping.");
|
log::debug!("Not yet time to remind, skipping.");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_overdue(ctx, due_date) {
|
if is_overdue(ctx, &info.due_date) {
|
||||||
log::debug!("Task {task} is already overdue!");
|
log::debug!("Task {task} is already overdue!");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((reminded_date, reminder_type)) = check_task_reminded(ctx, task).await? {
|
match &info.reminded {
|
||||||
if reminder_type == ReminderType::Urgent {
|
task::ReminderState::RemindedUrgently { .. } => {
|
||||||
log::debug!("Was already reminded urgently, skipping.");
|
log::debug!("Was already reminded urgently, skipping.");
|
||||||
} else if &reminded_date < due_date && is_overdue(ctx, due_date) {
|
|
||||||
log::info!("Task {task} is now overdue, reminding again urgently...");
|
|
||||||
remind_task(ctx, task, ReminderType::Urgent, batching_interval).await?;
|
|
||||||
} else {
|
|
||||||
log::debug!("Was already reminded, skipping.");
|
|
||||||
}
|
}
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("Reminding {task} ...");
|
task::ReminderState::Reminded { date } => {
|
||||||
remind_task(ctx, task, ReminderType::Basic, batching_interval).await?;
|
if date < info.due_date && is_overdue(ctx, &info.due_date) {
|
||||||
reminded += 1;
|
log::info!("Task {task} is now overdue, reminding again urgently...");
|
||||||
|
remind_task(ctx, task, ReminderType::Urgent, batching_interval).await?;
|
||||||
|
reminded += 1;
|
||||||
|
} else {
|
||||||
|
log::debug!("Was already reminded, skipping.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task::ReminderState::NotReminded => {
|
||||||
|
log::info!("Reminding {task} ...");
|
||||||
|
remind_task(ctx, task, ReminderType::Basic, batching_interval).await?;
|
||||||
|
reminded += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if reminded == 0 {
|
if reminded == 0 {
|
||||||
|
|
@ -61,64 +66,8 @@ pub async fn remind_all_tasks(ctx: &crate::Context, tasks: &[task::Task]) -> any
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
const SIGNATURE_BASIC: &'static str = "<small>\\[TaskBot Comment\\]</small>";
|
pub const SIGNATURE_BASIC: &'static str = "<small>\\[TaskBot Comment\\]</small>";
|
||||||
const SIGNATURE_URGENT: &'static str = "<small>\\[TaskBot Comment (Urgent)\\]</small>";
|
pub const SIGNATURE_URGENT: &'static str = "<small>\\[TaskBot Comment (Urgent)\\]</small>";
|
||||||
|
|
||||||
async fn check_task_reminded(
|
|
||||||
ctx: &crate::Context,
|
|
||||||
task: &task::Task,
|
|
||||||
) -> anyhow::Result<Option<(jiff::Zoned, ReminderType)>> {
|
|
||||||
let mut timeline = ctx
|
|
||||||
.forgejo
|
|
||||||
.issue_get_comments_and_timeline(
|
|
||||||
&ctx.owner,
|
|
||||||
&ctx.repo,
|
|
||||||
task.issue_number.into(),
|
|
||||||
forgejo_api::structs::IssueGetCommentsAndTimelineQuery {
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.all()
|
|
||||||
.await
|
|
||||||
.with_context(|| format!("Failed to fetch timeline for {task}"))?;
|
|
||||||
|
|
||||||
// Should not be necessary, but let's be safe.
|
|
||||||
timeline.sort_by_key(|event| event.created_at);
|
|
||||||
|
|
||||||
let mut has_reminder = None;
|
|
||||||
let mut has_urgent_reminder = None;
|
|
||||||
for event in timeline.iter().rev() {
|
|
||||||
match (event.r#type.as_deref(), &event.body) {
|
|
||||||
(Some("reopen"), _) => {
|
|
||||||
// When we find the event where the issue was last reopened, we stop.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
(Some("comment"), Some(body)) => {
|
|
||||||
if body.contains(SIGNATURE_URGENT) {
|
|
||||||
log::debug!("Found urgent reminder for issue #{}.", task.issue_number);
|
|
||||||
has_urgent_reminder = Some(created_at_time(event)?);
|
|
||||||
} else if body.contains(SIGNATURE_BASIC) {
|
|
||||||
log::debug!("Found reminder for issue #{}.", task.issue_number);
|
|
||||||
has_reminder = Some(created_at_time(event)?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Ignore all other events.
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = has_urgent_reminder
|
|
||||||
.map(|t| (t, ReminderType::Urgent))
|
|
||||||
.or_else(|| has_reminder.map(|t| (t, ReminderType::Basic)));
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn created_at_time(ev: &forgejo_api::structs::TimelineComment) -> anyhow::Result<jiff::Zoned> {
|
|
||||||
Ok(util::time_to_jiff(ev.created_at.with_context(|| {
|
|
||||||
format!("Timeline event {:?} without created_at time", ev.r#type,)
|
|
||||||
})?))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn remind_task(
|
async fn remind_task(
|
||||||
ctx: &crate::Context,
|
ctx: &crate::Context,
|
||||||
|
|
@ -126,7 +75,7 @@ async fn remind_task(
|
||||||
reminder_type: ReminderType,
|
reminder_type: ReminderType,
|
||||||
batching_interval: BatchingInterval,
|
batching_interval: BatchingInterval,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let mut body = match (reminder_type, batching_interval) {
|
let body = match (reminder_type, batching_interval) {
|
||||||
(ReminderType::Basic, BatchingInterval::Monthly) => format!(
|
(ReminderType::Basic, BatchingInterval::Monthly) => format!(
|
||||||
"\
|
"\
|
||||||
Beep boop. This task is due this month, go take care of it!"
|
Beep boop. This task is due this month, go take care of it!"
|
||||||
|
|
@ -168,13 +117,7 @@ Hello again. This task is overdue, please take care of it ASAP!"
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_batching_interval(
|
fn find_batching_interval(task: &task::Task, info: &task::ScheduledInfo) -> BatchingInterval {
|
||||||
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 {
|
if let Some(recurring) = &task.recurring {
|
||||||
match &recurring.interval {
|
match &recurring.interval {
|
||||||
task::RecurringInterval::Months(_) => BatchingInterval::Monthly,
|
task::RecurringInterval::Months(_) => BatchingInterval::Monthly,
|
||||||
|
|
@ -185,12 +128,19 @@ fn find_batching_interval(
|
||||||
// For tasks that are not recurring, the batching interval is determined based on how
|
// For tasks that are not recurring, the batching interval is determined based on how
|
||||||
// far in the future the task is due.
|
// far in the future the task is due.
|
||||||
|
|
||||||
let weeks_until_due = time_until_due
|
let task_duration = &info.due_date - &info.start_date;
|
||||||
.total((jiff::Unit::Week, jiff::SpanRelativeTo::days_are_24_hours()))
|
|
||||||
|
let task_weeks = task_duration
|
||||||
|
.total((jiff::Unit::Week, &info.start_date))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
if weeks_until_due >= 3. {
|
|
||||||
|
let task_months = task_duration
|
||||||
|
.total((jiff::Unit::Month, &info.start_date))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if task_months >= 1. {
|
||||||
BatchingInterval::Monthly
|
BatchingInterval::Monthly
|
||||||
} else if weeks_until_due >= 1. {
|
} else if task_weeks >= 1. {
|
||||||
BatchingInterval::Weekly
|
BatchingInterval::Weekly
|
||||||
} else {
|
} else {
|
||||||
BatchingInterval::OnTheDay
|
BatchingInterval::OnTheDay
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,13 @@ 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(_) } => {
|
task::State::Scheduled(_) => {
|
||||||
log::debug!("Task {task} is already scheduled. No action.");
|
log::debug!("Task {task} is already scheduled. No action.");
|
||||||
continue
|
continue;
|
||||||
},
|
}
|
||||||
|
|
||||||
// Invalid state, will warn about this
|
// Invalid state, will warn about this
|
||||||
task::State::Open { due: None } => {
|
task::State::Open => {
|
||||||
log::warn!("Task {task} is recurring but has no due date. Scheduling from today.");
|
log::warn!("Task {task} is recurring but has no due date. Scheduling from today.");
|
||||||
&ctx.timestamp
|
&ctx.timestamp
|
||||||
}
|
}
|
||||||
|
|
|
||||||
28
src/task.rs
28
src/task.rs
|
|
@ -15,22 +15,28 @@ pub struct Task {
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum State {
|
pub enum State {
|
||||||
/// The task is open and pending completion.
|
/// The task is open but has no due date.
|
||||||
///
|
Open,
|
||||||
/// An optional due date may be present.
|
|
||||||
Open { due: Option<jiff::Zoned> },
|
/// The task is scheduled and waiting for completion.
|
||||||
|
Scheduled(ScheduledInfo),
|
||||||
|
|
||||||
/// The task has been completed at the specified time.
|
/// The task has been completed at the specified time.
|
||||||
Completed { date: jiff::Zoned },
|
Completed { date: jiff::Zoned },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
#[derive(Debug, Clone)]
|
||||||
pub fn due_date_if_open(&self) -> Option<&jiff::Zoned> {
|
pub struct ScheduledInfo {
|
||||||
match self {
|
pub due_date: jiff::Zoned,
|
||||||
State::Open { due } => due.as_ref(),
|
pub start_date: jiff::Zoned,
|
||||||
_ => None,
|
pub reminded: ReminderState,
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum ReminderState {
|
||||||
|
NotReminded,
|
||||||
|
Reminded { date: jiff::Zoned },
|
||||||
|
RemindedUrgently { date: jiff::Zoned },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
pub fn time_to_jiff(t: time::OffsetDateTime) -> jiff::Zoned {
|
pub fn time_to_jiff(t: time::OffsetDateTime) -> jiff::Zoned {
|
||||||
let tz = jiff::tz::TimeZone::fixed(jiff::tz::offset(t.offset().whole_hours()));
|
let tz = jiff::tz::TimeZone::fixed(jiff::tz::offset(t.offset().whole_hours()));
|
||||||
|
|
||||||
jiff::Timestamp::new(t.unix_timestamp(), 0).unwrap()
|
jiff::Timestamp::new(t.unix_timestamp(), 0)
|
||||||
|
.unwrap()
|
||||||
.to_zoned(tz)
|
.to_zoned(tz)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue