Did some things...

This commit is contained in:
Joan
2025-09-15 22:45:46 +02:00
parent d3b4cd7eaa
commit 2bfdb539be
9 changed files with 410 additions and 195 deletions

View File

@@ -21,8 +21,6 @@ from datetime import time as dtime
logger = logging.getLogger(__name__)
# --- Conversation Handler for Raffle Creation ---
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
user = update.message.from_user
args = context.args
@@ -36,33 +34,37 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
madrid_tz = pytz.timezone("Europe/Madrid")
current_time = datetime.now(madrid_tz)
if current_time.time() >= dtime(20, 55) and current_time.time() <= dtime(22, 0):
await update.message.reply_text("No puedes unirte a la rifa en este momento.")
await update.message.reply_text("No puedes unirte al sorteo en este momento.")
return
if len(get_reserved_numbers(user.id, raffle_id)) > 0:
await update.message.reply_text("Ya tienes participaciones reservadas para este sorteo. Por favor, completa la donación o espera a que caduquen antes de unirte de nuevo.")
return
# The user wants to join this raffle.
# Start the private conversation for number selection.
logger.info(f"User {user.id} started bot with join link for raffle {raffle_id}")
context.user_data['joining_raffle_id'] = raffle_id
keyboard = generate_numbers_keyboard(raffle_id, user.id)
await update.message.reply_text(
f"¡Hola! Vamos a unirnos a la rifa '{raffle['name']}'.\n\n"
f"El precio por papeleta es de {raffle['price']}€.\n\n"
f"¡Hola! Vamos a unirnos al sorteo '{raffle['name']}'.\n\n"
f"🌍 Envío internacional: {'Sí ✅' if raffle['international_shipping'] else 'No ❌'}\n"
f"La donación mínima es de {raffle['price']}€.\n\n"
"Por favor, selecciona tus números:",
reply_markup=keyboard
)
else:
await update.message.reply_text("Esta rifa ya no está activa o no tiene números disponibles.")
await update.message.reply_text("Este sorteo ya no está activo o no tiene números disponibles.")
except (ValueError, IndexError):
await update.message.reply_text("Enlace de participación inválido.")
else:
# Generic start message
await update.message.reply_text("Hola, soy el bot de rifas. Puedes participar desde los anuncios en los canales.")
await update.message.reply_text("Hola, soy el bot de La Suerte es Tuya. Puedes participar desde los anuncios en los canales.")
async def new_raffle_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Starts the conversation to create a new raffle. Asks for channels."""
user_id = update.message.from_user.id
if user_id not in ADMIN_IDS:
await update.message.reply_text("No tienes permiso para crear rifas.")
await update.message.reply_text("No tienes permiso para crear sorteos.")
return ConversationHandler.END
if not CHANNELS:
@@ -72,8 +74,8 @@ async def new_raffle_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -
context.user_data['new_raffle'] = {'channel': ""} # Initialize data for this user
keyboard = generate_channel_selection_keyboard()
await update.message.reply_text(
"Vamos a crear una nueva rifa.\n\n"
"**Paso 1:** Selecciona el canal donde se publicará la rifa.",
"Vamos a crear un nuevo sorteo.\n\n"
"**Paso 1:** Selecciona el canal donde se publicará el sorteo.",
reply_markup=keyboard,
parse_mode=ParseMode.MARKDOWN
)
@@ -91,7 +93,7 @@ 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** de la rifa.",
"Canal seleccionad. Ahora, por favor, envía el **título** del sorteo.",
parse_mode=ParseMode.MARKDOWN
)
return TYPING_TITLE
@@ -108,7 +110,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** de la rifa.", parse_mode=ParseMode.MARKDOWN)
await update.message.reply_text("Título guardado. Ahora envía la **descripción** del sorteo.", parse_mode=ParseMode.MARKDOWN)
return TYPING_DESCRIPTION
async def receive_description(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
@@ -119,7 +121,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 la rifa.", parse_mode=ParseMode.MARKDOWN)
await update.message.reply_text("Descripción guardada. Ahora envía la **imagen** para el sorteo.", parse_mode=ParseMode.MARKDOWN)
return SENDING_IMAGE
async def receive_image(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
@@ -132,7 +134,24 @@ async def receive_image(update: Update, context: ContextTypes.DEFAULT_TYPE) -> i
context.user_data['new_raffle']['image_file_id'] = photo_file_id
await update.message.reply_text(
"Imagen guardada.\nAhora, introduce el precio por número para el canal seleccionado (solo el número, ej: 5).",
"Imagen guardada. Ahora, ¿el sorteo permite envíos internacionales? Responde con '' o 'No'.",
parse_mode=ParseMode.MARKDOWN
)
return INTERNATIONAL_SHIPPING
async def receive_international_shipping(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Receives whether international shipping is available."""
text = update.message.text.strip().lower()
if text in ['', 'si', 's', 'yes', 'y']:
context.user_data['new_raffle']['international_shipping'] = 1
elif text in ['no', 'n']:
context.user_data['new_raffle']['international_shipping'] = 0
else:
await update.message.reply_text("Por favor, responde con '' o 'No'.")
return INTERNATIONAL_SHIPPING
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
)
return TYPING_PRICE_FOR_CHANNEL
@@ -144,8 +163,8 @@ async def receive_price_for_channel(update: Update, context: ContextTypes.DEFAUL
try:
price = int(price_text)
if not (0 <= price <= 999): # Allow higher prices maybe
raise ValueError("El precio debe ser un número entre 0 y 999.")
if not (1 <= price <= 999): # Allow higher prices maybe
raise ValueError("El precio debe ser un número entre 1 y 999.")
except ValueError:
channel_alias = REVERSE_CHANNELS.get(channel_id, f"ID:{channel_id}")
await update.message.reply_text(f"Precio inválido para {channel_alias}. Debe ser un número (ej: 5). Inténtalo de nuevo.")
@@ -164,13 +183,14 @@ async def _show_creation_confirmation(update: Update, context: ContextTypes.DEFA
price = raffle_data.get('price', 0)
confirmation_text = (
"¡Perfecto! Revisa los datos de la rifa:\n\n"
"¡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"💶 **Precio:** {price}\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 esta rifa?"
"¿Confirmas la creación de este sorteo?"
)
keyboard = generate_confirmation_keyboard()
# Message from which confirmation is triggered is the last price input message.
@@ -209,10 +229,10 @@ async def confirm_creation(update: Update, context: ContextTypes.DEFAULT_TYPE) -
raffle_id = create_raffle(name, description, price, image_file_id, channel_id)
if raffle_id:
await context.bot.send_message(query.from_user.id, f"✅ ¡Rifa '{name}' creada con éxito!")
await context.bot.send_message(query.from_user.id, f"✅ ¡Sorteo '{name}' creado con éxito!")
await _announce_raffle_in_channels(context, raffle_id, query.from_user.id, initial_announcement=True)
else:
await context.bot.send_message(query.from_user.id, f"❌ Error al guardar la rifa. Nombre '{name}' podría existir.")
await context.bot.send_message(query.from_user.id, f"❌ Error al guardar el sorteo. Nombre '{name}' podría existir.")
elif query.data == CANCEL_CREATION_CALLBACK:
await query.edit_message_caption("Creación cancelada.", reply_markup=None)
@@ -225,11 +245,11 @@ async def cancel_creation_command(update: Update, context: ContextTypes.DEFAULT_
user_id = update.message.from_user.id
if 'new_raffle' in context.user_data:
context.user_data.pop('new_raffle', None)
await update.message.reply_text("Creación de rifa cancelada.")
await update.message.reply_text("Creación de sorteo cancelada.")
logger.info(f"Admin {user_id} cancelled raffle creation via /cancel.")
return ConversationHandler.END
else:
await update.message.reply_text("No hay ninguna creación de rifa en curso para cancelar.")
await update.message.reply_text("No hay ninguna creación de sorteo en curso para cancelar.")
return ConversationHandler.END # Or return current state if applicable
# ... (other handlers and functions) ...
@@ -249,7 +269,7 @@ async def incorrect_input_type(update: Update, context: ContextTypes.DEFAULT_TYP
current_conversation_data = context.user_data['new_raffle']
current_state_key = '_new_raffle_conv_state'
current_state = context.user_data[current_state_key]
conversation_type = "creación de rifa"
conversation_type = "creación de sorteo"
else:
# Not in a known conversation, or state tracking key is different.
# This message might be outside any conversation this handler is for.
@@ -273,24 +293,24 @@ async def incorrect_input_type(update: Update, context: ContextTypes.DEFAULT_TYP
logger.warning(f"User {user_id} sent incorrect input type during {conversation_type} (State: {current_state}). Message: {update.message.text or '<Not Text>'}")
# --- Handle incorrect input for RAFFLE CREATION states ---
if conversation_type == "creación de rifa":
if conversation_type == "creación de sorteo":
if current_state == SENDING_IMAGE:
await update.message.reply_text(
"Por favor, envía una IMAGEN para la rifa, no texto u otro tipo de archivo.\n"
"Por favor, envía una IMAGEN para el sorteo, no texto u otro tipo de archivo.\n"
"Si quieres cancelar, usa /cancelar."
)
return SENDING_IMAGE # Stay in the image sending state
elif current_state == TYPING_TITLE:
await update.message.reply_text("Por favor, envía TEXTO para el título de la rifa. Usa /cancelar para salir.")
await update.message.reply_text("Por favor, envía TEXTO para el título del sorteo. Usa /cancelar para salir.")
return TYPING_TITLE
elif current_state == TYPING_DESCRIPTION:
await update.message.reply_text("Por favor, envía TEXTO para la descripción de la rifa. Usa /cancelar para salir.")
await update.message.reply_text("Por favor, envía TEXTO para la descripción del sorteo. Usa /cancelar para salir.")
return TYPING_DESCRIPTION
elif current_state == TYPING_PRICE_FOR_CHANNEL:
channel_id_for_price = current_conversation_data.get('current_channel_for_price')
channel_alias = REVERSE_CHANNELS.get(channel_id_for_price, f"ID:{channel_id_for_price}") if channel_id_for_price else "el canal actual"
await update.message.reply_text(
f"Por favor, envía un NÚMERO para el precio de la rifa en {channel_alias}.\n"
f"Por favor, envía un NÚMERO para el precio del sorteo en {channel_alias}.\n"
"Usa /cancelar para salir."
)
return TYPING_PRICE_FOR_CHANNEL
@@ -299,8 +319,8 @@ async def incorrect_input_type(update: Update, context: ContextTypes.DEFAULT_TYP
# Generic fallback message if specific state isn't handled above for some reason
await update.message.reply_text(
"Entrada no válida para este paso de la conversación.\n"
"Si estás creando una rifa, usa /cancelar para salir.\n"
"Si estás editando una rifa, usa /cancelar_edicion para salir."
"Si estás creando un sorteo, usa /cancelar para salir.\n"
"Si estás editando un sorteo, usa /cancelar_edicion para salir."
)
return current_state # Return to the state the conversation was in
@@ -384,14 +404,14 @@ async def number_callback(update: Update, context: CallbackContext):
page = 0 if number < 50 else 1
except ValueError:
logger.warning(f"Invalid number value in callback: {value}")
await query.answer("Papeleta no válida.")
await query.answer("Participación no válida.")
return
elif action == "random_num":
# Handle random number selection
try:
remaining_free_numbers = get_remaining_numbers(raffle_id)
if not remaining_free_numbers:
await query.answer("No hay papeletas disponibles para seleccionar aleatoriamente.")
await query.answer("No hay participaciones disponibles para seleccionar aleatoriamente.")
return
else:
# Select a random number from the available ones
@@ -401,7 +421,7 @@ async def number_callback(update: Update, context: CallbackContext):
page = 0 if int(number_string) < 50 else 1
except ValueError:
logger.warning(f"Invalid random number value in callback: {value}")
await query.answer("Papeleta aleatoria no válido.")
await query.answer("Participación aleatoria no válida.")
return
logger.debug(f"User {user_id} interacted with number {number_string} for raffle {raffle_id}")
@@ -419,30 +439,30 @@ async def number_callback(update: Update, context: CallbackContext):
if participant_step == "waiting_for_payment":
# User clicked a number they have reserved -> Deselect it
remove_reserved_number(participant_db_id, number_string)
await query.answer(f"Has quitado la reserva de la papeleta {number_string}.")
await query.answer(f"Has quitado la reserva de la participación {number_string}.")
logger.info(f"User {user_id} deselected reserved number {number_string} for raffle {raffle_id}")
# Refresh keyboard
keyboard = generate_numbers_keyboard(raffle_id, user_id, page=page)
await query.edit_message_reply_markup(reply_markup=keyboard)
elif participant_step == "completed":
# User clicked a number they have paid for -> Inform them
await query.answer(f"Ya tienes la papeleta {number_string} (pagado).")
await query.answer(f"Ya tienes la participación {number_string} (donada).")
logger.debug(f"User {user_id} clicked their own completed number {number_string}")
else:
# Should not happen with current steps, but catch just in case
await query.answer(f"Estado desconocido para tu papeleta {number_string}.")
await query.answer(f"Estado desconocido para tu participación {number_string}.")
logger.warning(f"User {user_id} clicked number {number_string} with unexpected step {participant_step}")
else:
# User clicked a number taken by someone else
status_msg = "reservada" if participant_step == "waiting_for_payment" else "comprada"
await query.answer(f"La papeleta {number_string} ya ha sido {status_msg} por otro usuario.")
status_msg = "reservada" if participant_step == "waiting_for_payment" else "donada"
await query.answer(f"La participación {number_string} ya ha sido {status_msg} por otro usuario.")
logger.debug(f"User {user_id} clicked number {number_string} taken by user {participant_user_id}")
else:
# Number is free -> Reserve it for the user
reserve_number(user_id, username, raffle_id, number_string)
await query.answer(f"Papeleta {number_string} reservada para ti. Confirma tu selección cuando termines.")
await query.answer(f"Participación {number_string} reservada para ti. Confirma tu selección cuando termines.")
logger.info(f"User {user_id} reserved number {number_string} for raffle {raffle_id}")
# Refresh keyboard to show the lock icon
keyboard = generate_numbers_keyboard(raffle_id, user_id, page=page)
@@ -465,19 +485,19 @@ async def confirm_callback(update: Update, context: CallbackContext):
reserved_numbers = get_reserved_numbers(user_id, raffle_id) # Returns a list of strings
if not reserved_numbers:
await query.answer("No has seleccionado ninguna papeleta nueva para confirmar.")
await query.answer("No has seleccionado ninguna participación nueva para confirmar.")
return
await query.answer("Generando enlace de pago...") # Give feedback
await query.answer("Generando enlace de donación...") # Give feedback
raffle_info = get_raffle(raffle_id)
if not raffle_info:
logger.error(f"Cannot find raffle {raffle_id} during confirmation for user {user_id}")
# Use query.edit_message_caption or text depending on what was sent before
try:
await query.edit_message_caption("Error: No se pudo encontrar la información de la rifa.", reply_markup=None)
await query.edit_message_caption("Error: No se pudo encontrar la información del sorteo.", reply_markup=None)
except BadRequest:
await query.edit_message_text("Error: No se pudo encontrar la información de la rifa.", reply_markup=None)
await query.edit_message_text("Error: No se pudo encontrar la información del sorteo.", reply_markup=None)
return
# Get participant DB ID (needed for setting invoice ID)
@@ -486,9 +506,9 @@ async def confirm_callback(update: Update, context: CallbackContext):
if not participant:
logger.error(f"Cannot find participant record for user {user_id}, raffle {raffle_id} in 'waiting_for_payment' step during confirmation.")
try:
await query.edit_message_caption("Error: No se encontró tu registro. Selecciona las papeletas de nuevo.", reply_markup=None)
await query.edit_message_caption("Error: No se encontró tu registro. Selecciona las participaciones de nuevo.", reply_markup=None)
except BadRequest:
await query.edit_message_text("Error: No se encontró tu registro. Selecciona las papeletas de nuevo.", reply_markup=None)
await query.edit_message_text("Error: No se encontró tu registro. Selecciona las participaciones de nuevo.", reply_markup=None)
return
participant_db_id = participant['id']
@@ -497,37 +517,16 @@ async def confirm_callback(update: Update, context: CallbackContext):
logger.error(f"Price not found for raffle {raffle_id} during confirmation for user {user_id}")
await query.answer("Error: Ha habido un problema desconocido, contacta con el administrador.", show_alert=True)
return
total_price = len(reserved_numbers) * price_per_number
# Generate a unique invoice ID for PayPal
#invoice_id = str(uuid.uuid4())
current_timestamp = time.time()
paypal_link, invoice_id = create_paypal_order(get_paypal_access_token(), total_price)
mark_reservation_pending(participant_db_id, invoice_id, current_timestamp)
# Construct PayPal link
# Using _xclick for simple payments. Consider PayPal REST API for more robust integration if needed.
# paypal_link = (
# f"https://sandbox.paypal.com/cgi-bin/webscr?"
# f"cmd=_xclick&business={PAYPAL_EMAIL}"
# f"&item_name=Numeros Rifa con ID: {raffle_info['id']} ({', '.join(reserved_numbers)})" # Item name for clarity
# f"&amount={total_price:.2f}" # Format price to 2 decimal places
# f"&currency_code=EUR"
# f"&invoice={invoice_id}" # CRITICAL: This links the payment back
# f"&verify_url={WEBHOOK_URL}" # IMPORTANT: Set your actual webhook URL here!
# # custom field can be used for extra data if needed, e.g., participant_db_id again
# f"&custom={participant_db_id}"
# f"&return=https://t.me/{BOT_NAME}" # Optional: URL after successful payment
# f"&cancel_return=https://t.me/{BOT_NAME}" # Optional: URL if user cancels on PayPal
# )
paypal_link, invoice_id = create_paypal_order(get_paypal_access_token(), total_price, raffle_info['id'], reserved_numbers)
mark_reservation_pending(participant_db_id, invoice_id)
# Log the PayPal link for debugging
logger.info(f"Generated PayPal link for user {user_id}: {paypal_link}")
# Define the button text and create the URL button
paypal_button_text = "💳 Pagar con PayPal 💳"
paypal_button_text = "💳 Donar con PayPal 💳"
paypal_button = InlineKeyboardButton(paypal_button_text, url=paypal_link)
# Create the InlineKeyboardMarkup containing the button
@@ -536,13 +535,13 @@ async def confirm_callback(update: Update, context: CallbackContext):
# Modify the message text - remove the link placeholder, adjust instruction
payment_message = (
f"👍 Selección Confirmada 👍\n\n"
f"Papeletas reservadas: {', '.join(reserved_numbers)}\n"
f"Precio total: {total_price:.2f}\n\n"
f"Participaciones reservadas: {', '.join(reserved_numbers)}\n"
f"Donación mínima: {total_price:.2f}\n\n"
f"El ID de la factura es: {invoice_id}\n"
f"Pulsa el botón para completar el pago en PayPal:\n" # Adjusted instruction
f"Pulsa el botón para completar la donación en PayPal:\n" # Adjusted instruction
# Link is now in the button below
f"⚠️ Tienes {RESERVATION_TIMEOUT_MINUTES} minutos para pagar antes de que las papeletas se liberen.\n\n"
f"Una vez hayas pagado, se te notificará aquí. El pago puede tardar hasta 5 minutos en procesarse, sé paciente."
f"⚠️ Tienes {RESERVATION_TIMEOUT_MINUTES} minutos para pagar antes de que las participaciones se liberen.\n\n"
f"Una vez hayas donado, se te notificará aquí. La donación puede tardar hasta 5 minutos en procesarse, sé paciente."
)
try:
@@ -574,26 +573,26 @@ async def cancel_callback(update: Update, context: CallbackContext):
reserved_numbers = get_reserved_numbers(user_id, raffle_id)
if not reserved_numbers:
await query.answer("No tienes ninguna selección de papeletas pendiente para cancelar.")
await query.answer("No tienes ninguna selección de participaciones pendiente para cancelar.")
# Optionally, revert the message back to the initial raffle selection prompt or just inform the user.
# Let's just edit the message to say nothing is pending.
try:
await query.edit_message_caption("No hay papeletas reservadas para cancelar.", reply_markup=None)
await query.edit_message_caption("No hay participaciones reservadas para cancelar.", reply_markup=None)
except BadRequest: # If message was text, not photo caption
await query.edit_message_text("No hay papeletas reservadas para cancelar.", reply_markup=None)
await query.edit_message_text("No hay participaciones reservadas para cancelar.", reply_markup=None)
return
# Cancel the reservation in the database
cancelled = cancel_reserved_numbers(user_id, raffle_id) # This function deletes the 'waiting_for_payment' row
if cancelled: # Check if the function indicated success (e.g., return True or affected rows)
await query.answer(f"Selección cancelada. Papeletas liberadas: {', '.join(reserved_numbers)}")
await query.answer(f"Selección cancelada. Participaciones liberadas: {', '.join(reserved_numbers)}")
logger.info(f"User {user_id} cancelled reservation for raffle {raffle_id}. Numbers: {reserved_numbers}")
# Edit the message to confirm cancellation
try:
await query.edit_message_caption("Tu selección de papeletas ha sido cancelada y las papeletas han sido liberadas.", reply_markup=None)
await query.edit_message_caption("Tu selección de participaciones ha sido cancelada y las participaciones han sido liberadas.", reply_markup=None)
except BadRequest:
await query.edit_message_text("Tu selección de papeletas ha sido cancelada y las papeletas han sido liberadas.", reply_markup=None)
await query.edit_message_text("Tu selección de participaciones ha sido cancelada y las participaciones han sido liberadas.", reply_markup=None)
# Optionally, you could send the user back to the raffle selection list or number grid.
# For simplicity, we just end the interaction here.
else:
@@ -609,7 +608,7 @@ async def end_raffle_logic(context: ContextTypes.DEFAULT_TYPE, raffle_id: int, w
if not raffle_details:
logger.error(f"Attempted to end non-existent raffle ID {raffle_id}")
try:
await context.bot.send_message(admin_user_id, f"Error: No se encontró la rifa con ID {raffle_id}.")
await context.bot.send_message(admin_user_id, f"Error: No se encontró el sorteo con ID {raffle_id}.")
except Exception as e:
logger.error(f"Failed to send error message to admin {admin_user_id}: {e}")
return False
@@ -619,7 +618,7 @@ async def end_raffle_logic(context: ContextTypes.DEFAULT_TYPE, raffle_id: int, w
if not raffle_details['active']:
logger.warning(f"Admin {admin_user_id} tried to end already inactive raffle '{raffle_name}' (ID: {raffle_id})")
try:
await context.bot.send_message(admin_user_id, f"La rifa '{raffle_name}' ya estaba terminada.")
await context.bot.send_message(admin_user_id, f"El sorteo '{raffle_name}' ya estaba terminado.")
except Exception as e:
logger.error(f"Failed to send 'already inactive' message to admin {admin_user_id}: {e}")
return False
@@ -628,7 +627,7 @@ async def end_raffle_logic(context: ContextTypes.DEFAULT_TYPE, raffle_id: int, w
if not end_raffle(raffle_id):
logger.error(f"Failed to mark raffle ID {raffle_id} as inactive in the database.")
try:
await context.bot.send_message(admin_user_id, f"Error al marcar la rifa '{raffle_name}' como terminada en la base de datos.")
await context.bot.send_message(admin_user_id, f"Error al marcar el sorteo '{raffle_name}' como terminado en la base de datos.")
except Exception as e:
logger.error(f"Failed to send DB error message to admin {admin_user_id}: {e}")
return False
@@ -641,19 +640,20 @@ 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 de la Rifa '{raffle_name}'!** 🎯🏆🎯\n\n"
announcement += f"Detalles de la rifa: https://t.me/{channel_alias}/{get_main_message_id(raffle_id)}\n"
announcement += f"Papeletas ganadoras: **{formatted_winner_numbers}**\n\n" if len(winner_numbers) > 1 else f"Papeleta ganadora: **{formatted_winner_numbers}**\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"
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:
announcement += "No hubo ganadores para estas papeletas." if len(winner_numbers) > 1 else "No hubo ganador para esta papeleta."
announcement += "No hubo ganadores para estas participaciones." if len(winner_numbers) > 1 else "No hubo ganador para esta participación."
announcement += f"\nPuedes comprobar los resultados en {JUEGOS_ONCE_URL}"
announcement += "\n\nGracias a todos por participar. Mantente atento a futuras rifas."
announcement += "\n\nGracias a todos por participar. Mantente atento a futuros sorteos."
main_announcement = f"🎯🏆🎯 **Rifa '{raffle_name}' Terminada** 🎯🏆🎯\n\n"
main_announcement = f"🎯🏆🎯 **Sorteo '{raffle_name}' terminado** 🎯🏆🎯\n\n"
main_announcement += f"{raffle_details['description']}\n\n"
main_announcement += f"💵 **Precio por papeleta:** {raffle_details['price']}\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_message_id = get_main_message_id(raffle_id)
@@ -668,6 +668,31 @@ async def end_raffle_logic(context: ContextTypes.DEFAULT_TYPE, raffle_id: int, w
except Exception as e:
logger.error(f"Failed to announce winners in channel {channel_alias} (ID: {channel_id_str}): {e}")
# Notify admin of success
try:
for admin_user_id in ADMIN_IDS:
await context.bot.send_message(admin_user_id, f"Sorteo '{raffle_name}' terminado y ganadores anunciados.")
number_of_participations = get_total_participations(raffle_id)
price_per_number = raffle_details['price']
invoice_ids = get_all_invoice_ids(raffle_id)
for inv_id in invoice_ids:
gross, net, fees = get_paypal_amounts_for_invoice(inv_id)
total_gross += gross
total_net += net
total_fees += fees
await context.bot.send_message(
admin_user_id,
f"Resumen del sorteo '{raffle_name}':\n"
f"- Participaciones totales: {number_of_participations}\n"
f"- Donación mínima por participación: {price_per_number}\n"
f"- Total recaudado: {total_gross}\n"
f"- Total en comisiones de PayPal: {total_fees:.2f}\n"
f"- Beneficio neto estimado: {total_net}"
)
except Exception as e:
logger.error(f"Failed to notify admin {admin_user_id} about ended raffle '{raffle_name}': {e}")
return True
# --- Admin Menu Handlers ---
@@ -710,8 +735,8 @@ async def admin_menu_callback(update: Update, context: ContextTypes.DEFAULT_TYPE
if data == ADMIN_MENU_CREATE:
# Guide admin to use the conversation command
await query.edit_message_text(
"Para crear una nueva rifa, por favor, inicia la conversación usando el comando:\n\n"
"/crear_rifa\n\n"
"Para crear un nuevo sorteo, inicia la conversación usando el comando:\n\n"
"/crear_sorteo\n\n"
"Pulsa el botón abajo para volver al menú principal.",
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Volver al Menú", callback_data=ADMIN_MENU_BACK_MAIN)]])
)
@@ -720,7 +745,7 @@ 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 = "**Rifas Activas**\n\nSelecciona una rifa para ver detalles, anunciar o terminar:" if active_raffles else "**Rifas Activas**\n\nNo hay rifas activas."
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)
elif data == ADMIN_MENU_BACK_MAIN:
@@ -730,39 +755,40 @@ async def admin_menu_callback(update: Update, context: ContextTypes.DEFAULT_TYPE
# --- Raffle Specific Actions ---
elif data.startswith(ADMIN_VIEW_RAFFLE_PREFIX):
try:
await query.answer("Cargando detalles del sorteo...")
raffle_id = int(data[len(ADMIN_VIEW_RAFFLE_PREFIX):])
logger.info(f"Admin {user_id} requested details for raffle {raffle_id}")
details_text = format_raffle_details(raffle_id) # Use helper
details_keyboard = generate_admin_raffle_details_keyboard(raffle_id)
await query.edit_message_text(details_text, reply_markup=details_keyboard, parse_mode=ParseMode.MARKDOWN)
await query.edit_message_text(details_text, reply_markup=details_keyboard, parse_mode=ParseMode.HTML)
except (ValueError, IndexError):
logger.error(f"Invalid callback data for view raffle: {data}")
await query.edit_message_text("Error: ID de rifa inválido.", reply_markup=generate_admin_list_raffles_keyboard())
await query.edit_message_text("Error: ID de sorteo inválido.", reply_markup=generate_admin_list_raffles_keyboard())
elif data.startswith(ADMIN_ANNOUNCE_RAFFLE_PREFIX):
try:
raffle_id = int(data[len(ADMIN_ANNOUNCE_RAFFLE_PREFIX):])
logger.info(f"Admin {user_id} requested re-announce for raffle {raffle_id}")
await query.edit_message_text(f"📢 Re-anunciando la rifa {get_raffle_name(raffle_id)}...", reply_markup=None) # Give feedback
await query.edit_message_text(f"📢 Re-anunciando el sorteo {get_raffle_name(raffle_id)}...", reply_markup=None) # Give feedback
await _announce_raffle_in_channels(context, raffle_id, user_id)
# Optionally go back to the list or details view after announcing
keyboard = generate_admin_list_raffles_keyboard() # Go back to list
await context.bot.send_message(user_id, "Volviendo a la lista de rifas:", reply_markup=keyboard)
await context.bot.send_message(user_id, "Volviendo a la lista de sorteos:", reply_markup=keyboard)
except (ValueError, IndexError):
logger.error(f"Invalid callback data for announce raffle: {data}")
await query.edit_message_text("Error: ID de rifa inválido.", reply_markup=generate_admin_list_raffles_keyboard())
await query.edit_message_text("Error: ID de sorteo inválido.", reply_markup=generate_admin_list_raffles_keyboard())
elif data.startswith(ADMIN_END_RAFFLE_PROMPT_PREFIX):
try:
raffle_id = int(data[len(ADMIN_END_RAFFLE_PROMPT_PREFIX):])
except (ValueError, IndexError):
logger.error(f"Invalid callback data for end raffle prompt: {data}")
await query.edit_message_text("Error: ID de rifa inválido.", reply_markup=generate_admin_main_menu_keyboard())
await query.edit_message_text("Error: ID de sorteo inválido.", reply_markup=generate_admin_main_menu_keyboard())
return
raffle = get_raffle(raffle_id)
if not raffle or not raffle['active']:
await query.edit_message_text("Esta rifa no existe o ya ha terminado.", reply_markup=generate_admin_list_raffles_keyboard())
await query.edit_message_text("Este sorteo no existe o ya ha terminado.", reply_markup=generate_admin_list_raffles_keyboard())
return
# Store raffle ID and set flag to expect winner numbers
@@ -772,8 +798,8 @@ 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 la rifa: **{raffle['name']}**\n\n"
"Por favor, envía ahora las **papeletas 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
)
@@ -784,10 +810,10 @@ 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("**Rifas Activas**\n\nSelecciona una rifa para terminarla:", 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.MARKDOWN)
elif data == ADMIN_NO_OP:
# Just ignore clicks on placeholder buttons like "No hay rifas activas"
# Just ignore clicks on placeholder buttons like "No hay sorteos activos"
pass
async def admin_receive_winner_numbers(update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -825,8 +851,8 @@ async def admin_receive_winner_numbers(update: Update, context: ContextTypes.DEF
logger.warning(f"Invalid winner numbers format from admin {user_id}: {e}")
keyboard = generate_admin_cancel_end_keyboard()
await update.message.reply_text(
f"❌ Papeletas inválidas: {e}\n\n"
"Por favor, envía las papeletas ganadoras (0-99) separadas por espacios (ej: `7 23 81`).",
f"❌ Participaciones inválidas: {e}\n\n"
"Por favor, envía las participaciones ganadoras (0-99) separadas por espacios (ej: `7 23 81`).",
reply_markup=keyboard,
parse_mode=ParseMode.MARKDOWN
)
@@ -839,7 +865,7 @@ async def admin_receive_winner_numbers(update: Update, context: ContextTypes.DEF
context.user_data.pop('admin_ending_raffle_id', None)
# Call the refactored ending logic
await update.message.reply_text(f"Procesando finalización con papeletas: {', '.join(f'{n:02}' for n in winner_numbers)}...")
await update.message.reply_text(f"Procesando finalización con participaciones: {', '.join(f'{n:02}' for n in winner_numbers)}...")
success = await end_raffle_logic(context, raffle_id_to_end, winner_numbers, user_id)
# Send admin back to the main menu after processing
@@ -852,14 +878,14 @@ async def _announce_raffle_in_channels(context: ContextTypes.DEFAULT_TYPE, raffl
if not raffle:
logger.warning(f"Admin {admin_user_id} tried to announce non-existent raffle {raffle_id}.")
try:
await context.bot.send_message(admin_user_id, f"Error: El ID de la rifa {raffle_id} no existe.")
await context.bot.send_message(admin_user_id, f"Error: El ID del sorteo {raffle_id} no existe.")
except Exception as e:
logger.error(f"Failed to send 'raffle not found' error to admin {admin_user_id}: {e}")
return False
if not initial_announcement and not raffle['active']: # For re-announcements, it must be active
logger.warning(f"Admin {admin_user_id} tried to re-announce inactive raffle {raffle_id} ('{raffle['name']}').")
try:
await context.bot.send_message(admin_user_id, f"Error: La rifa '{raffle['name']}' (ID {raffle_id}) no está activa y no se puede re-anunciar.")
await context.bot.send_message(admin_user_id, f"Error: El sorteo '{raffle['name']}' (ID {raffle_id}) no está activo y no se puede re-anunciar.")
except Exception as e:
logger.error(f"Failed to send 'raffle inactive' error to admin {admin_user_id}: {e}")
return False
@@ -878,11 +904,12 @@ 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"🏆 **¡{'Nueva ' if initial_announcement else ''}Rifa Disponible!** 🏆\n\n"
f"🏆 **¡{'Nuevo ' if initial_announcement else ''}Sorteo Disponible!** 🏆\n\n"
f"🌟 **{raffle_name}** 🌟\n\n"
f"{raffle_description}\n\n"
f"💵 **Precio por papeleta:** {price}\n"
f"🎟️ **Papeletas 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}"
)