Added telegram bot WEB App
Build and Publish / Build and Publish Docker Image (push) Successful in 6m0s

This commit is contained in:
Ultradesu
2026-07-01 12:44:53 +03:00
parent 3a4bc23a58
commit 77cde17ef9
10 changed files with 710 additions and 63 deletions
+71 -32
View File
@@ -151,6 +151,9 @@ struct TelegramLinkStatusResponse {
bot_url: String,
linked: bool,
declined: bool,
pending: bool,
pending_secret: Option<String>,
pending_expires_at: Option<i64>,
telegram_id: Option<String>,
}
@@ -164,11 +167,6 @@ struct TelegramWebAppLinkRequest {
init_data: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
struct TelegramManualLinkRequest {
telegram_id: String,
}
const TELEGRAM_INIT_DATA_MAX_AGE: Duration = Duration::from_secs(86_400);
async fn vpn_clients_handler(
@@ -294,10 +292,9 @@ async fn telegram_link_webapp_handler(
.into_response()
}
async fn telegram_link_manual_handler(
async fn telegram_link_start_handler(
session: Session,
db: Database,
Json(request): Json<TelegramManualLinkRequest>,
) -> cot::Result<cot::response::Response> {
let mut user = match api_user_record(&session, &db).await? {
Ok(user) => user,
@@ -314,12 +311,15 @@ async fn telegram_link_manual_handler(
));
}
let telegram_id = match normalize_manual_telegram_id(&request.telegram_id) {
Ok(value) => value,
Err(response) => return Ok(response),
};
if let Some(response) = link_telegram_id(&db, &mut user, &telegram_id).await? {
return Ok(response);
let code = create_unique_telegram_link_code(&db).await?;
let now = telegram::now_unix_seconds();
user.set_telegram_link_code(&db, Some(&code), Some(now))
.await
.map_err(|e| cot::Error::internal(format!("failed to save Telegram link code: {e}")))?;
if user.telegram_id() == Some("") {
user.set_telegram_id(&db, None).await.map_err(|e| {
cot::Error::internal(format!("failed to reset Telegram preference: {e}"))
})?;
}
Json(TelegramLinkResponse {
@@ -347,7 +347,7 @@ async fn telegram_link_decline_handler(
));
}
user.set_telegram_id(&db, Some(""))
user.decline_telegram_link(&db)
.await
.map_err(|e| cot::Error::internal(format!("failed to save Telegram preference: {e}")))?;
@@ -357,6 +357,26 @@ async fn telegram_link_decline_handler(
.into_response()
}
async fn telegram_link_unlink_handler(
session: Session,
db: Database,
) -> cot::Result<cot::response::Response> {
let mut user = match api_user_record(&session, &db).await? {
Ok(user) => user,
Err(response) => return Ok(response),
};
let (config, _) = AppConfig::load_with_db(&db).await;
user.clear_telegram_link(&db)
.await
.map_err(|e| cot::Error::internal(format!("failed to clear Telegram link: {e}")))?;
Json(TelegramLinkResponse {
status: telegram_status_response(&config, &user),
})
.into_response()
}
async fn create_vpn_client_handler(
session: Session,
db: Database,
@@ -626,6 +646,17 @@ fn telegram_status_response(config: &AppConfig, user: &User) -> TelegramLinkStat
.as_deref()
.is_some_and(|value| !value.is_empty());
let declined = telegram_id.as_deref() == Some("");
let pending = user.telegram_link_code().is_some()
&& telegram::link_code_is_active(user.telegram_link_code_created_at());
let pending_secret = pending
.then(|| user.telegram_link_code().map(str::to_owned))
.flatten();
let pending_expires_at = pending
.then(|| {
user.telegram_link_code_created_at()
.map(telegram::link_code_expires_at)
})
.flatten();
TelegramLinkStatusResponse {
enabled: config.telegram_bot_enabled,
@@ -633,6 +664,9 @@ fn telegram_status_response(config: &AppConfig, user: &User) -> TelegramLinkStat
bot_username,
linked,
declined,
pending,
pending_secret,
pending_expires_at,
telegram_id: linked.then_some(telegram_id).flatten(),
}
}
@@ -649,21 +683,21 @@ fn telegram_bot_url(username: &str) -> String {
}
}
fn normalize_manual_telegram_id(telegram_id: &str) -> Result<String, cot::response::Response> {
let telegram_id = telegram_id.trim();
if telegram_id.is_empty()
|| telegram_id.len() > 32
|| !telegram_id.bytes().all(|byte| byte.is_ascii_digit())
{
return Err(json_error_typed(
cot::http::StatusCode::BAD_REQUEST,
"telegram_id_invalid",
"Telegram ID is invalid",
"Paste the numeric Telegram ID returned by the bot.",
"",
));
async fn create_unique_telegram_link_code(db: &Database) -> cot::Result<String> {
for _ in 0..8 {
let code = telegram::generate_link_code().map_err(cot::Error::internal)?;
let existing = User::get_by_telegram_link_code(db, &code)
.await
.map_err(|e| {
cot::Error::internal(format!("failed to check Telegram link code: {e}"))
})?;
if existing.is_none() {
return Ok(code);
}
}
Ok(telegram_id.to_owned())
Err(cot::Error::internal(
"failed to generate unique Telegram link code",
))
}
async fn link_telegram_id(
@@ -686,7 +720,7 @@ async fn link_telegram_id(
}
}
user.set_telegram_id(db, Some(telegram_id))
user.complete_telegram_link(db, telegram_id)
.await
.map_err(|e| cot::Error::internal(format!("failed to save Telegram ID: {e}")))?;
Ok(None)
@@ -798,15 +832,20 @@ impl App for ApiApp {
"api_telegram_link_webapp",
),
Route::with_api_handler_and_name(
"/telegram-link/manual",
api_post(telegram_link_manual_handler),
"api_telegram_link_manual",
"/telegram-link/start",
api_post(telegram_link_start_handler),
"api_telegram_link_start",
),
Route::with_api_handler_and_name(
"/telegram-link/decline",
api_post(telegram_link_decline_handler),
"api_telegram_link_decline",
),
Route::with_api_handler_and_name(
"/telegram-link/unlink",
api_post(telegram_link_unlink_handler),
"api_telegram_link_unlink",
),
Route::with_api_handler_and_name(
"/vpn-clients/sync",
api_post(sync_vpn_clients_handler),