diff --git a/core/base.py b/core/base.py index e795b1c..b13d361 100644 --- a/core/base.py +++ b/core/base.py @@ -452,6 +452,39 @@ class Base: except AssertionError as ae: self.logs.error(f'{ae}') + def is_thread_alive(self, thread_name: str) -> bool: + """Check if the thread is still running! using the is_alive method of Threads. + + Args: + thread_name (str): The thread name + + Returns: + bool: True if is alive + """ + for thread in self.running_threads: + if thread.name.lower() == thread_name.lower(): + if thread.is_alive(): + return True + else: + return False + + return False + + def is_thread_exist(self, thread_name: str) -> bool: + """Check if the thread exist in the local var (running_threads) + + Args: + thread_name (str): The thread name + + Returns: + bool: True if the thread exist + """ + for thread in self.running_threads: + if thread.name.lower() == thread_name.lower(): + return True + + return False + def thread_count(self, thread_name: str) -> int: """This method return the number of existing threads currently running or not running diff --git a/core/utils.py b/core/utils.py index b832335..b89551d 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,6 +1,7 @@ ''' Main utils library. ''' +import gc from pathlib import Path from re import sub from typing import Literal, Optional, Any @@ -50,6 +51,25 @@ def get_datetime() -> datetime: """ return datetime.now() +def run_python_garbage_collector() -> int: + """Run Python garbage collector + + Returns: + int: The number of unreachable objects is returned. + """ + return gc.collect() + +def get_number_gc_objects(your_object_to_count: Optional[Any] = None) -> int: + """Get The number of objects tracked by the collector (excluding the list returned). + + Returns: + int: Number of tracked objects by the collector + """ + if your_object_to_count is None: + return len(gc.get_objects()) + + return sum(1 for obj in gc.get_objects() if isinstance(obj, your_object_to_count)) + def generate_random_string(lenght: int) -> str: """Retourn une chaîne aléatoire en fonction de la longueur spécifiée. diff --git a/defender.py b/defender.py index 9e5e200..2248040 100644 --- a/defender.py +++ b/defender.py @@ -5,6 +5,7 @@ from core import installation # Requierements : # # Python3.10 or higher # # SQLAlchemy, requests, psutil # +# unrealircd-rpc-py # # UnrealIRCD 6.2.2 or higher # ############################################# diff --git a/mods/clone/mod_clone.py b/mods/clone/mod_clone.py index 78a4591..43bde4a 100644 --- a/mods/clone/mod_clone.py +++ b/mods/clone/mod_clone.py @@ -175,9 +175,9 @@ class Clone: if len(cmd) == 1: self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} clone connect NUMBER GROUP_NAME INTERVAL") - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} clone kill [all | nickname]") - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} clone join [all | nickname] #channel") - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} clone part [all | nickname] #channel") + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} clone kill [all | group_name | nickname]") + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} clone join [all | group_name | nickname] #channel") + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} clone part [all | group_name | nickname] #channel") self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} clone list") return None @@ -296,11 +296,31 @@ class Clone: case 'list': try: - clone_count = len(self.Clone.UID_CLONE_DB) - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f">> Number of connected clones: {clone_count}") - for clone_name in self.Clone.UID_CLONE_DB: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, - msg=f">> Nickname: {clone_name.nickname} | Username: {clone_name.username} | Realname: {clone_name.realname} | Vhost: {clone_name.vhost} | UID: {clone_name.uid} | Group: {clone_name.group} | Connected: {clone_name.connected}") + # Syntax. /msg defender clone list + header = f" {'Nickname':<12}| {'Real name':<25}| {'Group name':<15}| {'Connected':<35}" + line = "-"*67 + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=header) + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" {line}") + group_name = cmd[2] if len(cmd) > 2 else None + + if group_name is None: + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Number of connected clones: {len(self.Clone.UID_CLONE_DB)}") + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" {line}") + for clone_name in self.Clone.UID_CLONE_DB: + self.Protocol.send_notice( + nick_from=dnickname, + nick_to=fromuser, + msg=f" {clone_name.nickname:<12}| {clone_name.realname:<25}| {clone_name.group:<15}| {clone_name.connected:<35}") + else: + if not self.Clone.group_exists(group_name): + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg="This Group name doesn't exist!") + return None + clones = self.Clone.get_clones_from_groupname(group_name) + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Number of connected clones: {len(clones)}") + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" {line}") + for clone in clones: + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, + msg=f" {clone.nickname:<12}| {clone.realname:<25}| {clone.group:<15}| {clone.connected:<35}") except Exception as err: self.Logs.error(f'{err}') diff --git a/mods/jsonrpc/mod_jsonrpc.py b/mods/jsonrpc/mod_jsonrpc.py index 0504856..70cb37e 100644 --- a/mods/jsonrpc/mod_jsonrpc.py +++ b/mods/jsonrpc/mod_jsonrpc.py @@ -1,6 +1,7 @@ -import gc import logging import asyncio +import mods.jsonrpc.utils as utils +import mods.jsonrpc.threads as thds from time import sleep from types import SimpleNamespace from typing import TYPE_CHECKING @@ -36,6 +37,9 @@ class Jsonrpc(): # Add Base object to the module (Mandatory) self.Base = ircInstance.Base + # Add Main Utils (Mandatory) + self.MainUtils = ircInstance.Utils + # Add logs object to the module (Mandatory) self.Logs = ircInstance.Loader.Logs @@ -47,9 +51,15 @@ class Jsonrpc(): # Is RPC Active? self.is_streaming = False + + # Module Utils + self.Utils = utils + + # Module threads + self.Threads = thds # Run Garbage collector. - self.Base.create_timer(10, gc.collect) + self.Base.create_timer(10, self.MainUtils.run_python_garbage_collector) # Create module commands (Mandatory) self.Irc.build_command(1, self.module_name, 'jsonrpc', 'Activate the JSON RPC Live connection [ON|OFF]') @@ -62,9 +72,6 @@ class Jsonrpc(): # Log the module self.Logs.debug(f'Module {self.module_name} loaded ...') - def compter_instances(self, cls) -> int: - return sum(1 for obj in gc.get_objects() if isinstance(obj, cls)) - def __init_module(self) -> None: logging.getLogger('websockets').setLevel(logging.WARNING) @@ -83,8 +90,7 @@ class Jsonrpc(): username=self.Config.JSONRPC_USER, password=self.Config.JSONRPC_PASSWORD, callback_object_instance=self, - callback_method_or_function_name='callback_sent_to_irc', - debug_level=10 + callback_method_or_function_name='callback_sent_to_irc' ) if self.UnrealIrcdRpcLive.get_error.code != 0: @@ -113,7 +119,7 @@ class Jsonrpc(): raise Exception(f"[JSONRPC ERROR] {self.Rpc.get_error.message}") if self.ModConfig.jsonrpc == 1: - self.Base.create_thread(self.thread_start_jsonrpc, run_once=True) + self.Base.create_thread(func=self.Threads.thread_subscribe, func_args=(self, ), run_once=True) return None @@ -181,59 +187,6 @@ class Jsonrpc(): return None - def thread_start_jsonrpc(self): - response: dict[str, dict] = {} - - if self.UnrealIrcdRpcLive.get_error.code == 0: - self.is_streaming = True - response = asyncio.run(self.UnrealIrcdRpcLive.subscribe(["all"])) - else: - self.Protocol.send_priv_msg( - nick_from=self.Config.SERVICE_NICKNAME, - msg=f"[{self.Config.COLORS.red}ERROR{self.Config.COLORS.nogc}] {self.UnrealIrcdRpcLive.get_error.message}", - channel=self.Config.SERVICE_CHANLOG - ) - - if response is None: - return - - code = response.get('error', {}).get('code', 0) - message = response.get('error', {}).get('message', None) - - if code == 0: - self.Protocol.send_priv_msg( - nick_from=self.Config.SERVICE_NICKNAME, - msg=f"[{self.Config.COLORS.green}JSONRPC{self.Config.COLORS.nogc}] Stream is OFF", - channel=self.Config.SERVICE_CHANLOG - ) - else: - self.Protocol.send_priv_msg( - nick_from=self.Config.SERVICE_NICKNAME, - msg=f"[{self.Config.COLORS.red}JSONRPC{self.Config.COLORS.nogc}] Stream has crashed! {code} - {message}", - channel=self.Config.SERVICE_CHANLOG - ) - - def thread_stop_jsonrpc(self) -> None: - - response: dict[str, dict] = asyncio.run(self.UnrealIrcdRpcLive.unsubscribe()) - self.Logs.debug("[JSONRPC UNLOAD] Unsubscribe from the stream!") - self.is_streaming = False - self.__update_configuration('jsonrpc', 0) - - if response is None: - print(f"... Response is None ?! {response}") - return None - - code = response.get('error', {}).get('code', 0) - message = response.get('error', {}).get('message', None) - - if code != 0: - self.Protocol.send_priv_msg( - nick_from=self.Config.SERVICE_NICKNAME, - msg=f"[{self.Config.COLORS.red}JSONRPC ERROR{self.Config.COLORS.nogc}] {message} ({code})", - channel=self.Config.SERVICE_CHANLOG - ) - def __load_module_configuration(self) -> None: """### Load Module Configuration """ @@ -249,7 +202,7 @@ class Jsonrpc(): except TypeError as te: self.Logs.critical(te) - def __update_configuration(self, param_key: str, param_value: str): + def update_configuration(self, param_key: str, param_value: str) -> None: """Update the local and core configuration Args: @@ -265,8 +218,8 @@ class Jsonrpc(): msg=f"[{self.Config.COLORS.green}JSONRPC INFO{self.Config.COLORS.nogc}] Shutting down RPC system!", channel=self.Config.SERVICE_CHANLOG ) - self.Base.create_thread(func=self.thread_stop_jsonrpc, run_once=True) - self.__update_configuration('jsonrpc', 0) + self.Base.create_thread(func=self.Threads.thread_unsubscribe, func_args=(self, ), run_once=True) + self.update_configuration('jsonrpc', 0) self.Logs.debug(f"Unloading {self.module_name}") return None @@ -286,54 +239,42 @@ class Jsonrpc(): case 'jsonrpc': try: - option = str(cmd[1]).lower() - - if len(command) == 1: + if len(cmd) < 2: self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'/msg {dnickname} jsonrpc on') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'/msg {dnickname} jsonrpc off') + return None + option = str(cmd[1]).lower() match option: case 'on': + thread_name = 'thread_subscribe' + if self.Base.is_thread_alive(thread_name): + self.Protocol.send_priv_msg(nick_from=dnickname, channel=dchannel, msg=f"The Subscription is running") + return None + elif self.Base.is_thread_exist(thread_name): + self.Protocol.send_priv_msg( + nick_from=dnickname, channel=dchannel, + msg=f"The subscription is not running, wait untill the process will be cleaned up" + ) + return None - # for logger_name, logger in logging.root.manager.loggerDict.items(): - # if isinstance(logger, logging.Logger): - # self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"{logger_name} - {logger.level}") - - for thread in self.Base.running_threads: - if thread.name == 'thread_start_jsonrpc': - if thread.is_alive(): - self.Protocol.send_priv_msg( - nick_from=self.Config.SERVICE_NICKNAME, - msg=f"Thread {thread.name} is running", - channel=dchannel - ) - else: - self.Protocol.send_priv_msg( - nick_from=self.Config.SERVICE_NICKNAME, - msg=f"Thread {thread.name} is not running, wait untill the process will be cleaned up", - channel=dchannel - ) - - self.Base.create_thread(self.thread_start_jsonrpc, run_once=True) - self.__update_configuration('jsonrpc', 1) + self.Base.create_thread(func=self.Threads.thread_subscribe, func_args=(self, ), run_once=True) + self.update_configuration('jsonrpc', 1) case 'off': - self.Base.create_thread(func=self.thread_stop_jsonrpc, run_once=True) - self.__update_configuration('jsonrpc', 0) + self.Base.create_thread(func=self.Threads.thread_unsubscribe, func_args=(self, ), run_once=True) + self.update_configuration('jsonrpc', 0) except IndexError as ie: self.Logs.error(ie) case 'jruser': try: - option = str(cmd[1]).lower() - - if len(command) == 1: + if len(cmd) < 2: self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'/msg {dnickname} jruser get nickname') - + option = str(cmd[1]).lower() match option: - case 'get': nickname = str(cmd[2]) uid_to_get = self.User.get_uid(nickname) @@ -378,10 +319,10 @@ class Jsonrpc(): case 'jrinstances': try: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"GC Collect: {gc.collect()}") - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Nombre d'instance LiveWebsock: {self.compter_instances(LiveWebsocket)}") - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Nombre d'instance LiveUnixSocket: {self.compter_instances(LiveUnixSocket)}") - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Nombre d'instance Loader: {self.compter_instances(Loader)}") - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Nombre de toute les instances: {len(gc.get_objects())}") + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"GC Collect: {self.MainUtils.run_python_garbage_collector()}") + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Nombre d'instance LiveWebsock: {self.MainUtils.get_number_gc_objects(LiveWebsocket)}") + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Nombre d'instance LiveUnixSocket: {self.MainUtils.get_number_gc_objects(LiveUnixSocket)}") + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Nombre d'instance Loader: {self.MainUtils.get_number_gc_objects(Loader)}") + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Nombre de toute les instances: {self.MainUtils.get_number_gc_objects()}") except Exception as err: self.Logs.error(f"Unknown Error: {err}") \ No newline at end of file diff --git a/mods/jsonrpc/threads.py b/mods/jsonrpc/threads.py new file mode 100644 index 0000000..f35b902 --- /dev/null +++ b/mods/jsonrpc/threads.py @@ -0,0 +1,60 @@ +import asyncio +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from mods.jsonrpc.mod_jsonrpc import Jsonrpc + +def thread_subscribe(uplink: 'Jsonrpc') -> None: + response: dict[str, dict] = {} + snickname = uplink.Config.SERVICE_NICKNAME + schannel = uplink.Config.SERVICE_CHANLOG + + if uplink.UnrealIrcdRpcLive.get_error.code == 0: + uplink.is_streaming = True + response = asyncio.run(uplink.UnrealIrcdRpcLive.subscribe(["all"])) + else: + uplink.Protocol.send_priv_msg(nick_from=snickname, + msg=f"[{uplink.Config.COLORS.red}JSONRPC ERROR{uplink.Config.COLORS.nogc}] {uplink.UnrealIrcdRpcLive.get_error.message}", + channel=schannel + ) + + if response is None: + return + + code = response.get('error', {}).get('code', 0) + message = response.get('error', {}).get('message', None) + + if code == 0: + uplink.Protocol.send_priv_msg( + nick_from=snickname, + msg=f"[{uplink.Config.COLORS.green}JSONRPC{uplink.Config.COLORS.nogc}] Stream is OFF", + channel=schannel + ) + else: + uplink.Protocol.send_priv_msg( + nick_from=snickname, + msg=f"[{uplink.Config.COLORS.red}JSONRPC{uplink.Config.COLORS.nogc}] Stream has crashed! {code} - {message}", + channel=schannel + ) + +def thread_unsubscribe(uplink: 'Jsonrpc') -> None: + + response: dict[str, dict] = asyncio.run(uplink.UnrealIrcdRpcLive.unsubscribe()) + uplink.Logs.debug("[JSONRPC UNLOAD] Unsubscribe from the stream!") + uplink.is_streaming = False + uplink.update_configuration('jsonrpc', 0) + snickname = uplink.Config.SERVICE_NICKNAME + schannel = uplink.Config.SERVICE_CHANLOG + + if response is None: + return None + + code = response.get('error', {}).get('code', 0) + message = response.get('error', {}).get('message', None) + + if code != 0: + uplink.Protocol.send_priv_msg( + nick_from=snickname, + msg=f"[{uplink.Config.COLORS.red}JSONRPC ERROR{uplink.Config.COLORS.nogc}] {message} ({code})", + channel=schannel + ) diff --git a/mods/jsonrpc/utils.py b/mods/jsonrpc/utils.py new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7c5a081 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +certifi==2024.12.14 +charset-normalizer==3.4.1 +Faker==33.1.2 +greenlet==3.1.1 +idna==3.10 +psutil==6.1.1 +python-dateutil==2.9.0.post0 +requests==2.32.3 +six==1.17.0 +SQLAlchemy==2.0.36 +typing_extensions==4.12.2 +unrealircd-rpc-py==2.0.4 +urllib3==2.3.0 +websockets==14.1