diff --git a/README.md b/README.md index 72fd0cf..a2a672e 100644 --- a/README.md +++ b/README.md @@ -32,30 +32,34 @@ Il permet aux opérateurs de gérer efficacement un canal, tout en offrant aux u - Système d'exploitation Linux (Windows non supporté) - Un server UnrealIRCD corréctement configuré - Python version 3.10 ou supérieure - - Bash: +```bash + # Bash $ git clone https://github.com/adator85/IRC_DEFENDER_MODULES.git - - Renommer le fichier exemple_configuration.json en configuration.json - - Configurer le fichier configuration.json + # Renommer le fichier exemple_configuration.json en configuration.json + # Configurer le fichier configuration.json $ python3 main.py - +``` Si votre configuration est bonne, votre service est censé etre connecté a votre réseau IRC Pour Les prochains lancement de defender vous devez utiliser la commande suivante: - Bash: - $ systemctl --user [start | stop | restart | status] defender - +```bash + # Bash + $ systemctl --user [start | stop | restart | status] defender +``` # Installation manuelle: - Bash: - $ git clone https://github.com/adator85/IRC_DEFENDER_MODULES.git - $ cd IRC_DEFENDER_MODULES - $ python3 -m venv .pyenv - $ source .pyenv/bin/activate - (pyenv)$ pip install sqlalchemy, psutil, requests, faker - - Créer un service nommé "defender.service" pour votre service et placer le dans "/PATH/TO/USER/.config/systemd/user/" - - Si le dossier n'existe pas il faut les créer - $ sudo systemctl --user start defender +```bash + # Bash + $ git clone https://github.com/adator85/IRC_DEFENDER_MODULES.git + $ cd IRC_DEFENDER_MODULES + $ python3 -m venv .pyenv + $ source .pyenv/bin/activate + (pyenv)$ pip install sqlalchemy, psutil, requests, faker, unrealircd_rpc_py + # Créer un service nommé "defender.service" + # pour votre service et placer le dans "/PATH/TO/USER/.config/systemd/user/" + # Si le dossier n'existe pas il faut les créer + $ sudo systemctl --user start defender +``` # Configuration ``` SERVEUR (Serveur) diff --git a/core/base.py b/core/base.py index 6a2e1cc..ce38702 100644 --- a/core/base.py +++ b/core/base.py @@ -1,8 +1,21 @@ -import time, threading, os, random, socket, hashlib, ipaddress, logging, requests, json, re, ast +import os +import re +import json +import time +import random +import socket +import hashlib +import logging +import threading +import ipaddress + +import ast +import requests + from dataclasses import fields from typing import Union, Literal from base64 import b64decode, b64encode -from datetime import datetime +from datetime import datetime, timedelta, timezone from sqlalchemy import create_engine, Engine, Connection, CursorResult from sqlalchemy.sql import text from core.loadConf import ConfigDataModel @@ -106,7 +119,11 @@ class Base: Cette fonction retourne un UNIXTIME de type 12365456 Return: 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()) unixtime = int( time.time() ) + return unixtime def get_datetime(self) -> str: diff --git a/core/connection.py b/core/connection.py index 4aca76c..a6e2583 100644 --- a/core/connection.py +++ b/core/connection.py @@ -2,10 +2,10 @@ import socket import ssl import traceback from ssl import SSLSocket +from typing import Union from core.loadConf import Config from core.Model import Clones from core.base import Base -from typing import Union class Connection: diff --git a/core/installation.py b/core/installation.py index 4d14d80..ec7400b 100644 --- a/core/installation.py +++ b/core/installation.py @@ -1,8 +1,8 @@ +import os +from sys import exit from dataclasses import dataclass from subprocess import check_call, run, CalledProcessError, PIPE from platform import python_version, python_version_tuple -from sys import exit -import os class Install: diff --git a/core/irc.py b/core/irc.py index 0de7a53..cc6612a 100644 --- a/core/irc.py +++ b/core/irc.py @@ -1,10 +1,17 @@ -import ssl, re, importlib, sys, time, threading, socket, traceback +import sys +import socket +import threading +import ssl +import re +import importlib +import time +import traceback from ssl import SSLSocket from datetime import datetime, timedelta -from typing import Union, Literal +from typing import Union from core.loadConf import Config -from core.Model import User, Admin, Channel, Clones from core.base import Base +from core.Model import User, Admin, Channel, Clones class Irc: @@ -492,6 +499,39 @@ class Irc: self.Base.logs.error(f"Something went wrong with a module you want to load : {e}") self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :[ {self.Config.COLORS.red}ERROR{self.Config.COLORS.black} ]: {e}") + def unload_module(self, mod_name: str) -> bool: + """Unload a module + + Args: + mod_name (str): Module name ex mod_defender + + Returns: + 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 + + if class_name in self.loaded_classes: + self.loaded_classes[class_name].unload() + for level, command in self.loaded_classes[class_name].commands_level.items(): + # Supprimer la commande de la variable commands + for c in self.loaded_classes[class_name].commands_level[level]: + self.commands.remove(c) + self.commands_level[level].remove(c) + + del self.loaded_classes[class_name] + + # Supprimer le module de la base de données + self.Base.db_delete_module(module_name) + + self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :Module {module_name} supprimé") + return True + + except Exception as err: + self.Base.logs.error(f"General Error: {err}") + return False + def insert_db_admin(self, uid:str, level:int) -> None: if self.User.get_User(uid) is None: @@ -997,7 +1037,9 @@ class Irc: recieved_unixtime = int(arg[1].replace('\x01','')) current_unixtime = self.Base.get_unixtime() ping_response = current_unixtime - recieved_unixtime - self.send2socket(f':{dnickname} NOTICE {user_trigger} :\x01PING {str(ping_response)} secs\x01') + + self.send2socket(f'PONG :{recieved_unixtime}') + self.send2socket(f':{dnickname} NOTICE {user_trigger} :\x01PING {recieved_unixtime} secs\x01') return False if not arg[0].lower() in self.commands: @@ -1285,7 +1327,6 @@ class Irc: case 'help': - help = '' count_level_definition = 0 get_admin = self.Admin.get_Admin(uid) if not get_admin is None: @@ -1300,18 +1341,15 @@ class Irc: if int(user_level) >= int(count_level_definition): self.send2socket(f':{dnickname} NOTICE {fromuser} : ***************** {self.Config.COLORS.nogc}[ {self.Config.COLORS.green}LEVEL {str(levDef)} {self.Config.COLORS.nogc}] *****************') - count_commands = 0 - help = '' - for comm in self.commands_level[count_level_definition]: - help += f"{comm.upper()}" - if int(count_commands) < len(self.commands_level[count_level_definition])-1: - help += ' | ' - count_commands += 1 - - self.send2socket(f':{dnickname} NOTICE {fromuser} : {help}') + batch = 7 + for i in range(0, len(self.commands_level[count_level_definition]), batch): + groupe = self.commands_level[count_level_definition][i:i + batch] # Extraire le groupe + batch_commands = ' | '.join(groupe) + self.send2socket(f':{dnickname} NOTICE {fromuser} : {batch_commands}') count_level_definition += 1 + self.send2socket(f':{dnickname} NOTICE {fromuser} : ') self.send2socket(f':{dnickname} NOTICE {fromuser} : ***************** FIN DES COMMANDES *****************') @@ -1320,27 +1358,12 @@ class Irc: self.load_module(fromuser, str(cmd[1])) case 'unload': - # unload mod_dktmb + # unload mod_defender try: module_name = str(cmd[1]).lower() # Le nom du module. exemple: mod_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() - for level, command in self.loaded_classes[class_name].commands_level.items(): - # Supprimer la commande de la variable commands - for c in self.loaded_classes[class_name].commands_level[level]: - self.commands.remove(c) - self.commands_level[level].remove(c) - - del self.loaded_classes[class_name] - - # Supprimer le module de la base de données - self.Base.db_delete_module(module_name) - - self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :Module {module_name} supprimé") - except: - self.Base.logs.error(f"Something went wrong with a module you want to load") + self.unload_module(module_name) + except Exception as err: + self.Base.logs.error(f"General Error: {err}") case 'reload': # reload mod_dktmb diff --git a/core/loadConf.py b/core/loadConf.py index 675f08f..cbf4d49 100644 --- a/core/loadConf.py +++ b/core/loadConf.py @@ -1,4 +1,5 @@ -import json, sys +import sys +import json from os import sep from typing import Union, Literal from dataclasses import dataclass, field @@ -16,7 +17,7 @@ class ColorModel: red: str = "\x0304" yellow: str = "\x0306" bold: str = "\x02" - nogc: str = "\x03" + nogc: str = "\x03" @dataclass class ConfigDataModel: @@ -85,10 +86,19 @@ class ConfigDataModel: """The password of the admin of the service""" JSONRPC_URL: str + """The RPC url, if local https://127.0.0.1:PORT/api should be fine""" + JSONRPC_PATH_TO_SOCKET_FILE: str + """The full path of the socket file (/PATH/TO/YOUR/UNREALIRCD/SOCKET/FILE.socket)""" + JSONRPC_METHOD: str + """3 methods are available; requests/socket/unixsocket""" + JSONRPC_USER: str + """The RPC User defined in your unrealircd.conf""" + JSONRPC_PASSWORD: str + """The RPC Password defined in your unrealircd.conf""" SALON_JAIL: str """The JAIL channel (ex. #jail)""" diff --git a/mods/mod_clone.py b/mods/mod_clone.py index 6c3c8fb..b3d2865 100644 --- a/mods/mod_clone.py +++ b/mods/mod_clone.py @@ -147,7 +147,7 @@ class Clone(): del clone_to_kill - # If LIST empty then stop this thread + # If no more clones then stop this thread if not self.Clone.UID_CLONE_DB: break diff --git a/mods/mod_command.py b/mods/mod_command.py index d86720e..43e5b26 100644 --- a/mods/mod_command.py +++ b/mods/mod_command.py @@ -37,7 +37,8 @@ class Command(): 1: ['join', 'part'], 2: ['owner', 'deowner', 'op', 'deop', 'halfop', 'dehalfop', 'voice', 'devoice', 'opall', 'deopall', 'devoiceall', 'voiceall', 'ban', - 'unban','kick', 'kickban', 'umode', 'svsjoin', 'svspart', 'svsnick'] + 'unban','kick', 'kickban', 'umode', 'svsjoin', 'svspart', 'svsnick', 'topic', + 'wallops', 'globops','gnotice','whois', 'names', 'invite', 'inviteme'] } # Init the module @@ -91,7 +92,7 @@ class Command(): ) ''' - self.Base.db_execute_query(table_logs) + # self.Base.db_execute_query(table_logs) return None def __load_module_configuration(self) -> None: @@ -161,6 +162,27 @@ class Command(): def cmd(self, data:list) -> None: + service_id = self.Config.SERVICE_ID # Defender serveur id + dnickname = self.Config.SERVICE_NICKNAME + dchanlog = self.Config.SERVICE_CHANLOG + red = self.Config.COLORS.red + nogc = self.Config.COLORS.nogc + cmd = list(data).copy() + + if len(cmd) < 2: + return None + + match cmd[1]: + # [':irc.deb.biz.st', '403', 'Dev-PyDefender', '#Z', ':No', 'such', 'channel'] + case '403' | '401': + try: + message = ' '.join(cmd[2:]) + self.Irc.send2socket(f":{dnickname} PRIVMSG {dchanlog} :[{red}ERROR MSG{nogc}] {message}") + except KeyError as ke: + self.Base.logs.error(ke) + except Exception as err: + self.Logs.warning(f'Unknown Error: {str(err)}') + return None def _hcmds(self, user:str, channel: any, cmd: list, fullcmd: list = []) -> None: @@ -577,6 +599,163 @@ class Command(): except Exception as err: self.Logs.warning(f'Unknown Error: {str(err)}') + case 'topic': + try: + if len(cmd) == 1: + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} :/msg {dnickname} TOPIC #channel THE_TOPIC_MESSAGE") + return None + + chan = str(cmd[1]) + if not self.Base.Is_Channel(chan): + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} :The channel must start with #") + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} :/msg {dnickname} TOPIC #channel THE_TOPIC_MESSAGE") + return None + + topic_msg = ' '.join(cmd[2:]).strip() + + if topic_msg: + self.Irc.send2socket(f':{dnickname} TOPIC {chan} :{topic_msg}') + else: + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} :You need to specify the topic") + + except KeyError as ke: + self.Base.logs.error(ke) + except Exception as err: + self.Logs.warning(f'Unknown Error: {str(err)}') + + case 'wallops': + try: + if len(cmd) == 1: + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} :/msg {dnickname} WALLOPS THE_WALLOPS_MESSAGE") + return None + + wallops_msg = ' '.join(cmd[1:]).strip() + + if wallops_msg: + self.Irc.send2socket(f':{dnickname} WALLOPS {wallops_msg} ({dnickname})') + else: + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} :You need to specify the wallops message") + + except KeyError as ke: + self.Base.logs.error(ke) + except Exception as err: + self.Logs.warning(f'Unknown Error: {str(err)}') + + case 'globops': + try: + if len(cmd) == 1: + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} :/msg {dnickname} GLOBOPS THE_GLOBOPS_MESSAGE") + return None + + globops_msg = ' '.join(cmd[1:]).strip() + + if globops_msg: + self.Irc.send2socket(f':{dnickname} GLOBOPS {globops_msg} ({dnickname})') + else: + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} :You need to specify the globops message") + + except KeyError as ke: + self.Base.logs.error(ke) + except Exception as err: + self.Logs.warning(f'Unknown Error: {str(err)}') + + case 'gnotice': + try: + if len(cmd) == 1: + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} :/msg {dnickname} {str(cmd[0]).upper()} THE_GLOBAL_NOTICE_MESSAGE") + return None + + gnotice_msg = ' '.join(cmd[1:]).strip() + + if gnotice_msg: + self.Irc.send2socket(f':{dnickname} NOTICE $*.* :[{self.Config.COLORS.red}GLOBAL NOTICE{self.Config.COLORS.nogc}] {gnotice_msg}') + else: + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} :You need to specify the global notice message") + + except KeyError as ke: + self.Base.logs.error(ke) + except Exception as err: + self.Logs.warning(f'Unknown Error: {str(err)}') + + case 'whois': + try: + if len(cmd) == 1: + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} :/msg {dnickname} {str(cmd[0]).upper()} NICKNAME") + return None + + nickname = str(cmd[1]) + + if self.User.get_nickname(nickname) is None: + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} :Nickname not found !") + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} :/msg {dnickname} {str(cmd[0]).upper()} NICKNAME") + return None + + self.Irc.send2socket(f':{dnickname} WHOIS {nickname}') + + except KeyError as ke: + self.Base.logs.error(ke) + except Exception as err: + self.Logs.warning(f'Unknown Error: {str(err)}') + + case 'names': + try: + if len(cmd) == 1: + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} :/msg {dnickname} {str(cmd[0]).upper()} #CHANNEL") + return None + + chan = str(cmd[1]) + + if not self.Base.Is_Channel(chan): + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} :The channel must start with #") + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} :/msg {dnickname} {str(cmd[0]).upper()} #channel") + return None + + self.Irc.send2socket(f':{dnickname} NAMES {chan}') + + except KeyError as ke: + self.Base.logs.error(ke) + except Exception as err: + self.Logs.warning(f'Unknown Error: {str(err)}') + + case 'invite': + try: + if len(cmd) < 3: + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} :/msg {dnickname} {str(cmd[0]).upper()} #CHANNEL NICKNAME") + return None + + chan = str(cmd[1]) + nickname = str(cmd[2]) + + if not self.Base.Is_Channel(chan): + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} :The channel must start with #") + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} :/msg {dnickname} {str(cmd[0]).upper()} #channel nickname") + return None + + if self.User.get_nickname(nickname) is None: + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} :Nickname not found !") + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} :/msg {dnickname} {str(cmd[0]).upper()} #channel NICKNAME") + return None + + self.Irc.send2socket(f':{dnickname} INVITE {nickname} {chan}') + + except KeyError as ke: + self.Base.logs.error(ke) + except Exception as err: + self.Logs.warning(f'Unknown Error: {str(err)}') + + case 'inviteme': + try: + if len(cmd) == 0: + self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} :/msg {dnickname} {str(cmd[0]).upper()}") + return None + + self.Irc.send2socket(f':{dnickname} INVITE {fromuser} {self.Config.SERVICE_CHANLOG}') + + except KeyError as ke: + self.Base.logs.error(ke) + except Exception as err: + self.Logs.warning(f'Unknown Error: {str(err)}') + case 'umode': try: # .umode nickname +mode diff --git a/mods/mod_defender.py b/mods/mod_defender.py index 5c196bb..15674e0 100644 --- a/mods/mod_defender.py +++ b/mods/mod_defender.py @@ -1,7 +1,12 @@ +import socket +import json +import time +import re +import psutil +import requests from dataclasses import dataclass, fields, field from datetime import datetime from typing import Union -import re, socket, psutil, requests, json, time from sys import exit from core.irc import Irc from core.Model import User diff --git a/mods/mod_jsonrpc.py b/mods/mod_jsonrpc.py index c5e0bec..6754194 100644 --- a/mods/mod_jsonrpc.py +++ b/mods/mod_jsonrpc.py @@ -59,13 +59,20 @@ class Jsonrpc(): self.__load_module_configuration() # End of mandatory methods you can start your customization # - self.UnrealIrcdRpcLive: Live = None + self.UnrealIrcdRpcLive: Live = Live(path_to_socket_file=self.Config.JSONRPC_PATH_TO_SOCKET_FILE, + callback_object_instance=self, + callback_method_name='callback_sent_to_irc' + ) + self.Rpc: Loader = Loader( req_method=self.Config.JSONRPC_METHOD, url=self.Config.JSONRPC_URL, username=self.Config.JSONRPC_USER, password=self.Config.JSONRPC_PASSWORD ) + + self.subscribed = False + if self.Rpc.Error.code != 0: self.Irc.sendPrivMsg(f"[{self.Config.COLORS.red}ERROR{self.Config.COLORS.nogc}] {self.Rpc.Error.message}", self.Config.SERVICE_CHANLOG) @@ -114,12 +121,9 @@ class Jsonrpc(): def thread_start_jsonrpc(self): - self.UnrealIrcdRpcLive = Live(path_to_socket_file=self.Config.JSONRPC_PATH_TO_SOCKET_FILE, - callback_object_instance=self, - callback_method_name='callback_sent_to_irc' - ) if self.UnrealIrcdRpcLive.Error.code == 0: self.UnrealIrcdRpcLive.subscribe() + self.subscribed = True else: self.Irc.sendPrivMsg(f"[{self.Config.COLORS.red}ERROR{self.Config.COLORS.nogc}] {self.UnrealIrcdRpcLive.Error.message}", self.Config.SERVICE_CHANLOG) @@ -148,7 +152,8 @@ class Jsonrpc(): self.Base.db_update_core_config(self.module_name, self.ModConfig, param_key, param_value) def unload(self) -> None: - self.UnrealIrcdRpcLive.unsubscribe() + if self.UnrealIrcdRpcLive.Error.code != -1: + self.UnrealIrcdRpcLive.unsubscribe() return None def cmd(self, data:list) -> None: @@ -198,23 +203,22 @@ class Jsonrpc(): if uid_to_get is None: return None - rpc = Loader( - req_method=self.Config.JSONRPC_METHOD, - url=self.Config.JSONRPC_URL, - username=self.Config.JSONRPC_USER, - password=self.Config.JSONRPC_PASSWORD - ) + rpc = self.Rpc UserInfo = rpc.User.get(uid_to_get) if rpc.Error.code != 0: self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} :{rpc.Error.message}') return None + chan_list = [] + for chan in UserInfo.channels: + chan_list.append(chan["name"]) + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} :UID : {UserInfo.id}') self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} :NICKNAME : {UserInfo.name}') self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} :USERNAME : {UserInfo.username}') self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} :REALNAME : {UserInfo.realname}') - self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} :CHANNELS : {UserInfo.channels}') + self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} :CHANNELS : {chan_list}') self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} :SECURITY GROUP : {UserInfo.security_groups}') self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} :REPUTATION : {UserInfo.reputation}') diff --git a/version.json b/version.json index 85ed6d2..3872b1a 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "5.3.1" + "version": "5.3.2" } \ No newline at end of file