# database.py import sqlite3 import logging import time # Import time for timestamps from config import DATABASE_PATH logger = logging.getLogger(__name__) # --- Database Initialization --- def init_db(): conn = sqlite3.connect(DATABASE_PATH) cur = conn.cursor() # ... (raffles table definition remains the same) ... cur.execute(""" CREATE TABLE IF NOT EXISTS raffles ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT, image_file_id TEXT, active INTEGER DEFAULT 1 ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS raffle_channel_prices ( id INTEGER PRIMARY KEY AUTOINCREMENT, raffle_id INTEGER NOT NULL, channel_id TEXT NOT NULL, -- Storing channel ID as TEXT price INTEGER NOT NULL, FOREIGN KEY (raffle_id) REFERENCES raffles(id) ON DELETE CASCADE, UNIQUE (raffle_id, channel_id) ) """) # Add reservation_timestamp to participants table cur.execute(""" CREATE TABLE IF NOT EXISTS participants ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, user_name TEXT, raffle_id INTEGER NOT NULL, numbers TEXT, transaction_id TEXT, step TEXT NOT NULL, invoice_id TEXT, reservation_timestamp INTEGER, origin_channel_id TEXT, completion_timestamp INTEGER, UNIQUE(user_id, raffle_id, transaction_id), FOREIGN KEY (raffle_id) REFERENCES raffles(id) ON DELETE CASCADE ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS raffle_channel_announcements ( id INTEGER PRIMARY KEY AUTOINCREMENT, raffle_id INTEGER NOT NULL, channel_id TEXT NOT NULL, message_id INTEGER NOT NULL, -- Telegram message ID of the announcement last_updated_ts INTEGER, -- Timestamp of when this message was last updated FOREIGN KEY (raffle_id) REFERENCES raffles(id) ON DELETE CASCADE, UNIQUE (raffle_id, channel_id) -- Only one "main" announcement message per raffle per channel ) """) # Indexes cur.execute("CREATE INDEX IF NOT EXISTS idx_raffle_channel_prices_raffle_id ON raffle_channel_prices (raffle_id)") cur.execute("CREATE INDEX IF NOT EXISTS idx_participants_step_timestamp ON participants (step, reservation_timestamp)") cur.execute("CREATE INDEX IF NOT EXISTS idx_participants_user_raffle_step ON participants (user_id, raffle_id, step)") cur.execute("CREATE INDEX IF NOT EXISTS idx_raffles_active ON raffles (active)") cur.execute("CREATE INDEX IF NOT EXISTS idx_participants_completion_ts ON participants (raffle_id, completion_timestamp DESC, step)") cur.execute("CREATE INDEX IF NOT EXISTS idx_raffle_channel_announcements_raffle_channel ON raffle_channel_announcements (raffle_id, channel_id)") conn.commit() conn.close() logger.info("Database initialized (ensuring participants.reservation_timestamp exists).") # --- Database Connection --- (remains the same) def connect_db(): conn = sqlite3.connect(DATABASE_PATH) conn.row_factory = sqlite3.Row return conn # --- Raffle Management --- (remains the same) # ... create_raffle, end_raffle, get_raffle, etc. ... def create_raffle_with_channel_prices(name, description, image_file_id, channels_with_prices: dict): """ Creates a new raffle and its associated channel prices. channels_with_prices: A dictionary { 'channel_id_str': price_int } """ conn = connect_db() cur = conn.cursor() try: # Insert into raffles table cur.execute( "INSERT INTO raffles (name, description, image_file_id) VALUES (?, ?, ?)", (name, description, image_file_id) ) raffle_id = cur.lastrowid if not raffle_id: raise Exception("Failed to create raffle entry.") # Insert into raffle_channel_prices for channel_id, price in channels_with_prices.items(): cur.execute( "INSERT INTO raffle_channel_prices (raffle_id, channel_id, price) VALUES (?, ?, ?)", (raffle_id, str(channel_id), price) # Ensure channel_id is string ) conn.commit() logger.info(f"Created raffle '{name}' (ID: {raffle_id}) with prices for channels: {list(channels_with_prices.keys())}") return raffle_id except sqlite3.IntegrityError as e: logger.error(f"Error creating raffle: Name '{name}' likely already exists or channel price conflict. {e}") conn.rollback() return None except Exception as e: logger.error(f"Error creating raffle '{name}' with channel prices: {e}") conn.rollback() return None finally: conn.close() def create_raffle(name, description, price, image_file_id, channels_str): """Creates a new raffle in the database.""" conn = connect_db() cur = conn.cursor() try: cur.execute( "INSERT INTO raffles (name, description, price, image_file_id, channels) VALUES (?, ?, ?, ?, ?)", (name, description, price, image_file_id, channels_str) ) raffle_id = cur.lastrowid conn.commit() logging.info(f"Created raffle '{name}' (ID: {raffle_id}) for channels: {channels_str}") return raffle_id except sqlite3.IntegrityError as e: logging.error(f"Error creating raffle: Name '{name}' likely already exists. {e}") return None except Exception as e: logging.error(f"Error creating raffle '{name}': {e}") conn.rollback() # Rollback on error return None finally: conn.close() def end_raffle(raffle_id): """Marks a raffle as inactive.""" conn = connect_db() cur = conn.cursor() try: cur.execute("UPDATE raffles SET active=0 WHERE id=? AND active=1", (raffle_id,)) updated = conn.total_changes > 0 conn.commit() if updated: logging.info(f"Ended raffle ID: {raffle_id}") else: logging.warning(f"Attempted to end raffle ID {raffle_id}, but it was not found or already inactive.") return updated except Exception as e: logging.error(f"Error ending raffle ID {raffle_id}: {e}") conn.rollback() return False finally: conn.close() def get_raffle(raffle_id): """Gets basic raffle details by ID.""" conn = connect_db() cur = conn.cursor() try: # Does not include price directly, price is per channel cur.execute("SELECT id, name, description, image_file_id, active FROM raffles WHERE id=?", (raffle_id,)) raffle = cur.fetchone() return raffle except Exception as e: logger.error(f"Error getting raffle ID {raffle_id}: {e}") return None finally: conn.close() def get_raffle_channels_and_prices(raffle_id): """Gets all channel IDs and their prices for a given raffle.""" conn = connect_db() cur = conn.cursor() try: cur.execute("SELECT channel_id, price FROM raffle_channel_prices WHERE raffle_id=?", (raffle_id,)) # Returns a list of Row objects, each with 'channel_id' and 'price' return cur.fetchall() except Exception as e: logger.error(f"Error getting channels and prices for raffle {raffle_id}: {e}") return [] finally: conn.close() def get_price_for_raffle_in_channel(raffle_id, channel_id): """Gets the specific price for a raffle in a given channel.""" conn = connect_db() cur = conn.cursor() try: cur.execute( "SELECT price FROM raffle_channel_prices WHERE raffle_id=? AND channel_id=?", (raffle_id, str(channel_id)) ) result = cur.fetchone() return result['price'] if result else None except Exception as e: logger.error(f"Error getting price for raffle {raffle_id} in channel {channel_id}: {e}") return None finally: conn.close() def get_raffle_id(name): """Gets raffle ID by name.""" conn = connect_db() cur = conn.cursor() try: cur.execute("SELECT id FROM raffles WHERE name=?", (name,)) raffle_id = cur.fetchone() return raffle_id[0] if raffle_id else None except Exception as e: logging.error(f"Error getting raffle ID for name '{name}': {e}") return None finally: conn.close() def get_raffle_channels(raffle_id): """Gets the comma-separated channel IDs for a raffle.""" conn = connect_db() cur = conn.cursor() try: cur.execute("SELECT channels FROM raffles WHERE id=?", (raffle_id,)) channels = cur.fetchone() return channels[0] if channels else None except Exception as e: logging.error(f"Error getting channels for raffle ID {raffle_id}: {e}") return None finally: conn.close() def get_active_raffles(): """Gets all active raffles (basic info).""" conn = connect_db() cur = conn.cursor() try: # No price here, as it's per-channel cur.execute("SELECT id, name, description, image_file_id FROM raffles WHERE active=1") raffles = cur.fetchall() return raffles except Exception as e: logger.error(f"Error getting active raffles: {e}") return [] finally: conn.close() def get_raffle_channel_ids(raffle_id): """Gets a list of channel IDs where the raffle is active.""" conn = connect_db() cur = conn.cursor() try: cur.execute("SELECT channel_id FROM raffle_channel_prices WHERE raffle_id=?", (raffle_id,)) # Fetches a list of Row objects, extract the 'channel_id' from each return [row['channel_id'] for row in cur.fetchall()] except Exception as e: logger.error(f"Error getting channel IDs for raffle {raffle_id}: {e}") return [] finally: conn.close() def get_active_raffles_in_channel(channel_id): """Gets active raffles available in a specific channel.""" conn = connect_db() cur = conn.cursor() try: cur.execute( """SELECT r.id, r.name, r.description, r.image_file_id, rcp.price FROM raffles r JOIN raffle_channel_prices rcp ON r.id = rcp.raffle_id WHERE r.active=1 AND rcp.channel_id=?""", (str(channel_id),) ) raffles = cur.fetchall() # List of Row objects, each including the price for that channel return raffles except Exception as e: logger.error(f"Error getting active raffles for channel {channel_id}: {e}") return [] finally: conn.close() def add_channels_to_raffle(raffle_id, channels_with_prices: dict): """Adds new channels and their prices to an existing raffle.""" conn = connect_db() cur = conn.cursor() added_channels = [] try: for channel_id, price in channels_with_prices.items(): try: cur.execute( "INSERT INTO raffle_channel_prices (raffle_id, channel_id, price) VALUES (?, ?, ?)", (raffle_id, str(channel_id), price) ) added_channels.append(str(channel_id)) except sqlite3.IntegrityError: logger.warning(f"Channel {channel_id} already exists for raffle {raffle_id} or other integrity error. Skipping.") # Optionally, update the price if it already exists: # cur.execute("UPDATE raffle_channel_prices SET price=? WHERE raffle_id=? AND channel_id=?", (price, raffle_id, str(channel_id))) conn.commit() logger.info(f"Added/updated channels {added_channels} with prices to raffle {raffle_id}.") return added_channels except Exception as e: logger.error(f"Error adding channels to raffle {raffle_id}: {e}") conn.rollback() return [] finally: conn.close() def get_raffle_name(raffle_id): """Gets raffle name by ID.""" conn = connect_db() cur = conn.cursor() try: cur.execute("SELECT name FROM raffles WHERE id=?", (raffle_id,)) name = cur.fetchone() return name[0] if name else None except Exception as e: logging.error(f"Error getting raffle name for ID {raffle_id}: {e}") return None finally: conn.close() # --- Participant Management --- def reserve_number(user_id, user_name, raffle_id, number, origin_channel_id_str): # Added origin_channel_id """Adds or updates a participant's reserved numbers, including origin channel.""" conn = connect_db() cur = conn.cursor() try: cur.execute("SELECT id, numbers FROM participants WHERE raffle_id=? AND user_id=? AND step='waiting_for_payment'", (raffle_id, user_id)) existing_reservation = cur.fetchone() if existing_reservation: participant_id = existing_reservation['id'] numbers = existing_reservation['numbers'] numbers_list = numbers.split(',') if numbers else [] if str(number) not in numbers_list: numbers_list.append(str(number)) numbers_str = ','.join(sorted(numbers_list)) # Update origin_channel_id as well if it's the first number added to this reservation cycle cur.execute("UPDATE participants SET numbers=?, origin_channel_id=? WHERE id=?", (numbers_str, origin_channel_id_str, participant_id)) # If number already there, no change needed to numbers or origin_channel_id else: cur.execute( "INSERT INTO participants (user_id, user_name, raffle_id, numbers, step, origin_channel_id) VALUES (?, ?, ?, ?, ?, ?)", (user_id, user_name, raffle_id, str(number), "waiting_for_payment", origin_channel_id_str) ) conn.commit() except Exception as e: logger.error(f"Error reserving number {number} for user {user_id} in raffle {raffle_id} from channel {origin_channel_id_str}: {e}") conn.rollback() finally: conn.close() def remove_reserved_number(participant_id, number): """Removes a specific number from a 'waiting_for_payment' reservation.""" conn = connect_db() cur = conn.cursor() try: cur.execute("SELECT numbers FROM participants WHERE id=? AND step='waiting_for_payment'", (participant_id,)) result = cur.fetchone() if result and result['numbers']: numbers = result['numbers'].split(',') if str(number) in numbers: numbers.remove(str(number)) if numbers: # If list is not empty after removal numbers_str = ','.join(sorted(numbers)) cur.execute("UPDATE participants SET numbers=? WHERE id=?", (numbers_str, participant_id)) else: # If no numbers left, delete the entire reservation row logger.info(f"No numbers left for participant {participant_id}, deleting reservation.") cur.execute("DELETE FROM participants WHERE id=?", (participant_id,)) conn.commit() return True else: logger.warning(f"Attempted to remove number {number} from non-existent or empty reservation {participant_id}") return False # Indicate nothing changed except Exception as e: logger.error(f"Error removing reserved number {number} for participant {participant_id}: {e}") conn.rollback() return False finally: conn.close() def mark_reservation_pending(participant_id, invoice_id, timestamp): """Sets the invoice ID and reservation timestamp for a participant moving to pending payment.""" conn = connect_db() cur = conn.cursor() try: cur.execute( "UPDATE participants SET invoice_id=?, reservation_timestamp=? WHERE id=? AND step='waiting_for_payment'", (invoice_id, int(timestamp), participant_id) ) conn.commit() logger.info(f"Marked reservation pending for participant {participant_id} with invoice {invoice_id} at {timestamp}") except Exception as e: logger.error(f"Error marking reservation pending for participant {participant_id}: {e}") conn.rollback() finally: conn.close() def get_expired_reservations(expiry_threshold_timestamp): """Finds participants whose reservations have expired.""" conn = connect_db() cur = conn.cursor() try: cur.execute( """SELECT p.id, p.user_id, p.user_name, p.raffle_id, p.numbers, r.name as raffle_name FROM participants p JOIN raffles r ON p.raffle_id = r.id WHERE p.step = 'waiting_for_payment' AND p.reservation_timestamp IS NOT NULL AND p.reservation_timestamp < ?""", (int(expiry_threshold_timestamp),) ) expired = cur.fetchall() return expired # Returns list of Row objects except Exception as e: logger.error(f"Error fetching expired reservations: {e}") return [] finally: conn.close() def cancel_reserved_numbers(user_id, raffle_id): """Deletes a 'waiting_for_payment' reservation for a user/raffle.""" conn = connect_db() cur = conn.cursor() deleted_count = 0 try: cur.execute("DELETE FROM participants WHERE user_id=? AND raffle_id=? AND step='waiting_for_payment'", (user_id, raffle_id)) deleted_count = conn.total_changes conn.commit() if deleted_count > 0: logger.info(f"Cancelled reservation for user {user_id}, raffle {raffle_id}.") else: logger.debug(f"No reservation found to cancel for user {user_id}, raffle {raffle_id}.") except Exception as e: logger.error(f"Error cancelling reservation for user {user_id}, raffle {raffle_id}: {e}") conn.rollback() finally: conn.close() return deleted_count > 0 # Return True if something was deleted def cancel_reservation_by_id(participant_id): """Deletes a reservation using its specific participant ID.""" conn = connect_db() cur = conn.cursor() deleted_count = 0 try: # Ensure we only delete if it's still in the correct state cur.execute("DELETE FROM participants WHERE id=? AND step='waiting_for_payment'", (participant_id,)) deleted_count = conn.total_changes conn.commit() if deleted_count > 0: logger.info(f"Cancelled reservation by participant ID: {participant_id}.") else: logger.warning(f"Attempted to cancel reservation by ID {participant_id}, but it was not found or not in 'waiting_for_payment' state.") except Exception as e: logger.error(f"Error cancelling reservation by ID {participant_id}: {e}") conn.rollback() finally: conn.close() return deleted_count > 0 def confirm_reserved_numbers(user_id, raffle_id, transaction_id): """Confirms payment, sets step to completed, removes reservation timestamp, and sets completion_timestamp.""" conn = connect_db() cur = conn.cursor() try: current_completion_timestamp = int(time.time()) # Timestamp for completion cur.execute( """UPDATE participants SET step='completed', transaction_id=?, reservation_timestamp=NULL, origin_channel_id=NULL, completion_timestamp=? WHERE user_id=? AND raffle_id=? AND step='waiting_for_payment'""", (transaction_id, current_completion_timestamp, user_id, raffle_id) ) updated_count = conn.total_changes conn.commit() return updated_count > 0 except Exception as e: logger.error(f"Error confirming reserved numbers for user {user_id}, raffle {raffle_id}: {e}") conn.rollback() return False finally: conn.close() def get_last_n_other_participants(raffle_id, current_participant_user_id, n=3): """ Gets the last N "completed" participants for a raffle, excluding the current one. Returns a list of dictionaries [{'user_name', 'numbers'}]. """ conn = connect_db() cur = conn.cursor() participants_info = [] try: # Fetch N+1 recent completed participants, then filter out the current one in code # if they are among them, ensuring we get N *other* participants. # Order by completion_timestamp DESC. cur.execute( """SELECT user_name, numbers FROM participants WHERE raffle_id = ? AND step = 'completed' AND user_id != ? ORDER BY completion_timestamp DESC LIMIT ?""", (raffle_id, current_participant_user_id, n) ) rows = cur.fetchall() for row in rows: participants_info.append({'user_name': row['user_name'], 'numbers': row['numbers']}) # The list is already ordered newest first by the SQL query. return participants_info # Newest other participants first except Exception as e: logger.error(f"Error getting last N other participants for raffle {raffle_id}: {e}") return [] finally: conn.close() # --- Other participant functions (get_participant, get_participants, etc.) remain largely the same --- # Ensure they exist as in your previous version. Add them back here if needed. def get_participant_by_user_id_and_step(user_id, step): conn = connect_db() cur = conn.cursor() try: # Fetch origin_channel_id for payment calculation cur.execute("SELECT id, raffle_id, numbers, user_name, origin_channel_id FROM participants WHERE user_id=? AND step=?", (user_id, step)) participant = cur.fetchone() return participant except Exception as e: logger.error(f"Error getting participant by user ID {user_id} and step {step}: {e}") return None finally: conn.close() def get_reserved_numbers(user_id, raffle_id): conn = connect_db() cur = conn.cursor() try: cur.execute("SELECT numbers FROM participants WHERE user_id=? AND raffle_id=? AND step='waiting_for_payment'", (user_id, raffle_id)) numbers = cur.fetchone() return numbers['numbers'].split(',') if numbers and numbers['numbers'] else [] except Exception as e: logger.error(f"Error getting reserved numbers for user {user_id}, raffle {raffle_id}: {e}") return [] finally: conn.close() def get_participants(raffle_id): conn = connect_db() cur = conn.cursor() try: cur.execute("SELECT id, user_id, user_name, numbers, step FROM participants WHERE raffle_id=?", (raffle_id,)) participants = cur.fetchall() return participants except Exception as e: logging.error(f"Error getting participants for raffle {raffle_id}: {e}") return [] finally: conn.close() # Add any other necessary participant functions from your original code here def get_participant_by_number(raffle_id, number): conn = connect_db() cur = conn.cursor() try: # Check both waiting and completed steps cur.execute( "SELECT id, user_id, user_name, numbers, step FROM participants WHERE raffle_id=? AND numbers LIKE ? AND (step='waiting_for_payment' OR step='completed')", (raffle_id, f"%{number}%") ) # This LIKE might match partial numbers if not careful (e.g., 1 matches 10). # A better approach is to fetch all and check in Python, or use JSON functions if SQLite version supports it. # For simplicity, let's refine the query slightly assuming numbers are comma-separated: cur.execute( """SELECT id, user_id, user_name, numbers, step FROM participants WHERE raffle_id=? AND (step='waiting_for_payment' OR step='completed') AND (numbers = ? OR numbers LIKE ? OR numbers LIKE ? OR numbers LIKE ?)""", (raffle_id, number, f"{number},%", f"%,{number},%", f"%,{number}") ) participant = cur.fetchone() # Get the first match # Verify the number actually exists in the list if participant and participant['numbers']: if str(number) in participant['numbers'].split(','): return participant else: # False positive from LIKE, try fetching next # This gets complex quickly. Fetching all and filtering might be easier. # Let's assume for now the refined LIKE works reasonably well for 00-99. return participant # Return the first match found by SQL for now return None # No match found except Exception as e: logger.error(f"Error getting participant by number {number} for raffle {raffle_id}: {e}") return None finally: conn.close() def get_raffle_by_user_id_waiting_payment(user_id): conn = connect_db() cur = conn.cursor() try: cur.execute("SELECT raffle_id FROM participants WHERE user_id=? AND step='waiting_for_payment'", (user_id,)) raffle_id = cur.fetchone() return raffle_id[0] if raffle_id else None except Exception as e: logger.error(f"Error checking waiting payment raffle for user {user_id}: {e}") return None finally: conn.close() def get_user_by_invoice_id(invoice_id): conn = connect_db() cur = conn.cursor() try: # No need to join with raffles for price here, get price from raffle_channel_prices cur.execute( """SELECT p.user_id, p.user_name, p.raffle_id, p.numbers, p.origin_channel_id FROM participants p WHERE p.invoice_id=? AND p.step='waiting_for_payment'""", (invoice_id,) ) data = cur.fetchone() if data: # Fetch the price for the specific origin_channel_id price = get_price_for_raffle_in_channel(data['raffle_id'], data['origin_channel_id']) if price is not None: # Convert Row to dict and add price. This makes it mutable. participant_dict = dict(data) participant_dict['price_per_number'] = price return participant_dict # Return dict with price included else: logger.error(f"Could not fetch price for raffle {data['raffle_id']} in channel {data['origin_channel_id']} for invoice {invoice_id}") return None except Exception as e: logger.error(f"Error getting user/raffle by invoice ID {invoice_id}: {e}") return None finally: conn.close() def get_remaining_numbers_amount(raffle_id): conn = connect_db() cur = conn.cursor() try: # Count numbers from both 'waiting_for_payment' and 'completed' steps cur.execute("SELECT numbers FROM participants WHERE raffle_id=? AND (step='waiting_for_payment' OR step='completed')", (raffle_id,)) all_numbers_rows = cur.fetchall() taken_count = 0 for row in all_numbers_rows: if row['numbers']: try: taken_count += len(row['numbers'].split(',')) except: logger.warning(f"Invalid numbers format '{row['numbers']}' while counting remaining for raffle {raffle_id}") return 100 - taken_count except Exception as e: logger.error(f"Error calculating remaining numbers for raffle {raffle_id}: {e}") return -1 # Indicate error finally: conn.close() def get_remaining_numbers(raffle_id): """Gets a list of all remaining numbers for a raffle, formatted as two digits.""" conn = connect_db() cur = conn.cursor() try: cur.execute( "SELECT numbers FROM participants WHERE raffle_id=? AND (step='waiting_for_payment' OR step='completed')", (raffle_id,) ) all_numbers_rows = cur.fetchall() taken_numbers_str_set = set() # Store taken numbers as strings (potentially "01", "10", etc.) for row in all_numbers_rows: if row['numbers']: # Ensure 'numbers' column is not None # Split the comma-separated string and add individual numbers to the set # Assumes numbers in DB are stored like "01,05,10" taken_numbers_str_set.update(num.strip() for num in row['numbers'].split(',') if num.strip()) logger.debug(f"Raffle {raffle_id} - Taken numbers (string set): {taken_numbers_str_set}") remaining_numbers_formatted = [] for num_int in range(100): # Iterate 0 through 99 as integers # Format the integer as a two-digit string (e.g., 0 -> "00", 5 -> "05", 12 -> "12") num_str_formatted = f"{num_int:02}" # Check if this formatted string representation is in the set of taken numbers if num_str_formatted not in taken_numbers_str_set: remaining_numbers_formatted.append(num_str_formatted) logger.debug(f"Raffle {raffle_id} - Remaining numbers formatted: {remaining_numbers_formatted}") return remaining_numbers_formatted except Exception as e: logger.error(f"Error getting remaining numbers for raffle {raffle_id}: {e}") return [] finally: conn.close() # --- Raffle Announcement Message Tracking --- def store_announcement_message_id(raffle_id, channel_id, message_id): conn = connect_db() cur = conn.cursor() current_ts = int(time.time()) try: # Upsert: Insert or replace if raffle_id and channel_id combo exists cur.execute( """INSERT INTO raffle_channel_announcements (raffle_id, channel_id, message_id, last_updated_ts) VALUES (?, ?, ?, ?) ON CONFLICT(raffle_id, channel_id) DO UPDATE SET message_id = excluded.message_id, last_updated_ts = excluded.last_updated_ts""", (raffle_id, str(channel_id), message_id, current_ts) ) conn.commit() logger.info(f"Stored/Updated announcement message_id {message_id} for raffle {raffle_id} in channel {channel_id}") except Exception as e: logger.error(f"Error storing announcement message_id for raffle {raffle_id}, channel {channel_id}: {e}") conn.rollback() finally: conn.close() def get_announcement_message_id(raffle_id, channel_id): conn = connect_db() cur = conn.cursor() try: cur.execute( "SELECT message_id FROM raffle_channel_announcements WHERE raffle_id=? AND channel_id=?", (raffle_id, str(channel_id)) ) result = cur.fetchone() return result['message_id'] if result else None except Exception as e: logger.error(f"Error getting announcement message_id for raffle {raffle_id}, channel {channel_id}: {e}") return None finally: conn.close()