967 lines
50 KiB
Python
967 lines
50 KiB
Python
import logging
|
|
import time
|
|
import random
|
|
from telegram import Update
|
|
from telegram.ext import (
|
|
ContextTypes,
|
|
CallbackContext,
|
|
ConversationHandler,
|
|
)
|
|
from telegram.constants import ParseMode
|
|
from telegram.error import Forbidden, BadRequest
|
|
|
|
from database import *
|
|
from config import *
|
|
from helpers import *
|
|
from keyboards import *
|
|
|
|
import pytz
|
|
from datetime import datetime
|
|
from datetime import time as dtime
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
user = update.message.from_user
|
|
args = context.args
|
|
if args and args[0].startswith("join_"):
|
|
try:
|
|
raffle_id = int(args[0].split("_")[1])
|
|
raffle = get_raffle(raffle_id) # Get raffle details
|
|
remaining_numbers_amount = get_remaining_numbers_amount(raffle_id)
|
|
if raffle and raffle['active'] and remaining_numbers_amount > 0:
|
|
# Check what time is it, if it's between 20:55 and 22:00, users can't join
|
|
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("Lo siento, no puedes unirte a sorteos entre las 20:55 y las 22:00 (hora de España) para evitar conflictos con el sorteo en directo. Inténtalo de nuevo más tarde.")
|
|
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 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("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 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 sorteos.")
|
|
return ConversationHandler.END
|
|
|
|
if not CHANNELS:
|
|
await update.message.reply_text("No hay canales configurados. Añade CHANNEL_IDS al .env")
|
|
return ConversationHandler.END
|
|
|
|
context.user_data['new_raffle'] = {'channel': ""} # Initialize data for this user
|
|
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.",
|
|
reply_markup=keyboard,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
return SELECTING_CHANNEL
|
|
|
|
async def select_channel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
|
"""Handles channel selection or finishing selection."""
|
|
query = update.callback_query
|
|
await query.answer()
|
|
|
|
callback_data = query.data
|
|
|
|
if callback_data.startswith(SELECT_CHANNEL_PREFIX):
|
|
channel_id = callback_data[len(SELECT_CHANNEL_PREFIX):]
|
|
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
|
|
)
|
|
return TYPING_TITLE
|
|
|
|
# Should not happen, but good practice
|
|
await context.bot.send_message(chat_id=query.from_user.id, text="Opción inválida.")
|
|
return SELECTING_CHANNEL
|
|
|
|
async def receive_title(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
|
"""Receives the raffle title and asks for description."""
|
|
title = update.message.text.strip()
|
|
if not title:
|
|
await update.message.reply_text("El título no puede estar vacío. Inténtalo de nuevo.")
|
|
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)
|
|
return TYPING_DESCRIPTION
|
|
|
|
async def receive_description(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
|
"""Receives the raffle description and asks for price."""
|
|
description = update.message.text.strip()
|
|
if not description:
|
|
await update.message.reply_text("La descripción no puede estar vacía. Inténtalo de nuevo.")
|
|
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)
|
|
return SENDING_IMAGE
|
|
|
|
async def receive_image(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
|
"""Receives image, then asks for prices for selected channels."""
|
|
if not update.message.photo:
|
|
await update.message.reply_text("Por favor, envía una imagen.")
|
|
return SENDING_IMAGE
|
|
|
|
photo_file_id = update.message.photo[-1].file_id
|
|
context.user_data['new_raffle']['image_file_id'] = photo_file_id
|
|
|
|
await update.message.reply_text(
|
|
"Imagen guardada. Ahora, ¿el sorteo permite envíos internacionales? Responde con 'Sí' 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 ['sí', '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 'Sí' 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
|
|
|
|
async def receive_price_for_channel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
|
"""Receives price for the current channel, stores it, and asks for next or confirms."""
|
|
price_text = update.message.text.strip()
|
|
channel_id = context.user_data['new_raffle']['channel']
|
|
|
|
try:
|
|
price = int(price_text)
|
|
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.")
|
|
return TYPING_PRICE_FOR_CHANNEL # Stay in this state
|
|
|
|
context.user_data['new_raffle']['price'] = price
|
|
logger.info(f"Price for channel {channel_id} set to {price}")
|
|
|
|
await _show_creation_confirmation(update, context)
|
|
return CONFIRMING_CREATION
|
|
|
|
async def _show_creation_confirmation(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""Shows the final confirmation message before creating the raffle."""
|
|
raffle_data = context.user_data['new_raffle']
|
|
channel_id = raffle_data.get('channel', "")
|
|
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"
|
|
"¿Confirmas la creación de este sorteo?"
|
|
)
|
|
keyboard = generate_confirmation_keyboard()
|
|
# Message from which confirmation is triggered is the last price input message.
|
|
# We need to send the photo with this as caption.
|
|
await context.bot.send_photo(
|
|
chat_id=update.message.chat_id, # Send to the admin's chat
|
|
photo=raffle_data['image_file_id'],
|
|
caption=confirmation_text,
|
|
reply_markup=keyboard,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
|
|
async def confirm_creation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
|
query = update.callback_query
|
|
await query.answer()
|
|
user_data = context.user_data.get('new_raffle')
|
|
|
|
if not user_data: # Should not happen
|
|
await query.edit_message_caption("Error: No se encontraron datos. Empieza de nuevo.", reply_markup=None)
|
|
return ConversationHandler.END
|
|
|
|
if query.data == CONFIRM_CREATION_CALLBACK:
|
|
await query.edit_message_caption("Confirmado. Creando y anunciando...", reply_markup=None)
|
|
|
|
name = user_data.get('title')
|
|
description = user_data.get('description')
|
|
price = user_data.get('price')
|
|
image_file_id = user_data.get('image_file_id')
|
|
channel_id = user_data.get('channel')
|
|
|
|
if not all([name, description, image_file_id, price]):
|
|
await context.bot.send_message(query.from_user.id, "Faltan datos. Creación cancelada.")
|
|
context.user_data.pop('new_raffle', None)
|
|
return ConversationHandler.END
|
|
|
|
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"✅ ¡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 el sorteo. Nombre '{name}' podría existir.")
|
|
|
|
elif query.data == CANCEL_CREATION_CALLBACK:
|
|
await query.edit_message_caption("Creación cancelada.", reply_markup=None)
|
|
|
|
context.user_data.pop('new_raffle', None)
|
|
return ConversationHandler.END
|
|
|
|
async def cancel_creation_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
|
"""Handles /cancel command during the conversation."""
|
|
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 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 sorteo en curso para cancelar.")
|
|
return ConversationHandler.END # Or return current state if applicable
|
|
|
|
# ... (other handlers and functions) ...
|
|
|
|
async def incorrect_input_type(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
|
"""
|
|
Handles messages that are not of the expected type in the current conversation state.
|
|
This function tries to determine which conversation (creation or edit) is active.
|
|
"""
|
|
user_id = update.message.from_user.id
|
|
current_conversation_data = None
|
|
current_state_key = None # To store the key like '_new_raffle_conv_state'
|
|
conversation_type = "desconocida" # For logging
|
|
|
|
# Try to determine which conversation is active by checking user_data
|
|
if 'new_raffle' in context.user_data and '_new_raffle_conv_state' in context.user_data: # Assuming PTB stores state like this
|
|
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 sorteo"
|
|
else:
|
|
# Not in a known conversation, or state tracking key is different.
|
|
# This message might be outside any conversation this handler is for.
|
|
# logger.debug(f"User {user_id} sent unexpected message, but not in a tracked conversation state for incorrect_input_type.")
|
|
# For safety, if it's a fallback in a ConversationHandler, returning the current state (if known) or END is best.
|
|
# If this is a global fallback, it shouldn't interfere.
|
|
# If it's a fallback *within* a ConversationHandler, PTB should handle current state.
|
|
# For this specific function, we assume it's called as a fallback in one of the convs.
|
|
active_conv_state = context.user_data.get(ConversationHandler.STATE) # More generic way to get current state of active conv
|
|
if active_conv_state:
|
|
await update.message.reply_text(
|
|
"Entrada no válida para este paso. "
|
|
"Usa /cancelar o /cancelar_edicion si quieres salir del proceso actual."
|
|
)
|
|
return active_conv_state # Return to the current state of the conversation
|
|
else:
|
|
# logger.debug("No active conversation detected by ConversationHandler.STATE for incorrect_input_type.")
|
|
return ConversationHandler.END # Or simply don't reply if it's truly unexpected
|
|
|
|
|
|
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 sorteo":
|
|
if current_state == SENDING_IMAGE:
|
|
await update.message.reply_text(
|
|
"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 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 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 del sorteo en {channel_alias}.\n"
|
|
"Usa /cancelar para salir."
|
|
)
|
|
return TYPING_PRICE_FOR_CHANNEL
|
|
# Add more states if needed (e.g., if SELECTING_CHANNELS expects only callbacks)
|
|
|
|
# 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 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
|
|
|
|
# Handle number selection callback
|
|
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
|
|
|
|
try:
|
|
data = query.data.split(':')
|
|
action = data[0] # "number"
|
|
raffle_id = int(data[1])
|
|
if action != "random_num":
|
|
value = data[2] # Can be number string or "next"/"prev"
|
|
else:
|
|
value = ""
|
|
except (IndexError, ValueError):
|
|
logger.error(f"Invalid callback data in number_callback: {query.data}")
|
|
await query.answer("Error: Acción inválida.")
|
|
return
|
|
|
|
# --- Handle Paging ---
|
|
if value == "next":
|
|
# Determine current page (requires inspecting the current keyboard, which is complex)
|
|
# Easier: Store current page in user_data or derive from button structure if possible.
|
|
# Simplest robust approach: Assume the keyboard generation knows the max pages.
|
|
# Let's try generating the next page's keyboard.
|
|
# We need the *current* page to calculate the *next* page.
|
|
# Hacky way: find the "next" button in the *current* keyboard's markup. If it exists, assume page 0.
|
|
current_page = 0 # Default assumption
|
|
if query.message.reply_markup:
|
|
for row in query.message.reply_markup.inline_keyboard:
|
|
for button in row:
|
|
if button.callback_data == f"number:{raffle_id}:prev":
|
|
current_page = 1 # If prev exists, we must be on page 1
|
|
break
|
|
next_page = current_page + 1
|
|
# Basic check: Assume max 2 pages (0-49, 50-99)
|
|
if next_page <= 1:
|
|
keyboard = generate_numbers_keyboard(raffle_id, user_id, page=next_page)
|
|
await query.edit_message_reply_markup(reply_markup=keyboard)
|
|
await query.answer(f"Mostrando página {next_page + 1}")
|
|
else:
|
|
await query.answer("Ya estás en la última página.")
|
|
return
|
|
|
|
elif value == "prev":
|
|
# Similar logic to find current page.
|
|
current_page = 1 # Default assumption if prev is clicked
|
|
if query.message.reply_markup:
|
|
has_next = False
|
|
for row in query.message.reply_markup.inline_keyboard:
|
|
for button in row:
|
|
if button.callback_data == f"number:{raffle_id}:next":
|
|
has_next = True
|
|
break
|
|
if not has_next: # If no "next" button, we must be on page 1
|
|
current_page = 1
|
|
else: # If "next" exists, we must be on page 0 (edge case, shouldn't happen if prev was clicked)
|
|
current_page = 0 # Should logically be 1 if prev was clicked.
|
|
|
|
prev_page = current_page - 1
|
|
if prev_page >= 0:
|
|
keyboard = generate_numbers_keyboard(raffle_id, user_id, page=prev_page)
|
|
await query.edit_message_reply_markup(reply_markup=keyboard)
|
|
await query.answer(f"Mostrando página {prev_page + 1}")
|
|
else:
|
|
await query.answer("Ya estás en la primera página.")
|
|
return
|
|
|
|
# --- Handle Number Selection/Deselection ---
|
|
if action == "number":
|
|
try:
|
|
number = int(value)
|
|
if not (0 <= number <= 99):
|
|
raise ValueError("Number out of range")
|
|
number_string = f"{number:02}"
|
|
# Determine page number for refresh
|
|
page = 0 if number < 50 else 1
|
|
except ValueError:
|
|
logger.warning(f"Invalid number value in callback: {value}")
|
|
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 participaciones disponibles para seleccionar aleatoriamente.")
|
|
return
|
|
else:
|
|
# Select a random number from the available ones
|
|
number_string = random.choice(remaining_free_numbers)
|
|
if not (0 <= int(number_string) <= 99):
|
|
raise ValueError("Random number out of range")
|
|
page = 0 if int(number_string) < 50 else 1
|
|
except ValueError:
|
|
logger.warning(f"Invalid random number value in callback: {value}")
|
|
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}")
|
|
|
|
# Check the status of the number
|
|
participant_data = get_participant_by_number(raffle_id, number_string) # Check anyone holding this number
|
|
|
|
if participant_data:
|
|
participant_user_id = participant_data['user_id']
|
|
participant_step = participant_data['step']
|
|
participant_db_id = participant_data['id'] # The ID from the participants table
|
|
user_name = participant_data['user_name'] or participant_data['user_id']
|
|
|
|
if participant_user_id == user_id:
|
|
# User clicked a number they already interact with
|
|
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 participación {number_string}.")
|
|
logger.info(f"User {user_id} (Name: {user_name}) 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 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 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 "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"Participación {number_string} reservada para ti. Confirma tu selección cuando termines.")
|
|
logger.info(f"User {user_id} (Name: {username}) 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)
|
|
await query.edit_message_reply_markup(reply_markup=keyboard)
|
|
|
|
|
|
async def confirm_callback(update: Update, context: CallbackContext):
|
|
"""Handles the 'Confirmar Selección' button click."""
|
|
query = update.callback_query
|
|
user_id = query.from_user.id
|
|
|
|
try:
|
|
raffle_id = int(query.data.split(":")[1])
|
|
except (IndexError, ValueError):
|
|
logger.error(f"Invalid callback data received in confirm_callback: {query.data}")
|
|
await query.answer("Error: Acción inválida.")
|
|
return
|
|
|
|
# Get numbers reserved by this user for this raffle
|
|
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 participación nueva para confirmar.")
|
|
return
|
|
|
|
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 del sorteo.", reply_markup=None)
|
|
except BadRequest:
|
|
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)
|
|
# We assume reserve_number created the row if it didn't exist
|
|
participant = get_participant_by_user_id_and_step(user_id, "waiting_for_payment")
|
|
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 participaciones de nuevo.", reply_markup=None)
|
|
except BadRequest:
|
|
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']
|
|
|
|
price_per_number = raffle_info['price']
|
|
if price_per_number is None:
|
|
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
|
|
|
|
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 = "💳 Donar con PayPal 💳"
|
|
paypal_button = InlineKeyboardButton(paypal_button_text, url=paypal_link)
|
|
|
|
# Create the InlineKeyboardMarkup containing the button
|
|
payment_keyboard = InlineKeyboardMarkup([[paypal_button]])
|
|
|
|
# Modify the message text - remove the link placeholder, adjust instruction
|
|
payment_message = (
|
|
f"👍 Selección Confirmada 👍\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 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 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:
|
|
await query.edit_message_text(
|
|
text=payment_message,
|
|
reply_markup=payment_keyboard, # Use the keyboard with the button
|
|
disable_web_page_preview=True # Preview not needed as link is in button
|
|
# No parse_mode needed
|
|
)
|
|
logger.debug(f"Edited message text (fallback) for user {user_id} with PayPal button.")
|
|
except Exception as text_e:
|
|
logger.error(f"Failed to edit message text as fallback for user {user_id}: {text_e}")
|
|
# Send new message with the button
|
|
await context.bot.send_message(user_id, payment_message, reply_markup=payment_keyboard, disable_web_page_preview=True)
|
|
|
|
async def cancel_callback(update: Update, context: CallbackContext):
|
|
"""Handles the 'Cancelar Selección' button click."""
|
|
query = update.callback_query
|
|
user_id = query.from_user.id
|
|
|
|
try:
|
|
raffle_id = int(query.data.split(":")[1])
|
|
except (IndexError, ValueError):
|
|
logger.error(f"Invalid callback data received in cancel_callback: {query.data}")
|
|
await query.answer("Error: Acción inválida.")
|
|
return
|
|
|
|
# Get currently reserved (waiting_for_payment) numbers for this user/raffle
|
|
reserved_numbers = get_reserved_numbers(user_id, raffle_id)
|
|
|
|
if not reserved_numbers:
|
|
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 participaciones reservadas para cancelar.", reply_markup=None)
|
|
except BadRequest: # If message was text, not photo caption
|
|
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. 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 participaciones ha sido cancelada y las participaciones han sido liberadas.", reply_markup=None)
|
|
except BadRequest:
|
|
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:
|
|
logger.error(f"Failed to cancel reservation in DB for user {user_id}, raffle {raffle_id}.")
|
|
await query.answer("Error al cancelar la reserva. Contacta con un administrador.")
|
|
# Don't change the message, let the user know there was an error
|
|
|
|
# --- Helper Function for Ending Raffle (Refactored Logic) ---
|
|
|
|
async def end_raffle_logic(context: ContextTypes.DEFAULT_TYPE, raffle_id: int, winner_numbers: list[int], admin_user_id: int):
|
|
"""Core logic to end a raffle, find winners, and announce."""
|
|
raffle_details = get_raffle(raffle_id) # Gets basic info (name, description, active status)
|
|
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ó 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
|
|
|
|
raffle_name = raffle_details['name']
|
|
|
|
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"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
|
|
|
|
# End the raffle in DB
|
|
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 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
|
|
|
|
logger.info(f"Raffle '{raffle_name}' (ID: {raffle_id}) marked as ended by admin {admin_user_id}.")
|
|
|
|
# Get winners and format announcement
|
|
channel_id_str = raffle_details['channel_id']
|
|
channel_alias = REVERSE_CHANNELS.get(channel_id_str, f"ID:{channel_id_str}")
|
|
|
|
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"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 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 futuros sorteos."
|
|
|
|
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_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)
|
|
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}).")
|
|
except BadRequest as e:
|
|
logger.error(f"Bad request announcing winners in channel {channel_alias} (ID: {channel_id_str}): {e}")
|
|
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 ---
|
|
|
|
async def admin_menu(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""Handles the /menu command for admins in private chat."""
|
|
user = update.message.from_user
|
|
chat = update.message.chat
|
|
|
|
if user.id not in ADMIN_IDS:
|
|
# Ignore silently if not admin
|
|
return
|
|
|
|
if chat.type != 'private':
|
|
try:
|
|
await update.message.reply_text("El comando /menu solo funciona en chat privado conmigo.")
|
|
# Optionally delete the command from the group
|
|
await context.bot.delete_message(chat.id, update.message.message_id)
|
|
except Exception as e:
|
|
logger.warning(f"Could not reply/delete admin /menu command in group {chat.id}: {e}")
|
|
return
|
|
|
|
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)
|
|
|
|
async def admin_menu_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""Handles callbacks from the admin menus."""
|
|
query = update.callback_query
|
|
user_id = query.from_user.id
|
|
|
|
# Ensure the callback is from an admin
|
|
if user_id not in ADMIN_IDS:
|
|
await query.answer("No tienes permiso.", show_alert=True)
|
|
return
|
|
|
|
await query.answer() # Acknowledge callback
|
|
data = query.data
|
|
|
|
if data == ADMIN_MENU_CREATE:
|
|
# Guide admin to use the conversation command
|
|
await query.edit_message_text(
|
|
"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)]])
|
|
)
|
|
|
|
elif data == ADMIN_MENU_LIST:
|
|
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)
|
|
|
|
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)
|
|
|
|
# --- 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.HTML)
|
|
except (ValueError, IndexError):
|
|
logger.error(f"Invalid callback data for view raffle: {data}")
|
|
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 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 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 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 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("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
|
|
context.user_data['admin_ending_raffle_id'] = raffle_id
|
|
context.user_data['expecting_winners_for_raffle'] = raffle_id # Store ID for context check
|
|
logger.info(f"Admin {user_id} prompted to end raffle {raffle_id} ('{raffle['name']}'). Expecting winner numbers.")
|
|
|
|
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`).",
|
|
reply_markup=keyboard,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
elif data == ADMIN_CANCEL_END_PROCESS:
|
|
# Clear the flags
|
|
context.user_data.pop('admin_ending_raffle_id', None)
|
|
context.user_data.pop('expecting_winners_for_raffle', None)
|
|
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)
|
|
|
|
elif data == ADMIN_NO_OP:
|
|
# Just ignore clicks on placeholder buttons like "No hay sorteos activos"
|
|
pass
|
|
|
|
async def admin_receive_winner_numbers(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""Handles the text message containing winner numbers from admin."""
|
|
user_id = update.message.from_user.id
|
|
chat_id = update.message.chat_id
|
|
|
|
# Basic checks: admin, private chat, expecting numbers
|
|
if user_id not in ADMIN_IDS or update.message.chat.type != 'private':
|
|
return # Ignore irrelevant messages
|
|
|
|
expecting_raffle_id = context.user_data.get('expecting_winners_for_raffle')
|
|
ending_raffle_id = context.user_data.get('admin_ending_raffle_id')
|
|
|
|
# Check if we are actually expecting numbers for the specific raffle ID
|
|
if not expecting_raffle_id or expecting_raffle_id != ending_raffle_id or not ending_raffle_id:
|
|
# Not expecting input, or state mismatch. Could be a normal message.
|
|
# logger.debug(f"Received text from admin {user_id} but not expecting winners or mismatch.")
|
|
return
|
|
|
|
logger.info(f"Admin {user_id} submitted winner numbers for raffle {ending_raffle_id}: {update.message.text}")
|
|
|
|
# Parse and validate winner numbers
|
|
try:
|
|
numbers_text = update.message.text.strip()
|
|
if not numbers_text:
|
|
raise ValueError("Input is empty.")
|
|
# Split by space, convert to int, filter range, remove duplicates, sort
|
|
winner_numbers = sorted(list(set(
|
|
int(n) for n in numbers_text.split() if n.isdigit() and 0 <= int(n) <= 99
|
|
)))
|
|
if not winner_numbers:
|
|
raise ValueError("No valid numbers between 0 and 99 found.")
|
|
except ValueError as e:
|
|
logger.warning(f"Invalid winner numbers format from admin {user_id}: {e}")
|
|
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`).",
|
|
reply_markup=keyboard,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
# Keep expecting input
|
|
return
|
|
|
|
# Clear the expectation flags *before* processing
|
|
raffle_id_to_end = ending_raffle_id # Store locally before clearing
|
|
context.user_data.pop('expecting_winners_for_raffle', None)
|
|
context.user_data.pop('admin_ending_raffle_id', None)
|
|
|
|
# Call the refactored ending logic
|
|
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
|
|
keyboard = generate_admin_main_menu_keyboard()
|
|
await context.bot.send_message(chat_id=user_id, text="Volviendo al Menú Principal...", reply_markup=keyboard)
|
|
|
|
async def _announce_raffle_in_channels(context: ContextTypes.DEFAULT_TYPE, raffle_id: int, admin_user_id: int, initial_announcement: bool = False):
|
|
"""Fetches raffle details and sends announcement to its configured channels."""
|
|
raffle = get_raffle(raffle_id)
|
|
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 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: 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
|
|
|
|
raffle_name = raffle['name']
|
|
image_file_id = raffle['image_file_id']
|
|
raffle_description = raffle['description'] # Get description for caption
|
|
price = raffle['price']
|
|
channel_id_str = raffle['channel_id']
|
|
|
|
# Get remaining numbers ONCE before the loop for efficiency
|
|
remaining_count = get_remaining_numbers_amount(raffle_id)
|
|
|
|
logger.info(f"Admin {admin_user_id} initiating {'initial ' if initial_announcement else 're-'}announcement for raffle {raffle_id} ('{raffle_name}')")
|
|
|
|
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"{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"📜 Normas y condiciones: {TYC_DOCUMENT_URL}"
|
|
)
|
|
|
|
message_args = {"parse_mode": ParseMode.MARKDOWN}
|
|
if image_file_id:
|
|
message_args["photo"] = image_file_id
|
|
message_args["caption"] = announce_caption
|
|
message_args["reply_markup"] = generate_channel_participate_keyboard(raffle_id)
|
|
send_method = context.bot.send_photo
|
|
else:
|
|
message_args["text"] = announce_caption
|
|
message_args["reply_markup"] = generate_channel_participate_keyboard(raffle_id)
|
|
send_method = context.bot.send_message
|
|
|
|
sent_message = None # Initialize sent_message variable
|
|
try:
|
|
# --- 1. Send the message ---
|
|
sent_message = await send_method(chat_id=int(channel_id_str), **message_args)
|
|
store_main_message_id(raffle_id, sent_message.message_id)
|
|
logger.info(f"Announcement sent to channel {channel_alias} (ID: {channel_id_str}) for raffle {raffle_id}.")
|
|
|
|
# --- 2. Attempt to pin the sent message ---
|
|
try:
|
|
# Disable notification for re-announcements or if initial is false
|
|
# Enable notification for the very first announcement.
|
|
disable_pin_notification = not initial_announcement
|
|
await context.bot.pin_chat_message(
|
|
chat_id=int(channel_id_str),
|
|
message_id=sent_message.message_id,
|
|
disable_notification=disable_pin_notification
|
|
)
|
|
logger.info(f"Pinned announcement message {sent_message.message_id} in channel {channel_alias}.")
|
|
except Forbidden as pin_e_forbidden:
|
|
logger.warning(f"Could not pin message in channel {channel_alias} (Forbidden): {pin_e_forbidden}")
|
|
except BadRequest as pin_e_bad_request:
|
|
logger.warning(f"Could not pin message in channel {channel_alias} (Bad Request, e.g. no messages to pin): {pin_e_bad_request}")
|
|
except Exception as pin_e:
|
|
logger.warning(f"Could not pin message {sent_message.message_id if sent_message else 'N/A'} in channel {channel_alias}: {pin_e}")
|
|
|
|
except Forbidden:
|
|
logger.error(f"Permission error: Cannot send announcement to channel {channel_alias} (ID: {channel_id_str}).")
|
|
except BadRequest as e:
|
|
logger.error(f"Bad request sending announcement to channel {channel_alias} (ID: {channel_id_str}): {e}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to send announcement to channel {channel_alias} (ID: {channel_id_str}): {e}")
|
|
|
|
try:
|
|
msg_to_admin = "Anuncio enviado con éxito."
|
|
await context.bot.send_message(admin_user_id, msg_to_admin, parse_mode=ParseMode.MARKDOWN)
|
|
except Exception as e:
|
|
logger.error(f"Failed to send announcement summary to admin {admin_user_id}: {e}")
|
|
|
|
return True
|