Parcourir la source

Record notifications into database

This recors notifications received by the webhook into the database, independent
of repository configuration.
Mark Rousskov il y a 5 ans
Parent
commit
05948adfb4
10 fichiers modifiés avec 116 ajouts et 6 suppressions
  1. 1 0
      Cargo.lock
  2. 1 1
      Cargo.toml
  3. 1 0
      src/config.rs
  4. 2 2
      src/db.rs
  5. 1 1
      src/db/notifications.rs
  6. 25 0
      src/github.rs
  7. 6 0
      src/handlers.rs
  8. 77 0
      src/handlers/notification.rs
  9. 1 0
      src/lib.rs
  10. 1 2
      src/main.rs

+ 1 - 0
Cargo.lock

@@ -127,6 +127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
  "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)",
  "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 

+ 1 - 1
Cargo.toml

@@ -27,7 +27,7 @@ futures = { version = "0.3", default-features = false, features = ["std"] }
 uuid = { version = "0.8", features = ["v4"] }
 url = "2.1.0"
 once_cell = "1"
-chrono = "0.4"
+chrono = { version = "0.4", features = ["serde"] }
 tokio-postgres = { version = "0.5", features = ["with-chrono-0_4"] }
 postgres-native-tls = "0.3"
 native-tls = "0.2"

+ 1 - 0
src/config.rs

@@ -19,6 +19,7 @@ pub(crate) struct Config {
     pub(crate) assign: Option<AssignConfig>,
     pub(crate) ping: Option<PingConfig>,
     pub(crate) nominate: Option<NominateConfig>,
+    pub(crate) notification: Option<()>,
 }
 
 #[derive(PartialEq, Eq, Debug, serde::Deserialize)]

+ 2 - 2
src/db.rs

@@ -1,5 +1,5 @@
 use anyhow::Context as _;
-use tokio_postgres::Client as DbClient;
+pub use tokio_postgres::Client as DbClient;
 
 pub mod notifications;
 
@@ -55,7 +55,7 @@ static MIGRATIONS: &[&str] = &[
     "
 CREATE TABLE notifications (
     notification_id BIGSERIAL PRIMARY KEY,
-    user_id INTEGER,
+    user_id BIGINT,
     origin_url TEXT NOT NULL,
     origin_html TEXT,
     time TIMESTAMP WITH TIME ZONE

+ 1 - 1
src/db/notifications.rs

@@ -2,7 +2,7 @@ use chrono::{DateTime, FixedOffset};
 use tokio_postgres::Client as DbClient;
 
 pub struct Notification {
-    pub user_id: i32,
+    pub user_id: i64,
     pub username: String,
     pub origin_url: String,
     pub origin_html: String,

+ 25 - 0
src/github.rs

@@ -1,5 +1,6 @@
 use anyhow::Context;
 
+use chrono::{FixedOffset, Utc};
 use futures::stream::{FuturesUnordered, StreamExt};
 use once_cell::sync::OnceCell;
 use reqwest::header::{AUTHORIZATION, USER_AGENT};
@@ -65,6 +66,21 @@ impl User {
             .map_or(false, |w| w.members.iter().any(|g| g.github == self.login));
         Ok(map["all"].members.iter().any(|g| g.github == self.login) || is_triager)
     }
+
+    // Returns the ID of the given user, if the user is in the `all` team.
+    pub async fn get_id<'a>(&'a self, client: &'a GithubClient) -> anyhow::Result<Option<usize>> {
+        let url = format!("{}/teams.json", rust_team_data::v1::BASE_URL);
+        let permission: rust_team_data::v1::Teams = client
+            .json(client.raw().get(&url))
+            .await
+            .context("could not get team data")?;
+        let map = permission.teams;
+        Ok(map["all"]
+            .members
+            .iter()
+            .find(|g| g.github == self.login)
+            .map(|u| u.github_id))
+    }
 }
 
 pub async fn get_team(
@@ -106,6 +122,7 @@ pub struct PullRequestDetails {
 pub struct Issue {
     pub number: u64,
     pub body: String,
+    created_at: chrono::DateTime<Utc>,
     title: String,
     html_url: String,
     user: User,
@@ -124,6 +141,7 @@ pub struct Comment {
     pub body: String,
     pub html_url: String,
     pub user: User,
+    pub updated_at: chrono::DateTime<Utc>,
 }
 
 #[derive(Debug)]
@@ -463,6 +481,13 @@ impl Event {
             Event::IssueComment(e) => &e.comment.user,
         }
     }
+
+    pub fn time(&self) -> chrono::DateTime<FixedOffset> {
+        match self {
+            Event::Issue(e) => e.issue.created_at.into(),
+            Event::IssueComment(e) => e.comment.updated_at.into(),
+        }
+    }
 }
 
 trait RequestSend: Sized {

+ 6 - 0
src/handlers.rs

@@ -24,6 +24,7 @@ impl fmt::Display for HandlerError {
 macro_rules! handlers {
     ($($name:ident = $handler:expr,)*) => {
         $(mod $name;)*
+        mod notification;
 
         pub async fn handle(ctx: &Context, event: &Event) -> Result<(), HandlerError> {
             $(
@@ -52,6 +53,11 @@ macro_rules! handlers {
                     )));
                 }
             })*
+
+            if let Err(e) = notification::handle(ctx, event).await {
+                log::error!("failed to process event {:?} with notification handler: {}", event, e);
+            }
+
             Ok(())
         }
     }

+ 77 - 0
src/handlers/notification.rs

@@ -0,0 +1,77 @@
+//! Purpose: Allow any user to ping a pre-selected group of people on GitHub via comments.
+//!
+//! The set of "teams" which can be pinged is intentionally restricted via configuration.
+//!
+//! Parsing is done in the `parser::command::ping` module.
+
+use crate::db::notifications;
+use crate::{
+    github::{self, Event},
+    handlers::Context,
+};
+use anyhow::Context as _;
+use regex::Regex;
+use std::convert::TryFrom;
+
+lazy_static::lazy_static! {
+    static ref PING_RE: Regex = Regex::new(r#"@([-\w\d]+)"#,).unwrap();
+}
+
+pub async fn handle(ctx: &Context, event: &Event) -> anyhow::Result<()> {
+    if let Event::Issue(e) = event {
+        if e.action != github::IssuesAction::Opened {
+            // skip events other than opening the issue to avoid retriggering commands in the
+            // issue body
+            return Ok(());
+        }
+    }
+
+    if let Event::IssueComment(e) = event {
+        if e.action != github::IssueCommentAction::Created {
+            // skip events other than creating a comment to avoid
+            // renotifying
+            //
+            // FIXME: implement smart tracking to allow rerunning only if
+            // the notification is "new" (i.e. edit adds a ping)
+            return Ok(());
+        }
+    }
+
+    let body = match event.comment_body() {
+        Some(v) => v,
+        // Skip events that don't have comment bodies associated
+        None => return Ok(()),
+    };
+
+    let caps = PING_RE
+        .captures_iter(body)
+        .map(|c| c.get(1).unwrap().as_str().to_owned())
+        .collect::<Vec<_>>();
+    for login in caps {
+        let user = github::User { login };
+        let id = user
+            .get_id(&ctx.github)
+            .await
+            .with_context(|| format!("failed to get user {} ID", user.login))?;
+        let id = match id {
+            Some(id) => id,
+            // If the user was not in the team(s) then just don't record it.
+            None => return Ok(()),
+        };
+        notifications::record_ping(
+            &ctx.db,
+            &notifications::Notification {
+                user_id: i64::try_from(id)
+                    .with_context(|| format!("user id {} out of bounds", id))?,
+                username: user.login,
+                origin_url: event.html_url().unwrap().to_owned(),
+                origin_html: body.to_owned(),
+                time: event.time(),
+            },
+        )
+        .await
+        .context("failed to record ping")?;
+    }
+
+    Ok(())
+}

+ 1 - 0
src/lib.rs

@@ -6,6 +6,7 @@ use interactions::ErrorComment;
 use std::fmt;
 
 pub mod config;
+pub mod db;
 pub mod github;
 pub mod handlers;
 pub mod interactions;

+ 1 - 2
src/main.rs

@@ -7,10 +7,9 @@ use native_tls::{Certificate, TlsConnector};
 use postgres_native_tls::MakeTlsConnector;
 use reqwest::Client;
 use std::{env, net::SocketAddr, sync::Arc};
-use triagebot::{github, handlers::Context, payload, EventName};
+use triagebot::{db, github, handlers::Context, payload, EventName};
 use uuid::Uuid;
 
-mod db;
 mod logger;
 
 async fn serve_req(req: Request<Body>, ctx: Arc<Context>) -> Result<Response<Body>, hyper::Error> {