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..cb7b5599 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, @@ -188,6 +189,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 +213,105 @@ 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 { + message: ZulipMessage, +} + +#[derive(serde::Deserialize, Debug)] +struct ZulipMessage { + subject: String, // ex.: "[weekly] 2023-04-13" +} + +async fn get_zulip_msg(ctx: &Context, id: u64) -> anyhow::Result { + let bot_api_token = env::var("ZULIP_API_TOKEN").expect("ZULIP_API_TOKEN"); + let zulip_resp = ctx + .github + .raw() + .get(format!( + "https://rust-lang.zulipchat.com/api/v1/messages/{}", + id + )) + .basic_auth(BOT_EMAIL, Some(&bot_api_token)) + .send() + .await?; + + let zulip_msg_data = zulip_resp.json::().await.expect("TODO"); + log::debug!("Zulip reply {:?}", zulip_msg_data); + Ok(zulip_msg_data) +} + +// Add a comment to a Github issue/pr issuing 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 message to build the complete URL + let zulip_msg = get_zulip_msg(ctx, message.id).await?; + + // 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 { + let zulip_stream = "245100-t-compiler/wg-prioritization/alerts"; + let mut zulip_msg_link = format!( + "https://rust-lang.zulipchat.com/#narrow/stream/{}/topic/{}/near/{}", + zulip_stream, zulip_msg.message.subject, message.id + ); + // Encode url and replace "%" with "." + // (apparently Zulip does that to public URLs) + urlencoding::encode(&zulip_msg_link); + zulip_msg_link = zulip_msg_link.replace("%", "."); + + 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("owner", "repo") + .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 to post 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