DiscordBotLinuxMonitor 1.6.8__tar.gz → 1.7.0__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.8 → discordbotlinuxmonitor-1.7.0}/DiscordBotLinuxMonitor.egg-info/PKG-INFO +1 -1
- {discordbotlinuxmonitor-1.6.8 → discordbotlinuxmonitor-1.7.0}/PKG-INFO +1 -1
- {discordbotlinuxmonitor-1.6.8 → discordbotlinuxmonitor-1.7.0}/discordbotlinuxmonitor/__main__.py +58 -0
- {discordbotlinuxmonitor-1.6.8 → discordbotlinuxmonitor-1.7.0}/discordbotlinuxmonitor/discordbotlinuxmonitor.py +512 -18
- {discordbotlinuxmonitor-1.6.8 → discordbotlinuxmonitor-1.7.0}/setup.py +1 -1
- {discordbotlinuxmonitor-1.6.8 → discordbotlinuxmonitor-1.7.0}/DiscordBotLinuxMonitor.egg-info/SOURCES.txt +0 -0
- {discordbotlinuxmonitor-1.6.8 → discordbotlinuxmonitor-1.7.0}/DiscordBotLinuxMonitor.egg-info/dependency_links.txt +0 -0
- {discordbotlinuxmonitor-1.6.8 → discordbotlinuxmonitor-1.7.0}/DiscordBotLinuxMonitor.egg-info/requires.txt +0 -0
- {discordbotlinuxmonitor-1.6.8 → discordbotlinuxmonitor-1.7.0}/DiscordBotLinuxMonitor.egg-info/top_level.txt +0 -0
- {discordbotlinuxmonitor-1.6.8 → discordbotlinuxmonitor-1.7.0}/LICENSE.md +0 -0
- {discordbotlinuxmonitor-1.6.8 → discordbotlinuxmonitor-1.7.0}/README.md +0 -0
- {discordbotlinuxmonitor-1.6.8 → discordbotlinuxmonitor-1.7.0}/discordbotlinuxmonitor/__init__.py +0 -0
- {discordbotlinuxmonitor-1.6.8 → discordbotlinuxmonitor-1.7.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: DiscordBotLinuxMonitor
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
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.
|
|
3
|
+
Version: 1.7.0
|
|
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
|
{discordbotlinuxmonitor-1.6.8 → discordbotlinuxmonitor-1.7.0}/discordbotlinuxmonitor/__main__.py
RENAMED
|
@@ -52,104 +52,162 @@ def main() -> None:
|
|
|
52
52
|
await discord_bot_linux_monitor.on_ready()
|
|
53
53
|
|
|
54
54
|
@discord_bot.tree.command(name="force_sync", description="[Private] 🔄 Force command synchronization 🔄")
|
|
55
|
+
@app_commands.checks.cooldown(3, 20.0)
|
|
55
56
|
async def force_sync(interaction: discord.Interaction) -> None: # type: ignore
|
|
56
57
|
await discord_bot_linux_monitor.force_sync(interaction)
|
|
57
58
|
|
|
58
59
|
@discord_bot.tree.command(name="version", description="🤖 Show bot version 🤖")
|
|
60
|
+
@app_commands.checks.cooldown(3, 20.0)
|
|
59
61
|
async def version(interaction: discord.Interaction) -> None: # type: ignore
|
|
60
62
|
await discord_bot_linux_monitor.version(interaction)
|
|
61
63
|
|
|
62
64
|
@discord_bot.tree.command(name="usage", description="📊 View disk space, CPU, RAM, ... 📊")
|
|
65
|
+
@app_commands.checks.cooldown(3, 20.0)
|
|
63
66
|
async def usage(interaction: discord.Interaction) -> None: # type: ignore
|
|
64
67
|
await discord_bot_linux_monitor.usage(interaction)
|
|
65
68
|
|
|
66
69
|
@discord_bot.tree.command(name="os_infos", description="🖥️ View basic system information 🖥️")
|
|
70
|
+
@app_commands.checks.cooldown(3, 20.0)
|
|
67
71
|
async def os_infos(interaction: discord.Interaction) -> None: # type: ignore
|
|
68
72
|
await discord_bot_linux_monitor.os_infos(interaction)
|
|
69
73
|
|
|
70
74
|
@discord_bot.tree.command(name="users", description="[Private] 👥 View connected users 👥")
|
|
75
|
+
@app_commands.checks.cooldown(3, 20.0)
|
|
71
76
|
async def users(interaction: discord.Interaction) -> None: # type: ignore
|
|
72
77
|
await discord_bot_linux_monitor.users(interaction)
|
|
73
78
|
|
|
74
79
|
@discord_bot.tree.command(name="user_logins", description="[Private] 👥 View last user connections 👥")
|
|
80
|
+
@app_commands.checks.cooldown(3, 20.0)
|
|
75
81
|
async def user_logins(interaction: discord.Interaction) -> None: # type: ignore
|
|
76
82
|
await discord_bot_linux_monitor.user_logins(interaction)
|
|
77
83
|
|
|
78
84
|
@discord_bot.tree.command(name="ping", description="🌐 Ping websites 🌐")
|
|
85
|
+
@app_commands.checks.cooldown(3, 20.0)
|
|
79
86
|
async def ping(interaction: discord.Interaction) -> None: # type: ignore
|
|
80
87
|
await discord_bot_linux_monitor.ping(interaction)
|
|
81
88
|
|
|
82
89
|
@discord_bot.tree.command(name="websites", description="🌐 Check websites access (GET requests) 🌐")
|
|
90
|
+
@app_commands.checks.cooldown(3, 20.0)
|
|
83
91
|
async def websites(interaction: discord.Interaction) -> None: # type: ignore
|
|
84
92
|
await discord_bot_linux_monitor.websites(interaction)
|
|
85
93
|
|
|
86
94
|
@discord_bot.tree.command(name="certificates", description="🔒 Check SSL certificates 🔒")
|
|
95
|
+
@app_commands.checks.cooldown(3, 20.0)
|
|
87
96
|
async def certificates(interaction: discord.Interaction) -> None: # type: ignore
|
|
88
97
|
await discord_bot_linux_monitor.certificates(interaction)
|
|
89
98
|
|
|
90
99
|
@discord_bot.tree.command(name="reboot_server", description="[Private] 🔄 Restart the entire server 🔄")
|
|
100
|
+
@app_commands.checks.cooldown(1, 60.0) # 1 use per 60 seconds - dangerous operation
|
|
91
101
|
async def reboot(interaction: discord.Interaction) -> None: # type: ignore
|
|
92
102
|
await discord_bot_linux_monitor.reboot(interaction)
|
|
93
103
|
|
|
94
104
|
@discord_bot.tree.command(name="services_status", description="🩺 Check services are running 🩺")
|
|
105
|
+
@app_commands.checks.cooldown(3, 20.0)
|
|
95
106
|
async def services_status(interaction: discord.Interaction) -> None: # type: ignore
|
|
96
107
|
await discord_bot_linux_monitor.services_status(interaction)
|
|
97
108
|
|
|
98
109
|
@discord_bot.tree.command(name="restart_all", description="🚀 Restart all services 🚀")
|
|
110
|
+
@app_commands.checks.cooldown(1, 30.0) # 1 use per 30 seconds
|
|
99
111
|
async def restart_all(interaction: discord.Interaction) -> None: # type: ignore
|
|
100
112
|
await discord_bot_linux_monitor.restart_all(interaction)
|
|
101
113
|
|
|
102
114
|
@discord_bot.tree.command(name="restart_service", description="🚀 Restart a service 🚀")
|
|
103
115
|
@app_commands.autocomplete(service_name=discord_bot_linux_monitor.autocomplete_service_name)
|
|
116
|
+
@app_commands.checks.cooldown(3, 20.0) # 3 uses per 20 seconds
|
|
104
117
|
async def restart_service(interaction: discord.Interaction, service_name: str) -> None: # type: ignore
|
|
105
118
|
await discord_bot_linux_monitor.restart_service(interaction, service_name)
|
|
106
119
|
|
|
107
120
|
@discord_bot.tree.command(name="stop_service", description="🚫 Stop a service 🚫")
|
|
108
121
|
@app_commands.autocomplete(service_name=discord_bot_linux_monitor.autocomplete_service_name)
|
|
122
|
+
@app_commands.checks.cooldown(3, 20.0) # 3 uses per 20 seconds
|
|
109
123
|
async def stop_service(interaction: discord.Interaction, service_name: str) -> None: # type: ignore
|
|
110
124
|
await discord_bot_linux_monitor.stop_service(interaction, service_name)
|
|
111
125
|
|
|
112
126
|
@discord_bot.tree.command(name="list_services", description="📋 List all available services 📋")
|
|
127
|
+
@app_commands.checks.cooldown(3, 20.0)
|
|
113
128
|
async def list_services(interaction: discord.Interaction) -> None: # type: ignore
|
|
114
129
|
await discord_bot_linux_monitor.list_services(interaction)
|
|
115
130
|
|
|
116
131
|
@discord_bot.tree.command(name="ports", description="🔒 Check ports 🔒")
|
|
132
|
+
@app_commands.checks.cooldown(3, 20.0)
|
|
117
133
|
async def ports(interaction: discord.Interaction) -> None: # type: ignore
|
|
118
134
|
await discord_bot_linux_monitor.ports(interaction)
|
|
119
135
|
|
|
120
136
|
@discord_bot.tree.command(name="list_processes", description="[Private] 📋 List active processes (ordered by RAM usage) 📋")
|
|
137
|
+
@app_commands.checks.cooldown(3, 20.0)
|
|
121
138
|
async def list_processes(interaction: discord.Interaction) -> None: # type: ignore
|
|
122
139
|
await discord_bot_linux_monitor.list_processes(interaction, order_by_ram=True)
|
|
123
140
|
|
|
124
141
|
@discord_bot.tree.command(name="list_processes_by_cpu_usage", description="[Private] 📋 List active processes (ordered by CPU usage) 📋")
|
|
142
|
+
@app_commands.checks.cooldown(3, 20.0)
|
|
125
143
|
async def list_processes_by_cpu_usage(interaction: discord.Interaction) -> None: # type: ignore
|
|
126
144
|
await discord_bot_linux_monitor.list_processes(interaction, order_by_ram=False)
|
|
127
145
|
|
|
128
146
|
@discord_bot.tree.command(name="kill_process", description="[Private] 🚫 Stop a process by PID 🚫")
|
|
147
|
+
@app_commands.checks.cooldown(3, 20.0)
|
|
129
148
|
async def kill_process(interaction: discord.Interaction, pid: int) -> None: # type: ignore
|
|
130
149
|
await discord_bot_linux_monitor.kill_process(interaction, pid)
|
|
131
150
|
|
|
132
151
|
@discord_bot.tree.command(name="clear_channel_messages", description="[Private] 🧹 Remove all messages from a channel 🧹")
|
|
152
|
+
@app_commands.checks.cooldown(1, 120.0) # 1 use per 120 seconds (2 minutes) - very destructive
|
|
133
153
|
async def clear_channel_messages(interaction: discord.Interaction, channel: discord.TextChannel) -> None: # type: ignore
|
|
134
154
|
await discord_bot_linux_monitor.clear_channel_messages(interaction, channel)
|
|
135
155
|
|
|
136
156
|
@discord_bot.tree.command(name="list_clearable_channels", description="[Private] 🧹 List text channels and bot clear permissions 🧹")
|
|
157
|
+
@app_commands.checks.cooldown(3, 20.0)
|
|
137
158
|
async def list_clearable_channels(interaction: discord.Interaction) -> None: # type: ignore
|
|
138
159
|
await discord_bot_linux_monitor.list_clearable_channels(interaction)
|
|
139
160
|
|
|
140
161
|
@discord_bot.tree.command(name="list_commands", description="📋 List all available commands 📋")
|
|
162
|
+
@app_commands.checks.cooldown(3, 20.0)
|
|
141
163
|
async def list_commands(interaction: discord.Interaction) -> None: # type: ignore
|
|
142
164
|
await discord_bot_linux_monitor.list_commands(interaction)
|
|
143
165
|
|
|
166
|
+
@discord_bot.tree.command(name="help", description="🔍 Show Discord bot commands with info and cooldowns 🔍")
|
|
167
|
+
@app_commands.checks.cooldown(3, 20.0)
|
|
168
|
+
async def help_command(interaction: discord.Interaction, command_name: str = "") -> None: # type: ignore
|
|
169
|
+
await discord_bot_linux_monitor.show_help(interaction, command_name)
|
|
170
|
+
|
|
171
|
+
@discord_bot.tree.command(name="list_periodic_channels_cleanup", description="🧹 Show auto-cleanup channel configuration and permission status 🧹")
|
|
172
|
+
@app_commands.checks.cooldown(3, 20.0)
|
|
173
|
+
async def list_periodic_channels_cleanup(interaction: discord.Interaction) -> None: # type: ignore
|
|
174
|
+
await discord_bot_linux_monitor.show_list_periodic_channels_cleanup(interaction)
|
|
175
|
+
|
|
144
176
|
@discord_bot.tree.command(name="execute_command", description="🚀 Execute a command (optional parameters) 🚀")
|
|
145
177
|
@app_commands.autocomplete(command_name=discord_bot_linux_monitor.autocomplete_command_name)
|
|
178
|
+
@app_commands.checks.cooldown(3, 20.0) # 3 uses per 20 seconds
|
|
146
179
|
async def execute_command(interaction: discord.Interaction, command_name: str, parameters: str = "") -> None: # type: ignore
|
|
147
180
|
await discord_bot_linux_monitor.execute_command(interaction, command_name=command_name, parameters=parameters)
|
|
148
181
|
|
|
149
182
|
@discord_bot.tree.command(name="execute_all_commands", description="🚀 Execute all commands 🚀")
|
|
183
|
+
@app_commands.checks.cooldown(1, 60.0) # 1 use per 60 seconds
|
|
150
184
|
async def execute_all_commands(interaction: discord.Interaction) -> None: # type: ignore
|
|
151
185
|
await discord_bot_linux_monitor.execute_all_commands(interaction)
|
|
152
186
|
|
|
187
|
+
@discord_bot.tree.error
|
|
188
|
+
async def on_app_command_error(interaction: discord.Interaction, error: app_commands.AppCommandError) -> None: # type: ignore
|
|
189
|
+
"""Handle cooldown and other command errors gracefully."""
|
|
190
|
+
if isinstance(error, app_commands.CommandOnCooldown):
|
|
191
|
+
retry_after = error.retry_after
|
|
192
|
+
await interaction.response.send_message(
|
|
193
|
+
content=f"⏳ This command is on cooldown. Please wait {retry_after:.1f} seconds before using it again.",
|
|
194
|
+
ephemeral=True
|
|
195
|
+
)
|
|
196
|
+
elif isinstance(error, app_commands.MissingPermissions):
|
|
197
|
+
await interaction.response.send_message(
|
|
198
|
+
content="❌ You don't have permission to use this command.",
|
|
199
|
+
ephemeral=True
|
|
200
|
+
)
|
|
201
|
+
else:
|
|
202
|
+
logging.error(msg=f"Unhandled command error: {error}")
|
|
203
|
+
try:
|
|
204
|
+
await interaction.response.send_message(
|
|
205
|
+
content="❌ An unexpected error occurred. Please try again later.",
|
|
206
|
+
ephemeral=True
|
|
207
|
+
)
|
|
208
|
+
except discord.InteractionResponded:
|
|
209
|
+
logging.warning(msg="Interaction already responded when handling error")
|
|
210
|
+
|
|
153
211
|
#endregion
|
|
154
212
|
|
|
155
213
|
# Start the discord bot
|
|
@@ -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.
|
|
36
|
+
__version__ = "1.7.0 (2026/08/29)"
|
|
37
37
|
__status__ = "Usable for any Linux project"
|
|
38
38
|
|
|
39
39
|
# pyright: reportMissingTypeStubs=false
|
|
@@ -44,13 +44,76 @@ from discord.app_commands.models import AppCommand
|
|
|
44
44
|
from discord import app_commands
|
|
45
45
|
from discord.ext import commands
|
|
46
46
|
import json
|
|
47
|
-
from typing import List, Union, Awaitable, Callable, Any, Dict, Optional
|
|
47
|
+
from typing import List, Union, Awaitable, Callable, Any, Dict, Optional, Tuple
|
|
48
48
|
from datetime import datetime, timedelta, timezone
|
|
49
49
|
|
|
50
50
|
import asyncio
|
|
51
|
-
|
|
51
|
+
import functools
|
|
52
|
+
import time
|
|
52
53
|
import logging
|
|
53
54
|
|
|
55
|
+
|
|
56
|
+
class ConfirmationView(discord.ui.View):
|
|
57
|
+
"""Confirmation dialog for dangerous commands."""
|
|
58
|
+
def __init__(self, timeout: float = 30.0):
|
|
59
|
+
super().__init__(timeout=timeout)
|
|
60
|
+
self.confirmed: bool = False
|
|
61
|
+
|
|
62
|
+
@discord.ui.button(label="✅ Confirm", style=discord.ButtonStyle.red)
|
|
63
|
+
async def confirm_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
|
64
|
+
self.confirmed = True
|
|
65
|
+
await interaction.response.defer()
|
|
66
|
+
self.stop()
|
|
67
|
+
|
|
68
|
+
@discord.ui.button(label="❌ Cancel", style=discord.ButtonStyle.gray)
|
|
69
|
+
async def cancel_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
|
70
|
+
self.confirmed = False
|
|
71
|
+
await interaction.response.defer()
|
|
72
|
+
self.stop()
|
|
73
|
+
|
|
74
|
+
async def on_timeout(self) -> None:
|
|
75
|
+
self.confirmed = False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class PaginationView(discord.ui.View):
|
|
79
|
+
"""Pagination view for long messages with prev/next buttons."""
|
|
80
|
+
def __init__(self, chunks: List[str], timeout: float = 180.0):
|
|
81
|
+
super().__init__(timeout=timeout)
|
|
82
|
+
self.chunks = chunks
|
|
83
|
+
self.current_page: int = 0
|
|
84
|
+
self.total_pages: int = len(chunks)
|
|
85
|
+
self.message: Optional[discord.Message] = None
|
|
86
|
+
self._update_button_states()
|
|
87
|
+
|
|
88
|
+
def _update_button_states(self) -> None:
|
|
89
|
+
"""Update button disabled states based on current page."""
|
|
90
|
+
self.prev_button.disabled = self.current_page <= 0
|
|
91
|
+
self.next_button.disabled = self.current_page >= self.total_pages - 1
|
|
92
|
+
|
|
93
|
+
@discord.ui.button(label="◀️ Previous", style=discord.ButtonStyle.gray)
|
|
94
|
+
async def prev_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
|
95
|
+
if self.current_page > 0:
|
|
96
|
+
self.current_page -= 1
|
|
97
|
+
self._update_button_states()
|
|
98
|
+
await interaction.response.defer()
|
|
99
|
+
if self.message:
|
|
100
|
+
await self.message.edit(view=self)
|
|
101
|
+
|
|
102
|
+
@discord.ui.button(label="▶️ Next", style=discord.ButtonStyle.gray)
|
|
103
|
+
async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
|
104
|
+
if self.current_page < self.total_pages - 1:
|
|
105
|
+
self.current_page += 1
|
|
106
|
+
self._update_button_states()
|
|
107
|
+
await interaction.response.defer()
|
|
108
|
+
if self.message:
|
|
109
|
+
await self.message.edit(view=self)
|
|
110
|
+
|
|
111
|
+
async def on_timeout(self) -> None:
|
|
112
|
+
"""Disable buttons after timeout."""
|
|
113
|
+
for item in self.children:
|
|
114
|
+
item.disabled = True
|
|
115
|
+
|
|
116
|
+
|
|
54
117
|
class DiscordBotLinuxMonitor:
|
|
55
118
|
|
|
56
119
|
#region Initialization
|
|
@@ -74,6 +137,9 @@ class DiscordBotLinuxMonitor:
|
|
|
74
137
|
self.force_sync_on_startup: bool = force_sync_on_startup
|
|
75
138
|
intents: discord.Intents = discord.Intents.default()
|
|
76
139
|
self.bot = commands.Bot(command_prefix=self.command_prefix, intents=intents)
|
|
140
|
+
|
|
141
|
+
# Initialize cleanup task
|
|
142
|
+
self.cleanup_task: Optional[asyncio.Task] = None
|
|
77
143
|
|
|
78
144
|
def _init_and_check_configuration(self) -> None:
|
|
79
145
|
"""
|
|
@@ -166,6 +232,47 @@ class DiscordBotLinuxMonitor:
|
|
|
166
232
|
|
|
167
233
|
#region Private methods
|
|
168
234
|
|
|
235
|
+
def _get_utc_timestamp(self) -> str:
|
|
236
|
+
"""Get current timestamp in UTC with timezone info."""
|
|
237
|
+
return datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')
|
|
238
|
+
|
|
239
|
+
def _log_command_audit(self, user: discord.User, guild: Optional[discord.Guild], channel: Optional[discord.TextChannel], command: str, details: str = "") -> None:
|
|
240
|
+
"""Log command execution for audit trail."""
|
|
241
|
+
timestamp = self._get_utc_timestamp()
|
|
242
|
+
guild_name = guild.name if guild else "Unknown"
|
|
243
|
+
channel_name = channel.name if channel else "Unknown"
|
|
244
|
+
details_str = f" | {details}" if details else ""
|
|
245
|
+
logging.info(msg=f"[AUDIT] {timestamp} | User: {user} (ID: {user.id}) | Guild: {guild_name} | Channel: #{channel_name} | Command: {command}{details_str}")
|
|
246
|
+
|
|
247
|
+
@functools.lru_cache(maxsize=128)
|
|
248
|
+
def _get_cached_command_names(self, is_private: bool) -> List[Tuple[str, str]]:
|
|
249
|
+
"""Cache command names permanently since they never change at runtime."""
|
|
250
|
+
return self.monitoring.get_command_names(is_private=is_private)
|
|
251
|
+
|
|
252
|
+
@functools.lru_cache(maxsize=128)
|
|
253
|
+
def _get_cached_service_names(self, is_private: bool) -> List[Tuple[str, str]]:
|
|
254
|
+
"""Cache service names permanently since they never change at runtime."""
|
|
255
|
+
return self.monitoring.get_service_names(is_private=is_private)
|
|
256
|
+
|
|
257
|
+
def _is_periodic_cleanup_enabled(self) -> bool:
|
|
258
|
+
"""Check if periodic channel cleanup is enabled in config."""
|
|
259
|
+
cleanup_config = self.config.get('periodic_channel_cleanup', {}) # type: ignore
|
|
260
|
+
return cleanup_config.get('enabled', False)
|
|
261
|
+
|
|
262
|
+
def _get_cleanup_interval(self) -> float:
|
|
263
|
+
"""Get the interval (in seconds) between cleanup cycles."""
|
|
264
|
+
cleanup_config = self.config.get('periodic_channel_cleanup', {}) # type: ignore
|
|
265
|
+
return float(cleanup_config.get('duration_in_sec_wait_between_each_execution', 604800))
|
|
266
|
+
|
|
267
|
+
def _get_cleanup_initial_delay(self) -> float:
|
|
268
|
+
"""Get the initial delay (in seconds) before first cleanup execution."""
|
|
269
|
+
cleanup_config = self.config.get('periodic_channel_cleanup', {}) # type: ignore
|
|
270
|
+
return float(cleanup_config.get('duration_in_sec_before_first_execution', 604800))
|
|
271
|
+
|
|
272
|
+
def _should_cleanup_start_immediately(self) -> bool:
|
|
273
|
+
"""Check if cleanup should start immediately (duration = 0)."""
|
|
274
|
+
return self._get_cleanup_initial_delay() == 0
|
|
275
|
+
|
|
169
276
|
def _check_if_valid_guild(self, guild: Union[None,discord.Guild]) -> bool:
|
|
170
277
|
if guild is None:
|
|
171
278
|
return False
|
|
@@ -325,7 +432,7 @@ class DiscordBotLinuxMonitor:
|
|
|
325
432
|
|
|
326
433
|
embed.add_field(name="🕗 Last deleted message", value=(last_deleted_preview if last_deleted_preview != "" else "N/A")[:1024], inline=False)
|
|
327
434
|
|
|
328
|
-
embed.set_footer(text=f"Bot v{__version__} • Last update: {
|
|
435
|
+
embed.set_footer(text=f"Bot v{__version__} • Last update: {self._get_utc_timestamp()}")
|
|
329
436
|
return embed
|
|
330
437
|
|
|
331
438
|
def _infer_embed_color(self, text: str, is_error: bool = False) -> "discord.Color":
|
|
@@ -339,7 +446,7 @@ class DiscordBotLinuxMonitor:
|
|
|
339
446
|
def _build_result_embed(self, title: str, description: str, color: "discord.Color") -> "discord.Embed":
|
|
340
447
|
embed = discord.Embed(title=title[:256], color=color)
|
|
341
448
|
embed.description = (description if description.strip() != "" else "No answer.")[:4096]
|
|
342
|
-
embed.set_footer(text=f"Bot v{__version__} • {
|
|
449
|
+
embed.set_footer(text=f"Bot v{__version__} • {self._get_utc_timestamp()}")
|
|
343
450
|
return embed
|
|
344
451
|
|
|
345
452
|
def _split_text_for_embed(self, text: str, max_length: int = 4000) -> List[str]:
|
|
@@ -355,21 +462,87 @@ class DiscordBotLinuxMonitor:
|
|
|
355
462
|
chunks.append(remaining)
|
|
356
463
|
return chunks
|
|
357
464
|
|
|
358
|
-
async def _interaction_followup_send_embed(self, interaction: discord.Interaction, title: str, msg: str, icon: str = "", is_error: bool = False, ephemeral: bool = False) -> None:
|
|
465
|
+
async def _interaction_followup_send_embed(self, interaction: discord.Interaction, title: str, msg: str, icon: str = "", is_error: bool = False, ephemeral: bool = False, enable_pagination: bool = True) -> None:
|
|
466
|
+
"""Send an embed message via interaction followup. Handles pagination for long messages."""
|
|
359
467
|
if msg is None or msg.strip() == "":
|
|
360
468
|
msg = "No answer."
|
|
361
469
|
|
|
362
|
-
|
|
470
|
+
# Only log info for non-error responses to reduce log spam
|
|
471
|
+
if not is_error:
|
|
472
|
+
logging.info(msg=f"Sending embed follow-up '{title}'")
|
|
363
473
|
|
|
364
474
|
color: "discord.Color" = self._infer_embed_color(text=msg, is_error=is_error)
|
|
365
475
|
display_title: str = f"{icon} {title}".strip()
|
|
366
476
|
chunks: List[str] = self._split_text_for_embed(text=msg)
|
|
367
477
|
|
|
368
478
|
try:
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
479
|
+
# If single chunk or pagination disabled, send normally
|
|
480
|
+
if len(chunks) <= 1 or not enable_pagination:
|
|
481
|
+
for index, chunk in enumerate(chunks):
|
|
482
|
+
page_title: str = display_title if len(chunks) == 1 else f"{display_title} ({index + 1}/{len(chunks)})"
|
|
483
|
+
embed: "discord.Embed" = self._build_result_embed(title=page_title, description=chunk, color=color)
|
|
484
|
+
try:
|
|
485
|
+
await interaction.followup.send(embed=embed, ephemeral=ephemeral)
|
|
486
|
+
except discord.InteractionResponded:
|
|
487
|
+
logging.debug(msg=f"Interaction already responded for {title}")
|
|
488
|
+
return
|
|
489
|
+
else:
|
|
490
|
+
# Multiple chunks: use pagination
|
|
491
|
+
class PaginatedEmbedView(discord.ui.View):
|
|
492
|
+
def __init__(self, paginator_self, chunks: List[str], title_template: str, color: discord.Color, ephemeral: bool):
|
|
493
|
+
super().__init__(timeout=180.0)
|
|
494
|
+
self.paginator = paginator_self
|
|
495
|
+
self.chunks = chunks
|
|
496
|
+
self.title_template = title_template
|
|
497
|
+
self.color = color
|
|
498
|
+
self.current_page = 0
|
|
499
|
+
self.message: Optional[discord.Message] = None
|
|
500
|
+
self.ephemeral = ephemeral
|
|
501
|
+
self._update_buttons()
|
|
502
|
+
|
|
503
|
+
def _update_buttons(self) -> None:
|
|
504
|
+
self.prev_btn.disabled = self.current_page <= 0
|
|
505
|
+
self.next_btn.disabled = self.current_page >= len(self.chunks) - 1
|
|
506
|
+
|
|
507
|
+
async def _update_embed(self, interaction: discord.Interaction) -> None:
|
|
508
|
+
chunk = self.chunks[self.current_page]
|
|
509
|
+
page_title = f"{self.title_template} ({self.current_page + 1}/{len(self.chunks)})"
|
|
510
|
+
embed = self.paginator._build_result_embed(title=page_title, description=chunk, color=self.color)
|
|
511
|
+
await interaction.response.defer()
|
|
512
|
+
if self.message:
|
|
513
|
+
await self.message.edit(embed=embed, view=self)
|
|
514
|
+
|
|
515
|
+
@discord.ui.button(label="◀️", style=discord.ButtonStyle.gray)
|
|
516
|
+
async def prev_btn(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
|
517
|
+
if self.current_page > 0:
|
|
518
|
+
self.current_page -= 1
|
|
519
|
+
self._update_buttons()
|
|
520
|
+
await self._update_embed(interaction)
|
|
521
|
+
|
|
522
|
+
@discord.ui.button(label="▶️", style=discord.ButtonStyle.gray)
|
|
523
|
+
async def next_btn(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
|
|
524
|
+
if self.current_page < len(self.chunks) - 1:
|
|
525
|
+
self.current_page += 1
|
|
526
|
+
self._update_buttons()
|
|
527
|
+
await self._update_embed(interaction)
|
|
528
|
+
|
|
529
|
+
async def on_timeout(self) -> None:
|
|
530
|
+
for item in self.children:
|
|
531
|
+
item.disabled = True
|
|
532
|
+
if self.message:
|
|
533
|
+
try:
|
|
534
|
+
await self.message.edit(view=self)
|
|
535
|
+
except Exception:
|
|
536
|
+
pass
|
|
537
|
+
|
|
538
|
+
view = PaginatedEmbedView(self, chunks, display_title, color, ephemeral)
|
|
539
|
+
embed = self._build_result_embed(title=f"{display_title} (1/{len(chunks)})", description=chunks[0], color=color)
|
|
540
|
+
try:
|
|
541
|
+
view.message = await interaction.followup.send(embed=embed, view=view, ephemeral=ephemeral)
|
|
542
|
+
except discord.InteractionResponded:
|
|
543
|
+
logging.debug(msg=f"Interaction already responded for {title}")
|
|
544
|
+
return
|
|
545
|
+
|
|
373
546
|
except Exception as e:
|
|
374
547
|
logging.error(msg=f"Error while sending embed follow-up message: {e}")
|
|
375
548
|
|
|
@@ -439,6 +612,97 @@ class DiscordBotLinuxMonitor:
|
|
|
439
612
|
continue
|
|
440
613
|
raise
|
|
441
614
|
|
|
615
|
+
async def _setup_periodic_cleanup_task(self) -> None:
|
|
616
|
+
"""Setup and start the periodic channel cleanup task."""
|
|
617
|
+
if not self._is_periodic_cleanup_enabled():
|
|
618
|
+
logging.info(msg="Periodic channel cleanup is disabled in config")
|
|
619
|
+
return
|
|
620
|
+
|
|
621
|
+
logging.info(msg="Setting up periodic channel cleanup task")
|
|
622
|
+
self.cleanup_task = asyncio.create_task(self._periodic_cleanup_loop())
|
|
623
|
+
|
|
624
|
+
async def _periodic_cleanup_loop(self) -> None:
|
|
625
|
+
"""Loop that periodically cleans configured channels."""
|
|
626
|
+
await self.bot.wait_until_ready()
|
|
627
|
+
|
|
628
|
+
# Wait before first execution if not immediate
|
|
629
|
+
if not self._should_cleanup_start_immediately():
|
|
630
|
+
initial_delay = self._get_cleanup_initial_delay()
|
|
631
|
+
logging.info(msg=f"Periodic cleanup scheduled for {self._format_duration(initial_delay)} from now")
|
|
632
|
+
await asyncio.sleep(initial_delay)
|
|
633
|
+
|
|
634
|
+
while True:
|
|
635
|
+
try:
|
|
636
|
+
for guild in self.bot.guilds:
|
|
637
|
+
if guild.id == self.server_id:
|
|
638
|
+
await self._execute_cleanup_cycle(guild)
|
|
639
|
+
except Exception as e:
|
|
640
|
+
logging.error(msg=f"Error in cleanup loop: {e}")
|
|
641
|
+
|
|
642
|
+
interval = self._get_cleanup_interval()
|
|
643
|
+
await asyncio.sleep(interval)
|
|
644
|
+
|
|
645
|
+
async def _execute_cleanup_cycle(self, guild: discord.Guild) -> None:
|
|
646
|
+
"""Execute cleanup for all configured channels."""
|
|
647
|
+
cleanup_config = self.config.get('periodic_channel_cleanup', {}) # type: ignore
|
|
648
|
+
channels_config = cleanup_config.get('channels', [])
|
|
649
|
+
|
|
650
|
+
if not channels_config:
|
|
651
|
+
return
|
|
652
|
+
|
|
653
|
+
logging.info(msg=f"Starting periodic cleanup cycle for {len(channels_config)} channels")
|
|
654
|
+
|
|
655
|
+
for channel_config in channels_config:
|
|
656
|
+
try:
|
|
657
|
+
channel_name = channel_config.get('channel_name')
|
|
658
|
+
min_days = channel_config.get('min_days_to_keep', 7)
|
|
659
|
+
description = channel_config.get('description', '')
|
|
660
|
+
|
|
661
|
+
if not channel_name:
|
|
662
|
+
logging.warning(msg="Cleanup config entry missing 'channel_name'")
|
|
663
|
+
continue
|
|
664
|
+
|
|
665
|
+
channel = discord.utils.get(guild.text_channels, name=channel_name)
|
|
666
|
+
if not channel:
|
|
667
|
+
logging.warning(msg=f"Cleanup channel '{channel_name}' not found in guild")
|
|
668
|
+
continue
|
|
669
|
+
|
|
670
|
+
# Check permissions
|
|
671
|
+
bot_member = guild.me
|
|
672
|
+
if bot_member is None:
|
|
673
|
+
logging.warning(msg="Unable to resolve bot member for cleanup")
|
|
674
|
+
continue
|
|
675
|
+
|
|
676
|
+
permissions = channel.permissions_for(bot_member)
|
|
677
|
+
if not permissions.manage_messages or not permissions.read_message_history:
|
|
678
|
+
logging.warning(msg=f"Bot missing permissions for cleanup in channel '{channel_name}'")
|
|
679
|
+
continue
|
|
680
|
+
|
|
681
|
+
cutoff_date = datetime.now(timezone.utc) - timedelta(days=min_days)
|
|
682
|
+
deleted_count = 0
|
|
683
|
+
|
|
684
|
+
async for message in channel.history(oldest_first=False):
|
|
685
|
+
if message.created_at < cutoff_date:
|
|
686
|
+
try:
|
|
687
|
+
await self._delete_message_with_rate_limit_retry(
|
|
688
|
+
message=message,
|
|
689
|
+
reason=f"Periodic cleanup - keeping messages newer than {min_days} days"
|
|
690
|
+
)
|
|
691
|
+
deleted_count += 1
|
|
692
|
+
except Exception as e:
|
|
693
|
+
logging.warning(msg=f"Failed to delete message in cleanup: {e}")
|
|
694
|
+
# Continue with next message
|
|
695
|
+
continue
|
|
696
|
+
|
|
697
|
+
log_msg = f"Periodic cleanup #{channel_name}: Deleted {deleted_count} messages (kept last {min_days} days)"
|
|
698
|
+
if description:
|
|
699
|
+
log_msg += f" - {description}"
|
|
700
|
+
logging.info(msg=log_msg)
|
|
701
|
+
|
|
702
|
+
except Exception as e:
|
|
703
|
+
logging.error(msg=f"Error cleaning up channel '{channel_config.get('channel_name', 'unknown')}': {e}")
|
|
704
|
+
continue
|
|
705
|
+
|
|
442
706
|
#endregion
|
|
443
707
|
|
|
444
708
|
#region BOT COMMANDS AND EVENTS DEFINITIONS
|
|
@@ -560,6 +824,9 @@ class DiscordBotLinuxMonitor:
|
|
|
560
824
|
if self.force_sync_on_startup:
|
|
561
825
|
await self._force_sync()
|
|
562
826
|
|
|
827
|
+
# Setup periodic channel cleanup task
|
|
828
|
+
await self._setup_periodic_cleanup_task()
|
|
829
|
+
|
|
563
830
|
async def force_sync(self, interaction: discord.Interaction) -> None:
|
|
564
831
|
if not self._check_if_valid_guild(guild=interaction.guild):
|
|
565
832
|
return
|
|
@@ -586,9 +853,9 @@ class DiscordBotLinuxMonitor:
|
|
|
586
853
|
return
|
|
587
854
|
|
|
588
855
|
out_msg: str = (
|
|
589
|
-
f"🤖
|
|
590
|
-
f"
|
|
591
|
-
f"
|
|
856
|
+
f"🤖 Discord bot version: {__version__}\n"
|
|
857
|
+
f"- Linux Monitor library version: {self.monitoring.get_raw_version()}\n"
|
|
858
|
+
f"- 🐍 Python compatibility: {__python_version__}"
|
|
592
859
|
)
|
|
593
860
|
embed = self._build_result_embed(title="🤖 Bot Version", description=out_msg, color=discord.Color.blurple())
|
|
594
861
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
|
@@ -767,10 +1034,28 @@ class DiscordBotLinuxMonitor:
|
|
|
767
1034
|
await interaction.response.send_message(content="❌ Public channels do not allow this command.", ephemeral=True)
|
|
768
1035
|
return
|
|
769
1036
|
|
|
770
|
-
#
|
|
771
|
-
await interaction.response.defer()
|
|
1037
|
+
# Show confirmation dialog
|
|
1038
|
+
await interaction.response.defer(ephemeral=True)
|
|
1039
|
+
|
|
1040
|
+
view = ConfirmationView()
|
|
1041
|
+
confirmation_msg = await interaction.followup.send(
|
|
1042
|
+
content="⚠️ **DANGEROUS OPERATION** ⚠️\n\nYou are about to reboot the entire server. This will disconnect all users and services!\n\nAre you sure?",
|
|
1043
|
+
view=view,
|
|
1044
|
+
ephemeral=True
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
await view.wait()
|
|
1048
|
+
|
|
1049
|
+
if not view.confirmed:
|
|
1050
|
+
await confirmation_msg.edit(content="❌ Server reboot cancelled.", view=None)
|
|
1051
|
+
self._log_command_audit(interaction.user, interaction.guild, interaction.channel, "reboot", "CANCELLED")
|
|
1052
|
+
return
|
|
1053
|
+
|
|
1054
|
+
# Log the audit trail
|
|
1055
|
+
self._log_command_audit(interaction.user, interaction.guild, interaction.channel, "reboot", "CONFIRMED")
|
|
772
1056
|
|
|
773
1057
|
try:
|
|
1058
|
+
await confirmation_msg.edit(content="⏳ Rebooting server...", view=None)
|
|
774
1059
|
out_msg: str = await self.monitoring.reboot_server()
|
|
775
1060
|
await self._interaction_followup_send_embed(interaction=interaction, title="Server Reboot", icon="🔁", msg=out_msg)
|
|
776
1061
|
|
|
@@ -805,6 +1090,9 @@ class DiscordBotLinuxMonitor:
|
|
|
805
1090
|
if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
|
|
806
1091
|
return
|
|
807
1092
|
|
|
1093
|
+
# Log the audit trail
|
|
1094
|
+
self._log_command_audit(interaction.user, interaction.guild, interaction.channel, "restart_all")
|
|
1095
|
+
|
|
808
1096
|
# Say to the user that the command is being processed
|
|
809
1097
|
await interaction.response.defer()
|
|
810
1098
|
|
|
@@ -826,6 +1114,9 @@ class DiscordBotLinuxMonitor:
|
|
|
826
1114
|
if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
|
|
827
1115
|
return
|
|
828
1116
|
|
|
1117
|
+
# Log the audit trail
|
|
1118
|
+
self._log_command_audit(interaction.user, interaction.guild, interaction.channel, "restart_service", f"service={service_name}")
|
|
1119
|
+
|
|
829
1120
|
# Indiquer que la commande est en cours de traitement
|
|
830
1121
|
await interaction.response.defer()
|
|
831
1122
|
|
|
@@ -848,6 +1139,9 @@ class DiscordBotLinuxMonitor:
|
|
|
848
1139
|
if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
|
|
849
1140
|
return
|
|
850
1141
|
|
|
1142
|
+
# Log the audit trail
|
|
1143
|
+
self._log_command_audit(interaction.user, interaction.guild, interaction.channel, "stop_service", f"service={service_name}")
|
|
1144
|
+
|
|
851
1145
|
# Indiquer que la commande est en cours de traitement
|
|
852
1146
|
await interaction.response.defer()
|
|
853
1147
|
|
|
@@ -937,6 +1231,9 @@ class DiscordBotLinuxMonitor:
|
|
|
937
1231
|
await interaction.response.send_message(content="❌ Public channels do not allow this command.", ephemeral=True)
|
|
938
1232
|
return
|
|
939
1233
|
|
|
1234
|
+
# Log the audit trail
|
|
1235
|
+
self._log_command_audit(interaction.user, interaction.guild, interaction.channel, "kill_process", f"pid={pid}")
|
|
1236
|
+
|
|
940
1237
|
# Indiquer que la commande est en cours de traitement
|
|
941
1238
|
await interaction.response.defer()
|
|
942
1239
|
|
|
@@ -978,6 +1275,9 @@ class DiscordBotLinuxMonitor:
|
|
|
978
1275
|
|
|
979
1276
|
await interaction.response.defer(ephemeral=True)
|
|
980
1277
|
|
|
1278
|
+
# Log the audit trail for this destructive action
|
|
1279
|
+
self._log_command_audit(interaction.user, interaction.guild, interaction.channel, "clear_channel_messages", f"target_channel=#{channel.name}")
|
|
1280
|
+
|
|
981
1281
|
deleted_count: int = 0
|
|
982
1282
|
rate_limit_retries: int = 0
|
|
983
1283
|
delete_reason: str = f"Requested by {interaction.user} from private channel"
|
|
@@ -1191,6 +1491,200 @@ class DiscordBotLinuxMonitor:
|
|
|
1191
1491
|
logging.exception(msg=out_msg)
|
|
1192
1492
|
await self._interaction_followup_send_embed(interaction=interaction, title="Available Commands", icon="📖", msg=out_msg, is_error=True)
|
|
1193
1493
|
|
|
1494
|
+
async def show_list_periodic_channels_cleanup(self, interaction: discord.Interaction) -> None:
|
|
1495
|
+
"""Show auto-cleanup channel configuration and permission status."""
|
|
1496
|
+
if not self._check_if_valid_guild(guild=interaction.guild):
|
|
1497
|
+
return
|
|
1498
|
+
if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
|
|
1499
|
+
return
|
|
1500
|
+
if not self._is_private_channel(channel=interaction.channel): # type: ignore
|
|
1501
|
+
await interaction.response.send_message(content="❌ This command is only available in private channels.", ephemeral=True)
|
|
1502
|
+
return
|
|
1503
|
+
|
|
1504
|
+
await interaction.response.defer()
|
|
1505
|
+
|
|
1506
|
+
try:
|
|
1507
|
+
# Check if cleanup is enabled
|
|
1508
|
+
if not self._is_periodic_cleanup_enabled():
|
|
1509
|
+
await self._interaction_followup_send_embed(
|
|
1510
|
+
interaction=interaction,
|
|
1511
|
+
title="Channel Auto-Cleanup Configuration",
|
|
1512
|
+
icon="🧹",
|
|
1513
|
+
msg="⚠️ **Periodic channel cleanup is DISABLED** in config"
|
|
1514
|
+
)
|
|
1515
|
+
return
|
|
1516
|
+
|
|
1517
|
+
cleanup_config = self.config.get('periodic_channel_cleanup', {}) # type: ignore
|
|
1518
|
+
channels_config = cleanup_config.get('channels', [])
|
|
1519
|
+
|
|
1520
|
+
if not channels_config:
|
|
1521
|
+
await self._interaction_followup_send_embed(
|
|
1522
|
+
interaction=interaction,
|
|
1523
|
+
title="Channel Auto-Cleanup Configuration",
|
|
1524
|
+
icon="🧹",
|
|
1525
|
+
msg="⚠️ **No channels configured** for auto-cleanup"
|
|
1526
|
+
)
|
|
1527
|
+
return
|
|
1528
|
+
|
|
1529
|
+
guild = interaction.guild
|
|
1530
|
+
bot_member = guild.me if guild else None
|
|
1531
|
+
|
|
1532
|
+
lines = [
|
|
1533
|
+
f"**Status**: ✅ Enabled",
|
|
1534
|
+
f"**Next Cleanup**: {self._format_duration(self._get_cleanup_initial_delay())} from bot start",
|
|
1535
|
+
f"**Cleanup Interval**: Every {self._format_duration(self._get_cleanup_interval())}",
|
|
1536
|
+
"",
|
|
1537
|
+
"**Configured Channels**:",
|
|
1538
|
+
""
|
|
1539
|
+
]
|
|
1540
|
+
|
|
1541
|
+
for idx, channel_config in enumerate(channels_config, 1):
|
|
1542
|
+
channel_name = channel_config.get('channel_name', 'unknown')
|
|
1543
|
+
min_days = channel_config.get('min_days_to_keep', 7)
|
|
1544
|
+
description = channel_config.get('description', '')
|
|
1545
|
+
|
|
1546
|
+
# Find channel
|
|
1547
|
+
channel = discord.utils.get(guild.text_channels, name=channel_name) if guild else None
|
|
1548
|
+
channel_status = "✅" if channel else "❌"
|
|
1549
|
+
|
|
1550
|
+
# Check permissions if channel exists
|
|
1551
|
+
perms_status = "✅"
|
|
1552
|
+
perms_details = []
|
|
1553
|
+
if channel and bot_member:
|
|
1554
|
+
permissions = channel.permissions_for(bot_member)
|
|
1555
|
+
if not permissions.manage_messages:
|
|
1556
|
+
perms_status = "❌"
|
|
1557
|
+
perms_details.append("missing `manage_messages`")
|
|
1558
|
+
if not permissions.read_message_history:
|
|
1559
|
+
perms_status = "❌"
|
|
1560
|
+
perms_details.append("missing `read_message_history`")
|
|
1561
|
+
elif channel and not bot_member:
|
|
1562
|
+
perms_status = "⚠️"
|
|
1563
|
+
perms_details.append("bot member not found")
|
|
1564
|
+
elif not channel:
|
|
1565
|
+
perms_status = "N/A"
|
|
1566
|
+
perms_details.append("channel not found")
|
|
1567
|
+
|
|
1568
|
+
# Build channel line
|
|
1569
|
+
channel_line = f"**{idx}. {channel_status} #{channel_name}**"
|
|
1570
|
+
if description:
|
|
1571
|
+
channel_line += f"\n 📝 {description}"
|
|
1572
|
+
channel_line += f"\n 🕐 Keep messages: Last {min_days} days"
|
|
1573
|
+
channel_line += f"\n 🔐 Permissions: {perms_status}"
|
|
1574
|
+
if perms_details:
|
|
1575
|
+
channel_line += f" ({', '.join(perms_details)})"
|
|
1576
|
+
|
|
1577
|
+
lines.append(channel_line)
|
|
1578
|
+
lines.append("")
|
|
1579
|
+
|
|
1580
|
+
# Summary
|
|
1581
|
+
summary_icon = "✅"
|
|
1582
|
+
all_ok = all(
|
|
1583
|
+
discord.utils.get(guild.text_channels, name=ch.get('channel_name', '')) and
|
|
1584
|
+
(guild.me and
|
|
1585
|
+
guild.me.permissions_for(discord.utils.get(guild.text_channels, name=ch.get('channel_name', ''))).manage_messages and
|
|
1586
|
+
guild.me.permissions_for(discord.utils.get(guild.text_channels, name=ch.get('channel_name', ''))).read_message_history)
|
|
1587
|
+
for ch in channels_config
|
|
1588
|
+
)
|
|
1589
|
+
if not all_ok:
|
|
1590
|
+
summary_icon = "⚠️"
|
|
1591
|
+
|
|
1592
|
+
lines.append(f"**Overall Status**: {summary_icon} " + ("All channels properly configured" if all_ok else "Some issues need attention"))
|
|
1593
|
+
|
|
1594
|
+
msg = "\n".join(lines)
|
|
1595
|
+
is_error = not all_ok
|
|
1596
|
+
|
|
1597
|
+
await self._interaction_followup_send_embed(
|
|
1598
|
+
interaction=interaction,
|
|
1599
|
+
title="Channel Auto-Cleanup Configuration",
|
|
1600
|
+
icon="🧹",
|
|
1601
|
+
msg=msg,
|
|
1602
|
+
is_error=is_error
|
|
1603
|
+
)
|
|
1604
|
+
|
|
1605
|
+
except Exception as e:
|
|
1606
|
+
out_msg = f"**Internal error checking cleanup configuration**:\n```sh\n{e}\n```"
|
|
1607
|
+
logging.exception(msg=out_msg)
|
|
1608
|
+
await self._interaction_followup_send_embed(
|
|
1609
|
+
interaction=interaction,
|
|
1610
|
+
title="Channel Auto-Cleanup Configuration",
|
|
1611
|
+
icon="🧹",
|
|
1612
|
+
msg=out_msg,
|
|
1613
|
+
is_error=True
|
|
1614
|
+
)
|
|
1615
|
+
|
|
1616
|
+
async def show_help(self, interaction: discord.Interaction, command_name: str = "") -> None:
|
|
1617
|
+
"""Show help for Discord bot commands with cooldown info."""
|
|
1618
|
+
if not self._check_if_valid_guild(guild=interaction.guild):
|
|
1619
|
+
return
|
|
1620
|
+
if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
|
|
1621
|
+
return
|
|
1622
|
+
|
|
1623
|
+
await interaction.response.defer()
|
|
1624
|
+
|
|
1625
|
+
try:
|
|
1626
|
+
# Get all commands from the bot tree
|
|
1627
|
+
commands_list: List[str] = []
|
|
1628
|
+
|
|
1629
|
+
# Command metadata with descriptions, cooldowns, and requirements
|
|
1630
|
+
command_info = {
|
|
1631
|
+
"force_sync": {"desc": "🔄 Force command synchronization", "cooldown": "3/20s", "private": True},
|
|
1632
|
+
"version": {"desc": "🤖 Show bot version", "cooldown": "3/20s", "private": False},
|
|
1633
|
+
"usage": {"desc": "📊 View disk space, CPU, RAM", "cooldown": "3/20s", "private": False},
|
|
1634
|
+
"os_infos": {"desc": "🖥️ View basic system info", "cooldown": "3/20s", "private": False},
|
|
1635
|
+
"users": {"desc": "👥 View connected users (private)", "cooldown": "3/20s", "private": True},
|
|
1636
|
+
"user_logins": {"desc": "👥 View last user connections (private)", "cooldown": "3/20s", "private": True},
|
|
1637
|
+
"ping": {"desc": "🌐 Ping websites", "cooldown": "3/20s", "private": False},
|
|
1638
|
+
"websites": {"desc": "🌐 Check website access", "cooldown": "3/20s", "private": False},
|
|
1639
|
+
"certificates": {"desc": "🔒 Check SSL certificates", "cooldown": "3/20s", "private": False},
|
|
1640
|
+
"reboot_server": {"desc": "🔄 Restart the entire server (private, requires confirmation)", "cooldown": "1/60s", "private": True},
|
|
1641
|
+
"services_status": {"desc": "🩺 Check services status", "cooldown": "3/20s", "private": False},
|
|
1642
|
+
"restart_all": {"desc": "🚀 Restart all services", "cooldown": "1/30s", "private": False},
|
|
1643
|
+
"restart_service": {"desc": "🚀 Restart a specific service", "cooldown": "3/20s", "private": False},
|
|
1644
|
+
"stop_service": {"desc": "🛑 Stop a service", "cooldown": "3/20s", "private": False},
|
|
1645
|
+
"list_services": {"desc": "📋 List all services", "cooldown": "3/20s", "private": False},
|
|
1646
|
+
"ports": {"desc": "🔒 Check ports", "cooldown": "3/20s", "private": False},
|
|
1647
|
+
"list_processes": {"desc": "📋 List processes by RAM (private)", "cooldown": "3/20s", "private": True},
|
|
1648
|
+
"list_processes_by_cpu_usage": {"desc": "📋 List processes by CPU (private)", "cooldown": "3/20s", "private": True},
|
|
1649
|
+
"kill_process": {"desc": "☠️ Kill process by PID (private)", "cooldown": "3/20s", "private": True},
|
|
1650
|
+
"clear_channel_messages": {"desc": "🧹 Clear channel messages (private, destructive)", "cooldown": "1/120s", "private": True},
|
|
1651
|
+
"list_clearable_channels": {"desc": "🧹 Show clearable channels (private)", "cooldown": "3/20s", "private": True},
|
|
1652
|
+
"list_commands": {"desc": "📋 List Linux monitor commands", "cooldown": "3/20s", "private": False},
|
|
1653
|
+
"execute_command": {"desc": "🚀 Execute a Linux command", "cooldown": "3/20s", "private": False},
|
|
1654
|
+
"execute_all_commands": {"desc": "🚀 Execute all Linux commands", "cooldown": "1/60s", "private": False},
|
|
1655
|
+
"help": {"desc": "🔍 Show this help message", "cooldown": "3/20s", "private": False},
|
|
1656
|
+
"list_periodic_channels_cleanup": {"desc": "🧹 Show auto-cleanup channel configuration and permission status", "cooldown": "3/20s", "private": True},
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
# Filter commands based on channel privacy
|
|
1660
|
+
is_private_channel = self._is_private_channel(channel=interaction.channel) # type: ignore
|
|
1661
|
+
filtered_commands = {k: v for k, v in command_info.items() if v.get("private", False) == is_private_channel or not v.get("private", False)}
|
|
1662
|
+
|
|
1663
|
+
# Filter by command name if provided
|
|
1664
|
+
if command_name:
|
|
1665
|
+
command_name_lower = command_name.lower()
|
|
1666
|
+
matching_cmds = {k: v for k, v in filtered_commands.items() if command_name_lower in k.lower()}
|
|
1667
|
+
if not matching_cmds:
|
|
1668
|
+
out_msg = f"❌ No commands found matching '{command_name}'"
|
|
1669
|
+
await self._interaction_followup_send_embed(interaction=interaction, title="Help", icon="🔍", msg=out_msg)
|
|
1670
|
+
return
|
|
1671
|
+
command_info = matching_cmds
|
|
1672
|
+
else:
|
|
1673
|
+
command_info = filtered_commands
|
|
1674
|
+
|
|
1675
|
+
# Build help text
|
|
1676
|
+
out_msg = ""
|
|
1677
|
+
for cmd, info in sorted(command_info.items()):
|
|
1678
|
+
private_indicator = "🔒 Private" if info["private"] else "🌐 Public"
|
|
1679
|
+
out_msg += f"**/{cmd}** — {info['desc']}\n"
|
|
1680
|
+
out_msg += f" └─ {private_indicator} | ⏳ Cooldown: {info['cooldown']}\n\n"
|
|
1681
|
+
|
|
1682
|
+
await self._interaction_followup_send_embed(interaction=interaction, title="Discord Bot Commands Help", icon="🔍", msg=out_msg)
|
|
1683
|
+
except Exception as e:
|
|
1684
|
+
out_msg = f"**Internal error retrieving help**:\n```sh\n{e}\n```"
|
|
1685
|
+
logging.exception(msg=out_msg)
|
|
1686
|
+
await self._interaction_followup_send_embed(interaction=interaction, title="Help", icon="🔍", msg=out_msg, is_error=True)
|
|
1687
|
+
|
|
1194
1688
|
|
|
1195
1689
|
async def autocomplete_command_name(self, interaction: discord.Interaction, current: str) -> List["app_commands.Choice[str]"]:
|
|
1196
1690
|
# Suggest the configured command names so the user picks from a list instead of typing them.
|
|
@@ -1198,7 +1692,7 @@ class DiscordBotLinuxMonitor:
|
|
|
1198
1692
|
is_private: bool = self._is_private_channel(channel=interaction.channel) # type: ignore
|
|
1199
1693
|
current_lower: str = current.lower()
|
|
1200
1694
|
choices: List["app_commands.Choice[str]"] = []
|
|
1201
|
-
for command_name, display_name in self.
|
|
1695
|
+
for command_name, display_name in self._get_cached_command_names(is_private=is_private):
|
|
1202
1696
|
if current_lower == "" or current_lower in command_name.lower() or current_lower in display_name.lower():
|
|
1203
1697
|
label: str = f"{command_name} — {display_name}"
|
|
1204
1698
|
choices.append(app_commands.Choice(name=label[:100], value=command_name))
|
|
@@ -1215,7 +1709,7 @@ class DiscordBotLinuxMonitor:
|
|
|
1215
1709
|
is_private: bool = self._is_private_channel(channel=interaction.channel) # type: ignore
|
|
1216
1710
|
current_lower: str = current.lower()
|
|
1217
1711
|
choices: List["app_commands.Choice[str]"] = []
|
|
1218
|
-
for service_name, display_name in self.
|
|
1712
|
+
for service_name, display_name in self._get_cached_service_names(is_private=is_private):
|
|
1219
1713
|
if current_lower == "" or current_lower in service_name.lower() or current_lower in display_name.lower():
|
|
1220
1714
|
label: str = f"{service_name} — {display_name}"
|
|
1221
1715
|
choices.append(app_commands.Choice(name=label[:100], value=service_name))
|
|
@@ -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.
|
|
9
|
+
version="1.7.0",
|
|
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.8 → discordbotlinuxmonitor-1.7.0}/discordbotlinuxmonitor/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|