122 Commits

Author SHA1 Message Date
adator
c371910066 Creating JSONRPC Server 2025-11-16 19:13:26 +01:00
adator
2e422c93e5 Base.py refactoring. Nothing fancy. 2025-11-16 13:35:09 +01:00
adator
ba989b7f26 Update debug message. 2025-11-16 13:22:33 +01:00
adator
fc01de34b2 Adding new debug messages when module exist or not 2025-11-16 13:21:57 +01:00
adator
a3dcc20a06 Handle SETHOST response to update the vhost of the user 2025-11-16 13:20:11 +01:00
adator
7ffc58d4ff Trigger a thread clean-up before running a new thread 2025-11-16 13:15:49 +01:00
adator
6a0d4e2286 Updating some translation, refactoring the code! 2025-11-11 03:49:16 +01:00
adator
7dd15f2dac Updating translation! 2025-11-11 03:48:37 +01:00
adator
10cad7cda6 Refactoring code! 2025-11-11 03:48:20 +01:00
adator
999072a88a Refactoring unreal6 code! 2025-11-11 03:47:02 +01:00
adator
8932e1441a Update docstring of the test module. 2025-11-10 23:38:19 +01:00
adator
9cee758b6f Remove unused imported library! 2025-11-10 23:13:17 +01:00
adator
511e0c0715 Introduce MOD_HEADER in all modules. impact modules.py, definition.py, unreal6 protocol. 2025-11-10 23:09:13 +01:00
adator
371c8fb5f1 Exclude modules.txt files from the commit 2025-11-10 23:08:18 +01:00
adator
401e785383 Remove deprecated class (abstractproperty). 2025-11-10 00:15:53 +01:00
adator
a7efede75e Introduce MOD_HEADER constante in all modules as mandatory constante. 2025-11-10 00:13:35 +01:00
adator
a1254c7a39 Refactoring the Code, Dispatch server responses to all modules including UID, avoid multi calls to get_user, get_nickname... 2025-11-09 23:39:19 +01:00
adator
c05990f862 Refactoring the Code! 2025-11-09 23:37:42 +01:00
adator
de2b5fa8e2 Refactoring the Code, comment the code to dispatch the server response to all modules 2025-11-09 23:36:58 +01:00
adator
371645149d Refactoring the Code, clean unsused methods. avoid multi calls to get_user, get_nickname ... 2025-11-09 23:35:27 +01:00
adator
a6cf11ae2a Update the log level when userobj is not found! 2025-11-09 23:33:17 +01:00
adator
445cbc27b0 exclude users.txt file 2025-11-09 23:32:21 +01:00
adator
f9eb374798 When fp is None, return False. log when login via fingerprint. 2025-11-09 20:53:30 +01:00
adator
17cb2ada5f Fix server response, fix ircd parser aswell, update first setup on base.py 2025-11-08 21:21:38 +01:00
adator
b52a57f95a Fix library path in settings.py 2025-11-07 22:46:09 +01:00
adator
1bfd95c291 refactoring code 2025-11-06 20:12:28 +01:00
adator
0c6c3cd6ac Refactoring the code, create new folder modules. 2025-11-02 22:52:27 +01:00
adator
0e6384c26c modify and move protocol interface to interfaces folder. refactoring all dependencies 2025-11-02 22:21:55 +01:00
adator
79c1b94a92 Update parse_quit, now it returns MUser object and the reason. 2025-11-02 21:28:44 +01:00
adator
5a1432c1e6 Update parse_uid, now it returns MUser object. 2025-11-02 21:17:15 +01:00
adator
34b5b4204e Update parse_privmsg, now it returns sender, reciever, channel objects and the message 2025-11-02 20:58:56 +01:00
adator
ff58cbb022 Adding serveur_protocol to the configuration exemple. 2025-11-02 00:16:42 +01:00
adator
6450418859 Update docstring 2025-11-02 00:16:04 +01:00
adator
9f2da13f88 Unload the module when the protocol is not unreal6 2025-11-02 00:15:43 +01:00
adator
0117e1dd3a Update the protocol inspircd to work with the protocol interfaces. 2025-11-02 00:14:36 +01:00
adator
deb76baf30 Update the version of defender 6.3.2>>6.3.3 2025-11-01 22:24:57 +01:00
adator
29f049b3c3 Update the protocol interface, no more __init__ constructor needed in the child class! 2025-11-01 22:20:49 +01:00
adator
fb41a13d0a Move mod_clone to use the module interface. 2025-11-01 22:11:15 +01:00
adator
769ab8b632 update rest of modules to fit requirements 2025-11-01 22:00:08 +01:00
adator
2fbe75b83e update module management 2025-11-01 17:39:16 +01:00
adator
8abae5df3e fix db_patch, the table name is no more hard coded! 2025-11-01 15:57:46 +01:00
adator
1a71a6eb4d 1st version of module interface! 2025-10-30 00:35:50 +01:00
adator
b182aa8bcb Fix mod_jsonrpc module! Selecting the correct parameter based on the JSONRPC_METHOD value in the configruation.yaml 2025-10-29 00:02:57 +01:00
adator
e5a5f01603 Adding 'make update' to update from git repository. remove previous installation.py file, the update of packages is done via install.py file. 2025-10-28 23:33:43 +01:00
adator
99f8949681 Create Makefile installation; update copyright core command. TODO replace installation.py script. 2025-10-28 01:02:27 +01:00
adator
05b15f2f18 Merge pull request #95 from adator85/fix-install
Fix the previous configuration.json check in the installation file
2025-10-26 21:16:38 +01:00
adator
35c3faf68c Fix the previous configuration.json check in the installation file 2025-10-26 21:15:05 +01:00
adator
2e9bfd2c3b Merge pull request #94 from adator85/v6.3.2
V6.3.2
2025-10-26 21:10:39 +01:00
adator
80131b7b7a Update version.json 2025-10-26 21:10:19 +01:00
adator
ffb30f12ec Remove json configuration and replace it by yaml configuration files. 2025-10-26 21:00:50 +01:00
adator
b7b61081be Fix get_datetime call and update some docstring. 2025-10-25 00:10:32 +02:00
adator
030b706b65 Update the private message when fingerprint auth is used 2025-10-19 20:42:35 +02:00
adator
c428ea2b41 If no fingerprint available, cert command will trigger an error. 2025-10-19 12:33:30 +02:00
adator
9036e4f626 Merge pull request #93 from adator85/v6.3.0
update mod_jsonrpc and configuration file
2025-10-18 22:48:20 +02:00
adator
fd79ada13d update mod_jsonrpc and configuration file 2025-10-18 22:46:38 +02:00
adator
8323f6cc9b Merge pull request #92 from adator85/v6.3.0
V6.3.0
2025-10-18 20:54:35 +02:00
adator
6fcd553481 Merge branch 'main' into v6.3.0 2025-10-18 20:54:26 +02:00
adator
5cd82a174d remove .vscode/ folder 2025-10-18 20:53:35 +02:00
adator
beec16f39d Update to the 3.0.0 V 2025-10-18 20:49:39 +02:00
adator
a043a58f45 Update to the 3.0.0 V 2025-10-18 20:49:21 +02:00
adator
fd9643eddc Add command handler system. Starting adapt the modules to fit other protocls. 2025-09-09 22:37:41 +02:00
adator
ed1a048603 Merge pull request #91 from adator85/v6.2.5-fix
Fix reputation issue by adding tls_cipher in the datamodel
2025-09-03 22:10:21 +02:00
adator
3dfde9b1aa Fix reputation issue by adding tls_cipher in the datamodel 2025-09-03 22:09:29 +02:00
adator
5e35a10193 Merge pull request #90 from adator85/v6.2.5-fix
Fix reputation issue by adding tls_cipher in the datamodel
2025-09-03 22:06:15 +02:00
adator
ff776541d7 Fix reputation issue by adding tls_cipher in the datamodel 2025-09-03 22:05:34 +02:00
adator
6b7fd16a44 Connectecting to inspircd 2025-09-03 22:01:52 +02:00
adator
e79c15188e Quick updates:
- Set default language for admins when running the db patch
    - Updating addaccess command.
    - Update levels for some commands in mod_command.
2025-08-30 23:09:03 +02:00
adator
b306968115 Merge pull request #89 from adator85/v6.2.5
V6.2.5
2025-08-29 21:45:20 +02:00
adator
184e90adce New updates changelog:
- Update info command (mod_defender.py)
    - Update help on commands (mod_clone.py)
2025-08-29 21:43:22 +02:00
adator
c7b88150b5 New updates for v6.2.5:
- Adding tls_cipher to MUser, MAdmin and MClient
    - Switch parser command in Irc Instance (To Monitor closly)
    - New auth method in Admin.py
    - Adding the capability to auto auth Admins via their fingerprints
    - Update few core translation.
2025-08-27 00:52:48 +02:00
adator
02f0608b75 Adding current_admin to the global settings:
- New property current_admin added to the Settings.py
    - Fix also translation function
2025-08-26 01:41:15 +02:00
adator
25bbddf459 Adding language field to admin db and local variable 2025-08-25 23:22:50 +02:00
adator
0c6fcb7710 New features on branch v6.2.5:
- New capability in base.py to patch the database
    - Some minor updates on installation.py.
    - Translation feature:
        - New library requirement (pyyaml)
        - New translation systeme implemented.
        - New class translation.py added.
        - Module folder updated by adding new folder language.
        - Core module updated as well with new language folder.
2025-08-25 22:31:07 +02:00
adator
0a2e3f724b Merge pull request #88 from adator85/v6.2.2
Deleting the admin when quit IRC
2025-08-24 03:44:26 +02:00
adator
1224604460 Deleting the admin when quit IRC 2025-08-24 03:43:39 +02:00
adator
43072acecc Merge pull request #87 from adator85/v6.2.1
Adding sasl authentication
2025-08-24 03:11:33 +02:00
adator
bcf6b6b675 Adding sasl authentication 2025-08-24 03:10:45 +02:00
adator
b50205c766 Merge pull request #86 from adator85/v6.2.0
V6.2.1 Adding main module utils and rehash utils to manager reload/re…
2025-08-23 19:29:06 +02:00
adator
bd95b6b448 V6.2.1 Adding main module utils and rehash utils to manager reload/rehash/restart 2025-08-23 19:26:22 +02:00
adator
ebbad1d9e4 Merge pull request #85 from adator85/v6.2.0
Fix removing a user when he leave a channel
2025-08-21 01:39:11 +02:00
adator
ae1f0ed424 Fix removing a user when he leave a channel 2025-08-21 01:38:23 +02:00
adator
80854aea98 Merge pull request #84 from adator85/v6.2.0
Update jsonrpc lib version
2025-08-21 01:07:15 +02:00
adator
483638dab4 Update jsonrpc lib version 2025-08-21 01:06:49 +02:00
adator
4806de4cca Merge pull request #83 from adator85/v6.2.0
Remove requirements.txt
2025-08-21 01:02:55 +02:00
adator
f4b76eaf09 Remove requirements.txt 2025-08-21 01:02:28 +02:00
adator
37617fc286 Merge pull request #82 from adator85/v6.2.0
V6.2.0
2025-08-21 01:00:12 +02:00
adator
06fc6c4d82 First version to merge 2025-08-21 00:59:13 +02:00
adator
0a4e185fe8 Update module load and reload./update also mod_jsonrpc 2025-08-20 17:17:25 +02:00
adator
4c327940dd Adding new classes, todo: fix jsonrpc module 2025-08-19 03:06:56 +02:00
adator
a15a5b1026 Updating command module 2025-08-17 22:48:35 +02:00
adator
25262d4049 Moving some methods to utils.py, creating new logs class 2025-08-17 22:41:51 +02:00
adator
8d23827e1e Updating new classes into votekick module 2025-08-16 21:27:00 +02:00
adator85
f5212deacf Update Votekick first part comment 2025-08-16 02:27:03 +02:00
adator85
3fc49e9069 Update Votekick module! following the same structure as other modules 2025-08-16 02:11:21 +02:00
adator85
e075b7b8d5 Updating gitignore file 2025-08-15 16:22:33 +02:00
adator85
30a89e7d14 Updating gitignore file 2025-08-15 16:19:56 +02:00
adator85
aee1dca124 Importing the MAdmin directly from definition (admin.py) 2025-08-15 16:17:32 +02:00
adator85
9e255e806d Adding some comments, editing methods names (admin.py) 2025-08-15 16:14:35 +02:00
adator85
6b22d786e3 Adding some comments, editing methods names 2025-08-15 15:47:01 +02:00
adator
21a2619f49 Updating mod_clone by adding action on groups. reloading the module is now using Settings.set_cache and get_cache 2025-08-10 02:31:50 +02:00
adator
1686c4a0b5 updating namings convension to stay coherent 2025-08-09 12:17:45 +02:00
adator
5629dcfde6 updating namings in user.py and channel.py to stay coherent 2025-08-09 03:53:57 +02:00
adator
f3fe3c43cb updating namings in admin.py to stay coherent 2025-08-09 03:48:56 +02:00
adator
c0cd4db3af updating namings to stay coherent 2025-08-09 03:43:44 +02:00
adator
9ea5ae50d5 Updating cmd by handling all functions, Threads and timers and schemas in separate files. code should be clear 2025-08-09 03:35:30 +02:00
adator
0a655b2df0 Moving modules in separate folders 2025-08-08 17:38:45 +02:00
adator
7a50bc9632 Merge pull request #81 from adator85/update-mod-defender
Nickname not connected to the network
2025-08-07 23:38:52 +02:00
adator
275d37ed2d Nickname not connected to the network 2025-08-07 23:38:32 +02:00
adator
9ae6e5bf4c Merge pull request #80 from adator85/update-mod-defender
Updating this output message again
2025-08-07 23:36:19 +02:00
adator
2feed6878f Updating this output message again 2025-08-07 23:35:52 +02:00
adator
0b173e8d18 Merge pull request #79 from adator85/update-mod-defender
Updating response message.
2025-08-07 23:34:49 +02:00
adator
d2b46f2618 Updating response message. 2025-08-07 23:34:16 +02:00
adator
7a9bcff988 Merge pull request #78 from adator85/update-mod-defender
Updating reputation help command
2025-08-07 23:24:09 +02:00
adator
cc80e35482 Updating reputation help command 2025-08-07 23:23:25 +02:00
adator
d1568106f9 Merge pull request #77 from adator85/update-mod-defender
New command in mod_defender (reputation release)
2025-08-07 23:18:23 +02:00
adator
9df18de0b1 mod_defender:
- New command added in reputation (release)
    to release a known user.
base.py:
    - Switch deprecated getName() method
    to name attribute (In threading methods)
2025-08-07 23:15:18 +02:00
adator
64f1490942 Merge pull request #76 from adator85/upgrade-mod-jsonrpc
Moving rpc module to rpc package v2
2025-08-07 16:27:40 +02:00
adator
9ddb2a28b2 Moving rpc module to rpc package v2 2025-08-07 16:26:59 +02:00
adator
eacfe0d087 Merge pull request #75 from adator85/update/admin.py
Update level of the debug for admin.py
2025-08-07 02:00:38 +02:00
adator
680a446c2c Update level of the debug for admin.py 2025-08-07 02:00:04 +02:00
adator
a10aa9b94e Merge pull request #74 from adator85/jsonrpc/update
Update jsonrpc module with new version of unrealircd-rpc-py
2025-08-07 01:39:55 +02:00
adator
56265985f7 Update jsonrpc module with new version of unrealircd-rpc-py 2025-08-07 01:39:21 +02:00
71 changed files with 9985 additions and 6539 deletions

8
.gitignore vendored
View File

@@ -1,7 +1,15 @@
.pyenv/ .pyenv/
.vscode/
.venv/
.idea/
db/ db/
logs/ logs/
__pycache__/ __pycache__/
configuration.json configuration.json
configuration.yaml
configuration_inspircd.json
configuration_unreal6.json
*.log *.log
test.py test.py
users.txt
modules.txt

65
Makefile Normal file
View File

@@ -0,0 +1,65 @@
OS := $(shell uname -s)
CURRENT_USER := $(shell whoami)
PYTHON_VERSION := $(shell python3 -V)
HOME_DIR := $(shell echo $$HOME)
SHELL := /bin/bash
install:
ifeq ($(wildcard config/configuration.yaml),)
$(error You must provide the Configuration file: config/configuration.yaml)
endif
ifeq ($(OS), Linux)
$(info Installation for os : $(OS))
$(info Python version: $(PYTHON_VERSION))
$(info Home directory: $(HOME_DIR))
@python3 core/install.py --check-version
@if [ $$? -eq 0 ]; then \
echo "Python Version OK! Well done :)"; \
else \
echo "Error: Script failed with exit code $$?"; \
exit 1; \
fi
$(info Creating the systemd user folder...)
mkdir -p $(HOME_DIR)/.config/systemd/user
$(info Creating Python Virtual Environment...)
python3 -m venv .pyenv
@. .pyenv/bin/activate && \
python -m pip install --upgrade pip && \
pip cache purge && \
pip install -r requirements.txt
@. .pyenv/bin/activate && python core/install.py --install
loginctl enable-linger $(CURRENT_USER)
@sleep 2
@export echo $DBUS_SESSION_BUS_ADDRESS && \
systemctl --user daemon-reload && \
systemctl --user start defender
endif
clean:
ifeq ($(OS), Linux)
@export echo $DBUS_SESSION_BUS_ADDRESS && \
systemctl --user stop defender
$(info Defender has been stopped...)
@if [ -e .pyenv ]; then \
rm -rf .pyenv; \
echo "Virtual Env has been removed!"; \
fi
@if [ -e $(HOME_DIR)/.config/systemd/user/defender.service ]; then \
rm $(HOME_DIR)/.config/systemd/user/defender.service; \
echo "Systemd file has been removed!"; \
fi
@export echo $DBUS_SESSION_BUS_ADDRESS && systemctl --user daemon-reload && echo "Systemd Daemon reloaded!"
endif
update:
ifeq ($(OS), Linux)
$(info Starting update from the main repository...)
@. .pyenv/bin/activate && python core/install.py --git-update
$(info Update done!)
endif

139
README.md
View File

@@ -34,10 +34,11 @@ Il permet aux opérateurs de gérer efficacement un canal, tout en offrant aux u
- Python version 3.10 ou supérieure - Python version 3.10 ou supérieure
```bash ```bash
# Bash # Bash
$ git clone https://github.com/adator85/IRC_DEFENDER_MODULES.git $ git clone https://github.com/adator85/DEFENDER.git defender
$ cd defender/
# Renommer le fichier exemple_configuration.json en configuration.json # Renommer le fichier exemple_configuration.json en configuration.json
# Configurer le fichier configuration.json # Configurer le fichier configuration.json
$ python3 main.py $ make install
``` ```
Si votre configuration est bonne, votre service est censé etre connecté a votre réseau IRC 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: Pour Les prochains lancement de defender vous devez utiliser la commande suivante:
@@ -49,11 +50,11 @@ Pour Les prochains lancement de defender vous devez utiliser la commande suivant
# Installation manuelle: # Installation manuelle:
```bash ```bash
# Bash # Bash
$ git clone https://github.com/adator85/IRC_DEFENDER_MODULES.git $ git clone https://github.com/adator85/DEFENDER.git defender
$ cd IRC_DEFENDER_MODULES $ cd defender/
$ python3 -m venv .pyenv $ python3 -m venv .pyenv
$ source .pyenv/bin/activate $ source .pyenv/bin/activate
(pyenv)$ pip install sqlalchemy, psutil, requests, faker, unrealircd_rpc_py (pyenv)$ pip install -r requirements.txt
# Créer un service nommé "defender.service" # Créer un service nommé "defender.service"
# pour votre service et placer le dans "/PATH/TO/USER/.config/systemd/user/" # pour votre service et placer le dans "/PATH/TO/USER/.config/systemd/user/"
@@ -104,87 +105,91 @@ Pour Les prochains lancement de defender vous devez utiliser la commande suivant
GLINE_DURATION: Durée de bannissement temporaire d'un utilisateur en minutes. (default : "30") GLINE_DURATION: Durée de bannissement temporaire d'un utilisateur en minutes. (default : "30")
DEBUG (Debug) DEBUG (Debug)
DEBUG_LEVEL: Niveau de verbosité des messages de debug (plus grand est le nombre, plus il y a d'informations). (default : 20) Pour une production DEBUG_LEVEL: Niveau de verbosité des messages de debug (plus petit est le nombre, plus il y a d'informations). (default : 20) Pour une production
DEBUG_HARD: Généralement utiliser pour les developpeurs.
``` ```
Modification de la configuration Modification de la configuration
Vous devez modifier le fichier configuration.json en remplaçant les valeurs par défaut avec vos propres informations. Assurez-vous de bien lire la description de chaque paramètre pour une configuration optimale du service. Vous devez modifier le fichier configuration.yaml en remplaçant les valeurs par défaut avec vos propres informations. Assurez-vous de bien lire la description de chaque paramètre pour une configuration optimale du service.
## Exemple de configuration de base ## Exemple de configuration de base
```json ```yaml
{ configuration:
"SERVEUR_IP": "IP.DE.TON.SERVER", SERVEUR_IP: "YOUR.SERVER.IP"
"SERVEUR_HOSTNAME": "HOST.DE.TON.SERVER", SERVEUR_HOSTNAME: "YOUR.SERVER.HOST"
"SERVEUR_LINK": "LINK.DE.TON.SERVER", SERVEUR_LINK: "LINK.DE.TON.SERVER"
"SERVEUR_PORT": 6901, SERVEUR_PORT: 7002
"SERVEUR_PASSWORD": "MOT_DE_PASS_DE_TON_LINK", SERVEUR_PASSWORD: "YOUR_LINK_PASSWORD"
"SERVEUR_ID": "10Z", SERVEUR_ID: "006"
"SERVEUR_SSL": true, SERVEUR_SSL: true
"SERVICE_NAME": "defender", SERVICE_NAME: "defender"
"SERVICE_NICKNAME": "PyDefender", SERVICE_NICKNAME: "PyDefender"
"SERVICE_REALNAME": "Python Defender Security", SERVICE_REALNAME: "Python Defender Security"
"SERVICE_USERNAME": "PyDefender", SERVICE_USERNAME: "PyDefender"
"SERVICE_HOST": "HOST.DE.TON.DEFENDER", SERVICE_HOST: "HOST.DE.TON.DEFENDER"
SERVICE_INFO: "Network IRC Service"
SERVICE_CHANLOG: "#services"
SERVICE_SMODES: "+ioqBS"
SERVICE_CMODES: "ntsOP"
SERVICE_UMODES: "o"
SERVICE_PREFIX: "!"
"OWNER": "TON_NICK_NAME", OWNER: "TON_NICK_NAME"
"PASSWORD": "TON_PASSWORD" PASSWORD: "TON_PASSWORD"
}
``` ```
## Exemple complet de configuration ## Exemple complet de configuration
```json ```yaml
{ configuration:
"SERVEUR_IP": "YOUR.SERVER.IP", SERVEUR_IP: "YOUR.SERVER.IP"
"SERVEUR_HOSTNAME": "YOUR.SERVER.HOST", SERVEUR_HOSTNAME: "YOUR.SERVER.HOST"
"SERVEUR_LINK": "LINK.DE.TON.SERVER", SERVEUR_LINK: "LINK.DE.TON.SERVER"
"SERVEUR_PORT": 6901, SERVEUR_PORT: 7002
"SERVEUR_PASSWORD": "YOUR_LINK_PASSWORD", SERVEUR_PASSWORD: "YOUR_LINK_PASSWORD"
"SERVEUR_ID": "10Z", SERVEUR_ID: "006"
"SERVEUR_SSL": true, SERVEUR_SSL: true
"SERVICE_NAME": "defender", SERVICE_NAME: "defender"
"SERVICE_NICKNAME": "PyDefender", SERVICE_NICKNAME: "PyDefender"
"SERVICE_REALNAME": "Python Defender Security", SERVICE_REALNAME: "Python Defender Security"
"SERVICE_USERNAME": "PyDefender", SERVICE_USERNAME: "PyDefender"
"SERVICE_HOST": "HOST.DE.TON.DEFENDER", SERVICE_HOST: "HOST.DE.TON.DEFENDER"
"SERVICE_INFO": "Network IRC Service", SERVICE_INFO: "Network IRC Service"
"SERVICE_CHANLOG": "#services", SERVICE_CHANLOG: "#services"
"SERVICE_SMODES": "+ioqBS", SERVICE_SMODES: "+ioqBS"
"SERVICE_CMODES": "ntsOP", SERVICE_CMODES: "ntsOP"
"SERVICE_UMODES": "o", SERVICE_UMODES: "o"
"SERVICE_PREFIX": "!", SERVICE_PREFIX: "!"
"OWNER": "TON_NICK_NAME", OWNER: "TON_NICK_NAME"
"PASSWORD": "TON_PASSWORD", PASSWORD: "TON_PASSWORD"
"JSONRPC_URL": "https://your.domaine.com:8600/api", JSONRPC_URL: "https://your.domaine.com:8600/api"
"JSONRPC_PATH_TO_SOCKET_FILE": "/PATH/TO/YOUR/IRCD/data/rpc.socket", JSONRPC_PATH_TO_SOCKET_FILE: "/PATH/TO/YOUR/IRCD/data/rpc.socket"
"JSONRPC_METHOD": "socket", JSONRPC_METHOD: "unixsocket"
"JSONRPC_USER": "YOUR_RPC_USER", JSONRPC_USER: "YOUR_RPC_USER"
"JSONRPC_PASSWORD": "YOUR_RPC_PASSWORD", JSONRPC_PASSWORD: "YOUR_RPC_PASSWORD"
"SALON_JAIL": "#jail", SALON_JAIL: "#jail"
"SALON_JAIL_MODES": "sS", SALON_JAIL_MODES: "sS"
"SALON_LIBERER": "#welcome", SALON_LIBERER: "#welcome"
"CLONE_CHANNEL": "#clones", CLONE_CHANNEL: "#clones"
"CLONE_CMODES": "+nts", CLONE_CMODES: "+nts"
"CLONE_LOG_HOST_EXEMPT": ["HOST.TO.SKIP"], CLONE_LOG_HOST_EXEMPT: ["HOST.TO.SKIP"]
"CLONE_CHANNEL_PASSWORD": "YOUR_CHANNEL_PASSWORD", CLONE_CHANNEL_PASSWORD: "YOUR_CHANNEL_PASSWORD"
"API_TIMEOUT": 2, API_TIMEOUT: 2
"PORTS_TO_SCAN": [3028, 8080, 1080, 1085, 4145, 9050], PORTS_TO_SCAN: [3028 8080 1080 1085 4145 9050]
"WHITELISTED_IP": ["127.0.0.1"], WHITELISTED_IP: ["127.0.0.1"]
"GLINE_DURATION": "30", GLINE_DURATION: "30"
"DEBUG_LEVEL": 20 DEBUG_LEVEL: 20
DEBUG_HARD: true
}
``` ```
# \\!/ Attention \\!/ # \\!/ Attention \\!/
@@ -192,7 +197,7 @@ Le mot de passe de l'administrateur et le mot de passe du service doivent être
Ne partagez pas vos informations de connexion au serveur IRC avec des tiers. Ne partagez pas vos informations de connexion au serveur IRC avec des tiers.
a votre premiere connexion vous devez tapez a votre premiere connexion vous devez tapez
``` ```
/msg [NomDuService] auth [nickname] [password] /msg [NomDuService] firstauth [nickname] [password]
-- Une fois identifié tapez la commande suivante -- Une fois identifié tapez la commande suivante
/msg [NomDuService] editaccess [nickname] [Nouveau-Password] 5 /msg [NomDuService] editaccess [nickname] [Nouveau-Password] 5
``` ```

View File

@@ -1,48 +0,0 @@
{
"SERVEUR_IP": "YOUR.SERVER.IP",
"SERVEUR_HOSTNAME": "YOUR.SERVER.HOST",
"SERVEUR_LINK": "LINK.DE.TON.SERVER",
"SERVEUR_PORT": 7002,
"SERVEUR_PASSWORD": "YOUR_LINK_PASSWORD",
"SERVEUR_ID": "006",
"SERVEUR_SSL": true,
"SERVICE_NAME": "defender",
"SERVICE_NICKNAME": "PyDefender",
"SERVICE_REALNAME": "Python Defender Security",
"SERVICE_USERNAME": "PyDefender",
"SERVICE_HOST": "HOST.DE.TON.DEFENDER",
"SERVICE_INFO": "Network IRC Service",
"SERVICE_CHANLOG": "#services",
"SERVICE_SMODES": "+ioqBS",
"SERVICE_CMODES": "ntsOP",
"SERVICE_UMODES": "o",
"SERVICE_PREFIX": "!",
"OWNER": "TON_NICK_NAME",
"PASSWORD": "TON_PASSWORD",
"JSONRPC_URL": "https://your.domaine.com:8600/api",
"JSONRPC_PATH_TO_SOCKET_FILE": "/PATH/TO/YOUR/IRCD/data/rpc.socket",
"JSONRPC_METHOD": "socket",
"JSONRPC_USER": "YOUR_RPC_USER",
"JSONRPC_PASSWORD": "YOUR_RPC_PASSWORD",
"SALON_JAIL": "#jail",
"SALON_JAIL_MODES": "sS",
"SALON_LIBERER": "#welcome",
"CLONE_CHANNEL": "#clones",
"CLONE_CMODES": "+nts",
"CLONE_LOG_HOST_EXEMPT": ["HOST.TO.SKIP"],
"CLONE_CHANNEL_PASSWORD": "YOUR_CHANNEL_PASSWORD",
"API_TIMEOUT": 2,
"PORTS_TO_SCAN": [3028, 8080, 1080, 1085, 4145, 9050],
"WHITELISTED_IP": ["127.0.0.1"],
"GLINE_DURATION": "30",
"DEBUG_LEVEL": 20
}

View File

@@ -0,0 +1,48 @@
configuration:
SERVEUR_IP: "YOUR.SERVER.IP"
SERVEUR_HOSTNAME: "YOUR.SERVER.HOST"
SERVEUR_LINK: "LINK.DE.TON.SERVER"
SERVEUR_PORT: 7002
SERVEUR_PASSWORD: "YOUR_LINK_PASSWORD"
SERVEUR_ID: "006"
SERVEUR_SSL: true
SERVEUR_PROTOCOL: "unreal6" # unreal6 or inspircd
SERVICE_NAME: "defender"
SERVICE_NICKNAME: "PyDefender"
SERVICE_REALNAME: "Python Defender Security"
SERVICE_USERNAME: "PyDefender"
SERVICE_HOST: "HOST.DE.TON.DEFENDER"
SERVICE_INFO: "Network IRC Service"
SERVICE_CHANLOG: "#services"
SERVICE_SMODES: "+ioqBS"
SERVICE_CMODES: "ntsOP"
SERVICE_UMODES: "o"
SERVICE_PREFIX: "!"
OWNER: "TON_NICK_NAME"
PASSWORD: "TON_PASSWORD"
JSONRPC_URL: "https://your.domaine.com:8600/api"
JSONRPC_PATH_TO_SOCKET_FILE: "/PATH/TO/YOUR/IRCD/data/rpc.socket"
JSONRPC_METHOD: "unixsocket"
JSONRPC_USER: "YOUR_RPC_USER"
JSONRPC_PASSWORD: "YOUR_RPC_PASSWORD"
SALON_JAIL: "#jail"
SALON_JAIL_MODES: "sS"
SALON_LIBERER: "#welcome"
CLONE_CHANNEL: "#clones"
CLONE_CMODES: "+nts"
CLONE_LOG_HOST_EXEMPT: ["HOST.TO.SKIP"]
CLONE_CHANNEL_PASSWORD: "YOUR_CHANNEL_PASSWORD"
API_TIMEOUT: 2
PORTS_TO_SCAN: [3028, 8080, 1080, 1085, 4145, 9050]
WHITELISTED_IP: ["127.0.0.1", "192.168.1.1"]
GLINE_DURATION: "30"
DEBUG_LEVEL: 20
DEBUG_HARD: true

View File

@@ -2,35 +2,32 @@ import os
import re import re
import json import json
import time import time
import random
import socket import socket
import hashlib
import logging
import threading import threading
import ipaddress import ipaddress
import ast import ast
import requests import requests
from dataclasses import fields from dataclasses import fields
from typing import Union, Literal, TYPE_CHECKING from typing import Any, Optional, TYPE_CHECKING
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
from datetime import datetime, timedelta, timezone
from sqlalchemy import create_engine, Engine, Connection, CursorResult from sqlalchemy import create_engine, Engine, Connection, CursorResult
from sqlalchemy.sql import text from sqlalchemy.sql import text
from core.definition import MConfig
if TYPE_CHECKING: if TYPE_CHECKING:
from core.classes.settings import Settings from core.loader import Loader
class Base: 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.Loader = loader
self.Settings: Settings = settings self.Config = loader.Config
self.init_log_system() # Demarrer le systeme de log self.Settings = loader.Settings
self.check_for_new_version(True) # Verifier si une nouvelle version est disponible self.Utils = loader.Utils
self.logs = loader.Logs
# Check if new Defender version is available
self.check_for_new_version(True)
# Liste des timers en cours # 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
@@ -47,9 +44,17 @@ class Base:
# Création du lock # Création du lock
self.lock = self.Settings.LOCK self.lock = self.Settings.LOCK
self.install: bool = False # Initialisation de la variable d'installation # Init install variable
self.engine, self.cursor = self.db_init() # Initialisation de la connexion a la base de données self.install: bool = False
self.__create_db() # Initialisation de la base de données
# Init database connection
self.engine, self.cursor = self.db_init()
# Create the database
# self.__create_db()
def init(self) -> None:
self.__create_db()
def __set_current_defender_version(self) -> None: def __set_current_defender_version(self) -> None:
"""This will put the current version of Defender """This will put the current version of Defender
@@ -136,120 +141,26 @@ class Base:
except Exception as err: except Exception as err:
self.logs.error(f'General Error: {err}') self.logs.error(f'General Error: {err}')
def get_unixtime(self) -> int:
"""
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:
"""
Retourne une date au format string (24-12-2023 20:50:59)
"""
currentdate = datetime.now().strftime('%d-%m-%Y %H:%M:%S')
return currentdate
def get_all_modules(self) -> list:
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())
return all_modules
def create_log(self, log_message: str) -> None: def create_log(self, log_message: str) -> None:
"""Enregiste les logs """Enregiste les logs
Args: Args:
string (str): Le message a enregistrer log_message (str): Le message a enregistrer
Returns: Returns:
None: Aucun retour None: Aucun retour
""" """
sql_insert = f"INSERT INTO {self.Config.TABLE_LOG} (datetime, server_msg) VALUES (:datetime, :server_msg)" 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) self.db_execute_query(sql_insert, mes_donnees)
return None 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 """Enregistre les commandes envoyées par les utilisateurs
Args: Args:
user_cmd (str): The user who performed the command
cmd (str): la commande a enregistrer cmd (str): la commande a enregistrer
""" """
cmd_list = cmd.split() cmd_list = cmd.split()
@@ -260,70 +171,10 @@ class Base:
cmd = ' '.join(cmd_list) cmd = ' '.join(cmd_list)
insert_cmd_query = f"INSERT INTO {self.Config.TABLE_COMMAND} (datetime, user, commande) VALUES (:datetime, :user, :commande)" 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) 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
Args:
module_name (str): le non du module a chercher dans la base de données
Returns:
bool: True si le module existe déja dans la base de données sinon False
"""
query = f"SELECT id FROM {self.Config.TABLE_MODULE} WHERE module_name = :module_name"
mes_donnes = {'module_name': module_name}
results = self.db_execute_query(query, mes_donnes)
if results.fetchall():
return True
else:
return False
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
"""
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}
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
def db_update_module(self, user_cmd: str, module_name: str) -> None:
"""Modifie la date et le user qui a rechargé le module
Args:
user_cmd (str): le user qui a rechargé le module
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}
self.db_execute_query(update_cmd_query, mes_donnees)
return False
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
"""
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
def db_sync_core_config(self, module_name: str, dataclassObj: object) -> bool: def db_sync_core_config(self, module_name: str, dataclassObj: object) -> bool:
"""Sync module local parameters with the database """Sync module local parameters with the database
@@ -341,7 +192,7 @@ class Base:
""" """
try: try:
response = True response = True
current_date = self.get_datetime() current_date = self.Utils.get_sdatetime()
core_table = self.Config.TABLE_CONFIG core_table = self.Config.TABLE_CONFIG
# Add local parameters to DB # Add local parameters to DB
@@ -391,7 +242,7 @@ class Base:
result = response.fetchall() result = response.fetchall()
for param, value in result: for param, value in result:
if type(getattr(dataclassObj, param)) == list: if isinstance(getattr(dataclassObj, param), list):
value = ast.literal_eval(value) value = ast.literal_eval(value)
setattr(dataclassObj, param, self.int_if_possible(value)) setattr(dataclassObj, param, self.int_if_possible(value))
@@ -418,7 +269,7 @@ class Base:
isParamExist = result.fetchone() isParamExist = result.fetchone()
if not isParamExist is None: if not isParamExist is None:
mes_donnees = {'datetime': self.get_datetime(), mes_donnees = {'datetime': self.Utils.get_sdatetime(),
'module_name': module_name, 'module_name': module_name,
'param_key': param_key, 'param_key': param_key,
'param_value': param_value 'param_value': param_value
@@ -443,20 +294,21 @@ class Base:
user = self.db_execute_query(f"SELECT id FROM {self.Config.TABLE_ADMIN}") user = self.db_execute_query(f"SELECT id FROM {self.Config.TABLE_ADMIN}")
if not user.fetchall(): if not user.fetchall():
admin = self.Config.OWNER 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, 'user': admin,
'password': password, 'password': password,
'hostname': '*', 'hostname': '*',
'vhost': '*', 'vhost': '*',
'language': 'EN',
'level': 5 'level': 5
} }
self.db_execute_query(f""" self.db_execute_query(f"""
INSERT INTO {self.Config.TABLE_ADMIN} INSERT INTO {self.Config.TABLE_ADMIN}
(createdOn, user, password, hostname, vhost, level) (createdOn, user, password, hostname, vhost, language, level)
VALUES VALUES
(:createdOn, :user, :password, :hostname, :vhost, :level)""" (:createdOn, :user, :password, :hostname, :vhost, :language, :level)"""
, mes_donnees) , mes_donnees)
return None return None
@@ -465,15 +317,18 @@ class Base:
try: try:
t = threading.Timer(interval=time_to_wait, function=func, args=func_args) t = threading.Timer(interval=time_to_wait, function=func, args=func_args)
t.setName(func.__name__) t.name = func.__name__
t.start() t.start()
self.running_timers.append(t) self.running_timers.append(t)
self.logs.debug(f"-- Timer ID : {str(t.ident)} | Running Threads : {len(threading.enumerate())}") self.logs.debug(f"-- Timer ID : {str(t.ident)} | Running Threads : {len(threading.enumerate())}")
return None
except AssertionError as ae: except AssertionError as ae:
self.logs.error(f'Assertion Error -> {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: 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 """Create a new thread and store it into running_threads variable
@@ -484,21 +339,57 @@ class Base:
run_once (bool, optional): If you want to ensure that this method/function run once. Defaults to False. run_once (bool, optional): If you want to ensure that this method/function run once. Defaults to False.
""" """
try: try:
# Clean unused threads first
self.garbage_collector_thread()
func_name = func.__name__ func_name = func.__name__
if run_once: if run_once:
for thread in self.running_threads: for thread in self.running_threads:
if thread.getName() == func_name: if thread.name == func_name:
return None return None
th = threading.Thread(target=func, args=func_args, name=str(func_name), daemon=daemon) th = threading.Thread(target=func, args=func_args, name=str(func_name), daemon=daemon)
th.start() th.start()
self.running_threads.append(th) self.running_threads.append(th)
self.logs.debug(f"-- Thread ID : {str(th.ident)} | Thread name : {th.getName()} | Running Threads : {len(threading.enumerate())}") self.logs.debug(f"-- Thread ID : {str(th.ident)} | Thread name : {th.name} | Running Threads : {len(threading.enumerate())}")
except AssertionError as ae: except Exception as err:
self.logs.error(f'{ae}') self.logs.error(err, exc_info=True)
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: def thread_count(self, thread_name: str) -> int:
"""This method return the number of existing threads """This method return the number of existing threads
@@ -514,7 +405,7 @@ class Base:
count = 0 count = 0
for thr in self.running_threads: for thr in self.running_threads:
if thread_name == thr.getName(): if thread_name == thr.name:
count += 1 count += 1
return count return count
@@ -523,12 +414,11 @@ class Base:
"""Methode qui supprime les timers qui ont finis leurs job """Methode qui supprime les timers qui ont finis leurs job
""" """
try: try:
for timer in self.running_timers: for timer in self.running_timers:
if not timer.is_alive(): if not timer.is_alive():
timer.cancel() timer.cancel()
self.running_timers.remove(timer) self.running_timers.remove(timer)
self.logs.info(f"-- Timer {str(timer)} removed") self.logs.debug(f"-- Timer {str(timer)} removed")
else: else:
self.logs.debug(f"--* Timer {str(timer)} Still running ...") self.logs.debug(f"--* Timer {str(timer)} Still running ...")
@@ -540,15 +430,14 @@ class Base:
""" """
try: try:
for thread in self.running_threads: for thread in self.running_threads:
if thread.getName() != 'heartbeat': if thread.name != 'heartbeat':
# print(thread.getName(), thread.is_alive(), sep=' / ')
if not thread.is_alive(): if not thread.is_alive():
self.running_threads.remove(thread) self.running_threads.remove(thread)
self.logs.info(f"-- Thread {str(thread.getName())} {str(thread.native_id)} removed") self.logs.debug(f"-- Thread {str(thread.name)} {str(thread.native_id)} has been removed!")
# print(threading.enumerate()) # print(threading.enumerate())
except AssertionError as ae: except Exception as err:
self.logs.error(f'Assertion Error -> {ae}') self.logs.error(err, exc_info=True)
def garbage_collector_sockets(self) -> None: def garbage_collector_sockets(self) -> None:
@@ -568,19 +457,19 @@ class Base:
self.logs.debug(f"=======> Checking for Timers to stop") self.logs.debug(f"=======> Checking for Timers to stop")
for timer in self.running_timers: for timer in self.running_timers:
while timer.is_alive(): while timer.is_alive():
self.logs.debug(f"> waiting for {timer.getName()} to close") self.logs.debug(f"> waiting for {timer.name} to close")
timer.cancel() timer.cancel()
time.sleep(0.2) time.sleep(0.2)
self.running_timers.remove(timer) self.running_timers.remove(timer)
self.logs.debug(f"> Cancelling {timer.getName()} {timer.native_id}") self.logs.debug(f"> Cancelling {timer.name} {timer.native_id}")
self.logs.debug(f"=======> Checking for Threads to stop") self.logs.debug(f"=======> Checking for Threads to stop")
for thread in self.running_threads: for thread in self.running_threads:
if thread.getName() == 'heartbeat' and thread.is_alive(): if thread.name == 'heartbeat' and thread.is_alive():
self.execute_periodic_action() self.execute_periodic_action()
self.logs.debug(f"> Running the last periodic action") self.logs.debug(f"> Running the last periodic action")
self.running_threads.remove(thread) self.running_threads.remove(thread)
self.logs.debug(f"> Cancelling {thread.getName()} {thread.native_id}") self.logs.debug(f"> Cancelling {thread.name} {thread.native_id}")
self.logs.debug(f"=======> Checking for Sockets to stop") self.logs.debug(f"=======> Checking for Sockets to stop")
for soc in self.running_sockets: for soc in self.running_sockets:
@@ -604,7 +493,7 @@ class Base:
engine = create_engine(f'sqlite:///{full_path_db}.db', echo=False) engine = create_engine(f'sqlite:///{full_path_db}.db', echo=False)
cursor = engine.connect() cursor = engine.connect()
self.logs.info("-- database connexion has been initiated") self.logs.info("-- Database connexion has been initiated")
return engine, cursor return engine, cursor
def __create_db(self) -> None: def __create_db(self) -> None:
@@ -657,6 +546,8 @@ class Base:
hostname TEXT, hostname TEXT,
vhost TEXT, vhost TEXT,
password TEXT, password TEXT,
fingerprint TEXT,
language TEXT,
level INTEGER level INTEGER
) )
''' '''
@@ -683,9 +574,12 @@ class Base:
self.db_execute_query(table_core_channel) self.db_execute_query(table_core_channel)
self.db_execute_query(table_core_config) self.db_execute_query(table_core_config)
# Patch database
self.db_patch(self.Config.TABLE_ADMIN, "language", "TEXT")
if self.install: if self.install:
self.db_record_module('sys', 'mod_command', 1) self.Loader.ModuleUtils.db_register_module('mod_command', 'sys', True)
self.db_record_module('sys', 'mod_defender', 1) self.Loader.ModuleUtils.db_register_module('mod_defender', 'sys', True)
self.install = False self.install = False
return None return None
@@ -703,6 +597,27 @@ class Base:
return response return response
def db_is_column_exist(self, table_name: str, column_name: str) -> bool:
q = self.db_execute_query(f"PRAGMA table_info({table_name})")
existing_columns = [col[1] for col in q.fetchall()]
if column_name in existing_columns:
return True
else:
return False
def db_patch(self, table_name: str, column_name: str, column_type: str) -> bool:
if not self.db_is_column_exist(table_name, column_name):
patch = f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_type}"
update_row = f"UPDATE {table_name} SET language = 'EN' WHERE language is null"
self.db_execute_query(patch)
self.db_execute_query(update_row)
self.logs.debug(f"The patch has been applied")
self.logs.debug(f"Table name: {table_name}, Column name: {column_name}, Column type: {column_type}")
return True
else:
return False
def db_close(self) -> None: def db_close(self) -> None:
try: try:
@@ -710,19 +625,6 @@ class Base:
except AttributeError as ae: except AttributeError as ae:
self.logs.error(f"Attribute Error : {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): def int_if_possible(self, value):
"""Convertit la valeur reçue en entier, si possible. """Convertit la valeur reçue en entier, si possible.
Sinon elle retourne la valeur initiale. Sinon elle retourne la valeur initiale.
@@ -741,14 +643,14 @@ class Base:
except TypeError: except TypeError:
return value 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 """Convert a value to int
Args: Args:
value (any): Value to convert to int if possible value (any): Value to convert to int if possible
Returns: Returns:
Union[int, None]: Return the int value or None if not possible int: Return the int value or None if not possible
""" """
try: try:
response = int(value) response = int(value)
@@ -789,18 +691,17 @@ class Base:
self.logs.error(f'General Error: {err}') self.logs.error(f'General Error: {err}')
return False 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: try:
binary_ip = b64decode(ip_b64encoded)
decoded_ip = ipaddress.ip_address(binary_ip) decoded_ip = ipaddress.ip_address(binary_ip)
return decoded_ip.exploded return decoded_ip.exploded
except ValueError as ve: except ValueError as ve:
self.logs.critical(f'This remote ip is not valid : {ve}') self.logs.critical(f'This remote ip ({ip_b64encoded}) is not valid : {ve}')
return None 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) binary_ip = socket.inet_aton(remote_ip_address)
try: try:
@@ -814,15 +715,6 @@ class Base:
self.logs.critical(f'General Error: {err}') self.logs.critical(f'General Error: {err}')
return None 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: def execute_periodic_action(self) -> None:
if not self.periodic_func: if not self.periodic_func:
@@ -862,23 +754,3 @@ class Base:
self.logs.debug(f'Method to execute : {str(self.periodic_func)}') self.logs.debug(f'Method to execute : {str(self.periodic_func)}')
return None 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}')

View File

@@ -1,127 +0,0 @@
from typing import Union
import core.definition as df
from core.base import Base
class Admin:
UID_ADMIN_DB: list[df.MAdmin] = []
def __init__(self, baseObj: Base) -> None:
self.Logs = baseObj.logs
pass
def insert(self, newAdmin: df.MAdmin) -> bool:
result = False
exist = False
for record in self.UID_ADMIN_DB:
if record.uid == newAdmin.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
if not exist:
self.UID_ADMIN_DB.append(newAdmin)
result = True
self.Logs.debug(f'UID ({newAdmin.uid}) has been created')
if not result:
self.Logs.critical(f'The User Object was not inserted {newAdmin}')
return result
def update_nickname(self, uid: str, newNickname: str) -> bool:
result = False
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
if not result:
self.Logs.critical(f'The new nickname {newNickname} was not updated, uid = {uid}')
return result
def update_level(self, nickname: str, newLevel: int) -> bool:
result = False
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
if not result:
self.Logs.critical(f'The new level {newLevel} was not updated, nickname = {nickname}')
return result
def delete(self, uidornickname: str) -> bool:
result = False
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:
# 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
if not result:
self.Logs.critical(f'The UID {uidornickname} was not deleted')
return result
def get_Admin(self, uidornickname: str) -> Union[df.MAdmin, None]:
Admin = None
for record in self.UID_ADMIN_DB:
if record.uid == uidornickname:
Admin = record
elif record.nickname == uidornickname:
Admin = record
#self.Logs.debug(f'Search {uidornickname} -- result = {Admin}')
return Admin
def get_uid(self, uidornickname:str) -> Union[str, None]:
uid = None
for record in self.UID_ADMIN_DB:
if record.uid == uidornickname:
uid = record.uid
if record.nickname == uidornickname:
uid = record.uid
self.Logs.debug(f'The UID that you are looking for {uidornickname} has been found {uid}')
return uid
def get_nickname(self, uidornickname:str) -> Union[str, None]:
nickname = None
for record in self.UID_ADMIN_DB:
if record.nickname == uidornickname:
nickname = record.nickname
if record.uid == uidornickname:
nickname = record.nickname
self.Logs.debug(f'The value {uidornickname} -- {nickname}')
return nickname

View File

@@ -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

View File

@@ -1,57 +0,0 @@
from json import load
from sys import exit
from os import sep
from typing import Union
from core.definition import MConfig
class Configuration:
def __init__(self) -> None:
self.ConfigObject: MConfig = self.__load_service_configuration()
return None
def __load_json_service_configuration(self):
try:
conf_filename = f'config{sep}configuration.json'
with open(conf_filename, 'r') as configuration_data:
configuration:dict[str, Union[str, int, list, dict]] = load(configuration_data)
return configuration
except FileNotFoundError as fe:
print(f'FileNotFound: {fe}')
print('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')
def __load_service_configuration(self) -> MConfig:
try:
import_config = self.__load_json_service_configuration()
Model_keys = MConfig().__dict__
model_key_list: list = []
json_config_key_list: list = []
for key in Model_keys:
model_key_list.append(key)
for key in import_config:
json_config_key_list.append(key)
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 \!/")
ConfigObject: MConfig = MConfig(
**import_config
)
return ConfigObject
except TypeError as te:
print(te)

View File

@@ -0,0 +1,125 @@
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Optional
from dataclasses import dataclass
if TYPE_CHECKING:
from core.irc import Irc
class IModule(ABC):
@abstractmethod
@dataclass
class ModConfModel:
"""The Model containing the module parameters
"""
def __init__(self, uplink: 'Irc') -> None:
# Module name (Mandatory)
self.module_name = 'mod_' + str(self.__class__.__name__).lower()
# Add Irc Object to the module (Mandatory)
self.Irc = uplink
# Add Loader object to the module (Mandatory)
self.Loader = uplink.Loader
# Add Protocol to the module (Mandatory)
self.Protocol = uplink.Protocol
# Add Global Configuration to the module (Mandatory)
self.Config = uplink.Config
# Add Settings to the module (Mandatory)
self.Settings = uplink.Settings
# Add Base object to the module (Mandatory)
self.Base = uplink.Base
# Add Main Utils (Mandatory)
self.MainUtils = uplink.Utils
# Add logs object to the module (Mandatory)
self.Logs = uplink.Loader.Logs
# Add User object to the module (Mandatory)
self.User = uplink.User
# Add Client object to the module (Mandatory)
self.Client = uplink.Client
# Add Admin object to the module (Mandatory)
self.Admin = uplink.Admin
# Add Channel object to the module (Mandatory)
self.Channel = uplink.Channel
# Add Reputation object to the module (Optional)
self.Reputation = uplink.Reputation
# Load the child classes
self.load()
# Inspect child classes
self.inspect_class()
self.create_tables()
# Sync the configuration with core configuration (Mandatory)
uplink.Base.db_sync_core_config(self.module_name, self.ModConfig)
# Log the module
self.Logs.debug(f'Loading Module {self.module_name} ...')
def update_configuration(self, param_key: str, param_value: str) -> None:
"""Update the local and core configuration
Args:
param_key (str): The parameter key
param_value (str): The parameter value
"""
self.Base.db_update_core_config(self.module_name, self.ModConfig, param_key, param_value)
def inspect_class(self):
if not hasattr(self, 'ModConfig'):
raise AttributeError("The Module must init ModConfig attribute in the load method!")
if not hasattr(self, 'MOD_HEADER'):
raise NotImplementedError(f"You must declare the header of the module in {self.__class__.__name__}!")
@abstractmethod
def create_tables(self) -> None:
"""Method that will create the database if it does not exist.
A single Session for this class will be created, which will be used within this class/module.
Returns:
None: No return is expected
"""
@abstractmethod
def load(self) -> None:
"""This method is executed when the module is loaded or reloaded.
"""
@abstractmethod
def unload(self) -> None:
"""This method is executed when the module is unloaded or reloaded.
"""
@abstractmethod
def cmd(self, data: list) -> None:
"""When recieving server messages.
Args:
data (list): The recieved message
"""
@abstractmethod
def hcmds(self, user: str, channel: Optional[str], cmd: list[str], fullcmd: Optional[list[str]] = None) -> None:
"""These are the commands recieved from a client
Args:
user (str): The client
channel (str|None): The channel if available
cmd (list): The user command sent
fullcmd (list, optional): The full server message. Defaults to [].
"""

View File

@@ -0,0 +1,576 @@
from abc import ABC, abstractmethod
from typing import Optional, TYPE_CHECKING
from core.classes.protocols.command_handler import CommandHandler
if TYPE_CHECKING:
from core.definition import MClient, MSasl, MUser, MChannel
from core.irc import Irc
class IProtocol(ABC):
Handler: Optional[CommandHandler] = None
def __init__(self, uplink: 'Irc'):
self.name: Optional[str] = None
self.protocol_version: int = -1
self.known_protocol: set[str] = set()
self._Irc = uplink
self._Config = uplink.Config
self._Base = uplink.Base
self._Settings = uplink.Base.Settings
self._Utils = uplink.Loader.Utils
self._Logs = uplink.Loader.Logs
self._User = uplink.User
self._Channel = uplink.Channel
self.Handler = CommandHandler(uplink.Loader)
self.init_protocol()
self._Logs.info(f"[PROTOCOL] Protocol [{self.__class__.__name__}] loaded!")
@abstractmethod
def init_protocol(self):
"""Init protocol
"""
@abstractmethod
def get_ircd_protocol_poisition(self, cmd: list[str], log: bool = False) -> tuple[int, Optional[str]]:
"""Get the position of known commands
Args:
cmd (list[str]): The server response
log (bool): If true it will log in the logger
Returns:
tuple[int, Optional[str]]: The position and the command.
"""
@abstractmethod
def register_command(self):
"""Register all commands that you need to handle
"""
@abstractmethod
def send2socket(self, message: str, print_log: bool = True) -> None:
"""Envoit les commandes à envoyer au serveur.
Args:
message (str): contient la commande à envoyer au serveur.
print_log (bool): If True then print logs
"""
@abstractmethod
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
could be either channel or nickname not both together
Args:
msg (str): The message to send
nick_from (str): The sender nickname
channel (str, optional): The receiver channel. Defaults to None.
nick_to (str, optional): The reciever nickname. Defaults to None.
"""
@abstractmethod
def send_notice(self, nick_from: str, nick_to: str, msg: str) -> None:
"""Sending NOTICE by batches
Args:
msg (str): The message to send to the server
nick_from (str): The sender Nickname
nick_to (str): The reciever nickname
"""
@abstractmethod
def send_link(self) -> None:
"""Créer le link et envoyer les informations nécessaires pour la
connexion au serveur.
"""
@abstractmethod
def send_gline(self, nickname: str, hostname: str, set_by: str, expire_timestamp: int, set_at_timestamp: int, reason: str) -> None:
"""Send a gline command to the server
Args:
nickname (str): The nickname of the client.
hostname (str): The hostname of the client.
set_by (str): The nickname who send the gline
expire_timestamp (int): Expire timestamp
set_at_timestamp (int): Set at timestamp
reason (str): The reason of the gline.
"""
@abstractmethod
def send_set_nick(self, newnickname: str) -> None:
"""Change nickname of the server
\n This method will also update the User object
Args:
newnickname (str): New nickname of the server
"""
@abstractmethod
def send_set_mode(self, modes: str, *, nickname: Optional[str] = None, channel_name: Optional[str] = None, params: Optional[str] = None) -> None:
"""Set a mode to channel or to a nickname or for a user in a channel
Args:
modes (str): The selected mode
nickname (Optional[str]): The nickname
channel_name (Optional[str]): The channel name
params (Optional[str]): Parameters like password.
"""
@abstractmethod
def send_squit(self, server_id: str, server_link: str, reason: str) -> None:
"""_summary_
Args:
server_id (str): _description_
server_link (str): _description_
reason (str): _description_
"""
@abstractmethod
def send_ungline(self, nickname:str, hostname: str) -> None:
"""_summary_
Args:
nickname (str): _description_
hostname (str): _description_
"""
@abstractmethod
def send_kline(self, nickname: str, hostname: str, set_by: str, expire_timestamp: int, set_at_timestamp: int, reason: str) -> None:
"""_summary_
Args:
nickname (str): _description_
hostname (str): _description_
set_by (str): _description_
expire_timestamp (int): _description_
set_at_timestamp (int): _description_
reason (str): _description_
"""
@abstractmethod
def send_unkline(self, nickname:str, hostname: str) -> None:
"""_summary_
Args:
nickname (str): _description_
hostname (str): _description_
"""
@abstractmethod
def send_sjoin(self, channel: str) -> None:
"""Server will join a channel with pre defined umodes
Args:
channel (str): Channel to join
"""
@abstractmethod
def send_sapart(self, nick_to_sapart: str, channel_name: str) -> None:
"""_summary_
Args:
nick_to_sapart (str): _description_
channel_name (str): _description_
"""
@abstractmethod
def send_sajoin(self, nick_to_sajoin: str, channel_name: str) -> None:
"""_summary_
Args:
nick_to_sajoin (str): _description_
channel_name (str): _description_
"""
@abstractmethod
def send_svspart(self, nick_to_part: str, channels: list[str], reason: str) -> None:
"""_summary_
Args:
nick_to_part (str): _description_
channels (list[str]): _description_
reason (str): _description_
"""
@abstractmethod
def send_svsjoin(self, nick_to_part: str, channels: list[str], keys: list[str]) -> None:
"""_summary_
Args:
nick_to_part (str): _description_
channels (list[str]): _description_
keys (list[str]): _description_
"""
@abstractmethod
def send_svsmode(self, nickname: str, user_mode: str) -> None:
"""_summary_
Args:
nickname (str): _description_
user_mode (str): _description_
"""
@abstractmethod
def send_svs2mode(self, nickname: str, user_mode: str) -> None:
"""_summary_
Args:
nickname (str): _description_
user_mode (str): _description_
"""
@abstractmethod
def send_svslogin(self, client_uid: str, user_account: str) -> None:
"""Log a client into his account.
Args:
client_uid (str): Client UID
user_account (str): The account of the user
"""
@abstractmethod
def send_svslogout(self, client_obj: 'MClient') -> None:
"""Logout a client from his account
Args:
client_obj (MClient): The Client UID
"""
@abstractmethod
def send_quit(self, uid: str, reason: str, print_log: bool = True) -> None:
"""Send quit message
- Delete uid from User object
- Delete uid from Reputation object
Args:
uid (str): The UID or the Nickname
reason (str): The reason for the quit
print_log (bool): If True then print logs
"""
@abstractmethod
def send_uid(self, nickname:str, username: str, hostname: str, uid:str, umodes: str, vhost: str, remote_ip: str, realname: str, print_log: bool = True) -> None:
"""Send UID to the server
- Insert User to User Object
Args:
nickname (str): Nickname of the client
username (str): Username of the client
hostname (str): Hostname of the client you want to create
uid (str): UID of the client you want to create
umodes (str): umodes of the client you want to create
vhost (str): vhost of the client you want to create
remote_ip (str): remote_ip of the client you want to create
realname (str): realname of the client you want to create
print_log (bool, optional): print logs if true. Defaults to True.
"""
@abstractmethod
def send_join_chan(self, uidornickname: str, channel: str, password: str = None, print_log: bool = True) -> None:
"""Joining a channel
Args:
uidornickname (str): UID or nickname that need to join
channel (str): channel to join
password (str, optional): The password of the channel to join. Default to None
print_log (bool, optional): Write logs. Defaults to True.
"""
@abstractmethod
def send_part_chan(self, uidornickname:str, channel: str, print_log: bool = True) -> None:
"""Part from a channel
Args:
uidornickname (str): UID or nickname that need to join
channel (str): channel to join
print_log (bool, optional): Write logs. Defaults to True.
"""
@abstractmethod
def send_mode_chan(self, channel_name: str, channel_mode: str) -> None:
"""_summary_
Args:
channel_name (str): _description_
channel_mode (str): _description_
"""
@abstractmethod
def send_raw(self, raw_command: str) -> None:
"""Send raw message to the server
Args:
raw_command (str): The raw command you want to send.
"""
# ------------------------------------------------------------------------
# COMMON IRC PARSER
# ------------------------------------------------------------------------
@abstractmethod
def parse_uid(self, server_msg: list[str]) -> Optional['MUser']:
"""Parse UID and return dictionary.
Args:
server_msg (list[str]): The UID IRCD message
Returns:
Optional[MUser]: The MUser object or None
"""
@abstractmethod
def parse_quit(self, server_msg: list[str]) -> tuple[Optional['MUser'], str]:
"""Parse quit and return dictionary.
>>> [':97KAAAAAB', 'QUIT', ':Quit:', 'this', 'is', 'my', 'reason', 'to', 'quit']
Args:
server_msg (list[str]): The server message to parse
Returns:
tuple[MUser, str]: The User Who Quit Object and the reason.
"""
@abstractmethod
def parse_nick(self, server_msg: list[str]) -> tuple[Optional['MUser'], str, str]:
"""Parse nick changes and return dictionary.
>>> [':97KAAAAAC', 'NICK', 'testinspir', '1757360740']
Args:
server_msg (list[str]): The server message to parse
Returns:
tuple(MUser, newnickname(str), timestamp(str)): Tuple of the response.
>>> MUser, newnickname, timestamp
"""
@abstractmethod
def parse_privmsg(self, server_msg: list[str]) -> tuple[Optional['MUser'], Optional['MUser'], Optional['MChannel'], str]:
"""Parse PRIVMSG message.
>>> [':97KAAAAAE', 'PRIVMSG', '#welcome', ':This', 'is', 'my', 'public', 'message']
Args:
server_msg (list[str]): The server message to parse
Returns:
tuple[MUser(Sender), MUser(Reciever), MChannel, str]: Sender user model, reciever user model, Channel model, messgae.
"""
# ------------------------------------------------------------------------
# EVENT HANDLER
# ------------------------------------------------------------------------
@abstractmethod
def on_svs2mode(self, server_msg: list[str]) -> None:
"""Handle svs2mode coming from a server
>>> [':00BAAAAAG', 'SVS2MODE', '001U01R03', '-r']
Args:
server_msg (list[str]): Original server message
"""
@abstractmethod
def on_mode(self, server_msg: list[str]) -> None:
"""Handle mode coming from a server
Args:
server_msg (list[str]): Original server message
"""
@abstractmethod
def on_umode2(self, server_msg: list[str]) -> None:
"""Handle umode2 coming from a server
>>> [':adator_', 'UMODE2', '-i']
Args:
server_msg (list[str]): Original server message
"""
@abstractmethod
def on_quit(self, server_msg: list[str]) -> None:
"""Handle quit coming from a server
Args:
server_msg (list[str]): Original server message
"""
@abstractmethod
def on_squit(self, server_msg: list[str]) -> None:
"""Handle squit coming from a server
Args:
server_msg (list[str]): Original server message
"""
@abstractmethod
def on_protoctl(self, server_msg: list[str]) -> None:
"""Handle protoctl coming from a server
Args:
server_msg (list[str]): Original server message
"""
@abstractmethod
def on_nick(self, server_msg: list[str]) -> None:
"""Handle nick coming from a server
new nickname
Args:
server_msg (list[str]): Original server message
"""
@abstractmethod
def on_sjoin(self, server_msg: list[str]) -> None:
"""Handle sjoin coming from a server
Args:
server_msg (list[str]): Original server message
"""
@abstractmethod
def on_part(self, server_msg: list[str]) -> None:
"""Handle part coming from a server
Args:
server_msg (list[str]): Original server message
"""
@abstractmethod
def on_eos(self, server_msg: list[str]) -> None:
"""Handle EOS coming from a server
Args:
server_msg (list[str]): Original server message
"""
@abstractmethod
def on_reputation(self, server_msg: list[str]) -> None:
"""Handle REPUTATION coming from a server
Args:
server_msg (list[str]): Original server message
"""
@abstractmethod
def on_uid(self, server_msg: list[str]) -> None:
"""Handle uid message coming from the server
Args:
server_msg (list[str]): Original server message
"""
@abstractmethod
def on_privmsg(self, server_msg: list[str]) -> None:
"""Handle PRIVMSG message coming from the server
Args:
server_msg (list[str]): Original server message
"""
@abstractmethod
def on_server_ping(self, server_msg: list[str]) -> None:
"""Send a PONG message to the server
Args:
server_msg (list[str]): List of str coming from the server
"""
@abstractmethod
def on_server(self, server_msg: list[str]) -> None:
"""_summary_
Args:
server_msg (list[str]): _description_
"""
@abstractmethod
def on_version(self, server_msg: list[str]) -> None:
"""Sending Server Version to the server
Args:
server_msg (list[str]): List of str coming from the server
"""
@abstractmethod
def on_time(self, server_msg: list[str]) -> None:
"""Sending TIME answer to a requestor
Args:
server_msg (list[str]): List of str coming from the server
"""
@abstractmethod
def on_ping(self, server_msg: list[str]) -> None:
"""Sending a PING answer to requestor
Args:
server_msg (list[str]): List of str coming from the server
"""
@abstractmethod
def on_version_msg(self, server_msg: list[str]) -> None:
"""Handle version coming from the server
\n ex. /version Defender
Args:
server_msg (list[str]): Original message from the server
"""
@abstractmethod
def on_smod(self, server_msg: list[str]) -> None:
"""Handle SMOD message coming from the server
Args:
server_msg (list[str]): Original server message
"""
@abstractmethod
def on_sasl(self, server_msg: list[str]) -> Optional['MSasl']:
"""Handle SASL coming from a server
Args:
server_msg (list[str]): Original server message
Returns:
"""
@abstractmethod
def on_sasl_authentication_process(self, sasl_model: 'MSasl') -> bool:
"""Finalize sasl authentication
Args:
sasl_model (MSasl): The sasl dataclass model
Returns:
bool: True if success
"""
@abstractmethod
def on_md(self, server_msg: list[str]) -> None:
"""Handle MD responses
[':001', 'MD', 'client', '001MYIZ03', 'certfp', ':d1235648...']
Args:
server_msg (list[str]): The server reply
"""
@abstractmethod
def on_kick(self, server_msg: list[str]) -> None:
"""When a user is kicked out from a channel
Eg. ['@unrealircd.org...', ':001', 'KICK', '#jsonrpc', '001ELW13T', ':Kicked', 'from', 'JSONRPC', 'User']
Args:
server_msg (list[str]): The server message
"""
@abstractmethod
def on_sethost(self, server_msg: list[str]) -> None:
"""On SETHOST command
>>> [':001DN7305', 'SETHOST', ':netadmin.example.org']
Args:
server_msg (list[str]): _description_
"""

View File

@@ -0,0 +1,224 @@
from typing import TYPE_CHECKING, Optional
from core.definition import MAdmin
if TYPE_CHECKING:
from core.loader import Loader
class Admin:
UID_ADMIN_DB: list[MAdmin] = []
def __init__(self, loader: 'Loader') -> None:
"""
Args:
loader (Loader): The Loader Instance.
"""
self.Logs = loader.Logs
self.Base = loader.Base
self.Setting = loader.Settings
self.Config = loader.Config
self.User = loader.User
self.Definition = loader.Definition
def insert(self, new_admin: MAdmin) -> bool:
"""Insert a new admin object model
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 == new_admin.uid:
self.Logs.debug(f'{record.uid} already exist')
return False
self.UID_ADMIN_DB.append(new_admin)
self.Logs.debug(f'A new admin ({new_admin.nickname}) has been created')
return True
def update_nickname(self, uid: str, new_admin_nickname: str) -> bool:
"""Update nickname of an admin
Args:
uid (str): The Admin UID
new_admin_nickname (str): The new nickname of the admin
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 = new_admin_nickname
self.Logs.debug(f'UID ({record.uid}) has been updated with new nickname {new_admin_nickname}')
return True
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, new_admin_level: int) -> bool:
"""Update the admin level
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 = new_admin_level
self.Logs.debug(f'Admin ({record.nickname}) has been updated with new level {new_admin_level}')
return True
self.Logs.debug(f'The new level {new_admin_level} was not updated, nickname = {nickname} - The Client is not an admin')
return False
def delete(self, uidornickname: str) -> bool:
"""Delete admin
Args:
uidornickname (str): The UID or nickname of the admin
Returns:
bool: True if the admin has been deleted
"""
admin_obj = self.get_admin(uidornickname)
if admin_obj:
self.UID_ADMIN_DB.remove(admin_obj)
self.Logs.debug(f'UID ({admin_obj.uid}) has been deleted')
return True
self.Logs.debug(f'The UID {uidornickname} was not deleted')
return False
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
"""
for record in self.UID_ADMIN_DB:
if record.uid == uidornickname:
return record
elif record.nickname.lower() == uidornickname.lower():
return record
return None
def get_uid(self, uidornickname:str) -> Optional[str]:
"""Get the UID of the admin
Args:
uidornickname (str): The UID or nickname of the admin
Returns:
Optional[str]: The UID of the admin
"""
for record in self.UID_ADMIN_DB:
if record.uid == uidornickname:
return record.uid
if record.nickname.lower() == uidornickname.lower():
return record.uid
return 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
"""
for record in self.UID_ADMIN_DB:
if record.nickname.lower() == uidornickname.lower():
return record.nickname
if record.uid == uidornickname:
return record.nickname
return None
def get_language(self, uidornickname: str) -> Optional[str]:
"""Get the language of the admin
Args:
uidornickname (str): The user ID or the Nickname of the admin
Returns:
Optional[str]: The language selected by the admin.
"""
admin = self.get_admin(uidornickname)
if admin is None:
return None
return admin.language
def db_auth_admin_via_fingerprint(self, fp: str, uidornickname: str) -> bool:
"""Check the fingerprint
Args:
fp (str): The unique fingerprint of the user
uidornickname (str): The UID or the Nickname of the user
Returns:
bool: True if found
"""
if fp is None:
return False
query = f"SELECT user, level, language FROM {self.Config.TABLE_ADMIN} WHERE fingerprint = :fp"
data = {'fp': fp}
exe = self.Base.db_execute_query(query, data)
result = exe.fetchone()
if result:
account = result[0]
level = result[1]
language = result[2]
user_obj = self.User.get_user(uidornickname)
if user_obj:
admin_obj = self.Definition.MAdmin(**user_obj.to_dict(), account=account, level=level, language=language)
if self.insert(admin_obj):
self.Setting.current_admin = admin_obj
self.Logs.debug(f"[Fingerprint login] {user_obj.nickname} ({admin_obj.account}) has been logged in successfully!")
return True
return False
def db_is_admin_exist(self, admin_nickname: str) -> bool:
"""Verify if the admin exist in the database!
Args:
admin_nickname (str): The nickname admin to check.
Returns:
bool: True if the admin exist otherwise False.
"""
mes_donnees = {'admin': admin_nickname}
query_search_user = f"SELECT id FROM {self.Config.TABLE_ADMIN} WHERE user = :admin"
r = self.Base.db_execute_query(query_search_user, mes_donnees)
exist_user = r.fetchone()
if exist_user:
return True
else:
return False

View File

@@ -1,12 +1,9 @@
from re import findall from re import findall
from typing import Union, Literal, TYPE_CHECKING from typing import Any, Optional, Literal, TYPE_CHECKING
from dataclasses import asdict
from core.classes import user
if TYPE_CHECKING: if TYPE_CHECKING:
from core.definition import MChannel from core.definition import MChannel
from core.base import Base from core.loader import Loader
class Channel: class Channel:
@@ -14,18 +11,21 @@ class Channel:
"""List that contains all the Channels objects (ChannelModel) """List that contains all the Channels objects (ChannelModel)
""" """
def __init__(self, baseObj: 'Base') -> None: def __init__(self, loader: 'Loader'):
"""
self.Logs = baseObj.logs Args:
self.Base = baseObj loader (Loader): The Loader Instance
"""
self.Logs = loader.Logs
self.Base = loader.Base
self.Utils = loader.Utils
return None def insert(self, new_channel: 'MChannel') -> bool:
def insert(self, newChan: 'MChannel') -> bool:
"""This method will insert a new channel and if the channel exist it will update the user list (uids) """This method will insert a new channel and if the channel exist it will update the user list (uids)
Args: Args:
newChan (ChannelModel): The channel model object new_channel (MChannel): The channel model object
Returns: Returns:
bool: True if new channel, False if channel exist (However UID could be updated) bool: True if new channel, False if channel exist (However UID could be updated)
@@ -33,17 +33,17 @@ class Channel:
result = False result = False
exist = False exist = False
if not self.Is_Channel(newChan.name): if not self.is_valid_channel(new_channel.name):
self.Logs.error(f"The channel {newChan.name} is not valid, channel must start with #") self.Logs.error(f"The channel {new_channel.name} is not valid, channel must start with #")
return False return False
for record in self.UID_CHANNEL_DB: 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 # If the channel exist, update the user list and do not go further
exist = True exist = True
# self.Logs.debug(f'{record.name} already exist') # self.Logs.debug(f'{record.name} already exist')
for user in newChan.uids: for user in new_channel.uids:
record.uids.append(user) record.uids.append(user)
# Supprimer les doublons # Supprimer les doublons
@@ -54,41 +54,57 @@ class Channel:
if not exist: if not exist:
# If the channel don't exist, then create it # If the channel don't exist, then create it
newChan.name = newChan.name.lower() new_channel.name = new_channel.name.lower()
self.UID_CHANNEL_DB.append(newChan) self.UID_CHANNEL_DB.append(new_channel)
result = True result = True
# self.Logs.debug(f'New Channel Created: ({newChan})') # self.Logs.debug(f'New Channel Created: ({new_channel})')
if not result: 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() self.clean_channel()
return result return result
def delete(self, channel_name: str) -> bool: 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 return False
self.UID_CHANNEL_DB.remove(chanObj) self.UID_CHANNEL_DB.remove(chan_obj)
return True return True
def delete_user_from_channel(self, channel_name: str, uid: str) -> bool: 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: try:
result = False result = False
chan_obj = self.get_channel(channel_name)
chanObj = self.get_Channel(channel_name.lower()) if chan_obj is None:
if chanObj is None:
return result return result
for userid in chanObj.uids: for userid in chan_obj.uids:
if self.Base.clean_uid(userid) == self.Base.clean_uid(uid): if self.Utils.clean_uid(userid) == self.Utils.clean_uid(uid):
chanObj.uids.remove(userid) chan_obj.uids.remove(userid)
result = True result = True
self.clean_channel() self.clean_channel()
@@ -96,16 +112,24 @@ class Channel:
return result return result
except ValueError as ve: except ValueError as ve:
self.Logs.error(f'{ve}') self.Logs.error(f'{ve}')
return False
def delete_user_from_all_channel(self, uid:str) -> bool: 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: try:
result = False result = False
for record in self.UID_CHANNEL_DB: for record in self.UID_CHANNEL_DB:
for user_id in record.uids: 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) record.uids.remove(user_id)
# self.Logs.debug(f'The UID {uid} has been removed, here is the new object: {record}')
result = True result = True
self.clean_channel() self.clean_channel()
@@ -113,108 +137,111 @@ class Channel:
return result return result
except ValueError as ve: except ValueError as ve:
self.Logs.error(f'{ve}') self.Logs.error(f'{ve}')
return False
def add_user_to_a_channel(self, channel_name: str, uid: str) -> bool: 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: try:
result = False chan_obj = self.get_channel(channel_name)
chanObj = self.get_Channel(channel_name)
self.Logs.debug(f"** {__name__}")
if chanObj is None: if chan_obj is None:
result = self.insert(MChannel(channel_name, uids=[uid])) # Create a new channel if the channel don't exist
# self.Logs.debug(f"** {__name__} - result: {result}") self.Logs.debug(f"New channel will be created ({channel_name} - {uid})")
# self.Logs.debug(f'New Channel Created: ({chanObj})') return self.insert(MChannel(channel_name, uids=[uid]))
return result
chanObj.uids.append(uid) chan_obj.uids.append(uid)
del_duplicates = list(set(chanObj.uids)) del_duplicates = list(set(chan_obj.uids))
chanObj.uids = del_duplicates chan_obj.uids = del_duplicates
# self.Logs.debug(f'New Channel Created: ({chanObj})')
return True return True
except Exception as err: except Exception as err:
self.Logs.error(f'{err}') self.Logs.error(f'{err}')
return False
def is_user_present_in_channel(self, channel_name: str, uid: str) -> bool: def is_user_present_in_channel(self, channel_name: str, uid: str) -> bool:
"""Check if a user is present in the channel """Check if a user is present in the channel
Args: Args:
channel_name (str): The channel to check channel_name (str): The channel name to check
uid (str): The UID uid (str): The client UID
Returns: Returns:
bool: True if the user is present in the channel 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: 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: for chan_uid in chan.uids:
if self.Base.clean_uid(chan_uid) == clean_uid: if self.Utils.clean_uid(chan_uid) == clean_uid:
user_found = True return True
break
return user_found return False
def clean_channel(self) -> None: def clean_channel(self) -> None:
"""Remove Channels if empty """If channel doesn't contain any client this method will remove the channel
""" """
try: try:
for record in self.UID_CHANNEL_DB: for record in self.UID_CHANNEL_DB:
if not record.uids: if not record.uids:
self.UID_CHANNEL_DB.remove(record) 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 return None
except Exception as err: except Exception as err:
self.Logs.error(f'{err}') self.Logs.error(f'{err}')
def get_Channel(self, channel_name: str) -> Union['MChannel', None]: def get_channel(self, channel_name: str) -> Optional['MChannel']:
"""Get the channel object
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
Args: 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 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: Returns:
bool: True if the string is a channel / False if this is not a channel bool: True if the string is a channel / False if this is not a channel
""" """
try: try:
if channelToCheck is None: if channel_to_check is None:
return False return False
pattern = fr'^#' pattern = fr'^#'
isChannel = findall(pattern, channelToCheck) is_channel = findall(pattern, channel_to_check)
if not isChannel: if not is_channel:
return False return False
else: else:
return True return True
except TypeError as te: except TypeError as te:
self.Logs.error(f'TypeError: [{channelToCheck}] - {te}') self.Logs.error(f'TypeError: [{channel_to_check}] - {te}')
return False
except Exception as err: except Exception as err:
self.Logs.error(f'Error Not defined: {err}') self.Logs.error(f'Error Not defined: {err}')
return False
def db_query_channel(self, action: Literal['add','del'], module_name: str, channel_name: str) -> bool: def db_query_channel(self, action: Literal['add','del'], module_name: str, channel_name: str) -> bool:
"""You can add a channel or delete a channel. """You can add a channel or delete a channel.
@@ -228,7 +255,7 @@ class Channel:
bool: True if action done bool: True if action done
""" """
try: 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 core_table = self.Base.Config.TABLE_CHANNEL
if not channel_name: if not channel_name:
@@ -240,13 +267,13 @@ class Channel:
case 'add': case 'add':
mes_donnees = {'module_name': module_name, 'channel_name': channel_name} 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) 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: if is_channel_exist is None:
mes_donnees = {'datetime': self.Base.get_datetime(), 'channel_name': channel_name, 'module_name': module_name} 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) 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: if insert.rowcount:
self.Logs.debug(f'New channel added: channel={channel_name} / module_name={module_name}') self.Logs.debug(f'Channel added to DB: channel={channel_name} / module_name={module_name}')
return True return True
else: else:
return False return False
@@ -256,14 +283,12 @@ class Channel:
response = self.Base.db_execute_query(f"DELETE FROM {core_table} WHERE channel_name = :channel_name AND module_name = :module_name", mes_donnes) response = self.Base.db_execute_query(f"DELETE FROM {core_table} WHERE channel_name = :channel_name AND module_name = :module_name", mes_donnes)
if response.rowcount > 0: if response.rowcount > 0:
self.Logs.debug(f'Channel deleted: channel={channel_name} / module: {module_name}') self.Logs.debug(f'Channel deleted from DB: channel={channel_name} / module: {module_name}')
return True return True
else: else:
return False return False
case _:
return False
except Exception as err: except Exception as err:
self.Logs.error(err) self.Logs.error(err)
return False

View File

@@ -1,58 +1,59 @@
from re import sub from re import sub
from typing import Union, TYPE_CHECKING from typing import Any, Optional, Union, TYPE_CHECKING
from dataclasses import asdict
if TYPE_CHECKING: if TYPE_CHECKING:
from core.base import Base from core.loader import Loader
from core.definition import MClient from core.definition import MClient
class Client: class Client:
CLIENT_DB: list['MClient'] = [] CLIENT_DB: list['MClient'] = []
def __init__(self, baseObj: 'Base') -> None: def __init__(self, loader: 'Loader'):
"""
self.Logs = baseObj.logs Args:
self.Base = baseObj loader (Loader): The Loader instance.
"""
self.Logs = loader.Logs
self.Base = loader.Base
return None def insert(self, new_client: 'MClient') -> bool:
def insert(self, newUser: 'MClient') -> bool:
"""Insert a new User object """Insert a new User object
Args: Args:
newUser (UserModel): New userModel object new_client (MClient): New Client object
Returns: Returns:
bool: True if inserted 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 # User already created return False
return False return False
self.CLIENT_DB.append(newUser) self.CLIENT_DB.append(new_client)
return True 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 """Update the nickname starting from the UID
Args: Args:
uid (str): UID of the user uid (str): UID of the user
newNickname (str): New nickname new_nickname (str): New nickname
Returns: Returns:
bool: True if updated 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 return False
userObj.nickname = newNickname user_obj.nickname = new_nickname
return True return True
@@ -67,16 +68,16 @@ class Client:
bool: True if user mode has been updaed bool: True if user mode has been updaed
""" """
response = True 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 return False
action = modes[0] action = modes[0]
new_modes = modes[1:] new_modes = modes[1:]
existing_umodes = userObj.umodes existing_umodes = user_obj.umodes
umodes = userObj.umodes umodes = user_obj.umodes
if action == '+': if action == '+':
@@ -95,7 +96,7 @@ class Client:
final_umodes_liste = [x for x in self.Base.Settings.PROTOCTL_USER_MODES if x in liste_umodes] final_umodes_liste = [x for x in self.Base.Settings.PROTOCTL_USER_MODES if x in liste_umodes]
final_umodes = ''.join(final_umodes_liste) final_umodes = ''.join(final_umodes_liste)
userObj.umodes = f"+{final_umodes}" user_obj.umodes = f"+{final_umodes}"
return response return response
@@ -109,16 +110,16 @@ class Client:
bool: True if deleted 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 return False
self.CLIENT_DB.remove(userObj) self.CLIENT_DB.remove(user_obj)
return True return True
def get_Client(self, uidornickname: str) -> Union['MClient', None]: def get_Client(self, uidornickname: str) -> Optional['MClient']:
"""Get The Client Object model """Get The Client Object model
Args: Args:
@@ -127,16 +128,15 @@ class Client:
Returns: Returns:
UserModel|None: The UserModel Object | None UserModel|None: The UserModel Object | None
""" """
User = None
for record in self.CLIENT_DB: for record in self.CLIENT_DB:
if record.uid == uidornickname: if record.uid == uidornickname:
User = record return record
elif record.nickname == uidornickname: 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 """Get the UID of the user starting from the UID or the Nickname
Args: Args:
@@ -146,12 +146,12 @@ class Client:
str|None: Return the UID 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 None
return userObj.uid return client_obj.uid
def get_nickname(self, uidornickname:str) -> Union[str, None]: def get_nickname(self, uidornickname:str) -> Union[str, None]:
"""Get the Nickname starting from UID or the nickname """Get the Nickname starting from UID or the nickname
@@ -162,14 +162,14 @@ class Client:
Returns: Returns:
str|None: the nickname 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 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 """Transform User Object to a dictionary
Args: Args:
@@ -178,14 +178,14 @@ class Client:
Returns: Returns:
Union[dict[str, any], None]: User Object as a dictionary or None 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 None
return asdict(userObj) return client_obj.to_dict()
def is_exist(self, uidornikname: str) -> bool: def is_exist(self, uidornickname: str) -> bool:
"""Check if the UID or the nickname exist in the USER DB """Check if the UID or the nickname exist in the USER DB
Args: Args:
@@ -194,9 +194,9 @@ class Client:
Returns: Returns:
bool: True if exist bool: True if exist
""" """
userObj = self.get_Client(uidornickname=uidornikname) user_obj = self.get_Client(uidornickname=uidornickname)
if userObj is None: if user_obj is None:
return False return False
return True return True
@@ -235,9 +235,9 @@ class Client:
""" """
pattern = fr'[:|@|%|\+|~|\*]*' pattern = fr'[:|@|%|\+|~|\*]*'
parsed_UID = sub(pattern, '', uid) parsed_uid = sub(pattern, '', uid)
if not parsed_UID: if not parsed_uid:
return None return None
return parsed_UID return parsed_uid

View File

@@ -0,0 +1,109 @@
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'):
"""
Args:
loader (Loader): The Loader instance.
"""
self.Loader = loader
self.Base = loader.Base
self.Logs = loader.Logs
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.lower() and command.module_name.lower() == module_name.lower():
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 drop_command_by_module(self, module_name: str) -> bool:
"""Drop all command by module
Args:
module_name (str): The module name
Returns:
bool: True
"""
tmp_model: list[MCommand] = []
for command in self.DB_COMMANDS:
if command.module_name.lower() == module_name.lower():
tmp_model.append(command)
for c in tmp_model:
self.DB_COMMANDS.remove(c)
self.Logs.debug(f"[COMMAND] Drop command for module {module_name}")
return True
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
def is_client_allowed_to_run_command(self, nickname: str, command_name: str) -> bool:
admin = self.Loader.Admin.get_admin(nickname)
admin_level = admin.level if admin else 0
commands = self.get_commands_by_level(admin_level)
if command_name.lower() in [command.command_name.lower() for command in commands]:
return True
return False
def is_command_exist(self, command_name: str) -> bool:
"""Check if the command name exist
Args:
command_name (str): The command name you want to check
Returns:
bool: True if the command exist
"""
if command_name.lower() in [command.command_name.lower() for command in self.get_ordered_commands()]:
return True
return False

View File

@@ -0,0 +1,60 @@
import sys
import yaml
from json import load
from sys import exit
from os import sep
from typing import Any, Optional, Union, TYPE_CHECKING
from core.definition import MConfig
if TYPE_CHECKING:
from core.loader import Loader
class Configuration:
def __init__(self, loader: 'Loader') -> None:
self.Loader = loader
self.Logs = loader.Logs
self.configuration_model = self.__load_service_configuration()
loader.ServiceLogging.set_file_handler_level(self._config_model.DEBUG_LEVEL)
loader.ServiceLogging.set_stdout_handler_level(self._config_model.DEBUG_LEVEL)
loader.ServiceLogging.update_handler_format(self._config_model.DEBUG_HARD)
return None
@property
def configuration_model(self) -> MConfig:
return self._config_model
@configuration_model.setter
def configuration_model(self, conf_model: MConfig):
self._config_model = conf_model
def __load_config_file(self) -> Optional[dict[str, Any]]:
try:
conf_filename = f'config{sep}configuration.yaml'
with open(conf_filename, 'r') as conf:
configuration: dict[str, dict[str, Any]] = yaml.safe_load(conf)
return configuration.get('configuration', None)
except FileNotFoundError as fe:
self.Logs.error(f'FileNotFound: {fe}')
self.Logs.error('Configuration file not found please create config/configuration.yaml')
exit("Configuration file not found please create config/configuration.yaml")
def __load_service_configuration(self) -> MConfig:
try:
import_config = self.__load_config_file()
if import_config is None:
self.Logs.error("Error While importing configuration file!", exc_info=True)
raise Exception("Error While importing yaml configuration")
list_key_to_remove: list[str] = [key_to_del for key_to_del in import_config if key_to_del not in MConfig().get_attributes()]
for key_to_remove in list_key_to_remove:
import_config.pop(key_to_remove, None)
self.Logs.warning(f"[!] The key {key_to_remove} is not expected, it has been removed from the system ! please remove it from configuration.json file [!]")
self.Logs.debug(f"[LOADING CONFIGURATION]: Loading configuration with {len(import_config)} parameters!")
return MConfig(**import_config)
except TypeError as te:
self.Logs.error(te)

View File

@@ -0,0 +1,131 @@
import importlib
import sys
import time
from typing import TYPE_CHECKING
import socket
if TYPE_CHECKING:
from core.irc import Irc
# Modules impacted by rehashing!
REHASH_MODULES = [
'core.definition',
'core.utils',
'core.classes.modules.config',
'core.base',
'core.classes.modules.commands',
'core.classes.modules.rpc',
'core.classes.interfaces.iprotocol',
'core.classes.interfaces.imodule',
'core.classes.protocols.command_handler',
'core.classes.protocols.factory',
'core.classes.protocols.unreal6',
'core.classes.protocols.inspircd'
]
def restart_service(uplink: 'Irc', reason: str = "Restarting with no reason!") -> None:
"""
Args:
uplink (Irc): The Irc instance
reason (str): The reason of the restart.
"""
# reload modules.
for module in uplink.ModuleUtils.model_get_loaded_modules().copy():
uplink.ModuleUtils.unload_one_module(uplink, module.module_name)
uplink.Base.garbage_collector_thread()
uplink.Logs.debug(f'[{uplink.Config.SERVICE_NICKNAME} RESTART]: Reloading configuration!')
uplink.Protocol.send_squit(server_id=uplink.Config.SERVEUR_ID, server_link=uplink.Config.SERVEUR_LINK, reason=reason)
uplink.Logs.debug('Restarting Defender ...')
uplink.IrcSocket.shutdown(socket.SHUT_RDWR)
uplink.IrcSocket.close()
while uplink.IrcSocket.fileno() != -1:
time.sleep(0.5)
uplink.Logs.warning('-- Waiting for socket to close ...')
# Reload configuration
uplink.Loader.Config = uplink.Loader.ConfModule.Configuration(uplink.Loader).configuration_model
uplink.Loader.Base = uplink.Loader.BaseModule.Base(uplink.Loader)
for mod in REHASH_MODULES:
importlib.reload(sys.modules[mod])
uplink.Protocol = uplink.Loader.PFactory.get()
uplink.Protocol.register_command()
uplink.ModuleUtils.model_clear() # Clear loaded modules.
uplink.User.UID_DB.clear() # Clear User Object
uplink.Channel.UID_CHANNEL_DB.clear() # Clear Channel Object
uplink.Client.CLIENT_DB.clear() # Clear Client object
uplink.init_service_user()
uplink.Utils.create_socket(uplink)
uplink.Protocol.send_link()
uplink.Config.DEFENDER_RESTART = 0
def rehash_service(uplink: 'Irc', nickname: str) -> None:
need_a_restart = ["SERVEUR_ID"]
uplink.Settings.set_cache('db_commands', uplink.Commands.DB_COMMANDS)
uplink.Loader.RpcServer.stop_server()
restart_flag = False
config_model_bakcup = uplink.Config
mods = REHASH_MODULES
for mod in mods:
importlib.reload(sys.modules[mod])
uplink.Protocol.send_priv_msg(
nick_from=uplink.Config.SERVICE_NICKNAME,
msg=f'[REHASH] Module [{mod}] reloaded',
channel=uplink.Config.SERVICE_CHANLOG
)
uplink.Utils = sys.modules['core.utils']
uplink.Config = uplink.Loader.Config = uplink.Loader.ConfModule.Configuration(uplink.Loader).configuration_model
uplink.Config.HSID = config_model_bakcup.HSID
uplink.Config.DEFENDER_INIT = config_model_bakcup.DEFENDER_INIT
uplink.Config.DEFENDER_RESTART = config_model_bakcup.DEFENDER_RESTART
uplink.Config.SSL_VERSION = config_model_bakcup.SSL_VERSION
uplink.Config.CURRENT_VERSION = config_model_bakcup.CURRENT_VERSION
uplink.Config.LATEST_VERSION = config_model_bakcup.LATEST_VERSION
conf_bkp_dict: dict = config_model_bakcup.to_dict()
config_dict: dict = uplink.Config.to_dict()
for key, value in conf_bkp_dict.items():
if config_dict[key] != value and key != 'COLORS':
uplink.Protocol.send_priv_msg(
nick_from=uplink.Config.SERVICE_NICKNAME,
msg=f'[{key}]: {value} ==> {config_dict[key]}',
channel=uplink.Config.SERVICE_CHANLOG
)
if key in need_a_restart:
restart_flag = True
if config_model_bakcup.SERVICE_NICKNAME != uplink.Config.SERVICE_NICKNAME:
uplink.Protocol.send_set_nick(uplink.Config.SERVICE_NICKNAME)
if restart_flag:
uplink.Config.SERVEUR_ID = config_model_bakcup.SERVEUR_ID
uplink.Protocol.send_priv_msg(
nick_from=uplink.Config.SERVICE_NICKNAME,
channel=uplink.Config.SERVICE_CHANLOG,
msg='You need to restart defender !')
# Reload Main Commands Module
uplink.Commands = uplink.Loader.CommandModule.Command(uplink.Loader)
uplink.Loader.RpcServer = uplink.Loader.RpcServerModule.JSONRPCServer(uplink.Loader)
uplink.Loader.RpcServer.start_server()
uplink.Commands.DB_COMMANDS = uplink.Settings.get_cache('db_commands')
uplink.Loader.Base = uplink.Loader.BaseModule.Base(uplink.Loader)
uplink.Protocol = uplink.Loader.PFactory.get()
uplink.Protocol.register_command()
# Reload Service modules
for module in uplink.ModuleUtils.model_get_loaded_modules().copy():
uplink.ModuleUtils.reload_one_module(uplink, module.module_name, nickname)
return None

View File

@@ -1,23 +1,28 @@
from typing import Union from typing import TYPE_CHECKING, Optional
from core.definition import MReputation from core.definition import MReputation
from core.base import Base
if TYPE_CHECKING:
from core.loader import Loader
class Reputation: class Reputation:
UID_REPUTATION_DB: list[MReputation] = [] UID_REPUTATION_DB: list[MReputation] = []
def __init__(self, baseObj: Base) -> None: def __init__(self, loader: 'Loader'):
"""
self.Logs = baseObj.logs Args:
self.MReputation: MReputation = MReputation loader (Loader): The Loader instance.
"""
return None self.Logs = loader.Logs
self.MReputation: Optional[MReputation] = None
def insert(self, newReputationUser: MReputation) -> bool: def insert(self, new_reputation_user: MReputation) -> bool:
"""Insert a new Reputation User object """Insert a new Reputation User object
Args: Args:
newReputationUser (MReputation): New Reputation Model object new_reputation_user (MReputation): New Reputation Model object
Returns: Returns:
bool: True if inserted bool: True if inserted
@@ -26,39 +31,39 @@ class Reputation:
exist = False exist = False
for record in self.UID_REPUTATION_DB: 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 # If the user exist then return False and do not go further
exist = True exist = True
self.Logs.debug(f'{record.uid} already exist') self.Logs.debug(f'{record.uid} already exist')
return result return result
if not exist: if not exist:
self.UID_REPUTATION_DB.append(newReputationUser) self.UID_REPUTATION_DB.append(new_reputation_user)
result = True 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: 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 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 """Update the nickname starting from the UID
Args: Args:
uid (str): UID of the user uid (str): UID of the user
newNickname (str): New nickname new_nickname (str): New nickname
Returns: Returns:
bool: True if updated 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 return False
reputationObj.nickname = newNickname reputation_obj.nickname = new_nickname
return True return True
@@ -89,7 +94,7 @@ class Reputation:
return result return result
def get_Reputation(self, uidornickname: str) -> Union[MReputation, None]: def get_reputation(self, uidornickname: str) -> Optional[MReputation]:
"""Get The User Object model """Get The User Object model
Args: Args:
@@ -98,16 +103,15 @@ class Reputation:
Returns: Returns:
UserModel|None: The UserModel Object | None UserModel|None: The UserModel Object | None
""" """
User = None
for record in self.UID_REPUTATION_DB: for record in self.UID_REPUTATION_DB:
if record.uid == uidornickname: if record.uid == uidornickname:
User = record return record
elif record.nickname == uidornickname: 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 """Get the UID of the user starting from the UID or the Nickname
Args: Args:
@@ -117,14 +121,14 @@ class Reputation:
str|None: Return the UID 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 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 """Get the Nickname starting from UID or the nickname
Args: Args:
@@ -133,12 +137,12 @@ class Reputation:
Returns: Returns:
str|None: the nickname 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 None
return reputationObj.nickname return reputation_obj.nickname
def is_exist(self, uidornickname: str) -> bool: def is_exist(self, uidornickname: str) -> bool:
"""Check if the UID or the nickname exist in the reputation DB """Check if the UID or the nickname exist in the reputation DB
@@ -150,9 +154,9 @@ class Reputation:
bool: True if exist bool: True if exist
""" """
reputationObj = self.get_Reputation(uidornickname) reputation_obj = self.get_reputation(uidornickname)
if reputationObj is None: if isinstance(reputation_obj, MReputation):
return False
else:
return True return True
return False

View File

@@ -0,0 +1,240 @@
import base64
import json
import logging
from enum import Enum
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import TYPE_CHECKING, Any, Optional
from core.classes.modules.rpc.rpc_user import RPCUser
from core.classes.modules.rpc.rpc_channel import RPCChannel
from core.classes.modules.rpc.rpc_command import RPCCommand
if TYPE_CHECKING:
from core.loader import Loader
ProxyLoader: Optional['Loader'] = None
class RPCRequestHandler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
pass
def do_POST(self):
logs = ProxyLoader.Logs
self.server_version = 'Defender6'
self.sys_version = ProxyLoader.Config.CURRENT_VERSION
content_length = int(self.headers['Content-Length'])
body = self.rfile.read(content_length)
request_data: dict = json.loads(body)
rip, rport = self.client_address
if not self.authenticate(request_data):
return None
response_data = {
'jsonrpc': '2.0',
'id': request_data.get('id', 123)
}
method = request_data.get("method")
params: dict[str, Any] = request_data.get("params", {})
response_data['method'] = method
http_code = 200
match method:
case 'user.list':
user = RPCUser(ProxyLoader)
response_data['result'] = user.user_list()
logs.debug(f'[RPC] {method} recieved from {rip}:{rport}')
del user
case 'user.get':
user = RPCUser(ProxyLoader)
uid_or_nickname = params.get('uid_or_nickname', None)
response_data['result'] = user.user_get(uid_or_nickname)
logs.debug(f'[RPC] {method} recieved from {rip}:{rport}')
del user
case 'channel.list':
channel = RPCChannel(ProxyLoader)
response_data['result'] = channel.channel_list()
logs.debug(f'[RPC] {method} recieved from {rip}:{rport}')
del channel
case 'command.list':
command = RPCCommand(ProxyLoader)
response_data['result'] = command.command_list()
logs.debug(f'[RPC] {method} recieved from {rip}:{rport}')
del command
case 'command.get.by.module':
command = RPCCommand(ProxyLoader)
module_name = params.get('name', None)
response_data['result'] = command.command_get_by_module(module_name)
logs.debug(f'[RPC] {method} recieved from {rip}:{rport}')
del command
case 'command.get.by.name':
command = RPCCommand(ProxyLoader)
command_name = params.get('name', None)
response_data['result'] = command.command_get_by_name(command_name)
logs.debug(f'[RPC] {method} recieved from {rip}:{rport}')
del command
case _:
response_data['error'] = create_error_response(JSONRPCErrorCode.METHOD_NOT_FOUND)
logs.debug(f'[RPC ERROR] {method} recieved from {rip}:{rport}')
http_code = 404
self.send_response(http_code)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(response_data).encode('utf-8'))
return None
def do_GET(self):
self.server_version = 'Defender6'
self.sys_version = ProxyLoader.Config.CURRENT_VERSION
content_length = int(self.headers['Content-Length'])
body = self.rfile.read(content_length)
request_data: dict = json.loads(body)
if not self.authenticate(request_data):
return None
response_data = {'jsonrpc': '2.0', 'id': request_data.get('id', 321),
'error': create_error_response(JSONRPCErrorCode.INVALID_REQUEST)}
self.send_response(404)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(response_data).encode('utf-8'))
return None
def authenticate(self, request_data: dict) -> bool:
logs = ProxyLoader.Logs
auth = self.headers.get('Authorization', None)
if auth is None:
self.send_auth_error(request_data)
return False
# Authorization header format: Basic base64(username:password)
auth_type, auth_string = auth.split(' ', 1)
if auth_type.lower() != 'basic':
self.send_auth_error(request_data)
return False
try:
# Decode the base64-encoded username:password
decoded_credentials = base64.b64decode(auth_string).decode('utf-8')
username, password = decoded_credentials.split(":", 1)
# Check the username and password.
for rpcuser in ProxyLoader.Irc.Config.RPC_USERS:
if rpcuser.get('USERNAME', None) == username and rpcuser.get('PASSWORD', None) == password:
return True
self.send_auth_error(request_data)
return False
except Exception as e:
self.send_auth_error(request_data)
logs.error(e)
return False
def send_auth_error(self, request_data: dict) -> None:
response_data = {
'jsonrpc': '2.0',
'id': request_data.get('id', 123),
'error': create_error_response(JSONRPCErrorCode.AUTHENTICATION_ERROR)
}
self.send_response(401)
self.send_header('WWW-Authenticate', 'Basic realm="Authorization Required"')
self.end_headers()
self.wfile.write(json.dumps(response_data).encode('utf-8'))
class JSONRPCServer:
def __init__(self, loader: 'Loader'):
global ProxyLoader
ProxyLoader = loader
self._Loader = loader
self._Base = loader.Base
self._Logs = loader.Logs
self.rpc_server: Optional[HTTPServer] = None
self.connected: bool = False
def start_server(self, server_class=HTTPServer, handler_class=RPCRequestHandler, *, hostname: str = 'localhost', port: int = 5000):
logging.getLogger('http.server').setLevel(logging.CRITICAL)
server_address = (hostname, port)
self.rpc_server = server_class(server_address, handler_class)
self._Logs.debug(f"Server ready on http://{hostname}:{port}...")
self._Base.create_thread(self.thread_start_rpc_server, (), True)
def thread_start_rpc_server(self) -> None:
self._Loader.Irc.Protocol.send_priv_msg(
self._Loader.Config.SERVICE_NICKNAME, "Defender RPC Server has started successfuly!", self._Loader.Config.SERVICE_CHANLOG
)
self.connected = True
self.rpc_server.serve_forever()
ProxyLoader.Logs.debug(f"RPC Server down!")
def stop_server(self):
self._Base.create_thread(self.thread_stop_rpc_server)
def thread_stop_rpc_server(self):
self.rpc_server.shutdown()
ProxyLoader.Logs.debug(f"RPC Server shutdown!")
self.rpc_server.server_close()
ProxyLoader.Logs.debug(f"RPC Server clean-up!")
self._Base.garbage_collector_thread()
self._Loader.Irc.Protocol.send_priv_msg(
self._Loader.Config.SERVICE_NICKNAME, "Defender RPC Server has stopped successfuly!", self._Loader.Config.SERVICE_CHANLOG
)
self.connected = False
class JSONRPCErrorCode(Enum):
PARSE_ERROR = -32700 # Syntax error in the request (malformed JSON)
INVALID_REQUEST = -32600 # Invalid Request (incorrect structure or missing fields)
METHOD_NOT_FOUND = -32601 # Method not found (the requested method does not exist)
INVALID_PARAMS = -32602 # Invalid Params (the parameters provided are incorrect)
INTERNAL_ERROR = -32603 # Internal Error (an internal server error occurred)
# Custom application-specific errors (beyond standard JSON-RPC codes)
CUSTOM_ERROR = 1001 # Custom application-defined error (e.g., user not found)
AUTHENTICATION_ERROR = 1002 # Authentication failure (e.g., invalid credentials)
PERMISSION_ERROR = 1003 # Permission error (e.g., user does not have access to this method)
RESOURCE_NOT_FOUND = 1004 # Resource not found (e.g., the requested resource does not exist)
DUPLICATE_REQUEST = 1005 # Duplicate request (e.g., a similar request has already been processed)
def description(self):
"""Returns a description associated with each error code"""
descriptions = {
JSONRPCErrorCode.PARSE_ERROR: "The JSON request is malformed.",
JSONRPCErrorCode.INVALID_REQUEST: "The request is invalid (missing or incorrect fields).",
JSONRPCErrorCode.METHOD_NOT_FOUND: "The requested method could not be found.",
JSONRPCErrorCode.INVALID_PARAMS: "The parameters provided are invalid.",
JSONRPCErrorCode.INTERNAL_ERROR: "An internal error occurred on the server.",
JSONRPCErrorCode.CUSTOM_ERROR: "A custom error defined by the application.",
JSONRPCErrorCode.AUTHENTICATION_ERROR: "User authentication failed.",
JSONRPCErrorCode.PERMISSION_ERROR: "User does not have permission to access this method.",
JSONRPCErrorCode.RESOURCE_NOT_FOUND: "The requested resource could not be found.",
JSONRPCErrorCode.DUPLICATE_REQUEST: "The request is a duplicate or is already being processed.",
}
return descriptions.get(self, "Unknown error")
def create_error_response(error_code: JSONRPCErrorCode, details: dict = None) -> dict[str, str]:
"""Create a JSON-RPC error!"""
response = {
"code": error_code.value,
"message": error_code.description(),
}
if details:
response["data"] = details
return response

View File

@@ -0,0 +1,12 @@
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from core.loader import Loader
class RPCChannel:
def __init__(self, loader: 'Loader'):
self._Loader = loader
self._Channel = loader.Channel
def channel_list(self) -> list[dict]:
return [chan.to_dict() for chan in self._Channel.UID_CHANNEL_DB]

View File

@@ -0,0 +1,21 @@
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from core.loader import Loader
class RPCCommand:
def __init__(self, loader: 'Loader'):
self._Loader = loader
self._Command = loader.Commands
def command_list(self) -> list[dict]:
return [command.to_dict() for command in self._Command.DB_COMMANDS]
def command_get_by_module(self, module_name: str) -> list[dict]:
return [command.to_dict() for command in self._Command.DB_COMMANDS if command.module_name.lower() == module_name.lower()]
def command_get_by_name(self, command_name: str) -> dict:
for command in self._Command.DB_COMMANDS:
if command.command_name.lower() == command_name.lower():
return command.to_dict()
return {}

View File

@@ -0,0 +1,30 @@
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from core.loader import Loader
from core.definition import MUser
class RPCUser:
def __init__(self, loader: 'Loader'):
self._Loader = loader
self._User = loader.User
def user_list(self) -> list[dict]:
users = self._User.UID_DB.copy()
copy_users: list['MUser'] = []
for user in users:
copy_user = user.copy()
copy_user.connexion_datetime = copy_user.connexion_datetime.strftime('%d-%m-%Y')
copy_users.append(copy_user)
return [user.to_dict() for user in copy_users]
def user_get(self, uidornickname: str) -> Optional[dict]:
user = self._User.get_user(uidornickname)
if user:
user_copy = user.copy()
user_copy.connexion_datetime = user_copy.connexion_datetime.strftime('%d-%m-%Y')
return user_copy.to_dict()
return None

View File

@@ -0,0 +1,75 @@
from typing import Optional, TYPE_CHECKING
if TYPE_CHECKING:
from core.definition import MSasl
from core.loader import Loader
class Sasl:
DB_SASL: list['MSasl'] = []
def __init__(self, loader: 'Loader'):
"""
Args:
loader (Loader): The Loader instance.
"""
self.Logs = loader.Logs # logger
def insert_sasl_client(self, psasl: 'MSasl') -> bool:
"""Insert a new Sasl authentication
Args:
psasl (MSasl): New userModel object
Returns:
bool: True if inserted
"""
if psasl is None:
return False
sasl_obj = self.get_sasl_obj(psasl.client_uid)
if sasl_obj is not None:
# User already created return False
return False
self.DB_SASL.append(psasl)
return True
def delete_sasl_client(self, client_uid: str) -> bool:
"""Delete the User starting from the UID
Args:
client_uid (str): UID of the user
Returns:
bool: True if deleted
"""
sasl_obj = self.get_sasl_obj(client_uid)
if sasl_obj is None:
return False
self.DB_SASL.remove(sasl_obj)
return True
def get_sasl_obj(self, client_uid: str) -> Optional['MSasl']:
"""Get sasl client Object model
Args:
client_uid (str): UID of the client
Returns:
UserModel|None: The SASL Object | None
"""
for record in self.DB_SASL:
if record.client_uid == client_uid:
return record
return None

View File

@@ -0,0 +1,127 @@
"""This class should never be reloaded.
"""
from logging import Logger
from threading import Timer, Thread, RLock
from socket import socket
from typing import Any, Optional, TYPE_CHECKING
from core.definition import MSModule, MAdmin
if TYPE_CHECKING:
from core.classes.modules.user import User
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] = []
RUNNING_SOCKETS: list[socket] = []
PERIODIC_FUNC: dict[str, Any] = {}
LOCK: RLock = RLock()
CONSOLE: bool = False
MAIN_SERVER_HOSTNAME: str = None
MAIN_SERVER_ID: str = None
PROTOCTL_PREFIX_MODES_SIGNES : dict[str, str] = {}
PROTOCTL_PREFIX_SIGNES_MODES : dict[str, str] = {}
PROTOCTL_USER_MODES: list[str] = []
PROTOCTL_CHANNEL_MODES: list[str] = []
PROTOCTL_PREFIX: list[str] = []
SMOD_MODULES: list[MSModule] = []
"""List contains all Server modules"""
__CACHE: dict[str, Any] = {}
"""Use set_cache or get_cache instead"""
__TRANSLATION: dict[str, list[list[str]]] = dict()
"""Translation Varibale"""
__LANG: str = "EN"
__INSTANCE_OF_USER_UTILS: Optional['User'] = None
"""Instance of the User Utils class"""
__CURRENT_ADMIN: Optional['MAdmin'] = None
"""The Current Admin Object Model"""
__LOGGER: Optional[Logger] = None
"""Instance of the logger"""
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, None) is not None:
return self.__CACHE.pop(key)
return None
def get_cache_size(self) -> int:
return len(self.__CACHE)
def clear_cache(self) -> None:
self.__CACHE.clear()
def show_cache(self) -> dict[str, Any]:
return self.__CACHE.copy()
@property
def global_translation(self) -> dict[str, list[list[str]]]:
"""Get/set global translation variable"""
return self.__TRANSLATION
@global_translation.setter
def global_translation(self, translation_var: dict) -> None:
self.__TRANSLATION = translation_var
@property
def global_lang(self) -> str:
"""Global default language."""
return self.__LANG
@global_lang.setter
def global_lang(self, lang: str) -> None:
self.__LANG = lang
@property
def global_user(self) -> 'User':
return self.__INSTANCE_OF_USER_UTILS
@global_user.setter
def global_user(self, user_utils_instance: 'User') -> None:
self.__INSTANCE_OF_USER_UTILS = user_utils_instance
@property
def current_admin(self) -> MAdmin:
"""Current admin data model."""
return self.__CURRENT_ADMIN
@current_admin.setter
def current_admin(self, current_admin: MAdmin) -> None:
self.__CURRENT_ADMIN = current_admin
@property
def global_logger(self) -> Logger:
"""Global logger Instance"""
return self.__LOGGER
@global_logger.setter
def global_logger(self, logger: Logger) -> None:
self.__LOGGER = logger
global_settings = Settings()

View File

@@ -0,0 +1,96 @@
import yaml
import yaml.scanner
from os import sep
from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.loader import Loader
class Translation:
def __init__(self, loader: 'Loader') -> None:
"""
Args:
loader (Loader): The Loader instance.
"""
self.Logs = loader.Logs
self.Settings = loader.Settings
def get_translation(self) -> dict[str, list[list[str]]]:
try:
translation: dict[str, list[list[str]]] = dict()
sfs: dict[str, list[list[str]]] = {}
module_translation_directory = Path("mods")
core_translation_directory = Path("core")
sfs_core = self.get_subfolders_name(core_translation_directory.__str__())
sfs_module = self.get_subfolders_name(module_translation_directory.__str__())
# Combine the 2 dict
for d in (sfs_core, sfs_module):
for k, v in d.items():
sfs.setdefault(k, []).extend(v)
loaded_files: list[str] = []
for module, filenames in sfs.items():
translation[module] = []
for filename in filenames:
with open(f"{filename}", "r", encoding="utf-8") as fyaml:
data: dict[str, list[dict[str, str]]] = yaml.safe_load(fyaml)
if not isinstance(data, dict):
continue
for key, list_trad in data.items():
for vlist in list_trad:
translation[module].append([vlist["orig"], vlist["trad"]])
loaded_files.append(f"{filename}")
return translation
except yaml.scanner.ScannerError as se:
self.Logs.error(f"[!] {se} [!]")
return {}
except yaml.YAMLError as ye:
if hasattr(ye, 'problem_mark'):
mark = ye.problem_mark
self.Logs.error(f"Error YAML: {ye.with_traceback(None)}")
self.Logs.error("Error position: (%s:%s)" % (mark.line+1, mark.column+1))
return {}
except yaml.error.MarkedYAMLError as me:
self.Logs.error(f"[!] {me} [!]")
return {}
except Exception as err:
self.Logs.error(f'General Error: {err}', exc_info=True)
return {}
finally:
self.Logs.debug("Translation files loaded")
for f in loaded_files:
self.Logs.debug(f" - {f}")
def get_subfolders_name(self, directory: str) -> dict[str, list[str]]:
try:
translation_information: dict[str, list[str]] = dict()
main_directory = Path(directory)
# Init the dictionnary
for subfolder in main_directory.rglob(f'*language{sep}*{sep}*.yaml'):
if subfolder.name != '__pycache__':
translation_information[subfolder.parent.name.lower()] = []
for subfolder in main_directory.rglob(f'*language{sep}*{sep}*.yaml'):
if subfolder.name != '__pycache__':
translation_information[subfolder.parent.name.lower()].append(subfolder)
return translation_information
except Exception as err:
self.Logs.error(f'General Error: {err}')
return {}

View File

@@ -1,23 +1,26 @@
from re import sub from re import sub
from typing import Union, TYPE_CHECKING from typing import Any, Optional, TYPE_CHECKING
from dataclasses import asdict from datetime import datetime
if TYPE_CHECKING: if TYPE_CHECKING:
from core.base import Base from core.loader import Loader
from core.definition import MUser from core.definition import MUser
class User: class User:
UID_DB: list['MUser'] = [] UID_DB: list['MUser'] = []
def __init__(self, baseObj: 'Base') -> None: @property
def get_current_user(self) -> 'MUser':
return self.current_user
self.Logs = baseObj.logs def __init__(self, loader: 'Loader'):
self.Base = baseObj
return None self.Logs = loader.Logs
self.Base = loader.Base
self.current_user: Optional['MUser'] = None
def insert(self, newUser: 'MUser') -> bool: def insert(self, new_user: 'MUser') -> bool:
"""Insert a new User object """Insert a new User object
Args: Args:
@@ -27,33 +30,32 @@ class User:
bool: True if inserted bool: True if inserted
""" """
userObj = self.get_User(newUser.uid) user_obj = self.get_user(new_user.uid)
if not user_obj is None:
if not userObj is None:
# User already created return False # User already created return False
return False return False
self.UID_DB.append(newUser) self.UID_DB.append(new_user)
return True 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 """Update the nickname starting from the UID
Args: Args:
uid (str): UID of the user uid (str): UID of the user
newNickname (str): New nickname new_nickname (str): New nickname
Returns: Returns:
bool: True if updated 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 return False
userObj.nickname = newNickname user_obj.nickname = new_nickname
self.Logs.debug(f"UID ({uid}) has benn update with new nickname ({new_nickname}).")
return True return True
def update_mode(self, uidornickname: str, modes: str) -> bool: def update_mode(self, uidornickname: str, modes: str) -> bool:
@@ -67,16 +69,16 @@ class User:
bool: True if user mode has been updaed bool: True if user mode has been updaed
""" """
response = True 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 return False
action = modes[0] action = modes[0]
new_modes = modes[1:] new_modes = modes[1:]
existing_umodes = userObj.umodes existing_umodes = user_obj.umodes
umodes = userObj.umodes umodes = user_obj.umodes
if action == '+': if action == '+':
@@ -95,7 +97,7 @@ class User:
final_umodes_liste = [x for x in self.Base.Settings.PROTOCTL_USER_MODES if x in liste_umodes] final_umodes_liste = [x for x in self.Base.Settings.PROTOCTL_USER_MODES if x in liste_umodes]
final_umodes = ''.join(final_umodes_liste) final_umodes = ''.join(final_umodes_liste)
userObj.umodes = f"+{final_umodes}" user_obj.umodes = f"+{final_umodes}"
return response return response
@@ -109,16 +111,16 @@ class User:
bool: True if deleted 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 return False
self.UID_DB.remove(userObj) self.UID_DB.remove(user_obj)
return True return True
def get_User(self, uidornickname: str) -> Union['MUser', None]: def get_user(self, uidornickname: str) -> Optional['MUser']:
"""Get The User Object model """Get The User Object model
Args: Args:
@@ -127,16 +129,17 @@ class User:
Returns: Returns:
UserModel|None: The UserModel Object | None UserModel|None: The UserModel Object | None
""" """
User = None
for record in self.UID_DB: for record in self.UID_DB:
if record.uid == uidornickname: if record.uid == uidornickname:
User = record self.current_user = record
return record
elif record.nickname == uidornickname: elif record.nickname == uidornickname:
User = record self.current_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 """Get the UID of the user starting from the UID or the Nickname
Args: Args:
@@ -146,14 +149,15 @@ class User:
str|None: Return the UID 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 None
return userObj.uid self.current_user = user_obj
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 """Get the Nickname starting from UID or the nickname
Args: Args:
@@ -162,14 +166,15 @@ class User:
Returns: Returns:
str|None: the nickname 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 None
return userObj.nickname self.current_user = user_obj
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 """Transform User Object to a dictionary
Args: Args:
@@ -178,12 +183,12 @@ class User:
Returns: Returns:
Union[dict[str, any], None]: User Object as a dictionary or None 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 None
return asdict(userObj) return user_obj.to_dict()
def is_exist(self, uidornikname: str) -> bool: def is_exist(self, uidornikname: str) -> bool:
"""Check if the UID or the nickname exist in the USER DB """Check if the UID or the nickname exist in the USER DB
@@ -194,14 +199,14 @@ class User:
Returns: Returns:
bool: True if exist 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 False
return True return True
def clean_uid(self, uid: str) -> Union[str, None]: def clean_uid(self, uid: str) -> Optional[str]:
"""Clean UID by removing @ / % / + / ~ / * / : """Clean UID by removing @ / % / + / ~ / * / :
Args: Args:
@@ -218,3 +223,34 @@ class User:
return None return None
return parsed_UID 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

View File

@@ -1,19 +0,0 @@
from typing import Literal, TYPE_CHECKING
from .protocols.unreal6 import Unrealircd6
from .protocols.inspircd import Inspircd
if TYPE_CHECKING:
from core.irc import Irc
class Protocol:
def __init__(self, protocol: Literal['unreal6','inspircd'], ircInstance: 'Irc'):
self.Protocol = None
match protocol:
case 'unreal6':
self.Protocol: Unrealircd6 = Unrealircd6(ircInstance)
case 'inspircd':
self.Protocol: Inspircd = Inspircd(ircInstance)
case _:
self.Protocol: Unrealircd6 = Unrealircd6(ircInstance)

View File

@@ -0,0 +1,54 @@
from typing import Optional, TYPE_CHECKING
if TYPE_CHECKING:
from core.definition import MIrcdCommand
from core.loader import Loader
class CommandHandler:
DB_IRCDCOMMS: list['MIrcdCommand'] = []
DB_SUBSCRIBE: list = []
def __init__(self, loader: 'Loader'):
"""Init method
Args:
loader (Loader): The loader Object
"""
self.__Logs = loader.Logs
def register(self, ircd_command_model: 'MIrcdCommand') -> None:
"""Register a new command in the Handler
Args:
ircd_command_model (MIrcdCommand): The IRCD Command Object
"""
ircd_command = self.get_registred_ircd_command(ircd_command_model.command_name)
if ircd_command is None:
self.__Logs.debug(f'[IRCD COMMAND HANDLER] New IRCD command ({ircd_command_model.command_name}) added to the handler.')
self.DB_IRCDCOMMS.append(ircd_command_model)
return None
else:
self.__Logs.debug(f'[IRCD COMMAND HANDLER] This IRCD command ({ircd_command.command_name}) already exist in the handler.')
return None
def get_registred_ircd_command(self, command_name: str) -> Optional['MIrcdCommand']:
"""Get the registred IRCD command model
Returns:
MIrcdCommand: The IRCD Command object
"""
com = command_name.upper()
for ircd_com in self.DB_IRCDCOMMS:
if com == ircd_com.command_name.upper():
return ircd_com
return None
def get_ircd_commands(self) -> list['MIrcdCommand']:
"""Get the list of IRCD Commands
Returns:
list[MIrcdCommand]: a list of all registred commands
"""
return self.DB_IRCDCOMMS.copy()

View File

@@ -0,0 +1,33 @@
from typing import TYPE_CHECKING, Optional
from .unreal6 import Unrealircd6
from .inspircd import Inspircd
from ..interfaces.iprotocol import IProtocol
if TYPE_CHECKING:
from core.irc import Irc
class ProtocolFactorty:
def __init__(self, uplink: 'Irc'):
"""ProtocolFactory init.
Args:
uplink (Irc): The Irc object
"""
self.__Config = uplink.Config
self.__uplink = uplink
def get(self) -> Optional[IProtocol]:
protocol = self.__Config.SERVEUR_PROTOCOL
match protocol:
case 'unreal6':
self.__uplink.Logs.debug(f"[PROTOCOL] {protocol} has been loaded")
return Unrealircd6(self.__uplink)
case 'inspircd':
self.__uplink.Logs.debug(f"[PROTOCOL] {protocol} has been loaded")
return Inspircd(self.__uplink)
case _:
self.__uplink.Logs.critical(f"[PROTOCOL ERROR] This protocol name ({protocol} is not valid!)")
raise Exception("Unknown protocol!")

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +0,0 @@
from threading import Timer, Thread, RLock
from socket import socket
class Settings:
RUNNING_TIMERS: list[Timer] = []
RUNNING_THREADS: list[Thread] = []
RUNNING_SOCKETS: list[socket] = []
PERIODIC_FUNC: dict[object] = {}
LOCK: RLock = RLock()
CONSOLE: bool = False
PROTOCTL_USER_MODES: list[str] = []
PROTOCTL_PREFIX: list[str] = []

View File

@@ -1,10 +1,30 @@
from datetime import datetime from datetime import datetime
from dataclasses import dataclass, field from json import dumps
from typing import Literal from dataclasses import dataclass, field, asdict, fields, replace
from typing import Literal, Any, Optional
from os import sep from os import sep
@dataclass @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 copy(self):
"""Return the object of a dataclass a json str."""
return replace(self)
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""" """Model Client for registred nickname"""
uid: str = None uid: str = None
account: str = None account: str = None
@@ -14,6 +34,8 @@ class MClient:
hostname: str = None hostname: str = None
umodes: str = None umodes: str = None
vhost: str = None vhost: str = None
fingerprint: str = None
tls_cipher: str = None
isWebirc: bool = False isWebirc: bool = False
isWebsocket: bool = False isWebsocket: bool = False
remote_ip: str = None remote_ip: str = None
@@ -22,7 +44,7 @@ class MClient:
connexion_datetime: datetime = field(default=datetime.now()) connexion_datetime: datetime = field(default=datetime.now())
@dataclass @dataclass
class MUser: class MUser(MainModel):
"""Model User""" """Model User"""
uid: str = None uid: str = None
@@ -32,6 +54,8 @@ class MUser:
hostname: str = None hostname: str = None
umodes: str = None umodes: str = None
vhost: str = None vhost: str = None
fingerprint: str = None
tls_cipher: str = None
isWebirc: bool = False isWebirc: bool = False
isWebsocket: bool = False isWebsocket: bool = False
remote_ip: str = None remote_ip: str = None
@@ -40,26 +64,30 @@ class MUser:
connexion_datetime: datetime = field(default=datetime.now()) connexion_datetime: datetime = field(default=datetime.now())
@dataclass @dataclass
class MAdmin: class MAdmin(MainModel):
"""Model Admin""" """Model Admin"""
uid: str = None uid: str = None
account: str = None
nickname: str = None nickname: str = None
username: str = None username: str = None
realname: str = None realname: str = None
hostname: str = None hostname: str = None
umodes: str = None umodes: str = None
vhost: str = None vhost: str = None
fingerprint: str = None
tls_cipher: str = None
isWebirc: bool = False isWebirc: bool = False
isWebsocket: bool = False isWebsocket: bool = False
remote_ip: str = None remote_ip: str = None
score_connexion: int = 0 score_connexion: int = 0
geoip: str = None geoip: str = None
connexion_datetime: datetime = field(default=datetime.now()) connexion_datetime: datetime = field(default=datetime.now())
language: str = "EN"
level: int = 0 level: int = 0
@dataclass @dataclass
class MReputation: class MReputation(MainModel):
"""Model Reputation""" """Model Reputation"""
uid: str = None uid: str = None
nickname: str = None nickname: str = None
@@ -68,6 +96,8 @@ class MReputation:
hostname: str = None hostname: str = None
umodes: str = None umodes: str = None
vhost: str = None vhost: str = None
fingerprint: str = None
tls_cipher: str = None
isWebirc: bool = False isWebirc: bool = False
isWebsocket: bool = False isWebsocket: bool = False
remote_ip: str = None remote_ip: str = None
@@ -77,7 +107,7 @@ class MReputation:
secret_code: str = None secret_code: str = None
@dataclass @dataclass
class MChannel: class MChannel(MainModel):
"""Model Channel""" """Model Channel"""
name: str = None name: str = None
@@ -92,7 +122,7 @@ class MChannel:
""" """
@dataclass @dataclass
class ColorModel: class ColorModel(MainModel):
white: str = "\x0300" white: str = "\x0300"
black: str = "\x0301" black: str = "\x0301"
blue: str = "\x0302" blue: str = "\x0302"
@@ -104,7 +134,7 @@ class ColorModel:
underline: str = "\x1F" underline: str = "\x1F"
@dataclass @dataclass
class MConfig: class MConfig(MainModel):
"""Model Configuration""" """Model Configuration"""
SERVEUR_IP: str = "127.0.0.1" SERVEUR_IP: str = "127.0.0.1"
@@ -170,12 +200,18 @@ class MConfig:
SERVICE_ID: str = field(init=False) SERVICE_ID: str = field(init=False)
"""The service unique ID""" """The service unique ID"""
LANG: str = "EN"
"""The default language of Defender. default: EN"""
OWNER: str = "admin" OWNER: str = "admin"
"""The nickname of the admin of the service""" """The nickname of the admin of the service"""
PASSWORD: str = "password" PASSWORD: str = "password"
"""The password of the admin of the service""" """The password of the admin of the service"""
RPC_USERS: list[dict] = field(default_factory=list)
"""The Defender rpc users"""
JSONRPC_URL: str = None JSONRPC_URL: str = None
"""The RPC url, if local https://127.0.0.1:PORT/api should be fine""" """The RPC url, if local https://127.0.0.1:PORT/api should be fine"""
@@ -230,6 +266,9 @@ class MConfig:
DEBUG_LEVEL:Literal[10, 20, 30, 40, 50] = 20 DEBUG_LEVEL:Literal[10, 20, 30, 40, 50] = 20
"""Logs level: DEBUG 10 | INFO 20 | WARNING 30 | ERROR 40 | CRITICAL 50. (default: 20)""" """Logs level: DEBUG 10 | INFO 20 | WARNING 30 | ERROR 40 | CRITICAL 50. (default: 20)"""
DEBUG_HARD: bool = False
"""Adding filename, function name and the line number to the logs. Default False"""
LOGGING_NAME: str = "defender" LOGGING_NAME: str = "defender"
"""The name of the Logging instance""" """The name of the Logging instance"""
@@ -305,16 +344,54 @@ class MConfig:
"""0: utf-8 | 1: iso-8859-1""" """0: utf-8 | 1: iso-8859-1"""
@dataclass @dataclass
class MClone: class MCommand(MainModel):
"""Model Clone""" module_name: str = None
connected: bool = False command_name: str = None
uid: str = None description: str = None
nickname: str = None command_level: int = 0
username: str = None
realname: str = None @dataclass
channels: list = field(default_factory=list) class MModule(MainModel):
vhost: str = None module_name: str = None
hostname: str = 'localhost' class_name: str = None
umodes: str = None class_instance: Optional[Any] = None
remote_ip: str = '127.0.0.1'
group: str = 'Default' @dataclass
class DefenderModuleHeader(MainModel):
name: str = ''
version: str = ''
description: str = ''
author: str = ''
core_version: str = ''
@dataclass
class MSModule:
"""Server Modules model"""
name: str = None
version: str = None
type: str = None
@dataclass
class MSasl(MainModel):
"""Sasl model"""
remote_ip: Optional[str] = None
mechanisme: Optional[str] = None
message_type: Optional[str] = None
client_uid: Optional[str] = None
username: Optional[str] = None
password: Optional[str] = None
fingerprint: Optional[str] = None
language: str = "EN"
auth_success: bool = False
level: int = 0
@dataclass
class MRegister:
command_name: str
func: Any
@dataclass
class MIrcdCommand:
command_name: str
func: Any

150
core/install.py Normal file
View File

@@ -0,0 +1,150 @@
import argparse
import os
import sys
import json
from dataclasses import dataclass
from subprocess import check_call, CalledProcessError, check_output
from pathlib import Path
from platform import python_version_tuple
import traceback
parser = argparse.ArgumentParser(description="Python Installation Code")
parser.add_argument('--check-version', action='store_true', help='Check if the python version is ok!')
parser.add_argument('--install', action='store_true', help='Run the installation')
parser.add_argument('--git-update', action='store_true', help='Update from git (main repository)')
args = parser.parse_args()
PYTHON_REQUIRED_VERSION = (3, 10, 0)
PYTHON_SYSTEM_VERSION = tuple(map(int, python_version_tuple()))
ROOT_PATH = os.getcwd()
PYENV = Path(ROOT_PATH).joinpath('.pyenv/bin/python') if os.name != 'nt' else Path(ROOT_PATH).joinpath('.pyenv/Scripts/python.exe')
PIPENV = Path(f'{ROOT_PATH}/.pyenv/bin/pip') if os.name != 'nt' else Path(f'{ROOT_PATH}/.pyenv/Scripts/pip.exe')
USER_HOME_DIRECTORY = Path.home()
SYSTEMD_PATH = Path(USER_HOME_DIRECTORY).joinpath('.config', 'systemd', 'user')
PY_EXEC = 'defender.py'
SERVICE_FILE_NAME = 'defender.service'
@dataclass
class Package:
name: str = None
version: str = None
def __load_required_package_versions() -> list[Package]:
"""This will create Package model with package names and required version
"""
try:
DB_PACKAGES: list[Package] = []
version_filename = Path(ROOT_PATH).joinpath('version.json') # f'.{os.sep}version.json'
with open(version_filename, 'r') as version_data:
package_info:dict[str, str] = json.load(version_data)
for name, version in package_info.items():
if name == 'version':
continue
DB_PACKAGES.append(
Package(name=name, version=version)
)
return DB_PACKAGES
except FileNotFoundError as fe:
print(f"File not found: {fe}")
except Exception as err:
print(f"General Error: {err}")
def update_packages() -> None:
try:
newVersion = False
db_packages = __load_required_package_versions()
print(ROOT_PATH)
if sys.prefix not in PYENV.__str__():
print(f"You are probably running a new installation or you are not using your virtual env {PYENV}")
return newVersion
print(f"> Checking for dependencies versions ==> WAIT")
for package in db_packages:
newVersion = False
_required_version = package.version
_installed_version: str = None
output = check_output([PIPENV, 'show', package.name])
for line in output.decode().splitlines():
if line.startswith('Version:'):
_installed_version = line.split(':')[1].strip()
break
required_version = tuple(map(int, _required_version.split('.')))
installed_version = tuple(map(int, _installed_version.split('.')))
if required_version > installed_version:
print(f'> New version of {package.name} is available {installed_version} ==> {required_version}')
newVersion = True
if newVersion:
check_call([PIPENV, 'install', '--upgrade', package.name])
print(f"> Dependencies versions ==> OK")
return newVersion
except CalledProcessError:
print(f"[!] Package {package.name} not installed [!]")
except Exception as err:
print(f"UpdatePackage Error: {err}")
traceback.print_exc()
def run_git_update() -> None:
check_call(['git', 'pull', 'origin', 'main'])
def check_python_requirement():
if PYTHON_SYSTEM_VERSION < PYTHON_REQUIRED_VERSION:
raise RuntimeError(f"Your Python Version is not meeting the requirement, System Version: {PYTHON_SYSTEM_VERSION} < Required Version {PYTHON_REQUIRED_VERSION}")
def create_service_file():
pyenv = PYENV
systemd_path = SYSTEMD_PATH
py_exec = PY_EXEC
service_file_name = SERVICE_FILE_NAME
if not Path(systemd_path).exists():
print("[!] Folder not available")
sys.exit(1)
contain = f'''[Unit]
Description=Defender IRC Service
[Service]
ExecStart={pyenv} {py_exec}
WorkingDirectory={ROOT_PATH}
SyslogIdentifier=Defender
Restart=on-failure
[Install]
WantedBy=default.target
'''
with open(Path(systemd_path).joinpath(service_file_name), "w") as file:
file.write(contain)
print('Service file generated with current configuration')
print('Running IRC Service ...')
print(f"#"*24)
print("Installation complete ...")
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 {ROOT_PATH}{os.sep}logs{os.sep}defender.log")
print(f"#"*24)
def main():
if args.check_version:
check_python_requirement()
sys.exit(0)
if args.install:
create_service_file()
sys.exit(0)
if args.git_update:
run_git_update()
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -1,331 +0,0 @@
import os
import json
from sys import exit, prefix
from dataclasses import dataclass
from subprocess import check_call, run, CalledProcessError, PIPE, check_output
from platform import python_version, python_version_tuple
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]
python_current_version: str
defender_install_folder: str
venv_folder: str
venv_cmd_installation: list
venv_cmd_requirements: list
venv_pip_executable: str
venv_python_executable: str
@dataclass
class Package:
name: str = None
version: str = None
DB_PACKAGES: list[Package] = []
def __init__(self) -> None:
self.set_configuration()
if self.skip_install:
self.install_dependencies()
self.check_packages_version()
return None
self.check_packages_version()
# Sinon tester les dependances python et les installer avec pip
if self.do_install():
self.install_dependencies()
self.create_service_file()
self.print_final_message()
return None
def set_configuration(self):
self.skip_install = False
defender_install_folder = os.getcwd()
venv_folder = '.pyenv'
unix_user_home_directory = os.path.expanduser("~")
unix_systemd_folder = os.path.join(unix_user_home_directory, '.config', 'systemd', 'user')
defender_main_executable = os.path.join(defender_install_folder, 'defender.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(),
python_current_version=python_version(),
defender_install_folder=defender_install_folder,
venv_folder=venv_folder,
venv_cmd_installation=['python3', '-m', 'venv', venv_folder],
venv_cmd_requirements=['sqlalchemy','psutil','requests','faker','unrealircd_rpc_py'],
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'
)
if not self.check_python_version():
# If the Python version is not good then Exit
exit("/!\\ Python version error /!\\")
if not os.path.exists(os.path.join(self.config.defender_install_folder, 'config', 'configuration.json')):
# If configuration file do not exist
exit("/!\\ Configuration file (core/configuration.json) doesn't exist! please create it /!\\")
# Exclude Windows OS from the installation
if os.name == 'nt':
# If windows, modify pip and python virtual environment executable
self.config.venv_pip_executable = f'{os.path.join(defender_install_folder, venv_folder, "Scripts")}{os.sep}pip.exe'
self.config.venv_python_executable = f'{os.path.join(defender_install_folder, venv_folder, "Scripts")}{os.sep}python.exe'
self.skip_install = True
return False
if self.is_root():
exit(f'/!\\ I highly not recommend running Defender as root /!\\')
self.skip_install = True
return False
def is_root(self) -> bool:
if os.geteuid() != 0:
print('> User without privileges ==> OK')
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:
full_service_file_path = os.path.join(self.config.unix_systemd_folder, self.config.service_file_name)
if not os.path.exists(full_service_file_path):
print(f'/!\\ Service file does not exist /!\\')
return True
# Check if virtual env exist
if not os.path.exists(f'{os.path.join(self.config.defender_install_folder, self.config.venv_folder)}'):
self.run_subprocess(self.config.venv_cmd_installation)
print(f'/!\\ Virtual env does not exist run the install /!\\')
return True
def run_subprocess(self, command:list) -> None:
print(f'> {command}')
try:
check_call(command)
print("The command completed successfully.")
except CalledProcessError as e:
print(f"The command failed with the return code: {e.returncode}")
print(f"Try to install dependencies ...")
exit(5)
def get_packages_version_from_json(self) -> None:
"""This will create Package model with package names and required version
"""
try:
version_filename = f'.{os.sep}version.json'
with open(version_filename, 'r') as version_data:
package_info:dict[str, str] = json.load(version_data)
for name, version in package_info.items():
if name == 'version':
continue
self.DB_PACKAGES.append(
self.Package(name=name, version=version)
)
return None
except FileNotFoundError as fe:
print(f"File not found: {fe}")
except Exception as err:
print(f"General Error: {err}")
def check_packages_version(self) -> bool:
try:
newVersion = False
self.get_packages_version_from_json()
if not self.config.venv_folder in prefix:
print(f"You are probably running a new installation or you are not using your virtual env {self.config.venv_folder}")
return newVersion
print(f"> Checking for dependencies versions ==> WAIT")
for package in self.DB_PACKAGES:
newVersion = False
required_version = package.version
installed_version = None
output = check_output([self.config.venv_pip_executable, 'show', package.name])
for line in output.decode().splitlines():
if line.startswith('Version:'):
installed_version = line.split(':')[1].strip()
break
required_major, required_minor, required_patch = required_version.split('.')
installed_major, installed_minor, installed_patch = installed_version.split('.')
if required_major > installed_major:
print(f'> New version of {package.name} is available {installed_version} ==> {required_version}')
newVersion = True
elif required_major == installed_major and required_minor > installed_minor:
print(f'> New version of {package.name} is available {installed_version} ==> {required_version}')
newVersion = True
elif required_major == installed_major and required_minor == installed_minor and required_patch > installed_patch:
print(f'> New version of {package.name} is available {installed_version} ==> {required_version}')
newVersion = True
if newVersion:
self.run_subprocess([self.config.venv_pip_executable, 'install', '--upgrade', package.name])
print(f"> Dependencies versions ==> OK")
return newVersion
except CalledProcessError:
print(f"/!\\ Package {package.name} not installed /!\\")
except Exception as err:
print(f"General Error: {err}")
def check_python_version(self) -> bool:
"""Test si la version de python est autorisée ou non
Returns:
bool: True si la version de python est autorisé sinon False
"""
# Current system version
sys_major, sys_minor, sys_patch = self.config.python_current_version_tuple
# min python version required
python_required_version = self.config.python_min_version.split('.')
min_major, min_minor = tuple((python_required_version[0], python_required_version[1]))
if int(sys_major) < int(min_major):
print(f"## Your python version must be greather than or equal to {self.config.python_min_version} ##")
return False
elif (int(sys_major) <= int(min_major)) and (int(sys_minor) < int(min_minor)):
print(f"## Your python version must be greather than or equal to {self.config.python_min_version} ##")
return False
print(f"> Version of python : {self.config.python_current_version} ==> OK")
return True
def check_package(self, package_name) -> bool:
try:
# 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 as cpe:
print(cpe)
return False
def install_dependencies(self) -> None:
"""### Verifie les dépendances si elles sont installées
- Test si les modules sont installés
- Met a jour pip
- Install les modules manquants
"""
do_install = False
# Check if virtual env exist
if not os.path.exists(f'{os.path.join(self.config.defender_install_folder, self.config.venv_folder)}'):
self.run_subprocess(self.config.venv_cmd_installation)
do_install = True
for module in self.config.venv_cmd_requirements:
if not self.check_package(module):
do_install = True
if not do_install:
return None
print("===> Vider le cache de pip")
self.run_subprocess([self.config.venv_pip_executable, 'cache', 'purge'])
print("===> Verifier si pip est a jour")
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é')
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é")
else:
print(f"==> {module} already installed")
def create_service_file(self) -> None:
full_service_file_path = os.path.join(self.config.unix_systemd_folder, self.config.service_file_name)
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]
Description=Defender IRC Service
[Service]
ExecStart={self.config.venv_python_executable} {self.config.defender_main_executable}
WorkingDirectory={self.config.defender_install_folder}
SyslogIdentifier=Defender
Restart=on-failure
[Install]
WantedBy=default.target
'''
# Check if user systemd is available (.config/systemd/user/)
if not os.path.exists(self.config.unix_systemd_folder):
self.run_subprocess(['mkdir', '-p', self.config.unix_systemd_folder])
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 ...')
self.run_subprocess(self.config.service_cmd_daemon_reload)
self.run_subprocess(self.config.service_cmd_executable)
else:
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 ...')
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("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)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
traduction:
# Message help
- orig: "Access denied!"
trad: "Accès refusé."
- orig: "Wrong password!"
trad: "Mot de passe incorrect!"
- orig: "%s - %sLoaded%s by %s on %s"
trad: "%s - %sChargé%s par %s le %s"
- orig: "Module %s loaded!"
trad: "Module %s chargé!"
- orig: "cmd method is not available in the module (%s)"
trad: "La méthode cmd n'est pas disponible dans le module (%s)"
- orig: "[%sMODULE ERROR%s] Module %s is facing issues! %s"
trad: "[%sMODULE ERREUR%s] Le module %s a rencontré une erreur! %s"
- orig: "%s - %sNot Loaded%s"
trad: "%s - %sNon chargé%s"
- orig: "Successfuly connected to %s"
trad: "Connecter a %s avec succés"
- orig: "[ %sINFORMATION%s ] >> %s is ready!"
trad: "[ %sINFORMATION%s ] >> %s est prêt!"

View File

@@ -1,34 +1,79 @@
from core.classes import user, admin, client, channel, clone, reputation, settings from logging import Logger
from core.classes.modules.settings import global_settings
from core.classes.modules import translation, user, admin, client, channel, reputation, settings, sasl
import core.logs as logs
import core.definition as df import core.definition as df
import core.base as baseModule import core.utils as utils
import core.classes.config as confModule import core.base as base_mod
import core.module as module_mod
import core.classes.modules.commands as commands_mod
import core.classes.modules.config as conf_mod
import core.classes.modules.rpc.rpc as rpc_mod
import core.irc as irc
import core.classes.protocols.factory as factory
class Loader: class Loader:
def __init__(self): def __init__(self):
# Load Modules # Load Main Modules
self.Definition: df = df self.Definition: df = df
self.ConfModule: confModule = confModule self.ConfModule: conf_mod = conf_mod
self.BaseModule: baseModule = baseModule self.BaseModule: base_mod = base_mod
self.CommandModule: commands_mod = commands_mod
self.LoggingModule: logs = logs
self.RpcServerModule: rpc_mod = rpc_mod
self.Utils: utils = utils
# Load Classes # Load Classes
self.Settings: settings = settings.Settings() self.Settings: settings.Settings = global_settings
self.Config: df.MConfig = self.ConfModule.Configuration().ConfigObject self.ServiceLogging: logs.ServiceLogging = self.LoggingModule.ServiceLogging()
self.Base: baseModule.Base = self.BaseModule.Base(self.Config, self.Settings) self.Logs: Logger = self.ServiceLogging.get_logger()
self.User: user.User = user.User(self.Base) self.Config: df.MConfig = self.ConfModule.Configuration(self).configuration_model
self.Client: client.Client = client.Client(self.Base) self.Settings.global_lang = self.Config.LANG if self.Config.LANG else "EN"
self.Admin: admin.Admin = admin.Admin(self.Base) self.Settings.global_logger = self.Logs
self.Channel: channel.Channel = channel.Channel(self.Base) self.Translation: translation.Translation = translation.Translation(self)
self.Clone: clone.Clone = clone.Clone(self.Base) self.Settings.global_translation = self.Translation.get_translation()
self.Reputation: reputation.Reputation = reputation.Reputation(self.Base) self.Base: base_mod.Base = self.BaseModule.Base(self)
self.User: user.User = user.User(self)
self.Settings.global_user = self.User
self.Client: client.Client = client.Client(self)
self.Admin: admin.Admin = admin.Admin(self)
self.Channel: channel.Channel = channel.Channel(self)
self.Reputation: reputation.Reputation = reputation.Reputation(self)
self.Commands: commands_mod.Command = commands_mod.Command(self)
self.ModuleUtils: module_mod.Module = module_mod.Module(self)
self.Sasl: sasl.Sasl = sasl.Sasl(self)
self.Irc: irc.Irc = irc.Irc(self)
self.PFactory: factory.ProtocolFactorty = factory.ProtocolFactorty(self.Irc)
self.RpcServer: rpc_mod.JSONRPCServer = rpc_mod.JSONRPCServer(self)
self.Base.init()
self.Logs.debug(self.Utils.tr("Loader %s success", __name__))

170
core/logs.py Normal file
View File

@@ -0,0 +1,170 @@
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.remove_logger(loggin_name) # Remove logger if exists
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.logs.debug(f"[STDOUT LEVEL] New level {level}")
self.stdout_handler.setLevel(level)
def set_file_handler_level(self, level: int) -> None:
self.logs.debug(f"[LOG FILE LEVEL] new level {level}")
self.file_handler.setLevel(level)
def update_handler_format(self, debug_hard: bool = False) -> None:
"""Updating logging formatter format!
Args:
debug_hard (bool, optional): If true you will have filename,
function name and the line number. Defaults to False.
"""
# Updating logging formatter
if debug_hard:
new_formatter = logging.Formatter(
fmt='%(asctime)s - %(levelname)s - %(message)s (%(filename)s:%(funcName)s:%(lineno)d)',
datefmt='%Y-%m-%d %H:%M:%S'
)
else:
new_formatter = logging.Formatter(
fmt='%(asctime)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
for handler in self.logs.handlers:
handler.setFormatter(new_formatter)
def regenerate_handlers(self, logger: logging.Logger) -> logging.Logger:
os_sep = self.OS_SEP
logging_name = self.LOGGING_NAME
debug_file_level = self.DEBUG_FILE_LEVEL
debug_stdout_level = self.DEBUG_STDOUT_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
logger.addHandler(self.file_handler)
logger.addHandler(self.stdout_handler)
# Apply the filter
logger.addFilter(self.replace_filter)
logger.info(f'REGENRATING LOGGER {self.LOGGING_NAME}')
return logger
def replace_filter(self, record: logging.LogRecord) -> bool:
response = True
filter: list[str] = self.LOG_FILTERS
# 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 to write the log!

488
core/module.py Normal file
View File

@@ -0,0 +1,488 @@
'''
This is the main operational file to handle modules
'''
from pathlib import Path
import sys
import importlib
from types import ModuleType
from typing import TYPE_CHECKING, Optional
from core.definition import DefenderModuleHeader, MModule
from core.utils import tr
if TYPE_CHECKING:
from core.loader import Loader
from core.irc import Irc
class Module:
DB_MODULES: list[MModule] = []
DB_MODULE_HEADERS: list[DefenderModuleHeader] = []
def __init__(self, loader: 'Loader') -> None:
self.__Loader = loader
self.__Base = loader.Base
self.__Logs = loader.Logs
self.__Utils = loader.Utils
self.__Config = loader.Config
def get_all_available_modules(self) -> list[str]:
"""Get list of all main modules
using this pattern mod_*.py
all files starting with mod_
Returns:
list[str]: List of all module names.
"""
base_path = Path('mods')
modules_available = [file.name.replace('.py', '') for file in base_path.rglob('mod_*.py')]
self.__Logs.debug(f"Modules available: {modules_available}")
return modules_available
def get_module_information(self, module_name: str) -> tuple[Optional[str], Optional[str], Optional[str]]:
# module_name : mod_defender
if not module_name.lower().startswith('mod_'):
return None, None, None
module_name = module_name.lower() # --> mod_defender
module_folder = module_name.split('_')[1].lower() # --> defender
class_name = module_name.split('_')[1].capitalize() # --> Defender
self.__Logs.debug(f"Module information Folder: {module_folder}, Name: {module_name}, Class: {class_name}")
return module_folder, module_name, class_name
def get_module_header(self, module_name: str) -> Optional[DefenderModuleHeader]:
for mod_h in self.DB_MODULE_HEADERS:
if module_name.lower() == mod_h.name.lower():
self.__Logs.debug(f"Module Header found: {mod_h}")
return mod_h
return None
def create_module_header(self, module_header: dict[str, str]) -> bool:
"""Create a new module header into DB_MODULE_HEADERS
Args:
module_header (dict[str, str]): The module header
Returns:
bool: True if the module header has been created.
"""
mod_header = DefenderModuleHeader(**module_header)
if self.get_module_header(mod_header.name) is None:
self.__Logs.debug(f"[MOD_HEADER] The module header has been created! ({mod_header.name} v{mod_header.version})")
self.DB_MODULE_HEADERS.append(mod_header)
return True
return False
def delete_module_header(self, module_name: str) -> bool:
mod_header = self.get_module_header(module_name)
if mod_header is not None:
self.__Logs.debug(f"[MOD_HEADER] The module header has been deleted ({mod_header.name} v{mod_header.version})")
self.DB_MODULE_HEADERS.remove(mod_header)
return True
self.__Logs.debug(f"[MOD_HEADER ERROR] Impossible to remove the module header ({module_name})")
return False
def load_one_module(self, uplink: 'Irc', module_name: str, nickname: str, is_default: bool = False) -> bool:
module_folder, module_name, class_name = self.get_module_information(module_name)
if module_folder is None or module_name is None or class_name is None:
self.__Logs.error(f"There is an error with the module name! {module_folder}, {module_name}, {class_name}")
return False
if self.is_module_exist_in_sys_module(module_name):
self.__Logs.debug(f"Module [{module_folder}.{module_name}] already loaded!")
if self.model_is_module_exist(module_name):
# Si le module existe dans la variable globale retourne False
self.__Logs.debug(f"Module [{module_folder}.{module_name}] exist in the local variable!")
uplink.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}",
channel=self.__Config.SERVICE_CHANLOG
)
return False
return self.reload_one_module(uplink, module_name, nickname)
# Charger le module
try:
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(uplink) # Créer une nouvelle instance de la classe
self.create_module_header(create_instance_of_the_class.MOD_HEADER)
except AttributeError as attr:
red = uplink.Config.COLORS.red
nogc = uplink.Config.COLORS.nogc
uplink.Protocol.send_priv_msg(
nick_from=self.__Config.SERVICE_NICKNAME,
msg=tr("[%sMODULE ERROR%s] Module %s is facing issues! %s", red, nogc, module_name, attr),
channel=self.__Config.SERVICE_CHANLOG
)
self.__Logs.error(msg=attr, exc_info=True)
return False
if not hasattr(create_instance_of_the_class, 'cmd'):
uplink.Protocol.send_priv_msg(
nick_from=self.__Config.SERVICE_NICKNAME,
msg=tr("cmd method is not available in the module (%s)", module_name),
channel=self.__Config.SERVICE_CHANLOG
)
self.__Logs.critical(f"The Module {module_name} has not been loaded because cmd method is not available")
self.db_delete_module(module_name)
return False
# Charger la nouvelle class dans la variable globale
if self.model_insert_module(MModule(module_name, class_name, create_instance_of_the_class)):
# Enregistrer le module dans la base de données
self.db_register_module(module_name, nickname, is_default)
uplink.Protocol.send_priv_msg(
nick_from=self.__Config.SERVICE_NICKNAME,
msg=tr("Module %s loaded!", module_name),
channel=self.__Config.SERVICE_CHANLOG
)
self.__Logs.debug(f"Module {class_name} has been loaded")
return True
return False
def load_all_modules(self) -> bool:
...
def reload_one_module(self, uplink: 'Irc', module_name: str, nickname: str) -> bool:
"""Reloading one module and insert it into the model as well as the database
Args:
uplink (Irc): The Irc service instance
module_name (str): The module name
nickname (str): The nickname
Returns:
bool: True if the module has been reloaded
"""
module_folder, module_name, class_name = self.get_module_information(module_name)
red = self.__Config.COLORS.red
nogc = self.__Config.COLORS.nogc
try:
if self.is_module_exist_in_sys_module(module_name):
module_model = self.model_get_module(module_name)
if module_model:
self.delete_module_header(module_model.class_instance.MOD_HEADER['name'])
module_model.class_instance.unload()
else:
uplink.Protocol.send_priv_msg(
nick_from=self.__Config.SERVICE_NICKNAME,
msg=f"[ {red}RELOAD MODULE ERROR{nogc} ] Module [{module_folder}.{module_name}] hasn't been reloaded! You must use {self.__Config.SERVICE_PREFIX}load {module_name}",
channel=self.__Config.SERVICE_CHANLOG
)
self.__Logs.debug(f"Module [{module_folder}.{module_name}] not found! Please use {self.__Config.SERVICE_PREFIX}load {module_name}")
return False
# reload module dependencies
self.reload_all_modules_with_all_dependencies(f'mods.{module_folder}')
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(uplink)
self.create_module_header(new_instance.MOD_HEADER)
module_model.class_instance = new_instance
# Créer le module dans la base de données
self.db_register_module(module_name, nickname)
uplink.Protocol.send_priv_msg(
nick_from=self.__Config.SERVICE_NICKNAME,
msg=f"Module [{module_folder}.{module_name}] has been reloaded!",
channel=self.__Config.SERVICE_CHANLOG
)
self.__Logs.debug(f"Module [{module_folder}.{module_name}] reloaded!")
return True
else:
# Module is not loaded! Nothing to reload
self.__Logs.debug(f"[RELOAD MODULE ERROR] [{module_folder}.{module_name}] is not loaded! You must use {self.__Config.SERVICE_PREFIX}load {module_name}")
uplink.Protocol.send_priv_msg(
nick_from=self.__Config.SERVICE_NICKNAME,
msg=f"[ {red}RELOAD MODULE ERROR{nogc} ] Module [{module_folder}.{module_name}] is not loaded! You must use {self.__Config.SERVICE_PREFIX}load {module_name}",
channel=self.__Config.SERVICE_CHANLOG
)
return False
except (TypeError, AttributeError, KeyError, Exception) as err:
self.__Logs.error(f"[RELOAD MODULE ERROR]: {err}", exc_info=True)
uplink.Protocol.send_priv_msg(
nick_from=self.__Config.SERVICE_NICKNAME,
msg=f"[RELOAD MODULE ERROR]: {err}",
channel=self.__Config.SERVICE_CHANLOG
)
self.db_delete_module(module_name)
def reload_all_modules(self) -> bool:
...
def reload_all_modules_with_all_dependencies(self, prefix: str = 'mods') -> bool:
"""
Reload all modules in sys.modules that start with the given prefix.
Useful for reloading a full package during development.
"""
modules_to_reload = []
# 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))
# 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')
except Exception as err:
self.__Logs.error(f'[LOAD_MODULE] Module {module} failed [!] - {err}')
def unload_one_module(self, uplink: 'Irc', module_name: str, keep_in_db: bool = True) -> bool:
"""Unload a module
Args:
uplink (Irc): The Irc instance
module_name (str): Module name ex mod_defender
keep_in_db (bool): Keep in database
Returns:
bool: True if success
"""
try:
# Le nom du module. exemple: mod_defender
red = self.__Config.COLORS.red
nogc = self.__Config.COLORS.nogc
module_folder, module_name, class_name = self.get_module_information(module_name)
module = self.model_get_module(module_name)
if module is None:
self.__Logs.debug(f"[ UNLOAD MODULE ERROR ] This module {module_name} is not loaded!")
self.db_delete_module(module_name)
uplink.Protocol.send_priv_msg(
nick_from=self.__Config.SERVICE_NICKNAME,
msg=f"[ {red}UNLOAD MODULE ERROR{nogc} ] This module {module_name} is not loaded!",
channel=self.__Config.SERVICE_CHANLOG
)
return False
if module:
self.delete_module_header(module.class_instance.MOD_HEADER['name'])
module.class_instance.unload()
self.DB_MODULES.remove(module)
# Delete from the sys.modules.
if sys.modules.get(f'mods.{module_folder}.{module_name}'):
del sys.modules[f"mods.{module_folder}.{module_name}"]
if sys.modules.get(f'mods.{module_folder}.{module_name}'):
self.__Logs.debug(f"Module mods.{module_folder}.{module_name} still in the sys.modules")
# Supprimer le module de la base de données
if not keep_in_db:
self.db_delete_module(module_name)
uplink.Protocol.send_priv_msg(
nick_from=self.__Config.SERVICE_NICKNAME,
msg=f"[ UNLOAD MODULE INFO ] Module {module_name} has been unloaded!",
channel=self.__Config.SERVICE_CHANLOG
)
self.__Logs.debug(f"[ UNLOAD MODULE ] {module_name} has been unloaded!")
return True
self.__Logs.debug(f"[UNLOAD MODULE]: Module {module_name} not found in DB_MODULES variable!")
return False
except Exception as err:
self.__Logs.error(f"General Error: {err}", exc_info=True)
return False
def unload_all_modules(self) -> bool:
...
def is_module_exist_in_sys_module(self, module_name: str) -> bool:
"""Check if the module exist in the sys.modules
This will check only in the folder mods/
Args:
module_name (str): The module name
Returns:
bool: True if the module exist
"""
module_folder, module_name, class_name = self.get_module_information(module_name)
if "mods." + module_folder + "." + module_name in sys.modules:
self.__Logs.debug(f"[SYS MODULE] (mods.{module_folder}.{module_name}) found in sys.modules")
return True
self.__Logs.debug(f"[SYS MODULE] (mods.{module_folder}.{module_name}) not found in sys.modules")
return False
'''
ALL METHODS RELATED TO THE MModule MODEL DATACLASS
'''
def model_get_module(self, module_name: str) -> Optional[MModule]:
"""Get The module model object if exist otherwise it returns None
Args:
module_name (str): The module name you want to fetch
Returns:
Optional[MModule]: The Module Model Object
"""
for module in self.DB_MODULES:
if module.module_name.lower() == module_name.lower():
self.__Logs.debug(f"[MODEL MODULE GET] The module {module_name} has been found in the model DB_MODULES")
return module
self.__Logs.debug(f"[MODEL MODULE GET] The module {module_name} not found in the model DB_MODULES")
return None
def model_get_loaded_modules(self) -> list[MModule]:
"""Get the instance of DB_MODULES.
Warning: You should use a copy if you want to loop through the list!
Returns:
list[MModule]: A list of module model object
"""
# self.__Logs.debug(f"[MODEL MODULE LOADED MODULES] {len(self.DB_MODULES)} modules found!")
return self.DB_MODULES
def model_insert_module(self, module_model: MModule) -> bool:
"""Insert a new module model object
Args:
module_model (MModule): The module model object
Returns:
bool: True if the model has been inserted
"""
module = self.model_get_module(module_model.module_name)
if module is None:
self.DB_MODULES.append(module_model)
self.__Logs.debug(f"[MODEL MODULE INSERT] The module {module_model.module_name} has been inserted in the local variable model DB_MODULES")
return True
self.__Logs.debug(f"[MODEL MODULE INSERT] The module {module_model.module_name} already exist in the local variable model DB_MODULES")
return False
def model_clear(self) -> None:
"""Clear DB_MODULES list!
"""
self.DB_MODULES.clear()
self.__Logs.debug("[MODEL MODULE CLEAR] The local variable model DB_MODULES has been cleared")
return None
def model_is_module_exist(self, module_name: str) -> bool:
"""Check if the module exist in the module model object
Args:
module_name (str): The module name
Returns:
bool: True if the module_name exist
"""
if self.model_get_module(module_name):
self.__Logs.debug(f"[MODEL MODULE EXIST] The module {module_name} exist in the local model DB_MODULES!")
return True
self.__Logs.debug(f"[MODEL MODULE EXIST] The module {module_name} is not available in the local model DB_MODULES!")
return False
'''
OPERATION DEDICATED TO DATABASE MANAGEMENT
'''
def db_load_all_existing_modules(self, uplink: 'Irc') -> bool:
"""Charge les modules qui existe déja dans la base de données
Returns:
None: Aucun retour requis, elle charge puis c'est tout
"""
self.__Logs.debug("[DB LOAD MODULE] Loading modules from the database!")
result = self.__Base.db_execute_query(f"SELECT module_name FROM {self.__Config.TABLE_MODULE}")
for r in result.fetchall():
self.load_one_module(uplink, r[0], 'sys', True)
return True
def db_is_module_exist(self, module_name: str) -> bool:
"""Check if the module exist in the database
Args:
module_name (str): The module name you want to check
Returns:
bool: True if the module exist in the database
"""
query = f"SELECT id FROM {self.__Config.TABLE_MODULE} WHERE module_name = :module_name"
mes_donnes = {'module_name': module_name.lower()}
results = self.__Base.db_execute_query(query, mes_donnes)
if results.fetchall():
self.__Logs.debug(f"[DB MODULE EXIST] The module {module_name} exist in the database!")
return True
else:
self.__Logs.debug(f"[DB MODULE EXIST] The module {module_name} is not available in the database!")
return False
def db_register_module(self, module_name: str, nickname: str, is_default: bool = False) -> bool:
"""Insert a new module in the database
Args:
module_name (str): The module name
nickname (str): The user who loaded the module
isdefault (int): Is this a default module. Default 0
"""
if not self.db_is_module_exist(module_name):
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.__Utils.get_sdatetime(), 'user': nickname, 'module_name': module_name.lower(), 'isdefault': is_default}
insert = self.__Base.db_execute_query(insert_cmd_query, mes_donnees)
if insert.rowcount > 0:
self.__Logs.debug(f"[DB REGISTER MODULE] Module {module_name} has been inserted to the database!")
return True
else:
self.__Logs.debug(f"[DB REGISTER MODULE] Module {module_name} not inserted to the database!")
return False
self.__Logs.debug(f"[DB REGISTER MODULE] Module {module_name} already exist in the database! Nothing to insert!")
return False
def db_update_module(self, module_name: str, nickname: str) -> None:
"""Update the datetime and the user that updated the module
Args:
module_name (str): The module name to update
nickname (str): The nickname who updated the module
"""
update_cmd_query = f"UPDATE {self.__Config.TABLE_MODULE} SET datetime = :datetime, LOWER(user) = :user WHERE LOWER(module_name) = :module_name"
mes_donnees = {'datetime': self.__Utils.get_sdatetime(), 'user': nickname.lower(), 'module_name': module_name.lower()}
result = self.__Base.db_execute_query(update_cmd_query, mes_donnees)
if result.rowcount > 0:
self.__Logs.debug(f"[DB UPDATE MODULE] Module {module_name} has been updated!")
return True
else:
self.__Logs.debug(f"[DB UPDATE MODULE] Module {module_name} not found! Nothing to update!")
return False
def db_delete_module(self, module_name:str) -> None:
"""Delete a module from the database
Args:
module_name (str): The module name you want to delete
"""
insert_cmd_query = f"DELETE FROM {self.__Config.TABLE_MODULE} WHERE LOWER(module_name) = :module_name"
mes_donnees = {'module_name': module_name.lower()}
delete = self.__Base.db_execute_query(insert_cmd_query, mes_donnees)
if delete.rowcount > 0:
self.__Logs.debug(f"[DB MODULE DELETE] The module {module_name} has been deleted from the dabatase!")
return True
self.__Logs.debug(f"[DB MODULE DELETE] The module {module_name} is not available in the database! Nothing to delete!")
return False

View File

@@ -1,25 +1,81 @@
from typing import Literal, Union """
Main utils library.
"""
import gc
import ssl
import socket
import sys
from pathlib import Path
from re import match, sub
from typing import Literal, Optional, Any, TYPE_CHECKING
from datetime import datetime from datetime import datetime
from time import time from time import time
from random import choice from random import choice
from hashlib import md5, sha3_512 from hashlib import md5, sha3_512
from core.classes.modules.settings import global_settings
def convert_to_int(value: any) -> Union[int, None]: if TYPE_CHECKING:
from core.irc import Irc
def tr(message: str, *args) -> str:
"""Translation Engine system
```python
example:
_('Hello my firstname is %s and my lastname is %s', firstname, lastname)
```
Args:
message (str): The message to translate
*args (any) : Whatever the variable you want to pass
Returns:
str: The translated message
"""
count_args = len(args) # Count number of args sent
count_placeholder = message.count('%s') # Count number of placeholder in the message
is_args_available = True if args else False
g = global_settings
try:
# Access to admin object
client_language = g.current_admin.language if g.current_admin else g.global_lang
if count_args != count_placeholder:
g.global_logger.error(f"Translation: Original message: {message} | Args: {count_args} - Placeholder: {count_placeholder}")
return message
if g.global_lang is None:
return message % args if is_args_available else message
if client_language.lower() == 'en':
return message % args if is_args_available else message
for trads in g.global_translation[client_language.lower()]:
if sub(r"\s+", "", message) == sub(r"\s+", "", trads[0]):
return trads[1] % args if is_args_available else trads[1]
return message % args if is_args_available else message
except KeyError as ke:
g.global_logger.error(f"KeyError: {ke}")
return message % args if is_args_available else message
except Exception as err:
global_settings.global_logger.error(f"General Error: {err} / {message}")
return message
def convert_to_int(value: Any) -> Optional[int]:
"""Convert a value to int """Convert a value to int
Args: Args:
value (any): Value to convert to int if possible value (Any): Value to convert to int if possible
Returns: Returns:
Union[int, None]: Return the int value or None if not possible int: Return the int value or None if not possible
""" """
try: try:
value_to_int = int(value) value_to_int = int(value)
return value_to_int return value_to_int
except ValueError: except ValueError:
return None return None
except Exception:
return None
def get_unixtime() -> int: def get_unixtime() -> int:
"""Cette fonction retourne un UNIXTIME de type 12365456 """Cette fonction retourne un UNIXTIME de type 12365456
@@ -27,9 +83,12 @@ def get_unixtime() -> int:
Returns: Returns:
int: Current time in seconds since the Epoch (int) 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()) return int(time())
def get_datetime() -> str: def get_sdatetime() -> str:
"""Retourne une date au format string (24-12-2023 20:50:59) """Retourne une date au format string (24-12-2023 20:50:59)
Returns: Returns:
@@ -38,6 +97,75 @@ def get_datetime() -> str:
currentdate = datetime.now().strftime('%d-%m-%Y %H:%M:%S') currentdate = datetime.now().strftime('%d-%m-%Y %H:%M:%S')
return currentdate return currentdate
def get_datetime() -> datetime:
"""
Return the current datetime in a datetime object
"""
return datetime.now()
def get_ssl_context() -> ssl.SSLContext:
"""Generate the ssl context
Returns:
SSLContext: The SSL Context
"""
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
return ctx
def create_socket(uplink: 'Irc') -> None:
"""Create a socket to connect SSL or Normal connection
"""
try:
soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM or socket.SOCK_NONBLOCK)
connexion_information = (uplink.Config.SERVEUR_IP, uplink.Config.SERVEUR_PORT)
if uplink.Config.SERVEUR_SSL:
# Create SSL Context object
ssl_context = get_ssl_context()
ssl_connexion = ssl_context.wrap_socket(soc, server_hostname=uplink.Config.SERVEUR_HOSTNAME)
ssl_connexion.connect(connexion_information)
uplink.IrcSocket = ssl_connexion
uplink.Config.SSL_VERSION = uplink.IrcSocket.version()
uplink.Logs.info(f"-- Connected using SSL : Version = {uplink.Config.SSL_VERSION}")
else:
soc.connect(connexion_information)
uplink.IrcSocket = soc
uplink.Logs.info("-- Connected in a normal mode!")
return None
except (ssl.SSLEOFError, ssl.SSLError) as soe:
uplink.Logs.critical(f"[SSL ERROR]: {soe}")
except OSError as oe:
uplink.Logs.critical(f"[OS Error]: {oe}")
if 'connection refused' in str(oe).lower():
sys.exit(oe.__str__())
if oe.errno == 10053:
sys.exit(oe.__str__())
except AttributeError as ae:
uplink.Logs.critical(f"AttributeError: {ae}")
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: def generate_random_string(lenght: int) -> str:
"""Retourn une chaîne aléatoire en fonction de la longueur spécifiée. """Retourn une chaîne aléatoire en fonction de la longueur spécifiée.
@@ -49,26 +177,69 @@ def generate_random_string(lenght: int) -> str:
return randomize return randomize
def hash(password: str, algorithm: Literal["md5, sha3_512"] = 'md5') -> str: def hash_password(password: str, algorithm: Literal["md5", "sha3_512"] = 'md5') -> str:
"""Retourne un mot de passe chiffré en fonction de l'algorithme utilisé """Return the crypted password following the selected algorithm
Args: Args:
password (str): Le password en clair password (str): The plain text password
algorithm (str): L'algorithm a utilisé algorithm (str): The algorithm to use
Returns: Returns:
str: Le password haché str: The crypted password, default md5
""" """
match algorithm: match algorithm:
case 'md5': case 'md5':
password = md5(password.encode()).hexdigest() hashed_password = md5(password.encode()).hexdigest()
return password return hashed_password
case 'sha3_512': case 'sha3_512':
password = sha3_512(password.encode()).hexdigest() hashed_password = sha3_512(password.encode()).hexdigest()
return password return hashed_password
case _: case _:
password = md5(password.encode()).hexdigest() hashed_password = md5(password.encode()).hexdigest()
return password return hashed_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
def hide_sensitive_data(srvmsg: list[str]) -> list[str]:
try:
srv_msg = srvmsg.copy()
privmsg_index = srv_msg.index('PRIVMSG')
auth_index = privmsg_index + 2
if match(r'^:{1}\W?(auth)$', srv_msg[auth_index]) is None:
return srv_msg
for l in range(auth_index + 1, len(srv_msg)):
srv_msg[l] = '*' * len(srv_msg[l])
return srv_msg
except ValueError:
return srvmsg

View File

@@ -1,24 +1,22 @@
from core import installation from core import install
############################################# #############################################
# @Version : 6 # # @Version : 6.3 #
# Requierements : # # Requierements : #
# Python3.10 or higher # # Python3.10 or higher #
# SQLAlchemy, requests, psutil # # SQLAlchemy, requests, psutil #
# unrealircd-rpc-py, pyyaml #
# UnrealIRCD 6.2.2 or higher # # UnrealIRCD 6.2.2 or higher #
############################################# #############################################
try: try:
# install.update_packages()
installation.Install()
from core.loader import Loader from core.loader import Loader
from core.irc import Irc loader = Loader()
# loader = Loader() loader.Irc.init_irc()
ircInstance = Irc(Loader())
ircInstance.init_irc(ircInstance)
except AssertionError as ae: except AssertionError as ae:
print(f'Assertion Error -> {ae}') print(f'Assertion Error -> {ae}')
except KeyboardInterrupt as k: except KeyboardInterrupt as k:
ircInstance.Base.execute_periodic_action() # ircInstance.Base.execute_periodic_action()
...

163
mods/clone/clone_manager.py Normal file
View File

@@ -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

View File

@@ -0,0 +1,4 @@
traduction:
# Message help
- orig: "Hi my name is clone-es"
trad: "Hola mi name is clone-es"

View File

@@ -0,0 +1,6 @@
traduction:
# Message help
- orig: "You are now logged in"
trad: "Vous étes désomais identifier"
- orig: "NSUser ==> nsuid: %s | cuid: %s | Account: %s | Nickname: %s | email: %s"
trad: "NSUser ==> nsuid: %s | cuid: %s | Compte: %s | Pseudo: %s | email: %s"

View File

311
mods/clone/mod_clone.py Normal file
View File

@@ -0,0 +1,311 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, Any
from core.classes.interfaces.imodule import IModule
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 faker import Faker
class Clone(IModule):
@dataclass
class ModConfModel(schemas.ModConfModel):
...
MOD_HEADER: dict[str, str] = {
'name':'Clone',
'version':'1.0.0',
'description':'Connect thousands of clones to your IRCD, by group. You can use them as security moderation.',
'author':'Defender Team',
'core_version':'Defender-6'
}
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
Returns:
None: Aucun retour n'es attendu
"""
table_channel = '''CREATE TABLE IF NOT EXISTS clone_list (
id INTEGER PRIMARY KEY AUTOINCREMENT,
datetime TEXT,
nickname TEXT,
username TEXT
)
'''
# self.Base.db_execute_query(table_channel)
return None
def load(self) -> None:
self.ModConfig = self.ModConfModel()
self.stop = False
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')
self.Channel.db_query_channel(action='add', module_name=self.module_name, channel_name=self.Config.CLONE_CHANNEL)
self.Protocol.send_sjoin(self.Config.CLONE_CHANNEL)
self.Protocol.send_set_mode('+o', nickname=self.Config.SERVICE_NICKNAME, channel_name=self.Config.CLONE_CHANNEL)
self.Protocol.send_set_mode('+nts', channel_name=self.Config.CLONE_CHANNEL)
self.Protocol.send_set_mode('+k', channel_name=self.Config.CLONE_CHANNEL, params=self.Config.CLONE_CHANNEL_PASSWORD)
def unload(self) -> None:
"""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.send_set_mode('-nts', channel_name=self.Config.CLONE_CHANNEL)
self.Protocol.send_set_mode('-k', channel_name=self.Config.CLONE_CHANNEL)
self.Protocol.send_part_chan(self.Config.SERVICE_NICKNAME, self.Config.CLONE_CHANNEL)
self.Irc.Commands.drop_command_by_module(self.module_name)
return None
def cmd(self, data:list) -> None:
try:
if not data or len(data) < 2:
return None
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 'PRIVMSG':
self.Utils.handle_on_privmsg(self, cmd)
return None
case 'QUIT':
return None
case _:
return None
except Exception as 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:
try:
if len(cmd) < 1:
return
command = str(cmd[0]).lower()
fromuser = user
dnickname = self.Config.SERVICE_NICKNAME
match command:
case '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 | 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 [group name]")
return None
option = str(cmd[1]).lower()
match option:
case 'connect':
try:
# clone connect 5 GroupName 3
self.stop = False
number_of_clones = int(cmd[2])
group = str(cmd[3]).lower()
connection_interval = int(cmd[4]) if len(cmd) == 5 else 0.2
self.Base.create_thread(
func=self.Threads.thread_connect_clones,
func_args=(self, number_of_clones, group, False, connection_interval)
)
except Exception as err:
self.Logs.error(f'{err}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} clone connect [number of clone you want to connect] [Group] [freq]")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Exemple /msg {dnickname} clone connect 6 Ambiance")
case 'kill':
try:
# clone kill [ALL | group name | nickname]
self.stop = True
option = str(cmd[2])
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:
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}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} clone kill [all | group name | nickname]")
case 'join':
try:
# clone join [all | group name | nickname] #channel
option = str(cmd[2])
clone_channel_to_join = str(cmd[3])
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.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}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} clone join [all | group name | nickname] #channel")
case 'part':
try:
# clone part [all | nickname] #channel
option = str(cmd[2])
clone_channel_to_part = str(cmd[3])
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.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)
except Exception as err:
self.Logs.error(f'{err}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} clone part [all | group name | nickname] #channel")
case 'list':
try:
# Syntax. /msg defender clone list <group_name>
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}')
case 'say':
try:
# clone say clone_nickname #channel message
clone_name = str(cmd[2])
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.nickname_exists(clone_name):
self.Protocol.send_notice(
nick_from=dnickname,
nick_to=fromuser,
msg=f"/msg {dnickname} clone say [clone_nickname] #channel message"
)
return None
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:
self.Logs.error(f'{err}')
self.Protocol.send_notice(
nick_from=dnickname,
nick_to=fromuser,
msg=f"/msg {dnickname} clone say [clone_nickname] #channel message"
)
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 | 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 [group name]")
except IndexError as ie:
self.Logs.error(f'Index Error: {ie}')
except Exception as err:
self.Logs.error(f'General Error: {err}')

22
mods/clone/schemas.py Normal file
View File

@@ -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] = []

44
mods/clone/threads.py Normal file
View File

@@ -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

198
mods/clone/utils.py Normal file
View File

@@ -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]) -> None:
senderObj, recieverObj, channel, message = uplink.Protocol.parse_privmsg(srvmsg)
if senderObj is not None:
if senderObj.hostname in uplink.Config.CLONE_LOG_HOST_EXEMPT:
return
senderMsg = message
clone_obj = recieverObj
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
)
return None

File diff suppressed because it is too large Load Diff

246
mods/command/utils.py Normal file
View File

@@ -0,0 +1,246 @@
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":{service_id} MODE {channel_name} {mode} {client}")
uplink.Protocol.send_set_mode(mode, nickname=client, channel_name=channel_name)
return None
# deop nickname
if len(cmd) == 2:
nickname = cmd[1]
# uplink.Protocol.send2socket(f":{service_id} MODE {channel_name} {mode} {nickname}")
uplink.Protocol.send_set_mode(mode, nickname=nickname, channel_name=channel_name)
return None
nickname = cmd[2]
# uplink.Protocol.send2socket(f":{service_id} MODE {channel_name} {mode} {nickname}")
uplink.Protocol.send_set_mode(mode, nickname=nickname, channel_name=channel_name)
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:
if len(cmd) < 2:
raise IndexError(f"{cmd[0].upper()} is expecting the channel parameter")
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:
if len(cmd) < 2:
raise IndexError(f"{cmd[0].upper()} is expecting the channel parameter")
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

View File

@@ -0,0 +1,901 @@
from dataclasses import dataclass
from typing import Any
from core.classes.interfaces.imodule import IModule
import mods.defender.schemas as schemas
import mods.defender.utils as utils
import mods.defender.threads as thds
from core.utils import tr
class Defender(IModule):
@dataclass
class ModConfModel(schemas.ModConfModel):
...
MOD_HEADER: dict[str, str] = {
'name':'Defender',
'version':'1.0.0',
'description':'Defender main module that uses the reputation security.',
'author':'Defender Team',
'core_version':'Defender-6'
}
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
"""
# table_autoop = '''CREATE TABLE IF NOT EXISTS defender_autoop (
# id INTEGER PRIMARY KEY AUTOINCREMENT,
# datetime TEXT,
# nickname TEXT,
# channel TEXT
# )
# '''
# self.Base.db_execute_query(table_autoop)
# self.Base.db_execute_query(table_config)
# self.Base.db_execute_query(table_trusted)
return None
def load(self):
# Add module schemas
self.Schemas = schemas
# Add utils functions
self.Utils = utils
# Variable qui va contenir les options de configuration du module Defender
self.ModConfig: schemas.ModConfModel = self.ModConfModel()
# Create module commands (Mandatory)
self.Irc.build_command(0, self.module_name, 'code', 'Display the code or key for access')
self.Irc.build_command(1, self.module_name, 'info', 'Provide information about the channel or server')
self.Irc.build_command(1, self.module_name, 'autolimit', 'Automatically set channel user limits')
self.Irc.build_command(3, self.module_name, 'reputation', 'Check or manage user reputation')
self.Irc.build_command(3, self.module_name, 'proxy_scan', 'Scan users for proxy connections')
self.Irc.build_command(3, self.module_name, 'flood', 'Handle flood detection and mitigation')
self.Irc.build_command(3, self.module_name, 'status', 'Check the status of the server or bot')
self.Irc.build_command(3, self.module_name, 'show_reputation', 'Display reputation information')
self.Irc.build_command(3, self.module_name, 'sentinel', 'Monitor and guard the channel or server')
self.timeout = self.Config.API_TIMEOUT
# Listes qui vont contenir les ip a scanner avec les différentes API
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 = self.freeipapi_isRunning = self.cloudfilt_isRunning = True
self.psutil_isRunning = self.localscan_isRunning = self.reputationTimer_isRunning = True
self.autolimit_isRunning = True
# Variable qui va contenir les users
self.flood_system = {}
# Contient les premieres informations de connexion
self.reputation_first_connexion = {'ip': '', 'score': -1}
# Laisser vide si aucune clé
self.abuseipdb_key = '13c34603fee4d2941a2c443cc5c77fd750757ca2a2c1b304bd0f418aff80c24be12651d1a3cfe674'
self.cloudfilt_key = 'r1gEtjtfgRQjtNBDMxsg'
# Démarrer les threads pour démarrer les api
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=thds.thread_autolimit, func_args=(self, ))
if self.ModConfig.reputation == 1:
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}")
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.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
self.cloudfilt_isRunning:bool = False
self.psutil_isRunning:bool = False
self.localscan_isRunning:bool = False
self.reputationTimer_isRunning:bool = False
self.autolimit_isRunning: bool = False
self.Irc.Commands.drop_command_by_module(self.module_name)
return None
def insert_db_trusted(self, uid: str, nickname:str) -> None:
u = self.User.get_user(uid)
if u is None:
return None
uid = u.uid
nickname = u.nickname
query = "SELECT id FROM def_trusted WHERE user = ?"
exec_query = self.Base.db_execute_query(query, {"user": nickname})
response = exec_query.fetchone()
if response is not None:
q_insert = "INSERT INTO def_trusted (datetime, user, host, vhost) VALUES (?, ?, ?, ?)"
mes_donnees = {'datetime': self.Loader.Utils.get_datetime(), 'user': nickname, 'host': '*', 'vhost': '*'}
exec_query = self.Base.db_execute_query(q_insert, mes_donnees)
pass
def join_saved_channels(self) -> None:
"""_summary_
"""
try:
result = self.Base.db_execute_query(f"SELECT distinct channel_name FROM {self.Config.TABLE_CHANNEL}")
channels = result.fetchall()
jail_chan = self.Config.SALON_JAIL
jail_chan_mode = self.Config.SALON_JAIL_MODES
service_id = self.Config.SERVICE_ID
dumodes = self.Config.SERVICE_UMODES
dnickname = self.Config.SERVICE_NICKNAME
for channel in channels:
chan = channel[0]
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}")
return None
except Exception as err:
self.Logs.error(f"General Error: {err}")
def cmd(self, data: list[str]) -> None:
if not data or len(data) < 2:
return None
cmd = data.copy() if isinstance(data, list) else list(data).copy()
try:
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':
self.Utils.handle_on_mode(self, cmd)
return None
case 'PRIVMSG':
self.Utils.handle_on_privmsg(self, cmd)
return None
case 'UID':
self.Utils.handle_on_uid(self, cmd)
return None
case 'SJOIN':
self.Utils.handle_on_sjoin(self, cmd)
return None
case 'SLOG':
self.Utils.handle_on_slog(self, cmd)
return None
case 'NICK':
self.Utils.handle_on_nick(self, cmd)
return None
case 'QUIT':
self.Utils.handle_on_quit(self, cmd)
return None
case _:
return None
except KeyError as ke:
self.Logs.error(f"{ke} / {cmd} / length {str(len(cmd))}")
except IndexError as ie:
self.Logs.error(f"{ie} / {cmd} / length {str(len(cmd))}")
except Exception as err:
self.Logs.error(f"General Error: {err}", exc_info=True)
def hcmds(self, user: str, channel: Any, cmd: list, fullcmd: list = []) -> None:
u = self.User.get_user(user)
if u is None:
return None
command = str(cmd[0]).lower()
fromuser = u.nickname
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
dumodes = self.Config.SERVICE_UMODES # Les modes de Defender
service_id = self.Config.SERVICE_ID # Defender serveur id
jail_chan = self.Config.SALON_JAIL # Salon pot de miel
jail_chan_mode = self.Config.SALON_JAIL_MODES # Mode du salon "pot de miel"
match command:
case 'show_reputation':
if not self.Reputation.UID_REPUTATION_DB:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg="No one is suspected")
for suspect in self.Reputation.UID_REPUTATION_DB:
self.Protocol.send_notice(nick_from=dnickname,
nick_to=fromuser,
msg=f" Uid: {suspect.uid} | Nickname: {suspect.nickname} | Reputation: {suspect.score_connexion} | Secret code: {suspect.secret_code} | Connected on: {suspect.connexion_datetime}")
case 'code':
try:
release_code = cmd[1]
jailed_nickname = u.nickname
jailed_UID = u.uid
get_reputation = self.Reputation.get_reputation(jailed_UID)
if get_reputation is None:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=" No code is requested ...")
return False
jailed_IP = get_reputation.remote_ip
jailed_salon = self.Config.SALON_JAIL
reputation_seuil = self.ModConfig.reputation_seuil
welcome_salon = self.Config.SALON_LIBERER
self.Logs.debug(f"IP de {jailed_nickname} : {jailed_IP}")
link = self.Config.SERVEUR_LINK
color_green = self.Config.COLORS.green
color_black = self.Config.COLORS.black
if release_code == get_reputation.secret_code:
self.Protocol.send_priv_msg(nick_from=dnickname, msg="Bon mot de passe. Allez du vent !", channel=jailed_salon)
if self.ModConfig.reputation_ban_all_chan == 1:
for chan in self.Channel.UID_CHANNEL_DB:
if chan.name != jailed_salon:
self.Protocol.send2socket(f":{service_id} MODE {chan.name} -b {jailed_nickname}!*@*")
self.Reputation.delete(jailed_UID)
self.Logs.debug(f'{jailed_UID} - {jailed_nickname} removed from REPUTATION_DB')
self.Protocol.send_sapart(nick_to_sapart=jailed_nickname, channel_name=jailed_salon)
self.Protocol.send_sajoin(nick_to_sajoin=jailed_nickname, channel_name=welcome_salon)
self.Protocol.send2socket(f":{link} REPUTATION {jailed_IP} {self.ModConfig.reputation_score_after_release}")
u.score_connexion = reputation_seuil + 1
self.Protocol.send_priv_msg(nick_from=dnickname,
msg=f"[{color_green} MOT DE PASS CORRECT {color_black}] : You have now the right to enjoy the network !",
nick_to=jailed_nickname)
else:
self.Protocol.send_priv_msg(
nick_from=dnickname,
msg="Mauvais password",
channel=jailed_salon
)
self.Protocol.send_priv_msg(
nick_from=dnickname,
msg=f"[{color_green} MAUVAIS PASSWORD {color_black}] You have typed a wrong code. for recall your password is: {self.Config.SERVICE_PREFIX}code {get_reputation.secret_code}",
nick_to=jailed_nickname
)
except IndexError as ie:
self.Logs.error(f'Index Error: {ie}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} code [code]")
except KeyError as ke:
self.Logs.error(f'_hcmd code: KeyError {ke}')
case 'autolimit':
try:
# autolimit on
# autolimit set [amount] [interval]
if len(cmd) < 2:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {self.Config.SERVICE_NICKNAME} {command.upper()} ON")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {self.Config.SERVICE_NICKNAME} {command.upper()} SET [AMOUNT] [INTERVAL]")
return None
arg = str(cmd[1]).lower()
match arg:
case 'on':
if self.ModConfig.autolimit == 0:
self.update_configuration('autolimit', 1)
self.autolimit_isRunning = True
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)
case 'off':
if self.ModConfig.autolimit == 1:
self.update_configuration('autolimit', 0)
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[{self.Config.COLORS.green}AUTOLIMIT{self.Config.COLORS.nogc}] Deactivated", 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 Deactivated", channel=self.Config.SERVICE_CHANLOG)
case 'set':
amount = int(cmd[2])
interval = int(cmd[3])
self.update_configuration('autolimit_amount', amount)
self.update_configuration('autolimit_interval', interval)
self.Protocol.send_priv_msg(
nick_from=dnickname,
msg=f"[{self.Config.COLORS.green}AUTOLIMIT{self.Config.COLORS.nogc}] Amount set to ({amount}) | Interval set to ({interval})",
channel=self.Config.SERVICE_CHANLOG
)
case _:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {self.Config.SERVICE_NICKNAME} {command.upper()} ON")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {self.Config.SERVICE_NICKNAME} {command.upper()} SET [AMOUNT] [INTERVAL]")
except Exception as err:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {self.Config.SERVICE_NICKNAME} {command.upper()} ON")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {self.Config.SERVICE_NICKNAME} {command.upper()} SET [AMOUNT] [INTERVAL]")
self.Logs.error(f"Value Error -> {err}")
case 'reputation':
# .reputation [on/off] --> activate or deactivate reputation system
# .reputation set banallchan [on/off] --> activate or deactivate ban in all channel
# .reputation set limit [xxxx] --> change the reputation threshold
# .reputation release [nick]
# .reputation [arg1] [arg2] [arg3]
try:
len_cmd = len(cmd)
if len_cmd < 2:
raise IndexError("Showing help!")
activation = str(cmd[1]).lower()
# Nous sommes dans l'activation ON / OFF
if len_cmd == 2:
key = 'reputation'
if activation == 'on':
if self.ModConfig.reputation == 1:
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {self.Config.COLORS.green}REPUTATION{self.Config.COLORS.black} ] : Already activated", channel=dchanlog)
return False
# self.update_db_configuration('reputation', 1)
self.update_configuration(key, 1)
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {self.Config.COLORS.green}REPUTATION{self.Config.COLORS.black} ] : Activated by {fromuser}", channel=dchanlog)
self.Protocol.send_join_chan(uidornickname=dnickname, channel=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}")
if self.ModConfig.reputation_sg == 1:
for chan in self.Channel.UID_CHANNEL_DB:
if chan.name != jail_chan:
self.Protocol.send2socket(f":{service_id} MODE {chan.name} +b ~security-group:unknown-users")
self.Protocol.send2socket(f":{service_id} MODE {chan.name} +eee ~security-group:webirc-users ~security-group:known-users ~security-group:websocket-users")
self.Channel.db_query_channel('add', self.module_name, jail_chan)
if activation == 'off':
if self.ModConfig.reputation == 0:
self.Protocol.send_priv_msg(
nick_from=dnickname,
msg=f"[ {self.Config.COLORS.green}REPUTATION{self.Config.COLORS.black} ] : Already deactivated",
channel=dchanlog
)
return False
self.update_configuration(key, 0)
self.Protocol.send_priv_msg(
nick_from=dnickname,
msg=f"[ {self.Config.COLORS.red}REPUTATION{self.Config.COLORS.black} ] : Deactivated by {fromuser}",
channel=dchanlog
)
self.Protocol.send2socket(f":{service_id} SAMODE {jail_chan} -{dumodes} {dnickname}")
self.Protocol.send2socket(f":{service_id} MODE {jail_chan} -sS")
self.Protocol.send2socket(f":{service_id} PART {jail_chan}")
for chan in self.Channel.UID_CHANNEL_DB:
if chan.name != jail_chan:
self.Protocol.send2socket(f":{service_id} MODE {chan.name} -b ~security-group:unknown-users")
self.Protocol.send2socket(f":{service_id} MODE {chan.name} -eee ~security-group:webirc-users ~security-group:known-users ~security-group:websocket-users")
self.Channel.db_query_channel('del', self.module_name, jail_chan)
if len_cmd == 3:
get_options = str(cmd[1]).lower()
match get_options:
case 'release':
# .reputation release [nick]
p = self.Protocol
link = self.Config.SERVEUR_LINK
jailed_salon = self.Config.SALON_JAIL
welcome_salon = self.Config.SALON_LIBERER
client_obj = self.User.get_user(str(cmd[2]))
if client_obj is None:
p.send_notice(nick_from=dnickname,
nick_to=fromuser,
msg=f"This nickname ({str(cmd[2])}) is not connected to the network!")
return None
client_to_release = self.Reputation.get_reputation(client_obj.uid)
if client_to_release is None:
p.send_notice(nick_from=dnickname,
nick_to=fromuser, msg=f"This nickname ({str(cmd[2])}) doesn't exist in the reputation databalse!")
return None
if self.Reputation.delete(client_to_release.uid):
p.send_priv_msg(
nick_from=dnickname,
msg=f"[ {self.Config.COLORS.green}REPUTATION RELEASE{self.Config.COLORS.black} ] : {client_to_release.nickname} has been released",
channel=dchanlog)
p.send_notice(nick_from=dnickname,
nick_to=fromuser, msg=f"This nickname has been released from reputation system")
p.send_notice(nick_from=dnickname,
nick_to=client_to_release.nickname, msg=f"You have been released from the reputation system by ({fromuser})")
p.send_sapart(nick_to_sapart=client_to_release.nickname, channel_name=jailed_salon)
p.send_sajoin(nick_to_sajoin=client_to_release.nickname, channel_name=welcome_salon)
p.send2socket(f":{link} REPUTATION {client_to_release.remote_ip} {self.ModConfig.reputation_score_after_release}")
return None
else:
p.send_priv_msg(
nick_from=dnickname,
msg=f"[ {self.Config.COLORS.red}REPUTATION RELEASE ERROR{self.Config.COLORS.black} ] : "
f"{client_to_release.nickname} has not been released! as he is not in the reputation database",
channel=dchanlog
)
if len_cmd > 4:
get_set = str(cmd[1]).lower()
if get_set != 'set':
raise IndexError('Showing help')
get_options = str(cmd[2]).lower()
match get_options:
case 'banallchan':
key = 'reputation_ban_all_chan'
get_value = str(cmd[3]).lower()
if get_value == 'on':
if self.ModConfig.reputation_ban_all_chan == 1:
self.Protocol.send_priv_msg(
nick_from=dnickname,
msg=f"[ {self.Config.COLORS.red}BAN ON ALL CHANS{self.Config.COLORS.black} ] : Already activated",
channel=dchanlog
)
return False
# self.update_db_configuration(key, 1)
self.update_configuration(key, 1)
self.Protocol.send_priv_msg(
nick_from=dnickname,
msg=f"[ {self.Config.COLORS.green}BAN ON ALL CHANS{self.Config.COLORS.black} ] : Activated by {fromuser}",
channel=dchanlog
)
elif get_value == 'off':
if self.ModConfig.reputation_ban_all_chan == 0:
self.Protocol.send_priv_msg(
nick_from=dnickname,
msg=f"[ {self.Config.COLORS.red}BAN ON ALL CHANS{self.Config.COLORS.black} ] : Already deactivated",
channel=dchanlog
)
return False
# self.update_db_configuration(key, 0)
self.update_configuration(key, 0)
self.Protocol.send_priv_msg(
nick_from=dnickname,
msg=f"[ {self.Config.COLORS.green}BAN ON ALL CHANS{self.Config.COLORS.black} ] : Deactivated by {fromuser}",
channel=dchanlog
)
case 'limit':
reputation_seuil = int(cmd[3])
key = 'reputation_seuil'
# self.update_db_configuration(key, reputation_seuil)
self.update_configuration(key, reputation_seuil)
self.Protocol.send_priv_msg(
nick_from=dnickname,
msg=f"[ {self.Config.COLORS.green}REPUTATION SEUIL{self.Config.COLORS.black} ] : Limit set to {str(reputation_seuil)} by {fromuser}",
channel=dchanlog
)
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Reputation set to {reputation_seuil}")
case 'timer':
reputation_timer = int(cmd[3])
key = 'reputation_timer'
self.update_configuration(key, reputation_timer)
self.Protocol.send_priv_msg(
nick_from=dnickname,
msg=f"[ {self.Config.COLORS.green}REPUTATION TIMER{self.Config.COLORS.black} ] : Timer set to {str(reputation_timer)} minute(s) by {fromuser}",
channel=dchanlog
)
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Reputation set to {reputation_timer}")
case 'score_after_release':
reputation_score_after_release = int(cmd[3])
key = 'reputation_score_after_release'
self.update_configuration(key, reputation_score_after_release)
self.Protocol.send_priv_msg(
nick_from=dnickname,
msg=f"[ {self.Config.COLORS.green}REPUTATION SCORE AFTER RELEASE{self.Config.COLORS.black} ] : Reputation score after release set to {str(reputation_score_after_release)} by {fromuser}",
channel=dchanlog
)
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Reputation score after release set to {reputation_score_after_release}")
case 'security_group':
reputation_sg = int(cmd[3])
key = 'reputation_sg'
self.update_configuration(key, reputation_sg)
self.Protocol.send_priv_msg(
nick_from=dnickname,
msg=f"[ {self.Config.COLORS.green}REPUTATION SECURITY-GROUP{self.Config.COLORS.black} ] : Reputation Security-group set to {str(reputation_sg)} by {fromuser}",
channel=dchanlog
)
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Reputation score after release set to {reputation_sg}")
case _:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} reputation [ON/OFF]")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} reputation release [nickname]")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} reputation set banallchan [ON/OFF]")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} reputation set limit [1234]")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} reputation set score_after_release [1234]")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} reputation set timer [1234]")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} reputation set action [kill|None]")
except IndexError as ie:
self.Logs.warning(f'{ie}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} reputation [ON/OFF]")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} reputation release [nickname]")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} reputation set banallchan [ON/OFF]")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} reputation set limit [1234]")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} reputation set score_after_release [1234]")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} reputation set timer [1234]")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" Right command : /msg {dnickname} reputation set action [kill|None]")
except ValueError as ve:
self.Logs.warning(f'{ve}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=" La valeur devrait etre un entier >= 0")
case 'proxy_scan':
# .proxy_scan set local_scan on/off --> Va activer le scan des ports
# .proxy_scan set psutil_scan on/off --> Active les informations de connexion a la machine locale
# .proxy_scan set abuseipdb_scan on/off --> Active le scan via l'api abuseipdb
len_cmd = len(cmd)
color_green = self.Config.COLORS.green
color_red = self.Config.COLORS.red
color_black = self.Config.COLORS.black
if len_cmd == 4:
set_key = str(cmd[1]).lower()
if set_key != 'set':
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' Right command : /msg {dnickname} proxy_scan set local_scan [ON/OFF]')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' Right command : /msg {dnickname} proxy_scan set psutil_scan [ON/OFF]')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' Right command : /msg {dnickname} proxy_scan set abuseipdb_scan [ON/OFF]')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' Right command : /msg {dnickname} proxy_scan set freeipapi_scan [ON/OFF]')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' Right command : /msg {dnickname} proxy_scan set cloudfilt_scan [ON/OFF]')
option = str(cmd[2]).lower() # => local_scan, psutil_scan, abuseipdb_scan
action = str(cmd[3]).lower() # => on / off
match option:
case 'local_scan':
if action == 'on':
if self.ModConfig.local_scan == 1:
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Already activated", channel=dchanlog)
return None
self.update_configuration(option, 1)
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Activated by {fromuser}", channel=dchanlog)
elif action == 'off':
if self.ModConfig.local_scan == 0:
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_red}PROXY_SCAN {option.upper()}{color_black} ] : Already Deactivated", channel=dchanlog)
return None
self.update_configuration(option, 0)
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_red}PROXY_SCAN {option.upper()}{color_black} ] : Deactivated by {fromuser}", channel=dchanlog)
case 'psutil_scan':
if action == 'on':
if self.ModConfig.psutil_scan == 1:
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Already activated", channel=dchanlog)
return None
self.update_configuration(option, 1)
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Activated by {fromuser}", channel=dchanlog)
elif action == 'off':
if self.ModConfig.psutil_scan == 0:
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_red}PROXY_SCAN {option.upper()}{color_black} ] : Already Deactivated", channel=dchanlog)
return None
self.update_configuration(option, 0)
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_red}PROXY_SCAN {option.upper()}{color_black} ] : Deactivated by {fromuser}", channel=dchanlog)
case 'abuseipdb_scan':
if action == 'on':
if self.ModConfig.abuseipdb_scan == 1:
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Already activated", channel=dchanlog)
return None
self.update_configuration(option, 1)
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Activated by {fromuser}", channel=dchanlog)
elif action == 'off':
if self.ModConfig.abuseipdb_scan == 0:
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_red}PROXY_SCAN {option.upper()}{color_black} ] : Already Deactivated", channel=dchanlog)
return None
self.update_configuration(option, 0)
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_red}PROXY_SCAN {option.upper()}{color_black} ] : Deactivated by {fromuser}", channel=dchanlog)
case 'freeipapi_scan':
if action == 'on':
if self.ModConfig.freeipapi_scan == 1:
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Already activated", channel=dchanlog)
return None
self.update_configuration(option, 1)
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Activated by {fromuser}", channel=dchanlog)
elif action == 'off':
if self.ModConfig.freeipapi_scan == 0:
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_red}PROXY_SCAN {option.upper()}{color_black} ] : Already Deactivated", channel=dchanlog)
return None
self.update_configuration(option, 0)
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_red}PROXY_SCAN {option.upper()}{color_black} ] : Deactivated by {fromuser}", channel=dchanlog)
case 'cloudfilt_scan':
if action == 'on':
if self.ModConfig.cloudfilt_scan == 1:
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Already activated", channel=dchanlog)
return None
self.update_configuration(option, 1)
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Activated by {fromuser}", channel=dchanlog)
elif action == 'off':
if self.ModConfig.cloudfilt_scan == 0:
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_red}PROXY_SCAN {option.upper()}{color_black} ] : Already Deactivated", channel=dchanlog)
return None
self.update_configuration(option, 0)
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_red}PROXY_SCAN {option.upper()}{color_black} ] : Deactivated by {fromuser}", channel=dchanlog)
case _:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' Right command : /msg {dnickname} proxy_scan set local_scan [ON/OFF]')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' Right command : /msg {dnickname} proxy_scan set psutil_scan [ON/OFF]')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' Right command : /msg {dnickname} proxy_scan set abuseipdb_scan [ON/OFF]')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' Right command : /msg {dnickname} proxy_scan set freeipapi_scan [ON/OFF]')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' Right command : /msg {dnickname} proxy_scan set cloudfilt_scan [ON/OFF]')
else:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' Right command : /msg {dnickname} proxy_scan set local_scan [ON/OFF]')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' Right command : /msg {dnickname} proxy_scan set psutil_scan [ON/OFF]')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' Right command : /msg {dnickname} proxy_scan set abuseipdb_scan [ON/OFF]')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' Right command : /msg {dnickname} proxy_scan set freeipapi_scan [ON/OFF]')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' Right command : /msg {dnickname} proxy_scan set cloudfilt_scan [ON/OFF]')
case 'flood':
# .flood on/off
# .flood set flood_message 5
# .flood set flood_time 1
# .flood set flood_timer 20
try:
len_cmd = len(cmd)
if len_cmd == 2:
activation = str(cmd[1]).lower()
key = 'flood'
if activation == 'on':
if self.ModConfig.flood == 1:
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {self.Config.COLORS.green}FLOOD{self.Config.COLORS.black} ] : Already activated", channel=dchanlog)
return False
self.update_configuration(key, 1)
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {self.Config.COLORS.green}FLOOD{self.Config.COLORS.black} ] : Activated by {fromuser}", channel=dchanlog)
if activation == 'off':
if self.ModConfig.flood == 0:
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {self.Config.COLORS.red}FLOOD{self.Config.COLORS.black} ] : Already Deactivated", channel=dchanlog)
return False
self.update_configuration(key, 0)
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {self.Config.COLORS.green}FLOOD{self.Config.COLORS.black} ] : Deactivated by {fromuser}", channel=dchanlog)
if len_cmd == 4:
set_key = str(cmd[2]).lower()
if str(cmd[1]).lower() == 'set':
match set_key:
case 'flood_message':
key = 'flood_message'
set_value = int(cmd[3])
self.update_configuration(key, set_value)
self.Protocol.send_priv_msg(nick_from=dnickname,
msg=f"[ {self.Config.COLORS.green}FLOOD{self.Config.COLORS.black} ] : Flood message set to {set_value} by {fromuser}",
channel=dchanlog)
case 'flood_time':
key = 'flood_time'
set_value = int(cmd[3])
self.update_configuration(key, set_value)
self.Protocol.send_priv_msg(nick_from=dnickname,
msg=f"[ {self.Config.COLORS.green}FLOOD{self.Config.COLORS.black} ] : Flood time set to {set_value} by {fromuser}",
channel=dchanlog)
case 'flood_timer':
key = 'flood_timer'
set_value = int(cmd[3])
self.update_configuration(key, set_value)
self.Protocol.send_priv_msg(nick_from=dnickname,
msg=f"[ {self.Config.COLORS.green}FLOOD{self.Config.COLORS.black} ] : Flood timer set to {set_value} by {fromuser}",
channel=dchanlog)
case _:
pass
except ValueError as ve:
self.Logs.error(f"{self.__class__.__name__} Value Error : {ve}")
case 'status':
color_green = self.Config.COLORS.green
color_red = self.Config.COLORS.red
color_black = self.Config.COLORS.black
nogc = self.Config.COLORS.nogc
try:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' [{color_green if self.ModConfig.reputation == 1 else color_red}Reputation{nogc}] ==> {self.ModConfig.reputation}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' reputation_seuil ==> {self.ModConfig.reputation_seuil}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' reputation_after_release ==> {self.ModConfig.reputation_score_after_release}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' reputation_ban_all_chan ==> {self.ModConfig.reputation_ban_all_chan}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' reputation_timer ==> {self.ModConfig.reputation_timer}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=' [Proxy_scan]')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' {color_green if self.ModConfig.local_scan == 1 else color_red}local_scan{nogc} ==> {self.ModConfig.local_scan}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' {color_green if self.ModConfig.psutil_scan == 1 else color_red}psutil_scan{nogc} ==> {self.ModConfig.psutil_scan}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' {color_green if self.ModConfig.abuseipdb_scan == 1 else color_red}abuseipdb_scan{nogc} ==> {self.ModConfig.abuseipdb_scan}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' {color_green if self.ModConfig.freeipapi_scan == 1 else color_red}freeipapi_scan{nogc} ==> {self.ModConfig.freeipapi_scan}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' {color_green if self.ModConfig.cloudfilt_scan == 1 else color_red}cloudfilt_scan{nogc} ==> {self.ModConfig.cloudfilt_scan}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' [{color_green if self.ModConfig.autolimit == 1 else color_red}Autolimit{nogc}] ==> {self.ModConfig.autolimit}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' {color_green if self.ModConfig.autolimit == 1 else color_red}Autolimit Amount{nogc} ==> {self.ModConfig.autolimit_amount}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' {color_green if self.ModConfig.autolimit == 1 else color_red}Autolimit Interval{nogc} ==> {self.ModConfig.autolimit_interval}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' [{color_green if self.ModConfig.flood == 1 else color_red}Flood{nogc}] ==> {self.ModConfig.flood}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=' flood_action ==> Coming soon')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' flood_message ==> {self.ModConfig.flood_message}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' flood_time ==> {self.ModConfig.flood_time}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' flood_timer ==> {self.ModConfig.flood_timer}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' [{color_green if self.ModConfig.flood == 1 else color_red}Sentinel{nogc}] ==> {self.ModConfig.sentinel}')
except KeyError as ke:
self.Logs.error(f"Key Error : {ke}")
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 = [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}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' USERNAME : {UserObject.username}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' REALNAME : {UserObject.realname}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' HOSTNAME : {UserObject.hostname}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' VHOST : {UserObject.vhost}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' IP : {UserObject.remote_ip}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' Country : {UserObject.geoip}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' WebIrc : {UserObject.isWebirc}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' WebWebsocket : {UserObject.isWebsocket}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' REPUTATION : {UserObject.score_connexion}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' MODES : {UserObject.umodes}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f' CHANNELS : {", ".join(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"This user {nickoruid} doesn't exist")
except KeyError as ke:
self.Logs.warning(f"Key error info user : {ke}")
case 'sentinel':
# .sentinel on
activation = str(cmd[1]).lower()
channel_to_dont_quit = [self.Config.SALON_JAIL, self.Config.SERVICE_CHANLOG]
if activation == 'on':
result = self.Base.db_execute_query(f"SELECT distinct channel_name FROM {self.Config.TABLE_CHANNEL}")
channels = result.fetchall()
channel_in_db = [channel[0] for channel in channels]
channel_to_dont_quit.extend(channel_in_db)
self.update_configuration('sentinel', 1)
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)
self.Protocol.send_priv_msg(dnickname, f"Sentinel mode activated on {channel}", channel=chan.name)
return None
if activation == 'off':
result = self.Base.db_execute_query(f"SELECT distinct channel_name FROM {self.Config.TABLE_CHANNEL}")
channels = result.fetchall()
channel_in_db = [channel[0] for channel in channels]
channel_to_dont_quit.extend(channel_in_db)
self.update_configuration('sentinel', 0)
for chan in self.Channel.UID_CHANNEL_DB:
if chan.name not in channel_to_dont_quit:
self.Protocol.send_part_chan(uidornickname=dnickname, channel=chan.name)
self.Protocol.send_priv_msg(dnickname, f"Sentinel mode deactivated on {channel}", channel=chan.name)
self.join_saved_channels()
return None

36
mods/defender/schemas.py Normal file
View File

@@ -0,0 +1,36 @@
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
sentinel: int = 0
@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] = []

167
mods/defender/threads.py Normal file
View File

@@ -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

723
mods/defender/utils.py Normal file
View File

@@ -0,0 +1,723 @@
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']
sender, reciever, channel, message = uplink.Protocol.parse_privmsg(srvmsg)
if uplink.ModConfig.sentinel == 1 and channel.name != uplink.Config.SERVICE_CHANLOG:
uplink.Protocol.send_priv_msg(uplink.Config.SERVICE_NICKNAME, f"{sender.nickname} say on {channel.name}: {' '.join(message)}", uplink.Config.SERVICE_CHANLOG)
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]):
"""Handle nickname changes.
>>> srvmsg = ['@unrealircd.org...', ':001MZQ0RB', 'NICK', 'newnickname', '1754663712']
>>> [':97KAAAAAC', 'NICK', 'testinspir', '1757360740']
Args:
irc_instance (Irc): The Irc instance
srvmsg (list[str]): The Server MSG
confmodel (ModConfModel): The Module Configuration
"""
p = uplink.Protocol
u, new_nickname, timestamp = p.parse_nick(srvmsg)
if u is None:
uplink.Logs.error(f"[USER OBJ ERROR {timestamp}] - {srvmsg}")
return None
uid = u.uid
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 = new_nickname
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]):
"""Handle on quit message
>>> srvmsg = ['@unrealircd.org...', ':001MZQ0RB', 'QUIT', ':Quit:', 'quit message']
Args:
uplink (Irc): The Defender Module instance
srvmsg (list[str]): The Server MSG
"""
p = uplink.Protocol
userobj, reason = p.parse_quit(srvmsg)
confmodel = uplink.ModConfig
if userobj is None:
uplink.Logs.debug(f"This UID do not exist anymore: {srvmsg}")
return None
ban_all_chan = uplink.Base.int_if_possible(confmodel.reputation_ban_all_chan)
jail_salon = uplink.Config.SALON_JAIL
service_id = uplink.Config.SERVICE_ID
get_user_reputation = uplink.Reputation.get_reputation(userobj.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(userobj.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
"""
_User = uplink.Protocol.parse_uid(srvmsg)
gconfig = uplink.Config
irc = uplink.Irc
confmodel = uplink.ModConfig
# If Init then do nothing
if gconfig.DEFENDER_INIT == 1:
return None
# Get User information
if _User is None:
irc.Logs.warning(f'Error when parsing UID', exc_info=True)
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 #
####################
# [:<sid>] UID <uid> <ts> <nick> <real-host> <displayed-host> <real-user> <ip> <signon> <modes> [<mode-parameters>]+ :<real>
# [:<sid>] UID nickname hopcount timestamp username hostname uid servicestamp umodes virthost cloakedhost ip :gecos
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
uid_to_clean = []
if reputation_flag == 0 or reputation_timer == 0 or not irc.Reputation.UID_REPUTATION_DB:
return None
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}')

236
mods/jsonrpc/mod_jsonrpc.py Normal file
View File

@@ -0,0 +1,236 @@
import logging
from unrealircd_rpc_py.objects.Definition import LiveRPCResult
from core.classes.interfaces.imodule import IModule
import mods.jsonrpc.utils as utils
import mods.jsonrpc.threads as thds
from dataclasses import dataclass
from unrealircd_rpc_py.ConnectionFactory import ConnectionFactory
from unrealircd_rpc_py.LiveConnectionFactory import LiveConnectionFactory
class Jsonrpc(IModule):
@dataclass
class ModConfModel:
"""The Model containing the module parameters
"""
jsonrpc: int = 0
MOD_HEADER: dict[str, str] = {
'name':'JsonRPC',
'version':'1.0.0',
'description':'Module using the unrealircd-rpc-py library',
'author':'Defender Team',
'core_version':'Defender-6'
}
def callback_sent_to_irc(self, response: LiveRPCResult) -> None:
dnickname = self.Config.SERVICE_NICKNAME
dchanlog = self.Config.SERVICE_CHANLOG
green = self.Config.COLORS.green
nogc = self.Config.COLORS.nogc
bold = self.Config.COLORS.bold
red = self.Config.COLORS.red
if response.error.code != 0:
self.Protocol.send_priv_msg(nick_from=dnickname,
msg=f"[{bold}{red}JSONRPC ERROR{nogc}{bold}] {response.error.message} ({response.error.code})",
channel=dchanlog)
return None
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 ''
event_id = response.result.event_id if hasattr(response.result, 'event_id') else ''
log_source = response.result.log_source if hasattr(response.result, 'log_source') else ''
msg = response.result.msg if hasattr(response.result, 'msg') else ''
build_msg = f"{green}{log_source}{nogc}: [{bold}{level}{bold}] {subsystem}.{event_id} - {msg}"
self.Protocol.send_priv_msg(nick_from=dnickname, msg=build_msg, channel=dchanlog)
return None
def create_tables(self) -> None:
return None
def load(self) -> None:
logging.getLogger('websockets').setLevel(logging.WARNING)
logging.getLogger('unrealircd-rpc-py').setLevel(logging.CRITICAL)
logging.getLogger('unrealircd-liverpc-py').setLevel(logging.CRITICAL)
self.ModConfig = self.ModConfModel(jsonrpc=0)
if self.Config.SERVEUR_PROTOCOL != 'unreal6':
self.Loader.ModuleUtils.unload_one_module(self.Irc, self.module_name, False)
return None
# 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')
try:
self.Rpc = ConnectionFactory(self.Config.DEBUG_LEVEL).get(self.Config.JSONRPC_METHOD)
self.LiveRpc = LiveConnectionFactory(self.Config.DEBUG_LEVEL).get(self.Config.JSONRPC_METHOD)
sync_unixsocket = {'path_to_socket_file': self.Config.JSONRPC_PATH_TO_SOCKET_FILE}
sync_http = {'url': self.Config.JSONRPC_URL, 'username': self.Config.JSONRPC_USER, 'password': self.Config.JSONRPC_PASSWORD}
live_unixsocket = {'path_to_socket_file': self.Config.JSONRPC_PATH_TO_SOCKET_FILE,
'callback_object_instance' : self, 'callback_method_or_function_name': 'callback_sent_to_irc'}
live_http = {'url': self.Config.JSONRPC_URL, 'username': self.Config.JSONRPC_USER, 'password': self.Config.JSONRPC_PASSWORD,
'callback_object_instance' : self, 'callback_method_or_function_name': 'callback_sent_to_irc'}
sync_param = sync_unixsocket if self.Config.JSONRPC_METHOD == 'unixsocket' else sync_http
live_param = live_unixsocket if self.Config.JSONRPC_METHOD == 'unixsocket' else live_http
self.Rpc.setup(sync_param)
self.LiveRpc.setup(live_param)
if self.ModConfig.jsonrpc == 1:
self.Base.create_thread(func=self.Threads.thread_subscribe, func_args=(self, ), run_once=True)
return None
except Exception as err:
self.Protocol.send_priv_msg(
nick_from=self.Config.SERVICE_NICKNAME,
msg=f"[{self.Config.COLORS.red}JSONRPC ERROR{self.Config.COLORS.nogc}] {err.__str__()}",
channel=self.Config.SERVICE_CHANLOG
)
self.Logs.error(f"JSONRPC ERROR: {err.__str__()}")
def unload(self) -> None:
if self.Config.SERVEUR_PROTOCOL != 'unreal6':
self.Loader.ModuleUtils.unload_one_module(self.Irc, self.module_name, False)
return None
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.Irc.Commands.drop_command_by_module(self.module_name)
self.Logs.debug(f"Unloading {self.module_name}")
return None
def cmd(self, data: list) -> None:
return None
def hcmds(self, user:str, channel: any, cmd: list, fullcmd: list = []) -> None:
command = str(cmd[0]).lower()
dnickname = self.Config.SERVICE_NICKNAME
dchannel = self.Config.SERVICE_CHANLOG
fromuser = user
fromchannel = str(channel) if not channel is None else None
match command:
case 'jsonrpc':
try:
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
self.Base.create_thread(func=self.Threads.thread_subscribe, func_args=(self, ), run_once=True)
self.update_configuration('jsonrpc', 1)
case 'off':
self.Base.create_thread(func=self.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:
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])
rpc = self.Rpc
UserInfo = rpc.User.get(nickname)
if UserInfo.error.code != 0:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'{UserInfo.error.message}')
return None
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.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}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'IP : {UserInfo.ip}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'COUNTRY CODE : {UserInfo.geoip.country_code}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'ASN : {UserInfo.geoip.asn}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'ASNAME : {UserInfo.geoip.asname}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'CLOAKED HOST : {UserInfo.user.cloakedhost}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'HOSTNAME : {UserInfo.hostname}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'VHOST : {UserInfo.user.vhost}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'CLIENT PORT : {UserInfo.client_port}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'SERVER PORT : {UserInfo.server_port}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'CERTFP : {UserInfo.tls.certfp}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'CIPHER : {UserInfo.tls.cipher}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'IDLE SINCE : {UserInfo.idle_since}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'CONNECTED SINCE : {UserInfo.connected_since}')
except IndexError as ie:
self.Logs.error(ie)
case 'jrinstances':
try:
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(LiveConnectionFactory)}")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Nombre d'instance ConnectionFactory: {self.MainUtils.get_number_gc_objects(ConnectionFactory)}")
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}")

53
mods/jsonrpc/threads.py Normal file
View File

@@ -0,0 +1,53 @@
import asyncio
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from mods.jsonrpc.mod_jsonrpc import Jsonrpc
def thread_subscribe(uplink: 'Jsonrpc') -> None:
snickname = uplink.Config.SERVICE_NICKNAME
schannel = uplink.Config.SERVICE_CHANLOG
uplink.is_streaming = True
response = asyncio.run(uplink.LiveRpc.subscribe(["all"]))
if response.error.code != 0:
uplink.Protocol.send_priv_msg(nick_from=snickname,
msg=f"[{uplink.Config.COLORS.red}JSONRPC ERROR{uplink.Config.COLORS.nogc}] {response.error.message}",
channel=schannel
)
code = response.error.code
message = response.error.message
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 = asyncio.run(uplink.LiveRpc.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
code = response.error.code
message = response.error.message
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
)

0
mods/jsonrpc/utils.py Normal file
View File

View File

@@ -1,439 +0,0 @@
from dataclasses import dataclass
import random, faker, time, logging
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.irc import Irc
class Clone():
@dataclass
class ModConfModel:
clone_nicknames: list[str]
def __init__(self, ircInstance: 'Irc') -> None:
# Module name (Mandatory)
self.module_name = 'mod_' + str(self.__class__.__name__).lower()
# Add Irc Object to the module (Mandatory)
self.Irc = ircInstance
# Add Irc Protocol Object to the module (Mandatory)
self.Protocol = ircInstance.Protocol
# Add Global Configuration to the module (Mandatory)
self.Config = ircInstance.Config
# Add Base object to the module (Mandatory)
self.Base = ircInstance.Base
# Add logs object to the module (Mandatory)
self.Logs = ircInstance.Base.logs
# Add User object to the module (Mandatory)
self.User = ircInstance.User
# Add Channel object to the module (Mandatory)
self.Channel = ircInstance.Channel
# Add clone object to the module (Optionnal)
self.Clone = ircInstance.Clone
self.Definition = ircInstance.Loader.Definition
# Créer les nouvelles commandes du module
self.Irc.build_command(1, self.module_name, 'clone', 'Connect, join, part, kill and say clones')
# Init the module (Mandatory)
self.__init_module()
# Log the module
self.Logs.debug(f'Module {self.module_name} loaded ...')
def __init_module(self) -> None:
# Créer les tables necessaire a votre module (ce n'es pas obligatoire)
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()
self.Channel.db_query_channel(action='add', module_name=self.module_name, channel_name=self.Config.CLONE_CHANNEL)
self.Protocol.send_join_chan(self.Config.SERVICE_NICKNAME, self.Config.CLONE_CHANNEL)
self.Protocol.send2socket(f":{self.Config.SERVICE_NICKNAME} SAMODE {self.Config.CLONE_CHANNEL} +o {self.Config.SERVICE_NICKNAME}")
self.Protocol.send2socket(f":{self.Config.SERVICE_NICKNAME} MODE {self.Config.CLONE_CHANNEL} +nts")
self.Protocol.send2socket(f":{self.Config.SERVICE_NICKNAME} MODE {self.Config.CLONE_CHANNEL} +k {self.Config.CLONE_CHANNEL_PASSWORD}")
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
"""
table_channel = '''CREATE TABLE IF NOT EXISTS clone_list (
id INTEGER PRIMARY KEY AUTOINCREMENT,
datetime TEXT,
nickname TEXT,
username TEXT
)
'''
# self.Base.db_execute_query(table_channel)
return None
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(
clone_nicknames=[]
)
# 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)
def unload(self) -> None:
"""Cette methode sera executée a chaque désactivation ou
rechargement de module
"""
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")
self.Protocol.send2socket(f":{self.Config.SERVICE_NICKNAME} MODE {self.Config.CLONE_CHANNEL} -k {self.Config.CLONE_CHANNEL_PASSWORD}")
self.Protocol.send_part_chan(self.Config.SERVICE_NICKNAME, self.Config.CLONE_CHANNEL)
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:
return None
match cmd[1]:
case 'REPUTATION':
pass
if len(cmd) < 3:
return None
match cmd[2]:
case 'PRIVMSG':
# print(cmd)
uid_sender = self.User.clean_uid(cmd[1])
senderObj = self.User.get_User(uid_sender)
if senderObj.hostname in self.Config.CLONE_LOG_HOST_EXEMPT:
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
)
except Exception as err:
self.Logs.error(f'General Error: {err}')
def hcmds(self, user:str, channel: any, cmd: list, fullcmd: list = []) -> None:
try:
command = str(cmd[0]).lower()
fromuser = user
dnickname = self.Config.SERVICE_NICKNAME # Defender nickname
match command:
case '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 list")
option = str(cmd[1]).lower()
match option:
case 'connect':
try:
# clone connect 5 GroupName 3
self.stop = False
number_of_clones = int(cmd[2])
group = str(cmd[3]).lower()
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)
)
except Exception as err:
self.Logs.error(f'{err}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} clone connect [number of clone you want to connect] [Group]")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Exemple /msg {dnickname} clone connect 6 Ambiance")
case 'kill':
try:
# clone kill [all | nickname]
self.stop = True
clone_name = str(cmd[2])
clone_to_kill: list[str] = []
if clone_name.lower() == 'all':
self.Base.create_thread(func=self.thread_kill_clones, func_args=(fromuser, ))
else:
cloneObj = self.Clone.get_Clone(clone_name)
if not cloneObj is None:
self.Protocol.send_quit(cloneObj.uid, 'Goood bye', print_log=False)
except Exception as err:
self.Logs.error(f'{err}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} clone kill all")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} clone kill clone_nickname")
case 'join':
try:
# clone join [all | nickname] #channel
clone_name = str(cmd[2])
clone_channel_to_join = str(cmd[3])
if clone_name.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)
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)
except Exception as err:
self.Logs.error(f'{err}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} clone join all #channel")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} clone join clone_nickname #channel")
case 'part':
try:
# clone part [all | nickname] #channel
clone_name = str(cmd[2])
clone_channel_to_part = str(cmd[3])
if clone_name.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)
else:
if self.Clone.exists(clone_name):
clone_uid = self.Clone.get_uid(clone_name)
if not clone_uid is None:
self.Protocol.send_part_chan(uidornickname=clone_uid, channel=clone_channel_to_part, print_log=False)
except Exception as err:
self.Logs.error(f'{err}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} clone part all #channel")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} clone part clone_nickname #channel")
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}")
except Exception as err:
self.Logs.error(f'{err}')
case 'say':
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
final_message = ' '.join(cmd[4:])
if clone_channel is None or not self.Clone.exists(clone_name):
self.Protocol.send_notice(
nick_from=dnickname,
nick_to=fromuser,
msg=f"/msg {dnickname} clone say [clone_nickname] #channel message"
)
return None
if self.Clone.exists(clone_name):
self.Protocol.send_priv_msg(nick_from=clone_name, msg=final_message, channel=clone_channel)
except Exception as err:
self.Logs.error(f'{err}')
self.Protocol.send_notice(
nick_from=dnickname,
nick_to=fromuser,
msg=f"/msg {dnickname} clone say [clone_nickname] #channel message"
)
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 list")
except IndexError as ie:
self.Logs.error(f'Index Error: {ie}')
except Exception as err:
self.Logs.error(f'Index Error: {err}')

File diff suppressed because it is too large Load Diff

View File

@@ -1,317 +0,0 @@
import logging
from typing import TYPE_CHECKING
from dataclasses import dataclass
from unrealircd_rpc_py.Live import Live
from unrealircd_rpc_py.Loader import Loader
if TYPE_CHECKING:
from core.irc import Irc
class Jsonrpc():
@dataclass
class ModConfModel:
"""The Model containing the module parameters
"""
jsonrpc: int = 0
def __init__(self, ircInstance: 'Irc') -> None:
# Module name (Mandatory)
self.module_name = 'mod_' + str(self.__class__.__name__).lower()
# Add Irc Object to the module (Mandatory)
self.Irc = ircInstance
# Add Protocol to the module (Mandatory)
self.Protocol = ircInstance.Protocol
# Add Global Configuration to the module (Mandatory)
self.Config = ircInstance.Config
# Add Base object to the module (Mandatory)
self.Base = ircInstance.Base
# Add logs object to the module (Mandatory)
self.Logs = ircInstance.Base.logs
# Add User object to the module (Mandatory)
self.User = ircInstance.User
# Add Channel object to the module (Mandatory)
self.Channel = ircInstance.Channel
# 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')
# Init the module
self.__init_module()
# Log the module
self.Logs.debug(f'Module {self.module_name} loaded ...')
def __init_module(self) -> None:
logging.getLogger('websockets').setLevel(logging.WARNING)
logging.getLogger('unrealircd-rpc-py').setLevel(logging.CRITICAL)
# Create you own tables (Mandatory)
# self.__create_tables()
# Load module configuration and sync with core one (Mandatory)
self.__load_module_configuration()
# End of mandatory methods you can start your customization #
self.UnrealIrcdRpcLive: Live = Live(
req_method='websocket',
url=self.Config.JSONRPC_URL,
username=self.Config.JSONRPC_USER,
password=self.Config.JSONRPC_PASSWORD,
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.Protocol.send_priv_msg(
nick_from=self.Config.SERVICE_NICKNAME,
msg=f"[{self.Config.COLORS.red}ERROR{self.Config.COLORS.nogc}] {self.Rpc.Error.message}",
channel=self.Config.SERVICE_CHANLOG
)
if self.UnrealIrcdRpcLive.Error.code != 0:
self.Protocol.send_priv_msg(
nick_from=self.Config.SERVICE_NICKNAME,
msg=f"[{self.Config.COLORS.red}ERROR{self.Config.COLORS.nogc}] {self.UnrealIrcdRpcLive.Error.message}",
channel=self.Config.SERVICE_CHANLOG
)
if self.ModConfig.jsonrpc == 1:
self.Base.create_thread(self.thread_start_jsonrpc, run_once=True)
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
"""
table_logs = '''CREATE TABLE IF NOT EXISTS test_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
datetime TEXT,
server_msg TEXT
)
'''
self.Base.db_execute_query(table_logs)
return None
def callback_sent_to_irc(self, json_response: str):
dnickname = self.Config.SERVICE_NICKNAME
dchanlog = self.Config.SERVICE_CHANLOG
green = self.Config.COLORS.green
nogc = self.Config.COLORS.nogc
bold = self.Config.COLORS.bold
red = self.Config.COLORS.red
if json_response.result == True:
self.Protocol.send_priv_msg(
nick_from=self.Config.SERVICE_NICKNAME,
msg=f"[{bold}{green}JSONRPC{nogc}{bold}] Event activated",
channel=dchanlog)
return None
level = json_response.result.level
subsystem = json_response.result.subsystem
event_id = json_response.result.event_id
log_source = json_response.result.log_source
msg = json_response.result.msg
build_msg = f"{green}{log_source}{nogc}: [{bold}{level}{bold}] {subsystem}.{event_id} - {msg}"
self.Protocol.send_priv_msg(nick_from=dnickname, msg=build_msg, channel=dchanlog)
def thread_start_jsonrpc(self):
if self.UnrealIrcdRpcLive.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.Error.message}",
channel=self.Config.SERVICE_CHANLOG
)
def __load_module_configuration(self) -> None:
"""### Load Module Configuration
"""
try:
# Build the default configuration model (Mandatory)
self.ModConfig = self.ModConfModel(jsonrpc=0)
# 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)
def __update_configuration(self, param_key: str, param_value: str):
"""Update the local and core configuration
Args:
param_key (str): The parameter key
param_value (str): The parameter value
"""
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()
return None
def cmd(self, data:list) -> None:
return None
def hcmds(self, user:str, channel: any, cmd: list, fullcmd: list = []) -> None:
command = str(cmd[0]).lower()
dnickname = self.Config.SERVICE_NICKNAME
dchannel = self.Config.SERVICE_CHANLOG
fromuser = user
fromchannel = str(channel) if not channel is None else None
match command:
case 'jsonrpc':
try:
option = str(cmd[1]).lower()
if len(command) == 1:
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')
match option:
case 'on':
# 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.getName() == 'thread_start_jsonrpc':
if thread.is_alive():
self.Protocol.send_priv_msg(
nick_from=self.Config.SERVICE_NICKNAME,
msg=f"Thread {thread.getName()} is running",
channel=dchannel
)
else:
self.Protocol.send_priv_msg(
nick_from=self.Config.SERVICE_NICKNAME,
msg=f"Thread {thread.getName()} 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)
case 'off':
self.UnrealIrcdRpcLive.unsubscribe()
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:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'/msg {dnickname} jruser get nickname')
match option:
case 'get':
nickname = str(cmd[2])
uid_to_get = self.User.get_uid(nickname)
if uid_to_get is None:
return None
rpc = self.Rpc
UserInfo = rpc.User.get(uid_to_get)
if rpc.Error.code != 0:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'{rpc.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'SECURITY GROUP : {UserInfo.user.security_groups}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'REPUTATION : {UserInfo.user.reputation}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'IP : {UserInfo.ip}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'COUNTRY CODE : {UserInfo.geoip.country_code}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'ASN : {UserInfo.geoip.asn}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'ASNAME : {UserInfo.geoip.asname}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'CLOAKED HOST : {UserInfo.user.cloakedhost}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'HOSTNAME : {UserInfo.hostname}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'VHOST : {UserInfo.user.vhost}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'CLIENT PORT : {UserInfo.client_port}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'SERVER PORT : {UserInfo.server_port}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'CERTFP : {UserInfo.tls.certfp}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'CIPHER : {UserInfo.tls.cipher}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'IDLE SINCE : {UserInfo.idle_since}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'CONNECTED SINCE : {UserInfo.connected_since}')
except IndexError as ie:
self.Logs.error(ie)
case 'ia':
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")
except Exception as err:
self.Logs.error(f"Unknown Error: {err}")

View File

@@ -1,157 +0,0 @@
from typing import TYPE_CHECKING
from dataclasses import dataclass, fields
if TYPE_CHECKING:
from core.irc import Irc
class Test():
@dataclass
class ModConfModel:
"""The Model containing the module parameters
"""
param_exemple1: str
param_exemple2: int
def __init__(self, ircInstance: 'Irc') -> None:
# Module name (Mandatory)
self.module_name = 'mod_' + str(self.__class__.__name__).lower()
# Add Irc Object to the module (Mandatory)
self.Irc = ircInstance
# Add Loader Object to the module (Mandatory)
self.Loader = ircInstance.Loader
# Add server protocol Object to the module (Mandatory)
self.Protocol = ircInstance.Protocol
# Add Global Configuration to the module (Mandatory)
self.Config = ircInstance.Config
# Add Base object to the module (Mandatory)
self.Base = ircInstance.Base
# Add logs object to the module (Mandatory)
self.Logs = ircInstance.Base.logs
# Add User object to the module (Mandatory)
self.User = ircInstance.User
# Add Channel object to the module (Mandatory)
self.Channel = ircInstance.Channel
# Add Reputation object to the module (Optional)
self.Reputation = ircInstance.Reputation
# Create module commands (Mandatory)
self.Irc.build_command(0, self.module_name, 'test-command', 'Execute a test command')
self.Irc.build_command(1, self.module_name, 'test_level_1', 'Execute a level 1 test command')
self.Irc.build_command(2, self.module_name, 'test_level_2', 'Execute a level 2 test command')
self.Irc.build_command(3, self.module_name, 'test_level_3', 'Execute a level 3 test command')
# Init the module
self.__init_module()
# Log the module
self.Logs.debug(f'Module {self.module_name} loaded ...')
def __init_module(self) -> None:
# Create you own tables (Mandatory)
self.__create_tables()
# Load module configuration and sync with core one (Mandatory)
self.__load_module_configuration()
# End of mandatory methods you can start your customization #
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
"""
table_logs = '''CREATE TABLE IF NOT EXISTS test_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
datetime TEXT,
server_msg TEXT
)
'''
self.Base.db_execute_query(table_logs)
return None
def __load_module_configuration(self) -> None:
"""### Load Module Configuration
"""
try:
# Build the default configuration model (Mandatory)
self.ModConfig = self.ModConfModel(param_exemple1='param value 1', param_exemple2=1)
# 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)
def __update_configuration(self, param_key: str, param_value: str):
"""Update the local and core configuration
Args:
param_key (str): The parameter key
param_value (str): The parameter value
"""
self.Base.db_update_core_config(self.module_name, self.ModConfig, param_key, param_value)
def unload(self) -> None:
return None
def cmd(self, data:list) -> None:
try:
cmd = list(data).copy()
return None
except KeyError as ke:
self.Logs.error(f"Key Error: {ke}")
except IndexError as ie:
self.Logs.error(f"{ie} / {cmd} / length {str(len(cmd))}")
except Exception as err:
self.Logs.error(f"General Error: {err}")
def hcmds(self, user:str, channel: any, cmd: list, fullcmd: list = []) -> None:
command = str(cmd[0]).lower()
dnickname = self.Config.SERVICE_NICKNAME
fromuser = user
fromchannel = str(channel) if not channel is None else None
match command:
case 'test-command':
try:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg="This is a notice to the sender ...")
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"This is private message to the sender ...", nick_to=fromuser)
if not fromchannel is None:
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"This is private 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")
except Exception as err:
self.Logs.error(f"Unknown Error: {err}")

110
mods/test/mod_test.py Normal file
View File

@@ -0,0 +1,110 @@
from typing import Any
from core.classes.interfaces.imodule import IModule
from dataclasses import dataclass
class Test(IModule):
@dataclass
class ModConfModel:
"""The Model containing the module parameters (Mandatory)
you can leave it without params.
just use pass | if you leave it empty, in the load() method just init empty object ==> self.ModConfig = ModConfModel()
"""
param_exemple1: str
param_exemple2: int
MOD_HEADER: dict[str, str] = {
'name':'Test',
'version':'1.0.0',
'description':'The test module',
'author':'Defender Team',
'core_version':'Defender-6'
}
"""Module Header (Mandatory)"""
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
"""
table_logs = '''CREATE TABLE IF NOT EXISTS test_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
datetime TEXT,
server_msg TEXT
)
'''
# self.Base.db_execute_query(table_logs)
return None
def load(self) -> None:
"""### Load Module Configuration (Mandatory)
"""
# Create module commands (Mandatory)
self.Irc.build_command(0, self.module_name, 'test-command', 'Execute a test command')
self.Irc.build_command(1, self.module_name, 'test_level_1', 'Execute a level 1 test command')
self.Irc.build_command(2, self.module_name, 'test_level_2', 'Execute a level 2 test command')
self.Irc.build_command(3, self.module_name, 'test_level_3', 'Execute a level 3 test command')
# Build the default configuration model (Mandatory)
self.ModConfig = self.ModConfModel(param_exemple1='str', param_exemple2=1)
def unload(self) -> None:
"""### This method is called when you unload or you reload the module (Mandatory)"""
self.Irc.Commands.drop_command_by_module(self.module_name)
return None
def cmd(self, data: list[str]) -> None:
"""All messages coming from the IRCD server will be handled using this method (Mandatory)
Args:
data (list): Messages coming from the IRCD server.
"""
cmd = list(data).copy()
try:
return None
except Exception as err:
self.Logs.error(f"General Error: {err}")
def hcmds(self, user: str, channel: Any, cmd: list, fullcmd: list = []) -> None:
"""All messages coming from the user commands (Mandatory)
Args:
user (str): The user who send the request.
channel (Any): The channel from where is coming the message (could be None).
cmd (list): The messages coming from the IRCD server.
fullcmd (list, optional): The full messages coming from the IRCD server. Defaults to [].
"""
u = self.User.get_user(user)
c = self.Channel.get_channel(channel) if self.Channel.is_valid_channel(channel) else None
if u is None:
return None
command = str(cmd[0]).lower()
dnickname = self.Config.SERVICE_NICKNAME
match command:
case 'test-command':
try:
self.Protocol.send_notice(nick_from=dnickname, nick_to=u.nickname, msg="This is a notice to the sender ...")
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"This is private message to the sender ...", nick_to=u.nickname)
if c is not None:
self.Protocol.send_priv_msg(nick_from=dnickname, msg=f"This is private message to the sender ...", channel=c.name)
# How to update your module configuration
self.update_configuration('param_exemple2', 7)
self.update_configuration('param_exemple1', 'my_value')
# Log if you want the result
self.Logs.debug(f"Test logs ready")
except Exception as err:
self.Logs.error(f"Unknown Error: {err}")

View File

@@ -1,83 +1,39 @@
from typing import TYPE_CHECKING """
File : mod_votekick.py
Version : 1.0.2
Description : Manages votekick sessions for multiple channels.
Handles activation, ongoing vote checks, and cleanup.
Author : adator
Created : 2025-08-16
Last Updated: 2025-11-01
-----------------------------------------
"""
from dataclasses import dataclass
import re import re
from dataclasses import dataclass, field from core.classes.interfaces.imodule import IModule
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 Any, Optional
if TYPE_CHECKING: class Votekick(IModule):
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():
@dataclass @dataclass
class VoteChannelModel: class ModConfModel(schemas.VoteChannelModel):
channel_name: str ...
target_user: str
voter_users: list
vote_for: int
vote_against: int
VOTE_CHANNEL_DB:list[VoteChannelModel] = [] MOD_HEADER: dict[str, str] = {
'name':'votekick',
'version':'1.0.2',
'description':'Channel Democraty',
'author':'Defender Team',
'core_version':'Defender-6'
}
def __init__(self, ircInstance: 'Irc') -> None: def create_tables(self) -> None:
# Module name (Mandatory)
self.module_name = 'mod_' + str(self.__class__.__name__).lower()
# Add Irc Object to the module
self.Irc = ircInstance
# Add Loader Object to the module (Mandatory)
self.Loader = ircInstance.Loader
# Add server protocol Object to the module (Mandatory)
self.Protocol = ircInstance.Protocol
# Add Global Configuration to the module
self.Config = ircInstance.Config
# Add Base object to the module
self.Base = ircInstance.Base
# Add logs object to the module
self.Logs = ircInstance.Base.logs
# Add User object to the module
self.User = ircInstance.User
# Add Channel object to the module
self.Channel = ircInstance.Channel
# Créer les nouvelles commandes du module
self.Irc.build_command(1, self.module_name, 'vote', 'The kick vote module')
# Init the module
self.__init_module()
# Log the module
self.Logs.debug(f'-- Module {self.module_name} loaded ...')
def __init_module(self) -> None:
# Add admin object to retrieve admin users
self.Admin = self.Irc.Admin
self.__create_tables()
self.join_saved_channels()
return None
def __create_tables(self) -> None:
"""Methode qui va créer la base de donnée si elle n'existe pas. """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 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: Returns:
None: Aucun retour n'es attendu None: Aucun retour n'es attendu
@@ -101,13 +57,45 @@ class Votekick():
self.Base.db_execute_query(table_vote) self.Base.db_execute_query(table_vote)
return None return None
def load(self) -> None:
self.ModConfig = self.ModConfModel()
# Add VoteKick Manager
self.VoteKickManager = VotekickManager(self)
# Add Utils module
self.ModUtils = utils
# Add Schemas module
self.Schemas = schemas
# Add Threads module
self.Threads = thds
self.ModUtils.join_saved_channels(self)
metadata = self.Settings.get_cache('VOTEKICK')
if metadata is not None:
self.VoteKickManager.VOTE_CHANNEL_DB = metadata
# Créer les nouvelles commandes du module
self.Irc.build_command(1, self.module_name, 'vote', 'The kick vote module')
def unload(self) -> None: def unload(self) -> None:
try: try:
for chan in self.VOTE_CHANNEL_DB: # Cache the local DB with current votes.
if self.VoteKickManager.VOTE_CHANNEL_DB:
self.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.Protocol.send_part_chan(uidornickname=self.Config.SERVICE_ID, channel=chan.channel_name)
self.VOTE_CHANNEL_DB = [] self.VoteKickManager.VOTE_CHANNEL_DB = []
self.Logs.debug(f'Delete memory DB VOTE_CHANNEL_DB: {self.VOTE_CHANNEL_DB}') self.Logs.debug(f'Delete memory DB VOTE_CHANNEL_DB: {self.VoteKickManager.VOTE_CHANNEL_DB}')
self.Irc.Commands.drop_command_by_module(self.module_name)
return None return None
except UnboundLocalError as ne: except UnboundLocalError as ne:
@@ -117,136 +105,27 @@ class Votekick():
except Exception as err: except Exception as err:
self.Logs.error(f'General Error: {err}') self.Logs.error(f'General Error: {err}')
def init_vote_system(self, channel: str) -> bool:
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
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):
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: def cmd(self, data: list) -> None:
if not data or len(data) < 2:
return None
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
try: try:
cmd = list(data).copy()
match command:
case 'PRIVMSG':
return None
case 'QUIT':
return None
case _:
return None return None
except KeyError as ke: except KeyError as ke:
@@ -256,11 +135,12 @@ class Votekick():
except Exception as err: except Exception as err:
self.Logs.error(f"General Error: {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 # cmd is the command starting from the user command
# full cmd is sending the entire server response # full cmd is sending the entire server response
command = str(cmd[0]).lower() command = str(cmd[0]).lower()
fullcmd = fullcmd
dnickname = self.Config.SERVICE_NICKNAME dnickname = self.Config.SERVICE_NICKNAME
fromuser = user fromuser = user
fromchannel = channel fromchannel = channel
@@ -287,32 +167,25 @@ class Votekick():
case 'activate': case 'activate':
try: try:
# vote activate #channel # 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') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' :Your are not allowed to execute this command')
return None 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: 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.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( if self.VoteKickManager.activate_new_channel(sentchannel):
self.VoteChannelModel(
channel_name=sentchannel,
target_user='',
voter_users=[],
vote_for=0,
vote_against=0
)
)
self.Channel.db_query_channel('add', self.module_name, sentchannel) self.Channel.db_query_channel('add', self.module_name, sentchannel)
self.Protocol.send_join_chan(uidornickname=dnickname, channel=sentchannel) self.Protocol.send_join_chan(uidornickname=dnickname, channel=sentchannel)
self.Protocol.send2socket(f":{dnickname} SAMODE {sentchannel} +o {dnickname}") self.Protocol.send2socket(f":{dnickname} SAMODE {sentchannel} +o {dnickname}")
self.Protocol.send_priv_msg(nick_from=dnickname, self.Protocol.send_priv_msg(nick_from=dnickname,
msg="You can now use !submit <nickname> to decide if he will stay or not on this channel ", msg="You can now use !submit <nickname> to decide if he will stay or not on this channel ",
channel=sentchannel channel=sentchannel
) )
return None
except Exception as err: except Exception as err:
self.Logs.error(f'{err}') self.Logs.error(f'{err}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} {command} {option} #channel') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} {command} {option} #channel')
@@ -321,23 +194,21 @@ class Votekick():
case 'deactivate': case 'deactivate':
try: try:
# vote deactivate #channel # 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") self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f" Your are not allowed to execute this command")
return None 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: 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.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.send2socket(f":{dnickname} SAMODE {sentchannel} -o {dnickname}")
self.Protocol.send_part_chan(uidornickname=dnickname, channel=sentchannel) self.Protocol.send_part_chan(uidornickname=dnickname, channel=sentchannel)
for chan in self.VOTE_CHANNEL_DB: if self.VoteKickManager.drop_vote_channel_model(sentchannel):
if chan.channel_name == sentchannel: self.Channel.db_query_channel('del', self.module_name, sentchannel)
self.VOTE_CHANNEL_DB.remove(chan) return None
self.Channel.db_query_channel('del', self.module_name, chan.channel_name)
self.Logs.debug(f"The Channel {sentchannel} has been deactivated from the vote system")
except Exception as err: except Exception as err:
self.Logs.error(f'{err}') self.Logs.error(f'{err}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f" /msg {dnickname} {command} {option} #channel") self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f" /msg {dnickname} {command} {option} #channel")
@@ -347,20 +218,11 @@ class Votekick():
try: try:
# vote + # vote +
channel = fromchannel channel = fromchannel
for chan in self.VOTE_CHANNEL_DB: if self.VoteKickManager.action_vote(channel, fromuser, '+'):
if chan.channel_name == channel: self.Protocol.send_priv_msg(nick_from=dnickname, msg="Vote recorded, thank you",channel=channel)
if fromuser in chan.voter_users:
self.Protocol.send_priv_msg(nick_from=dnickname,
msg="You already submitted a vote",
channel=channel
)
else: else:
chan.vote_for += 1 self.Protocol.send_priv_msg(nick_from=dnickname, msg="You already submitted a vote", channel=channel)
chan.voter_users.append(fromuser)
self.Protocol.send_priv_msg(nick_from=dnickname,
msg="Vote recorded, thank you",
channel=channel
)
except Exception as err: except Exception as err:
self.Logs.error(f'{err}') self.Logs.error(f'{err}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} {command} {option}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} {command} {option}')
@@ -370,20 +232,11 @@ class Votekick():
try: try:
# vote - # vote -
channel = fromchannel channel = fromchannel
for chan in self.VOTE_CHANNEL_DB: if self.VoteKickManager.action_vote(channel, fromuser, '-'):
if chan.channel_name == channel: self.Protocol.send_priv_msg(nick_from=dnickname, msg="Vote recorded, thank you",channel=channel)
if fromuser in chan.voter_users:
self.Protocol.send_priv_msg(nick_from=dnickname,
msg="You already submitted a vote",
channel=channel
)
else: else:
chan.vote_against += 1 self.Protocol.send_priv_msg(nick_from=dnickname, msg="You already submitted a vote", channel=channel)
chan.voter_users.append(fromuser)
self.Protocol.send_priv_msg(nick_from=dnickname,
msg="Vote recorded, thank you",
channel=channel
)
except Exception as err: except Exception as err:
self.Logs.error(f'{err}') self.Logs.error(f'{err}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} {command} {option}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} {command} {option}')
@@ -392,7 +245,7 @@ class Votekick():
case 'cancel': case 'cancel':
try: try:
# vote cancel # 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') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' Your are not allowed to execute this command')
return None return None
@@ -400,9 +253,9 @@ class Votekick():
self.Logs.error(f"The channel is not known, defender can't cancel the vote") 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') 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: if vote.channel_name == channel:
self.init_vote_system(channel) if self.VoteKickManager.init_vote_system(channel):
self.Protocol.send_priv_msg(nick_from=dnickname, self.Protocol.send_priv_msg(nick_from=dnickname,
msg="Vote system re-initiated", msg="Vote system re-initiated",
channel=channel channel=channel
@@ -416,7 +269,7 @@ class Votekick():
case 'status': case 'status':
try: try:
# vote status # vote status
for chan in self.VOTE_CHANNEL_DB: for chan in self.VoteKickManager.VOTE_CHANNEL_DB:
if chan.channel_name == channel: if chan.channel_name == channel:
self.Protocol.send_priv_msg(nick_from=dnickname, 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))}", 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 +283,25 @@ class Votekick():
case 'submit': case 'submit':
try: try:
# vote submit nickname # 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') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' Your are not allowed to execute this command')
return None return None
nickname_submitted = cmd[2] nickname_submitted = cmd[2]
uid_submitted = self.User.get_uid(nickname_submitted) uid_submitted = self.User.get_uid(nickname_submitted)
user_submitted = self.User.get_User(nickname_submitted) user_submitted = self.User.get_user(nickname_submitted)
ongoing_user = None
# check if there is an ongoing vote # check if there is an ongoing vote
if self.is_vote_ongoing(channel): if self.VoteKickManager.is_vote_ongoing(channel):
for vote in self.VOTE_CHANNEL_DB: votec = self.VoteKickManager.get_vote_channel_model(channel)
if vote.channel_name == channel: if votec:
ongoing_user = self.User.get_nickname(vote.target_user) ongoing_user = self.User.get_nickname(votec.target_user)
self.Protocol.send_priv_msg(nick_from=dnickname, self.Protocol.send_priv_msg(nick_from=dnickname,
msg=f"There is an ongoing vote on {ongoing_user}", msg=f"There is an ongoing vote on {ongoing_user}",
channel=channel channel=channel
) )
return False return None
# check if the user exist # check if the user exist
if user_submitted is None: if user_submitted is None:
@@ -456,24 +309,24 @@ class Votekick():
msg=f"This nickname <{nickname_submitted}> do not exist", msg=f"This nickname <{nickname_submitted}> do not exist",
channel=channel channel=channel
) )
return False return None
uid_cleaned = self.Base.clean_uid(uid_submitted) uid_cleaned = self.Loader.Utils.clean_uid(uid_submitted)
ChannelInfo = self.Channel.get_Channel(channel) channel_obj = self.Channel.get_channel(channel)
if ChannelInfo is None: 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') 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 = [] clean_uids_in_channel: list = []
for uid in ChannelInfo.uids: for uid in channel_obj.uids:
clean_uids_in_channel.append(self.Base.clean_uid(uid)) clean_uids_in_channel.append(self.Loader.Utils.clean_uid(uid))
if not uid_cleaned in clean_uids_in_channel: if not uid_cleaned in clean_uids_in_channel:
self.Protocol.send_priv_msg(nick_from=dnickname, self.Protocol.send_priv_msg(nick_from=dnickname,
msg=f"This nickname <{nickname_submitted}> is not available in this channel", msg=f"This nickname <{nickname_submitted}> is not available in this channel",
channel=channel channel=channel
) )
return False return None
# check if Ircop or Service or Bot # check if Ircop or Service or Bot
pattern = fr'[o|B|S]' pattern = fr'[o|B|S]'
@@ -483,9 +336,9 @@ class Votekick():
msg="You cant vote for this user ! he/she is protected", msg="You cant vote for this user ! he/she is protected",
channel=channel 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: if chan.channel_name == channel:
chan.target_user = self.User.get_uid(nickname_submitted) chan.target_user = self.User.get_uid(nickname_submitted)
@@ -494,7 +347,7 @@ class Votekick():
channel=channel 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, self.Protocol.send_priv_msg(nick_from=dnickname,
msg="This vote will end after 60 secondes", msg="This vote will end after 60 secondes",
channel=channel channel=channel
@@ -508,33 +361,34 @@ class Votekick():
case 'verdict': case 'verdict':
try: try:
# vote verdict # 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') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f'Your are not allowed to execute this command')
return None return None
for chan in self.VOTE_CHANNEL_DB: votec = self.VoteKickManager.get_vote_channel_model(channel)
if chan.channel_name == channel: if votec:
target_user = self.User.get_nickname(chan.target_user) target_user = self.User.get_nickname(votec.target_user)
if chan.vote_for > chan.vote_against: if votec.vote_for >= votec.vote_against:
self.Protocol.send_priv_msg(nick_from=dnickname, 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", 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 channel=channel
) )
self.Protocol.send2socket(f":{dnickname} KICK {channel} {target_user} Following the vote, you are not welcome in {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: else:
self.Protocol.send_priv_msg( self.Protocol.send_priv_msg(
nick_from=dnickname, 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 channel=channel
) )
# Init the system if self.VoteKickManager.init_vote_system(channel):
if self.init_vote_system(channel):
self.Protocol.send_priv_msg( self.Protocol.send_priv_msg(
nick_from=dnickname, nick_from=dnickname,
msg="System vote re initiated", msg="System vote re initiated",
channel=channel channel=channel
) )
return None
except Exception as err: except Exception as err:
self.Logs.error(f'{err}') self.Logs.error(f'{err}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} {command} {option}') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} {command} {option}')
@@ -549,3 +403,7 @@ class Votekick():
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 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 submit nickname')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote verdict') self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote verdict')
return None
case _:
return None

11
mods/votekick/schemas.py Normal file
View File

@@ -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

40
mods/votekick/threads.py Normal file
View File

@@ -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

74
mods/votekick/utils.py Normal file
View File

@@ -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

View File

@@ -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.MainUtils
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

6
requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
Faker==37.12.0
psutil==7.1.2
PyYAML==6.0.3
requests==2.32.5
SQLAlchemy==2.0.44
unrealircd_rpc_py==3.0.2

View File

@@ -1,9 +1,10 @@
{ {
"version": "6.1.2", "version": "6.3.3",
"requests": "2.32.3", "requests": "2.32.5",
"psutil": "6.0.0", "psutil": "7.1.2",
"unrealircd_rpc_py": "1.0.7", "unrealircd_rpc_py": "3.0.1",
"sqlalchemy": "2.0.35", "sqlalchemy": "2.0.44",
"faker": "30.1.0" "faker": "37.12.0",
"pyyaml": "6.0.3"
} }