diff options
| author | salmonik <raisersoft@proton.me> | 2026-07-08 02:13:35 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-07-08 02:13:35 +0200 |
| commit | cd8ed8bc5793985df2c1692072a932ceee517b95 (patch) | |
| tree | 42d18a4140259b5c68b4dd05b7fcb398f78b8d7f /cogs/cron.py | |
| parent | Merge pull request 'Voice related fixes, alongside simplifying assistant' (#5... (diff) | |
| download | senator-sperma-cd8ed8bc5793985df2c1692072a932ceee517b95.tar.gz senator-sperma-cd8ed8bc5793985df2c1692072a932ceee517b95.zip | |
OPEN THE RUST GATES
Signed-off-by: salmonik <raisersoft@proton.me>
Diffstat (limited to 'cogs/cron.py')
| -rw-r--r-- | cogs/cron.py | 403 |
1 files changed, 0 insertions, 403 deletions
diff --git a/cogs/cron.py b/cogs/cron.py deleted file mode 100644 index ee88085..0000000 --- a/cogs/cron.py +++ /dev/null | |||
| @@ -1,403 +0,0 @@ | |||
| 1 | # This file is part of Senator Sperma | ||
| 2 | # Licensed under the GPL-3.0 License | ||
| 3 | # Copyright (C) 2026 Salmonik @ salmonik@tuta.io | ||
| 4 | # https://codeberg.org/salmonik/senator-sperma | ||
| 5 | |||
| 6 | import asyncio | ||
| 7 | import logging | ||
| 8 | from datetime import datetime | ||
| 9 | from venv import logger | ||
| 10 | |||
| 11 | import discord | ||
| 12 | from croniter import croniter | ||
| 13 | from discord.ext import commands | ||
| 14 | |||
| 15 | from cogs.utils import file_search as search_utils | ||
| 16 | from cogs.voice import get_voice_manager | ||
| 17 | from cogs.utils.database import get_connection, init_database | ||
| 18 | |||
| 19 | logger = logging.getLogger(__name__) | ||
| 20 | |||
| 21 | init_database() | ||
| 22 | |||
| 23 | |||
| 24 | class Cron(commands.Cog): | ||
| 25 | def __init__(self, bot): | ||
| 26 | self.bot = bot | ||
| 27 | self.scheduled_tasks = [] | ||
| 28 | self.load_tasks() | ||
| 29 | self.task = bot.loop.create_task(self.schedule_checker()) | ||
| 30 | |||
| 31 | def load_tasks(self): | ||
| 32 | try: | ||
| 33 | with get_connection() as conn: | ||
| 34 | c = conn.cursor() | ||
| 35 | c.execute("SELECT id, cron_expression, type, channel_id, active, audio_filename, message FROM cron_jobs") | ||
| 36 | rows = c.fetchall() | ||
| 37 | |||
| 38 | self.scheduled_tasks = [ | ||
| 39 | { | ||
| 40 | "id": row[0], | ||
| 41 | "cron_expression": row[1], | ||
| 42 | "type": row[2], | ||
| 43 | "channel_id": row[3], | ||
| 44 | "active": bool(row[4]), | ||
| 45 | "audio_filename": row[5], | ||
| 46 | "message": row[6], | ||
| 47 | } | ||
| 48 | for row in rows | ||
| 49 | ] | ||
| 50 | logger.info(f"Loaded {len(self.scheduled_tasks)} cron jobs from database") | ||
| 51 | except Exception as e: | ||
| 52 | logger.error(f"Error loading cron jobs: {e}") | ||
| 53 | self.scheduled_tasks = [] | ||
| 54 | |||
| 55 | def save_tasks(self): | ||
| 56 | try: | ||
| 57 | with get_connection() as conn: | ||
| 58 | c = conn.cursor() | ||
| 59 | c.execute("PRAGMA foreign_keys = ON") | ||
| 60 | |||
| 61 | c.execute("DELETE FROM cron_jobs") | ||
| 62 | |||
| 63 | for task in self.scheduled_tasks: | ||
| 64 | c.execute( | ||
| 65 | """INSERT INTO cron_jobs (id, cron_expression, type, channel_id, active, audio_filename, message) | ||
| 66 | VALUES (?, ?, ?, ?, ?, ?, ?)""", | ||
| 67 | ( | ||
| 68 | task.get("id"), | ||
| 69 | task["cron_expression"], | ||
| 70 | task["type"], | ||
| 71 | task["channel_id"], | ||
| 72 | int(task.get("active", True)), | ||
| 73 | task.get("audio_filename"), | ||
| 74 | task.get("message"), | ||
| 75 | ), | ||
| 76 | ) | ||
| 77 | conn.commit() | ||
| 78 | except Exception as e: | ||
| 79 | logger.error(f"Error saving cron jobs: {e}") | ||
| 80 | |||
| 81 | async def schedule_checker(self): | ||
| 82 | """Background task to check and execute scheduled jobs""" | ||
| 83 | await self.bot.wait_until_ready() | ||
| 84 | while not self.bot.is_closed(): | ||
| 85 | try: | ||
| 86 | now = datetime.now() | ||
| 87 | for task in self.scheduled_tasks: | ||
| 88 | if not task.get("active", True): | ||
| 89 | continue | ||
| 90 | if self.should_run_task(task, now): | ||
| 91 | await self.execute_task(task) | ||
| 92 | await asyncio.sleep(30) | ||
| 93 | except Exception as e: | ||
| 94 | logger.error(f"Error in schedule checker: {e}") | ||
| 95 | await asyncio.sleep(30) | ||
| 96 | |||
| 97 | def should_run_task(self, task, now): | ||
| 98 | """Check if a task should run now based on its cron expression""" | ||
| 99 | try: | ||
| 100 | cron_expr = task["cron_expression"] | ||
| 101 | cron = croniter(cron_expr, now) | ||
| 102 | prev_time = cron.get_prev(datetime) | ||
| 103 | time_diff = abs((now - prev_time).total_seconds()) | ||
| 104 | result = time_diff <= 30 | ||
| 105 | logger.debug( | ||
| 106 | f"Cron '{cron_expr}': prev={prev_time}, now={now}, diff={time_diff}, run={result}" | ||
| 107 | ) | ||
| 108 | return result | ||
| 109 | except Exception as e: | ||
| 110 | logger.error( | ||
| 111 | f"Error checking cron expression {task.get('cron_expression')}: {e}" | ||
| 112 | ) | ||
| 113 | return False | ||
| 114 | |||
| 115 | async def execute_task(self, task): | ||
| 116 | """Execute a scheduled task""" | ||
| 117 | try: | ||
| 118 | task_type = task["type"] | ||
| 119 | channel_id = task["channel_id"] | ||
| 120 | |||
| 121 | channel = self.bot.get_channel(channel_id) | ||
| 122 | if not channel: | ||
| 123 | logger.warning(f"Channel {channel_id} not found for task ID: {task.get('id')}") | ||
| 124 | return | ||
| 125 | |||
| 126 | logger.info(f"Found channel {channel} (type: {type(channel).__name__})") | ||
| 127 | |||
| 128 | perms = channel.permissions_for(channel.guild.me) | ||
| 129 | logger.debug( | ||
| 130 | f"Bot permissions: send_messages={perms.send_messages}, attach_files={perms.attach_files}" | ||
| 131 | ) | ||
| 132 | |||
| 133 | if task_type in ("zagraj_audio", "audio"): | ||
| 134 | await self.play_audio_task(task, channel) | ||
| 135 | elif task_type in ("wyślij_wiadomość", "send_message", "wiadomość"): | ||
| 136 | await self.send_message_task(task, channel) | ||
| 137 | |||
| 138 | logger.info(f"Executed task {task['id']} of type {task_type}") | ||
| 139 | except Exception as e: | ||
| 140 | logger.error(f"Error executing task {task.get('id')}: {e}") | ||
| 141 | |||
| 142 | async def play_audio_task(self, task, channel): | ||
| 143 | """Play audio task implementation using voice manager""" | ||
| 144 | audio_url = task["audio_filename"] | ||
| 145 | |||
| 146 | if isinstance(channel, discord.VoiceChannel): | ||
| 147 | voice_mgr = get_voice_manager(self.bot) | ||
| 148 | |||
| 149 | try: | ||
| 150 | audio_source = discord.FFmpegPCMAudio(audio_url) | ||
| 151 | success = await voice_mgr.play_source(channel, audio_source) | ||
| 152 | |||
| 153 | if success: | ||
| 154 | logger.info(f"Finished playing audio: {audio_url}") | ||
| 155 | else: | ||
| 156 | raise Exception("Playback failed") | ||
| 157 | |||
| 158 | await voice_mgr.disconnect(channel.guild) | ||
| 159 | except Exception as e: | ||
| 160 | logger.error(f"Error playing audio: {e}") | ||
| 161 | try: | ||
| 162 | await voice_mgr.disconnect(channel.guild) | ||
| 163 | except Exception: | ||
| 164 | pass | ||
| 165 | if channel.guild.system_channel: | ||
| 166 | await channel.guild.system_channel.send( | ||
| 167 | f"Błąd podczas odgrywania audio: {audio_url}" | ||
| 168 | ) | ||
| 169 | else: | ||
| 170 | if hasattr(channel, "send"): | ||
| 171 | await channel.send("Odtwarzanie audio wymaga kanału głosowego") | ||
| 172 | |||
| 173 | async def send_message_task(self, task, channel): | ||
| 174 | """Send message task implementation""" | ||
| 175 | message = task["message"] | ||
| 176 | logger.info(f"Message to send: '{message}' | Channel: {channel.id}") | ||
| 177 | await channel.send(message) | ||
| 178 | logger.debug("Message sent successfully") | ||
| 179 | |||
| 180 | async def audio_autocomplete(self, ctx: discord.AutocompleteContext): | ||
| 181 | """Autocomplete for audio files from file_search""" | ||
| 182 | query = ctx.value.lower() if ctx.value else "" | ||
| 183 | if not query: | ||
| 184 | return "" | ||
| 185 | |||
| 186 | audio_extensions = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "mp4", "mkv"] | ||
| 187 | results = await search_utils.UniversalSearcher.search_copyparty( | ||
| 188 | query, max_results=25, file_extensions=audio_extensions | ||
| 189 | ) | ||
| 190 | |||
| 191 | choices = [] | ||
| 192 | for url in results[:25]: | ||
| 193 | filename = url.split("/")[-1] | ||
| 194 | display_name = filename[:98] + "…" if len(filename) > 100 else filename | ||
| 195 | choices.append(discord.OptionChoice(name=display_name, value=url)) | ||
| 196 | |||
| 197 | return ( | ||
| 198 | choices | ||
| 199 | if choices | ||
| 200 | else [discord.OptionChoice(name="Brak wyników", value="")] | ||
| 201 | ) | ||
| 202 | |||
| 203 | def _get_baseurl(self): | ||
| 204 | """Get base URL from settings""" | ||
| 205 | try: | ||
| 206 | from cogs.utils.settings import get_baseurl | ||
| 207 | |||
| 208 | return get_baseurl() | ||
| 209 | except Exception: | ||
| 210 | return "" | ||
| 211 | |||
| 212 | trigger_group = discord.SlashCommandGroup("cron", "komendy cron") | ||
| 213 | |||
| 214 | @trigger_group.command( | ||
| 215 | name="dodaj_audio", description="Dodaj nowe zadanie cron audio" | ||
| 216 | ) | ||
| 217 | async def cron_add_audio( | ||
| 218 | self, | ||
| 219 | ctx, | ||
| 220 | cron_expression: str = discord.Option( | ||
| 221 | str, | ||
| 222 | "Ekspresja cron (m H D M Y. '37 21 * * *' wykonuje zadanie o 21:37 codziennie)", | ||
| 223 | required=True, | ||
| 224 | ), | ||
| 225 | channel: discord.VoiceChannel = discord.Option( | ||
| 226 | discord.VoiceChannel, | ||
| 227 | "Kanał głosowy na którym będzie wykonywana akcja", | ||
| 228 | required=True, | ||
| 229 | ), | ||
| 230 | audio_filename: str = discord.Option( | ||
| 231 | str, | ||
| 232 | "Plik audio z serwera copyparty", | ||
| 233 | required=True, | ||
| 234 | autocomplete=audio_autocomplete, | ||
| 235 | ), | ||
| 236 | ): | ||
| 237 | try: | ||
| 238 | croniter(cron_expression) | ||
| 239 | except Exception: | ||
| 240 | await ctx.respond( | ||
| 241 | "Zła składnia cron, zapytaj się lokalnego eksperta o pomoc", | ||
| 242 | ephemeral=True, | ||
| 243 | ) | ||
| 244 | return | ||
| 245 | |||
| 246 | task_id = max([t["id"] for t in self.scheduled_tasks], default=0) + 1 | ||
| 247 | |||
| 248 | task = { | ||
| 249 | "id": task_id, | ||
| 250 | "cron_expression": cron_expression, | ||
| 251 | "type": "audio", | ||
| 252 | "channel_id": channel.id, | ||
| 253 | "active": True, | ||
| 254 | } | ||
| 255 | |||
| 256 | task["audio_filename"] = audio_filename | ||
| 257 | |||
| 258 | self.scheduled_tasks.append(task) | ||
| 259 | self.save_tasks() | ||
| 260 | |||
| 261 | await ctx.respond( | ||
| 262 | f"Zadanie cron dodane. ID: {task_id}\nSkładnia: `{cron_expression}`\nAkcja: audio\nKanał: <#{channel.id}>" | ||
| 263 | ) | ||
| 264 | |||
| 265 | @trigger_group.command( | ||
| 266 | name="dodaj_wiadomość", description="Dodaj nowe zadanie cron wiadomość" | ||
| 267 | ) | ||
| 268 | async def cron_add_message( | ||
| 269 | self, | ||
| 270 | ctx, | ||
| 271 | cron_expression: str = discord.Option( | ||
| 272 | str, | ||
| 273 | "Ekspresja cron (m H D M Y. '37 21 * * *' wykonuje zadanie o 21:37 codziennie)", | ||
| 274 | required=True, | ||
| 275 | ), | ||
| 276 | channel: discord.TextChannel = discord.Option( | ||
| 277 | discord.TextChannel, "Kanał na którym będzie wykonana akcja", required=True | ||
| 278 | ), | ||
| 279 | message: str = discord.Option(str, "Wiadomość do wysłania", required=True), | ||
| 280 | ): | ||
| 281 | """Add a new cron job""" | ||
| 282 | |||
| 283 | try: | ||
| 284 | croniter(cron_expression) | ||
| 285 | except Exception: | ||
| 286 | await ctx.respond( | ||
| 287 | "Zła składnia cron, zapytaj się lokalnego eksperta o pomoc", | ||
| 288 | ephemeral=True, | ||
| 289 | ) | ||
| 290 | return | ||
| 291 | |||
| 292 | task_id = max([t["id"] for t in self.scheduled_tasks], default=0) + 1 | ||
| 293 | |||
| 294 | task = { | ||
| 295 | "id": task_id, | ||
| 296 | "cron_expression": cron_expression, | ||
| 297 | "type": "wiadomość", | ||
| 298 | "channel_id": channel.id, | ||
| 299 | "active": True, | ||
| 300 | "message": message, | ||
| 301 | } | ||
| 302 | |||
| 303 | self.scheduled_tasks.append(task) | ||
| 304 | self.save_tasks() | ||
| 305 | |||
| 306 | await ctx.respond( | ||
| 307 | f"Zadanie cron dodane. ID: {task_id}\nSkładnia: `{cron_expression}`\nAkcja: wiadomość\nKanał: <#{channel.id}>" | ||
| 308 | ) | ||
| 309 | |||
| 310 | @trigger_group.command( | ||
| 311 | name="pokaż", description="Pokaż wszystkie zadania cron" | ||
| 312 | ) | ||
| 313 | async def cron_list(self, ctx): | ||
| 314 | """List all cron jobs""" | ||
| 315 | if not self.scheduled_tasks: | ||
| 316 | await ctx.respond("Brak zadań cron") | ||
| 317 | return | ||
| 318 | |||
| 319 | embed = discord.Embed(title="Zaplanowane prace cron", color=0x00FF00) | ||
| 320 | |||
| 321 | for task in self.scheduled_tasks: | ||
| 322 | channel = self.bot.get_channel(task["channel_id"]) | ||
| 323 | channel_name = f"#{channel}" | ||
| 324 | status = "AKTYWNA" if task.get("active", True) else "PAUZA" | ||
| 325 | |||
| 326 | if task["type"] in ("zagraj_audio", "audio"): | ||
| 327 | action_desc = f"Zagraj audio: `{task.get('audio_filename', 'N/A')}`" | ||
| 328 | else: | ||
| 329 | msg = task.get("message", "") | ||
| 330 | action_desc = ( | ||
| 331 | f"Wyślij wiadomość: `{msg[:50]}{'...' if len(msg) > 50 else ''}`" | ||
| 332 | ) | ||
| 333 | |||
| 334 | embed.add_field( | ||
| 335 | name=f"Zadanie #{task['id']} ({status})", | ||
| 336 | value=f"**Czas:** `{task['cron_expression']}`\n**Akcja:** {action_desc}\n**Kanał:** {channel_name}", | ||
| 337 | inline=False, | ||
| 338 | ) | ||
| 339 | |||
| 340 | await ctx.respond(embed=embed) | ||
| 341 | |||
| 342 | @trigger_group.command(name="usuń", description="Usuń zadanie cron z ID") | ||
| 343 | async def cron_remove( | ||
| 344 | self, | ||
| 345 | ctx, | ||
| 346 | job_id: int = discord.Option( | ||
| 347 | int, "ID zadania cron do usunięcia", required=True | ||
| 348 | ), | ||
| 349 | ): | ||
| 350 | """Remove a cron job by ID""" | ||
| 351 | for i, task in enumerate(self.scheduled_tasks): | ||
| 352 | if task["id"] == job_id: | ||
| 353 | self.scheduled_tasks.pop(i) | ||
| 354 | self.save_tasks() | ||
| 355 | await ctx.respond(f"Usunięto zadanie cron #{job_id}") | ||
| 356 | return | ||
| 357 | |||
| 358 | await ctx.respond(f"Nie znaleziono zadania cron z ID {job_id}") | ||
| 359 | |||
| 360 | @trigger_group.command(name="pauza", description="Zapauzuj zadanie cron z ID") | ||
| 361 | async def cron_pause( | ||
| 362 | self, | ||
| 363 | ctx, | ||
| 364 | job_id: int = discord.Option( | ||
| 365 | int, "ID zadania cron do zatrzymania", required=True | ||
| 366 | ), | ||
| 367 | ): | ||
| 368 | """Pause a cron job by ID""" | ||
| 369 | for task in self.scheduled_tasks: | ||
| 370 | if task["id"] == job_id: | ||
| 371 | task["active"] = False | ||
| 372 | self.save_tasks() | ||
| 373 | await ctx.respond(f"Zapauzowano zadanie cron #{job_id}") | ||
| 374 | return | ||
| 375 | |||
| 376 | await ctx.respond(f"Nie znaleziono zadania cron z ID {job_id}") | ||
| 377 | |||
| 378 | @trigger_group.command(name="wznów", description="Wznów zadanie cron z ID") | ||
| 379 | async def cron_resume( | ||
| 380 | self, | ||
| 381 | ctx, | ||
| 382 | job_id: int = discord.Option( | ||
| 383 | int, "ID zadania cron do wznowienia", required=True | ||
| 384 | ), | ||
| 385 | ): | ||
| 386 | """Resume a paused cron job by ID""" | ||
| 387 | for task in self.scheduled_tasks: | ||
| 388 | if task["id"] == job_id: | ||
| 389 | task["active"] = True | ||
| 390 | self.save_tasks() | ||
| 391 | await ctx.respond(f"Wznowiono zadanie cron z ID #{job_id}") | ||
| 392 | return | ||
| 393 | |||
| 394 | await ctx.respond(f"Nie znaleziono zadania cron z ID {job_id}") | ||
| 395 | |||
| 396 | def cog_unload(self): | ||
| 397 | """Clean up when cog is unloaded""" | ||
| 398 | if hasattr(self, "task"): | ||
| 399 | self.task.cancel() | ||
| 400 | |||
| 401 | |||
| 402 | def setup(bot): | ||
| 403 | bot.add_cog(Cron(bot)) | ||
