#[macro_use] extern crate log; use anyhow::Result; use axum::async_trait; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use vkteams_bot::bot::webhook::{AppState, WebhookState}; use vkteams_bot::prelude::*; // Environment variable for the chat id const CHAT_ID: &str = "VKTEAMS_CHAT_ID"; const TMPL_NAME: &str = "alert"; // define the Tera template lazy_static::lazy_static! { static ref TEMPLATES:tera::Tera = { let mut tera = tera::Tera::default(); tera.add_template_file( format!("examples/templates/{TMPL_NAME}.tmpl"), Some(TMPL_NAME), ) .unwrap(); tera }; } #[derive(Debug, Clone)] pub struct ExtendState { bot: Bot, chat_id: ChatId, path: String, } // Must implement FromRef trait to extract the substate impl axum::extract::FromRef> for ExtendState { fn from_ref(state: &AppState) -> Self { state.ext.to_owned() } } // Default implementation for the ExtendState impl Default for ExtendState { fn default() -> Self { let bot = Bot::default(); let chat_id = ChatId( std::env::var(CHAT_ID) .expect("Unable to find VKTEAMS_CHAT_ID in .env file") .to_string(), ); let path = format!("/alert/{chat_id}"); Self { bot, chat_id, path } } } #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct PrometheusMessage { pub alerts: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub common_annotations: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub common_labels: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub external_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub group_key: Option, #[serde(skip_serializing_if = "Option::is_none")] pub group_labels: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub truncated_alerts: Option, pub receiver: String, pub status: AlertStatus, pub version: String, } #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct Alert { pub annotations: HashMap, #[serde(skip_serializing_if = "Option::is_none")] pub ends_at: Option, #[serde(skip_serializing_if = "Option::is_none")] pub fingerprint: Option, #[serde(skip_serializing_if = "Option::is_none")] pub starts_at: Option, #[serde(skip_serializing_if = "Option::is_none")] pub status: Option, pub labels: HashMap, } #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub enum AlertStatus { Resolved, Firing, } // Must implement WebhookState trait to handle the webhook #[async_trait] impl WebhookState for ExtendState { type WebhookType = PrometheusMessage; fn get_path(&self) -> String { self.path.clone() } async fn handler(&self, msg: Self::WebhookType) -> Result<()> { // Parse the webhook message and render inti template let parser = MessageTextParser::from_tmpl(TEMPLATES.to_owned()).set_ctx(msg, TMPL_NAME); // Make request for bot API let req = RequestMessagesSendText::new(self.chat_id.to_owned()).set_text(parser); // Send request to the bot API match self.bot.send_api_request(req).await { Ok(_) => Ok(()), Err(e) => Err(e.into()), } } } #[tokio::main] pub async fn main() -> Result<()> { // Load .env file dotenvy::dotenv().expect("unable to load .env file"); // Initialize logger pretty_env_logger::init(); info!("Starting..."); // Run the web app vkteams_bot::bot::webhook::run_app(ExtendState::default()) .await .unwrap(); Ok(()) }