diff options
Diffstat (limited to '')
| -rw-r--r-- | cogs/assistant.py | 2 | ||||
| -rw-r--r-- | cogs/compression.py | 2 | ||||
| -rw-r--r-- | cogs/conversion.py | 6 | ||||
| -rw-r--r-- | cogs/cron.py | 103 | ||||
| -rw-r--r-- | cogs/music/play_commands.py | 8 | ||||
| -rw-r--r-- | cogs/trigger.py | 84 | ||||
| -rw-r--r-- | cogs/uploader.py | 9 | ||||
| -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 | ||||
| -rw-r--r-- | cogs/vclogs.py | 70 | ||||
| -rw-r--r-- | cogs/voice.py | 22 | ||||
| -rw-r--r-- | cogs/web.py | 4 |
13 files changed, 342 insertions, 166 deletions
diff --git a/cogs/assistant.py b/cogs/assistant.py index 21e4989..9926361 100644 --- a/cogs/assistant.py +++ b/cogs/assistant.py | |||
| @@ -100,7 +100,7 @@ class Jarvis(commands.Cog): | |||
| 100 | @staticmethod | 100 | @staticmethod |
| 101 | def _build_payload(messages): | 101 | def _build_payload(messages): |
| 102 | return { | 102 | return { |
| 103 | "model": "arcee-ai/trinity-large-preview:free", | 103 | "model": os.getenv("OPENROUTER_MODEL"), |
| 104 | "messages": messages, | 104 | "messages": messages, |
| 105 | "response_format": Jarvis._get_response_schema(), | 105 | "response_format": Jarvis._get_response_schema(), |
| 106 | "plugins": [{"id": "response-healing"}] | 106 | "plugins": [{"id": "response-healing"}] |
diff --git a/cogs/compression.py b/cogs/compression.py index acc9eda..7171abb 100644 --- a/cogs/compression.py +++ b/cogs/compression.py | |||
| @@ -38,7 +38,7 @@ class Compression(commands.Cog): | |||
| 38 | self.bot = bot | 38 | self.bot = bot |
| 39 | self.temp_dir = get_temp_dir() | 39 | self.temp_dir = get_temp_dir() |
| 40 | self._cached_urls = {} | 40 | self._cached_urls = {} |
| 41 | self._baseurl = os.getenv("COPYPARTY_URL") or get_baseurl() | 41 | self._baseurl = get_baseurl() |
| 42 | 42 | ||
| 43 | async def _file_autocomplete(self, ctx: AutocompleteContext): | 43 | async def _file_autocomplete(self, ctx: AutocompleteContext): |
| 44 | query = ctx.value.lower() if ctx.value else "" | 44 | query = ctx.value.lower() if ctx.value else "" |
diff --git a/cogs/conversion.py b/cogs/conversion.py index 1117caf..455bfde 100644 --- a/cogs/conversion.py +++ b/cogs/conversion.py | |||
| @@ -35,11 +35,11 @@ class Conversion(commands.Cog): | |||
| 35 | def __init__(self, bot): | 35 | def __init__(self, bot): |
| 36 | self.bot = bot | 36 | self.bot = bot |
| 37 | from cogs.utils.temp import get_temp_dir | 37 | from cogs.utils.temp import get_temp_dir |
| 38 | from cogs.utils.settings import get_baseurl | 38 | from cogs.utils.settings import get_baseurl, get_uploadurl |
| 39 | self.temp_dir = get_temp_dir() | 39 | self.temp_dir = get_temp_dir() |
| 40 | self._cached_urls = {} | 40 | self._cached_urls = {} |
| 41 | self._baseurl = os.getenv("COPYPARTY_URL") or get_baseurl() | 41 | self._baseurl = get_baseurl() |
| 42 | self._uploadurl = os.getenv("COPYPARTY_URL_UPLOAD") | 42 | self._uploadurl = get_uploadurl() |
| 43 | 43 | ||
| 44 | async def _file_autocomplete(self, ctx: AutocompleteContext): | 44 | async def _file_autocomplete(self, ctx: AutocompleteContext): |
| 45 | query = ctx.value.lower() if ctx.value else "" | 45 | query = ctx.value.lower() if ctx.value else "" |
diff --git a/cogs/cron.py b/cogs/cron.py index 4a63e3a..b4f494a 100644 --- a/cogs/cron.py +++ b/cogs/cron.py | |||
| @@ -4,9 +4,7 @@ | |||
| 4 | # https://codeberg.org/salmonik/senator-sperma | 4 | # https://codeberg.org/salmonik/senator-sperma |
| 5 | 5 | ||
| 6 | import asyncio | 6 | import asyncio |
| 7 | import json | ||
| 8 | import logging | 7 | import logging |
| 9 | import os | ||
| 10 | from datetime import datetime | 8 | from datetime import datetime |
| 11 | from venv import logger | 9 | from venv import logger |
| 12 | 10 | ||
| @@ -16,37 +14,74 @@ from discord.ext import commands | |||
| 16 | 14 | ||
| 17 | from cogs.utils import file_search as search_utils | 15 | from cogs.utils import file_search as search_utils |
| 18 | from cogs.voice import get_voice_manager | 16 | from cogs.voice import get_voice_manager |
| 17 | from cogs.utils.database import get_connection, init_database | ||
| 19 | 18 | ||
| 20 | logger = logging.getLogger(__name__) | 19 | logger = logging.getLogger(__name__) |
| 21 | 20 | ||
| 21 | init_database() | ||
| 22 | |||
| 22 | 23 | ||
| 23 | class Cron(commands.Cog): | 24 | class Cron(commands.Cog): |
| 24 | def __init__(self, bot): | 25 | def __init__(self, bot): |
| 25 | self.bot = bot | 26 | self.bot = bot |
| 26 | self.scheduled_tasks = [] | 27 | self.scheduled_tasks = [] |
| 27 | self.cron_file = "data/cron_jobs.json" | ||
| 28 | self.load_tasks() | 28 | self.load_tasks() |
| 29 | self.task = bot.loop.create_task(self.schedule_checker()) | 29 | self.task = bot.loop.create_task(self.schedule_checker()) |
| 30 | 30 | ||
| 31 | def load_tasks(self): | 31 | def load_tasks(self): |
| 32 | """Load scheduled tasks from file""" | ||
| 33 | try: | 32 | try: |
| 34 | if os.path.exists(self.cron_file): | 33 | conn = get_connection() |
| 35 | with open(self.cron_file, "r") as f: | 34 | try: |
| 36 | self.scheduled_tasks = json.load(f) | 35 | c = conn.cursor() |
| 37 | logger.info(f"Loaded {len(self.scheduled_tasks)} cron jobs") | 36 | c.execute("SELECT id, cron_expression, type, channel_id, active, audio_filename, message FROM cron_jobs") |
| 38 | else: | 37 | rows = c.fetchall() |
| 39 | self.scheduled_tasks = [] | 38 | finally: |
| 39 | conn.close() | ||
| 40 | |||
| 41 | self.scheduled_tasks = [ | ||
| 42 | { | ||
| 43 | "id": row[0], | ||
| 44 | "cron_expression": row[1], | ||
| 45 | "type": row[2], | ||
| 46 | "channel_id": row[3], | ||
| 47 | "active": bool(row[4]), | ||
| 48 | "audio_filename": row[5], | ||
| 49 | "message": row[6], | ||
| 50 | } | ||
| 51 | for row in rows | ||
| 52 | ] | ||
| 53 | logger.info(f"Loaded {len(self.scheduled_tasks)} cron jobs from database") | ||
| 40 | except Exception as e: | 54 | except Exception as e: |
| 41 | logger.error(f"Error loading cron jobs: {e}") | 55 | logger.error(f"Error loading cron jobs: {e}") |
| 42 | self.scheduled_tasks = [] | 56 | self.scheduled_tasks = [] |
| 43 | 57 | ||
| 44 | def save_tasks(self): | 58 | def save_tasks(self): |
| 45 | """Save scheduled tasks to file""" | ||
| 46 | try: | 59 | try: |
| 47 | os.makedirs(os.path.dirname(self.cron_file), exist_ok=True) | 60 | conn = get_connection() |
| 48 | with open(self.cron_file, "w") as f: | 61 | try: |
| 49 | json.dump(self.scheduled_tasks, f, indent=2) | 62 | c = conn.cursor() |
| 63 | c.execute("PRAGMA foreign_keys = ON") | ||
| 64 | |||
| 65 | c.execute("DELETE FROM cron_jobs") | ||
| 66 | |||
| 67 | for task in self.scheduled_tasks: | ||
| 68 | c.execute( | ||
| 69 | """INSERT INTO cron_jobs (id, cron_expression, type, channel_id, active, audio_filename, message) | ||
| 70 | VALUES (?, ?, ?, ?, ?, ?, ?)""", | ||
| 71 | ( | ||
| 72 | task.get("id"), | ||
| 73 | task["cron_expression"], | ||
| 74 | task["type"], | ||
| 75 | task["channel_id"], | ||
| 76 | int(task.get("active", True)), | ||
| 77 | task.get("audio_filename"), | ||
| 78 | task.get("message"), | ||
| 79 | ), | ||
| 80 | ) | ||
| 81 | |||
| 82 | conn.commit() | ||
| 83 | finally: | ||
| 84 | conn.close() | ||
| 50 | except Exception as e: | 85 | except Exception as e: |
| 51 | logger.error(f"Error saving cron jobs: {e}") | 86 | logger.error(f"Error saving cron jobs: {e}") |
| 52 | 87 | ||
| @@ -132,11 +167,13 @@ class Cron(commands.Cog): | |||
| 132 | while voice_client.is_playing(): | 167 | while voice_client.is_playing(): |
| 133 | await asyncio.sleep(1) | 168 | await asyncio.sleep(1) |
| 134 | 169 | ||
| 170 | logger.info("Audio playback finished normally - disconnecting") | ||
| 135 | await voice_client.disconnect() | 171 | await voice_client.disconnect() |
| 136 | logger.info(f"Finished playing audio: {audio_url}") | 172 | logger.info(f"Finished playing audio: {audio_url}") |
| 137 | except Exception as e: | 173 | except Exception as e: |
| 138 | logger.error(f"Error playing audio: {e}") | 174 | logger.error(f"Error playing audio: {e}") |
| 139 | if voice_client.is_connected(): | 175 | if voice_client.is_connected(): |
| 176 | logger.info("Audio playback failed - disconnecting") | ||
| 140 | await voice_client.disconnect() | 177 | await voice_client.disconnect() |
| 141 | if channel.guild.system_channel: | 178 | if channel.guild.system_channel: |
| 142 | await channel.guild.system_channel.send( | 179 | await channel.guild.system_channel.send( |
| @@ -176,8 +213,19 @@ class Cron(commands.Cog): | |||
| 176 | else [discord.OptionChoice(name="Brak wyników", value="")] | 213 | else [discord.OptionChoice(name="Brak wyników", value="")] |
| 177 | ) | 214 | ) |
| 178 | 215 | ||
| 179 | @commands.slash_command( | 216 | def _get_baseurl(self): |
| 180 | name="cron_dodaj_audio", description="Dodaj nowe zadanie cron audio" | 217 | """Get base URL from settings""" |
| 218 | try: | ||
| 219 | from cogs.utils.settings import get_baseurl | ||
| 220 | |||
| 221 | return get_baseurl() | ||
| 222 | except Exception: | ||
| 223 | return "" | ||
| 224 | |||
| 225 | trigger_group = discord.SlashCommandGroup("cron", "komendy cron") | ||
| 226 | |||
| 227 | @trigger_group.command( | ||
| 228 | name="dodaj_audio", description="Dodaj nowe zadanie cron audio" | ||
| 181 | ) | 229 | ) |
| 182 | async def cron_add_audio( | 230 | async def cron_add_audio( |
| 183 | self, | 231 | self, |
| @@ -227,8 +275,8 @@ class Cron(commands.Cog): | |||
| 227 | f"Zadanie cron dodane. ID: {task_id}\nSkładnia: `{cron_expression}`\nAkcja: audio\nKanał: <#{channel.id}>" | 275 | f"Zadanie cron dodane. ID: {task_id}\nSkładnia: `{cron_expression}`\nAkcja: audio\nKanał: <#{channel.id}>" |
| 228 | ) | 276 | ) |
| 229 | 277 | ||
| 230 | @commands.slash_command( | 278 | @trigger_group.command( |
| 231 | name="cron_dodaj_wiadomość", description="Dodaj nowe zadanie cron wiadomość" | 279 | name="dodaj_wiadomość", description="Dodaj nowe zadanie cron wiadomość" |
| 232 | ) | 280 | ) |
| 233 | async def cron_add_message( | 281 | async def cron_add_message( |
| 234 | self, | 282 | self, |
| @@ -272,17 +320,8 @@ class Cron(commands.Cog): | |||
| 272 | f"Zadanie cron dodane. ID: {task_id}\nSkładnia: `{cron_expression}`\nAkcja: wiadomość\nKanał: <#{channel.id}>" | 320 | f"Zadanie cron dodane. ID: {task_id}\nSkładnia: `{cron_expression}`\nAkcja: wiadomość\nKanał: <#{channel.id}>" |
| 273 | ) | 321 | ) |
| 274 | 322 | ||
| 275 | def _get_baseurl(self): | 323 | @trigger_group.command( |
| 276 | """Get base URL from settings""" | 324 | name="pokaż", description="Pokaż wszystkie zadania cron" |
| 277 | try: | ||
| 278 | from cogs.utils.settings import get_baseurl | ||
| 279 | |||
| 280 | return get_baseurl() | ||
| 281 | except Exception: | ||
| 282 | return "" | ||
| 283 | |||
| 284 | @commands.slash_command( | ||
| 285 | name="cron_pokaż", description="Pokaż wszystkie zadania cron" | ||
| 286 | ) | 325 | ) |
| 287 | async def cron_list(self, ctx): | 326 | async def cron_list(self, ctx): |
| 288 | """List all cron jobs""" | 327 | """List all cron jobs""" |
| @@ -313,7 +352,7 @@ class Cron(commands.Cog): | |||
| 313 | 352 | ||
| 314 | await ctx.respond(embed=embed) | 353 | await ctx.respond(embed=embed) |
| 315 | 354 | ||
| 316 | @commands.slash_command(name="cron_usuń", description="Usuń zadanie cron z ID") | 355 | @trigger_group.command(name="usuń", description="Usuń zadanie cron z ID") |
| 317 | async def cron_remove( | 356 | async def cron_remove( |
| 318 | self, | 357 | self, |
| 319 | ctx, | 358 | ctx, |
| @@ -331,7 +370,7 @@ class Cron(commands.Cog): | |||
| 331 | 370 | ||
| 332 | await ctx.respond(f"Nie znaleziono zadania cron z ID {job_id}") | 371 | await ctx.respond(f"Nie znaleziono zadania cron z ID {job_id}") |
| 333 | 372 | ||
| 334 | @commands.slash_command(name="cron_pauza", description="Zapauzuj zadanie cron z ID") | 373 | @trigger_group.command(name="pauza", description="Zapauzuj zadanie cron z ID") |
| 335 | async def cron_pause( | 374 | async def cron_pause( |
| 336 | self, | 375 | self, |
| 337 | ctx, | 376 | ctx, |
| @@ -349,7 +388,7 @@ class Cron(commands.Cog): | |||
| 349 | 388 | ||
| 350 | await ctx.respond(f"Nie znaleziono zadania cron z ID {job_id}") | 389 | await ctx.respond(f"Nie znaleziono zadania cron z ID {job_id}") |
| 351 | 390 | ||
| 352 | @commands.slash_command(name="cron_wznów", description="Wznów zadanie cron z ID") | 391 | @trigger_group.command(name="wznów", description="Wznów zadanie cron z ID") |
| 353 | async def cron_resume( | 392 | async def cron_resume( |
| 354 | self, | 393 | self, |
| 355 | ctx, | 394 | ctx, |
diff --git a/cogs/music/play_commands.py b/cogs/music/play_commands.py index 5373436..c53dbc7 100644 --- a/cogs/music/play_commands.py +++ b/cogs/music/play_commands.py | |||
| @@ -18,6 +18,7 @@ from discord.ext import commands | |||
| 18 | 18 | ||
| 19 | from cogs.music import audio_playback, media_processing | 19 | from cogs.music import audio_playback, media_processing |
| 20 | from cogs.utils.file_search import UniversalSearcher | 20 | from cogs.utils.file_search import UniversalSearcher |
| 21 | from cogs.utils.settings import get_uploadurl | ||
| 21 | 22 | ||
| 22 | logger = logging.getLogger(__name__) | 23 | logger = logging.getLogger(__name__) |
| 23 | 24 | ||
| @@ -229,14 +230,13 @@ class PlayCommands(commands.Cog): | |||
| 229 | 230 | ||
| 230 | try: | 231 | try: |
| 231 | logger.info(f"Starting async upload: {file_path}") | 232 | logger.info(f"Starting async upload: {file_path}") |
| 232 | baseurl = os.getenv("COPYPARTY_URL_UPLOAD") | 233 | baseurl = get_uploadurl() |
| 233 | password = os.getenv("COPYPARTY_PASSWORD", "") | 234 | password = os.getenv("COPYPARTY_PASSWORD", "") |
| 234 | 235 | ||
| 235 | logger.info(f"Upload - baseurl: {baseurl}, password set: {bool(password)}") | 236 | logger.info(f"Upload - baseurl: {baseurl}, password set: {bool(password)}") |
| 236 | 237 | ||
| 237 | if not baseurl: | 238 | if baseurl == "http://localhost:3939": |
| 238 | logger.error("COPYPARTY_URL_UPLOAD not set!") | 239 | logger.warning("COPYPARTY_URL_UPLOAD not set! Using default") |
| 239 | return | ||
| 240 | 240 | ||
| 241 | with open(file_path, "rb") as f: | 241 | with open(file_path, "rb") as f: |
| 242 | content = f.read() | 242 | content = f.read() |
diff --git a/cogs/trigger.py b/cogs/trigger.py index 02e5573..ce80d43 100644 --- a/cogs/trigger.py +++ b/cogs/trigger.py | |||
| @@ -7,13 +7,13 @@ import os | |||
| 7 | import discord | 7 | import discord |
| 8 | from discord.ext import commands | 8 | from discord.ext import commands |
| 9 | import random | 9 | import random |
| 10 | import sqlite3 | ||
| 11 | import logging | 10 | import logging |
| 12 | 11 | ||
| 13 | logger = logging.getLogger(__name__) | 12 | from cogs.utils.database import get_connection, init_database |
| 14 | 13 | ||
| 14 | logger = logging.getLogger(__name__) | ||
| 15 | 15 | ||
| 16 | DB_PATH = 'data/triggers.db' | 16 | init_database() |
| 17 | 17 | ||
| 18 | _SELECT_KEYWORD_SQL = "SELECT keyword FROM trigger_keywords WHERE trigger_id=?" | 18 | _SELECT_KEYWORD_SQL = "SELECT keyword FROM trigger_keywords WHERE trigger_id=?" |
| 19 | _SELECT_RESPONSE_SQL = "SELECT response_text FROM trigger_responses WHERE trigger_id=?" | 19 | _SELECT_RESPONSE_SQL = "SELECT response_text FROM trigger_responses WHERE trigger_id=?" |
| @@ -22,88 +22,12 @@ _TRIGGER_NOT_FOUND_MSG = "Trigger not found." | |||
| 22 | _TRIGGER_NAME_OPTION = "the name of the trigger" | 22 | _TRIGGER_NAME_OPTION = "the name of the trigger" |
| 23 | 23 | ||
| 24 | 24 | ||
| 25 | def get_connection(): | ||
| 26 | return sqlite3.connect(DB_PATH) | ||
| 27 | |||
| 28 | |||
| 29 | class Trigger(commands.Cog): | 25 | class Trigger(commands.Cog): |
| 30 | def __init__(self, bot): | 26 | def __init__(self, bot): |
| 31 | self.bot = bot | 27 | self.bot = bot |
| 32 | self.db_check() | ||
| 33 | 28 | ||
| 34 | trigger_group = discord.SlashCommandGroup("trigger", "trigger commands") | 29 | trigger_group = discord.SlashCommandGroup("trigger", "trigger commands") |
| 35 | 30 | ||
| 36 | @staticmethod | ||
| 37 | def db_check(): | ||
| 38 | if not os.path.exists('data'): | ||
| 39 | os.makedirs('data') | ||
| 40 | |||
| 41 | db_exists = os.path.exists(DB_PATH) | ||
| 42 | if not db_exists: | ||
| 43 | logger.warning("Triggers database not found. Creating new database.") | ||
| 44 | |||
| 45 | try: | ||
| 46 | conn = sqlite3.connect(DB_PATH) | ||
| 47 | c = conn.cursor() | ||
| 48 | |||
| 49 | # Enable foreign key constraints | ||
| 50 | c.execute('PRAGMA foreign_keys = ON') | ||
| 51 | |||
| 52 | # Create triggers table | ||
| 53 | c.execute(''' | ||
| 54 | CREATE TABLE IF NOT EXISTS triggers ( | ||
| 55 | id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| 56 | name TEXT NOT NULL UNIQUE, | ||
| 57 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||
| 58 | last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP | ||
| 59 | )''') | ||
| 60 | |||
| 61 | # Create trigger_keywords table with foreign key constraint | ||
| 62 | c.execute(''' | ||
| 63 | CREATE TABLE IF NOT EXISTS trigger_keywords ( | ||
| 64 | id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| 65 | trigger_id INTEGER NOT NULL, | ||
| 66 | keyword TEXT NOT NULL, | ||
| 67 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||
| 68 | FOREIGN KEY (trigger_id) REFERENCES triggers(id) ON DELETE CASCADE, | ||
| 69 | UNIQUE(trigger_id, keyword) | ||
| 70 | )''') | ||
| 71 | |||
| 72 | # Create trigger_responses table with foreign key constraint | ||
| 73 | c.execute(''' | ||
| 74 | CREATE TABLE IF NOT EXISTS trigger_responses ( | ||
| 75 | id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| 76 | trigger_id INTEGER NOT NULL, | ||
| 77 | response_text TEXT NOT NULL, | ||
| 78 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||
| 79 | FOREIGN KEY (trigger_id) REFERENCES triggers(id) ON DELETE CASCADE | ||
| 80 | )''') | ||
| 81 | |||
| 82 | # Create indexes for better query performance | ||
| 83 | c.execute('CREATE INDEX IF NOT EXISTS idx_trigger_keywords_trigger_id ON trigger_keywords(trigger_id)') | ||
| 84 | c.execute('CREATE INDEX IF NOT EXISTS idx_trigger_keywords_keyword ON trigger_keywords(keyword)') | ||
| 85 | c.execute('CREATE INDEX IF NOT EXISTS idx_trigger_responses_trigger_id ON trigger_responses(trigger_id)') | ||
| 86 | |||
| 87 | # Run integrity check | ||
| 88 | integrity_check = c.execute('PRAGMA integrity_check').fetchone() | ||
| 89 | if integrity_check and integrity_check[0] == 'ok': | ||
| 90 | logger.info("Triggers database integrity check passed") | ||
| 91 | else: | ||
| 92 | logger.warning(f"Triggers database integrity check: {integrity_check}") | ||
| 93 | |||
| 94 | conn.commit() | ||
| 95 | |||
| 96 | except sqlite3.Error as e: | ||
| 97 | logger.error(f"Triggers database error: {e}") | ||
| 98 | raise | ||
| 99 | |||
| 100 | finally: | ||
| 101 | if 'conn' in locals(): | ||
| 102 | conn.close() | ||
| 103 | |||
| 104 | if not db_exists: | ||
| 105 | logger.info("Triggers database initialized successfully") | ||
| 106 | |||
| 107 | @commands.Cog.listener() | 31 | @commands.Cog.listener() |
| 108 | async def on_message(self, message): | 32 | async def on_message(self, message): |
| 109 | if message.author == self.bot.user: | 33 | if message.author == self.bot.user: |
| @@ -130,7 +54,7 @@ class Trigger(commands.Cog): | |||
| 130 | 54 | ||
| 131 | @staticmethod | 55 | @staticmethod |
| 132 | def get_type(name): | 56 | def get_type(name): |
| 133 | with sqlite3.connect(DB_PATH) as conn: | 57 | with get_connection() as conn: |
| 134 | c = conn.cursor() | 58 | c = conn.cursor() |
| 135 | c.execute("SELECT name FROM triggers") | 59 | c.execute("SELECT name FROM triggers") |
| 136 | return [row[0] for row in c.fetchall()] | 60 | return [row[0] for row in c.fetchall()] |
diff --git a/cogs/uploader.py b/cogs/uploader.py index 68a3938..df6908e 100644 --- a/cogs/uploader.py +++ b/cogs/uploader.py | |||
| @@ -7,11 +7,12 @@ import os | |||
| 7 | import aiofiles | 7 | import aiofiles |
| 8 | import requests | 8 | import requests |
| 9 | import urllib.parse | 9 | import urllib.parse |
| 10 | from typing import List, Optional | 10 | from typing import Optional |
| 11 | import discord | 11 | import discord |
| 12 | from discord.ext import commands | 12 | from discord.ext import commands |
| 13 | from discord import AutocompleteContext | 13 | from discord import AutocompleteContext |
| 14 | from cogs.utils import file_search as search_utils | 14 | from cogs.utils import file_search as search_utils |
| 15 | from cogs.utils.settings import get_uploadurl | ||
| 15 | import logging | 16 | import logging |
| 16 | 17 | ||
| 17 | logger = logging.getLogger(__name__) | 18 | logger = logging.getLogger(__name__) |
| @@ -23,7 +24,7 @@ class Uploader(commands.Cog): | |||
| 23 | self.bot = bot | 24 | self.bot = bot |
| 24 | self.temp_dir = get_temp_dir() | 25 | self.temp_dir = get_temp_dir() |
| 25 | self._cached_urls = {} | 26 | self._cached_urls = {} |
| 26 | self._uploadurl = os.getenv("COPYPARTY_URL_UPLOAD") | 27 | self._uploadurl = get_uploadurl() |
| 27 | 28 | ||
| 28 | async def _file_autocomplete(self, ctx: AutocompleteContext): | 29 | async def _file_autocomplete(self, ctx: AutocompleteContext): |
| 29 | query = ctx.value.lower() if ctx.value else "" | 30 | query = ctx.value.lower() if ctx.value else "" |
| @@ -77,8 +78,8 @@ class Uploader(commands.Cog): | |||
| 77 | 78 | ||
| 78 | async def _upload_file(self, file_path: str) -> Optional[str]: | 79 | async def _upload_file(self, file_path: str) -> Optional[str]: |
| 79 | """Upload a file and return the URL""" | 80 | """Upload a file and return the URL""" |
| 80 | if not self._uploadurl: | 81 | if self._uploadurl == "http://localhost:3939": |
| 81 | logger.error("COPYPARTY_URL_UPLOAD not set!") | 82 | logger.warning("COPYPARTY_URL_UPLOAD not set! Using default") |
| 82 | return None | 83 | return None |
| 83 | 84 | ||
| 84 | return await self._upload_to_copyparty(file_path) | 85 | return await self._upload_to_copyparty(file_path) |
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("/") | ||
diff --git a/cogs/vclogs.py b/cogs/vclogs.py index d0c4825..404ffe0 100644 --- a/cogs/vclogs.py +++ b/cogs/vclogs.py | |||
| @@ -4,7 +4,6 @@ | |||
| 4 | # https://codeberg.org/salmonik/senator-sperma | 4 | # https://codeberg.org/salmonik/senator-sperma |
| 5 | 5 | ||
| 6 | import datetime | 6 | import datetime |
| 7 | import json | ||
| 8 | import logging | 7 | import logging |
| 9 | import os | 8 | import os |
| 10 | import asyncio | 9 | import asyncio |
| @@ -13,24 +12,69 @@ from discord.ext import commands | |||
| 13 | from gtts import gTTS | 12 | from gtts import gTTS |
| 14 | from cogs.voice import get_voice_manager | 13 | from cogs.voice import get_voice_manager |
| 15 | from cogs.utils.settings import get_volume, save_settings | 14 | from cogs.utils.settings import get_volume, save_settings |
| 15 | from cogs.utils.database import get_connection, init_database | ||
| 16 | 16 | ||
| 17 | import tempfile | 17 | import tempfile |
| 18 | 18 | ||
| 19 | logger = logging.getLogger(__name__) | 19 | logger = logging.getLogger(__name__) |
| 20 | 20 | ||
| 21 | ACTIVITY_FILE = os.path.join(tempfile.gettempdir(), 'voice_activity.json') | 21 | init_database() |
| 22 | 22 | ||
| 23 | 23 | ||
| 24 | def load_activity_data(): | 24 | def load_activity_data(): |
| 25 | if os.path.exists(ACTIVITY_FILE): | 25 | try: |
| 26 | with open(ACTIVITY_FILE, 'r') as f: | 26 | conn = get_connection() |
| 27 | return json.load(f) | 27 | c = conn.cursor() |
| 28 | return {} | 28 | c.execute("SELECT user_id, join_time, total_active, total_afk, is_afk, afk_start, active_start, channel_name FROM voice_activity") |
| 29 | rows = c.fetchall() | ||
| 30 | conn.close() | ||
| 31 | |||
| 32 | return { | ||
| 33 | row[0]: { | ||
| 34 | "join_time": row[1], | ||
| 35 | "total_active": row[2], | ||
| 36 | "total_afk": row[3], | ||
| 37 | "is_afk": bool(row[4]), | ||
| 38 | "afk_start": row[5], | ||
| 39 | "active_start": row[6], | ||
| 40 | "channel_name": row[7], | ||
| 41 | } | ||
| 42 | for row in rows | ||
| 43 | } | ||
| 44 | except Exception as e: | ||
| 45 | logger.error(f"Error loading voice activity: {e}") | ||
| 46 | return {} | ||
| 29 | 47 | ||
| 30 | 48 | ||
| 31 | def save_activity_data(data): | 49 | def save_activity_data(data): |
| 32 | with open(ACTIVITY_FILE, 'w') as f: | 50 | try: |
| 33 | json.dump(data, f, indent=4) | 51 | conn = get_connection() |
| 52 | c = conn.cursor() | ||
| 53 | c.execute("PRAGMA foreign_keys = ON") | ||
| 54 | |||
| 55 | c.execute("DELETE FROM voice_activity") | ||
| 56 | |||
| 57 | for user_id, user_data in data.items(): | ||
| 58 | c.execute( | ||
| 59 | """INSERT INTO voice_activity (user_id, join_time, total_active, total_afk, is_afk, afk_start, active_start, channel_name) | ||
| 60 | VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", | ||
| 61 | ( | ||
| 62 | user_id, | ||
| 63 | user_data.get("join_time"), | ||
| 64 | user_data.get("total_active", 0), | ||
| 65 | user_data.get("total_afk", 0), | ||
| 66 | int(user_data.get("is_afk", False)), | ||
| 67 | user_data.get("afk_start"), | ||
| 68 | user_data.get("active_start"), | ||
| 69 | user_data.get("channel_name"), | ||
| 70 | ), | ||
| 71 | ) | ||
| 72 | |||
| 73 | conn.commit() | ||
| 74 | conn.close() | ||
| 75 | logger.info(f"Saved voice activity for {len(data)} users") | ||
| 76 | except Exception as e: | ||
| 77 | logger.error(f"Error saving voice activity: {e}") | ||
| 34 | 78 | ||
| 35 | 79 | ||
| 36 | def format_duration(seconds): | 80 | def format_duration(seconds): |
| @@ -168,7 +212,7 @@ class VcLogs(commands.Cog): | |||
| 168 | } | 212 | } |
| 169 | save_activity_data(activity_data) | 213 | save_activity_data(activity_data) |
| 170 | 214 | ||
| 171 | message = f"[{current_time}] {member.name} dołączył na {after.channel.name}" | 215 | message = f"[{current_time}] {member.name} dołączył na {after.channel.name} <t:{int(current_ts)}:R>" |
| 172 | tts_text = f"{member.display_name} sie zespawnił" | 216 | tts_text = f"{member.display_name} sie zespawnił" |
| 173 | tts_channel = after.channel | 217 | tts_channel = after.channel |
| 174 | await log_channel.send(message) | 218 | await log_channel.send(message) |
| @@ -189,7 +233,7 @@ class VcLogs(commands.Cog): | |||
| 189 | afk_str = format_duration(data['total_afk']) | 233 | afk_str = format_duration(data['total_afk']) |
| 190 | 234 | ||
| 191 | message = ( | 235 | message = ( |
| 192 | f"[{current_time}] {member.name} rozłączył się z {before.channel.name}\n" | 236 | f"[{current_time}] {member.name} rozłączył się z {before.channel.name} <t:{int(current_ts)}:R>\n" |
| 193 | f"Aktywny: {active_str} | AFK: {afk_str}" | 237 | f"Aktywny: {active_str} | AFK: {afk_str}" |
| 194 | ) | 238 | ) |
| 195 | tts_text = f"{member.display_name} jest chujem" | 239 | tts_text = f"{member.display_name} jest chujem" |
| @@ -200,7 +244,7 @@ class VcLogs(commands.Cog): | |||
| 200 | del activity_data[user_id] | 244 | del activity_data[user_id] |
| 201 | save_activity_data(activity_data) | 245 | save_activity_data(activity_data) |
| 202 | else: | 246 | else: |
| 203 | message = f"[{current_time}] {member.name} rozłączył się z {before.channel.name}" | 247 | message = f"[{current_time}] {member.name} rozłączył się z {before.channel.name} <t:{int(current_ts)}:R>" |
| 204 | tts_text = f"{member.display_name} jest chujem" | 248 | tts_text = f"{member.display_name} jest chujem" |
| 205 | tts_channel = before.channel | 249 | tts_channel = before.channel |
| 206 | await log_channel.send(message) | 250 | await log_channel.send(message) |
| @@ -224,7 +268,7 @@ class VcLogs(commands.Cog): | |||
| 224 | else: | 268 | else: |
| 225 | activity = "odgłuchnął" | 269 | activity = "odgłuchnął" |
| 226 | 270 | ||
| 227 | message = ( f"[{current_time}] {member.name} {activity} się na {after.channel.name}\n" | 271 | message = ( f"[{current_time}] {member.name} {activity} się na {after.channel.name} <t:{int(current_ts)}:R>\n" |
| 228 | f"AFK: {afk_str}" ) | 272 | f"AFK: {afk_str}" ) |
| 229 | await log_channel.send(message) | 273 | await log_channel.send(message) |
| 230 | logger.info(f"User returned from AFK: {message}") | 274 | logger.info(f"User returned from AFK: {message}") |
| @@ -244,7 +288,7 @@ class VcLogs(commands.Cog): | |||
| 244 | activity = "ogłuchnął" | 288 | activity = "ogłuchnął" |
| 245 | 289 | ||
| 246 | active_str = format_duration(active_since_start) | 290 | active_str = format_duration(active_since_start) |
| 247 | message = ( f"[{current_time}] {member.name} {activity} się na {after.channel.name}\n" | 291 | message = ( f"[{current_time}] {member.name} {activity} się na {after.channel.name} <t:{int(current_ts)}:R>\n" |
| 248 | f"Aktywny: {active_str}" ) | 292 | f"Aktywny: {active_str}" ) |
| 249 | await log_channel.send(message) | 293 | await log_channel.send(message) |
| 250 | logger.info(f"User went AFK: {message}") | 294 | logger.info(f"User went AFK: {message}") |
diff --git a/cogs/voice.py b/cogs/voice.py index 319bb16..046c2a8 100644 --- a/cogs/voice.py +++ b/cogs/voice.py | |||
| @@ -45,6 +45,7 @@ class VoiceManager: | |||
| 45 | return voice_client | 45 | return voice_client |
| 46 | except Exception as e: | 46 | except Exception as e: |
| 47 | logger.error(f"Failed to move voice client: {e}") | 47 | logger.error(f"Failed to move voice client: {e}") |
| 48 | logger.info("Disconnecting after failed move attempt") | ||
| 48 | try: | 49 | try: |
| 49 | await voice_client.disconnect() | 50 | await voice_client.disconnect() |
| 50 | except Exception: | 51 | except Exception: |
| @@ -55,6 +56,25 @@ class VoiceManager: | |||
| 55 | voice_client = await channel.connect(timeout=30.0, reconnect=False) | 56 | voice_client = await channel.connect(timeout=30.0, reconnect=False) |
| 56 | logger.info(f"Connected to voice channel: {channel}") | 57 | logger.info(f"Connected to voice channel: {channel}") |
| 57 | return voice_client | 58 | return voice_client |
| 59 | except discord.ClientException as e: | ||
| 60 | if "Already connected" in str(e): | ||
| 61 | logger.warning(f"Stale voice state detected, forcing disconnect and reconnect: {e}") | ||
| 62 | voice_client = discord.utils.get(self.bot.voice_clients, guild=guild) | ||
| 63 | if voice_client: | ||
| 64 | try: | ||
| 65 | await voice_client.disconnect() | ||
| 66 | except Exception: | ||
| 67 | pass | ||
| 68 | await asyncio.sleep(0.3) | ||
| 69 | try: | ||
| 70 | voice_client = await channel.connect(timeout=30.0, reconnect=False) | ||
| 71 | logger.info(f"Reconnected to voice channel: {channel}") | ||
| 72 | return voice_client | ||
| 73 | except Exception as e2: | ||
| 74 | logger.error(f"Failed to reconnect after stale cleanup: {e2}") | ||
| 75 | return None | ||
| 76 | logger.error(f"Failed to connect to voice channel: {e}") | ||
| 77 | return None | ||
| 58 | except Exception as e: | 78 | except Exception as e: |
| 59 | logger.error(f"Failed to connect to voice channel: {e}") | 79 | logger.error(f"Failed to connect to voice channel: {e}") |
| 60 | return None | 80 | return None |
| @@ -113,6 +133,7 @@ class VoiceManager: | |||
| 113 | voice_client = await self.get_voice_client(guild) | 133 | voice_client = await self.get_voice_client(guild) |
| 114 | if voice_client: | 134 | if voice_client: |
| 115 | voice_client.stop() | 135 | voice_client.stop() |
| 136 | logger.info("stop() called - disconnecting from voice") | ||
| 116 | await voice_client.disconnect() | 137 | await voice_client.disconnect() |
| 117 | logger.info(f"Stopped and disconnected from guild {guild}") | 138 | logger.info(f"Stopped and disconnected from guild {guild}") |
| 118 | 139 | ||
| @@ -174,6 +195,7 @@ class Voice(commands.Cog): | |||
| 174 | 195 | ||
| 175 | if voice_client and not voice_client.is_connected(): | 196 | if voice_client and not voice_client.is_connected(): |
| 176 | try: | 197 | try: |
| 198 | logger.info("Voice client exists but not connected - disconnecting stale client") | ||
| 177 | await voice_client.disconnect() | 199 | await voice_client.disconnect() |
| 178 | except Exception: | 200 | except Exception: |
| 179 | pass | 201 | pass |
diff --git a/cogs/web.py b/cogs/web.py index 607d917..eea13b1 100644 --- a/cogs/web.py +++ b/cogs/web.py | |||
| @@ -67,7 +67,7 @@ class Web(commands.Cog): | |||
| 67 | logger.info(f"Query: {query}") | 67 | logger.info(f"Query: {query}") |
| 68 | 68 | ||
| 69 | if response.status_code != 200: | 69 | if response.status_code != 200: |
| 70 | await ctx.respond(content=f"Error: {response.status_code}") | 70 | await ctx.respond(content=f"Error: {response.status_code}", ephemeral=True) |
| 71 | logger.error(f"Error: {response.status_code}") | 71 | logger.error(f"Error: {response.status_code}") |
| 72 | return | 72 | return |
| 73 | 73 | ||
| @@ -75,7 +75,7 @@ class Web(commands.Cog): | |||
| 75 | links = soup.find_all("a", class_="result__a") | 75 | links = soup.find_all("a", class_="result__a") |
| 76 | 76 | ||
| 77 | if not links: | 77 | if not links: |
| 78 | await ctx.respond(content="No results found.") | 78 | await ctx.respond(content="No results found.", ephemeral=True) |
| 79 | logger.warning("No results found") | 79 | logger.warning("No results found") |
| 80 | return | 80 | return |
| 81 | 81 | ||
