diff --git a/.gitignore b/.gitignore index 9867c4e..2428eb1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .pyenv/ +.venv/ +.idea/ db/ logs/ __pycache__/ diff --git a/core/base.py b/core/base.py index 66b4809..b13d361 100644 --- a/core/base.py +++ b/core/base.py @@ -1,6 +1,8 @@ +import importlib import os import re import json +import sys import time import random import socket @@ -8,41 +10,44 @@ import hashlib import logging import threading import ipaddress - import ast import requests - +from pathlib import Path +from types import ModuleType from dataclasses import fields -from typing import Union, Literal, TYPE_CHECKING +from typing import Any, Optional, TYPE_CHECKING from base64 import b64decode, b64encode from datetime import datetime, timedelta, timezone from sqlalchemy import create_engine, Engine, Connection, CursorResult from sqlalchemy.sql import text -from core.definition import MConfig if TYPE_CHECKING: - from core.classes.settings import Settings + from core.loader import Loader class Base: - def __init__(self, Config: MConfig, settings: 'Settings') -> None: + def __init__(self, loader: 'Loader') -> None: - self.Config = Config # Assigner l'objet de configuration - self.Settings: Settings = settings - self.init_log_system() # Demarrer le systeme de log + self.Loader = loader + self.Config = loader.Config + self.Settings = loader.Settings + self.Utils = loader.Utils + self.logs = loader.Logs + + # self.init_log_system() # Demarrer le systeme de log self.check_for_new_version(True) # Verifier si une nouvelle version est disponible # Liste des timers en cours - self.running_timers:list[threading.Timer] = self.Settings.RUNNING_TIMERS + self.running_timers: list[threading.Timer] = self.Settings.RUNNING_TIMERS # Liste des threads en cours - self.running_threads:list[threading.Thread] = self.Settings.RUNNING_THREADS + self.running_threads: list[threading.Thread] = self.Settings.RUNNING_THREADS # Les sockets ouvert self.running_sockets: list[socket.socket] = self.Settings.RUNNING_SOCKETS # Liste des fonctions en attentes - self.periodic_func:dict[object] = self.Settings.PERIODIC_FUNC + self.periodic_func: dict[object] = self.Settings.PERIODIC_FUNC # Création du lock self.lock = self.Settings.LOCK @@ -136,120 +141,62 @@ class Base: except Exception as err: self.logs.error(f'General Error: {err}') - def get_unixtime(self) -> int: + def get_all_modules(self) -> list[str]: + """Get list of all main modules + using this pattern mod_*.py + + Returns: + list[str]: List of module names. """ - Cette fonction retourne un UNIXTIME de type 12365456 - Return: Current time in seconds since the Epoch (int) + base_path = Path('mods') + return [file.name.replace('.py', '') for file in base_path.rglob('mod_*.py')] + + def reload_modules_with_dependencies(self, prefix: str = 'mods'): """ - cet_offset = timezone(timedelta(hours=2)) - now_cet = datetime.now(cet_offset) - unixtime_cet = int(now_cet.timestamp()) - unixtime = int( time.time() ) - - return unixtime - - def get_datetime(self) -> str: + Reload all modules in sys.modules that start with the given prefix. + Useful for reloading a full package during development. """ - Retourne une date au format string (24-12-2023 20:50:59) - """ - currentdate = datetime.now().strftime('%d-%m-%Y %H:%M:%S') - return currentdate + modules_to_reload = [] - def get_all_modules(self) -> list: + # Collect target modules + for name, module in sys.modules.items(): + if ( + isinstance(module, ModuleType) + and module is not None + and name.startswith(prefix) + ): + modules_to_reload.append((name, module)) - all_files = os.listdir('mods/') - all_modules: list = [] - for module in all_files: - if module.endswith('.py') and not module == '__init__.py': - all_modules.append(module.replace('.py', '').lower()) + # Sort to reload submodules before parent modules + for name, module in sorted(modules_to_reload, key=lambda x: x[0], reverse=True): + try: + if 'mod_' not in name and 'schemas' not in name: + importlib.reload(module) + self.logs.debug(f'[LOAD_MODULE] Module {module} success') - return all_modules + except Exception as err: + self.logs.error(f'[LOAD_MODULE] Module {module} failed [!] - {err}') def create_log(self, log_message: str) -> None: """Enregiste les logs Args: - string (str): Le message a enregistrer + log_message (str): Le message a enregistrer Returns: None: Aucun retour """ sql_insert = f"INSERT INTO {self.Config.TABLE_LOG} (datetime, server_msg) VALUES (:datetime, :server_msg)" - mes_donnees = {'datetime': str(self.get_datetime()),'server_msg': f'{log_message}'} + mes_donnees = {'datetime': str(self.Utils.get_sdatetime()),'server_msg': f'{log_message}'} self.db_execute_query(sql_insert, mes_donnees) return None - def init_log_system(self) -> None: - # Create folder if not available - logs_directory = f'logs{self.Config.OS_SEP}' - if not os.path.exists(f'{logs_directory}'): - os.makedirs(logs_directory) - - # Init logs object - self.logs = logging.getLogger(self.Config.LOGGING_NAME) - self.logs.setLevel(self.Config.DEBUG_LEVEL) - - # Add Handlers - file_hanlder = logging.FileHandler(f'logs{self.Config.OS_SEP}defender.log',encoding='UTF-8') - file_hanlder.setLevel(self.Config.DEBUG_LEVEL) - - stdout_handler = logging.StreamHandler() - stdout_handler.setLevel(50) - - # Define log format - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(filename)s - %(lineno)d - %(funcName)s - %(message)s') - - # Apply log format - file_hanlder.setFormatter(formatter) - stdout_handler.setFormatter(formatter) - - # Add handler to logs - self.logs.addHandler(file_hanlder) - self.logs.addHandler(stdout_handler) - - # Apply the filter - self.logs.addFilter(self.replace_filter) - - # self.logs.Logger('defender').addFilter(self.replace_filter) - self.logs.info('#################### STARTING DEFENDER ####################') - - return None - - def replace_filter(self, record: logging.LogRecord) -> bool: - - response = True - filter: list[str] = ['PING', f":{self.Config.SERVICE_PREFIX}auth"] - - # record.msg = record.getMessage().replace("PING", "[REDACTED]") - if self.Settings.CONSOLE: - print(record.getMessage()) - - for f in filter: - if f in record.getMessage(): - response = False - - return response # Retourne True pour permettre l'affichage du message - - def delete_logger(self, logger_name: str) -> None: - - # Récupérer le logger - logger = logging.getLogger(logger_name) - - # Retirer tous les gestionnaires du logger et les fermer - for handler in logger.handlers[:]: # Utiliser une copie de la liste - logger.removeHandler(handler) - handler.close() - - # Supprimer le logger du dictionnaire global - logging.Logger.manager.loggerDict.pop(logger_name, None) - - return None - - def log_cmd(self, user_cmd:str, cmd:str) -> None: + def log_cmd(self, user_cmd: str, cmd: str) -> None: """Enregistre les commandes envoyées par les utilisateurs Args: + user_cmd (str): The user who performed the command cmd (str): la commande a enregistrer """ cmd_list = cmd.split() @@ -260,10 +207,10 @@ class Base: cmd = ' '.join(cmd_list) insert_cmd_query = f"INSERT INTO {self.Config.TABLE_COMMAND} (datetime, user, commande) VALUES (:datetime, :user, :commande)" - mes_donnees = {'datetime': self.get_datetime(), 'user': user_cmd, 'commande': cmd} + mes_donnees = {'datetime': self.Utils.get_sdatetime(), 'user': user_cmd, 'commande': cmd} self.db_execute_query(insert_cmd_query, mes_donnees) - return False + return None def db_isModuleExist(self, module_name:str) -> bool: """Teste si un module existe déja dans la base de données @@ -283,22 +230,24 @@ class Base: else: return False - def db_record_module(self, user_cmd:str, module_name:str, isdefault:int = 0) -> None: + def db_record_module(self, user_cmd: str, module_name: str, isdefault: int = 0) -> None: """Enregistre les modules dans la base de données Args: - cmd (str): le module a enregistrer + user_cmd (str): The user who performed the command + module_name (str): The module name + isdefault (int): Is this a default module. Default 0 """ if not self.db_isModuleExist(module_name): self.logs.debug(f"Le module {module_name} n'existe pas alors ont le créer") insert_cmd_query = f"INSERT INTO {self.Config.TABLE_MODULE} (datetime, user, module_name, isdefault) VALUES (:datetime, :user, :module_name, :isdefault)" - mes_donnees = {'datetime': self.get_datetime(), 'user': user_cmd, 'module_name': module_name, 'isdefault': isdefault} + mes_donnees = {'datetime': self.Utils.get_sdatetime(), 'user': user_cmd, 'module_name': module_name, 'isdefault': isdefault} self.db_execute_query(insert_cmd_query, mes_donnees) else: self.logs.debug(f"Le module {module_name} existe déja dans la base de données") - return False + return None def db_update_module(self, user_cmd: str, module_name: str) -> None: """Modifie la date et le user qui a rechargé le module @@ -308,22 +257,22 @@ class Base: module_name (str): le module a rechargé """ update_cmd_query = f"UPDATE {self.Config.TABLE_MODULE} SET datetime = :datetime, user = :user WHERE module_name = :module_name" - mes_donnees = {'datetime': self.get_datetime(), 'user': user_cmd, 'module_name': module_name} + mes_donnees = {'datetime': self.Utils.get_sdatetime(), 'user': user_cmd, 'module_name': module_name} self.db_execute_query(update_cmd_query, mes_donnees) - return False + return None def db_delete_module(self, module_name:str) -> None: """Supprime les modules de la base de données Args: - cmd (str): le module a supprimer + module_name (str): The module name you want to delete """ insert_cmd_query = f"DELETE FROM {self.Config.TABLE_MODULE} WHERE module_name = :module_name" mes_donnees = {'module_name': module_name} self.db_execute_query(insert_cmd_query, mes_donnees) - return False + return None def db_sync_core_config(self, module_name: str, dataclassObj: object) -> bool: """Sync module local parameters with the database @@ -341,7 +290,7 @@ class Base: """ try: response = True - current_date = self.get_datetime() + current_date = self.Utils.get_sdatetime() core_table = self.Config.TABLE_CONFIG # Add local parameters to DB @@ -391,7 +340,7 @@ class Base: result = response.fetchall() for param, value in result: - if type(getattr(dataclassObj, param)) == list: + if isinstance(getattr(dataclassObj, param), list): value = ast.literal_eval(value) setattr(dataclassObj, param, self.int_if_possible(value)) @@ -418,7 +367,7 @@ class Base: isParamExist = result.fetchone() if not isParamExist is None: - mes_donnees = {'datetime': self.get_datetime(), + mes_donnees = {'datetime': self.Utils.get_sdatetime(), 'module_name': module_name, 'param_key': param_key, 'param_value': param_value @@ -443,9 +392,9 @@ class Base: user = self.db_execute_query(f"SELECT id FROM {self.Config.TABLE_ADMIN}") if not user.fetchall(): admin = self.Config.OWNER - password = self.crypt_password(self.Config.PASSWORD) + password = self.Utils.hash_password(self.Config.PASSWORD) - mes_donnees = {'createdOn': self.get_datetime(), + mes_donnees = {'createdOn': self.Utils.get_sdatetime(), 'user': admin, 'password': password, 'hostname': '*', @@ -472,8 +421,11 @@ class Base: self.logs.debug(f"-- Timer ID : {str(t.ident)} | Running Threads : {len(threading.enumerate())}") + return None + except AssertionError as ae: self.logs.error(f'Assertion Error -> {ae}') + return None def create_thread(self, func:object, func_args: tuple = (), run_once:bool = False, daemon: bool = True) -> None: """Create a new thread and store it into running_threads variable @@ -500,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 @@ -709,19 +694,6 @@ class Base: except AttributeError as ae: self.logs.error(f"Attribute Error : {ae}") - def crypt_password(self, password:str) -> str: - """Retourne un mot de passe chiffré en MD5 - - Args: - password (str): Le password en clair - - Returns: - str: Le password en MD5 - """ - md5_password = hashlib.md5(password.encode()).hexdigest() - - return md5_password - def int_if_possible(self, value): """Convertit la valeur reçue en entier, si possible. Sinon elle retourne la valeur initiale. @@ -740,14 +712,14 @@ class Base: except TypeError: return value - def convert_to_int(self, value: any) -> Union[int, None]: + def convert_to_int(self, value: Any) -> Optional[int]: """Convert a value to int Args: value (any): Value to convert to int if possible Returns: - Union[int, None]: Return the int value or None if not possible + int: Return the int value or None if not possible """ try: response = int(value) @@ -788,7 +760,7 @@ class Base: self.logs.error(f'General Error: {err}') return False - def decode_ip(self, ip_b64encoded: str) -> Union[str, None]: + def decode_ip(self, ip_b64encoded: str) -> Optional[str]: binary_ip = b64decode(ip_b64encoded) try: @@ -799,7 +771,7 @@ class Base: self.logs.critical(f'This remote ip is not valid : {ve}') return None - def encode_ip(self, remote_ip_address: str) -> Union[str, None]: + def encode_ip(self, remote_ip_address: str) -> Optional[str]: binary_ip = socket.inet_aton(remote_ip_address) try: @@ -813,15 +785,6 @@ class Base: self.logs.critical(f'General Error: {err}') return None - def get_random(self, lenght:int) -> str: - """ - Retourn une chaîne aléatoire en fonction de la longueur spécifiée. - """ - caracteres = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' - randomize = ''.join(random.choice(caracteres) for _ in range(lenght)) - - return randomize - def execute_periodic_action(self) -> None: if not self.periodic_func: @@ -861,23 +824,3 @@ class Base: self.logs.debug(f'Method to execute : {str(self.periodic_func)}') return None - - def clean_uid(self, uid:str) -> Union[str, None]: - """Clean UID by removing @ / % / + / ~ / * / : - - Args: - uid (str): The UID to clean - - Returns: - str: Clean UID without any sign - """ - try: - if uid is None: - return None - - pattern = fr'[:|@|%|\+|~|\*]*' - parsed_UID = re.sub(pattern, '', uid) - - return parsed_UID - except TypeError as te: - self.logs.error(f'Type Error: {te}') diff --git a/core/classes/admin.py b/core/classes/admin.py index 9cf22b2..e7ce183 100644 --- a/core/classes/admin.py +++ b/core/classes/admin.py @@ -1,127 +1,157 @@ -from typing import Union -import core.definition as df +from typing import TYPE_CHECKING, Optional from core.base import Base +from core.definition import MAdmin +if TYPE_CHECKING: + from core.loader import Loader class Admin: - UID_ADMIN_DB: list[df.MAdmin] = [] + UID_ADMIN_DB: list[MAdmin] = [] - def __init__(self, baseObj: Base) -> None: - self.Logs = baseObj.logs - pass + def __init__(self, loader: 'Loader') -> None: + self.Logs = loader.Logs - def insert(self, newAdmin: df.MAdmin) -> bool: + def insert(self, new_admin: MAdmin) -> bool: + """Insert a new admin object model - result = False - exist = False + Args: + new_admin (MAdmin): The new admin object model to insert + + Returns: + bool: True if it was inserted + """ for record in self.UID_ADMIN_DB: - if record.uid == newAdmin.uid: + if record.uid == new_admin.uid: # If the admin exist then return False and do not go further - exist = True self.Logs.debug(f'{record.uid} already exist') - return result + return False - if not exist: - self.UID_ADMIN_DB.append(newAdmin) - result = True - self.Logs.debug(f'UID ({newAdmin.uid}) has been created') + self.UID_ADMIN_DB.append(new_admin) + self.Logs.debug(f'A new admin ({new_admin.nickname}) has been created') + return True - if not result: - self.Logs.critical(f'The User Object was not inserted {newAdmin}') + def update_nickname(self, uid: str, new_admin_nickname: str) -> bool: + """Update nickname of an admin - return result + Args: + uid (str): The Admin UID + new_admin_nickname (str): The new nickname of the admin - def update_nickname(self, uid: str, newNickname: str) -> bool: - - result = False + Returns: + bool: True if the nickname has been updated. + """ for record in self.UID_ADMIN_DB: if record.uid == uid: # If the admin exist, update and do not go further - record.nickname = newNickname - result = True - self.Logs.debug(f'UID ({record.uid}) has been updated with new nickname {newNickname}') - return result + record.nickname = new_admin_nickname + self.Logs.debug(f'UID ({record.uid}) has been updated with new nickname {new_admin_nickname}') + return True - if not result: - self.Logs.debug(f'The new nickname {newNickname} was not updated, uid = {uid} - The Client is not an admin') - return result + self.Logs.debug(f'The new nickname {new_admin_nickname} was not updated, uid = {uid} - The Client is not an admin') + return False - def update_level(self, nickname: str, newLevel: int) -> bool: + def update_level(self, nickname: str, new_admin_level: int) -> bool: + """Update the admin level - result = False + Args: + nickname (str): The admin nickname + new_admin_level (int): The new level of the admin + + Returns: + bool: True if the admin level has been updated + """ for record in self.UID_ADMIN_DB: if record.nickname == nickname: # If the admin exist, update and do not go further - record.level = newLevel - result = True - self.Logs.debug(f'Admin ({record.nickname}) has been updated with new level {newLevel}') - return result + record.level = new_admin_level + self.Logs.debug(f'Admin ({record.nickname}) has been updated with new level {new_admin_level}') + return True - if not result: - self.Logs.debug(f'The new level {newLevel} was not updated, nickname = {nickname} - The Client is not an admin') + self.Logs.debug(f'The new level {new_admin_level} was not updated, nickname = {nickname} - The Client is not an admin') - return result + return False def delete(self, uidornickname: str) -> bool: + """Delete admin - result = False + Args: + uidornickname (str): The UID or nickname of the admin + + Returns: + bool: True if the admin has been deleted + """ for record in self.UID_ADMIN_DB: if record.uid == uidornickname: # If the admin exist, delete and do not go further self.UID_ADMIN_DB.remove(record) - result = True self.Logs.debug(f'UID ({record.uid}) has been deleted') - return result - if record.nickname == uidornickname: + return True + if record.nickname.lower() == uidornickname.lower(): # If the admin exist, delete and do not go further self.UID_ADMIN_DB.remove(record) - result = True self.Logs.debug(f'nickname ({record.nickname}) has been deleted') - return result + return True - if not result: - self.Logs.critical(f'The UID {uidornickname} was not deleted') + self.Logs.debug(f'The UID {uidornickname} was not deleted') - return result + return False - def get_Admin(self, uidornickname: str) -> Union[df.MAdmin, None]: + def get_admin(self, uidornickname: str) -> Optional[MAdmin]: + """Get the admin object model + + Args: + uidornickname (str): UID or Nickname of the admin + + Returns: + Optional[MAdmin]: The MAdmin object model if exist + """ - Admin = None for record in self.UID_ADMIN_DB: if record.uid == uidornickname: - Admin = record - elif record.nickname == uidornickname: - Admin = record + return record + elif record.nickname.lower() == uidornickname.lower(): + return record - #self.Logs.debug(f'Search {uidornickname} -- result = {Admin}') + return None - return Admin + def get_uid(self, uidornickname:str) -> Optional[str]: + """Get the UID of the admin - def get_uid(self, uidornickname:str) -> Union[str, None]: + Args: + uidornickname (str): The UID or nickname of the admin + + Returns: + Optional[str]: The UID of the admin + """ - uid = None for record in self.UID_ADMIN_DB: if record.uid == uidornickname: - uid = record.uid - if record.nickname == uidornickname: - uid = record.uid + return record.uid + if record.nickname.lower() == uidornickname.lower(): + return record.uid - self.Logs.debug(f'The UID that you are looking for {uidornickname} has been found {uid}') - return uid + return None - def get_nickname(self, uidornickname:str) -> Union[str, None]: + def get_nickname(self, uidornickname:str) -> Optional[str]: + """Get the nickname of the admin + + Args: + uidornickname (str): The UID or the nickname of the admin + + Returns: + Optional[str]: The nickname of the admin + """ - nickname = None for record in self.UID_ADMIN_DB: - if record.nickname == uidornickname: - nickname = record.nickname + if record.nickname.lower() == uidornickname.lower(): + return record.nickname if record.uid == uidornickname: - nickname = record.nickname - self.Logs.debug(f'The value {uidornickname} -- {nickname}') - return nickname \ No newline at end of file + return record.nickname + + return None \ No newline at end of file diff --git a/core/classes/channel.py b/core/classes/channel.py index 608b051..ff74229 100644 --- a/core/classes/channel.py +++ b/core/classes/channel.py @@ -1,12 +1,9 @@ from re import findall -from typing import Union, Literal, TYPE_CHECKING -from dataclasses import asdict - -from core.classes import user +from typing import Any, Optional, Literal, TYPE_CHECKING if TYPE_CHECKING: from core.definition import MChannel - from core.base import Base + from core.loader import Loader class Channel: @@ -14,18 +11,19 @@ class Channel: """List that contains all the Channels objects (ChannelModel) """ - def __init__(self, baseObj: 'Base') -> None: + def __init__(self, loader: 'Loader') -> None: - self.Logs = baseObj.logs - self.Base = baseObj + self.Logs = loader.Logs + self.Base = loader.Base + self.Utils = loader.Utils return None - def insert(self, newChan: 'MChannel') -> bool: + def insert(self, new_channel: 'MChannel') -> bool: """This method will insert a new channel and if the channel exist it will update the user list (uids) Args: - newChan (ChannelModel): The channel model object + new_channel (MChannel): The channel model object Returns: bool: True if new channel, False if channel exist (However UID could be updated) @@ -33,17 +31,17 @@ class Channel: result = False exist = False - if not self.Is_Channel(newChan.name): - self.Logs.error(f"The channel {newChan.name} is not valid, channel must start with #") + if not self.is_valid_channel(new_channel.name): + self.Logs.error(f"The channel {new_channel.name} is not valid, channel must start with #") return False for record in self.UID_CHANNEL_DB: - if record.name.lower() == newChan.name.lower(): + if record.name.lower() == new_channel.name.lower(): # If the channel exist, update the user list and do not go further exist = True # self.Logs.debug(f'{record.name} already exist') - for user in newChan.uids: + for user in new_channel.uids: record.uids.append(user) # Supprimer les doublons @@ -54,41 +52,58 @@ class Channel: if not exist: # If the channel don't exist, then create it - newChan.name = newChan.name.lower() - self.UID_CHANNEL_DB.append(newChan) + new_channel.name = new_channel.name.lower() + self.UID_CHANNEL_DB.append(new_channel) result = True - # self.Logs.debug(f'New Channel Created: ({newChan})') + # self.Logs.debug(f'New Channel Created: ({new_channel})') if not result: - self.Logs.critical(f'The Channel Object was not inserted {newChan}') + self.Logs.critical(f'The Channel Object was not inserted {new_channel}') self.clean_channel() return result def delete(self, channel_name: str) -> bool: + """Delete channel from the UID_CHANNEL_DB - chanObj = self.get_Channel(channel_name) + Args: + channel_name (str): The Channel name - if chanObj is None: + Returns: + bool: True if it was deleted + """ + + chan_obj = self.get_channel(channel_name) + + if chan_obj is None: return False - self.UID_CHANNEL_DB.remove(chanObj) + self.UID_CHANNEL_DB.remove(chan_obj) return True def delete_user_from_channel(self, channel_name: str, uid:str) -> bool: + """Delete a user from a channel + + Args: + channel_name (str): The channel name + uid (str): The Client UID + + Returns: + bool: True if the client has been deleted from the channel + """ try: result = False - chanObj = self.get_Channel(channel_name.lower()) + chan_obj = self.get_channel(channel_name.lower()) - if chanObj is None: + if chan_obj is None: return result - for userid in chanObj.uids: - if self.Base.clean_uid(userid) == self.Base.clean_uid(uid): - chanObj.uids.remove(userid) + for userid in chan_obj.uids: + if self.Utils.clean_uid(userid) == self.Utils.clean_uid(uid): + chan_obj.uids.remove(userid) result = True self.clean_channel() @@ -98,14 +113,21 @@ class Channel: self.Logs.error(f'{ve}') def delete_user_from_all_channel(self, uid:str) -> bool: + """Delete a client from all channels + + Args: + uid (str): The client UID + + Returns: + bool: True if the client has been deleted from all channels + """ try: result = False for record in self.UID_CHANNEL_DB: for user_id in record.uids: - if self.Base.clean_uid(user_id) == self.Base.clean_uid(uid): + if self.Utils.clean_uid(user_id) == self.Utils.clean_uid(uid): record.uids.remove(user_id) - # self.Logs.debug(f'The UID {uid} has been removed, here is the new object: {record}') result = True self.clean_channel() @@ -115,104 +137,113 @@ class Channel: self.Logs.error(f'{ve}') def add_user_to_a_channel(self, channel_name: str, uid: str) -> bool: + """Add a client to a channel + + Args: + channel_name (str): The channel name + uid (str): The client UID + + Returns: + bool: True is the clien has been added + """ try: - result = False - chanObj = self.get_Channel(channel_name) - self.Logs.debug(f"** {__name__}") + chan_obj = self.get_channel(channel_name) - if chanObj is None: - result = self.insert(MChannel(channel_name, uids=[uid])) - # self.Logs.debug(f"** {__name__} - result: {result}") - # self.Logs.debug(f'New Channel Created: ({chanObj})') - return result + if chan_obj is None: + # Create a new channel if the channel don't exist + self.Logs.debug(f"New channel will be created ({channel_name} - {uid})") + return self.insert(MChannel(channel_name, uids=[uid])) - chanObj.uids.append(uid) - del_duplicates = list(set(chanObj.uids)) - chanObj.uids = del_duplicates - # self.Logs.debug(f'New Channel Created: ({chanObj})') + chan_obj.uids.append(uid) + del_duplicates = list(set(chan_obj.uids)) + chan_obj.uids = del_duplicates return True except Exception as err: self.Logs.error(f'{err}') + return False def is_user_present_in_channel(self, channel_name: str, uid: str) -> bool: """Check if a user is present in the channel Args: - channel_name (str): The channel to check - uid (str): The UID + channel_name (str): The channel name to check + uid (str): The client UID Returns: bool: True if the user is present in the channel """ - user_found = False - chan = self.get_Channel(channel_name=channel_name) + chan = self.get_channel(channel_name=channel_name) if chan is None: - return user_found + return False - clean_uid = self.Base.clean_uid(uid=uid) + clean_uid = self.Utils.clean_uid(uid=uid) for chan_uid in chan.uids: - if self.Base.clean_uid(chan_uid) == clean_uid: - user_found = True - break + if self.Utils.clean_uid(chan_uid) == clean_uid: + return True - return user_found + return False def clean_channel(self) -> None: - """Remove Channels if empty + """If channel doesn't contain any client this method will remove the channel """ try: for record in self.UID_CHANNEL_DB: if not record.uids: self.UID_CHANNEL_DB.remove(record) - # self.Logs.debug(f'The Channel {record.name} has been removed, here is the new object: {record}') + return None except Exception as err: self.Logs.error(f'{err}') - def get_Channel(self, channel_name: str) -> Union['MChannel', None]: - - Channel = None - - for record in self.UID_CHANNEL_DB: - if record.name == channel_name: - Channel = record - - return Channel - - def get_Channel_AsDict(self, chan_name: str) -> Union[dict[str, any], None]: - - chanObj = self.get_Channel(chan_name=chan_name) - - if not chanObj is None: - chan_as_dict = asdict(chanObj) - return chan_as_dict - else: - return None - - def Is_Channel(self, channelToCheck: str) -> bool: - """Check if the string has the # caractere and return True if this is a channel + def get_channel(self, channel_name: str) -> Optional['MChannel']: + """Get the channel object Args: - channelToCheck (str): The string to test if it is a channel or not + channel_name (str): The Channel name + + Returns: + MChannel: The channel object model if exist else None + """ + + for record in self.UID_CHANNEL_DB: + if record.name.lower() == channel_name.lower(): + return record + + return None + + def get_channel_asdict(self, channel_name: str) -> Optional[dict[str, Any]]: + + channel_obj: Optional['MChannel'] = self.get_channel(channel_name) + + if channel_obj is None: + return None + + return channel_obj.to_dict() + + def is_valid_channel(self, channel_to_check: str) -> bool: + """Check if the string has the # caractere and return True if this is a valid channel + + Args: + channel_to_check (str): The string to test if it is a channel or not Returns: bool: True if the string is a channel / False if this is not a channel """ try: - if channelToCheck is None: + if channel_to_check is None: return False pattern = fr'^#' - isChannel = findall(pattern, channelToCheck) + isChannel = findall(pattern, channel_to_check) if not isChannel: return False else: return True except TypeError as te: - self.Logs.error(f'TypeError: [{channelToCheck}] - {te}') + self.Logs.error(f'TypeError: [{channel_to_check}] - {te}') except Exception as err: self.Logs.error(f'Error Not defined: {err}') @@ -228,7 +259,7 @@ class Channel: bool: True if action done """ try: - channel_name = channel_name.lower() if self.Is_Channel(channel_name) else None + channel_name = channel_name.lower() if self.is_valid_channel(channel_name) else None core_table = self.Base.Config.TABLE_CHANNEL if not channel_name: @@ -240,10 +271,10 @@ class Channel: case 'add': mes_donnees = {'module_name': module_name, 'channel_name': channel_name} response = self.Base.db_execute_query(f"SELECT id FROM {core_table} WHERE module_name = :module_name AND channel_name = :channel_name", mes_donnees) - isChannelExist = response.fetchone() + is_channel_exist = response.fetchone() - if isChannelExist is None: - mes_donnees = {'datetime': self.Base.get_datetime(), 'channel_name': channel_name, 'module_name': module_name} + if is_channel_exist is None: + mes_donnees = {'datetime': self.Utils.get_sdatetime(), 'channel_name': channel_name, 'module_name': module_name} insert = self.Base.db_execute_query(f"INSERT INTO {core_table} (datetime, channel_name, module_name) VALUES (:datetime, :channel_name, :module_name)", mes_donnees) if insert.rowcount: self.Logs.debug(f'New channel added: channel={channel_name} / module_name={module_name}') @@ -266,4 +297,3 @@ class Channel: except Exception as err: self.Logs.error(err) - diff --git a/core/classes/client.py b/core/classes/client.py index fd54ff0..f7afb41 100644 --- a/core/classes/client.py +++ b/core/classes/client.py @@ -1,39 +1,36 @@ from re import sub -from typing import Union, TYPE_CHECKING -from dataclasses import asdict +from typing import Any, Optional, Union, TYPE_CHECKING if TYPE_CHECKING: - from core.base import Base + from core.loader import Loader from core.definition import MClient class Client: CLIENT_DB: list['MClient'] = [] - def __init__(self, baseObj: 'Base') -> None: + def __init__(self, loader: 'Loader'): - self.Logs = baseObj.logs - self.Base = baseObj + self.Logs = loader.Logs + self.Base = loader.Base - return None - - def insert(self, newUser: 'MClient') -> bool: + def insert(self, new_client: 'MClient') -> bool: """Insert a new User object Args: - newUser (UserModel): New userModel object + new_client (MClient): New Client object Returns: bool: True if inserted """ - userObj = self.get_Client(newUser.uid) + client_obj = self.get_Client(new_client.uid) - if not userObj is None: + if not client_obj is None: # User already created return False return False - self.CLIENT_DB.append(newUser) + self.CLIENT_DB.append(new_client) return True @@ -47,12 +44,12 @@ class Client: Returns: bool: True if updated """ - userObj = self.get_Client(uidornickname=uid) + user_obj = self.get_Client(uidornickname=uid) - if userObj is None: + if user_obj is None: return False - userObj.nickname = newNickname + user_obj.nickname = newNickname return True @@ -67,16 +64,16 @@ class Client: bool: True if user mode has been updaed """ response = True - userObj = self.get_Client(uidornickname=uidornickname) + user_obj = self.get_Client(uidornickname=uidornickname) - if userObj is None: + if user_obj is None: return False action = modes[0] new_modes = modes[1:] - existing_umodes = userObj.umodes - umodes = userObj.umodes + existing_umodes = user_obj.umodes + umodes = user_obj.umodes if action == '+': @@ -95,7 +92,7 @@ class Client: final_umodes_liste = [x for x in self.Base.Settings.PROTOCTL_USER_MODES if x in liste_umodes] final_umodes = ''.join(final_umodes_liste) - userObj.umodes = f"+{final_umodes}" + user_obj.umodes = f"+{final_umodes}" return response @@ -109,16 +106,16 @@ class Client: bool: True if deleted """ - userObj = self.get_Client(uidornickname=uid) + user_obj = self.get_Client(uidornickname=uid) - if userObj is None: + if user_obj is None: return False - self.CLIENT_DB.remove(userObj) + self.CLIENT_DB.remove(user_obj) return True - def get_Client(self, uidornickname: str) -> Union['MClient', None]: + def get_Client(self, uidornickname: str) -> Optional['MClient']: """Get The Client Object model Args: @@ -127,16 +124,15 @@ class Client: Returns: UserModel|None: The UserModel Object | None """ - User = None for record in self.CLIENT_DB: if record.uid == uidornickname: - User = record + return record elif record.nickname == uidornickname: - User = record + return record - return User + return None - def get_uid(self, uidornickname:str) -> Union[str, None]: + def get_uid(self, uidornickname:str) -> Optional[str]: """Get the UID of the user starting from the UID or the Nickname Args: @@ -146,12 +142,12 @@ class Client: str|None: Return the UID """ - userObj = self.get_Client(uidornickname=uidornickname) + client_obj = self.get_Client(uidornickname=uidornickname) - if userObj is None: + if client_obj is None: return None - return userObj.uid + return client_obj.uid def get_nickname(self, uidornickname:str) -> Union[str, None]: """Get the Nickname starting from UID or the nickname @@ -162,14 +158,14 @@ class Client: Returns: str|None: the nickname """ - userObj = self.get_Client(uidornickname=uidornickname) + client_obj = self.get_Client(uidornickname=uidornickname) - if userObj is None: + if client_obj is None: return None - return userObj.nickname + return client_obj.nickname - def get_Client_AsDict(self, uidornickname: str) -> Union[dict[str, any], None]: + def get_client_asdict(self, uidornickname: str) -> Optional[dict[str, Any]]: """Transform User Object to a dictionary Args: @@ -178,12 +174,12 @@ class Client: Returns: Union[dict[str, any], None]: User Object as a dictionary or None """ - userObj = self.get_Client(uidornickname=uidornickname) + client_obj = self.get_Client(uidornickname=uidornickname) - if userObj is None: + if client_obj is None: return None - return asdict(userObj) + return client_obj.to_dict() def is_exist(self, uidornikname: str) -> bool: """Check if the UID or the nickname exist in the USER DB @@ -194,9 +190,9 @@ class Client: Returns: bool: True if exist """ - userObj = self.get_Client(uidornickname=uidornikname) + user_obj = self.get_Client(uidornickname=uidornikname) - if userObj is None: + if user_obj is None: return False return True diff --git a/core/classes/clone.py b/core/classes/clone.py deleted file mode 100644 index 7315822..0000000 --- a/core/classes/clone.py +++ /dev/null @@ -1,161 +0,0 @@ -from dataclasses import asdict -from core.definition import MClone -from typing import Union -from core.base import Base - -class Clone: - - UID_CLONE_DB: list[MClone] = [] - - def __init__(self, baseObj: Base) -> None: - - self.Logs = baseObj.logs - - return None - - def insert(self, newCloneObject: MClone) -> bool: - """Create new Clone object - - Args: - newCloneObject (CloneModel): New CloneModel object - - Returns: - bool: True if inserted - """ - result = False - exist = False - - for record in self.UID_CLONE_DB: - if record.nickname == newCloneObject.nickname: - # If the user exist then return False and do not go further - exist = True - self.Logs.warning(f'Nickname {record.nickname} already exist') - return result - if record.uid == newCloneObject.uid: - exist = True - self.Logs.warning(f'UID: {record.uid} already exist') - return result - - if not exist: - self.UID_CLONE_DB.append(newCloneObject) - result = True - # self.Logs.debug(f'New Clone Object Created: ({newCloneObject})') - - if not result: - self.Logs.critical(f'The Clone Object was not inserted {newCloneObject}') - - return result - - def delete(self, uidornickname: str) -> bool: - """Delete the Clone Object starting from the nickname or the UID - - Args: - uidornickname (str): UID or nickname of the clone - - Returns: - bool: True if deleted - """ - - cloneObj = self.get_Clone(uidornickname=uidornickname) - - if cloneObj is None: - return False - - self.UID_CLONE_DB.remove(cloneObj) - - return True - - def exists(self, nickname: str) -> bool: - """Check if the nickname exist - - Args: - nickname (str): Nickname of the clone - - Returns: - bool: True if the nickname exist - """ - response = False - - for cloneObject in self.UID_CLONE_DB: - if cloneObject.nickname == nickname: - response = True - - return response - - def uid_exists(self, uid: str) -> bool: - """Check if the nickname exist - - Args: - uid (str): uid of the clone - - Returns: - bool: True if the nickname exist - """ - response = False - - for cloneObject in self.UID_CLONE_DB: - if cloneObject.uid == uid: - response = True - - return response - - def get_Clone(self, uidornickname: str) -> Union[MClone, None]: - """Get MClone object or None - - Args: - uidornickname (str): The UID or the Nickname - - Returns: - Union[MClone, None]: Return MClone object or None - """ - cloneObj = None - - for clone in self.UID_CLONE_DB: - if clone.uid == uidornickname: - cloneObj = clone - if clone.nickname == uidornickname: - cloneObj = clone - - return cloneObj - - def get_uid(self, uidornickname: str) -> Union[str, None]: - """Get the UID of the clone starting from the UID or the Nickname - - Args: - uidornickname (str): UID or Nickname - - Returns: - str|None: Return the UID - """ - uid = None - for record in self.UID_CLONE_DB: - if record.uid == uidornickname: - uid = record.uid - if record.nickname == uidornickname: - uid = record.uid - - # if not uid is None: - # self.Logs.debug(f'The UID that you are looking for {uidornickname} has been found {uid}') - - return uid - - def get_Clone_AsDict(self, uidornickname: str) -> Union[dict[str, any], None]: - - cloneObj = self.get_Clone(uidornickname=uidornickname) - - if not cloneObj is None: - cloneObj_as_dict = asdict(cloneObj) - return cloneObj_as_dict - else: - return None - - def kill(self, nickname:str) -> bool: - - response = False - - for cloneObject in self.UID_CLONE_DB: - if cloneObject.nickname == nickname: - cloneObject.alive = False # Kill the clone - response = True - - return response \ No newline at end of file diff --git a/core/classes/commands.py b/core/classes/commands.py new file mode 100644 index 0000000..64e9e93 --- /dev/null +++ b/core/classes/commands.py @@ -0,0 +1,59 @@ +from typing import TYPE_CHECKING, Optional +from core.definition import MCommand + +if TYPE_CHECKING: + from core.loader import Loader + +class Command: + + DB_COMMANDS: list['MCommand'] = [] + + def __init__(self, loader: 'Loader'): + self.Base = loader.Base + + def build(self, new_command_obj: MCommand) -> bool: + + command = self.get_command(new_command_obj.command_name, new_command_obj.module_name) + if command is None: + self.DB_COMMANDS.append(new_command_obj) + return True + + # Update command if it exist + # Removing the object + if self.drop_command(command.command_name, command.module_name): + # Add the new object + self.DB_COMMANDS.append(new_command_obj) + return True + + return False + + def get_command(self, command_name: str, module_name: str) -> Optional[MCommand]: + + for command in self.DB_COMMANDS: + if command.command_name.lower() == command_name and command.module_name == module_name: + return command + + return None + + def drop_command(self, command_name: str, module_name: str) -> bool: + + cmd = self.get_command(command_name, module_name) + if cmd is not None: + self.DB_COMMANDS.remove(cmd) + return True + + return False + + def get_ordered_commands(self) -> list[MCommand]: + return sorted(self.DB_COMMANDS, key=lambda c: (c.command_level, c.module_name)) + + def get_commands_by_level(self, level: int = 0) -> Optional[list[MCommand]]: + + cmd_list = self.get_ordered_commands() + new_list: list[MCommand] = [] + + for cmd in cmd_list: + if cmd.command_level <= level: + new_list.append(cmd) + + return new_list \ No newline at end of file diff --git a/core/classes/config.py b/core/classes/config.py index 1958200..ca2f7ce 100644 --- a/core/classes/config.py +++ b/core/classes/config.py @@ -3,13 +3,13 @@ from sys import exit from os import sep from typing import Union from core.definition import MConfig - +from logging import Logger class Configuration: - def __init__(self) -> None: - + def __init__(self, logs: Logger) -> None: + self.Logs = logs self.ConfigObject: MConfig = self.__load_service_configuration() return None @@ -22,18 +22,18 @@ class Configuration: return configuration except FileNotFoundError as fe: - print(f'FileNotFound: {fe}') - print('Configuration file not found please create config/configuration.json') + self.Logs.error(f'FileNotFound: {fe}') + self.Logs.error('Configuration file not found please create config/configuration.json') exit(0) except KeyError as ke: - print(f'Key Error: {ke}') - print('The key must be defined in core/configuration.json') + self.Logs.error(f'Key Error: {ke}') + self.Logs.error('The key must be defined in core/configuration.json') def __load_service_configuration(self) -> MConfig: try: import_config = self.__load_json_service_configuration() - Model_keys = MConfig().__dict__ + Model_keys = MConfig().to_dict() model_key_list: list = [] json_config_key_list: list = [] @@ -46,12 +46,13 @@ class Configuration: for json_conf in json_config_key_list: if not json_conf in model_key_list: import_config.pop(json_conf, None) - print(f"\!/ The key {json_conf} is not expected, it has been removed from the system ! please remove it from configuration.json file \!/") + self.Logs.warning(f"[!] The key {json_conf} is not expected, it has been removed from the system ! please remove it from configuration.json file [!]") ConfigObject: MConfig = MConfig( **import_config ) return ConfigObject + except TypeError as te: - print(te) \ No newline at end of file + self.Logs.error(te) \ No newline at end of file diff --git a/core/classes/protocols/inspircd.py b/core/classes/protocols/inspircd.py index 39da912..67bc57e 100644 --- a/core/classes/protocols/inspircd.py +++ b/core/classes/protocols/inspircd.py @@ -14,8 +14,10 @@ class Inspircd: self.__Irc = ircInstance self.__Config = ircInstance.Config self.__Base = ircInstance.Base + self.__Utils = ircInstance.Loader.Utils + self.__Logs = ircInstance.Loader.Logs - self.__Base.logs.info(f"** Loading protocol [{__name__}]") + self.__Logs.info(f"** Loading protocol [{__name__}]") def send2socket(self, message: str, print_log: bool = True) -> None: """Envoit les commandes à envoyer au serveur. @@ -27,24 +29,24 @@ class Inspircd: with self.__Base.lock: self.__Irc.IrcSocket.send(f"{message}\r\n".encode(self.__Config.SERVEUR_CHARSET[0])) if print_log: - self.__Base.logs.debug(f'<< {message}') + self.__Logs.debug(f'<< {message}') except UnicodeDecodeError as ude: - self.__Base.logs.error(f'Decode Error try iso-8859-1 - {ude} - {message}') + self.__Logs.error(f'Decode Error try iso-8859-1 - {ude} - {message}') self.__Irc.IrcSocket.send(f"{message}\r\n".encode(self.__Config.SERVEUR_CHARSET[1],'replace')) except UnicodeEncodeError as uee: - self.__Base.logs.error(f'Encode Error try iso-8859-1 - {uee} - {message}') + self.__Logs.error(f'Encode Error try iso-8859-1 - {uee} - {message}') self.__Irc.IrcSocket.send(f"{message}\r\n".encode(self.__Config.SERVEUR_CHARSET[1],'replace')) except AssertionError as ae: - self.__Base.logs.warning(f'Assertion Error {ae} - message: {message}') + self.__Logs.warning(f'Assertion Error {ae} - message: {message}') except SSLEOFError as soe: - self.__Base.logs.error(f"SSLEOFError: {soe} - {message}") + self.__Logs.error(f"SSLEOFError: {soe} - {message}") except SSLError as se: - self.__Base.logs.error(f"SSLError: {se} - {message}") + self.__Logs.error(f"SSLError: {se} - {message}") except OSError as oe: - self.__Base.logs.error(f"OSError: {oe} - {message}") + self.__Logs.error(f"OSError: {oe} - {message}") except AttributeError as ae: - self.__Base.logs.critical(f"Attribute Error: {ae}") + self.__Logs.critical(f"Attribute Error: {ae}") def send_priv_msg(self, nick_from: str, msg: str, channel: str = None, nick_to: str = None): """Sending PRIVMSG to a channel or to a nickname by batches @@ -61,7 +63,7 @@ class Inspircd: User_to = self.__Irc.User.get_User(nick_to) if nick_to is None else None if User_from is None: - self.__Base.logs.error(f"The sender nickname [{nick_from}] do not exist") + self.__Logs.error(f"The sender nickname [{nick_from}] do not exist") return None if not channel is None: @@ -74,7 +76,7 @@ class Inspircd: batch = str(msg)[i:i+batch_size] self.send2socket(f":{nick_from} PRIVMSG {User_to.uid} :{batch}") except Exception as err: - self.__Base.logs.error(f"General Error: {err}") + self.__Logs.error(f"General Error: {err}") def send_notice(self, nick_from: str, nick_to: str, msg: str) -> None: """Sending NOTICE by batches @@ -90,7 +92,7 @@ class Inspircd: User_to = self.__Irc.User.get_User(nick_to) if User_from is None or User_to is None: - self.__Base.logs.error(f"The sender [{nick_from}] or the Reciever [{nick_to}] do not exist") + self.__Logs.error(f"The sender [{nick_from}] or the Reciever [{nick_to}] do not exist") return None for i in range(0, len(str(msg)), batch_size): @@ -98,9 +100,9 @@ class Inspircd: self.send2socket(f":{User_from.uid} NOTICE {User_to.uid} :{batch}") except Exception as err: - self.__Base.logs.error(f"General Error: {err}") + self.__Logs.error(f"General Error: {err}") - def link(self): + def send_link(self): """Créer le link et envoyer les informations nécessaires pour la connexion au serveur. """ @@ -122,7 +124,7 @@ class Inspircd: service_id = self.__Config.SERVICE_ID version = self.__Config.CURRENT_VERSION - unixtime = self.__Base.get_unixtime() + unixtime = self.__Utils.get_unixtime() self.send2socket(f"CAPAB START 1206") @@ -132,7 +134,7 @@ class Inspircd: self.send2socket(f"BURST {unixtime}") self.send2socket(f":{server_id} ENDBURST") - self.__Base.logs.debug(f'>> {__name__} Link information sent to the server') + self.__Logs.debug(f'>> {__name__} Link information sent to the server') def gline(self, nickname: str, hostname: str, set_by: str, expire_timestamp: int, set_at_timestamp: int, reason: str) -> None: # TKL + G user host set_by expire_timestamp set_at_timestamp :reason @@ -141,12 +143,12 @@ class Inspircd: return None - def set_nick(self, newnickname: str) -> None: + def send_set_nick(self, newnickname: str) -> None: self.send2socket(f":{self.__Config.SERVICE_NICKNAME} NICK {newnickname}") return None - def squit(self, server_id: str, server_link: str, reason: str) -> None: + def send_squit(self, server_id: str, server_link: str, reason: str) -> None: if not reason: reason = 'Service Shutdown' @@ -154,26 +156,26 @@ class Inspircd: self.send2socket(f":{server_id} SQUIT {server_link} :{reason}") return None - def ungline(self, nickname:str, hostname: str) -> None: + def send_ungline(self, nickname:str, hostname: str) -> None: self.send2socket(f":{self.__Config.SERVEUR_ID} TKL - G {nickname} {hostname} {self.__Config.SERVICE_NICKNAME}") return None - def kline(self, nickname: str, hostname: str, set_by: str, expire_timestamp: int, set_at_timestamp: int, reason: str) -> None: + def send_kline(self, nickname: str, hostname: str, set_by: str, expire_timestamp: int, set_at_timestamp: int, reason: str) -> None: # TKL + k user host set_by expire_timestamp set_at_timestamp :reason self.send2socket(f":{self.__Config.SERVEUR_ID} TKL + k {nickname} {hostname} {set_by} {expire_timestamp} {set_at_timestamp} :{reason}") return None - def sjoin(self, channel: str) -> None: + def send_sjoin(self, channel: str) -> None: - if not self.__Irc.Channel.Is_Channel(channel): - self.__Base.logs.error(f"The channel [{channel}] is not valid") + if not self.__Irc.Channel.is_valid_channel(channel): + self.__Logs.error(f"The channel [{channel}] is not valid") return None - self.send2socket(f":{self.__Config.SERVEUR_ID} SJOIN {self.__Base.get_unixtime()} {channel} + :{self.__Config.SERVICE_ID}") + self.send2socket(f":{self.__Config.SERVEUR_ID} SJOIN {self.__Utils.get_unixtime()} {channel} + :{self.__Config.SERVICE_ID}") # Add defender to the channel uids list self.__Irc.Channel.insert(self.__Irc.Loader.Definition.MChannel(name=channel, uids=[self.__Config.SERVICE_ID])) @@ -186,22 +188,22 @@ class Inspircd: uidornickname (str): The UID or the Nickname reason (str): The reason for the quit """ - userObj = self.__Irc.User.get_User(uidornickname=uid) - cloneObj = self.__Irc.Clone.get_Clone(uidornickname=uid) + user_obj = self.__Irc.User.get_User(uidornickname=uid) + clone_obj = self.__Irc.Clone.get_clone(uidornickname=uid) reputationObj = self.__Irc.Reputation.get_Reputation(uidornickname=uid) - if not userObj is None: - self.send2socket(f":{userObj.uid} QUIT :{reason}", print_log=print_log) - self.__Irc.User.delete(userObj.uid) + if not user_obj is None: + self.send2socket(f":{user_obj.uid} QUIT :{reason}", print_log=print_log) + self.__Irc.User.delete(user_obj.uid) - if not cloneObj is None: - self.__Irc.Clone.delete(cloneObj.uid) + if not clone_obj is None: + self.__Irc.Clone.delete(clone_obj.uid) if not reputationObj is None: self.__Irc.Reputation.delete(reputationObj.uid) if not self.__Irc.Channel.delete_user_from_all_channel(uid): - self.__Base.logs.error(f"The UID [{uid}] has not been deleted from all channels") + self.__Logs.error(f"The UID [{uid}] has not been deleted from all channels") return None @@ -220,9 +222,9 @@ class Inspircd: print_log (bool, optional): print logs if true. Defaults to True. """ # {self.Config.SERVEUR_ID} UID - # {clone.nickname} 1 {self.Base.get_unixtime()} {clone.username} {clone.hostname} {clone.uid} * {clone.umodes} {clone.vhost} * {self.Base.encode_ip(clone.remote_ip)} :{clone.realname} + # {clone.nickname} 1 {self.__Utils.get_unixtime()} {clone.username} {clone.hostname} {clone.uid} * {clone.umodes} {clone.vhost} * {self.Base.encode_ip(clone.remote_ip)} :{clone.realname} try: - unixtime = self.__Base.get_unixtime() + unixtime = self.__Utils.get_unixtime() encoded_ip = self.__Base.encode_ip(remote_ip) # Create the user @@ -241,7 +243,7 @@ class Inspircd: return None except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def send_join_chan(self, uidornickname: str, channel: str, password: str = None, print_log: bool = True) -> None: """Joining a channel @@ -259,8 +261,8 @@ class Inspircd: if userObj is None: return None - if not self.__Irc.Channel.Is_Channel(channel): - self.__Base.logs.error(f"The channel [{channel}] is not valid") + if not self.__Irc.Channel.is_valid_channel(channel): + self.__Logs.error(f"The channel [{channel}] is not valid") return None self.send2socket(f":{userObj.uid} JOIN {channel} {passwordChannel}", print_log=print_log) @@ -281,11 +283,11 @@ class Inspircd: userObj = self.__Irc.User.get_User(uidornickname) if userObj is None: - self.__Base.logs.error(f"The user [{uidornickname}] is not valid") + self.__Logs.error(f"The user [{uidornickname}] is not valid") return None - if not self.__Irc.Channel.Is_Channel(channel): - self.__Base.logs.error(f"The channel [{channel}] is not valid") + if not self.__Irc.Channel.is_valid_channel(channel): + self.__Logs.error(f"The channel [{channel}] is not valid") return None self.send2socket(f":{userObj.uid} PART {channel}", print_log=print_log) @@ -294,7 +296,7 @@ class Inspircd: self.__Irc.Channel.delete_user_from_channel(channel, userObj.uid) return None - def unkline(self, nickname:str, hostname: str) -> None: + def send_unkline(self, nickname:str, hostname: str) -> None: self.send2socket(f":{self.__Config.SERVEUR_ID} TKL - K {nickname} {hostname} {self.__Config.SERVICE_NICKNAME}") @@ -321,14 +323,14 @@ class Inspircd: # TODO : User object should be able to update user modes if self.__Irc.User.update_mode(userObj.uid, userMode): return None - # self.__Base.logs.debug(f"Updating user mode for [{userObj.nickname}] [{old_umodes}] => [{userObj.umodes}]") + # self.__Logs.debug(f"Updating user mode for [{userObj.nickname}] [{old_umodes}] => [{userObj.umodes}]") return None except IndexError as ie: - self.__Base.logs.error(f"{__name__} - Index Error: {ie}") + self.__Logs.error(f"{__name__} - Index Error: {ie}") except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_quit(self, serverMsg: list[str]) -> None: """Handle quit coming from a server @@ -349,9 +351,9 @@ class Inspircd: return None except IndexError as ie: - self.__Base.logs.error(f"{__name__} - Index Error: {ie}") + self.__Logs.error(f"{__name__} - Index Error: {ie}") except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_squit(self, serverMsg: list[str]) -> None: """Handle squit coming from a server @@ -408,9 +410,9 @@ class Inspircd: return None except IndexError as ie: - self.__Base.logs.error(f"{__name__} - Index Error: {ie}") + self.__Logs.error(f"{__name__} - Index Error: {ie}") except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_sjoin(self, serverMsg: list[str]) -> None: """Handle sjoin coming from a server @@ -443,7 +445,7 @@ class Inspircd: # Boucle qui va ajouter l'ensemble des users (UID) for i in range(start_boucle, len(serverMsg)): parsed_UID = str(serverMsg[i]) - clean_uid = self.__Irc.User.clean_uid(parsed_UID) + clean_uid = self.__Utils.clean_uid(parsed_UID) if not clean_uid is None and len(clean_uid) == 9: list_users.append(parsed_UID) @@ -457,9 +459,9 @@ class Inspircd: return None except IndexError as ie: - self.__Base.logs.error(f"{__name__} - Index Error: {ie}") + self.__Logs.error(f"{__name__} - Index Error: {ie}") except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_part(self, serverMsg: list[str]) -> None: """Handle part coming from a server @@ -478,9 +480,9 @@ class Inspircd: return None except IndexError as ie: - self.__Base.logs.error(f"{__name__} - Index Error: {ie}") + self.__Logs.error(f"{__name__} - Index Error: {ie}") except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_uid(self, serverMsg: list[str]) -> None: """Handle uid message coming from the server @@ -541,9 +543,9 @@ class Inspircd: ) return None except IndexError as ie: - self.__Base.logs.error(f"{__name__} - Index Error: {ie}") + self.__Logs.error(f"{__name__} - Index Error: {ie}") except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_server_ping(self, serverMsg: list[str]) -> None: """Send a PONG message to the server @@ -561,7 +563,7 @@ class Inspircd: return None except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_version(self, serverMsg: list[str]) -> None: """Sending Server Version to the server @@ -573,7 +575,7 @@ class Inspircd: # Réponse a un CTCP VERSION try: - nickname = self.__Irc.User.get_nickname(self.__Base.clean_uid(serverMsg[1])) + nickname = self.__Irc.User.get_nickname(self.__Utils.clean_uid(serverMsg[1])) dnickname = self.__Config.SERVICE_NICKNAME arg = serverMsg[4].replace(':', '') @@ -585,7 +587,7 @@ class Inspircd: return None except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_time(self, serverMsg: list[str]) -> None: """Sending TIME answer to a requestor @@ -597,10 +599,10 @@ class Inspircd: # Réponse a un CTCP VERSION try: - nickname = self.__Irc.User.get_nickname(self.__Base.clean_uid(serverMsg[1])) + nickname = self.__Irc.User.get_nickname(self.__Utils.clean_uid(serverMsg[1])) dnickname = self.__Config.SERVICE_NICKNAME arg = serverMsg[4].replace(':', '') - current_datetime = self.__Base.get_datetime() + current_datetime = self.__Utils.get_sdatetime() if nickname is None: return None @@ -610,7 +612,7 @@ class Inspircd: return None except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_ping(self, serverMsg: list[str]) -> None: """Sending a PING answer to requestor @@ -622,7 +624,7 @@ class Inspircd: # Réponse a un CTCP VERSION try: - nickname = self.__Irc.User.get_nickname(self.__Base.clean_uid(serverMsg[1])) + nickname = self.__Irc.User.get_nickname(self.__Utils.clean_uid(serverMsg[1])) dnickname = self.__Config.SERVICE_NICKNAME arg = serverMsg[4].replace(':', '') @@ -631,7 +633,7 @@ class Inspircd: if arg == '\x01PING': recieved_unixtime = int(serverMsg[5].replace('\x01','')) - current_unixtime = self.__Base.get_unixtime() + current_unixtime = self.__Utils.get_unixtime() ping_response = current_unixtime - recieved_unixtime # self.__Irc.send2socket(f':{dnickname} NOTICE {nickname} :\x01PING {ping_response} secs\x01') @@ -643,7 +645,7 @@ class Inspircd: return None except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_version_msg(self, serverMsg: list[str]) -> None: """Handle version coming from the server @@ -653,7 +655,7 @@ class Inspircd: """ try: # ['@label=0073', ':0014E7P06', 'VERSION', 'PyDefender'] - getUser = self.__Irc.User.get_User(self.__Irc.User.clean_uid(serverMsg[1])) + getUser = self.__Irc.User.get_User(self.__Utils.clean_uid(serverMsg[1])) if getUser is None: return None @@ -668,4 +670,4 @@ class Inspircd: return None except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") diff --git a/core/classes/protocols/unreal6.py b/core/classes/protocols/unreal6.py index 21c5b9f..c1c785f 100644 --- a/core/classes/protocols/unreal6.py +++ b/core/classes/protocols/unreal6.py @@ -1,6 +1,6 @@ from re import match, findall, search from datetime import datetime -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING, Optional from ssl import SSLEOFError, SSLError if TYPE_CHECKING: @@ -16,14 +16,33 @@ class Unrealircd6: self.__Config = ircInstance.Config self.__Base = ircInstance.Base self.__Settings = ircInstance.Base.Settings + self.__Utils = ircInstance.Loader.Utils + self.__Logs = ircInstance.Loader.Logs - self.known_protocol = ['SJOIN', 'UID', 'MD', 'QUIT', 'SQUIT', + self.known_protocol: set[str] = {'SJOIN', 'UID', 'MD', 'QUIT', 'SQUIT', 'EOS', 'PRIVMSG', 'MODE', 'UMODE2', 'VERSION', 'REPUTATION', 'SVS2MODE', - 'SLOG', 'NICK', 'PART', 'PONG' - ] + 'SLOG', 'NICK', 'PART', 'PONG', + 'PROTOCTL', 'SERVER', 'SMOD', 'TKL', 'NETINFO'} - self.__Base.logs.info(f"** Loading protocol [{__name__}]") + self.__Logs.info(f"** Loading protocol [{__name__}]") + + def get_ircd_protocol_poisition(self, cmd: list[str]) -> tuple[int, Optional[str]]: + """Get the position of known commands + + Args: + cmd (list[str]): The server response + + Returns: + tuple[int, Optional[str]]: The position and the command. + """ + for index, token in enumerate(cmd): + if token.upper() in self.known_protocol: + return index, token.upper() + + self.__Logs.debug(f"[IRCD LOGS] You need to handle this response: {cmd}") + + return (-1, None) def send2socket(self, message: str, print_log: bool = True) -> None: """Envoit les commandes à envoyer au serveur. @@ -35,24 +54,24 @@ class Unrealircd6: with self.__Base.lock: self.__Irc.IrcSocket.send(f"{message}\r\n".encode(self.__Config.SERVEUR_CHARSET[0])) if print_log: - self.__Base.logs.debug(f'<< {message}') + self.__Logs.debug(f'<< {message}') except UnicodeDecodeError as ude: - self.__Base.logs.error(f'Decode Error try iso-8859-1 - {ude} - {message}') + self.__Logs.error(f'Decode Error try iso-8859-1 - {ude} - {message}') self.__Irc.IrcSocket.send(f"{message}\r\n".encode(self.__Config.SERVEUR_CHARSET[1],'replace')) except UnicodeEncodeError as uee: - self.__Base.logs.error(f'Encode Error try iso-8859-1 - {uee} - {message}') + self.__Logs.error(f'Encode Error try iso-8859-1 - {uee} - {message}') self.__Irc.IrcSocket.send(f"{message}\r\n".encode(self.__Config.SERVEUR_CHARSET[1],'replace')) except AssertionError as ae: - self.__Base.logs.warning(f'Assertion Error {ae} - message: {message}') + self.__Logs.warning(f'Assertion Error {ae} - message: {message}') except SSLEOFError as soe: - self.__Base.logs.error(f"SSLEOFError: {soe} - {message}") + self.__Logs.error(f"SSLEOFError: {soe} - {message}") except SSLError as se: - self.__Base.logs.error(f"SSLError: {se} - {message}") + self.__Logs.error(f"SSLError: {se} - {message}") except OSError as oe: - self.__Base.logs.error(f"OSError: {oe} - {message}") + self.__Logs.error(f"OSError: {oe} - {message}") except AttributeError as ae: - self.__Base.logs.critical(f"Attribute Error: {ae}") + self.__Logs.critical(f"Attribute Error: {ae}") def send_priv_msg(self, nick_from: str, msg: str, channel: str = None, nick_to: str = None): """Sending PRIVMSG to a channel or to a nickname by batches @@ -69,7 +88,7 @@ class Unrealircd6: User_to = self.__Irc.User.get_User(nick_to) if not nick_to is None else None if User_from is None: - self.__Base.logs.error(f"The sender nickname [{nick_from}] do not exist") + self.__Logs.error(f"The sender nickname [{nick_from}] do not exist") return None if not channel is None: @@ -83,8 +102,8 @@ class Unrealircd6: self.send2socket(f":{nick_from} PRIVMSG {User_to.uid} :{batch}") except Exception as err: - self.__Base.logs.error(f"General Error: {err}") - self.__Base.logs.error(f"General Error: {nick_from} - {channel} - {nick_to}") + self.__Logs.error(f"General Error: {err}") + self.__Logs.error(f"General Error: {nick_from} - {channel} - {nick_to}") def send_notice(self, nick_from: str, nick_to: str, msg: str) -> None: """Sending NOTICE by batches @@ -100,7 +119,7 @@ class Unrealircd6: User_to = self.__Irc.User.get_User(nick_to) if User_from is None or User_to is None: - self.__Base.logs.error(f"The sender [{nick_from}] or the Reciever [{nick_to}] do not exist") + self.__Logs.error(f"The sender [{nick_from}] or the Reciever [{nick_to}] do not exist") return None for i in range(0, len(str(msg)), batch_size): @@ -108,9 +127,9 @@ class Unrealircd6: self.send2socket(f":{User_from.uid} NOTICE {User_to.uid} :{batch}") except Exception as err: - self.__Base.logs.error(f"General Error: {err}") + self.__Logs.error(f"General Error: {err}") - def parse_server_msg(self, server_msg: list[str]) -> Union[str, None]: + def parse_server_msg(self, server_msg: list[str]) -> Optional[str]: """Parse the server message and return the command Args: @@ -144,7 +163,7 @@ class Unrealircd6: return None - def link(self): + def send_link(self): """Créer le link et envoyer les informations nécessaires pour la connexion au serveur. """ @@ -167,7 +186,7 @@ class Unrealircd6: service_id = self.__Config.SERVICE_ID version = self.__Config.CURRENT_VERSION - unixtime = self.__Base.get_unixtime() + unixtime = self.__Utils.get_unixtime() self.send2socket(f":{server_id} PASS :{password}", print_log=False) self.send2socket(f":{server_id} PROTOCTL SID NOQUIT NICKv2 SJOIN SJ3 NICKIP TKLEXT2 NEXTBANS CLK EXTSWHOIS MLOCK MTAGS") @@ -177,20 +196,20 @@ class Unrealircd6: self.send2socket(f":{server_id} SERVER {link} 1 :{info}") self.send2socket(f":{server_id} {nickname} :Reserved for services") self.send2socket(f":{server_id} UID {nickname} 1 {unixtime} {username} {host} {service_id} * {smodes} * * fwAAAQ== :{realname}") - self.sjoin(chan) + self.send_sjoin(chan) self.send2socket(f":{server_id} TKL + Q * {nickname} {host} 0 {unixtime} :Reserved for services") self.send2socket(f":{service_id} MODE {chan} {cmodes}") - self.__Base.logs.debug(f'>> {__name__} Link information sent to the server') + self.__Logs.debug(f'>> {__name__} Link information sent to the server') - def gline(self, nickname: str, hostname: str, set_by: str, expire_timestamp: int, set_at_timestamp: int, reason: str) -> None: + def send_gline(self, nickname: str, hostname: str, set_by: str, expire_timestamp: int, set_at_timestamp: int, reason: str) -> None: # TKL + G user host set_by expire_timestamp set_at_timestamp :reason self.send2socket(f":{self.__Config.SERVEUR_ID} TKL + G {nickname} {hostname} {set_by} {expire_timestamp} {set_at_timestamp} :{reason}") return None - def set_nick(self, newnickname: str) -> None: + def send_set_nick(self, newnickname: str) -> None: """Change nickname of the server \n This method will also update the User object Args: @@ -202,7 +221,7 @@ class Unrealircd6: self.__Irc.User.update_nickname(userObj.uid, newnickname) return None - def squit(self, server_id: str, server_link: str, reason: str) -> None: + def send_squit(self, server_id: str, server_link: str, reason: str) -> None: if not reason: reason = 'Service Shutdown' @@ -210,36 +229,36 @@ class Unrealircd6: self.send2socket(f":{server_id} SQUIT {server_link} :{reason}") return None - def ungline(self, nickname:str, hostname: str) -> None: + def send_ungline(self, nickname:str, hostname: str) -> None: self.send2socket(f":{self.__Config.SERVEUR_ID} TKL - G {nickname} {hostname} {self.__Config.SERVICE_NICKNAME}") return None - def kline(self, nickname: str, hostname: str, set_by: str, expire_timestamp: int, set_at_timestamp: int, reason: str) -> None: + def send_kline(self, nickname: str, hostname: str, set_by: str, expire_timestamp: int, set_at_timestamp: int, reason: str) -> None: # TKL + k user host set_by expire_timestamp set_at_timestamp :reason self.send2socket(f":{self.__Config.SERVEUR_ID} TKL + k {nickname} {hostname} {set_by} {expire_timestamp} {set_at_timestamp} :{reason}") return None - def unkline(self, nickname:str, hostname: str) -> None: + def send_unkline(self, nickname:str, hostname: str) -> None: self.send2socket(f":{self.__Config.SERVEUR_ID} TKL - K {nickname} {hostname} {self.__Config.SERVICE_NICKNAME}") return None - def sjoin(self, channel: str) -> None: + def send_sjoin(self, channel: str) -> None: """Server will join a channel with pre defined umodes Args: channel (str): Channel to join """ - if not self.__Irc.Channel.Is_Channel(channel): - self.__Base.logs.error(f"The channel [{channel}] is not valid") + if not self.__Irc.Channel.is_valid_channel(channel): + self.__Logs.error(f"The channel [{channel}] is not valid") return None - self.send2socket(f":{self.__Config.SERVEUR_ID} SJOIN {self.__Base.get_unixtime()} {channel} {self.__Config.SERVICE_UMODES} :{self.__Config.SERVICE_ID}") + self.send2socket(f":{self.__Config.SERVEUR_ID} SJOIN {self.__Utils.get_unixtime()} {channel} {self.__Config.SERVICE_UMODES} :{self.__Config.SERVICE_ID}") self.send2socket(f":{self.__Config.SERVICE_ID} MODE {channel} {self.__Config.SERVICE_UMODES} {self.__Config.SERVICE_ID}") # Add defender to the channel uids list @@ -257,7 +276,7 @@ class Unrealircd6: try: userObj = self.__Irc.User.get_User(uidornickname=nick_to_sapart) - chanObj = self.__Irc.Channel.get_Channel(channel_name) + chanObj = self.__Irc.Channel.get_channel(channel_name) service_uid = self.__Config.SERVICE_ID if userObj is None or chanObj is None: @@ -269,7 +288,7 @@ class Unrealircd6: return None except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def send_sajoin(self, nick_to_sajoin: str, channel_name: str) -> None: """_summary_ @@ -281,7 +300,7 @@ class Unrealircd6: try: userObj = self.__Irc.User.get_User(uidornickname=nick_to_sajoin) - chanObj = self.__Irc.Channel.get_Channel(channel_name) + chanObj = self.__Irc.Channel.get_channel(channel_name) service_uid = self.__Config.SERVICE_ID if userObj is None: @@ -290,7 +309,7 @@ class Unrealircd6: if chanObj is None: # Channel not exist - if not self.__Irc.Channel.Is_Channel(channel_name): + if not self.__Irc.Channel.is_valid_channel(channel_name): # Incorrect channel: leave return None @@ -306,7 +325,7 @@ class Unrealircd6: return None except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def send_svs_mode(self, nickname: str, user_mode: str) -> None: try: @@ -325,34 +344,29 @@ class Unrealircd6: return None except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def send_quit(self, uid: str, reason: str, print_log: True) -> None: """Send quit message - Delete uid from User object - - Delete uid from Clone object - Delete uid from Reputation object Args: uidornickname (str): The UID or the Nickname reason (str): The reason for the quit """ - userObj = self.__Irc.User.get_User(uidornickname=uid) - cloneObj = self.__Irc.Clone.get_Clone(uidornickname=uid) + user_obj = self.__Irc.User.get_User(uidornickname=uid) reputationObj = self.__Irc.Reputation.get_Reputation(uidornickname=uid) - if not userObj is None: - self.send2socket(f":{userObj.uid} QUIT :{reason}", print_log=print_log) - self.__Irc.User.delete(userObj.uid) - - if not cloneObj is None: - self.__Irc.Clone.delete(cloneObj.uid) + if not user_obj is None: + self.send2socket(f":{user_obj.uid} QUIT :{reason}", print_log=print_log) + self.__Irc.User.delete(user_obj.uid) if not reputationObj is None: self.__Irc.Reputation.delete(reputationObj.uid) if not self.__Irc.Channel.delete_user_from_all_channel(uid): - self.__Base.logs.error(f"The UID [{uid}] has not been deleted from all channels") + self.__Logs.error(f"The UID [{uid}] has not been deleted from all channels") return None @@ -371,9 +385,9 @@ class Unrealircd6: print_log (bool, optional): print logs if true. Defaults to True. """ # {self.Config.SERVEUR_ID} UID - # {clone.nickname} 1 {self.Base.get_unixtime()} {clone.username} {clone.hostname} {clone.uid} * {clone.umodes} {clone.vhost} * {self.Base.encode_ip(clone.remote_ip)} :{clone.realname} + # {clone.nickname} 1 {self.__Utils.get_unixtime()} {clone.username} {clone.hostname} {clone.uid} * {clone.umodes} {clone.vhost} * {self.Base.encode_ip(clone.remote_ip)} :{clone.realname} try: - unixtime = self.__Base.get_unixtime() + unixtime = self.__Utils.get_unixtime() encoded_ip = self.__Base.encode_ip(remote_ip) # Create the user @@ -392,7 +406,7 @@ class Unrealircd6: return None except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def send_join_chan(self, uidornickname: str, channel: str, password: str = None, print_log: bool = True) -> None: """Joining a channel @@ -410,8 +424,8 @@ class Unrealircd6: if userObj is None: return None - if not self.__Irc.Channel.Is_Channel(channel): - self.__Base.logs.error(f"The channel [{channel}] is not valid") + if not self.__Irc.Channel.is_valid_channel(channel): + self.__Logs.error(f"The channel [{channel}] is not valid") return None self.send2socket(f":{userObj.uid} JOIN {channel} {passwordChannel}", print_log=print_log) @@ -447,11 +461,11 @@ class Unrealircd6: userObj = self.__Irc.User.get_User(uidornickname) if userObj is None: - self.__Base.logs.error(f"The user [{uidornickname}] is not valid") + self.__Logs.error(f"The user [{uidornickname}] is not valid") return None - if not self.__Irc.Channel.Is_Channel(channel): - self.__Base.logs.error(f"The channel [{channel}] is not valid") + if not self.__Irc.Channel.is_valid_channel(channel): + self.__Logs.error(f"The channel [{channel}] is not valid") return None self.send2socket(f":{userObj.uid} PART {channel}", print_log=print_log) @@ -462,9 +476,9 @@ class Unrealircd6: def send_mode_chan(self, channel_name: str, channel_mode: str) -> None: - channel = self.__Irc.Channel.Is_Channel(channelToCheck=channel_name) + channel = self.__Irc.Channel.is_valid_channel(channel_name) if not channel: - self.__Base.logs.error(f'The channel [{channel_name}] is not correct') + self.__Logs.error(f'The channel [{channel_name}] is not correct') return None self.send2socket(f":{self.__Config.SERVICE_NICKNAME} MODE {channel_name} {channel_mode}") @@ -502,9 +516,9 @@ class Unrealircd6: return None except IndexError as ie: - self.__Base.logs.error(f"{__name__} - Index Error: {ie}") + self.__Logs.error(f"{__name__} - Index Error: {ie}") except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_mode(self, serverMsg: list[str]) -> None: """Handle mode coming from a server @@ -538,14 +552,14 @@ class Unrealircd6: # TODO : User object should be able to update user modes if self.__Irc.User.update_mode(userObj.uid, userMode): return None - # self.__Base.logs.debug(f"Updating user mode for [{userObj.nickname}] [{old_umodes}] => [{userObj.umodes}]") + # self.__Logs.debug(f"Updating user mode for [{userObj.nickname}] [{old_umodes}] => [{userObj.umodes}]") return None except IndexError as ie: - self.__Base.logs.error(f"{__name__} - Index Error: {ie}") + self.__Logs.error(f"{__name__} - Index Error: {ie}") except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_quit(self, serverMsg: list[str]) -> None: """Handle quit coming from a server @@ -562,14 +576,13 @@ class Unrealircd6: self.__Irc.User.delete(uid_who_quit) self.__Irc.Client.delete(uid_who_quit) self.__Irc.Reputation.delete(uid_who_quit) - self.__Irc.Clone.delete(uid_who_quit) return None except IndexError as ie: - self.__Base.logs.error(f"{__name__} - Index Error: {ie}") + self.__Logs.error(f"{__name__} - Index Error: {ie}") except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_squit(self, serverMsg: list[str]) -> None: """Handle squit coming from a server @@ -649,9 +662,9 @@ class Unrealircd6: return None except IndexError as ie: - self.__Base.logs.error(f"{__name__} - Index Error: {ie}") + self.__Logs.error(f"{__name__} - Index Error: {ie}") except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_sjoin(self, serverMsg: list[str]) -> None: """Handle sjoin coming from a server @@ -688,7 +701,7 @@ class Unrealircd6: # Boucle qui va ajouter l'ensemble des users (UID) for i in range(start_boucle, len(serverMsg_copy)): parsed_UID = str(serverMsg_copy[i]) - clean_uid = self.__Irc.User.clean_uid(parsed_UID) + clean_uid = self.__Utils.clean_uid(parsed_UID) if not clean_uid is None and len(clean_uid) == 9: list_users.append(clean_uid) @@ -702,9 +715,9 @@ class Unrealircd6: return None except IndexError as ie: - self.__Base.logs.error(f"{__name__} - Index Error: {ie}") + self.__Logs.error(f"{__name__} - Index Error: {ie}") except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_part(self, serverMsg: list[str]) -> None: """Handle part coming from a server @@ -723,9 +736,9 @@ class Unrealircd6: return None except IndexError as ie: - self.__Base.logs.error(f"{__name__} - Index Error: {ie}") + self.__Logs.error(f"{__name__} - Index Error: {ie}") except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_eos(self, serverMsg: list[str]) -> None: """Handle EOS coming from a server @@ -757,16 +770,16 @@ class Unrealircd6: print(f"# VERSION : {version} ") print(f"################################################") - self.__Base.logs.info(f"################### DEFENDER ###################") - self.__Base.logs.info(f"# SERVICE CONNECTE ") - self.__Base.logs.info(f"# SERVEUR : {self.__Config.SERVEUR_IP} ") - self.__Base.logs.info(f"# PORT : {self.__Config.SERVEUR_PORT} ") - self.__Base.logs.info(f"# SSL : {self.__Config.SERVEUR_SSL} ") - self.__Base.logs.info(f"# SSL VER : {self.__Config.SSL_VERSION} ") - self.__Base.logs.info(f"# NICKNAME : {self.__Config.SERVICE_NICKNAME} ") - self.__Base.logs.info(f"# CHANNEL : {self.__Config.SERVICE_CHANLOG} ") - self.__Base.logs.info(f"# VERSION : {version} ") - self.__Base.logs.info(f"################################################") + self.__Logs.info(f"################### DEFENDER ###################") + self.__Logs.info(f"# SERVICE CONNECTE ") + self.__Logs.info(f"# SERVEUR : {self.__Config.SERVEUR_IP} ") + self.__Logs.info(f"# PORT : {self.__Config.SERVEUR_PORT} ") + self.__Logs.info(f"# SSL : {self.__Config.SERVEUR_SSL} ") + self.__Logs.info(f"# SSL VER : {self.__Config.SSL_VERSION} ") + self.__Logs.info(f"# NICKNAME : {self.__Config.SERVICE_NICKNAME} ") + self.__Logs.info(f"# CHANNEL : {self.__Config.SERVICE_CHANLOG} ") + self.__Logs.info(f"# VERSION : {version} ") + self.__Logs.info(f"################################################") if self.__Base.check_for_new_version(False): self.send_priv_msg( @@ -789,11 +802,11 @@ class Unrealircd6: return None except IndexError as ie: - self.__Base.logs.error(f"{__name__} - Key Error: {ie}") + self.__Logs.error(f"{__name__} - Key Error: {ie}") except KeyError as ke: - self.__Base.logs.error(f"{__name__} - Key Error: {ke}") + self.__Logs.error(f"{__name__} - Key Error: {ke}") except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_reputation(self, serverMsg: list[str]) -> None: """Handle REPUTATION coming from a server @@ -824,7 +837,7 @@ class Unrealircd6: self.__Irc.first_score = 0 self.Logs.error(f'Value Error {__name__}: {ve}') except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_uid(self, serverMsg: list[str]) -> None: """Handle uid message coming from the server @@ -885,9 +898,9 @@ class Unrealircd6: ) return None except IndexError as ie: - self.__Base.logs.error(f"{__name__} - Index Error: {ie}") + self.__Logs.error(f"{__name__} - Index Error: {ie}") except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_privmsg(self, serverMsg: list[str]) -> None: """Handle PRIVMSG message coming from the server @@ -908,11 +921,11 @@ class Unrealircd6: if cmd[2] == 'PRIVMSG' and cmd[4] == ':auth': data_copy = cmd.copy() data_copy[6] = '**********' - self.__Base.logs.debug(f">> {data_copy}") + self.__Logs.debug(f">> {data_copy}") else: - self.__Base.logs.debug(f">> {cmd}") + self.__Logs.debug(f">> {cmd}") else: - self.__Base.logs.debug(f">> {cmd}") + self.__Logs.debug(f">> {cmd}") get_uid_or_nickname = str(cmd[0].replace(':','')) user_trigger = self.__Irc.User.get_nickname(get_uid_or_nickname) @@ -927,7 +940,7 @@ class Unrealircd6: arg = convert_to_string.split() arg.remove(f':{self.__Config.SERVICE_PREFIX}') if not arg[0].lower() in self.__Irc.module_commands_list: - self.__Base.logs.debug(f"This command {arg[0]} is not available") + self.__Logs.debug(f"This command {arg[0]} is not available") self.send_notice( nick_from=self.__Config.SERVICE_NICKNAME, nick_to=user_trigger, @@ -938,7 +951,7 @@ class Unrealircd6: cmd_to_send = convert_to_string.replace(':','') self.__Base.log_cmd(user_trigger, cmd_to_send) - fromchannel = str(cmd[2]).lower() if self.__Irc.Channel.Is_Channel(cmd[2]) else None + fromchannel = str(cmd[2]).lower() if self.__Irc.Channel.is_valid_channel(cmd[2]) else None self.__Irc.hcmds(user_trigger, fromchannel, arg, cmd) if cmd[2] == self.__Config.SERVICE_ID: @@ -966,7 +979,7 @@ class Unrealircd6: return False if not arg[0].lower() in self.__Irc.module_commands_list: - self.__Base.logs.debug(f"This command {arg[0]} sent by {user_trigger} is not available") + self.__Logs.debug(f"This command {arg[0]} sent by {user_trigger} is not available") return False cmd_to_send = convert_to_string.replace(':','') @@ -974,17 +987,17 @@ class Unrealircd6: fromchannel = None if len(arg) >= 2: - fromchannel = str(arg[1]).lower() if self.__Irc.Channel.Is_Channel(arg[1]) else None + fromchannel = str(arg[1]).lower() if self.__Irc.Channel.is_valid_channel(arg[1]) else None self.__Irc.hcmds(user_trigger, fromchannel, arg, cmd) return None except KeyError as ke: - self.__Base.logs.error(f"Key Error: {ke}") + self.__Logs.error(f"Key Error: {ke}") except AttributeError as ae: - self.__Base.logs.error(f"Attribute Error: {ae}") + self.__Logs.error(f"Attribute Error: {ae}") except Exception as err: - self.__Base.logs.error(f"General Error: {err} - {srv_msg}") + self.__Logs.error(f"General Error: {err} - {srv_msg}") def on_server_ping(self, serverMsg: list[str]) -> None: """Send a PONG message to the server @@ -999,7 +1012,7 @@ class Unrealircd6: return None except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_version(self, serverMsg: list[str]) -> None: """Sending Server Version to the server @@ -1011,7 +1024,7 @@ class Unrealircd6: # Réponse a un CTCP VERSION try: - nickname = self.__Irc.User.get_nickname(self.__Base.clean_uid(serverMsg[1])) + nickname = self.__Irc.User.get_nickname(self.__Utils.clean_uid(serverMsg[1])) dnickname = self.__Config.SERVICE_NICKNAME arg = serverMsg[4].replace(':', '') @@ -1023,7 +1036,7 @@ class Unrealircd6: return None except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_time(self, serverMsg: list[str]) -> None: """Sending TIME answer to a requestor @@ -1035,10 +1048,10 @@ class Unrealircd6: # Réponse a un CTCP VERSION try: - nickname = self.__Irc.User.get_nickname(self.__Base.clean_uid(serverMsg[1])) + nickname = self.__Irc.User.get_nickname(self.__Utils.clean_uid(serverMsg[1])) dnickname = self.__Config.SERVICE_NICKNAME arg = serverMsg[4].replace(':', '') - current_datetime = self.__Base.get_datetime() + current_datetime = self.__Utils.get_sdatetime() if nickname is None: return None @@ -1048,7 +1061,7 @@ class Unrealircd6: return None except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_ping(self, serverMsg: list[str]) -> None: """Sending a PING answer to requestor @@ -1060,7 +1073,7 @@ class Unrealircd6: # Réponse a un CTCP VERSION try: - nickname = self.__Irc.User.get_nickname(self.__Base.clean_uid(serverMsg[1])) + nickname = self.__Irc.User.get_nickname(self.__Utils.clean_uid(serverMsg[1])) dnickname = self.__Config.SERVICE_NICKNAME arg = serverMsg[4].replace(':', '') @@ -1069,7 +1082,7 @@ class Unrealircd6: if arg == '\x01PING': recieved_unixtime = int(serverMsg[5].replace('\x01','')) - current_unixtime = self.__Base.get_unixtime() + current_unixtime = self.__Utils.get_unixtime() ping_response = current_unixtime - recieved_unixtime # self.__Irc.send2socket(f':{dnickname} NOTICE {nickname} :\x01PING {ping_response} secs\x01') @@ -1081,7 +1094,7 @@ class Unrealircd6: return None except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") def on_version_msg(self, serverMsg: list[str]) -> None: """Handle version coming from the server @@ -1095,7 +1108,7 @@ class Unrealircd6: if '@' in list(serverMsg_copy[0])[0]: serverMsg_copy.pop(0) - getUser = self.__Irc.User.get_User(self.__Irc.User.clean_uid(serverMsg_copy[0])) + getUser = self.__Irc.User.get_User(self.__Utils.clean_uid(serverMsg_copy[0])) if getUser is None: return None @@ -1113,4 +1126,4 @@ class Unrealircd6: return None except Exception as err: - self.__Base.logs.error(f"{__name__} - General Error: {err}") + self.__Logs.error(f"{__name__} - General Error: {err}") diff --git a/core/classes/reputation.py b/core/classes/reputation.py index 64a2b27..859f62c 100644 --- a/core/classes/reputation.py +++ b/core/classes/reputation.py @@ -1,23 +1,23 @@ -from typing import Union +from typing import TYPE_CHECKING, Optional from core.definition import MReputation -from core.base import Base + +if TYPE_CHECKING: + from core.loader import Loader class Reputation: UID_REPUTATION_DB: list[MReputation] = [] - def __init__(self, baseObj: Base) -> None: + def __init__(self, loader: 'Loader'): - self.Logs = baseObj.logs + self.Logs = loader.Logs self.MReputation: MReputation = MReputation - return None - - def insert(self, newReputationUser: MReputation) -> bool: + def insert(self, new_reputation_user: MReputation) -> bool: """Insert a new Reputation User object Args: - newReputationUser (MReputation): New Reputation Model object + new_reputation_user (MReputation): New Reputation Model object Returns: bool: True if inserted @@ -26,23 +26,23 @@ class Reputation: exist = False for record in self.UID_REPUTATION_DB: - if record.uid == newReputationUser.uid: + if record.uid == new_reputation_user.uid: # If the user exist then return False and do not go further exist = True self.Logs.debug(f'{record.uid} already exist') return result if not exist: - self.UID_REPUTATION_DB.append(newReputationUser) + self.UID_REPUTATION_DB.append(new_reputation_user) result = True - self.Logs.debug(f'New Reputation User Captured: ({newReputationUser})') + self.Logs.debug(f'New Reputation User Captured: ({new_reputation_user})') if not result: - self.Logs.critical(f'The Reputation User Object was not inserted {newReputationUser}') + self.Logs.critical(f'The Reputation User Object was not inserted {new_reputation_user}') return result - def update(self, uid: str, newNickname: str) -> bool: + def update(self, uid: str, new_nickname: str) -> bool: """Update the nickname starting from the UID Args: @@ -53,12 +53,12 @@ class Reputation: bool: True if updated """ - reputationObj = self.get_Reputation(uid) + reputation_obj = self.get_Reputation(uid) - if reputationObj is None: + if reputation_obj is None: return False - reputationObj.nickname = newNickname + reputation_obj.nickname = new_nickname return True @@ -89,7 +89,7 @@ class Reputation: return result - def get_Reputation(self, uidornickname: str) -> Union[MReputation, None]: + def get_Reputation(self, uidornickname: str) -> Optional[MReputation]: """Get The User Object model Args: @@ -98,16 +98,15 @@ class Reputation: Returns: UserModel|None: The UserModel Object | None """ - User = None for record in self.UID_REPUTATION_DB: if record.uid == uidornickname: - User = record + return record elif record.nickname == uidornickname: - User = record + return record - return User + return None - def get_uid(self, uidornickname:str) -> Union[str, None]: + def get_uid(self, uidornickname: str) -> Optional[str]: """Get the UID of the user starting from the UID or the Nickname Args: @@ -117,14 +116,14 @@ class Reputation: str|None: Return the UID """ - reputationObj = self.get_Reputation(uidornickname) + reputation_obj = self.get_Reputation(uidornickname) - if reputationObj is None: + if reputation_obj is None: return None - return reputationObj.uid + return reputation_obj.uid - def get_nickname(self, uidornickname:str) -> Union[str, None]: + def get_nickname(self, uidornickname: str) -> Optional[str]: """Get the Nickname starting from UID or the nickname Args: @@ -133,12 +132,12 @@ class Reputation: Returns: str|None: the nickname """ - reputationObj = self.get_Reputation(uidornickname) + reputation_obj = self.get_Reputation(uidornickname) - if reputationObj is None: + if reputation_obj is None: return None - return reputationObj.nickname + return reputation_obj.nickname def is_exist(self, uidornickname: str) -> bool: """Check if the UID or the nickname exist in the reputation DB @@ -150,9 +149,9 @@ class Reputation: bool: True if exist """ - reputationObj = self.get_Reputation(uidornickname) + reputation_obj = self.get_Reputation(uidornickname) - if reputationObj is None: - return False - else: + if isinstance(reputation_obj, MReputation): return True + + return False diff --git a/core/classes/settings.py b/core/classes/settings.py index 5132a45..57d5d13 100644 --- a/core/classes/settings.py +++ b/core/classes/settings.py @@ -1,7 +1,14 @@ +'''This class should never be reloaded. +''' from threading import Timer, Thread, RLock from socket import socket +from typing import Any, Optional class Settings: + """This Class will never be reloaded. + Means that the variables are available during + the whole life of the app + """ RUNNING_TIMERS: list[Timer] = [] RUNNING_THREADS: list[Thread] = [] @@ -13,3 +20,30 @@ class Settings: PROTOCTL_USER_MODES: list[str] = [] PROTOCTL_PREFIX: list[str] = [] + + __CACHE: dict[str, Any] = {} + """Use set_cache or get_cache instead""" + + def set_cache(self, key: str, value_to_cache: Any): + """When you want to store a variable + + Ex. + ```python + set_cache('MY_KEY', {'key1': 'value1', 'key2', 'value2'}) + ``` + Args: + key (str): The key you want to add. + value_to_cache (Any): The Value you want to store. + """ + self.__CACHE[key] = value_to_cache + + def get_cache(self, key) -> Optional[Any]: + """It returns the value associated to the key and finally it removes the entry""" + if self.__CACHE.get(key): + return self.__CACHE.pop(key) + + return None + + def get_cache_size(self) -> int: + return len(self.__CACHE) + \ No newline at end of file diff --git a/core/classes/user.py b/core/classes/user.py index 17451de..379a851 100644 --- a/core/classes/user.py +++ b/core/classes/user.py @@ -1,23 +1,21 @@ from re import sub -from typing import Union, TYPE_CHECKING -from dataclasses import asdict +from typing import Any, Optional, TYPE_CHECKING +from datetime import datetime if TYPE_CHECKING: - from core.base import Base + from core.loader import Loader from core.definition import MUser class User: UID_DB: list['MUser'] = [] - def __init__(self, baseObj: 'Base') -> None: + def __init__(self, loader: 'Loader'): - self.Logs = baseObj.logs - self.Base = baseObj + self.Logs = loader.Logs + self.Base = loader.Base - return None - - def insert(self, newUser: 'MUser') -> bool: + def insert(self, new_user: 'MUser') -> bool: """Insert a new User object Args: @@ -27,32 +25,32 @@ class User: bool: True if inserted """ - userObj = self.get_User(newUser.uid) + user_obj = self.get_User(new_user.uid) - if not userObj is None: + if not user_obj is None: # User already created return False return False - self.UID_DB.append(newUser) + self.UID_DB.append(new_user) return True - def update_nickname(self, uid: str, newNickname: str) -> bool: + def update_nickname(self, uid: str, new_nickname: str) -> bool: """Update the nickname starting from the UID Args: uid (str): UID of the user - newNickname (str): New nickname + new_nickname (str): New nickname Returns: bool: True if updated """ - userObj = self.get_User(uidornickname=uid) + user_obj = self.get_User(uidornickname=uid) - if userObj is None: + if user_obj is None: return False - userObj.nickname = newNickname + user_obj.nickname = new_nickname return True @@ -67,16 +65,16 @@ class User: bool: True if user mode has been updaed """ response = True - userObj = self.get_User(uidornickname=uidornickname) + user_obj = self.get_User(uidornickname=uidornickname) - if userObj is None: + if user_obj is None: return False action = modes[0] new_modes = modes[1:] - existing_umodes = userObj.umodes - umodes = userObj.umodes + existing_umodes = user_obj.umodes + umodes = user_obj.umodes if action == '+': @@ -95,7 +93,7 @@ class User: final_umodes_liste = [x for x in self.Base.Settings.PROTOCTL_USER_MODES if x in liste_umodes] final_umodes = ''.join(final_umodes_liste) - userObj.umodes = f"+{final_umodes}" + user_obj.umodes = f"+{final_umodes}" return response @@ -109,16 +107,16 @@ class User: bool: True if deleted """ - userObj = self.get_User(uidornickname=uid) + user_obj = self.get_User(uidornickname=uid) - if userObj is None: + if user_obj is None: return False - self.UID_DB.remove(userObj) + self.UID_DB.remove(user_obj) return True - def get_User(self, uidornickname: str) -> Union['MUser', None]: + def get_User(self, uidornickname: str) -> Optional['MUser']: """Get The User Object model Args: @@ -127,16 +125,15 @@ class User: Returns: UserModel|None: The UserModel Object | None """ - User = None for record in self.UID_DB: if record.uid == uidornickname: - User = record + return record elif record.nickname == uidornickname: - User = record + return record - return User + return None - def get_uid(self, uidornickname:str) -> Union[str, None]: + def get_uid(self, uidornickname:str) -> Optional[str]: """Get the UID of the user starting from the UID or the Nickname Args: @@ -146,14 +143,14 @@ class User: str|None: Return the UID """ - userObj = self.get_User(uidornickname=uidornickname) + user_obj = self.get_User(uidornickname=uidornickname) - if userObj is None: + if user_obj is None: return None - return userObj.uid + return user_obj.uid - def get_nickname(self, uidornickname:str) -> Union[str, None]: + def get_nickname(self, uidornickname:str) -> Optional[str]: """Get the Nickname starting from UID or the nickname Args: @@ -162,14 +159,14 @@ class User: Returns: str|None: the nickname """ - userObj = self.get_User(uidornickname=uidornickname) + user_obj = self.get_User(uidornickname=uidornickname) - if userObj is None: + if user_obj is None: return None - return userObj.nickname + return user_obj.nickname - def get_User_AsDict(self, uidornickname: str) -> Union[dict[str, any], None]: + def get_user_asdict(self, uidornickname: str) -> Optional[dict[str, Any]]: """Transform User Object to a dictionary Args: @@ -178,12 +175,12 @@ class User: Returns: Union[dict[str, any], None]: User Object as a dictionary or None """ - userObj = self.get_User(uidornickname=uidornickname) + user_obj = self.get_User(uidornickname=uidornickname) - if userObj is None: + if user_obj is None: return None - return asdict(userObj) + return user_obj.to_dict() def is_exist(self, uidornikname: str) -> bool: """Check if the UID or the nickname exist in the USER DB @@ -194,14 +191,14 @@ class User: Returns: bool: True if exist """ - userObj = self.get_User(uidornickname=uidornikname) + user_obj = self.get_User(uidornickname=uidornikname) - if userObj is None: + if user_obj is None: return False return True - def clean_uid(self, uid: str) -> Union[str, None]: + def clean_uid(self, uid: str) -> Optional[str]: """Clean UID by removing @ / % / + / ~ / * / : Args: @@ -217,4 +214,35 @@ class User: if not parsed_UID: return None - return parsed_UID \ No newline at end of file + return parsed_UID + + def get_user_uptime_in_minutes(self, uidornickname: str) -> float: + """Retourne depuis quand l'utilisateur est connecté (in minutes). + + Args: + uid (str): The uid or le nickname + + Returns: + int: How long in minutes has the user been connected? + """ + + get_user = self.get_User(uidornickname) + if get_user is None: + return 0 + + # Convertir la date enregistrée dans UID_DB en un objet {datetime} + connected_time_string = get_user.connexion_datetime + + if isinstance(connected_time_string, datetime): + connected_time = connected_time_string + else: + connected_time = datetime.strptime(connected_time_string, "%Y-%m-%d %H:%M:%S.%f") + + # What time is it ? + current_datetime = datetime.now() + + uptime = current_datetime - connected_time + convert_to_minutes = uptime.seconds / 60 + uptime_minutes = round(number=convert_to_minutes, ndigits=2) + + return uptime_minutes diff --git a/core/definition.py b/core/definition.py index fea388e..0398c50 100644 --- a/core/definition.py +++ b/core/definition.py @@ -1,10 +1,26 @@ from datetime import datetime -from dataclasses import dataclass, field -from typing import Literal +from json import dumps +from dataclasses import dataclass, field, asdict, fields +from typing import Literal, Any from os import sep @dataclass -class MClient: +class MainModel: + """Parent Model contains important methods""" + def to_dict(self) -> dict[str, Any]: + """Return the fields of a dataclass instance as a new dictionary mapping field names to field values.""" + return asdict(self) + + def to_json(self) -> str: + """Return the object of a dataclass a json str.""" + return dumps(self.to_dict()) + + def get_attributes(self) -> list[str]: + """Return a list of attributes name""" + return [f.name for f in fields(self)] + +@dataclass +class MClient(MainModel): """Model Client for registred nickname""" uid: str = None account: str = None @@ -22,7 +38,7 @@ class MClient: connexion_datetime: datetime = field(default=datetime.now()) @dataclass -class MUser: +class MUser(MainModel): """Model User""" uid: str = None @@ -40,7 +56,7 @@ class MUser: connexion_datetime: datetime = field(default=datetime.now()) @dataclass -class MAdmin: +class MAdmin(MainModel): """Model Admin""" uid: str = None @@ -59,7 +75,7 @@ class MAdmin: level: int = 0 @dataclass -class MReputation: +class MReputation(MainModel): """Model Reputation""" uid: str = None nickname: str = None @@ -77,7 +93,7 @@ class MReputation: secret_code: str = None @dataclass -class MChannel: +class MChannel(MainModel): """Model Channel""" name: str = None @@ -92,7 +108,7 @@ class MChannel: """ @dataclass -class ColorModel: +class ColorModel(MainModel): white: str = "\x0300" black: str = "\x0301" blue: str = "\x0302" @@ -104,7 +120,7 @@ class ColorModel: underline: str = "\x1F" @dataclass -class MConfig: +class MConfig(MainModel): """Model Configuration""" SERVEUR_IP: str = "127.0.0.1" @@ -305,16 +321,8 @@ class MConfig: """0: utf-8 | 1: iso-8859-1""" @dataclass -class MClone: - """Model Clone""" - connected: bool = False - uid: str = None - nickname: str = None - username: str = None - realname: str = None - channels: list = field(default_factory=list) - vhost: str = None - hostname: str = 'localhost' - umodes: str = None - remote_ip: str = '127.0.0.1' - group: str = 'Default' +class MCommand(MainModel): + module_name: str = None + command_name: str = None + description: str = None + command_level: int = 0 diff --git a/core/installation.py b/core/installation.py index c7ae319..f0c14b2 100644 --- a/core/installation.py +++ b/core/installation.py @@ -261,21 +261,21 @@ class Install: if not do_install: return None - print("===> Vider le cache de pip") + print("===> Clean pip cache") self.run_subprocess([self.config.venv_pip_executable, 'cache', 'purge']) - print("===> Verifier si pip est a jour") + print("===> Check if pip is up to date") self.run_subprocess([self.config.venv_python_executable, '-m', 'pip', 'install', '--upgrade', 'pip']) if not self.check_package('greenlet'): self.run_subprocess([self.config.venv_pip_executable, 'install', '--only-binary', ':all:', 'greenlet']) - print('====> Module Greenlet installé') + print('====> Greenlet installed') for module in self.config.venv_cmd_requirements: if not self.check_package(module): print("### Trying to install missing python packages ###") self.run_subprocess([self.config.venv_pip_executable, 'install', module]) - print(f"====> Module {module} installé") + print(f"====> Module {module} installed!") else: print(f"==> {module} already installed") @@ -307,8 +307,8 @@ WantedBy=default.target with open(full_service_file_path, 'w+') as servicefile: servicefile.write(contain) servicefile.close() - print(f'Service file generated with current configuration') - print(f'Running Defender IRC Service ...') + print('Service file generated with current configuration') + print('Running IRC Service ...') self.run_subprocess(self.config.service_cmd_daemon_reload) self.run_subprocess(self.config.service_cmd_executable) @@ -316,8 +316,8 @@ WantedBy=default.target with open(full_service_file_path, 'w+') as servicefile: servicefile.write(contain) servicefile.close() - print(f'Service file generated with current configuration') - print(f'Running Defender IRC Service ...') + print('Service file generated with current configuration') + print('Running IRC Service ...') self.run_subprocess(self.config.service_cmd_daemon_reload) self.run_subprocess(self.config.service_cmd_executable) diff --git a/core/irc.py b/core/irc.py index f2ef8ff..94f5b91 100644 --- a/core/irc.py +++ b/core/irc.py @@ -8,9 +8,10 @@ import time import traceback from ssl import SSLSocket from datetime import datetime, timedelta -from typing import Union +from typing import Optional, Union from core.loader import Loader from core.classes.protocol import Protocol +from core.classes.commands import Command class Irc: _instance = None @@ -30,6 +31,9 @@ class Irc: # Load the configuration self.Config = self.Loader.Config + # Load Main utils functions + self.Utils = self.Loader.Utils + # Date et heure de la premiere connexion de Defender self.defender_connexion_datetime = self.Config.DEFENDER_CONNEXION_DATETIME @@ -50,7 +54,7 @@ class Irc: self.Base = self.Loader.Base # Logger - self.Logs = self.Loader.Base.logs + self.Logs = self.Loader.Logs # Get Settings. self.Settings = self.Base.Settings @@ -67,9 +71,6 @@ class Irc: # Use Channel Instance self.Channel = self.Loader.Channel - # Use Clones Instance - self.Clone = self.Loader.Clone - # Use Reputation Instance self.Reputation = self.Loader.Reputation @@ -83,7 +84,11 @@ class Irc: self.first_connexion_ip: str = None # Define the dict that will contain all loaded modules - self.loaded_classes:dict[str, 'Irc'] = {} # Definir la variable qui contiendra la liste modules chargés + self.loaded_classes:dict[str, 'Irc'] = {} + + # Load Commands Utils + self.Commands = self.Loader.Commands + """Command utils""" # Global full module commands that contains level, module name, commands and description self.module_commands: dict[int, dict[str, dict[str, str]]] = {} @@ -122,7 +127,7 @@ class Irc: # Define the IrcSocket object - self.IrcSocket:Union[socket.socket, SSLSocket] = None + self.IrcSocket: Union[socket.socket, SSLSocket] = None self.__create_table() self.Base.create_thread(func=self.heartbeat, func_args=(self.beat, )) @@ -140,6 +145,7 @@ class Irc: self.init_service_user() self.__create_socket() self.__connect_to_irc(ircInstance) + except AssertionError as ae: self.Logs.critical(f'Assertion error: {ae}') @@ -192,6 +198,7 @@ class Irc: self.Logs.critical(f"AttributeError: {ae} - {soc.fileno()}") def __ssl_context(self) -> ssl.SSLContext: + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE @@ -209,7 +216,7 @@ class Irc: protocol=self.Config.SERVEUR_PROTOCOL, ircInstance=self.ircObject ).Protocol - self.Protocol.link() # Etablir le link en fonction du protocol choisi + self.Protocol.send_link() # Etablir le link en fonction du protocol choisi self.signal = True # Une variable pour initier la boucle infinie self.__join_saved_channels() # Join existing channels self.load_existing_modules() # Charger les modules existant dans la base de données @@ -226,14 +233,15 @@ class Irc: self.Logs.warning('--* Waiting for socket to close ...') # Reload configuration + self.Loader.Logs = self.Loader.LoggingModule.ServiceLogging().get_logger() self.Logs.debug('Reloading configuration') - self.Config = self.Loader.ConfModule.Configuration().ConfigObject - self.Base = self.Loader.BaseModule.Base(self.Config, self.Settings) + self.Config = self.Loader.ConfModule.Configuration(self.Logs).ConfigObject + self.Base = self.Loader.BaseModule.Base(self.Loader) self.Protocol = Protocol(self.Config.SERVEUR_PROTOCOL, ircInstance).Protocol self.init_service_user() self.__create_socket() - self.Protocol.link() + self.Protocol.send_link() self.__join_saved_channels() self.load_existing_modules() self.Config.DEFENDER_RESTART = 0 @@ -295,7 +303,7 @@ class Irc: if result_query: for chan_name in result_query: chan = chan_name[0] - self.Protocol.sjoin(channel=chan) + self.Protocol.send_sjoin(channel=chan) def send_response(self, responses:list[bytes]) -> None: try: @@ -341,12 +349,43 @@ class Irc: self.module_commands.setdefault(level, {}).setdefault(module_name, {}).update({command_name: command_description}) self.module_commands_list.append(command_name) + # Build Model. + self.Commands.build(self.Loader.Definition.MCommand(module_name, command_name, command_description, level)) + return None - def generate_help_menu(self, nickname: str) -> None: + def generate_help_menu(self, nickname: str, module: Optional[str] = None) -> None: # Check if the nickname is an admin - admin_obj = self.Admin.get_Admin(nickname) + p = self.Protocol + admin_obj = self.Admin.get_admin(nickname) + dnickname = self.Config.SERVICE_NICKNAME + color_nogc = self.Config.COLORS.nogc + color_black = self.Config.COLORS.black + current_level = 0 + + if admin_obj is not None: + current_level = admin_obj.level + + p.send_notice(nick_from=dnickname,nick_to=nickname, msg=f" ***************** LISTE DES COMMANDES *****************") + header = f" {'Level':<8}| {'Command':<25}| {'Module':<15}| {'Description':<35}" + line = "-"*75 + p.send_notice(nick_from=dnickname,nick_to=nickname, msg=header) + p.send_notice(nick_from=dnickname,nick_to=nickname, msg=f" {line}") + for cmd in self.Commands.get_commands_by_level(current_level): + if module is None or cmd.module_name.lower() == module.lower(): + p.send_notice( + nick_from=dnickname, + nick_to=nickname, + msg=f" {color_black}{cmd.command_level:<8}{color_nogc}| {cmd.command_name:<25}| {cmd.module_name:<15}| {cmd.description:<35}" + ) + + return None + + def generate_help_menu_bakcup(self, nickname: str) -> None: + + # Check if the nickname is an admin + admin_obj = self.Admin.get_admin(nickname) dnickname = self.Config.SERVICE_NICKNAME color_bold = self.Config.COLORS.bold color_nogc = self.Config.COLORS.nogc @@ -380,7 +419,7 @@ class Irc: def is_cmd_allowed(self, nickname: str, command_name: str) -> bool: - admin_obj = self.Admin.get_Admin(nickname) + admin_obj = self.Admin.get_admin(nickname) current_level = 0 if admin_obj is not None: @@ -494,16 +533,15 @@ class Irc: try: # module_name : mod_voice module_name = module_name.lower() - class_name = module_name.split('_')[1].capitalize() # ==> Voice + module_folder = module_name.split('_')[1].lower() # ==> voice + class_name = module_name.split('_')[1].capitalize() # ==> Voice - # print(self.loaded_classes) - - # Si le module est déja chargé - if 'mods.' + module_name in sys.modules: - self.Logs.info("Module déja chargé ...") - self.Logs.info('module name = ' + module_name) + # Check if the module is already loaded. + if 'mods.' + module_folder + '.' + module_name in sys.modules: + self.Logs.debug(f"Module [{module_folder}.{module_name}] already loaded!") if class_name in self.loaded_classes: # Si le module existe dans la variable globale retourne False + self.Logs.debug(f"Module [{module_folder}.{module_name}] exist in the local variable!") self.Protocol.send_priv_msg( nick_from=self.Config.SERVICE_NICKNAME, msg=f"Le module {module_name} est déja chargé ! si vous souhaiter le recharge tapez {self.Config.SERVICE_PREFIX}reload {module_name}", @@ -511,7 +549,7 @@ class Irc: ) return False - the_module = sys.modules['mods.' + module_name] + the_module = sys.modules[f'mods.{module_folder}.{module_name}'] importlib.reload(the_module) my_class = getattr(the_module, class_name, None) new_instance = my_class(self.ircObject) @@ -526,11 +564,11 @@ class Irc: msg=f"Module {module_name} chargé", channel=self.Config.SERVICE_CHANLOG ) - return False + self.Logs.debug(f"Module [{module_folder}.{module_name}] reloaded!") + return True # Charger le module - loaded_module = importlib.import_module(f"mods.{module_name}") - + loaded_module = importlib.import_module(f'mods.{module_folder}.{module_name}') my_class = getattr(loaded_module, class_name, None) # Récuperer le nom de classe create_instance_of_the_class = my_class(self.ircObject) # Créer une nouvelle instance de la classe @@ -557,7 +595,7 @@ class Irc: channel=self.Config.SERVICE_CHANLOG ) - self.Logs.info(f"Module {class_name} has been loaded") + self.Logs.debug(f"Module {class_name} has been loaded") return True @@ -565,18 +603,21 @@ class Irc: self.Logs.error(f"MODULE_NOT_FOUND: {moduleNotFound}") self.Protocol.send_priv_msg( nick_from=self.Config.SERVICE_NICKNAME, - msg=f"[ {self.Config.COLORS.red}MODULE_NOT_FOUND{self.Config.COLORS.black} ]: {moduleNotFound}", + msg=f"[ {self.Config.COLORS.red}MODULE ERROR{self.Config.COLORS.black} ]: {moduleNotFound}", channel=self.Config.SERVICE_CHANLOG ) self.Base.db_delete_module(module_name) + return False + except Exception as err: - self.Logs.error(f"Something went wrong with a module you want to load : {err}") + self.Logs.error(f"[LOAD MODULE ERROR]: {err}", exc_info=True) self.Protocol.send_priv_msg( nick_from=self.Config.SERVICE_NICKNAME, - msg=f"[ {self.Config.COLORS.red}ERROR{self.Config.COLORS.black} ]: {err}", + msg=f"[ {self.Config.COLORS.red}MODULE ERROR{self.Config.COLORS.black} ]: {err}", channel=self.Config.SERVICE_CHANLOG ) self.Base.db_delete_module(module_name) + return False def unload_module(self, mod_name: str) -> bool: """Unload a module @@ -588,21 +629,28 @@ class Irc: bool: True if success """ try: - module_name = mod_name.lower() # Le nom du module. exemple: mod_defender - class_name = module_name.split('_')[1].capitalize() # Nom de la class. exemple: Defender + # Le nom du module. exemple: mod_defender + module_name = mod_name.lower() + module_folder = module_name.split('_')[1].lower() # ==> defender + class_name = module_name.split('_')[1].capitalize() # Nom de la class. exemple: Defender if class_name in self.loaded_classes: self.loaded_classes[class_name].unload() del self.loaded_classes[class_name] + # Delete from the sys. + if sys.modules.get(f'{module_folder}.{module_name}'): + del sys.modules[f"{module_folder}.{module_name}"] + # Supprimer le module de la base de données self.Base.db_delete_module(module_name) self.Protocol.send_priv_msg( nick_from=self.Config.SERVICE_NICKNAME, - msg=f"Module {module_name} supprimé", + msg=f"[ MODULE INFO ] Module {module_name} has been deleted!", channel=self.Config.SERVICE_CHANLOG ) + self.Logs.debug(f"[ MODULE ] {module_name} has been deleted!") return True except Exception as err: @@ -612,13 +660,17 @@ class Irc: def reload_module(self, from_user: str, mod_name: str) -> bool: try: module_name = mod_name.lower() # ==> mod_defender + module_folder = module_name.split('_')[1].lower() # ==> defender class_name = module_name.split('_')[1].capitalize() # ==> Defender - if 'mods.' + module_name in sys.modules: + if f'mods.{module_folder}.{module_name}' in sys.modules: self.Logs.info('Unload the module ...') self.loaded_classes[class_name].unload() self.Logs.info('Module Already Loaded ... reloading the module ...') - the_module = sys.modules['mods.' + module_name] + + # Load dependencies + self.Base.reload_modules_with_dependencies(f'mods.{module_folder}') + the_module = sys.modules[f'mods.{module_folder}.{module_name}'] importlib.reload(the_module) # Supprimer la class déja instancier @@ -681,7 +733,7 @@ class Irc: if self.User.get_User(uid) is None: return None - getUser = self.User.get_User_AsDict(uid) + getUser = self.User.get_user_asdict(uid) level = int(level) @@ -696,7 +748,7 @@ class Irc: def delete_db_admin(self, uid:str) -> None: - if self.Admin.get_Admin(uid) is None: + if self.Admin.get_admin(uid) is None: return None if not self.Admin.delete(uid): @@ -732,7 +784,7 @@ class Irc: hostname = get_user.hostname vhost = get_user.vhost - spassword = self.Base.crypt_password(password) + spassword = self.Loader.Utils.hash_password(password) mes_donnees = {'admin': nickname} query_search_user = f"SELECT id FROM {self.Config.TABLE_ADMIN} WHERE user=:admin" @@ -741,7 +793,7 @@ class Irc: # On verifie si le user exist dans la base if not exist_user: - mes_donnees = {'datetime': self.Base.get_datetime(), 'user': nickname, 'password': spassword, 'hostname': hostname, 'vhost': vhost, 'level': level} + mes_donnees = {'datetime': self.Utils.get_sdatetime(), 'user': nickname, 'password': spassword, 'hostname': hostname, 'vhost': vhost, 'level': level} self.Base.db_execute_query(f'''INSERT INTO {self.Config.TABLE_ADMIN} (createdOn, user, password, hostname, vhost, level) VALUES (:datetime, :user, :password, :hostname, :vhost, :level) @@ -763,7 +815,7 @@ class Irc: log_msg (str): the message to log """ try: - mes_donnees = {'datetime': self.Base.get_datetime(), 'server_msg': log_msg} + mes_donnees = {'datetime': self.Utils.get_sdatetime(), 'server_msg': log_msg} self.Base.db_execute_query(f'INSERT INTO {self.Config.TABLE_LOG} (datetime, server_msg) VALUES (:datetime, :server_msg)', mes_donnees) return None @@ -813,16 +865,13 @@ class Irc: case 'PING': self.Protocol.on_server_ping(serverMsg=original_response) - self.Logs.debug(f"** handle {parsed_protocol}") return None case 'SJOIN': self.Protocol.on_sjoin(serverMsg=original_response) - self.Logs.debug(f"** handle {parsed_protocol}") case 'EOS': self.Protocol.on_eos(serverMsg=original_response) - self.Logs.debug(f"** handle {parsed_protocol}") case 'UID': try: @@ -831,48 +880,37 @@ class Irc: for classe_name, classe_object in self.loaded_classes.items(): classe_object.cmd(original_response) - self.Logs.debug(f"** handle {parsed_protocol}") - except Exception as err: self.Logs.error(f'General Error: {err}') case 'QUIT': self.Protocol.on_quit(serverMsg=original_response) - self.Logs.debug(f"** handle {parsed_protocol}") case 'PROTOCTL': self.Protocol.on_protoctl(serverMsg=original_response) - self.Logs.debug(f"** handle {parsed_protocol}") case 'SVS2MODE': # >> [':00BAAAAAG', 'SVS2MODE', '001U01R03', '-r'] self.Protocol.on_svs2mode(serverMsg=original_response) - self.Logs.debug(f"** handle {parsed_protocol}") case 'SQUIT': self.Protocol.on_squit(serverMsg=original_response) - self.Logs.debug(f"** handle {parsed_protocol}") case 'PART': self.Protocol.on_part(serverMsg=parsed_protocol) - self.Logs.debug(f"** handle {parsed_protocol}") case 'VERSION': self.Protocol.on_version_msg(serverMsg=original_response) - self.Logs.debug(f"** handle {parsed_protocol}") case 'UMODE2': # [':adator_', 'UMODE2', '-i'] self.Protocol.on_umode2(serverMsg=original_response) - self.Logs.debug(f"** handle {parsed_protocol}") case 'NICK': self.Protocol.on_nick(serverMsg=original_response) - self.Logs.debug(f"** handle {parsed_protocol}") case 'REPUTATION': self.Protocol.on_reputation(serverMsg=original_response) - self.Logs.debug(f"** handle {parsed_protocol}") case 'SLOG': # TODO self.Logs.debug(f"** handle {parsed_protocol}") @@ -882,7 +920,6 @@ class Irc: case 'PRIVMSG': self.Protocol.on_privmsg(serverMsg=original_response) - self.Logs.debug(f"** handle {parsed_protocol}") case 'PONG': # TODO self.Logs.debug(f"** handle {parsed_protocol}") @@ -910,10 +947,9 @@ class Irc: classe_object.cmd(original_response) except IndexError as ie: - self.Logs.error(f"{ie} / {original_response} / length {str(len(original_response))}") + self.Logs.error(f"IndexError: {ie}") except Exception as err: - self.Logs.error(f"General Error: {err}") - self.Logs.error(f"General Error: {traceback.format_exc()}") + self.Logs.error(f"General Error: {err}", exc_info=True) def hcmds(self, user: str, channel: Union[str, None], cmd: list, fullcmd: list = []) -> None: """Create @@ -1081,7 +1117,7 @@ class Irc: return False if not user_to_log is None: - mes_donnees = {'user': user_to_log, 'password': self.Base.crypt_password(password)} + mes_donnees = {'user': user_to_log, 'password': self.Loader.Utils.hash_password(password)} query = f"SELECT id, level FROM {self.Config.TABLE_ADMIN} WHERE user = :user AND password = :password" result = self.Base.db_execute_query(query, mes_donnees) user_from_db = result.fetchone() @@ -1134,9 +1170,9 @@ class Irc: return None user_to_edit = cmd[1] - user_password = self.Base.crypt_password(cmd[2]) + user_password = self.Loader.Utils.hash_password(cmd[2]) - get_admin = self.Admin.get_Admin(fromuser) + get_admin = self.Admin.get_admin(fromuser) if get_admin is None: self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"This user {fromuser} has no Admin access") return None @@ -1200,7 +1236,7 @@ class Irc: self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"{self.Config.SERVICE_PREFIX}delaccess [USER] [CONFIRMUSER]") return None - get_admin = self.Admin.get_Admin(fromuser) + get_admin = self.Admin.get_admin(fromuser) if get_admin is None: self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"This user {fromuser} has no admin access") @@ -1273,9 +1309,9 @@ class Irc: # If the account doesn't exist then insert into database data_to_record = { - 'createdOn': self.Base.get_datetime(), 'account': fromuser, + 'createdOn': self.Utils.get_sdatetime(), 'account': fromuser, 'nickname': user_obj.nickname, 'hostname': user_obj.hostname, 'vhost': user_obj.vhost, 'realname': user_obj.realname, 'email': email, - 'password': self.Base.crypt_password(password=password), 'level': 0 + 'password': self.Loader.Utils.hash_password(password=password), 'level': 0 } insert_to_db = self.Base.db_execute_query(f""" @@ -1310,7 +1346,7 @@ class Irc: return None account = str(cmd[1]) # account - encrypted_password = self.Base.crypt_password(cmd[2]) + encrypted_password = self.Loader.Utils.hash_password(cmd[2]) user_obj = self.User.get_User(fromuser) client_obj = self.Client.get_Client(user_obj.uid) @@ -1382,9 +1418,10 @@ class Irc: self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} {command.upper()} ") case 'help': - - self.generate_help_menu(nickname=fromuser) - + # Syntax. !help [module_name] + module_name = str(cmd[1]) if len(cmd) == 2 else None + self.generate_help_menu(nickname=fromuser, module=module_name) + case 'load': try: # Load a module ex: .load mod_defender @@ -1434,7 +1471,7 @@ class Irc: nick_to=fromuser, msg=f"Arrêt du service {dnickname}" ) - self.Protocol.squit(server_id=self.Config.SERVEUR_ID, server_link=self.Config.SERVEUR_LINK, reason=final_reason) + self.Protocol.send_squit(server_id=self.Config.SERVEUR_ID, server_link=self.Config.SERVEUR_LINK, reason=final_reason) self.Logs.info(f'Arrêt du server {dnickname}') self.Config.DEFENDER_RESTART = 0 self.signal = False @@ -1459,18 +1496,23 @@ class Irc: self.User.UID_DB.clear() # Clear User Object self.Channel.UID_CHANNEL_DB.clear() # Clear Channel Object - self.Base.delete_logger(self.Config.LOGGING_NAME) + self.Base.garbage_collector_thread() - self.Protocol.squit(server_id=self.Config.SERVEUR_ID, server_link=self.Config.SERVEUR_LINK, reason=final_reason) + self.Protocol.send_squit(server_id=self.Config.SERVEUR_ID, server_link=self.Config.SERVEUR_LINK, reason=final_reason) self.Logs.info(f'Redémarrage du server {dnickname}') self.loaded_classes.clear() self.Config.DEFENDER_RESTART = 1 # Set restart status to 1 saying that the service will restart self.Config.DEFENDER_INIT = 1 # set init to 1 saying that the service will be re initiated + self.Loader.ServiceLogging.remove_logger() case 'rehash': + self.Loader.ServiceLogging.remove_logger() + self.Loader.ServiceLogging = self.Loader.LoggingModule.ServiceLogging() + self.Loader.Logs = self.Loader.ServiceLogging.get_logger() + need_a_restart = ["SERVEUR_ID"] restart_flag = False - Config_bakcup = self.Config.__dict__.copy() + Config_bakcup = self.Config.to_dict().copy() serveur_id = self.Config.SERVEUR_ID service_nickname = self.Config.SERVICE_NICKNAME hsid = self.Config.HSID @@ -1490,7 +1532,7 @@ class Irc: importlib.reload(mod_definition) importlib.reload(mod_config) - self.Config = self.Loader.ConfModule.Configuration().ConfigObject + self.Config = self.Loader.ConfModule.Configuration(self.Loader.Logs).ConfigObject self.Config.HSID = hsid self.Config.DEFENDER_INIT = defender_init self.Config.DEFENDER_RESTART = defender_restart @@ -1500,7 +1542,7 @@ class Irc: importlib.reload(mod_base) conf_bkp_dict: dict = Config_bakcup - config_dict: dict = self.Config.__dict__ + config_dict: dict = self.Config.to_dict() for key, value in conf_bkp_dict.items(): if config_dict[key] != value and key != 'COLORS': @@ -1513,14 +1555,13 @@ class Irc: restart_flag = True if service_nickname != self.Config.SERVICE_NICKNAME: - self.Protocol.set_nick(self.Config.SERVICE_NICKNAME) + self.Protocol.send_set_nick(self.Config.SERVICE_NICKNAME) if restart_flag: self.Config.SERVEUR_ID = serveur_id self.Protocol.send_priv_msg(nick_from=self.Config.SERVICE_NICKNAME, msg='You need to restart defender !', channel=self.Config.SERVICE_CHANLOG) - self.Base.delete_logger(self.Config.LOGGING_NAME) - self.Base = self.Loader.BaseModule.Base(self.Config, self.Settings) + self.Base = self.Loader.BaseModule.Base(self.Loader) importlib.reload(mod_unreal6) importlib.reload(mod_protocol) diff --git a/core/loader.py b/core/loader.py index d897c04..6a75451 100644 --- a/core/loader.py +++ b/core/loader.py @@ -1,34 +1,45 @@ -from core.classes import user, admin, client, channel, clone, reputation, settings +from logging import Logger +from core.classes import user, admin, client, channel, reputation, settings, commands +import core.logs as logs import core.definition as df -import core.base as baseModule -import core.classes.config as confModule +import core.utils as utils +import core.base as base_module +import core.classes.config as conf_module class Loader: def __init__(self): - # Load Modules + # Load Main Modules self.Definition: df = df - self.ConfModule: confModule = confModule + self.ConfModule: conf_module = conf_module - self.BaseModule: baseModule = baseModule + self.BaseModule: base_module = base_module + + self.Utils: utils = utils + + self.LoggingModule: logs = logs # Load Classes - self.Settings: settings = settings.Settings() + self.ServiceLogging: logs.ServiceLogging = logs.ServiceLogging() - self.Config: df.MConfig = self.ConfModule.Configuration().ConfigObject + self.Logs: Logger = self.ServiceLogging.get_logger() - self.Base: baseModule.Base = self.BaseModule.Base(self.Config, self.Settings) + self.Settings: settings.Settings = settings.Settings() - self.User: user.User = user.User(self.Base) + self.Config: df.MConfig = self.ConfModule.Configuration(self.Logs).ConfigObject - self.Client: client.Client = client.Client(self.Base) + self.Base: base_module.Base = self.BaseModule.Base(self) - self.Admin: admin.Admin = admin.Admin(self.Base) + self.User: user.User = user.User(self) - self.Channel: channel.Channel = channel.Channel(self.Base) + self.Client: client.Client = client.Client(self) - self.Clone: clone.Clone = clone.Clone(self.Base) + self.Admin: admin.Admin = admin.Admin(self) - self.Reputation: reputation.Reputation = reputation.Reputation(self.Base) + self.Channel: channel.Channel = channel.Channel(self) + + self.Reputation: reputation.Reputation = reputation.Reputation(self) + + self.Commands: commands.Command = commands.Command(self) diff --git a/core/logs.py b/core/logs.py new file mode 100644 index 0000000..bef6ab2 --- /dev/null +++ b/core/logs.py @@ -0,0 +1,112 @@ +import logging +from os import path, makedirs, sep +from typing import Optional + +class ServiceLogging: + + def __init__(self, loggin_name: str = "defender"): + """Create the Logging object + """ + self.OS_SEP = sep + self.LOGGING_NAME = loggin_name + self.DEBUG_LEVEL, self.DEBUG_FILE_LEVEL, self.DEBUG_STDOUT_LEVEL = (10, 10, 10) + self.SERVER_PREFIX = None + self.LOGGING_CONSOLE = True + + self.LOG_FILTERS: list[str] = ['PING', f":{self.SERVER_PREFIX}auth", "['PASS'"] + + self.file_handler = None + self.stdout_handler = None + + self.logs: logging.Logger = self.start_log_system() + + def get_logger(self) -> logging.Logger: + + logs_obj: logging.Logger = self.logs + + return logs_obj + + def remove_logger(self, logger_name: Optional[str] = None) -> None: + + if logger_name is None: + logger_name = self.LOGGING_NAME + + # Récupérer le logger + logger = logging.getLogger(logger_name) + + # Retirer tous les gestionnaires du logger et les fermer + for handler in logger.handlers[:]: # Utiliser une copie de la liste + # print(handler) + logger.removeHandler(handler) + handler.close() + + # Supprimer le logger du dictionnaire global + logging.Logger.manager.loggerDict.pop(logger_name, None) + + return None + + def start_log_system(self) -> logging.Logger: + + os_sep = self.OS_SEP + logging_name = self.LOGGING_NAME + debug_level = self.DEBUG_LEVEL + debug_file_level = self.DEBUG_FILE_LEVEL + debug_stdout_level = self.DEBUG_STDOUT_LEVEL + + # Create folder if not available + logs_directory = f'logs{os_sep}' + if not path.exists(f'{logs_directory}'): + makedirs(logs_directory) + + # Init logs object + logs = logging.getLogger(logging_name) + logs.setLevel(debug_level) + + # Add Handlers + self.file_handler = logging.FileHandler(f'logs{os_sep}{logging_name}.log',encoding='UTF-8') + self.file_handler.setLevel(debug_file_level) + + self.stdout_handler = logging.StreamHandler() + self.stdout_handler.setLevel(debug_stdout_level) + + # Define log format + formatter = logging.Formatter( + fmt='%(asctime)s - %(levelname)s - %(message)s (%(filename)s:%(funcName)s:%(lineno)d)', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + # Apply log format + self.file_handler.setFormatter(formatter) + self.stdout_handler.setFormatter(formatter) + + # Add handler to logs + logs.addHandler(self.file_handler) + logs.addHandler(self.stdout_handler) + + # Apply the filter + logs.addFilter(self.replace_filter) + + logs.info(f'#################### STARTING {self.LOGGING_NAME} ####################') + + return logs + + def set_stdout_handler_level(self, level: int) -> None: + self.stdout_handler.setLevel(level) + + def set_file_handler_level(self, level: int) -> None: + self.file_handler.setLevel(level) + + def replace_filter(self, record: logging.LogRecord) -> bool: + + response = True + filter: list[str] = ['PING', f":{self.SERVER_PREFIX}auth", "['PASS'"] + + # record.msg = record.getMessage().replace("PING", "[REDACTED]") + # if self.LOGGING_CONSOLE: + # print(record.getMessage()) + + for f in filter: + if f in record.getMessage(): + response = False + + return response # Retourne True pour permettre l'affichage du message diff --git a/core/utils.py b/core/utils.py index 9289f13..b89551d 100644 --- a/core/utils.py +++ b/core/utils.py @@ -1,25 +1,29 @@ -from typing import Literal, Union -from datetime import datetime +''' +Main utils library. +''' +import gc +from pathlib import Path +from re import sub +from typing import Literal, Optional, Any +from datetime import datetime, timedelta, timezone from time import time from random import choice from hashlib import md5, sha3_512 -def convert_to_int(value: any) -> Union[int, None]: +def convert_to_int(value: Any) -> Optional[int]: """Convert a value to int Args: - value (any): Value to convert to int if possible + value (Any): Value to convert to int if possible Returns: - Union[int, None]: Return the int value or None if not possible + int: Return the int value or None if not possible """ try: value_to_int = int(value) return value_to_int except ValueError: return None - except Exception: - return None def get_unixtime() -> int: """Cette fonction retourne un UNIXTIME de type 12365456 @@ -27,9 +31,12 @@ def get_unixtime() -> int: Returns: int: Current time in seconds since the Epoch (int) """ + cet_offset = timezone(timedelta(hours=2)) + now_cet = datetime.now(cet_offset) + unixtime_cet = int(now_cet.timestamp()) return int(time()) -def get_datetime() -> str: +def get_sdatetime() -> str: """Retourne une date au format string (24-12-2023 20:50:59) Returns: @@ -38,6 +45,31 @@ def get_datetime() -> str: currentdate = datetime.now().strftime('%d-%m-%Y %H:%M:%S') return currentdate +def get_datetime() -> datetime: + """ + Return the current datetime in a datetime object + """ + 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. @@ -49,15 +81,15 @@ def generate_random_string(lenght: int) -> str: return randomize -def hash(password: str, algorithm: Literal["md5, sha3_512"] = 'md5') -> str: - """Retourne un mot de passe chiffré en fonction de l'algorithme utilisé +def hash_password(password: str, algorithm: Literal["md5, sha3_512"] = 'md5') -> str: + """Return the crypted password following the selected algorithm Args: - password (str): Le password en clair - algorithm (str): L'algorithm a utilisé + password (str): The plain text password + algorithm (str): The algorithm to use Returns: - str: Le password haché + str: The crypted password, default md5 """ match algorithm: @@ -72,3 +104,30 @@ def hash(password: str, algorithm: Literal["md5, sha3_512"] = 'md5') -> str: case _: password = md5(password.encode()).hexdigest() return password + +def get_all_modules() -> list[str]: + """Get list of all main modules + using this pattern mod_*.py + + Returns: + list[str]: List of module names. + """ + base_path = Path('mods') + return [file.name.replace('.py', '') for file in base_path.rglob('mod_*.py')] + +def clean_uid(uid: str) -> Optional[str]: + """Clean UID by removing @ / % / + / ~ / * / : + + Args: + uid (str): The UID to clean + + Returns: + str: Clean UID without any sign + """ + if uid is None: + return None + + pattern = fr'[:|@|%|\+|~|\*]*' + parsed_UID = sub(pattern, '', uid) + + return parsed_UID diff --git a/defender.py b/defender.py index cbe0869..2248040 100644 --- a/defender.py +++ b/defender.py @@ -1,10 +1,11 @@ from core import installation ############################################# -# @Version : 6 # +# @Version : 6.2 # # Requierements : # # Python3.10 or higher # # SQLAlchemy, requests, psutil # +# unrealircd-rpc-py # # UnrealIRCD 6.2.2 or higher # ############################################# @@ -14,7 +15,6 @@ try: from core.loader import Loader from core.irc import Irc - # loader = Loader() ircInstance = Irc(Loader()) ircInstance.init_irc(ircInstance) diff --git a/mods/clone/clone_manager.py b/mods/clone/clone_manager.py new file mode 100644 index 0000000..486795b --- /dev/null +++ b/mods/clone/clone_manager.py @@ -0,0 +1,163 @@ +from typing import Optional, TYPE_CHECKING +from mods.clone.schemas import MClone + +if TYPE_CHECKING: + from mods.clone.mod_clone import Clone + +class CloneManager: + + UID_CLONE_DB: list[MClone] = [] + + def __init__(self, uplink: 'Clone'): + + self.Logs = uplink.Logs + + def insert(self, new_clone_object: MClone) -> bool: + """Create new Clone object + + Args: + new_clone_object (MClone): New Clone object + + Returns: + bool: True if inserted + """ + if new_clone_object is None: + self.Logs.debug('New Clone object must not be None') + return False + + for record in self.UID_CLONE_DB: + if record.nickname == new_clone_object.nickname or record.uid == new_clone_object.uid: + # If the user exist then return False and do not go further + self.Logs.debug(f'Nickname/UID {record.nickname}/{record.uid} already exist') + return False + + self.UID_CLONE_DB.append(new_clone_object) + self.Logs.debug(f'New Clone object created: {new_clone_object}') + return True + + def delete(self, uidornickname: str) -> bool: + """Delete the Clone Object starting from the nickname or the UID + + Args: + uidornickname (str): UID or nickname of the clone + + Returns: + bool: True if deleted + """ + + clone_obj = self.get_clone(uidornickname=uidornickname) + + if clone_obj is None: + return False + + self.UID_CLONE_DB.remove(clone_obj) + + return True + + def nickname_exists(self, nickname: str) -> bool: + """Check if the nickname exist + + Args: + nickname (str): Nickname of the clone + + Returns: + bool: True if the nickname exist + """ + clone = self.get_clone(nickname) + if isinstance(clone, MClone): + return True + + return False + + def uid_exists(self, uid: str) -> bool: + """Check if the nickname exist + + Args: + uid (str): uid of the clone + + Returns: + bool: True if the nickname exist + """ + clone = self.get_clone(uid) + if isinstance(clone, MClone): + return True + + return False + + def group_exists(self, groupname: str) -> bool: + """Verify if a group exist + + Args: + groupname (str): The group name + + Returns: + bool: _description_ + """ + for clone in self.UID_CLONE_DB: + if clone.group.strip().lower() == groupname.strip().lower(): + return True + + return False + + def get_clone(self, uidornickname: str) -> Optional[MClone]: + """Get MClone object or None + + Args: + uidornickname (str): The UID or the Nickname + + Returns: + Union[MClone, None]: Return MClone object or None + """ + for clone in self.UID_CLONE_DB: + if clone.uid == uidornickname: + return clone + if clone.nickname == uidornickname: + return clone + + return None + + def get_clones_from_groupname(self, groupname: str) -> list[MClone]: + """Get list of clone objects by group name + + Args: + groupname (str): The group name + + Returns: + list[MClone]: List of clones in the group + """ + group_of_clone: list[MClone] = [] + + if self.group_exists(groupname): + for clone in self.UID_CLONE_DB: + if clone.group.strip().lower() == groupname.strip().lower(): + group_of_clone.append(clone) + + return group_of_clone + + def get_uid(self, uidornickname: str) -> Optional[str]: + """Get the UID of the clone starting from the UID or the Nickname + + Args: + uidornickname (str): UID or Nickname + + Returns: + str|None: Return the UID + """ + for record in self.UID_CLONE_DB: + if record.uid == uidornickname: + return record.uid + if record.nickname == uidornickname: + return record.uid + + return None + + def kill(self, nickname:str) -> bool: + + response = False + + for clone in self.UID_CLONE_DB: + if clone.nickname == nickname: + clone.alive = False # Kill the clone + response = True + + return response \ No newline at end of file diff --git a/mods/mod_clone.py b/mods/clone/mod_clone.py similarity index 53% rename from mods/mod_clone.py rename to mods/clone/mod_clone.py index 0231255..43bde4a 100644 --- a/mods/mod_clone.py +++ b/mods/clone/mod_clone.py @@ -1,46 +1,62 @@ -from dataclasses import dataclass -import random, faker, time, logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional, Any +import mods.clone.utils as utils +import mods.clone.threads as thds +import mods.clone.schemas as schemas +from mods.clone.clone_manager import CloneManager if TYPE_CHECKING: from core.irc import Irc + from faker import Faker -class Clone(): +class Clone: - @dataclass - class ModConfModel: - clone_nicknames: list[str] - - def __init__(self, ircInstance: 'Irc') -> None: + def __init__(self, irc_instance: 'Irc') -> None: # Module name (Mandatory) self.module_name = 'mod_' + str(self.__class__.__name__).lower() # Add Irc Object to the module (Mandatory) - self.Irc = ircInstance + self.Irc = irc_instance # Add Irc Protocol Object to the module (Mandatory) - self.Protocol = ircInstance.Protocol + self.Protocol = irc_instance.Protocol # Add Global Configuration to the module (Mandatory) - self.Config = ircInstance.Config + self.Config = irc_instance.Config # Add Base object to the module (Mandatory) - self.Base = ircInstance.Base + self.Base = irc_instance.Base # Add logs object to the module (Mandatory) - self.Logs = ircInstance.Base.logs + self.Logs = irc_instance.Loader.Logs # Add User object to the module (Mandatory) - self.User = ircInstance.User + self.User = irc_instance.User # Add Channel object to the module (Mandatory) - self.Channel = ircInstance.Channel + self.Channel = irc_instance.Channel + + # Add global definitions + self.Definition = irc_instance.Loader.Definition - # Add clone object to the module (Optionnal) - self.Clone = ircInstance.Clone + # The Global Settings + self.Settings = irc_instance.Loader.Settings - self.Definition = ircInstance.Loader.Definition + self.Schemas = schemas + + self.Utils = utils + + self.Threads = thds + + self.Faker: Optional['Faker'] = self.Utils.create_faker_object('en_GB') + + self.Clone = CloneManager(self) + + metadata = self.Settings.get_cache('UID_CLONE_DB') + + if metadata is not None: + self.Clone.UID_CLONE_DB = metadata + self.Logs.debug(f"Cache Size = {self.Settings.get_cache_size()}") # Créer les nouvelles commandes du module self.Irc.build_command(1, self.module_name, 'clone', 'Connect, join, part, kill and say clones') @@ -57,10 +73,6 @@ class Clone(): self.__create_tables() self.stop = False - logging.getLogger('faker').setLevel(logging.CRITICAL) - - self.fakeEN = faker.Faker('en_GB') - self.fakeFR = faker.Faker('fr_FR') # Load module configuration (Mandatory) self.__load_module_configuration() @@ -75,8 +87,6 @@ class Clone(): def __create_tables(self) -> None: """Methode qui va créer la base de donnée si elle n'existe pas. Une Session unique pour cette classe sera crée, qui sera utilisé dans cette classe / module - Args: - database_name (str): Nom de la base de données ( pas d'espace dans le nom ) Returns: None: Aucun retour n'es attendu @@ -99,9 +109,7 @@ class Clone(): """ try: # Variable qui va contenir les options de configuration du module Defender - self.ModConfig = self.ModConfModel( - clone_nicknames=[] - ) + self.ModConfig = self.Schemas.ModConfModel() # Sync the configuration with core configuration (Mandatory) # self.Base.db_sync_core_config(self.module_name, self.ModConfig) @@ -115,6 +123,8 @@ class Clone(): """Cette methode sera executée a chaque désactivation ou rechargement de module """ + # Store Clones DB into the global Settings to retrieve it after the reload. + self.Settings.set_cache('UID_CLONE_DB', self.Clone.UID_CLONE_DB) self.Channel.db_query_channel(action='del', module_name=self.module_name, channel_name=self.Config.CLONE_CHANNEL) self.Protocol.send2socket(f":{self.Config.SERVICE_NICKNAME} MODE {self.Config.CLONE_CHANNEL} -nts") @@ -123,175 +133,41 @@ class Clone(): return None - def generate_vhost(self) -> str: - - fake = self.fakeEN - - rand_1 = fake.random_elements(['A','B','C','D','E','F','0','1','2','3','4','5','6','7','8','9'], unique=True, length=8) - rand_2 = fake.random_elements(['A','B','C','D','E','F','0','1','2','3','4','5','6','7','8','9'], unique=True, length=8) - rand_3 = fake.random_elements(['A','B','C','D','E','F','0','1','2','3','4','5','6','7','8','9'], unique=True, length=8) - - vhost = ''.join(rand_1) + '.' + ''.join(rand_2) + '.' + ''.join(rand_3) + '.IP' - return vhost - - def generate_clones(self, group: str = 'Default', auto_remote_ip: bool = False) -> None: - try: - - fakeEN = self.fakeEN - fakeFR = self.fakeFR - unixtime = self.Base.get_unixtime() - - chaine = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' - generate_uid = fakeEN.random_sample(chaine, 6) - uid = self.Config.SERVEUR_ID + ''.join(generate_uid) - - umodes = self.Config.CLONE_UMODES - - # Generate Username - chaine = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' - new_username = fakeEN.random_sample(chaine, 9) - username = ''.join(new_username) - - # Create realname XX F|M Department - gender = fakeEN.random_choices(['F','M'], 1) - gender = ''.join(gender) - - if gender == 'F': - nickname = fakeEN.first_name_female() - elif gender == 'M': - nickname = fakeEN.first_name_male() - else: - nickname = fakeEN.first_name() - - age = random.randint(20, 60) - department = fakeFR.department_name() - realname = f'{age} {gender} {department}' - - decoded_ip = fakeEN.ipv4_private() if auto_remote_ip else '127.0.0.1' - hostname = fakeEN.hostname() - - vhost = self.generate_vhost() - - checkNickname = self.Clone.exists(nickname=nickname) - checkUid = self.Clone.uid_exists(uid=uid) - - while checkNickname: - caracteres = '0123456789' - randomize = ''.join(random.choice(caracteres) for _ in range(2)) - nickname = nickname + str(randomize) - checkNickname = self.Clone.exists(nickname=nickname) - - while checkUid: - chaine = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' - generate_uid = fakeEN.random_sample(chaine, 6) - uid = self.Config.SERVEUR_ID + ''.join(generate_uid) - checkUid = self.Clone.uid_exists(uid=uid) - - clone = self.Definition.MClone( - connected=False, - nickname=nickname, - username=username, - realname=realname, - hostname=hostname, - umodes=umodes, - uid=uid, - remote_ip=decoded_ip, - vhost=vhost, - group=group, - channels=[] - ) - - self.Clone.insert(clone) - - return None - - except AttributeError as ae: - self.Logs.error(f'Attribute Error : {ae}') - except Exception as err: - self.Logs.error(f"General Error: {err}") - - def thread_connect_clones(self, number_of_clones:int , group: str = 'Default', auto_remote_ip: bool = False, interval: float = 0.2) -> None: - - for i in range(0, number_of_clones): - self.generate_clones(group=group, auto_remote_ip=auto_remote_ip) - - for clone in self.Clone.UID_CLONE_DB: - - if self.stop: - print(f"Stop creating clones ...") - self.stop = False - break - - if not clone.connected: - self.Protocol.send_uid(clone.nickname, clone.username, clone.hostname, clone.uid, clone.umodes, clone.vhost, clone.remote_ip, clone.realname, print_log=False) - self.Protocol.send_join_chan(uidornickname=clone.uid, channel=self.Config.CLONE_CHANNEL, password=self.Config.CLONE_CHANNEL_PASSWORD, print_log=False) - - time.sleep(interval) - clone.connected = True - - def thread_kill_clones(self, fromuser: str) -> None: - - clone_to_kill: list[str] = [] - for clone in self.Clone.UID_CLONE_DB: - clone_to_kill.append(clone.uid) - - for clone_uid in clone_to_kill: - self.Protocol.send_quit(clone_uid, 'Gooood bye', print_log=False) - - del clone_to_kill - - return None - def cmd(self, data:list) -> None: try: - service_id = self.Config.SERVICE_ID # Defender serveur id - cmd = list(data).copy() - - if len(cmd) < 2: + if not data or len(data) < 2: return None - match cmd[1]: - - case 'REPUTATION': - pass - - if len(cmd) < 3: + cmd = data.copy() if isinstance(data, list) else list(data).copy() + index, command = self.Irc.Protocol.get_ircd_protocol_poisition(cmd) + if index == -1: return None - match cmd[2]: + match command: + case 'PRIVMSG': - # print(cmd) - uid_sender = self.User.clean_uid(cmd[1]) - senderObj = self.User.get_User(uid_sender) + return self.Utils.handle_on_privmsg(self, cmd) - if senderObj.hostname in self.Config.CLONE_LOG_HOST_EXEMPT: - return None + case 'QUIT': + return None - if not senderObj is None: - senderMsg = ' '.join(cmd[4:]) - getClone = self.Clone.get_Clone(cmd[3]) - - if getClone is None: - return None - - if getClone.uid != self.Config.SERVICE_ID: - final_message = f"{senderObj.nickname}!{senderObj.username}@{senderObj.hostname} > {senderMsg.lstrip(':')}" - self.Protocol.send_priv_msg( - nick_from=getClone.uid, - msg=final_message, - channel=self.Config.CLONE_CHANNEL - ) + case _: + return None except Exception as err: - self.Logs.error(f'General Error: {err}') + self.Logs.error(f'General Error: {err}', exc_info=True) + return None - def hcmds(self, user:str, channel: any, cmd: list, fullcmd: list = []) -> None: + def hcmds(self, user: str, channel: Any, cmd: list, fullcmd: list = []) -> None: try: + + if len(cmd) < 1: + return + command = str(cmd[0]).lower() fromuser = user - - dnickname = self.Config.SERVICE_NICKNAME # Defender nickname + dnickname = self.Config.SERVICE_NICKNAME match command: @@ -299,10 +175,11 @@ 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 option = str(cmd[1]).lower() @@ -317,8 +194,8 @@ class Clone(): connection_interval = int(cmd[4]) if len(cmd) == 5 else 0.2 self.Base.create_thread( - func=self.thread_connect_clones, - func_args=(number_of_clones, group, False, connection_interval) + func=self.Threads.thread_connect_clones, + func_args=(self, number_of_clones, group, False, connection_interval) ) except Exception as err: @@ -328,18 +205,28 @@ class Clone(): case 'kill': try: - # clone kill [all | nickname] + # clone kill [ALL | group name | nickname] self.stop = True - clone_name = str(cmd[2]) - clone_to_kill: list[str] = [] + option = str(cmd[2]) - if clone_name.lower() == 'all': - self.Base.create_thread(func=self.thread_kill_clones, func_args=(fromuser, )) + if option.lower() == 'all': + self.Base.create_thread(func=self.Threads.thread_kill_clones, func_args=(self, )) + + elif self.Clone.group_exists(option): + list_of_clones_in_group = self.Clone.get_clones_from_groupname(option) + + if len(list_of_clones_in_group) > 0: + self.Logs.debug(f"[Clone Kill Group] - Killing {len(list_of_clones_in_group)} clones in the group {option}") + + for clone in list_of_clones_in_group: + self.Protocol.send_quit(clone.uid, "Now i am leaving irc but i'll come back soon ...", print_log=False) + self.Clone.delete(clone.uid) else: - cloneObj = self.Clone.get_Clone(clone_name) - if not cloneObj is None: - self.Protocol.send_quit(cloneObj.uid, 'Goood bye', print_log=False) + clone_obj = self.Clone.get_clone(option) + if not clone_obj is None: + self.Protocol.send_quit(clone_obj.uid, 'Goood bye', print_log=False) + self.Clone.delete(clone_obj.uid) except Exception as err: self.Logs.error(f'{err}') @@ -348,19 +235,28 @@ class Clone(): case 'join': try: - # clone join [all | nickname] #channel - clone_name = str(cmd[2]) + # clone join [all | group name | nickname] #channel + option = str(cmd[2]) clone_channel_to_join = str(cmd[3]) - if clone_name.lower() == 'all': + if option.lower() == 'all': for clone in self.Clone.UID_CLONE_DB: self.Protocol.send_join_chan(uidornickname=clone.uid, channel=clone_channel_to_join, print_log=False) + elif self.Clone.group_exists(option): + list_of_clones_in_group = self.Clone.get_clones_from_groupname(option) + + if len(list_of_clones_in_group) > 0: + self.Logs.debug(f"[Clone Join Group] - Joining {len(list_of_clones_in_group)} clones from group {option} in the channel {clone_channel_to_join}") + + for clone in list_of_clones_in_group: + self.Protocol.send_join_chan(uidornickname=clone.nickname, channel=clone_channel_to_join, print_log=False) + else: - if self.Clone.exists(clone_name): - if not self.Clone.get_uid(clone_name) is None: - self.Protocol.send_join_chan(uidornickname=clone_name, channel=clone_channel_to_join, print_log=False) + if self.Clone.nickname_exists(option): + clone_uid = self.Clone.get_clone(option).uid + self.Protocol.send_join_chan(uidornickname=clone_uid, channel=clone_channel_to_join, print_log=False) except Exception as err: self.Logs.error(f'{err}') @@ -369,18 +265,27 @@ class Clone(): case 'part': try: - # clone part [all | nickname] #channel - clone_name = str(cmd[2]) + # clone part [all | nickname] #channel + option = str(cmd[2]) clone_channel_to_part = str(cmd[3]) - if clone_name.lower() == 'all': + if option.lower() == 'all': for clone in self.Clone.UID_CLONE_DB: self.Protocol.send_part_chan(uidornickname=clone.uid, channel=clone_channel_to_part, print_log=False) + elif self.Clone.group_exists(option): + list_of_clones_in_group = self.Clone.get_clones_from_groupname(option) + + if len(list_of_clones_in_group) > 0: + self.Logs.debug(f"[Clone Part Group] - Part {len(list_of_clones_in_group)} clones from group {option} from the channel {clone_channel_to_part}") + + for clone in list_of_clones_in_group: + self.Protocol.send_part_chan(uidornickname=clone.uid, channel=clone_channel_to_part, print_log=False) + else: - if self.Clone.exists(clone_name): - clone_uid = self.Clone.get_uid(clone_name) + if self.Clone.nickname_exists(option): + clone_uid = self.Clone.get_uid(option) if not clone_uid is None: self.Protocol.send_part_chan(uidornickname=clone_uid, channel=clone_channel_to_part, print_log=False) @@ -391,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}') @@ -403,11 +328,11 @@ class Clone(): try: # clone say clone_nickname #channel message clone_name = str(cmd[2]) - clone_channel = str(cmd[3]) if self.Channel.Is_Channel(str(cmd[3])) else None + clone_channel = str(cmd[3]) if self.Channel.is_valid_channel(str(cmd[3])) else None final_message = ' '.join(cmd[4:]) - if clone_channel is None or not self.Clone.exists(clone_name): + if clone_channel is None or not self.Clone.nickname_exists(clone_name): self.Protocol.send_notice( nick_from=dnickname, nick_to=fromuser, @@ -415,7 +340,7 @@ class Clone(): ) return None - if self.Clone.exists(clone_name): + if self.Clone.nickname_exists(clone_name): self.Protocol.send_priv_msg(nick_from=clone_name, msg=final_message, channel=clone_channel) except Exception as err: @@ -428,12 +353,12 @@ class Clone(): case _: 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") except IndexError as ie: self.Logs.error(f'Index Error: {ie}') except Exception as err: - self.Logs.error(f'Index Error: {err}') + self.Logs.error(f'General Error: {err}') diff --git a/mods/clone/schemas.py b/mods/clone/schemas.py new file mode 100644 index 0000000..d45fafe --- /dev/null +++ b/mods/clone/schemas.py @@ -0,0 +1,22 @@ +from core.definition import MainModel, dataclass, field + +@dataclass +class ModConfModel(MainModel): + clone_nicknames: list[str] = field(default_factory=list) + +@dataclass +class MClone(MainModel): + """Model Clone""" + connected: bool = False + uid: str = None + nickname: str = None + username: str = None + realname: str = None + channels: list = field(default_factory=list) + vhost: str = None + hostname: str = 'localhost' + umodes: str = None + remote_ip: str = '127.0.0.1' + group: str = 'Default' + +DB_CLONES: list[MClone] = [] \ No newline at end of file diff --git a/mods/clone/threads.py b/mods/clone/threads.py new file mode 100644 index 0000000..c8c216a --- /dev/null +++ b/mods/clone/threads.py @@ -0,0 +1,44 @@ +from typing import TYPE_CHECKING +from time import sleep + +if TYPE_CHECKING: + from mods.clone.mod_clone import Clone + +def thread_connect_clones(uplink: 'Clone', + number_of_clones:int , + group: str = 'Default', + auto_remote_ip: bool = False, + interval: float = 0.2 + ): + + for i in range(0, number_of_clones): + uplink.Utils.create_new_clone( + uplink=uplink, + faker_instance=uplink.Faker, + group=group, + auto_remote_ip=auto_remote_ip + ) + + for clone in uplink.Clone.UID_CLONE_DB: + + if uplink.stop: + print(f"Stop creating clones ...") + uplink.stop = False + break + + if not clone.connected: + uplink.Protocol.send_uid(clone.nickname, clone.username, clone.hostname, clone.uid, clone.umodes, clone.vhost, clone.remote_ip, clone.realname, print_log=False) + uplink.Protocol.send_join_chan(uidornickname=clone.uid, channel=uplink.Config.CLONE_CHANNEL, password=uplink.Config.CLONE_CHANNEL_PASSWORD, print_log=False) + + sleep(interval) + clone.connected = True + +def thread_kill_clones(uplink: 'Clone'): + + clone_to_kill = uplink.Clone.UID_CLONE_DB.copy() + + for clone in clone_to_kill: + uplink.Protocol.send_quit(clone.uid, 'Gooood bye', print_log=False) + uplink.Clone.delete(clone.uid) + + del clone_to_kill diff --git a/mods/clone/utils.py b/mods/clone/utils.py new file mode 100644 index 0000000..ce509a1 --- /dev/null +++ b/mods/clone/utils.py @@ -0,0 +1,198 @@ +import logging +import random +from typing import Optional, TYPE_CHECKING +from faker import Faker + +logging.getLogger('faker').setLevel(logging.CRITICAL) + +if TYPE_CHECKING: + from mods.clone.mod_clone import Clone + +def create_faker_object(faker_local: Optional[str] = 'en_GB') -> Faker: + """Create a new faker object + + Args: + faker_local (Optional[str], optional): _description_. Defaults to 'en_GB'. + + Returns: + Faker: The Faker Object + """ + if faker_local not in ['en_GB', 'fr_FR']: + faker_local = 'en_GB' + + return Faker(faker_local) + +def generate_uid_for_clone(faker_instance: 'Faker', server_id: str) -> str: + chaine = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + return server_id + ''.join(faker_instance.random_sample(chaine, 6)) + +def generate_vhost_for_clone(faker_instance: 'Faker') -> str: + """Generate new vhost for the clone + + Args: + faker_instance (Faker): The Faker instance + + Returns: + str: _description_ + """ + rand_1 = faker_instance.random_elements(['A','B','C','D','E','F','0','1','2','3','4','5','6','7','8','9'], unique=True, length=8) + rand_2 = faker_instance.random_elements(['A','B','C','D','E','F','0','1','2','3','4','5','6','7','8','9'], unique=True, length=8) + rand_3 = faker_instance.random_elements(['A','B','C','D','E','F','0','1','2','3','4','5','6','7','8','9'], unique=True, length=8) + + vhost = ''.join(rand_1) + '.' + ''.join(rand_2) + '.' + ''.join(rand_3) + '.IP' + return vhost + +def generate_username_for_clone(faker_instance: 'Faker') -> str: + """Generate vhosts for clones + + Returns: + str: The vhost + """ + chaine = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + return ''.join(faker_instance.random_sample(chaine, 9)) + +def generate_realname_for_clone(faker_instance: 'Faker') -> tuple[int, str, str]: + """Generate realname for clone + Ex: XX F|M Department + Args: + faker_instance (Faker): _description_ + + Returns: + tuple: Age | Gender | Department + """ + # Create realname XX F|M Department + gender = faker_instance.random_choices(['F','M'], 1) + gender = ''.join(gender) + age = random.randint(20, 60) + if faker_instance.locales[0] == 'fr_FR': + department = faker_instance.department_name() + else: + department = faker_instance.city() + + return (age, gender, department) + +def generate_nickname_for_clone(faker_instance: 'Faker', gender: Optional[str] = 'AUTO') -> str: + """Generate nickname for clone + + Args: + faker_instance (Faker): The Faker Instance + gender (str): The Gender.Default F + + Returns: + str: Nickname Based on the Gender + """ + if gender.upper() == 'AUTO' or gender.upper() not in ['F', 'M']: + # Generate new gender + gender = faker_instance.random_choices(['F','M'], 1) + gender = ''.join(gender) + + if gender.upper() == 'F': + return faker_instance.first_name_female() + elif gender.upper() == 'M': + return faker_instance.first_name_male() + +def generate_ipv4_for_clone(faker_instance: 'Faker', auto: bool = True) -> str: + """Generate remote ipv4 for clone + + Args: + faker_instance (Faker): The Faker Instance + auto (bool): Set auto generation of ip or 127.0.0.1 will be returned + + Returns: + str: Remote IPV4 + """ + return faker_instance.ipv4_private() if auto else '127.0.0.1' + +def generate_hostname_for_clone(faker_instance: 'Faker') -> str: + """Generate hostname for clone + + Args: + faker_instance (Faker): The Faker Instance + + Returns: + str: New hostname + """ + return faker_instance.hostname() + +def create_new_clone(uplink: 'Clone', faker_instance: 'Faker', group: str = 'Default', auto_remote_ip: bool = False) -> bool: + """Create a new Clone object in the DB_CLONES. + + Args: + faker_instance (Faker): The Faker instance + + Returns: + bool: True if it was created + """ + faker = faker_instance + + uid = generate_uid_for_clone(faker, uplink.Config.SERVEUR_ID) + umodes = uplink.Config.CLONE_UMODES + + # Generate Username + username = generate_username_for_clone(faker) + + # Generate realname (XX F|M Department) + age, gender, department = generate_realname_for_clone(faker) + realname = f'{age} {gender} {department}' + + # Generate nickname + nickname = generate_nickname_for_clone(faker, gender) + + # Generate decoded ipv4 and hostname + decoded_ip = generate_ipv4_for_clone(faker, auto_remote_ip) + hostname = generate_hostname_for_clone(faker) + vhost = generate_vhost_for_clone(faker) + + checkNickname = uplink.Clone.nickname_exists(nickname) + checkUid = uplink.Clone.uid_exists(uid=uid) + + while checkNickname: + caracteres = '0123456789' + randomize = ''.join(random.choice(caracteres) for _ in range(2)) + nickname = nickname + str(randomize) + checkNickname = uplink.Clone.nickname_exists(nickname) + + while checkUid: + uid = generate_uid_for_clone(faker, uplink.Config.SERVEUR_ID) + checkUid = uplink.Clone.uid_exists(uid=uid) + + clone = uplink.Schemas.MClone( + connected=False, + nickname=nickname, + username=username, + realname=realname, + hostname=hostname, + umodes=umodes, + uid=uid, + remote_ip=decoded_ip, + vhost=vhost, + group=group, + channels=[] + ) + + uplink.Clone.insert(clone) + + return True + +def handle_on_privmsg(uplink: 'Clone', srvmsg: list[str]): + + uid_sender = uplink.Irc.Utils.clean_uid(srvmsg[1]) + senderObj = uplink.User.get_User(uid_sender) + + if senderObj.hostname in uplink.Config.CLONE_LOG_HOST_EXEMPT: + return + + if not senderObj is None: + senderMsg = ' '.join(srvmsg[4:]) + clone_obj = uplink.Clone.get_clone(srvmsg[3]) + + if clone_obj is None: + return + + if clone_obj.uid != uplink.Config.SERVICE_ID: + final_message = f"{senderObj.nickname}!{senderObj.username}@{senderObj.hostname} > {senderMsg.lstrip(':')}" + uplink.Protocol.send_priv_msg( + nick_from=clone_obj.uid, + msg=final_message, + channel=uplink.Config.CLONE_CHANNEL + ) diff --git a/mods/mod_command.py b/mods/command/mod_command.py similarity index 65% rename from mods/mod_command.py rename to mods/command/mod_command.py index f3b8874..75d77c6 100644 --- a/mods/mod_command.py +++ b/mods/command/mod_command.py @@ -1,5 +1,6 @@ -from typing import Union, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING from dataclasses import dataclass +import mods.command.utils as utils if TYPE_CHECKING: from core.irc import Irc @@ -34,8 +35,11 @@ class Command: # Add Base object to the module (Mandatory) self.Base = ircInstance.Base + # Add main Utils to the module + self.MainUtils = ircInstance.Utils + # Add logs object to the module (Mandatory) - self.Logs = ircInstance.Base.logs + self.Logs = ircInstance.Loader.Logs # Add User object to the module (Mandatory) self.User = ircInstance.User @@ -46,6 +50,9 @@ class Command: # Add Channel object to the module (Mandatory) self.Channel = ircInstance.Channel + # Module Utils + self.mod_utils = utils + self.Irc.build_command(1, self.module_name, 'join', 'Join a channel') self.Irc.build_command(1, self.module_name, 'assign', 'Assign a user to a role or task') self.Irc.build_command(1, self.module_name, 'part', 'Leave a channel') @@ -271,7 +278,7 @@ class Command: user_uid = self.User.clean_uid(cmd[5]) userObj: MUser = self.User.get_User(user_uid) - channel_name = cmd[4] if self.Channel.Is_Channel(cmd[4]) else None + channel_name = cmd[4] if self.Channel.is_valid_channel(cmd[4]) else None client_obj = self.Client.get_Client(user_uid) nickname = userObj.nickname if userObj is not None else None @@ -296,7 +303,7 @@ class Command: except Exception as err: self.Logs.error(f"General Error: {err}") - def hcmds(self, uidornickname: str, channel_name: Union[str, None], cmd: list, fullcmd: list = []) -> None: + def hcmds(self, uidornickname: str, channel_name: Optional[str], cmd: list, fullcmd: list = []): command = str(cmd[0]).lower() dnickname = self.Config.SERVICE_NICKNAME @@ -307,293 +314,78 @@ class Command: fromchannel = channel_name match command: - case "automode": - # automode set nickname [+/-mode] #channel - # automode set adator +o #channel + + case 'automode': try: - option: str = str(cmd[1]).lower() - match option: - case 'set': - allowed_modes: list[str] = self.Base.Settings.PROTOCTL_PREFIX # ['q','a','o','h','v'] - - if len(cmd) < 5: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} {command.upper()} [nickname] [+/-mode] [#channel]") - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"AutoModes available: {' / '.join(allowed_modes)}") - return None - - # userObj: MUser = self.User.get_User(str(cmd[2])) - nickname = str(cmd[2]) - mode = str(cmd[3]) - chan: str = str(cmd[4]).lower() if self.Channel.Is_Channel(cmd[4]) else None - sign = mode[0] if mode.startswith( ('+', '-')) else None - clean_mode = mode[1:] if len(mode) > 0 else None - - if sign is None: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg="You must provide the flag mode + or -") - return None - - if clean_mode not in allowed_modes: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"You should use one of those modes {' / '.join(allowed_modes)}") - return None - - if chan is None: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"You should use one of those modes {' / '.join(allowed_modes)}") - return None - - db_data: dict[str, str] = {"nickname": nickname, "channel": chan} - db_query = self.Base.db_execute_query(query="SELECT id FROM command_automode WHERE nickname = :nickname and channel = :channel", params=db_data) - db_result = db_query.fetchone() - - if db_result is not None: - if sign == '+': - db_data = {"updated_on": self.Base.get_datetime(), "nickname": nickname, "channel": chan, "mode": mode} - db_result = self.Base.db_execute_query(query="UPDATE command_automode SET mode = :mode, updated_on = :updated_on WHERE nickname = :nickname and channel = :channel", - params=db_data) - if db_result.rowcount > 0: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Automode {mode} edited for {nickname} in {chan}") - elif sign == '-': - db_data = {"nickname": nickname, "channel": chan, "mode": f"+{clean_mode}"} - db_result = self.Base.db_execute_query(query="DELETE FROM command_automode WHERE nickname = :nickname and channel = :channel and mode = :mode", - params=db_data) - if db_result.rowcount > 0: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Automode {mode} deleted for {nickname} in {chan}") - else: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"The mode [{mode}] has not been found for {nickname} in channel {chan}") - - return None - - # Instert a new automode - if sign == '+': - db_data = {"created_on": self.Base.get_datetime(), "updated_on": self.Base.get_datetime(), "nickname": nickname, "channel": chan, "mode": mode} - db_query = self.Base.db_execute_query( - query="INSERT INTO command_automode (created_on, updated_on, nickname, channel, mode) VALUES (:created_on, :updated_on, :nickname, :channel, :mode)", - params=db_data - ) - - if db_query.rowcount > 0: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Automode {mode} applied to {nickname} in {chan}") - if self.Channel.is_user_present_in_channel(chan, self.User.get_uid(nickname)): - self.Protocol.send2socket(f":{service_id} MODE {chan} {mode} {nickname}") - else: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"AUTOMODE {mode} cannot be added to {nickname} in {chan} because it doesn't exist") - - case 'list': - db_query: CursorResult = self.Base.db_execute_query("SELECT nickname, channel, mode FROM command_automode") - db_results: Sequence[Row] = db_query.fetchall() - - if not db_results: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, - msg="There is no automode to display.") - - for db_result in db_results: - db_nickname, db_channel, db_mode = db_result - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, - msg=f"Nickname: {db_nickname} | Channel: {db_channel} | Mode: {db_mode}") - - case _: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} {command.upper()} SET [nickname] [+/-mode] [#channel]") - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} {command.upper()} LIST") - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"[AUTOMODES AVAILABLE] are {' / '.join(allowed_modes)}") - + self.mod_utils.set_automode(self, cmd, fromuser) except IndexError: self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} {command.upper()} SET [nickname] [+/-mode] [#channel]") self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} {command.upper()} LIST") - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"[AUTOMODES AVAILABLE] are {' / '.join(self.Base.Settings.PROTOCTL_PREFIX)}") + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"[AUTOMODES AVAILABLE] are {' / '.join(self.Loader.Settings.PROTOCTL_PREFIX)}") except Exception as err: self.Logs.error(f"General Error: {err}") case 'deopall': try: - self.Protocol.send2socket(f":{service_id} SVSMODE {fromchannel} -o") - - except IndexError as ie: - self.Logs.warning(f'_hcmd OP: {str(ie)}') + self.mod_utils.set_deopall(self, fromchannel) except Exception as err: - self.Logs.warning(f'Unknown Error: {str(err)}') + self.Logs.error(f'Unknown Error: {str(err)}') case 'devoiceall': try: - self.Protocol.send2socket(f":{service_id} SVSMODE {fromchannel} -v") - - except IndexError as e: - self.Logs.warning(f'_hcmd OP: {str(e)}') + self.mod_utils.set_devoiceall(self, fromchannel) except Exception as err: - self.Logs.warning(f'Unknown Error: {str(err)}') + self.Logs.error(f'Unknown Error: {str(err)}') case 'voiceall': try: - chan_info = self.Channel.get_Channel(fromchannel) - set_mode = 'v' - mode:str = '' - users:str = '' - uids_split = [chan_info.uids[i:i + 6] for i in range(0, len(chan_info.uids), 6)] - - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} +{set_mode} {dnickname}") - for uid in uids_split: - for i in range(0, len(uid)): - mode += set_mode - users += f'{self.User.get_nickname(self.Base.clean_uid(uid[i]))} ' - if i == len(uid) - 1: - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} +{mode} {users}") - mode = '' - users = '' - except IndexError as e: - self.Logs.warning(f'_hcmd OP: {str(e)}') + self.mod_utils.set_mode_to_all(self, fromchannel, '+', 'v') except Exception as err: self.Logs.warning(f'Unknown Error: {str(err)}') case 'opall': try: - chan_info = self.Channel.get_Channel(fromchannel) - set_mode = 'o' - mode:str = '' - users:str = '' - uids_split = [chan_info.uids[i:i + 6] for i in range(0, len(chan_info.uids), 6)] - - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} +{set_mode} {dnickname}") - for uid in uids_split: - for i in range(0, len(uid)): - mode += set_mode - users += f'{self.User.get_nickname(self.Base.clean_uid(uid[i]))} ' - if i == len(uid) - 1: - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} +{mode} {users}") - mode = '' - users = '' - except IndexError as e: - self.Logs.warning(f'_hcmd OP: {str(e)}') + self.mod_utils.set_mode_to_all(self, fromchannel, '+', 'o') except Exception as err: self.Logs.warning(f'Unknown Error: {str(err)}') case 'op': - # /mode #channel +o user - # .op #channel user - # /msg dnickname op #channel user - # [':adator', 'PRIVMSG', '#services', ':.o', '#services', 'dktmb'] try: - if fromchannel is None: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} op [#SALON] [NICKNAME]") - return False - - if len(cmd) == 1: - self.Protocol.send2socket(f":{dnickname} MODE {fromchannel} +o {fromuser}") - return True - - # deop nickname - if len(cmd) == 2: - nickname = cmd[1] - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} +o {nickname}") - return True - - nickname = cmd[2] - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} +o {nickname}") - + self.mod_utils.set_operation(self, cmd, fromchannel, fromuser, '+o') except IndexError as e: - self.Logs.warning(f'_hcmd OP: {str(e)}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} op [#SALON] [NICKNAME]") except Exception as err: self.Logs.warning(f'Unknown Error: {str(err)}') case 'deop': - # /mode #channel -o user - # .deop #channel user try: - if fromchannel is None: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} deop [#SALON] [NICKNAME]") - return False - - if len(cmd) == 1: - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} -o {fromuser}") - return True - - # deop nickname - if len(cmd) == 2: - nickname = cmd[1] - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} -o {nickname}") - return True - - nickname = cmd[2] - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} -o {nickname}") - + self.mod_utils.set_operation(self, cmd, fromchannel, fromuser, '-o') except IndexError as e: - self.Logs.warning(f'_hcmd DEOP: {str(e)}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} deop [#SALON] [NICKNAME]") except Exception as err: self.Logs.warning(f'Unknown Error: {str(err)}') case 'owner': - # /mode #channel +q user - # .owner #channel user try: - if fromchannel is None: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} owner [#SALON] [NICKNAME]") - return False - - if len(cmd) == 1: - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} +q {fromuser}") - return True - - # owner nickname - if len(cmd) == 2: - nickname = cmd[1] - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} +q {nickname}") - return True - - nickname = cmd[2] - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} +q {nickname}") + self.mod_utils.set_operation(self, cmd, fromchannel, fromuser, '+q') except IndexError as e: - self.Logs.warning(f'_hcmd OWNER: {str(e)}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} owner [#SALON] [NICKNAME]") except Exception as err: self.Logs.warning(f'Unknown Error: {str(err)}') case 'deowner': - # /mode #channel -q user - # .deowner #channel user try: - if fromchannel is None: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} deowner [#SALON] [NICKNAME]") - return False - - if len(cmd) == 1: - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} -q {fromuser}") - return True - - # deowner nickname - if len(cmd) == 2: - nickname = cmd[1] - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} -q {nickname}") - return True - - nickname = cmd[2] - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} -q {nickname}") + self.mod_utils.set_operation(self, cmd, fromchannel, fromuser, '-q') except IndexError as e: - self.Logs.warning(f'_hcmd DEOWNER: {str(e)}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} deowner [#SALON] [NICKNAME]") except Exception as err: self.Logs.warning(f'Unknown Error: {str(err)}') case 'protect': - # /mode #channel +a user - # .protect #channel user try: - if fromchannel is None: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} {command.upper()} [#SALON] [NICKNAME]") - return False - - if len(cmd) == 1: - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} +a {fromuser}") - return True - - # deowner nickname - if len(cmd) == 2: - nickname = cmd[1] - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} +a {nickname}") - return True - - nickname = cmd[2] - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} +a {nickname}") + self.mod_utils.set_operation(self, cmd, fromchannel, fromuser, '+a') except IndexError as e: self.Logs.warning(f'_hcmd DEOWNER: {str(e)}') @@ -602,25 +394,8 @@ class Command: self.Logs.warning(f'Unknown Error: {str(err)}') case 'deprotect': - # /mode #channel -a user - # .deprotect #channel user try: - if fromchannel is None: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} {command.upper()} [#SALON] [NICKNAME]") - return False - - if len(cmd) == 1: - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} -a {fromuser}") - return True - - # deowner nickname - if len(cmd) == 2: - nickname = cmd[1] - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} -a {nickname}") - return True - - nickname = cmd[2] - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} -a {nickname}") + self.mod_utils.set_operation(self, cmd, fromchannel, fromuser, '-a') except IndexError as e: self.Logs.warning(f'_hcmd DEOWNER: {str(e)}') @@ -629,125 +404,42 @@ class Command: self.Logs.warning(f'Unknown Error: {str(err)}') case 'halfop': - # /mode #channel +h user - # .halfop #channel user try: - if fromchannel is None: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} halfop [#SALON] [NICKNAME]") - return False - - if len(cmd) == 1: - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} +h {fromuser}") - return True - - # deop nickname - if len(cmd) == 2: - nickname = cmd[1] - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} +h {nickname}") - return True - - nickname = cmd[2] - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} +h {nickname}") + self.mod_utils.set_operation(self, cmd, fromchannel, fromuser, '+h') except IndexError as e: - self.Logs.warning(f'_hcmd halfop: {str(e)}') - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} halfop [#SALON] [NICKNAME]") + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} {command} [#SALON] [NICKNAME]") except Exception as err: self.Logs.warning(f'Unknown Error: {str(err)}') case 'dehalfop': - # /mode #channel -h user - # .dehalfop #channel user try: - if fromchannel is None: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} dehalfop [#SALON] [NICKNAME]") - return False - - if len(cmd) == 1: - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} -h {fromuser}") - return True - - # dehalfop nickname - if len(cmd) == 2: - nickname = cmd[1] - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} -h {nickname}") - return True - - nickname = cmd[2] - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} -h {nickname}") + self.mod_utils.set_operation(self, cmd, fromchannel, fromuser, '-h') except IndexError as e: - self.Logs.warning(f'_hcmd DEHALFOP: {str(e)}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} dehalfop [#SALON] [NICKNAME]") except Exception as err: self.Logs.warning(f'Unknown Error: {str(err)}') case 'voice': - # /mode #channel +v user - # .voice #channel user try: - if fromchannel is None: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} voice [#SALON] [NICKNAME]") - return False - - if len(cmd) == 1: - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} +v {fromuser}") - return True - - # voice nickname - if len(cmd) == 2: - nickname = cmd[1] - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} +v {nickname}") - return True - - nickname = cmd[2] - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} +v {nickname}") - + self.mod_utils.set_operation(self, cmd, fromchannel, fromuser, '+v') except IndexError as e: - self.Logs.warning(f'_hcmd VOICE: {str(e)}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} voice [#SALON] [NICKNAME]") except Exception as err: self.Logs.warning(f'Unknown Error: {str(err)}') case 'devoice': - # /mode #channel -v user - # .devoice #channel user try: - if fromchannel is None: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} devoice [#SALON] [NICKNAME]") - return False - - if len(cmd) == 1: - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} -v {fromuser}") - return True - - # dehalfop nickname - if len(cmd) == 2: - nickname = cmd[1] - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} -v {nickname}") - return True - - nickname = cmd[2] - self.Protocol.send2socket(f":{service_id} MODE {fromchannel} -v {nickname}") - + self.mod_utils.set_operation(self, cmd, fromchannel, fromuser, '-v') except IndexError as e: - self.Logs.warning(f'_hcmd DEVOICE: {str(e)}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} devoice [#SALON] [NICKNAME]") except Exception as err: self.Logs.warning(f'Unknown Error: {str(err)}') case 'ban': - # .ban #channel nickname try: - sentchannel = str(cmd[1]) if self.Channel.Is_Channel(cmd[1]) else None - if sentchannel is None: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} {command.upper()} [#SALON] [NICKNAME]") - return False - - nickname = cmd[2] - - self.Protocol.send2socket(f":{service_id} MODE {sentchannel} +b {nickname}!*@*") - self.Logs.debug(f'{fromuser} has banned {nickname} from {sentchannel}') + self.mod_utils.set_ban(self, cmd, '+', fromuser) except IndexError as e: self.Logs.warning(f'_hcmd BAN: {str(e)}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} {command.upper()} [#SALON] [NICKNAME]") @@ -755,97 +447,43 @@ class Command: self.Logs.warning(f'Unknown Error: {str(err)}') case 'unban': - # .unban #channel nickname try: - sentchannel = str(cmd[1]) if self.Channel.Is_Channel(cmd[1]) else None - if sentchannel is None: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} ban [#SALON] [NICKNAME]") - return False - nickname = cmd[2] - - self.Protocol.send2socket(f":{service_id} MODE {sentchannel} -b {nickname}!*@*") - self.Logs.debug(f'{fromuser} has unbanned {nickname} from {sentchannel}') - + self.mod_utils.set_ban(self, cmd, '-', fromuser) except IndexError as e: self.Logs.warning(f'_hcmd UNBAN: {str(e)}') - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} unban [#SALON] [NICKNAME]") + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} {command.upper()} [#SALON] [NICKNAME]") except Exception as err: self.Logs.warning(f'Unknown Error: {str(err)}') case 'kick': - # .kick #channel nickname reason try: - sentchannel = str(cmd[1]) if self.Channel.Is_Channel(cmd[1]) else None - if sentchannel is None: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} ban [#SALON] [NICKNAME]") - return False - nickname = cmd[2] - final_reason = ' '.join(cmd[3:]) - - self.Protocol.send2socket(f":{service_id} KICK {sentchannel} {nickname} {final_reason}") - self.Logs.debug(f'{fromuser} has kicked {nickname} from {sentchannel} : {final_reason}') - + self.mod_utils.set_kick(self, cmd, fromuser) except IndexError as e: self.Logs.warning(f'_hcmd KICK: {str(e)}') - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} kick [#SALON] [NICKNAME] [REASON]") + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} {command.upper()} [#SALON] [NICKNAME] [REASON]") except Exception as err: self.Logs.warning(f'Unknown Error: {str(err)}') case 'kickban': - # .kickban #channel nickname reason try: - sentchannel = str(cmd[1]) if self.Channel.Is_Channel(cmd[1]) else None - if sentchannel is None: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} ban [#SALON] [NICKNAME]") - return False - nickname = cmd[2] - final_reason = ' '.join(cmd[3:]) - - self.Protocol.send2socket(f":{service_id} KICK {sentchannel} {nickname} {final_reason}") - self.Protocol.send2socket(f":{service_id} MODE {sentchannel} +b {nickname}!*@*") - self.Logs.debug(f'{fromuser} has kicked and banned {nickname} from {sentchannel} : {final_reason}') - + self.mod_utils.set_kickban(self, cmd, fromuser) except IndexError as e: self.Logs.warning(f'_hcmd KICKBAN: {str(e)}') - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} kickban [#SALON] [NICKNAME] [REASON]") + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} {command.upper()} [#SALON] [NICKNAME] [REASON]") except Exception as err: self.Logs.warning(f'Unknown Error: {str(err)}') case 'join' | 'assign': - try: - sent_channel = str(cmd[1]) if self.Channel.Is_Channel(cmd[1]) else None - if sent_channel is None: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"{self.Config.SERVICE_PREFIX}JOIN #channel") - return False - - # self.Protocol.send2socket(f':{service_id} JOIN {sent_channel}') - self.Protocol.send_join_chan(uidornickname=dnickname,channel=sent_channel) - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" {dnickname} JOINED {sent_channel}") - self.Channel.db_query_channel('add', self.module_name, sent_channel) - + self.mod_utils.set_assign_channel_to_service(self, cmd, fromuser) except IndexError as ie: - self.Logs.error(f'{ie}') + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} {command.upper()} [#SALON]") except Exception as err: self.Logs.warning(f'Unknown Error: {str(err)}') case 'part' | 'unassign': - try: - sent_channel = str(cmd[1]) if self.Channel.Is_Channel(cmd[1]) else None - if sent_channel is None: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"{self.Config.SERVICE_PREFIX}PART #channel") - return False - - if sent_channel == dchanlog: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" {dnickname} CAN'T LEFT {sent_channel} AS IT IS LOG CHANNEL") - return False - - self.Protocol.send_part_chan(uidornickname=dnickname, channel=sent_channel) - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" {dnickname} LEFT {sent_channel}") - - self.Channel.db_query_channel('del', self.module_name, sent_channel) - + self.mod_utils.set_unassign_channel_to_service(self, cmd, fromuser) except IndexError as ie: self.Logs.error(f'{ie}') except Exception as err: @@ -859,7 +497,7 @@ class Command: return None chan = str(cmd[1]) - if not self.Channel.Is_Channel(chan): + if not self.Channel.is_valid_channel(chan): self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg="The channel must start with #") self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} TOPIC #channel THE_TOPIC_MESSAGE") return None @@ -959,7 +597,7 @@ class Command: chan = str(cmd[1]) - if not self.Channel.Is_Channel(chan): + if not self.Channel.is_valid_channel(chan): self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg="The channel must start with #") self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} {str(cmd[0]).upper()} #channel") return None @@ -980,7 +618,7 @@ class Command: nickname = str(cmd[1]) chan = str(cmd[2]) - if not self.Channel.Is_Channel(chan): + if not self.Channel.is_valid_channel(chan): self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg="The channel must start with #") self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} {str(cmd[0]).upper()} NICKNAME #CHANNEL") return None @@ -1051,7 +689,7 @@ class Command: if len(cmd) == 2: channel_mode = cmd[1] - if self.Channel.Is_Channel(fromchannel): + if self.Channel.is_valid_channel(fromchannel): self.Protocol.send2socket(f":{dnickname} MODE {fromchannel} {channel_mode}") else: self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : Channel [{fromchannel}] is not correct should start with #") @@ -1146,7 +784,7 @@ class Command: # .svsnick nickname newnickname nickname = str(cmd[1]) newnickname = str(cmd[2]) - unixtime = self.Base.get_unixtime() + unixtime = self.MainUtils.get_unixtime() if self.User.get_nickname(nickname) is None: self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" This nickname do not exist") @@ -1192,7 +830,7 @@ class Command: nickname = str(cmd[1]) hostname = str(cmd[2]) - set_at_timestamp = self.Base.get_unixtime() + set_at_timestamp = self.MainUtils.get_unixtime() expire_time = (60 * 60 * 24) + set_at_timestamp gline_reason = ' '.join(cmd[3:]) @@ -1201,7 +839,7 @@ class Command: self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" /msg {dnickname} {command.upper()} nickname host reason") return None - self.Protocol.gline(nickname=nickname, hostname=hostname, set_by=dnickname, expire_timestamp=expire_time, set_at_timestamp=set_at_timestamp, reason=gline_reason) + self.Protocol.send_gline(nickname=nickname, hostname=hostname, set_by=dnickname, expire_timestamp=expire_time, set_at_timestamp=set_at_timestamp, reason=gline_reason) except KeyError as ke: self.Logs.error(ke) @@ -1222,7 +860,7 @@ class Command: hostname = str(cmd[2]) # self.Protocol.send2socket(f":{self.Config.SERVEUR_ID} TKL - G {nickname} {hostname} {dnickname}") - self.Protocol.ungline(nickname=nickname, hostname=hostname) + self.Protocol.send_ungline(nickname=nickname, hostname=hostname) except KeyError as ke: self.Logs.error(ke) @@ -1240,7 +878,7 @@ class Command: nickname = str(cmd[1]) hostname = str(cmd[2]) - set_at_timestamp = self.Base.get_unixtime() + set_at_timestamp = self.MainUtils.get_unixtime() expire_time = (60 * 60 * 24) + set_at_timestamp gline_reason = ' '.join(cmd[3:]) @@ -1249,7 +887,7 @@ class Command: self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" /msg {dnickname} {command.upper()} nickname host reason") return None - self.Protocol.kline(nickname=nickname, hostname=hostname, set_by=dnickname, expire_timestamp=expire_time, set_at_timestamp=set_at_timestamp, reason=gline_reason) + self.Protocol.send_kline(nickname=nickname, hostname=hostname, set_by=dnickname, expire_timestamp=expire_time, set_at_timestamp=set_at_timestamp, reason=gline_reason) except KeyError as ke: self.Logs.error(ke) @@ -1269,7 +907,7 @@ class Command: nickname = str(cmd[1]) hostname = str(cmd[2]) - self.Protocol.unkline(nickname=nickname, hostname=hostname) + self.Protocol.send_unkline(nickname=nickname, hostname=hostname) except KeyError as ke: self.Logs.error(ke) @@ -1288,7 +926,7 @@ class Command: nickname = str(cmd[1]) hostname = str(cmd[2]) - set_at_timestamp = self.Base.get_unixtime() + set_at_timestamp = self.MainUtils.get_unixtime() expire_time = (60 * 60 * 24) + set_at_timestamp shun_reason = ' '.join(cmd[3:]) diff --git a/mods/command/utils.py b/mods/command/utils.py new file mode 100644 index 0000000..c01e70b --- /dev/null +++ b/mods/command/utils.py @@ -0,0 +1,237 @@ +from typing import TYPE_CHECKING, Literal, Optional + +if TYPE_CHECKING: + from mods.command.mod_command import Command + + +def set_automode(uplink: 'Command', cmd: list[str], client: str) -> None: + + command: str = str(cmd[0]).lower() + option: str = str(cmd[1]).lower() + allowed_modes: list[str] = uplink.Loader.Settings.PROTOCTL_PREFIX # ['q','a','o','h','v'] + dnickname = uplink.Config.SERVICE_NICKNAME + service_id = uplink.Config.SERVICE_ID + fromuser = client + + match option: + case 'set': + if len(cmd) < 5: + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} {command.upper()} [nickname] [+/-mode] [#channel]") + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"AutoModes available: {' / '.join(allowed_modes)}") + return None + + nickname = str(cmd[2]) + mode = str(cmd[3]) + chan: str = str(cmd[4]).lower() if uplink.Channel.is_valid_channel(cmd[4]) else None + sign = mode[0] if mode.startswith( ('+', '-')) else None + clean_mode = mode[1:] if len(mode) > 0 else None + + if sign is None: + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg="You must provide the flag mode + or -") + return None + + if clean_mode not in allowed_modes: + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"You should use one of those modes {' / '.join(allowed_modes)}") + return None + + if chan is None: + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"You should use one of those modes {' / '.join(allowed_modes)}") + return None + + db_data: dict[str, str] = {"nickname": nickname, "channel": chan} + db_query = uplink.Base.db_execute_query(query="SELECT id FROM command_automode WHERE nickname = :nickname and channel = :channel", params=db_data) + db_result = db_query.fetchone() + + if db_result is not None: + if sign == '+': + db_data = {"updated_on": uplink.MainUtils.get_sdatetime(), "nickname": nickname, "channel": chan, "mode": mode} + db_result = uplink.Base.db_execute_query(query="UPDATE command_automode SET mode = :mode, updated_on = :updated_on WHERE nickname = :nickname and channel = :channel", + params=db_data) + if db_result.rowcount > 0: + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Automode {mode} edited for {nickname} in {chan}") + elif sign == '-': + db_data = {"nickname": nickname, "channel": chan, "mode": f"+{clean_mode}"} + db_result = uplink.Base.db_execute_query(query="DELETE FROM command_automode WHERE nickname = :nickname and channel = :channel and mode = :mode", + params=db_data) + if db_result.rowcount > 0: + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Automode {mode} deleted for {nickname} in {chan}") + else: + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"The mode [{mode}] has not been found for {nickname} in channel {chan}") + + return None + + # Instert a new automode + if sign == '+': + db_data = {"created_on": uplink.MainUtils.get_sdatetime(), "updated_on": uplink.MainUtils.get_sdatetime(), "nickname": nickname, "channel": chan, "mode": mode} + db_query = uplink.Base.db_execute_query( + query="INSERT INTO command_automode (created_on, updated_on, nickname, channel, mode) VALUES (:created_on, :updated_on, :nickname, :channel, :mode)", + params=db_data + ) + + if db_query.rowcount > 0: + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Automode {mode} applied to {nickname} in {chan}") + if uplink.Channel.is_user_present_in_channel(chan, uplink.User.get_uid(nickname)): + uplink.Protocol.send2socket(f":{service_id} MODE {chan} {mode} {nickname}") + else: + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"AUTOMODE {mode} cannot be added to {nickname} in {chan} because it doesn't exist") + + case 'list': + db_query = uplink.Base.db_execute_query("SELECT nickname, channel, mode FROM command_automode") + db_results = db_query.fetchall() + + if not db_results: + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, + msg="There is no automode to display.") + + for db_result in db_results: + db_nickname, db_channel, db_mode = db_result + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, + msg=f"Nickname: {db_nickname} | Channel: {db_channel} | Mode: {db_mode}") + + case _: + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} {command.upper()} SET [nickname] [+/-mode] [#channel]") + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} {command.upper()} LIST") + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"[AUTOMODES AVAILABLE] are {' / '.join(allowed_modes)}") + +def set_deopall(uplink: 'Command', channel_name: str) -> None: + + service_id = uplink.Config.SERVICE_ID + uplink.Protocol.send2socket(f":{service_id} SVSMODE {channel_name} -o") + return None + +def set_devoiceall(uplink: 'Command', channel_name: str) -> None: + + service_id = uplink.Config.SERVICE_ID + uplink.Protocol.send2socket(f":{service_id} SVSMODE {channel_name} -v") + return None + +def set_mode_to_all(uplink: 'Command', channel_name: str, action: Literal['+', '-'], pmode: str) -> None: + + chan_info = uplink.Channel.get_channel(channel_name) + service_id = uplink.Config.SERVICE_ID + dnickname = uplink.Config.SERVICE_NICKNAME + set_mode = pmode + mode:str = '' + users:str = '' + uids_split = [chan_info.uids[i:i + 6] for i in range(0, len(chan_info.uids), 6)] + + uplink.Protocol.send2socket(f":{service_id} MODE {channel_name} {action}{set_mode} {dnickname}") + for uid in uids_split: + for i in range(0, len(uid)): + mode += set_mode + users += f'{uplink.User.get_nickname(uplink.MainUtils.clean_uid(uid[i]))} ' + if i == len(uid) - 1: + uplink.Protocol.send2socket(f":{service_id} MODE {channel_name} {action}{mode} {users}") + mode = '' + users = '' + +def set_operation(uplink: 'Command', cmd: list[str], channel_name: Optional[str], client: str, mode: str) -> None: + + dnickname = uplink.Config.SERVICE_NICKNAME + service_id = uplink.Config.SERVICE_ID + if channel_name is None: + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=client, msg=f" Right command : /msg {dnickname} {mode} [#SALON] [NICKNAME]") + return False + + if len(cmd) == 1: + uplink.Protocol.send2socket(f":{dnickname} MODE {channel_name} {mode} {client}") + return None + + # deop nickname + if len(cmd) == 2: + nickname = cmd[1] + uplink.Protocol.send2socket(f":{service_id} MODE {channel_name} {mode} {nickname}") + return None + + nickname = cmd[2] + uplink.Protocol.send2socket(f":{service_id} MODE {channel_name} {mode} {nickname}") + return None + +def set_ban(uplink: 'Command', cmd: list[str], action: Literal['+', '-'], client: str) -> None: + + command = str(cmd[0]) + dnickname = uplink.Config.SERVICE_NICKNAME + service_id = uplink.Config.SERVICE_ID + sentchannel = str(cmd[1]) if uplink.Channel.is_valid_channel(cmd[1]) else None + + if sentchannel is None: + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=client, msg=f" Right command : /msg {dnickname} {command.upper()} [#SALON] [NICKNAME]") + return None + + nickname = cmd[2] + + uplink.Protocol.send2socket(f":{service_id} MODE {sentchannel} {action}b {nickname}!*@*") + uplink.Logs.debug(f'{client} has banned {nickname} from {sentchannel}') + return None + +def set_kick(uplink: 'Command', cmd: list[str], client: str) -> None: + + command = str(cmd[0]) + dnickname = uplink.Config.SERVICE_NICKNAME + service_id = uplink.Config.SERVICE_ID + + sentchannel = str(cmd[1]) if uplink.Channel.is_valid_channel(cmd[1]) else None + if sentchannel is None: + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=client, msg=f" Right command : /msg {dnickname} {command} [#SALON] [NICKNAME]") + return False + + nickname = cmd[2] + final_reason = ' '.join(cmd[3:]) + + uplink.Protocol.send2socket(f":{service_id} KICK {sentchannel} {nickname} {final_reason}") + uplink.Logs.debug(f'{client} has kicked {nickname} from {sentchannel} : {final_reason}') + return None + +def set_kickban(uplink: 'Command', cmd: list[str], client: str) -> None: + + command = str(cmd[0]) + dnickname = uplink.Config.SERVICE_NICKNAME + service_id = uplink.Config.SERVICE_ID + + sentchannel = str(cmd[1]) if uplink.Channel.is_valid_channel(cmd[1]) else None + if sentchannel is None: + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=client, msg=f" Right command : /msg {dnickname} {command} [#SALON] [NICKNAME]") + return False + nickname = cmd[2] + final_reason = ' '.join(cmd[3:]) + + uplink.Protocol.send2socket(f":{service_id} KICK {sentchannel} {nickname} {final_reason}") + uplink.Protocol.send2socket(f":{service_id} MODE {sentchannel} +b {nickname}!*@*") + uplink.Logs.debug(f'{client} has kicked and banned {nickname} from {sentchannel} : {final_reason}') + +def set_assign_channel_to_service(uplink: 'Command', cmd: list[str], client: str) -> None: + + command = str(cmd[0]) + dnickname = uplink.Config.SERVICE_NICKNAME + sent_channel = str(cmd[1]) if uplink.Channel.is_valid_channel(cmd[1]) else None + if sent_channel is None: + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=client, msg=f" Right command : /msg {dnickname} {command.upper()} [#SALON]") + return None + + # self.Protocol.send2socket(f':{service_id} JOIN {sent_channel}') + uplink.Protocol.send_join_chan(uidornickname=dnickname,channel=sent_channel) + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=client, msg=f" Has joined {sent_channel}") + uplink.Channel.db_query_channel('add', uplink.module_name, sent_channel) + + return None + +def set_unassign_channel_to_service(uplink: 'Command', cmd: list[str], client: str) -> None: + + command = str(cmd[0]) + dnickname = uplink.Config.SERVICE_NICKNAME + dchanlog = uplink.Config.SERVICE_CHANLOG + + sent_channel = str(cmd[1]) if uplink.Channel.is_valid_channel(cmd[1]) else None + if sent_channel is None: + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=client, msg=f" Right command : /msg {dnickname} {command.upper()} [#SALON]") + return None + + if sent_channel == dchanlog: + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=client, msg=f"[!] CAN'T LEFT {sent_channel} AS IT IS LOG CHANNEL [!]") + return None + + uplink.Protocol.send_part_chan(uidornickname=dnickname, channel=sent_channel) + uplink.Protocol.send_notice(nick_from=dnickname, nick_to=client, msg=f" Has left {sent_channel}") + + uplink.Channel.db_query_channel('del', uplink.module_name, sent_channel) + return None \ No newline at end of file diff --git a/mods/mod_defender.py b/mods/defender/mod_defender.py similarity index 55% rename from mods/mod_defender.py rename to mods/defender/mod_defender.py index e28be2b..d12ea8a 100644 --- a/mods/mod_defender.py +++ b/mods/defender/mod_defender.py @@ -1,85 +1,54 @@ -import socket -import json -import time -import re -from webbrowser import get -import psutil -import requests -from dataclasses import dataclass -from datetime import datetime -from typing import Union, TYPE_CHECKING -import core.definition as df - -# Le module crée devra réspecter quelques conditions -# 1. Le nom de la classe devra toujours s'appeler comme le module. Exemple => nom de class Defender | nom du module mod_defender -# 2. la methode __init__ devra toujours avoir les parametres suivant (self, irc:object) -# 1 . Créer la variable Irc dans le module -# 2 . Récuperer la configuration dans une variable -# 3 . Définir et enregistrer les nouvelles commandes -# 4 . Créer vos tables, en utilisant toujours le nom des votre classe en minuscule ==> defender_votre-table -# 3. Methode suivantes: -# cmd(self, data:list) -# hcmds(self, user:str, cmd: list) -# unload(self) +import traceback +import mods.defender.schemas as schemas +import mods.defender.utils as utils +import mods.defender.threads as thds +from typing import TYPE_CHECKING if TYPE_CHECKING: from core.irc import Irc +class Defender: -class Defender(): - - @dataclass - class ModConfModel: - reputation: int - reputation_timer: int - reputation_seuil: int - reputation_score_after_release: int - reputation_ban_all_chan: int - reputation_sg: int - local_scan: int - psutil_scan: int - abuseipdb_scan: int - freeipapi_scan: int - cloudfilt_scan: int - flood: int - flood_message: int - flood_time: int - flood_timer: int - autolimit: int - autolimit_amount: int - autolimit_interval: int - - def __init__(self, ircInstance: 'Irc') -> None: + def __init__(self, irc_instance: 'Irc') -> None: # Module name (Mandatory) self.module_name = 'mod_' + str(self.__class__.__name__).lower() # Add Irc Object to the module (Mandatory) - self.Irc = ircInstance + self.Irc = irc_instance # Add Loader Object to the module (Mandatory) - self.Loader = ircInstance.Loader + self.Loader = irc_instance.Loader # Add server protocol Object to the module (Mandatory) - self.Protocol = ircInstance.Protocol + self.Protocol = irc_instance.Protocol # Add Global Configuration to the module (Mandatory) - self.Config = ircInstance.Config + self.Config = irc_instance.Config # Add Base object to the module (Mandatory) - self.Base = ircInstance.Base + self.Base = irc_instance.Base # Add logs object to the module (Mandatory) - self.Logs = ircInstance.Base.logs + self.Logs = irc_instance.Loader.Logs # Add User object to the module (Mandatory) - self.User = ircInstance.User + self.User = irc_instance.User # Add Channel object to the module (Mandatory) - self.Channel = ircInstance.Channel + self.Channel = irc_instance.Channel + + # Add Settings object to save objects when reloading modules (Mandatory) + self.Settings = irc_instance.Settings # Add Reputation object to the module (Optional) - self.Reputation = ircInstance.Reputation + self.Reputation = irc_instance.Reputation + + # Add module schemas + self.Schemas = schemas + + # Add utils functions + self.Utils = utils # Create module commands (Mandatory) self.Irc.build_command(0, self.module_name, 'code', 'Display the code or key for access') @@ -97,7 +66,7 @@ class Defender(): self.__init_module() # Log the module - self.Logs.debug(f'-- Module {self.module_name} loaded ...') + self.Logs.debug(f'-- Module {self.module_name} V2 loaded ...') def __init_module(self) -> None: @@ -111,11 +80,11 @@ class Defender(): self.timeout = self.Config.API_TIMEOUT # Listes qui vont contenir les ip a scanner avec les différentes API - self.abuseipdb_UserModel: list[df.MUser] = [] - self.freeipapi_UserModel: list[df.MUser] = [] - self.cloudfilt_UserModel: list[df.MUser] = [] - self.psutil_UserModel: list[df.MUser] = [] - self.localscan_UserModel: list[df.MUser] = [] + self.Schemas.DB_ABUSEIPDB_USERS = [] + self.Schemas.DB_FREEIPAPI_USERS = [] + self.Schemas.DB_CLOUDFILT_USERS = [] + self.Schemas.DB_PSUTIL_USERS = [] + self.Schemas.DB_LOCALSCAN_USERS = [] # Variables qui indique que les threads sont en cours d'éxecutions self.abuseipdb_isRunning:bool = True @@ -137,18 +106,19 @@ class Defender(): self.cloudfilt_key = 'r1gEtjtfgRQjtNBDMxsg' # Démarrer les threads pour démarrer les api - self.Base.create_thread(func=self.thread_freeipapi_scan) - self.Base.create_thread(func=self.thread_cloudfilt_scan) - self.Base.create_thread(func=self.thread_abuseipdb_scan) - self.Base.create_thread(func=self.thread_local_scan) - self.Base.create_thread(func=self.thread_psutil_scan) - self.Base.create_thread(func=self.thread_reputation_timer) + self.Base.create_thread(func=thds.thread_freeipapi_scan, func_args=(self, )) + self.Base.create_thread(func=thds.thread_cloudfilt_scan, func_args=(self, )) + self.Base.create_thread(func=thds.thread_abuseipdb_scan, func_args=(self, )) + self.Base.create_thread(func=thds.thread_local_scan, func_args=(self, )) + self.Base.create_thread(func=thds.thread_psutil_scan, func_args=(self, )) + + self.Base.create_thread(func=thds.thread_apply_reputation_sanctions, func_args=(self, )) if self.ModConfig.autolimit == 1: - self.Base.create_thread(func=self.thread_autolimit) + self.Base.create_thread(func=thds.thread_autolimit, func_args=(self, )) if self.ModConfig.reputation == 1: - self.Protocol.sjoin(self.Config.SALON_JAIL) + self.Protocol.send_sjoin(self.Config.SALON_JAIL) self.Protocol.send2socket(f":{self.Config.SERVICE_NICKNAME} SAMODE {self.Config.SALON_JAIL} +o {self.Config.SERVICE_NICKNAME}") return None @@ -179,37 +149,56 @@ class Defender(): def __load_module_configuration(self) -> None: """### Load Module Configuration """ - try: - # Variable qui va contenir les options de configuration du module Defender - self.ModConfig = self.ModConfModel( - reputation=0, reputation_timer=1, reputation_seuil=26, reputation_score_after_release=27, - reputation_ban_all_chan=0,reputation_sg=1, - local_scan=0, psutil_scan=0, abuseipdb_scan=0, freeipapi_scan=0, cloudfilt_scan=0, - flood=0, flood_message=5, flood_time=1, flood_timer=20, - autolimit=1, autolimit_amount=3, autolimit_interval=3 - ) + # Variable qui va contenir les options de configuration du module Defender + self.ModConfig = self.Schemas.ModConfModel() - # Sync the configuration with core configuration (Mandatory) - self.Base.db_sync_core_config(self.module_name, self.ModConfig) + # Sync the configuration with core configuration (Mandatory) + self.Base.db_sync_core_config(self.module_name, self.ModConfig) - return None - - except TypeError as te: - self.Logs.critical(te) + return None def __update_configuration(self, param_key: str, param_value: str): self.Base.db_update_core_config(self.module_name, self.ModConfig, param_key, param_value) + def __onload(self): + + abuseipdb = self.Settings.get_cache('ABUSEIPDB') + freeipapi = self.Settings.get_cache('FREEIPAPI') + cloudfilt = self.Settings.get_cache('CLOUDFILT') + psutils = self.Settings.get_cache('PSUTIL') + localscan = self.Settings.get_cache('LOCALSCAN') + + if abuseipdb: + self.Schemas.DB_ABUSEIPDB_USERS = abuseipdb + + if freeipapi: + self.Schemas.DB_FREEIPAPI_USERS = freeipapi + + if cloudfilt: + self.Schemas.DB_CLOUDFILT_USERS = cloudfilt + + if psutils: + self.Schemas.DB_PSUTIL_USERS = psutils + + if localscan: + self.Schemas.DB_LOCALSCAN_USERS = localscan + def unload(self) -> None: """Cette methode sera executée a chaque désactivation ou rechargement de module """ - self.abuseipdb_UserModel: list[df.MUser] = [] - self.freeipapi_UserModel: list[df.MUser] = [] - self.cloudfilt_UserModel: list[df.MUser] = [] - self.psutil_UserModel: list[df.MUser] = [] - self.localscan_UserModel: list[df.MUser] = [] + self.Settings.set_cache('ABUSEIPDB', self.Schemas.DB_ABUSEIPDB_USERS) + self.Settings.set_cache('FREEIPAPI', self.Schemas.DB_FREEIPAPI_USERS) + self.Settings.set_cache('CLOUDFILT', self.Schemas.DB_CLOUDFILT_USERS) + self.Settings.set_cache('PSUTIL', self.Schemas.DB_PSUTIL_USERS) + self.Settings.set_cache('LOCALSCAN', self.Schemas.DB_LOCALSCAN_USERS) + + self.Schemas.DB_ABUSEIPDB_USERS = [] + self.Schemas.DB_FREEIPAPI_USERS = [] + self.Schemas.DB_CLOUDFILT_USERS = [] + self.Schemas.DB_PSUTIL_USERS = [] + self.Schemas.DB_LOCALSCAN_USERS = [] self.abuseipdb_isRunning:bool = False self.freeipapi_isRunning:bool = False @@ -218,6 +207,7 @@ class Defender(): self.localscan_isRunning:bool = False self.reputationTimer_isRunning:bool = False self.autolimit_isRunning: bool = False + return None def insert_db_trusted(self, uid: str, nickname:str) -> None: @@ -249,7 +239,7 @@ class Defender(): for channel in channels: chan = channel[0] - self.Protocol.sjoin(chan) + self.Protocol.send_sjoin(chan) if chan == jail_chan: self.Protocol.send2socket(f":{service_id} SAMODE {jail_chan} +{dumodes} {dnickname}") self.Protocol.send2socket(f":{service_id} MODE {jail_chan} +{jail_chan_mode}") @@ -259,228 +249,6 @@ class Defender(): except Exception as err: self.Logs.error(f"General Error: {err}") - def get_user_uptime_in_minutes(self, uidornickname:str) -> float: - """Retourne depuis quand l'utilisateur est connecté (en secondes ). - - Args: - uid (str): le uid ou le nickname de l'utilisateur - - Returns: - int: Temps de connexion de l'utilisateur en secondes - """ - - get_user = self.User.get_User(uidornickname) - if get_user is None: - return 0 - - # Convertir la date enregistrée dans UID_DB en un objet {datetime} - connected_time_string = get_user.connexion_datetime - - if isinstance(connected_time_string, datetime): - connected_time = connected_time_string - else: - connected_time = datetime.strptime(connected_time_string, "%Y-%m-%d %H:%M:%S.%f") - - # Quelle heure est-il ? - current_datetime = datetime.now() - - uptime = current_datetime - connected_time - convert_to_minutes = uptime.seconds / 60 - uptime_minutes = round(number=convert_to_minutes, ndigits=2) - - return uptime_minutes - - def system_reputation(self, uid: str)-> None: - # Reputation security - # - Activation ou désactivation du système --> OK - # - Le user sera en mesure de changer la limite de la réputation --> OK - # - Defender devra envoyer l'utilisateur sur un salon défini dans la configuration, {jail_chan} - # - Defender devra bloquer cet utilisateur sur le salon qui sera en mode (+m) - # - Defender devra envoyer un message du type "Merci de taper cette comande /msg {nomdudefender} {un code généré aléatoirement} - # - Defender devra reconnaître le code - # - Defender devra libérer l'utilisateur et l'envoyer vers un salon défini dans la configuration {welcome_chan} - # - Defender devra intégrer une liste d'IDs (pseudo/host) exemptés de 'Reputation security' malgré un score de rép. faible et un pseudo non enregistré. - try: - - get_reputation = self.Reputation.get_Reputation(uid) - - if get_reputation is None: - self.Logs.error(f'UID {uid} has not been found') - return False - - salon_logs = self.Config.SERVICE_CHANLOG - salon_jail = self.Config.SALON_JAIL - - code = get_reputation.secret_code - jailed_nickname = get_reputation.nickname - jailed_score = get_reputation.score_connexion - - color_red = self.Config.COLORS.red - color_black = self.Config.COLORS.black - color_bold = self.Config.COLORS.bold - nogc = self.Config.COLORS.nogc - service_id = self.Config.SERVICE_ID - service_prefix = self.Config.SERVICE_PREFIX - reputation_ban_all_chan = self.ModConfig.reputation_ban_all_chan - - if not get_reputation.isWebirc: - # Si le user ne vient pas de webIrc - - self.Protocol.send_sajoin(nick_to_sajoin=jailed_nickname, channel_name=salon_jail) - self.Protocol.send_priv_msg(nick_from=self.Config.SERVICE_NICKNAME, - msg=f" [{color_red} REPUTATION {nogc}] : Connexion de {jailed_nickname} ({jailed_score}) ==> {salon_jail}", - channel=salon_logs - ) - self.Protocol.send_notice( - nick_from=self.Config.SERVICE_NICKNAME, - nick_to=jailed_nickname, - msg=f"[{color_red} {jailed_nickname} {color_black}] : Merci de tapez la commande suivante {color_bold}{service_prefix}code {code}{color_bold}" - ) - if reputation_ban_all_chan == 1: - for chan in self.Channel.UID_CHANNEL_DB: - if chan.name != salon_jail: - self.Protocol.send2socket(f":{service_id} MODE {chan.name} +b {jailed_nickname}!*@*") - self.Protocol.send2socket(f":{service_id} KICK {chan.name} {jailed_nickname}") - - self.Logs.info(f"system_reputation : {jailed_nickname} à été capturé par le système de réputation") - # self.Irc.create_ping_timer(int(self.ModConfig.reputation_timer) * 60, 'Defender', 'system_reputation_timer') - # self.Base.create_timer(int(self.ModConfig.reputation_timer) * 60, self.system_reputation_timer) - else: - self.Logs.info(f"system_reputation : {jailed_nickname} à été supprimé du système de réputation car connecté via WebIrc ou il est dans la 'Trusted list'") - self.Reputation.delete(uid) - - except IndexError as e: - self.Logs.error(f"system_reputation : {str(e)}") - - def system_reputation_timer(self) -> None: - try: - reputation_flag = self.ModConfig.reputation - reputation_timer = self.ModConfig.reputation_timer - reputation_seuil = self.ModConfig.reputation_seuil - ban_all_chan = self.ModConfig.reputation_ban_all_chan - service_id = self.Config.SERVICE_ID - dchanlog = self.Config.SERVICE_CHANLOG - color_red = self.Config.COLORS.red - nogc = self.Config.COLORS.nogc - salon_jail = self.Config.SALON_JAIL - - if reputation_flag == 0: - return None - elif reputation_timer == 0: - return None - - uid_to_clean = [] - - for user in self.Reputation.UID_REPUTATION_DB: - if not user.isWebirc: # Si il ne vient pas de WebIRC - if self.get_user_uptime_in_minutes(user.uid) >= reputation_timer and int(user.score_connexion) <= int(reputation_seuil): - self.Protocol.send_priv_msg( - nick_from=service_id, - msg=f"[{color_red} REPUTATION {nogc}] : Action sur {user.nickname} aprés {str(reputation_timer)} minutes d'inactivité", - channel=dchanlog - ) - self.Protocol.send2socket(f":{service_id} KILL {user.nickname} After {str(reputation_timer)} minutes of inactivity you should reconnect and type the password code") - self.Protocol.send2socket(f":{self.Config.SERVEUR_LINK} REPUTATION {user.remote_ip} 0") - - self.Logs.info(f"Nickname: {user.nickname} KILLED after {str(reputation_timer)} minutes of inactivity") - uid_to_clean.append(user.uid) - - for uid in uid_to_clean: - # Suppression des éléments dans {UID_DB} et {REPUTATION_DB} - for chan in self.Channel.UID_CHANNEL_DB: - if chan.name != salon_jail and ban_all_chan == 1: - get_user_reputation = self.Reputation.get_Reputation(uid) - self.Protocol.send2socket(f":{service_id} MODE {chan.name} -b {get_user_reputation.nickname}!*@*") - - # Lorsqu'un utilisateur quitte, il doit être supprimé de {UID_DB}. - self.Channel.delete_user_from_all_channel(uid) - self.Reputation.delete(uid) - self.User.delete(uid) - - except AssertionError as ae: - self.Logs.error(f'Assertion Error -> {ae}') - - def thread_reputation_timer(self) -> None: - try: - while self.reputationTimer_isRunning: - self.system_reputation_timer() - time.sleep(5) - - return None - except ValueError as ve: - self.Logs.error(f"thread_reputation_timer Error : {ve}") - - def _execute_flood_action(self, action:str, channel:str) -> None: - """DO NOT EXECUTE THIS FUNCTION WITHOUT THREADING - - Args: - action (str): _description_ - timer (int): _description_ - nickname (str): _description_ - channel (str): _description_ - - Returns: - _type_: _description_ - """ - service_id = self.Config.SERVICE_ID - match action: - case 'mode-m': - # Action -m sur le salon - self.Protocol.send2socket(f":{service_id} MODE {channel} -m") - case _: - pass - - return None - - def flood(self, detected_user:str, channel:str) -> None: - - if self.ModConfig.flood == 0: - return None - - if not self.Channel.Is_Channel(channelToCheck=channel): - return None - - flood_time = self.ModConfig.flood_time - flood_message = self.ModConfig.flood_message - flood_timer = self.ModConfig.flood_timer - service_id = self.Config.SERVICE_ID - dnickname = self.Config.SERVICE_NICKNAME - color_red = self.Config.COLORS.red - color_bold = self.Config.COLORS.bold - - get_detected_uid = self.User.get_uid(detected_user) - get_detected_nickname = self.User.get_nickname(detected_user) - - unixtime = self.Base.get_unixtime() - get_diff_secondes = 0 - - if get_detected_uid not in self.flood_system: - self.flood_system[get_detected_uid] = { - 'nbr_msg': 0, - 'first_msg_time': unixtime - } - - self.flood_system[get_detected_uid]['nbr_msg'] += 1 - get_diff_secondes = unixtime - self.flood_system[get_detected_uid]['first_msg_time'] - if get_diff_secondes > flood_time: - self.flood_system[get_detected_uid]['first_msg_time'] = unixtime - self.flood_system[get_detected_uid]['nbr_msg'] = 0 - get_diff_secondes = unixtime - self.flood_system[get_detected_uid]['first_msg_time'] - - elif self.flood_system[get_detected_uid]['nbr_msg'] > flood_message: - self.Irc.Base.logs.info('system de flood detecté') - self.Protocol.send_priv_msg( - nick_from=dnickname, - msg=f"{color_red} {color_bold} Flood detected. Apply the +m mode (Ô_o)", - channel=channel - ) - self.Protocol.send2socket(f":{service_id} MODE {channel} +m") - self.Irc.Base.logs.info(f'FLOOD Détecté sur {get_detected_nickname} mode +m appliqué sur le salon {channel}') - self.flood_system[get_detected_uid]['nbr_msg'] = 0 - self.flood_system[get_detected_uid]['first_msg_time'] = unixtime - - self.Base.create_timer(flood_timer, self._execute_flood_action, ('mode-m', channel)) - def run_db_action_timer(self, wait_for: float = 0) -> None: query = f"SELECT param_key FROM {self.Config.TABLE_CONFIG}" @@ -504,673 +272,53 @@ class Defender(): return None - def scan_ports(self, userModel: df.MUser) -> None: - """local_scan - - Args: - userModel (UserModel): _description_ - """ - User = userModel - remote_ip = User.remote_ip - username = User.username - hostname = User.hostname - nickname = User.nickname - fullname = f'{nickname}!{username}@{hostname}' - - if remote_ip in self.Config.WHITELISTED_IP: - return None - - for port in self.Config.PORTS_TO_SCAN: - try: - newSocket = '' - newSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM or socket.SOCK_NONBLOCK) - newSocket.settimeout(0.5) - - connection = (remote_ip, self.Base.int_if_possible(port)) - newSocket.connect(connection) - - self.Protocol.send_priv_msg( - nick_from=self.Config.SERVICE_NICKNAME, - msg=f"[ {self.Config.COLORS.red}PROXY_SCAN{self.Config.COLORS.nogc} ] {fullname} ({remote_ip}) : Port [{str(port)}] ouvert sur l'adresse ip [{remote_ip}]", - channel=self.Config.SERVICE_CHANLOG - ) - # print(f"=======> Le port {str(port)} est ouvert !!") - self.Base.running_sockets.append(newSocket) - # print(newSocket) - newSocket.shutdown(socket.SHUT_RDWR) - newSocket.close() - - except (socket.timeout, ConnectionRefusedError): - self.Logs.info(f"Le port {remote_ip}:{str(port)} est fermé") - except AttributeError as ae: - self.Logs.warning(f"AttributeError ({remote_ip}): {ae}") - except socket.gaierror as err: - self.Logs.warning(f"Address Info Error ({remote_ip}): {err}") - finally: - # newSocket.shutdown(socket.SHUT_RDWR) - newSocket.close() - self.Logs.info('=======> Fermeture de la socket') - - def thread_local_scan(self) -> None: - try: - while self.localscan_isRunning: - - list_to_remove:list = [] - for user in self.localscan_UserModel: - self.scan_ports(user) - list_to_remove.append(user) - time.sleep(1) - - for user_model in list_to_remove: - self.localscan_UserModel.remove(user_model) - - time.sleep(1) - - return None - except ValueError as ve: - self.Logs.warning(f"thread_local_scan Error : {ve}") - - def get_ports_connexion(self, userModel: df.MUser) -> list[int]: - """psutil_scan for Linux (should be run on the same location as the unrealircd server) - - Args: - userModel (UserModel): The User Model Object - - Returns: - list[int]: list of ports - """ - try: - User = userModel - remote_ip = User.remote_ip - username = User.username - hostname = User.hostname - nickname = User.nickname - - if remote_ip in self.Config.WHITELISTED_IP: - return None - - connections = psutil.net_connections(kind='inet') - fullname = f'{nickname}!{username}@{hostname}' - - matching_ports = [conn.raddr.port for conn in connections if conn.raddr and conn.raddr.ip == remote_ip] - self.Logs.info(f"Connexion of {fullname} ({remote_ip}) using ports : {str(matching_ports)}") - - if matching_ports: - self.Protocol.send_priv_msg( - nick_from=self.Config.SERVICE_NICKNAME, - msg=f"[ {self.Config.COLORS.red}PSUTIL_SCAN{self.Config.COLORS.black} ] {fullname} ({remote_ip}) : is using ports {matching_ports}", - channel=self.Config.SERVICE_CHANLOG - ) - - return matching_ports - - except psutil.AccessDenied as ad: - self.Logs.critical(f'psutil_scan: Permission error: {ad}') - - def thread_psutil_scan(self) -> None: - try: - - while self.psutil_isRunning: - - list_to_remove:list = [] - for user in self.psutil_UserModel: - self.get_ports_connexion(user) - list_to_remove.append(user) - time.sleep(1) - - for user_model in list_to_remove: - self.psutil_UserModel.remove(user_model) - - time.sleep(1) - - return None - except ValueError as ve: - self.Logs.warning(f"thread_psutil_scan Error : {ve}") - - def abuseipdb_scan(self, userModel: df.MUser) -> Union[dict[str, any], None]: - """Analyse l'ip avec AbuseIpDB - Cette methode devra etre lancer toujours via un thread ou un timer. - Args: - userModel (UserModel): l'objet User qui contient l'ip - - Returns: - dict[str, any] | None: les informations du provider - keys : 'score', 'country', 'isTor', 'totalReports' - """ - User = userModel - remote_ip = User.remote_ip - username = User.username - hostname = User.hostname - nickname = User.nickname - - if remote_ip in self.Config.WHITELISTED_IP: - return None - if self.ModConfig.abuseipdb_scan == 0: - return None - - if self.abuseipdb_key == '': - return None - - url = 'https://api.abuseipdb.com/api/v2/check' - querystring = { - 'ipAddress': remote_ip, - 'maxAgeInDays': '90' - } - - headers = { - 'Accept': 'application/json', - 'Key': self.abuseipdb_key - } - - try: - response = requests.request(method='GET', url=url, headers=headers, params=querystring, timeout=self.timeout) - - # Formatted output - decodedResponse = json.loads(response.text) - - if 'data' not in decodedResponse: - return None - - result = { - 'score': decodedResponse['data']['abuseConfidenceScore'], - 'country': decodedResponse['data']['countryCode'], - 'isTor': decodedResponse['data']['isTor'], - 'totalReports': decodedResponse['data']['totalReports'] - } - - service_id = self.Config.SERVICE_ID - service_chanlog = self.Config.SERVICE_CHANLOG - color_red = self.Config.COLORS.red - nogc = self.Config.COLORS.nogc - - # pseudo!ident@host - fullname = f'{nickname}!{username}@{hostname}' - - self.Protocol.send_priv_msg( - nick_from=service_id, - msg=f"[ {color_red}ABUSEIPDB_SCAN{nogc} ] : Connexion de {fullname} ({remote_ip}) ==> Score: {str(result['score'])} | Country : {result['country']} | Tor : {str(result['isTor'])} | Total Reports : {str(result['totalReports'])}", - channel=service_chanlog - ) - - if result['isTor']: - self.Protocol.send2socket(f":{service_id} GLINE +*@{remote_ip} {self.Config.GLINE_DURATION} This server do not allow Tor connexions {str(result['isTor'])} - Detected by Abuseipdb") - elif result['score'] >= 95: - self.Protocol.send2socket(f":{service_id} GLINE +*@{remote_ip} {self.Config.GLINE_DURATION} You were banned from this server because your abuse score is = {str(result['score'])} - Detected by Abuseipdb") - - response.close() - - return result - except KeyError as ke: - self.Logs.error(f"AbuseIpDb KeyError : {ke}") - except requests.ReadTimeout as rt: - self.Logs.error(f"AbuseIpDb Timeout : {rt}") - except requests.ConnectionError as ce: - self.Logs.error(f"AbuseIpDb Connection Error : {ce}") - except Exception as err: - self.Logs.error(f"General Error Abuseipdb : {err}") - - def thread_abuseipdb_scan(self) -> None: - try: - - while self.abuseipdb_isRunning: - - list_to_remove: list = [] - for user in self.abuseipdb_UserModel: - self.abuseipdb_scan(user) - list_to_remove.append(user) - time.sleep(1) - - for user_model in list_to_remove: - self.abuseipdb_UserModel.remove(user_model) - - time.sleep(1) - - return None - except ValueError as ve: - self.Logs.error(f"thread_abuseipdb_scan Error : {ve}") - - def freeipapi_scan(self, userModel: df.MUser) -> Union[dict[str, any], None]: - """Analyse l'ip avec Freeipapi - Cette methode devra etre lancer toujours via un thread ou un timer. - Args: - remote_ip (_type_): l'ip a analyser - - Returns: - dict[str, any] | None: les informations du provider - keys : 'countryCode', 'isProxy' - """ - User = userModel - remote_ip = User.remote_ip - username = User.username - hostname = User.hostname - nickname = User.nickname - - if remote_ip in self.Config.WHITELISTED_IP: - return None - if self.ModConfig.freeipapi_scan == 0: - return None - - service_id = self.Config.SERVICE_ID - service_chanlog = self.Config.SERVICE_CHANLOG - color_red = self.Config.COLORS.red - nogc = self.Config.COLORS.nogc - - url = f'https://freeipapi.com/api/json/{remote_ip}' - - headers = { - 'Accept': 'application/json', - } - - try: - response = requests.request(method='GET', url=url, headers=headers, timeout=self.timeout) - - # Formatted output - decodedResponse = json.loads(response.text) - - status_code = response.status_code - if status_code == 429: - self.Logs.warning('Too Many Requests - The rate limit for the API has been exceeded.') - return None - elif status_code != 200: - self.Logs.warning(f'status code = {str(status_code)}') - return None - - result = { - 'countryCode': decodedResponse['countryCode'] if 'countryCode' in decodedResponse else None, - 'isProxy': decodedResponse['isProxy'] if 'isProxy' in decodedResponse else None - } - - # pseudo!ident@host - fullname = f'{nickname}!{username}@{hostname}' - - self.Protocol.send_priv_msg( - nick_from=service_id, - msg=f"[ {color_red}FREEIPAPI_SCAN{nogc} ] : Connexion de {fullname} ({remote_ip}) ==> Proxy: {str(result['isProxy'])} | Country : {str(result['countryCode'])}", - channel=service_chanlog - ) - - if result['isProxy']: - self.Protocol.send2socket(f":{service_id} GLINE +*@{remote_ip} {self.Config.GLINE_DURATION} This server do not allow proxy connexions {str(result['isProxy'])} - detected by freeipapi") - response.close() - - return result - except KeyError as ke: - self.Logs.error(f"FREEIPAPI_SCAN KeyError : {ke}") - except Exception as err: - self.Logs.error(f"General Error Freeipapi : {err}") - - def thread_freeipapi_scan(self) -> None: - try: - - while self.freeipapi_isRunning: - - list_to_remove: list[df.MUser] = [] - for user in self.freeipapi_UserModel: - self.freeipapi_scan(user) - list_to_remove.append(user) - time.sleep(1) - - for user_model in list_to_remove: - self.freeipapi_UserModel.remove(user_model) - - time.sleep(1) - - return None - except ValueError as ve: - self.Logs.error(f"thread_freeipapi_scan Error : {ve}") - - def cloudfilt_scan(self, userModel: df.MUser) -> Union[dict[str, any], None]: - """Analyse l'ip avec cloudfilt - Cette methode devra etre lancer toujours via un thread ou un timer. - Args: - remote_ip (_type_): l'ip a analyser - - Returns: - dict[str, any] | None: les informations du provider - keys : 'countryCode', 'isProxy' - """ - User = userModel - remote_ip = User.remote_ip - username = User.username - hostname = User.hostname - nickname = User.nickname - - if remote_ip in self.Config.WHITELISTED_IP: - return None - if self.ModConfig.cloudfilt_scan == 0: - return None - if self.cloudfilt_key == '': - return None - - service_id = self.Config.SERVICE_ID - service_chanlog = self.Config.SERVICE_CHANLOG - color_red = self.Config.COLORS.red - nogc = self.Config.COLORS.nogc - - url = "https://developers18334.cloudfilt.com/" - - data = { - 'ip': remote_ip, - 'key': self.cloudfilt_key - } - - try: - response = requests.post(url=url, data=data) - # Formatted output - decodedResponse = json.loads(response.text) - status_code = response.status_code - if status_code != 200: - self.Logs.warning(f'Error connecting to cloudfilt API | Code: {str(status_code)}') - return None - - result = { - 'countryiso': decodedResponse['countryiso'] if 'countryiso' in decodedResponse else None, - 'listed': decodedResponse['listed'] if 'listed' in decodedResponse else None, - 'listed_by': decodedResponse['listed_by'] if 'listed_by' in decodedResponse else None, - 'host': decodedResponse['host'] if 'host' in decodedResponse else None - } - - # pseudo!ident@host - fullname = f'{nickname}!{username}@{hostname}' - - self.Protocol.send_priv_msg( - nick_from=service_id, - msg=f"[ {color_red}CLOUDFILT_SCAN{nogc} ] : Connexion de {fullname} ({remote_ip}) ==> Host: {str(result['host'])} | country: {str(result['countryiso'])} | listed: {str(result['listed'])} | listed by : {str(result['listed_by'])}", - channel=service_chanlog - ) - - if result['listed']: - self.Protocol.send2socket(f":{service_id} GLINE +*@{remote_ip} {self.Config.GLINE_DURATION} You connexion is listed as dangerous {str(result['listed'])} {str(result['listed_by'])} - detected by cloudfilt") - - response.close() - - return result - except KeyError as ke: - self.Logs.error(f"CLOUDFILT_SCAN KeyError : {ke}") - return None - - def thread_cloudfilt_scan(self) -> None: - try: - - while self.cloudfilt_isRunning: - - list_to_remove:list = [] - for user in self.cloudfilt_UserModel: - self.cloudfilt_scan(user) - list_to_remove.append(user) - time.sleep(1) - - for user_model in list_to_remove: - self.cloudfilt_UserModel.remove(user_model) - - time.sleep(1) - - return None - except ValueError as ve: - self.Logs.error(f"Thread_cloudfilt_scan Error : {ve}") - - def thread_autolimit(self) -> None: - - if self.ModConfig.autolimit == 0: - self.Logs.debug("autolimit deactivated ... canceling the thread") - return None - - while self.Irc.autolimit_started: - time.sleep(0.2) - - self.Irc.autolimit_started = True - init_amount = self.ModConfig.autolimit_amount - INIT = 1 - - # Copy Channels to a list of dict - chanObj_copy: list[dict[str, int]] = [{"name": c.name, "uids_count": len(c.uids)} for c in self.Channel.UID_CHANNEL_DB] - chan_list: list[str] = [c.name for c in self.Channel.UID_CHANNEL_DB] - - while self.autolimit_isRunning: - - if self.ModConfig.autolimit == 0: - self.Logs.debug("autolimit deactivated ... stopping the current thread") - break - - for chan in self.Channel.UID_CHANNEL_DB: - for chan_copy in chanObj_copy: - if chan_copy["name"] == chan.name and len(chan.uids) != chan_copy["uids_count"]: - self.Protocol.send2socket(f":{self.Config.SERVICE_ID} MODE {chan.name} +l {len(chan.uids) + self.ModConfig.autolimit_amount}") - chan_copy["uids_count"] = len(chan.uids) - - if chan.name not in chan_list: - chan_list.append(chan.name) - chanObj_copy.append({"name": chan.name, "uids_count": 0}) - - # Verifier si un salon a été vidé - current_chan_in_list = [d.name for d in self.Channel.UID_CHANNEL_DB] - for c in chan_list: - if c not in current_chan_in_list: - chan_list.remove(c) - - # Si c'est la premiere execution - if INIT == 1: - for chan in self.Channel.UID_CHANNEL_DB: - self.Protocol.send2socket(f":{self.Config.SERVICE_ID} MODE {chan.name} +l {len(chan.uids) + self.ModConfig.autolimit_amount}") - - # Si le nouveau amount est différent de l'initial - if init_amount != self.ModConfig.autolimit_amount: - init_amount = self.ModConfig.autolimit_amount - for chan in self.Channel.UID_CHANNEL_DB: - self.Protocol.send2socket(f":{self.Config.SERVICE_ID} MODE {chan.name} +l {len(chan.uids) + self.ModConfig.autolimit_amount}") - - INIT = 0 - - if self.autolimit_isRunning: - time.sleep(self.ModConfig.autolimit_interval) - - for chan in self.Channel.UID_CHANNEL_DB: - self.Protocol.send2socket(f":{self.Config.SERVICE_ID} MODE {chan.name} -l") - - self.Irc.autolimit_started = False - - return None - def cmd(self, data: list[str]) -> None: try: - service_id = self.Config.SERVICE_ID # Defender serveur id - cmd = list(data).copy() - - match cmd[1]: - - case 'REPUTATION': - # :001 REPUTATION 8.8.8.8 118 - try: - self.reputation_first_connexion['ip'] = cmd[2] - self.reputation_first_connexion['score'] = cmd[3] - if str(cmd[3]).find('*') != -1: - # If the reputation changed, we do not need to scan the IP - return None - - if not self.Base.is_valid_ip(cmd[2]): - return None - - # Possibilité de déclancher les bans a ce niveau. - except IndexError as ie: - self.Logs.error(f'cmd reputation: index error: {ie}') - - if len(cmd) < 3: + if not data or len(data) < 2: return None - match cmd[2]: + cmd = data.copy() if isinstance(data, list) else list(data).copy() + index, command = self.Irc.Protocol.get_ircd_protocol_poisition(cmd) + if index == -1: + return None + + match command: + + case 'REPUTATION': + self.Utils.handle_on_reputation(self, cmd) + return None case 'MODE': - # ['...', ':001XSCU0Q', 'MODE', '#jail', '+b', '~security-group:unknown-users'] - # ['@unrealircd.org/...', ':001C0MF01', 'MODE', '#services', '+l', '1'] - - channel = str(cmd[3]) - mode = str(cmd[4]) - group_to_check = str(cmd[5:]) - group_to_unban = '~security-group:unknown-users' - - if self.ModConfig.autolimit == 1: - if mode == '+l' or mode == '-l': - chan = self.Channel.get_Channel(channel) - self.Protocol.send2socket(f":{self.Config.SERVICE_ID} MODE {chan.name} +l {len(chan.uids) + self.ModConfig.autolimit_amount}") - - if self.Config.SALON_JAIL == channel: - if mode == '+b' and group_to_unban in group_to_check: - self.Protocol.send2socket(f":{service_id} MODE {self.Config.SALON_JAIL} -b ~security-group:unknown-users") - self.Protocol.send2socket(f":{service_id} MODE {self.Config.SALON_JAIL} -eee ~security-group:webirc-users ~security-group:known-users ~security-group:websocket-users") + self.Utils.handle_on_mode(self, cmd) + return None case 'PRIVMSG': - cmd.pop(0) - user_trigger = str(cmd[0]).replace(':','') - channel = cmd[2] - find_nickname = self.User.get_nickname(user_trigger) - self.flood(find_nickname, channel) + self.Utils.handle_on_privmsg(self, cmd) + return None case 'UID': - # If Init then do nothing - if self.Config.DEFENDER_INIT == 1: - return None - - # Supprimer la premiere valeur et finir le code normalement - cmd.pop(0) - - # Get User information - _User = self.User.get_User(str(cmd[7])) - - # If user is not service or IrcOp then scan them - if not re.match(r'^.*[S|o?].*$', _User.umodes): - self.abuseipdb_UserModel.append(_User) if self.ModConfig.abuseipdb_scan == 1 and _User.remote_ip not in self.Config.WHITELISTED_IP else None - self.freeipapi_UserModel.append(_User) if self.ModConfig.freeipapi_scan == 1 and _User.remote_ip not in self.Config.WHITELISTED_IP else None - self.cloudfilt_UserModel.append(_User) if self.ModConfig.cloudfilt_scan == 1 and _User.remote_ip not in self.Config.WHITELISTED_IP else None - self.psutil_UserModel.append(_User) if self.ModConfig.psutil_scan == 1 and _User.remote_ip not in self.Config.WHITELISTED_IP else None - self.localscan_UserModel.append(_User) if self.ModConfig.local_scan == 1 and _User.remote_ip not in self.Config.WHITELISTED_IP else None - - if _User is None: - self.Logs.critical(f'This UID: [{cmd[7]}] is not available please check why') - return None - - reputation_flag = self.ModConfig.reputation - reputation_seuil = self.ModConfig.reputation_seuil - - if self.Config.DEFENDER_INIT == 0: - # Si le user n'es pas un service ni un IrcOP - if not re.match(r'^.*[S|o?].*$', _User.umodes): - if reputation_flag == 1 and _User.score_connexion <= reputation_seuil: - # currentDateTime = self.Base.get_datetime() - self.Reputation.insert( - self.Loader.Definition.MReputation( - **_User.__dict__, - secret_code=self.Base.get_random(8) - # uid=_User.uid, nickname=_User.nickname, username=_User.username, realname=_User.realname, - # hostname=_User.hostname, umodes=_User.umodes, vhost=_User.vhost, ip=_User.remote_ip, score=_User.score_connexion, - # secret_code=self.Base.get_random(8), isWebirc=_User.isWebirc, isWebsocket=_User.isWebsocket, connected_datetime=currentDateTime, - # updated_datetime=currentDateTime - ) - ) - if self.Reputation.is_exist(_User.uid): - if reputation_flag == 1 and _User.score_connexion <= reputation_seuil: - self.system_reputation(_User.uid) - self.Logs.info('Démarrer le systeme de reputation') + self.Utils.handle_on_uid(self, cmd) + return None case 'SJOIN': - # ['@msgid=F9B7JeHL5pj9nN57cJ5pEr;time=2023-12-28T20:47:24.305Z', ':001', 'SJOIN', '1702138958', '#welcome', ':0015L1AHL'] - try: - cmd.pop(0) - parsed_chan = cmd[3] if self.Channel.Is_Channel(cmd[3]) else None - if self.ModConfig.reputation == 1: - parsed_UID = self.User.clean_uid(cmd[4]) - get_reputation = self.Reputation.get_Reputation(parsed_UID) - - if parsed_chan != self.Config.SALON_JAIL: - self.Protocol.send2socket(f":{service_id} MODE {parsed_chan} +b ~security-group:unknown-users") - self.Protocol.send2socket(f":{service_id} MODE {parsed_chan} +eee ~security-group:webirc-users ~security-group:known-users ~security-group:websocket-users") - - if get_reputation is not None: - isWebirc = get_reputation.isWebirc - - if not isWebirc: - if parsed_chan != self.Config.SALON_JAIL: - self.Protocol.send_sapart(nick_to_sapart=get_reputation.nickname, channel_name=parsed_chan) - - if self.ModConfig.reputation_ban_all_chan == 1 and not isWebirc: - if parsed_chan != self.Config.SALON_JAIL: - self.Protocol.send2socket(f":{service_id} MODE {parsed_chan} +b {get_reputation.nickname}!*@*") - self.Protocol.send2socket(f":{service_id} KICK {parsed_chan} {get_reputation.nickname}") - - self.Logs.debug(f'SJOIN parsed_uid : {parsed_UID}') - - except KeyError as ke: - self.Logs.error(f"key error SJOIN : {ke}") + self.Utils.handle_on_sjoin(self, cmd) + return None case 'SLOG': - # self.Base.scan_ports(cmd[7]) - cmd.pop(0) - - if not self.Base.is_valid_ip(cmd[7]): - return None - - # if self.ModConfig.local_scan == 1 and not cmd[7] in self.Config.WHITELISTED_IP: - # self.localscan_remote_ip.append(cmd[7]) - - # if self.ModConfig.psutil_scan == 1 and not cmd[7] in self.Config.WHITELISTED_IP: - # self.psutil_remote_ip.append(cmd[7]) - - # if self.ModConfig.abuseipdb_scan == 1 and not cmd[7] in self.Config.WHITELISTED_IP: - # self.abuseipdb_remote_ip.append(cmd[7]) - - # if self.ModConfig.freeipapi_scan == 1 and not cmd[7] in self.Config.WHITELISTED_IP: - # self.freeipapi_remote_ip.append(cmd[7]) - - # if self.ModConfig.cloudfilt_scan == 1 and not cmd[7] in self.Config.WHITELISTED_IP: - # self.cloudfilt_remote_ip.append(cmd[7]) + self.Utils.handle_on_slog(self, cmd) + return None case 'NICK': - # :0010BS24L NICK [NEWNICK] 1697917711 - # Changement de nickname - try: - cmd.pop(0) - uid = str(cmd[0]).replace(':','') - get_Reputation = self.Reputation.get_Reputation(uid) - jail_salon = self.Config.SALON_JAIL - service_id = self.Config.SERVICE_ID - - if get_Reputation is None: - self.Logs.debug(f'This UID: {uid} is not listed in the reputation dataclass') - return None - - # Update the new nickname - oldnick = get_Reputation.nickname - newnickname = cmd[2] - get_Reputation.nickname = newnickname - - # If ban in all channel is ON then unban old nickname an ban the new nickname - if self.ModConfig.reputation_ban_all_chan == 1: - for chan in self.Channel.UID_CHANNEL_DB: - if chan.name != jail_salon: - self.Protocol.send2socket(f":{service_id} MODE {chan.name} -b {oldnick}!*@*") - self.Protocol.send2socket(f":{service_id} MODE {chan.name} +b {newnickname}!*@*") - - except KeyError as ke: - self.Logs.error(f'cmd - NICK - KeyError: {ke}') + self.Utils.handle_on_nick(self, cmd) + return None case 'QUIT': - # :001N1WD7L QUIT :Quit: free_znc_1 - cmd.pop(0) - ban_all_chan = self.Base.int_if_possible(self.ModConfig.reputation_ban_all_chan) - user_id = str(cmd[0]).replace(':','') - final_UID = user_id + self.Utils.handle_on_quit(self, cmd) + return None - jail_salon = self.Config.SALON_JAIL - service_id = self.Config.SERVICE_ID - - get_user_reputation = self.Reputation.get_Reputation(final_UID) - - if get_user_reputation is not None: - final_nickname = get_user_reputation.nickname - for chan in self.Channel.UID_CHANNEL_DB: - if chan.name != jail_salon and ban_all_chan == 1: - self.Protocol.send2socket(f":{service_id} MODE {chan.name} -b {final_nickname}!*@*") - self.Reputation.delete(final_UID) + case _: + return None except KeyError as ke: self.Logs.error(f"{ke} / {cmd} / length {str(len(cmd))}") @@ -1178,13 +326,13 @@ class Defender(): self.Logs.error(f"{ie} / {cmd} / length {str(len(cmd))}") except Exception as err: self.Logs.error(f"General Error: {err}") + traceback.print_exc() def hcmds(self, user:str, channel: any, cmd: list, fullcmd: list = []) -> None: command = str(cmd[0]).lower() fromuser = user - fromchannel = channel if self.Channel.Is_Channel(channel) else None - channel = fromchannel + channel = fromchannel = channel if self.Channel.is_valid_channel(channel) else None dnickname = self.Config.SERVICE_NICKNAME # Defender nickname dchanlog = self.Config.SERVICE_CHANLOG # Defender chan log @@ -1289,7 +437,7 @@ class Defender(): if self.ModConfig.autolimit == 0: self.__update_configuration('autolimit', 1) self.autolimit_isRunning = True - self.Base.create_thread(self.thread_autolimit) + self.Base.create_thread(func=thds.thread_autolimit, func_args=(self, )) self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[{self.Config.COLORS.green}AUTOLIMIT{self.Config.COLORS.nogc}] Activated", channel=self.Config.SERVICE_CHANLOG) else: self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[{self.Config.COLORS.red}AUTOLIMIT{self.Config.COLORS.nogc}] Already activated", channel=self.Config.SERVICE_CHANLOG) @@ -1783,15 +931,15 @@ class Defender(): case 'info': try: + if len(cmd) < 2: + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Syntax. /msg {dnickname} INFO [nickname]") + return None + nickoruid = cmd[1] UserObject = self.User.get_User(nickoruid) if UserObject is not None: - channels: list = [] - for chan in self.Channel.UID_CHANNEL_DB: - for uid_in_chan in chan.uids: - if self.Base.clean_uid(uid_in_chan) == UserObject.uid: - channels.append(chan.name) + channels: list = [chan.name for chan in self.Channel.UID_CHANNEL_DB for uid_in_chan in chan.uids if self.Loader.Utils.clean_uid(uid_in_chan) == UserObject.uid] self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' UID : {UserObject.uid}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' NICKNAME : {UserObject.nickname}') @@ -1808,7 +956,7 @@ class Defender(): self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' CHANNELS : {channels}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' CONNECTION TIME : {UserObject.connexion_datetime}') else: - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f":{dnickname} NOTICE {fromuser} : This user {nickoruid} doesn't exist") + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"This user {nickoruid} doesn't exist") except KeyError as ke: self.Logs.warning(f"Key error info user : {ke}") @@ -1816,16 +964,17 @@ class Defender(): case 'sentinel': # .sentinel on activation = str(cmd[1]).lower() - service_id = self.Config.SERVICE_ID - channel_to_dont_quit = [self.Config.SALON_JAIL, self.Config.SERVICE_CHANLOG] if activation == 'on': for chan in self.Channel.UID_CHANNEL_DB: if chan.name not in channel_to_dont_quit: self.Protocol.send_join_chan(uidornickname=dnickname, channel=chan.name) + return None + if activation == 'off': for chan in self.Channel.UID_CHANNEL_DB: if chan.name not in channel_to_dont_quit: - self.Protocol.part(uidornickname=dnickname, channel=chan.name) + self.Protocol.send_part_chan(uidornickname=dnickname, channel=chan.name) self.join_saved_channels() + return None diff --git a/mods/defender/schemas.py b/mods/defender/schemas.py new file mode 100644 index 0000000..b404685 --- /dev/null +++ b/mods/defender/schemas.py @@ -0,0 +1,35 @@ +from core.definition import MainModel, dataclass, MUser + +@dataclass +class ModConfModel(MainModel): + reputation: int = 0 + reputation_timer: int = 1 + reputation_seuil: int = 26 + reputation_score_after_release: int = 27 + reputation_ban_all_chan: int = 0 + reputation_sg: int = 1 + local_scan: int = 0 + psutil_scan: int = 0 + abuseipdb_scan: int = 0 + freeipapi_scan: int = 0 + cloudfilt_scan: int = 0 + flood: int = 0 + flood_message: int = 5 + flood_time: int = 1 + flood_timer: int = 20 + autolimit: int = 0 + autolimit_amount: int = 3 + autolimit_interval: int = 3 + +@dataclass +class FloodUser(MainModel): + uid: str = None + nbr_msg: int = 0 + first_msg_time: int = 0 + +DB_FLOOD_USERS: list[FloodUser] = [] +DB_ABUSEIPDB_USERS: list[MUser] = [] +DB_FREEIPAPI_USERS: list[MUser] = [] +DB_CLOUDFILT_USERS: list[MUser] = [] +DB_PSUTIL_USERS: list[MUser] = [] +DB_LOCALSCAN_USERS: list[MUser] = [] \ No newline at end of file diff --git a/mods/defender/threads.py b/mods/defender/threads.py new file mode 100644 index 0000000..9f00077 --- /dev/null +++ b/mods/defender/threads.py @@ -0,0 +1,167 @@ +from typing import TYPE_CHECKING +from time import sleep + +if TYPE_CHECKING: + from mods.defender.mod_defender import Defender + +def thread_apply_reputation_sanctions(uplink: 'Defender'): + while uplink.reputationTimer_isRunning: + uplink.Utils.action_apply_reputation_santions(uplink) + sleep(5) + +def thread_cloudfilt_scan(uplink: 'Defender'): + + while uplink.cloudfilt_isRunning: + list_to_remove:list = [] + for user in uplink.Schemas.DB_CLOUDFILT_USERS: + uplink.Utils.action_scan_client_with_cloudfilt(uplink, user) + list_to_remove.append(user) + sleep(1) + + for user_model in list_to_remove: + uplink.Schemas.DB_CLOUDFILT_USERS.remove(user_model) + + sleep(1) + +def thread_freeipapi_scan(uplink: 'Defender'): + + while uplink.freeipapi_isRunning: + + list_to_remove: list = [] + for user in uplink.Schemas.DB_FREEIPAPI_USERS: + uplink.Utils.action_scan_client_with_freeipapi(uplink, user) + list_to_remove.append(user) + sleep(1) + + for user_model in list_to_remove: + uplink.Schemas.DB_FREEIPAPI_USERS.remove(user_model) + + sleep(1) + +def thread_abuseipdb_scan(uplink: 'Defender'): + + while uplink.abuseipdb_isRunning: + + list_to_remove: list = [] + for user in uplink.Schemas.DB_ABUSEIPDB_USERS: + uplink.Utils.action_scan_client_with_abuseipdb(uplink, user) + list_to_remove.append(user) + sleep(1) + + for user_model in list_to_remove: + uplink.Schemas.DB_ABUSEIPDB_USERS.remove(user_model) + + sleep(1) + +def thread_local_scan(uplink: 'Defender'): + + while uplink.localscan_isRunning: + list_to_remove:list = [] + for user in uplink.Schemas.DB_LOCALSCAN_USERS: + uplink.Utils.action_scan_client_with_local_socket(uplink, user) + list_to_remove.append(user) + sleep(1) + + for user_model in list_to_remove: + uplink.Schemas.DB_LOCALSCAN_USERS.remove(user_model) + + sleep(1) + +def thread_psutil_scan(uplink: 'Defender'): + + while uplink.psutil_isRunning: + + list_to_remove:list = [] + for user in uplink.Schemas.DB_PSUTIL_USERS: + uplink.Utils.action_scan_client_with_psutil(uplink, user) + list_to_remove.append(user) + sleep(1) + + for user_model in list_to_remove: + uplink.Schemas.DB_PSUTIL_USERS.remove(user_model) + + sleep(1) + +def thread_autolimit(uplink: 'Defender'): + + if uplink.ModConfig.autolimit == 0: + uplink.Logs.debug("autolimit deactivated ... canceling the thread") + return None + + while uplink.Irc.autolimit_started: + sleep(0.2) + + uplink.Irc.autolimit_started = True + init_amount = uplink.ModConfig.autolimit_amount + p = uplink.Protocol + INIT = 1 + + # Copy Channels to a list of dict + chanObj_copy: list[dict[str, int]] = [{"name": c.name, "uids_count": len(c.uids)} for c in uplink.Channel.UID_CHANNEL_DB] + chan_list: list[str] = [c.name for c in uplink.Channel.UID_CHANNEL_DB] + + while uplink.autolimit_isRunning: + + if uplink.ModConfig.autolimit == 0: + uplink.Logs.debug("autolimit deactivated ... stopping the current thread") + break + + for chan in uplink.Channel.UID_CHANNEL_DB: + for chan_copy in chanObj_copy: + if chan_copy["name"] == chan.name and len(chan.uids) != chan_copy["uids_count"]: + p.send2socket(f":{uplink.Config.SERVICE_ID} MODE {chan.name} +l {len(chan.uids) + uplink.ModConfig.autolimit_amount}") + chan_copy["uids_count"] = len(chan.uids) + + if chan.name not in chan_list: + chan_list.append(chan.name) + chanObj_copy.append({"name": chan.name, "uids_count": 0}) + + # Verifier si un salon a été vidé + current_chan_in_list = [d.name for d in uplink.Channel.UID_CHANNEL_DB] + for c in chan_list: + if c not in current_chan_in_list: + chan_list.remove(c) + + # Si c'est la premiere execution + if INIT == 1: + for chan in uplink.Channel.UID_CHANNEL_DB: + p.send2socket(f":{uplink.Config.SERVICE_ID} MODE {chan.name} +l {len(chan.uids) + uplink.ModConfig.autolimit_amount}") + + # Si le nouveau amount est différent de l'initial + if init_amount != uplink.ModConfig.autolimit_amount: + init_amount = uplink.ModConfig.autolimit_amount + for chan in uplink.Channel.UID_CHANNEL_DB: + p.send2socket(f":{uplink.Config.SERVICE_ID} MODE {chan.name} +l {len(chan.uids) + uplink.ModConfig.autolimit_amount}") + + INIT = 0 + + if uplink.autolimit_isRunning: + sleep(uplink.ModConfig.autolimit_interval) + + for chan in uplink.Channel.UID_CHANNEL_DB: + p.send2socket(f":{uplink.Config.SERVICE_ID} MODE {chan.name} -l") + + uplink.Irc.autolimit_started = False + + return None + +def timer_release_mode_mute(uplink: 'Defender', action: str, channel: str): + """DO NOT EXECUTE THIS FUNCTION WITHOUT THREADING + + Args: + action (str): _description_ + channel (str): The related channel + + """ + service_id = uplink.Config.SERVICE_ID + + if not uplink.Channel.is_valid_channel(channel): + uplink.Logs.debug(f"Channel is not valid {channel}") + return + + match action: + case 'mode-m': + # Action -m sur le salon + uplink.Protocol.send2socket(f":{service_id} MODE {channel} -m") + case _: + pass diff --git a/mods/defender/utils.py b/mods/defender/utils.py new file mode 100644 index 0000000..9855791 --- /dev/null +++ b/mods/defender/utils.py @@ -0,0 +1,710 @@ +from calendar import c +import socket +import psutil +import requests +import mods.defender.threads as dthreads +from json import loads +from re import match +from typing import TYPE_CHECKING, Optional +from mods.defender.schemas import FloodUser + +if TYPE_CHECKING: + from core.definition import MUser + from mods.defender.mod_defender import Defender + +def handle_on_reputation(uplink: 'Defender', srvmsg: list[str]): + """Handle reputation server message + >>> srvmsg = [':001', 'REPUTATION', '128.128.128.128', '0'] + >>> srvmsg = [':001', 'REPUTATION', '128.128.128.128', '*0'] + Args: + irc_instance (Irc): The Irc instance + srvmsg (list[str]): The Server MSG + """ + ip = srvmsg[2] + score = srvmsg[3] + + if str(ip).find('*') != -1: + # If the reputation changed, we do not need to scan the IP + return + + # Possibilité de déclancher les bans a ce niveau. + if not uplink.Base.is_valid_ip(ip): + return + +def handle_on_mode(uplink: 'Defender', srvmsg: list[str]): + """_summary_ + >>> srvmsg = ['@unrealircd.org/...', ':001C0MF01', 'MODE', '#services', '+l', '1'] + >>> srvmsg = ['...', ':001XSCU0Q', 'MODE', '#jail', '+b', '~security-group:unknown-users'] + Args: + irc_instance (Irc): The Irc instance + srvmsg (list[str]): The Server MSG + confmodel (ModConfModel): The Module Configuration + """ + irc = uplink.Irc + gconfig = uplink.Config + p = uplink.Protocol + confmodel = uplink.ModConfig + + channel = str(srvmsg[3]) + mode = str(srvmsg[4]) + group_to_check = str(srvmsg[5:]) + group_to_unban = '~security-group:unknown-users' + + if confmodel.autolimit == 1: + if mode == '+l' or mode == '-l': + chan = irc.Channel.get_channel(channel) + p.send2socket(f":{gconfig.SERVICE_ID} MODE {chan.name} +l {len(chan.uids) + confmodel.autolimit_amount}") + + if gconfig.SALON_JAIL == channel: + if mode == '+b' and group_to_unban in group_to_check: + p.send2socket(f":{gconfig.SERVICE_ID} MODE {gconfig.SALON_JAIL} -b ~security-group:unknown-users") + p.send2socket(f":{gconfig.SERVICE_ID} MODE {gconfig.SALON_JAIL} -eee ~security-group:webirc-users ~security-group:known-users ~security-group:websocket-users") + +def handle_on_privmsg(uplink: 'Defender', srvmsg: list[str]): + # ['@mtag....',':python', 'PRIVMSG', '#defender', ':zefzefzregreg', 'regg', 'aerg'] + action_on_flood(uplink, srvmsg) + return None + +def handle_on_sjoin(uplink: 'Defender', srvmsg: list[str]): + """If Joining a new channel, it applies group bans. + + >>> srvmsg = ['@msgid..', ':001', 'SJOIN', '1702138958', '#welcome', ':0015L1AHL'] + + Args: + irc_instance (Irc): The Irc instance + srvmsg (list[str]): The Server MSG + confmodel (ModConfModel): The Module Configuration + """ + irc = uplink.Irc + p = irc.Protocol + gconfig = uplink.Config + confmodel = uplink.ModConfig + + parsed_chan = srvmsg[4] if irc.Channel.is_valid_channel(srvmsg[4]) else None + parsed_UID = uplink.Loader.Utils.clean_uid(srvmsg[5]) + + if parsed_chan is None or parsed_UID is None: + return + + if confmodel.reputation == 1: + get_reputation = irc.Reputation.get_Reputation(parsed_UID) + + if parsed_chan != gconfig.SALON_JAIL: + p.send2socket(f":{gconfig.SERVICE_ID} MODE {parsed_chan} +b ~security-group:unknown-users") + p.send2socket(f":{gconfig.SERVICE_ID} MODE {parsed_chan} +eee ~security-group:webirc-users ~security-group:known-users ~security-group:websocket-users") + + if get_reputation is not None: + isWebirc = get_reputation.isWebirc + + if not isWebirc: + if parsed_chan != gconfig.SALON_JAIL: + p.send_sapart(nick_to_sapart=get_reputation.nickname, channel_name=parsed_chan) + + if confmodel.reputation_ban_all_chan == 1 and not isWebirc: + if parsed_chan != gconfig.SALON_JAIL: + p.send2socket(f":{gconfig.SERVICE_ID} MODE {parsed_chan} +b {get_reputation.nickname}!*@*") + p.send2socket(f":{gconfig.SERVICE_ID} KICK {parsed_chan} {get_reputation.nickname}") + + irc.Logs.debug(f'SJOIN parsed_uid : {parsed_UID}') + +def handle_on_slog(uplink: 'Defender', srvmsg: list[str]): + """Handling SLOG messages + >>> srvmsg = ['@unrealircd...', ':001', 'SLOG', 'info', 'blacklist', 'BLACKLIST_HIT', ':[Blacklist]', 'IP', '162.x.x.x', 'matches', 'blacklist', 'dronebl', '(dnsbl.dronebl.org/reply=6)'] + Args: + irc_instance (Irc): The Irc instance + srvmsg (list[str]): The Server MSG + confmodel (ModConfModel): The Module Configuration + """ + ['@unrealircd...', ':001', 'SLOG', 'info', 'blacklist', 'BLACKLIST_HIT', ':[Blacklist]', 'IP', '162.x.x.x', 'matches', 'blacklist', 'dronebl', '(dnsbl.dronebl.org/reply=6)'] + + if not uplink.Base.is_valid_ip(srvmsg[8]): + return None + + # if self.ModConfig.local_scan == 1 and not cmd[7] in self.Config.WHITELISTED_IP: + # self.localscan_remote_ip.append(cmd[7]) + + # if self.ModConfig.psutil_scan == 1 and not cmd[7] in self.Config.WHITELISTED_IP: + # self.psutil_remote_ip.append(cmd[7]) + + # if self.ModConfig.abuseipdb_scan == 1 and not cmd[7] in self.Config.WHITELISTED_IP: + # self.abuseipdb_remote_ip.append(cmd[7]) + + # if self.ModConfig.freeipapi_scan == 1 and not cmd[7] in self.Config.WHITELISTED_IP: + # self.freeipapi_remote_ip.append(cmd[7]) + + # if self.ModConfig.cloudfilt_scan == 1 and not cmd[7] in self.Config.WHITELISTED_IP: + # self.cloudfilt_remote_ip.append(cmd[7]) + + return None + +def handle_on_nick(uplink: 'Defender', srvmsg: list[str]): + """_summary_ + >>> srvmsg = ['@unrealircd.org...', ':001MZQ0RB', 'NICK', 'newnickname', '1754663712'] + Args: + irc_instance (Irc): The Irc instance + srvmsg (list[str]): The Server MSG + confmodel (ModConfModel): The Module Configuration + """ + uid = uplink.Loader.Utils.clean_uid(str(srvmsg[1])) + p = uplink.Protocol + confmodel = uplink.ModConfig + + get_reputation = uplink.Reputation.get_Reputation(uid) + jail_salon = uplink.Config.SALON_JAIL + service_id = uplink.Config.SERVICE_ID + + if get_reputation is None: + uplink.Logs.debug(f'This UID: {uid} is not listed in the reputation dataclass') + return None + + # Update the new nickname + oldnick = get_reputation.nickname + newnickname = srvmsg[3] + get_reputation.nickname = newnickname + + # If ban in all channel is ON then unban old nickname an ban the new nickname + if confmodel.reputation_ban_all_chan == 1: + for chan in uplink.Channel.UID_CHANNEL_DB: + if chan.name != jail_salon: + p.send2socket(f":{service_id} MODE {chan.name} -b {oldnick}!*@*") + p.send2socket(f":{service_id} MODE {chan.name} +b {newnickname}!*@*") + +def handle_on_quit(uplink: 'Defender', srvmsg: list[str]): + """_summary_ + >>> srvmsg = ['@unrealircd.org...', ':001MZQ0RB', 'QUIT', ':Quit:', 'quit message'] + Args: + uplink (Irc): The Defender Module instance + srvmsg (list[str]): The Server MSG + """ + p = uplink.Protocol + confmodel = uplink.ModConfig + + ban_all_chan = uplink.Base.int_if_possible(confmodel.reputation_ban_all_chan) + final_UID = uplink.Loader.Utils.clean_uid(str(srvmsg[1])) + jail_salon = uplink.Config.SALON_JAIL + service_id = uplink.Config.SERVICE_ID + get_user_reputation = uplink.Reputation.get_Reputation(final_UID) + + if get_user_reputation is not None: + final_nickname = get_user_reputation.nickname + for chan in uplink.Channel.UID_CHANNEL_DB: + if chan.name != jail_salon and ban_all_chan == 1: + p.send2socket(f":{service_id} MODE {chan.name} -b {final_nickname}!*@*") + uplink.Logs.debug(f"Mode -b {final_nickname} on channel {chan.name}") + + uplink.Reputation.delete(final_UID) + uplink.Logs.debug(f"Client {get_user_reputation.nickname} has been removed from Reputation local DB") + +def handle_on_uid(uplink: 'Defender', srvmsg: list[str]): + """_summary_ + >>> ['@s2s-md...', ':001', 'UID', 'nickname', '0', '1754675249', '...', '125-168-141-239.hostname.net', '001BAPN8M', + '0', '+iwx', '*', '32001BBE.25ACEFE7.429FE90D.IP', 'ZA2ic7w==', ':realname'] + + Args: + uplink (Defender): The Defender instance + srvmsg (list[str]): The Server MSG + """ + gconfig = uplink.Config + irc = uplink.Irc + confmodel = uplink.ModConfig + + # If Init then do nothing + if gconfig.DEFENDER_INIT == 1: + return None + + # Get User information + _User = irc.User.get_User(str(srvmsg[8])) + + if _User is None: + irc.Logs.warning(f'This UID: [{srvmsg[8]}] is not available please check why') + return + + # If user is not service or IrcOp then scan them + if not match(r'^.*[S|o?].*$', _User.umodes): + uplink.Schemas.DB_ABUSEIPDB_USERS.append(_User) if confmodel.abuseipdb_scan == 1 and _User.remote_ip not in gconfig.WHITELISTED_IP else None + uplink.Schemas.DB_FREEIPAPI_USERS.append(_User) if confmodel.freeipapi_scan == 1 and _User.remote_ip not in gconfig.WHITELISTED_IP else None + uplink.Schemas.DB_CLOUDFILT_USERS.append(_User) if confmodel.cloudfilt_scan == 1 and _User.remote_ip not in gconfig.WHITELISTED_IP else None + uplink.Schemas.DB_PSUTIL_USERS.append(_User) if confmodel.psutil_scan == 1 and _User.remote_ip not in gconfig.WHITELISTED_IP else None + uplink.Schemas.DB_LOCALSCAN_USERS.append(_User) if confmodel.local_scan == 1 and _User.remote_ip not in gconfig.WHITELISTED_IP else None + + reputation_flag = confmodel.reputation + reputation_seuil = confmodel.reputation_seuil + + if gconfig.DEFENDER_INIT == 0: + # Si le user n'es pas un service ni un IrcOP + if not match(r'^.*[S|o?].*$', _User.umodes): + if reputation_flag == 1 and _User.score_connexion <= reputation_seuil: + # currentDateTime = self.Base.get_datetime() + irc.Reputation.insert( + irc.Loader.Definition.MReputation( + **_User.to_dict(), + secret_code=irc.Utils.generate_random_string(8) + ) + ) + if irc.Reputation.is_exist(_User.uid): + if reputation_flag == 1 and _User.score_connexion <= reputation_seuil: + action_add_reputation_sanctions(uplink, _User.uid) + irc.Logs.info(f'[REPUTATION] Reputation system ON (Nickname: {_User.nickname}, uid: {_User.uid})') + +#################### +# ACTION FUNCTIONS # +#################### + +def action_on_flood(uplink: 'Defender', srvmsg: list[str]): + + confmodel = uplink.ModConfig + if confmodel.flood == 0: + return None + + irc = uplink.Irc + gconfig = uplink.Config + p = uplink.Protocol + flood_users = uplink.Schemas.DB_FLOOD_USERS + + user_trigger = str(srvmsg[1]).replace(':','') + channel = srvmsg[3] + User = irc.User.get_User(user_trigger) + + if User is None or not irc.Channel.is_valid_channel(channel_to_check=channel): + return + + flood_time = confmodel.flood_time + flood_message = confmodel.flood_message + flood_timer = confmodel.flood_timer + service_id = gconfig.SERVICE_ID + dnickname = gconfig.SERVICE_NICKNAME + color_red = gconfig.COLORS.red + color_bold = gconfig.COLORS.bold + + get_detected_uid = User.uid + get_detected_nickname = User.nickname + unixtime = irc.Utils.get_unixtime() + get_diff_secondes = 0 + + def get_flood_user(uid: str) -> Optional[FloodUser]: + for flood_user in flood_users: + if flood_user.uid == uid: + return flood_user + + fu = get_flood_user(get_detected_uid) + if fu is None: + fu = FloodUser(get_detected_uid, 0, unixtime) + flood_users.append(fu) + + fu.nbr_msg += 1 + + get_diff_secondes = unixtime - fu.first_msg_time + if get_diff_secondes > flood_time: + fu.first_msg_time = unixtime + fu.nbr_msg = 0 + get_diff_secondes = unixtime - fu.first_msg_time + elif fu.nbr_msg > flood_message: + irc.Logs.info('system de flood detecté') + p.send_priv_msg( + nick_from=dnickname, + msg=f"{color_red} {color_bold} Flood detected. Apply the +m mode (Ô_o)", + channel=channel + ) + p.send2socket(f":{service_id} MODE {channel} +m") + irc.Logs.info(f'FLOOD Détecté sur {get_detected_nickname} mode +m appliqué sur le salon {channel}') + fu.nbr_msg = 0 + fu.first_msg_time = unixtime + irc.Base.create_timer(flood_timer, dthreads.timer_release_mode_mute, (uplink, 'mode-m', channel)) + +def action_add_reputation_sanctions(uplink: 'Defender', jailed_uid: str ): + + irc = uplink.Irc + gconfig = uplink.Config + p = uplink.Protocol + confmodel = uplink.ModConfig + + get_reputation = irc.Reputation.get_Reputation(jailed_uid) + + if get_reputation is None: + irc.Logs.warning(f'UID {jailed_uid} has not been found') + return + + salon_logs = gconfig.SERVICE_CHANLOG + salon_jail = gconfig.SALON_JAIL + + code = get_reputation.secret_code + jailed_nickname = get_reputation.nickname + jailed_score = get_reputation.score_connexion + + color_red = gconfig.COLORS.red + color_black = gconfig.COLORS.black + color_bold = gconfig.COLORS.bold + nogc = gconfig.COLORS.nogc + service_id = gconfig.SERVICE_ID + service_prefix = gconfig.SERVICE_PREFIX + reputation_ban_all_chan = confmodel.reputation_ban_all_chan + + if not get_reputation.isWebirc: + # Si le user ne vient pas de webIrc + p.send_sajoin(nick_to_sajoin=jailed_nickname, channel_name=salon_jail) + p.send_priv_msg(nick_from=gconfig.SERVICE_NICKNAME, + msg=f" [{color_red} REPUTATION {nogc}] : Connexion de {jailed_nickname} ({jailed_score}) ==> {salon_jail}", + channel=salon_logs + ) + p.send_notice( + nick_from=gconfig.SERVICE_NICKNAME, + nick_to=jailed_nickname, + msg=f"[{color_red} {jailed_nickname} {color_black}] : Merci de tapez la commande suivante {color_bold}{service_prefix}code {code}{color_bold}" + ) + if reputation_ban_all_chan == 1: + for chan in irc.Channel.UID_CHANNEL_DB: + if chan.name != salon_jail: + p.send2socket(f":{service_id} MODE {chan.name} +b {jailed_nickname}!*@*") + p.send2socket(f":{service_id} KICK {chan.name} {jailed_nickname}") + + irc.Logs.info(f"[REPUTATION] {jailed_nickname} jailed (UID: {jailed_uid}, score: {jailed_score})") + else: + irc.Logs.info(f"[REPUTATION] {jailed_nickname} skipped (trusted or WebIRC)") + irc.Reputation.delete(jailed_uid) + +def action_apply_reputation_santions(uplink: 'Defender') -> None: + + irc = uplink.Irc + gconfig = uplink.Config + p = uplink.Protocol + confmodel = uplink.ModConfig + + reputation_flag = confmodel.reputation + reputation_timer = confmodel.reputation_timer + reputation_seuil = confmodel.reputation_seuil + ban_all_chan = confmodel.reputation_ban_all_chan + service_id = gconfig.SERVICE_ID + dchanlog = gconfig.SERVICE_CHANLOG + color_red = gconfig.COLORS.red + nogc = gconfig.COLORS.nogc + salon_jail = gconfig.SALON_JAIL + + if reputation_flag == 0: + return None + elif reputation_timer == 0: + return None + + uid_to_clean = [] + + for user in irc.Reputation.UID_REPUTATION_DB: + if not user.isWebirc: # Si il ne vient pas de WebIRC + if irc.User.get_user_uptime_in_minutes(user.uid) >= reputation_timer and int(user.score_connexion) <= int(reputation_seuil): + p.send_priv_msg( + nick_from=service_id, + msg=f"[{color_red} REPUTATION {nogc}] : Action sur {user.nickname} aprés {str(reputation_timer)} minutes d'inactivité", + channel=dchanlog + ) + p.send2socket(f":{service_id} KILL {user.nickname} After {str(reputation_timer)} minutes of inactivity you should reconnect and type the password code") + p.send2socket(f":{gconfig.SERVEUR_LINK} REPUTATION {user.remote_ip} 0") + + irc.Logs.info(f"Nickname: {user.nickname} KILLED after {str(reputation_timer)} minutes of inactivity") + uid_to_clean.append(user.uid) + + for uid in uid_to_clean: + # Suppression des éléments dans {UID_DB} et {REPUTATION_DB} + for chan in irc.Channel.UID_CHANNEL_DB: + if chan.name != salon_jail and ban_all_chan == 1: + get_user_reputation = irc.Reputation.get_Reputation(uid) + p.send2socket(f":{service_id} MODE {chan.name} -b {get_user_reputation.nickname}!*@*") + + # Lorsqu'un utilisateur quitte, il doit être supprimé de {UID_DB}. + irc.Channel.delete_user_from_all_channel(uid) + irc.Reputation.delete(uid) + irc.User.delete(uid) + +def action_scan_client_with_cloudfilt(uplink: 'Defender', user_model: 'MUser') -> Optional[dict[str, str]]: + """Analyse l'ip avec cloudfilt + Cette methode devra etre lancer toujours via un thread ou un timer. + Args: + uplink (Defender): Defender Instance + + Returns: + dict[str, any] | None: les informations du provider + keys : 'countryCode', 'isProxy' + """ + + remote_ip = user_model.remote_ip + username = user_model.username + hostname = user_model.hostname + nickname = user_model.nickname + p = uplink.Protocol + + if remote_ip in uplink.Config.WHITELISTED_IP: + return None + if uplink.ModConfig.cloudfilt_scan == 0: + return None + if uplink.cloudfilt_key == '': + return None + + service_id = uplink.Config.SERVICE_ID + service_chanlog = uplink.Config.SERVICE_CHANLOG + color_red = uplink.Config.COLORS.red + nogc = uplink.Config.COLORS.nogc + + url = "https://developers18334.cloudfilt.com/" + + data = { + 'ip': remote_ip, + 'key': uplink.cloudfilt_key + } + + response = requests.post(url=url, data=data) + # Formatted output + decoded_response: dict = loads(response.text) + status_code = response.status_code + if status_code != 200: + uplink.Logs.warning(f'Error connecting to cloudfilt API | Code: {str(status_code)}') + return + + result = { + 'countryiso': decoded_response.get('countryiso', None), + 'listed': decoded_response.get('listed', None), + 'listed_by': decoded_response.get('listed_by', None), + 'host': decoded_response.get('host', None) + } + + # pseudo!ident@host + fullname = f'{nickname}!{username}@{hostname}' + + p.send_priv_msg( + nick_from=service_id, + msg=f"[ {color_red}CLOUDFILT_SCAN{nogc} ] : Connexion de {fullname} ({remote_ip}) ==> Host: {str(result['host'])} | country: {str(result['countryiso'])} | listed: {str(result['listed'])} | listed by : {str(result['listed_by'])}", + channel=service_chanlog) + + uplink.Logs.debug(f"[CLOUDFILT SCAN] ({fullname}) connected from ({result['countryiso']}), Listed: {result['listed']}, by: {result['listed_by']}") + + if result['listed']: + p.send2socket(f":{service_id} GLINE +*@{remote_ip} {uplink.Config.GLINE_DURATION} Your connexion is listed as dangerous {str(result['listed'])} {str(result['listed_by'])} - detected by cloudfilt") + uplink.Logs.debug(f"[CLOUDFILT SCAN GLINE] Dangerous connection ({fullname}) from ({result['countryiso']}) Listed: {result['listed']}, by: {result['listed_by']}") + + response.close() + + return result + +def action_scan_client_with_freeipapi(uplink: 'Defender', user_model: 'MUser') -> Optional[dict[str, str]]: + """Analyse l'ip avec Freeipapi + Cette methode devra etre lancer toujours via un thread ou un timer. + Args: + uplink (Defender): The Defender object Instance + + Returns: + dict[str, any] | None: les informations du provider + keys : 'countryCode', 'isProxy' + """ + p = uplink.Protocol + remote_ip = user_model.remote_ip + username = user_model.username + hostname = user_model.hostname + nickname = user_model.nickname + + if remote_ip in uplink.Config.WHITELISTED_IP: + return None + if uplink.ModConfig.freeipapi_scan == 0: + return None + + service_id = uplink.Config.SERVICE_ID + service_chanlog = uplink.Config.SERVICE_CHANLOG + color_red = uplink.Config.COLORS.red + nogc = uplink.Config.COLORS.nogc + + url = f'https://freeipapi.com/api/json/{remote_ip}' + + headers = { + 'Accept': 'application/json', + } + + response = requests.request(method='GET', url=url, headers=headers, timeout=uplink.timeout) + + # Formatted output + decoded_response: dict = loads(response.text) + + status_code = response.status_code + if status_code == 429: + uplink.Logs.warning('Too Many Requests - The rate limit for the API has been exceeded.') + return None + elif status_code != 200: + uplink.Logs.warning(f'status code = {str(status_code)}') + return None + + result = { + 'countryCode': decoded_response.get('countryCode', None), + 'isProxy': decoded_response.get('isProxy', None) + } + + # pseudo!ident@host + fullname = f'{nickname}!{username}@{hostname}' + + p.send_priv_msg( + nick_from=service_id, + msg=f"[ {color_red}FREEIPAPI_SCAN{nogc} ] : Connexion de {fullname} ({remote_ip}) ==> Proxy: {str(result['isProxy'])} | Country : {str(result['countryCode'])}", + channel=service_chanlog) + uplink.Logs.debug(f"[FREEIPAPI SCAN] ({fullname}) connected from ({result['countryCode']}), Proxy: {result['isProxy']}") + + if result['isProxy']: + p.send2socket(f":{service_id} GLINE +*@{remote_ip} {uplink.Config.GLINE_DURATION} This server do not allow proxy connexions {str(result['isProxy'])} - detected by freeipapi") + uplink.Logs.debug(f"[FREEIPAPI SCAN GLINE] Server do not allow proxy connexions {result['isProxy']}") + + response.close() + + return result + +def action_scan_client_with_abuseipdb(uplink: 'Defender', user_model: 'MUser') -> Optional[dict[str, str]]: + """Analyse l'ip avec AbuseIpDB + Cette methode devra etre lancer toujours via un thread ou un timer. + Args: + uplink (Defender): Defender instance object + user_model (MUser): l'objet User qui contient l'ip + + Returns: + dict[str, str] | None: les informations du provider + """ + p = uplink.Protocol + remote_ip = user_model.remote_ip + username = user_model.username + hostname = user_model.hostname + nickname = user_model.nickname + + if remote_ip in uplink.Config.WHITELISTED_IP: + return None + if uplink.ModConfig.abuseipdb_scan == 0: + return None + + if uplink.abuseipdb_key == '': + return None + + url = 'https://api.abuseipdb.com/api/v2/check' + querystring = { + 'ipAddress': remote_ip, + 'maxAgeInDays': '90' + } + + headers = { + 'Accept': 'application/json', + 'Key': uplink.abuseipdb_key + } + + response = requests.request(method='GET', url=url, headers=headers, params=querystring, timeout=uplink.timeout) + + # Formatted output + decoded_response: dict[str, dict] = loads(response.text) + + if 'data' not in decoded_response: + return None + + result = { + 'score': decoded_response.get('data', {}).get('abuseConfidenceScore', 0), + 'country': decoded_response.get('data', {}).get('countryCode', None), + 'isTor': decoded_response.get('data', {}).get('isTor', None), + 'totalReports': decoded_response.get('data', {}).get('totalReports', 0) + } + + service_id = uplink.Config.SERVICE_ID + service_chanlog = uplink.Config.SERVICE_CHANLOG + color_red = uplink.Config.COLORS.red + nogc = uplink.Config.COLORS.nogc + + # pseudo!ident@host + fullname = f'{nickname}!{username}@{hostname}' + + p.send_priv_msg( + nick_from=service_id, + msg=f"[ {color_red}ABUSEIPDB_SCAN{nogc} ] : Connexion de {fullname} ({remote_ip}) ==> Score: {str(result['score'])} | Country : {result['country']} | Tor : {str(result['isTor'])} | Total Reports : {str(result['totalReports'])}", + channel=service_chanlog + ) + uplink.Logs.debug(f"[ABUSEIPDB SCAN] ({fullname}) connected from ({result['country']}), Score: {result['score']}, Tor: {result['isTor']}") + + if result['isTor']: + p.send2socket(f":{service_id} GLINE +*@{remote_ip} {uplink.Config.GLINE_DURATION} This server do not allow Tor connexions {str(result['isTor'])} - Detected by Abuseipdb") + uplink.Logs.debug(f"[ABUSEIPDB SCAN GLINE] Server do not allow Tor connections Tor: {result['isTor']}, Score: {result['score']}") + elif result['score'] >= 95: + p.send2socket(f":{service_id} GLINE +*@{remote_ip} {uplink.Config.GLINE_DURATION} You were banned from this server because your abuse score is = {str(result['score'])} - Detected by Abuseipdb") + uplink.Logs.debug(f"[ABUSEIPDB SCAN GLINE] Server do not high risk connections Country: {result['country']}, Score: {result['score']}") + + response.close() + + return result + +def action_scan_client_with_local_socket(uplink: 'Defender', user_model: 'MUser'): + """local_scan + + Args: + uplink (Defender): Defender instance object + user_model (MUser): l'objet User qui contient l'ip + """ + p = uplink.Protocol + remote_ip = user_model.remote_ip + username = user_model.username + hostname = user_model.hostname + nickname = user_model.nickname + fullname = f'{nickname}!{username}@{hostname}' + + if remote_ip in uplink.Config.WHITELISTED_IP: + return None + + for port in uplink.Config.PORTS_TO_SCAN: + try: + newSocket = '' + newSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM or socket.SOCK_NONBLOCK) + newSocket.settimeout(0.5) + + connection = (remote_ip, uplink.Base.int_if_possible(port)) + newSocket.connect(connection) + + p.send_priv_msg( + nick_from=uplink.Config.SERVICE_NICKNAME, + msg=f"[ {uplink.Config.COLORS.red}PROXY_SCAN{uplink.Config.COLORS.nogc} ] {fullname} ({remote_ip}) : Port [{str(port)}] ouvert sur l'adresse ip [{remote_ip}]", + channel=uplink.Config.SERVICE_CHANLOG + ) + # print(f"=======> Le port {str(port)} est ouvert !!") + uplink.Base.running_sockets.append(newSocket) + # print(newSocket) + newSocket.shutdown(socket.SHUT_RDWR) + newSocket.close() + + except (socket.timeout, ConnectionRefusedError): + uplink.Logs.info(f"Le port {remote_ip}:{str(port)} est fermé") + except AttributeError as ae: + uplink.Logs.warning(f"AttributeError ({remote_ip}): {ae}") + except socket.gaierror as err: + uplink.Logs.warning(f"Address Info Error ({remote_ip}): {err}") + finally: + # newSocket.shutdown(socket.SHUT_RDWR) + newSocket.close() + uplink.Logs.info('=======> Fermeture de la socket') + +def action_scan_client_with_psutil(uplink: 'Defender', user_model: 'MUser') -> list[int]: + """psutil_scan for Linux (should be run on the same location as the unrealircd server) + + Args: + userModel (UserModel): The User Model Object + + Returns: + list[int]: list of ports + """ + p = uplink.Protocol + remote_ip = user_model.remote_ip + username = user_model.username + hostname = user_model.hostname + nickname = user_model.nickname + + if remote_ip in uplink.Config.WHITELISTED_IP: + return None + + try: + connections = psutil.net_connections(kind='inet') + fullname = f'{nickname}!{username}@{hostname}' + + matching_ports = [conn.raddr.port for conn in connections if conn.raddr and conn.raddr.ip == remote_ip] + uplink.Logs.info(f"Connexion of {fullname} ({remote_ip}) using ports : {str(matching_ports)}") + + if matching_ports: + p.send_priv_msg( + nick_from=uplink.Config.SERVICE_NICKNAME, + msg=f"[ {uplink.Config.COLORS.red}PSUTIL_SCAN{uplink.Config.COLORS.black} ] {fullname} ({remote_ip}) : is using ports {matching_ports}", + channel=uplink.Config.SERVICE_CHANLOG + ) + + return matching_ports + + except psutil.AccessDenied as ad: + uplink.Logs.critical(f'psutil_scan: Permission error: {ad}') \ No newline at end of file diff --git a/mods/mod_jsonrpc.py b/mods/jsonrpc/mod_jsonrpc.py similarity index 67% rename from mods/mod_jsonrpc.py rename to mods/jsonrpc/mod_jsonrpc.py index 08d13c3..70cb37e 100644 --- a/mods/mod_jsonrpc.py +++ b/mods/jsonrpc/mod_jsonrpc.py @@ -1,7 +1,12 @@ 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 from dataclasses import dataclass -from unrealircd_rpc_py.Live import LiveWebsocket +from unrealircd_rpc_py.Live import LiveWebsocket, LiveUnixSocket from unrealircd_rpc_py.Loader import Loader if TYPE_CHECKING: @@ -32,8 +37,11 @@ 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.Base.logs + self.Logs = ircInstance.Loader.Logs # Add User object to the module (Mandatory) self.User = ircInstance.User @@ -41,9 +49,22 @@ class Jsonrpc(): # Add Channel object to the module (Mandatory) self.Channel = ircInstance.Channel + # 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, 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]') self.Irc.build_command(1, self.module_name, 'jruser', 'Get Information about a user using JSON RPC') + self.Irc.build_command(1, self.module_name, 'jrinstances', 'Get number of instances') # Init the module self.__init_module() @@ -55,6 +76,7 @@ class Jsonrpc(): logging.getLogger('websockets').setLevel(logging.WARNING) logging.getLogger('unrealircd-rpc-py').setLevel(logging.CRITICAL) + logging.getLogger('unrealircd-liverpc-py').setLevel(logging.CRITICAL) # Create you own tables (Mandatory) # self.__create_tables() @@ -70,15 +92,15 @@ class Jsonrpc(): callback_object_instance=self, callback_method_or_function_name='callback_sent_to_irc' ) - + if self.UnrealIrcdRpcLive.get_error.code != 0: - self.Logs.error(self.UnrealIrcdRpcLive.get_error.code, self.UnrealIrcdRpcLive.get_error.message) + self.Logs.error(f"{self.UnrealIrcdRpcLive.get_error.message} ({self.UnrealIrcdRpcLive.get_error.code})") 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 ) - return + raise Exception(f"[LIVE-JSONRPC ERROR] {self.UnrealIrcdRpcLive.get_error.message}") self.Rpc: Loader = Loader( req_method=self.Config.JSONRPC_METHOD, @@ -88,18 +110,17 @@ class Jsonrpc(): ) if self.Rpc.get_error.code != 0: - self.Logs.error(self.Rpc.get_error.code, self.Rpc.get_error.message) + self.Logs.error(f"{self.Rpc.get_error.message} ({self.Rpc.get_error.code})") self.Protocol.send_priv_msg( nick_from=self.Config.SERVICE_NICKNAME, - msg=f"[{self.Config.COLORS.red}ERROR{self.Config.COLORS.nogc}] {self.Rpc.get_error.message}", + msg=f"[{self.Config.COLORS.red}JSONRPC ERROR{self.Config.COLORS.nogc}] {self.Rpc.get_error.message}", channel=self.Config.SERVICE_CHANLOG ) - - self.subscribed = False + 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 def __create_tables(self) -> None: @@ -122,7 +143,7 @@ class Jsonrpc(): self.Base.db_execute_query(table_logs) return None - def callback_sent_to_irc(self, response): + def callback_sent_to_irc(self, response: SimpleNamespace) -> None: dnickname = self.Config.SERVICE_NICKNAME dchanlog = self.Config.SERVICE_CHANLOG @@ -131,13 +152,29 @@ class Jsonrpc(): bold = self.Config.COLORS.bold red = self.Config.COLORS.red - if hasattr(response, 'result'): - if isinstance(response.result, bool) and response.result: + if self.UnrealIrcdRpcLive.get_error.code != 0: + self.Protocol.send_priv_msg(nick_from=dnickname, + msg=f"[{bold}{red}JSONRPC ERROR{nogc}{bold}] {self.UnrealIrcdRpcLive.get_error.message}", + channel=dchanlog) + return None + + if hasattr(response, 'error'): + if response.error.code != 0: self.Protocol.send_priv_msg( - nick_from=self.Config.SERVICE_NICKNAME, - msg=f"[{bold}{green}JSONRPC{nogc}{bold}] Event activated", - channel=dchanlog) - return None + nick_from=self.Config.SERVICE_NICKNAME, + msg=f"[{bold}{red}JSONRPC{nogc}{bold}] JSONRPC Event activated on {self.Config.JSONRPC_URL}", + channel=dchanlog) + + return None + + if hasattr(response, 'result'): + if isinstance(response.result, bool): + if response.result: + self.Protocol.send_priv_msg( + nick_from=self.Config.SERVICE_NICKNAME, + msg=f"[{bold}{green}JSONRPC{nogc}{bold}] JSONRPC Event activated on {self.Config.JSONRPC_URL}", + channel=dchanlog) + return None level = response.result.level if hasattr(response.result, 'level') else '' subsystem = response.result.subsystem if hasattr(response.result, 'subsystem') else '' @@ -146,24 +183,9 @@ class Jsonrpc(): msg = response.result.msg if hasattr(response.result, 'msg') else '' build_msg = f"{green}{log_source}{nogc}: [{bold}{level}{bold}] {subsystem}.{event_id} - {msg}" - - # Check if there is an error - if self.UnrealIrcdRpcLive.get_error.code != 0: - self.Logs.error(f"RpcLiveError: {self.UnrealIrcdRpcLive.get_error.message}") - self.Protocol.send_priv_msg(nick_from=dnickname, msg=build_msg, channel=dchanlog) - - def thread_start_jsonrpc(self): - - if self.UnrealIrcdRpcLive.get_error.code == 0: - self.UnrealIrcdRpcLive.subscribe(["all"]) - self.subscribed = True - 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 - ) + + return None def __load_module_configuration(self) -> None: """### Load Module Configuration @@ -180,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: @@ -190,11 +212,18 @@ class Jsonrpc(): self.Base.db_update_core_config(self.module_name, self.ModConfig, param_key, param_value) def unload(self) -> None: - if self.UnrealIrcdRpcLive.Error.code != -1: - self.UnrealIrcdRpcLive.unsubscribe() + if self.is_streaming: + self.Protocol.send_priv_msg( + nick_from=self.Config.SERVICE_NICKNAME, + 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.Threads.thread_unsubscribe, func_args=(self, ), run_once=True) + self.update_configuration('jsonrpc', 0) + self.Logs.debug(f"Unloading {self.module_name}") return None - def cmd(self, data:list) -> None: + def cmd(self, data: list) -> None: return None @@ -210,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.UnrealIrcdRpcLive.unsubscribe() - 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) @@ -271,16 +288,13 @@ class Jsonrpc(): self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'{rpc.get_error.message}') return None - chan_list = [] - for chan in UserInfo.user.channels: - chan_list.append(chan.name) self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'UID : {UserInfo.id}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'NICKNAME : {UserInfo.name}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'USERNAME : {UserInfo.user.username}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'REALNAME : {UserInfo.user.realname}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'MODES : {UserInfo.user.modes}') - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'CHANNELS : {chan_list}') + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'CHANNELS : {[chan.name for chan in UserInfo.user.channels]}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'SECURITY GROUP : {UserInfo.user.security_groups}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'REPUTATION : {UserInfo.user.reputation}') @@ -303,22 +317,12 @@ class Jsonrpc(): except IndexError as ie: self.Logs.error(ie) - case 'ia': + case 'jrinstances': try: - - self.Base.create_thread(self.thread_ask_ia, ('',)) - - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" This is a notice to the sender ...") - self.Protocol.send_priv_msg(nick_from=dnickname, msg="This is private message to the sender ...", nick_to=fromuser) - - if not fromchannel is None: - self.Protocol.send_priv_msg(nick_from=dnickname, msg="This is channel message to the sender ...", channel=fromchannel) - - # How to update your module configuration - self.__update_configuration('param_exemple2', 7) - - # Log if you want the result - self.Logs.debug(f"Test logs ready") - + 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/mods/mod_test.py b/mods/test/mod_test.py similarity index 99% rename from mods/mod_test.py rename to mods/test/mod_test.py index 275f845..8372e6f 100644 --- a/mods/mod_test.py +++ b/mods/test/mod_test.py @@ -34,7 +34,7 @@ class Test(): self.Base = ircInstance.Base # Add logs object to the module (Mandatory) - self.Logs = ircInstance.Base.logs + self.Logs = ircInstance.Loader.Logs # Add User object to the module (Mandatory) self.User = ircInstance.User diff --git a/mods/mod_votekick.py b/mods/votekick/mod_votekick.py similarity index 54% rename from mods/mod_votekick.py rename to mods/votekick/mod_votekick.py index 54b7d03..54acd2d 100644 --- a/mods/mod_votekick.py +++ b/mods/votekick/mod_votekick.py @@ -1,59 +1,75 @@ -from typing import TYPE_CHECKING +""" + File : mod_votekick.py + Version : 1.0.0 + Description : Manages votekick sessions for multiple channels. + Handles activation, ongoing vote checks, and cleanup. + Author : adator + Created : 2025-08-16 + Last Updated: 2025-08-16 +----------------------------------------- +""" import re -from dataclasses import dataclass, field +import mods.votekick.schemas as schemas +import mods.votekick.utils as utils +from mods.votekick.votekick_manager import VotekickManager +import mods.votekick.threads as thds +from typing import TYPE_CHECKING, Any, Optional if TYPE_CHECKING: from core.irc import Irc -# Activer le systeme sur un salon (activate #salon) -# Le service devra se connecter au salon -# Le service devra se mettre en op -# Soumettre un nom de user (submit nickname) -# voter pour un ban (vote_for) -# voter contre un ban (vote_against) +class Votekick: - -class Votekick(): - - @dataclass - class VoteChannelModel: - channel_name: str - target_user: str - voter_users: list - vote_for: int - vote_against: int - - VOTE_CHANNEL_DB:list[VoteChannelModel] = [] - - def __init__(self, ircInstance: 'Irc') -> None: + def __init__(self, uplink: 'Irc') -> None: # Module name (Mandatory) self.module_name = 'mod_' + str(self.__class__.__name__).lower() # Add Irc Object to the module - self.Irc = ircInstance + self.Irc = uplink # Add Loader Object to the module (Mandatory) - self.Loader = ircInstance.Loader + self.Loader = uplink.Loader # Add server protocol Object to the module (Mandatory) - self.Protocol = ircInstance.Protocol + self.Protocol = uplink.Protocol # Add Global Configuration to the module - self.Config = ircInstance.Config + self.Config = uplink.Config # Add Base object to the module - self.Base = ircInstance.Base + self.Base = uplink.Base # Add logs object to the module - self.Logs = ircInstance.Base.logs + self.Logs = uplink.Logs # Add User object to the module - self.User = ircInstance.User + self.User = uplink.User # Add Channel object to the module - self.Channel = ircInstance.Channel + self.Channel = uplink.Channel + + # Add Utils. + self.Utils = uplink.Utils + + # Add Utils module + self.ModUtils = utils + + # Add Schemas module + self.Schemas = schemas + + # Add Threads module + self.Threads = thds + + # Add VoteKick Manager + self.VoteKickManager = VotekickManager(self) + + metadata = uplink.Loader.Settings.get_cache('VOTEKICK') + + if metadata is not None: + self.VoteKickManager.VOTE_CHANNEL_DB = metadata + # self.VOTE_CHANNEL_DB = metadata # Créer les nouvelles commandes du module self.Irc.build_command(1, self.module_name, 'vote', 'The kick vote module') @@ -69,15 +85,13 @@ class Votekick(): # Add admin object to retrieve admin users self.Admin = self.Irc.Admin self.__create_tables() - self.join_saved_channels() + self.ModUtils.join_saved_channels(self) return None def __create_tables(self) -> None: """Methode qui va créer la base de donnée si elle n'existe pas. Une Session unique pour cette classe sera crée, qui sera utilisé dans cette classe / module - Args: - database_name (str): Nom de la base de données ( pas d'espace dans le nom ) Returns: None: Aucun retour n'es attendu @@ -103,11 +117,14 @@ class Votekick(): def unload(self) -> None: try: - for chan in self.VOTE_CHANNEL_DB: + # Cache the local DB with current votes. + self.Loader.Settings.set_cache('VOTEKICK', self.VoteKickManager.VOTE_CHANNEL_DB) + + for chan in self.VoteKickManager.VOTE_CHANNEL_DB: self.Protocol.send_part_chan(uidornickname=self.Config.SERVICE_ID, channel=chan.channel_name) - self.VOTE_CHANNEL_DB = [] - self.Logs.debug(f'Delete memory DB VOTE_CHANNEL_DB: {self.VOTE_CHANNEL_DB}') + self.VoteKickManager.VOTE_CHANNEL_DB = [] + self.Logs.debug(f'Delete memory DB VOTE_CHANNEL_DB: {self.VoteKickManager.VOTE_CHANNEL_DB}') return None except UnboundLocalError as ne: @@ -117,137 +134,28 @@ class Votekick(): except Exception as err: self.Logs.error(f'General Error: {err}') - def init_vote_system(self, channel: str) -> bool: + def cmd(self, data: list) -> None: - response = False - for chan in self.VOTE_CHANNEL_DB: - if chan.channel_name == channel: - chan.target_user = '' - chan.voter_users = [] - chan.vote_against = 0 - chan.vote_for = 0 - response = True + if not data or len(data) < 2: + return None - return response - - def insert_vote_channel(self, ChannelObject: VoteChannelModel) -> bool: - result = False - found = False - for chan in self.VOTE_CHANNEL_DB: - if chan.channel_name == ChannelObject.channel_name: - found = True - - if not found: - self.VOTE_CHANNEL_DB.append(ChannelObject) - self.Logs.debug(f"The channel has been added {ChannelObject}") - # self.db_add_vote_channel(ChannelObject.channel_name) - - return result - - def db_add_vote_channel(self, channel:str) -> bool: - """Cette fonction ajoute les salons ou seront autoriser les votes - - Args: - channel (str): le salon à enregistrer. - """ - current_datetime = self.Base.get_datetime() - mes_donnees = {'channel': channel} - - response = self.Base.db_execute_query("SELECT id FROM votekick_channel WHERE channel = :channel", mes_donnees) - - isChannelExist = response.fetchone() - - if isChannelExist is None: - mes_donnees = {'datetime': current_datetime, 'channel': channel} - insert = self.Base.db_execute_query(f"INSERT INTO votekick_channel (datetime, channel) VALUES (:datetime, :channel)", mes_donnees) - if insert.rowcount > 0: - return True - else: - return False - else: - return False - - def db_delete_vote_channel(self, channel: str) -> bool: - """Cette fonction supprime les salons de join de Defender - - Args: - channel (str): le salon à enregistrer. - """ - mes_donnes = {'channel': channel} - response = self.Base.db_execute_query("DELETE FROM votekick_channel WHERE channel = :channel", mes_donnes) - - affected_row = response.rowcount - - if affected_row > 0: - return True - else: - return False - - def join_saved_channels(self) -> None: - - param = {'module_name': self.module_name} - result = self.Base.db_execute_query(f"SELECT id, channel_name FROM {self.Config.TABLE_CHANNEL} WHERE module_name = :module_name", param) - - channels = result.fetchall() - unixtime = self.Base.get_unixtime() - - for channel in channels: - id, chan = channel - self.insert_vote_channel(self.VoteChannelModel(channel_name=chan, target_user='', voter_users=[], vote_for=0, vote_against=0)) - self.Protocol.sjoin(channel=chan) - self.Protocol.send2socket(f":{self.Config.SERVICE_NICKNAME} SAMODE {chan} +o {self.Config.SERVICE_NICKNAME}") - - return None - - def is_vote_ongoing(self, channel: str) -> bool: - - response = False - for vote in self.VOTE_CHANNEL_DB: - if vote.channel_name == channel: - if vote.target_user: - response = True - - return response - - def timer_vote_verdict(self, channel: str) -> None: - - dnickname = self.Config.SERVICE_NICKNAME - - if not self.is_vote_ongoing(channel): + cmd = data.copy() if isinstance(data, list) else list(data).copy() + index, command = self.Irc.Protocol.get_ircd_protocol_poisition(cmd) + if index == -1: return None - for chan in self.VOTE_CHANNEL_DB: - if chan.channel_name == channel: - target_user = self.User.get_nickname(chan.target_user) - if chan.vote_for > chan.vote_against: - self.Protocol.send_priv_msg( - nick_from=dnickname, - msg=f"User {self.Config.COLORS.bold}{target_user}{self.Config.COLORS.nogc} has {chan.vote_against} votes against and {chan.vote_for} votes for. For this reason, it'll be kicked from the channel", - channel=channel - ) - self.Protocol.send2socket(f":{dnickname} KICK {channel} {target_user} Following the vote, you are not welcome in {channel}") - self.Channel.delete_user_from_channel(channel, self.User.get_uid(target_user)) - elif chan.vote_for <= chan.vote_against: - self.Protocol.send_priv_msg( - nick_from=dnickname, - msg=f"User {self.Config.COLORS.bold}{target_user}{self.Config.COLORS.nogc} has {chan.vote_against} votes against and {chan.vote_for} votes for. For this reason, it\'ll remain in the channel", - channel=channel - ) - - # Init the system - if self.init_vote_system(channel): - self.Protocol.send_priv_msg( - nick_from=dnickname, - msg="System vote re initiated", - channel=channel - ) - - return None - - def cmd(self, data:list) -> None: try: - cmd = list(data).copy() - return None + + match command: + + case 'PRIVMSG': + return None + + case 'QUIT': + return None + + case _: + return None except KeyError as ke: self.Logs.error(f"Key Error: {ke}") @@ -256,11 +164,12 @@ class Votekick(): except Exception as err: self.Logs.error(f"General Error: {err}") - def hcmds(self, user:str, channel: any, cmd: list, fullcmd: list = []) -> None: + def hcmds(self, user:str, channel: Any, cmd: list, fullcmd: Optional[list] = None) -> None: # cmd is the command starting from the user command # full cmd is sending the entire server response command = str(cmd[0]).lower() + fullcmd = fullcmd dnickname = self.Config.SERVICE_NICKNAME fromuser = user fromchannel = channel @@ -287,32 +196,25 @@ class Votekick(): case 'activate': try: # vote activate #channel - if self.Admin.get_Admin(fromuser) is None: + if self.Admin.get_admin(fromuser) is None: self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' :Your are not allowed to execute this command') return None - sentchannel = str(cmd[2]).lower() if self.Channel.Is_Channel(str(cmd[2]).lower()) else None + sentchannel = str(cmd[2]).lower() if self.Channel.is_valid_channel(str(cmd[2]).lower()) else None if sentchannel is None: self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f" The correct command is {self.Config.SERVICE_PREFIX}{command} {option} #CHANNEL") - self.insert_vote_channel( - self.VoteChannelModel( - channel_name=sentchannel, - target_user='', - voter_users=[], - vote_for=0, - vote_against=0 - ) - ) + if self.VoteKickManager.activate_new_channel(sentchannel): + self.Channel.db_query_channel('add', self.module_name, sentchannel) + self.Protocol.send_join_chan(uidornickname=dnickname, channel=sentchannel) + self.Protocol.send2socket(f":{dnickname} SAMODE {sentchannel} +o {dnickname}") + self.Protocol.send_priv_msg(nick_from=dnickname, + msg="You can now use !submit to decide if he will stay or not on this channel ", + channel=sentchannel + ) - self.Channel.db_query_channel('add', self.module_name, sentchannel) + return None - self.Protocol.send_join_chan(uidornickname=dnickname, channel=sentchannel) - self.Protocol.send2socket(f":{dnickname} SAMODE {sentchannel} +o {dnickname}") - self.Protocol.send_priv_msg(nick_from=dnickname, - msg="You can now use !submit to decide if he will stay or not on this channel ", - channel=sentchannel - ) except Exception as err: self.Logs.error(f'{err}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} {command} {option} #channel') @@ -321,23 +223,21 @@ class Votekick(): case 'deactivate': try: # vote deactivate #channel - if self.Admin.get_Admin(fromuser) is None: + if self.Admin.get_admin(fromuser) is None: self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f" Your are not allowed to execute this command") return None - sentchannel = str(cmd[2]).lower() if self.Channel.Is_Channel(str(cmd[2]).lower()) else None + sentchannel = str(cmd[2]).lower() if self.Channel.is_valid_channel(str(cmd[2]).lower()) else None if sentchannel is None: self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f" The correct command is {self.Config.SERVICE_PREFIX}{command} {option} #CHANNEL") self.Protocol.send2socket(f":{dnickname} SAMODE {sentchannel} -o {dnickname}") self.Protocol.send_part_chan(uidornickname=dnickname, channel=sentchannel) - for chan in self.VOTE_CHANNEL_DB: - if chan.channel_name == sentchannel: - self.VOTE_CHANNEL_DB.remove(chan) - self.Channel.db_query_channel('del', self.module_name, chan.channel_name) + if self.VoteKickManager.drop_vote_channel_model(sentchannel): + self.Channel.db_query_channel('del', self.module_name, sentchannel) + return None - self.Logs.debug(f"The Channel {sentchannel} has been deactivated from the vote system") except Exception as err: self.Logs.error(f'{err}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f" /msg {dnickname} {command} {option} #channel") @@ -347,20 +247,11 @@ class Votekick(): try: # vote + channel = fromchannel - for chan in self.VOTE_CHANNEL_DB: - if chan.channel_name == channel: - if fromuser in chan.voter_users: - self.Protocol.send_priv_msg(nick_from=dnickname, - msg="You already submitted a vote", - channel=channel - ) - else: - chan.vote_for += 1 - chan.voter_users.append(fromuser) - self.Protocol.send_priv_msg(nick_from=dnickname, - msg="Vote recorded, thank you", - channel=channel - ) + if self.VoteKickManager.action_vote(channel, fromuser, '+'): + self.Protocol.send_priv_msg(nick_from=dnickname, msg="Vote recorded, thank you",channel=channel) + else: + self.Protocol.send_priv_msg(nick_from=dnickname, msg="You already submitted a vote", channel=channel) + except Exception as err: self.Logs.error(f'{err}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} {command} {option}') @@ -370,20 +261,11 @@ class Votekick(): try: # vote - channel = fromchannel - for chan in self.VOTE_CHANNEL_DB: - if chan.channel_name == channel: - if fromuser in chan.voter_users: - self.Protocol.send_priv_msg(nick_from=dnickname, - msg="You already submitted a vote", - channel=channel - ) - else: - chan.vote_against += 1 - chan.voter_users.append(fromuser) - self.Protocol.send_priv_msg(nick_from=dnickname, - msg="Vote recorded, thank you", - channel=channel - ) + if self.VoteKickManager.action_vote(channel, fromuser, '-'): + self.Protocol.send_priv_msg(nick_from=dnickname, msg="Vote recorded, thank you",channel=channel) + else: + self.Protocol.send_priv_msg(nick_from=dnickname, msg="You already submitted a vote", channel=channel) + except Exception as err: self.Logs.error(f'{err}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} {command} {option}') @@ -392,7 +274,7 @@ class Votekick(): case 'cancel': try: # vote cancel - if self.Admin.get_Admin(fromuser) is None: + if self.Admin.get_admin(fromuser) is None: self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' Your are not allowed to execute this command') return None @@ -400,13 +282,13 @@ class Votekick(): self.Logs.error(f"The channel is not known, defender can't cancel the vote") self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' You need to specify the channel => /msg {dnickname} vote_cancel #channel') - for vote in self.VOTE_CHANNEL_DB: + for vote in self.VoteKickManager.VOTE_CHANNEL_DB: if vote.channel_name == channel: - self.init_vote_system(channel) - self.Protocol.send_priv_msg(nick_from=dnickname, - msg="Vote system re-initiated", - channel=channel - ) + if self.VoteKickManager.init_vote_system(channel): + self.Protocol.send_priv_msg(nick_from=dnickname, + msg="Vote system re-initiated", + channel=channel + ) except Exception as err: self.Logs.error(f'{err}') @@ -416,7 +298,7 @@ class Votekick(): case 'status': try: # vote status - for chan in self.VOTE_CHANNEL_DB: + for chan in self.VoteKickManager.VOTE_CHANNEL_DB: if chan.channel_name == channel: self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"Channel: {chan.channel_name} | Target: {self.User.get_nickname(chan.target_user)} | For: {chan.vote_for} | Against: {chan.vote_against} | Number of voters: {str(len(chan.voter_users))}", @@ -430,25 +312,25 @@ class Votekick(): case 'submit': try: # vote submit nickname - if self.Admin.get_Admin(fromuser) is None: + if self.Admin.get_admin(fromuser) is None: self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' Your are not allowed to execute this command') return None nickname_submitted = cmd[2] uid_submitted = self.User.get_uid(nickname_submitted) user_submitted = self.User.get_User(nickname_submitted) + ongoing_user = None # check if there is an ongoing vote - if self.is_vote_ongoing(channel): - for vote in self.VOTE_CHANNEL_DB: - if vote.channel_name == channel: - ongoing_user = self.User.get_nickname(vote.target_user) - - self.Protocol.send_priv_msg(nick_from=dnickname, - msg=f"There is an ongoing vote on {ongoing_user}", - channel=channel - ) - return False + if self.VoteKickManager.is_vote_ongoing(channel): + votec = self.VoteKickManager.get_vote_channel_model(channel) + if votec: + ongoing_user = self.User.get_nickname(votec.target_user) + self.Protocol.send_priv_msg(nick_from=dnickname, + msg=f"There is an ongoing vote on {ongoing_user}", + channel=channel + ) + return None # check if the user exist if user_submitted is None: @@ -456,24 +338,24 @@ class Votekick(): msg=f"This nickname <{nickname_submitted}> do not exist", channel=channel ) - return False + return None - uid_cleaned = self.Base.clean_uid(uid_submitted) - ChannelInfo = self.Channel.get_Channel(channel) - if ChannelInfo is None: + uid_cleaned = self.Loader.Utils.clean_uid(uid_submitted) + channel_obj = self.Channel.get_channel(channel) + if channel_obj is None: self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' This channel [{channel}] do not exist in the Channel Object') - return False + return None clean_uids_in_channel: list = [] - for uid in ChannelInfo.uids: - clean_uids_in_channel.append(self.Base.clean_uid(uid)) + for uid in channel_obj.uids: + clean_uids_in_channel.append(self.Loader.Utils.clean_uid(uid)) if not uid_cleaned in clean_uids_in_channel: self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"This nickname <{nickname_submitted}> is not available in this channel", channel=channel ) - return False + return None # check if Ircop or Service or Bot pattern = fr'[o|B|S]' @@ -483,9 +365,9 @@ class Votekick(): msg="You cant vote for this user ! he/she is protected", channel=channel ) - return False + return None - for chan in self.VOTE_CHANNEL_DB: + for chan in self.VoteKickManager.VOTE_CHANNEL_DB: if chan.channel_name == channel: chan.target_user = self.User.get_uid(nickname_submitted) @@ -494,7 +376,7 @@ class Votekick(): channel=channel ) - self.Base.create_timer(60, self.timer_vote_verdict, (channel, )) + self.Base.create_timer(60, self.Threads.timer_vote_verdict, (self, channel)) self.Protocol.send_priv_msg(nick_from=dnickname, msg="This vote will end after 60 secondes", channel=channel @@ -508,33 +390,34 @@ class Votekick(): case 'verdict': try: # vote verdict - if self.Admin.get_Admin(fromuser) is None: + if self.Admin.get_admin(fromuser) is None: self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f'Your are not allowed to execute this command') return None - - for chan in self.VOTE_CHANNEL_DB: - if chan.channel_name == channel: - target_user = self.User.get_nickname(chan.target_user) - if chan.vote_for > chan.vote_against: - self.Protocol.send_priv_msg(nick_from=dnickname, - msg=f"User {self.Config.COLORS.bold}{target_user}{self.Config.COLORS.nogc} has {chan.vote_against} votes against and {chan.vote_for} votes for. For this reason, it\'ll be kicked from the channel", + + votec = self.VoteKickManager.get_vote_channel_model(channel) + if votec: + target_user = self.User.get_nickname(votec.target_user) + if votec.vote_for >= votec.vote_against: + self.Protocol.send_priv_msg(nick_from=dnickname, + msg=f"User {self.Config.COLORS.bold}{target_user}{self.Config.COLORS.nogc} has {votec.vote_against} votes against and {votec.vote_for} votes for. For this reason, it\'ll be kicked from the channel", channel=channel ) - self.Protocol.send2socket(f":{dnickname} KICK {channel} {target_user} Following the vote, you are not welcome in {channel}") - elif chan.vote_for <= chan.vote_against: - self.Protocol.send_priv_msg( + self.Protocol.send2socket(f":{dnickname} KICK {channel} {target_user} Following the vote, you are not welcome in {channel}") + else: + self.Protocol.send_priv_msg( nick_from=dnickname, - msg=f"User {self.Config.COLORS.bold}{target_user}{self.Config.COLORS.nogc} has {chan.vote_against} votes against and {chan.vote_for} votes for. For this reason, it\'ll remain in the channel", + msg=f"User {self.Config.COLORS.bold}{target_user}{self.Config.COLORS.nogc} has {votec.vote_against} votes against and {votec.vote_for} votes for. For this reason, it\'ll remain in the channel", channel=channel ) - - # Init the system - if self.init_vote_system(channel): - self.Protocol.send_priv_msg( + + if self.VoteKickManager.init_vote_system(channel): + self.Protocol.send_priv_msg( nick_from=dnickname, msg="System vote re initiated", channel=channel ) + return None + except Exception as err: self.Logs.error(f'{err}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} {command} {option}') @@ -548,4 +431,8 @@ class Votekick(): self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote cancel') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote status') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote submit nickname') - self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote verdict') \ No newline at end of file + self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote verdict') + return None + + case _: + return None \ No newline at end of file diff --git a/mods/votekick/schemas.py b/mods/votekick/schemas.py new file mode 100644 index 0000000..6948f1e --- /dev/null +++ b/mods/votekick/schemas.py @@ -0,0 +1,11 @@ +from typing import Optional +from core.definition import MainModel +from dataclasses import dataclass, field + +@dataclass +class VoteChannelModel(MainModel): + channel_name: Optional[str] = None + target_user: Optional[str] = None + voter_users: list = field(default_factory=list) + vote_for: int = 0 + vote_against: int = 0 diff --git a/mods/votekick/threads.py b/mods/votekick/threads.py new file mode 100644 index 0000000..66d4edc --- /dev/null +++ b/mods/votekick/threads.py @@ -0,0 +1,40 @@ +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from mods.votekick.mod_votekick import Votekick + +def timer_vote_verdict(uplink: 'Votekick', channel: str) -> None: + + dnickname = uplink.Config.SERVICE_NICKNAME + + if not uplink.VoteKickManager.is_vote_ongoing(channel): + return None + + votec = uplink.VoteKickManager.get_vote_channel_model(channel) + if votec: + target_user = uplink.User.get_nickname(votec.target_user) + + if votec.vote_for >= votec.vote_against and votec.vote_for != 0: + uplink.Protocol.send_priv_msg(nick_from=dnickname, + msg=f"User {uplink.Config.COLORS.bold}{target_user}{uplink.Config.COLORS.nogc} has {votec.vote_against} votes against and {votec.vote_for} votes for. For this reason, it\'ll be kicked from the channel", + channel=channel + ) + uplink.Protocol.send2socket(f":{dnickname} KICK {channel} {target_user} Following the vote, you are not welcome in {channel}") + else: + uplink.Protocol.send_priv_msg( + nick_from=dnickname, + msg=f"User {uplink.Config.COLORS.bold}{target_user}{uplink.Config.COLORS.nogc} has {votec.vote_against} votes against and {votec.vote_for} votes for. For this reason, it\'ll remain in the channel", + channel=channel + ) + + if uplink.VoteKickManager.init_vote_system(channel): + uplink.Protocol.send_priv_msg( + nick_from=dnickname, + msg="System vote re initiated", + channel=channel + ) + + return None + + return None \ No newline at end of file diff --git a/mods/votekick/utils.py b/mods/votekick/utils.py new file mode 100644 index 0000000..7f28bc9 --- /dev/null +++ b/mods/votekick/utils.py @@ -0,0 +1,74 @@ +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from mods.votekick.mod_votekick import Votekick + +def add_vote_channel_to_database(uplink: 'Votekick', channel: str) -> bool: + """Adds a new channel to the votekick database if it doesn't already exist. + + This function checks if the specified channel is already registered in the + `votekick_channel` table. If not, it inserts a new entry with the current timestamp. + + Args: + uplink (Votekick): The main votekick system instance that provides access to utilities and database operations. + channel (str): The name of the channel to be added to the database. + + Returns: + bool: True if the channel was successfully inserted into the database. + False if the channel already exists or the insertion failed. + """ + current_datetime = uplink.Utils.get_sdatetime() + mes_donnees = {'channel': channel} + + response = uplink.Base.db_execute_query("SELECT id FROM votekick_channel WHERE channel = :channel", mes_donnees) + + is_channel_exist = response.fetchone() + + if is_channel_exist is None: + mes_donnees = {'datetime': current_datetime, 'channel': channel} + insert = uplink.Base.db_execute_query(f"INSERT INTO votekick_channel (datetime, channel) VALUES (:datetime, :channel)", mes_donnees) + if insert.rowcount > 0: + return True + else: + return False + else: + return False + +def delete_vote_channel_from_database(uplink: 'Votekick', channel: str) -> bool: + """Deletes a channel entry from the votekick database. + + This function removes the specified channel from the `votekick_channel` table + if it exists. It returns True if the deletion was successful. + + Args: + uplink (Votekick): The main votekick system instance used to execute the database operation. + channel (str): The name of the channel to be removed from the database. + + Returns: + bool: True if the channel was successfully deleted, False if no rows were affected. + """ + mes_donnes = {'channel': channel} + response = uplink.Base.db_execute_query("DELETE FROM votekick_channel WHERE channel = :channel", mes_donnes) + + affected_row = response.rowcount + + if affected_row > 0: + return True + else: + return False + +def join_saved_channels(uplink: 'Votekick') -> None: + + param = {'module_name': uplink.module_name} + result = uplink.Base.db_execute_query(f"SELECT id, channel_name FROM {uplink.Config.TABLE_CHANNEL} WHERE module_name = :module_name", param) + + channels = result.fetchall() + + for channel in channels: + id_, chan = channel + uplink.VoteKickManager.activate_new_channel(chan) + uplink.Protocol.send_sjoin(channel=chan) + uplink.Protocol.send2socket(f":{uplink.Config.SERVICE_NICKNAME} SAMODE {chan} +o {uplink.Config.SERVICE_NICKNAME}") + + return None \ No newline at end of file diff --git a/mods/votekick/votekick_manager.py b/mods/votekick/votekick_manager.py new file mode 100644 index 0000000..1eb7098 --- /dev/null +++ b/mods/votekick/votekick_manager.py @@ -0,0 +1,163 @@ +from typing import TYPE_CHECKING, Literal, Optional +from mods.votekick.schemas import VoteChannelModel + +if TYPE_CHECKING: + from mods.votekick.mod_votekick import Votekick + +class VotekickManager: + + VOTE_CHANNEL_DB:list[VoteChannelModel] = [] + + def __init__(self, uplink: 'Votekick'): + self.uplink = uplink + self.Logs = uplink.Logs + self.Utils = uplink.Utils + + def activate_new_channel(self, channel_name: str) -> bool: + """Activate a new channel in the votekick systeme + + Args: + channel_name (str): The channel name you want to activate + + Returns: + bool: True if it was activated + """ + votec = self.get_vote_channel_model(channel_name) + + if votec is None: + self.VOTE_CHANNEL_DB.append( + VoteChannelModel( + channel_name=channel_name, + target_user='', + voter_users=[], + vote_for=0, + vote_against=0 + ) + ) + self.Logs.debug(f"[VOTEKICK MANAGER] {channel_name} has been activated.") + return True + + return False + + def init_vote_system(self, channel_name: str) -> bool: + """Initializes or resets the votekick system for a given channel. + + This method clears the current target, voter list, and vote counts + in preparation for a new votekick session. + + Args: + channel_name (str): The name of the channel for which the votekick system should be initialized. + + Returns: + bool: True if the votekick system was successfully initialized, False if the channel is not found. + """ + votec = self.get_vote_channel_model(channel_name) + + if votec is None: + self.Logs.debug(f"[VOTEKICK MANAGER] The channel ({channel_name}) is not active!") + return False + + votec.target_user = '' + votec.voter_users = [] + votec.vote_for = 0 + votec.vote_against = 0 + self.Logs.debug(f"[VOTEKICK MANAGER] The channel ({channel_name}) has been successfully initialized!") + return True + + def get_vote_channel_model(self, channel_name: str) -> Optional[VoteChannelModel]: + """Get Vote Channel Object model + + Args: + channel_name (str): The channel name you want to activate + + Returns: + (VoteChannelModel | None): The VoteChannelModel if exist + """ + for vote in self.VOTE_CHANNEL_DB: + if vote.channel_name.lower() == channel_name.lower(): + self.Logs.debug(f"[VOTEKICK MANAGER] {channel_name} has been found in the VOTE_CHANNEL_DB") + return vote + + return None + + def drop_vote_channel_model(self, channel_name: str) -> bool: + """Drop a channel from the votekick system. + + Args: + channel_name (str): The channel name you want to drop + + Returns: + bool: True if the channel has been droped. + """ + votec = self.get_vote_channel_model(channel_name) + + if votec: + self.VOTE_CHANNEL_DB.remove(votec) + self.Logs.debug(f"[VOTEKICK MANAGER] {channel_name} has been removed from the VOTE_CHANNEL_DB") + return True + + return False + + def is_vote_ongoing(self, channel_name: str) -> bool: + """Check if there is an angoing vote on the channel provided + + Args: + channel_name (str): The channel name to check + + Returns: + bool: True if there is an ongoing vote on the channel provided. + """ + + votec = self.get_vote_channel_model(channel_name) + + if votec is None: + self.Logs.debug(f"[VOTEKICK MANAGER] {channel_name} is not activated!") + return False + + if votec.target_user: + self.Logs.debug(f'[VOTEKICK MANAGER] A vote is ongoing on {channel_name}') + return True + + self.Logs.debug(f'[VOTEKICK MANAGER] {channel_name} is activated but there is no ongoing vote!') + + return False + + def action_vote(self, channel_name: str, nickname: str, action: Literal['+', '-']) -> bool: + """ + Registers a vote (for or against) in an active votekick session on a channel. + + Args: + channel_name (str): The name of the channel where the votekick session is active. + nickname (str): The nickname of the user casting the vote. + action (Literal['+', '-']): The vote action. Use '+' to vote for kicking, '-' to vote against. + + Returns: + bool: True if the vote was successfully registered, False otherwise. + This can fail if: + - The action is invalid (not '+' or '-') + - The user has already voted + - The channel has no active votekick session + """ + if action not in ['+', '-']: + self.Logs.debug(f"[VOTEKICK MANAGER] The action must be + or - while you have provided ({action})") + return False + votec = self.get_vote_channel_model(channel_name) + + if votec: + client_obj = self.uplink.User.get_User(votec.target_user) + client_to_punish = votec.target_user if client_obj is None else client_obj.nickname + if nickname in votec.voter_users: + self.Logs.debug(f"[VOTEKICK MANAGER] This nickname ({nickname}) has already voted for ({client_to_punish})") + return False + else: + if action == '+': + votec.vote_for += 1 + elif action == '-': + votec.vote_against += 1 + + votec.voter_users.append(nickname) + self.Logs.debug(f"[VOTEKICK MANAGER] The ({nickname}) has voted to ban ({client_to_punish})") + return True + else: + self.Logs.debug(f"[VOTEKICK MANAGER] This channel {channel_name} is not active!") + return False 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 diff --git a/version.json b/version.json index 10eeed3..b25ce68 100644 --- a/version.json +++ b/version.json @@ -1,9 +1,9 @@ { - "version": "6.1.4", + "version": "6.2.0", "requests": "2.32.3", "psutil": "6.0.0", - "unrealircd_rpc_py": "2.0.0", + "unrealircd_rpc_py": "2.0.4", "sqlalchemy": "2.0.35", "faker": "30.1.0" } \ No newline at end of file