import datetime import logging import requests from database import * # Import all DB functions from config import * # Import constants if needed (like BOT_TOKEN for direct API calls, although better passed) from PIL import Image, ImageDraw, ImageFont import os from requests.auth import HTTPBasicAuth logger = logging.getLogger(__name__) def format_raffle_details(raffle_id): """Fetches and formats raffle details for display, including multi-channel prices.""" raffle = get_raffle(raffle_id) # Fetches basic info from 'raffles' table if not raffle: return "Error: No se encontró el sorteo." details = ( f"ℹ️ Detalles del sorteo ℹ️\n\n" f"ID: {raffle['id']}\n" f"Nombre: {raffle['name']}\n" f"Descripción:\n{raffle['description'][:100]}...\n\n" f"Envío internacional: {'Sí' if raffle['international_shipping'] else 'No'}\n" f"Activo: {'Sí' if raffle['active'] else 'No (Terminado)'}\n" f"Donación mínima (canal principal): {raffle['price']}€\n" ) # Image ID (optional display) if raffle['image_file_id']: details += f"ID Imagen: {raffle['image_file_id']} (Presente)\n" else: details += f"ID Imagen: (No asignada)\n" # Add participant count and remaining numbers participants = get_participants(raffle_id) # Fetches list of Rows completed_participants_count = 0 # pending_participants_count = 0 # If you want to show pending if participants: # Check if participants list is not None or empty completed_participants_count = sum(1 for p in participants if p['step'] == 'completed') # pending_participants_count = sum(1 for p in participants if p['step'] == 'waiting_for_payment') details += f"\nParticipantes Confirmados: {completed_participants_count}\n" # details += f"Reservas Pendientes: {pending_participants_count}\n" remaining_count = get_remaining_numbers_amount(raffle_id) details += f"Números Disponibles: {remaining_count if remaining_count >= 0 else 'Error al calcular'}\n" # Gross and net amounts total_gross = 0.0 total_fees = 0.0 total_net = 0.0 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_fees += fees total_net += net details += f"\nTotal Recaudado (bruto): {total_gross:.2f}€\n" details += f"Total Gastos (comisiones): {total_fees:.2f}€\n" details += f"Total Beneficio (neto): {total_net:.2f}€\n" return details def get_winners(raffle_id, winner_numbers_int): """Finds winners based on chosen numbers.""" participants = get_participants(raffle_id) # Gets all participants for the raffle winners = {} # { user_name: [list_of_winning_numbers_they_had] } if not participants: return "" # No participants, no winners winner_numbers_set = set(winner_numbers_int) for participant in participants: # Only consider completed participations as potential winners if participant['step'] != 'completed' or not participant['numbers']: continue user_id = participant['user_id'] user_name = participant['user_name'] or f"User_{user_id}" # Fallback name numbers_str = participant['numbers'] try: participant_numbers_set = {int(n) for n in numbers_str.split(',') if n.isdigit()} except ValueError: logger.warning(f"Invalid number format for participant {user_id} in raffle {raffle_id}: {numbers_str}") continue # Skip participant with bad data # Find the intersection won_numbers = winner_numbers_set.intersection(participant_numbers_set) if won_numbers: # Store the winning numbers (as strings, sorted) for this user won_numbers_str_sorted = sorted([f"{n:02}" for n in won_numbers]) if user_name not in winners: winners[user_name] = [] winners[user_name].extend(won_numbers_str_sorted) # Add potentially multiple matches if not winners: return "No hubo ganadores con esos números." # Format the output string winners_message_parts = [] for user_name, numbers in winners.items(): # Ensure numbers are unique in the final output per user unique_numbers_str = ", ".join(sorted(list(set(numbers)))) winners_message_parts.append(f"- @{user_name} acertó: {unique_numbers_str}") return "\n".join(winners_message_parts) # def generate_table_image(raffle_id): # """Generates the 10x10 grid image showing number status.""" # # Define image parameters # cols, rows = 10, 10 # cell_width, cell_height = 120, 50 # title_height_space = 70 # image_width = cols * cell_width # image_height = rows * cell_height + title_height_space # background_color = "white" # line_color = "black" # font_size = 16 # title_font_size = 24 # # Create image # img = Image.new("RGB", (image_width, image_height), background_color) # draw = ImageDraw.Draw(img) # # Load fonts (handle potential errors) # try: # # Ensure the font file exists or provide a fallback path # font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" # Example Linux path # if not os.path.exists(font_path): # font_path = "arial.ttf" # Try common Windows font # font = ImageFont.truetype(font_path, font_size) # title_font = ImageFont.truetype(font_path, title_font_size) # except IOError: # logger.warning("Specific font not found, using default PIL font.") # font = ImageFont.load_default() # # Adjust size for default font if needed, default doesn't take size arg directly # # title_font = ImageFont.truetype(font_path, title_font_size) # Need a default large font method # title_font = ImageFont.load_default() # Revert to default for title too for simplicity # # Draw Title # raffle_details = get_raffle(raffle_id) # if not raffle_details: # logger.error(f"Cannot generate image: Raffle {raffle_id} not found.") # # Draw error message on image # draw.text((10, 10), f"Error: Sorteo {raffle_id} no encontrado", fill="red", font=title_font) # img.save(f"/app/data/raffles/raffle_table_{raffle_id}_error.png") # return False # Indicate failure # raffle_name = raffle_details['name'] # title_text = f"Sorteo: {raffle_name}" # # Calculate text bounding box for centering # try: # # Use textbbox for more accurate centering # title_bbox = draw.textbbox((0, 0), title_text, font=title_font) # title_width = title_bbox[2] - title_bbox[0] # # title_height = title_bbox[3] - title_bbox[1] # Not needed for x centering # title_x = (image_width - title_width) / 2 # title_y = 10 # Padding from top # draw.text((title_x, title_y), title_text, fill=line_color, font=title_font) # except AttributeError: # Handle older PIL versions that might not have textbbox # # Fallback using textlength (less accurate) # title_width = draw.textlength(title_text, font=title_font) # title_x = (image_width - title_width) / 2 # title_y = 10 # draw.text((title_x, title_y), title_text, fill=line_color, font=title_font) # # Get participant data # participants = get_participants(raffle_id) # number_status = {} # { num_int: (user_name, status_color) } # if participants: # for p in participants: # if not p['numbers'] or p['step'] not in ['waiting_for_payment', 'completed']: # continue # user_name = p['user_name'] or f"User_{p['user_id']}" # Fallback name # status_color = "red" if p['step'] == 'waiting_for_payment' else "black" # Red=Reserved, Black=Completed # try: # nums = {int(n) for n in p['numbers'].split(',') if n.isdigit()} # for num in nums: # if 0 <= num <= 99: # number_status[num] = (user_name, status_color) # except ValueError: # logger.warning(f"Skipping invalid numbers '{p['numbers']}' for user {p['user_id']} in image generation.") # continue # # Draw Grid and Fill Numbers # for i in range(rows): # for j in range(cols): # num = i * cols + j # x1 = j * cell_width # y1 = i * cell_height + title_height_space # x2 = x1 + cell_width # y2 = y1 + cell_height # # Draw cell rectangle # draw.rectangle([x1, y1, x2, y2], outline=line_color) # # Prepare text and color # number_text = f"{num:02}" # text_fill = "blue" # Default color for free numbers # owner_text = "" # if num in number_status: # owner, status_color = number_status[num] # text_fill = status_color # # Truncate long usernames # max_name_len = 12 # owner_text = owner[:max_name_len] + ('…' if len(owner) > max_name_len else '') # # Position text within the cell # text_x = x1 + 10 # Padding from left # text_y_num = y1 + 5 # Padding for number line # text_y_owner = y1 + 5 + font_size + 2 # Padding for owner line (below number) # draw.text((text_x, text_y_num), number_text, fill=text_fill, font=font) # if owner_text: # draw.text((text_x, text_y_owner), owner_text, fill=text_fill, font=font) # # Ensure data directory exists # os.makedirs("/app/data/raffles", exist_ok=True) # # Save the image # image_path = f"/app/data/raffles/raffle_table_{raffle_id}.png" # try: # img.save(image_path) # logger.info(f"Generated raffle table image: {image_path}") # return True # Indicate success # except Exception as e: # logger.error(f"Failed to save raffle table image {image_path}: {e}") # return False # Indicate failure def generate_table_image(raffle_id): """Generates a fancier 10x10 raffle grid image with participant names and legend.""" # Parameters cols, rows = 10, 10 cell_width, cell_height = 120, 60 title_height_space = 90 title_bottom_padding = 30 # extra space between title and grid legend_height_space = 80 margin_x = 40 # left/right margin image_width = cols * cell_width + margin_x * 2 image_height = rows * cell_height + title_height_space + title_bottom_padding + legend_height_space background_color = "#fdfdfd" grid_line_color = "#666666" free_bg_color = "#e8f0ff" reserved_bg_color = "#ffe8e8" taken_bg_color = "#e8ffe8" font_size = 16 title_font_size = 28 legend_font_size = 18 # Create base image img = Image.new("RGB", (image_width, image_height), background_color) draw = ImageDraw.Draw(img) # Load fonts try: font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" if not os.path.exists(font_path): font_path = "arial.ttf" font = ImageFont.truetype(font_path, font_size) title_font = ImageFont.truetype(font_path, title_font_size) legend_font = ImageFont.truetype(font_path, legend_font_size) except IOError: font = ImageFont.load_default() title_font = ImageFont.load_default() legend_font = ImageFont.load_default() # --- Title Bar --- raffle_details = get_raffle(raffle_id) if not raffle_details: draw.text((10, 10), f"Error: Sorteo {raffle_id} no encontrado", fill="red", font=title_font) img.save(f"/app/data/raffles/raffle_table_{raffle_id}_error.png") return False raffle_name = raffle_details['name'] title_text = f"Sorteo: {raffle_name}" # Draw title bar (full width) title_bar_color = "#4a90e2" draw.rectangle([0, 0, image_width, title_height_space], fill=title_bar_color) # Centered title text title_bbox = draw.textbbox((0, 0), title_text, font=title_font) title_width = title_bbox[2] - title_bbox[0] title_height = title_bbox[3] - title_bbox[1] title_x = (image_width - title_width) / 2 title_y = (title_height_space - title_height) / 2 draw.text((title_x, title_y), title_text, fill="white", font=title_font) # --- Participants --- participants = get_participants(raffle_id) number_status = {} if participants: for p in participants: if not p['numbers'] or p['step'] not in ['waiting_for_payment', 'completed']: continue user_name = p['user_name'] or f"User_{p['user_id']}" status = p['step'] nums = {int(n) for n in p['numbers'].split(',') if n.isdigit()} for num in nums: if 0 <= num <= 99: number_status[num] = (user_name, status) # --- Grid --- grid_top = title_height_space + title_bottom_padding for i in range(rows): for j in range(cols): num = i * cols + j x1 = margin_x + j * cell_width y1 = grid_top + i * cell_height x2 = x1 + cell_width y2 = y1 + cell_height # Background color if num in number_status: owner, status = number_status[num] bg_color = reserved_bg_color if status == "waiting_for_payment" else taken_bg_color text_color = "#000000" else: owner, bg_color, text_color = "", free_bg_color, "#1a4db3" # Rounded rectangle cell radius = 12 draw.rounded_rectangle([x1+1, y1+1, x2-1, y2-1], radius, outline=grid_line_color, fill=bg_color) # Draw number number_text = f"{num:02}" draw.text((x1+10, y1+8), number_text, fill=text_color, font=font) # Draw owner if owner: max_name_len = 12 owner_text = owner[:max_name_len] + ('…' if len(owner) > max_name_len else '') draw.text((x1+10, y1+8+font_size+4), owner_text, fill=text_color, font=font) # --- Legend --- legend_y = image_height - legend_height_space + 20 legend_items = [ ("Libre", free_bg_color), ("Reservado", reserved_bg_color), ("Asignado", taken_bg_color) ] spacing = 280 # more spacing between legend items start_x = (image_width - (spacing * (len(legend_items)-1) + 140)) / 2 for i, (label, color) in enumerate(legend_items): box_x = start_x + i * spacing box_y = legend_y box_w, box_h = 34, 34 # Color box draw.rounded_rectangle([box_x, box_y, box_x+box_w, box_y+box_h], 6, fill=color, outline=grid_line_color) # Label text draw.text((box_x + box_w + 14, box_y + 6), label, fill="black", font=legend_font) # Save os.makedirs("/app/data/raffles", exist_ok=True) image_path = f"/app/data/raffles/raffle_table_{raffle_id}.png" img.save(image_path) return True def format_last_participants_list(participants_list: list) -> str: """ Formats the list of last participants for the announcement message. participants_list is a list of dicts: [{'user_name', 'numbers'}] """ if not participants_list: return "" # Return empty string if no other recent participants # Reverse the list so the oldest of the "last N" appears first in the formatted string # as per the example "nick1, nick2, nick3" implies chronological order of joining. # The DB query already returns newest first, so we reverse it for display. formatted_lines = ["Los últimos participantes en unirse (además del más reciente) han sido:"] for p_info in reversed(participants_list): # Display oldest of this batch first user_name = p_info.get('user_name', 'Usuario Anónimo') numbers_str = p_info.get('numbers', '') if numbers_str: num_list = numbers_str.split(',') if len(num_list) == 1: line = f" - {user_name}, con la participación: {num_list[0]}" else: line = f" - {user_name}, con las participaciones: {', '.join(num_list)}" formatted_lines.append(line) return "\n".join(formatted_lines) # Add a trailing newline def get_paypal_access_token(): old_token = get_paypal_access_token_db() if old_token: logger.info(f"Using cached PayPal access token") return old_token logger.info("Fetching new PayPal access token") url = f"{PAYPAL_URL}/v1/oauth2/token" headers = {"Accept": "application/json", "Accept-Language": "en_US"} data = {"grant_type": "client_credentials"} response = requests.post(url, headers=headers, data=data, auth=HTTPBasicAuth(PAYPAL_CLIENT_ID, PAYPAL_SECRET)) response.raise_for_status() store_paypal_access_token(response.json()["access_token"], response.json()["expires_in"]) return response.json()["access_token"] def create_paypal_order(access_token, value, raffle_id, numbers, user_name): url = f"{PAYPAL_URL}/v2/checkout/orders" headers = { "Content-Type": "application/json", "Authorization": f"Bearer {access_token}" } payload = { "intent": "CAPTURE", "purchase_units": [ { "amount": {"currency_code": "EUR", "value": f"{value:.2f}"}, "description": f"Donación para participar en el sorteo de HomeLabs Club (ID: {raffle_id}, Números: {numbers}, Usuario: {user_name})", } ], "application_context": { "locale": "es-ES", "return_url": f"https://t.me/{BOT_NAME}", "cancel_url": f"https://t.me/{BOT_NAME}" } } response = requests.post(url, headers=headers, json=payload) response.raise_for_status() order = response.json() # Extract the approval link approval_url = next(link["href"] for link in order["links"] if link["rel"] == "approve") return approval_url, order["id"] def get_paypal_amounts_for_invoice(invoice_id): """Fetches the gross, net, and fee amounts for a given PayPal invoice ID.""" access_token = get_paypal_access_token() url = f"{PAYPAL_URL}/v2/checkout/orders/{invoice_id}" headers = { "Content-Type": "application/json", "Authorization": f"Bearer {access_token}" } try: response = requests.get(url, headers=headers) response.raise_for_status() except requests.RequestException as e: logger.error(f"Error fetching PayPal invoice {invoice_id}: {e}") return 0.0, 0.0, 0.0 order = response.json() if order["status"] != "COMPLETED": logger.warning(f"Invoice {invoice_id} is not completed. Status: {order['status']}") return 0.0, 0.0, 0.0 gross_amount = float(order["purchase_units"][0]["amount"]["value"]) fee_amount = 0.0 net_amount = gross_amount # Fetch capture details to get fee information capture_id = order["purchase_units"][0]["payments"]["captures"][0]["id"] capture_url = f"{PAYPAL_URL}/v2/payments/captures/{capture_id}" capture_response = requests.get(capture_url, headers=headers) capture_response.raise_for_status() capture_details = capture_response.json() if "seller_receivable_breakdown" in capture_details: breakdown = capture_details["seller_receivable_breakdown"] fee_amount = float(breakdown.get("paypal_fee", {}).get("value", 0.0)) net_amount = float(breakdown.get("net_amount", {}).get("value", gross_amount)) return gross_amount, net_amount, fee_amount def is_vip_member_of_homelabs(user_id): """Checks if a Telegram user ID is a VIP member of HomeLabs via the Homelabs API.""" if not HOMELABS_API_TOKEN or not HOMELABS_API_URL: logger.warning("Homelabs API configuration is missing.") return False # If not configured, treat as non-member url = f"{HOMELABS_API_URL}/users/{user_id}" headers = { "X-API-Key": HOMELABS_API_TOKEN, "Accept": "application/json", } try: response = requests.get(url, headers=headers, timeout=5) if response.status_code == 200: data = response.json() if data.get("is_lifetime") == True: return True if data.get("member_until") >= datetime.datetime.now(datetime.timezone.utc).date().isoformat(): return True return False elif response.status_code == 404: return False # User not found, hence not a member else: logger.error(f"Unexpected response from Homelabs API: {response.status_code} - {response.text}") return False except requests.RequestException as e: logger.error(f"Error connecting to Homelabs API: {e}") return False def send_raffle_update_image(raffle_id, current_user_name=None, numbers=None, bot_token=None): """ Sends or updates the raffle table image to the appropriate channel. Can be used for payment confirmations or manual updates from admin. Args: raffle_id: The ID of the raffle current_user_name: Name of the user who just joined (optional, for payment updates) numbers: List of numbers the user selected (optional, for payment updates) bot_token: Telegram bot token for API calls Returns: bool: True if successful, False otherwise """ try: # Import here to avoid circular imports import json if not bot_token: bot_token = BOT_TOKEN TELEGRAM_API_URL = f"https://api.telegram.org/bot{bot_token}" # Generate table image if not generate_table_image(raffle_id): logger.error(f"Failed to generate raffle table image for {raffle_id}") return False image_path = f"/app/data/raffles/raffle_table_{raffle_id}.png" # Get raffle details raffle_details = get_raffle(raffle_id) if not raffle_details: logger.error(f"Could not fetch raffle details for ID {raffle_id}") return False raffle_name = raffle_details['name'] raffle_description_for_announce = raffle_details['description'][:350] channel_id_to_announce = raffle_details['channel_id'] original_price_per_number = raffle_details['price'] remaining_numbers_amount = get_remaining_numbers_amount(raffle_id) # Determine keyboard and caption based on remaining numbers keyboard = None if remaining_numbers_amount == 0: # Raffle is complete main_announcement = f"🎯🏆🎯 Sorteo '{raffle_name}' terminado 🎯🏆🎯\n\n" main_announcement += f"{raffle_description_for_announce}\n\n" main_announcement += f"🌍 Envío internacional: {'Sí ✅' if raffle_details['international_shipping'] else 'No ❌'}\n" main_announcement += f"💵 Donación mínima: {original_price_per_number}€\n" main_announcement += f"📜 Normas y condiciones: {TYC_DOCUMENT_URL}" # Update main message to remove participate button main_message_id = get_main_message_id(raffle_id) if main_message_id: requests.post(f"{TELEGRAM_API_URL}/editMessageCaption", json={ "chat_id": channel_id_to_announce, "message_id": main_message_id, "caption": main_announcement, "reply_markup": None, "parse_mode": "HTML" }) else: # Raffle still in progress url = f"https://t.me/{BOT_NAME}?start=join_{raffle_id}" keyboard = { "inline_keyboard": [ [ {"text": "✅ ¡Participar Ahora! ✅", "url": url} ] ] } main_announcement = f"🏆 Sorteo '{raffle_name}' en progreso 🏆\n\n" main_announcement += f"{raffle_description_for_announce}\n\n" main_announcement += f"🌍 Envío internacional: {'Sí ✅' if raffle_details['international_shipping'] else 'No ❌'}\n" main_announcement += f"💵 Donación mínima: {original_price_per_number}€\n" main_announcement += f"🗒️ Quedan {remaining_numbers_amount} participaciones disponibles. ¡Date prisa! 🗒️\n\n" main_announcement += f"📜 Normas y condiciones: {TYC_DOCUMENT_URL}" # Update main message main_message_id = get_main_message_id(raffle_id) if main_message_id: requests.post(f"{TELEGRAM_API_URL}/editMessageCaption", json={ "chat_id": channel_id_to_announce, "message_id": main_message_id, "caption": main_announcement, "reply_markup": keyboard, "parse_mode": "HTML" }) # Build caption for update message caption = "" # Add participant info if provided (for payment confirmations) if current_user_name and numbers: escaped_current_user_name = current_user_name numbers_text = "" if len(numbers) > 1: numbers_text = f"con las participaciones: {', '.join(numbers)}" else: numbers_text = f"con la participación: {', '.join(numbers)}" new_participant_line = f"🗳️ @{escaped_current_user_name} se ha unido al sorteo {numbers_text}. ¡Mucha suerte! 🗳️" caption += f"{new_participant_line}\n\n" # Add last participants last_other_participants = get_last_n_other_participants(raffle_id, n=4) last_participants_text = format_last_participants_list(last_other_participants) caption += f"{last_participants_text}\n\n" # Add remaining numbers info remaining_numbers_text = "" if remaining_numbers_amount > 10: remaining_numbers_text = f"🗒️ Todavía hay {remaining_numbers_amount} participaciones disponibles. 🗒️" elif remaining_numbers_amount == 1: remaining_numbers = get_remaining_numbers(raffle_id) remaining_numbers_text = f"⏰⏰⏰ ¡Última participación! ⏰⏰⏰\n\n" remaining_numbers_text += f"Queda la participación: {remaining_numbers[0]}" elif remaining_numbers_amount == 0: remaining_numbers_text = "⌛ ¡Ya no hay participaciones! ⌛\n\n" remaining_numbers_text += "¡El resultado del sorteo se dará a conocer a las 21:45h!" else: remaining_numbers = get_remaining_numbers(raffle_id) remaining_numbers_text = f"🔔🔔🔔 ¡Últimas {remaining_numbers_amount} participaciones disponibles! 🔔🔔🔔\n\n" remaining_numbers_text += f"Quedan las participaciones: {', '.join(remaining_numbers)}" caption += f"{remaining_numbers_text}\n\n" # Add raffle description and details caption += ( f"{raffle_description_for_announce}\n\n" f"🔎 Ver detalles: https://t.me/{REVERSE_CHANNELS.get(channel_id_to_announce)}/{get_main_message_id(raffle_id)}\n\n" f"🌍 Envío internacional: {'Sí ✅' if raffle_details['international_shipping'] else 'No ❌'}\n" f"💵 Donación mínima: {original_price_per_number}€\n\n" f"📜 Normas y condiciones: {TYC_DOCUMENT_URL} \n\n" ) # Send or update the image message update_message_id = get_update_message_id(raffle_id) sent_or_edited_message_id = None if update_message_id: logger.info(f"Attempting to delete old message {update_message_id} and send new one in channel {channel_id_to_announce}") # Try deleting old message first try: delete_payload = {'chat_id': channel_id_to_announce, 'message_id': update_message_id} delete_response = requests.post(f"{TELEGRAM_API_URL}/deleteMessage", data=delete_payload) if delete_response.status_code == 200: logger.info(f"Successfully deleted old message {update_message_id} in channel {channel_id_to_announce}") else: logger.warning(f"Failed to delete old message {update_message_id} in channel {channel_id_to_announce}: {delete_response.text}") except Exception as e_del: logger.warning(f"Error deleting old message {update_message_id}: {e_del}") # Always send new photo after delete attempt files = {'photo': open(image_path, 'rb')} data = { 'chat_id': channel_id_to_announce, 'caption': caption, 'parse_mode': 'HTML' } if keyboard: data['reply_markup'] = json.dumps(keyboard) try: response = requests.post(f"{TELEGRAM_API_URL}/sendPhoto", data=data, files=files) response.raise_for_status() logger.info(f"Sent new photo to channel {channel_id_to_announce}") message_data = response.json().get('result') if message_data and 'message_id' in message_data: sent_or_edited_message_id = message_data['message_id'] except requests.exceptions.RequestException as e: logger.error(f"Error sending new photo to channel {channel_id_to_announce}: {e}") return False finally: files['photo'].close() else: # No previous message, send new logger.info(f"No previous message found for raffle {raffle_id} in channel {channel_id_to_announce}. Sending new.") files = {'photo': open(image_path, 'rb')} data = { 'chat_id': channel_id_to_announce, 'caption': caption, 'parse_mode': 'HTML' } if keyboard: data['reply_markup'] = json.dumps(keyboard) try: response = requests.post(f"{TELEGRAM_API_URL}/sendPhoto", data=data, files=files) response.raise_for_status() logger.info(f"Sent photo to channel {channel_id_to_announce}") message_data = response.json().get('result') if message_data and 'message_id' in message_data: sent_or_edited_message_id = message_data['message_id'] except requests.exceptions.RequestException as e: logger.error(f"Error sending photo to channel {channel_id_to_announce}: {e}") return False finally: files['photo'].close() # Store the new message ID for future updates if sent_or_edited_message_id: store_update_message_id(raffle_id, sent_or_edited_message_id) return True except Exception as e: logger.error(f"Error in send_raffle_update_image for raffle {raffle_id}: {e}") return False