213 lines
9.2 KiB
Python
213 lines
9.2 KiB
Python
# app.py
|
|
import logging
|
|
import time
|
|
# REMOVE: from datetime import timedelta # Not needed directly for run_repeating interval
|
|
from telegram import Update
|
|
from telegram.ext import (
|
|
Application,
|
|
CommandHandler,
|
|
CallbackQueryHandler,
|
|
MessageHandler,
|
|
ConversationHandler,
|
|
filters,
|
|
ContextTypes,
|
|
)
|
|
from telegram.error import Forbidden, BadRequest
|
|
from newrelic_telemetry_sdk import Log, LogClient
|
|
|
|
# Import handlers and db/config
|
|
from handlers import *
|
|
from database import init_db, get_expired_reservations, cancel_reservation_by_id
|
|
from config import *
|
|
|
|
# Logging setup
|
|
logging.basicConfig(
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
|
|
)
|
|
|
|
# Suppress overly verbose logs from libraries
|
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
logging.getLogger("apscheduler").setLevel(logging.WARNING)
|
|
|
|
client = LogClient(license_key=NEWRELIC_API_KEY, host="log-api.eu.newrelic.com")
|
|
|
|
class NewRelicHandler(logging.Handler):
|
|
def emit(self, record):
|
|
try:
|
|
log = Log(
|
|
message=self.format(record),
|
|
level=record.levelname,
|
|
timestamp_ms=int(record.created * 1000),
|
|
attributes={
|
|
"logger": record.name,
|
|
"app_name": BOT_NAME,
|
|
"docker_container": "telerifas"
|
|
}
|
|
)
|
|
client.send(log)
|
|
except Exception:
|
|
self.handleError(record)
|
|
|
|
nr_handler = NewRelicHandler()
|
|
nr_handler.setFormatter(logging.Formatter("%(message)s"))
|
|
|
|
root_logger = logging.getLogger()
|
|
root_logger.addHandler(nr_handler)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Scheduler Job (Callback for PTB Job Queue)
|
|
async def check_expired_reservations(context: ContextTypes.DEFAULT_TYPE):
|
|
"""Job callback to cancel expired reservations and notify users."""
|
|
# context is automatically provided by PTB's Job Queue
|
|
# No need to get app from context explicitly here for bot access,
|
|
# context.bot can be used directly.
|
|
now = time.time()
|
|
timeout_seconds = RESERVATION_TIMEOUT_MINUTES * 60
|
|
expiry_threshold = now - timeout_seconds
|
|
logger.info(f"Running check_expired_reservations (Threshold: {expiry_threshold})")
|
|
|
|
expired_list = get_expired_reservations(expiry_threshold) # DB function remains the same
|
|
|
|
if not expired_list:
|
|
logger.info("No expired reservations found.")
|
|
return
|
|
|
|
logger.info(f"Found {len(expired_list)} expired reservations to cancel.")
|
|
cancelled_count = 0
|
|
for reservation in expired_list:
|
|
participant_id = reservation['id']
|
|
user_id = reservation['user_id']
|
|
raffle_id = reservation['raffle_id']
|
|
raffle_name = reservation['raffle_name']
|
|
numbers = reservation['numbers']
|
|
|
|
logger.warning(f"Expiring reservation for User ID: {user_id}, Raffle: {raffle_name} ({raffle_id}), Numbers: {numbers}")
|
|
|
|
# Cancel the reservation in the database first
|
|
if cancel_reservation_by_id(participant_id):
|
|
cancelled_count += 1
|
|
# Try to notify the user using context.bot
|
|
notification_text = (
|
|
f"⚠️ Tu reserva para el sorteo **{raffle_name}** ha expirado.\n\n"
|
|
f"Los números `{numbers}` han sido liberados porque el pago no se completó a tiempo.\n\n"
|
|
f"Puedes volver a intentarlo usando /sorteo en el grupo correspondiente."
|
|
)
|
|
try:
|
|
await context.bot.send_message(chat_id=user_id, text=notification_text, parse_mode=ParseMode.MARKDOWN)
|
|
logger.info(f"Notified user {user_id} about expired reservation.")
|
|
except Forbidden:
|
|
logger.warning(f"Cannot notify user {user_id} (Forbidden). Reservation cancelled anyway.")
|
|
except BadRequest as e:
|
|
logger.error(f"BadRequest notifying user {user_id}: {e}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to notify user {user_id} about expiry: {e}")
|
|
else:
|
|
logger.error(f"Failed to cancel reservation ID {participant_id} in DB (might have been processed already).")
|
|
|
|
logger.info(f"Finished check_expired_reservations. Cancelled {cancelled_count} reservations.")
|
|
|
|
|
|
# --- Main Function ---
|
|
def main():
|
|
init_db()
|
|
|
|
app = Application.builder().token(BOT_TOKEN).build()
|
|
logger.info("Bot application built.")
|
|
|
|
# --- Use PTB's Job Queue ---
|
|
job_queue = app.job_queue
|
|
job_interval_seconds = 5 * 60 # Check every 5 minutes
|
|
# Add the repeating job
|
|
job_queue.run_repeating(
|
|
callback=check_expired_reservations,
|
|
interval=job_interval_seconds,
|
|
first=10, # Start after 10 seconds (optional)
|
|
name="expire_check_job" # Optional name for the job
|
|
)
|
|
logger.info(f"Scheduled reservation check job to run every {job_interval_seconds} seconds.")
|
|
|
|
# --- Handlers (Remain the same) ---
|
|
# 1. Raffle Creation Conversation Handler
|
|
raffle_creation_conv = ConversationHandler(
|
|
entry_points=[CommandHandler("crear_sorteo", new_raffle_start)],
|
|
states={
|
|
SELECTING_CHANNELS: [CallbackQueryHandler(select_channels, pattern=f"^{SELECT_CHANNEL_PREFIX}.*")],
|
|
TYPING_TITLE: [MessageHandler(filters.TEXT & ~filters.COMMAND, receive_title)],
|
|
TYPING_DESCRIPTION: [MessageHandler(filters.TEXT & ~filters.COMMAND, receive_description)],
|
|
SENDING_IMAGE: [
|
|
MessageHandler(filters.PHOTO, receive_image),
|
|
MessageHandler(filters.TEXT & ~filters.COMMAND, incorrect_input_type)
|
|
],
|
|
TYPING_PRICE_FOR_CHANNELS: [
|
|
MessageHandler(filters.TEXT & ~filters.COMMAND, receive_price_for_channel)
|
|
],
|
|
CONFIRMING_CREATION: [
|
|
CallbackQueryHandler(confirm_creation, pattern=f"^{CONFIRM_CREATION_CALLBACK}$"),
|
|
CallbackQueryHandler(confirm_creation, pattern=f"^{CANCEL_CREATION_CALLBACK}$"),
|
|
],
|
|
},
|
|
fallbacks=[
|
|
CommandHandler("cancelar", cancel_creation_command),
|
|
MessageHandler(filters.TEXT & ~filters.COMMAND, incorrect_input_type),
|
|
],
|
|
)
|
|
app.add_handler(raffle_creation_conv)
|
|
|
|
# --- Raffle Edit Conversation Handler ---
|
|
raffle_edit_conv = ConversationHandler(
|
|
entry_points=[CallbackQueryHandler(admin_edit_select_raffle, pattern=f"^{ADMIN_EDIT_RAFFLE_SELECT}$")], # Triggered by main menu
|
|
states={
|
|
EDIT_SELECT_RAFFLE: [ # State after clicking "Edit Raffle"
|
|
CallbackQueryHandler(admin_edit_selected_raffle, pattern=f"^{ADMIN_EDIT_RAFFLE_PREFIX}\d+$")
|
|
],
|
|
EDIT_SELECT_NEW_CHANNELS: [
|
|
CallbackQueryHandler(admin_edit_select_new_channels, pattern=f"^{SELECT_CHANNEL_PREFIX}.*")
|
|
],
|
|
EDIT_TYPING_PRICE_FOR_NEW_CHANNELS: [
|
|
MessageHandler(filters.TEXT & ~filters.COMMAND, admin_edit_receive_price_for_new_channel)
|
|
],
|
|
EDIT_CONFIRM: [
|
|
CallbackQueryHandler(admin_confirm_edit_action, pattern="^(confirm_edit_action|cancel_edit_action)$")
|
|
],
|
|
},
|
|
fallbacks=[CommandHandler("cancelar_edicion", cancel_edit_command), # Specific cancel for this flow
|
|
CommandHandler("cancelar", cancel_edit_command)], # General cancel
|
|
map_to_parent={ # Example: If edit is cancelled, go back to main admin menu flow if possible
|
|
# ConversationHandler.END: ADMIN_MENU_LIST # Or handle return state differently
|
|
}
|
|
)
|
|
app.add_handler(raffle_edit_conv)
|
|
|
|
app.add_handler(CommandHandler("start", start, filters=filters.ChatType.PRIVATE))
|
|
# 2. Admin Menu Handlers
|
|
app.add_handler(CommandHandler("menu", admin_menu, filters=filters.ChatType.PRIVATE))
|
|
# Refined pattern to avoid clash with user number selection if prefixes overlap heavily
|
|
admin_pattern = (
|
|
f"^({ADMIN_MENU_CREATE}|{ADMIN_MENU_LIST}|{ADMIN_MENU_BACK_MAIN}|"
|
|
f"{ADMIN_VIEW_RAFFLE_PREFIX}\d+|{ADMIN_ANNOUNCE_RAFFLE_PREFIX}\d+|"
|
|
f"{ADMIN_END_RAFFLE_PROMPT_PREFIX}\d+|{ADMIN_CANCEL_END_PROCESS}|{ADMIN_NO_OP}|"
|
|
f"{ADMIN_EDIT_RAFFLE_SELECT})$"
|
|
)
|
|
|
|
app.add_handler(CallbackQueryHandler(admin_menu_callback, pattern=admin_pattern))
|
|
app.add_handler(MessageHandler(filters.TEXT & filters.ChatType.PRIVATE & ~filters.COMMAND, admin_receive_winner_numbers))
|
|
|
|
# 4. User command
|
|
app.add_handler(CommandHandler("sorteo", enter_raffle)) # Add filters=filters.ChatType.GROUPS if desired
|
|
|
|
# 5. User Callbacks
|
|
app.add_handler(CallbackQueryHandler(raffle_selected, pattern=r"^join:\d+$"))
|
|
app.add_handler(CallbackQueryHandler(number_callback, pattern=r"^(number:\d+:(\d+|prev|next)|random_num:\d+)$"))
|
|
#app.add_handler(CallbackQueryHandler(number_callback, pattern=r"^number:\d+:(?:\d{1,2}|prev|next)$"))
|
|
app.add_handler(CallbackQueryHandler(confirm_callback, pattern=r"^confirm:\d+$"))
|
|
app.add_handler(CallbackQueryHandler(cancel_callback, pattern=r"^cancel:\d+$"))
|
|
|
|
# --- Start Bot ---
|
|
# REMOVE: scheduler.start() # No longer needed
|
|
logger.info("Starting bot polling...")
|
|
app.run_polling(allowed_updates=Update.ALL_TYPES)
|
|
logger.info("Bot stopped.")
|
|
|
|
if __name__ == "__main__":
|
|
main() |