DiscordBotLinuxMonitor 1.6.2__tar.gz → 1.6.4__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.2
3
+ Version: 1.6.4
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 🚀
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: DiscordBotLinuxMonitor
3
- Version: 1.6.2
3
+ Version: 1.6.4
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 🚀
@@ -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 🚀
@@ -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)
@@ -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.2 (2026/08/24)"
36
+ __version__ = "1.6.4 (2026/08/24)"
37
37
  __status__ = "Usable for any Linux project"
38
38
 
39
39
  # pyright: reportMissingTypeStubs=false
@@ -286,6 +286,9 @@ class DiscordBotLinuxMonitor:
286
286
  logging.exception(msg=out_msg)
287
287
  return out_msg
288
288
 
289
+ def _welcome_message_with_version(self, welcome_message: str) -> str:
290
+ return f"{welcome_message}\n\n🤖 Bot version: {__version__}"
291
+
289
292
  def _get_rate_limit_wait_seconds(self, error: discord.HTTPException, fallback_seconds: float = 1.5) -> float:
290
293
  retry_after = getattr(error, "retry_after", None)
291
294
  if isinstance(retry_after, (int, float)) and retry_after > 0:
@@ -306,10 +309,33 @@ class DiscordBotLinuxMonitor:
306
309
 
307
310
  return fallback_seconds
308
311
 
312
+ def _format_duration(self, total_seconds: float) -> str:
313
+ seconds: int = max(0, int(total_seconds))
314
+
315
+ days, remainder = divmod(seconds, 86400)
316
+ hours, remainder = divmod(remainder, 3600)
317
+ minutes, secs = divmod(remainder, 60)
318
+
319
+ parts: List[str] = []
320
+ if days > 0:
321
+ parts.append(f"{days}d")
322
+ if hours > 0:
323
+ parts.append(f"{hours}h")
324
+ if minutes > 0:
325
+ parts.append(f"{minutes}m")
326
+ if secs > 0 or len(parts) == 0:
327
+ parts.append(f"{secs}s")
328
+
329
+ return " ".join(parts)
330
+
309
331
  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
332
  for attempt in range(max_retries + 1):
311
333
  try:
312
- await message.delete(reason=reason)
334
+ try:
335
+ await message.delete(reason=reason)
336
+ except TypeError:
337
+ # Some discord.py objects (e.g. PartialMessage) do not accept `reason`.
338
+ await message.delete()
313
339
  return
314
340
  except discord.NotFound:
315
341
  return
@@ -330,7 +356,11 @@ class DiscordBotLinuxMonitor:
330
356
  for attempt in range(max_retries + 1):
331
357
  try:
332
358
  if len(messages) == 1:
333
- await messages[0].delete(reason=reason)
359
+ try:
360
+ await messages[0].delete(reason=reason)
361
+ except TypeError:
362
+ # Some discord.py objects (e.g. PartialMessage) do not accept `reason`.
363
+ await messages[0].delete()
334
364
  else:
335
365
  await channel.delete_messages(messages, reason=reason)
336
366
  return
@@ -370,20 +400,20 @@ class DiscordBotLinuxMonitor:
370
400
  logging.info(msg="Found the public channel, it will be possible to do public commands.")
371
401
  public_channel_cmd_ready = True
372
402
  if self.welcome_message_for_public_commands != "":
373
- await self._channel_send_no_limit(channel=channel, msg=self.welcome_message_for_public_commands)
403
+ await self._channel_send_no_limit(channel=channel, msg=self._welcome_message_with_version(self.welcome_message_for_public_commands))
374
404
 
375
405
  if self.channel_name_for_private_commands != "" and channel.name == self.channel_name_for_private_commands:
376
406
  logging.info(msg="Found the private channel, it will be possible to do private commands.")
377
407
  private_channel_cmd_ready = True
378
408
  if self.welcome_message_for_private_commands != "":
379
- await self._channel_send_no_limit(channel=channel, msg=self.welcome_message_for_private_commands)
409
+ await self._channel_send_no_limit(channel=channel, msg=self._welcome_message_with_version(self.welcome_message_for_private_commands))
380
410
 
381
411
  if self.channel_name_for_public_error_tasks != "" and channel.name == self.channel_name_for_public_error_tasks:
382
412
  logging.info(msg="Found the public channel for error task, it will be possible to show public status issues if found periodically.")
383
413
  public_channel_error_task_ready = True
384
414
 
385
415
  if self.welcome_message_for_public_error_tasks != "":
386
- await self._channel_send_no_limit(channel=channel, msg=self.welcome_message_for_public_error_tasks)
416
+ await self._channel_send_no_limit(channel=channel, msg=self._welcome_message_with_version(self.welcome_message_for_public_error_tasks))
387
417
 
388
418
  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}').")
389
419
  public_channel_for_error_task: discord.TextChannel = channel
@@ -397,7 +427,7 @@ class DiscordBotLinuxMonitor:
397
427
  public_channel_info_task_ready = True
398
428
 
399
429
  if self.welcome_message_for_public_infos_tasks != "":
400
- await self._channel_send_no_limit(channel=channel, msg=self.welcome_message_for_public_infos_tasks)
430
+ await self._channel_send_no_limit(channel=channel, msg=self._welcome_message_with_version(self.welcome_message_for_public_infos_tasks))
401
431
 
402
432
  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}').")
403
433
  public_channel_for_info_task: discord.TextChannel = channel
@@ -411,7 +441,7 @@ class DiscordBotLinuxMonitor:
411
441
  private_channel_error_task_ready = True
412
442
 
413
443
  if self.welcome_message_for_private_error_tasks != "":
414
- await self._channel_send_no_limit(channel=channel, msg=self.welcome_message_for_private_error_tasks)
444
+ await self._channel_send_no_limit(channel=channel, msg=self._welcome_message_with_version(self.welcome_message_for_private_error_tasks))
415
445
 
416
446
  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}').")
417
447
  private_channel_for_error_task: discord.TextChannel = channel
@@ -425,7 +455,7 @@ class DiscordBotLinuxMonitor:
425
455
  private_channel_info_task_ready = True
426
456
 
427
457
  if self.welcome_message_for_private_infos_tasks != "":
428
- await self._channel_send_no_limit(channel=channel, msg=self.welcome_message_for_private_infos_tasks)
458
+ await self._channel_send_no_limit(channel=channel, msg=self._welcome_message_with_version(self.welcome_message_for_private_infos_tasks))
429
459
 
430
460
  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}').")
431
461
  private_channel_for_info_task: discord.TextChannel = channel
@@ -486,6 +516,18 @@ class DiscordBotLinuxMonitor:
486
516
  # Respond to the user
487
517
  await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
488
518
 
519
+ async def version(self, interaction: discord.Interaction) -> None:
520
+ if not self._check_if_valid_guild(guild=interaction.guild):
521
+ return
522
+ if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
523
+ return
524
+
525
+ out_msg: str = (
526
+ f"🤖 Bot version: {__version__}\n"
527
+ f"🐍 Python compatibility: {__python_version__}"
528
+ )
529
+ await interaction.response.send_message(content=out_msg, ephemeral=True)
530
+
489
531
  async def usage(self, interaction: discord.Interaction) -> None:
490
532
  if not self._check_if_valid_guild(guild=interaction.guild):
491
533
  return
@@ -880,6 +922,10 @@ class DiscordBotLinuxMonitor:
880
922
  started_monotonic: float = asyncio.get_running_loop().time()
881
923
  last_progress_monotonic: float = started_monotonic
882
924
  last_reported_deleted_count: int = 0
925
+ heartbeat_frames: List[str] = ["|", "/", "-", "\\"]
926
+ heartbeat_index: int = 0
927
+ heartbeat_stop_event = asyncio.Event()
928
+ heartbeat_task: Optional[asyncio.Task] = None
883
929
 
884
930
  def _on_rate_limit_hit() -> None:
885
931
  nonlocal rate_limit_retries
@@ -888,6 +934,7 @@ class DiscordBotLinuxMonitor:
888
934
  async def _update_progress(force: bool = False) -> None:
889
935
  nonlocal last_progress_monotonic
890
936
  nonlocal last_reported_deleted_count
937
+ nonlocal heartbeat_index
891
938
 
892
939
  now = asyncio.get_running_loop().time()
893
940
  should_update = force
@@ -899,12 +946,17 @@ class DiscordBotLinuxMonitor:
899
946
  if not should_update:
900
947
  return
901
948
 
902
- elapsed_seconds = int(now - started_monotonic)
949
+ elapsed_duration = self._format_duration(now - started_monotonic)
950
+ last_update = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
951
+ heartbeat_frame = heartbeat_frames[heartbeat_index % len(heartbeat_frames)]
952
+ heartbeat_index += 1
903
953
  progress_msg = (
904
954
  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"
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}"
908
960
  )
909
961
 
910
962
  try:
@@ -919,8 +971,16 @@ class DiscordBotLinuxMonitor:
919
971
  else:
920
972
  logging.warning(msg=f"Failed to update cleanup progress message: {e}")
921
973
 
974
+ async def _heartbeat_loop() -> None:
975
+ while not heartbeat_stop_event.is_set():
976
+ await asyncio.sleep(5)
977
+ if heartbeat_stop_event.is_set():
978
+ return
979
+ await _update_progress(force=True)
980
+
922
981
  try:
923
982
  await _update_progress(force=True)
983
+ heartbeat_task = asyncio.create_task(_heartbeat_loop())
924
984
  recent_batch: List[discord.Message] = []
925
985
 
926
986
  async for message in channel.history(limit=None, oldest_first=False):
@@ -943,12 +1003,15 @@ class DiscordBotLinuxMonitor:
943
1003
  deleted_count += len(recent_batch)
944
1004
  await _update_progress()
945
1005
 
946
- elapsed_seconds = int(asyncio.get_running_loop().time() - started_monotonic)
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")
947
1008
  out_msg = (
948
1009
  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"
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}"
952
1015
  )
953
1016
  await interaction.edit_original_response(content=out_msg)
954
1017
  except discord.HTTPException as e:
@@ -961,6 +1024,14 @@ class DiscordBotLinuxMonitor:
961
1024
  out_msg = f"**Internal error while clearing messages in channel '{channel.name}'**:\n```sh\n{e}\n```"
962
1025
  logging.exception(msg=out_msg)
963
1026
  await interaction.edit_original_response(content=out_msg)
1027
+ finally:
1028
+ heartbeat_stop_event.set()
1029
+ if heartbeat_task is not None and not heartbeat_task.done():
1030
+ heartbeat_task.cancel()
1031
+ try:
1032
+ await heartbeat_task
1033
+ except asyncio.CancelledError:
1034
+ pass
964
1035
 
965
1036
  async def list_commands(self, interaction: discord.Interaction) -> None:
966
1037
  if not self._check_if_valid_guild(guild=interaction.guild):
@@ -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.2",
9
+ version="1.6.4",
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",