DiscordBotLinuxMonitor 1.6.3__tar.gz → 1.6.5__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.
- {discordbotlinuxmonitor-1.6.3 → discordbotlinuxmonitor-1.6.5}/DiscordBotLinuxMonitor.egg-info/PKG-INFO +1 -1
- {discordbotlinuxmonitor-1.6.3 → discordbotlinuxmonitor-1.6.5}/PKG-INFO +1 -1
- {discordbotlinuxmonitor-1.6.3 → discordbotlinuxmonitor-1.6.5}/discordbotlinuxmonitor/discordbotlinuxmonitor.py +154 -21
- {discordbotlinuxmonitor-1.6.3 → discordbotlinuxmonitor-1.6.5}/setup.py +1 -1
- {discordbotlinuxmonitor-1.6.3 → discordbotlinuxmonitor-1.6.5}/DiscordBotLinuxMonitor.egg-info/SOURCES.txt +0 -0
- {discordbotlinuxmonitor-1.6.3 → discordbotlinuxmonitor-1.6.5}/DiscordBotLinuxMonitor.egg-info/dependency_links.txt +0 -0
- {discordbotlinuxmonitor-1.6.3 → discordbotlinuxmonitor-1.6.5}/DiscordBotLinuxMonitor.egg-info/requires.txt +0 -0
- {discordbotlinuxmonitor-1.6.3 → discordbotlinuxmonitor-1.6.5}/DiscordBotLinuxMonitor.egg-info/top_level.txt +0 -0
- {discordbotlinuxmonitor-1.6.3 → discordbotlinuxmonitor-1.6.5}/LICENSE.md +0 -0
- {discordbotlinuxmonitor-1.6.3 → discordbotlinuxmonitor-1.6.5}/README.md +0 -0
- {discordbotlinuxmonitor-1.6.3 → discordbotlinuxmonitor-1.6.5}/discordbotlinuxmonitor/__init__.py +0 -0
- {discordbotlinuxmonitor-1.6.3 → discordbotlinuxmonitor-1.6.5}/discordbotlinuxmonitor/__main__.py +0 -0
- {discordbotlinuxmonitor-1.6.3 → discordbotlinuxmonitor-1.6.5}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: DiscordBotLinuxMonitor
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.5
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: DiscordBotLinuxMonitor
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.5
|
|
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
|
|
@@ -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.
|
|
36
|
+
__version__ = "1.6.5 (2026/08/24)"
|
|
37
37
|
__status__ = "Usable for any Linux project"
|
|
38
38
|
|
|
39
39
|
# pyright: reportMissingTypeStubs=false
|
|
@@ -309,6 +309,87 @@ class DiscordBotLinuxMonitor:
|
|
|
309
309
|
|
|
310
310
|
return fallback_seconds
|
|
311
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
|
+
|
|
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
|
+
|
|
312
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:
|
|
313
394
|
for attempt in range(max_retries + 1):
|
|
314
395
|
try:
|
|
@@ -903,6 +984,12 @@ class DiscordBotLinuxMonitor:
|
|
|
903
984
|
started_monotonic: float = asyncio.get_running_loop().time()
|
|
904
985
|
last_progress_monotonic: float = started_monotonic
|
|
905
986
|
last_reported_deleted_count: int = 0
|
|
987
|
+
heartbeat_frames: List[str] = ["◐", "◓", "◑", "◒"]
|
|
988
|
+
heartbeat_index: int = 0
|
|
989
|
+
heartbeat_stop_event = asyncio.Event()
|
|
990
|
+
heartbeat_task: Optional[asyncio.Task] = None
|
|
991
|
+
last_deleted_message_preview: str = "N/A"
|
|
992
|
+
scanned_count: int = 0
|
|
906
993
|
|
|
907
994
|
def _on_rate_limit_hit() -> None:
|
|
908
995
|
nonlocal rate_limit_retries
|
|
@@ -911,6 +998,7 @@ class DiscordBotLinuxMonitor:
|
|
|
911
998
|
async def _update_progress(force: bool = False) -> None:
|
|
912
999
|
nonlocal last_progress_monotonic
|
|
913
1000
|
nonlocal last_reported_deleted_count
|
|
1001
|
+
nonlocal heartbeat_index
|
|
914
1002
|
|
|
915
1003
|
now = asyncio.get_running_loop().time()
|
|
916
1004
|
should_update = force
|
|
@@ -922,16 +1010,21 @@ class DiscordBotLinuxMonitor:
|
|
|
922
1010
|
if not should_update:
|
|
923
1011
|
return
|
|
924
1012
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1013
|
+
heartbeat_frame = heartbeat_frames[heartbeat_index % len(heartbeat_frames)]
|
|
1014
|
+
heartbeat_index += 1
|
|
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,
|
|
931
1024
|
)
|
|
932
1025
|
|
|
933
1026
|
try:
|
|
934
|
-
await interaction.edit_original_response(content=
|
|
1027
|
+
await interaction.edit_original_response(content=None, embed=embed)
|
|
935
1028
|
last_progress_monotonic = now
|
|
936
1029
|
last_reported_deleted_count = deleted_count
|
|
937
1030
|
except discord.HTTPException as e:
|
|
@@ -942,48 +1035,88 @@ class DiscordBotLinuxMonitor:
|
|
|
942
1035
|
else:
|
|
943
1036
|
logging.warning(msg=f"Failed to update cleanup progress message: {e}")
|
|
944
1037
|
|
|
1038
|
+
async def _heartbeat_loop() -> None:
|
|
1039
|
+
while not heartbeat_stop_event.is_set():
|
|
1040
|
+
await asyncio.sleep(5)
|
|
1041
|
+
if heartbeat_stop_event.is_set():
|
|
1042
|
+
return
|
|
1043
|
+
await _update_progress(force=True)
|
|
1044
|
+
|
|
945
1045
|
try:
|
|
946
1046
|
await _update_progress(force=True)
|
|
1047
|
+
heartbeat_task = asyncio.create_task(_heartbeat_loop())
|
|
947
1048
|
recent_batch: List[discord.Message] = []
|
|
948
1049
|
|
|
949
1050
|
async for message in channel.history(limit=None, oldest_first=False):
|
|
1051
|
+
scanned_count += 1
|
|
950
1052
|
if message.created_at >= two_weeks_ago:
|
|
951
1053
|
recent_batch.append(message)
|
|
952
1054
|
|
|
953
1055
|
# Bulk delete by chunks of 100 to reduce API calls and rate-limit pressure.
|
|
954
1056
|
if len(recent_batch) == 100:
|
|
1057
|
+
last_deleted_message_preview = self._get_message_preview(recent_batch[-1])
|
|
955
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)
|
|
956
1059
|
deleted_count += len(recent_batch)
|
|
957
1060
|
recent_batch = []
|
|
958
1061
|
await _update_progress()
|
|
959
1062
|
else:
|
|
1063
|
+
last_deleted_message_preview = self._get_message_preview(message)
|
|
960
1064
|
await self._delete_message_with_rate_limit_retry(message=message, reason=delete_reason, on_rate_limit=_on_rate_limit_hit)
|
|
961
1065
|
deleted_count += 1
|
|
962
1066
|
await _update_progress()
|
|
963
1067
|
|
|
964
1068
|
if len(recent_batch) > 0:
|
|
1069
|
+
last_deleted_message_preview = self._get_message_preview(recent_batch[-1])
|
|
965
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)
|
|
966
1071
|
deleted_count += len(recent_batch)
|
|
967
1072
|
await _update_progress()
|
|
968
1073
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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,
|
|
975
1082
|
)
|
|
976
|
-
await interaction.edit_original_response(content=
|
|
1083
|
+
await interaction.edit_original_response(content=None, embed=embed)
|
|
977
1084
|
except discord.HTTPException as e:
|
|
978
1085
|
if e.status == 429:
|
|
979
1086
|
_on_rate_limit_hit()
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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)
|
|
983
1099
|
except Exception as e:
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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)
|
|
1112
|
+
finally:
|
|
1113
|
+
heartbeat_stop_event.set()
|
|
1114
|
+
if heartbeat_task is not None and not heartbeat_task.done():
|
|
1115
|
+
heartbeat_task.cancel()
|
|
1116
|
+
try:
|
|
1117
|
+
await heartbeat_task
|
|
1118
|
+
except asyncio.CancelledError:
|
|
1119
|
+
pass
|
|
987
1120
|
|
|
988
1121
|
async def list_commands(self, interaction: discord.Interaction) -> None:
|
|
989
1122
|
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.
|
|
9
|
+
version="1.6.5",
|
|
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",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{discordbotlinuxmonitor-1.6.3 → discordbotlinuxmonitor-1.6.5}/discordbotlinuxmonitor/__init__.py
RENAMED
|
File without changes
|
{discordbotlinuxmonitor-1.6.3 → discordbotlinuxmonitor-1.6.5}/discordbotlinuxmonitor/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|