TaskBot/src/collect.rs
2026-03-01 18:18:49 +01:00

159 lines
5.8 KiB
Rust

use crate::task;
use crate::util;
use anyhow::Context as _;
use forgejo_api::structs::Issue;
pub async fn collect_tasks(ctx: &crate::Context) -> anyhow::Result<Vec<task::Task>> {
let issues = list_all_issues(ctx).await?;
let tasks = issues
.into_iter()
.map(|issue| {
task_from_issue(&issue).with_context(|| {
format!(
"Error while converting issue #{} to task",
issue.number.unwrap_or(-1)
)
})
})
.collect::<anyhow::Result<Vec<_>>>()?;
Ok(tasks)
}
fn task_from_issue(issue: &Issue) -> anyhow::Result<task::Task> {
Ok(task::Task {
issue_number: issue.get_number()?,
title: issue.get_title()?,
state: task_state_from_issue(issue)?,
recurring: task_recurring_from_issue_labels(issue)?,
})
}
fn task_state_from_issue(issue: &Issue) -> anyhow::Result<task::State> {
match issue.get_state()? {
forgejo_api::structs::StateType::Open => Ok(task::State::Open {
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>> {
let labels = issue.get_label_names()?;
let Some(recurring_label) = get_recurring_label(&labels)? else {
return Ok(None)
};
let interval = match recurring_label {
// Months
"Every Month" => task::RecurringInterval::Months(1),
"Every 2 Months" => task::RecurringInterval::Months(2),
"Every 3 Months" => task::RecurringInterval::Months(3),
"Every 4 Months" => task::RecurringInterval::Months(4),
"Every 5 Months" => task::RecurringInterval::Months(5),
"Every 6 Months" => task::RecurringInterval::Months(6),
"Every 7 Months" => task::RecurringInterval::Months(7),
"Every 8 Months" => task::RecurringInterval::Months(8),
"Every 9 Months" => task::RecurringInterval::Months(9),
"Every 10 Months" => task::RecurringInterval::Months(10),
"Every 11 Months" => task::RecurringInterval::Months(11),
"Every Year" => task::RecurringInterval::Months(12),
// Weeks
"Every Week" => task::RecurringInterval::Weeks(1),
"Every 2 Weeks" => task::RecurringInterval::Weeks(2),
"Every 3 Weeks" => task::RecurringInterval::Weeks(3),
"Every 4 Weeks" => task::RecurringInterval::Weeks(4),
"Every 5 Weeks" => task::RecurringInterval::Weeks(5),
"Every 6 Weeks" => task::RecurringInterval::Weeks(6),
"Every 7 Weeks" => task::RecurringInterval::Weeks(7),
"Every 8 Weeks" => task::RecurringInterval::Weeks(8),
"Every 9 Weeks" => task::RecurringInterval::Weeks(9),
"Every 10 Weeks" => task::RecurringInterval::Weeks(10),
// Days
"Every Day" => task::RecurringInterval::Days(1),
"Every 2 Days" => task::RecurringInterval::Days(2),
"Every 3 Days" => task::RecurringInterval::Days(3),
"Every 4 Days" => task::RecurringInterval::Days(4),
"Every 5 Days" => task::RecurringInterval::Days(5),
"Every 6 Days" => task::RecurringInterval::Days(6),
// Fallback
s => anyhow::bail!("Unknown recurring interval: {s:?}"),
};
Ok(Some(task::Recurring { interval }))
}
fn get_recurring_label(labels: &[String]) -> anyhow::Result<Option<&str>> {
let mut recurring_labels_iter = labels
.iter()
.filter_map(|label| label.strip_prefix("Recurring/"));
let Some(recurring_label) = recurring_labels_iter.next() else {
// No recurring label means this is not a recurring task
return Ok(None);
};
if recurring_labels_iter.next().is_some() {
anyhow::bail!("More than one Recurring/ label found on issue");
}
Ok(Some(recurring_label))
}
async fn list_all_issues(ctx: &crate::Context) -> anyhow::Result<Vec<Issue>> {
let issues = ctx
.forgejo
.issue_list_issues(
&ctx.owner,
&ctx.repo,
forgejo_api::structs::IssueListIssuesQuery {
// We also want the closed issues
state: Some(forgejo_api::structs::IssueListIssuesQueryState::All),
// Only issues
r#type: Some(forgejo_api::structs::IssueListIssuesQueryType::Issues),
..Default::default()
},
)
.all()
.await
.with_context(|| format!("Failed fetching issue list"))?;
Ok(issues)
}
trait IssueExt {
fn get_number(&self) -> anyhow::Result<u32>;
fn get_title(&self) -> anyhow::Result<String>;
fn get_state(&self) -> anyhow::Result<forgejo_api::structs::StateType>;
fn get_label_names(&self) -> anyhow::Result<Vec<String>>;
}
impl IssueExt for Issue {
fn get_number(&self) -> anyhow::Result<u32> {
Ok(self
.number
.context("Missing issue number")?
.try_into()
.context("Failed converting issue number to u32")?)
}
fn get_title(&self) -> anyhow::Result<String> {
Ok(self.title.as_ref().context("Missing issue title")?.clone())
}
fn get_state(&self) -> anyhow::Result<forgejo_api::structs::StateType> {
Ok(self.state.context("Issue has no state")?)
}
fn get_label_names(&self) -> anyhow::Result<Vec<std::string::String>> {
let labels = self.labels.as_ref().context("Issue without labels list")?;
let label_names = labels
.into_iter()
.map(|label| label.name.as_ref().context("Label without name").cloned())
.collect::<anyhow::Result<Vec<_>>>()?;
Ok(label_names)
}
}