use anyhow::Context as _; pub type CommentId = u64; pub struct BotCommentInfo { pub body: String, pub id: CommentId, } pub async fn make_bot_comment( ctx: &crate::Context, issue_number: u64, ) -> anyhow::Result { let initial_message = "_Please be patient, this issue is currently being integrated into the techtree..._"; let res = ctx .forgejo .issue_create_comment( &ctx.owner, &ctx.repo, issue_number, forgejo_api::structs::CreateIssueCommentOption { body: initial_message.to_owned(), updated_at: None, }, ) .await?; Ok(BotCommentInfo { id: res.id.unwrap(), body: initial_message.to_owned(), }) } pub async fn find_bot_comment( ctx: &crate::Context, issue_number: u64, ) -> anyhow::Result> { let mut comments = ctx .forgejo .issue_get_comments( &ctx.owner, &ctx.repo, issue_number, forgejo_api::structs::IssueGetCommentsQuery { ..Default::default() }, ) .await .context("Failed fetching comments for issue")?; comments.sort_by_key(|comment| comment.created_at); let maybe_bot_comment = comments .iter() .rev() .find(|comment| comment.user.as_ref().unwrap().id == Some(-2)); Ok(maybe_bot_comment.map(|c| BotCommentInfo { body: c.body.clone().unwrap_or("".to_owned()), id: c.id.unwrap(), })) } /// Find existing bot comment or create a new one. /// /// Returns a tuple of the comment information and a boolean indicating whether the comment was /// newly created. pub async fn find_or_make_bot_comment( ctx: &crate::Context, issue_number: u64, ) -> anyhow::Result<(BotCommentInfo, bool)> { if let Some(comment) = find_bot_comment(ctx, issue_number) .await .context("Failed to search for bot comment in issue")? { Ok((comment, false)) } else { make_bot_comment(ctx, issue_number) .await .context("Failed to make new bot comment in issue") .map(|c| (c, true)) } } pub async fn update_bot_comment( ctx: &crate::Context, id: CommentId, new_body: String, ) -> anyhow::Result<()> { ctx.forgejo .issue_edit_comment( &ctx.owner, &ctx.repo, id, forgejo_api::structs::EditIssueCommentOption { body: new_body, updated_at: None, }, ) .await .context("Failed to update comment body")?; Ok(()) } pub async fn update_bot_comment_from_subtree( ctx: &crate::Context, id: CommentId, subtree: &crate::tree::Subtree<'_>, hash: &str, ) -> anyhow::Result<()> { let mut mermaid = subtree.to_mermaid(&ctx.repo_url.to_string(), false); // When the mermaid graph gets too big, regenerate a simplified version. if mermaid.len() > 4990 { log::info!("Mermaid graph is too big, generating simplified version..."); mermaid = subtree.to_mermaid(&ctx.repo_url.to_string(), true); } let full_text = if mermaid.len() > 4990 { format!( r##"## Partial Techtree _Sorry, the partial techtree is still too big to be displayed for this element..._ Digest: {hash}; Last Updated: {timestamp}"##, timestamp = ctx.timestamp, ) } else { format!( r##"## Partial Techtree ```mermaid {mermaid} ``` Digest: {hash}; Last Updated: {timestamp}"##, timestamp = ctx.timestamp, ) }; update_bot_comment(&ctx, id, full_text).await?; Ok(()) } pub async fn remove_stale_label(ctx: &crate::Context, issue_number: u64) -> anyhow::Result<()> { let labels = ctx .forgejo .issue_get_labels(&ctx.owner, &ctx.repo, issue_number) .await .context("Failed fetching issue labels")?; let stale_label_id = labels .iter() .filter(|l| l.name.as_deref() == Some("Stale")) .next() .map(|l| l.id.unwrap()); if let Some(stale_label_id) = stale_label_id { log::info!("Removing `Stale` label from issue #{issue_number}..."); let res = ctx .forgejo .issue_remove_label( &ctx.owner, &ctx.repo, issue_number, stale_label_id, forgejo_api::structs::DeleteLabelsOption { updated_at: Some(time::OffsetDateTime::now_utc()), }, ) .await; if let Err(e) = res { // At the moment, the token for Forgejo Actions cannot remove issue labels. // See https://codeberg.org/forgejo/forgejo/issues/2415 log::warn!( "Failed to remove `Stale` label for #{issue_number}. This is a known Forgejo limitation at the moment... ({e})" ); } } Ok(()) }