Parcourir la source

Implement acknowledging notifications via Zulip

Mark Rousskov il y a 5 ans
Parent
commit
8cf3063d58
4 fichiers modifiés avec 114 ajouts et 2 suppressions
  1. 2 2
      src/db/notifications.rs
  2. 1 0
      src/lib.rs
  3. 26 0
      src/main.rs
  4. 85 0
      src/zulip.rs

+ 2 - 2
src/db/notifications.rs

@@ -27,11 +27,11 @@ pub async fn record_ping(db: &DbClient, notification: &Notification) -> anyhow::
 
 pub async fn delete_ping(db: &DbClient, user_id: i64, origin_url: &str) -> anyhow::Result<()> {
     db.execute(
-        "DELETE FROM notifications WHERE user_id = $1, origin_url = $2",
+        "DELETE FROM notifications WHERE user_id = $1 and origin_url = $2",
         &[&user_id, &origin_url],
     )
     .await
-    .context("deleting notification")?;
+    .context("delete notification query")?;
 
     Ok(())
 }

+ 1 - 0
src/lib.rs

@@ -13,6 +13,7 @@ pub mod interactions;
 pub mod notification_listing;
 pub mod payload;
 pub mod team;
+pub mod zulip;
 
 pub enum EventName {
     IssueComment,

+ 26 - 0
src/main.rs

@@ -41,6 +41,32 @@ async fn serve_req(req: Request<Body>, ctx: Arc<Context>) -> Result<Response<Bod
             )))
             .unwrap());
     }
+    if req.uri.path() == "/zulip-hook" {
+        let mut c = body_stream;
+        let mut payload = Vec::new();
+        while let Some(chunk) = c.next().await {
+            let chunk = chunk?;
+            payload.extend_from_slice(&chunk);
+        }
+
+        let req = match serde_json::from_slice(&payload) {
+            Ok(r) => r,
+            Err(e) => {
+                return Ok(Response::builder()
+                    .status(StatusCode::BAD_REQUEST)
+                    .body(Body::from(format!(
+                        "Did not send valid JSON request: {}",
+                        e
+                    )))
+                    .unwrap());
+            }
+        };
+
+        return Ok(Response::builder()
+            .status(StatusCode::OK)
+            .body(Body::from(triagebot::zulip::respond(&ctx, req).await))
+            .unwrap());
+    }
     if req.uri.path() != "/github-hook" {
         return Ok(Response::builder()
             .status(StatusCode::NOT_FOUND)

+ 85 - 0
src/zulip.rs

@@ -0,0 +1,85 @@
+use crate::db::notifications::delete_ping;
+use crate::handlers::Context;
+
+#[derive(Debug, serde::Deserialize)]
+pub struct Request {
+    /// Markdown body of the sent message.
+    data: String,
+
+    /// Metadata about this request.
+    message: Message,
+
+    /// Authentication token. The same for all Zulip messages.
+    token: String,
+}
+
+#[derive(Debug, serde::Deserialize)]
+struct Message {
+    sender_id: usize,
+    sender_full_name: String,
+}
+
+#[derive(serde::Serialize)]
+struct Response<'a> {
+    content: &'a str,
+}
+
+// Zulip User ID to GH User ID
+//
+// FIXME: replace with https://github.com/rust-lang/team/pull/222 once it lands
+static MAPPING: &[(usize, i64)] = &[(116122, 5047365), (119235, 1940490)];
+
+pub async fn respond(ctx: &Context, req: Request) -> String {
+    let expected_token = std::env::var("ZULIP_TOKEN").expect("`ZULIP_TOKEN` set for authorization");
+
+    if !openssl::memcmp::eq(req.token.as_bytes(), expected_token.as_bytes()) {
+        return serde_json::to_string(&Response {
+            content: "Invalid authorization.",
+        })
+        .unwrap();
+    }
+
+    log::trace!("zulip hook: {:?}", req);
+    let gh_id = match MAPPING
+        .iter()
+        .find(|(zulip, _)| *zulip == req.message.sender_id)
+    {
+        Some((_, gh_id)) => *gh_id,
+        None => {
+            return serde_json::to_string(&Response {
+                content: &format!(
+                "Unknown Zulip user. Please add `zulip-id = {}` to your file in rust-lang/team.",
+                req.message.sender_id),
+            })
+            .unwrap();
+        }
+    };
+
+    match two_words(&req.data) {
+        Some(["acknowledge", url]) => match delete_ping(&ctx.db, gh_id, url).await {
+            Ok(()) => serde_json::to_string(&Response {
+                content: &format!("Acknowledged {}.", url),
+            })
+            .unwrap(),
+            Err(e) => serde_json::to_string(&Response {
+                content: &format!("Failed to acknowledge {}: {:?}.", url, e),
+            })
+            .unwrap(),
+        },
+        _ => serde_json::to_string(&Response {
+            content: "Unknown command.",
+        })
+        .unwrap(),
+    }
+}
+
+fn two_words(s: &str) -> Option<[&str; 2]> {
+    let mut iter = s.split_whitespace();
+    let first = iter.next()?;
+    let second = iter.next()?;
+    if iter.next().is_some() {
+        return None;
+    }
+
+    return Some([first, second]);
+}