746 lines
31 KiB
Python
746 lines
31 KiB
Python
# 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() |