From 015aaeb753b33921141552b2ef49cfb78f1c1a56 Mon Sep 17 00:00:00 2001 From: Dipam Sen Date: Wed, 23 Jul 2025 16:02:05 +0000 Subject: [PATCH 1/3] add slack notifications --- README.md | 1 + backend/.env.template | 2 ++ backend/Cargo.toml | 2 +- backend/src/env.rs | 3 +++ backend/src/lib.rs | 1 + backend/src/main.rs | 1 + backend/src/routing/handlers.rs | 10 ++++++++ backend/src/slack.rs | 41 +++++++++++++++++++++++++++++++++ 8 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 backend/src/slack.rs diff --git a/README.md b/README.md index 19d04759..f8fa81c1 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,7 @@ Environment variables can be set using a `.env` file. Use the `.env.template` fi ##### Configuration +- `SLACK_WEBHOOK_URL`: URL of Slack webhook for sending notifications (ignored if empty). - `MAX_UPLOAD_LIMIT`: Maximum number of files that can be uploaded at once. - `LOG_LOCATION`: The path to a local logfile. - `STATIC_FILES_URL`: The URL of the static files server. (eg: `https://static.metakgp.org`) diff --git a/backend/.env.template b/backend/.env.template index c6a2f279..78fcfa75 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -13,6 +13,8 @@ GH_ADMIN_USERNAMES= JWT_SECRET= +SLACK_WEBHOOK_URL= + MAX_UPLOAD_LIMIT=10 LOG_LOCATION=./log/application.log diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 3eacd6af..3ed62fb8 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -15,7 +15,7 @@ flate2 = "1.0" hmac = "0.12.1" http = "1.1.0" jwt = "0.16.0" -reqwest = { version = "0.12.8", default-features = false, features = ["rustls-tls"] } +reqwest = { version = "0.12.8", default-features = false, features = ["rustls-tls", "json"] } serde = { version = "1.0.210", features = ["serde_derive"] } serde_json = "1.0.128" sha2 = "0.10.8" diff --git a/backend/src/env.rs b/backend/src/env.rs index d3f37593..926d4d8c 100644 --- a/backend/src/env.rs +++ b/backend/src/env.rs @@ -51,6 +51,9 @@ pub struct EnvVars { #[arg(env, default_value = "")] /// The usernames of the admins (additional to org team members, comma separated) pub gh_admin_usernames: String, + #[arg(env, default_value = "")] + /// URL of Slack webhook for sending notifications + pub slack_webhook_url: String, // Other configs #[arg(env, default_value = "10")] diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 8b74bfbe..902d2bf0 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -4,3 +4,4 @@ pub mod env; pub mod pathutils; pub mod qp; pub mod routing; +pub mod slack; diff --git a/backend/src/main.rs b/backend/src/main.rs index c7eb4286..8c333d8e 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -11,6 +11,7 @@ mod env; mod pathutils; mod qp; mod routing; +mod slack; #[tokio::main] async fn main() -> Result<(), Box> { diff --git a/backend/src/routing/handlers.rs b/backend/src/routing/handlers.rs index 7b713f2f..dce26fbf 100644 --- a/backend/src/routing/handlers.rs +++ b/backend/src/routing/handlers.rs @@ -24,6 +24,7 @@ use crate::{ auth::{self, Auth}, pathutils::PaperCategory, qp::{self, AdminDashboardQP, Exam, WithUrl}, + slack::send_slack_message, }; use super::{AppError, BackendResponse, RouterState, Status}; @@ -383,6 +384,15 @@ pub async fn upload( }); } + let unapproved_count = state.db.get_unapproved_papers().await?.len(); + + let _ = send_slack_message( + &state.env_vars.slack_webhook_url, + upload_statuses.len(), + unapproved_count, + ) + .await; + Ok(BackendResponse::ok( format!("Successfully processed {} files", upload_statuses.len()), upload_statuses, diff --git a/backend/src/slack.rs b/backend/src/slack.rs new file mode 100644 index 00000000..4e91f758 --- /dev/null +++ b/backend/src/slack.rs @@ -0,0 +1,41 @@ +//! Utils for Slack App integration. + +use color_eyre::eyre; +use http::StatusCode; + +/// Sends a notification to the Slack channel whenever a new paper is uploaded. +pub async fn send_slack_message( + webhook_url: &str, + count: usize, + unapproved: usize, +) -> Result<(), color_eyre::eyre::Error> { + if webhook_url.is_empty() { + return Ok(()); + } + + let message = format!( + "🔔 {} uploaded to IQPS!\n\n | Total Unapproved papers: *{}*", + if count == 1 { + "A new paper was".into() + } else { + format!("{} new papers were", count) + }, + unapproved + ); + + let client = reqwest::Client::new(); + let response = client + .post(webhook_url) + .json(&serde_json::json!({ "text": message })) + .send() + .await?; + + if response.status() != StatusCode::OK { + return Err(eyre::eyre!( + "Failed to send message to Slack: {}", + response.status() + )); + } + + Ok(()) +} From a4eca27e8eaaa9be7f35bb3abad3a62bf7f52471 Mon Sep 17 00:00:00 2001 From: Dipam Sen Date: Thu, 24 Jul 2025 08:57:23 +0000 Subject: [PATCH 2/3] use separate query for finding count --- ....timestamp-1753346859087-49b436aadee65.mjs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 frontend/vite.config.ts.timestamp-1753346859087-49b436aadee65.mjs diff --git a/frontend/vite.config.ts.timestamp-1753346859087-49b436aadee65.mjs b/frontend/vite.config.ts.timestamp-1753346859087-49b436aadee65.mjs new file mode 100644 index 00000000..3718bcf6 --- /dev/null +++ b/frontend/vite.config.ts.timestamp-1753346859087-49b436aadee65.mjs @@ -0,0 +1,26 @@ +// vite.config.ts +import { defineConfig } from "file:///home/dipamsen/projects/iqps/frontend/node_modules/vite/dist/node/index.js"; +import react from "file:///home/dipamsen/projects/iqps/frontend/node_modules/@vitejs/plugin-react/dist/index.mjs"; +var vite_config_default = defineConfig(() => { + return { + plugins: [ + react() + ], + build: { + target: "esnext", + rollupOptions: { + output: { + manualChunks(id) { + if (id.includes("node_modules")) { + return "vendor"; + } + } + } + } + } + }; +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvaG9tZS9kaXBhbXNlbi9wcm9qZWN0cy9pcXBzL2Zyb250ZW5kXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvaG9tZS9kaXBhbXNlbi9wcm9qZWN0cy9pcXBzL2Zyb250ZW5kL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9ob21lL2RpcGFtc2VuL3Byb2plY3RzL2lxcHMvZnJvbnRlbmQvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJztcbmltcG9ydCByZWFjdCBmcm9tICdAdml0ZWpzL3BsdWdpbi1yZWFjdCc7XG5cbi8vIGh0dHBzOi8vdml0ZWpzLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoKCkgPT4ge1xuICByZXR1cm4ge1xuICAgIHBsdWdpbnM6IFtcbiAgICAgIHJlYWN0KClcbiAgICBdLFxuICAgIGJ1aWxkOiB7XG4gICAgICB0YXJnZXQ6ICdlc25leHQnLFxuICAgICAgcm9sbHVwT3B0aW9uczoge1xuICAgICAgICBvdXRwdXQ6IHtcbiAgICAgICAgICBtYW51YWxDaHVua3MoaWQpIHtcbiAgICAgICAgICAgIGlmIChpZC5pbmNsdWRlcygnbm9kZV9tb2R1bGVzJykpIHtcbiAgICAgICAgICAgICAgcmV0dXJuICd2ZW5kb3InO1xuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgfVxuICAgIH1cbiAgfVxufSlcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBaVMsU0FBUyxvQkFBb0I7QUFDOVQsT0FBTyxXQUFXO0FBR2xCLElBQU8sc0JBQVEsYUFBYSxNQUFNO0FBQ2hDLFNBQU87QUFBQSxJQUNMLFNBQVM7QUFBQSxNQUNQLE1BQU07QUFBQSxJQUNSO0FBQUEsSUFDQSxPQUFPO0FBQUEsTUFDTCxRQUFRO0FBQUEsTUFDUixlQUFlO0FBQUEsUUFDYixRQUFRO0FBQUEsVUFDTixhQUFhLElBQUk7QUFDZixnQkFBSSxHQUFHLFNBQVMsY0FBYyxHQUFHO0FBQy9CLHFCQUFPO0FBQUEsWUFDVDtBQUFBLFVBQ0Y7QUFBQSxRQUNGO0FBQUEsTUFDRjtBQUFBLElBQ0Y7QUFBQSxFQUNGO0FBQ0YsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K From df1d7764aa41af685e9495260e57f20e1282962d Mon Sep 17 00:00:00 2001 From: Dipam Sen Date: Thu, 24 Jul 2025 08:59:22 +0000 Subject: [PATCH 3/3] use separate query for count --- backend/src/db/mod.rs | 11 ++++++++ backend/src/db/queries.rs | 4 +++ backend/src/routing/handlers.rs | 2 +- backend/src/slack.rs | 2 +- ....timestamp-1753346859087-49b436aadee65.mjs | 26 ------------------- 5 files changed, 17 insertions(+), 28 deletions(-) delete mode 100644 frontend/vite.config.ts.timestamp-1753346859087-49b436aadee65.mjs diff --git a/backend/src/db/mod.rs b/backend/src/db/mod.rs index 611cdf75..9b56f2b4 100644 --- a/backend/src/db/mod.rs +++ b/backend/src/db/mod.rs @@ -63,6 +63,17 @@ impl Database { .collect()) } + /// Returns the number of unapproved papers + pub async fn get_unapproved_papers_count( + &self, + ) -> Result { + let count: (i64,) = sqlx::query_as(queries::GET_UNAPPROVED_COUNT) + .fetch_one(&self.connection) + .await?; + + Ok(count.0) + } + /// Searches for papers from a given query. Uses some voodoo black magic by @rajivharlalka pub async fn search_papers( &self, diff --git a/backend/src/db/queries.rs b/backend/src/db/queries.rs index 972dacc1..93d78d08 100644 --- a/backend/src/db/queries.rs +++ b/backend/src/db/queries.rs @@ -105,6 +105,10 @@ pub fn get_all_unapproved_query() -> String { format!("SELECT {} FROM iqps WHERE approve_status = false and is_deleted=false ORDER BY upload_timestamp ASC", ADMIN_DASHBOARD_QP_FIELDS) } +/// Gets the count of unapproved papers in the database +pub const GET_UNAPPROVED_COUNT: &str = + "SELECT COUNT(*) FROM iqps WHERE approve_status = false AND is_deleted = false"; + /// Returns the query for searching question papers. It is mostly voodoo, see [blog post](https://rajivharlalka.in/posts/iqps-search-development/). /// /// The `exam_filter` argument is a vector of exam types to filter. Pass empty vector to disable the filter. diff --git a/backend/src/routing/handlers.rs b/backend/src/routing/handlers.rs index dce26fbf..a40db3bc 100644 --- a/backend/src/routing/handlers.rs +++ b/backend/src/routing/handlers.rs @@ -384,7 +384,7 @@ pub async fn upload( }); } - let unapproved_count = state.db.get_unapproved_papers().await?.len(); + let unapproved_count = state.db.get_unapproved_papers_count().await?; let _ = send_slack_message( &state.env_vars.slack_webhook_url, diff --git a/backend/src/slack.rs b/backend/src/slack.rs index 4e91f758..ade9c02d 100644 --- a/backend/src/slack.rs +++ b/backend/src/slack.rs @@ -7,7 +7,7 @@ use http::StatusCode; pub async fn send_slack_message( webhook_url: &str, count: usize, - unapproved: usize, + unapproved: i64, ) -> Result<(), color_eyre::eyre::Error> { if webhook_url.is_empty() { return Ok(()); diff --git a/frontend/vite.config.ts.timestamp-1753346859087-49b436aadee65.mjs b/frontend/vite.config.ts.timestamp-1753346859087-49b436aadee65.mjs deleted file mode 100644 index 3718bcf6..00000000 --- a/frontend/vite.config.ts.timestamp-1753346859087-49b436aadee65.mjs +++ /dev/null @@ -1,26 +0,0 @@ -// vite.config.ts -import { defineConfig } from "file:///home/dipamsen/projects/iqps/frontend/node_modules/vite/dist/node/index.js"; -import react from "file:///home/dipamsen/projects/iqps/frontend/node_modules/@vitejs/plugin-react/dist/index.mjs"; -var vite_config_default = defineConfig(() => { - return { - plugins: [ - react() - ], - build: { - target: "esnext", - rollupOptions: { - output: { - manualChunks(id) { - if (id.includes("node_modules")) { - return "vendor"; - } - } - } - } - } - }; -}); -export { - vite_config_default as default -}; -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvaG9tZS9kaXBhbXNlbi9wcm9qZWN0cy9pcXBzL2Zyb250ZW5kXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvaG9tZS9kaXBhbXNlbi9wcm9qZWN0cy9pcXBzL2Zyb250ZW5kL3ZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9ob21lL2RpcGFtc2VuL3Byb2plY3RzL2lxcHMvZnJvbnRlbmQvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJztcbmltcG9ydCByZWFjdCBmcm9tICdAdml0ZWpzL3BsdWdpbi1yZWFjdCc7XG5cbi8vIGh0dHBzOi8vdml0ZWpzLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoKCkgPT4ge1xuICByZXR1cm4ge1xuICAgIHBsdWdpbnM6IFtcbiAgICAgIHJlYWN0KClcbiAgICBdLFxuICAgIGJ1aWxkOiB7XG4gICAgICB0YXJnZXQ6ICdlc25leHQnLFxuICAgICAgcm9sbHVwT3B0aW9uczoge1xuICAgICAgICBvdXRwdXQ6IHtcbiAgICAgICAgICBtYW51YWxDaHVua3MoaWQpIHtcbiAgICAgICAgICAgIGlmIChpZC5pbmNsdWRlcygnbm9kZV9tb2R1bGVzJykpIHtcbiAgICAgICAgICAgICAgcmV0dXJuICd2ZW5kb3InO1xuICAgICAgICAgICAgfVxuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgfVxuICAgIH1cbiAgfVxufSlcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBaVMsU0FBUyxvQkFBb0I7QUFDOVQsT0FBTyxXQUFXO0FBR2xCLElBQU8sc0JBQVEsYUFBYSxNQUFNO0FBQ2hDLFNBQU87QUFBQSxJQUNMLFNBQVM7QUFBQSxNQUNQLE1BQU07QUFBQSxJQUNSO0FBQUEsSUFDQSxPQUFPO0FBQUEsTUFDTCxRQUFRO0FBQUEsTUFDUixlQUFlO0FBQUEsUUFDYixRQUFRO0FBQUEsVUFDTixhQUFhLElBQUk7QUFDZixnQkFBSSxHQUFHLFNBQVMsY0FBYyxHQUFHO0FBQy9CLHFCQUFPO0FBQUEsWUFDVDtBQUFBLFVBQ0Y7QUFBQSxRQUNGO0FBQUEsTUFDRjtBQUFBLElBQ0Y7QUFBQSxFQUNGO0FBQ0YsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K