Bug fixes
This commit is contained in:
@@ -124,8 +124,28 @@ struct BotContext {
|
||||
sem: Arc<tokio::sync::Semaphore>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
fn main() {
|
||||
// Windows default thread stack is 1MB; teloxide futures + large Telegram types
|
||||
// (CallbackQuery/Message ~5KB each) can exhaust this during dptree dispatch.
|
||||
// Spawn the tokio runtime on a thread with an 8MB stack.
|
||||
let stack_size = 8 * 1024 * 1024;
|
||||
std::thread::Builder::new()
|
||||
.name("cgcx-bot-main".into())
|
||||
.stack_size(stack_size)
|
||||
.spawn(|| {
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(4)
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("tokio runtime")
|
||||
.block_on(run_bot());
|
||||
})
|
||||
.expect("spawn main thread")
|
||||
.join()
|
||||
.expect("main thread panicked");
|
||||
}
|
||||
|
||||
async fn run_bot() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let config = Arc::new(Config::load().expect("Failed to load config"));
|
||||
@@ -175,6 +195,7 @@ async fn main() {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
async fn handle_message(
|
||||
bot: Bot,
|
||||
msg: Message,
|
||||
@@ -285,27 +306,33 @@ async fn handle_message(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
async fn handle_callback(
|
||||
bot: Bot,
|
||||
q: CallbackQuery,
|
||||
storage: Arc<InMemStorage<BotState>>,
|
||||
ctx: BotContext,
|
||||
) -> HandlerResult {
|
||||
let data = q.data.as_deref().unwrap_or("");
|
||||
let user = q.from;
|
||||
let user_id = user.id.0 as i64;
|
||||
// CallbackQuery (and the Message it may contain) are very large structs.
|
||||
// Extract only the fields we need and drop q before the first .await so
|
||||
// the compiler can keep the async state machine small.
|
||||
let callback_id = q.id.clone();
|
||||
let data = q.data.as_deref().unwrap_or("").to_string();
|
||||
let user_id = q.from.id.0 as i64;
|
||||
let chat_id = q.message.as_ref().map(|m| m.chat().id).unwrap_or(ChatId(user_id));
|
||||
let message_id = q.message.as_ref().map(|m| m.id());
|
||||
drop(q);
|
||||
|
||||
if !ctx.moderation.is_allowed(user_id).await {
|
||||
bot.answer_callback_query(&q.id).text("Not allowed").await?;
|
||||
bot.answer_callback_query(&callback_id).text("Not allowed").await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let chat_id = q.message.as_ref().map(|m| m.chat().id).unwrap_or(ChatId(user_id));
|
||||
let dialogue = BotDialogue { chat_id, storage };
|
||||
|
||||
let parts: Vec<&str> = data.split(':').collect();
|
||||
if parts.len() < 3 || parts[0] != "v1" {
|
||||
bot.answer_callback_query(&q.id).await?;
|
||||
bot.answer_callback_query(&callback_id).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -314,14 +341,14 @@ async fn handle_callback(
|
||||
"accept" => {
|
||||
let user_repo = UserRepo::new(ctx.db.conn());
|
||||
user_repo.set_accepted_terms(user_id).await?;
|
||||
if let Some(msg) = &q.message {
|
||||
bot.delete_message(chat_id, msg.id()).await.ok();
|
||||
if let Some(mid) = message_id {
|
||||
bot.delete_message(chat_id, mid).await.ok();
|
||||
}
|
||||
send_main_menu(&bot, chat_id, &dialogue).await?;
|
||||
}
|
||||
"reject" => {
|
||||
if let Some(msg) = &q.message {
|
||||
bot.delete_message(chat_id, msg.id()).await.ok();
|
||||
if let Some(mid) = message_id {
|
||||
bot.delete_message(chat_id, mid).await.ok();
|
||||
}
|
||||
dialogue.reset().await?;
|
||||
}
|
||||
@@ -358,7 +385,7 @@ async fn handle_callback(
|
||||
let state = dialogue.get_or_default().await?;
|
||||
if let BotState::UploadStaging { items, .. } = state {
|
||||
if items.is_empty() {
|
||||
bot.answer_callback_query(&q.id).text("No items to upload.").await?;
|
||||
bot.answer_callback_query(&callback_id).text("No items to upload.").await?;
|
||||
} else {
|
||||
let options = UploadOptions {
|
||||
allow_download: true,
|
||||
@@ -370,8 +397,8 @@ async fn handle_callback(
|
||||
}
|
||||
}
|
||||
"cancel" => {
|
||||
if let Some(msg) = &q.message {
|
||||
bot.edit_message_text(chat_id, msg.id(), "Upload cancelled.").await.ok();
|
||||
if let Some(mid) = message_id {
|
||||
bot.edit_message_text(chat_id, mid, "Upload cancelled.").await.ok();
|
||||
}
|
||||
dialogue.update(BotState::MainMenu).await?;
|
||||
}
|
||||
@@ -429,7 +456,7 @@ async fn handle_callback(
|
||||
_ => {}
|
||||
}
|
||||
|
||||
bot.answer_callback_query(&q.id).await?;
|
||||
bot.answer_callback_query(&callback_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -491,6 +518,7 @@ async fn send_staging_message(bot: &Bot, chat_id: ChatId, items: &[StagedItem],
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
async fn handle_staging_message(
|
||||
bot: &Bot,
|
||||
msg: Message,
|
||||
@@ -504,6 +532,8 @@ async fn handle_staging_message(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let chat_id = msg.chat.id;
|
||||
let caption = msg.caption().map(|s| s.to_string());
|
||||
let mut new_item = None;
|
||||
|
||||
match upload_type {
|
||||
@@ -516,7 +546,7 @@ async fn handle_staging_message(
|
||||
file_name: format!("photo_{}.jpg", items.len()),
|
||||
mime_type: "image/jpeg".to_string(),
|
||||
size: p.file.size as u64,
|
||||
caption: msg.caption().map(|s| s.to_string()),
|
||||
caption: caption.clone(),
|
||||
});
|
||||
}
|
||||
} else if let Some(video) = msg.video() {
|
||||
@@ -525,7 +555,7 @@ async fn handle_staging_message(
|
||||
file_name: video.file_name.clone().unwrap_or_else(|| format!("video_{}.mp4", items.len())),
|
||||
mime_type: "video/mp4".to_string(),
|
||||
size: video.file.size as u64,
|
||||
caption: msg.caption().map(|s| s.to_string()),
|
||||
caption: caption.clone(),
|
||||
});
|
||||
} else if let Some(audio) = msg.audio() {
|
||||
new_item = Some(StagedItem {
|
||||
@@ -533,7 +563,7 @@ async fn handle_staging_message(
|
||||
file_name: audio.file_name.clone().unwrap_or_else(|| format!("audio_{}.mp3", items.len())),
|
||||
mime_type: "audio/mpeg".to_string(),
|
||||
size: audio.file.size as u64,
|
||||
caption: msg.caption().map(|s| s.to_string()),
|
||||
caption: caption.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -544,7 +574,7 @@ async fn handle_staging_message(
|
||||
file_name: doc.file_name.clone().unwrap_or_else(|| format!("file_{}", items.len())),
|
||||
mime_type: doc.mime_type.clone().map(|m| m.to_string()).unwrap_or_else(|| "application/octet-stream".to_string()),
|
||||
size: doc.file.size as u64,
|
||||
caption: msg.caption().map(|s| s.to_string()),
|
||||
caption: caption.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -563,10 +593,12 @@ async fn handle_staging_message(
|
||||
}
|
||||
}
|
||||
|
||||
drop(msg);
|
||||
|
||||
if let Some(item) = new_item {
|
||||
items.push(item);
|
||||
dialogue.update(BotState::UploadStaging { items: items.clone(), upload_type }).await?;
|
||||
send_staging_message(bot, msg.chat.id, &items, upload_type).await?;
|
||||
send_staging_message(bot, chat_id, &items, upload_type).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -107,6 +107,19 @@ type AppResult<T> = Result<T, AppError>;
|
||||
async fn main() -> cgcx_core::Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Log panics so we can diagnose 500s that CatchPanicLayer swallows.
|
||||
std::panic::set_hook(Box::new(|info| {
|
||||
let msg = if let Some(s) = info.payload().downcast_ref::<&str>() {
|
||||
s.to_string()
|
||||
} else if let Some(s) = info.payload().downcast_ref::<String>() {
|
||||
s.clone()
|
||||
} else {
|
||||
"unknown panic payload".to_string()
|
||||
};
|
||||
let location = info.location().map(|l| format!("{}:{}", l.file(), l.line())).unwrap_or_default();
|
||||
tracing::error!("PANIC at {}: {}", location, msg);
|
||||
}));
|
||||
|
||||
let config = Arc::new(Config::load()?);
|
||||
config.validate()?;
|
||||
|
||||
@@ -161,24 +174,24 @@ async fn main() -> cgcx_core::Result<()> {
|
||||
let static_service = ServeDir::new("frontend/dist")
|
||||
.fallback(ServeFile::new("frontend/dist/index.html"));
|
||||
|
||||
let base_url = config.server.base_url.clone();
|
||||
let cors = CorsLayer::new()
|
||||
.allow_origin(AllowOrigin::predicate(move |origin: &HeaderValue, _request_parts: &_| {
|
||||
if let Ok(origin_str) = origin.to_str() {
|
||||
if origin_str == base_url {
|
||||
return true;
|
||||
}
|
||||
// Allow localhost origins for development
|
||||
if origin_str.starts_with("http://127.0.0.1:")
|
||||
|| origin_str.starts_with("http://localhost:")
|
||||
|| origin_str.starts_with("https://127.0.0.1:")
|
||||
|| origin_str.starts_with("https://localhost:")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
let mut origins: Vec<HeaderValue> = vec![
|
||||
config.server.base_url.parse().expect("invalid server.base_url"),
|
||||
];
|
||||
for origin in [
|
||||
"http://127.0.0.1:5173",
|
||||
"http://localhost:5173",
|
||||
"http://127.0.0.1:8090",
|
||||
"http://localhost:8090",
|
||||
] {
|
||||
if let Ok(hv) = origin.parse::<HeaderValue>() {
|
||||
if !origins.contains(&hv) {
|
||||
origins.push(hv);
|
||||
}
|
||||
false
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
let cors = CorsLayer::new()
|
||||
.allow_origin(AllowOrigin::list(origins))
|
||||
.allow_methods([Method::GET, Method::POST, Method::HEAD, Method::OPTIONS])
|
||||
.allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION, header::ACCEPT, header::ACCEPT_ENCODING, header::RANGE])
|
||||
.allow_credentials(true)
|
||||
|
||||
Reference in New Issue
Block a user