Skip to content

Commit 51013c9

Browse files
committed
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.
1 parent add83c3 commit 51013c9

File tree

3 files changed

+151
-8
lines changed

3 files changed

+151
-8
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ rand = "0.8.5"
4444
ignore = "0.4.18"
4545
postgres-types = { version = "0.2.4", features = ["derive"] }
4646
cron = { version = "0.12.0" }
47+
urlencoding = "2.1.2"
4748

4849
[dependencies.serde]
4950
version = "1"

src/zulip.rs

Lines changed: 143 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub struct Request {
2222

2323
#[derive(Debug, serde::Deserialize)]
2424
struct Message {
25+
id: u64,
2526
sender_id: u64,
2627
#[allow(unused)]
2728
recipient_id: u64,
@@ -45,7 +46,8 @@ struct ResponseOwned {
4546
content: String,
4647
}
4748

48-
pub const BOT_EMAIL: &str = "[email protected]";
49+
const BOT_EMAIL: &str = "[email protected]";
50+
const ZULIP_HOST: &str = "https://rust-lang.zulipchat.com";
4951

5052
pub async fn to_github_id(client: &GithubClient, zulip_id: usize) -> anyhow::Result<Option<i64>> {
5153
let map = crate::team_data::zulip_map(client).await?;
@@ -188,6 +190,15 @@ fn handle_command<'a>(
188190
})
189191
.unwrap(),
190192
},
193+
// @triagebot prio #12345 P-high
194+
Some("prio") => return match add_comment_to_issue(&ctx, message_data, words, CommentType::AssignIssuePriority).await {
195+
Ok(r) => r,
196+
Err(e) => serde_json::to_string(&Response {
197+
content: &format!("Failed to await at this time: {:?}", e),
198+
})
199+
.unwrap(),
200+
},
201+
191202
_ => {}
192203
}
193204
}
@@ -203,6 +214,130 @@ fn handle_command<'a>(
203214
})
204215
}
205216

217+
#[derive(PartialEq)]
218+
enum CommentType {
219+
AssignIssuePriority,
220+
}
221+
222+
// https://docs.zulip.com/api/outgoing-webhooks#outgoing-webhook-format
223+
#[derive(serde::Deserialize, Debug)]
224+
struct ZulipReply {
225+
messages: Vec<ZulipMessage>,
226+
}
227+
228+
#[derive(serde::Deserialize, Debug)]
229+
struct ZulipMessage {
230+
subject: String, // ex.: "[weekly] 2023-04-13"
231+
stream_id: u32,
232+
display_recipient: String, // ex. "t-compiler/major changes"
233+
}
234+
235+
async fn get_zulip_msg(ctx: &Context, msg_id: Option<u64>) -> anyhow::Result<ZulipReply> {
236+
let bot_api_token = env::var("ZULIP_API_TOKEN").expect("ZULIP_API_TOKEN");
237+
let zulip_user = env::var("ZULIP_USER").expect("ZULIP_USER");
238+
239+
let mut url = format!("{}/api/v1/messages?apply_markdown=false", ZULIP_HOST);
240+
241+
// TODO: Either pick a specific message of a Zulip topic or the first one
242+
if msg_id.is_some() {
243+
url = format!(
244+
"{}&num_before=0&num_after=0&anchor={}",
245+
url,
246+
msg_id.unwrap()
247+
)
248+
} else {
249+
url = format!("{}&num_before=1&num_after=1&anchor=oldest", url)
250+
}
251+
252+
let zulip_resp = ctx
253+
.github
254+
.raw()
255+
.get(url)
256+
.basic_auth(zulip_user, Some(&bot_api_token))
257+
.send()
258+
.await?;
259+
260+
let zulip_msg_data = zulip_resp.json::<ZulipReply>().await?;
261+
log::debug!("Zulip reply {:?}", zulip_msg_data);
262+
Ok(zulip_msg_data)
263+
}
264+
265+
// Add a comment to a Github issue/pr and issue a @rustbot command
266+
async fn add_comment_to_issue(
267+
ctx: &Context,
268+
message: &Message,
269+
mut words: impl Iterator<Item = &str> + std::fmt::Debug,
270+
ty: CommentType,
271+
) -> anyhow::Result<String> {
272+
// retrieve the original Zulip topic and rebuild the complete URL to it
273+
let zulip_msg = get_zulip_msg(ctx, None).await?;
274+
275+
if zulip_msg.messages.is_empty() {
276+
return Ok(serde_json::to_string(&Response {
277+
content: &format!("Failed creating comment on Github: could not retrieve Zulip topic"),
278+
})
279+
.unwrap());
280+
}
281+
282+
// comment example:
283+
// WG-prioritization assigning priority ([Zulip discussion](#)).
284+
// @rustbot label -I-prioritize +P-XXX
285+
let mut issue_id = 0;
286+
let mut comment = String::new();
287+
if ty == CommentType::AssignIssuePriority {
288+
// ex. "245100-t-compiler/wg-prioritization/alerts";
289+
let zulip_stream = format!(
290+
"{}-{}",
291+
zulip_msg.messages[0].stream_id, zulip_msg.messages[0].display_recipient
292+
);
293+
let zulip_msg_link = format!(
294+
"narrow/stream/{}/topic/{}/near/{}",
295+
zulip_stream, zulip_msg.messages[0].subject, message.id
296+
);
297+
// Don't urlencode, just replace spaces (Zulip custom URL encoding)
298+
let zulip_msg_link = zulip_msg_link.replace(" ", ".20");
299+
let zulip_msg_link = format!("{}/#{}", ZULIP_HOST, zulip_msg_link);
300+
log::debug!("Zulip link: {}", zulip_msg_link);
301+
302+
issue_id = words
303+
.next()
304+
.unwrap()
305+
.replace("#", "")
306+
.parse::<u64>()
307+
.unwrap();
308+
let p_label = words.next().unwrap();
309+
310+
comment = format!(
311+
"WG-prioritization assigning priority ([Zulip discussion]({}))
312+
\n\n@rustbot label -I-prioritize +{}",
313+
zulip_msg_link, p_label
314+
);
315+
}
316+
// else ... handle other comment type
317+
318+
let github_resp = ctx
319+
.octocrab
320+
.issues("rust-lang", "rust")
321+
.create_comment(issue_id.clone(), comment.clone())
322+
.await;
323+
324+
let _reply = match github_resp {
325+
Ok(data) => data,
326+
Err(e) => {
327+
return Ok(serde_json::to_string(&Response {
328+
content: &format!("Failed creating comment on Github: {:?}.", e),
329+
})
330+
.unwrap());
331+
}
332+
};
333+
log::debug!("Created comment on issue #{}: {:?}", issue_id, comment);
334+
335+
Ok(serde_json::to_string(&ResponseNotRequired {
336+
response_not_required: true,
337+
})
338+
.unwrap())
339+
}
340+
206341
// This does two things:
207342
// * execute the command for the other user
208343
// * 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(
249384
let members = ctx
250385
.github
251386
.raw()
252-
.get("https://rust-lang.zulipchat.com/api/v1/users")
387+
.get(format!("{}/api/v1/users", ZULIP_HOST))
253388
.basic_auth(BOT_EMAIL, Some(&bot_api_token))
254389
.send()
255390
.await;
@@ -402,7 +537,7 @@ impl Recipient<'_> {
402537
}
403538

404539
pub fn url(&self) -> String {
405-
format!("https://rust-lang.zulipchat.com/#narrow/{}", self.narrow())
540+
format!("{}/#narrow/{}", ZULIP_HOST, self.narrow())
406541
}
407542
}
408543

@@ -458,7 +593,7 @@ impl<'a> MessageApiRequest<'a> {
458593
}
459594

460595
Ok(client
461-
.post("https://rust-lang.zulipchat.com/api/v1/messages")
596+
.post(format!("{}/api/v1/messages", ZULIP_HOST))
462597
.basic_auth(BOT_EMAIL, Some(&bot_api_token))
463598
.form(&SerializedApi {
464599
type_: match self.recipient {
@@ -510,8 +645,8 @@ impl<'a> UpdateMessageApiRequest<'a> {
510645

511646
Ok(client
512647
.patch(&format!(
513-
"https://rust-lang.zulipchat.com/api/v1/messages/{}",
514-
self.message_id
648+
"{}/api/v1/messages/{}",
649+
ZULIP_HOST, self.message_id
515650
))
516651
.basic_auth(BOT_EMAIL, Some(&bot_api_token))
517652
.form(&SerializedApi {
@@ -723,8 +858,8 @@ impl<'a> AddReaction<'a> {
723858

724859
Ok(client
725860
.post(&format!(
726-
"https://rust-lang.zulipchat.com/api/v1/messages/{}/reactions",
727-
self.message_id
861+
"{}/api/v1/messages/{}/reactions",
862+
ZULIP_HOST, self.message_id
728863
))
729864
.basic_auth(BOT_EMAIL, Some(&bot_api_token))
730865
.form(&self)

0 commit comments

Comments
 (0)