First commit
This commit is contained in:
200
app/app.py
Normal file
200
app/app.py
Normal file
@@ -0,0 +1,200 @@
|
||||
# app.py
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
from datetime import time as dtime
|
||||
import pytz
|
||||
from bs4 import BeautifulSoup
|
||||
from telegram import Update
|
||||
from telegram.ext import (
|
||||
Application,
|
||||
CommandHandler,
|
||||
CallbackQueryHandler,
|
||||
MessageHandler,
|
||||
ConversationHandler,
|
||||
filters,
|
||||
ContextTypes,
|
||||
)
|
||||
from telegram.error import Forbidden, BadRequest
|
||||
# REMOVE: from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
|
||||
# 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
|
||||
)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
# REMOVE: logging.getLogger("apscheduler").setLevel(logging.WARNING) # No longer needed
|
||||
|
||||
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"Las papeletas `{numbers}` que tenías reservadas para la rifa **{raffle_name}** han sido liberadas.\n\n"
|
||||
f"Puedes volver a reservarlas, ¡pero tienes {RESERVATION_TIMEOUT_MINUTES} minutos para completar el pago!."
|
||||
)
|
||||
try:
|
||||
await context.bot.send_message(chat_id=user_id, text=notification_text, parse_mode=ParseMode.MARKDOWN)
|
||||
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.")
|
||||
|
||||
async def check_winner_numbers(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.
|
||||
logger.info(f"Running check_winner_numbers)")
|
||||
|
||||
# will check winner number comparing to ONCE's website depending on the day
|
||||
# and only if a raffle is already with all numbers sold
|
||||
|
||||
raffles = get_active_raffles()
|
||||
|
||||
for raffle in raffles:
|
||||
if get_remaining_numbers_amount(raffle['id']) == 0:
|
||||
weekday = datetime.today().weekday()
|
||||
if weekday <= 3:
|
||||
draw_name = DRAW_MAPPING['weekday']
|
||||
elif weekday == 4:
|
||||
draw_name = DRAW_MAPPING['friday']
|
||||
else:
|
||||
draw_name = DRAW_MAPPING['weekend']
|
||||
|
||||
response = requests.get(JUEGOS_ONCE_URL, timeout=10)
|
||||
response.raise_for_status()
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
|
||||
blocks = soup.find_all("div", class_=draw_name)
|
||||
for block in blocks:
|
||||
num = block.find(class_="num")
|
||||
winner_num = num.get_text(strip=True) if num else None
|
||||
|
||||
if winner_num:
|
||||
logger.info(f"Winner number for {raffle['name']} is {winner_num}")
|
||||
await end_raffle_logic(context, raffle['id'], [int(winner_num)%100], ADMIN_IDS[0])
|
||||
|
||||
# --- 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,
|
||||
name="expire_check_job"
|
||||
)
|
||||
logger.info(f"Scheduled reservation check job to run every {job_interval_seconds} seconds.")
|
||||
|
||||
madrid_tz = pytz.timezone("Europe/Madrid")
|
||||
job_queue.run_daily(
|
||||
callback=check_winner_numbers,
|
||||
time=dtime(hour=21, minute=45, tzinfo=madrid_tz),
|
||||
name="winner_check_job"
|
||||
)
|
||||
logger.info("Scheduled winner check job every day at 21:45 Madrid time.")
|
||||
|
||||
# --- Handlers (Remain the same) ---
|
||||
# 1. Raffle Creation Conversation Handler
|
||||
raffle_creation_conv = ConversationHandler(
|
||||
entry_points=[CommandHandler("crear_rifa", new_raffle_start)],
|
||||
states={
|
||||
SELECTING_CHANNEL: [CallbackQueryHandler(select_channel, 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_CHANNEL: [
|
||||
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)
|
||||
|
||||
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})$"
|
||||
)
|
||||
|
||||
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))
|
||||
|
||||
# 5. User Callbacks
|
||||
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()
|
||||
Reference in New Issue
Block a user