Initial commit
This commit is contained in:
746
app/database.py
Normal file
746
app/database.py
Normal file
@@ -0,0 +1,746 @@
|
||||
# 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()
|
||||
Reference in New Issue
Block a user