Spaces:
Runtime error
Runtime error
Download firebase_manager.py from usmanovrustam/skrapyx-api: direct link, hf CLI and curl.
- Browser
- Download file 15.2 kB
-
https://huggingface.co/spaces/usmanovrustam/skrapyx-api/resolve/d4822287c9a7a48bfeffaf6567121dd4b9468ab3/firebase_manager.py
- Command line
-
hf download hf://spaces/usmanovrustam/skrapyx-api@d4822287c9a7a48bfeffaf6567121dd4b9468ab3/firebase_manager.py
-
curl -L -o firebase_manager.py https://huggingface.co/spaces/usmanovrustam/skrapyx-api/resolve/d4822287c9a7a48bfeffaf6567121dd4b9468ab3/firebase_manager.py
15.2 kB
| import os | |
| import json | |
| import gzip | |
| from cryptography.fernet import Fernet | |
| import firebase_admin | |
| from firebase_admin import credentials, firestore, auth, storage | |
| from google.cloud.firestore_v1.base_query import FieldFilter | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| class FirebaseManager: | |
| _instance = None | |
| def __new__(cls): | |
| if cls._instance is None: | |
| cls._instance = super(FirebaseManager, cls).__new__(cls) | |
| cls._instance.initialized = False | |
| cls._instance.db = None | |
| cls._instance.bucket = None | |
| cls._instance._init_firebase() | |
| cls._instance._init_vault() | |
| return cls._instance | |
| def _init_vault(self): | |
| """Initialize encryption engine""" | |
| self.key = os.getenv("MASTER_ENCRYPTION_KEY") | |
| if self.key: | |
| try: | |
| self.cipher = Fernet(self.key.encode()) | |
| print("[Vault] Neural Encryption Engine: Online") | |
| except Exception as e: | |
| print(f"[Vault] Encryption Init Failed: {e}") | |
| self.cipher = None | |
| else: | |
| print("[Vault] WARNING: No Encryption Key found. Scans will be unencrypted.") | |
| self.cipher = None | |
| def _init_firebase(self): | |
| # 1. Try to load from environment variable (Production Best Practice) | |
| env_cred = os.getenv("FIREBASE_SERVICE_ACCOUNT") | |
| if env_cred: | |
| try: | |
| cred_dict = json.loads(env_cred) | |
| cred = credentials.Certificate(cred_dict) | |
| firebase_admin.initialize_app(cred, { | |
| 'storageBucket': os.getenv("FIREBASE_STORAGE_BUCKET", f"{cred.project_id}.appspot.com") | |
| }) | |
| self.db = firestore.client() | |
| self.bucket = storage.bucket() | |
| self.initialized = True | |
| print("[Firebase] Successfully initialized from Environment Variable") | |
| return | |
| except Exception as e: | |
| print(f"[Firebase] Env Init Failed: {e}") | |
| # 2. Fallback to Dynamic Key Detection (Local Development) | |
| search_paths = [ | |
| "config/store-screapper-firebase-adminsdk-fbsvc-5374dc9fc8.json", | |
| "data/serviceAccountKey.json" | |
| ] | |
| cred_path = None | |
| for p in search_paths: | |
| if os.path.exists(p): | |
| cred_path = p | |
| break | |
| if not cred_path and os.path.exists("config"): | |
| for f in os.listdir("config"): | |
| if f.endswith(".json") and "firebase-adminsdk" in f: | |
| cred_path = os.path.join("config", f) | |
| break | |
| if cred_path: | |
| try: | |
| cred = credentials.Certificate(cred_path) | |
| # Dynamic project discovery | |
| project_id = cred.project_id | |
| # Try appspot.com first, fallback to firebasestorage.app | |
| suffixes = [".appspot.com", ".firebasestorage.app"] | |
| bucket_found = False | |
| for suffix in suffixes: | |
| bucket_name = f"{project_id}{suffix}" | |
| try: | |
| if firebase_admin._apps: | |
| firebase_admin.delete_app(firebase_admin.get_app()) | |
| # Re-initialize with current candidate | |
| firebase_admin.initialize_app(cred, {'storageBucket': bucket_name}) | |
| test_bucket = storage.bucket() | |
| # CRITICAL: .exists() returns bool, must check it explicitly | |
| if test_bucket.exists(): | |
| self.db = firestore.client() | |
| self.bucket = test_bucket | |
| self.initialized = True | |
| bucket_found = True | |
| print(f"[Firebase] Connected to Storage Bucket: {bucket_name}") | |
| break | |
| else: | |
| print(f"[Firebase] Bucket {bucket_name} reported as non-existent.") | |
| except Exception as e: | |
| print(f"[Firebase] {bucket_name} initialization error: {e}") | |
| if not bucket_found: | |
| print("[Firebase] WARNING: No valid Storage Bucket found. Data will NOT be persisted to Cloud Storage.") | |
| # Still initialize Firestore if possible | |
| if not firebase_admin._apps: | |
| firebase_admin.initialize_app(cred) | |
| self.db = firestore.client() | |
| self.initialized = True | |
| self.bucket = None | |
| except Exception as e: | |
| print(f"[Firebase] Initialization fatal error: {e}. Local mode possible.") | |
| else: | |
| print("[Firebase] No service account key found in config/ or data/. Operating in Local Mode.") | |
| def get_projects(self, user_id): | |
| if not self.initialized: return {} | |
| # Personalized sub-collection access | |
| docs = self.db.collection("users").document(user_id).collection("projects").stream() | |
| return {doc.id: doc.to_dict() for doc in docs} | |
| def get_or_create_user(self, uid, email, name): | |
| if not self.initialized: return {"user_id": uid, "email": email, "credits": 1} | |
| doc_ref = self.db.collection('users').document(uid) | |
| doc = doc_ref.get() | |
| if doc.exists: | |
| profile = doc.to_dict() | |
| # Migration: Add username if missing | |
| if "username" not in profile: | |
| profile["username"] = email.split('@')[0] if email else "operator" | |
| doc_ref.set(profile, merge=True) | |
| return profile | |
| else: | |
| profile = { | |
| "user_id": uid, | |
| "email": email, | |
| "name": name, | |
| "username": email.split('@')[0] if email else "operator", | |
| "credits": 1, | |
| "is_pro": False, | |
| "subscription_expiry": None, | |
| "subscription_plan": None, | |
| "subscription_id": None, | |
| "created_at": firestore.SERVER_TIMESTAMP | |
| } | |
| doc_ref.set(profile) | |
| return profile | |
| def upload_scan(self, scan_id, data): | |
| """Uploads compressed & encrypted scan JSON to Firebase Storage""" | |
| if not self.initialized or not self.bucket: return False | |
| try: | |
| # 1. Serialize | |
| json_data = json.dumps(data).encode('utf-8') | |
| # 2. Compress (Lossless Gzip) | |
| compressed_data = gzip.compress(json_data, compresslevel=9) | |
| # 3. Encrypt (Optional but recommended) | |
| final_data = compressed_data | |
| content_type = 'application/x-gzip' | |
| if self.cipher: | |
| final_data = self.cipher.encrypt(compressed_data) | |
| content_type = 'application/octet-stream' | |
| blob = self.bucket.blob(f"scans/{scan_id}.bin") # Change extension to .bin for vault files | |
| blob.upload_from_string( | |
| data=final_data, | |
| content_type=content_type | |
| ) | |
| return True | |
| except Exception as e: | |
| print(f"[Firebase] Vault Upload failed: {e}") | |
| return False | |
| def download_scan(self, scan_id): | |
| """Downloads, decrypts and decompresses scan from Firebase Storage""" | |
| if not self.initialized or not self.bucket: return None | |
| try: | |
| # Try new .bin format first | |
| blob = self.bucket.blob(f"scans/{scan_id}.bin") | |
| if not blob.exists(): | |
| # Fallback to legacy .json format | |
| blob = self.bucket.blob(f"scans/{scan_id}.json") | |
| if not blob.exists(): return None | |
| print(f"[Vault] Legacy Scan Detected: {scan_id}") | |
| return json.loads(blob.download_as_text()) | |
| raw_bytes = blob.download_as_bytes() | |
| # 1. Decrypt if possible | |
| processed_data = raw_bytes | |
| if self.cipher: | |
| try: | |
| processed_data = self.cipher.decrypt(raw_bytes) | |
| except Exception: | |
| # If decryption fails, maybe it's just compressed or legacy | |
| pass | |
| # 2. Decompress | |
| try: | |
| decompressed = gzip.decompress(processed_data) | |
| return json.loads(decompressed.decode('utf-8')) | |
| except Exception: | |
| # Fallback for plain binary or failed decompression | |
| return json.loads(processed_data.decode('utf-8')) | |
| except Exception as e: | |
| print(f"[Firebase] Vault Download failed: {e}") | |
| return None | |
| def delete_scan(self, scan_id): | |
| """Deletes scan JSON from Firebase Storage""" | |
| if not self.initialized or not self.bucket: return False | |
| try: | |
| blob = self.bucket.blob(f"scans/{scan_id}.json") | |
| if blob.exists(): blob.delete() | |
| return True | |
| except Exception as e: | |
| print(f"[Firebase] Delete failed: {e}") | |
| return False | |
| def deduct_credit(self, user_id): | |
| if not self.initialized: return True | |
| user_ref = self.db.collection("users").document(user_id) | |
| def update_in_transaction(transaction, user_ref): | |
| snapshot = user_ref.get(transaction=transaction) | |
| if not snapshot.exists: return False | |
| # Skip deduction only for active Pro users | |
| if snapshot.get("is_pro") == True: | |
| return True | |
| credits = snapshot.get("credits") or 0 | |
| if credits > 0: | |
| transaction.update(user_ref, {"credits": credits - 1}) | |
| return True | |
| return False | |
| transaction = self.db.transaction() | |
| return update_in_transaction(transaction, user_ref) | |
| def add_credits(self, user_id, amount): | |
| """Atomically increment user credits in Firestore""" | |
| if not self.initialized: return False | |
| try: | |
| user_ref = self.db.collection("users").document(user_id) | |
| def update_in_transaction(transaction, user_ref): | |
| snapshot = user_ref.get(transaction=transaction) | |
| if not snapshot.exists: return False | |
| current = snapshot.get("credits") or 0 | |
| transaction.update(user_ref, {"credits": current + amount}) | |
| return True | |
| transaction = self.db.transaction() | |
| return update_in_transaction(transaction, user_ref) | |
| except Exception as e: | |
| print(f"[Firebase] Credit addition failed: {e}") | |
| return False | |
| def upgrade_to_pro(self, user_id, status=True, expiry_date=None, plan_type=None, subscription_id=None): | |
| """Enable Pro status for a user with subscription metadata""" | |
| if not self.initialized: return False | |
| try: | |
| user_ref = self.db.collection("users").document(user_id) | |
| update_data = { | |
| "is_pro": status, | |
| "credits": 99 if status else 0, | |
| "subscription_expiry": expiry_date, | |
| "subscription_plan": plan_type, | |
| "subscription_id": subscription_id | |
| } | |
| user_ref.update({k: v for k, v in update_data.items() if v is not None or not status}) | |
| return True | |
| except Exception as e: | |
| print(f"[Firebase] Pro upgrade failed: {e}") | |
| return False | |
| def enroll_in_metered(self, user_id, subscription_id=None): | |
| """Enroll user in the metered plan (pay-as-you-go) without granting bulk credits or pro status""" | |
| if not self.initialized: return False | |
| try: | |
| user_ref = self.db.collection("users").document(user_id) | |
| update_data = { | |
| "is_pro": False, | |
| "subscription_plan": "metered", | |
| "subscription_id": subscription_id | |
| } | |
| # We explicitly do NOT touch "credits" so they keep their 0 balance (triggering the $0.50 UI prompt) | |
| user_ref.update(update_data) | |
| return True | |
| except Exception as e: | |
| print(f"[Firebase] Metered enrollment failed: {e}") | |
| return False | |
| def save_project(self, user_id, project_id, data): | |
| if not self.initialized: return | |
| self.db.collection("users").document(user_id).collection("projects").document(project_id).set(data) | |
| def delete_project(self, user_id, project_id): | |
| if not self.initialized: return | |
| self.db.collection("users").document(user_id).collection("projects").document(project_id).delete() | |
| # Job Engine Integration (Personalized) | |
| def get_jobs(self, user_id): | |
| if not self.initialized: return {} | |
| docs = self.db.collection("users").document(user_id).collection("jobs").stream() | |
| return {doc.id: doc.to_dict() for doc in docs} | |
| def update_job(self, user_id, job_id, data): | |
| if not self.initialized: return | |
| self.db.collection("users").document(user_id).collection("jobs").document(job_id).set(data, merge=True) | |
| # Transaction Ledger | |
| def record_transaction(self, user_id, session_id, data): | |
| """Store transaction record in user sub-collection""" | |
| if not self.initialized: return False | |
| try: | |
| tx_ref = self.db.collection("users").document(user_id).collection("transactions").document(session_id) | |
| tx_ref.set(data) | |
| return True | |
| except Exception as e: | |
| print(f"[Firebase] Transaction record failed: {e}") | |
| return False | |
| def save_purchase(self, user_id, session_id, data): | |
| """Store minimal purchase marker for idempotency in user sub-collection""" | |
| if not self.initialized: return False | |
| try: | |
| p_ref = self.db.collection("users").document(user_id).collection("purchases").document(session_id) | |
| p_ref.set(data) | |
| return True | |
| except Exception as e: | |
| print(f"[Firebase] Purchase save failed: {e}") | |
| return False | |
| def get_purchase(self, user_id, session_id): | |
| """Check for existing purchase marker""" | |
| if not self.initialized: return None | |
| doc = self.db.collection("users").document(user_id).collection("purchases").document(session_id).get() | |
| return doc.to_dict() if doc.exists else None | |
| def get_transactions(self, user_id): | |
| """Fetch transaction history for user""" | |
| if not self.initialized: return [] | |
| docs = self.db.collection("users").document(user_id).collection("transactions").order_by("created_at", direction=firestore.Query.DESCENDING).stream() | |
| return [doc.to_dict() for doc in docs] | |
| firebase_manager = FirebaseManager() | |