| import os |
| import json |
| import logging |
| import uuid |
| from contextlib import contextmanager |
| from datetime import datetime, timedelta |
| from typing import Optional, Dict, Any, List |
|
|
| import psycopg2 |
| import psycopg2.extras |
| from psycopg2.pool import ThreadedConnectionPool |
| from supabase import create_client, Client |
|
|
| from config import settings |
|
|
| logger = logging.getLogger(__name__) |
|
|
| |
| |
| try: |
| from supabase._sync.client import SyncClient |
| |
| def safe_listen_to_auth_events(self, event, session): |
| access_token = self.supabase_key |
| if event in ["SIGNED_IN", "TOKEN_REFRESHED", "SIGNED_OUT"]: |
| self._postgrest = None |
| self._storage = None |
| self._functions = None |
| access_token = session.access_token if session else self.supabase_key |
|
|
| self.options.headers["Authorization"] = self._create_auth_header(access_token) |
|
|
| |
| if self.realtime is not None: |
| self.realtime.set_auth(access_token) |
|
|
| SyncClient._listen_to_auth_events = safe_listen_to_auth_events |
| logger.info("Successfully patched Supabase SyncClient._listen_to_auth_events for safety") |
| except Exception as e: |
| logger.error(f"Failed to patch Supabase SyncClient: {e}") |
|
|
|
|
| class Database: |
| _instance: Optional["Database"] = None |
| _pool: Optional[ThreadedConnectionPool] = None |
| _tables_ensured: bool = False |
| _ensuring_tables: bool = False |
|
|
| def __new__(cls): |
| if cls._instance is None: |
| cls._instance = super().__new__(cls) |
| return cls._instance |
|
|
| _COLUMN_ALLOWLISTS: Dict[str, frozenset] = { |
| "profiles": frozenset({"id", "email", "full_name", "avatar_url", "preferred_bible_version", |
| "notification_preferences", "spiritual_journey_notes", |
| "aria_custom_prompt", "aria_personal_context", "aria_voice", |
| "created_at", "updated_at"}), |
| "bible_study_sessions": frozenset({"id", "user_id", "book", "chapter", "verses", |
| "selected_text", "created_at", "updated_at"}), |
| "bible_study_messages": frozenset({"id", "session_id", "role", "content", "created_at"}), |
| "emotional_support_sessions": frozenset({"id", "user_id", "mood", "provided_scriptures", |
| "created_at", "updated_at"}), |
| "emotional_support_messages": frozenset({"id", "session_id", "role", "content", "created_at"}), |
| "devotion_settings": frozenset({"id", "user_id", "topics", "preferred_time", "frequency", |
| "created_at", "updated_at"}), |
| "devotions": frozenset({"id", "user_id", "title", "day_plan_summary", "scripture_reading", |
| "reflection", "prayer", "status", "scheduled_date", |
| "created_at", "updated_at"}), |
| "devotion_messages": frozenset({"id", "devotion_id", "role", "content", "created_at"}), |
| "notes": frozenset({"id", "user_id", "title", "content", "source_type", "source_reference", |
| "tags", "is_locked", "password_hash", "created_at", "updated_at"}), |
| "prayers": frozenset({"id", "user_id", "title", "content", "is_answered", "created_at", |
| "updated_at"}), |
| "ai_chat_sessions": frozenset({"id", "user_id", "title", "created_at", "updated_at"}), |
| "ai_chat_messages": frozenset({"id", "session_id", "role", "content", "created_at"}), |
| } |
|
|
| def _validate_columns(self, table: str, keys) -> None: |
| allowed = self._COLUMN_ALLOWLISTS.get(table, frozenset()) |
| bad = frozenset(keys) - allowed |
| if bad: |
| raise ValueError(f"Disallowed column(s) for table '{table}': {bad}") |
|
|
| _client: Optional[Client] = None |
|
|
| def __init__(self): |
| if hasattr(self, '_initialized'): |
| return |
| self._initialized = True |
| |
| |
| if settings.supabase_url and settings.supabase_key: |
| try: |
| self.__class__._client = create_client(settings.supabase_url, settings.supabase_key) |
| logger.info("Supabase client initialized with anon key") |
| except Exception as e: |
| logger.error(f"⚠️ Failed to initialize Supabase client (possibly invalid key): {e}") |
|
|
| if not self.__class__._pool: |
| try: |
| self.__class__._pool = ThreadedConnectionPool( |
| minconn=1, |
| maxconn=10, |
| dsn=settings.database_url, |
| ) |
| |
| self._ensure_tables() |
| except Exception as e: |
| logger.error(f"⚠️ Failed to initialize database connection pool on startup: {e}") |
| logger.warning("Database connection will be retried lazily during request handling.") |
|
|
| @property |
| def client(self) -> Client: |
| if not self.__class__._client: |
| raise RuntimeError("Supabase client is not initialized. Please check your SUPABASE_URL and SUPABASE_KEY configuration.") |
| return self.__class__._client |
|
|
|
|
| @contextmanager |
| def get_connection(self): |
| if not self.__class__._pool: |
| logger.info("Attempting lazy database pool initialization...") |
| try: |
| self.__class__._pool = ThreadedConnectionPool( |
| minconn=1, |
| maxconn=10, |
| dsn=settings.database_url, |
| ) |
| except Exception as e: |
| logger.error(f"Lazy pool initialization failed: {e}") |
| raise RuntimeError(f"Database connection pool not initialized: {e}") |
|
|
| if not self.__class__._tables_ensured: |
| self._ensure_tables() |
|
|
| conn = self._pool.getconn() |
| close_conn = False |
| try: |
| yield conn |
| conn.commit() |
| except (psycopg2.OperationalError, psycopg2.InterfaceError) as e: |
| logger.warning(f"Database connection error: {e}. Discarding connection from pool.") |
| close_conn = True |
| raise |
| except Exception: |
| try: |
| if conn and not conn.closed: |
| conn.rollback() |
| except Exception as rollback_err: |
| logger.warning(f"Failed to rollback connection: {rollback_err}") |
| close_conn = True |
| raise |
| finally: |
| try: |
| self._pool.putconn(conn, close=close_conn) |
| except Exception as put_err: |
| logger.error(f"Error returning connection to pool: {put_err}") |
|
|
| def _ensure_tables(self): |
| if self.__class__._tables_ensured or self.__class__._ensuring_tables: |
| return |
| |
| self.__class__._ensuring_tables = True |
| try: |
| |
| id_type = None |
| try: |
| with self.get_connection() as conn: |
| cur = conn.cursor() |
| cur.execute(""" |
| SELECT data_type |
| FROM information_schema.columns |
| WHERE table_schema = 'public' AND table_name = 'profiles' AND column_name = 'id' |
| UNION ALL |
| SELECT data_type |
| FROM information_schema.columns |
| WHERE table_schema = 'public' AND table_name = 'users' AND column_name = 'id' |
| """) |
| rows = cur.fetchall() |
| for r in rows: |
| if r and r[0]: |
| val = r[0].upper() |
| id_type = "UUID" if "UUID" in val else "TEXT" |
| logger.info(f"Detected users/profiles id type from database: {id_type}") |
| break |
| except Exception as e: |
| logger.warning(f"Failed to auto-detect users/profiles id type: {e}") |
|
|
| if not id_type: |
| is_supabase_db = "supabase.co" in settings.database_url or "supabase.com" in settings.database_url |
| id_type = "UUID" if is_supabase_db else "TEXT" |
| logger.info(f"Fallback id type determined: {id_type}") |
| |
| with self.get_connection() as conn: |
| cur = conn.cursor() |
|
|
| cur.execute(f""" |
| CREATE TABLE IF NOT EXISTS users ( |
| id {id_type} PRIMARY KEY, |
| email TEXT NOT NULL UNIQUE, |
| hashed_password TEXT NOT NULL, |
| created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() |
| ) |
| """) |
|
|
| cur.execute(f""" |
| CREATE TABLE IF NOT EXISTS profiles ( |
| id {id_type} PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, |
| email TEXT NOT NULL UNIQUE, |
| full_name TEXT, |
| avatar_url TEXT, |
| preferred_bible_version TEXT DEFAULT 'NIV', |
| notification_preferences TEXT DEFAULT '{{\"email\": true, \"push\": true}}', |
| spiritual_journey_notes TEXT, |
| aria_custom_prompt TEXT, |
| aria_personal_context TEXT, |
| aria_voice TEXT DEFAULT 'verse', |
| created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), |
| updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() |
| ) |
| """) |
|
|
| cur.execute(f""" |
| CREATE TABLE IF NOT EXISTS prayers ( |
| id TEXT PRIMARY KEY, |
| user_id {id_type} NOT NULL, |
| title TEXT, |
| content TEXT NOT NULL, |
| isanswered BOOLEAN DEFAULT FALSE, |
| created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() |
| ) |
| """) |
|
|
| cur.execute(f""" |
| CREATE TABLE IF NOT EXISTS bible_study_sessions ( |
| id {id_type} PRIMARY KEY, |
| user_id {id_type} NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, |
| book TEXT NOT NULL, |
| chapter INTEGER NOT NULL, |
| verses TEXT NOT NULL, |
| selected_text TEXT NOT NULL, |
| is_realtime BOOLEAN DEFAULT FALSE, |
| ai_explanation TEXT, |
| ai_context TEXT, |
| conversation_summary TEXT, |
| created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), |
| updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() |
| ) |
| """) |
|
|
| cur.execute(f""" |
| CREATE TABLE IF NOT EXISTS bible_study_messages ( |
| id {id_type} PRIMARY KEY, |
| session_id {id_type} NOT NULL REFERENCES bible_study_sessions(id) ON DELETE CASCADE, |
| role TEXT NOT NULL CHECK (role IN ('user', 'assistant')), |
| content TEXT NOT NULL, |
| created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() |
| ) |
| """) |
|
|
| cur.execute(f""" |
| CREATE TABLE IF NOT EXISTS emotional_support_sessions ( |
| id {id_type} PRIMARY KEY, |
| user_id {id_type} NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, |
| mood TEXT NOT NULL, |
| situation_description TEXT, |
| is_realtime BOOLEAN DEFAULT FALSE, |
| ai_response TEXT, |
| provided_scriptures TEXT, |
| prayer_suggestion TEXT, |
| created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), |
| updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() |
| ) |
| """) |
|
|
| cur.execute(f""" |
| CREATE TABLE IF NOT EXISTS emotional_support_messages ( |
| id {id_type} PRIMARY KEY, |
| session_id {id_type} NOT NULL REFERENCES emotional_support_sessions(id) ON DELETE CASCADE, |
| role TEXT NOT NULL CHECK (role IN ('user', 'assistant')), |
| content TEXT NOT NULL, |
| created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() |
| ) |
| """) |
|
|
| cur.execute(f""" |
| CREATE TABLE IF NOT EXISTS devotion_settings ( |
| user_id {id_type} PRIMARY KEY REFERENCES profiles(id) ON DELETE CASCADE, |
| preferred_time TEXT NOT NULL, |
| timezone TEXT NOT NULL, |
| duration_minutes INTEGER DEFAULT 15, |
| topics TEXT DEFAULT '[]', |
| auto_prayer BOOLEAN DEFAULT TRUE, |
| created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), |
| updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() |
| ) |
| """) |
|
|
| cur.execute(f""" |
| CREATE TABLE IF NOT EXISTS devotions ( |
| id {id_type} PRIMARY KEY, |
| user_id {id_type} NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, |
| scheduled_for TIMESTAMP WITH TIME ZONE NOT NULL, |
| day_plan_summary TEXT, |
| scripture_reading TEXT, |
| reflection_prompt TEXT, |
| user_reflection TEXT, |
| status TEXT DEFAULT 'scheduled' CHECK (status IN ('scheduled', 'completed', 'skipped')), |
| completed_at TIMESTAMP WITH TIME ZONE, |
| ai_prayer TEXT, |
| created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() |
| ) |
| """) |
|
|
| cur.execute(""" |
| CREATE TABLE IF NOT EXISTS bible_verses ( |
| id SERIAL PRIMARY KEY, |
| book TEXT NOT NULL, |
| chapter INTEGER NOT NULL, |
| verse INTEGER NOT NULL, |
| text TEXT NOT NULL, |
| version TEXT DEFAULT 'NIV', |
| UNIQUE (book, chapter, verse, version) |
| ) |
| """) |
|
|
| cur.execute(""" |
| CREATE TABLE IF NOT EXISTS scripture_references ( |
| id SERIAL PRIMARY KEY, |
| verse_id INTEGER NOT NULL REFERENCES bible_verses(id) ON DELETE CASCADE, |
| category TEXT NOT NULL, |
| tags TEXT NOT NULL, |
| context_description TEXT, |
| created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() |
| ) |
| """) |
|
|
| cur.execute(f""" |
| CREATE TABLE IF NOT EXISTS user_favorites ( |
| id TEXT PRIMARY KEY, |
| user_id {id_type} NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, |
| item_type TEXT NOT NULL CHECK (item_type IN ('verse', 'prayer', 'devotion')), |
| item_id TEXT NOT NULL, |
| notes TEXT, |
| created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), |
| UNIQUE (user_id, item_type, item_id) |
| ) |
| """) |
|
|
| cur.execute(f""" |
| CREATE TABLE IF NOT EXISTS journal_entries ( |
| id TEXT PRIMARY KEY, |
| user_id {id_type} NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, |
| title TEXT, |
| content TEXT NOT NULL, |
| mood TEXT, |
| related_scriptures TEXT, |
| created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), |
| updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() |
| ) |
| """) |
|
|
| cur.execute(f""" |
| CREATE TABLE IF NOT EXISTS cached_verses ( |
| id SERIAL PRIMARY KEY, |
| user_id {id_type} NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, |
| verse_text TEXT NOT NULL, |
| verse_reference TEXT NOT NULL, |
| aria_insight TEXT, |
| daily_manna TEXT, |
| cached_date TEXT NOT NULL |
| ) |
| """) |
|
|
| cur.execute(f""" |
| CREATE TABLE IF NOT EXISTS notes ( |
| id TEXT PRIMARY KEY, |
| user_id {id_type} NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, |
| title TEXT, |
| content TEXT NOT NULL, |
| source_type TEXT DEFAULT 'general', |
| source_reference TEXT, |
| tags TEXT DEFAULT '[]', |
| is_locked BOOLEAN DEFAULT FALSE, |
| password_hash TEXT, |
| created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), |
| updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() |
| ) |
| """) |
|
|
| cur.execute(f""" |
| CREATE TABLE IF NOT EXISTS devotion_messages ( |
| id {id_type} PRIMARY KEY, |
| devotion_id {id_type} NOT NULL REFERENCES devotions(id) ON DELETE CASCADE, |
| role TEXT NOT NULL CHECK (role IN ('user', 'assistant')), |
| content TEXT NOT NULL, |
| created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() |
| ) |
| """) |
|
|
| cur.execute(f""" |
| CREATE TABLE IF NOT EXISTS ai_chat_sessions ( |
| id {id_type} PRIMARY KEY, |
| user_id {id_type} NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, |
| title TEXT, |
| created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), |
| updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() |
| ) |
| """) |
|
|
| cur.execute(f""" |
| CREATE TABLE IF NOT EXISTS ai_chat_messages ( |
| id {id_type} PRIMARY KEY, |
| session_id {id_type} NOT NULL REFERENCES ai_chat_sessions(id) ON DELETE CASCADE, |
| role TEXT NOT NULL CHECK (role IN ('user', 'assistant')), |
| content TEXT NOT NULL, |
| created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() |
| ) |
| """) |
|
|
| cur.execute(""" |
| CREATE TABLE IF NOT EXISTS revoked_tokens ( |
| jti TEXT PRIMARY KEY, |
| revoked_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), |
| expires_at TIMESTAMP WITH TIME ZONE NOT NULL |
| ) |
| """) |
| cur.execute(f""" |
| CREATE TABLE IF NOT EXISTS refresh_tokens ( |
| token TEXT PRIMARY KEY, |
| user_id {id_type} NOT NULL, |
| expires_at TIMESTAMP WITH TIME ZONE NOT NULL, |
| revoked BOOLEAN DEFAULT FALSE, |
| created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() |
| ) |
| """) |
|
|
| |
| cur.execute("CREATE INDEX IF NOT EXISTS idx_bible_study_sessions_user_id ON bible_study_sessions (user_id)") |
| cur.execute("CREATE INDEX IF NOT EXISTS idx_bible_study_messages_session_id ON bible_study_messages (session_id)") |
| cur.execute("CREATE INDEX IF NOT EXISTS idx_emotional_support_sessions_user_id ON emotional_support_sessions (user_id)") |
| cur.execute("CREATE INDEX IF NOT EXISTS idx_devotions_user_id ON devotions (user_id)") |
| cur.execute("CREATE INDEX IF NOT EXISTS idx_devotion_messages_devotion_id ON devotion_messages (devotion_id)") |
| self.__class__._tables_ensured = True |
| except Exception: |
| logger.exception("Error ensuring tables") |
| finally: |
| self.__class__._ensuring_tables = False |
|
|
| def _cursor(self, conn): |
| return conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) |
|
|
| def _parse_json_fields(self, d: Dict) -> None: |
| for field in ('notification_preferences', 'provided_scriptures', 'scripture_reading', |
| 'related_scriptures', 'topics', 'tags'): |
| if field in d and isinstance(d[field], str): |
| try: |
| d[field] = json.loads(d[field]) |
| except json.JSONDecodeError: |
| pass |
|
|
| def _parse_verses_field(self, d: Dict) -> None: |
| val = d.get('verses') |
| if isinstance(val, str) and val.strip(): |
| try: |
| d['verses'] = [int(v.strip()) for v in val.split(',') if v.strip()] |
| except (ValueError, TypeError): |
| d['verses'] = [] |
| elif not isinstance(val, list): |
| if val is not None: |
| d['verses'] = [] |
|
|
| def to_dict(self, row) -> Optional[Dict]: |
| if row is None: |
| return None |
| d = dict(row) |
| self._parse_json_fields(d) |
| self._parse_verses_field(d) |
| return d |
|
|
| |
|
|
| def get_user_by_email(self, email: str) -> Optional[Dict[str, Any]]: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("SELECT * FROM users WHERE email = %s", (email,)) |
| return self.to_dict(cur.fetchone()) |
|
|
| def create_user(self, user_id: str, email: str, hashed_password: str) -> bool: |
| try: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute( |
| "INSERT INTO users (id, email, hashed_password) VALUES (%s, %s, %s) ON CONFLICT (id) DO NOTHING", |
| (user_id, email, hashed_password), |
| ) |
| return True |
| except Exception: |
| logger.exception("Error creating user") |
| return False |
|
|
| def revoke_token(self, jti: str, expires_at: str) -> None: |
| try: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute( |
| "INSERT INTO revoked_tokens (jti, expires_at) VALUES (%s, %s) ON CONFLICT DO NOTHING", |
| (jti, expires_at), |
| ) |
| except Exception: |
| logger.exception("Error revoking token") |
|
|
| def is_token_revoked(self, jti: str) -> bool: |
| try: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("SELECT 1 FROM revoked_tokens WHERE jti = %s", (jti,)) |
| return cur.fetchone() is not None |
| except Exception: |
| logger.exception("Error checking token revocation") |
| return False |
|
|
| def store_refresh_token(self, token: str, user_id: str, expires_at: str) -> None: |
| try: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute( |
| "INSERT INTO refresh_tokens (token, user_id, expires_at) VALUES (%s, %s, %s) ON CONFLICT DO NOTHING", |
| (token, user_id, expires_at), |
| ) |
| except Exception: |
| logger.exception("Error storing refresh token") |
|
|
| def get_refresh_token(self, token: str) -> Optional[Dict[str, Any]]: |
| try: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute( |
| "SELECT * FROM refresh_tokens WHERE token = %s AND revoked = FALSE AND expires_at > NOW()", |
| (token,), |
| ) |
| row = cur.fetchone() |
| return dict(row) if row else None |
| except Exception: |
| logger.exception("Error getting refresh token") |
| return None |
|
|
| def revoke_refresh_token(self, token: str) -> None: |
| try: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("UPDATE refresh_tokens SET revoked = TRUE WHERE token = %s", (token,)) |
| except Exception: |
| logger.exception("Error revoking refresh token") |
|
|
| |
|
|
| def get_profile(self, user_id: str) -> Optional[Dict[str, Any]]: |
| try: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("SELECT * FROM profiles WHERE id = %s", (user_id,)) |
| return self.to_dict(cur.fetchone()) |
| except Exception: |
| logger.exception("Error getting profile") |
| return None |
|
|
| def create_profile(self, profile_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: |
| try: |
| data = profile_data.copy() |
| if 'notification_preferences' in data and not isinstance(data['notification_preferences'], str): |
| data['notification_preferences'] = json.dumps(data['notification_preferences']) |
| self._validate_columns("profiles", data.keys()) |
| cols = ', '.join(data.keys()) |
| placeholders = ', '.join(['%s'] * len(data)) |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute(f"INSERT INTO profiles ({cols}) VALUES ({placeholders})", list(data.values())) |
| return self.get_profile(data['id']) |
| except Exception: |
| logger.exception("Error creating profile") |
| return None |
|
|
| def update_profile(self, user_id: str, profile_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: |
| try: |
| data = profile_data.copy() |
| if 'notification_preferences' in data and not isinstance(data['notification_preferences'], str): |
| data['notification_preferences'] = json.dumps(data['notification_preferences']) |
| self._validate_columns("profiles", data.keys()) |
| set_clause = ', '.join([f"{k} = %s" for k in data.keys()]) |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute( |
| f"UPDATE profiles SET {set_clause}, updated_at = NOW() WHERE id = %s", |
| list(data.values()) + [user_id], |
| ) |
| return self.get_profile(user_id) |
| except Exception: |
| logger.exception("Error updating profile") |
| return None |
|
|
| |
|
|
| def create_bible_study_session(self, session_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: |
| try: |
| data = session_data.copy() |
| if 'id' not in data: |
| data['id'] = str(uuid.uuid4()) |
| if isinstance(data.get('verses'), list): |
| data['verses'] = ','.join(map(str, data['verses'])) |
| self._validate_columns("bible_study_sessions", data.keys()) |
| cols = ', '.join(data.keys()) |
| placeholders = ', '.join(['%s'] * len(data)) |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute(f"INSERT INTO bible_study_sessions ({cols}) VALUES ({placeholders})", list(data.values())) |
| return self.get_bible_study_session(data['id']) |
| except Exception: |
| logger.exception("Error creating bible study session") |
| return None |
|
|
| def get_bible_study_sessions(self, user_id: str) -> List[Dict[str, Any]]: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("SELECT * FROM bible_study_sessions WHERE user_id = %s ORDER BY created_at DESC", (user_id,)) |
| return [self.to_dict(r) for r in cur.fetchall()] |
|
|
| def get_bible_study_session(self, session_id: str) -> Optional[Dict[str, Any]]: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("SELECT * FROM bible_study_sessions WHERE id = %s", (session_id,)) |
| return self.to_dict(cur.fetchone()) |
|
|
| def update_bible_study_session(self, session_id: str, session_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: |
| try: |
| data = session_data.copy() |
| self._validate_columns("bible_study_sessions", data.keys()) |
| set_clause = ', '.join([f"{k} = %s" for k in data.keys()]) |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute( |
| f"UPDATE bible_study_sessions SET {set_clause}, updated_at = NOW() WHERE id = %s", |
| list(data.values()) + [session_id], |
| ) |
| return self.get_bible_study_session(session_id) |
| except Exception: |
| logger.exception("Error updating session") |
| return None |
|
|
| def create_bible_study_message(self, message_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: |
| try: |
| data = message_data.copy() |
| if 'id' not in data: |
| data['id'] = str(uuid.uuid4()) |
| self._validate_columns("bible_study_messages", data.keys()) |
| cols = ', '.join(data.keys()) |
| placeholders = ', '.join(['%s'] * len(data)) |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute(f"INSERT INTO bible_study_messages ({cols}) VALUES ({placeholders})", list(data.values())) |
| cur.execute("SELECT * FROM bible_study_messages WHERE id = %s", (data['id'],)) |
| return self.to_dict(cur.fetchone()) |
| except Exception: |
| logger.exception("Error creating message") |
| return None |
|
|
| def get_bible_study_messages(self, session_id: str) -> List[Dict[str, Any]]: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("SELECT * FROM bible_study_messages WHERE session_id = %s ORDER BY created_at ASC", (session_id,)) |
| return [self.to_dict(r) for r in cur.fetchall()] |
|
|
| |
|
|
| def create_emotional_support_session(self, session_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: |
| try: |
| data = session_data.copy() |
| if 'id' not in data: |
| data['id'] = str(uuid.uuid4()) |
| if 'provided_scriptures' in data and not isinstance(data['provided_scriptures'], str): |
| data['provided_scriptures'] = json.dumps(data['provided_scriptures']) |
| self._validate_columns("emotional_support_sessions", data.keys()) |
| cols = ', '.join(data.keys()) |
| placeholders = ', '.join(['%s'] * len(data)) |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute(f"INSERT INTO emotional_support_sessions ({cols}) VALUES ({placeholders})", list(data.values())) |
| return self.get_emotional_support_session(data['id']) |
| except Exception: |
| logger.exception("Error creating emotional session") |
| return None |
|
|
| def get_emotional_support_sessions(self, user_id: str) -> List[Dict[str, Any]]: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("SELECT * FROM emotional_support_sessions WHERE user_id = %s ORDER BY created_at DESC", (user_id,)) |
| return [self.to_dict(r) for r in cur.fetchall()] |
|
|
| def get_emotional_support_session(self, session_id: str) -> Optional[Dict[str, Any]]: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("SELECT * FROM emotional_support_sessions WHERE id = %s", (session_id,)) |
| return self.to_dict(cur.fetchone()) |
|
|
| def update_emotional_support_session(self, session_id: str, session_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: |
| try: |
| data = session_data.copy() |
| if 'provided_scriptures' in data and not isinstance(data['provided_scriptures'], str): |
| data['provided_scriptures'] = json.dumps(data['provided_scriptures']) |
| self._validate_columns("emotional_support_sessions", data.keys()) |
| set_clause = ', '.join([f"{k} = %s" for k in data.keys()]) |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute( |
| f"UPDATE emotional_support_sessions SET {set_clause}, updated_at = NOW() WHERE id = %s", |
| list(data.values()) + [session_id], |
| ) |
| return self.get_emotional_support_session(session_id) |
| except Exception: |
| logger.exception("Error updating emotional session") |
| return None |
|
|
| def create_emotional_support_message(self, message_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: |
| try: |
| data = message_data.copy() |
| if 'id' not in data: |
| data['id'] = str(uuid.uuid4()) |
| self._validate_columns("emotional_support_messages", data.keys()) |
| cols = ', '.join(data.keys()) |
| placeholders = ', '.join(['%s'] * len(data)) |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute(f"INSERT INTO emotional_support_messages ({cols}) VALUES ({placeholders})", list(data.values())) |
| cur.execute("SELECT * FROM emotional_support_messages WHERE id = %s", (data['id'],)) |
| return self.to_dict(cur.fetchone()) |
| except Exception: |
| logger.exception("Error creating support message") |
| return None |
|
|
| def get_emotional_support_messages(self, session_id: str) -> List[Dict[str, Any]]: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("SELECT * FROM emotional_support_messages WHERE session_id = %s ORDER BY created_at ASC", (session_id,)) |
| return [self.to_dict(r) for r in cur.fetchall()] |
|
|
| |
|
|
| def get_devotion_settings(self, user_id: str) -> Optional[Dict[str, Any]]: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("SELECT * FROM devotion_settings WHERE user_id = %s", (user_id,)) |
| return self.to_dict(cur.fetchone()) |
|
|
| def create_devotion_settings(self, settings_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: |
| try: |
| data = settings_data.copy() |
| if 'topics' in data and not isinstance(data['topics'], str): |
| data['topics'] = json.dumps(data['topics']) |
| self._validate_columns("devotion_settings", data.keys()) |
| cols = ', '.join(data.keys()) |
| placeholders = ', '.join(['%s'] * len(data)) |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute(f"INSERT INTO devotion_settings ({cols}) VALUES ({placeholders})", list(data.values())) |
| return self.get_devotion_settings(data['user_id']) |
| except Exception: |
| logger.exception("Error creating devotion settings") |
| return None |
|
|
| def update_devotion_settings(self, user_id: str, settings_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: |
| try: |
| data = settings_data.copy() |
| if 'topics' in data and not isinstance(data['topics'], str): |
| data['topics'] = json.dumps(data['topics']) |
| self._validate_columns("devotion_settings", data.keys()) |
| set_clause = ', '.join([f"{k} = %s" for k in data.keys()]) |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute( |
| f"UPDATE devotion_settings SET {set_clause}, updated_at = NOW() WHERE user_id = %s", |
| list(data.values()) + [user_id], |
| ) |
| return self.get_devotion_settings(user_id) |
| except Exception: |
| logger.exception("Error updating devotion settings") |
| return None |
|
|
| def create_devotion(self, devotion_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: |
| try: |
| data = devotion_data.copy() |
| if 'id' not in data: |
| data['id'] = str(uuid.uuid4()) |
| if 'scripture_reading' in data and not isinstance(data['scripture_reading'], str): |
| data['scripture_reading'] = json.dumps(data['scripture_reading']) |
| self._validate_columns("devotions", data.keys()) |
| cols = ', '.join(data.keys()) |
| placeholders = ', '.join(['%s'] * len(data)) |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute(f"INSERT INTO devotions ({cols}) VALUES ({placeholders})", list(data.values())) |
| cur.execute("SELECT * FROM devotions WHERE id = %s", (data['id'],)) |
| return self.to_dict(cur.fetchone()) |
| except Exception: |
| logger.exception("Error creating devotion") |
| return None |
|
|
| def get_devotions(self, user_id: str) -> List[Dict[str, Any]]: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("SELECT * FROM devotions WHERE user_id = %s ORDER BY created_at DESC", (user_id,)) |
| return [self.to_dict(r) for r in cur.fetchall()] |
|
|
| def update_devotion(self, devotion_id: str, devotion_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: |
| try: |
| data = devotion_data.copy() |
| if 'scripture_reading' in data and not isinstance(data['scripture_reading'], str): |
| data['scripture_reading'] = json.dumps(data['scripture_reading']) |
| self._validate_columns("devotions", data.keys()) |
| set_clause = ', '.join([f"{k} = %s" for k in data.keys()]) |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute( |
| f"UPDATE devotions SET {set_clause} WHERE id = %s", |
| list(data.values()) + [devotion_id], |
| ) |
| cur.execute("SELECT * FROM devotions WHERE id = %s", (devotion_id,)) |
| return self.to_dict(cur.fetchone()) |
| except Exception: |
| logger.exception("Error updating devotion") |
| return None |
|
|
| def create_devotion_message(self, message_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: |
| try: |
| data = message_data.copy() |
| if 'id' not in data: |
| data['id'] = str(uuid.uuid4()) |
| self._validate_columns("devotion_messages", data.keys()) |
| cols = ', '.join(data.keys()) |
| placeholders = ', '.join(['%s'] * len(data)) |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute(f"INSERT INTO devotion_messages ({cols}) VALUES ({placeholders})", list(data.values())) |
| cur.execute("SELECT * FROM devotion_messages WHERE id = %s", (data['id'],)) |
| return self.to_dict(cur.fetchone()) |
| except Exception: |
| logger.exception("Error creating devotion message") |
| return None |
|
|
| def get_devotion_messages(self, devotion_id: str) -> List[Dict[str, Any]]: |
| try: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("SELECT * FROM devotion_messages WHERE devotion_id = %s ORDER BY created_at ASC", (devotion_id,)) |
| return [self.to_dict(r) for r in cur.fetchall()] |
| except Exception: |
| logger.exception("Error getting devotion messages") |
| return [] |
|
|
| |
|
|
| async def fetch_bible_chapter_from_api(self, book: str, chapter: int, version: str = "KJV") -> List[Dict[str, Any]]: |
| |
| |
| local_verses = self.get_bible_chapter_local(book, chapter, version) |
| if local_verses: |
| logger.info(f"Loaded {book} {chapter} ({version}) from local DB") |
| return local_verses |
|
|
| |
| logger.info(f"Fetching {book} {chapter} ({version}) from bible-api.com...") |
| fetched_verses = [] |
| try: |
| import httpx |
| formatted_book = book.replace(" ", "+") |
| url = f"https://bible-api.com/{formatted_book}+{chapter}?translation={version.lower()}" |
| async with httpx.AsyncClient(timeout=30.0) as client: |
| response = await client.get(url) |
| if response.status_code == 200: |
| data = response.json() |
| for v in data.get("verses", []): |
| fetched_verses.append({ |
| "book": book, "chapter": chapter, |
| "verse": v.get("verse"), |
| "text": v.get("text", "").strip(), |
| "version": version.upper(), |
| }) |
| if fetched_verses: |
| self.save_bible_verses(fetched_verses) |
| return fetched_verses |
| except Exception: |
| logger.exception("Error fetching from external Bible API") |
| return [] |
|
|
| def get_bible_chapter_local(self, book: str, chapter: int, version: str = "KJV") -> List[Dict[str, Any]]: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute( |
| "SELECT * FROM bible_verses WHERE LOWER(book) = LOWER(%s) AND chapter = %s AND version = %s ORDER BY verse ASC", |
| (book, chapter, version.upper()), |
| ) |
| return [dict(r) for r in cur.fetchall()] |
|
|
| def save_bible_verses(self, verses: List[Dict[str, Any]]): |
| try: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| for v in verses: |
| cur.execute( |
| """INSERT INTO bible_verses (book, chapter, verse, text, version) |
| VALUES (%s, %s, %s, %s, %s) |
| ON CONFLICT (book, chapter, verse, version) DO NOTHING""", |
| (v['book'], v['chapter'], v['verse'], v['text'], v.get('version', 'KJV').upper()), |
| ) |
| logger.info(f"Cached {len(verses)} verses locally") |
| except Exception: |
| logger.exception("Error saving verses to cache") |
|
|
| def get_bible_verse(self, book: str, chapter: int, verse: int, version: str = "KJV") -> Optional[Dict[str, Any]]: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute( |
| "SELECT * FROM bible_verses WHERE LOWER(book) = LOWER(%s) AND chapter = %s AND verse = %s AND version = %s", |
| (book, chapter, verse, version.upper()), |
| ) |
| return self.to_dict(cur.fetchone()) |
|
|
| def search_bible_verses(self, query: str) -> List[Dict[str, Any]]: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("SELECT * FROM bible_verses WHERE text ILIKE %s", (f"%{query}%",)) |
| return [self.to_dict(r) for r in cur.fetchall()] |
|
|
| def get_scripture_references(self, category: str) -> List[Dict[str, Any]]: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("SELECT * FROM scripture_references WHERE category = %s", (category,)) |
| return [self.to_dict(r) for r in cur.fetchall()] |
|
|
| |
|
|
| def get_user_activity(self, user_id: str, limit: int = 5) -> List[Dict[str, Any]]: |
| activities = [] |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
|
|
| cur.execute( |
| "SELECT id, book, chapter, created_at FROM bible_study_sessions WHERE user_id = %s ORDER BY created_at DESC LIMIT %s", |
| (user_id, limit), |
| ) |
| for row in cur.fetchall(): |
| activities.append({ |
| "type": "bible_study", "title": f"{row['book']} {row['chapter']}", |
| "subtitle": "Bible Study", "created_at": row['created_at'], "path": "/app/bible-study", |
| }) |
|
|
| cur.execute( |
| "SELECT id, mood, created_at FROM emotional_support_sessions WHERE user_id = %s ORDER BY created_at DESC LIMIT %s", |
| (user_id, limit), |
| ) |
| for row in cur.fetchall(): |
| activities.append({ |
| "type": "support", "title": row['mood'], |
| "subtitle": "Emotional Support", "created_at": row['created_at'], "path": "/app/emotional-support", |
| }) |
|
|
| cur.execute( |
| "SELECT id, day_plan_summary, created_at FROM devotions WHERE user_id = %s ORDER BY created_at DESC LIMIT %s", |
| (user_id, limit), |
| ) |
| for row in cur.fetchall(): |
| activities.append({ |
| "type": "devotion", "title": (row['day_plan_summary'] or "Daily Devotion")[:30], |
| "subtitle": "Devotion", "created_at": row['created_at'], "path": "/app/devotion", |
| }) |
|
|
| cur.execute( |
| "SELECT id, title, created_at FROM ai_chat_sessions WHERE user_id = %s ORDER BY created_at DESC LIMIT %s", |
| (user_id, limit), |
| ) |
| for row in cur.fetchall(): |
| activities.append({ |
| "type": "chat", "title": row['title'] or "Conversation", |
| "subtitle": "AI Chat", "created_at": row['created_at'], "path": "/app/ai-chat", |
| }) |
|
|
| activities.sort(key=lambda x: x["created_at"], reverse=True) |
| return activities[:limit] |
|
|
| |
|
|
| def create_note(self, user_id: str, note_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: |
| try: |
| data = note_data.copy() |
| data['id'] = str(uuid.uuid4()) |
| data['user_id'] = user_id |
| if 'tags' in data and not isinstance(data['tags'], str): |
| data['tags'] = json.dumps(data['tags']) |
| self._validate_columns("notes", data.keys()) |
| cols = ', '.join(data.keys()) |
| placeholders = ', '.join(['%s'] * len(data)) |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute(f"INSERT INTO notes ({cols}) VALUES ({placeholders})", list(data.values())) |
| cur.execute("SELECT * FROM notes WHERE id = %s", (data['id'],)) |
| return self.to_dict(cur.fetchone()) |
| except Exception: |
| logger.exception("Error creating note") |
| return None |
|
|
| def get_note(self, note_id: str, user_id: str) -> Optional[Dict[str, Any]]: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("SELECT * FROM notes WHERE id = %s AND user_id = %s", (note_id, user_id)) |
| return self.to_dict(cur.fetchone()) |
|
|
| def get_notes(self, user_id: str, source_type: Optional[str] = None) -> List[Dict[str, Any]]: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| if source_type: |
| cur.execute( |
| "SELECT * FROM notes WHERE user_id = %s AND source_type = %s ORDER BY created_at DESC", |
| (user_id, source_type), |
| ) |
| else: |
| cur.execute("SELECT * FROM notes WHERE user_id = %s ORDER BY created_at DESC", (user_id,)) |
| return [self.to_dict(r) for r in cur.fetchall()] |
|
|
| def delete_note(self, note_id: str, user_id: str) -> bool: |
| try: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("DELETE FROM notes WHERE id = %s AND user_id = %s", (note_id, user_id)) |
| return cur.rowcount > 0 |
| except Exception: |
| logger.exception("Error deleting note") |
| return False |
|
|
| def verify_note_password(self, note_id: str, user_id: str, password: str) -> bool: |
| from auth import verify_password |
| note = self.get_note(note_id, user_id) |
| if not note or not note.get('password_hash'): |
| return False |
| return verify_password(password, note['password_hash']) |
|
|
| def update_note(self, note_id: str, user_id: str, note_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: |
| try: |
| data = note_data.copy() |
| if 'tags' in data and not isinstance(data['tags'], str): |
| data['tags'] = json.dumps(data['tags']) |
| self._validate_columns("notes", data.keys()) |
| set_clause = ', '.join([f"{k} = %s" for k in data.keys()]) |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute( |
| f"UPDATE notes SET {set_clause}, updated_at = NOW() WHERE id = %s AND user_id = %s", |
| list(data.values()) + [note_id, user_id], |
| ) |
| cur.execute("SELECT * FROM notes WHERE id = %s", (note_id,)) |
| return self.to_dict(cur.fetchone()) |
| except Exception: |
| logger.exception("Error updating note") |
| return None |
|
|
| |
|
|
| def create_prayer(self, prayer_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: |
| try: |
| data = prayer_data.copy() |
| if 'id' not in data: |
| data['id'] = str(uuid.uuid4()) |
| self._validate_columns("prayers", data.keys()) |
| cols = ', '.join(data.keys()) |
| placeholders = ', '.join(['%s'] * len(data)) |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute(f"INSERT INTO prayers ({cols}) VALUES ({placeholders})", list(data.values())) |
| cur.execute("SELECT * FROM prayers WHERE id = %s", (data['id'],)) |
| return self.to_dict(cur.fetchone()) |
| except Exception: |
| logger.exception("Error creating prayer") |
| return None |
|
|
| def delete_prayer(self, prayer_id: str, user_id: str) -> bool: |
| try: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("DELETE FROM prayers WHERE id = %s AND user_id = %s", (prayer_id, user_id)) |
| return cur.rowcount > 0 |
| except Exception: |
| logger.exception("Error deleting prayer") |
| return False |
|
|
| def get_prayers(self, user_id: str) -> List[Dict[str, Any]]: |
| try: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("SELECT * FROM prayers WHERE user_id = %s ORDER BY created_at DESC", (user_id,)) |
| return [self.to_dict(r) for r in cur.fetchall()] |
| except Exception: |
| logger.exception("Error getting prayers") |
| return [] |
|
|
| |
|
|
| def get_user_stats(self, user_id: str) -> Dict[str, Any]: |
| stats = {"streak_days": 1, "time_today_minutes": 15, "total_reflections": 0, "streak_history": [False] * 7} |
| try: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
|
|
| cur.execute("SELECT COUNT(*) AS count FROM notes WHERE user_id = %s", (user_id,)) |
| stats["total_reflections"] = cur.fetchone()["count"] |
|
|
| today = datetime.now() |
| days_since_sunday = (today.weekday() + 1) % 7 |
| sunday = today - timedelta(days=days_since_sunday) |
|
|
| history = [] |
| for i in range(7): |
| day_check = sunday + timedelta(days=i) |
| day_str = day_check.strftime("%Y-%m-%d") |
| cur.execute( |
| """SELECT ( |
| EXISTS(SELECT 1 FROM devotions WHERE user_id = %s AND created_at::date = %s) OR |
| EXISTS(SELECT 1 FROM bible_study_sessions WHERE user_id = %s AND created_at::date = %s) OR |
| EXISTS(SELECT 1 FROM emotional_support_sessions WHERE user_id = %s AND created_at::date = %s) OR |
| EXISTS(SELECT 1 FROM notes WHERE user_id = %s AND created_at::date = %s) |
| ) AS has_activity""", |
| (user_id, day_str, user_id, day_str, user_id, day_str, user_id, day_str), |
| ) |
| history.append(bool(cur.fetchone()['has_activity'])) |
|
|
| stats["streak_history"] = history |
| today_idx = (today.weekday() + 1) % 7 |
| streak = 0 |
| for i in range(today_idx, -1, -1): |
| if history[i]: |
| streak += 1 |
| else: |
| break |
| stats["streak_days"] = streak |
|
|
| return stats |
| except Exception: |
| logger.exception("Error getting user stats") |
| return stats |
|
|
| |
|
|
| def get_cached_verse(self, user_id: str, date_str: str) -> Optional[Dict[str, Any]]: |
| import json as _json |
| try: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute( |
| "SELECT verse_text AS verse, verse_reference AS reference, aria_insight AS insight, daily_manna FROM cached_verses WHERE user_id = %s AND cached_date = %s", |
| (user_id, date_str), |
| ) |
| row = cur.fetchone() |
| if not row: |
| return None |
| result = dict(row) |
| manna = result.get("daily_manna") |
| if manna and isinstance(manna, str): |
| try: |
| result["daily_manna"] = _json.loads(manna) |
| except _json.JSONDecodeError: |
| pass |
| return result |
| except Exception: |
| logger.exception("Error getting cached verse") |
| return None |
|
|
| def save_cached_verse(self, user_id: str, date_str: str, verse_data: Dict[str, Any]) -> bool: |
| import json as _json |
| try: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("DELETE FROM cached_verses WHERE user_id = %s", (user_id,)) |
| manna = verse_data.get("daily_manna") |
| manna_str = _json.dumps(manna) if isinstance(manna, dict) else manna |
| cur.execute( |
| "INSERT INTO cached_verses (user_id, verse_text, verse_reference, aria_insight, daily_manna, cached_date) VALUES (%s, %s, %s, %s, %s, %s)", |
| (user_id, verse_data["verse"], verse_data["reference"], verse_data.get("insight"), manna_str, date_str), |
| ) |
| return True |
| except Exception: |
| logger.exception("Error saving cached verse") |
| return False |
|
|
| |
|
|
| def create_chat_session(self, user_id: str, title: str = "New Conversation") -> Optional[Dict[str, Any]]: |
| try: |
| session_id = str(uuid.uuid4()) |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute( |
| "INSERT INTO ai_chat_sessions (id, user_id, title) VALUES (%s, %s, %s)", |
| (session_id, user_id, title), |
| ) |
| return {"id": session_id, "user_id": user_id, "title": title} |
| except Exception: |
| logger.exception("Error creating chat session") |
| return None |
|
|
| def get_chat_session(self, session_id: str) -> Optional[Dict[str, Any]]: |
| try: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("SELECT * FROM ai_chat_sessions WHERE id = %s", (session_id,)) |
| return self.to_dict(cur.fetchone()) |
| except Exception: |
| logger.exception("Error getting chat session") |
| return None |
|
|
| def get_chat_sessions(self, user_id: str) -> List[Dict[str, Any]]: |
| try: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("SELECT * FROM ai_chat_sessions WHERE user_id = %s ORDER BY updated_at DESC", (user_id,)) |
| return [self.to_dict(r) for r in cur.fetchall()] |
| except Exception: |
| logger.exception("Error getting chat sessions") |
| return [] |
|
|
| def get_chat_session_messages(self, session_id: str) -> List[Dict[str, Any]]: |
| try: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute("SELECT * FROM ai_chat_messages WHERE session_id = %s ORDER BY created_at ASC", (session_id,)) |
| return [self.to_dict(r) for r in cur.fetchall()] |
| except Exception: |
| logger.exception("Error getting chat messages") |
| return [] |
|
|
| def create_chat_message(self, message_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: |
| try: |
| data = message_data.copy() |
| if 'id' not in data: |
| data['id'] = str(uuid.uuid4()) |
| self._validate_columns("ai_chat_messages", data.keys()) |
| cols = ', '.join(data.keys()) |
| placeholders = ', '.join(['%s'] * len(data)) |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute(f"INSERT INTO ai_chat_messages ({cols}) VALUES ({placeholders})", list(data.values())) |
| cur.execute( |
| "UPDATE ai_chat_sessions SET updated_at = NOW() WHERE id = %s", |
| (data['session_id'],), |
| ) |
| cur.execute("SELECT * FROM ai_chat_messages WHERE id = %s", (data['id'],)) |
| return self.to_dict(cur.fetchone()) |
| except Exception: |
| logger.exception("Error creating chat message") |
| return None |
|
|
| def update_chat_session_title(self, session_id: str, title: str) -> bool: |
| try: |
| with self.get_connection() as conn: |
| cur = self._cursor(conn) |
| cur.execute( |
| "UPDATE ai_chat_sessions SET title = %s, updated_at = NOW() WHERE id = %s", |
| (title, session_id), |
| ) |
| return cur.rowcount > 0 |
| except Exception: |
| logger.exception("Error updating chat session title") |
| return False |
|
|
|
|
| db = Database() |
|
|