Something went wrong. Try again.
Our Personal Data Server from scratch!
Something went wrong. Try again.
5.4 kB · 185 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186use axum::{ extract::State, http::{HeaderMap, StatusCode}, response::IntoResponse,};use serde::Deserialize;use tracing::{debug, info, warn};
use tranquil_pds::comms::comms_repo;use tranquil_pds::state::AppState;
#[derive(Deserialize)]struct TelegramUpdate { message: Option<TelegramMessage>,}
#[derive(Deserialize)]struct TelegramMessage { text: Option<String>, from: Option<TelegramUser>,}
#[derive(Deserialize)]struct TelegramUser { id: i64, username: Option<String>,}
pub async fn handle_telegram_webhook( State(state): State<AppState>, headers: HeaderMap, body: String,) -> impl IntoResponse { let expected_secret = match &tranquil_config::get().telegram.webhook_secret { Some(s) => s.clone(), None => { warn!("Telegram webhook called but TELEGRAM_WEBHOOK_SECRET is not configured"); return StatusCode::FORBIDDEN; } }; let provided = headers .get("x-telegram-bot-api-secret-token") .and_then(|v| v.to_str().ok()) .unwrap_or_default(); if provided != expected_secret { warn!("Telegram webhook received with invalid secret token"); return StatusCode::UNAUTHORIZED; }
let update: TelegramUpdate = match serde_json::from_str(&body) { Ok(u) => u, Err(_) => return StatusCode::OK, };
if let Some(message) = update.message { let is_start = message .text .as_deref() .is_some_and(|t| t.starts_with("/start"));
if is_start && let Some(from) = message.from && let Some(username) = from.username { let handle = match parse_start_handle(message.text.as_deref()) .map(tranquil_types::Handle::new) .transpose() { Ok(h) => h, Err(e) => { warn!( telegram_username = %username, error = %e, "Ignoring /start with an invalid handle" ); return StatusCode::OK; } };
debug!( telegram_username = %username, chat_id = from.id, handle = ?handle, "Received /start from Telegram user" ); match state .repos .user .store_telegram_chat_id(&username, from.id, handle.as_ref()) .await { Ok(Some(user_id)) => { info!( telegram_username = %username, chat_id = from.id, "Verified Telegram user and stored chat_id" ); if let Err(e) = comms_repo::enqueue_channel_verified( state.repos.user.as_ref(), state.repos.infra.as_ref(), user_id, tranquil_db_traits::CommsChannel::Telegram, &from.id.to_string(), &tranquil_config::get().server.hostname, ) .await { warn!(error = %e, "Failed to enqueue channel verified notification"); } } Ok(None) => { debug!( telegram_username = %username, "No matching user found for Telegram username" ); } Err(e) => { warn!( telegram_username = %username, error = %e, "Failed to store Telegram chat_id" ); } } } }
StatusCode::OK}
fn parse_start_handle(text: Option<&str>) -> Option<String> { text.and_then(|t| t.strip_prefix("/start ")) .map(|payload| payload.trim()) .filter(|p| !p.is_empty()) .map(|payload| payload.replace('_', "."))}
#[cfg(test)]mod tests { use super::*;
#[test] fn deep_link_underscores_decoded_to_dots() { assert_eq!( parse_start_handle(Some("/start lewis_buttercup_wizardry_systems")), Some("lewis.buttercup.wizardry.systems".to_string()), ); }
#[test] fn manual_handle_with_dots_passes_through() { assert_eq!( parse_start_handle(Some("/start lewis.buttercup.wizardry.systems")), Some("lewis.buttercup.wizardry.systems".to_string()), ); }
#[test] fn bare_start_returns_none() { assert_eq!(parse_start_handle(Some("/start")), None); }
#[test] fn start_with_trailing_space_returns_none() { assert_eq!(parse_start_handle(Some("/start ")), None); }
#[test] fn none_text_returns_none() { assert_eq!(parse_start_handle(None), None); }
#[test] fn non_start_command_returns_none() { assert_eq!(parse_start_handle(Some("/help")), None); }
#[test] fn payload_with_extra_whitespace_trimmed() { assert_eq!( parse_start_handle(Some("/start alice_example_com ")), Some("alice.example.com".to_string()), ); }}