diff --git a/Cargo.lock b/Cargo.lock index 1bb48e6..29db989 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -327,6 +327,7 @@ dependencies = [ "deadpool-sqlite", "futures", "git2", + "humantime", "lockable", "log", "octocrab", diff --git a/Cargo.toml b/Cargo.toml index 869a75f..1578c32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,3 +31,4 @@ octocrab = "*" url = "*" lockable = "*" version-compare = "*" +humantime = "*" diff --git a/src/chat/mod.rs b/src/chat/mod.rs index 479c0ee..f163e89 100644 --- a/src/chat/mod.rs +++ b/src/chat/mod.rs @@ -1,5 +1,6 @@ use std::{collections::BTreeSet, fmt, sync::Arc}; +use chrono::Utc; use git2::{BranchType, Oid}; use octocrab::models::IssueState; use teloxide::types::{ChatId, Message}; @@ -17,6 +18,7 @@ use crate::{ condition::{Action, Condition}, error::Error, github::{self, GitHubInfo}, + options, repo::{cache::query_cache_commit, resources::RepoResources}, utils::empty_or_start_new_line, }; @@ -275,18 +277,67 @@ pub async fn pr_issue_check( let commit = merged_pr_to_commit(resources, github_info, id, settings).await?; return Ok(PRIssueCheckResult::Merged(commit)); } - if issue.state == IssueState::Closed { - { + match issue.state { + IssueState::Open => { let mut locked = resources.settings.write().await; - locked + let issue_settings = locked .pr_issues - .remove(&id) + .get_mut(&id) .ok_or(Error::UnknownPRIssue(id))?; - }; - resources.save_settings().await?; - return Ok(PRIssueCheckResult::Closed); + match &issue_settings.closed_at { + Some(_) => { + issue_settings.closed_at = None; + drop(locked); + resources.save_settings().await?; + Ok(PRIssueCheckResult::Opened) + } + None => Ok(PRIssueCheckResult::Waiting), + } + } + IssueState::Closed => { + let mut locked = resources.settings.write().await; + let issue_settings = locked + .pr_issues + .get_mut(&id) + .ok_or(Error::UnknownPRIssue(id))?; + match issue_settings.closed_at { + None => { + issue_settings.closed_at = Some(issue.closed_at.unwrap_or_else(Utc::now)); + drop(locked); + resources.save_settings().await?; + Ok(PRIssueCheckResult::Closed) + } + Some(closed_at) => { + match (Utc::now() - closed_at).to_std() { + Ok(duration) => { + if duration >= *options::get().pr_issue_expire { + // expired + locked + .pr_issues + .remove(&id) + .ok_or(Error::UnknownPRIssue(id))?; + drop(locked); + resources.save_settings().await?; + Ok(PRIssueCheckResult::Expired) + } else { + // not expired + Ok(PRIssueCheckResult::Expiring) + } + } + Err(e) => { + log::error!("failed to calculate elapsed time for pr/issue {id}: {e}"); + Ok(PRIssueCheckResult::Unknown) + } + } + } + } + } + // issue state is non-exhaustive + s => { + log::warn!("unknown issue state: {s:?}"); + Ok(PRIssueCheckResult::Unknown) + } } - Ok(PRIssueCheckResult::Waiting) } pub async fn merged_pr_to_commit( diff --git a/src/chat/results.rs b/src/chat/results.rs index 75da427..fc573e0 100644 --- a/src/chat/results.rs +++ b/src/chat/results.rs @@ -45,6 +45,10 @@ pub struct BranchCheckResult { #[derive(Debug)] pub enum PRIssueCheckResult { Merged(String), + Opened, Closed, Waiting, + Expiring, + Expired, + Unknown, } diff --git a/src/chat/settings.rs b/src/chat/settings.rs index dbb7e77..18dd461 100644 --- a/src/chat/settings.rs +++ b/src/chat/settings.rs @@ -1,5 +1,6 @@ use std::collections::{BTreeMap, BTreeSet}; +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use teloxide::{types::User, utils::markdown}; use url::Url; @@ -26,6 +27,7 @@ pub struct CommitSettings { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PRIssueSettings { pub url: Url, + pub closed_at: Option>, #[serde(flatten)] pub notify: NotifySettings, } diff --git a/src/main.rs b/src/main.rs index a268df8..3264180 100644 --- a/src/main.rs +++ b/src/main.rs @@ -815,6 +815,7 @@ async fn pr_issue_add( let comment = optional_comment.unwrap_or_default(); let settings = PRIssueSettings { url, + closed_at: None, notify: NotifySettings { comment, subscribers, @@ -848,13 +849,15 @@ async fn pr_issue_check(bot: Bot, msg: Message, repo: String, id: u64) -> Result match result { PRIssueCheckResult::Merged(commit) => commit_check(bot, msg, repo, commit).await, PRIssueCheckResult::Closed => { - reply_to_msg( - &bot, - &msg, - format!("{pretty_id} has been closed \\(and removed\\)"), - ) - .parse_mode(ParseMode::MarkdownV2) - .await?; + reply_to_msg(&bot, &msg, format!("{pretty_id} has been closed")) + .parse_mode(ParseMode::MarkdownV2) + .await?; + Ok(()) + } + PRIssueCheckResult::Opened => { + reply_to_msg(&bot, &msg, format!("{pretty_id} has been opened")) + .parse_mode(ParseMode::MarkdownV2) + .await?; Ok(()) } PRIssueCheckResult::Waiting => { @@ -874,6 +877,27 @@ async fn pr_issue_check(bot: Bot, msg: Message, repo: String, id: u64) -> Result send.await?; Ok(()) } + PRIssueCheckResult::Expiring => { + let mut send = reply_to_msg(&bot, &msg, format!("{pretty_id} has been closed")) + .parse_mode(ParseMode::MarkdownV2); + send = try_attach_subscribe_button_markup( + msg.chat.id, + send, + "p", + &repo, + &id.to_string(), + ); + send.await?; + Ok(()) + } + PRIssueCheckResult::Expired => { + log::info!("stopped tracking to pr/issue {id}"); + Ok(()) + } + PRIssueCheckResult::Unknown => { + log::warn!("issue {id} state unknown"); + Ok(()) + } } } Err(Error::CommitExists(commit)) => { diff --git a/src/message.rs b/src/message.rs index ca461bc..d235a46 100644 --- a/src/message.rs +++ b/src/message.rs @@ -89,6 +89,18 @@ pub async fn pr_issue_id_pretty(resources: &RepoResources, id: u64) -> Result Result { + Ok(format!( + "{pretty_id} has been opened{notify}", + pretty_id = pr_issue_id_pretty(resources, id).await?, + notify = empty_or_start_new_line(&settings.notify.subscribers_markdown()), + )) +} + pub async fn pr_issue_merged_message( resources: &RepoResources, id: u64, diff --git a/src/options.rs b/src/options.rs index 74d823d..2b03923 100644 --- a/src/options.rs +++ b/src/options.rs @@ -1,4 +1,5 @@ use clap::Parser; +use humantime::Duration; use std::path::PathBuf; #[derive(Debug, Parser)] @@ -10,6 +11,8 @@ pub struct Options { pub cron: String, #[arg(short, long)] pub admin_chat_id: i64, + #[arg(short, long, default_value = "1week")] + pub pr_issue_expire: Duration, } pub static OPTIONS: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); diff --git a/src/update.rs b/src/update.rs index b7b219a..f01c73a 100644 --- a/src/update.rs +++ b/src/update.rs @@ -19,7 +19,7 @@ use crate::{ condition::Action, message::{ branch_check_message, commit_check_message, pr_issue_closed_message, - pr_issue_merged_message, + pr_issue_merged_message, pr_issue_opened_message, }, options, repo::{self, resources::RepoResources}, @@ -162,6 +162,13 @@ async fn update_chat_repo_pr_issue( .await?; Ok(()) } + PRIssueCheckResult::Opened => { + let message = pr_issue_opened_message(repo_resources, id, settings).await?; + bot.send_message(chat, message) + .parse_mode(ParseMode::MarkdownV2) + .await?; + Ok(()) + } PRIssueCheckResult::Closed => { let message = pr_issue_closed_message(repo_resources, id, settings).await?; bot.send_message(chat, message) @@ -170,6 +177,15 @@ async fn update_chat_repo_pr_issue( Ok(()) } PRIssueCheckResult::Waiting => Ok(()), + PRIssueCheckResult::Expiring => Ok(()), + PRIssueCheckResult::Expired => { + log::info!("stopped tracking to pr/issue {id}"); + Ok(()) + } + PRIssueCheckResult::Unknown => { + log::warn!("issue {id} state unknown"); + Ok(()) + } } }