Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
2 changes: 2 additions & 0 deletions backend/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ GH_ADMIN_USERNAMES=

JWT_SECRET=

SLACK_WEBHOOK_URL=

MAX_UPLOAD_LIMIT=10
LOG_LOCATION=./log/application.log

Expand Down
2 changes: 1 addition & 1 deletion backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
11 changes: 11 additions & 0 deletions backend/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ impl Database {
.collect())
}

/// Returns the number of unapproved papers
pub async fn get_unapproved_papers_count(
&self,
) -> Result<i64, sqlx::Error> {
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,
Expand Down
4 changes: 4 additions & 0 deletions backend/src/db/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions backend/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
1 change: 1 addition & 0 deletions backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ pub mod env;
pub mod pathutils;
pub mod qp;
pub mod routing;
pub mod slack;
1 change: 1 addition & 0 deletions backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod env;
mod pathutils;
mod qp;
mod routing;
mod slack;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
Expand Down
10 changes: 10 additions & 0 deletions backend/src/routing/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -383,6 +384,15 @@ pub async fn upload(
});
}

let unapproved_count = state.db.get_unapproved_papers_count().await?;

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,
Expand Down
41 changes: 41 additions & 0 deletions backend/src/slack.rs
Original file line number Diff line number Diff line change
@@ -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: i64,
) -> Result<(), color_eyre::eyre::Error> {
if webhook_url.is_empty() {
return Ok(());
}

let message = format!(
"🔔 {} uploaded to IQPS!\n\n<https://qp.metakgp.org/admin|Review> | 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(())
}