| |
| """ |
| CryptoRugMunch Bot β Database Layer |
| ==================================== |
| SQLite-based user tracking, subscriptions, scan counts, watchlists. |
| """ |
|
|
| import contextlib |
| import os |
| import sqlite3 |
| import time |
| from datetime import UTC, datetime, timedelta |
|
|
| DB_PATH = os.getenv("BOT_DB_PATH", "/root/backend/data/bot_users.db") |
|
|
|
|
| def get_db() -> sqlite3.Connection: |
| os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) |
| conn = sqlite3.connect(DB_PATH) |
| conn.row_factory = sqlite3.Row |
| conn.execute("PRAGMA journal_mode=WAL") |
| return conn |
|
|
|
|
| def init_db(): |
| conn = get_db() |
| conn.executescript(""" |
| CREATE TABLE IF NOT EXISTS users ( |
| user_id INTEGER PRIMARY KEY, |
| username TEXT, |
| first_name TEXT, |
| tier TEXT DEFAULT 'free', |
| tier_expires INTEGER DEFAULT 0, |
| total_scans INTEGER DEFAULT 0, |
| total_ai_msgs INTEGER DEFAULT 0, |
| created_at INTEGER, |
| last_active INTEGER, |
| is_banned INTEGER DEFAULT 0, |
| referral_code TEXT UNIQUE, |
| referred_by TEXT, |
| stars_balance INTEGER DEFAULT 0, |
| bonus_scans INTEGER DEFAULT 0, |
| bonus_ai_msgs INTEGER DEFAULT 0 |
| ); |
| CREATE TABLE IF NOT EXISTS weekly_usage ( |
| user_id INTEGER, |
| week_start TEXT, |
| scans INTEGER DEFAULT 0, |
| ai_msgs INTEGER DEFAULT 0, |
| PRIMARY KEY (user_id, week_start) |
| ); |
| CREATE TABLE IF NOT EXISTS watchlists ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| user_id INTEGER, |
| token_address TEXT, |
| chain TEXT, |
| symbol TEXT, |
| alert_price REAL, |
| alert_direction TEXT, |
| created_at INTEGER, |
| is_active INTEGER DEFAULT 1 |
| ); |
| CREATE TABLE IF NOT EXISTS scan_history ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| user_id INTEGER, |
| token_address TEXT, |
| chain TEXT, |
| scan_type TEXT, |
| risk_score REAL, |
| created_at INTEGER |
| ); |
| CREATE TABLE IF NOT EXISTS star_transactions ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| user_id INTEGER, |
| amount INTEGER, |
| action TEXT, |
| telegram_payment_id TEXT, |
| created_at INTEGER |
| ); |
| CREATE INDEX IF NOT EXISTS idx_weekly_user ON weekly_usage(user_id, week_start); |
| CREATE INDEX IF NOT EXISTS idx_watchlist_user ON watchlists(user_id, is_active); |
| CREATE INDEX IF NOT EXISTS idx_scan_user ON scan_history(user_id, created_at); |
| """) |
| |
| with contextlib.suppress(sqlite3.OperationalError): |
| conn.execute("ALTER TABLE users ADD COLUMN bonus_scans INTEGER DEFAULT 0") |
| with contextlib.suppress(sqlite3.OperationalError): |
| conn.execute("ALTER TABLE users ADD COLUMN bonus_ai_msgs INTEGER DEFAULT 0") |
| conn.commit() |
| conn.close() |
|
|
|
|
| |
|
|
|
|
| def get_or_create_user(user_id: int, username: str | None = None, first_name: str | None = None) -> dict: |
| conn = get_db() |
| row = conn.execute("SELECT * FROM users WHERE user_id = ?", (user_id,)).fetchone() |
| if row: |
| conn.execute( |
| "UPDATE users SET last_active = ?, username = COALESCE(?, username), first_name = COALESCE(?, first_name) WHERE user_id = ?", |
| (int(time.time()), username, first_name, user_id), |
| ) |
| conn.commit() |
| result = dict(row) |
| else: |
| now = int(time.time()) |
| import secrets |
|
|
| ref_code = secrets.token_hex(4).upper() |
| conn.execute( |
| "INSERT INTO users (user_id, username, first_name, created_at, last_active, referral_code) VALUES (?, ?, ?, ?, ?, ?)", |
| (user_id, username, first_name, now, now, ref_code), |
| ) |
| conn.commit() |
| row = conn.execute("SELECT * FROM users WHERE user_id = ?", (user_id,)).fetchone() |
| result = dict(row) |
| conn.close() |
| return result |
|
|
|
|
| def get_user_tier(user_id: int) -> str: |
| conn = get_db() |
| row = conn.execute("SELECT tier, tier_expires FROM users WHERE user_id = ?", (user_id,)).fetchone() |
| conn.close() |
| if not row: |
| return "free" |
| if row["tier"] != "free" and row["tier_expires"] > 0 and time.time() > row["tier_expires"]: |
| conn = get_db() |
| conn.execute("UPDATE users SET tier = 'free', tier_expires = 0 WHERE user_id = ?", (user_id,)) |
| conn.commit() |
| conn.close() |
| return "free" |
| return row["tier"] |
|
|
|
|
| def set_user_tier(user_id: int, tier: str, days: int = 30): |
| expires = int(time.time() + timedelta(days=days).total_seconds()) if tier != "free" else 0 |
| conn = get_db() |
| conn.execute("UPDATE users SET tier = ?, tier_expires = ? WHERE user_id = ?", (tier, expires, user_id)) |
| conn.commit() |
| conn.close() |
|
|
|
|
| |
|
|
|
|
| def _current_week_start() -> str: |
| """Return Monday of the current UTC week as YYYY-MM-DD.""" |
| now = datetime.now(UTC) |
| monday = now - timedelta(days=now.weekday()) |
| return monday.strftime("%Y-%m-%d") |
|
|
|
|
| def get_weekly_usage(user_id: int) -> tuple[int, int]: |
| week = _current_week_start() |
| conn = get_db() |
| row = conn.execute( |
| "SELECT scans, ai_msgs FROM weekly_usage WHERE user_id = ? AND week_start = ?", |
| (user_id, week), |
| ).fetchone() |
| conn.close() |
| return (row["scans"], row["ai_msgs"]) if row else (0, 0) |
|
|
|
|
| def increment_usage(user_id: int, scan: bool = False, ai_msg: bool = False): |
| """Consume 1 scan or AI msg. Tries weekly allotment first, then bonus balance.""" |
| week = _current_week_start() |
| conn = get_db() |
|
|
| if scan: |
| |
| row = conn.execute( |
| "SELECT scans FROM weekly_usage WHERE user_id = ? AND week_start = ?", (user_id, week) |
| ).fetchone() |
| user = conn.execute("SELECT bonus_scans FROM users WHERE user_id = ?", (user_id,)).fetchone() |
| from config import TIERS |
|
|
| tier = get_user_tier(user_id) |
| limit = TIERS.get(tier, TIERS["free"]).get("scans_per_week", 25) |
| current = row["scans"] if row else 0 |
|
|
| if current < limit: |
| |
| conn.execute( |
| """ |
| INSERT INTO weekly_usage (user_id, week_start, scans, ai_msgs) |
| VALUES (?, ?, 1, 0) |
| ON CONFLICT(user_id, week_start) DO UPDATE SET scans = scans + 1 |
| """, |
| (user_id, week), |
| ) |
| elif user and user["bonus_scans"] > 0: |
| |
| conn.execute("UPDATE users SET bonus_scans = bonus_scans - 1 WHERE user_id = ?", (user_id,)) |
| else: |
| conn.close() |
| return False |
|
|
| conn.execute("UPDATE users SET total_scans = total_scans + 1 WHERE user_id = ?", (user_id,)) |
|
|
| if ai_msg: |
| row = conn.execute( |
| "SELECT ai_msgs FROM weekly_usage WHERE user_id = ? AND week_start = ?", (user_id, week) |
| ).fetchone() |
| user = conn.execute("SELECT bonus_ai_msgs FROM users WHERE user_id = ?", (user_id,)).fetchone() |
| from config import TIERS |
|
|
| tier = get_user_tier(user_id) |
| limit = TIERS.get(tier, TIERS["free"]).get("ai_msgs_per_week", 15) |
| current = row["ai_msgs"] if row else 0 |
|
|
| if current < limit: |
| conn.execute( |
| """ |
| INSERT INTO weekly_usage (user_id, week_start, scans, ai_msgs) |
| VALUES (?, ?, 0, 1) |
| ON CONFLICT(user_id, week_start) DO UPDATE SET ai_msgs = ai_msgs + 1 |
| """, |
| (user_id, week), |
| ) |
| elif user and user["bonus_ai_msgs"] > 0: |
| conn.execute("UPDATE users SET bonus_ai_msgs = bonus_ai_msgs - 1 WHERE user_id = ?", (user_id,)) |
| else: |
| conn.close() |
| return False |
|
|
| conn.execute("UPDATE users SET total_ai_msgs = total_ai_msgs + 1 WHERE user_id = ?", (user_id,)) |
|
|
| conn.commit() |
| conn.close() |
| return True |
|
|
|
|
| def check_rate_limit(user_id: int, action: str = "scan") -> tuple[bool, int, int]: |
| """Returns (allowed, current_used, max_allowed). |
| 'allowed' is True if weekly allotment OR bonus balance has room.""" |
| from config import TIERS |
|
|
| tier = get_user_tier(user_id) |
| tier_config = TIERS.get(tier, TIERS["free"]) |
| scans_used, ai_used = get_weekly_usage(user_id) |
|
|
| conn = get_db() |
| user = conn.execute("SELECT bonus_scans, bonus_ai_msgs FROM users WHERE user_id = ?", (user_id,)).fetchone() |
| conn.close() |
| bonus_s = user["bonus_scans"] if user else 0 |
| bonus_a = user["bonus_ai_msgs"] if user else 0 |
|
|
| if action == "scan": |
| limit = tier_config.get("scans_per_week", 25) |
| current = scans_used |
| bonus = bonus_s |
| else: |
| limit = tier_config.get("ai_msgs_per_week", 15) |
| current = ai_used |
| bonus = bonus_a |
|
|
| |
| allowed = (current < limit) or (bonus > 0) |
| return allowed, current, limit |
|
|
|
|
| |
|
|
|
|
| def add_watchlist( |
| user_id: int, |
| token_address: str, |
| chain: str, |
| symbol: str | None = None, |
| alert_price: float | None = None, |
| alert_dir: str | None = None, |
| ) -> bool: |
| tier = get_user_tier(user_id) |
| max_items = {"free": 0, "scout": 10, "hunter": 50, "pro": -1}.get(tier, 0) |
| if max_items == 0: |
| return False |
| conn = get_db() |
| count = conn.execute( |
| "SELECT COUNT(*) as c FROM watchlists WHERE user_id = ? AND is_active = 1", (user_id,) |
| ).fetchone()["c"] |
| if max_items != -1 and count >= max_items: |
| conn.close() |
| return False |
| conn.execute( |
| "INSERT INTO watchlists (user_id, token_address, chain, symbol, alert_price, alert_direction, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", |
| (user_id, token_address, chain, symbol, alert_price, alert_dir, int(time.time())), |
| ) |
| conn.commit() |
| conn.close() |
| return True |
|
|
|
|
| def get_watchlist(user_id: int) -> list[dict]: |
| conn = get_db() |
| rows = conn.execute( |
| "SELECT * FROM watchlists WHERE user_id = ? AND is_active = 1 ORDER BY created_at DESC", |
| (user_id,), |
| ).fetchall() |
| conn.close() |
| return [dict(r) for r in rows] |
|
|
|
|
| def remove_watchlist(user_id: int, token_address: str) -> bool: |
| conn = get_db() |
| cursor = conn.execute( |
| "UPDATE watchlists SET is_active = 0 WHERE user_id = ? AND token_address = ?", |
| (user_id, token_address), |
| ) |
| conn.commit() |
| result = cursor.rowcount > 0 |
| conn.close() |
| return result |
|
|
|
|
| |
|
|
|
|
| def log_scan(user_id: int, token_address: str, chain: str, scan_type: str, risk_score: float): |
| conn = get_db() |
| conn.execute( |
| "INSERT INTO scan_history (user_id, token_address, chain, scan_type, risk_score, created_at) VALUES (?, ?, ?, ?, ?, ?)", |
| (user_id, token_address, chain, scan_type, risk_score, int(time.time())), |
| ) |
| conn.commit() |
| conn.close() |
|
|
|
|
| |
|
|
|
|
| def add_stars(user_id: int, amount: int, action: str, payment_id: str | None = None): |
| conn = get_db() |
| conn.execute("UPDATE users SET stars_balance = stars_balance + ? WHERE user_id = ?", (amount, user_id)) |
| conn.execute( |
| "INSERT INTO star_transactions (user_id, amount, action, telegram_payment_id, created_at) VALUES (?, ?, ?, ?, ?)", |
| (user_id, amount, action, payment_id, int(time.time())), |
| ) |
| conn.commit() |
| conn.close() |
|
|
|
|
| def spend_stars(user_id: int, amount: int, action: str) -> bool: |
| conn = get_db() |
| row = conn.execute("SELECT stars_balance FROM users WHERE user_id = ?", (user_id,)).fetchone() |
| if not row or row["stars_balance"] < amount: |
| conn.close() |
| return False |
| conn.execute("UPDATE users SET stars_balance = stars_balance - ? WHERE user_id = ?", (amount, user_id)) |
| conn.execute( |
| "INSERT INTO star_transactions (user_id, amount, action, created_at) VALUES (?, ?, ?, ?)", |
| (user_id, -amount, action, int(time.time())), |
| ) |
| conn.commit() |
| conn.close() |
| return True |
|
|
|
|
| def apply_top_up(user_id: int, pack_type: str, amount: int, stars_cost: int, payment_id: str | None = None) -> bool: |
| """Apply a top-up purchase: deduct stars, credit bonus balance.""" |
| conn = get_db() |
| row = conn.execute("SELECT stars_balance FROM users WHERE user_id = ?", (user_id,)).fetchone() |
| if not row or row["stars_balance"] < stars_cost: |
| conn.close() |
| return False |
| |
| conn.execute( |
| "UPDATE users SET stars_balance = stars_balance - ? WHERE user_id = ?", |
| (stars_cost, user_id), |
| ) |
| |
| if pack_type == "scans": |
| conn.execute("UPDATE users SET bonus_scans = bonus_scans + ? WHERE user_id = ?", (amount, user_id)) |
| elif pack_type == "ai_msgs": |
| conn.execute( |
| "UPDATE users SET bonus_ai_msgs = bonus_ai_msgs + ? WHERE user_id = ?", |
| (amount, user_id), |
| ) |
| |
| conn.execute( |
| "INSERT INTO star_transactions (user_id, amount, action, telegram_payment_id, created_at) VALUES (?, ?, ?, ?, ?)", |
| (user_id, -stars_cost, f"top_up_{pack_type}_{amount}", payment_id, int(time.time())), |
| ) |
| conn.commit() |
| conn.close() |
| return True |
|
|
|
|
| |
|
|
|
|
| def get_user_stats(user_id: int) -> dict: |
| conn = get_db() |
| user = conn.execute("SELECT * FROM users WHERE user_id = ?", (user_id,)).fetchone() |
| watchlist_count = conn.execute( |
| "SELECT COUNT(*) as c FROM watchlists WHERE user_id = ? AND is_active = 1", (user_id,) |
| ).fetchone()["c"] |
| conn.close() |
| if not user: |
| return {} |
| scans_used, ai_used = get_weekly_usage(user_id) |
| return { |
| **dict(user), |
| "watchlist_count": watchlist_count, |
| "scans_this_week": scans_used, |
| "ai_msgs_this_week": ai_used, |
| } |
|
|
|
|
| |
| init_db() |
|
|