feat: 支持自定义 webhook 模板,支持发送测试信息 (#551)

This commit is contained in:
ᴀᴍᴛᴏᴀᴇʀ
2025-12-05 00:21:36 +08:00
committed by GitHub
parent e76673d076
commit 7ef38a38ed
7 changed files with 114 additions and 13 deletions

View File

@@ -1,16 +1,20 @@
use std::sync::Arc;
use anyhow::Result;
use axum::Router;
use axum::extract::Extension;
use axum::routing::get;
use axum::routing::{get, post};
use axum::{Json, Router};
use sea_orm::DatabaseConnection;
use crate::api::wrapper::{ApiError, ApiResponse, ValidatedJson};
use crate::bilibili::BiliClient;
use crate::config::{Config, VersionedConfig};
use crate::notifier::Notifier;
pub(super) fn router() -> Router {
Router::new().route("/config", get(get_config).put(update_config))
Router::new()
.route("/config", get(get_config).put(update_config))
.route("/config/notifiers/ping", post(ping_notifiers))
}
/// 获取全局配置
@@ -27,3 +31,17 @@ pub async fn update_config(
let new_config = VersionedConfig::get().update(config, &db).await?;
Ok(ApiResponse::ok(new_config))
}
pub async fn ping_notifiers(
Extension(bili_client): Extension<Arc<BiliClient>>,
Json(mut notifier): Json<Notifier>,
) -> Result<ApiResponse<()>, ApiError> {
// 对于 webhook 类型的通知器测试,设置上 ignore_cache tag 以强制实时渲染
if let Notifier::Webhook { ignore_cache, .. } = &mut notifier {
*ignore_cache = Some(());
}
notifier
.notify(bili_client.inner_client(), "This is a test notification from BiliSync.")
.await?;
Ok(ApiResponse::ok(()))
}

View File

@@ -5,6 +5,7 @@ use handlebars::handlebars_helper;
use crate::config::versioned_cache::VersionedCache;
use crate::config::{Config, PathSafeTemplate};
use crate::notifier::{Notifier, webhook_template_content, webhook_template_key};
pub static TEMPLATE: LazyLock<VersionedCache<handlebars::Handlebars<'static>>> =
LazyLock::new(|| VersionedCache::new(create_template).expect("Failed to create handlebars template"));
@@ -17,6 +18,13 @@ fn create_template(config: &Config) -> Result<handlebars::Handlebars<'static>> {
handlebars.path_safe_register("favorite_default_path", config.favorite_default_path.clone())?;
handlebars.path_safe_register("collection_default_path", config.collection_default_path.clone())?;
handlebars.path_safe_register("submission_default_path", config.submission_default_path.clone())?;
if let Some(notifiers) = &config.notifiers {
for notifier in notifiers.iter() {
if let Notifier::Webhook { url, template, .. } = notifier {
handlebars.register_template_string(&webhook_template_key(url), webhook_template_content(template))?;
}
}
}
Ok(handlebars)
}

View File

@@ -1,17 +1,35 @@
use anyhow::Result;
use futures::future;
use reqwest::header;
use serde::{Deserialize, Serialize};
use crate::config::TEMPLATE;
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum Notifier {
Telegram { bot_token: String, chat_id: String },
Webhook { url: String },
Telegram {
bot_token: String,
chat_id: String,
},
Webhook {
url: String,
template: Option<String>,
#[serde(skip)]
// 一个内部辅助字段,用于决定是否强制渲染当前模板,在测试时使用
ignore_cache: Option<()>,
},
}
#[derive(Serialize)]
struct WebhookPayload<'a> {
text: &'a str,
pub fn webhook_template_key(url: &str) -> String {
format!("payload_{}", url)
}
pub fn webhook_template_content(template: &Option<String>) -> &str {
template
.as_deref()
.filter(|t| !t.trim().is_empty())
.unwrap_or(r#"{"text": "{{{message}}}"}"#)
}
pub trait NotifierAllExt {
@@ -33,9 +51,28 @@ impl Notifier {
let params = [("chat_id", chat_id.as_str()), ("text", message)];
client.post(&url).form(&params).send().await?;
}
Notifier::Webhook { url } => {
let payload = WebhookPayload { text: message };
client.post(url).json(&payload).send().await?;
Notifier::Webhook {
url,
template,
ignore_cache,
} => {
let key = webhook_template_key(url);
let data = serde_json::json!(
{
"message": message,
}
);
let handlebar = TEMPLATE.read();
let payload = match ignore_cache {
Some(_) => handlebar.render_template(webhook_template_content(template), &data)?,
None => handlebar.render(&key, &data)?,
};
client
.post(url)
.header(header::CONTENT_TYPE, "application/json")
.body(payload)
.send()
.await?;
}
}
Ok(())