diff --git a/README.md b/README.md index 85216d9a..26ec59db 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,12 @@ The general overview of what you will need to do: 6. Add a `triagebot.toml` file to the main branch of your GitHub repo with whichever services you want to try out. 7. Try interacting with your repo, such as issuing `@rustbot` commands or interacting with PRs and issues (depending on which services you enabled in `triagebot.toml`). Watch the logs from the server to see what's going on. +## Pull requests assignment backoffice + +At this stage, an integration with [Zulip](https://rust-lang.zulipchat.com) allowing Rust project contributors to check their pull request assignments. + +Read the documentation [here](./pr_assignment_backoffice.md). + ### Configure a database To use Postgres, you will need to install it and configure it: diff --git a/pr_assignment_backoffice.md b/pr_assignment_backoffice.md new file mode 100644 index 00000000..828f327a --- /dev/null +++ b/pr_assignment_backoffice.md @@ -0,0 +1,11 @@ +## Zulip integration + +Privmsg [the triagebot on Zulip](https://rust-lang.zulipchat.com/#narrow/pm-with/261224-triage-rust-lang-bot) and use the following commands: +- Check my PR assignments: + > work show + Will return something like: + ``` + Username: + Assigned PRs: #28467, #82645, #131265 + ``` + (or an error) diff --git a/src/handlers/pull_requests_assignment_update.rs b/src/handlers/pull_requests_assignment_update.rs index 2e08adfa..f14e6d08 100644 --- a/src/handlers/pull_requests_assignment_update.rs +++ b/src/handlers/pull_requests_assignment_update.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use crate::db::notifications::record_username; use crate::github::retrieve_pull_requests; use crate::jobs::Job; +use crate::ReviewPrefs; use anyhow::Context as _; use async_trait::async_trait; use tokio_postgres::Client as DbClient; @@ -70,3 +71,18 @@ WHERE review_prefs.user_id=$1"; .await .context("Insert DB error") } + +/// Get pull request assignments for a team member +pub async fn get_review_prefs(db: &DbClient, user_id: u64) -> anyhow::Result { + let q = " +SELECT username,r.* +FROM review_prefs r +JOIN users on r.user_id=users.user_id +WHERE user_id = $1;"; + let row = db + .query_one(q, &[&(user_id as i64)]) + .await + .context("Error retrieving review preferences") + .unwrap(); + Ok(row.into()) +} diff --git a/src/lib.rs b/src/lib.rs index b0d353b1..bf3df35a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ use crate::github::PullRequestDetails; use anyhow::Context; use handlers::HandlerError; use interactions::ErrorComment; +use serde::Serialize; use std::fmt; use tracing as log; @@ -125,6 +126,37 @@ impl From for WebhookError { } } +#[derive(Debug, Serialize)] +pub struct ReviewPrefs { + pub id: uuid::Uuid, + pub username: String, + pub user_id: i64, + pub assigned_prs: Vec, +} + +impl ReviewPrefs { + fn to_string(&self) -> String { + let prs = self + .assigned_prs + .iter() + .map(|pr| format!("#{}", pr)) + .collect::>() + .join(", "); + format!("Username: {}\nAssigned PRs: {}", self.username, prs) + } +} + +impl From for ReviewPrefs { + fn from(row: tokio_postgres::row::Row) -> Self { + Self { + id: row.get("id"), + username: row.get("username"), + user_id: row.get("user_id"), + assigned_prs: row.get("assigned_prs"), + } + } +} + pub fn deserialize_payload(v: &str) -> anyhow::Result { let mut deserializer = serde_json::Deserializer::from_str(&v); let res: Result = serde_path_to_error::deserialize(&mut deserializer); diff --git a/src/zulip.rs b/src/zulip.rs index 66e79cf9..f3546b88 100644 --- a/src/zulip.rs +++ b/src/zulip.rs @@ -2,6 +2,7 @@ use crate::db::notifications::add_metadata; use crate::db::notifications::{self, delete_ping, move_indices, record_ping, Identifier}; use crate::github::{self, GithubClient}; use crate::handlers::docs_update::docs_update; +use crate::handlers::pull_requests_assignment_update::get_review_prefs; use crate::handlers::Context; use anyhow::{format_err, Context as _}; use std::convert::TryInto; @@ -155,7 +156,9 @@ fn handle_command<'a>( Some("move") => move_notification(&ctx, gh_id, words).await .map_err(|e| format_err!("Failed to parse movement, expected `move `: {e:?}.")), Some("meta") => add_meta_notification(&ctx, gh_id, words).await - .map_err(|e| format_err!("Failed to parse movement, expected `move `: {e:?}.")), + .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:?}")), _ => { while let Some(word) = next { if word == "@**triagebot**" { @@ -199,6 +202,26 @@ fn handle_command<'a>( }) } +async fn query_pr_assignments( + ctx: &&Context, + gh_id: u64, + mut words: impl Iterator, +) -> anyhow::Result> { + let subcommand = match words.next() { + Some(subcommand) => subcommand, + None => anyhow::bail!("no subcommand provided"), + }; + + let db_client = ctx.db.get().await; + + let record = match subcommand { + "show" => get_review_prefs(&db_client, gh_id).await?, + _ => anyhow::bail!("Invalid subcommand."), + }; + + Ok(Some(record.to_string())) +} + // 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