DiscordBotLinuxMonitor 1.0.1__py3-none-any.whl

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.
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Quentin Comte-Gaz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,200 @@
1
+ Metadata-Version: 2.1
2
+ Name: DiscordBotLinuxMonitor
3
+ Version: 1.0.1
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
+ Home-page: https://github.com/QuentinCG/Linux-Monitor-Python-Library
6
+ Author: Quentin Comte-Gaz
7
+ Author-email: quentin@comte-gaz.com
8
+ License: MIT
9
+ Keywords: discord bot warning info linux monitor monitoring server service port ping ssl certificate disk folder cpu ram swap usage ip connection
10
+ Platform: Linux
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: POSIX :: Linux
14
+ Classifier: Natural Language :: English
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: System Administrators
17
+ Classifier: Topic :: System :: Monitoring
18
+ Classifier: Topic :: System :: Networking :: Monitoring
19
+ Classifier: Topic :: System :: Systems Administration
20
+ Classifier: Development Status :: 5 - Production/Stable
21
+ Classifier: Environment :: Console
22
+ Requires-Python: >=3.3
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE.md
25
+ Requires-Dist: linuxmonitor
26
+ Requires-Dist: discord.py
27
+ Requires-Dist: typing
28
+ Requires-Dist: asyncio
29
+
30
+ # Discord Bot Linux Monitor (Python library)
31
+ [![PyPI version](https://badge.fury.io/py/DiscordBotLinuxMonitor.svg)](https://pypi.org/project/DiscordBotLinuxMonitor/) [![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://github.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/blob/master/LICENSE.md) [![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://paypal.me/QuentinCG) [![Downloads](https://static.pepy.tech/badge/DiscordBotLinuxMonitor)](https://pepy.tech/project/DiscordBotLinuxMonitor) [![Downloads](https://static.pepy.tech/badge/DiscordBotLinuxMonitor/month)](https://pepy.tech/project/DiscordBotLinuxMonitor)
32
+
33
+ ## What is it
34
+
35
+ This python library is designed to be used as a Linux service to monitor the Linux server manually with discord commands and to send warning or information of the Linux server status to a discord channel periodically (if relevant).
36
+
37
+ It is possible to have separate 'private discord channel' and 'public discord channel' for:
38
+ - Sendind discord commands to the bot (visual example in next section)
39
+ - Getting periodic status of the Linux server (doing periodically some commands from next section)
40
+ - Getting warnings if there is an issue in the Linux server (periodic status check)
41
+
42
+ Example of warning:
43
+ <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/example/warning_from_periodic_check.jpg" height="400">
44
+
45
+ It is compatible with python 3+ and usable only on Linux.
46
+
47
+ <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/discord.png" width="300">
48
+
49
+ ## List of Discord commands:
50
+
51
+ ### Public and private discord channel commands
52
+ Displayed infos are not the same if you do the command in private or public channel (all info are displayed in private channel, some are hidden from public channels)
53
+
54
+ - `/usage`: 📊 View disk space, CPU, RAM, ... 📊
55
+ <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/example/usage.jpg" height="400">
56
+
57
+ - `/os_infos`: 🖥 View basic system information 🖥
58
+ <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/example/os_infos.jpg" height="300">
59
+
60
+ - `/ping`: 🌐 Ping websites 🌐
61
+ <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/example/ping.jpg" height="150">
62
+
63
+ - `/certificates`: 🔒 Check SSL certificates 🔒
64
+ <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/example/certificates.jpg" height="400">
65
+
66
+ - `/services_status`: 🩺 Check services are running 🩺
67
+ <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/example/services_status.jpg" height="150">
68
+
69
+ - `/restart_all`: 🚀 Restart all services 🚀
70
+ <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/example/restart_all.jpg" height="150">
71
+
72
+ - `/restart_service {service_name}`: 🚀 Restart a service 🚀
73
+ <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/example/restart_service.jpg" height="150">
74
+
75
+ - `/list_services`: 📋 List all available services 📋
76
+ <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/example/list_services.jpg" height="150">
77
+
78
+ - `/ports`: 🔒 Check ports 🔒
79
+ <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/example/ports.jpg" height="200">
80
+
81
+
82
+ ### Private discord channel commands:
83
+ - `/force_sync`: 🔄 Force discord command synchronization 🔄
84
+ <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/example/force_sync.jpg" height="300">
85
+
86
+ - `/users`: 👥 View connected users 👥
87
+ <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/example/users.jpg" height="150">
88
+
89
+ - `/user_logins`: 👥 View last user connections 👥
90
+ <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/example/user_logins.jpg" height="150">
91
+
92
+ - `/reboot_server`: 🔄 Restart the entire server 🔄
93
+ <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/example/reboot_server.jpg" height="100">
94
+
95
+ - `/list_processes`: 📋 List active processes 📋
96
+ <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/example/list_processes.jpg" height="200">
97
+
98
+ - `/kill_process`: 🚫 Stop a process by PID 🚫
99
+ <img src="https://raw.githubusercontent.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/master/example/kill_process.jpg" height="200">
100
+
101
+ ## How to install (for first launch)
102
+
103
+ - Install package calling `python -m pip install discordbotlinuxmonitor` (or `python setup.py install` from the root of this repository)
104
+ - Copy and edit [config-example.json file](https://github.com/QuentinCG/Discord-Bot-Linux-Monitor-Python-Library/blob/master/config-example.json) depending on your need (on first launch, remove all `restart_command` from config file to prevent potential looping service restart issues on your server in case your config file is not well configured)
105
+ - Launch the lib for testing it works:
106
+ ```shell
107
+ # Get help
108
+ python3 -m discordbotlinuxmonitor --help
109
+ # Use "--debug" to show more information during command
110
+ # Use "--nodebug" to not show any warning information during command
111
+
112
+ # Start the discord bot Linux monitor (First time)
113
+ python3 -m discordbotlinuxmonitor --config_file config-example.json --force_sync_on_startup True --debug
114
+
115
+ # Start the discord bot Linux monitor (after first time)
116
+ python3 -m discordbotlinuxmonitor --config_file config-example.json --force_sync_on_startup False
117
+ ```
118
+ - Go to discord (restart discord if you were already in it)
119
+ - You should see welcome messages on channels you configured in the config file and be able to communicate with the bot using command defined in previous section
120
+
121
+
122
+ ## How to install this lib as a service (to keep it running in the Linux server as a monitor)
123
+
124
+ - Stop the discord bot and create a service to have it running even after computer reboot:
125
+ ```sh
126
+ ### Define all base info
127
+ DISCORD_BOT_SERVICE_USER="discordbotlinuxmonitor"
128
+ DISCORD_BOT_SERVICE_GROUP="discordbotlinuxmonitor"
129
+ DISCORD_BOT_SERVICE_NAME="discord-bot"
130
+ DISCORD_BOT_SERVICE_FILE="/etc/systemd/system/${DISCORD_BOT_SERVICE_NAME}.service"
131
+ DISCORD_BOT_FOLDER="/opt/Discord/"
132
+
133
+ ### Add rights to user launching the library depending on what you want it to do ###
134
+ # Only if this library should be able to reboot the server on demand:
135
+ echo "$DISCORD_BOT_SERVICE_USER ALL=(ALL) NOPASSWD: /sbin/reboot" >> /etc/sudoers.d/$DISCORD_BOT_SERVICE_USER
136
+ # Only if this library should be able to kill a process on demand:
137
+ echo "$DISCORD_BOT_SERVICE_USER ALL=(ALL) NOPASSWD: /bin/kill" >> /etc/sudoers.d/$DISCORD_BOT_SERVICE_USER
138
+ # Add also all processes added in your config JSON file you want the library to be able to execute
139
+ # Example for the existing config-example.json file:
140
+ echo "$DISCORD_BOT_SERVICE_USER ALL=(ALL) NOPASSWD: /bin/systemctl" >> /etc/sudoers.d/$DISCORD_BOT_SERVICE_USER
141
+ echo "$DISCORD_BOT_SERVICE_USER ALL=(ALL) NOPASSWD: /etc/init.d/apache2" >> /etc/sudoers.d/$DISCORD_BOT_SERVICE_USER
142
+ echo "$DISCORD_BOT_SERVICE_USER ALL=(ALL) NOPASSWD: /etc/init.d/mariadb" >> /etc/sudoers.d/$DISCORD_BOT_SERVICE_USER
143
+
144
+ ### Create a specific user and group to launch the discord bot service ###
145
+ echo "Creating user $DISCORD_BOT_SERVICE_USER..."
146
+ sudo useradd -r -s /usr/sbin/nologin $DISCORD_BOT_SERVICE_USER
147
+ sudo mkdir -p /home/$DISCORD_BOT_SERVICE_USER
148
+ sudo chown $DISCORD_BOT_SERVICE_USER:$DISCORD_BOT_SERVICE_GROUP /home/$DISCORD_BOT_SERVICE_USER
149
+ sudo usermod -d /home/$DISCORD_BOT_SERVICE_USER $DISCORD_BOT_SERVICE_USER
150
+
151
+ ### Install DiscordBotLinuxMonitor lib for the user ###
152
+ echo "Installing DiscordBotLinuxMonitor for user $DISCORD_BOT_SERVICE_USER..."
153
+ sudo -u $DISCORD_BOT_SERVICE_USER -s
154
+ python3 -m venv /home/$DISCORD_BOT_SERVICE_USER/venv
155
+ source /home/$DISCORD_BOT_SERVICE_USER/venv/bin/activate
156
+ python3 -m pip install discordbotlinuxmonitor
157
+ deactivate
158
+ exit
159
+
160
+ # Create the Discord bot folder where you should put the json file
161
+ sudo mkdir $DISCORD_BOT_FOLDER
162
+
163
+ ### Create a systemd service file for the Discord bot ###
164
+ cat <<EOF > $DISCORD_BOT_SERVICE_FILE
165
+ [Unit]
166
+ Description=Discord Bot Linux Monitor Service
167
+ After=network.target
168
+
169
+ [Service]
170
+ ExecStart=/home/$DISCORD_BOT_SERVICE_USER/venv/bin/python3 -m discordbotlinuxmonitor --config_file ${DISCORD_BOT_FOLDER}config.json
171
+ WorkingDirectory=$DISCORD_BOT_FOLDER
172
+ User=$DISCORD_BOT_SERVICE_USER
173
+ Group=$DISCORD_BOT_SERVICE_GROUP
174
+ Restart=always
175
+ RestartSec=3
176
+
177
+ [Install]
178
+ WantedBy=multi-user.target
179
+ EOF
180
+
181
+ echo "Service file created at $DISCORD_BOT_SERVICE_FILE"
182
+
183
+ ### Reload systemd to recognize the new service ###
184
+ echo "Reloading systemd..."
185
+ sudo systemctl daemon-reload
186
+
187
+ ### Enable and start the service ###
188
+ echo "Enabling and starting the Discord bot service..."
189
+ sudo systemctl enable $DISCORD_BOT_SERVICE_NAME
190
+ ```
191
+ - Copy your config file into `$DISCORD_BOT_FOLDER/config.json`
192
+ - Launch the service: `sudo systemctl start $DISCORD_BOT_SERVICE_NAME`
193
+
194
+ ## License
195
+
196
+ This project is under MIT license. This means you can use it as you want (just don't delete the library header).
197
+
198
+ ## Contribute
199
+
200
+ If you want to add more examples or improve the library, just create a pull request with proper commit message and right wrapping.
@@ -0,0 +1,8 @@
1
+ discordbotlinuxmonitor/__init__.py,sha256=YwLKm4ALTaT7YNOTCO3X3m6yDOpybb1r5MvOF6Bt_Lg,74
2
+ discordbotlinuxmonitor/__main__.py,sha256=tOwcXUgZhtBDV9SzYButAsNdcux97vaKsoOXL9Xu13I,6138
3
+ discordbotlinuxmonitor/discordbotlinuxmonitor.py,sha256=YE6tydHspmMlEUeFj7T6HljxkgtR0R4i_ULwaBgGqpo,42432
4
+ DiscordBotLinuxMonitor-1.0.1.dist-info/LICENSE.md,sha256=nVjNnWWojuUQdnMdUf_YVeRMocBeapYhuLfIRQXXziI,1084
5
+ DiscordBotLinuxMonitor-1.0.1.dist-info/METADATA,sha256=l0IyJh0R6YKQnFA86Oek6rqjJzVPfCMD6IB1Uqut5wU,10942
6
+ DiscordBotLinuxMonitor-1.0.1.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
7
+ DiscordBotLinuxMonitor-1.0.1.dist-info/top_level.txt,sha256=xQ2GrL3jLRaO6NVSLrgtD3wGaqS7MeZzICTeqhyr5_w,23
8
+ DiscordBotLinuxMonitor-1.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (74.1.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ discordbotlinuxmonitor
@@ -0,0 +1 @@
1
+ from .discordbotlinuxmonitor import DiscordBotLinuxMonitor # type: ignore
@@ -0,0 +1,125 @@
1
+ from .discordbotlinuxmonitor import DiscordBotLinuxMonitor
2
+
3
+ import argparse
4
+ import sys
5
+ import logging
6
+
7
+ import discord
8
+
9
+ def main() -> None:
10
+ parser = argparse.ArgumentParser(description='System Management Tool controled from Discord')
11
+
12
+ # Define available arguments
13
+ parser.add_argument('--config_file', type=str, required=True, help='Path to the configuration file (must be a JSON file)')
14
+ parser.add_argument('--force_sync_on_startup', type=bool, required=True, help='Force discord command synchronization on startup (do it only the first time, because after, you will have a discord command to do it if really needed)')
15
+
16
+ parser.add_argument('--debug', action='store_true', help='Enable debug mode')
17
+ parser.add_argument('--nodebug', action='store_true', help='Disable all logs')
18
+
19
+ # Parse arguments
20
+ args = parser.parse_args()
21
+
22
+ # Enable or disable debug mode
23
+ if args.debug:
24
+ logging.getLogger().setLevel(logging.DEBUG)
25
+ elif args.nodebug:
26
+ logging.getLogger().setLevel(logging.CRITICAL)
27
+ else:
28
+ # Default to warning
29
+ logging.getLogger().setLevel(logging.WARNING)
30
+
31
+ # Ensure the config file is provided
32
+ if not args.config_file:
33
+ print("Error: --config_file is required")
34
+ parser.print_help()
35
+ sys.exit(1)
36
+ config_file: str = args.config_file
37
+
38
+ # Ensure the force_sync_on_startup is provided
39
+ if not args.force_sync_on_startup:
40
+ print("Error: --force_sync_on_startup is required")
41
+ parser.print_help()
42
+ sys.exit(1)
43
+ force_sync_on_startup: bool = args.force_sync_on_startup
44
+
45
+ # Prepare the Discord bot (will throw an exception if the configuration is invalid)
46
+ discord_bot_linux_monitor = DiscordBotLinuxMonitor(config_file=config_file, force_sync_on_startup=force_sync_on_startup)
47
+ discord_bot = discord_bot_linux_monitor.bot
48
+
49
+ #region BOT COMMANDS AND EVENTS REGIRSTRATION
50
+
51
+ @discord_bot.event
52
+ async def on_ready() -> None: # type: ignore
53
+ await discord_bot_linux_monitor.on_ready()
54
+
55
+ @discord_bot.tree.command(name="force_sync", description="[Private] 🔄 Force command synchronization 🔄")
56
+ async def force_sync(interaction: discord.Interaction) -> None: # type: ignore
57
+ await discord_bot_linux_monitor.force_sync(interaction)
58
+
59
+ @discord_bot.tree.command(name="usage", description="📊 View disk space, CPU, RAM, ... 📊")
60
+ async def usage(interaction: discord.Interaction) -> None: # type: ignore
61
+ await discord_bot_linux_monitor.usage(interaction)
62
+
63
+ @discord_bot.tree.command(name="os_infos", description="🖥️ View basic system information 🖥️")
64
+ async def os_infos(interaction: discord.Interaction) -> None: # type: ignore
65
+ await discord_bot_linux_monitor.os_infos(interaction)
66
+
67
+ @discord_bot.tree.command(name="users", description="[Private] 👥 View connected users 👥")
68
+ async def users(interaction: discord.Interaction) -> None: # type: ignore
69
+ await discord_bot_linux_monitor.users(interaction)
70
+
71
+ @discord_bot.tree.command(name="user_logins", description="[Private] 👥 View last user connections 👥")
72
+ async def user_logins(interaction: discord.Interaction) -> None: # type: ignore
73
+ await discord_bot_linux_monitor.user_logins(interaction)
74
+
75
+ @discord_bot.tree.command(name="ping", description="🌐 Ping websites 🌐")
76
+ async def ping(interaction: discord.Interaction) -> None: # type: ignore
77
+ await discord_bot_linux_monitor.ping(interaction)
78
+
79
+ @discord_bot.tree.command(name="certificates", description="🔒 Check SSL certificates 🔒")
80
+ async def certificates(interaction: discord.Interaction) -> None: # type: ignore
81
+ await discord_bot_linux_monitor.certificates(interaction)
82
+
83
+ @discord_bot.tree.command(name="reboot_server", description="[Private] 🔄 Restart the entire server 🔄")
84
+ async def reboot(interaction: discord.Interaction) -> None: # type: ignore
85
+ await discord_bot_linux_monitor.reboot(interaction)
86
+
87
+ @discord_bot.tree.command(name="services_status", description="🩺 Check services are running 🩺")
88
+ async def services_status(interaction: discord.Interaction) -> None: # type: ignore
89
+ await discord_bot_linux_monitor.services_status(interaction)
90
+
91
+ @discord_bot.tree.command(name="restart_all", description="🚀 Restart all services 🚀")
92
+ async def restart_all(interaction: discord.Interaction) -> None: # type: ignore
93
+ await discord_bot_linux_monitor.restart_all(interaction)
94
+
95
+ @discord_bot.tree.command(name="restart_service", description="🚀 Restart a service 🚀")
96
+ async def restart_service(interaction: discord.Interaction, service_name: str) -> None: # type: ignore
97
+ await discord_bot_linux_monitor.restart_service(interaction, service_name)
98
+
99
+ @discord_bot.tree.command(name="list_services", description="📋 List all available services 📋")
100
+ async def list_services(interaction: discord.Interaction) -> None: # type: ignore
101
+ await discord_bot_linux_monitor.list_services(interaction)
102
+
103
+ @discord_bot.tree.command(name="ports", description="🔒 Check ports 🔒")
104
+ async def ports(interaction: discord.Interaction) -> None: # type: ignore
105
+ await discord_bot_linux_monitor.ports(interaction)
106
+
107
+ @discord_bot.tree.command(name="list_processes", description="[Private] 📋 List active processes 📋")
108
+ async def list_processes(interaction: discord.Interaction) -> None: # type: ignore
109
+ await discord_bot_linux_monitor.list_processes(interaction)
110
+
111
+ @discord_bot.tree.command(name="kill_process", description="[Private] 🚫 Stop a process by PID 🚫")
112
+ async def kill_process(interaction: discord.Interaction, pid: int) -> None: # type: ignore
113
+ await discord_bot_linux_monitor.kill_process(interaction, pid)
114
+
115
+ #endregion
116
+
117
+ # Start the discord bot
118
+ logging.info(msg="Starting the Discord bot...")
119
+ try:
120
+ discord_bot.run(token=discord_bot_linux_monitor.server_token)
121
+ except Exception as e:
122
+ logging.exception(msg=f"Error while running the bot: {e}")
123
+
124
+ if __name__ == "__main__":
125
+ main()
@@ -0,0 +1,741 @@
1
+ """
2
+ Discord bot to:
3
+ - monitor a Linux server and inform about its status (in case of issues or periodical info) through Discord channels
4
+ - execute commands from Discord channels
5
+
6
+ Non exhaustive list of features (available by using it in shell or in python script):
7
+ - Do all checks bellow in a scheduled tasks and display the results only if there is an issue (only in console if using only the library)
8
+ - Do all checks bellow in a scheduled tasks and display the results every time (only in console if using only the library)
9
+
10
+ - Check CPU, RAM, SWAP, Temperature
11
+ - Check disk usage
12
+ - Check folder usage
13
+ - Check websites basic availability (ping)
14
+ - Check services status and restart them if needed
15
+ - Check certificates expiration and validity
16
+ - Check last user connections IPs
17
+ - Check uptime (to inform if the server has been rebooted)
18
+
19
+ - Get hostname, OS details, kernel version, server datetime, uptime
20
+ - Get connected users
21
+
22
+ - Get processes list (PID and name)
23
+ - Kill a process by PID
24
+
25
+ - Reboot server
26
+ """
27
+
28
+ __author__ = 'Quentin Comte-Gaz'
29
+ __email__ = "quentin@comte-gaz.com"
30
+ __license__ = "MIT License"
31
+ __copyright__ = "Copyright Quentin Comte-Gaz (2024)"
32
+ __python_version__ = "3.+"
33
+ __version__ = "1.0.1 (2024/09/14)"
34
+ __status__ = "Usable for any Linux project"
35
+
36
+ # pyright: reportMissingTypeStubs=false
37
+ from linuxmonitor import LinuxMonitor
38
+
39
+ import discord
40
+ from discord.app_commands.models import AppCommand
41
+ from discord.ext import commands
42
+ import json
43
+ from typing import List, Union, Awaitable, Callable
44
+
45
+ import asyncio
46
+
47
+ import logging
48
+
49
+ class DiscordBotLinuxMonitor:
50
+
51
+ #region Initialization
52
+
53
+ def __init__(self, config_file: str, force_sync_on_startup: bool) -> None:
54
+ logging.debug(msg=f"Loading configuration file {config_file}...")
55
+ with open(file=config_file, mode='r') as file:
56
+ self.config = json.load(file)
57
+
58
+ # Check if the configuration is correct
59
+ self._init_and_check_configuration()
60
+
61
+ # Initialize the LinuxMonitor
62
+ self.monitoring = LinuxMonitor(
63
+ config_file=config_file,
64
+ allow_scheduled_tasks_check_for_issues=(self.channel_name_for_public_error_tasks != "" or self.channel_name_for_private_error_tasks != ""), # type: ignore
65
+ allow_scheduled_task_show_info=(self.channel_name_for_public_infos_tasks != "" or self.channel_name_for_private_infos_tasks != "") # type: ignore
66
+ )
67
+
68
+ # Initialize the bot
69
+ self.force_sync_on_startup: bool = force_sync_on_startup
70
+ self.MAX_LENGTH_OF_DISCORD_MESSAGE = 2000 # Forced by Discord API
71
+ intents: discord.Intents = discord.Intents.default()
72
+ self.bot = commands.Bot(command_prefix=self.command_prefix, intents=intents)
73
+
74
+ def _init_and_check_configuration(self) -> None:
75
+ """
76
+ Check if the JSON configuration file is correct for discord usage.
77
+ """
78
+ # Check if the configuration is a dictionary
79
+ if not isinstance(self.config, dict):
80
+ raise ValueError("The configuration must be a dictionary")
81
+
82
+ # Check if the configuration contains the necessary keys
83
+ if 'discord_config' not in self.config: # type: ignore
84
+ raise ValueError("The configuration must contain the 'discord_config' key")
85
+
86
+ # Get the discord configuration (check if it is a dictionary)
87
+ discord_config: Dict[str, Any] = self.config.get('discord_config', {}) # type: ignore
88
+ if not isinstance(discord_config, dict):
89
+ raise ValueError("The discord configuration (discord_config) must be a dictionary")
90
+
91
+ if 'server_id' not in discord_config:
92
+ raise ValueError("The basic configuration must contain the 'server_id' key")
93
+ self.server_id: int = discord_config.get('server_id') # type: ignore
94
+
95
+ if 'server_token' not in discord_config:
96
+ raise ValueError("The basic configuration must contain the 'server_token' key")
97
+ self.server_token: str = discord_config.get('server_token') # type: ignore
98
+
99
+ if 'command_prefix' not in discord_config:
100
+ raise ValueError("The basic configuration must contain the 'command_prefix' key")
101
+ self.command_prefix: str = discord_config.get('command_prefix', "/") # type: ignore
102
+
103
+ if 'channel_name_for_private_commands' not in discord_config:
104
+ raise ValueError("The basic configuration must contain the 'channel_name_for_private_commands' key")
105
+ self.channel_name_for_private_commands: str = discord_config.get('channel_name_for_private_commands', "") # type: ignore
106
+
107
+ if 'channel_name_for_public_commands' not in discord_config:
108
+ raise ValueError("The basic configuration must contain the 'channel_name_for_public_commands' key")
109
+ self.channel_name_for_public_commands: str = discord_config.get('channel_name_for_public_commands', "") # type: ignore
110
+
111
+ if 'channel_name_for_private_error_tasks' not in discord_config:
112
+ raise ValueError("The basic configuration must contain the 'channel_name_for_private_error_tasks' key")
113
+ self.channel_name_for_private_error_tasks: str = discord_config.get('channel_name_for_private_error_tasks', "") # type: ignore
114
+
115
+ if 'channel_name_for_public_error_tasks' not in discord_config:
116
+ raise ValueError("The basic configuration must contain the 'channel_name_for_public_error_tasks' key")
117
+ self.channel_name_for_public_error_tasks: str = discord_config.get('channel_name_for_public_error_tasks', "") # type: ignore
118
+
119
+ if 'channel_name_for_private_infos_tasks' not in discord_config:
120
+ raise ValueError("The basic configuration must contain the 'channel_name_for_private_infos_tasks' key")
121
+ self.channel_name_for_private_infos_tasks: str = discord_config.get('channel_name_for_private_infos_tasks', "") # type: ignore
122
+
123
+ if 'channel_name_for_public_infos_tasks' not in discord_config:
124
+ raise ValueError("The basic configuration must contain the 'channel_name_for_public_infos_tasks' key")
125
+ self.channel_name_for_public_infos_tasks: str = discord_config.get('channel_name_for_public_infos_tasks', "") # type: ignore
126
+
127
+ if self.channel_name_for_private_commands != "": # type: ignore
128
+ if 'welcome_message_for_private_commands' not in discord_config:
129
+ raise ValueError("The basic configuration must contain the 'welcome_message_for_private_commands' key")
130
+ self.welcome_message_for_private_commands: str = discord_config.get('welcome_message_for_private_commands', "") # type: ignore
131
+
132
+ if self.channel_name_for_public_commands != "": # type: ignore
133
+ if 'welcome_message_for_public_commands' not in discord_config:
134
+ raise ValueError("The basic configuration must contain the 'welcome_message_for_public_commands' key")
135
+ self.welcome_message_for_public_commands: str = discord_config.get('welcome_message_for_public_commands', "") # type: ignore
136
+
137
+ if self.channel_name_for_private_error_tasks != "": # type: ignore
138
+ if 'welcome_message_for_private_error_tasks' not in discord_config:
139
+ raise ValueError("The basic configuration must contain the 'welcome_message_for_private_error_tasks' key")
140
+ self.welcome_message_for_private_error_tasks: str = discord_config.get('welcome_message_for_private_error_tasks', "") # type: ignore
141
+
142
+ if self.channel_name_for_public_error_tasks != "": # type: ignore
143
+ if 'welcome_message_for_public_error_tasks' not in discord_config:
144
+ raise ValueError("The basic configuration must contain the 'welcome_message_for_public_error_tasks' key")
145
+ self.welcome_message_for_public_error_tasks: str = discord_config.get('welcome_message_for_public_error_tasks', "") # type: ignore
146
+
147
+ if self.channel_name_for_private_infos_tasks != "": # type: ignore
148
+ if 'welcome_message_for_private_infos_tasks' not in discord_config:
149
+ raise ValueError("The basic configuration must contain the 'welcome_message_for_private_infos_tasks' key")
150
+ self.welcome_message_for_private_infos_tasks: str = discord_config.get('welcome_message_for_private_infos_tasks', "") # type: ignore
151
+
152
+ if self.channel_name_for_public_infos_tasks != "": # type: ignore
153
+ if 'welcome_message_for_public_infos_tasks' not in discord_config:
154
+ raise ValueError("The basic configuration must contain the 'welcome_message_for_public_infos_tasks' key")
155
+ self.welcome_message_for_public_infos_tasks: str = discord_config.get('welcome_message_for_public_infos_tasks', "") # type: ignore
156
+
157
+ # Check that at least one channel is defined
158
+ if self.channel_name_for_private_commands == "" and self.channel_name_for_public_commands == "" and self.channel_name_for_private_error_tasks == "" and self.channel_name_for_public_error_tasks == "" and self.channel_name_for_private_infos_tasks == "" and self.channel_name_for_public_infos_tasks == "": # type: ignore
159
+ raise ValueError("At least one channel must be defined (private or public) in the configuration file, else there is no point to use this lib (channel_name_for_private_commands, channel_name_for_public_commands, channel_name_for_private_error_tasks, channel_name_for_public_error_tasks, channel_name_for_private_infos_tasks, channel_name_for_public_infos_tasks)")
160
+
161
+ #endregion
162
+
163
+ #region Private methods
164
+
165
+ def _check_if_valid_guild(self, guild: Union[None,discord.Guild]) -> bool:
166
+ if guild is None:
167
+ return False
168
+
169
+ # Be sure it is from the correct server
170
+ if guild.id != self.server_id:
171
+ logging.warning(msg=f"Message received from invalid guild '{guild.name}' (id: '{guild.id}'), IGNORING THIS MESSAGE.")
172
+ return False
173
+ return True
174
+
175
+ async def _is_bot_channel_interaction(self, interaction: discord.Interaction, send_message_if_not_bot: bool) -> bool:
176
+ res: bool = self._is_bot_channel(channel=interaction.channel) # type: ignore
177
+ if not res and send_message_if_not_bot:
178
+ await interaction.response.send_message(content=f"❌ This is not the right channel to send commands to the bot. You need to communicate in the private channel '{self.channel_name_for_private_commands}' or the public channel '{self.channel_name_for_public_commands}'.", ephemeral=True)
179
+ return res
180
+
181
+ def _is_bot_channel(self, channel) -> bool: # type: ignore
182
+ return self._is_private_channel(channel=channel) or self._is_public_channel(channel=channel) # type: ignore
183
+
184
+ def _is_private_channel(self, channel) -> bool: # type: ignore
185
+ if channel is None:
186
+ return False
187
+
188
+ if self.channel_name_for_private_commands == "":
189
+ return False
190
+
191
+ res: bool = (self.channel_name_for_private_commands == channel.name) # type: ignore
192
+ return res # type: ignore
193
+
194
+ def _is_public_channel(self, channel) -> bool: # type: ignore
195
+ if channel is None:
196
+ return False
197
+
198
+ if self._is_private_channel(channel=channel): # type: ignore
199
+ return False
200
+
201
+ if self.channel_name_for_public_commands == "":
202
+ return False
203
+
204
+ res: bool = (self.channel_name_for_public_commands == channel.name) # type: ignore
205
+ return res # type: ignore
206
+
207
+ async def _channel_send_no_limit(self, channel: discord.TextChannel, msg: str) -> None:
208
+ logging.info(msg=f"Sending message to channel '{channel.name}':\n{msg}")
209
+
210
+ while len(msg) > self.MAX_LENGTH_OF_DISCORD_MESSAGE:
211
+ # Find the last newline within the limit
212
+ split_point = msg.rfind('\n', 0, self.MAX_LENGTH_OF_DISCORD_MESSAGE)
213
+ if split_point == -1: # No newline found, split at max_length
214
+ split_point = self.MAX_LENGTH_OF_DISCORD_MESSAGE
215
+
216
+ # Send the chunk and remove it from the message
217
+ await channel.send(content=msg[:split_point].rstrip())
218
+ msg = msg[split_point:].lstrip()
219
+
220
+ # Send the remaining message
221
+ if msg:
222
+ await channel.send(content=msg)
223
+
224
+ async def _interaction_followup_send_no_limit(self, interaction: discord.Interaction, msg: str) -> None:
225
+ logging.info(msg=f"Sending follow-up message:\n{msg}")
226
+
227
+ # Send the first chunk as a follow-up message
228
+ try:
229
+ if msg == "":
230
+ # Send generic interaction response if no message is returned
231
+ msg = "No answer."
232
+
233
+ if len(msg) > self.MAX_LENGTH_OF_DISCORD_MESSAGE:
234
+ # Find the last newline within the limit or just split at max_length
235
+ split_point = msg.rfind('\n', 0, self.MAX_LENGTH_OF_DISCORD_MESSAGE)
236
+ if split_point == -1: # No newline found, split at max_length
237
+ split_point = self.MAX_LENGTH_OF_DISCORD_MESSAGE
238
+
239
+ await interaction.followup.send(content=msg[:split_point].rstrip())
240
+ msg = msg[split_point:].lstrip()
241
+ else:
242
+ await interaction.followup.send(content=msg)
243
+ return # Exit if the message fits within the limit
244
+
245
+ except Exception as e:
246
+ logging.error(msg=f"Error while sending follow-up message: {e}")
247
+ return
248
+
249
+ # Handle additional messages that exceed the initial follow-up limit
250
+ while len(msg) > self.MAX_LENGTH_OF_DISCORD_MESSAGE:
251
+ split_point = msg.rfind('\n', 0, self.MAX_LENGTH_OF_DISCORD_MESSAGE)
252
+ if split_point == -1:
253
+ split_point = self.MAX_LENGTH_OF_DISCORD_MESSAGE
254
+
255
+ try:
256
+ await interaction.channel.send(content=msg[:split_point].rstrip()) # type: ignore
257
+ except Exception as e:
258
+ logging.error(msg=f"Error while sending message to channel: {e}")
259
+
260
+ msg = msg[split_point:].lstrip()
261
+
262
+ # Send any remaining message directly to the channel
263
+ if msg:
264
+ try:
265
+ await interaction.channel.send(content=msg) # type: ignore
266
+ except Exception as e:
267
+ logging.error(msg=f"Error while sending message to channel: {e}")
268
+
269
+ async def _force_sync(self) -> str:
270
+ try:
271
+ logging.info(msg="Forcing the sync of the bot's commands...")
272
+ synced: List[AppCommand] = await self.bot.tree.sync()
273
+
274
+ out_msg: str = f"🔄 **Commands synchronized successfully** 🔄"
275
+ for s in synced:
276
+ out_msg += f"\n- `{s.name}`: {s.description}"
277
+
278
+ logging.info(msg=out_msg)
279
+ return out_msg
280
+ except Exception as e:
281
+ out_msg = f"**Internal error during command synchronization**:\n```sh\n{e}\n```"
282
+ logging.exception(msg=out_msg)
283
+ return out_msg
284
+
285
+ #endregion
286
+
287
+ #region BOT COMMANDS AND EVENTS DEFINITIONS
288
+
289
+ async def on_ready(self) -> None:
290
+ logging.info(msg=f"Discord bot '{self.bot.user}' is ready.")
291
+ logging.info(msg=f"Connected to the following guilds (will check them): {[guild.name for guild in self.bot.guilds]}")
292
+ for guild in self.bot.guilds:
293
+ if guild.id == self.server_id:
294
+ # Found the correct guild
295
+ logging.info(msg=f"Bot '{self.bot.user}' is connected to the following wanted guild: '{guild.name}' (id: '{guild.id}')..")
296
+
297
+ public_channel_cmd_ready: bool = (self.channel_name_for_public_commands == "")
298
+ private_channel_cmd_ready: bool = (self.channel_name_for_private_commands == "")
299
+ public_channel_error_task_ready: bool = (self.channel_name_for_public_error_tasks == "")
300
+ private_channel_error_task_ready: bool = (self.channel_name_for_private_error_tasks == "")
301
+ public_channel_info_task_ready: bool = (self.channel_name_for_public_infos_tasks == "")
302
+ private_channel_info_task_ready: bool = (self.channel_name_for_private_infos_tasks == "")
303
+
304
+ for channel in guild.text_channels:
305
+ if self.channel_name_for_public_commands != "" and channel.name == self.channel_name_for_public_commands:
306
+ logging.info(msg="Found the public channel, it will be possible to do public commands.")
307
+ public_channel_cmd_ready = True
308
+ if self.welcome_message_for_public_commands != "":
309
+ await self._channel_send_no_limit(channel=channel, msg=self.welcome_message_for_public_commands)
310
+
311
+ if self.channel_name_for_private_commands != "" and channel.name == self.channel_name_for_private_commands:
312
+ logging.info(msg="Found the private channel, it will be possible to do private commands.")
313
+ private_channel_cmd_ready = True
314
+ if self.welcome_message_for_private_commands != "":
315
+ await self._channel_send_no_limit(channel=channel, msg=self.welcome_message_for_private_commands)
316
+
317
+ if self.channel_name_for_public_error_tasks != "" and channel.name == self.channel_name_for_public_error_tasks:
318
+ logging.info(msg="Found the public channel for error task, it will be possible to show public status issues if found periodically.")
319
+ public_channel_error_task_ready = True
320
+
321
+ if self.welcome_message_for_public_error_tasks != "":
322
+ await self._channel_send_no_limit(channel=channel, msg=self.welcome_message_for_public_error_tasks)
323
+
324
+ 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}').")
325
+ public_channel_for_error_task: discord.TextChannel = channel
326
+ send_message_public_error_task_func: Callable[[str], Awaitable[None]] = lambda msg: asyncio.create_task(self._channel_send_no_limit(channel=public_channel_for_error_task, msg=msg))
327
+
328
+ # Start the public schedule task
329
+ self.bot.loop.create_task(self.monitoring.schedule_task(handle_error_message=send_message_public_error_task_func, is_private=False))
330
+
331
+ if self.channel_name_for_public_infos_tasks != "" and channel.name == self.channel_name_for_public_infos_tasks:
332
+ logging.info(msg="Found the public channel for info task, it will be possible to show public info periodically.")
333
+ public_channel_info_task_ready = True
334
+
335
+ if self.welcome_message_for_public_infos_tasks != "":
336
+ await self._channel_send_no_limit(channel=channel, msg=self.welcome_message_for_public_infos_tasks)
337
+
338
+ 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}').")
339
+ public_channel_for_info_task: discord.TextChannel = channel
340
+ send_message_public_info_task_func: Callable[[str], Awaitable[None]] = lambda msg: asyncio.create_task(self._channel_send_no_limit(channel=public_channel_for_info_task, msg=msg))
341
+
342
+ # Start the public schedule task
343
+ self.bot.loop.create_task(self.monitoring.schedule_task_show_info(show_message=send_message_public_info_task_func, is_private=False))
344
+
345
+ if self.channel_name_for_private_error_tasks != "" and channel.name == self.channel_name_for_private_error_tasks:
346
+ logging.info(msg="Found the private channel for error task, it will be possible to show private and public status issues if found periodically.")
347
+ private_channel_error_task_ready = True
348
+
349
+ if self.welcome_message_for_private_error_tasks != "":
350
+ await self._channel_send_no_limit(channel=channel, msg=self.welcome_message_for_private_error_tasks)
351
+
352
+ 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}').")
353
+ private_channel_for_error_task: discord.TextChannel = channel
354
+ send_message_private_error_task_func: Callable[[str], Awaitable[None]] = lambda msg: asyncio.create_task(self._channel_send_no_limit(channel=private_channel_for_error_task, msg=msg))
355
+
356
+ # Start the private schedule task
357
+ self.bot.loop.create_task(self.monitoring.schedule_task(handle_error_message=send_message_private_error_task_func, is_private=True))
358
+
359
+ if self.channel_name_for_private_infos_tasks != "" and channel.name == self.channel_name_for_private_infos_tasks:
360
+ logging.info(msg="Found the private channel for info task, it will be possible to show private info periodically.")
361
+ private_channel_info_task_ready = True
362
+
363
+ if self.welcome_message_for_private_infos_tasks != "":
364
+ await self._channel_send_no_limit(channel=channel, msg=self.welcome_message_for_private_infos_tasks)
365
+
366
+ 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}').")
367
+ private_channel_for_info_task: discord.TextChannel = channel
368
+ send_message_private_info_task_func: Callable[[str], Awaitable[None]] = lambda msg: asyncio.create_task(self._channel_send_no_limit(channel=private_channel_for_info_task, msg=msg))
369
+
370
+ # Start the private schedule task
371
+ self.bot.loop.create_task(self.monitoring.schedule_task_show_info(show_message=send_message_private_info_task_func, is_private=True))
372
+
373
+ # Show a warning if a channel is not found and should be found
374
+ if not public_channel_cmd_ready:
375
+ logging.critical(msg=f"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
376
+ logging.critical(msg=f"Public channel '{self.channel_name_for_public_commands}' not found in server: '{guild.name}', public commands will not be available.")
377
+ logging.critical(msg=f"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
378
+ if not private_channel_cmd_ready:
379
+ logging.critical(msg=f"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
380
+ logging.critical(msg=f"Private channel '{self.channel_name_for_private_commands}' not found in server: '{guild.name}', private commands will not be available.")
381
+ logging.critical(msg=f"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
382
+ if not public_channel_error_task_ready:
383
+ logging.critical(msg=f"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
384
+ logging.critical(msg=f"Public channel '{self.channel_name_for_public_error_tasks}' not found in server: '{guild.name}', public status issues will not be shown periodically if found.")
385
+ logging.critical(msg=f"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
386
+ if not private_channel_error_task_ready:
387
+ logging.critical(msg=f"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
388
+ logging.critical(msg=f"Private channel '{self.channel_name_for_private_error_tasks}' not found in server: '{guild.name}', private and public status issues will not be shown periodically if found.")
389
+ logging.critical(msg=f"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
390
+ if not public_channel_info_task_ready:
391
+ logging.critical(msg=f"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
392
+ logging.critical(msg=f"Public channel '{self.channel_name_for_public_infos_tasks}' not found in server: '{guild.name}', public info will not be shown periodically.")
393
+ logging.critical(msg=f"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
394
+ if not private_channel_info_task_ready:
395
+ logging.critical(msg=f"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
396
+ logging.critical(msg=f"Private channel '{self.channel_name_for_private_infos_tasks}' not found in server: '{guild.name}', private info will not be shown periodically.")
397
+ logging.critical(msg=f"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
398
+ else:
399
+ # Found an undesired guild, disconnect
400
+ logging.info(msg=f"Bot '{self.bot.user}' is connected to the following UNDESIRED guild: '{guild.name}' (id: '{guild.id}'), IGNORING THIS GUILD.")
401
+
402
+ # Sync the bot's commands globally
403
+ if self.force_sync_on_startup:
404
+ await self._force_sync()
405
+
406
+ async def force_sync(self, interaction: discord.Interaction) -> None:
407
+ if not self._check_if_valid_guild(guild=interaction.guild):
408
+ return
409
+ if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
410
+ return
411
+
412
+ if not self._is_private_channel(channel=interaction.channel): # type: ignore
413
+ await interaction.response.send_message(content="❌ Public channels do not allow this command.", ephemeral=True)
414
+ return
415
+
416
+ # Say to the user that the command is being processed
417
+ await interaction.response.defer()
418
+
419
+ # Sync the bot's commands only on the 2 public and private channels
420
+ out_msg: str = await self._force_sync()
421
+
422
+ # Respond to the user
423
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
424
+
425
+ async def usage(self, interaction: discord.Interaction) -> None:
426
+ if not self._check_if_valid_guild(guild=interaction.guild):
427
+ return
428
+ if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
429
+ return
430
+
431
+ # Say to the user that the command is being processed
432
+ await interaction.response.defer()
433
+
434
+ try:
435
+ is_private: bool = self._is_private_channel(channel=interaction.channel) # type: ignore
436
+ out_msg: str = self.monitoring.check_all_disk_usage(is_private=is_private, display_only_if_critical=False)
437
+
438
+ msg: str = self.monitoring.check_all_folder_usage(is_private=is_private, display_only_if_critical=False)
439
+ if msg != "":
440
+ if out_msg != "":
441
+ out_msg += "\n"
442
+ out_msg += msg
443
+
444
+ if is_private:
445
+ out_msg += "\n"
446
+ out_msg += self.monitoring.check_cpu_usage(display_only_if_critical=False) + "\n"
447
+ out_msg += self.monitoring.check_ram_usage(display_only_if_critical=False) + "\n"
448
+ out_msg += self.monitoring.check_swap_usage(display_only_if_critical=False) + "\n"
449
+ out_msg += self.monitoring.check_cpu_temperature(display_only_if_critical=False) + "\n"
450
+ out_msg += self.monitoring.get_network_info()
451
+
452
+ # Respond to the user
453
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
454
+ except Exception as e:
455
+ out_msg = f"**Internal error retrieving usage info**:\n```sh\n{e}\n```"
456
+ logging.exception(msg=out_msg)
457
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
458
+
459
+ async def os_infos(self, interaction: discord.Interaction) -> None:
460
+ if not self._check_if_valid_guild(guild=interaction.guild):
461
+ return
462
+ if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
463
+ return
464
+
465
+ # Say to the user that the command is being processed
466
+ await interaction.response.defer()
467
+
468
+ try:
469
+ out_msg: str = self.monitoring.get_hostname() + "\n"
470
+ out_msg += self.monitoring.get_os_details() + "\n"
471
+ out_msg += self.monitoring.get_kernel_version() + "\n"
472
+ out_msg += self.monitoring.check_uptime(display_only_if_critical=False) + "\n"
473
+ out_msg += self.monitoring.get_server_datetime()
474
+
475
+ # Respond to the user
476
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
477
+ except Exception as e:
478
+ out_msg = f"**Internal error retrieving OS info**:\n```sh\n{e}\n```"
479
+ logging.exception(msg=out_msg)
480
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
481
+
482
+ async def users(self, interaction: discord.Interaction) -> None:
483
+ if not self._check_if_valid_guild(guild=interaction.guild):
484
+ return
485
+ if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
486
+ return
487
+ if not self._is_private_channel(channel=interaction.channel): # type: ignore
488
+ await interaction.response.send_message(content="❌ Public channels do not allow this command.", ephemeral=True)
489
+ return
490
+
491
+ # Say to the user that the command is being processed
492
+ await interaction.response.defer()
493
+
494
+ try:
495
+ out_msg: str = self.monitoring.get_connected_users()
496
+
497
+ # Respond to the user
498
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
499
+ except Exception as e:
500
+ out_msg = f"**Internal error retrieving connected users**:\n```sh\n{e}\n```"
501
+ logging.exception(msg=out_msg)
502
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
503
+
504
+ async def user_logins(self, interaction: discord.Interaction) -> None:
505
+ if not self._check_if_valid_guild(guild=interaction.guild):
506
+ return
507
+ if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
508
+ return
509
+
510
+ if not self._is_private_channel(channel=interaction.channel): # type: ignore
511
+ await interaction.response.send_message(content="❌ Public channels do not allow this command.", ephemeral=True)
512
+ return
513
+
514
+ # Indiquer que la commande est en cours de traitement
515
+ await interaction.response.defer()
516
+
517
+ try:
518
+ # Récupérer les dernières connexions des utilisateurs
519
+ out_msg: str = self.monitoring.check_all_recent_user_logins(display_only_if_critical=False)
520
+
521
+ # Répondre à l'utilisateur
522
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
523
+
524
+ except Exception as e:
525
+ out_msg = f"**Internal error retrieving last user connections**:\n```sh\n{e}\n```"
526
+ logging.exception(msg=out_msg)
527
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
528
+
529
+ async def ping(self, interaction: discord.Interaction) -> None:
530
+ if not self._check_if_valid_guild(guild=interaction.guild):
531
+ return
532
+ if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
533
+ return
534
+
535
+ # Say to the user that the command is being processed
536
+ await interaction.response.defer()
537
+
538
+ try:
539
+ is_private: bool = self._is_private_channel(channel=interaction.channel) # type: ignore
540
+ out_msg: str = self.monitoring.ping_all_websites(is_private=is_private, display_only_if_critical=False)
541
+
542
+ # Respond to the user
543
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
544
+ except Exception as e:
545
+ out_msg = f"**Internal error during websites ping **:\n```sh\n{e}\n```"
546
+ logging.exception(msg=out_msg)
547
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
548
+
549
+ async def certificates(self, interaction: discord.Interaction) -> None:
550
+ if not self._check_if_valid_guild(guild=interaction.guild):
551
+ return
552
+ if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
553
+ return
554
+
555
+ # Say to the user that the command is being processed
556
+ await interaction.response.defer()
557
+
558
+ try:
559
+ is_private: bool = self._is_private_channel(channel=interaction.channel) # type: ignore
560
+ out_msg: str = self.monitoring.check_all_certificates(is_private=is_private, display_only_if_critical=False)
561
+
562
+ # Respond to the user
563
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
564
+ except Exception as e:
565
+ out_msg = f"**Internal error during SSL certificate checks **:\n```sh\n{e}\n```"
566
+ logging.exception(msg=out_msg)
567
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
568
+
569
+ async def reboot(self, interaction: discord.Interaction) -> None:
570
+ if not self._check_if_valid_guild(guild=interaction.guild):
571
+ return
572
+ if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
573
+ return
574
+ if not self._is_private_channel(channel=interaction.channel): # type: ignore
575
+ await interaction.response.send_message(content="❌ Public channels do not allow this command.", ephemeral=True)
576
+ return
577
+
578
+ # Say to the user that the command is being processed
579
+ await interaction.response.defer()
580
+
581
+ try:
582
+ out_msg: str = self.monitoring.reboot_server()
583
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
584
+
585
+ except Exception as e:
586
+ out_msg = f"**Internal error during server reboot**:\n```sh\n{e}\n```"
587
+ logging.exception(msg=out_msg)
588
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
589
+
590
+ async def services_status(self, interaction: discord.Interaction) -> None:
591
+ if not self._check_if_valid_guild(guild=interaction.guild):
592
+ return
593
+ if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
594
+ return
595
+
596
+ # Say to the user that the command is being processed
597
+ await interaction.response.defer()
598
+
599
+ try:
600
+ is_private: bool = self._is_private_channel(channel=interaction.channel) # type: ignore
601
+ out_msg: str = self.monitoring.check_all_services_status(is_private=is_private)
602
+
603
+ # Respond to the user
604
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
605
+ except Exception as e:
606
+ out_msg = f"**Internal error checking services are running**:\n```sh\n{e}\n```"
607
+ logging.exception(msg=out_msg)
608
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
609
+
610
+ async def restart_all(self, interaction: discord.Interaction) -> None:
611
+ if not self._check_if_valid_guild(guild=interaction.guild):
612
+ return
613
+ if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
614
+ return
615
+
616
+ # Say to the user that the command is being processed
617
+ await interaction.response.defer()
618
+
619
+ try:
620
+ # Restart all services and get the results
621
+ is_private: bool = self._is_private_channel(channel=interaction.channel) # type: ignore
622
+ out_msg: str = self.monitoring.restart_all_services(is_private=is_private)
623
+
624
+ # Respond to the user
625
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
626
+ except Exception as e:
627
+ out_msg = f"**Internal error restarting all services**:\n```sh\n{e}\n```"
628
+ logging.exception(msg=out_msg)
629
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
630
+
631
+ async def restart_service(self, interaction: discord.Interaction, service_name: str) -> None:
632
+ if not self._check_if_valid_guild(guild=interaction.guild):
633
+ return
634
+ if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
635
+ return
636
+
637
+ # Indiquer que la commande est en cours de traitement
638
+ await interaction.response.defer()
639
+
640
+ try:
641
+ # Redémarrer le service et récupérer le message de sortie
642
+ is_private: bool = self._is_private_channel(channel=interaction.channel) # type: ignore
643
+ out_msg: str = self.monitoring.restart_service(is_private=is_private, service_name=service_name)
644
+
645
+ # Répondre à l'utilisateur
646
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
647
+
648
+ except Exception as e:
649
+ out_msg = f"**Internal error restarting service {service_name}**:\n```sh\n{e}\n```"
650
+ logging.exception(msg=out_msg)
651
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
652
+
653
+ async def list_services(self, interaction: discord.Interaction) -> None:
654
+ if not self._check_if_valid_guild(guild=interaction.guild):
655
+ return
656
+ if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
657
+ return
658
+
659
+ # Indiquer que la commande est en cours de traitement
660
+ await interaction.response.defer()
661
+
662
+ try:
663
+ # Récupérer la liste des services
664
+ is_private: bool = self._is_private_channel(channel=interaction.channel) # type: ignore
665
+ out_msg: str = self.monitoring.get_all_services_allowed_to_restart(is_private=is_private)
666
+
667
+ # Répondre à l'utilisateur
668
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
669
+ except Exception as e:
670
+ out_msg = f"**Internal error retrieving available services**:\n```sh\n{e}\n```"
671
+ logging.exception(msg=out_msg)
672
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
673
+
674
+ async def ports(self, interaction: discord.Interaction) -> None:
675
+ if not self._check_if_valid_guild(guild=interaction.guild):
676
+ return
677
+ if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
678
+ return
679
+
680
+ # Indiquer que la commande est en cours de traitement
681
+ await interaction.response.defer()
682
+
683
+ try:
684
+ # Récupérer le statut des ports
685
+ is_private: bool = self._is_private_channel(channel=interaction.channel) # type: ignore
686
+ out_msg: str = self.monitoring.check_all_ports(restart_if_down=True, is_private=is_private, display_only_if_critical=False)
687
+
688
+ # Répondre à l'utilisateur
689
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
690
+ except Exception as e:
691
+ out_msg = f"**Internal error checking ports**:\n```sh\n{e}\n```"
692
+ logging.exception(msg=out_msg)
693
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
694
+
695
+ async def list_processes(self, interaction: discord.Interaction) -> None:
696
+ if not self._check_if_valid_guild(guild=interaction.guild):
697
+ return
698
+ if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
699
+ return
700
+ if not self._is_private_channel(channel=interaction.channel): # type: ignore
701
+ await interaction.response.send_message(content="❌ Public channels do not allow this command.", ephemeral=True)
702
+ return
703
+
704
+ # Indiquer que la commande est en cours de traitement
705
+ await interaction.response.defer()
706
+
707
+ try:
708
+ # Récupérer la liste des processus actifs
709
+ out_msg: str = self.monitoring.get_ordered_processes()
710
+
711
+ # Répondre à l'utilisateur
712
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
713
+ except Exception as e:
714
+ out_msg = f"**Internal error retrieving active processes**:\n```sh\n{e}\n```"
715
+ logging.exception(msg=out_msg)
716
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
717
+
718
+ async def kill_process(self, interaction: discord.Interaction, pid: int) -> None:
719
+ if not self._check_if_valid_guild(guild=interaction.guild):
720
+ return
721
+ if not (await self._is_bot_channel_interaction(interaction=interaction, send_message_if_not_bot=True)):
722
+ return
723
+ if not self._is_private_channel(channel=interaction.channel): # type: ignore
724
+ await interaction.response.send_message(content="❌ Public channels do not allow this command.", ephemeral=True)
725
+ return
726
+
727
+ # Indiquer que la commande est en cours de traitement
728
+ await interaction.response.defer()
729
+
730
+ try:
731
+ # Arrêter le processus et récupérer le message de sortie
732
+ out_msg: str = self.monitoring.kill_process(pid=pid)
733
+
734
+ # Répondre à l'utilisateur
735
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
736
+ except Exception as e:
737
+ out_msg = f"**Internal error stopping process of PID {pid}**:\n```sh\n{e}\n```"
738
+ logging.exception(msg=out_msg)
739
+ await self._interaction_followup_send_no_limit(interaction=interaction, msg=out_msg)
740
+
741
+ #endregion