#![forbid(unsafe_code)] use log; use teloxide_core::{prelude::*, types::{Recipient, Chat}}; use teloxide::Bot; use tide::{Server, Request, Response}; use tokio::fs; mod gitea; const CHAT_ID: &str = dotenvy_macro::dotenv!("CHAT_ID", "Chat ID is not set!"); const TG_KEY: &str = dotenvy_macro::dotenv!("TG_KEY", "Telegram key is not set!"); const WH_SECRET: &str = dotenvy_macro::dotenv!("WH_SECRET", "Webhook secret is not set!"); const WH_LISTEN_URL: &str = dotenvy_macro::dotenv!("WH_URL", "Webhook listen URL is not set!"); #[derive(Debug, Clone)] struct SharedState { bot: Bot, chat: Chat } async fn start_tg() -> (Bot, Chat) { log::info!("Starting up telegram bot..."); let bot = Bot::new(TG_KEY); let me = bot.get_me().await.unwrap(); log::info!("Logged in to telegram as \"{}\"", me.first_name); let chat = bot.get_chat(Recipient::Id(ChatId(CHAT_ID.parse::().unwrap()))).await.unwrap(); log::info!("Using chat {}", { if chat.is_group() { chat.title().unwrap() } else { chat.first_name().unwrap() } }); (bot, chat) } async fn start_http(state: SharedState) -> Server { let mut app = tide::with_state(state); app.at("/pull_wh").post(webhook); app } async fn webhook(mut req: Request) -> tide::Result { let body = req.body_string().await.unwrap(); let pr = serde_json::from_str::(body.as_str()); let event_type = req.header("X-Gitea-Event-Type"); if event_type.is_none() { return Ok( Response::builder(400) .body("{\"error\":\"no event type (X-Gitea-Event-Type)\"}") .content_type("application/json") .build() ) } let event_type = event_type.unwrap().get(0).unwrap().to_string(); if event_type != "pull_request" { return Ok( Response::builder(200) .body("{\"status\":\"ignoring non-pr event\"}") .content_type("application/json") .build() ) } if pr.is_err() { return Ok(format!("Bad serialization: {}", pr.unwrap_err().to_string()).into()); } let pr = pr.unwrap(); let secret = req.header("Authorization"); if secret.is_none() { return Ok( Response::builder(401) // { error: "bad auth" } .body("{\"error\":\"bad auth\"}") .content_type("application/json") .build() ) } let secret = secret.unwrap().get(0).unwrap().to_string(); if secret != "Bearer ".to_string() + WH_SECRET { println!("{} != {}", secret, "Bearer ".to_string() + WH_SECRET); return Ok( Response::builder(401) .body("{\"error\":\"bad auth\"}") .content_type("application/json") .build() ) } if ! fs::try_exists(".pr-cache").await.unwrap() { fs::write(".pr-cache", "[]").await.unwrap(); } let cache = fs::read_to_string(".pr-cache").await.unwrap(); let cache = serde_json::from_str::>(&cache).unwrap(); if cache.iter().find(|x| **x == pr.pull_request.id as u64).is_some() { return Ok( Response::builder(200) .body("{\"status\":\"ignoring known PR\"}") .content_type("application/json") .build() ) } else { let mut cache = cache; cache.push(pr.pull_request.id as u64); fs::write(".pr-cache", serde_json::to_string(&cache).unwrap()).await.unwrap(); } let state = req.state().clone(); state.bot.send_message(state.chat.id, format!("New PR\n{} ({}#{}) by {}\n{}", pr.pull_request.title, pr.pull_request.head.repo.name, pr.pull_request.number, pr.pull_request.user.login, pr.pull_request.url)).await.unwrap(); state.bot.send_message(state.chat.id, "апрувните пж @bleki42 @balistiktw @x3paerz").await.unwrap(); Ok( Response::builder(200) .body("{ \"status\": \"sent\" }") .content_type("application/json") .build() ) } #[tokio::main] async fn main() { #[cfg(debug_assertions)] femme::with_level(log::LevelFilter::Debug); #[cfg(not(debug_assertions))] { femme::with_level(log::LevelFilter::Info); log::info!("Running in production"); } let (bot, chat) = start_tg().await; let http = start_http(SharedState { bot, chat }).await; log::info!("Listening for webhooks on {}", WH_LISTEN_URL); http.listen(WH_LISTEN_URL.clone()).await.unwrap(); }