DiscordBotLinuxMonitor 1.6.1__tar.gz → 1.6.3__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.1
3
+ Version: 1.6.3
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
@@ -98,6 +98,8 @@ Displayed infos are not the same if you do the command in private or public chan
98
98
 
99
99
  - `/list_commands`: 📋 List all available commands 📋
100
100
 
101
+ - `/version`: 🤖 Show current bot version 🤖
102
+
101
103
  - `/execute_command {command_name} [parameters]`: 🚀 Execute a command (optional parameters, only for commands with `accept_parameters` or a `{arg1}`/`{args}` placeholder) 🚀
102
104
 
103
105
  - `/execute_all_commands`: 🚀 Execute all commands 🚀
@@ -121,6 +123,8 @@ Displayed infos are not the same if you do the command in private or public chan
121
123
  - `/kill_process`: 🚫 Stop a process by PID 🚫
122
124
  <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/example/kill_process.jpg" height="200">
123
125
 
126
+ - `/clear_channel_messages {channel}`: 🧹 Remove all messages from a target channel (can clean any channel, command itself is private-only) 🧹
127
+
124
128
  ## How to install (for first launch)
125
129
 
126
130
  - Install package calling `python -m pip install discordbotlinuxmonitor` (or `python setup.py install` from the root of this repository)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: DiscordBotLinuxMonitor
3
- Version: 1.6.1
3
+ Version: 1.6.3
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
@@ -98,6 +98,8 @@ Displayed infos are not the same if you do the command in private or public chan
98
98
 
99
99
  - `/list_commands`: 📋 List all available commands 📋
100
100
 
101
+ - `/version`: 🤖 Show current bot version 🤖
102
+
101
103
  - `/execute_command {command_name} [parameters]`: 🚀 Execute a command (optional parameters, only for commands with `accept_parameters` or a `{arg1}`/`{args}` placeholder) 🚀
102
104
 
103
105
  - `/execute_all_commands`: 🚀 Execute all commands 🚀
@@ -121,6 +123,8 @@ Displayed infos are not the same if you do the command in private or public chan
121
123
  - `/kill_process`: 🚫 Stop a process by PID 🚫
122
124
  <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/example/kill_process.jpg" height="200">
123
125
 
126
+ - `/clear_channel_messages {channel}`: 🧹 Remove all messages from a target channel (can clean any channel, command itself is private-only) 🧹
127
+
124
128
  ## How to install (for first launch)
125
129
 
126
130
  - Install package calling `python -m pip install discordbotlinuxmonitor` (or `python setup.py install` from the root of this repository)
@@ -56,6 +56,8 @@ Displayed infos are not the same if you do the command in private or public chan
56
56
 
57
57
  - `/list_commands`: 📋 List all available commands 📋
58
58
 
59
+ - `/version`: 🤖 Show current bot version 🤖
60
+
59
61
  - `/execute_command {command_name} [parameters]`: 🚀 Execute a command (optional parameters, only for commands with `accept_parameters` or a `{arg1}`/`{args}` placeholder) 🚀
60
62
 
61
63
  - `/execute_all_commands`: 🚀 Execute all commands 🚀
@@ -79,6 +81,8 @@ Displayed infos are not the same if you do the command in private or public chan
79
81
  - `/kill_process`: 🚫 Stop a process by PID 🚫
80
82
  <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/example/kill_process.jpg" height="200">
81
83
 
84
+ - `/clear_channel_messages {channel}`: 🧹 Remove all messages from a target channel (can clean any channel, command itself is private-only) 🧹
85
+
82
86
  ## How to install (for first launch)
83
87
 
84
88
  - Install package calling `python -m pip install discordbotlinuxmonitor` (or `python setup.py install` from the root of this repository)
@@ -54,6 +54,10 @@ def main() -> None:
54
54
  async def force_sync(interaction: discord.Interaction) -> None: # type: ignore
55
55
  await discord_bot_linux_monitor.force_sync(interaction)
56
56
 
57
+ @discord_bot.tree.command(name="version", description="🤖 Show bot version 🤖")
58
+ async def version(interaction: discord.Interaction) -> None: # type: ignore
59
+ await discord_bot_linux_monitor.version(interaction)
60
+
57
61
  @discord_bot.tree.command(name="usage", description="📊 View disk space, CPU, RAM, ... 📊")
58
62
  async def usage(interaction: discord.Interaction) -> None: # type: ignore
59
63
  await discord_bot_linux_monitor.usage(interaction)
@@ -122,6 +126,10 @@ def main() -> None:
122
126
  async def kill_process(interaction: discord.Interaction, pid: int) -> None: # type: ignore
123
127
  await discord_bot_linux_monitor.kill_process(interaction, pid)
124
128
 
129
+ @discord_bot.tree.command(name="clear_channel_messages", description="[Private] 🧹 Remove all messages from a channel 🧹")
130
+ async def clear_channel_messages(interaction: discord.Interaction, channel: discord.TextChannel) -> None: # type: ignore
131
+ await discord_bot_linux_monitor.clear_channel_messages(interaction, channel)
132
+
125
133
  @discord_bot.tree.command(name="list_commands", description="📋 List all available commands 📋")
126
134
  async def list_commands(interaction: discord.Interaction) -> None: # type: ignore
127
135
  await discord_bot_linux_monitor.list_commands(interaction)
@@ -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.1 (2026/08/22)"
36
+ __version__ = "1.6.3 (2026/08/24)"
37
37
  __status__ = "Usable for any Linux project"
38
38
 
39
39
  # pyright: reportMissingTypeStubs=false
@@ -43,7 +43,8 @@ import discord
43
43
  from discord.app_commands.models import AppCommand
44
44
  from discord.ext import commands
45
45
  import json
46
- from typing import List, Union, Awaitable, Callable
46
+ from typing import List, Union, Awaitable, Callable, Any, Dict, Optional
47
+ from datetime import datetime, timedelta, timezone
47
48
 
48
49
  import asyncio
49
50
 
@@ -285,6 +286,77 @@ class DiscordBotLinuxMonitor:
285
286
  logging.exception(msg=out_msg)
286
287
  return out_msg
287
288
 
289
+ def _welcome_message_with_version(self, welcome_message: str) -> str:
290
+ return f"{welcome_message}\n\n🤖 Bot version: {__version__}"
291
+
292
+ def _get_rate_limit_wait_seconds(self, error: discord.HTTPException, fallback_seconds: float = 1.5) -> float:
293
+ retry_after = getattr(error, "retry_after", None)
294
+ if isinstance(retry_after, (int, float)) and retry_after > 0:
295
+ return float(retry_after)
296
+
297
+ response = getattr(error, "response", None)
298
+ if response is not None:
299
+ headers = getattr(response, "headers", None)
300
+ if headers is not None:
301
+ header_retry_after = headers.get("Retry-After")
302
+ if header_retry_after is not None:
303
+ try:
304
+ parsed_retry_after = float(header_retry_after)
305
+ if parsed_retry_after > 0:
306
+ return parsed_retry_after
307
+ except (ValueError, TypeError):
308
+ pass
309
+
310
+ return fallback_seconds
311
+
312
+ async def _delete_message_with_rate_limit_retry(self, message: discord.Message, reason: str, max_retries: int = 10, on_rate_limit: Optional[Callable[[], None]] = None) -> None:
313
+ for attempt in range(max_retries + 1):
314
+ try:
315
+ try:
316
+ await message.delete(reason=reason)
317
+ except TypeError:
318
+ # Some discord.py objects (e.g. PartialMessage) do not accept `reason`.
319
+ await message.delete()
320
+ return
321
+ except discord.NotFound:
322
+ return
323
+ except discord.HTTPException as e:
324
+ if e.status == 429 and attempt < max_retries:
325
+ if on_rate_limit is not None:
326
+ on_rate_limit()
327
+ wait_seconds = self._get_rate_limit_wait_seconds(error=e)
328
+ logging.warning(msg=f"Rate limited while deleting message {message.id}, waiting {wait_seconds:.2f}s before retry...")
329
+ await asyncio.sleep(wait_seconds)
330
+ continue
331
+ raise
332
+
333
+ async def _bulk_delete_messages_with_rate_limit_retry(self, channel: discord.TextChannel, messages: List[discord.Message], reason: str, max_retries: int = 10, on_rate_limit: Optional[Callable[[], None]] = None) -> None:
334
+ if len(messages) == 0:
335
+ return
336
+
337
+ for attempt in range(max_retries + 1):
338
+ try:
339
+ if len(messages) == 1:
340
+ try:
341
+ await messages[0].delete(reason=reason)
342
+ except TypeError:
343
+ # Some discord.py objects (e.g. PartialMessage) do not accept `reason`.
344
+ await messages[0].delete()
345
+ else:
346
+ await channel.delete_messages(messages, reason=reason)
347
+ return
348
+ except discord.NotFound:
349
+ return
350
+ except discord.HTTPException as e:
351
+ if e.status == 429 and attempt < max_retries:
352
+ if on_rate_limit is not None:
353
+ on_rate_limit()
354
+ wait_seconds = self._get_rate_limit_wait_seconds(error=e)
355
+ logging.warning(msg=f"Rate limited while bulk deleting {len(messages)} messages in '{channel.name}', waiting {wait_seconds:.2f}s before retry...")
356
+ await asyncio.sleep(wait_seconds)
357
+ continue
358
+ raise
359
+
288
360
  #endregion
289
361
 
290
362
  #region BOT COMMANDS AND EVENTS DEFINITIONS
@@ -309,20 +381,20 @@ class DiscordBotLinuxMonitor:
309
381
  logging.info(msg="Found the public channel, it will be possible to do public commands.")
310
382
  public_channel_cmd_ready = True
311
383
  if self.welcome_message_for_public_commands != "":
312
- await self._channel_send_no_limit(channel=channel, msg=self.welcome_message_for_public_commands)
384
+ await self._channel_send_no_limit(channel=channel, msg=self._welcome_message_with_version(self.welcome_message_for_public_commands))
313
385
 
314
386
  if self.channel_name_for_private_commands != "" and channel.name == self.channel_name_for_private_commands:
315
387
  logging.info(msg="Found the private channel, it will be possible to do private commands.")
316
388
  private_channel_cmd_ready = True
317
389
  if self.welcome_message_for_private_commands != "":
318
- await self._channel_send_no_limit(channel=channel, msg=self.welcome_message_for_private_commands)
390
+ await self._channel_send_no_limit(channel=channel, msg=self._welcome_message_with_version(self.welcome_message_for_private_commands))
319
391
 
320
392
  if self.channel_name_for_public_error_tasks != "" and channel.name == self.channel_name_for_public_error_tasks:
321
393
  logging.info(msg="Found the public channel for error task, it will be possible to show public status issues if found periodically.")
322
394
  public_channel_error_task_ready = True
323
395
 
324
396
  if self.welcome_message_for_public_error_tasks != "":
325
- await self._channel_send_no_limit(channel=channel, msg=self.welcome_message_for_public_error_tasks)
397
+ await self._channel_send_no_limit(channel=channel, msg=self._welcome_message_with_version(self.welcome_message_for_public_error_tasks))
326
398
 
327
399
  logging.info(msg=f"Activating automatic public follow and public service restart if down with '{self.bot.user}' and guild '{guild.name}' (id: '{guild.id}') on channel '{channel.name}' (id '{channel.id}').")
328
400
  public_channel_for_error_task: discord.TextChannel = channel
@@ -336,7 +408,7 @@ class DiscordBotLinuxMonitor:
336
408
  public_channel_info_task_ready = True
337
409
 
338
410
  if self.welcome_message_for_public_infos_tasks != "":
339
- await self._channel_send_no_limit(channel=channel, msg=self.welcome_message_for_public_infos_tasks)
411
+ await self._channel_send_no_limit(channel=channel, msg=self._welcome_message_with_version(self.welcome_message_for_public_infos_tasks))
340
412
 
341
413
  logging.info(msg=f"Activating automatic public follow info with '{self.bot.user}' and guild '{guild.name}' (id: '{guild.id}') on channel '{channel.name}' (id '{channel.id}').")
342
414
  public_channel_for_info_task: discord.TextChannel = channel
@@ -350,7 +422,7 @@ class DiscordBotLinuxMonitor:
350
422
  private_channel_error_task_ready = True
351
423
 
352
424
  if self.welcome_message_for_private_error_tasks != "":
353
- await self._channel_send_no_limit(channel=channel, msg=self.welcome_message_for_private_error_tasks)
425
+ await self._channel_send_no_limit(channel=channel, msg=self._welcome_message_with_version(self.welcome_message_for_private_error_tasks))
354
426
 
355
427
  logging.info(msg=f"Activating automatic private follow and public service restart if down with '{self.bot.user}' and guild '{guild.name}' (id: '{guild.id}') on channel '{channel.name}' (id '{channel.id}').")
356
428
  private_channel_for_error_task: discord.TextChannel = channel
@@ -364,7 +436,7 @@ class DiscordBotLinuxMonitor:
364
436
  private_channel_info_task_ready = True
365
437
 
366
438
  if self.welcome_message_for_private_infos_tasks != "":
367
- await self._channel_send_no_limit(channel=channel, msg=self.welcome_message_for_private_infos_tasks)
439
+ await self._channel_send_no_limit(channel=channel, msg=self._welcome_message_with_version(self.welcome_message_for_private_infos_tasks))
368
440
 
369
441
  logging.info(msg=f"Activating automatic private follow info with '{self.bot.user}' and guild '{guild.name}' (id: '{guild.id}') on channel '{channel.name}' (id '{channel.id}').")
370
442
  private_channel_for_info_task: discord.TextChannel = channel
@@ -425,6 +497,18 @@ class DiscordBotLinuxMonitor:
425
497
  # Respond to the user
426
498
  await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
427
499
 
500
+ async def version(self, interaction: discord.Interaction) -> None:
501
+ if not self._check_if_valid_guild(guild=interaction.guild):
502
+ return
503
+ if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
504
+ return
505
+
506
+ out_msg: str = (
507
+ f"🤖 Bot version: {__version__}\n"
508
+ f"🐍 Python compatibility: {__python_version__}"
509
+ )
510
+ await interaction.response.send_message(content=out_msg, ephemeral=True)
511
+
428
512
  async def usage(self, interaction: discord.Interaction) -> None:
429
513
  if not self._check_if_valid_guild(guild=interaction.guild):
430
514
  return
@@ -783,6 +867,124 @@ class DiscordBotLinuxMonitor:
783
867
  logging.exception(msg=out_msg)
784
868
  await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
785
869
 
870
+ async def clear_channel_messages(self, interaction: discord.Interaction, channel: discord.TextChannel) -> None:
871
+ if not self._check_if_valid_guild(guild=interaction.guild):
872
+ return
873
+ if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
874
+ return
875
+ if not self._is_private_channel(channel=interaction.channel): # type: ignore
876
+ await interaction.response.send_message(content="❌ Public channels do not allow this command.", ephemeral=True)
877
+ return
878
+
879
+ # Make sure only authorized users can trigger a destructive command.
880
+ permissions = channel.permissions_for(interaction.user) # type: ignore
881
+ if not permissions.manage_messages:
882
+ await interaction.response.send_message(content=f"❌ You need 'Manage Messages' permission on channel '{channel.name}' to clear it.", ephemeral=True)
883
+ return
884
+
885
+ bot_member = interaction.guild.me if interaction.guild is not None else None # type: ignore
886
+ if bot_member is None:
887
+ await interaction.response.send_message(content="❌ Unable to resolve bot permissions.", ephemeral=True)
888
+ return
889
+
890
+ bot_permissions = channel.permissions_for(bot_member)
891
+ if not bot_permissions.manage_messages or not bot_permissions.read_message_history:
892
+ await interaction.response.send_message(content=f"❌ Bot is missing required permissions on channel '{channel.name}' (Manage Messages and Read Message History).", ephemeral=True)
893
+ return
894
+
895
+ await interaction.response.defer(ephemeral=True)
896
+
897
+ deleted_count: int = 0
898
+ rate_limit_retries: int = 0
899
+ delete_reason: str = f"Requested by {interaction.user} from private channel"
900
+ two_weeks_ago = datetime.now(timezone.utc) - timedelta(days=14)
901
+ progress_every_seconds: float = 2.5
902
+ progress_every_deleted_messages: int = 200
903
+ started_monotonic: float = asyncio.get_running_loop().time()
904
+ last_progress_monotonic: float = started_monotonic
905
+ last_reported_deleted_count: int = 0
906
+
907
+ def _on_rate_limit_hit() -> None:
908
+ nonlocal rate_limit_retries
909
+ rate_limit_retries += 1
910
+
911
+ async def _update_progress(force: bool = False) -> None:
912
+ nonlocal last_progress_monotonic
913
+ nonlocal last_reported_deleted_count
914
+
915
+ now = asyncio.get_running_loop().time()
916
+ should_update = force
917
+ if not should_update:
918
+ enough_time_elapsed = (now - last_progress_monotonic) >= progress_every_seconds
919
+ enough_messages_deleted = (deleted_count - last_reported_deleted_count) >= progress_every_deleted_messages
920
+ should_update = enough_time_elapsed or enough_messages_deleted
921
+
922
+ if not should_update:
923
+ return
924
+
925
+ elapsed_seconds = int(now - started_monotonic)
926
+ progress_msg = (
927
+ f"🧹 Cleaning channel '{channel.name}'...\n"
928
+ f"Deleted messages: {deleted_count}\n"
929
+ f"Rate-limit waits: {rate_limit_retries}\n"
930
+ f"Elapsed: {elapsed_seconds}s"
931
+ )
932
+
933
+ try:
934
+ await interaction.edit_original_response(content=progress_msg)
935
+ last_progress_monotonic = now
936
+ last_reported_deleted_count = deleted_count
937
+ except discord.HTTPException as e:
938
+ if e.status == 429:
939
+ _on_rate_limit_hit()
940
+ wait_seconds = self._get_rate_limit_wait_seconds(error=e)
941
+ await asyncio.sleep(wait_seconds)
942
+ else:
943
+ logging.warning(msg=f"Failed to update cleanup progress message: {e}")
944
+
945
+ try:
946
+ await _update_progress(force=True)
947
+ recent_batch: List[discord.Message] = []
948
+
949
+ async for message in channel.history(limit=None, oldest_first=False):
950
+ if message.created_at >= two_weeks_ago:
951
+ recent_batch.append(message)
952
+
953
+ # Bulk delete by chunks of 100 to reduce API calls and rate-limit pressure.
954
+ if len(recent_batch) == 100:
955
+ await self._bulk_delete_messages_with_rate_limit_retry(channel=channel, messages=recent_batch, reason=delete_reason, on_rate_limit=_on_rate_limit_hit)
956
+ deleted_count += len(recent_batch)
957
+ recent_batch = []
958
+ await _update_progress()
959
+ else:
960
+ await self._delete_message_with_rate_limit_retry(message=message, reason=delete_reason, on_rate_limit=_on_rate_limit_hit)
961
+ deleted_count += 1
962
+ await _update_progress()
963
+
964
+ if len(recent_batch) > 0:
965
+ await self._bulk_delete_messages_with_rate_limit_retry(channel=channel, messages=recent_batch, reason=delete_reason, on_rate_limit=_on_rate_limit_hit)
966
+ deleted_count += len(recent_batch)
967
+ await _update_progress()
968
+
969
+ elapsed_seconds = int(asyncio.get_running_loop().time() - started_monotonic)
970
+ out_msg = (
971
+ f"✅ Cleared channel '{channel.name}'.\n"
972
+ f"Deleted messages: {deleted_count}\n"
973
+ f"Rate-limit waits: {rate_limit_retries}\n"
974
+ f"Elapsed: {elapsed_seconds}s"
975
+ )
976
+ await interaction.edit_original_response(content=out_msg)
977
+ except discord.HTTPException as e:
978
+ if e.status == 429:
979
+ _on_rate_limit_hit()
980
+ out_msg = f"**Discord API error while clearing messages in channel '{channel.name}'** (status {e.status}, retries: {rate_limit_retries}):\n```sh\n{e}\n```"
981
+ logging.exception(msg=out_msg)
982
+ await interaction.edit_original_response(content=out_msg)
983
+ except Exception as e:
984
+ out_msg = f"**Internal error while clearing messages in channel '{channel.name}'**:\n```sh\n{e}\n```"
985
+ logging.exception(msg=out_msg)
986
+ await interaction.edit_original_response(content=out_msg)
987
+
786
988
  async def list_commands(self, interaction: discord.Interaction) -> None:
787
989
  if not self._check_if_valid_guild(guild=interaction.guild):
788
990
  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.1",
9
+ version="1.6.3",
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",