File size: 7,389 Bytes
9d29c62 6daa01f 9d29c62 6daa01f 9d29c62 37bc59e 9d29c62 37bc59e a708c59 37bc59e a708c59 9d29c62 37bc59e 8c31f4f 9d29c62 a708c59 0c3327c a708c59 0c3327c a708c59 0c3327c 8c31f4f 0c3327c 5ae14ee a708c59 5ae14ee a708c59 8c31f4f 9d29c62 0c3327c 9d29c62 6daa01f a708c59 9d29c62 a708c59 9d29c62 6daa01f 37bc59e 6daa01f 37bc59e 6daa01f a3e4054 37bc59e a3e4054 6daa01f a32da26 6daa01f 9d29c62 42e9f8b 9d29c62 a708c59 37bc59e a708c59 9d29c62 42e9f8b 9d29c62 3091d31 9d29c62 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | import logging
import os
import firebase_admin
from firebase_admin import credentials, storage, firestore, auth
# Initialize logging
logger = logging.getLogger("BIT-LOG")
class FirebaseManager:
"""
V261.17: Manages Firebase Storage uploads for resilient audio playback.
Replaces local static file serving which causes timeouts and 404s.
"""
_instance = None
_bucket = None
_db = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(FirebaseManager, cls).__new__(cls)
# Initialization is now handled explicitly in main.py lifespan
return cls._instance
def initialize(self):
"""
V5.13.16: Explicit initialization for Firebase Admin SDK.
Guarantees initialize_app() runs once at startup.
"""
if firebase_admin._apps:
logger.info("ℹ️ [FIREBASE] SDK already initialized. Skipping.")
self._bucket = storage.bucket()
self._db = firestore.client()
return
try:
from config import STORAGE_BUCKET, IS_PRODUCTION, FIREBASE_CREDENTIALS_PATH
import json
logger.info("🛠️ [FIREBASE] Starting initialization...")
cred_dict = None
# Mission 1: Try loading from environment variables (checking multiple names for safety)
creds_str = os.environ.get("FIREBASE_CREDENTIALS") or os.environ.get("FIREBASE_CREDENTIALS_JSON")
if creds_str and len(creds_str.strip()) > 10:
try:
cred_dict = json.loads(creds_str)
logger.info("✅ [FIREBASE] Successfully parsed credentials from Environment Secrets!")
except Exception as e:
logger.error(f"❌ [FIREBASE] Failed to parse Environment Credentials: {e}")
# Fallback to local file only for local development (if no secret is set)
if not cred_dict and FIREBASE_CREDENTIALS_PATH:
if os.path.exists(FIREBASE_CREDENTIALS_PATH):
with open(FIREBASE_CREDENTIALS_PATH, "r", encoding="utf-8") as f:
cred_dict = json.load(f)
logger.info(f"📂 [FIREBASE] Loading credentials from file: {FIREBASE_CREDENTIALS_PATH}.")
else:
logger.warning(f"⚠️ [FIREBASE] Credentials file not found at {FIREBASE_CREDENTIALS_PATH}.")
if cred_dict:
cred = credentials.Certificate(cred_dict)
firebase_admin.initialize_app(cred, {
'storageBucket': STORAGE_BUCKET
})
logger.info(f"🚀 [FIREBASE] SDK Initialized successfully for {'PROD' if IS_PRODUCTION else 'DEV'}.")
else:
logger.error("❌ [FIREBASE] CRITICAL ERROR: Firebase credentials not found! Firebase is OFFLINE.")
self._bucket = storage.bucket()
self._db = firestore.client()
logger.info("✨ [FIREBASE] Storage and Firestore clients ready.")
except Exception as e:
logger.error(f"🔥 [FIREBASE] Initialization failed: {e}")
def get_db(self):
"""V2: Returns the initialized Firestore client."""
if not self._db:
# Fallback to lazy init if not explicitly initialized
self.initialize()
return self._db
def verify_token(self, id_token: str):
"""
V2: Verifies a Firebase Auth ID token.
Returns the decoded token dictionary (including 'uid') if valid, else None.
"""
if not firebase_admin._apps:
self.initialize()
try:
decoded_token = auth.verify_id_token(id_token)
return decoded_token
except Exception as e:
if "expired" in str(e).lower():
logger.warning("⚠️ [FIREBASE] Token expired.")
else:
logger.error(f"❌ [FIREBASE] Error verifying token: {e}")
return None
def upload_file(self, local_path: str, destination_blob_name: str) -> str:
"""
V273.1: Uploads local file and returns a Signed URL (CORS proof).
Signed URLs bypass public access restrictions and work reliably in production.
"""
if not self._bucket:
self.initialize()
if not self._bucket:
logger.error("❌ [FIREBASE] Not initialized. Cannot upload.")
return None
try:
blob = self._bucket.blob(destination_blob_name)
blob.upload_from_filename(local_path)
# V273.1: Generate Signed URL instead of public_url to fix CORS
import datetime
signed_url = blob.generate_signed_url(
version="v4",
expiration=datetime.timedelta(hours=24),
method="GET",
)
logger.info(f"UPLOAD: [FIREBASE] Signed URL generated for {destination_blob_name}")
return signed_url
except Exception as e:
logger.error(f"ERROR: [FIREBASE] Upload failed for {local_path}: {e}")
return None
def save_chat_message(self, uid: str, session_id: str, role: str, content: str, image_urls: list = None, metadata: dict = None):
"""
V318.0: Saves a message to the chat session history in Firestore.
Path: users/{uid}/chat_sessions/{session_id}/messages
"""
db = self.get_db()
if not db:
logger.error("❌ [FIREBASE] Firestore DB not available for saving chat.")
return
try:
messages_ref = db.collection('users').document(uid).collection('chat_sessions').document(session_id).collection('messages')
payload = {
"role": role,
"content": content,
"image_urls": image_urls or [],
"metadata": metadata or {},
"timestamp": firestore.SERVER_TIMESTAMP
}
messages_ref.add(payload)
logger.info(f"💾 [FIREBASE] Saved {role} message to session {session_id} for {uid}.")
except Exception as e:
logger.error(f"❌ [FIREBASE] Failed to save chat message: {e}")
def get_chat_history(self, uid: str, session_id: str, limit: int = 10):
"""
V318.0: Fetches the last N messages from a chat session, ordered by timestamp.
"""
db = self.get_db()
if not db:
return []
try:
messages_ref = db.collection('users').document(uid).collection('chat_sessions').document(session_id).collection('messages')
query = messages_ref.order_by("timestamp", direction=firestore.Query.DESCENDING).limit(limit)
# Note: Reversed order results (query is DESC, we want chronological ASC)
docs = query.get()
history = []
for doc in reversed(list(docs)):
history.append(doc.to_dict())
return history
except Exception as e:
logger.error(f"❌ [FIREBASE] Failed to fetch chat history: {e}")
return []
# Singleton accessor
firebase_manager = FirebaseManager()
|