diff options
| author | salmonik <raisersoft@proton.me> | 2026-04-28 20:05:36 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-04-28 20:05:36 +0200 |
| commit | 5675dfd20b24f1e0eef06b11d9ec41a894d3a5bf (patch) | |
| tree | caad1669db6fce506dd0494aeaf2a43f7e291679 /cogs/utils | |
| parent | better UX for web.py (diff) | |
| download | senator-sperma-5675dfd20b24f1e0eef06b11d9ec41a894d3a5bf.tar.gz senator-sperma-5675dfd20b24f1e0eef06b11d9ec41a894d3a5bf.zip | |
multitude of changes.
but primarly, use db for all data.
Signed-off-by: salmonik <raisersoft@proton.me>
Diffstat (limited to 'cogs/utils')
| -rw-r--r-- | cogs/utils/database.py | 111 | ||||
| -rw-r--r-- | cogs/utils/file_search.py | 18 | ||||
| -rw-r--r-- | cogs/utils/settings.py | 69 |
3 files changed, 172 insertions, 26 deletions
diff --git a/cogs/utils/database.py b/cogs/utils/database.py new file mode 100644 index 0000000..67cbc9f --- /dev/null +++ b/cogs/utils/database.py | |||
| @@ -0,0 +1,111 @@ | |||
| 1 | import os | ||
| 2 | import sqlite3 | ||
| 3 | import logging | ||
| 4 | |||
| 5 | logger = logging.getLogger(__name__) | ||
| 6 | |||
| 7 | DB_PATH = "data/senator.db" | ||
| 8 | |||
| 9 | _initialized = False | ||
| 10 | |||
| 11 | |||
| 12 | def get_connection(): | ||
| 13 | return sqlite3.connect(get_db_path()) | ||
| 14 | |||
| 15 | |||
| 16 | def get_db_path(): | ||
| 17 | return os.getenv("SENATOR_DB_PATH", DB_PATH) | ||
| 18 | |||
| 19 | |||
| 20 | def set_db_path(path): | ||
| 21 | global DB_PATH | ||
| 22 | DB_PATH = path | ||
| 23 | |||
| 24 | |||
| 25 | def init_database(): | ||
| 26 | global _initialized | ||
| 27 | if _initialized: | ||
| 28 | return | ||
| 29 | |||
| 30 | _initialized = True | ||
| 31 | |||
| 32 | db_path = get_db_path() | ||
| 33 | db_dir = os.path.dirname(db_path) | ||
| 34 | if db_dir and not os.path.exists(db_dir): | ||
| 35 | os.makedirs(db_dir) | ||
| 36 | |||
| 37 | conn = sqlite3.connect(db_path) | ||
| 38 | try: | ||
| 39 | c = conn.cursor() | ||
| 40 | |||
| 41 | c.execute("PRAGMA foreign_keys = ON") | ||
| 42 | |||
| 43 | c.execute(""" | ||
| 44 | CREATE TABLE IF NOT EXISTS settings ( | ||
| 45 | key TEXT PRIMARY KEY, | ||
| 46 | value TEXT | ||
| 47 | )""") | ||
| 48 | |||
| 49 | c.execute(""" | ||
| 50 | CREATE TABLE IF NOT EXISTS cron_jobs ( | ||
| 51 | id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| 52 | cron_expression TEXT NOT NULL, | ||
| 53 | type TEXT NOT NULL, | ||
| 54 | channel_id INTEGER NOT NULL, | ||
| 55 | active INTEGER DEFAULT 1, | ||
| 56 | audio_filename TEXT, | ||
| 57 | message TEXT, | ||
| 58 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | ||
| 59 | )""") | ||
| 60 | |||
| 61 | c.execute(""" | ||
| 62 | CREATE TABLE IF NOT EXISTS voice_activity ( | ||
| 63 | user_id TEXT PRIMARY KEY, | ||
| 64 | join_time REAL, | ||
| 65 | total_active REAL DEFAULT 0, | ||
| 66 | total_afk REAL DEFAULT 0, | ||
| 67 | is_afk INTEGER DEFAULT 0, | ||
| 68 | afk_start REAL, | ||
| 69 | active_start REAL, | ||
| 70 | channel_name TEXT, | ||
| 71 | last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP | ||
| 72 | )""") | ||
| 73 | |||
| 74 | c.execute("CREATE INDEX IF NOT EXISTS idx_voice_activity_user_id ON voice_activity(user_id)") | ||
| 75 | c.execute("CREATE INDEX IF NOT EXISTS idx_cron_jobs_active ON cron_jobs(active)") | ||
| 76 | |||
| 77 | c.execute(""" | ||
| 78 | CREATE TABLE IF NOT EXISTS triggers ( | ||
| 79 | id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| 80 | name TEXT NOT NULL UNIQUE, | ||
| 81 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||
| 82 | last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP | ||
| 83 | )""") | ||
| 84 | |||
| 85 | c.execute(""" | ||
| 86 | CREATE TABLE IF NOT EXISTS trigger_keywords ( | ||
| 87 | id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| 88 | trigger_id INTEGER NOT NULL, | ||
| 89 | keyword TEXT NOT NULL, | ||
| 90 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||
| 91 | FOREIGN KEY (trigger_id) REFERENCES triggers(id) ON DELETE CASCADE, | ||
| 92 | UNIQUE(trigger_id, keyword) | ||
| 93 | )""") | ||
| 94 | |||
| 95 | c.execute(""" | ||
| 96 | CREATE TABLE IF NOT EXISTS trigger_responses ( | ||
| 97 | id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| 98 | trigger_id INTEGER NOT NULL, | ||
| 99 | response_text TEXT NOT NULL, | ||
| 100 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||
| 101 | FOREIGN KEY (trigger_id) REFERENCES triggers(id) ON DELETE CASCADE | ||
| 102 | )""") | ||
| 103 | |||
| 104 | c.execute("CREATE INDEX IF NOT EXISTS idx_trigger_keywords_trigger_id ON trigger_keywords(trigger_id)") | ||
| 105 | c.execute("CREATE INDEX IF NOT EXISTS idx_trigger_keywords_keyword ON trigger_keywords(keyword)") | ||
| 106 | c.execute("CREATE INDEX IF NOT EXISTS idx_trigger_responses_trigger_id ON trigger_responses(trigger_id)") | ||
| 107 | |||
| 108 | conn.commit() | ||
| 109 | logger.info("Database initialized successfully") | ||
| 110 | finally: | ||
| 111 | conn.close() | ||
diff --git a/cogs/utils/file_search.py b/cogs/utils/file_search.py index 8111216..788f50c 100644 --- a/cogs/utils/file_search.py +++ b/cogs/utils/file_search.py | |||
| @@ -34,10 +34,22 @@ class UniversalSearcher: | |||
| 34 | query = query.strip() | 34 | query = query.strip() |
| 35 | logger.info(f"Starting copyparty search for query: '{query}'") | 35 | logger.info(f"Starting copyparty search for query: '{query}'") |
| 36 | 36 | ||
| 37 | # Build the query with optional extension filtering | 37 | tokens = query.split() |
| 38 | name_query = f'name like "*{query}*"' | 38 | if not tokens: |
| 39 | return [] | ||
| 40 | |||
| 41 | token_queries = [] | ||
| 42 | for token in tokens: | ||
| 43 | path_query = f'path like "*{token}*"' | ||
| 44 | name_query = f'name like "*{token}*"' | ||
| 45 | token_queries.append(f"({path_query} or {name_query})") | ||
| 46 | |||
| 47 | if len(token_queries) == 1: | ||
| 48 | name_query = token_queries[0] | ||
| 49 | else: | ||
| 50 | name_query = " and ".join(token_queries) | ||
| 51 | |||
| 39 | if file_extensions: | 52 | if file_extensions: |
| 40 | # Create extension filters using copyparty's .ext$ syntax | ||
| 41 | ext_filters = [f"name like \"*.{ext.lower()}\"" for ext in file_extensions] | 53 | ext_filters = [f"name like \"*.{ext.lower()}\"" for ext in file_extensions] |
| 42 | ext_query = " or ".join(ext_filters) | 54 | ext_query = " or ".join(ext_filters) |
| 43 | query = f"{name_query} and ({ext_query})" | 55 | query = f"{name_query} and ({ext_query})" |
diff --git a/cogs/utils/settings.py b/cogs/utils/settings.py index d66ce7f..7146b6c 100644 --- a/cogs/utils/settings.py +++ b/cogs/utils/settings.py | |||
| @@ -3,16 +3,18 @@ | |||
| 3 | # Copyright (C) 2026 Salmonik @ salmonik@tuta.io | 3 | # Copyright (C) 2026 Salmonik @ salmonik@tuta.io |
| 4 | # https://codeberg.org/salmonik/senator-sperma | 4 | # https://codeberg.org/salmonik/senator-sperma |
| 5 | 5 | ||
| 6 | import json | ||
| 7 | import logging | 6 | import logging |
| 8 | import aiofiles | 7 | import sqlite3 |
| 9 | import os | 8 | import os |
| 10 | from dataclasses import dataclass, field | 9 | from dataclasses import dataclass, field |
| 11 | 10 | ||
| 11 | from cogs.utils.database import get_connection, init_database | ||
| 12 | |||
| 12 | logger = logging.getLogger(__name__) | 13 | logger = logging.getLogger(__name__) |
| 13 | 14 | ||
| 14 | settings_file = "data/settings.json" | 15 | DEFAULT_BASEURL = "http://localhost:3939" |
| 15 | DEFAULT_BASEURL = "http://localhost:8080" | 16 | |
| 17 | init_database() | ||
| 16 | 18 | ||
| 17 | 19 | ||
| 18 | @dataclass | 20 | @dataclass |
| @@ -28,24 +30,22 @@ class Settings: | |||
| 28 | } | 30 | } |
| 29 | 31 | ||
| 30 | try: | 32 | try: |
| 31 | if os.path.exists(settings_file): | 33 | conn = get_connection() |
| 32 | async with aiofiles.open(settings_file, "r") as f: | 34 | c = conn.cursor() |
| 33 | content = await f.read() | 35 | c.execute("SELECT key, value FROM settings") |
| 34 | if content.strip(): | 36 | rows = c.fetchall() |
| 35 | self._data = json.loads(content) | 37 | conn.close() |
| 36 | self._sync_from_data() | 38 | |
| 37 | logger.info("Settings loaded into memory") | 39 | if rows: |
| 38 | else: | 40 | self._data = {row[0]: row[1] for row in rows} |
| 39 | self._data = default_settings.copy() | 41 | self._sync_from_data() |
| 40 | self._sync_from_data() | 42 | logger.info("Settings loaded from database") |
| 41 | await self.save() | ||
| 42 | logger.warning("Empty settings.json, reset to defaults") | ||
| 43 | else: | 43 | else: |
| 44 | self._data = default_settings.copy() | 44 | self._data = default_settings.copy() |
| 45 | self._sync_from_data() | 45 | self._sync_from_data() |
| 46 | await self.save() | 46 | await self.save() |
| 47 | logger.info("Created default settings.json") | 47 | logger.info("Created default settings in database") |
| 48 | except json.JSONDecodeError as e: | 48 | except Exception as e: |
| 49 | logger.error(f"Error loading settings: {e}") | 49 | logger.error(f"Error loading settings: {e}") |
| 50 | self._data = default_settings.copy() | 50 | self._data = default_settings.copy() |
| 51 | self._sync_from_data() | 51 | self._sync_from_data() |
| @@ -53,17 +53,28 @@ class Settings: | |||
| 53 | return self | 53 | return self |
| 54 | 54 | ||
| 55 | def _sync_from_data(self): | 55 | def _sync_from_data(self): |
| 56 | self.volume = self._data.get("volume", 1.0) | 56 | try: |
| 57 | self.volume = float(self._data.get("volume", 1.0)) | ||
| 58 | except (ValueError, TypeError): | ||
| 59 | self.volume = 1.0 | ||
| 57 | self.baseurl = self._data.get("baseurl", DEFAULT_BASEURL) | 60 | self.baseurl = self._data.get("baseurl", DEFAULT_BASEURL) |
| 58 | 61 | ||
| 59 | def _sync_to_data(self): | 62 | def _sync_to_data(self): |
| 60 | self._data["volume"] = self.volume | 63 | self._data["volume"] = str(self.volume) |
| 61 | self._data["baseurl"] = self.baseurl | 64 | self._data["baseurl"] = self.baseurl |
| 62 | 65 | ||
| 63 | async def save(self): | 66 | async def save(self): |
| 64 | self._sync_to_data() | 67 | self._sync_to_data() |
| 65 | async with aiofiles.open(settings_file, "w") as f: | 68 | conn = get_connection() |
| 66 | await f.write(json.dumps(self._data, indent=2)) | 69 | c = conn.cursor() |
| 70 | c.execute("PRAGMA foreign_keys = ON") | ||
| 71 | for key, value in self._data.items(): | ||
| 72 | c.execute( | ||
| 73 | "INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", | ||
| 74 | (key, value), | ||
| 75 | ) | ||
| 76 | conn.commit() | ||
| 77 | conn.close() | ||
| 67 | 78 | ||
| 68 | 79 | ||
| 69 | _settings_instance: Settings | None = None | 80 | _settings_instance: Settings | None = None |
| @@ -90,4 +101,16 @@ def get_volume() -> float: | |||
| 90 | 101 | ||
| 91 | 102 | ||
| 92 | def get_baseurl() -> str: | 103 | def get_baseurl() -> str: |
| 93 | return _settings_instance.baseurl.rstrip("/") if _settings_instance else DEFAULT_BASEURL | 104 | url = os.getenv("COPYPARTY_URL") |
| 105 | if not url: | ||
| 106 | logger.warning("COPYPARTY_URL not set! Using default http://localhost:3939") | ||
| 107 | return DEFAULT_BASEURL | ||
| 108 | return url.rstrip("/") | ||
| 109 | |||
| 110 | |||
| 111 | def get_uploadurl() -> str: | ||
| 112 | url = os.getenv("COPYPARTY_URL_UPLOAD") | ||
| 113 | if not url: | ||
| 114 | logger.warning("COPYPARTY_URL_UPLOAD not set! Using default http://localhost:3939") | ||
| 115 | return DEFAULT_BASEURL | ||
| 116 | return url.rstrip("/") | ||
