From b02e327aafd643b6fad536e94ade445f93c10834 Mon Sep 17 00:00:00 2001 From: Joan Date: Wed, 17 Sep 2025 17:57:44 +0200 Subject: [PATCH] Changed markdown to html --- app/app.py | 4 +- app/handlers.py | 91 ++++++++++++++++++++++------------------- app/helpers.py | 24 ++--------- app/paypal_processor.py | 22 +++++----- 4 files changed, 66 insertions(+), 75 deletions(-) diff --git a/app/app.py b/app/app.py index 7a1c614..0651f5d 100644 --- a/app/app.py +++ b/app/app.py @@ -92,11 +92,11 @@ async def check_expired_reservations(context: ContextTypes.DEFAULT_TYPE): cancelled_count += 1 # Try to notify the user using context.bot notification_text = ( - f"Las participaciones `{numbers}` que tenías reservadas para el sorteo **{raffle_name}** han sido liberadas.\n\n" + f"Las participaciones {numbers} que tenías reservadas para el sorteo {raffle_name} han sido liberadas.\n\n" f"Puedes volver a reservarlas, ¡pero tienes {RESERVATION_TIMEOUT_MINUTES} minutos para completar el pago!." ) try: - await context.bot.send_message(chat_id=user_id, text=notification_text, parse_mode=ParseMode.MARKDOWN) + await context.bot.send_message(chat_id=user_id, text=notification_text, parse_mode=ParseMode.HTML) logger.info(f"Notified user {user_id} (Name: {reservation['user_name']}) about expired reservation.") except Forbidden: logger.warning(f"Cannot notify user {user_id} (Forbidden). Reservation cancelled anyway.") diff --git a/app/handlers.py b/app/handlers.py index 0a538db..35b5bdf 100644 --- a/app/handlers.py +++ b/app/handlers.py @@ -24,6 +24,9 @@ logger = logging.getLogger(__name__) async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): user = update.message.from_user args = context.args + if not user.username: + await update.message.reply_text("Por favor, configura un nombre de usuario en Telegram para participar. ¡Es necesario para notificarte si ganas!") + return if args and args[0].startswith("join_"): try: raffle_id = int(args[0].split("_")[1]) @@ -75,9 +78,9 @@ async def new_raffle_start(update: Update, context: ContextTypes.DEFAULT_TYPE) - keyboard = generate_channel_selection_keyboard() await update.message.reply_text( "Vamos a crear un nuevo sorteo.\n\n" - "**Paso 1:** Selecciona el canal donde se publicará el sorteo.", + "Paso 1: Selecciona el canal donde se publicará el sorteo.", reply_markup=keyboard, - parse_mode=ParseMode.MARKDOWN + parse_mode=ParseMode.HTML ) return SELECTING_CHANNEL @@ -93,8 +96,8 @@ async def select_channel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> context.user_data['new_raffle']['channel'] = channel_id await query.edit_message_text( - "Canal seleccionad. Ahora, por favor, envía el **título** del sorteo.", - parse_mode=ParseMode.MARKDOWN + "Canal seleccionad. Ahora, por favor, envía el título del sorteo.", + parse_mode=ParseMode.HTML ) return TYPING_TITLE @@ -110,7 +113,7 @@ async def receive_title(update: Update, context: ContextTypes.DEFAULT_TYPE) -> i return TYPING_TITLE context.user_data['new_raffle']['title'] = title - await update.message.reply_text("Título guardado. Ahora envía la **descripción** del sorteo.", parse_mode=ParseMode.MARKDOWN) + await update.message.reply_text("Título guardado. Ahora envía la descripción del sorteo.", parse_mode=ParseMode.HTML) return TYPING_DESCRIPTION async def receive_description(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: @@ -121,7 +124,7 @@ async def receive_description(update: Update, context: ContextTypes.DEFAULT_TYPE return TYPING_DESCRIPTION context.user_data['new_raffle']['description'] = description - await update.message.reply_text("Descripción guardada. Ahora envía la **imagen** para el sorteo.", parse_mode=ParseMode.MARKDOWN) + await update.message.reply_text("Descripción guardada. Ahora envía la imagen para el sorteo.", parse_mode=ParseMode.HTML) return SENDING_IMAGE async def receive_image(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: @@ -135,7 +138,7 @@ async def receive_image(update: Update, context: ContextTypes.DEFAULT_TYPE) -> i await update.message.reply_text( "Imagen guardada. Ahora, ¿el sorteo permite envíos internacionales? Responde con 'Sí' o 'No'.", - parse_mode=ParseMode.MARKDOWN + parse_mode=ParseMode.HTML ) return INTERNATIONAL_SHIPPING @@ -152,7 +155,7 @@ async def receive_international_shipping(update: Update, context: ContextTypes.D await update.message.reply_text( "Perfecto. Ahora, introduce el precio por número para el canal seleccionado (solo el número, ej: 5).", - parse_mode=ParseMode.MARKDOWN + parse_mode=ParseMode.HTML ) return TYPING_PRICE_FOR_CHANNEL @@ -183,13 +186,13 @@ async def _show_creation_confirmation(update: Update, context: ContextTypes.DEFA price = raffle_data.get('price', 0) confirmation_text = ( - "¡Perfecto! Revisa los datos del sortoe:\n\n" - f"📌 **Título:** {raffle_data.get('title', 'N/A')}\n" - f"📝 **Descripción:** {raffle_data.get('description', 'N/A')}\n" - f"📺 **Canal:** {REVERSE_CHANNELS.get(channel_id, channel_id)}\n" - f"🌍 **Envío internacional:** {'Sí ✅' if raffle_data.get('international_shipping', 0) else 'No ❌'}\n" - f"💶 **Donación mínima:** {price}€\n" - f"🖼️ **Imagen:** (Adjunta)\n\n" + "¡Perfecto! Revisa los datos del sorteo:\n\n" + f"📌 Título: {raffle_data.get('title', 'N/A')}\n" + f"📝 Descripción: {raffle_data.get('description', 'N/A')}\n" + f"📺 Canal: {REVERSE_CHANNELS.get(channel_id, channel_id)}\n" + f"🌍 Envío internacional: {'Sí ✅' if raffle_data.get('international_shipping', 0) else 'No ❌'}\n" + f"💶 Donación mínima: {price}€\n" + f"🖼️ Imagen: (Adjunta)\n\n" "¿Confirmas la creación de este sorteo?" ) keyboard = generate_confirmation_keyboard() @@ -200,7 +203,7 @@ async def _show_creation_confirmation(update: Update, context: ContextTypes.DEFA photo=raffle_data['image_file_id'], caption=confirmation_text, reply_markup=keyboard, - parse_mode=ParseMode.MARKDOWN + parse_mode=ParseMode.HTML ) async def confirm_creation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: @@ -330,7 +333,11 @@ async def number_callback(update: Update, context: CallbackContext): """Handles clicks on the number buttons in the private chat.""" query = update.callback_query user_id = query.from_user.id - username = query.from_user.username or query.from_user.first_name + + if not query.from_user.username: + await query.answer("Por favor, configura un nombre de usuario en Telegram para participar.") + return + username = query.from_user.username try: data = query.data.split(':') @@ -642,9 +649,9 @@ async def end_raffle_logic(context: ContextTypes.DEFAULT_TYPE, raffle_id: int, w winners_str = get_winners(raffle_id, winner_numbers) formatted_winner_numbers = ", ".join(f"{n:02}" for n in sorted(winner_numbers)) - announcement = f"🎯🏆🎯 **¡Resultados del Sorteo '{raffle_name}'!** 🎯🏆🎯\n\n" + announcement = f"🎯🏆🎯 ¡Resultados del Sorteo '{raffle_name}'! 🎯🏆🎯\n\n" announcement += f"Detalles del sorteo: https://t.me/{channel_alias}/{get_main_message_id(raffle_id)}\n" - announcement += f"Participaciones ganadoras: **{formatted_winner_numbers}**\n\n" if len(winner_numbers) > 1 else f"Participación ganadora: **{formatted_winner_numbers}**\n\n" + announcement += f"Participaciones ganadoras: {formatted_winner_numbers}\n\n" if len(winner_numbers) > 1 else f"Participación ganadora: {formatted_winner_numbers}\n\n" if winners_str: # Ensure winners_str is not empty or a "no winners" message itself announcement += f"Ganadores:\n{winners_str}\n\n¡Felicidades!" if len(winner_numbers) > 1 else f"Ganador:\n{winners_str}\n\n¡Felicidades!" else: @@ -652,16 +659,16 @@ async def end_raffle_logic(context: ContextTypes.DEFAULT_TYPE, raffle_id: int, w announcement += f"\nPuedes comprobar los resultados en {JUEGOS_ONCE_URL}" announcement += "\n\nGracias a todos por participar. Mantente atento a futuros sorteos." - main_announcement = f"🎯🏆🎯 **Sorteo '{raffle_name}' terminado** 🎯🏆🎯\n\n" + main_announcement = f"🎯🏆🎯 Sorteo '{raffle_name}' terminado 🎯🏆🎯\n\n" main_announcement += f"{raffle_details['description']}\n\n" - main_announcement += f"🌍 **Envío internacional:** {'Sí ✅' if raffle_details['international_shipping'] else 'No ❌'}\n" - main_announcement += f"💵 **Donación mínima:** {raffle_details['price']}€\n" - main_announcement += f"📜 Normas y condiciones: {TYC_DOCUMENT_URL}" + main_announcement += f"🌍 Envío internacional: {'Sí ✅' if raffle_details['international_shipping'] else 'No ❌'}\n" + main_announcement += f"💵 Donación mínima: {raffle_details['price']}€\n" + main_announcement += f"📜 Normas y condiciones: {TYC_DOCUMENT_URL}" main_message_id = get_main_message_id(raffle_id) try: - await context.bot.send_message(chat_id=int(channel_id_str), text=announcement, parse_mode=ParseMode.MARKDOWN) - await context.bot.edit_message_caption(chat_id=int(channel_id_str), message_id=main_message_id, caption=main_announcement, reply_markup=None, parse_mode=ParseMode.MARKDOWN) + await context.bot.send_message(chat_id=int(channel_id_str), text=announcement, parse_mode=ParseMode.HTML) + await context.bot.edit_message_caption(chat_id=int(channel_id_str), message_id=main_message_id, caption=main_announcement, reply_markup=None, parse_mode=ParseMode.HTML) logger.info(f"Announced winners for raffle {raffle_id} in channel {channel_alias} (ID: {channel_id_str})") except Forbidden: logger.error(f"Permission error announcing winners in channel {channel_alias} (ID: {channel_id_str}).") @@ -719,7 +726,7 @@ async def admin_menu(update: Update, context: ContextTypes.DEFAULT_TYPE): logger.info(f"Admin {user.id} accessed /menu") keyboard = generate_admin_main_menu_keyboard() - await update.message.reply_text("🛠️ **Menú de Administrador** 🛠️", reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN) + await update.message.reply_text("🛠️ Menú de Administrador 🛠️", reply_markup=keyboard, parse_mode=ParseMode.HTML) async def admin_menu_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): """Handles callbacks from the admin menus.""" @@ -747,12 +754,12 @@ async def admin_menu_callback(update: Update, context: ContextTypes.DEFAULT_TYPE logger.info(f"Admin {user_id} requested raffle list.") keyboard = generate_admin_list_raffles_keyboard() active_raffles = get_active_raffles() - message_text = "**Sorteos Activos**\n\nSelecciona un sorteo para ver detalles, anunciar o terminar:" if active_raffles else "**Sorteos Activas**\n\nNo hay sorteos activos." - await query.edit_message_text(message_text, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN) + message_text = "Sorteos Activos\n\nSelecciona un sorteo para ver detalles, anunciar o terminar:" if active_raffles else "Sorteos Activas\n\nNo hay sorteos activos." + await query.edit_message_text(message_text, reply_markup=keyboard, parse_mode=ParseMode.HTML) elif data == ADMIN_MENU_BACK_MAIN: keyboard = generate_admin_main_menu_keyboard() - await query.edit_message_text("🛠️ **Menú de Administrador** 🛠️", reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN) + await query.edit_message_text("🛠️ Menú de Administrador 🛠️", reply_markup=keyboard, parse_mode=ParseMode.HTML) # --- Raffle Specific Actions --- elif data.startswith(ADMIN_VIEW_RAFFLE_PREFIX): @@ -800,10 +807,10 @@ async def admin_menu_callback(update: Update, context: ContextTypes.DEFAULT_TYPE keyboard = generate_admin_cancel_end_keyboard() await query.edit_message_text( - f"Vas a terminar el sorteo: **{raffle['name']}**\n\n" - "Por favor, envía ahora las **participaciones ganadoras** separadas por espacios (ej: `7 23 81`).", + f"Vas a terminar el sorteo: {raffle['name']}\n\n" + "Por favor, envía ahora las participaciones ganadoras separadas por espacios (ej: 7 23 81).", reply_markup=keyboard, - parse_mode=ParseMode.MARKDOWN + parse_mode=ParseMode.HTML ) elif data == ADMIN_CANCEL_END_PROCESS: # Clear the flags @@ -812,7 +819,7 @@ async def admin_menu_callback(update: Update, context: ContextTypes.DEFAULT_TYPE logger.info(f"Admin {user_id} cancelled the raffle end process.") # Go back to the raffle list keyboard = generate_admin_list_raffles_keyboard() - await query.edit_message_text("**Sorteos Activos**\n\nSelecciona un sorteo para terminarlo:", reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN) + await query.edit_message_text("Sorteos Activos\n\nSelecciona un sorteo para terminarlo:", reply_markup=keyboard, parse_mode=ParseMode.HTML) elif data == ADMIN_NO_OP: # Just ignore clicks on placeholder buttons like "No hay sorteos activos" @@ -854,9 +861,9 @@ async def admin_receive_winner_numbers(update: Update, context: ContextTypes.DEF keyboard = generate_admin_cancel_end_keyboard() await update.message.reply_text( f"❌ Participaciones inválidas: {e}\n\n" - "Por favor, envía las participaciones ganadoras (0-99) separadas por espacios (ej: `7 23 81`).", + "Por favor, envía las participaciones ganadoras (0-99) separadas por espacios (ej: 7 23 81).", reply_markup=keyboard, - parse_mode=ParseMode.MARKDOWN + parse_mode=ParseMode.HTML ) # Keep expecting input return @@ -906,16 +913,16 @@ async def _announce_raffle_in_channels(context: ContextTypes.DEFAULT_TYPE, raffl channel_alias = REVERSE_CHANNELS.get(channel_id_str, f"ID:{channel_id_str}") announce_caption = ( - f"🏆 **¡{'Nuevo ' if initial_announcement else ''}Sorteo Disponible!** 🏆\n\n" - f"🌟 **{raffle_name}** 🌟\n\n" + f"🏆 ¡{'Nuevo ' if initial_announcement else ''}Sorteo Disponible! 🏆\n\n" + f"🌟 {raffle_name} 🌟\n\n" f"{raffle_description}\n\n" - f"🌍 **Envío internacional:** {'Sí ✅' if raffle['international_shipping'] else 'No ❌'}\n" - f"💵 **Donación mínima:** {price}€\n" - f"🎟️ **Participaciones disponibles:** {remaining_count if remaining_count >= 0 else 'N/A'}\n\n" + f"🌍 Envío internacional: {'Sí ✅' if raffle['international_shipping'] else 'No ❌'}\n" + f"💵 Donación mínima: {price}€\n" + f"🎟️ Participaciones disponibles: {remaining_count if remaining_count >= 0 else 'N/A'}\n\n" f"📜 Normas y condiciones: {TYC_DOCUMENT_URL}" ) - message_args = {"parse_mode": ParseMode.MARKDOWN} + message_args = {"parse_mode": ParseMode.HTML} if image_file_id: message_args["photo"] = image_file_id message_args["caption"] = announce_caption @@ -960,7 +967,7 @@ async def _announce_raffle_in_channels(context: ContextTypes.DEFAULT_TYPE, raffl try: msg_to_admin = "Anuncio enviado con éxito." - await context.bot.send_message(admin_user_id, msg_to_admin, parse_mode=ParseMode.MARKDOWN) + await context.bot.send_message(admin_user_id, msg_to_admin, parse_mode=ParseMode.HTML) except Exception as e: logger.error(f"Failed to send announcement summary to admin {admin_user_id}: {e}") diff --git a/app/helpers.py b/app/helpers.py index ea70928..7a4bd7d 100644 --- a/app/helpers.py +++ b/app/helpers.py @@ -77,7 +77,7 @@ def get_winners(raffle_id, winner_numbers_int): continue user_id = participant['user_id'] - user_name = escape_markdown_v2_chars_for_username(participant['user_name']) or f"User_{user_id}" # Fallback name + user_name = participant['user_name'] or f"User_{user_id}" # Fallback name numbers_str = participant['numbers'] try: @@ -104,7 +104,7 @@ def get_winners(raffle_id, winner_numbers_int): for user_name, numbers in winners.items(): # Ensure numbers are unique in the final output per user unique_numbers_str = ", ".join(sorted(list(set(numbers)))) - winners_message_parts.append(f"- @{escape_markdown_v2_chars_for_username(user_name)} acertó: **{unique_numbers_str}**") + winners_message_parts.append(f"- @{user_name} acertó: {unique_numbers_str}") return "\n".join(winners_message_parts) @@ -371,22 +371,6 @@ def generate_table_image(raffle_id): img.save(image_path) return True - -def escape_markdown_v2_chars_for_username(text: str) -> str: - """Escapes characters for MarkdownV2, specifically for usernames.""" - # For usernames, usually only _ and * are problematic if not part of actual formatting - # Other MarkdownV2 special characters: `[` `]` `(` `)` `~` `>` `#` `+` `-` `=` `|` `{` `}` `.` `!` - # We are most concerned with _ in @user_name context. - # A more comprehensive list of characters to escape for general text: - # escape_chars = r'_*[]()~`>#+-=|{}.!' - # For just usernames in this context, focus on what breaks @user_name - escape_chars = r'_*`[' # Adding ` and [ just in case they appear in odd usernames - - # Python's re.escape escapes all non-alphanumerics. - # We only want to escape specific markdown control characters within the username. - # For usernames, simply escaping '_' is often enough for the @mention issue. - return "".join(['\\' + char if char in escape_chars else char for char in text]) - def format_last_participants_list(participants_list: list) -> str: """ Formats the list of last participants for the announcement message. @@ -405,9 +389,9 @@ def format_last_participants_list(participants_list: list) -> str: if numbers_str: num_list = numbers_str.split(',') if len(num_list) == 1: - line = f" - {escape_markdown_v2_chars_for_username(user_name)}, con la papeleta: {num_list[0]}" + line = f" - {user_name}, con la papeleta: {num_list[0]}" else: - line = f" - {escape_markdown_v2_chars_for_username(user_name)}, con las papeletas: {', '.join(num_list)}" + line = f" - {user_name}, con las papeletas: {', '.join(num_list)}" formatted_lines.append(line) return "\n".join(formatted_lines) # Add a trailing newline diff --git a/app/paypal_processor.py b/app/paypal_processor.py index 88ba9d3..fa33513 100644 --- a/app/paypal_processor.py +++ b/app/paypal_processor.py @@ -9,7 +9,7 @@ import os # Import os to get BOT_TOKEN # Import necessary functions from your project structure # Adjust the path if paypal_processor.py is not in the root 'app' directory # Assuming it can access the other modules directly as in the docker setup: -from helpers import generate_table_image, format_last_participants_list, escape_markdown_v2_chars_for_username, get_paypal_access_token +from helpers import generate_table_image, format_last_participants_list, get_paypal_access_token from database import ( get_user_by_invoice_id, confirm_reserved_numbers, get_raffle_name, get_raffle, @@ -222,18 +222,18 @@ def receive_paypal_payment(invoice_id, payment_status, payment_amount_str): # If it's the last number, update the main message and delete the participate button if remaining_numbers_amount == 0: keyboard = None - main_announcement = f"🎯🏆🎯 **Sorteo '{raffle_name}' terminado** 🎯🏆🎯\n\n" + main_announcement = f"🎯🏆🎯 Sorteo '{raffle_name}' terminado 🎯🏆🎯\n\n" main_announcement += f"{raffle_details['description']}\n\n" main_announcement += f"🌍 Envío internacional: {'Sí ✅' if raffle_details['international_shipping'] else 'No ❌'}\n" - main_announcement += f"💵 **Donación mínima:** {raffle_details['price']}€\n" - main_announcement += f"📜 Normas y condiciones: {TYC_DOCUMENT_URL}" + main_announcement += f"💵 Donación mínima: {raffle_details['price']}€\n" + main_announcement += f"📜 Normas y condiciones: {TYC_DOCUMENT_URL}" main_message_id = get_main_message_id(raffle_id) requests.post(f"{TELEGRAM_API_URL}/editMessageCaption", json={ "chat_id": channel_id_to_announce, "message_id": main_message_id, "caption": main_announcement, "reply_markup": keyboard, - "parse_mode": "Markdown" + "parse_mode": "HTML" }) else: url = f"https://t.me/{BOT_NAME}?start=join_{raffle_id}" @@ -247,22 +247,22 @@ def receive_paypal_payment(invoice_id, payment_status, payment_amount_str): main_announcement = f"🏆 Sorteo '{raffle_name}' en progreso 🏆\n\n" main_announcement += f"{raffle_details['description']}\n\n" main_announcement += f"🌍 Envío internacional: {'Sí ✅' if raffle_details['international_shipping'] else 'No ❌'}\n" - main_announcement += f"💵 **Donación mínima:** {raffle_details['price']}€\n" + main_announcement += f"💵 Donación mínima: {raffle_details['price']}€\n" main_announcement += f"🗒️ Quedan {remaining_numbers_amount} participaciones disponibles. ¡Date prisa! 🗒️\n\n" - main_announcement += f"📜 Normas y condiciones: {TYC_DOCUMENT_URL}" + main_announcement += f"📜 Normas y condiciones: {TYC_DOCUMENT_URL}" main_message_id = get_main_message_id(raffle_id) requests.post(f"{TELEGRAM_API_URL}/editMessageCaption", json={ "chat_id": channel_id_to_announce, "message_id": main_message_id, "caption": main_announcement, "reply_markup": keyboard, - "parse_mode": "Markdown" + "parse_mode": "HTML" }) last_other_participants = get_last_n_other_participants(raffle_id, n=4) last_participants_text = format_last_participants_list(last_other_participants) - escaped_current_user_name = escape_markdown_v2_chars_for_username(current_user_name) + escaped_current_user_name = current_user_name numbers_text = "" if len(numbers) > 1: @@ -317,7 +317,7 @@ def receive_paypal_payment(invoice_id, payment_status, payment_amount_str): logger.warning(f"Error deleting old message {update_message_id}: {e_del}. Will send new.") # Always send new photo after delete attempt, ensures updated image is shown - new_msg_info = send_telegram_photo(channel_id_to_announce, image_path, caption=caption, keyboard=keyboard, parse_mode='Markdown') + new_msg_info = send_telegram_photo(channel_id_to_announce, image_path, caption=caption, keyboard=keyboard, parse_mode='HTML') if new_msg_info and isinstance(new_msg_info, dict) and 'message_id' in new_msg_info: # If send_telegram_photo returns message object sent_or_edited_message_id = new_msg_info['message_id'] elif isinstance(new_msg_info, bool) and new_msg_info is True: # If it just returns True/False @@ -328,7 +328,7 @@ def receive_paypal_payment(invoice_id, payment_status, payment_amount_str): else: # No previous message, send new logger.info(f"No previous message found for raffle {raffle_id} in channel {channel_id_to_announce}. Sending new.") - new_msg_info = send_telegram_photo(channel_id_to_announce, image_path, caption=caption, keyboard=keyboard, parse_mode='Markdown') + new_msg_info = send_telegram_photo(channel_id_to_announce, image_path, caption=caption, keyboard=keyboard, parse_mode='HTML') # Similar logic to get sent_or_edited_message_id as above if new_msg_info and isinstance(new_msg_info, dict) and 'message_id' in new_msg_info: sent_or_edited_message_id = new_msg_info['message_id']