| import os |
| import logging |
| import random |
| import shutil |
| import re |
| import asyncio |
| import signal |
| import traceback |
| import stat |
| from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup |
| from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes, ConversationHandler, CallbackQueryHandler |
| from typing import Dict, Set, Tuple, Optional |
|
|
| |
| logging.basicConfig( |
| format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', |
| level=logging.DEBUG |
| ) |
| logger = logging.getLogger(__name__) |
|
|
| |
| telegram_logger = logging.getLogger('telegram') |
| telegram_logger.setLevel(logging.DEBUG) |
|
|
| |
| AWAITING_CORRECTION = 1 |
| AWAITING_VALIDATION = 2 |
|
|
| |
| |
| user_tasks = {} |
|
|
| |
| chunks_in_process = set() |
|
|
| |
| DATA_DIR = os.environ.get('DATA_DIR', 'data') |
| PENDING_DIR = os.path.join(DATA_DIR, 'pending') |
| PENDING_VOICE_DIR = os.path.join(PENDING_DIR, 'voice') |
| PENDING_TEXT_DIR = os.path.join(PENDING_DIR, 'text') |
| PROCESSED_DIR = os.path.join(DATA_DIR, 'processed') |
| PROCESSED_VOICE_DIR = os.path.join(PROCESSED_DIR, 'voice') |
| PROCESSED_TEXT_DIR = os.path.join(PROCESSED_DIR, 'text') |
|
|
| |
|
|
|
|
| def ensure_directory_with_permissions(directory): |
| """Create directory if it doesn't exist and set permissions.""" |
| try: |
| if not os.path.exists(directory): |
| os.makedirs(directory, exist_ok=True) |
| |
| os.chmod(directory, stat.S_IRWXU | stat.S_IRWXG | |
| stat.S_IRWXO) |
| logger.info(f"Directory ensured with permissions: {directory}") |
| except Exception as e: |
| logger.error(f"Error setting permissions for {directory}: {str(e)}") |
|
|
|
|
| |
| ensure_directory_with_permissions(PENDING_VOICE_DIR) |
| ensure_directory_with_permissions(PENDING_TEXT_DIR) |
| ensure_directory_with_permissions(PROCESSED_VOICE_DIR) |
| ensure_directory_with_permissions(PROCESSED_TEXT_DIR) |
|
|
| |
| application = None |
|
|
|
|
| def extract_sequence_number(filename: str) -> Optional[int]: |
| """Extract sequence number from filename, e.g., 'Sound 100.wav' -> 100.""" |
| match = re.search(r'(\d+)', filename) |
| if match: |
| return int(match.group(1)) |
| return None |
|
|
|
|
| def get_files_by_sequence_number(): |
| """Create dictionaries mapping sequence numbers to filenames.""" |
| voice_files = {} |
| text_files = {} |
|
|
| |
| if os.path.exists(PENDING_VOICE_DIR): |
| for filename in os.listdir(PENDING_VOICE_DIR): |
| if filename.endswith('.wav'): |
| seq_num = extract_sequence_number(filename) |
| if seq_num is not None: |
| voice_files[seq_num] = filename |
|
|
| |
| if os.path.exists(PENDING_TEXT_DIR): |
| for filename in os.listdir(PENDING_TEXT_DIR): |
| if filename.endswith('.txt'): |
| seq_num = extract_sequence_number(filename) |
| if seq_num is not None: |
| text_files[seq_num] = filename |
|
|
| return voice_files, text_files |
|
|
|
|
| async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: |
| """Send a welcome message when the /start command is issued.""" |
| logger.info(f"Start command received from user {update.effective_user.id}") |
| try: |
| await update.message.reply_text( |
| "السلام عليكم في The Algerian Darija Transcription Correction Bot!\n\n" |
| ''' |
| 🌟 قواعد التنميط اللغوي والبدائل 🌟 |
| |
| |
| 🟦🟦🟦 قواعد الكتابة 🟦🟦🟦 |
| |
| 🔷 القاعدة 00 |
| يُسمح فقط باستخدام الكلمات والحروف العربية في عملية التنصيص. يُمنع استعمال الأحرف الأجنبية أو ما يُعرف بـ "العربيزي". |
| 📝 مثال: "على" ✅ وليس "3la" ❌ |
| |
| 🔷 القاعدة 01 |
| يجوز إلصاق الجار والمجرور بالفعل مباشرة دون فصله. |
| 📝 مثال: قالولي ✅ بدلاً من قالوا لي ❌ |
| |
| 🔷 القاعدة 02 |
| لا يُشترط الالتزام بألف الجماعة في الأفعال. |
| 📝 مثال: قالو ✅ بدلاً من قالوا ❌ |
| |
| 🔷 القاعدة 03 |
| يتم استبدال همزة الألف (أ) بحرف (ا) دائماً. |
| 📝 مثال: اكتوب ✅ بدلاً من أكتب ❌ |
| |
| 🔷 القاعدة 04 |
| يُكتب الضمير المتصل (هو) بالشكل الصحيح دون تغيير. |
| 📝 مثال: عنده ✅ بدلاً من عندو ❌ |
| |
| 🔷 القاعدة 05 |
| عند النطق، يتم استبدال حرف (ت) بحرف (ث) إذا نُطق كذلك. |
| 📝 مثال: ثاني ✅ بدلاً من تاني ❌ |
| |
| 🔷 القاعدة 06 |
| تحويل الفاء الملتصقة بالاسم إلى حرف الجر "في". |
| 📝 مثال: في البيت ✅ بدلاً من فالبيت ❌ |
| |
| 🔷 القاعدة 07 |
| يُفضل وصل "ما" و"واو العطف" بالكلمة مباشرة. |
| 📝 مثال: ماقولتش ✅ بدلاً من ماقولتش ❌ |
| |
| 🔷 القاعدة 09 |
| تُحافظ حروف المستقبل مثل "عنقول" أو "كنقول" على شكلها الأصلي دون تغيير. |
| 📝 مثال: عنقول ✅ (لا تغيير) |
| |
| |
| 🟩🟩🟩 البدائل اللغوية 🟩🟩🟩 |
| 🚫 اللفظ غير المعياري ← ✅ اللفظ المعياري |
| |
| درك ← ضرك ✅ |
| |
| علابالي ← على بالي ✅ |
| |
| شويا ← شوية ✅ |
| |
| واش ← وش ✅ |
| |
| ڨالو ← قالو ✅ |
| |
| نتاع / نتع / تع ← تاع ✅ |
| |
| بالصح / بصاح ← بصح ✅ |
| |
| قتلك ← قلتلك ✅ |
| |
| يقول لك ← يقولك ✅ |
| |
| عنا ← عندنا ✅ |
| |
| هاداك / هذاك / هاذاك ← هداك ✅ |
| |
| لي / ليه / ليك ← اللي / له / لك ✅ |
| |
| |
| 😉 قم بعمل pin أو تثبيت لهذه الرسالة لكي يسهل عليك الرجوع إليها لاحقا لمراجعة القواعد‼️ |
| ''' |
| "استعمل /correct للبدأ في تصحيح النصوص\n" |
| "استعمل /skip لتجاوز مقطع صوتي معين\n" |
| "استعمل /cancel لإلغاء العملية\n" |
| ) |
| logger.info("Start message sent successfully") |
| except Exception as e: |
| logger.error(f"Error sending start message: {e}") |
|
|
|
|
| async def get_next_available_chunk() -> Tuple[Optional[str], Optional[str], Optional[int]]: |
| """Find the next available audio chunk that isn't being processed. |
| Returns (voice_filename, text_filename, sequence_number) or (None, None, None)""" |
| voice_files, text_files = get_files_by_sequence_number() |
|
|
| |
| common_seq_nums = set(voice_files.keys()).intersection( |
| set(text_files.keys())) |
|
|
| |
| available_seq_nums = [ |
| seq_num for seq_num in common_seq_nums if seq_num not in chunks_in_process] |
|
|
| logger.debug(f"Available sequence numbers: {available_seq_nums}") |
|
|
| if available_seq_nums: |
| |
| seq_num = random.choice(available_seq_nums) |
| logger.debug(f"Selected sequence number: {seq_num}") |
| return voice_files[seq_num], text_files[seq_num], seq_num |
|
|
| logger.debug("No available chunks found") |
| return None, None, None |
|
|
|
|
| async def send_chunk_to_user(update: Update, user_id: int, voice_filename: str, text_filename: str, seq_num: int) -> bool: |
| """Send audio chunk and text to user. Returns True if successful, False otherwise.""" |
| |
| audio_path = os.path.join(PENDING_VOICE_DIR, voice_filename) |
|
|
| |
| if not os.path.exists(audio_path): |
| logger.error(f"Audio file not found: {audio_path}") |
| await update.message.reply_text("Error: Audio file not found. Please try again.") |
| return False |
|
|
| |
| text_path = os.path.join(PENDING_TEXT_DIR, text_filename) |
| if not os.path.exists(text_path): |
| logger.error(f"Text file not found: {text_path}") |
| await update.message.reply_text("Error: Text file not found. Please try again.") |
| return False |
|
|
| try: |
| with open(text_path, 'r', encoding='utf-8') as f: |
| original_text = f.read().strip() |
| except Exception as e: |
| logger.error(f"Error reading text file: {e}") |
| await update.message.reply_text(f"Error reading transcription file. Please try again.") |
| return False |
|
|
| |
| try: |
| with open(audio_path, 'rb') as audio_file: |
| await update.message.reply_voice(voice=audio_file) |
|
|
| await update.message.reply_text( |
| f"من فضلك استمع إلى المقطع الصوتي وأعد كتابة النص.\n" |
| f"النص الأصلي:" |
| ) |
| await update.message.reply_text( |
| f"{original_text}" |
| ) |
| await update.message.reply_text( |
| f"من فضلك قم بإرسال النص الذي تم تصحيحه أو استعمل /skip لتجاوز هذا المقطع." |
| f"استعمل /cancel لإلغاء العملية." |
| ) |
|
|
| logger.info(f"Successfully sent audio and text to user {user_id}") |
| return True |
| except Exception as e: |
| logger.error(f"Error sending audio or text: {e}") |
| await update.message.reply_text("Error sending audio. Please try again.") |
| return False |
|
|
|
|
| async def correct(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: |
| """Start the correction process.""" |
| logger.info( |
| f"تم استقبال أمر التصحيح من المستعمل {update.effective_user.id}") |
| user_id = update.effective_user.id |
|
|
| |
| if user_id in user_tasks: |
| await update.message.reply_text("أنت الآن في غضون عملية تصحيح. من فضلك قم بإكمالها أو ألغي العملية عبر /cancel.") |
| current_state = AWAITING_VALIDATION if len( |
| user_tasks[user_id]) > 3 else AWAITING_CORRECTION |
| return current_state |
|
|
| |
| ensure_directory_with_permissions(PENDING_VOICE_DIR) |
| ensure_directory_with_permissions(PENDING_TEXT_DIR) |
|
|
| |
| logger.debug( |
| f"PENDING_VOICE_DIR contents: {os.listdir(PENDING_VOICE_DIR)}") |
| logger.debug(f"PENDING_TEXT_DIR contents: {os.listdir(PENDING_TEXT_DIR)}") |
|
|
| |
| voice_filename, text_filename, seq_num = await get_next_available_chunk() |
|
|
| if not voice_filename or not text_filename: |
| await update.message.reply_text("لا توجد نصوص قيد الانتظار في الوقت الحالي. حاول مرة أخرى لاحقًا.") |
| return ConversationHandler.END |
|
|
| |
| chunks_in_process.add(seq_num) |
| user_tasks[user_id] = (voice_filename, text_filename, seq_num) |
|
|
| |
| success = await send_chunk_to_user(update, user_id, voice_filename, text_filename, seq_num) |
|
|
| if not success: |
| |
| chunks_in_process.remove(seq_num) |
| del user_tasks[user_id] |
| return ConversationHandler.END |
|
|
| return AWAITING_CORRECTION |
|
|
|
|
| async def skip_chunk(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: |
| """Skip the current chunk and get a new one.""" |
| user_id = update.effective_user.id |
|
|
| if user_id not in user_tasks: |
| await update.message.reply_text("أنت لا تملك مهمة تصحيح نشطة. استخدم /correct للبدء.") |
| return ConversationHandler.END |
|
|
| |
| current_task = user_tasks[user_id] |
| if len(current_task) >= 3: |
| _, _, current_seq_num = current_task[:3] |
|
|
| |
| chunks_in_process.discard(current_seq_num) |
| logger.info(f"User {user_id} skipped chunk {current_seq_num}") |
|
|
| |
| del user_tasks[user_id] |
|
|
| await update.message.reply_text("تجاوزت المقطع الصوتي. دعني أبحث عن مقطع آخر لك.") |
|
|
| |
| voice_filename, text_filename, seq_num = await get_next_available_chunk() |
|
|
| if not voice_filename or not text_filename: |
| await update.message.reply_text("لا توجد نصوص قيد الانتظار في الوقت الحالي. حاول مرة أخرى لاحقًا.") |
| return ConversationHandler.END |
|
|
| |
| chunks_in_process.add(seq_num) |
| user_tasks[user_id] = (voice_filename, text_filename, seq_num) |
|
|
| |
| success = await send_chunk_to_user(update, user_id, voice_filename, text_filename, seq_num) |
|
|
| if not success: |
| |
| chunks_in_process.remove(seq_num) |
| del user_tasks[user_id] |
| return ConversationHandler.END |
|
|
| return AWAITING_CORRECTION |
|
|
|
|
| async def receive_correction(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: |
| """Receive the user's correction and ask for validation.""" |
| user_id = update.effective_user.id |
|
|
| if user_id not in user_tasks: |
| await update.message.reply_text("أنت لا تملك مهمة تصحيح نشطة. استخدم /correct للبدء.") |
| return ConversationHandler.END |
|
|
| voice_filename, text_filename, seq_num = user_tasks[user_id] |
| corrected_text = update.message.text |
|
|
| |
| user_tasks[user_id] = (voice_filename, text_filename, |
| seq_num, corrected_text) |
|
|
| |
| keyboard = [ |
| [ |
| InlineKeyboardButton("✅ نعم, أنا متأكد من التصحيح", |
| callback_data="validate_yes"), |
| InlineKeyboardButton("❌ لا, أريد التصحيح مرة أخرى", |
| callback_data="validate_no") |
| ] |
| ] |
| reply_markup = InlineKeyboardMarkup(keyboard) |
|
|
| await update.message.reply_text( |
| f"النص الذي قمت بتصحيحه:\n{corrected_text}\n\n" |
| f"هل أنت متأكد من التصحيح؟", |
| reply_markup=reply_markup |
| ) |
|
|
| return AWAITING_VALIDATION |
|
|
|
|
| async def handle_validation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: |
| """Handle the validation response.""" |
| query = update.callback_query |
| await query.answer() |
|
|
| user_id = query.from_user.id |
|
|
| if user_id not in user_tasks or len(user_tasks[user_id]) < 4: |
| await query.edit_message_text("أنت لا تملك مهمة تصحيح نشطة. استخدم /correct للبدء.") |
| return ConversationHandler.END |
|
|
| voice_filename, text_filename, seq_num, corrected_text = user_tasks[user_id] |
|
|
| if query.data == "validate_yes": |
| |
| success = await save_final_correction(query, user_id, voice_filename, text_filename, seq_num, corrected_text) |
| if success: |
| await query.edit_message_text( |
| "شكرا. تصحيحك تم حفظه\n" |
| "استعمل /correct لاستقبال عملية تصحيح جديدة" |
| ) |
| return ConversationHandler.END |
| else: |
| |
| |
| user_tasks[user_id] = (voice_filename, text_filename, seq_num) |
| await query.edit_message_text( |
| "حدث خطأ أثناء حفظ التصحيح. من فضلك أعد إرساله." |
| ) |
| return AWAITING_CORRECTION |
|
|
| elif query.data == "validate_no": |
| |
| user_tasks[user_id] = (voice_filename, text_filename, seq_num) |
| await query.edit_message_text( |
| "من فضلك أعد كتابة النص التصحيحي مرة أخرى." |
| ) |
| return AWAITING_CORRECTION |
|
|
| return AWAITING_VALIDATION |
|
|
|
|
| async def save_final_correction(query, user_id: int, voice_filename: str, text_filename: str, seq_num: int, corrected_text: str) -> bool: |
| """Save the final validated correction.""" |
| |
| original_txt_path = os.path.join(PENDING_TEXT_DIR, text_filename) |
| original_wav_path = os.path.join(PENDING_VOICE_DIR, voice_filename) |
|
|
| |
| user_processed_text_dir = os.path.join( |
| PROCESSED_TEXT_DIR, f"user_{user_id}") |
|
|
| |
| logger.debug(f"Creating text directory: {user_processed_text_dir}") |
|
|
| try: |
| |
| ensure_directory_with_permissions(user_processed_text_dir) |
|
|
| |
| processed_txt_path = os.path.join( |
| user_processed_text_dir, text_filename) |
|
|
| |
| logger.debug( |
| f"Original text path: {original_txt_path}, exists: {os.path.exists(original_txt_path)}") |
| logger.debug( |
| f"Original wav path: {original_wav_path}, exists: {os.path.exists(original_wav_path)}") |
| logger.debug(f"Processed text path: {processed_txt_path}") |
|
|
| |
| logger.debug(f"Saving corrected text to {processed_txt_path}") |
| with open(processed_txt_path, 'w', encoding='utf-8') as f: |
| f.write(corrected_text) |
| |
| os.chmod(processed_txt_path, stat.S_IRUSR | stat.S_IWUSR | |
| stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH) |
|
|
| |
| logger.debug( |
| f"Verifying processed text file exists: {os.path.exists(processed_txt_path)}") |
|
|
| |
| if os.path.exists(original_txt_path): |
| logger.debug(f"Removing original text file at {original_txt_path}") |
| os.remove(original_txt_path) |
| else: |
| logger.warning( |
| f"Could not remove original text file as it doesn't exist at {original_txt_path}") |
|
|
| if os.path.exists(original_wav_path): |
| logger.debug(f"Removing original wav file at {original_wav_path}") |
| os.remove(original_wav_path) |
| else: |
| logger.warning( |
| f"Could not remove original wav file as it doesn't exist at {original_wav_path}") |
|
|
| |
| logger.debug(f"Removing seq_num {seq_num} from chunks_in_process") |
| chunks_in_process.remove(seq_num) |
|
|
| logger.debug(f"Removing user {user_id} from user_tasks") |
| del user_tasks[user_id] |
|
|
| logger.info( |
| f"Successfully saved validated correction for user {user_id}") |
| return True |
|
|
| except Exception as e: |
| |
| error_msg = f"Error saving correction: {str(e)}" |
| logger.error(error_msg) |
| logger.error(traceback.format_exc()) |
|
|
| |
| if "Permission denied" in str(e): |
| await query.edit_message_text("Error: Permission denied while saving files. Please contact the administrator.") |
| elif "No such file or directory" in str(e): |
| await query.edit_message_text("Error: File not found. The system couldn't find one of the files.") |
| else: |
| await query.edit_message_text(f"Error saving your correction: {str(e)}. Please try again.") |
|
|
| return False |
|
|
|
|
| async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: |
| """Cancel the current correction task.""" |
| user_id = update.effective_user.id |
|
|
| if user_id in user_tasks: |
| task_info = user_tasks[user_id] |
| if len(task_info) >= 3: |
| seq_num = task_info[2] |
| chunks_in_process.discard(seq_num) |
| del user_tasks[user_id] |
|
|
| await update.message.reply_text("العملية قد ألغيت :( استعمل /correct للبدء تصحيح جديد.") |
| else: |
| await update.message.reply_text("لا توجد عملية تصحيح نشطة لإلغائها.") |
|
|
| return ConversationHandler.END |
|
|
|
|
| async def status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: |
| """Show status of pending and processed chunks.""" |
| logger.info( |
| f"Status command received from user {update.effective_user.id}") |
|
|
| |
| pending_voice_count = len( |
| [f for f in os.listdir(PENDING_VOICE_DIR) if f.endswith('.wav')]) |
| pending_text_count = len( |
| [f for f in os.listdir(PENDING_TEXT_DIR) if f.endswith('.txt')]) |
|
|
| |
| voice_files, text_files = get_files_by_sequence_number() |
| matching_pairs = len( |
| set(voice_files.keys()).intersection(set(text_files.keys()))) |
|
|
| |
| processed_text_count = 0 |
|
|
| |
| for root, dirs, files in os.walk(PROCESSED_TEXT_DIR): |
| processed_text_count += len([f for f in files if f.endswith('.txt')]) |
|
|
| active_tasks = len(user_tasks) |
|
|
| |
| permissions_info = "" |
| try: |
| |
| can_write_text = os.access(PROCESSED_TEXT_DIR, os.W_OK) |
| permissions_info = f"\n• Write permissions: Text: {can_write_text}" |
| except Exception as e: |
| permissions_info = f"\n• Error checking permissions: {str(e)}" |
|
|
| await update.message.reply_text( |
| f"📊 Transcription Status:\n" |
| f"• Pending voice files: {pending_voice_count}\n" |
| f"• Pending text files: {pending_text_count}\n" |
| f"• Matching pending pairs: {matching_pairs}\n" |
| f"• Processed text files: {processed_text_count}\n" |
| f"• Active tasks: {active_tasks}{permissions_info}\n" |
| f"• Note: Only corrected text files are saved, voice files are not copied to processed folder" |
| ) |
|
|
|
|
| async def debug_info(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: |
| """Command to show debug information about directories and permissions.""" |
| user_id = update.effective_user.id |
|
|
| |
| debug_info = [ |
| "📁 Directory Check:", |
| f"• DATA_DIR: {DATA_DIR}, exists: {os.path.exists(DATA_DIR)}", |
| f"• PENDING_DIR: {PENDING_DIR}, exists: {os.path.exists(PENDING_DIR)}", |
| f"• PENDING_VOICE_DIR: {PENDING_VOICE_DIR}, exists: {os.path.exists(PENDING_VOICE_DIR)}", |
| f"• PENDING_TEXT_DIR: {PENDING_TEXT_DIR}, exists: {os.path.exists(PENDING_TEXT_DIR)}", |
| f"• PROCESSED_DIR: {PROCESSED_DIR}, exists: {os.path.exists(PROCESSED_DIR)}", |
| f"• PROCESSED_TEXT_DIR: {PROCESSED_TEXT_DIR}, exists: {os.path.exists(PROCESSED_TEXT_DIR)}", |
| ] |
|
|
| |
| try: |
| permission_info = [ |
| "🔑 Permission Check:", |
| f"• PENDING_VOICE_DIR writable: {os.access(PENDING_VOICE_DIR, os.W_OK)}", |
| f"• PENDING_TEXT_DIR writable: {os.access(PENDING_TEXT_DIR, os.W_OK)}", |
| f"• PROCESSED_TEXT_DIR writable: {os.access(PROCESSED_TEXT_DIR, os.W_OK)}", |
| ] |
| debug_info.extend(permission_info) |
| except Exception as e: |
| debug_info.append(f"Error checking permissions: {str(e)}") |
|
|
| |
| task_info = [ |
| "📋 Active Tasks:", |
| f"• Number of active users: {len(user_tasks)}", |
| f"• Active chunks: {len(chunks_in_process)}", |
| ] |
| debug_info.extend(task_info) |
|
|
| await update.message.reply_text("\n".join(debug_info)) |
|
|
|
|
| async def test_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: |
| """Test handler to verify bot is receiving messages.""" |
| logger.info(f"Test command received from user {update.effective_user.id}") |
| await update.message.reply_text("Bot is working! ✅") |
|
|
|
|
| async def setup_application(): |
| """Setup the Application with all handlers.""" |
| global application |
|
|
| |
| token = os.environ.get("TELEGRAM_TOKEN") |
| if not token: |
| logger.error("TELEGRAM_TOKEN environment variable not set!") |
| return None |
|
|
| logger.info(f"Setting up bot with token: {token[:10]}...") |
|
|
| |
| application = Application.builder().token(token).build() |
|
|
| |
| application.add_handler(CommandHandler("test", test_handler)) |
|
|
| |
| conv_handler = ConversationHandler( |
| entry_points=[CommandHandler("correct", correct)], |
| states={ |
| AWAITING_CORRECTION: [ |
| MessageHandler(filters.TEXT & ~filters.COMMAND, |
| receive_correction), |
| CommandHandler("skip", skip_chunk), |
| ], |
| AWAITING_VALIDATION: [ |
| CallbackQueryHandler(handle_validation, pattern="^validate_"), |
| ], |
| }, |
| fallbacks=[ |
| CommandHandler("cancel", cancel), |
| CommandHandler("skip", skip_chunk), |
| ], |
| name="correction_conversation", |
| ) |
|
|
| application.add_handler(conv_handler) |
|
|
| |
| application.add_handler(CommandHandler("start", start)) |
| application.add_handler(CommandHandler("status", status)) |
| application.add_handler(CommandHandler("cancel", cancel)) |
| application.add_handler(CommandHandler( |
| "skip", skip_chunk)) |
| application.add_handler(CommandHandler("debug", debug_info)) |
|
|
| logger.info("All handlers added successfully") |
| return application |
|
|
|
|
| async def start_bot(): |
| """Start the bot with proper signal handling.""" |
| global application |
|
|
| logger.info("Starting bot setup...") |
|
|
| |
| app = await setup_application() |
| if not app: |
| logger.error("Failed to create application") |
| return |
|
|
| logger.info("Application created successfully") |
|
|
| |
| try: |
| await app.initialize() |
| logger.info("Application initialized") |
|
|
| await app.start() |
| logger.info("Application started") |
|
|
| |
| await app.updater.start_polling(drop_pending_updates=False) |
| logger.info("Started polling for updates") |
|
|
| except Exception as e: |
| logger.error(f"Error starting bot: {e}") |
| return |
|
|
| |
| loop = asyncio.get_event_loop() |
|
|
| for signal_name in ('SIGINT', 'SIGTERM'): |
| try: |
| loop.add_signal_handler( |
| getattr(signal, signal_name), |
| lambda s=signal_name: asyncio.create_task(shutdown(s)) |
| ) |
| except NotImplementedError: |
| |
| logger.info("Signal handlers not supported on this platform") |
| pass |
|
|
| try: |
| |
| logger.info("Bot is now running and waiting for messages...") |
| await asyncio.Event().wait() |
| finally: |
| |
| await shutdown("Manual") |
|
|
|
|
| async def shutdown(signal_type): |
| """Cleanup tasks tied to the service's shutdown.""" |
| global application |
|
|
| logger.info(f"Received exit signal {signal_type}...") |
|
|
| |
| if application: |
| logger.info("Stopping application...") |
| await application.updater.stop() |
| await application.stop() |
| await application.shutdown() |
|
|
| |
| tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] |
| for task in tasks: |
| task.cancel() |
|
|
| |
| if tasks: |
| logger.info(f"Waiting for {len(tasks)} tasks to complete...") |
| await asyncio.gather(*tasks, return_exceptions=True) |
|
|
| logger.info("Application shutdown complete") |
|
|
|
|
| async def main(): |
| """Main function to setup and run the bot.""" |
| |
| await start_bot() |
|
|
| |
| if __name__ == "__main__": |
| asyncio.run(main()) |
|
|