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()