import json import os import datetime from threading import Lock from config import IS_PRODUCTION # Added for V3.1.3 Quota Override import tempfile import shutil LIMITS_FILE = "student_limits.json" # V260.9: Use /tmp or local for usage stats to avoid permission errors USAGE_FILENAME = "usage_stats.json" # Function to get writable usage path def get_usage_file_path(): # Try local first if os.access(".", os.W_OK): return USAGE_FILENAME # Fallback to temp dir return os.path.join(tempfile.gettempdir(), USAGE_FILENAME) USAGE_FILE = get_usage_file_path() # V261: Persistent user tracking ALL_USERS_FILE = "all_users.json" class QuotaManager: def __init__(self): self.lock = Lock() print(f"📊 [QUOTA] Using stats file: {USAGE_FILE}") self._load_limits() self._ensure_files() def _ensure_files(self): if not os.path.exists(LIMITS_FILE): try: with open(LIMITS_FILE, 'w', encoding='utf-8') as f: json.dump({"default_limit": 30, "students": {}}, f) except PermissionError: print(f"⚠️ [QUOTA] Cannot create defaults limits file (Read-only fs)") if not os.path.exists(USAGE_FILE): try: with open(USAGE_FILE, 'w', encoding='utf-8') as f: json.dump({}, f) except Exception as e: print(f"⚠️ [QUOTA] Failed to init usage file: {e}") # V261: Ensure persistent user list exists if not os.path.exists(ALL_USERS_FILE): try: with open(ALL_USERS_FILE, 'w', encoding='utf-8') as f: json.dump({}, f) print(f"📊 [QUOTA] Created {ALL_USERS_FILE}") except Exception as e: print(f"⚠️ [QUOTA] Failed to init all_users file: {e}") def _load_limits(self): try: with open(LIMITS_FILE, 'r', encoding='utf-8') as f: self.limits_config = json.load(f) except Exception as e: print(f"⚠️ [QUOTA] Failed to load limits: {e}") self.limits_config = {"default_limit": 30, "students": {}, "blocked_users": []} # V3.1.3: DEV Quota Reset if not IS_PRODUCTION: self.limits_config["default_limit"] = 1000 def _load_usage(self): try: with open(USAGE_FILE, 'r', encoding='utf-8') as f: return json.load(f) except Exception: return {} def _save_usage(self, usage_data): try: with open(USAGE_FILE, 'w', encoding='utf-8') as f: json.dump(usage_data, f, indent=2) except Exception as e: print(f"⚠️ [QUOTA] Failed to save usage: {e}") def _load_all_users(self): try: with open(ALL_USERS_FILE, 'r', encoding='utf-8') as f: return json.load(f) except: return {} def _save_all_users(self, data): try: with open(ALL_USERS_FILE, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2) except Exception as e: print(f"⚠️ [QUOTA] Failed to save all_users: {e}") def get_today_key(self): return datetime.date.today().isoformat() def track_user(self, student_name): """V261: Updates the last_seen for a user in persistent storage""" if not student_name: return try: with self.lock: users = self._load_all_users() users[student_name] = { "last_seen": datetime.datetime.now().isoformat(), "id": student_name } self._save_all_users(users) except Exception as e: print(f"⚠️ [QUOTA] Failed to track user: {e}") def check_limit(self, student_name): """ Returns: (allowed: bool, message: str, current_usage: int, limit: int) """ if not student_name: return True, "No name", 0, 9999 # V261: Track user on every check self.track_user(student_name) # Refresh limits config in case it changed manually self._load_limits() if student_name in self.limits_config.get("blocked_users", []): return False, "User is blocked", 0, 0 limit = self.limits_config.get("students", {}).get(student_name, self.limits_config.get("default_limit", 30)) # Admin override or high limit if limit < 0: return True, "Unlimited", 0, -1 today = self.get_today_key() with self.lock: usage_data = self._load_usage() today_usage = usage_data.get(today, {}) current_count = today_usage.get(student_name, 0) if current_count >= limit: return False, f"Daily limit reached ({limit})", current_count, limit return True, "Allowed", current_count, limit def increment_usage(self, student_name): if not student_name: return today = self.get_today_key() with self.lock: usage_data = self._load_usage() if today not in usage_data: usage_data[today] = {} usage_data[today][student_name] = usage_data[today].get(student_name, 0) + 1 self._save_usage(usage_data) print(f"📊 [QUOTA] Incremented for {student_name}. Date: {today}") # ================= ADMIN METHODS (V261) ================= def get_all_users_data(self): """Merges all_users (history), limits (config), and usage (today)""" all_users = self._load_all_users() # {id: {last_seen...}} limits = self.limits_config today_usage = self._load_usage().get(self.get_today_key(), {}) result = [] # Merge known users from history for uid, udata in all_users.items(): limit = limits.get("students", {}).get(uid, limits.get("default_limit", 30)) usage = today_usage.get(uid, 0) result.append({ "id": uid, "last_seen": udata.get("last_seen"), "limit": limit, "usage_today": usage, "is_blocked": uid in limits.get("blocked_users", []) }) # Also include users in limits config who might not be in history yet (?) # (Optional, skipping for now to keep it simple) # Sort by most recently seen result.sort(key=lambda x: x.get("last_seen", ""), reverse=True) return result def set_user_limit(self, user_id, new_limit): """Updates the limit for a specific user""" with self.lock: self._load_limits() if "students" not in self.limits_config: self.limits_config["students"] = {} try: limit_int = int(new_limit) self.limits_config["students"][user_id] = limit_int # Save back to file with open(LIMITS_FILE, 'w', encoding='utf-8') as f: json.dump(self.limits_config, f, indent=4) return True, "Updated" except Exception as e: return False, str(e) # Global instance quota_manager = QuotaManager()