refactor(plugins): comprehensive code review - ~35 fixes across 14 plugins
Phase 1 - Plugin code review (14/14 plugins): - Security: 3x token leak in print→logger.debug, Bearer prefix handling - Bug: bare except→specific exceptions, HorseState type safety, sync→async - Critical: response_model undefined, route dead code, sync blocking event loop - Quality: 11x print()→logger, variable name shadowing, consistent logging Phase 2 - Deep analysis: - Fix: payout int truncation→max(1, round(amount*odds)) - Fix: room_store get_lock race condition→dict.setdefault() - Verify: data_manager f-string SQL is safe (uses ? placeholders) Infrastructure: review reports generated for all plugins.
This commit is contained in:
@@ -1,294 +1,303 @@
|
||||
import asyncio
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from typing import Tuple, List, Dict, Any
|
||||
from .config import Config
|
||||
from .database import PointsDatabase
|
||||
|
||||
|
||||
class PointsAPI:
|
||||
"""Points system API for managing user points."""
|
||||
|
||||
def __init__(self, config: Config):
|
||||
self.config = config
|
||||
self.db = PointsDatabase(config)
|
||||
self._lock = threading.Lock()
|
||||
|
||||
async def get_balance(self, user_id: str) -> int:
|
||||
"""Get user's current points balance."""
|
||||
return await asyncio.to_thread(self.db.get_user_balance, user_id)
|
||||
|
||||
async def add_points(
|
||||
self, user_id: str, amount: int, source: str, reason: str = None
|
||||
) -> Tuple[bool, int]:
|
||||
"""Add points to user account.
|
||||
|
||||
Returns: (success, new_balance)
|
||||
"""
|
||||
# Parameter validation
|
||||
if not isinstance(amount, int) or amount <= 0:
|
||||
return False, 0
|
||||
if not user_id or not source:
|
||||
return False, 0
|
||||
|
||||
# Operation limit validation
|
||||
if self.config.POINTS_MAX_PER_OPERATION > 0:
|
||||
if amount > self.config.POINTS_MAX_PER_OPERATION:
|
||||
return False, 0
|
||||
|
||||
def _add():
|
||||
with self._lock:
|
||||
conn = self.db.get_connection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
# Ensure user exists
|
||||
self.db.ensure_user_exists(user_id)
|
||||
|
||||
# Get current balance
|
||||
cursor.execute(
|
||||
"SELECT points FROM user_points WHERE user_id = ?",
|
||||
(user_id,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
current_balance = row["points"] if row else 0
|
||||
|
||||
# Check balance limit
|
||||
new_balance = current_balance + amount
|
||||
if self.config.POINTS_MAX_BALANCE > 0:
|
||||
if new_balance > self.config.POINTS_MAX_BALANCE:
|
||||
conn.close()
|
||||
return False, current_balance
|
||||
|
||||
# Update balance and total_earned
|
||||
now = datetime.now().isoformat()
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE user_points
|
||||
SET points = ?, total_earned = total_earned + ?, updated_at = ?
|
||||
WHERE user_id = ?
|
||||
""",
|
||||
(new_balance, amount, now, user_id),
|
||||
)
|
||||
|
||||
# Write transaction log
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO point_transactions
|
||||
(user_id, amount, balance_after, source, reason, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(user_id, amount, new_balance, source, reason, now),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return True, new_balance
|
||||
except Exception:
|
||||
conn.close()
|
||||
return False, 0
|
||||
|
||||
return await asyncio.to_thread(_add)
|
||||
|
||||
async def spend_points(
|
||||
self, user_id: str, amount: int, source: str, reason: str = None
|
||||
) -> Tuple[bool, int]:
|
||||
"""Spend points from user account.
|
||||
|
||||
Returns: (success, new_balance)
|
||||
"""
|
||||
# Parameter validation
|
||||
if not isinstance(amount, int) or amount <= 0:
|
||||
return False, 0
|
||||
if not user_id or not source:
|
||||
return False, 0
|
||||
|
||||
# Operation limit validation
|
||||
if self.config.POINTS_MAX_PER_OPERATION > 0:
|
||||
if amount > self.config.POINTS_MAX_PER_OPERATION:
|
||||
return False, 0
|
||||
|
||||
def _spend():
|
||||
with self._lock:
|
||||
conn = self.db.get_connection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
# Ensure user exists
|
||||
self.db.ensure_user_exists(user_id)
|
||||
|
||||
# Get current balance
|
||||
cursor.execute(
|
||||
"SELECT points FROM user_points WHERE user_id = ?",
|
||||
(user_id,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
current_balance = row["points"] if row else 0
|
||||
|
||||
# Check sufficient balance
|
||||
if current_balance < amount:
|
||||
conn.close()
|
||||
return False, current_balance
|
||||
|
||||
# Update balance and total_spent
|
||||
new_balance = current_balance - amount
|
||||
now = datetime.now().isoformat()
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE user_points
|
||||
SET points = ?, total_spent = total_spent + ?, updated_at = ?
|
||||
WHERE user_id = ?
|
||||
""",
|
||||
(new_balance, amount, now, user_id),
|
||||
)
|
||||
|
||||
# Write transaction log (amount as negative)
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO point_transactions
|
||||
(user_id, amount, balance_after, source, reason, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(user_id, -amount, new_balance, source, reason, now),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return True, new_balance
|
||||
except Exception:
|
||||
conn.close()
|
||||
return False, 0
|
||||
|
||||
return await asyncio.to_thread(_spend)
|
||||
|
||||
async def set_points(
|
||||
self, user_id: str, amount: int, source: str, reason: str = None
|
||||
) -> Tuple[bool, int]:
|
||||
"""Set user's points to exact amount.
|
||||
|
||||
Returns: (success, new_balance)
|
||||
"""
|
||||
# Parameter validation
|
||||
if not isinstance(amount, int) or amount < 0:
|
||||
return False, 0
|
||||
if not user_id or not source:
|
||||
return False, 0
|
||||
|
||||
def _set():
|
||||
with self._lock:
|
||||
conn = self.db.get_connection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
# Ensure user exists
|
||||
self.db.ensure_user_exists(user_id)
|
||||
|
||||
# Get current balance
|
||||
cursor.execute(
|
||||
"SELECT points, total_earned FROM user_points WHERE user_id = ?",
|
||||
(user_id,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
current_balance = row["points"] if row else 0
|
||||
current_earned = row["total_earned"] if row else 0
|
||||
|
||||
# If new value equals old value, return without writing
|
||||
if current_balance == amount:
|
||||
conn.close()
|
||||
return True, amount
|
||||
|
||||
# Calculate difference for total_earned (only positive diff)
|
||||
diff = amount - current_balance
|
||||
earned_diff = max(0, diff)
|
||||
|
||||
# Update balance and total_earned
|
||||
now = datetime.now().isoformat()
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE user_points
|
||||
SET points = ?, total_earned = total_earned + ?, updated_at = ?
|
||||
WHERE user_id = ?
|
||||
""",
|
||||
(amount, earned_diff, now, user_id),
|
||||
)
|
||||
|
||||
# Write transaction log
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO point_transactions
|
||||
(user_id, amount, balance_after, source, reason, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(user_id, diff, amount, source, reason, now),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return True, amount
|
||||
except Exception:
|
||||
conn.close()
|
||||
return False, 0
|
||||
|
||||
return await asyncio.to_thread(_set)
|
||||
|
||||
async def get_transactions(
|
||||
self, user_id: str, limit: int = 20, offset: int = 0
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get transaction history for a user.
|
||||
|
||||
Returns: List of transaction dicts
|
||||
"""
|
||||
# Normalize parameters
|
||||
limit = max(1, min(100, limit))
|
||||
offset = max(0, offset)
|
||||
|
||||
def _get():
|
||||
conn = self.db.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT id, user_id, amount, balance_after, source, reason, created_at
|
||||
FROM point_transactions
|
||||
WHERE user_id = ?
|
||||
ORDER BY id DESC
|
||||
LIMIT ? OFFSET ?
|
||||
""",
|
||||
(user_id, limit, offset),
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
return await asyncio.to_thread(_get)
|
||||
|
||||
async def get_ranking(
|
||||
self, limit: int = 10, order_by: str = "points"
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get points ranking.
|
||||
|
||||
Returns: List of ranking dicts with rank field
|
||||
"""
|
||||
# Normalize parameters
|
||||
limit = max(1, min(100, limit))
|
||||
if order_by not in ("points", "total_earned"):
|
||||
order_by = "points"
|
||||
|
||||
def _get():
|
||||
conn = self.db.get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
order_column = "points" if order_by == "points" else "total_earned"
|
||||
query = f"""
|
||||
SELECT
|
||||
RANK() OVER (ORDER BY {order_column} DESC) as rank,
|
||||
user_id,
|
||||
points,
|
||||
total_earned,
|
||||
total_spent
|
||||
FROM user_points
|
||||
ORDER BY {order_column} DESC, user_id ASC
|
||||
LIMIT ?
|
||||
"""
|
||||
cursor.execute(query, (limit,))
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
return await asyncio.to_thread(_get)
|
||||
import asyncio
|
||||
import logging
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from typing import Tuple, List, Dict, Any
|
||||
from .config import Config
|
||||
from .database import PointsDatabase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PointsAPI:
|
||||
"""Points system API for managing user points."""
|
||||
|
||||
def __init__(self, config: Config):
|
||||
self.config = config
|
||||
self.db = PointsDatabase(config)
|
||||
self._lock = threading.Lock()
|
||||
|
||||
async def get_balance(self, user_id: str) -> int:
|
||||
"""Get user's current points balance."""
|
||||
return await asyncio.to_thread(self.db.get_user_balance, user_id)
|
||||
|
||||
async def add_points(
|
||||
self, user_id: str, amount: int, source: str, reason: str = None
|
||||
) -> Tuple[bool, int]:
|
||||
"""Add points to user account.
|
||||
|
||||
Returns: (success, new_balance)
|
||||
"""
|
||||
# Parameter validation
|
||||
if not isinstance(amount, int) or amount <= 0:
|
||||
return False, 0
|
||||
if not user_id or not source:
|
||||
return False, 0
|
||||
|
||||
# Operation limit validation
|
||||
if self.config.POINTS_MAX_PER_OPERATION > 0:
|
||||
if amount > self.config.POINTS_MAX_PER_OPERATION:
|
||||
return False, 0
|
||||
|
||||
def _add():
|
||||
with self._lock:
|
||||
conn = self.db.get_connection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
# Ensure user exists
|
||||
self.db.ensure_user_exists(user_id, conn)
|
||||
|
||||
# Get current balance
|
||||
cursor.execute(
|
||||
"SELECT points FROM user_points WHERE user_id = ?",
|
||||
(user_id,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
current_balance = row["points"] if row else 0
|
||||
|
||||
# Check balance limit
|
||||
new_balance = current_balance + amount
|
||||
if self.config.POINTS_MAX_BALANCE > 0:
|
||||
if new_balance > self.config.POINTS_MAX_BALANCE:
|
||||
conn.close()
|
||||
return False, current_balance
|
||||
|
||||
# Update balance and total_earned
|
||||
now = datetime.now().isoformat()
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE user_points
|
||||
SET points = ?, total_earned = total_earned + ?, updated_at = ?
|
||||
WHERE user_id = ?
|
||||
""",
|
||||
(new_balance, amount, now, user_id),
|
||||
)
|
||||
|
||||
# Write transaction log
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO point_transactions
|
||||
(user_id, amount, balance_after, source, reason, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(user_id, amount, new_balance, source, reason, now),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return True, new_balance
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
conn.close()
|
||||
logger.error(f"add_points failed for {user_id}: {e}")
|
||||
return False, 0
|
||||
|
||||
return await asyncio.to_thread(_add)
|
||||
|
||||
async def spend_points(
|
||||
self, user_id: str, amount: int, source: str, reason: str = None
|
||||
) -> Tuple[bool, int]:
|
||||
"""Spend points from user account.
|
||||
|
||||
Returns: (success, new_balance)
|
||||
"""
|
||||
# Parameter validation
|
||||
if not isinstance(amount, int) or amount <= 0:
|
||||
return False, 0
|
||||
if not user_id or not source:
|
||||
return False, 0
|
||||
|
||||
# Operation limit validation
|
||||
if self.config.POINTS_MAX_PER_OPERATION > 0:
|
||||
if amount > self.config.POINTS_MAX_PER_OPERATION:
|
||||
return False, 0
|
||||
|
||||
def _spend():
|
||||
with self._lock:
|
||||
conn = self.db.get_connection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
# Ensure user exists
|
||||
self.db.ensure_user_exists(user_id, conn)
|
||||
|
||||
# Get current balance
|
||||
cursor.execute(
|
||||
"SELECT points FROM user_points WHERE user_id = ?",
|
||||
(user_id,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
current_balance = row["points"] if row else 0
|
||||
|
||||
# Check sufficient balance
|
||||
if current_balance < amount:
|
||||
conn.close()
|
||||
return False, current_balance
|
||||
|
||||
# Update balance and total_spent
|
||||
new_balance = current_balance - amount
|
||||
now = datetime.now().isoformat()
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE user_points
|
||||
SET points = ?, total_spent = total_spent + ?, updated_at = ?
|
||||
WHERE user_id = ?
|
||||
""",
|
||||
(new_balance, amount, now, user_id),
|
||||
)
|
||||
|
||||
# Write transaction log (amount as negative)
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO point_transactions
|
||||
(user_id, amount, balance_after, source, reason, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(user_id, -amount, new_balance, source, reason, now),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return True, new_balance
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
conn.close()
|
||||
logger.error(f"spend_points failed for {user_id}: {e}")
|
||||
return False, 0
|
||||
|
||||
return await asyncio.to_thread(_spend)
|
||||
|
||||
async def set_points(
|
||||
self, user_id: str, amount: int, source: str, reason: str = None
|
||||
) -> Tuple[bool, int]:
|
||||
"""Set user's points to exact amount.
|
||||
|
||||
Returns: (success, new_balance)
|
||||
"""
|
||||
# Parameter validation
|
||||
if not isinstance(amount, int) or amount < 0:
|
||||
return False, 0
|
||||
if not user_id or not source:
|
||||
return False, 0
|
||||
|
||||
def _set():
|
||||
with self._lock:
|
||||
conn = self.db.get_connection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
# Ensure user exists
|
||||
self.db.ensure_user_exists(user_id, conn)
|
||||
|
||||
# Get current balance
|
||||
cursor.execute(
|
||||
"SELECT points, total_earned FROM user_points WHERE user_id = ?",
|
||||
(user_id,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
current_balance = row["points"] if row else 0
|
||||
current_earned = row["total_earned"] if row else 0
|
||||
|
||||
# If new value equals old value, return without writing
|
||||
if current_balance == amount:
|
||||
conn.close()
|
||||
return True, amount
|
||||
|
||||
# Calculate difference for total_earned (only positive diff)
|
||||
diff = amount - current_balance
|
||||
earned_diff = max(0, diff)
|
||||
|
||||
# Update balance and total_earned
|
||||
now = datetime.now().isoformat()
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE user_points
|
||||
SET points = ?, total_earned = total_earned + ?, updated_at = ?
|
||||
WHERE user_id = ?
|
||||
""",
|
||||
(amount, earned_diff, now, user_id),
|
||||
)
|
||||
|
||||
# Write transaction log
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO point_transactions
|
||||
(user_id, amount, balance_after, source, reason, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(user_id, diff, amount, source, reason, now),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return True, amount
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
conn.close()
|
||||
logger.error(f"set_points failed for {user_id}: {e}")
|
||||
return False, 0
|
||||
|
||||
return await asyncio.to_thread(_set)
|
||||
|
||||
async def get_transactions(
|
||||
self, user_id: str, limit: int = 20, offset: int = 0
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get transaction history for a user.
|
||||
|
||||
Returns: List of transaction dicts
|
||||
"""
|
||||
# Normalize parameters
|
||||
limit = max(1, min(100, limit))
|
||||
offset = max(0, offset)
|
||||
|
||||
def _get():
|
||||
conn = self.db.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT id, user_id, amount, balance_after, source, reason, created_at
|
||||
FROM point_transactions
|
||||
WHERE user_id = ?
|
||||
ORDER BY id DESC
|
||||
LIMIT ? OFFSET ?
|
||||
""",
|
||||
(user_id, limit, offset),
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
return await asyncio.to_thread(_get)
|
||||
|
||||
async def get_ranking(
|
||||
self, limit: int = 10, order_by: str = "points"
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get points ranking.
|
||||
|
||||
Returns: List of ranking dicts with rank field
|
||||
"""
|
||||
# Normalize parameters
|
||||
limit = max(1, min(100, limit))
|
||||
if order_by not in ("points", "total_earned"):
|
||||
order_by = "points"
|
||||
|
||||
def _get():
|
||||
conn = self.db.get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
order_column = "points" if order_by == "points" else "total_earned"
|
||||
query = f"""
|
||||
SELECT
|
||||
RANK() OVER (ORDER BY {order_column} DESC) as rank,
|
||||
user_id,
|
||||
points,
|
||||
total_earned,
|
||||
total_spent
|
||||
FROM user_points
|
||||
ORDER BY {order_column} DESC, user_id ASC
|
||||
LIMIT ?
|
||||
"""
|
||||
cursor.execute(query, (limit,))
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
return await asyncio.to_thread(_get)
|
||||
|
||||
@@ -1,100 +1,104 @@
|
||||
import sqlite3
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict, Any
|
||||
from .config import Config
|
||||
|
||||
|
||||
class PointsDatabase:
|
||||
"""SQLite database handler for points system."""
|
||||
|
||||
def __init__(self, config: Config):
|
||||
self.config = config
|
||||
self.db_path = config.POINTS_DB_FILE
|
||||
self._ensure_db_dir()
|
||||
self._init_db()
|
||||
|
||||
def _ensure_db_dir(self):
|
||||
"""Create database directory if it doesn't exist."""
|
||||
db_dir = os.path.dirname(self.db_path)
|
||||
if db_dir:
|
||||
os.makedirs(db_dir, exist_ok=True)
|
||||
|
||||
def _init_db(self):
|
||||
"""Initialize database tables."""
|
||||
conn = sqlite3.connect(self.db_path, timeout=5.0)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create user_points table
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS user_points (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
points INTEGER NOT NULL DEFAULT 0 CHECK(points >= 0),
|
||||
total_earned INTEGER NOT NULL DEFAULT 0,
|
||||
total_spent INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
# Create point_transactions table
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS point_transactions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
balance_after INTEGER NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
reason TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
# Create indexes
|
||||
cursor.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_transactions_user_id ON point_transactions(user_id)"
|
||||
)
|
||||
cursor.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_transactions_source ON point_transactions(source)"
|
||||
)
|
||||
cursor.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_transactions_created_at ON point_transactions(created_at)"
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def get_connection(self) -> sqlite3.Connection:
|
||||
"""Get a database connection."""
|
||||
conn = sqlite3.connect(self.db_path, timeout=5.0)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def get_user_balance(self, user_id: str) -> int:
|
||||
"""Get user's current points balance."""
|
||||
conn = self.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT points FROM user_points WHERE user_id = ?", (user_id,))
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
return row["points"] if row else 0
|
||||
|
||||
def ensure_user_exists(self, user_id: str) -> None:
|
||||
"""Create user account if it doesn't exist."""
|
||||
conn = self.get_connection()
|
||||
cursor = conn.cursor()
|
||||
now = datetime.now().isoformat()
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO user_points
|
||||
(user_id, points, total_earned, total_spent, created_at, updated_at)
|
||||
VALUES (?, 0, 0, 0, ?, ?)
|
||||
""",
|
||||
(user_id, now, now),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
import sqlite3
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict, Any
|
||||
from .config import Config
|
||||
|
||||
|
||||
class PointsDatabase:
|
||||
"""SQLite database handler for points system."""
|
||||
|
||||
def __init__(self, config: Config):
|
||||
self.config = config
|
||||
self.db_path = config.POINTS_DB_FILE
|
||||
self._ensure_db_dir()
|
||||
self._init_db()
|
||||
|
||||
def _ensure_db_dir(self):
|
||||
"""Create database directory if it doesn't exist."""
|
||||
db_dir = os.path.dirname(self.db_path)
|
||||
if db_dir:
|
||||
os.makedirs(db_dir, exist_ok=True)
|
||||
|
||||
def _init_db(self):
|
||||
"""Initialize database tables."""
|
||||
conn = sqlite3.connect(self.db_path, timeout=5.0)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create user_points table
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS user_points (
|
||||
user_id TEXT PRIMARY KEY,
|
||||
points INTEGER NOT NULL DEFAULT 0 CHECK(points >= 0),
|
||||
total_earned INTEGER NOT NULL DEFAULT 0,
|
||||
total_spent INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
# Create point_transactions table
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS point_transactions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL,
|
||||
amount INTEGER NOT NULL,
|
||||
balance_after INTEGER NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
reason TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
# Create indexes
|
||||
cursor.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_transactions_user_id ON point_transactions(user_id)"
|
||||
)
|
||||
cursor.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_transactions_source ON point_transactions(source)"
|
||||
)
|
||||
cursor.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_transactions_created_at ON point_transactions(created_at)"
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def get_connection(self) -> sqlite3.Connection:
|
||||
"""Get a database connection."""
|
||||
conn = sqlite3.connect(self.db_path, timeout=5.0)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def get_user_balance(self, user_id: str) -> int:
|
||||
"""Get user's current points balance."""
|
||||
conn = self.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT points FROM user_points WHERE user_id = ?", (user_id,))
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
return row["points"] if row else 0
|
||||
|
||||
def ensure_user_exists(self, user_id: str, conn=None) -> None:
|
||||
"""Create user account if it doesn't exist. Reuses provided conn if given."""
|
||||
should_close = False
|
||||
if conn is None:
|
||||
conn = self.get_connection()
|
||||
should_close = True
|
||||
cursor = conn.cursor()
|
||||
now = datetime.now().isoformat()
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO user_points
|
||||
(user_id, points, total_earned, total_spent, created_at, updated_at)
|
||||
VALUES (?, 0, 0, 0, ?, ?)
|
||||
""",
|
||||
(user_id, now, now),
|
||||
)
|
||||
if should_close:
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
Reference in New Issue
Block a user