diff --git a/modules/db_manager.py b/modules/db_manager.py index f65ceea..09f2092 100644 --- a/modules/db_manager.py +++ b/modules/db_manager.py @@ -38,7 +38,7 @@ class DBManager: def _init_database(self): """Initialize the SQLite database with required tables""" try: - with sqlite3.connect(self.db_path) as conn: + with sqlite3.connect(str(self.db_path), timeout=30.0) as conn: cursor = conn.cursor() # Create geocoding_cache table for weather command optimization @@ -215,7 +215,7 @@ class DBManager: def get_cached_geocoding(self, query: str) -> Tuple[Optional[float], Optional[float]]: """Get cached geocoding result for a query""" try: - with sqlite3.connect(self.db_path) as conn: + with sqlite3.connect(str(self.db_path), timeout=30.0) as conn: cursor = conn.cursor() cursor.execute(''' SELECT latitude, longitude FROM geocoding_cache @@ -236,7 +236,7 @@ class DBManager: if not isinstance(cache_hours, int) or cache_hours < 1 or cache_hours > 87600: # Max 10 years raise ValueError(f"cache_hours must be an integer between 1 and 87600, got: {cache_hours}") - with sqlite3.connect(self.db_path) as conn: + with sqlite3.connect(str(self.db_path), timeout=30.0) as conn: cursor = conn.cursor() # Use parameter binding instead of string formatting cursor.execute(''' @@ -252,7 +252,7 @@ class DBManager: def get_cached_value(self, cache_key: str, cache_type: str) -> Optional[str]: """Get cached value for a key and type""" try: - with sqlite3.connect(self.db_path) as conn: + with sqlite3.connect(str(self.db_path), timeout=30.0) as conn: cursor = conn.cursor() cursor.execute(''' SELECT cache_value FROM generic_cache @@ -273,7 +273,7 @@ class DBManager: if not isinstance(cache_hours, int) or cache_hours < 1 or cache_hours > 87600: # Max 10 years raise ValueError(f"cache_hours must be an integer between 1 and 87600, got: {cache_hours}") - with sqlite3.connect(self.db_path) as conn: + with sqlite3.connect(str(self.db_path), timeout=30.0) as conn: cursor = conn.cursor() # Use parameter binding instead of string formatting cursor.execute(''' @@ -308,7 +308,7 @@ class DBManager: def cleanup_expired_cache(self): """Remove expired cache entries from all cache tables""" try: - with sqlite3.connect(self.db_path) as conn: + with sqlite3.connect(str(self.db_path), timeout=30.0) as conn: cursor = conn.cursor() # Clean up geocoding cache @@ -331,7 +331,7 @@ class DBManager: def cleanup_geocoding_cache(self): """Remove expired geocoding cache entries""" try: - with sqlite3.connect(self.db_path) as conn: + with sqlite3.connect(str(self.db_path), timeout=30.0) as conn: cursor = conn.cursor() cursor.execute("DELETE FROM geocoding_cache WHERE expires_at < datetime('now')") deleted_count = cursor.rowcount @@ -344,7 +344,7 @@ class DBManager: def get_database_stats(self) -> Dict[str, Any]: """Get database statistics""" try: - with sqlite3.connect(self.db_path) as conn: + with sqlite3.connect(str(self.db_path), timeout=30.0) as conn: cursor = conn.cursor() stats = {} @@ -380,7 +380,7 @@ class DBManager: def vacuum_database(self): """Optimize database by reclaiming unused space""" try: - with sqlite3.connect(self.db_path) as conn: + with sqlite3.connect(str(self.db_path), timeout=30.0) as conn: conn.execute("VACUUM") self.logger.info("Database vacuum completed") except Exception as e: @@ -398,7 +398,7 @@ class DBManager: if not re.match(r'^[a-z_][a-z0-9_]*$', table_name): raise ValueError(f"Invalid table name format: {table_name}") - with sqlite3.connect(self.db_path) as conn: + with sqlite3.connect(str(self.db_path), timeout=30.0) as conn: cursor = conn.cursor() # Table names cannot be parameterized, but we've validated against whitelist cursor.execute(f'CREATE TABLE IF NOT EXISTS {table_name} ({schema})') @@ -422,7 +422,7 @@ class DBManager: # Extra safety: log critical action self.logger.warning(f"CRITICAL: Dropping table '{table_name}'") - with sqlite3.connect(self.db_path) as conn: + with sqlite3.connect(str(self.db_path), timeout=30.0) as conn: cursor = conn.cursor() # Table names cannot be parameterized, but we've validated against whitelist cursor.execute(f'DROP TABLE IF EXISTS {table_name}') @@ -435,7 +435,7 @@ class DBManager: def execute_query(self, query: str, params: Tuple = ()) -> List[Dict]: """Execute a custom query and return results as list of dictionaries""" try: - with sqlite3.connect(self.db_path) as conn: + with sqlite3.connect(str(self.db_path), timeout=30.0) as conn: conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(query, params) @@ -448,7 +448,7 @@ class DBManager: def execute_update(self, query: str, params: Tuple = ()) -> int: """Execute an update/insert/delete query and return number of affected rows""" try: - with sqlite3.connect(self.db_path) as conn: + with sqlite3.connect(str(self.db_path), timeout=30.0) as conn: cursor = conn.cursor() cursor.execute(query, params) conn.commit() @@ -461,7 +461,7 @@ class DBManager: def set_metadata(self, key: str, value: str): """Set a metadata value for the bot""" try: - with sqlite3.connect(self.db_path) as conn: + with sqlite3.connect(str(self.db_path), timeout=30.0) as conn: cursor = conn.cursor() cursor.execute(''' INSERT OR REPLACE INTO bot_metadata (key, value, updated_at) @@ -474,7 +474,7 @@ class DBManager: def get_metadata(self, key: str) -> Optional[str]: """Get a metadata value for the bot""" try: - with sqlite3.connect(self.db_path) as conn: + with sqlite3.connect(str(self.db_path), timeout=30.0) as conn: cursor = conn.cursor() cursor.execute('SELECT value FROM bot_metadata WHERE key = ?', (key,)) result = cursor.fetchone() diff --git a/modules/feed_manager.py b/modules/feed_manager.py index dd7170c..4cb57c7 100644 --- a/modules/feed_manager.py +++ b/modules/feed_manager.py @@ -11,6 +11,7 @@ import time import hashlib import html import re +import os from datetime import datetime, timezone from typing import Dict, List, Optional, Any, Tuple from pathlib import Path @@ -272,7 +273,7 @@ class FeedManager: # Query database for all processed item IDs for this feed try: - with sqlite3.connect(self.db_path) as conn: + with sqlite3.connect(str(self.db_path), timeout=30.0) as conn: cursor = conn.cursor() cursor.execute(''' SELECT DISTINCT item_id FROM feed_activity @@ -431,7 +432,7 @@ class FeedManager: # Query database for all processed item IDs for this feed try: - with sqlite3.connect(self.db_path) as conn: + with sqlite3.connect(str(self.db_path), timeout=30.0) as conn: cursor = conn.cursor() cursor.execute(''' SELECT DISTINCT item_id FROM feed_activity @@ -1181,7 +1182,8 @@ class FeedManager: """Process queued feed messages and send them at configured intervals""" try: # Get all unsent messages, ordered by priority and queue time - with sqlite3.connect(self.db_path) as conn: + db_path = str(self.db_path) # Ensure string, not Path object + with sqlite3.connect(db_path, timeout=30.0) as conn: conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(''' @@ -1225,7 +1227,7 @@ class FeedManager: if success: # Mark as sent - with sqlite3.connect(self.db_path) as conn: + with sqlite3.connect(db_path, timeout=30.0) as conn: cursor = conn.cursor() cursor.execute(''' UPDATE feed_message_queue @@ -1250,6 +1252,15 @@ class FeedManager: except Exception as e: db_path = getattr(self, 'db_path', 'unknown') + db_path_str = str(db_path) if db_path != 'unknown' else 'unknown' self.logger.error(f"Error processing message queue: {e}") - self.logger.error(f"Database path: {db_path} (exists: {Path(db_path).exists() if isinstance(db_path, str) else False})") + if db_path_str != 'unknown': + path_obj = Path(db_path_str) + self.logger.error(f"Database path: {db_path_str} (exists: {path_obj.exists()}, readable: {os.access(db_path_str, os.R_OK) if path_obj.exists() else False}, writable: {os.access(db_path_str, os.W_OK) if path_obj.exists() else False})") + # Check parent directory permissions + if path_obj.exists(): + parent = path_obj.parent + self.logger.error(f"Parent directory: {parent} (exists: {parent.exists()}, writable: {os.access(str(parent), os.W_OK) if parent.exists() else False})") + else: + self.logger.error(f"Database path: {db_path_str}") diff --git a/modules/scheduler.py b/modules/scheduler.py index 1fb821a..df48e29 100644 --- a/modules/scheduler.py +++ b/modules/scheduler.py @@ -11,6 +11,7 @@ import datetime import pytz import sqlite3 import json +import os from typing import Dict, Tuple from pathlib import Path @@ -260,10 +261,10 @@ class MessageScheduler: async def _process_channel_operations(self): """Process pending channel operations from the web viewer""" try: - db_path = self.bot.db_manager.db_path + db_path = str(self.bot.db_manager.db_path) # Ensure string, not Path object # Get pending operations - with sqlite3.connect(db_path) as conn: + with sqlite3.connect(db_path, timeout=30.0) as conn: conn.row_factory = sqlite3.Row cursor = conn.cursor() @@ -322,7 +323,7 @@ class MessageScheduler: error_msg = "Failed to remove channel" # Update operation status - with sqlite3.connect(db_path) as conn: + with sqlite3.connect(db_path, timeout=30.0) as conn: cursor = conn.cursor() if success: cursor.execute(''' @@ -346,7 +347,7 @@ class MessageScheduler: self.logger.error(f"Error processing channel operation {op_id}: {e}") # Mark as failed try: - with sqlite3.connect(db_path) as conn: + with sqlite3.connect(db_path, timeout=30.0) as conn: cursor = conn.cursor() cursor.execute(''' UPDATE channel_operations @@ -361,5 +362,14 @@ class MessageScheduler: except Exception as e: db_path = getattr(self.bot.db_manager, 'db_path', 'unknown') + db_path_str = str(db_path) if db_path != 'unknown' else 'unknown' self.logger.error(f"Error in _process_channel_operations: {e}") - self.logger.error(f"Database path: {db_path} (exists: {Path(db_path).exists() if isinstance(db_path, str) else False})") + if db_path_str != 'unknown': + path_obj = Path(db_path_str) + self.logger.error(f"Database path: {db_path_str} (exists: {path_obj.exists()}, readable: {os.access(db_path_str, os.R_OK) if path_obj.exists() else False}, writable: {os.access(db_path_str, os.W_OK) if path_obj.exists() else False})") + # Check parent directory permissions + if path_obj.exists(): + parent = path_obj.parent + self.logger.error(f"Parent directory: {parent} (exists: {parent.exists()}, writable: {os.access(str(parent), os.W_OK) if parent.exists() else False})") + else: + self.logger.error(f"Database path: {db_path_str}")