aboutsummaryrefslogtreecommitdiffstats
path: root/cogs/cron.py
diff options
context:
space:
mode:
authorsalmonik <raisersoft@proton.me>2026-07-08 02:13:35 +0200
committersalmonik <raisersoft@proton.me>2026-07-08 02:13:35 +0200
commitcd8ed8bc5793985df2c1692072a932ceee517b95 (patch)
tree42d18a4140259b5c68b4dd05b7fcb398f78b8d7f /cogs/cron.py
parentMerge pull request 'Voice related fixes, alongside simplifying assistant' (#5... (diff)
downloadsenator-sperma-cd8ed8bc5793985df2c1692072a932ceee517b95.tar.gz
senator-sperma-cd8ed8bc5793985df2c1692072a932ceee517b95.zip
OPEN THE RUST GATES
Signed-off-by: salmonik <raisersoft@proton.me>
Diffstat (limited to '')
-rw-r--r--cogs/cron.py403
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
6import asyncio
7import logging
8from datetime import datetime
9from venv import logger
10
11import discord
12from croniter import croniter
13from discord.ext import commands
14
15from cogs.utils import file_search as search_utils
16from cogs.voice import get_voice_manager
17from cogs.utils.database import get_connection, init_database
18
19logger = logging.getLogger(__name__)
20
21init_database()
22
23
24class 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
402def setup(bot):
403 bot.add_cog(Cron(bot))