diff --git a/README.md b/README.md index 436926bf..7d722888 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,32 @@ gh webhook forward --repo=ehuss/triagebot-test --events=* \ Where the value in `--secret` is the secret value you place in `GITHUB_WEBHOOK_SECRET` in the `.env` file, and `--repo` is the repo you want to test against. +You can test webhooks with `cURL`. For example to test the Zulip hooks (commands sent to the +Triagebot from the Rust lang Zulip), you start the triagebot on localhost:8000 and then simulate a +Zulip hook payload: +``` sh +curl http://localhost:8000/zulip-hook \ + -H "Content-Type: application/json" \ + -d '{ + "data": "", + "token": "", + "message": { + "sender_id": , + "recipient_id": , + "sender_full_name": "Randolph Carter", + "sender_email": "r.carter@rust-lang.org", + "type": "stream" + } + }' +``` + +Where: +- `CMD` is the exact command you would issue @triagebot on Zulip (ex. open a direct chat with the + bot and send "work show") +- `ZULIP_TOKEN`: can be anything. Must correspond to the env var `$ZULIP_TOKEN` on your workstation +- `YOUR_ID`: your GitHub user ID. Must be existing in your local triagebot database (table `users` and as + foreign key also in `review_prefs`) + #### ngrok The following is an example of using to provide webhook forwarding. diff --git a/src/db.rs b/src/db.rs index c74606cf..ff3add0d 100644 --- a/src/db.rs +++ b/src/db.rs @@ -344,4 +344,5 @@ CREATE EXTENSION IF NOT EXISTS intarray;", " CREATE UNIQUE INDEX IF NOT EXISTS review_prefs_user_id ON review_prefs(user_id); ", + "ALTER TABLE review_prefs ADD COLUMN max_assigned_prs INT DEFAULT NULL;", ]; diff --git a/src/handlers/pull_requests_assignment_update.rs b/src/handlers/pull_requests_assignment_update.rs index fe544afe..1bbd612f 100644 --- a/src/handlers/pull_requests_assignment_update.rs +++ b/src/handlers/pull_requests_assignment_update.rs @@ -84,3 +84,20 @@ WHERE r.user_id = $1;"; .unwrap(); Ok(row.into()) } + +pub async fn set_review_prefs( + db: &DbClient, + user_id: u64, + pref_max_prs: u32, +) -> anyhow::Result { + let q = " +UPDATE review_prefs r +SET max_assigned_prs = $1 +ROM users u +WHERE r.user_id=$2 AND u.user_id=r.user_id;"; + let res = db + .execute(q, &[&(pref_max_prs as i32), &(user_id as i64)]) + .await + .context("Error retrieving review preferences")?; + Ok(res) +} diff --git a/src/lib.rs b/src/lib.rs index 9a8e8569..bc663ba1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -131,17 +131,27 @@ pub struct ReviewPrefs { pub username: String, pub user_id: i64, pub assigned_prs: Vec, + pub max_assigned_prs: Option, } -impl ReviewPrefs { - fn to_string(&self) -> String { +impl fmt::Display for ReviewPrefs { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let prs = self .assigned_prs .iter() .map(|pr| format!("#{}", pr)) .collect::>() .join(", "); - format!("Username: {}\nAssigned PRs: {}", self.username, prs) + let max = if self.max_assigned_prs.is_none() { + "" + } else { + &format!("{}", self.max_assigned_prs.expect("NaN")) + }; + write!( + f, + "Username: {}\nAssigned PRs: {}\nCurrent review capacity: {}", + self.username, prs, max + ) } } @@ -152,6 +162,7 @@ impl From for ReviewPrefs { username: row.get("username"), user_id: row.get("user_id"), assigned_prs: row.get("assigned_prs"), + max_assigned_prs: row.get("max_assigned_prs"), } } } diff --git a/src/zulip.rs b/src/zulip.rs index 8e77ab82..0b2b8f2a 100644 --- a/src/zulip.rs +++ b/src/zulip.rs @@ -3,7 +3,7 @@ use crate::db::notifications::{self, delete_ping, move_indices, record_ping, Ide use crate::github::{get_id_for_username, GithubClient}; use crate::handlers::docs_update::docs_update; use crate::handlers::project_goals::{self, ping_project_goals_owners}; -use crate::handlers::pull_requests_assignment_update::get_review_prefs; +use crate::handlers::pull_requests_assignment_update::{get_review_prefs, set_review_prefs}; use crate::handlers::Context; use anyhow::{format_err, Context as _}; use std::env; @@ -159,7 +159,7 @@ fn handle_command<'a>( Some("meta") => add_meta_notification(&ctx, gh_id, words).await .map_err(|e| format_err!("Failed to parse `meta` command. Synopsis: meta : Add to your notification identified by (>0)\n\nError: {e:?}")), Some("work") => query_pr_assignments(&ctx, gh_id, words).await - .map_err(|e| format_err!("Failed to parse `work` command. Synopsis: work : shows your current PRs assignment\n\nError: {e:?}")), + .map_err(|e| format_err!("Failed to parse `work` command. Synopsis:\nwork : shows your current PRs assignment\nwork set : set your max number of assigned PRs to review\n\nError: {e:?}")), _ => { while let Some(word) = next { if word == "@**triagebot**" { @@ -241,6 +241,21 @@ async fn query_pr_assignments( gh_id: u64, mut words: impl Iterator, ) -> anyhow::Result> { + let testers = [ + 3161395, // jhpratt + 5910697, // nadriel + 6098822, // apiraino + 20113453, // matthewjasper + 31162821, // jackh726 + 39484203, // jieyouxu + 43851243, // fee1-dead + 74931857, // albertlarsan68 + ]; + + if !testers.contains(&gh_id) { + anyhow::bail!("Sorry, this feature is currently restricted to testers.") + } + let subcommand = match words.next() { Some(subcommand) => subcommand, None => anyhow::bail!("no subcommand provided"), @@ -248,18 +263,33 @@ async fn query_pr_assignments( let db_client = ctx.db.get().await; - let record = match subcommand { + let reply = match subcommand { "show" => { - let rec = get_review_prefs(&db_client, gh_id).await; - if rec.is_err() { - anyhow::bail!("No preferences set.") - } - rec? + let rec = get_review_prefs(&db_client, gh_id) + .await + .context("Could not query review preferences")?; + rec.to_string() + } + "set" => { + let pref_max_prs = match words.next() { + Some(max_value) => { + if words.next().is_some() { + anyhow::bail!("Too many parameters."); + } + max_value.parse::().context( + "Wrong parameter format. Must be a positive integer (and fit a u32).", + )? + } + None => anyhow::bail!("Missing parameter."), + }; + set_review_prefs(&db_client, gh_id, pref_max_prs) + .await + .context("Error occurred while setting review preferences.")?; + format!("Review capacity set to {}", pref_max_prs) } _ => anyhow::bail!("Invalid subcommand."), }; - - Ok(Some(record.to_string())) + Ok(Some(reply)) } // This does two things: