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> { 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::>>()?; Ok(tasks) } fn task_from_issue(issue: &Issue) -> anyhow::Result { 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 { 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> { 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> { 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> { 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; fn get_title(&self) -> anyhow::Result; fn get_state(&self) -> anyhow::Result; fn get_label_names(&self) -> anyhow::Result>; } impl IssueExt for Issue { fn get_number(&self) -> anyhow::Result { Ok(self .number .context("Missing issue number")? .try_into() .context("Failed converting issue number to u32")?) } fn get_title(&self) -> anyhow::Result { Ok(self.title.as_ref().context("Missing issue title")?.clone()) } fn get_state(&self) -> anyhow::Result { Ok(self.state.context("Issue has no state")?) } fn get_label_names(&self) -> anyhow::Result> { 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::>>()?; Ok(label_names) } }