diff --git a/core/base.py b/core/base.py index 18a28c7..2c55877 100644 --- a/core/base.py +++ b/core/base.py @@ -9,9 +9,9 @@ from core.loadConf import ConfigDataModel class Base: - CORE_DB_PATH = 'core' + os.sep + 'db' + os.sep # Le dossier bases de données core - MODS_DB_PATH = 'mods' + os.sep + 'db' + os.sep # Le dossier bases de données des modules - PYTHON_MIN_VERSION = '3.10' # Version min de python + # CORE_DB_PATH = 'core' + os.sep + 'db' + os.sep # Le dossier bases de données core + # MODS_DB_PATH = 'mods' + os.sep + 'db' + os.sep # Le dossier bases de données des modules + # PYTHON_MIN_VERSION = '3.10' # Version min de python def __init__(self, Config: ConfigDataModel) -> None: @@ -26,6 +26,7 @@ class Base: self.lock = threading.RLock() # Création du lock + self.install: bool = False # Initialisation de la variable d'installation self.engine, self.cursor = self.db_init() # Initialisation de la connexion a la base de données self.__create_db() # Initialisation de la base de données @@ -200,7 +201,7 @@ class Base: else: return False - def db_record_module(self, user_cmd:str, module_name:str) -> 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: @@ -210,7 +211,7 @@ class Base: 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': 0} + mes_donnees = {'datetime': self.get_datetime(), '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") @@ -532,6 +533,7 @@ class Base: full_path_db = self.Config.db_path + self.Config.db_name if not os.path.exists(db_directory): + self.install = True os.makedirs(db_directory) engine = create_engine(f'sqlite:///{full_path_db}.db', echo=False) @@ -600,6 +602,11 @@ class Base: self.db_execute_query(table_core_channel) self.db_execute_query(table_core_config) + if self.install: + self.db_record_module('sys', 'mod_command', 1) + self.db_record_module('sys', 'mod_defender', 1) + self.install = False + return None def db_execute_query(self, query:str, params:dict = {}) -> CursorResult: diff --git a/core/installation.py b/core/installation.py index 2780b52..4fe97e9 100644 --- a/core/installation.py +++ b/core/installation.py @@ -1,6 +1,4 @@ -from importlib.util import find_spec from dataclasses import dataclass -from pathlib import Path from subprocess import check_call, run, CalledProcessError, PIPE from platform import python_version, python_version_tuple from sys import exit @@ -10,9 +8,11 @@ class Install: @dataclass class CoreConfig: + install_log_file: str unix_systemd_folder: str service_file_name: str service_cmd_executable: list + service_cmd_daemon_reload: list defender_main_executable: str python_min_version: str python_current_version_tuple: tuple[str, str, str] @@ -27,14 +27,17 @@ class Install: def __init__(self) -> None: self.set_configuration() - - if self.skip_install: - return None if not self.check_python_version(): # Tester si c'est la bonne version de python exit("Python Version Error") else: + + if self.skip_install: + return None + + print(f'Configuration loaded : {self.config}') + # Sinon tester les dependances python et les installer avec pip if self.do_install(): @@ -56,9 +59,11 @@ class Install: defender_main_executable = os.path.join(defender_install_folder, 'main.py') self.config = self.CoreConfig( + install_log_file='install.log', unix_systemd_folder=unix_systemd_folder, service_file_name='defender.service', service_cmd_executable=['systemctl', '--user', 'start', 'defender'], + service_cmd_daemon_reload=['systemctl', '--user', 'daemon-reload'], defender_main_executable=defender_main_executable, python_min_version='3.10', python_current_version_tuple=python_version_tuple(), @@ -70,12 +75,24 @@ class Install: venv_pip_executable=f'{os.path.join(defender_install_folder, venv_folder, "bin")}{os.sep}pip', venv_python_executable=f'{os.path.join(defender_install_folder, venv_folder, "bin")}{os.sep}python' ) - # Exclude Windows OS if os.name == 'nt': #print('/!\\ Skip installation /!\\') self.skip_install = True + else: + if self.is_root(): + self.skip_install = True + + def is_root(self) -> bool: + + if os.geteuid() != 0: + print('User without privileges ==> PASS') + return False + elif os.geteuid() == 0: + print('/!\\ Do not use root to install Defender /!\\') + exit("Do not use root to install Defender") + return True def do_install(self) -> bool: @@ -93,12 +110,12 @@ class Install: def run_subprocess(self, command:list) -> None: - print(command) + print(f'> {command}') try: check_call(command) - print("La commande s'est terminée avec succès.") + print("The command completed successfully.") except CalledProcessError as e: - print(f"La commande a échoué avec le code de retour :{e.returncode}") + print(f"The command failed with the return code: {e.returncode}") print(f"Try to install dependencies ...") exit(5) @@ -123,7 +140,7 @@ class Install: print(f"## Your python version must be greather than or equal to {self.config.python_current_version} ##") return False - print(f"===> Version of python : {self.config.python_current_version} ==> OK") + print(f"> Version of python : {self.config.python_current_version} ==> OK") return True @@ -133,7 +150,8 @@ class Install: # Run a command in the virtual environment's Python to check if the package is installed run([self.config.venv_python_executable, '-c', f'import {package_name}'], check=True, stdout=PIPE, stderr=PIPE) return True - except CalledProcessError: + except CalledProcessError as cpe: + print(cpe) return False def install_dependencies(self) -> None: @@ -162,7 +180,7 @@ class Install: print("===> Verifier si pip est a jour") self.run_subprocess([self.config.venv_python_executable, '-m', 'pip', 'install', '--upgrade', 'pip']) - if find_spec('greenlet') is None: + if not self.check_package('greenlet'): self.run_subprocess([self.config.venv_pip_executable, 'install', '--only-binary', ':all:', 'greenlet']) print('====> Module Greenlet installé') @@ -180,6 +198,7 @@ class Install: if os.path.exists(full_service_file_path): print(f'/!\\ Service file already exist /!\\') + self.run_subprocess(self.config.service_cmd_executable) return None contain = f'''[Unit] @@ -192,7 +211,7 @@ SyslogIdentifier=Defender Restart=on-failure [Install] -WantedBy=multi-user.target +WantedBy=default.target ''' # Check if user systemd is available (.config/systemd/user/) if not os.path.exists(self.config.unix_systemd_folder): @@ -203,6 +222,7 @@ WantedBy=multi-user.target servicefile.close() print(f'Service file generated with current configuration') print(f'Running Defender IRC Service ...') + self.run_subprocess(self.config.service_cmd_daemon_reload) self.run_subprocess(self.config.service_cmd_executable) else: @@ -211,13 +231,14 @@ WantedBy=multi-user.target servicefile.close() print(f'Service file generated with current configuration') print(f'Running Defender IRC Service ...') + self.run_subprocess(self.config.service_cmd_daemon_reload) self.run_subprocess(self.config.service_cmd_executable) def print_final_message(self) -> None: print(f"#"*24) print("Installation complete ...") - print("You must change environment using the command below") - print(f"source {self.config.defender_install_folder}{os.sep}{self.config.venv_folder}{os.sep}bin{os.sep}activate") + print("If the configuration is correct, then you must see your service connected to your irc server") + print(f"If any issue, you can see the log file for debug {self.config.defender_install_folder}{os.sep}logs{os.sep}defender.log") print(f"#"*24) exit(1) diff --git a/core/irc.py b/core/irc.py index 794e3ca..99853d9 100644 --- a/core/irc.py +++ b/core/irc.py @@ -1,7 +1,7 @@ import ssl, re, importlib, sys, time, threading, socket from ssl import SSLSocket from datetime import datetime, timedelta -from typing import Union +from typing import Union, Literal from core.loadConf import Config from core.Model import User, Admin, Channel, Clones from core.base import Base @@ -21,6 +21,8 @@ class Irc: self.INIT = 1 # Variable d'intialisation | 1 -> indique si le programme est en cours d'initialisation self.RESTART = 0 # Variable pour le redemarrage du bot | 0 -> indique que le programme n'es pas en cours de redemarrage self.CHARSET = ['utf-8', 'iso-8859-1'] # Charset utiliser pour décoder/encoder les messages + """0: utf-8 | 1: iso-8859-1""" + self.SSL_VERSION = None # Version SSL self.Config = Config().ConfigObject @@ -200,19 +202,22 @@ class Irc: version = self.Config.current_version unixtime = self.Base.get_unixtime() + charset = self.CHARSET[0] # Envoyer un message d'identification - writer.send(f":{sid} PASS :{password}\r\n".encode('utf-8')) - writer.send(f":{sid} PROTOCTL SID NOQUIT NICKv2 SJOIN SJ3 NICKIP TKLEXT2 NEXTBANS CLK EXTSWHOIS MLOCK MTAGS\r\n".encode('utf-8')) - # writer.send(f":{sid} PROTOCTL NICKv2 VHP UMODE2 NICKIP SJOIN SJOIN2 SJ3 NOQUIT TKLEXT MLOCK SID MTAGS\r\n".encode('utf-8')) - writer.send(f":{sid} PROTOCTL EAUTH={link},,,{service_name}-v{version}\r\n".encode('utf-8')) - writer.send(f":{sid} PROTOCTL SID={sid}\r\n".encode('utf-8')) - writer.send(f":{sid} SERVER {link} 1 :{info}\r\n".encode('utf-8')) - writer.send(f":{sid} {nickname} :Reserved for services\r\n".encode('utf-8')) - writer.send(f":{sid} UID {nickname} 1 {unixtime} {username} {host} {service_id} * {smodes} * * * :{realname}\r\n".encode('utf-8')) - writer.send(f":{sid} SJOIN {unixtime} {chan} + :{service_id}\r\n".encode('utf-8')) - writer.send(f":{sid} MODE {chan} +{cmodes}\r\n".encode('utf-8')) - writer.send(f":{sid} SAMODE {chan} +{umodes} {nickname}\r\n".encode('utf-8')) + writer.send(f":{sid} PASS :{password}\r\n".encode(charset)) + writer.send(f":{sid} PROTOCTL SID NOQUIT NICKv2 SJOIN SJ3 NICKIP TKLEXT2 NEXTBANS CLK EXTSWHOIS MLOCK MTAGS\r\n".encode(charset)) + # writer.send(f":{sid} PROTOCTL NICKv2 VHP UMODE2 NICKIP SJOIN SJOIN2 SJ3 NOQUIT TKLEXT MLOCK SID MTAGS\r\n".encode(charset)) + writer.send(f":{sid} PROTOCTL EAUTH={link},,,{service_name}-v{version}\r\n".encode(charset)) + writer.send(f":{sid} PROTOCTL SID={sid}\r\n".encode(charset)) + writer.send(f":{sid} SERVER {link} 1 :{info}\r\n".encode(charset)) + writer.send(f":{sid} {nickname} :Reserved for services\r\n".encode(charset)) + writer.send(f":{sid} UID {nickname} 1 {unixtime} {username} {host} {service_id} * {smodes} * * * :{realname}\r\n".encode(charset)) + writer.send(f":{sid} SJOIN {unixtime} {chan} + :{service_id}\r\n".encode(charset)) + writer.send(f":{sid} TKL + Q * {nickname} {host} 0 {unixtime} :Reserved for services\r\n".encode(charset)) + + writer.send(f":{service_id} MODE {chan} +{cmodes}\r\n".encode(charset)) + writer.send(f":{service_id} MODE {chan} +{umodes} {service_id}\r\n".encode(charset)) self.Base.logs.debug('Link information sent to the server') @@ -221,9 +226,9 @@ class Irc: self.Base.logs.critical(f'{ae}') def __join_saved_channels(self) -> None: - - core_table = 'core_channel' - + """## Joining saved channels""" + core_table = self.Config.table_channel + query = f'''SELECT distinct channel_name FROM {core_table}''' exec_query = self.Base.db_execute_query(query) result_query = exec_query.fetchall() @@ -686,7 +691,10 @@ class Irc: else: version = f'{current_version}' - self.send2socket(f"JOIN {self.Config.SERVICE_CHANLOG}") + # self.send2socket(f":{self.Config.SERVICE_NICKNAME} SVSJOIN {self.Config.SERVICE_NICKNAME} {self.Config.SERVICE_CHANLOG}") + # self.send2socket(f":{self.Config.SERVICE_NICKNAME} MODE {self.Config.SERVICE_CHANLOG} +o {self.Config.SERVICE_NICKNAME}") + # self.send2socket(f":{self.Config.SERVICE_NICKNAME} MODE {self.Config.SERVICE_CHANLOG} +{self.Config.SERVICE_CMODES}") + print(f"################### DEFENDER ###################") print(f"# SERVICE CONNECTE ") print(f"# SERVEUR : {self.Config.SERVEUR_IP} ") @@ -775,8 +783,8 @@ class Irc: for i in range(start_boucle, len(cmd)): parsed_UID = str(cmd[i]) # pattern = fr'[:|@|%|\+|~|\*]*' - pattern = fr':' - parsed_UID = re.sub(pattern, '', parsed_UID) + # pattern = fr':' + # parsed_UID = re.sub(pattern, '', parsed_UID) clean_uid = self.Base.clean_uid(parsed_UID) if len(clean_uid) == 9: list_users.append(parsed_UID) diff --git a/core/loadConf.py b/core/loadConf.py index 79557b7..7002207 100644 --- a/core/loadConf.py +++ b/core/loadConf.py @@ -1,6 +1,6 @@ import json, sys from os import sep -from typing import Union +from typing import Union, Literal from dataclasses import dataclass, field ########################################## @@ -11,58 +11,128 @@ from dataclasses import dataclass, field class ConfigDataModel: SERVEUR_IP: str - SERVEUR_HOSTNAME: str # Le hostname du serveur IRC - SERVEUR_LINK: str # Host attendu par votre IRCd (ex. dans votre link block pour Unrealircd) - SERVEUR_PORT: int # Port du link - SERVEUR_PASSWORD: str # Mot de passe du link (Privilégiez argon2 sur Unrealircd) - SERVEUR_ID: str # SID (identification) du bot en tant que Services - SERVEUR_SSL: bool # Activer la connexion SSL + """Server public IP (could be 127.0.0.1 localhost)""" - SERVICE_NAME: str # Le nom du service - SERVICE_NICKNAME: str # Nick du bot sur IRC - SERVICE_REALNAME: str # Realname du bot - SERVICE_USERNAME: str # Ident du bot - SERVICE_HOST: str # Host du bot - SERVICE_INFO: str # swhois du bot - SERVICE_CHANLOG: str # Salon des logs et autres messages issus du bot - SERVICE_SMODES: str # Mode du service - SERVICE_CMODES: str # Mode du salon (#ChanLog) que le bot appliquera à son entrée - SERVICE_UMODES: str # Mode que le bot pourra se donner à sa connexion au salon chanlog - SERVICE_PREFIX: str # Prefix pour envoyer les commandes au bot - SERVICE_ID: str = field(init=False) # L'identifiant du service + SERVEUR_HOSTNAME: str + """IRC Server Hostname (your.hostname.extension)""" - OWNER: str # Identifiant du compte admin - PASSWORD: str # Mot de passe du compte admin + SERVEUR_LINK: str + """The link hostname (should be the same as your unrealircd link block)""" - SALON_JAIL: str # Salon pot de miel - SALON_JAIL_MODES: str # Mode du salon pot de miel - SALON_LIBERER: str # Le salon ou sera envoyé l'utilisateur clean + SERVEUR_PORT: int + """Server port as configured in your unrealircd link block""" - API_TIMEOUT: int # Timeout des api's + SERVEUR_PASSWORD: str + """Your link password""" - PORTS_TO_SCAN: list # Liste des ports a scanné pour une detection de proxy - WHITELISTED_IP: list # IP a ne pas scanner - GLINE_DURATION: str # La durée du gline + SERVEUR_ID: str + """Service identification could be Z01 should be unique""" - DEBUG_LEVEL: int # Le niveau des logs DEBUG 10 | INFO 20 | WARNING 30 | ERROR 40 | CRITICAL 50 + SERVEUR_SSL: bool + """Activate SSL connexion""" + + SERVICE_NAME: str + """Service name (Ex. Defender)""" + + SERVICE_NICKNAME: str + """Nickname of the service (Ex. Defender)""" + + SERVICE_REALNAME: str + """Realname of the service""" + + SERVICE_USERNAME: str + """Username of the service""" + + SERVICE_HOST: str + """The service hostname""" + + SERVICE_INFO: str + """Swhois of the service""" + + SERVICE_CHANLOG: str + """The channel used by the service (ex. #services)""" + + SERVICE_SMODES: str + """The service mode (ex. +ioqBS)""" + + SERVICE_CMODES: str + """The mode of the log channel (ex. ntsO)""" + + SERVICE_UMODES: str + """The mode of the service when joining chanlog (ex. o, the service will be operator in the chanlog)""" + + SERVICE_PREFIX: str + """The default prefix to communicate with the service""" + + SERVICE_ID: str = field(init=False) + """The service unique ID""" + + OWNER: str + """The nickname of the admin of the service""" + + PASSWORD: str + """The password of the admin of the service""" + + SALON_JAIL: str + """The JAIL channel (ex. #jail)""" + + SALON_JAIL_MODES: str + """The jail channel modes (ex. sS)""" + + SALON_LIBERER: str + """Channel where the nickname will be released""" + + API_TIMEOUT: int + """Default api timeout in second""" + + PORTS_TO_SCAN: list + """List of ports to scan available for proxy_scan in the mod_defender module""" + + WHITELISTED_IP: list + """List of remote IP to don't scan""" + + GLINE_DURATION: str + """Gline duration""" + + DEBUG_LEVEL:Literal[10, 20, 30, 40, 50] # Le niveau des logs DEBUG 10 | INFO 20 | WARNING 30 | ERROR 40 | CRITICAL 50 + """Logs level: DEBUG 10 | INFO 20 | WARNING 30 | ERROR 40 | CRITICAL 50""" CONFIG_COLOR: dict[str, str] table_admin: str + """Admin table""" + table_commande: str + """Core command table""" + table_log: str + """Core log table""" + table_module: str + """Core module table""" + table_config: str + """Core configuration table""" + table_channel: str + """Core channel table""" current_version: str + """Current version of Defender""" + latest_version: str + """The Latest version fetched from github""" + db_name: str + """The database name""" + db_path: str + """The database path""" def __post_init__(self): # Initialiser SERVICE_ID après la création de l'objet self.SERVICE_ID:str = f"{self.SERVEUR_ID}AAAAAB" + """The service ID which is SERVEUR_ID and AAAAAB""" class Config: diff --git a/mods/mod_command.py b/mods/mod_command.py index cba38ef..7baee47 100644 --- a/mods/mod_command.py +++ b/mods/mod_command.py @@ -35,7 +35,7 @@ class Command(): # Create module commands (Mandatory) self.commands_level = { 1: ['join', 'part'], - 2: ['owner', 'deowner', 'op', 'deop', 'halfop', 'dehalfop', 'voice', 'devoice', 'ban', 'unban','kick', 'kickban', 'umode'] + 2: ['owner', 'deowner', 'op', 'deop', 'halfop', 'dehalfop', 'voice', 'devoice', 'deopall', 'devoiceall', 'voiceall', 'ban', 'unban','kick', 'kickban', 'umode'] } # Init the module @@ -97,7 +97,7 @@ class Command(): """ try: # Build the default configuration model (Mandatory) - self.ModConfig = self.ModConfModel(param_exemple1='param value 1', param_exemple2=1) + self.ModConfig = self.ModConfModel() # Sync the configuration with core configuration (Mandatory) self.Base.db_sync_core_config(self.module_name, self.ModConfig) @@ -172,6 +172,26 @@ class Command(): match command: + case 'deopall': + try: + self.Irc.send2socket(f":{service_id} SVSMODE {fromchannel} -o") + + except IndexError as e: + self.Logs.warning(f'_hcmd OP: {str(e)}') + + case 'devoiceall': + try: + self.Irc.send2socket(f":{service_id} SVSMODE {fromchannel} -v") + + except IndexError as e: + self.Logs.warning(f'_hcmd OP: {str(e)}') + + case 'voiceall': + chan_info = self.Channel.get_Channel(fromchannel) + for uid in chan_info.uids: + self.Irc.send2socket(f":{service_id} MODE {fromchannel} +v {self.User.get_nickname(self.Base.clean_uid(uid))}") + + case 'op': # /mode #channel +o user # .op #channel user diff --git a/mods/mod_votekick.py b/mods/mod_votekick.py index d6b8bab..79cff94 100644 --- a/mods/mod_votekick.py +++ b/mods/mod_votekick.py @@ -218,15 +218,18 @@ class Votekick(): dnickname = self.Config.SERVICE_NICKNAME + if not self.is_vote_ongoing(channel): + 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.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :The user {self.Config.CONFIG_COLOR["gras"]}{target_user}{self.Config.CONFIG_COLOR["nogc"]} will be kicked from this channel') + self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :User {self.Config.CONFIG_COLOR["gras"]}{target_user}{self.Config.CONFIG_COLOR["nogc"]} has {chan.vote_against} votes against and {chan.vote_for} votes for. For this reason, it\'ll be kicked from the channel') self.Irc.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.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :This user [{target_user}] will stay on this channel') + self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :User {self.Config.CONFIG_COLOR["gras"]}{target_user}{self.Config.CONFIG_COLOR["nogc"]} has {chan.vote_against} votes against and {chan.vote_for} votes for. For this reason, it\'ll remain in the channel') # Init the system if self.init_vote_system(channel): @@ -323,10 +326,10 @@ class Votekick(): if chan.channel_name == channel: target_user = self.User.get_nickname(chan.target_user) if chan.vote_for > chan.vote_against: - self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :The user {self.Config.CONFIG_COLOR["gras"]}{target_user}{self.Config.CONFIG_COLOR["nogc"]} will be kicked from this channel') + self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :User {self.Config.CONFIG_COLOR["gras"]}{target_user}{self.Config.CONFIG_COLOR["nogc"]} has {chan.vote_against} votes against and {chan.vote_for} votes for. For this reason, it\'ll be kicked from the channel') self.Irc.send2socket(f":{dnickname} KICK {channel} {target_user} Following the vote, you are not welcome in {channel}") elif chan.vote_for <= chan.vote_against: - self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :This user will stay on this channel') + self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :User {self.Config.CONFIG_COLOR["gras"]}{target_user}{self.Config.CONFIG_COLOR["nogc"]} has {chan.vote_against} votes against and {chan.vote_for} votes for. For this reason, it\'ll remain in the channel') # Init the system if self.init_vote_system(channel): diff --git a/version.json b/version.json index 52c9599..7bffc70 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "5.1.0" + "version": "5.1.5" } \ No newline at end of file