DiscordBotLinuxMonitor 1.6.7__tar.gz → 1.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: DiscordBotLinuxMonitor
3
- Version: 1.6.7
3
+ Version: 1.7.0
4
4
  Summary: From discord channels: Get information and warning status of Linux server like service, port, ping, ssl certificate, disk/folder/cpu/ram/swap usage, ip connection, ... (Python and shell library, Linux ONLY)
5
5
  Home-page: https://github.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library
6
6
  Author: Quentin Comte-Gaz
@@ -22,7 +22,7 @@ Classifier: Environment :: Console
22
22
  Requires-Python: >=3.3
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE.md
25
- Requires-Dist: linuxmonitor~=1.5.9
25
+ Requires-Dist: linuxmonitor~=1.5.12
26
26
  Requires-Dist: discord.py
27
27
  Requires-Dist: typing
28
28
  Requires-Dist: asyncio
@@ -1,4 +1,4 @@
1
- linuxmonitor~=1.5.9
1
+ linuxmonitor~=1.5.12
2
2
  discord.py
3
3
  typing
4
4
  asyncio
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: DiscordBotLinuxMonitor
3
- Version: 1.6.7
3
+ Version: 1.7.0
4
4
  Summary: From discord channels: Get information and warning status of Linux server like service, port, ping, ssl certificate, disk/folder/cpu/ram/swap usage, ip connection, ... (Python and shell library, Linux ONLY)
5
5
  Home-page: https://github.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library
6
6
  Author: Quentin Comte-Gaz
@@ -22,7 +22,7 @@ Classifier: Environment :: Console
22
22
  Requires-Python: >=3.3
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE.md
25
- Requires-Dist: linuxmonitor~=1.5.9
25
+ Requires-Dist: linuxmonitor~=1.5.12
26
26
  Requires-Dist: discord.py
27
27
  Requires-Dist: typing
28
28
  Requires-Dist: asyncio
@@ -52,102 +52,162 @@ def main() -> None:
52
52
  await discord_bot_linux_monitor.on_ready()
53
53
 
54
54
  @discord_bot.tree.command(name="force_sync", description="[Private] 🔄 Force command synchronization 🔄")
55
+ @app_commands.checks.cooldown(3, 20.0)
55
56
  async def force_sync(interaction: discord.Interaction) -> None: # type: ignore
56
57
  await discord_bot_linux_monitor.force_sync(interaction)
57
58
 
58
59
  @discord_bot.tree.command(name="version", description="🤖 Show bot version 🤖")
60
+ @app_commands.checks.cooldown(3, 20.0)
59
61
  async def version(interaction: discord.Interaction) -> None: # type: ignore
60
62
  await discord_bot_linux_monitor.version(interaction)
61
63
 
62
64
  @discord_bot.tree.command(name="usage", description="📊 View disk space, CPU, RAM, ... 📊")
65
+ @app_commands.checks.cooldown(3, 20.0)
63
66
  async def usage(interaction: discord.Interaction) -> None: # type: ignore
64
67
  await discord_bot_linux_monitor.usage(interaction)
65
68
 
66
69
  @discord_bot.tree.command(name="os_infos", description="🖥️ View basic system information 🖥️")
70
+ @app_commands.checks.cooldown(3, 20.0)
67
71
  async def os_infos(interaction: discord.Interaction) -> None: # type: ignore
68
72
  await discord_bot_linux_monitor.os_infos(interaction)
69
73
 
70
74
  @discord_bot.tree.command(name="users", description="[Private] 👥 View connected users 👥")
75
+ @app_commands.checks.cooldown(3, 20.0)
71
76
  async def users(interaction: discord.Interaction) -> None: # type: ignore
72
77
  await discord_bot_linux_monitor.users(interaction)
73
78
 
74
79
  @discord_bot.tree.command(name="user_logins", description="[Private] 👥 View last user connections 👥")
80
+ @app_commands.checks.cooldown(3, 20.0)
75
81
  async def user_logins(interaction: discord.Interaction) -> None: # type: ignore
76
82
  await discord_bot_linux_monitor.user_logins(interaction)
77
83
 
78
84
  @discord_bot.tree.command(name="ping", description="🌐 Ping websites 🌐")
85
+ @app_commands.checks.cooldown(3, 20.0)
79
86
  async def ping(interaction: discord.Interaction) -> None: # type: ignore
80
87
  await discord_bot_linux_monitor.ping(interaction)
81
88
 
82
89
  @discord_bot.tree.command(name="websites", description="🌐 Check websites access (GET requests) 🌐")
90
+ @app_commands.checks.cooldown(3, 20.0)
83
91
  async def websites(interaction: discord.Interaction) -> None: # type: ignore
84
92
  await discord_bot_linux_monitor.websites(interaction)
85
93
 
86
94
  @discord_bot.tree.command(name="certificates", description="🔒 Check SSL certificates 🔒")
95
+ @app_commands.checks.cooldown(3, 20.0)
87
96
  async def certificates(interaction: discord.Interaction) -> None: # type: ignore
88
97
  await discord_bot_linux_monitor.certificates(interaction)
89
98
 
90
99
  @discord_bot.tree.command(name="reboot_server", description="[Private] 🔄 Restart the entire server 🔄")
100
+ @app_commands.checks.cooldown(1, 60.0) # 1 use per 60 seconds - dangerous operation
91
101
  async def reboot(interaction: discord.Interaction) -> None: # type: ignore
92
102
  await discord_bot_linux_monitor.reboot(interaction)
93
103
 
94
104
  @discord_bot.tree.command(name="services_status", description="🩺 Check services are running 🩺")
105
+ @app_commands.checks.cooldown(3, 20.0)
95
106
  async def services_status(interaction: discord.Interaction) -> None: # type: ignore
96
107
  await discord_bot_linux_monitor.services_status(interaction)
97
108
 
98
109
  @discord_bot.tree.command(name="restart_all", description="🚀 Restart all services 🚀")
110
+ @app_commands.checks.cooldown(1, 30.0) # 1 use per 30 seconds
99
111
  async def restart_all(interaction: discord.Interaction) -> None: # type: ignore
100
112
  await discord_bot_linux_monitor.restart_all(interaction)
101
113
 
102
114
  @discord_bot.tree.command(name="restart_service", description="🚀 Restart a service 🚀")
115
+ @app_commands.autocomplete(service_name=discord_bot_linux_monitor.autocomplete_service_name)
116
+ @app_commands.checks.cooldown(3, 20.0) # 3 uses per 20 seconds
103
117
  async def restart_service(interaction: discord.Interaction, service_name: str) -> None: # type: ignore
104
118
  await discord_bot_linux_monitor.restart_service(interaction, service_name)
105
119
 
106
120
  @discord_bot.tree.command(name="stop_service", description="🚫 Stop a service 🚫")
121
+ @app_commands.autocomplete(service_name=discord_bot_linux_monitor.autocomplete_service_name)
122
+ @app_commands.checks.cooldown(3, 20.0) # 3 uses per 20 seconds
107
123
  async def stop_service(interaction: discord.Interaction, service_name: str) -> None: # type: ignore
108
124
  await discord_bot_linux_monitor.stop_service(interaction, service_name)
109
125
 
110
126
  @discord_bot.tree.command(name="list_services", description="📋 List all available services 📋")
127
+ @app_commands.checks.cooldown(3, 20.0)
111
128
  async def list_services(interaction: discord.Interaction) -> None: # type: ignore
112
129
  await discord_bot_linux_monitor.list_services(interaction)
113
130
 
114
131
  @discord_bot.tree.command(name="ports", description="🔒 Check ports 🔒")
132
+ @app_commands.checks.cooldown(3, 20.0)
115
133
  async def ports(interaction: discord.Interaction) -> None: # type: ignore
116
134
  await discord_bot_linux_monitor.ports(interaction)
117
135
 
118
136
  @discord_bot.tree.command(name="list_processes", description="[Private] 📋 List active processes (ordered by RAM usage) 📋")
137
+ @app_commands.checks.cooldown(3, 20.0)
119
138
  async def list_processes(interaction: discord.Interaction) -> None: # type: ignore
120
139
  await discord_bot_linux_monitor.list_processes(interaction, order_by_ram=True)
121
140
 
122
141
  @discord_bot.tree.command(name="list_processes_by_cpu_usage", description="[Private] 📋 List active processes (ordered by CPU usage) 📋")
142
+ @app_commands.checks.cooldown(3, 20.0)
123
143
  async def list_processes_by_cpu_usage(interaction: discord.Interaction) -> None: # type: ignore
124
144
  await discord_bot_linux_monitor.list_processes(interaction, order_by_ram=False)
125
145
 
126
146
  @discord_bot.tree.command(name="kill_process", description="[Private] 🚫 Stop a process by PID 🚫")
147
+ @app_commands.checks.cooldown(3, 20.0)
127
148
  async def kill_process(interaction: discord.Interaction, pid: int) -> None: # type: ignore
128
149
  await discord_bot_linux_monitor.kill_process(interaction, pid)
129
150
 
130
151
  @discord_bot.tree.command(name="clear_channel_messages", description="[Private] 🧹 Remove all messages from a channel 🧹")
152
+ @app_commands.checks.cooldown(1, 120.0) # 1 use per 120 seconds (2 minutes) - very destructive
131
153
  async def clear_channel_messages(interaction: discord.Interaction, channel: discord.TextChannel) -> None: # type: ignore
132
154
  await discord_bot_linux_monitor.clear_channel_messages(interaction, channel)
133
155
 
134
156
  @discord_bot.tree.command(name="list_clearable_channels", description="[Private] 🧹 List text channels and bot clear permissions 🧹")
157
+ @app_commands.checks.cooldown(3, 20.0)
135
158
  async def list_clearable_channels(interaction: discord.Interaction) -> None: # type: ignore
136
159
  await discord_bot_linux_monitor.list_clearable_channels(interaction)
137
160
 
138
161
  @discord_bot.tree.command(name="list_commands", description="📋 List all available commands 📋")
162
+ @app_commands.checks.cooldown(3, 20.0)
139
163
  async def list_commands(interaction: discord.Interaction) -> None: # type: ignore
140
164
  await discord_bot_linux_monitor.list_commands(interaction)
141
165
 
166
+ @discord_bot.tree.command(name="help", description="🔍 Show Discord bot commands with info and cooldowns 🔍")
167
+ @app_commands.checks.cooldown(3, 20.0)
168
+ async def help_command(interaction: discord.Interaction, command_name: str = "") -> None: # type: ignore
169
+ await discord_bot_linux_monitor.show_help(interaction, command_name)
170
+
171
+ @discord_bot.tree.command(name="list_periodic_channels_cleanup", description="🧹 Show auto-cleanup channel configuration and permission status 🧹")
172
+ @app_commands.checks.cooldown(3, 20.0)
173
+ async def list_periodic_channels_cleanup(interaction: discord.Interaction) -> None: # type: ignore
174
+ await discord_bot_linux_monitor.show_list_periodic_channels_cleanup(interaction)
175
+
142
176
  @discord_bot.tree.command(name="execute_command", description="🚀 Execute a command (optional parameters) 🚀")
143
177
  @app_commands.autocomplete(command_name=discord_bot_linux_monitor.autocomplete_command_name)
178
+ @app_commands.checks.cooldown(3, 20.0) # 3 uses per 20 seconds
144
179
  async def execute_command(interaction: discord.Interaction, command_name: str, parameters: str = "") -> None: # type: ignore
145
180
  await discord_bot_linux_monitor.execute_command(interaction, command_name=command_name, parameters=parameters)
146
181
 
147
182
  @discord_bot.tree.command(name="execute_all_commands", description="🚀 Execute all commands 🚀")
183
+ @app_commands.checks.cooldown(1, 60.0) # 1 use per 60 seconds
148
184
  async def execute_all_commands(interaction: discord.Interaction) -> None: # type: ignore
149
185
  await discord_bot_linux_monitor.execute_all_commands(interaction)
150
186
 
187
+ @discord_bot.tree.error
188
+ async def on_app_command_error(interaction: discord.Interaction, error: app_commands.AppCommandError) -> None: # type: ignore
189
+ """Handle cooldown and other command errors gracefully."""
190
+ if isinstance(error, app_commands.CommandOnCooldown):
191
+ retry_after = error.retry_after
192
+ await interaction.response.send_message(
193
+ content=f"⏳ This command is on cooldown. Please wait {retry_after:.1f} seconds before using it again.",
194
+ ephemeral=True
195
+ )
196
+ elif isinstance(error, app_commands.MissingPermissions):
197
+ await interaction.response.send_message(
198
+ content="❌ You don't have permission to use this command.",
199
+ ephemeral=True
200
+ )
201
+ else:
202
+ logging.error(msg=f"Unhandled command error: {error}")
203
+ try:
204
+ await interaction.response.send_message(
205
+ content="❌ An unexpected error occurred. Please try again later.",
206
+ ephemeral=True
207
+ )
208
+ except discord.InteractionResponded:
209
+ logging.warning(msg="Interaction already responded when handling error")
210
+
151
211
  #endregion
152
212
 
153
213
  # Start the discord bot
@@ -33,7 +33,7 @@ __email__ = "quentin@comte-gaz.com"
33
33
  __license__ = "MIT License"
34
34
  __copyright__ = "Copyright Quentin Comte-Gaz (2026)"
35
35
  __python_version__ = "3.+"
36
- __version__ = "1.6.7 (2026/08/24)"
36
+ __version__ = "1.7.0 (2026/08/29)"
37
37
  __status__ = "Usable for any Linux project"
38
38
 
39
39
  # pyright: reportMissingTypeStubs=false
@@ -44,13 +44,76 @@ from discord.app_commands.models import AppCommand
44
44
  from discord import app_commands
45
45
  from discord.ext import commands
46
46
  import json
47
- from typing import List, Union, Awaitable, Callable, Any, Dict, Optional
47
+ from typing import List, Union, Awaitable, Callable, Any, Dict, Optional, Tuple
48
48
  from datetime import datetime, timedelta, timezone
49
49
 
50
50
  import asyncio
51
-
51
+ import functools
52
+ import time
52
53
  import logging
53
54
 
55
+
56
+ class ConfirmationView(discord.ui.View):
57
+ """Confirmation dialog for dangerous commands."""
58
+ def __init__(self, timeout: float = 30.0):
59
+ super().__init__(timeout=timeout)
60
+ self.confirmed: bool = False
61
+
62
+ @discord.ui.button(label="✅ Confirm", style=discord.ButtonStyle.red)
63
+ async def confirm_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
64
+ self.confirmed = True
65
+ await interaction.response.defer()
66
+ self.stop()
67
+
68
+ @discord.ui.button(label="❌ Cancel", style=discord.ButtonStyle.gray)
69
+ async def cancel_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
70
+ self.confirmed = False
71
+ await interaction.response.defer()
72
+ self.stop()
73
+
74
+ async def on_timeout(self) -> None:
75
+ self.confirmed = False
76
+
77
+
78
+ class PaginationView(discord.ui.View):
79
+ """Pagination view for long messages with prev/next buttons."""
80
+ def __init__(self, chunks: List[str], timeout: float = 180.0):
81
+ super().__init__(timeout=timeout)
82
+ self.chunks = chunks
83
+ self.current_page: int = 0
84
+ self.total_pages: int = len(chunks)
85
+ self.message: Optional[discord.Message] = None
86
+ self._update_button_states()
87
+
88
+ def _update_button_states(self) -> None:
89
+ """Update button disabled states based on current page."""
90
+ self.prev_button.disabled = self.current_page <= 0
91
+ self.next_button.disabled = self.current_page >= self.total_pages - 1
92
+
93
+ @discord.ui.button(label="◀️ Previous", style=discord.ButtonStyle.gray)
94
+ async def prev_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
95
+ if self.current_page > 0:
96
+ self.current_page -= 1
97
+ self._update_button_states()
98
+ await interaction.response.defer()
99
+ if self.message:
100
+ await self.message.edit(view=self)
101
+
102
+ @discord.ui.button(label="▶️ Next", style=discord.ButtonStyle.gray)
103
+ async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
104
+ if self.current_page < self.total_pages - 1:
105
+ self.current_page += 1
106
+ self._update_button_states()
107
+ await interaction.response.defer()
108
+ if self.message:
109
+ await self.message.edit(view=self)
110
+
111
+ async def on_timeout(self) -> None:
112
+ """Disable buttons after timeout."""
113
+ for item in self.children:
114
+ item.disabled = True
115
+
116
+
54
117
  class DiscordBotLinuxMonitor:
55
118
 
56
119
  #region Initialization
@@ -74,6 +137,9 @@ class DiscordBotLinuxMonitor:
74
137
  self.force_sync_on_startup: bool = force_sync_on_startup
75
138
  intents: discord.Intents = discord.Intents.default()
76
139
  self.bot = commands.Bot(command_prefix=self.command_prefix, intents=intents)
140
+
141
+ # Initialize cleanup task
142
+ self.cleanup_task: Optional[asyncio.Task] = None
77
143
 
78
144
  def _init_and_check_configuration(self) -> None:
79
145
  """
@@ -166,6 +232,47 @@ class DiscordBotLinuxMonitor:
166
232
 
167
233
  #region Private methods
168
234
 
235
+ def _get_utc_timestamp(self) -> str:
236
+ """Get current timestamp in UTC with timezone info."""
237
+ return datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')
238
+
239
+ def _log_command_audit(self, user: discord.User, guild: Optional[discord.Guild], channel: Optional[discord.TextChannel], command: str, details: str = "") -> None:
240
+ """Log command execution for audit trail."""
241
+ timestamp = self._get_utc_timestamp()
242
+ guild_name = guild.name if guild else "Unknown"
243
+ channel_name = channel.name if channel else "Unknown"
244
+ details_str = f" | {details}" if details else ""
245
+ logging.info(msg=f"[AUDIT] {timestamp} | User: {user} (ID: {user.id}) | Guild: {guild_name} | Channel: #{channel_name} | Command: {command}{details_str}")
246
+
247
+ @functools.lru_cache(maxsize=128)
248
+ def _get_cached_command_names(self, is_private: bool) -> List[Tuple[str, str]]:
249
+ """Cache command names permanently since they never change at runtime."""
250
+ return self.monitoring.get_command_names(is_private=is_private)
251
+
252
+ @functools.lru_cache(maxsize=128)
253
+ def _get_cached_service_names(self, is_private: bool) -> List[Tuple[str, str]]:
254
+ """Cache service names permanently since they never change at runtime."""
255
+ return self.monitoring.get_service_names(is_private=is_private)
256
+
257
+ def _is_periodic_cleanup_enabled(self) -> bool:
258
+ """Check if periodic channel cleanup is enabled in config."""
259
+ cleanup_config = self.config.get('periodic_channel_cleanup', {}) # type: ignore
260
+ return cleanup_config.get('enabled', False)
261
+
262
+ def _get_cleanup_interval(self) -> float:
263
+ """Get the interval (in seconds) between cleanup cycles."""
264
+ cleanup_config = self.config.get('periodic_channel_cleanup', {}) # type: ignore
265
+ return float(cleanup_config.get('duration_in_sec_wait_between_each_execution', 604800))
266
+
267
+ def _get_cleanup_initial_delay(self) -> float:
268
+ """Get the initial delay (in seconds) before first cleanup execution."""
269
+ cleanup_config = self.config.get('periodic_channel_cleanup', {}) # type: ignore
270
+ return float(cleanup_config.get('duration_in_sec_before_first_execution', 604800))
271
+
272
+ def _should_cleanup_start_immediately(self) -> bool:
273
+ """Check if cleanup should start immediately (duration = 0)."""
274
+ return self._get_cleanup_initial_delay() == 0
275
+
169
276
  def _check_if_valid_guild(self, guild: Union[None,discord.Guild]) -> bool:
170
277
  if guild is None:
171
278
  return False
@@ -325,7 +432,7 @@ class DiscordBotLinuxMonitor:
325
432
 
326
433
  embed.add_field(name="🕗 Last deleted message", value=(last_deleted_preview if last_deleted_preview != "" else "N/A")[:1024], inline=False)
327
434
 
328
- embed.set_footer(text=f"Bot v{__version__} • Last update: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
435
+ embed.set_footer(text=f"Bot v{__version__} • Last update: {self._get_utc_timestamp()}")
329
436
  return embed
330
437
 
331
438
  def _infer_embed_color(self, text: str, is_error: bool = False) -> "discord.Color":
@@ -339,7 +446,7 @@ class DiscordBotLinuxMonitor:
339
446
  def _build_result_embed(self, title: str, description: str, color: "discord.Color") -> "discord.Embed":
340
447
  embed = discord.Embed(title=title[:256], color=color)
341
448
  embed.description = (description if description.strip() != "" else "No answer.")[:4096]
342
- embed.set_footer(text=f"Bot v{__version__} • {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
449
+ embed.set_footer(text=f"Bot v{__version__} • {self._get_utc_timestamp()}")
343
450
  return embed
344
451
 
345
452
  def _split_text_for_embed(self, text: str, max_length: int = 4000) -> List[str]:
@@ -355,21 +462,87 @@ class DiscordBotLinuxMonitor:
355
462
  chunks.append(remaining)
356
463
  return chunks
357
464
 
358
- async def _interaction_followup_send_embed(self, interaction: discord.Interaction, title: str, msg: str, icon: str = "", is_error: bool = False, ephemeral: bool = False) -> None:
465
+ async def _interaction_followup_send_embed(self, interaction: discord.Interaction, title: str, msg: str, icon: str = "", is_error: bool = False, ephemeral: bool = False, enable_pagination: bool = True) -> None:
466
+ """Send an embed message via interaction followup. Handles pagination for long messages."""
359
467
  if msg is None or msg.strip() == "":
360
468
  msg = "No answer."
361
469
 
362
- logging.info(msg=f"Sending embed follow-up '{title}':\n{msg}")
470
+ # Only log info for non-error responses to reduce log spam
471
+ if not is_error:
472
+ logging.info(msg=f"Sending embed follow-up '{title}'")
363
473
 
364
474
  color: "discord.Color" = self._infer_embed_color(text=msg, is_error=is_error)
365
475
  display_title: str = f"{icon} {title}".strip()
366
476
  chunks: List[str] = self._split_text_for_embed(text=msg)
367
477
 
368
478
  try:
369
- for index, chunk in enumerate(chunks):
370
- page_title: str = display_title if len(chunks) == 1 else f"{display_title} ({index + 1}/{len(chunks)})"
371
- embed: "discord.Embed" = self._build_result_embed(title=page_title, description=chunk, color=color)
372
- await interaction.followup.send(embed=embed, ephemeral=ephemeral)
479
+ # If single chunk or pagination disabled, send normally
480
+ if len(chunks) <= 1 or not enable_pagination:
481
+ for index, chunk in enumerate(chunks):
482
+ page_title: str = display_title if len(chunks) == 1 else f"{display_title} ({index + 1}/{len(chunks)})"
483
+ embed: "discord.Embed" = self._build_result_embed(title=page_title, description=chunk, color=color)
484
+ try:
485
+ await interaction.followup.send(embed=embed, ephemeral=ephemeral)
486
+ except discord.InteractionResponded:
487
+ logging.debug(msg=f"Interaction already responded for {title}")
488
+ return
489
+ else:
490
+ # Multiple chunks: use pagination
491
+ class PaginatedEmbedView(discord.ui.View):
492
+ def __init__(self, paginator_self, chunks: List[str], title_template: str, color: discord.Color, ephemeral: bool):
493
+ super().__init__(timeout=180.0)
494
+ self.paginator = paginator_self
495
+ self.chunks = chunks
496
+ self.title_template = title_template
497
+ self.color = color
498
+ self.current_page = 0
499
+ self.message: Optional[discord.Message] = None
500
+ self.ephemeral = ephemeral
501
+ self._update_buttons()
502
+
503
+ def _update_buttons(self) -> None:
504
+ self.prev_btn.disabled = self.current_page <= 0
505
+ self.next_btn.disabled = self.current_page >= len(self.chunks) - 1
506
+
507
+ async def _update_embed(self, interaction: discord.Interaction) -> None:
508
+ chunk = self.chunks[self.current_page]
509
+ page_title = f"{self.title_template} ({self.current_page + 1}/{len(self.chunks)})"
510
+ embed = self.paginator._build_result_embed(title=page_title, description=chunk, color=self.color)
511
+ await interaction.response.defer()
512
+ if self.message:
513
+ await self.message.edit(embed=embed, view=self)
514
+
515
+ @discord.ui.button(label="◀️", style=discord.ButtonStyle.gray)
516
+ async def prev_btn(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
517
+ if self.current_page > 0:
518
+ self.current_page -= 1
519
+ self._update_buttons()
520
+ await self._update_embed(interaction)
521
+
522
+ @discord.ui.button(label="▶️", style=discord.ButtonStyle.gray)
523
+ async def next_btn(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
524
+ if self.current_page < len(self.chunks) - 1:
525
+ self.current_page += 1
526
+ self._update_buttons()
527
+ await self._update_embed(interaction)
528
+
529
+ async def on_timeout(self) -> None:
530
+ for item in self.children:
531
+ item.disabled = True
532
+ if self.message:
533
+ try:
534
+ await self.message.edit(view=self)
535
+ except Exception:
536
+ pass
537
+
538
+ view = PaginatedEmbedView(self, chunks, display_title, color, ephemeral)
539
+ embed = self._build_result_embed(title=f"{display_title} (1/{len(chunks)})", description=chunks[0], color=color)
540
+ try:
541
+ view.message = await interaction.followup.send(embed=embed, view=view, ephemeral=ephemeral)
542
+ except discord.InteractionResponded:
543
+ logging.debug(msg=f"Interaction already responded for {title}")
544
+ return
545
+
373
546
  except Exception as e:
374
547
  logging.error(msg=f"Error while sending embed follow-up message: {e}")
375
548
 
@@ -439,6 +612,97 @@ class DiscordBotLinuxMonitor:
439
612
  continue
440
613
  raise
441
614
 
615
+ async def _setup_periodic_cleanup_task(self) -> None:
616
+ """Setup and start the periodic channel cleanup task."""
617
+ if not self._is_periodic_cleanup_enabled():
618
+ logging.info(msg="Periodic channel cleanup is disabled in config")
619
+ return
620
+
621
+ logging.info(msg="Setting up periodic channel cleanup task")
622
+ self.cleanup_task = asyncio.create_task(self._periodic_cleanup_loop())
623
+
624
+ async def _periodic_cleanup_loop(self) -> None:
625
+ """Loop that periodically cleans configured channels."""
626
+ await self.bot.wait_until_ready()
627
+
628
+ # Wait before first execution if not immediate
629
+ if not self._should_cleanup_start_immediately():
630
+ initial_delay = self._get_cleanup_initial_delay()
631
+ logging.info(msg=f"Periodic cleanup scheduled for {self._format_duration(initial_delay)} from now")
632
+ await asyncio.sleep(initial_delay)
633
+
634
+ while True:
635
+ try:
636
+ for guild in self.bot.guilds:
637
+ if guild.id == self.server_id:
638
+ await self._execute_cleanup_cycle(guild)
639
+ except Exception as e:
640
+ logging.error(msg=f"Error in cleanup loop: {e}")
641
+
642
+ interval = self._get_cleanup_interval()
643
+ await asyncio.sleep(interval)
644
+
645
+ async def _execute_cleanup_cycle(self, guild: discord.Guild) -> None:
646
+ """Execute cleanup for all configured channels."""
647
+ cleanup_config = self.config.get('periodic_channel_cleanup', {}) # type: ignore
648
+ channels_config = cleanup_config.get('channels', [])
649
+
650
+ if not channels_config:
651
+ return
652
+
653
+ logging.info(msg=f"Starting periodic cleanup cycle for {len(channels_config)} channels")
654
+
655
+ for channel_config in channels_config:
656
+ try:
657
+ channel_name = channel_config.get('channel_name')
658
+ min_days = channel_config.get('min_days_to_keep', 7)
659
+ description = channel_config.get('description', '')
660
+
661
+ if not channel_name:
662
+ logging.warning(msg="Cleanup config entry missing 'channel_name'")
663
+ continue
664
+
665
+ channel = discord.utils.get(guild.text_channels, name=channel_name)
666
+ if not channel:
667
+ logging.warning(msg=f"Cleanup channel '{channel_name}' not found in guild")
668
+ continue
669
+
670
+ # Check permissions
671
+ bot_member = guild.me
672
+ if bot_member is None:
673
+ logging.warning(msg="Unable to resolve bot member for cleanup")
674
+ continue
675
+
676
+ permissions = channel.permissions_for(bot_member)
677
+ if not permissions.manage_messages or not permissions.read_message_history:
678
+ logging.warning(msg=f"Bot missing permissions for cleanup in channel '{channel_name}'")
679
+ continue
680
+
681
+ cutoff_date = datetime.now(timezone.utc) - timedelta(days=min_days)
682
+ deleted_count = 0
683
+
684
+ async for message in channel.history(oldest_first=False):
685
+ if message.created_at < cutoff_date:
686
+ try:
687
+ await self._delete_message_with_rate_limit_retry(
688
+ message=message,
689
+ reason=f"Periodic cleanup - keeping messages newer than {min_days} days"
690
+ )
691
+ deleted_count += 1
692
+ except Exception as e:
693
+ logging.warning(msg=f"Failed to delete message in cleanup: {e}")
694
+ # Continue with next message
695
+ continue
696
+
697
+ log_msg = f"Periodic cleanup #{channel_name}: Deleted {deleted_count} messages (kept last {min_days} days)"
698
+ if description:
699
+ log_msg += f" - {description}"
700
+ logging.info(msg=log_msg)
701
+
702
+ except Exception as e:
703
+ logging.error(msg=f"Error cleaning up channel '{channel_config.get('channel_name', 'unknown')}': {e}")
704
+ continue
705
+
442
706
  #endregion
443
707
 
444
708
  #region BOT COMMANDS AND EVENTS DEFINITIONS
@@ -560,6 +824,9 @@ class DiscordBotLinuxMonitor:
560
824
  if self.force_sync_on_startup:
561
825
  await self._force_sync()
562
826
 
827
+ # Setup periodic channel cleanup task
828
+ await self._setup_periodic_cleanup_task()
829
+
563
830
  async def force_sync(self, interaction: discord.Interaction) -> None:
564
831
  if not self._check_if_valid_guild(guild=interaction.guild):
565
832
  return
@@ -586,9 +853,9 @@ class DiscordBotLinuxMonitor:
586
853
  return
587
854
 
588
855
  out_msg: str = (
589
- f"🤖 Bot version: {__version__}\n"
590
- f" Linux Monitor library version: {self.monitoring.get_raw_version()}\n"
591
- f"�🐍 Python compatibility: {__python_version__}"
856
+ f"🤖 Discord bot version: {__version__}\n"
857
+ f"- Linux Monitor library version: {self.monitoring.get_raw_version()}\n"
858
+ f"- 🐍 Python compatibility: {__python_version__}"
592
859
  )
593
860
  embed = self._build_result_embed(title="🤖 Bot Version", description=out_msg, color=discord.Color.blurple())
594
861
  await interaction.response.send_message(embed=embed, ephemeral=True)
@@ -767,10 +1034,28 @@ class DiscordBotLinuxMonitor:
767
1034
  await interaction.response.send_message(content="❌ Public channels do not allow this command.", ephemeral=True)
768
1035
  return
769
1036
 
770
- # Say to the user that the command is being processed
771
- await interaction.response.defer()
1037
+ # Show confirmation dialog
1038
+ await interaction.response.defer(ephemeral=True)
1039
+
1040
+ view = ConfirmationView()
1041
+ confirmation_msg = await interaction.followup.send(
1042
+ content="⚠️ **DANGEROUS OPERATION** ⚠️\n\nYou are about to reboot the entire server. This will disconnect all users and services!\n\nAre you sure?",
1043
+ view=view,
1044
+ ephemeral=True
1045
+ )
1046
+
1047
+ await view.wait()
1048
+
1049
+ if not view.confirmed:
1050
+ await confirmation_msg.edit(content="❌ Server reboot cancelled.", view=None)
1051
+ self._log_command_audit(interaction.user, interaction.guild, interaction.channel, "reboot", "CANCELLED")
1052
+ return
1053
+
1054
+ # Log the audit trail
1055
+ self._log_command_audit(interaction.user, interaction.guild, interaction.channel, "reboot", "CONFIRMED")
772
1056
 
773
1057
  try:
1058
+ await confirmation_msg.edit(content="⏳ Rebooting server...", view=None)
774
1059
  out_msg: str = await self.monitoring.reboot_server()
775
1060
  await self._interaction_followup_send_embed(interaction=interaction, title="Server Reboot", icon="🔁", msg=out_msg)
776
1061
 
@@ -805,6 +1090,9 @@ class DiscordBotLinuxMonitor:
805
1090
  if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
806
1091
  return
807
1092
 
1093
+ # Log the audit trail
1094
+ self._log_command_audit(interaction.user, interaction.guild, interaction.channel, "restart_all")
1095
+
808
1096
  # Say to the user that the command is being processed
809
1097
  await interaction.response.defer()
810
1098
 
@@ -826,6 +1114,9 @@ class DiscordBotLinuxMonitor:
826
1114
  if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
827
1115
  return
828
1116
 
1117
+ # Log the audit trail
1118
+ self._log_command_audit(interaction.user, interaction.guild, interaction.channel, "restart_service", f"service={service_name}")
1119
+
829
1120
  # Indiquer que la commande est en cours de traitement
830
1121
  await interaction.response.defer()
831
1122
 
@@ -848,6 +1139,9 @@ class DiscordBotLinuxMonitor:
848
1139
  if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
849
1140
  return
850
1141
 
1142
+ # Log the audit trail
1143
+ self._log_command_audit(interaction.user, interaction.guild, interaction.channel, "stop_service", f"service={service_name}")
1144
+
851
1145
  # Indiquer que la commande est en cours de traitement
852
1146
  await interaction.response.defer()
853
1147
 
@@ -937,6 +1231,9 @@ class DiscordBotLinuxMonitor:
937
1231
  await interaction.response.send_message(content="❌ Public channels do not allow this command.", ephemeral=True)
938
1232
  return
939
1233
 
1234
+ # Log the audit trail
1235
+ self._log_command_audit(interaction.user, interaction.guild, interaction.channel, "kill_process", f"pid={pid}")
1236
+
940
1237
  # Indiquer que la commande est en cours de traitement
941
1238
  await interaction.response.defer()
942
1239
 
@@ -978,6 +1275,9 @@ class DiscordBotLinuxMonitor:
978
1275
 
979
1276
  await interaction.response.defer(ephemeral=True)
980
1277
 
1278
+ # Log the audit trail for this destructive action
1279
+ self._log_command_audit(interaction.user, interaction.guild, interaction.channel, "clear_channel_messages", f"target_channel=#{channel.name}")
1280
+
981
1281
  deleted_count: int = 0
982
1282
  rate_limit_retries: int = 0
983
1283
  delete_reason: str = f"Requested by {interaction.user} from private channel"
@@ -1191,6 +1491,200 @@ class DiscordBotLinuxMonitor:
1191
1491
  logging.exception(msg=out_msg)
1192
1492
  await self._interaction_followup_send_embed(interaction=interaction, title="Available Commands", icon="📖", msg=out_msg, is_error=True)
1193
1493
 
1494
+ async def show_list_periodic_channels_cleanup(self, interaction: discord.Interaction) -> None:
1495
+ """Show auto-cleanup channel configuration and permission status."""
1496
+ if not self._check_if_valid_guild(guild=interaction.guild):
1497
+ return
1498
+ if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
1499
+ return
1500
+ if not self._is_private_channel(channel=interaction.channel): # type: ignore
1501
+ await interaction.response.send_message(content="❌ This command is only available in private channels.", ephemeral=True)
1502
+ return
1503
+
1504
+ await interaction.response.defer()
1505
+
1506
+ try:
1507
+ # Check if cleanup is enabled
1508
+ if not self._is_periodic_cleanup_enabled():
1509
+ await self._interaction_followup_send_embed(
1510
+ interaction=interaction,
1511
+ title="Channel Auto-Cleanup Configuration",
1512
+ icon="🧹",
1513
+ msg="⚠️ **Periodic channel cleanup is DISABLED** in config"
1514
+ )
1515
+ return
1516
+
1517
+ cleanup_config = self.config.get('periodic_channel_cleanup', {}) # type: ignore
1518
+ channels_config = cleanup_config.get('channels', [])
1519
+
1520
+ if not channels_config:
1521
+ await self._interaction_followup_send_embed(
1522
+ interaction=interaction,
1523
+ title="Channel Auto-Cleanup Configuration",
1524
+ icon="🧹",
1525
+ msg="⚠️ **No channels configured** for auto-cleanup"
1526
+ )
1527
+ return
1528
+
1529
+ guild = interaction.guild
1530
+ bot_member = guild.me if guild else None
1531
+
1532
+ lines = [
1533
+ f"**Status**: ✅ Enabled",
1534
+ f"**Next Cleanup**: {self._format_duration(self._get_cleanup_initial_delay())} from bot start",
1535
+ f"**Cleanup Interval**: Every {self._format_duration(self._get_cleanup_interval())}",
1536
+ "",
1537
+ "**Configured Channels**:",
1538
+ ""
1539
+ ]
1540
+
1541
+ for idx, channel_config in enumerate(channels_config, 1):
1542
+ channel_name = channel_config.get('channel_name', 'unknown')
1543
+ min_days = channel_config.get('min_days_to_keep', 7)
1544
+ description = channel_config.get('description', '')
1545
+
1546
+ # Find channel
1547
+ channel = discord.utils.get(guild.text_channels, name=channel_name) if guild else None
1548
+ channel_status = "✅" if channel else "❌"
1549
+
1550
+ # Check permissions if channel exists
1551
+ perms_status = "✅"
1552
+ perms_details = []
1553
+ if channel and bot_member:
1554
+ permissions = channel.permissions_for(bot_member)
1555
+ if not permissions.manage_messages:
1556
+ perms_status = "❌"
1557
+ perms_details.append("missing `manage_messages`")
1558
+ if not permissions.read_message_history:
1559
+ perms_status = "❌"
1560
+ perms_details.append("missing `read_message_history`")
1561
+ elif channel and not bot_member:
1562
+ perms_status = "⚠️"
1563
+ perms_details.append("bot member not found")
1564
+ elif not channel:
1565
+ perms_status = "N/A"
1566
+ perms_details.append("channel not found")
1567
+
1568
+ # Build channel line
1569
+ channel_line = f"**{idx}. {channel_status} #{channel_name}**"
1570
+ if description:
1571
+ channel_line += f"\n 📝 {description}"
1572
+ channel_line += f"\n 🕐 Keep messages: Last {min_days} days"
1573
+ channel_line += f"\n 🔐 Permissions: {perms_status}"
1574
+ if perms_details:
1575
+ channel_line += f" ({', '.join(perms_details)})"
1576
+
1577
+ lines.append(channel_line)
1578
+ lines.append("")
1579
+
1580
+ # Summary
1581
+ summary_icon = "✅"
1582
+ all_ok = all(
1583
+ discord.utils.get(guild.text_channels, name=ch.get('channel_name', '')) and
1584
+ (guild.me and
1585
+ guild.me.permissions_for(discord.utils.get(guild.text_channels, name=ch.get('channel_name', ''))).manage_messages and
1586
+ guild.me.permissions_for(discord.utils.get(guild.text_channels, name=ch.get('channel_name', ''))).read_message_history)
1587
+ for ch in channels_config
1588
+ )
1589
+ if not all_ok:
1590
+ summary_icon = "⚠️"
1591
+
1592
+ lines.append(f"**Overall Status**: {summary_icon} " + ("All channels properly configured" if all_ok else "Some issues need attention"))
1593
+
1594
+ msg = "\n".join(lines)
1595
+ is_error = not all_ok
1596
+
1597
+ await self._interaction_followup_send_embed(
1598
+ interaction=interaction,
1599
+ title="Channel Auto-Cleanup Configuration",
1600
+ icon="🧹",
1601
+ msg=msg,
1602
+ is_error=is_error
1603
+ )
1604
+
1605
+ except Exception as e:
1606
+ out_msg = f"**Internal error checking cleanup configuration**:\n```sh\n{e}\n```"
1607
+ logging.exception(msg=out_msg)
1608
+ await self._interaction_followup_send_embed(
1609
+ interaction=interaction,
1610
+ title="Channel Auto-Cleanup Configuration",
1611
+ icon="🧹",
1612
+ msg=out_msg,
1613
+ is_error=True
1614
+ )
1615
+
1616
+ async def show_help(self, interaction: discord.Interaction, command_name: str = "") -> None:
1617
+ """Show help for Discord bot commands with cooldown info."""
1618
+ if not self._check_if_valid_guild(guild=interaction.guild):
1619
+ return
1620
+ if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
1621
+ return
1622
+
1623
+ await interaction.response.defer()
1624
+
1625
+ try:
1626
+ # Get all commands from the bot tree
1627
+ commands_list: List[str] = []
1628
+
1629
+ # Command metadata with descriptions, cooldowns, and requirements
1630
+ command_info = {
1631
+ "force_sync": {"desc": "🔄 Force command synchronization", "cooldown": "3/20s", "private": True},
1632
+ "version": {"desc": "🤖 Show bot version", "cooldown": "3/20s", "private": False},
1633
+ "usage": {"desc": "📊 View disk space, CPU, RAM", "cooldown": "3/20s", "private": False},
1634
+ "os_infos": {"desc": "🖥️ View basic system info", "cooldown": "3/20s", "private": False},
1635
+ "users": {"desc": "👥 View connected users (private)", "cooldown": "3/20s", "private": True},
1636
+ "user_logins": {"desc": "👥 View last user connections (private)", "cooldown": "3/20s", "private": True},
1637
+ "ping": {"desc": "🌐 Ping websites", "cooldown": "3/20s", "private": False},
1638
+ "websites": {"desc": "🌐 Check website access", "cooldown": "3/20s", "private": False},
1639
+ "certificates": {"desc": "🔒 Check SSL certificates", "cooldown": "3/20s", "private": False},
1640
+ "reboot_server": {"desc": "🔄 Restart the entire server (private, requires confirmation)", "cooldown": "1/60s", "private": True},
1641
+ "services_status": {"desc": "🩺 Check services status", "cooldown": "3/20s", "private": False},
1642
+ "restart_all": {"desc": "🚀 Restart all services", "cooldown": "1/30s", "private": False},
1643
+ "restart_service": {"desc": "🚀 Restart a specific service", "cooldown": "3/20s", "private": False},
1644
+ "stop_service": {"desc": "🛑 Stop a service", "cooldown": "3/20s", "private": False},
1645
+ "list_services": {"desc": "📋 List all services", "cooldown": "3/20s", "private": False},
1646
+ "ports": {"desc": "🔒 Check ports", "cooldown": "3/20s", "private": False},
1647
+ "list_processes": {"desc": "📋 List processes by RAM (private)", "cooldown": "3/20s", "private": True},
1648
+ "list_processes_by_cpu_usage": {"desc": "📋 List processes by CPU (private)", "cooldown": "3/20s", "private": True},
1649
+ "kill_process": {"desc": "☠️ Kill process by PID (private)", "cooldown": "3/20s", "private": True},
1650
+ "clear_channel_messages": {"desc": "🧹 Clear channel messages (private, destructive)", "cooldown": "1/120s", "private": True},
1651
+ "list_clearable_channels": {"desc": "🧹 Show clearable channels (private)", "cooldown": "3/20s", "private": True},
1652
+ "list_commands": {"desc": "📋 List Linux monitor commands", "cooldown": "3/20s", "private": False},
1653
+ "execute_command": {"desc": "🚀 Execute a Linux command", "cooldown": "3/20s", "private": False},
1654
+ "execute_all_commands": {"desc": "🚀 Execute all Linux commands", "cooldown": "1/60s", "private": False},
1655
+ "help": {"desc": "🔍 Show this help message", "cooldown": "3/20s", "private": False},
1656
+ "list_periodic_channels_cleanup": {"desc": "🧹 Show auto-cleanup channel configuration and permission status", "cooldown": "3/20s", "private": True},
1657
+ }
1658
+
1659
+ # Filter commands based on channel privacy
1660
+ is_private_channel = self._is_private_channel(channel=interaction.channel) # type: ignore
1661
+ filtered_commands = {k: v for k, v in command_info.items() if v.get("private", False) == is_private_channel or not v.get("private", False)}
1662
+
1663
+ # Filter by command name if provided
1664
+ if command_name:
1665
+ command_name_lower = command_name.lower()
1666
+ matching_cmds = {k: v for k, v in filtered_commands.items() if command_name_lower in k.lower()}
1667
+ if not matching_cmds:
1668
+ out_msg = f"❌ No commands found matching '{command_name}'"
1669
+ await self._interaction_followup_send_embed(interaction=interaction, title="Help", icon="🔍", msg=out_msg)
1670
+ return
1671
+ command_info = matching_cmds
1672
+ else:
1673
+ command_info = filtered_commands
1674
+
1675
+ # Build help text
1676
+ out_msg = ""
1677
+ for cmd, info in sorted(command_info.items()):
1678
+ private_indicator = "🔒 Private" if info["private"] else "🌐 Public"
1679
+ out_msg += f"**/{cmd}** — {info['desc']}\n"
1680
+ out_msg += f" └─ {private_indicator} | ⏳ Cooldown: {info['cooldown']}\n\n"
1681
+
1682
+ await self._interaction_followup_send_embed(interaction=interaction, title="Discord Bot Commands Help", icon="🔍", msg=out_msg)
1683
+ except Exception as e:
1684
+ out_msg = f"**Internal error retrieving help**:\n```sh\n{e}\n```"
1685
+ logging.exception(msg=out_msg)
1686
+ await self._interaction_followup_send_embed(interaction=interaction, title="Help", icon="🔍", msg=out_msg, is_error=True)
1687
+
1194
1688
 
1195
1689
  async def autocomplete_command_name(self, interaction: discord.Interaction, current: str) -> List["app_commands.Choice[str]"]:
1196
1690
  # Suggest the configured command names so the user picks from a list instead of typing them.
@@ -1198,7 +1692,7 @@ class DiscordBotLinuxMonitor:
1198
1692
  is_private: bool = self._is_private_channel(channel=interaction.channel) # type: ignore
1199
1693
  current_lower: str = current.lower()
1200
1694
  choices: List["app_commands.Choice[str]"] = []
1201
- for command_name, display_name in self.monitoring.get_command_names(is_private=is_private):
1695
+ for command_name, display_name in self._get_cached_command_names(is_private=is_private):
1202
1696
  if current_lower == "" or current_lower in command_name.lower() or current_lower in display_name.lower():
1203
1697
  label: str = f"{command_name} — {display_name}"
1204
1698
  choices.append(app_commands.Choice(name=label[:100], value=command_name))
@@ -1209,6 +1703,23 @@ class DiscordBotLinuxMonitor:
1209
1703
  logging.error(msg=f"Error while building command autocomplete: {e}")
1210
1704
  return []
1211
1705
 
1706
+ async def autocomplete_service_name(self, interaction: discord.Interaction, current: str) -> List["app_commands.Choice[str]"]:
1707
+ # Suggest the configured service names so the user picks from a list instead of typing them.
1708
+ try:
1709
+ is_private: bool = self._is_private_channel(channel=interaction.channel) # type: ignore
1710
+ current_lower: str = current.lower()
1711
+ choices: List["app_commands.Choice[str]"] = []
1712
+ for service_name, display_name in self._get_cached_service_names(is_private=is_private):
1713
+ if current_lower == "" or current_lower in service_name.lower() or current_lower in display_name.lower():
1714
+ label: str = f"{service_name} — {display_name}"
1715
+ choices.append(app_commands.Choice(name=label[:100], value=service_name))
1716
+ if len(choices) >= 25: # Discord limits autocomplete to 25 choices
1717
+ break
1718
+ return choices
1719
+ except Exception as e:
1720
+ logging.error(msg=f"Error while building service autocomplete: {e}")
1721
+ return []
1722
+
1212
1723
  async def execute_command(self, interaction: discord.Interaction, command_name: str, parameters: str = "") -> None:
1213
1724
  if not self._check_if_valid_guild(guild=interaction.guild):
1214
1725
  return
@@ -6,7 +6,7 @@ with io.open(file='README.md', mode='r', encoding='utf-8') as readme_file:
6
6
 
7
7
  setup(
8
8
  name="DiscordBotLinuxMonitor",
9
- version="1.6.7",
9
+ version="1.7.0",
10
10
  description="From discord channels: Get information and warning status of Linux server like service, port, ping, ssl certificate, disk/folder/cpu/ram/swap usage, ip connection, ... (Python and shell library, Linux ONLY)",
11
11
  long_description=readme,
12
12
  long_description_content_type="text/markdown",
@@ -36,7 +36,7 @@ setup(
36
36
  keywords='discord bot warning info linux monitor monitoring server service port ping ssl certificate disk folder cpu ram swap usage ip connection',
37
37
  platforms='Linux',
38
38
  install_requires=[
39
- "linuxmonitor~=1.5.9", # follows the {MAJOR}.{MINOR}.x version range (because LinuxMonitor and DiscordBotLinuxMonitor are tightly coupled and should have same major and minor version)
39
+ "linuxmonitor~=1.5.12", # follows the {MAJOR}.{MINOR}.x version range (because LinuxMonitor and DiscordBotLinuxMonitor are tightly coupled and should have same major and minor version)
40
40
  "discord.py",
41
41
  "typing",
42
42
  "asyncio",