From 51013c995727b3a43ac60976e8340328841b4a16 Mon Sep 17 00:00:00 2001 From: apiraino Date: Tue, 18 Apr 2023 12:18:41 +0200 Subject: [PATCH] Triagebot learns how to comment on GitHub In this first version triagebot learns how to post a comment on GitHub to assign priority to an issue marked as regression. The code should allow for any kind of comment to be created. --- Cargo.lock | 7 +++ Cargo.toml | 1 + src/zulip.rs | 151 ++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 151 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 94bb1f8e..47106ad8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2067,6 +2067,7 @@ dependencies = [ "tracing", "tracing-subscriber", "url", + "urlencoding", "uuid", ] @@ -2224,6 +2225,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" + [[package]] name = "uuid" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index ae0e1de9..14ab2856 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ rand = "0.8.5" ignore = "0.4.18" postgres-types = { version = "0.2.4", features = ["derive"] } cron = { version = "0.12.0" } +urlencoding = "2.1.2" [dependencies.serde] version = "1" diff --git a/src/zulip.rs b/src/zulip.rs index 5f943982..1625c2c4 100644 --- a/src/zulip.rs +++ b/src/zulip.rs @@ -22,6 +22,7 @@ pub struct Request { #[derive(Debug, serde::Deserialize)] struct Message { + id: u64, sender_id: u64, #[allow(unused)] recipient_id: u64, @@ -45,7 +46,8 @@ struct ResponseOwned { content: String, } -pub const BOT_EMAIL: &str = "triage-rust-lang-bot@zulipchat.com"; +const BOT_EMAIL: &str = "triage-rust-lang-bot@zulipchat.com"; +const ZULIP_HOST: &str = "https://rust-lang.zulipchat.com"; pub async fn to_github_id(client: &GithubClient, zulip_id: usize) -> anyhow::Result> { let map = crate::team_data::zulip_map(client).await?; @@ -188,6 +190,15 @@ fn handle_command<'a>( }) .unwrap(), }, + // @triagebot prio #12345 P-high + Some("prio") => return match add_comment_to_issue(&ctx, message_data, words, CommentType::AssignIssuePriority).await { + Ok(r) => r, + Err(e) => serde_json::to_string(&Response { + content: &format!("Failed to await at this time: {:?}", e), + }) + .unwrap(), + }, + _ => {} } } @@ -203,6 +214,130 @@ fn handle_command<'a>( }) } +#[derive(PartialEq)] +enum CommentType { + AssignIssuePriority, +} + +// https://docs.zulip.com/api/outgoing-webhooks#outgoing-webhook-format +#[derive(serde::Deserialize, Debug)] +struct ZulipReply { + messages: Vec, +} + +#[derive(serde::Deserialize, Debug)] +struct ZulipMessage { + subject: String, // ex.: "[weekly] 2023-04-13" + stream_id: u32, + display_recipient: String, // ex. "t-compiler/major changes" +} + +async fn get_zulip_msg(ctx: &Context, msg_id: Option) -> anyhow::Result { + let bot_api_token = env::var("ZULIP_API_TOKEN").expect("ZULIP_API_TOKEN"); + let zulip_user = env::var("ZULIP_USER").expect("ZULIP_USER"); + + let mut url = format!("{}/api/v1/messages?apply_markdown=false", ZULIP_HOST); + + // TODO: Either pick a specific message of a Zulip topic or the first one + if msg_id.is_some() { + url = format!( + "{}&num_before=0&num_after=0&anchor={}", + url, + msg_id.unwrap() + ) + } else { + url = format!("{}&num_before=1&num_after=1&anchor=oldest", url) + } + + let zulip_resp = ctx + .github + .raw() + .get(url) + .basic_auth(zulip_user, Some(&bot_api_token)) + .send() + .await?; + + let zulip_msg_data = zulip_resp.json::().await?; + log::debug!("Zulip reply {:?}", zulip_msg_data); + Ok(zulip_msg_data) +} + +// Add a comment to a Github issue/pr and issue a @rustbot command +async fn add_comment_to_issue( + ctx: &Context, + message: &Message, + mut words: impl Iterator + std::fmt::Debug, + ty: CommentType, +) -> anyhow::Result { + // retrieve the original Zulip topic and rebuild the complete URL to it + let zulip_msg = get_zulip_msg(ctx, None).await?; + + if zulip_msg.messages.is_empty() { + return Ok(serde_json::to_string(&Response { + content: &format!("Failed creating comment on Github: could not retrieve Zulip topic"), + }) + .unwrap()); + } + + // comment example: + // WG-prioritization assigning priority ([Zulip discussion](#)). + // @rustbot label -I-prioritize +P-XXX + let mut issue_id = 0; + let mut comment = String::new(); + if ty == CommentType::AssignIssuePriority { + // ex. "245100-t-compiler/wg-prioritization/alerts"; + let zulip_stream = format!( + "{}-{}", + zulip_msg.messages[0].stream_id, zulip_msg.messages[0].display_recipient + ); + let zulip_msg_link = format!( + "narrow/stream/{}/topic/{}/near/{}", + zulip_stream, zulip_msg.messages[0].subject, message.id + ); + // Don't urlencode, just replace spaces (Zulip custom URL encoding) + let zulip_msg_link = zulip_msg_link.replace(" ", ".20"); + let zulip_msg_link = format!("{}/#{}", ZULIP_HOST, zulip_msg_link); + log::debug!("Zulip link: {}", zulip_msg_link); + + issue_id = words + .next() + .unwrap() + .replace("#", "") + .parse::() + .unwrap(); + let p_label = words.next().unwrap(); + + comment = format!( + "WG-prioritization assigning priority ([Zulip discussion]({})) + \n\n@rustbot label -I-prioritize +{}", + zulip_msg_link, p_label + ); + } + // else ... handle other comment type + + let github_resp = ctx + .octocrab + .issues("rust-lang", "rust") + .create_comment(issue_id.clone(), comment.clone()) + .await; + + let _reply = match github_resp { + Ok(data) => data, + Err(e) => { + return Ok(serde_json::to_string(&Response { + content: &format!("Failed creating comment on Github: {:?}.", e), + }) + .unwrap()); + } + }; + log::debug!("Created comment on issue #{}: {:?}", issue_id, comment); + + Ok(serde_json::to_string(&ResponseNotRequired { + response_not_required: true, + }) + .unwrap()) +} + // This does two things: // * execute the command for the other user // * tell the user executed for that a command was run as them by the user @@ -249,7 +384,7 @@ async fn execute_for_other_user( let members = ctx .github .raw() - .get("https://rust-lang.zulipchat.com/api/v1/users") + .get(format!("{}/api/v1/users", ZULIP_HOST)) .basic_auth(BOT_EMAIL, Some(&bot_api_token)) .send() .await; @@ -402,7 +537,7 @@ impl Recipient<'_> { } pub fn url(&self) -> String { - format!("https://rust-lang.zulipchat.com/#narrow/{}", self.narrow()) + format!("{}/#narrow/{}", ZULIP_HOST, self.narrow()) } } @@ -458,7 +593,7 @@ impl<'a> MessageApiRequest<'a> { } Ok(client - .post("https://rust-lang.zulipchat.com/api/v1/messages") + .post(format!("{}/api/v1/messages", ZULIP_HOST)) .basic_auth(BOT_EMAIL, Some(&bot_api_token)) .form(&SerializedApi { type_: match self.recipient { @@ -510,8 +645,8 @@ impl<'a> UpdateMessageApiRequest<'a> { Ok(client .patch(&format!( - "https://rust-lang.zulipchat.com/api/v1/messages/{}", - self.message_id + "{}/api/v1/messages/{}", + ZULIP_HOST, self.message_id )) .basic_auth(BOT_EMAIL, Some(&bot_api_token)) .form(&SerializedApi { @@ -723,8 +858,8 @@ impl<'a> AddReaction<'a> { Ok(client .post(&format!( - "https://rust-lang.zulipchat.com/api/v1/messages/{}/reactions", - self.message_id + "{}/api/v1/messages/{}/reactions", + ZULIP_HOST, self.message_id )) .basic_auth(BOT_EMAIL, Some(&bot_api_token)) .form(&self)