DiscordBotLinuxMonitor 1.6.1__tar.gz → 1.6.2__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.2
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
@@ -121,6 +121,8 @@ Displayed infos are not the same if you do the command in private or public chan
121
121
  - `/kill_process`: 🚫 Stop a process by PID 🚫
122
122
  <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/example/kill_process.jpg" height="200">
123
123
 
124
+ - `/clear_channel_messages {channel}`: 🧹 Remove all messages from a target channel (can clean any channel, command itself is private-only) 🧹
125
+
124
126
  ## How to install (for first launch)
125
127
 
126
128
  - 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.2
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
@@ -121,6 +121,8 @@ Displayed infos are not the same if you do the command in private or public chan
121
121
  - `/kill_process`: 🚫 Stop a process by PID 🚫
122
122
  <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/example/kill_process.jpg" height="200">
123
123
 
124
+ - `/clear_channel_messages {channel}`: 🧹 Remove all messages from a target channel (can clean any channel, command itself is private-only) 🧹
125
+
124
126
  ## How to install (for first launch)
125
127
 
126
128
  - Install package calling `python -m pip install discordbotlinuxmonitor` (or `python setup.py install` from the root of this repository)
@@ -79,6 +79,8 @@ Displayed infos are not the same if you do the command in private or public chan
79
79
  - `/kill_process`: 🚫 Stop a process by PID 🚫
80
80
  <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/example/kill_process.jpg" height="200">
81
81
 
82
+ - `/clear_channel_messages {channel}`: 🧹 Remove all messages from a target channel (can clean any channel, command itself is private-only) 🧹
83
+
82
84
  ## How to install (for first launch)
83
85
 
84
86
  - Install package calling `python -m pip install discordbotlinuxmonitor` (or `python setup.py install` from the root of this repository)
@@ -122,6 +122,10 @@ def main() -> None:
122
122
  async def kill_process(interaction: discord.Interaction, pid: int) -> None: # type: ignore
123
123
  await discord_bot_linux_monitor.kill_process(interaction, pid)
124
124
 
125
+ @discord_bot.tree.command(name="clear_channel_messages", description="[Private] 🧹 Remove all messages from a channel 🧹")
126
+ async def clear_channel_messages(interaction: discord.Interaction, channel: discord.TextChannel) -> None: # type: ignore
127
+ await discord_bot_linux_monitor.clear_channel_messages(interaction, channel)
128
+
125
129
  @discord_bot.tree.command(name="list_commands", description="📋 List all available commands 📋")
126
130
  async def list_commands(interaction: discord.Interaction) -> None: # type: ignore
127
131
  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.2 (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,66 @@ class DiscordBotLinuxMonitor:
285
286
  logging.exception(msg=out_msg)
286
287
  return out_msg
287
288
 
289
+ def _get_rate_limit_wait_seconds(self, error: discord.HTTPException, fallback_seconds: float = 1.5) -> float:
290
+ retry_after = getattr(error, "retry_after", None)
291
+ if isinstance(retry_after, (int, float)) and retry_after > 0:
292
+ return float(retry_after)
293
+
294
+ response = getattr(error, "response", None)
295
+ if response is not None:
296
+ headers = getattr(response, "headers", None)
297
+ if headers is not None:
298
+ header_retry_after = headers.get("Retry-After")
299
+ if header_retry_after is not None:
300
+ try:
301
+ parsed_retry_after = float(header_retry_after)
302
+ if parsed_retry_after > 0:
303
+ return parsed_retry_after
304
+ except (ValueError, TypeError):
305
+ pass
306
+
307
+ return fallback_seconds
308
+
309
+ 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:
310
+ for attempt in range(max_retries + 1):
311
+ try:
312
+ await message.delete(reason=reason)
313
+ return
314
+ except discord.NotFound:
315
+ return
316
+ except discord.HTTPException as e:
317
+ if e.status == 429 and attempt < max_retries:
318
+ if on_rate_limit is not None:
319
+ on_rate_limit()
320
+ wait_seconds = self._get_rate_limit_wait_seconds(error=e)
321
+ logging.warning(msg=f"Rate limited while deleting message {message.id}, waiting {wait_seconds:.2f}s before retry...")
322
+ await asyncio.sleep(wait_seconds)
323
+ continue
324
+ raise
325
+
326
+ 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:
327
+ if len(messages) == 0:
328
+ return
329
+
330
+ for attempt in range(max_retries + 1):
331
+ try:
332
+ if len(messages) == 1:
333
+ await messages[0].delete(reason=reason)
334
+ else:
335
+ await channel.delete_messages(messages, reason=reason)
336
+ return
337
+ except discord.NotFound:
338
+ return
339
+ except discord.HTTPException as e:
340
+ if e.status == 429 and attempt < max_retries:
341
+ if on_rate_limit is not None:
342
+ on_rate_limit()
343
+ wait_seconds = self._get_rate_limit_wait_seconds(error=e)
344
+ logging.warning(msg=f"Rate limited while bulk deleting {len(messages)} messages in '{channel.name}', waiting {wait_seconds:.2f}s before retry...")
345
+ await asyncio.sleep(wait_seconds)
346
+ continue
347
+ raise
348
+
288
349
  #endregion
289
350
 
290
351
  #region BOT COMMANDS AND EVENTS DEFINITIONS
@@ -783,6 +844,124 @@ class DiscordBotLinuxMonitor:
783
844
  logging.exception(msg=out_msg)
784
845
  await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
785
846
 
847
+ async def clear_channel_messages(self, interaction: discord.Interaction, channel: discord.TextChannel) -> None:
848
+ if not self._check_if_valid_guild(guild=interaction.guild):
849
+ return
850
+ if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
851
+ return
852
+ if not self._is_private_channel(channel=interaction.channel): # type: ignore
853
+ await interaction.response.send_message(content="❌ Public channels do not allow this command.", ephemeral=True)
854
+ return
855
+
856
+ # Make sure only authorized users can trigger a destructive command.
857
+ permissions = channel.permissions_for(interaction.user) # type: ignore
858
+ if not permissions.manage_messages:
859
+ await interaction.response.send_message(content=f"❌ You need 'Manage Messages' permission on channel '{channel.name}' to clear it.", ephemeral=True)
860
+ return
861
+
862
+ bot_member = interaction.guild.me if interaction.guild is not None else None # type: ignore
863
+ if bot_member is None:
864
+ await interaction.response.send_message(content="❌ Unable to resolve bot permissions.", ephemeral=True)
865
+ return
866
+
867
+ bot_permissions = channel.permissions_for(bot_member)
868
+ if not bot_permissions.manage_messages or not bot_permissions.read_message_history:
869
+ await interaction.response.send_message(content=f"❌ Bot is missing required permissions on channel '{channel.name}' (Manage Messages and Read Message History).", ephemeral=True)
870
+ return
871
+
872
+ await interaction.response.defer(ephemeral=True)
873
+
874
+ deleted_count: int = 0
875
+ rate_limit_retries: int = 0
876
+ delete_reason: str = f"Requested by {interaction.user} from private channel"
877
+ two_weeks_ago = datetime.now(timezone.utc) - timedelta(days=14)
878
+ progress_every_seconds: float = 2.5
879
+ progress_every_deleted_messages: int = 200
880
+ started_monotonic: float = asyncio.get_running_loop().time()
881
+ last_progress_monotonic: float = started_monotonic
882
+ last_reported_deleted_count: int = 0
883
+
884
+ def _on_rate_limit_hit() -> None:
885
+ nonlocal rate_limit_retries
886
+ rate_limit_retries += 1
887
+
888
+ async def _update_progress(force: bool = False) -> None:
889
+ nonlocal last_progress_monotonic
890
+ nonlocal last_reported_deleted_count
891
+
892
+ now = asyncio.get_running_loop().time()
893
+ should_update = force
894
+ if not should_update:
895
+ enough_time_elapsed = (now - last_progress_monotonic) >= progress_every_seconds
896
+ enough_messages_deleted = (deleted_count - last_reported_deleted_count) >= progress_every_deleted_messages
897
+ should_update = enough_time_elapsed or enough_messages_deleted
898
+
899
+ if not should_update:
900
+ return
901
+
902
+ elapsed_seconds = int(now - started_monotonic)
903
+ progress_msg = (
904
+ f"🧹 Cleaning channel '{channel.name}'...\n"
905
+ f"Deleted messages: {deleted_count}\n"
906
+ f"Rate-limit waits: {rate_limit_retries}\n"
907
+ f"Elapsed: {elapsed_seconds}s"
908
+ )
909
+
910
+ try:
911
+ await interaction.edit_original_response(content=progress_msg)
912
+ last_progress_monotonic = now
913
+ last_reported_deleted_count = deleted_count
914
+ except discord.HTTPException as e:
915
+ if e.status == 429:
916
+ _on_rate_limit_hit()
917
+ wait_seconds = self._get_rate_limit_wait_seconds(error=e)
918
+ await asyncio.sleep(wait_seconds)
919
+ else:
920
+ logging.warning(msg=f"Failed to update cleanup progress message: {e}")
921
+
922
+ try:
923
+ await _update_progress(force=True)
924
+ recent_batch: List[discord.Message] = []
925
+
926
+ async for message in channel.history(limit=None, oldest_first=False):
927
+ if message.created_at >= two_weeks_ago:
928
+ recent_batch.append(message)
929
+
930
+ # Bulk delete by chunks of 100 to reduce API calls and rate-limit pressure.
931
+ if len(recent_batch) == 100:
932
+ await self._bulk_delete_messages_with_rate_limit_retry(channel=channel, messages=recent_batch, reason=delete_reason, on_rate_limit=_on_rate_limit_hit)
933
+ deleted_count += len(recent_batch)
934
+ recent_batch = []
935
+ await _update_progress()
936
+ else:
937
+ await self._delete_message_with_rate_limit_retry(message=message, reason=delete_reason, on_rate_limit=_on_rate_limit_hit)
938
+ deleted_count += 1
939
+ await _update_progress()
940
+
941
+ if len(recent_batch) > 0:
942
+ await self._bulk_delete_messages_with_rate_limit_retry(channel=channel, messages=recent_batch, reason=delete_reason, on_rate_limit=_on_rate_limit_hit)
943
+ deleted_count += len(recent_batch)
944
+ await _update_progress()
945
+
946
+ elapsed_seconds = int(asyncio.get_running_loop().time() - started_monotonic)
947
+ out_msg = (
948
+ f"✅ Cleared channel '{channel.name}'.\n"
949
+ f"Deleted messages: {deleted_count}\n"
950
+ f"Rate-limit waits: {rate_limit_retries}\n"
951
+ f"Elapsed: {elapsed_seconds}s"
952
+ )
953
+ await interaction.edit_original_response(content=out_msg)
954
+ except discord.HTTPException as e:
955
+ if e.status == 429:
956
+ _on_rate_limit_hit()
957
+ 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```"
958
+ logging.exception(msg=out_msg)
959
+ await interaction.edit_original_response(content=out_msg)
960
+ except Exception as e:
961
+ out_msg = f"**Internal error while clearing messages in channel '{channel.name}'**:\n```sh\n{e}\n```"
962
+ logging.exception(msg=out_msg)
963
+ await interaction.edit_original_response(content=out_msg)
964
+
786
965
  async def list_commands(self, interaction: discord.Interaction) -> None:
787
966
  if not self._check_if_valid_guild(guild=interaction.guild):
788
967
  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.2",
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",