You've already forked AstralRinth
Slack webhook for payout source threshold alerts (#4353)
* Slack webhook for payout alerts * add PAYOUT_ALERT_SLACK_WEBHOOK to check_env_vars * Fix commit * Fix webhook format * Add new env vars in .env.local * Rename env vars, fire webhook on error * Fix compilation * Clippy * Fix CI * Add env vars to .env.docker-compose
This commit is contained in:
committed by
GitHub
parent
af3b829449
commit
58aac642a9
@@ -25,6 +25,11 @@ MODERATION_SLACK_WEBHOOK=
|
||||
PUBLIC_DISCORD_WEBHOOK=
|
||||
CLOUDFLARE_INTEGRATION=false
|
||||
|
||||
PAYOUT_ALERT_SLACK_WEBHOOK=none
|
||||
TREMENDOUS_BALANCE_ALERT_THRESHOLD=0
|
||||
PAYPAL_BALANCE_ALERT_THRESHOLD=0
|
||||
BREX_BALANCE_ALERT_THRESHOLD=0
|
||||
|
||||
STORAGE_BACKEND=local
|
||||
MOCK_FILE_PATH=/tmp/modrinth
|
||||
|
||||
|
||||
@@ -25,6 +25,11 @@ MODERATION_SLACK_WEBHOOK=
|
||||
PUBLIC_DISCORD_WEBHOOK=
|
||||
CLOUDFLARE_INTEGRATION=false
|
||||
|
||||
PAYOUT_ALERT_SLACK_WEBHOOK=none
|
||||
TREMENDOUS_BALANCE_ALERT_THRESHOLD=0
|
||||
PAYPAL_BALANCE_ALERT_THRESHOLD=0
|
||||
BREX_BALANCE_ALERT_THRESHOLD=0
|
||||
|
||||
STORAGE_BACKEND=local
|
||||
MOCK_FILE_PATH=/tmp/modrinth
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ pub mod email;
|
||||
pub mod oauth;
|
||||
pub mod templates;
|
||||
pub mod validate;
|
||||
pub use crate::auth::email::send_email;
|
||||
pub use checks::{
|
||||
filter_enlisted_projects_ids, filter_enlisted_version_ids,
|
||||
filter_visible_collections, filter_visible_project_ids,
|
||||
filter_visible_projects,
|
||||
};
|
||||
pub use email::send_email;
|
||||
use serde::{Deserialize, Serialize};
|
||||
// pub use pat::{generate_pat, PersonalAccessToken};
|
||||
pub use validate::{check_is_moderator_from_headers, get_user_from_headers};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::queue::payouts::{
|
||||
PayoutsQueue, insert_bank_balances, process_payout,
|
||||
PayoutsQueue, insert_bank_balances_and_webhook, process_payout,
|
||||
};
|
||||
use crate::search::indexing::index_projects;
|
||||
use crate::{database, search};
|
||||
@@ -59,7 +59,7 @@ impl BackgroundTask {
|
||||
pub async fn update_bank_balances(pool: sqlx::Pool<Postgres>) {
|
||||
let payouts_queue = PayoutsQueue::new();
|
||||
|
||||
match insert_bank_balances(&payouts_queue, &pool).await {
|
||||
match insert_bank_balances_and_webhook(&payouts_queue, &pool).await {
|
||||
Ok(_) => info!("Bank balances updated successfully"),
|
||||
Err(error) => error!(%error, "Bank balances update failed"),
|
||||
}
|
||||
|
||||
@@ -489,6 +489,8 @@ pub fn check_env_vars() -> bool {
|
||||
|
||||
failed |= check_var::<String>("COMPLIANCE_PAYOUT_THRESHOLD");
|
||||
|
||||
failed |= check_var::<String>("PAYOUT_ALERT_SLACK_WEBHOOK");
|
||||
|
||||
failed |= check_var::<String>("ARCHON_URL");
|
||||
|
||||
failed
|
||||
|
||||
@@ -665,7 +665,7 @@ impl AutomatedModerationQueue {
|
||||
.await?;
|
||||
|
||||
if let Ok(webhook_url) = dotenvy::var("MODERATION_SLACK_WEBHOOK") {
|
||||
crate::util::webhook::send_slack_webhook(
|
||||
crate::util::webhook::send_slack_project_webhook(
|
||||
project.inner.id.into(),
|
||||
&pool,
|
||||
&redis,
|
||||
|
||||
@@ -4,12 +4,16 @@ use crate::models::payouts::{
|
||||
};
|
||||
use crate::models::projects::MonetizationStatus;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::webhook::{
|
||||
PayoutSourceAlertType, send_slack_payout_source_alert_webhook,
|
||||
};
|
||||
use base64::Engine;
|
||||
use chrono::{DateTime, Datelike, Duration, NaiveTime, TimeZone, Utc};
|
||||
use dashmap::DashMap;
|
||||
use futures::TryStreamExt;
|
||||
use reqwest::Method;
|
||||
use rust_decimal::Decimal;
|
||||
use rust_decimal::prelude::ToPrimitive;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
@@ -1080,29 +1084,34 @@ pub async fn insert_payouts(
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn insert_bank_balances(
|
||||
pub async fn insert_bank_balances_and_webhook(
|
||||
payouts: &PayoutsQueue,
|
||||
pool: &PgPool,
|
||||
) -> Result<(), ApiError> {
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let paypal = PayoutsQueue::get_paypal_balance()
|
||||
.await
|
||||
.inspect_err(|error| error!(%error, "Failure getting PayPal balance"))
|
||||
.ok();
|
||||
let paypal_result = PayoutsQueue::get_paypal_balance().await;
|
||||
let brex_result = PayoutsQueue::get_brex_balance().await;
|
||||
let tremendous_result = payouts.get_tremendous_balance().await;
|
||||
|
||||
let brex = PayoutsQueue::get_brex_balance()
|
||||
.await
|
||||
.inspect_err(|error| error!(%error, "Failure getting Brex balance"))
|
||||
.ok();
|
||||
|
||||
let tremendous = payouts
|
||||
.get_tremendous_balance()
|
||||
.await
|
||||
.inspect_err(
|
||||
|error| error!(%error, "Failure getting Tremendous balance"),
|
||||
)
|
||||
.ok();
|
||||
let paypal = check_balance_with_webhook(
|
||||
"paypal",
|
||||
"PAYPAL_BALANCE_ALERT_THRESHOLD",
|
||||
paypal_result,
|
||||
)
|
||||
.await?;
|
||||
let brex = check_balance_with_webhook(
|
||||
"brex",
|
||||
"BREX_BALANCE_ALERT_THRESHOLD",
|
||||
brex_result,
|
||||
)
|
||||
.await?;
|
||||
let tremendous = check_balance_with_webhook(
|
||||
"tremendous",
|
||||
"TREMENDOUS_BALANCE_ALERT_THRESHOLD",
|
||||
tremendous_result,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut insert_account_types = Vec::new();
|
||||
let mut insert_amounts = Vec::new();
|
||||
@@ -1112,29 +1121,26 @@ pub async fn insert_bank_balances(
|
||||
let now = Utc::now();
|
||||
let today = now.date_naive().and_time(NaiveTime::MIN).and_utc();
|
||||
|
||||
let mut add_balance =
|
||||
|account_type: &str, balance: Option<AccountBalance>| {
|
||||
if let Some(balance) = balance {
|
||||
insert_account_types.push(account_type.to_string());
|
||||
insert_amounts.push(balance.available);
|
||||
insert_pending.push(false);
|
||||
insert_recorded.push(today);
|
||||
let mut add_balance = |account_type: &str, balance: &AccountBalance| {
|
||||
insert_account_types.push(account_type.to_string());
|
||||
insert_amounts.push(balance.available);
|
||||
insert_pending.push(false);
|
||||
insert_recorded.push(today);
|
||||
|
||||
insert_account_types.push(account_type.to_string());
|
||||
insert_amounts.push(balance.pending);
|
||||
insert_pending.push(true);
|
||||
insert_recorded.push(today);
|
||||
}
|
||||
};
|
||||
insert_account_types.push(account_type.to_string());
|
||||
insert_amounts.push(balance.pending);
|
||||
insert_pending.push(true);
|
||||
insert_recorded.push(today);
|
||||
};
|
||||
|
||||
if let Some(paypal) = paypal {
|
||||
add_balance("paypal", paypal);
|
||||
add_balance("paypal", &paypal);
|
||||
}
|
||||
if let Some(brex) = brex {
|
||||
add_balance("brex", brex);
|
||||
add_balance("brex", &brex);
|
||||
}
|
||||
if let Some(tremendous) = tremendous {
|
||||
add_balance("tremendous", tremendous);
|
||||
add_balance("tremendous", &tremendous);
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
@@ -1156,3 +1162,54 @@ pub async fn insert_bank_balances(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_balance_with_webhook(
|
||||
source: &str,
|
||||
threshold_env_var_name: &str,
|
||||
result: Result<Option<AccountBalance>, ApiError>,
|
||||
) -> Result<Option<AccountBalance>, ApiError> {
|
||||
let maybe_threshold = dotenvy::var(threshold_env_var_name)
|
||||
.ok()
|
||||
.and_then(|x| x.parse::<u64>().ok())
|
||||
.filter(|x| *x != 0);
|
||||
let payout_alert_webhook = dotenvy::var("PAYOUT_ALERT_SLACK_WEBHOOK")?;
|
||||
|
||||
match &result {
|
||||
Ok(Some(account_balance)) => {
|
||||
if let Some(threshold) = maybe_threshold
|
||||
&& let Some(available) =
|
||||
account_balance.available.trunc().to_u64()
|
||||
&& available <= threshold
|
||||
{
|
||||
send_slack_payout_source_alert_webhook(
|
||||
PayoutSourceAlertType::UnderThreshold {
|
||||
source: source.to_owned(),
|
||||
threshold,
|
||||
current_balance: available,
|
||||
},
|
||||
&payout_alert_webhook,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Err(error) => {
|
||||
error!(%error, "Failure getting balance for payout source '{source}'");
|
||||
|
||||
if maybe_threshold.is_some() {
|
||||
send_slack_payout_source_alert_webhook(
|
||||
PayoutSourceAlertType::CheckFailure {
|
||||
source: source.to_owned(),
|
||||
display_error: error.to_string(),
|
||||
},
|
||||
&payout_alert_webhook,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(result.ok().flatten())
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ pub async fn delphi_result_ingest(
|
||||
}
|
||||
}
|
||||
|
||||
crate::util::webhook::send_slack_webhook(
|
||||
crate::util::webhook::send_slack_project_webhook(
|
||||
body.project_id,
|
||||
&pool,
|
||||
&redis,
|
||||
|
||||
@@ -119,6 +119,8 @@ pub enum ApiError {
|
||||
Payments(String),
|
||||
#[error("Discord Error: {0}")]
|
||||
Discord(String),
|
||||
#[error("Slack Webhook Error: {0}")]
|
||||
Slack(String),
|
||||
#[error("Captcha Error. Try resubmitting the form.")]
|
||||
Turnstile,
|
||||
#[error("Error while decoding Base62: {0}")]
|
||||
@@ -182,6 +184,7 @@ impl ApiError {
|
||||
ApiError::Io(..) => "io_error",
|
||||
ApiError::RateLimitError(..) => "ratelimit_error",
|
||||
ApiError::Stripe(..) => "stripe_error",
|
||||
ApiError::Slack(..) => "slack_error",
|
||||
},
|
||||
description: self.to_string(),
|
||||
}
|
||||
@@ -220,6 +223,7 @@ impl actix_web::ResponseError for ApiError {
|
||||
ApiError::Io(..) => StatusCode::BAD_REQUEST,
|
||||
ApiError::RateLimitError(..) => StatusCode::TOO_MANY_REQUESTS,
|
||||
ApiError::Stripe(..) => StatusCode::FAILED_DEPENDENCY,
|
||||
ApiError::Slack(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -434,7 +434,7 @@ pub async fn project_edit(
|
||||
if user.role.is_mod()
|
||||
&& let Ok(webhook_url) = dotenvy::var("MODERATION_SLACK_WEBHOOK")
|
||||
{
|
||||
crate::util::webhook::send_slack_webhook(
|
||||
crate::util::webhook::send_slack_project_webhook(
|
||||
project_item.inner.id.into(),
|
||||
&pool,
|
||||
&redis,
|
||||
|
||||
@@ -178,7 +178,76 @@ async fn get_webhook_metadata(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_slack_webhook(
|
||||
pub enum PayoutSourceAlertType {
|
||||
UnderThreshold {
|
||||
source: String,
|
||||
threshold: u64,
|
||||
current_balance: u64,
|
||||
},
|
||||
CheckFailure {
|
||||
source: String,
|
||||
display_error: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl PayoutSourceAlertType {
|
||||
pub fn message(&self) -> String {
|
||||
match self {
|
||||
PayoutSourceAlertType::UnderThreshold {
|
||||
source,
|
||||
threshold,
|
||||
current_balance,
|
||||
} => format!(
|
||||
"\u{1f6a8} *Payout Source Alert*\n\nPayout source '{source}' has an available balance under the ${threshold} threshold.\nBalance: ${current_balance}."
|
||||
),
|
||||
PayoutSourceAlertType::CheckFailure {
|
||||
source,
|
||||
display_error,
|
||||
} => format!(
|
||||
"\u{1f6a8} *Payout Source Alert*\n\nFAILED TO CHECK payout source '{source}' balance.\nError: {display_error}"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_slack_payout_source_alert_webhook(
|
||||
alert: PayoutSourceAlertType,
|
||||
webhook_url: &str,
|
||||
) -> Result<(), ApiError> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
client
|
||||
.post(webhook_url)
|
||||
.json(&serde_json::json!({
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": alert.message()
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "context",
|
||||
"elements": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": format!("via labrinth • <!date^{}^{{date_short_pretty}} at {{time}}|Unknown date>", Utc::now().timestamp())
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ApiError::Slack("Error while sending projects webhook".to_string())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_slack_project_webhook(
|
||||
project_id: ProjectId,
|
||||
pool: &PgPool,
|
||||
redis: &RedisPool,
|
||||
@@ -288,7 +357,7 @@ pub async fn send_slack_webhook(
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ApiError::Discord(
|
||||
ApiError::Slack(
|
||||
"Error while sending projects webhook".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Reference in New Issue
Block a user