DiscordBotLinuxMonitor 1.6.4__tar.gz → 1.6.6__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.4
3
+ Version: 1.6.6
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
@@ -125,6 +125,8 @@ Displayed infos are not the same if you do the command in private or public chan
125
125
 
126
126
  - `/clear_channel_messages {channel}`: 🧹 Remove all messages from a target channel (can clean any channel, command itself is private-only) 🧹
127
127
 
128
+ - `/list_clearable_channels`: 🧹 List text channels and whether the bot can clear them (View + Read History + Manage Messages), private-only 🧹
129
+
128
130
  ## How to install (for first launch)
129
131
 
130
132
  - 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.4
3
+ Version: 1.6.6
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
@@ -125,6 +125,8 @@ Displayed infos are not the same if you do the command in private or public chan
125
125
 
126
126
  - `/clear_channel_messages {channel}`: 🧹 Remove all messages from a target channel (can clean any channel, command itself is private-only) 🧹
127
127
 
128
+ - `/list_clearable_channels`: 🧹 List text channels and whether the bot can clear them (View + Read History + Manage Messages), private-only 🧹
129
+
128
130
  ## How to install (for first launch)
129
131
 
130
132
  - Install package calling `python -m pip install discordbotlinuxmonitor` (or `python setup.py install` from the root of this repository)
@@ -83,6 +83,8 @@ Displayed infos are not the same if you do the command in private or public chan
83
83
 
84
84
  - `/clear_channel_messages {channel}`: 🧹 Remove all messages from a target channel (can clean any channel, command itself is private-only) 🧹
85
85
 
86
+ - `/list_clearable_channels`: 🧹 List text channels and whether the bot can clear them (View + Read History + Manage Messages), private-only 🧹
87
+
86
88
  ## How to install (for first launch)
87
89
 
88
90
  - Install package calling `python -m pip install discordbotlinuxmonitor` (or `python setup.py install` from the root of this repository)
@@ -130,6 +130,10 @@ def main() -> None:
130
130
  async def clear_channel_messages(interaction: discord.Interaction, channel: discord.TextChannel) -> None: # type: ignore
131
131
  await discord_bot_linux_monitor.clear_channel_messages(interaction, channel)
132
132
 
133
+ @discord_bot.tree.command(name="list_clearable_channels", description="[Private] 🧹 List text channels and bot clear permissions 🧹")
134
+ async def list_clearable_channels(interaction: discord.Interaction) -> None: # type: ignore
135
+ await discord_bot_linux_monitor.list_clearable_channels(interaction)
136
+
133
137
  @discord_bot.tree.command(name="list_commands", description="📋 List all available commands 📋")
134
138
  async def list_commands(interaction: discord.Interaction) -> None: # type: ignore
135
139
  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.4 (2026/08/24)"
36
+ __version__ = "1.6.6 (2026/08/24)"
37
37
  __status__ = "Usable for any Linux project"
38
38
 
39
39
  # pyright: reportMissingTypeStubs=false
@@ -328,6 +328,68 @@ class DiscordBotLinuxMonitor:
328
328
 
329
329
  return " ".join(parts)
330
330
 
331
+ def _get_message_preview(self, message: discord.Message, max_length: int = 80) -> str:
332
+ content: str = str(getattr(message, "content", "") or "").strip()
333
+
334
+ if content == "":
335
+ attachments = getattr(message, "attachments", [])
336
+ embeds = getattr(message, "embeds", [])
337
+ if len(attachments) > 0:
338
+ content = f"[{len(attachments)} attachment(s)]"
339
+ elif len(embeds) > 0:
340
+ content = f"[{len(embeds)} embed(s)]"
341
+ else:
342
+ content = "[no text]"
343
+
344
+ content = content.replace("\n", " ").replace("\r", " ")
345
+ created_at = getattr(message, "created_at", None)
346
+ if created_at is not None:
347
+ try:
348
+ timestamp = created_at.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
349
+ except Exception:
350
+ timestamp = str(created_at)
351
+ else:
352
+ timestamp = "unknown-time"
353
+
354
+ if len(content) > max_length:
355
+ content = content[:max_length - 3] + "..."
356
+
357
+ return f"[{timestamp}] {content}"
358
+
359
+ def _build_cleanup_embed(self, channel_name: str, state: str, deleted_count: int, rate_limit_retries: int, elapsed_seconds: float, last_deleted_preview: str, spinner_frame: str = "", scanned_count: Optional[int] = None, error_text: str = "") -> "discord.Embed":
360
+ # state is one of: "running", "done", "error".
361
+ if state == "done":
362
+ color = discord.Color.green()
363
+ title = "🧹 Channel Cleanup — Completed"
364
+ status_value = "✅ Completed"
365
+ elif state == "error":
366
+ color = discord.Color.red()
367
+ title = "🧹 Channel Cleanup — Failed"
368
+ status_value = "❌ Failed"
369
+ else:
370
+ color = discord.Color.blurple()
371
+ title = "🧹 Channel Cleanup — In progress"
372
+ status_value = f"{spinner_frame} Working…".strip()
373
+
374
+ embed = discord.Embed(title=title, color=color)
375
+ embed.description = f"Target channel: **#{channel_name}**"
376
+
377
+ embed.add_field(name="Status", value=status_value, inline=True)
378
+ embed.add_field(name="🗑️ Deleted", value=f"**{deleted_count}** message(s)", inline=True)
379
+ embed.add_field(name="⏳ Elapsed", value=self._format_duration(elapsed_seconds), inline=True)
380
+
381
+ if scanned_count is not None:
382
+ embed.add_field(name="🔎 Scanned", value=f"{scanned_count} message(s)", inline=True)
383
+ embed.add_field(name="🚦 Rate-limit waits", value=str(rate_limit_retries), inline=True)
384
+
385
+ if error_text != "":
386
+ embed.add_field(name="⚠️ Error", value=f"```sh\n{error_text[:1000]}\n```", inline=False)
387
+
388
+ embed.add_field(name="🕗 Last deleted message", value=(last_deleted_preview if last_deleted_preview != "" else "N/A")[:1024], inline=False)
389
+
390
+ embed.set_footer(text=f"Bot v{__version__} • Last update: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
391
+ return embed
392
+
331
393
  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:
332
394
  for attempt in range(max_retries + 1):
333
395
  try:
@@ -922,10 +984,12 @@ class DiscordBotLinuxMonitor:
922
984
  started_monotonic: float = asyncio.get_running_loop().time()
923
985
  last_progress_monotonic: float = started_monotonic
924
986
  last_reported_deleted_count: int = 0
925
- heartbeat_frames: List[str] = ["|", "/", "-", "\\"]
987
+ heartbeat_frames: List[str] = ["", "", "", ""]
926
988
  heartbeat_index: int = 0
927
989
  heartbeat_stop_event = asyncio.Event()
928
990
  heartbeat_task: Optional[asyncio.Task] = None
991
+ last_deleted_message_preview: str = "N/A"
992
+ scanned_count: int = 0
929
993
 
930
994
  def _on_rate_limit_hit() -> None:
931
995
  nonlocal rate_limit_retries
@@ -946,21 +1010,21 @@ class DiscordBotLinuxMonitor:
946
1010
  if not should_update:
947
1011
  return
948
1012
 
949
- elapsed_duration = self._format_duration(now - started_monotonic)
950
- last_update = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
951
1013
  heartbeat_frame = heartbeat_frames[heartbeat_index % len(heartbeat_frames)]
952
1014
  heartbeat_index += 1
953
- progress_msg = (
954
- f"🧹 Cleaning channel '{channel.name}'...\n"
955
- f" - Status: {heartbeat_frame} Working...\n"
956
- f" - Deleted messages: {deleted_count}\n"
957
- f" - Rate-limit waits: {rate_limit_retries}\n"
958
- f" - Elapsed: {elapsed_duration}\n"
959
- f" - Last update: {last_update}"
1015
+ embed = self._build_cleanup_embed(
1016
+ channel_name=channel.name,
1017
+ state="running",
1018
+ deleted_count=deleted_count,
1019
+ rate_limit_retries=rate_limit_retries,
1020
+ elapsed_seconds=now - started_monotonic,
1021
+ last_deleted_preview=last_deleted_message_preview,
1022
+ spinner_frame=heartbeat_frame,
1023
+ scanned_count=scanned_count,
960
1024
  )
961
1025
 
962
1026
  try:
963
- await interaction.edit_original_response(content=progress_msg)
1027
+ await interaction.edit_original_response(content=None, embed=embed)
964
1028
  last_progress_monotonic = now
965
1029
  last_reported_deleted_count = deleted_count
966
1030
  except discord.HTTPException as e:
@@ -984,46 +1048,67 @@ class DiscordBotLinuxMonitor:
984
1048
  recent_batch: List[discord.Message] = []
985
1049
 
986
1050
  async for message in channel.history(limit=None, oldest_first=False):
1051
+ scanned_count += 1
987
1052
  if message.created_at >= two_weeks_ago:
988
1053
  recent_batch.append(message)
989
1054
 
990
1055
  # Bulk delete by chunks of 100 to reduce API calls and rate-limit pressure.
991
1056
  if len(recent_batch) == 100:
1057
+ last_deleted_message_preview = self._get_message_preview(recent_batch[-1])
992
1058
  await self._bulk_delete_messages_with_rate_limit_retry(channel=channel, messages=recent_batch, reason=delete_reason, on_rate_limit=_on_rate_limit_hit)
993
1059
  deleted_count += len(recent_batch)
994
1060
  recent_batch = []
995
1061
  await _update_progress()
996
1062
  else:
1063
+ last_deleted_message_preview = self._get_message_preview(message)
997
1064
  await self._delete_message_with_rate_limit_retry(message=message, reason=delete_reason, on_rate_limit=_on_rate_limit_hit)
998
1065
  deleted_count += 1
999
1066
  await _update_progress()
1000
1067
 
1001
1068
  if len(recent_batch) > 0:
1069
+ last_deleted_message_preview = self._get_message_preview(recent_batch[-1])
1002
1070
  await self._bulk_delete_messages_with_rate_limit_retry(channel=channel, messages=recent_batch, reason=delete_reason, on_rate_limit=_on_rate_limit_hit)
1003
1071
  deleted_count += len(recent_batch)
1004
1072
  await _update_progress()
1005
1073
 
1006
- elapsed_duration = self._format_duration(asyncio.get_running_loop().time() - started_monotonic)
1007
- last_update = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
1008
- out_msg = (
1009
- f"✅ Cleared channel '{channel.name}'.\n"
1010
- f" - Status: Completed\n"
1011
- f" - Deleted messages: {deleted_count}\n"
1012
- f" - Rate-limit waits: {rate_limit_retries}\n"
1013
- f" - Elapsed: {elapsed_duration}\n"
1014
- f" - Last update: {last_update}"
1074
+ embed = self._build_cleanup_embed(
1075
+ channel_name=channel.name,
1076
+ state="done",
1077
+ deleted_count=deleted_count,
1078
+ rate_limit_retries=rate_limit_retries,
1079
+ elapsed_seconds=asyncio.get_running_loop().time() - started_monotonic,
1080
+ last_deleted_preview=last_deleted_message_preview,
1081
+ scanned_count=scanned_count,
1015
1082
  )
1016
- await interaction.edit_original_response(content=out_msg)
1083
+ await interaction.edit_original_response(content=None, embed=embed)
1017
1084
  except discord.HTTPException as e:
1018
1085
  if e.status == 429:
1019
1086
  _on_rate_limit_hit()
1020
- 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```"
1021
- logging.exception(msg=out_msg)
1022
- await interaction.edit_original_response(content=out_msg)
1087
+ embed = self._build_cleanup_embed(
1088
+ channel_name=channel.name,
1089
+ state="error",
1090
+ deleted_count=deleted_count,
1091
+ rate_limit_retries=rate_limit_retries,
1092
+ elapsed_seconds=asyncio.get_running_loop().time() - started_monotonic,
1093
+ last_deleted_preview=last_deleted_message_preview,
1094
+ scanned_count=scanned_count,
1095
+ error_text=f"Discord API error (status {e.status}): {e}",
1096
+ )
1097
+ logging.exception(msg=f"Discord API error while clearing messages in channel '{channel.name}': {e}")
1098
+ await interaction.edit_original_response(content=None, embed=embed)
1023
1099
  except Exception as e:
1024
- out_msg = f"**Internal error while clearing messages in channel '{channel.name}'**:\n```sh\n{e}\n```"
1025
- logging.exception(msg=out_msg)
1026
- await interaction.edit_original_response(content=out_msg)
1100
+ embed = self._build_cleanup_embed(
1101
+ channel_name=channel.name,
1102
+ state="error",
1103
+ deleted_count=deleted_count,
1104
+ rate_limit_retries=rate_limit_retries,
1105
+ elapsed_seconds=asyncio.get_running_loop().time() - started_monotonic,
1106
+ last_deleted_preview=last_deleted_message_preview,
1107
+ scanned_count=scanned_count,
1108
+ error_text=str(e),
1109
+ )
1110
+ logging.exception(msg=f"Internal error while clearing messages in channel '{channel.name}': {e}")
1111
+ await interaction.edit_original_response(content=None, embed=embed)
1027
1112
  finally:
1028
1113
  heartbeat_stop_event.set()
1029
1114
  if heartbeat_task is not None and not heartbeat_task.done():
@@ -1033,6 +1118,55 @@ class DiscordBotLinuxMonitor:
1033
1118
  except asyncio.CancelledError:
1034
1119
  pass
1035
1120
 
1121
+ async def list_clearable_channels(self, interaction: discord.Interaction) -> None:
1122
+ if not self._check_if_valid_guild(guild=interaction.guild):
1123
+ return
1124
+ if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
1125
+ return
1126
+ if not self._is_private_channel(channel=interaction.channel): # type: ignore
1127
+ await interaction.response.send_message(content="❌ Public channels do not allow this command.", ephemeral=True)
1128
+ return
1129
+
1130
+ await interaction.response.defer(ephemeral=True)
1131
+
1132
+ guild = interaction.guild
1133
+ bot_member = guild.me if guild is not None else None # type: ignore
1134
+ if guild is None or bot_member is None:
1135
+ await interaction.followup.send(content="❌ Unable to resolve the guild or bot member.", ephemeral=True)
1136
+ return
1137
+
1138
+ try:
1139
+ clearable_lines: List[str] = []
1140
+ blocked_lines: List[str] = []
1141
+
1142
+ for text_channel in guild.text_channels:
1143
+ perms = text_channel.permissions_for(bot_member)
1144
+ can_view: bool = perms.view_channel
1145
+ can_read_history: bool = perms.read_message_history
1146
+ can_manage: bool = perms.manage_messages
1147
+ is_clearable: bool = can_view and can_read_history and can_manage
1148
+
1149
+ line = (
1150
+ f"{'✅' if is_clearable else '❌'} #{text_channel.name} "
1151
+ f"(View: {'✔' if can_view else '✘'}, "
1152
+ f"History: {'✔' if can_read_history else '✘'}, "
1153
+ f"Manage: {'✔' if can_manage else '✘'})"
1154
+ )
1155
+ if is_clearable:
1156
+ clearable_lines.append(line)
1157
+ else:
1158
+ blocked_lines.append(line)
1159
+
1160
+ out_msg = f"🧹 **Clearable channels report** ({len(clearable_lines)} clearable / {len(guild.text_channels)} text channels)\n\n"
1161
+ out_msg += "**✅ Clearable:**\n" + ("\n".join(clearable_lines) if clearable_lines else "None") + "\n\n"
1162
+ out_msg += "**❌ Blocked (missing bot permission):**\n" + ("\n".join(blocked_lines) if blocked_lines else "None")
1163
+
1164
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
1165
+ except Exception as e:
1166
+ out_msg = f"**Internal error listing clearable channels**:\n```sh\n{e}\n```"
1167
+ logging.exception(msg=out_msg)
1168
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
1169
+
1036
1170
  async def list_commands(self, interaction: discord.Interaction) -> None:
1037
1171
  if not self._check_if_valid_guild(guild=interaction.guild):
1038
1172
  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.4",
9
+ version="1.6.6",
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",