195 Commits

Author SHA1 Message Date
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
adator
be36c56ceb Merge pull request #73 from adator85/update/protocol
Update/protocol
2025-07-20 19:44:49 +02:00
adator
ac332e6802 Update version number 2025-07-20 19:42:59 +02:00
adator
6063ceba35 Update Protocol Connection 2025-07-20 19:41:10 +02:00
adator
2204c4fdf8 Merge pull request #72 from adator85/V6.X.X
V6.1.1 Updates
2024-12-08 23:03:01 +01:00
adator
befe452df8 V6.1.1 Updates 2024-12-08 23:02:36 +01:00
adator
b98a20ad45 Merge pull request #71 from adator85/V6.X.X
V6.1.0 update the help command
2024-12-08 12:56:18 +01:00
adator
cb042a5411 V6.1.0 update the help command 2024-12-08 12:52:36 +01:00
adator
dc63df08cf Merge pull request #70 from adator85/V6.X.X
Update on_version method
2024-11-22 23:27:15 +01:00
adator
a3edf48120 Update on_version method 2024-11-22 23:26:39 +01:00
adator
a3a61c332f Merge pull request #69 from adator85/V6.X.X
V6.0.4
2024-11-18 23:50:25 +01:00
adator
f7664c9874 V6.0.4 2024-11-18 23:49:45 +01:00
adator
eeaacddbf2 Merge pull request #68 from adator85/V6.X.X
Update to version 6.0.3
2024-11-17 21:09:51 +01:00
adator
d37c152160 Update to version 6.0.3 2024-11-17 21:09:14 +01:00
adator
39412fc1c0 Merge pull request #67 from adator85/V6.X.X
V6.0.2
2024-11-15 22:14:55 +01:00
adator
b81f502b95 V6.0.2 2024-11-15 22:14:11 +01:00
adator
e148659d00 Merge pull request #66 from adator85/V6.X.X
V6.0.1
2024-11-11 23:38:26 +01:00
adator
44da01945c V6.0.1 2024-11-11 23:38:05 +01:00
adator
71a7d29b08 Merge pull request #65 from adator85/V6.X.X
Update the version 6
2024-11-11 15:39:20 +01:00
adator
bd9713006a Update the version 6 2024-11-11 15:38:38 +01:00
adator
71170baf1a Merge pull request #64 from adator85/V6.X.X
Fix clone reply
2024-11-07 23:43:02 +01:00
adator
4825775b73 Fix clone reply 2024-11-07 23:42:11 +01:00
adator
1b20435b83 Merge pull request #63 from adator85/V6.X.X
fix main file name
2024-11-07 23:11:26 +01:00
adator
96ebf0511b fix main file name 2024-11-07 23:10:18 +01:00
adator
008dacfde6 Merge pull request #62 from adator85/V6.X.X
Disable console
2024-11-07 23:03:47 +01:00
adator
91a2eafd82 Disable console 2024-11-07 23:03:21 +01:00
adator
5347c45579 Merge pull request #61 from adator85/V6.X.X
V6.0.0
2024-11-07 22:50:18 +01:00
adator
a93d69214e Activate installation 2024-11-07 22:49:41 +01:00
adator
63130fbc06 Merge pull request #60 from adator85/V6.X.X
V6.0.0
2024-11-07 22:46:20 +01:00
adator
b8cd2f244b First version 6.0.0 2024-11-07 22:45:25 +01:00
adator
395dec47be Connexion to inspircd done ... 2024-11-07 00:51:09 +01:00
adator
709e8d4419 Could be the first version 6-rc 2024-11-06 01:15:11 +01:00
adator
e07b047b6a Update all Protocol calls 2024-11-03 18:49:04 +01:00
adator
cbae3dce96 many updates 2024-11-02 23:22:36 +01:00
adator
9d9ede0e80 First Version 6 2024-11-01 23:52:22 +01:00
adator
860e265979 Latest release for version 5 2024-11-01 14:05:26 +01:00
adator
b27b503d78 Merge pull request #59 from adator85/V5.X.X
V5.3.9
2024-10-13 22:22:00 +02:00
adator
f7c80d190e V5.3.9 2024-10-13 22:21:01 +02:00
adator
36e3835e6c Merge pull request #58 from adator85/V5.X.X
update websocket connection
2024-10-06 21:58:24 +02:00
adator
9bfe5925f8 update websocket connection 2024-10-06 21:57:37 +02:00
adator
2c78025bfb Merge pull request #57 from adator85/V5.X.X
V5.3.8
2024-10-06 21:49:13 +02:00
adator
c3187e81dd V5.3.8 2024-10-06 21:46:16 +02:00
adator
979ba40c05 Merge pull request #56 from adator85/V5.X.X
V5.x.x
2024-10-04 01:10:33 +02:00
adator
f0c0a2d06a check packages versions just after windows check 2024-10-04 01:07:37 +02:00
adator
2be6ece27f Exclude windows from update packages 2024-10-04 01:03:36 +02:00
adator
6801c981ab V5.3.7 2024-10-04 00:54:31 +02:00
adator
fd88df1017 Exec pip from virtual env 2024-10-04 00:22:50 +02:00
adator
ad5b7ffbf2 Check if virtual env is available 2024-10-04 00:08:14 +02:00
adator
2b7f1d8bf3 V5.3.7 Defender can update packages 2024-10-03 23:56:01 +02:00
adator
cea69c1580 Merge pull request #54 from adator85/dev
V5.3.6
2024-10-02 23:40:38 +02:00
adator
f5ff9259e8 V5.3.6 2024-10-02 23:38:42 +02:00
adator
c404cc3234 Merge pull request #53 from adator85/dev
Dev
2024-10-02 00:06:54 +02:00
adator
12c7e5e832 V5.3.5:
- core/irc.py : Nothing special except
        a notice sent to the user that the command
        is not available
    - mod_clone:
        When the user unload the module, the
        server will unset modes of #clone channel
    - mod_defender:
        * adding a try block to catch errors
        * adding a mode block to check if
        the jail channel is not having +b mode
        * ensure defender is +o in jail channel
    - mod_test:
        * adding some try block to catch errors
2024-10-02 00:01:13 +02:00
adator
3cdee5fddf V5.3.5 2024-10-01 01:17:00 +02:00
adator
80b329dd5d Merge pull request #52 from adator85/dev
You need to have unrealirc_rpc_py v1.0.3
2024-09-30 01:40:36 +02:00
adator
f2b5c48fc9 You need to have unrealirc_rpc_py v1.0.3 2024-09-30 01:40:01 +02:00
adator
f7b49c151f Merge pull request #51 from adator85/dev
V5.3.4
2024-09-29 22:43:28 +02:00
adator
e2a1ec5866 V5.3.4 2024-09-29 22:42:28 +02:00
adator
1ee9b7e3ff Merge pull request #50 from adator85/dev
Dev
2024-09-26 01:00:39 +02:00
adator
cc53eae875 Adding more commands (V5.3.3) 2024-09-26 01:00:06 +02:00
adator
8a80840a6a V5.3.2 additional commands 2024-09-25 00:21:44 +02:00
adator
4d0087623c Merge pull request #49 from adator85/dev
unsubscribe before unload
2024-09-22 23:54:22 +02:00
adator
e25baea0ef unsubscribe before unload 2024-09-22 23:53:56 +02:00
adator
dc20f5ec3c Merge pull request #48 from adator85/dev
Dev
2024-09-22 23:20:37 +02:00
adator
ee02566343 V5.3.1 2024-09-22 23:20:12 +02:00
adator
11eedbb191 Include mod_jsonrpc 2024-09-22 22:23:13 +02:00
adator
110cae3b84 Merge pull request #47 from adator85/dev
Update the way to clean exceptions and bans
2024-09-22 16:45:54 +02:00
adator
7e5e2d4643 Update the way to clean exceptions and bans 2024-09-22 16:45:21 +02:00
adator
857cbfc85d Merge pull request #46 from adator85/dev
Send the ready msg to channel log
2024-09-22 16:33:49 +02:00
adator
7422bcad45 Send the ready msg to channel log 2024-09-22 16:33:21 +02:00
adator
3518589e9c Merge pull request #45 from adator85/dev
Fix ConfModel error
2024-09-22 16:31:35 +02:00
adator
0cf1262d31 Fix ConfModel error 2024-09-22 16:31:00 +02:00
adator
e14c97de03 Merge pull request #44 from adator85/dev
V5.3.0
2024-09-22 16:20:45 +02:00
adator
ff603ab2a4 V5.3.0 2024-09-22 16:20:02 +02:00
adator
69360be3ad Merge pull request #43 from adator85/dev
Update info command
2024-09-21 20:28:29 +02:00
adator
d7503768b6 Update info command 2024-09-21 20:28:02 +02:00
adator
bfa90c6bd5 Merge pull request #42 from adator85/dev
V5.2.9
2024-09-21 20:22:42 +02:00
adator
b5503d23d7 V5.2.9 2024-09-21 20:22:09 +02:00
adator
5c8378a0e7 Merge pull request #41 from adator85/dev
V5.2.9
2024-09-21 16:43:14 +02:00
adator
7be3f51bf4 V5.2.9 2024-09-21 16:28:50 +02:00
adator
e3b212ea88 Merge pull request #40 from adator85/dev
finetune clone connection
2024-09-20 23:13:33 +02:00
adator
2c0510b2a3 finetune clone connection 2024-09-20 23:12:57 +02:00
adator
0c2a350d38 Merge pull request #39 from adator85/dev
Dev
2024-09-20 21:12:59 +02:00
adator
ee039322d4 V5.2.7 2024-09-19 21:06:07 +02:00
adator
8f08a1e77f update readme.md 2024-09-18 20:29:33 +02:00
adator
c59dd16e87 V5.2.6 2024-09-18 20:21:44 +02:00
adator
0f31e67be6 check python version fix 2024-09-18 19:10:15 +02:00
adator
3cd2077f63 update installation module 2024-09-18 19:08:08 +02:00
adator
9c78ad0860 Same version, copyright changed 2024-09-17 01:46:09 +02:00
adator
487f9a2762 V5.2.5 2024-09-17 01:43:24 +02:00
adator
a7de16f7ad V5.2.4 multiple changes related to clones 2024-09-17 00:14:53 +02:00
adator
c1c0b480ce V5.2.3 If missing MOTD 2024-09-16 21:14:04 +02:00
adator
66ea492593 removing old code 2024-09-16 00:57:27 +02:00
adator
d459fd662f Changing hostname modification 2024-09-16 00:41:43 +02:00
adator
5d3a2b0e64 Changing Proxy_scan to PSUTIL_scan 2024-09-15 23:32:44 +02:00
adator
2f681db2d7 Adding Geoip to the UserModel 2024-09-15 23:29:32 +02:00
adator
7585db4f62 V5.2.2 2024-09-15 22:39:34 +02:00
adator
1cea8d0601 Merge pull request #38 from adator85/dev
V5.2.1
2024-09-15 03:09:22 +02:00
adator
1984511db8 V5.2.1 2024-09-15 03:08:49 +02:00
adator
652b400d5e Merge pull request #37 from adator85/dev
update mode clone
2024-09-15 02:50:27 +02:00
adator
ce47739a93 update mode clone 2024-09-15 02:49:42 +02:00
adator
2f8b965b59 Merge pull request #36 from adator85/dev
Dev
2024-09-15 02:04:32 +02:00
adator
c7047ec3d6 V5.2.0 2024-09-15 02:03:38 +02:00
adator
eddba81cf0 remove mod_jsonrpc.py 2024-09-14 01:32:16 +02:00
adator
59e634951f V5.1.9 2024-09-14 01:25:13 +02:00
adator
37684eaede With unrealircd json rpc 2024-09-13 23:51:13 +02:00
adator
3c043cefd8 Merge pull request #35 from adator85/dev
V5.1.8
2024-09-08 00:42:57 +02:00
adator
e6156fa301 V5.1.8 2024-09-08 00:42:18 +02:00
adator
59a75cecd8 Merge pull request #34 from adator85/dev
V5.1.7
2024-09-03 00:21:32 +02:00
adator
58e3ebd287 V5.1.7 2024-09-03 00:19:13 +02:00
adator
71053437a7 Merge pull request #33 from adator85/dev
V5.1.6
2024-09-01 22:15:54 +02:00
adator
322759c5ef V5.1.6 2024-09-01 22:15:24 +02:00
adator
7796d05206 Merge pull request #32 from adator85/dev
Update vote kick commands
2024-09-01 18:55:57 +02:00
adator
3ba884216f Update vote kick commands 2024-09-01 18:55:30 +02:00
adator
5f2567f9e5 Merge pull request #31 from adator85/dev
mod_command update
2024-09-01 17:30:05 +02:00
adator
2ce19ee877 mod_command update 2024-09-01 17:29:33 +02:00
adator
aaa1dd9a1a Merge pull request #30 from adator85/dev
adding Say command for clones
2024-09-01 16:40:25 +02:00
adator
351fd6edaf adding Say command for clones 2024-09-01 16:39:49 +02:00
adator
a02f2f9a26 Merge pull request #29 from adator85/dev
update mod_clone module
2024-09-01 15:54:50 +02:00
adator
d0c17d69de update mod_clone module 2024-09-01 15:54:23 +02:00
adator
d73adb6f0b Merge pull request #28 from adator85/dev
update readme
2024-09-01 15:35:59 +02:00
adator
eb9402dd8e update readme 2024-09-01 15:34:49 +02:00
adator
b812e64992 Merge pull request #27 from adator85/dev
Dev
2024-09-01 15:24:18 +02:00
adator
35d5e7a2b5 Update readme.md 2024-09-01 15:23:48 +02:00
adator
21a825c92d Remove install.py 2024-09-01 15:18:32 +02:00
adator
9bd1f68df2 Merge pull request #26 from adator85/dev
Dev
2024-09-01 14:59:38 +02:00
adator
3fcfa0296d . 2024-09-01 14:40:28 +02:00
adator
bcf972d08b . 2024-09-01 14:34:57 +02:00
adator
1348ead6cd remove logging library 2024-09-01 14:33:41 +02:00
adator
f6ebab4780 . 2024-09-01 14:21:25 +02:00
adator
608ec57593 . 2024-09-01 13:59:26 +02:00
adator
f392f2fb2f . 2024-09-01 13:56:55 +02:00
adator
489e1e7b0a . 2024-09-01 13:47:18 +02:00
adator
3d79270ca0 . 2024-09-01 13:42:39 +02:00
adator
e60ada4260 Fix logging instance 2024-09-01 13:41:15 +02:00
adator
ccb9f307b4 Few changes see changelog 2024-09-01 13:39:02 +02:00
adator
2fc8f2d346 Fix issue unexpected argument 2024-09-01 13:14:51 +02:00
adator
e3ada04f2a daemon_reload cmd. 2024-09-01 13:09:47 +02:00
adator
6ba0551fee v5.1.5 2024-09-01 12:51:54 +02:00
adator
f44b08bf36 Merge pull request #25 from adator85/dev
fix Installation
2024-08-29 01:36:38 +02:00
adator
6142b4257f fix Installation 2024-08-29 01:36:12 +02:00
adator
1a19e1613a Merge pull request #24 from adator85/dev
Fix Bug installation
2024-08-29 01:31:19 +02:00
adator
ab15cce82b Fix Bug installation 2024-08-29 01:30:10 +02:00
adator
cdc15b7b47 Merge pull request #23 from adator85/dev
Dev
2024-08-29 01:16:55 +02:00
adator
01dcc90d63 V5.1.0 2024-08-29 01:13:55 +02:00
adator
9cd089ee6e Fix some issues 2024-08-28 00:50:50 +02:00
adator
c635851d19 New version 2024-08-28 00:13:14 +02:00
54 changed files with 11748 additions and 4873 deletions

4
.gitignore vendored
View File

@@ -1,7 +1,9 @@
.pyenv/
.venv/
.idea/
db/
logs/
__pycache__/
configuration.json
install.log
*.log
test.py

255
README.md
View File

@@ -1,4 +1,9 @@
# IRC-DEFENDER
![Static Badge](https://img.shields.io/badge/UnrealIRCd-6.2.2%20or%20later-green)
![Static Badge](https://img.shields.io/badge/Python3-3.10%20or%20later-green)
![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fadator85%2FIRC_DEFENDER_MODULES%2Fmain%2Fversion.json&query=version&label=Current%20Version)
![Static Badge](https://img.shields.io/badge/Maintained-Yes-green)
Defender est un service IRC basé sur la sécurité des réseaux IRC ( UnrealIRCD )
Il permet d'ajouter une sécurité supplémentaire pour vérifier les users connectés au réseau
en demandant aux user un code de validation.
@@ -9,9 +14,9 @@ Il permet aux opérateurs de gérer efficacement un canal, tout en offrant aux u
Kick: Expulser un utilisateur du canal.
Ban: Interdire définitivement l'accès au canal.
Unban: Lever une interdiction.
Op/Deop: Attribuer ou retirer les droits d'opérateur.
Op/Deop/Opall/Deopall: Attribuer ou retirer les droits d'opérateur.
Halfop/Dehalfop: Attribuer ou retirer les droits
Voice/Devoice: Attribuer ou retirer les droits de voix.
Voice/Devoice/VoiceAll/DevoiceAll: Attribuer ou retirer les droits de voix.
Système de quarantaine:
Mise en quarantaine: Isoler temporairement un utilisateur dans un canal privé.
@@ -25,77 +30,209 @@ Il permet aux opérateurs de gérer efficacement un canal, tout en offrant aux u
Prérequis:
- Système d'exploitation Linux (Windows non supporté)
- Droits d'administrateur (root) pour l'exécution du script
- Un server UnrealIRCD corréctement configuré
- Python version 3.10 ou supérieure
Bash:
```bash
# Bash
$ git clone https://github.com/adator85/IRC_DEFENDER_MODULES.git
- Renommer le fichier exemple_configuration.json en configuration.json
- Configurer le fichier configuration.json
$ sudo python3 install.py
# Renommer le fichier exemple_configuration.json en configuration.json
# Configurer le fichier configuration.json
$ python3 main.py
```
Si votre configuration est bonne, votre service est censé etre connecté a votre réseau IRC
Pour Les prochains lancement de defender vous devez utiliser la commande suivante:
```bash
# Bash
$ systemctl --user [start | stop | restart | status] defender
```
# Installation manuelle:
Bash:
$ git clone https://github.com/adator85/IRC_DEFENDER_MODULES.git
$ cd IRC_DEFENDER_MODULES
$ python3 -m venv .pyenv
$ source .pyenv/bin/activate
- Créer un service nommé "Defender.service" pour votre service et placer le dans "/etc/systemd/system/"
$ sudo systemctl start Defender
```bash
# Bash
$ git clone https://github.com/adator85/IRC_DEFENDER_MODULES.git
$ cd IRC_DEFENDER_MODULES
$ python3 -m venv .pyenv
$ source .pyenv/bin/activate
(pyenv)$ pip install sqlalchemy, psutil, requests, faker, unrealircd_rpc_py
# Créer un service nommé "defender.service"
# pour votre service et placer le dans "/PATH/TO/USER/.config/systemd/user/"
# Si le dossier n'existe pas il faut les créer
$ sudo systemctl --user start defender
```
# Configuration
```
SERVEUR (Serveur)
SERVEUR_IP: Adresse IP du serveur IRC à rejoindre.
SERVEUR_HOSTNAME: Nom d'hôte du serveur IRC à rejoindre (optionnel).
SERVEUR_LINK: Lien vers le serveur IRC (optionnel).
SERVEUR_PORT: Port de connexion au serveur IRC.
SERVEUR_PASSWORD: Mot de passe d'enregistrement du service sur le serveur IRC.
SERVEUR_ID: Identifiant unique du service.
SERVEUR_SSL: Active la connexion SSL sécurisée au serveur IRC (true/false).
SERVICE (Service)
SERVICE_NAME: Nom du service IRC.
SERVICE_NICKNAME: Surnom utilisé par le service sur le serveur IRC.
SERVICE_REALNAME: Nom réel du service affiché sur le serveur IRC.
SERVICE_USERNAME: Nom d'utilisateur utilisé par le service pour se connecter au serveur IRC.
SERVICE_HOST: Nom d'hôte du service affiché sur le serveur IRC (optionnel).
SERVICE_INFO: Description du service.
SERVICE_CHANLOG: Canal utilisé pour la journalisation des actions du service.
SERVICE_SMODES: Modes serveur appliqués aux canaux rejoints par le service.
SERVICE_CMODES: Modes de canal appliqués aux canaux rejoints par le service.
SERVICE_UMODES: Modes utilisateur appliqués au service.
SERVICE_PREFIX: Caractère utilisé comme préfixe des commandes du service.
COMPTE (Compte)
OWNER: Nom d'utilisateur possédant les droits d'administration du service.
PASSWORD: Mot de passe de l'administrateur du service.
CANAUX (Canaux)
SALON_JAIL: Canal utilisé comme prison pour les utilisateurs sanctionnés.
SALON_JAIL_MODES: Modes appliqués au canal de prison.
SALON_LIBERER: Canal utilisé pour la libération des utilisateurs sanctionnés.
API (API)
API_TIMEOUT: Durée maximale d'attente d'une réponse de l'API en secondes.
SCANNER (Scanner)
PORTS_TO_SCAN: Liste des ports à scanner pour détecter des serveurs potentiellement malveillants.
SÉCURITÉ (Sécurité)
WHITELISTED_IP: Liste d'adresses IP autorisées à contourner certaines restrictions.
GLINE_DURATION: Durée de bannissement temporaire d'un utilisateur en minutes.
DEBUG (Debug)
DEBUG_LEVEL: Niveau de verbosité des messages de debug (plus grand est le nombre, plus il y a d'informations).
COULEURS (Couleurs)
CONFIG_COLOR: Dictionnaire contenant des codes de couleurs IRC pour un meilleur affichage des messages.
* SERVEUR_IP: Adresse IP du serveur IRC à rejoindre. (default : 127.0.0.1)
* SERVEUR_HOSTNAME: Nom d'hôte du serveur IRC à rejoindre (optionnel).
* SERVEUR_LINK: Lien vers le serveur IRC (optionnel).
* SERVEUR_PORT: Port de connexion au serveur IRC.
* SERVEUR_PASSWORD: Mot de passe d'enregistrement du service sur le serveur IRC.
SERVEUR_ID: Identifiant unique du service. (default : 19Z)
SERVEUR_SSL: Active la connexion SSL sécurisée au serveur IRC (true/false) (default : false).
SERVICE (Service)
SERVICE_NAME: Nom du service IRC. (default : Defender)
SERVICE_NICKNAME: Surnom utilisé par le service sur le serveur IRC. (default : Defender)
SERVICE_REALNAME: Nom réel du service affiché sur le serveur IRC. (default : Defender Security)
SERVICE_USERNAME: Nom d'utilisateur utilisé par le service pour se connecter au serveur IRC. (default : IRCSecurity)
SERVICE_HOST: Nom d'hôte du service affiché sur le serveur IRC (optionnel). (default : defender.local.network)
SERVICE_INFO: Description du service. (default : Defender Network IRC Service)
SERVICE_CHANLOG: Canal utilisé pour la journalisation des actions du service. (default : #services)
SERVICE_SMODES: Modes serveur appliqués aux canaux rejoints par le service. (default : +ioqBS)
SERVICE_CMODES: Modes de canal appliqués aux canaux rejoints par le service. (default : ntsOP)
SERVICE_UMODES: Modes utilisateur appliqués au service. (default : o)
SERVICE_PREFIX: Caractère utilisé comme préfixe des commandes du service. (default : !)
COMPTE (Compte)
OWNER: Nom d'utilisateur possédant les droits d'administration du service. (default : admin)
PASSWORD: Mot de passe de l'administrateur du service. (default : admin)
CANAUX (Canaux)
SALON_JAIL: Canal utilisé comme prison pour les utilisateurs sanctionnés. (default : #jail)
SALON_JAIL_MODES: Modes appliqués au canal de prison. (default : sS)
SALON_LIBERER: Canal utilisé pour la libération des utilisateurs sanctionnés. (default : #welcome)
API (API)
API_TIMEOUT: Durée maximale d'attente d'une réponse de l'API en secondes. (default : 2)
SCANNER (Scanner)
PORTS_TO_SCAN: Liste des ports à scanner pour détecter des serveurs potentiellement malveillants. (default : [])
SÉCURITÉ (Sécurité)
WHITELISTED_IP: Liste d'adresses IP autorisées à contourner certaines restrictions. (default : ['127.0.0.1'])
GLINE_DURATION: Durée de bannissement temporaire d'un utilisateur en minutes. (default : "30")
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
```
Modification de la configuration
Vous devez modifier le fichier config.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.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.
Attention
## Exemple de configuration de base
```json
{
"SERVEUR_IP": "IP.DE.TON.SERVER",
"SERVEUR_HOSTNAME": "HOST.DE.TON.SERVER",
"SERVEUR_LINK": "LINK.DE.TON.SERVER",
"SERVEUR_PORT": 6901,
"SERVEUR_PASSWORD": "MOT_DE_PASS_DE_TON_LINK",
"SERVEUR_ID": "10Z",
"SERVEUR_SSL": true,
Le mot de passe de l'administrateur et le mot de passe du service doivent être modifiés pour des raisons de sécurité.
Ne partagez pas vos informations de connexion au serveur IRC avec des tiers.
"SERVICE_NAME": "defender",
"SERVICE_NICKNAME": "PyDefender",
"SERVICE_REALNAME": "Python Defender Security",
"SERVICE_USERNAME": "PyDefender",
"SERVICE_HOST": "HOST.DE.TON.DEFENDER",
#Extension:
"OWNER": "TON_NICK_NAME",
"PASSWORD": "TON_PASSWORD"
}
```
## Exemple complet de configuration
```json
{
"SERVEUR_IP": "YOUR.SERVER.IP",
"SERVEUR_HOSTNAME": "YOUR.SERVER.HOST",
"SERVEUR_LINK": "LINK.DE.TON.SERVER",
"SERVEUR_PORT": 6901,
"SERVEUR_PASSWORD": "YOUR_LINK_PASSWORD",
"SERVEUR_ID": "10Z",
"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
}
```
# \\!/ Attention \\!/
Le mot de passe de l'administrateur et le mot de passe du service doivent être modifiés pour des raisons de sécurité.
Ne partagez pas vos informations de connexion au serveur IRC avec des tiers.
a votre premiere connexion vous devez tapez
```
/msg [NomDuService] auth [nickname] [password]
-- Une fois identifié tapez la commande suivante
/msg [NomDuService] editaccess [nickname] [Nouveau-Password] 5
```
# Unrealircd configuration
```
listen {
ip *;
port 6901;
options { tls; serversonly; }
}
link LINK.DE.TON.SERVER
{
incoming {
mask *;
bind-ip *;
port 6901;
//options { tls; };
}
outgoing {
bind-ip *; /* ou une IP précise */
hostname LINK.DE.TON.SERVER;
port 6901;
//options { tls; }
}
password "YOUR_LINK_PASSWORD";
class servers;
}
ulines {
LINK.DE.TON.SERVER;
}
```
# Extension:
Le code est modulaire et conçu pour être facilement étendu. Vous pouvez ajouter de nouvelles commandes, de nouvelles fonctionnalités (mods/mod_test.py est un exemple pour bien demarrer la création de son module).
# Contributions:

View File

@@ -0,0 +1,48 @@
{
"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

@@ -1,404 +0,0 @@
from dataclasses import dataclass, field
from datetime import datetime
from typing import Union
from core.base import Base
class User:
@dataclass
class UserModel:
uid: str
nickname: str
username: str
hostname: str
umodes: str
vhost: str
isWebirc: bool
remote_ip: str
score_connexion: int
connexion_datetime: datetime = field(default=datetime.now())
UID_DB: list[UserModel] = []
def __init__(self, Base: Base) -> None:
self.log = Base.logs
pass
def insert(self, newUser: UserModel) -> bool:
"""Insert a new User object
Args:
newUser (UserModel): New userModel object
Returns:
bool: True if inserted
"""
result = False
exist = False
for record in self.UID_DB:
if record.uid == newUser.uid:
# If the user exist then return False and do not go further
exist = True
self.log.debug(f'{record.uid} already exist')
return result
if not exist:
self.UID_DB.append(newUser)
result = True
self.log.debug(f'New User Created: ({newUser})')
if not result:
self.log.critical(f'The User Object was not inserted {newUser}')
return result
def update(self, uid: str, newNickname: str) -> bool:
"""Update the nickname starting from the UID
Args:
uid (str): UID of the user
newNickname (str): New nickname
Returns:
bool: True if updated
"""
result = False
for record in self.UID_DB:
if record.uid == uid:
# If the user exist then update and return True and do not go further
record.nickname = newNickname
result = True
self.log.debug(f'UID ({record.uid}) has been updated with new nickname {newNickname}')
return result
if not result:
self.log.critical(f'The new nickname {newNickname} was not updated, uid = {uid}')
return result
def delete(self, uid: str) -> bool:
"""Delete the User starting from the UID
Args:
uid (str): UID of the user
Returns:
bool: True if deleted
"""
result = False
for record in self.UID_DB:
if record.uid == uid:
# If the user exist then remove and return True and do not go further
self.UID_DB.remove(record)
result = True
self.log.debug(f'UID ({record.uid}) has been deleted')
return result
if not result:
self.log.critical(f'The UID {uid} was not deleted')
return result
def get_User(self, uidornickname: str) -> Union[UserModel, None]:
"""Get The User Object model
Args:
uidornickname (str): UID or Nickname
Returns:
UserModel|None: The UserModel Object | None
"""
User = None
for record in self.UID_DB:
if record.uid == uidornickname:
User = record
elif record.nickname == uidornickname:
User = record
self.log.debug(f'Search {uidornickname} -- result = {User}')
return User
def get_uid(self, uidornickname:str) -> Union[str, None]:
"""Get the UID of the user 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_DB:
if record.uid == uidornickname:
uid = record.uid
if record.nickname == uidornickname:
uid = record.uid
self.log.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]:
"""Get the Nickname starting from UID or the nickname
Args:
uidornickname (str): UID or Nickname of the user
Returns:
str|None: the nickname
"""
nickname = None
for record in self.UID_DB:
if record.nickname == uidornickname:
nickname = record.nickname
if record.uid == uidornickname:
nickname = record.nickname
self.log.debug(f'The value to check {uidornickname} -> {nickname}')
return nickname
class Admin:
@dataclass
class AdminModel:
uid: str
nickname: str
username: str
hostname: str
umodes: str
vhost: str
level: int
connexion_datetime: datetime = field(default=datetime.now())
UID_ADMIN_DB: list[AdminModel] = []
def __init__(self, Base: Base) -> None:
self.log = Base.logs
pass
def insert(self, newAdmin: AdminModel) -> 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.log.debug(f'{record.uid} already exist')
return result
if not exist:
self.UID_ADMIN_DB.append(newAdmin)
result = True
self.log.debug(f'UID ({newAdmin.uid}) has been created')
if not result:
self.log.critical(f'The User Object was not inserted {newAdmin}')
return result
def update(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.log.debug(f'UID ({record.uid}) has been updated with new nickname {newNickname}')
return result
if not result:
self.log.critical(f'The new nickname {newNickname} was not updated, uid = {uid}')
return result
def delete(self, uid: str) -> bool:
result = False
for record in self.UID_ADMIN_DB:
if record.uid == uid:
# If the admin exist, delete and do not go further
self.UID_ADMIN_DB.remove(record)
result = True
self.log.debug(f'UID ({record.uid}) has been created')
return result
if not result:
self.log.critical(f'The UID {uid} was not deleted')
return result
def get_Admin(self, uidornickname: str) -> Union[AdminModel, None]:
Admin = None
for record in self.UID_ADMIN_DB:
if record.uid == uidornickname:
Admin = record
elif record.nickname == uidornickname:
Admin = record
self.log.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.log.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.log.debug(f'The value {uidornickname} -- {nickname}')
return nickname
class Channel:
@dataclass
class ChannelModel:
name: str
"""### Channel name
It include the #"""
uids: list
"""### List of UID available in the channel
including their modes ~ @ % + *
Returns:
list: The list of UID's including theirs modes
"""
UID_CHANNEL_DB: list[ChannelModel] = []
"""List that contains all the Channels objects (ChannelModel)
"""
def __init__(self, Base: Base) -> None:
self.log = Base.logs
self.Base = Base
pass
def insert(self, newChan: ChannelModel) -> bool:
"""This method will insert a new channel and if the channel exist it will update the user list (uids)
Args:
newChan (ChannelModel): The channel model object
Returns:
bool: True if new channel, False if channel exist (However UID could be updated)
"""
result = False
exist = False
for record in self.UID_CHANNEL_DB:
if record.name == newChan.name:
# If the channel exist, update the user list and do not go further
exist = True
self.log.debug(f'{record.name} already exist')
for user in newChan.uids:
record.uids.append(user)
# Supprimer les doublons
del_duplicates = list(set(record.uids))
record.uids = del_duplicates
self.log.debug(f'Updating a new UID to the channel {record}')
return result
if not exist:
# If the channel don't exist, then create it
self.UID_CHANNEL_DB.append(newChan)
result = True
self.log.debug(f'New Channel Created: ({newChan})')
if not result:
self.log.critical(f'The Channel Object was not inserted {newChan}')
return result
def delete(self, name: str) -> bool:
result = False
for record in self.UID_CHANNEL_DB:
if record.name == name:
# If the channel exist, then remove it and return True.
# As soon as the channel found, return True and stop the loop
self.UID_CHANNEL_DB.remove(record)
result = True
self.log.debug(f'Channel ({record.name}) has been created')
return result
if not result:
self.log.critical(f'The Channel {name} was not deleted')
return result
def delete_user_from_channel(self, chan_name: str, uid:str) -> bool:
try:
result = False
for record in self.UID_CHANNEL_DB:
if record.name == chan_name:
for user_id in record.uids:
if self.Base.clean_uid(user_id) == uid:
record.uids.remove(user_id)
self.log.debug(f'The UID {uid} has been removed, here is the new object: {record}')
result = True
for record in self.UID_CHANNEL_DB:
if not record.uids:
self.UID_CHANNEL_DB.remove(record)
self.log.debug(f'The Channel {record.name} has been removed, here is the new object: {record}')
return result
except ValueError as ve:
self.log.error(f'{ve}')
def delete_user_from_all_channel(self, uid:str) -> bool:
try:
result = False
for record in self.UID_CHANNEL_DB:
for user_id in record.uids:
if self.Base.clean_uid(user_id) == self.Base.clean_uid(uid):
record.uids.remove(user_id)
self.log.debug(f'The UID {uid} has been removed, here is the new object: {record}')
result = True
for record in self.UID_CHANNEL_DB:
if not record.uids:
self.UID_CHANNEL_DB.remove(record)
self.log.debug(f'The Channel {record.name} has been removed, here is the new object: {record}')
return result
except ValueError as ve:
self.log.error(f'{ve}')
def get_Channel(self, name: str) -> Union[ChannelModel, None]:
Channel = None
for record in self.UID_CHANNEL_DB:
if record.name == name:
Channel = record
self.log.debug(f'Search {name} -- result = {Channel}')
return Channel

View File

@@ -1,35 +1,56 @@
import time, threading, os, random, socket, hashlib, ipaddress, logging, requests, json, re
from typing import Union
from base64 import b64decode
from datetime import datetime
import importlib
import os
import re
import json
import sys
import time
import socket
import threading
import ipaddress
import ast
import requests
from pathlib import Path
from types import ModuleType
from dataclasses import fields
from typing import Any, Optional, TYPE_CHECKING
from base64 import b64decode, b64encode
from sqlalchemy import create_engine, Engine, Connection, CursorResult
from sqlalchemy.sql import text
from core.loadConf import ConfigDataModel
if TYPE_CHECKING:
from core.loader import Loader
class Base:
CORE_DB_PATH = 'core' + os.sep + 'db' + os.sep # Le dossier bases de données core
MODS_DB_PATH = 'mods' + os.sep + 'db' + os.sep # Le dossier bases de données des modules
PYTHON_MIN_VERSION = '3.10' # Version min de python
def __init__(self, loader: 'Loader') -> None:
def __init__(self, Config: ConfigDataModel) -> None:
self.Loader = loader
self.Config = loader.Config
self.Settings = loader.Settings
self.Utils = loader.Utils
self.logs = loader.Logs
self.Config = Config # Assigner l'objet de configuration
self.init_log_system() # Demarrer le systeme de log
self.check_for_new_version(True) # Verifier si une nouvelle version est disponible
self.check_for_new_version(True) # Verifier si une nouvelle version est disponible
self.running_timers:list[threading.Timer] = [] # Liste des timers en cours
self.running_threads:list[threading.Thread] = [] # Liste des threads en cours
self.running_sockets: list[socket.socket] = [] # Les sockets ouvert
self.periodic_func:dict[object] = {} # Liste des fonctions en attentes
# Liste des timers en cours
self.running_timers: list[threading.Timer] = self.Settings.RUNNING_TIMERS
self.lock = threading.RLock() # Création du lock
# Liste des threads en cours
self.running_threads: list[threading.Thread] = self.Settings.RUNNING_THREADS
# Les sockets ouvert
self.running_sockets: list[socket.socket] = self.Settings.RUNNING_SOCKETS
# Liste des fonctions en attentes
self.periodic_func: dict[object] = self.Settings.PERIODIC_FUNC
# Création du lock
self.lock = self.Settings.LOCK
self.install: bool = False # Initialisation de la variable d'installation
self.engine, self.cursor = self.db_init() # Initialisation de la connexion a la base de données
self.__create_db() # Initialisation de la base de données
self.db_create_first_admin() # Créer un nouvel admin si la base de données est vide
def __set_current_defender_version(self) -> None:
"""This will put the current version of Defender
located in version.json
@@ -39,17 +60,15 @@ class Base:
with open(version_filename, 'r') as version_data:
current_version:dict[str, str] = json.load(version_data)
# self.DEFENDER_VERSION = current_version["version"]
self.Config.current_version = current_version['version']
self.Config.CURRENT_VERSION = current_version['version']
return None
def __get_latest_defender_version(self) -> None:
try:
self.logs.debug(f'Looking for a new version available on Github')
print(f'===> Looking for a new version available on Github')
self.logs.debug(f'-- Looking for a new version available on Github')
token = ''
json_url = f'https://raw.githubusercontent.com/adator85/IRC_DEFENDER_MODULES/main/version.json'
json_url = f'https://raw.githubusercontent.com/adator85/DEFENDER/main/version.json'
headers = {
'Authorization': f'token {token}',
'Accept': 'application/vnd.github.v3.raw' # Indique à GitHub que nous voulons le contenu brut du fichier
@@ -63,7 +82,7 @@ class Base:
response.raise_for_status() # Vérifie si la requête a réussi
json_response:dict = response.json()
# self.LATEST_DEFENDER_VERSION = json_response["version"]
self.Config.latest_version = json_response['version']
self.Config.LATEST_VERSION = json_response['version']
return None
except requests.HTTPError as err:
@@ -72,19 +91,27 @@ class Base:
self.logs.warning(f'Github not available to fetch latest version')
def check_for_new_version(self, online:bool) -> bool:
"""Check if there is a new version available
Args:
online (bool): True if you want to get the version from github (main branch)
Returns:
bool: True if there is a new version available
"""
try:
self.logs.debug(f'Checking for a new service version')
self.logs.debug(f'-- Checking for a new service version')
# Assigner la version actuelle de Defender
self.__set_current_defender_version()
# Récuperer la dernier version disponible dans github
if online:
self.logs.debug(f'Retrieve the latest version from Github')
self.logs.debug(f'-- Retrieve the latest version from Github')
self.__get_latest_defender_version()
isNewVersion = False
latest_version = self.Config.latest_version
current_version = self.Config.current_version
latest_version = self.Config.LATEST_VERSION
current_version = self.Config.CURRENT_VERSION
curr_major , curr_minor, curr_patch = current_version.split('.')
last_major, last_minor, last_patch = latest_version.split('.')
@@ -104,139 +131,167 @@ class Base:
return isNewVersion
except ValueError as ve:
self.logs.error(f'Impossible to convert in version number : {ve}')
def get_unixtime(self) -> int:
"""
Cette fonction retourne un UNIXTIME de type 12365456
Return: Current time in seconds since the Epoch (int)
"""
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
except AttributeError as atterr:
self.logs.error(f'Attribute Error: {atterr}')
except Exception as err:
self.logs.error(f'General Error: {err}')
def create_log(self, log_message: str) -> None:
"""Enregiste les logs
Args:
string (str): Le message a enregistrer
log_message (str): Le message a enregistrer
Returns:
None: Aucun retour
"""
sql_insert = f"INSERT INTO {self.Config.table_log} (datetime, server_msg) VALUES (:datetime, :server_msg)"
mes_donnees = {'datetime': str(self.get_datetime()),'server_msg': f'{log_message}'}
sql_insert = f"INSERT INTO {self.Config.TABLE_LOG} (datetime, server_msg) VALUES (:datetime, :server_msg)"
mes_donnees = {'datetime': str(self.Utils.get_sdatetime()),'server_msg': f'{log_message}'}
self.db_execute_query(sql_insert, mes_donnees)
return None
def init_log_system(self) -> None:
# Create folder if not available
logs_directory = f'logs{os.sep}'
if not os.path.exists(f'{logs_directory}'):
os.makedirs(logs_directory)
# Init logs object
self.logs = logging
self.logs.basicConfig(level=self.Config.DEBUG_LEVEL,
filename='logs/defender.log',
encoding='UTF-8',
format='%(asctime)s - %(levelname)s - %(filename)s - %(lineno)d - %(funcName)s - %(message)s')
self.logs.info('#################### STARTING INTERCEPTOR HQ ####################')
return None
def log_cmd(self, user_cmd:str, cmd:str) -> None:
def log_cmd(self, user_cmd: str, cmd: str) -> None:
"""Enregistre les commandes envoyées par les utilisateurs
Args:
user_cmd (str): The user who performed the command
cmd (str): la commande a enregistrer
"""
cmd_list = cmd.split()
if len(cmd_list) == 3:
if cmd_list[0].replace('.', '') == 'auth':
if cmd_list[0].replace(self.Config.SERVICE_PREFIX, '') == 'auth':
cmd_list[1] = '*******'
cmd_list[2] = '*******'
cmd = ' '.join(cmd_list)
insert_cmd_query = f"INSERT INTO {self.Config.table_commande} (datetime, user, commande) VALUES (:datetime, :user, :commande)"
mes_donnees = {'datetime': self.get_datetime(), 'user': user_cmd, 'commande': cmd}
insert_cmd_query = f"INSERT INTO {self.Config.TABLE_COMMAND} (datetime, user, commande) VALUES (:datetime, :user, :commande)"
mes_donnees = {'datetime': self.Utils.get_sdatetime(), 'user': user_cmd, 'commande': cmd}
self.db_execute_query(insert_cmd_query, mes_donnees)
return False
return None
def db_isModuleExist(self, module_name:str) -> bool:
"""Teste si un module existe déja dans la base de données
def db_sync_core_config(self, module_name: str, dataclassObj: object) -> bool:
"""Sync module local parameters with the database
if new module then local param will be stored in the database
if old module then db param will be moved to the local dataclassObj
if new local param it will be stored in the database
if local param was removed then it will also be removed from the database
Args:
module_name (str): le non du module a chercher dans la base de données
module_name (str): The module name ex. mod_defender
dataclassObj (object): The Dataclass object
Returns:
bool: True si le module existe déja dans la base de données sinon False
bool: _description_
"""
query = f"SELECT id FROM {self.Config.table_module} WHERE module = :module"
mes_donnes = {'module': module_name}
results = self.db_execute_query(query, mes_donnes)
try:
response = True
current_date = self.Utils.get_sdatetime()
core_table = self.Config.TABLE_CONFIG
if results.fetchall():
return True
else:
# Add local parameters to DB
for field in fields(dataclassObj):
param_key = field.name
param_value = str(getattr(dataclassObj, field.name))
param_to_search = {'module_name': module_name, 'param_key': param_key}
search_query = f'''SELECT id FROM {core_table} WHERE module_name = :module_name AND param_key = :param_key'''
excecute_search_query = self.db_execute_query(search_query, param_to_search)
result_search_query = excecute_search_query.fetchone()
if result_search_query is None:
# If param and module_name doesn't exist create the record
param_to_insert = {'datetime': current_date,'module_name': module_name,
'param_key': param_key,'param_value': param_value
}
insert_query = f'''INSERT INTO {core_table} (datetime, module_name, param_key, param_value)
VALUES (:datetime, :module_name, :param_key, :param_value)
'''
execution = self.db_execute_query(insert_query, param_to_insert)
if execution.rowcount > 0:
self.logs.debug(f'New parameter added to the database: {param_key} --> {param_value}')
# Delete from DB unused parameter
query_select = f"SELECT module_name, param_key, param_value FROM {core_table} WHERE module_name = :module_name"
parameter = {'module_name': module_name}
execute_query_select = self.db_execute_query(query_select, parameter)
result_query_select = execute_query_select.fetchall()
for result in result_query_select:
db_mod_name, db_param_key, db_param_value = result
if not hasattr(dataclassObj, db_param_key):
mes_donnees = {'param_key': db_param_key, 'module_name': db_mod_name}
execute_delete = self.db_execute_query(f'DELETE FROM {core_table} WHERE module_name = :module_name and param_key = :param_key', mes_donnees)
row_affected = execute_delete.rowcount
if row_affected > 0:
self.logs.debug(f'A parameter has been deleted from the database: {db_param_key} --> {db_param_value} | Mod: {db_mod_name}')
# Sync local variable with Database
query = f"SELECT param_key, param_value FROM {core_table} WHERE module_name = :module_name"
parameter = {'module_name': module_name}
response = self.db_execute_query(query, parameter)
result = response.fetchall()
for param, value in result:
if isinstance(getattr(dataclassObj, param), list):
value = ast.literal_eval(value)
setattr(dataclassObj, param, self.int_if_possible(value))
return response
except AttributeError as attrerr:
self.logs.error(f'Attribute Error: {attrerr}')
except Exception as err:
self.logs.error(err)
return False
def db_record_module(self, user_cmd:str, module_name:str) -> None:
"""Enregistre les modules dans la base de données
def db_update_core_config(self, module_name:str, dataclassObj: object, param_key:str, param_value: str) -> bool:
Args:
cmd (str): le module a enregistrer
"""
core_table = self.Config.TABLE_CONFIG
# Check if the param exist
if not hasattr(dataclassObj, param_key):
self.logs.error(f"Le parametre {param_key} n'existe pas dans la variable global")
return False
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) VALUES (:datetime, :user, :module)"
mes_donnees = {'datetime': self.get_datetime(), 'user': user_cmd, 'module': module_name}
self.db_execute_query(insert_cmd_query, mes_donnees)
# self.db_close_session(self.session)
mes_donnees = {'module_name': module_name, 'param_key': param_key, 'param_value': param_value}
search_param_query = f"SELECT id FROM {core_table} WHERE module_name = :module_name AND param_key = :param_key"
result = self.db_execute_query(search_param_query, mes_donnees)
isParamExist = result.fetchone()
if not isParamExist is None:
mes_donnees = {'datetime': self.Utils.get_sdatetime(),
'module_name': module_name,
'param_key': param_key,
'param_value': param_value
}
query = f'''UPDATE {core_table} SET datetime = :datetime, param_value = :param_value WHERE module_name = :module_name AND param_key = :param_key'''
update = self.db_execute_query(query, mes_donnees)
updated_rows = update.rowcount
if updated_rows > 0:
setattr(dataclassObj, param_key, self.int_if_possible(param_value))
self.logs.debug(f'Parameter updated : {param_key} - {param_value} | Module: {module_name}')
else:
self.logs.error(f'Parameter NOT updated : {param_key} - {param_value} | Module: {module_name}')
else:
self.logs.debug(f"Le module {module_name} existe déja dans la base de données")
self.logs.error(f'Parameter and Module do not exist: Param ({param_key}) - Value ({param_value}) | Module ({module_name})')
return False
self.logs.debug(dataclassObj)
def db_delete_module(self, module_name:str) -> None:
"""Supprime les modules de la base de données
Args:
cmd (str): le module a enregistrer
"""
insert_cmd_query = f"DELETE FROM {self.Config.table_module} WHERE module = :module"
mes_donnees = {'module': module_name}
self.db_execute_query(insert_cmd_query, mes_donnees)
return False
return True
def db_create_first_admin(self) -> None:
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():
admin = self.Config.OWNER
password = self.crypt_password(self.Config.PASSWORD)
password = self.Utils.hash_password(self.Config.PASSWORD)
mes_donnees = {'createdOn': self.get_datetime(),
mes_donnees = {'createdOn': self.Utils.get_sdatetime(),
'user': admin,
'password': password,
'hostname': '*',
@@ -244,7 +299,7 @@ class Base:
'level': 5
}
self.db_execute_query(f"""
INSERT INTO {self.Config.table_admin}
INSERT INTO {self.Config.TABLE_ADMIN}
(createdOn, user, password, hostname, vhost, level)
VALUES
(:createdOn, :user, :password, :hostname, :vhost, :level)"""
@@ -256,50 +311,107 @@ class Base:
try:
t = threading.Timer(interval=time_to_wait, function=func, args=func_args)
t.setName(func.__name__)
t.name = func.__name__
t.start()
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:
self.logs.error(f'Assertion Error -> {ae}')
return None
def create_thread(self, func:object, func_args: tuple = (), run_once:bool = False) -> 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
Args:
func (object): The method/function you want to execute via this thread
func_args (tuple, optional): Arguments of the function/method. Defaults to ().
run_once (bool, optional): If you want to ensure that this method/function run once. Defaults to False.
"""
try:
func_name = func.__name__
if run_once:
for thread in self.running_threads:
if thread.getName() == func_name:
if thread.name == func_name:
return None
# if func_name in self.running_threads:
# print(f"HeartBeat is running")
# return None
th = threading.Thread(target=func, args=func_args, name=str(func_name), daemon=True)
th = threading.Thread(target=func, args=func_args, name=str(func_name), daemon=daemon)
th.start()
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:
self.logs.error(f'{ae}')
def is_thread_alive(self, thread_name: str) -> bool:
"""Check if the thread is still running! using the is_alive method of Threads.
Args:
thread_name (str): The thread name
Returns:
bool: True if is alive
"""
for thread in self.running_threads:
if thread.name.lower() == thread_name.lower():
if thread.is_alive():
return True
else:
return False
return False
def is_thread_exist(self, thread_name: str) -> bool:
"""Check if the thread exist in the local var (running_threads)
Args:
thread_name (str): The thread name
Returns:
bool: True if the thread exist
"""
for thread in self.running_threads:
if thread.name.lower() == thread_name.lower():
return True
return False
def thread_count(self, thread_name: str) -> int:
"""This method return the number of existing threads
currently running or not running
Args:
thread_name (str): The name of the thread
Returns:
int: Number of threads
"""
with self.lock:
count = 0
for thr in self.running_threads:
if thread_name == thr.name:
count += 1
return count
def garbage_collector_timer(self) -> None:
"""Methode qui supprime les timers qui ont finis leurs job
"""
try:
for timer in self.running_timers:
if not timer.is_alive():
timer.cancel()
self.running_timers.remove(timer)
self.logs.info(f"Timer {str(timer)} removed")
self.logs.debug(f"-- Timer {str(timer)} removed")
else:
self.logs.debug(f"===> Timer {str(timer)} Still running ...")
self.logs.debug(f"--* Timer {str(timer)} Still running ...")
except AssertionError as ae:
self.logs.error(f'Assertion Error -> {ae}')
@@ -309,10 +421,10 @@ class Base:
"""
try:
for thread in self.running_threads:
if thread.getName() != 'heartbeat':
if thread.name != 'heartbeat':
if not thread.is_alive():
self.running_threads.remove(thread)
self.logs.info(f"Thread {str(thread.getName())} {str(thread.native_id)} removed")
self.logs.info(f"-- Thread {str(thread.name)} {str(thread.native_id)} removed")
# print(threading.enumerate())
except AssertionError as ae:
@@ -327,7 +439,7 @@ class Base:
soc.close()
self.running_sockets.remove(soc)
self.logs.debug(f"Socket ==> closed {str(soc.fileno())}")
self.logs.debug(f"-- Socket ==> closed {str(soc.fileno())}")
def shutdown(self) -> None:
"""Methode qui va préparer l'arrêt complêt du service
@@ -336,19 +448,19 @@ class Base:
self.logs.debug(f"=======> Checking for Timers to stop")
for timer in self.running_timers:
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()
time.sleep(0.2)
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")
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.logs.debug(f"> Running the last periodic action")
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")
for soc in self.running_sockets:
@@ -363,27 +475,37 @@ class Base:
def db_init(self) -> tuple[Engine, Connection]:
db_directory = self.Config.db_path
full_path_db = self.Config.db_path + self.Config.db_name
db_directory = self.Config.DB_PATH
full_path_db = self.Config.DB_PATH + self.Config.DB_NAME
if not os.path.exists(db_directory):
self.install = True
os.makedirs(db_directory)
engine = create_engine(f'sqlite:///{full_path_db}.db', echo=False)
cursor = engine.connect()
self.logs.info("database connexion has been initiated")
self.logs.info("-- database connexion has been initiated")
return engine, cursor
def __create_db(self) -> None:
table_logs = f'''CREATE TABLE IF NOT EXISTS {self.Config.table_log} (
table_core_log = f'''CREATE TABLE IF NOT EXISTS {self.Config.TABLE_LOG} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
datetime TEXT,
server_msg TEXT
)
'''
table_cmds = f'''CREATE TABLE IF NOT EXISTS {self.Config.table_commande} (
table_core_config = f'''CREATE TABLE IF NOT EXISTS {self.Config.TABLE_CONFIG} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
datetime TEXT,
module_name TEXT,
param_key TEXT,
param_value TEXT
)
'''
table_core_log_command = f'''CREATE TABLE IF NOT EXISTS {self.Config.TABLE_COMMAND} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
datetime TEXT,
user TEXT,
@@ -391,29 +513,61 @@ class Base:
)
'''
table_modules = f'''CREATE TABLE IF NOT EXISTS {self.Config.table_module} (
table_core_module = f'''CREATE TABLE IF NOT EXISTS {self.Config.TABLE_MODULE} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
datetime TEXT,
user TEXT,
module TEXT
module_name TEXT,
isdefault INTEGER
)
'''
table_core_channel = f'''CREATE TABLE IF NOT EXISTS {self.Config.TABLE_CHANNEL} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
datetime TEXT,
module_name TEXT,
channel_name TEXT
)
'''
table_admins = f'''CREATE TABLE IF NOT EXISTS {self.Config.table_admin} (
table_core_admin = f'''CREATE TABLE IF NOT EXISTS {self.Config.TABLE_ADMIN} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
createdOn TEXT,
user TEXT,
hostname TEXT,
vhost TEXT,
password TEXT,
fingerprint TEXT,
level INTEGER
)
'''
self.db_execute_query(table_logs)
self.db_execute_query(table_cmds)
self.db_execute_query(table_modules)
self.db_execute_query(table_admins)
table_core_client = f'''CREATE TABLE IF NOT EXISTS {self.Config.TABLE_CLIENT} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
createdOn TEXT,
account TEXT,
nickname TEXT,
hostname TEXT,
vhost TEXT,
realname TEXT,
email TEXT,
password TEXT,
level INTEGER
)
'''
self.db_execute_query(table_core_log)
self.db_execute_query(table_core_log_command)
self.db_execute_query(table_core_module)
self.db_execute_query(table_core_admin)
self.db_execute_query(table_core_client)
self.db_execute_query(table_core_channel)
self.db_execute_query(table_core_config)
if self.install:
self.Loader.ModuleUtils.db_register_module('mod_command', 'sys', True)
self.Loader.ModuleUtils.db_register_module('mod_defender', 'sys', True)
self.install = False
return None
@@ -437,19 +591,6 @@ class Base:
except AttributeError as ae:
self.logs.error(f"Attribute Error : {ae}")
def crypt_password(self, password:str) -> str:
"""Retourne un mot de passe chiffré en MD5
Args:
password (str): Le password en clair
Returns:
str: Le password en MD5
"""
md5_password = hashlib.md5(password.encode()).hexdigest()
return md5_password
def int_if_possible(self, value):
"""Convertit la valeur reçue en entier, si possible.
Sinon elle retourne la valeur initiale.
@@ -468,7 +609,24 @@ class Base:
except TypeError:
return value
def is_valid_ip(self, ip_to_control:str) -> bool:
def convert_to_int(self, value: Any) -> Optional[int]:
"""Convert a value to int
Args:
value (any): Value to convert to int if possible
Returns:
int: Return the int value or None if not possible
"""
try:
response = int(value)
return response
except ValueError:
return None
except TypeError:
return None
def is_valid_ip(self, ip_to_control: str) -> bool:
try:
if ip_to_control in self.Config.WHITELISTED_IP:
@@ -479,7 +637,27 @@ class Base:
except ValueError:
return False
def decode_ip(self, ip_b64encoded: str) -> Union[str, None]:
def is_valid_email(self, email_to_control: str) -> bool:
"""Check if the email is valid
Args:
email_to_control (str): email to control
Returns:
bool: True is the email is correct
"""
try:
pattern = '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'
if re.match(pattern, email_to_control):
return True
else:
return False
except Exception as err:
self.logs.error(f'General Error: {err}')
return False
def decode_ip(self, ip_b64encoded: str) -> Optional[str]:
binary_ip = b64decode(ip_b64encoded)
try:
@@ -490,14 +668,19 @@ class Base:
self.logs.critical(f'This remote ip is not valid : {ve}')
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))
def encode_ip(self, remote_ip_address: str) -> Optional[str]:
return randomize
binary_ip = socket.inet_aton(remote_ip_address)
try:
encoded_ip = b64encode(binary_ip).decode()
return encoded_ip
except ValueError as ve:
self.logs.critical(f'This remote ip is not valid : {ve}')
return None
except Exception as err:
self.logs.critical(f'General Error: {err}')
return None
def execute_periodic_action(self) -> None:
@@ -518,35 +701,23 @@ class Base:
# Vider le dictionnaire de fonction
self.periodic_func.clear()
def clean_uid(self, uid:str) -> str:
"""Clean UID by removing @ / % / + / Owner / and *
def execute_dynamic_method(self, obj: object, method_name: str, params: list) -> None:
"""#### Ajouter les méthodes a éxecuter dans un dictionnaire
Les methodes seront exécuter par heartbeat.
Args:
uid (str): The UID to clean
obj (object): Une instance de la classe qui va etre executer
method_name (str): Le nom de la méthode a executer
params (list): les parametres a faire passer
Returns:
str: Clean UID without any sign
None: aucun retour attendu
"""
self.periodic_func[len(self.periodic_func) + 1] = {
'object': obj,
'method_name': method_name,
'param': params
}
pattern = fr'[@|%|\+|~|\*]*'
parsed_UID = re.sub(pattern, '', uid)
return parsed_UID
def Is_Channel(self, channelToCheck: str) -> bool:
"""Check if the string has the # caractere and return True if this is a channel
Args:
channelToCheck (str): The string to test if it is a channel or not
Returns:
bool: True if the string is a channel / False if this is not a channel
"""
pattern = fr'^#'
isChannel = re.findall(pattern, channelToCheck)
if not isChannel:
return False
else:
return True
self.logs.debug(f'Method to execute : {str(self.periodic_func)}')
return None

155
core/classes/admin.py Normal file
View File

@@ -0,0 +1,155 @@
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:
self.Logs = loader.Logs
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
"""
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)
self.Logs.debug(f'UID ({record.uid}) has been deleted')
return True
if record.nickname.lower() == uidornickname.lower():
# If the admin exist, delete and do not go further
self.UID_ADMIN_DB.remove(record)
self.Logs.debug(f'nickname ({record.nickname}) 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

298
core/classes/channel.py Normal file
View File

@@ -0,0 +1,298 @@
from re import findall
from typing import Any, Optional, Literal, TYPE_CHECKING
if TYPE_CHECKING:
from core.definition import MChannel
from core.loader import Loader
class Channel:
UID_CHANNEL_DB: list['MChannel'] = []
"""List that contains all the Channels objects (ChannelModel)
"""
def __init__(self, loader: 'Loader') -> None:
self.Logs = loader.Logs
self.Base = loader.Base
self.Utils = loader.Utils
return None
def insert(self, new_channel: 'MChannel') -> bool:
"""This method will insert a new channel and if the channel exist it will update the user list (uids)
Args:
new_channel (MChannel): The channel model object
Returns:
bool: True if new channel, False if channel exist (However UID could be updated)
"""
result = False
exist = False
if not self.is_valid_channel(new_channel.name):
self.Logs.error(f"The channel {new_channel.name} is not valid, channel must start with #")
return False
for record in self.UID_CHANNEL_DB:
if record.name.lower() == new_channel.name.lower():
# If the channel exist, update the user list and do not go further
exist = True
# self.Logs.debug(f'{record.name} already exist')
for user in new_channel.uids:
record.uids.append(user)
# Supprimer les doublons
del_duplicates = list(set(record.uids))
record.uids = del_duplicates
# self.Logs.debug(f'Updating a new UID to the channel {record}')
return result
if not exist:
# If the channel don't exist, then create it
new_channel.name = new_channel.name.lower()
self.UID_CHANNEL_DB.append(new_channel)
result = True
# self.Logs.debug(f'New Channel Created: ({new_channel})')
if not result:
self.Logs.critical(f'The Channel Object was not inserted {new_channel}')
self.clean_channel()
return result
def delete(self, channel_name: str) -> bool:
"""Delete channel from the UID_CHANNEL_DB
Args:
channel_name (str): The Channel name
Returns:
bool: True if it was deleted
"""
chan_obj = self.get_channel(channel_name)
if chan_obj is None:
return False
self.UID_CHANNEL_DB.remove(chan_obj)
return True
def delete_user_from_channel(self, channel_name: str, uid: str) -> bool:
"""Delete a user from a channel
Args:
channel_name (str): The channel name
uid (str): The Client UID
Returns:
bool: True if the client has been deleted from the channel
"""
try:
result = False
chan_obj = self.get_channel(channel_name)
if chan_obj is None:
return result
for userid in chan_obj.uids:
if self.Utils.clean_uid(userid) == self.Utils.clean_uid(uid):
chan_obj.uids.remove(userid)
result = True
self.clean_channel()
return result
except ValueError as ve:
self.Logs.error(f'{ve}')
def delete_user_from_all_channel(self, uid:str) -> bool:
"""Delete a client from all channels
Args:
uid (str): The client UID
Returns:
bool: True if the client has been deleted from all channels
"""
try:
result = False
for record in self.UID_CHANNEL_DB:
for user_id in record.uids:
if self.Utils.clean_uid(user_id) == self.Utils.clean_uid(uid):
record.uids.remove(user_id)
result = True
self.clean_channel()
return result
except ValueError as ve:
self.Logs.error(f'{ve}')
def add_user_to_a_channel(self, channel_name: str, uid: str) -> bool:
"""Add a client to a channel
Args:
channel_name (str): The channel name
uid (str): The client UID
Returns:
bool: True is the clien has been added
"""
try:
chan_obj = self.get_channel(channel_name)
if chan_obj is None:
# Create a new channel if the channel don't exist
self.Logs.debug(f"New channel will be created ({channel_name} - {uid})")
return self.insert(MChannel(channel_name, uids=[uid]))
chan_obj.uids.append(uid)
del_duplicates = list(set(chan_obj.uids))
chan_obj.uids = del_duplicates
return True
except Exception as err:
self.Logs.error(f'{err}')
return False
def is_user_present_in_channel(self, channel_name: str, uid: str) -> bool:
"""Check if a user is present in the channel
Args:
channel_name (str): The channel name to check
uid (str): The client UID
Returns:
bool: True if the user is present in the channel
"""
chan = self.get_channel(channel_name=channel_name)
if chan is None:
return False
clean_uid = self.Utils.clean_uid(uid=uid)
for chan_uid in chan.uids:
if self.Utils.clean_uid(chan_uid) == clean_uid:
return True
return False
def clean_channel(self) -> None:
"""If channel doesn't contain any client this method will remove the channel
"""
try:
for record in self.UID_CHANNEL_DB:
if not record.uids:
self.UID_CHANNEL_DB.remove(record)
return None
except Exception as err:
self.Logs.error(f'{err}')
def get_channel(self, channel_name: str) -> Optional['MChannel']:
"""Get the channel object
Args:
channel_name (str): The Channel name
Returns:
MChannel: The channel object model if exist else None
"""
for record in self.UID_CHANNEL_DB:
if record.name.lower() == channel_name.lower():
return record
return None
def get_channel_asdict(self, channel_name: str) -> Optional[dict[str, Any]]:
channel_obj: Optional['MChannel'] = self.get_channel(channel_name)
if channel_obj is None:
return None
return channel_obj.to_dict()
def is_valid_channel(self, channel_to_check: str) -> bool:
"""Check if the string has the # caractere and return True if this is a valid channel
Args:
channel_to_check (str): The string to test if it is a channel or not
Returns:
bool: True if the string is a channel / False if this is not a channel
"""
try:
if channel_to_check is None:
return False
pattern = fr'^#'
isChannel = findall(pattern, channel_to_check)
if not isChannel:
return False
else:
return True
except TypeError as te:
self.Logs.error(f'TypeError: [{channel_to_check}] - {te}')
except Exception as err:
self.Logs.error(f'Error Not defined: {err}')
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.
Args:
action (Literal['add','del']): Action on the database
module_name (str): The module name (mod_test)
channel_name (str): The channel name (With #)
Returns:
bool: True if action done
"""
try:
channel_name = channel_name.lower() if self.is_valid_channel(channel_name) else None
core_table = self.Base.Config.TABLE_CHANNEL
if not channel_name:
self.Logs.warning(f'The channel [{channel_name}] is not correct')
return False
match action:
case 'add':
mes_donnees = {'module_name': module_name, 'channel_name': channel_name}
response = self.Base.db_execute_query(f"SELECT id FROM {core_table} WHERE module_name = :module_name AND channel_name = :channel_name", mes_donnees)
is_channel_exist = response.fetchone()
if is_channel_exist is None:
mes_donnees = {'datetime': self.Utils.get_sdatetime(), 'channel_name': channel_name, 'module_name': module_name}
insert = self.Base.db_execute_query(f"INSERT INTO {core_table} (datetime, channel_name, module_name) VALUES (:datetime, :channel_name, :module_name)", mes_donnees)
if insert.rowcount:
self.Logs.debug(f'New channel added: channel={channel_name} / module_name={module_name}')
return True
else:
return False
case 'del':
mes_donnes = {'channel_name': channel_name, 'module_name': module_name}
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:
self.Logs.debug(f'Channel deleted: channel={channel_name} / module: {module_name}')
return True
else:
return False
case _:
return False
except Exception as err:
self.Logs.error(err)

239
core/classes/client.py Normal file
View File

@@ -0,0 +1,239 @@
from re import sub
from typing import Any, Optional, Union, TYPE_CHECKING
if TYPE_CHECKING:
from core.loader import Loader
from core.definition import MClient
class Client:
CLIENT_DB: list['MClient'] = []
def __init__(self, loader: 'Loader'):
self.Logs = loader.Logs
self.Base = loader.Base
def insert(self, new_client: 'MClient') -> bool:
"""Insert a new User object
Args:
new_client (MClient): New Client object
Returns:
bool: True if inserted
"""
client_obj = self.get_Client(new_client.uid)
if not client_obj is None:
# User already created return False
return False
self.CLIENT_DB.append(new_client)
return True
def update_nickname(self, uid: str, newNickname: str) -> bool:
"""Update the nickname starting from the UID
Args:
uid (str): UID of the user
newNickname (str): New nickname
Returns:
bool: True if updated
"""
user_obj = self.get_Client(uidornickname=uid)
if user_obj is None:
return False
user_obj.nickname = newNickname
return True
def update_mode(self, uidornickname: str, modes: str) -> bool:
"""Updating user mode
Args:
uidornickname (str): The UID or Nickname of the user
modes (str): new modes to update
Returns:
bool: True if user mode has been updaed
"""
response = True
user_obj = self.get_Client(uidornickname=uidornickname)
if user_obj is None:
return False
action = modes[0]
new_modes = modes[1:]
existing_umodes = user_obj.umodes
umodes = user_obj.umodes
if action == '+':
for nm in new_modes:
if nm not in existing_umodes:
umodes += nm
elif action == '-':
for nm in new_modes:
if nm in existing_umodes:
umodes = umodes.replace(nm, '')
else:
return False
liste_umodes = list(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)
user_obj.umodes = f"+{final_umodes}"
return response
def delete(self, uid: str) -> bool:
"""Delete the User starting from the UID
Args:
uid (str): UID of the user
Returns:
bool: True if deleted
"""
user_obj = self.get_Client(uidornickname=uid)
if user_obj is None:
return False
self.CLIENT_DB.remove(user_obj)
return True
def get_Client(self, uidornickname: str) -> Optional['MClient']:
"""Get The Client Object model
Args:
uidornickname (str): UID or Nickname
Returns:
UserModel|None: The UserModel Object | None
"""
for record in self.CLIENT_DB:
if record.uid == uidornickname:
return record
elif record.nickname == uidornickname:
return record
return None
def get_uid(self, uidornickname:str) -> Optional[str]:
"""Get the UID of the user starting from the UID or the Nickname
Args:
uidornickname (str): UID or Nickname
Returns:
str|None: Return the UID
"""
client_obj = self.get_Client(uidornickname=uidornickname)
if client_obj is None:
return None
return client_obj.uid
def get_nickname(self, uidornickname:str) -> Union[str, None]:
"""Get the Nickname starting from UID or the nickname
Args:
uidornickname (str): UID or Nickname of the user
Returns:
str|None: the nickname
"""
client_obj = self.get_Client(uidornickname=uidornickname)
if client_obj is None:
return None
return client_obj.nickname
def get_client_asdict(self, uidornickname: str) -> Optional[dict[str, Any]]:
"""Transform User Object to a dictionary
Args:
uidornickname (str): The UID or The nickname
Returns:
Union[dict[str, any], None]: User Object as a dictionary or None
"""
client_obj = self.get_Client(uidornickname=uidornickname)
if client_obj is None:
return None
return client_obj.to_dict()
def is_exist(self, uidornikname: str) -> bool:
"""Check if the UID or the nickname exist in the USER DB
Args:
uidornickname (str): The UID or the NICKNAME
Returns:
bool: True if exist
"""
user_obj = self.get_Client(uidornickname=uidornikname)
if user_obj is None:
return False
return True
def db_is_account_exist(self, account: str) -> bool:
"""Check if the account exist in the database
Args:
account (str): The account to check
Returns:
bool: True if exist
"""
table_client = self.Base.Config.TABLE_CLIENT
account_to_check = {'account': account.lower()}
account_to_check_query = self.Base.db_execute_query(f"""
SELECT id FROM {table_client} WHERE LOWER(account) = :account
""", account_to_check)
account_to_check_result = account_to_check_query.fetchone()
if account_to_check_result:
self.Logs.error(f"Account ({account}) already exist")
return True
return False
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
"""
pattern = fr'[:|@|%|\+|~|\*]*'
parsed_UID = sub(pattern, '', uid)
if not parsed_UID:
return None
return parsed_UID

83
core/classes/commands.py Normal file
View File

@@ -0,0 +1,83 @@
from typing import TYPE_CHECKING, Optional
from core.definition import MCommand
if TYPE_CHECKING:
from core.loader import Loader
class Command:
DB_COMMANDS: list['MCommand'] = []
def __init__(self, loader: 'Loader'):
self.Loader = loader
self.Base = loader.Base
def build(self, new_command_obj: MCommand) -> bool:
command = self.get_command(new_command_obj.command_name, new_command_obj.module_name)
if command is None:
self.DB_COMMANDS.append(new_command_obj)
return True
# Update command if it exist
# Removing the object
if self.drop_command(command.command_name, command.module_name):
# Add the new object
self.DB_COMMANDS.append(new_command_obj)
return True
return False
def get_command(self, command_name: str, module_name: str) -> Optional[MCommand]:
for command in self.DB_COMMANDS:
if command.command_name.lower() == command_name and command.module_name == module_name:
return command
return None
def drop_command(self, command_name: str, module_name: str) -> bool:
cmd = self.get_command(command_name, module_name)
if cmd is not None:
self.DB_COMMANDS.remove(cmd)
return True
return False
def get_ordered_commands(self) -> list[MCommand]:
return sorted(self.DB_COMMANDS, key=lambda c: (c.command_level, c.module_name))
def get_commands_by_level(self, level: int = 0) -> Optional[list[MCommand]]:
cmd_list = self.get_ordered_commands()
new_list: list[MCommand] = []
for cmd in cmd_list:
if cmd.command_level <= level:
new_list.append(cmd)
return new_list
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 in [command.command_name 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

64
core/classes/config.py Normal file
View File

@@ -0,0 +1,64 @@
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._config_model: MConfig = 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
def get_config_model(self) -> MConfig:
return self._config_model
def __load_json_service_configuration(self) -> Optional[dict[str, Any]]:
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:
self.Logs.error(f'FileNotFound: {fe}')
self.Logs.error('Configuration file not found please create config/configuration.json')
exit(0)
except KeyError as ke:
self.Logs.error(f'Key Error: {ke}')
self.Logs.error('The key must be defined in core/configuration.json')
def __load_service_configuration(self) -> MConfig:
try:
import_config = self.__load_json_service_configuration()
Model_keys = MConfig().to_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)
self.Logs.warning(f"[!] The key {json_conf} is not expected, it has been removed from the system ! please remove it from configuration.json file [!]")
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)

19
core/classes/protocol.py Normal file
View File

@@ -0,0 +1,19 @@
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,668 @@
from re import match, findall
from datetime import datetime
from typing import TYPE_CHECKING
from ssl import SSLEOFError, SSLError
if TYPE_CHECKING:
from core.irc import Irc
class Inspircd:
def __init__(self, ircInstance: 'Irc'):
self.name = 'InspIRCd-4'
self.__Irc = ircInstance
self.__Config = ircInstance.Config
self.__Base = ircInstance.Base
self.__Utils = ircInstance.Loader.Utils
self.__Logs = ircInstance.Loader.Logs
self.__Logs.info(f"** Loading protocol [{__name__}]")
def send2socket(self, message: str, print_log: bool = True) -> None:
"""Envoit les commandes à envoyer au serveur.
Args:
string (Str): contient la commande à envoyer au serveur.
"""
try:
with self.__Base.lock:
self.__Irc.IrcSocket.send(f"{message}\r\n".encode(self.__Config.SERVEUR_CHARSET[0]))
if print_log:
self.__Logs.debug(f'<< {message}')
except UnicodeDecodeError as ude:
self.__Logs.error(f'Decode Error try iso-8859-1 - {ude} - {message}')
self.__Irc.IrcSocket.send(f"{message}\r\n".encode(self.__Config.SERVEUR_CHARSET[1],'replace'))
except UnicodeEncodeError as uee:
self.__Logs.error(f'Encode Error try iso-8859-1 - {uee} - {message}')
self.__Irc.IrcSocket.send(f"{message}\r\n".encode(self.__Config.SERVEUR_CHARSET[1],'replace'))
except AssertionError as ae:
self.__Logs.warning(f'Assertion Error {ae} - message: {message}')
except SSLEOFError as soe:
self.__Logs.error(f"SSLEOFError: {soe} - {message}")
except SSLError as se:
self.__Logs.error(f"SSLError: {se} - {message}")
except OSError as oe:
self.__Logs.error(f"OSError: {oe} - {message}")
except AttributeError as ae:
self.__Logs.critical(f"Attribute Error: {ae}")
def send_priv_msg(self, nick_from: str, msg: str, channel: str = None, nick_to: str = None):
"""Sending PRIVMSG to a channel or to a nickname by batches
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.
"""
try:
batch_size = self.__Config.BATCH_SIZE
User_from = self.__Irc.User.get_user(nick_from)
User_to = self.__Irc.User.get_user(nick_to) if nick_to is None else None
if User_from is None:
self.__Logs.error(f"The sender nickname [{nick_from}] do not exist")
return None
if not channel is None:
for i in range(0, len(str(msg)), batch_size):
batch = str(msg)[i:i+batch_size]
self.send2socket(f":{User_from.uid} PRIVMSG {channel} :{batch}")
if not nick_to is None:
for i in range(0, len(str(msg)), batch_size):
batch = str(msg)[i:i+batch_size]
self.send2socket(f":{nick_from} PRIVMSG {User_to.uid} :{batch}")
except Exception as err:
self.__Logs.error(f"General Error: {err}")
def send_notice(self, nick_from: str, nick_to: str, msg: str) -> None:
"""Sending NOTICE by batches
Args:
msg (str): The message to send to the server
nick_from (str): The sender Nickname
nick_to (str): The reciever nickname
"""
try:
batch_size = self.__Config.BATCH_SIZE
User_from = self.__Irc.User.get_user(nick_from)
User_to = self.__Irc.User.get_user(nick_to)
if User_from is None or User_to is None:
self.__Logs.error(f"The sender [{nick_from}] or the Reciever [{nick_to}] do not exist")
return None
for i in range(0, len(str(msg)), batch_size):
batch = str(msg)[i:i+batch_size]
self.send2socket(f":{User_from.uid} NOTICE {User_to.uid} :{batch}")
except Exception as err:
self.__Logs.error(f"General Error: {err}")
def send_link(self):
"""Créer le link et envoyer les informations nécessaires pour la
connexion au serveur.
"""
nickname = self.__Config.SERVICE_NICKNAME
username = self.__Config.SERVICE_USERNAME
realname = self.__Config.SERVICE_REALNAME
chan = self.__Config.SERVICE_CHANLOG
info = self.__Config.SERVICE_INFO
smodes = self.__Config.SERVICE_SMODES
cmodes = self.__Config.SERVICE_CMODES
umodes = self.__Config.SERVICE_UMODES
host = self.__Config.SERVICE_HOST
service_name = self.__Config.SERVICE_NAME
password = self.__Config.SERVEUR_PASSWORD
link = self.__Config.SERVEUR_LINK
server_id = self.__Config.SERVEUR_ID
service_id = self.__Config.SERVICE_ID
version = self.__Config.CURRENT_VERSION
unixtime = self.__Utils.get_unixtime()
self.send2socket(f"CAPAB START 1206")
self.send2socket(f"CAPAB CAPABILITIES :NICKMAX=30 CHANMAX=64 MAXMODES=20 IDENTMAX=10 MAXQUIT=255 MAXTOPIC=307 MAXKICK=255 MAXREAL=128 MAXAWAY=200 MAXHOST=64 MAXLINE=512 CASEMAPPING=ascii GLOBOPS=0")
self.send2socket(f"CAPAB END")
self.send2socket(f"SERVER {link} {password} {server_id} :{info}")
self.send2socket(f"BURST {unixtime}")
self.send2socket(f":{server_id} ENDBURST")
self.__Logs.debug(f'>> {__name__} Link information sent to the server')
def gline(self, nickname: str, hostname: str, set_by: str, expire_timestamp: int, set_at_timestamp: int, reason: str) -> None:
# TKL + G user host set_by expire_timestamp set_at_timestamp :reason
self.send2socket(f":{self.__Config.SERVEUR_ID} TKL + G {nickname} {hostname} {set_by} {expire_timestamp} {set_at_timestamp} :{reason}")
return None
def send_set_nick(self, newnickname: str) -> None:
self.send2socket(f":{self.__Config.SERVICE_NICKNAME} NICK {newnickname}")
return None
def send_squit(self, server_id: str, server_link: str, reason: str) -> None:
if not reason:
reason = 'Service Shutdown'
self.send2socket(f":{server_id} SQUIT {server_link} :{reason}")
return None
def send_ungline(self, nickname:str, hostname: str) -> None:
self.send2socket(f":{self.__Config.SERVEUR_ID} TKL - G {nickname} {hostname} {self.__Config.SERVICE_NICKNAME}")
return None
def send_kline(self, nickname: str, hostname: str, set_by: str, expire_timestamp: int, set_at_timestamp: int, reason: str) -> None:
# TKL + k user host set_by expire_timestamp set_at_timestamp :reason
self.send2socket(f":{self.__Config.SERVEUR_ID} TKL + k {nickname} {hostname} {set_by} {expire_timestamp} {set_at_timestamp} :{reason}")
return None
def send_sjoin(self, channel: str) -> None:
if not self.__Irc.Channel.is_valid_channel(channel):
self.__Logs.error(f"The channel [{channel}] is not valid")
return None
self.send2socket(f":{self.__Config.SERVEUR_ID} SJOIN {self.__Utils.get_unixtime()} {channel} + :{self.__Config.SERVICE_ID}")
# Add defender to the channel uids list
self.__Irc.Channel.insert(self.__Irc.Loader.Definition.MChannel(name=channel, uids=[self.__Config.SERVICE_ID]))
return None
def send_quit(self, uid: str, reason: str, print_log: True) -> None:
"""Send quit message
Args:
uidornickname (str): The UID or the Nickname
reason (str): The reason for the quit
"""
user_obj = self.__Irc.User.get_user(uidornickname=uid)
reputationObj = self.__Irc.Reputation.get_Reputation(uidornickname=uid)
if not user_obj is None:
self.send2socket(f":{user_obj.uid} QUIT :{reason}", print_log=print_log)
self.__Irc.User.delete(user_obj.uid)
if not reputationObj is None:
self.__Irc.Reputation.delete(reputationObj.uid)
if not self.__Irc.Channel.delete_user_from_all_channel(uid):
self.__Logs.error(f"The UID [{uid}] has not been deleted from all channels")
return None
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
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.
"""
# {self.Config.SERVEUR_ID} UID
# {clone.nickname} 1 {self.__Utils.get_unixtime()} {clone.username} {clone.hostname} {clone.uid} * {clone.umodes} {clone.vhost} * {self.Base.encode_ip(clone.remote_ip)} :{clone.realname}
try:
unixtime = self.__Utils.get_unixtime()
encoded_ip = self.__Base.encode_ip(remote_ip)
# Create the user
self.__Irc.User.insert(
self.__Irc.Loader.Definition.MUser(
uid=uid, nickname=nickname, username=username,
realname=realname,hostname=hostname, umodes=umodes,
vhost=vhost, remote_ip=remote_ip
)
)
uid_msg = f":{self.__Config.SERVEUR_ID} UID {nickname} 1 {unixtime} {username} {hostname} {uid} * {umodes} {vhost} * {encoded_ip} :{realname}"
self.send2socket(uid_msg, print_log=print_log)
return None
except Exception as err:
self.__Logs.error(f"{__name__} - General Error: {err}")
def send_join_chan(self, uidornickname: str, channel: str, password: str = None, print_log: bool = True) -> None:
"""Joining a channel
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.
"""
userObj = self.__Irc.User.get_user(uidornickname)
passwordChannel = password if not password is None else ''
if userObj is None:
return None
if not self.__Irc.Channel.is_valid_channel(channel):
self.__Logs.error(f"The channel [{channel}] is not valid")
return None
self.send2socket(f":{userObj.uid} JOIN {channel} {passwordChannel}", print_log=print_log)
# Add defender to the channel uids list
self.__Irc.Channel.insert(self.__Irc.Loader.Definition.MChannel(name=channel, uids=[userObj.uid]))
return None
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.
"""
userObj = self.__Irc.User.get_user(uidornickname)
if userObj is None:
self.__Logs.error(f"The user [{uidornickname}] is not valid")
return None
if not self.__Irc.Channel.is_valid_channel(channel):
self.__Logs.error(f"The channel [{channel}] is not valid")
return None
self.send2socket(f":{userObj.uid} PART {channel}", print_log=print_log)
# Add defender to the channel uids list
self.__Irc.Channel.delete_user_from_channel(channel, userObj.uid)
return None
def send_unkline(self, nickname:str, hostname: str) -> None:
self.send2socket(f":{self.__Config.SERVEUR_ID} TKL - K {nickname} {hostname} {self.__Config.SERVICE_NICKNAME}")
return None
def on_umode2(self, serverMsg: list[str]) -> None:
"""Handle umode2 coming from a server
Args:
serverMsg (list[str]): Original server message
"""
try:
# [':adator_', 'UMODE2', '-iwx']
userObj = self.__Irc.User.get_user(str(serverMsg[0]).lstrip(':'))
userMode = serverMsg[2]
if userObj is None: # If user is not created
return None
# save previous user modes
old_umodes = userObj.umodes
# TODO : User object should be able to update user modes
if self.__Irc.User.update_mode(userObj.uid, userMode):
return None
# self.__Logs.debug(f"Updating user mode for [{userObj.nickname}] [{old_umodes}] => [{userObj.umodes}]")
return None
except IndexError as ie:
self.__Logs.error(f"{__name__} - Index Error: {ie}")
except Exception as err:
self.__Logs.error(f"{__name__} - General Error: {err}")
def on_quit(self, serverMsg: list[str]) -> None:
"""Handle quit coming from a server
Args:
serverMsg (list[str]): Original server message
"""
try:
# ['@unrealircd.org/userhost=...@192.168.1.10;unrealircd.org/userip=...@192.168.1.10;msgid=CssUrV08BzekYuq7BfvPHn;time=2024-11-02T15:03:33.182Z', ':001JKNY0N', 'QUIT', ':Quit:', '....']
uid_who_quit = str(serverMsg[1]).lstrip(':')
self.__Irc.Channel.delete_user_from_all_channel(uid_who_quit)
self.__Irc.User.delete(uid_who_quit)
self.__Irc.Reputation.delete(uid_who_quit)
return None
except IndexError as ie:
self.__Logs.error(f"{__name__} - Index Error: {ie}")
except Exception as err:
self.__Logs.error(f"{__name__} - General Error: {err}")
def on_squit(self, serverMsg: list[str]) -> None:
"""Handle squit coming from a server
Args:
serverMsg (list[str]): Original server message
"""
# ['@msgid=QOEolbRxdhpVW5c8qLkbAU;time=2024-09-21T17:33:16.547Z', 'SQUIT', 'defender.deb.biz.st', ':Connection', 'closed']
server_hostname = serverMsg[2]
uid_to_delete = None
for s_user in self.__Irc.User.UID_DB:
if s_user.hostname == server_hostname and 'S' in s_user.umodes:
uid_to_delete = s_user.uid
if uid_to_delete is None:
return None
self.__Irc.User.delete(uid_to_delete)
self.__Irc.Channel.delete_user_from_all_channel(uid_to_delete)
return None
def on_protoctl(self, serverMsg: list[str]) -> None:
"""Handle protoctl coming from a server
Args:
serverMsg (list[str]): Original server message
"""
if len(serverMsg) > 5:
if '=' in serverMsg[5]:
serveur_hosting_id = str(serverMsg[5]).split('=')
self.__Config.HSID = serveur_hosting_id[1]
return None
def on_nick(self, serverMsg: list[str]) -> None:
"""Handle nick coming from a server
new nickname
Args:
serverMsg (list[str]): Original server message
"""
try:
# ['@unrealircd.org/geoip=FR;unrealircd.org/', ':001OOU2H3', 'NICK', 'WebIrc', '1703795844']
# Changement de nickname
uid = str(serverMsg[1]).lstrip(':')
newnickname = serverMsg[3]
self.__Irc.User.update_nickname(uid, newnickname)
self.__Irc.Client.update_nickname(uid, newnickname)
self.__Irc.Admin.update_nickname(uid, newnickname)
return None
except IndexError as ie:
self.__Logs.error(f"{__name__} - Index Error: {ie}")
except Exception as err:
self.__Logs.error(f"{__name__} - General Error: {err}")
def on_sjoin(self, serverMsg: list[str]) -> None:
"""Handle sjoin coming from a server
Args:
serverMsg (list[str]): Original server message
"""
try:
# ['@msgid=5sTwGdj349D82L96p749SY;time=2024-08-15T09:50:23.528Z', ':001', 'SJOIN', '1721564574', '#welcome', ':001JD94QH']
# ['@msgid=bvceb6HthbLJapgGLXn1b0;time=2024-08-15T09:50:11.464Z', ':001', 'SJOIN', '1721564574', '#welcome', '+lnrt', '13', ':001CIVLQF', '+11ZAAAAAB', '001QGR10C', '*@0014UE10B', '001NL1O07', '001SWZR05', '001HB8G04', '@00BAAAAAJ', '0019M7101']
# ['@msgid=SKUeuVzOrTShRDduq8VerX;time=2024-08-23T19:37:04.266Z', ':001', 'SJOIN', '1723993047', '#welcome', '+lnrt', '13',
# ':001T6VU3F', '001JGWB2K', '@11ZAAAAAB',
# '001F16WGR', '001X9YMGQ', '*+001DYPFGP', '@00BAAAAAJ', '001AAGOG9', '001FMFVG8', '001DAEEG7',
# '&~G:unknown-users', '"~G:websocket-users', '"~G:known-users', '"~G:webirc-users']
serverMsg.pop(0)
channel = str(serverMsg[3]).lower()
len_cmd = len(serverMsg)
list_users:list = []
occurence = 0
start_boucle = 0
# Trouver le premier user
for i in range(len_cmd):
s: list = findall(fr':', serverMsg[i])
if s:
occurence += 1
if occurence == 2:
start_boucle = i
# Boucle qui va ajouter l'ensemble des users (UID)
for i in range(start_boucle, len(serverMsg)):
parsed_UID = str(serverMsg[i])
clean_uid = self.__Utils.clean_uid(parsed_UID)
if not clean_uid is None and len(clean_uid) == 9:
list_users.append(parsed_UID)
if list_users:
self.__Irc.Channel.insert(
self.__Irc.Loader.Definition.MChannel(
name=channel,
uids=list_users
)
)
return None
except IndexError as ie:
self.__Logs.error(f"{__name__} - Index Error: {ie}")
except Exception as err:
self.__Logs.error(f"{__name__} - General Error: {err}")
def on_part(self, serverMsg: list[str]) -> None:
"""Handle part coming from a server
Args:
serverMsg (list[str]): Original server message
"""
try:
# ['@unrealircd.org/geoip=FR;unrealircd.org/userhost=50d6492c@80.214.73.44;unrealircd.org/userip=50d6492c@80.214.73.44;msgid=YSIPB9q4PcRu0EVfC9ci7y-/mZT0+Gj5FLiDSZshH5NCw;time=2024-08-15T15:35:53.772Z',
# ':001EPFBRD', 'PART', '#welcome', ':WEB', 'IRC', 'Paris']
uid = str(serverMsg[1]).lstrip(':')
channel = str(serverMsg[3]).lower()
self.__Irc.Channel.delete_user_from_channel(channel, uid)
return None
except IndexError as ie:
self.__Logs.error(f"{__name__} - Index Error: {ie}")
except Exception as err:
self.__Logs.error(f"{__name__} - General Error: {err}")
def on_uid(self, serverMsg: list[str]) -> None:
"""Handle uid message coming from the server
Args:
serverMsg (list[str]): Original server message
"""
# ['@s2s-md/geoip=cc=GB|cd=United\\sKingdom|asn=16276|asname=OVH\\sSAS;s2s-md/tls_cipher=TLSv1.3-TLS_CHACHA20_POLY1305_SHA256;s2s-md/creationtime=1721564601',
# ':001', 'UID', 'albatros', '0', '1721564597', 'albatros', 'vps-91b2f28b.vps.ovh.net',
# '001HB8G04', '0', '+iwxz', 'Clk-A62F1D18.vps.ovh.net', 'Clk-A62F1D18.vps.ovh.net', 'MyZBwg==', ':...']
try:
isWebirc = True if 'webirc' in serverMsg[0] else False
isWebsocket = True if 'websocket' in serverMsg[0] else False
uid = str(serverMsg[8])
nickname = str(serverMsg[3])
username = str(serverMsg[6])
hostname = str(serverMsg[7])
umodes = str(serverMsg[10])
vhost = str(serverMsg[11])
if not 'S' in umodes:
remote_ip = self.__Base.decode_ip(str(serverMsg[13]))
else:
remote_ip = '127.0.0.1'
# extract realname
realname = ' '.join(serverMsg[14:]).lstrip(':')
# Extract Geoip information
pattern = r'^.*geoip=cc=(\S{2}).*$'
geoip_match = match(pattern, serverMsg[0])
if geoip_match:
geoip = geoip_match.group(1)
else:
geoip = None
score_connexion = 0
self.__Irc.User.insert(
self.__Irc.Loader.Definition.MUser(
uid=uid,
nickname=nickname,
username=username,
realname=realname,
hostname=hostname,
umodes=umodes,
vhost=vhost,
isWebirc=isWebirc,
isWebsocket=isWebsocket,
remote_ip=remote_ip,
geoip=geoip,
score_connexion=score_connexion,
connexion_datetime=datetime.now()
)
)
return None
except IndexError as ie:
self.__Logs.error(f"{__name__} - Index Error: {ie}")
except Exception as err:
self.__Logs.error(f"{__name__} - General Error: {err}")
def on_server_ping(self, serverMsg: list[str]) -> None:
"""Send a PONG message to the server
Args:
serverMsg (list[str]): List of str coming from the server
"""
try:
# InspIRCd 3:
# <- :3IN PING 808
# -> :808 PONG 3IN
hsid = str(serverMsg[0]).replace(':','')
self.send2socket(f":{self.__Config.SERVEUR_ID} PONG {hsid}", print_log=True)
return None
except Exception as err:
self.__Logs.error(f"{__name__} - General Error: {err}")
def on_version(self, serverMsg: list[str]) -> None:
"""Sending Server Version to the server
Args:
serverMsg (list[str]): List of str coming from the server
"""
# ['@unrealircd.org/userhost=StatServ@stats.deb.biz.st;draft/bot;bot;msgid=ehfAq3m2yjMjhgWEfi1UCS;time=2024-10-26T13:49:06.299Z', ':00BAAAAAI', 'PRIVMSG', '12ZAAAAAB', ':\x01VERSION\x01']
# Réponse a un CTCP VERSION
try:
nickname = self.__Irc.User.get_nickname(self.__Utils.clean_uid(serverMsg[1]))
dnickname = self.__Config.SERVICE_NICKNAME
arg = serverMsg[4].replace(':', '')
if nickname is None:
return None
if arg == '\x01VERSION\x01':
self.send2socket(f':{dnickname} NOTICE {nickname} :\x01VERSION Service {self.__Config.SERVICE_NICKNAME} V{self.__Config.CURRENT_VERSION}\x01')
return None
except Exception as err:
self.__Logs.error(f"{__name__} - General Error: {err}")
def on_time(self, serverMsg: list[str]) -> None:
"""Sending TIME answer to a requestor
Args:
serverMsg (list[str]): List of str coming from the server
"""
# ['@unrealircd.org/userhost=StatServ@stats.deb.biz.st;draft/bot;bot;msgid=ehfAq3m2yjMjhgWEfi1UCS;time=2024-10-26T13:49:06.299Z', ':00BAAAAAI', 'PRIVMSG', '12ZAAAAAB', ':\x01TIME\x01']
# Réponse a un CTCP VERSION
try:
nickname = self.__Irc.User.get_nickname(self.__Utils.clean_uid(serverMsg[1]))
dnickname = self.__Config.SERVICE_NICKNAME
arg = serverMsg[4].replace(':', '')
current_datetime = self.__Utils.get_sdatetime()
if nickname is None:
return None
if arg == '\x01TIME\x01':
self.send2socket(f':{dnickname} NOTICE {nickname} :\x01TIME {current_datetime}\x01')
return None
except Exception as err:
self.__Logs.error(f"{__name__} - General Error: {err}")
def on_ping(self, serverMsg: list[str]) -> None:
"""Sending a PING answer to requestor
Args:
serverMsg (list[str]): List of str coming from the server
"""
# ['@unrealircd.org/userhost=StatServ@stats.deb.biz.st;draft/bot;bot;msgid=ehfAq3m2yjMjhgWEfi1UCS;time=2024-10-26T13:49:06.299Z', ':001INC60B', 'PRIVMSG', '12ZAAAAAB', ':\x01PING', '762382207\x01']
# Réponse a un CTCP VERSION
try:
nickname = self.__Irc.User.get_nickname(self.__Utils.clean_uid(serverMsg[1]))
dnickname = self.__Config.SERVICE_NICKNAME
arg = serverMsg[4].replace(':', '')
if nickname is None:
return None
if arg == '\x01PING':
recieved_unixtime = int(serverMsg[5].replace('\x01',''))
current_unixtime = self.__Utils.get_unixtime()
ping_response = current_unixtime - recieved_unixtime
# self.__Irc.send2socket(f':{dnickname} NOTICE {nickname} :\x01PING {ping_response} secs\x01')
self.send_notice(
nick_from=dnickname,
nick_to=nickname,
msg=f"\x01PING {ping_response} secs\x01"
)
return None
except Exception as err:
self.__Logs.error(f"{__name__} - General Error: {err}")
def on_version_msg(self, serverMsg: list[str]) -> None:
"""Handle version coming from the server
Args:
serverMsg (list[str]): Original message from the server
"""
try:
# ['@label=0073', ':0014E7P06', 'VERSION', 'PyDefender']
getUser = self.__Irc.User.get_user(self.__Utils.clean_uid(serverMsg[1]))
if getUser is None:
return None
response_351 = f"{self.__Config.SERVICE_NAME.capitalize()}-{self.__Config.CURRENT_VERSION} {self.__Config.SERVICE_HOST} {self.name}"
self.send2socket(f':{self.__Config.SERVICE_HOST} 351 {getUser.nickname} {response_351}')
modules = self.__Irc.ModuleUtils.get_all_available_modules()
response_005 = ' | '.join(modules)
self.send2socket(f':{self.__Config.SERVICE_HOST} 005 {getUser.nickname} {response_005} are supported by this server')
return None
except Exception as err:
self.__Logs.error(f"{__name__} - General Error: {err}")

File diff suppressed because it is too large Load Diff

115
core/classes/rehash.py Normal file
View File

@@ -0,0 +1,115 @@
import importlib
import sys
import time
from typing import TYPE_CHECKING
import socket
from core.classes.protocol import Protocol
if TYPE_CHECKING:
from core.irc import Irc
# Modules impacted by rehashing!
REHASH_MODULES = [
'core.definition',
'core.utils',
'core.classes.config',
'core.base',
'core.classes.commands',
'core.classes.protocols.unreal6',
'core.classes.protocols.inspircd',
'core.classes.protocol'
]
def restart_service(uplink: 'Irc', reason: str = "Restarting with no reason!") -> None:
# reload modules.
for module in uplink.ModuleUtils.model_get_loaded_modules().copy():
uplink.ModuleUtils.unload_one_module(uplink, module.module_name)
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.Base.garbage_collector_thread()
# Reload configuration
uplink.Config = uplink.Loader.ConfModule.Configuration(uplink.Loader).get_config_model()
uplink.Base = uplink.Loader.BaseModule.Base(uplink.Loader)
uplink.Protocol = Protocol(uplink.Config.SERVEUR_PROTOCOL, uplink.ircObject).Protocol
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="Defender Power off")
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 ...')
uplink.init_service_user()
uplink.Utils.create_socket(uplink)
uplink.Protocol.send_link()
uplink.join_saved_channels()
uplink.ModuleUtils.db_load_all_existing_modules(uplink)
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)
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.Config = uplink.Loader.ConfModule.Configuration(uplink.Loader).get_config_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.Commands.DB_COMMANDS = uplink.Settings.get_cache('db_commands')
uplink.Base = uplink.Loader.BaseModule.Base(uplink.Loader)
uplink.Protocol = Protocol(uplink.Config.SERVEUR_PROTOCOL, uplink.ircObject).Protocol
# 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

157
core/classes/reputation.py Normal file
View File

@@ -0,0 +1,157 @@
from typing import TYPE_CHECKING, Optional
from core.definition import MReputation
if TYPE_CHECKING:
from core.loader import Loader
class Reputation:
UID_REPUTATION_DB: list[MReputation] = []
def __init__(self, loader: 'Loader'):
self.Logs = loader.Logs
self.MReputation: MReputation = MReputation
def insert(self, new_reputation_user: MReputation) -> bool:
"""Insert a new Reputation User object
Args:
new_reputation_user (MReputation): New Reputation Model object
Returns:
bool: True if inserted
"""
result = False
exist = False
for record in self.UID_REPUTATION_DB:
if record.uid == new_reputation_user.uid:
# If the user exist then return False and do not go further
exist = True
self.Logs.debug(f'{record.uid} already exist')
return result
if not exist:
self.UID_REPUTATION_DB.append(new_reputation_user)
result = True
self.Logs.debug(f'New Reputation User Captured: ({new_reputation_user})')
if not result:
self.Logs.critical(f'The Reputation User Object was not inserted {new_reputation_user}')
return result
def update(self, uid: str, new_nickname: str) -> bool:
"""Update the nickname starting from the UID
Args:
uid (str): UID of the user
newNickname (str): New nickname
Returns:
bool: True if updated
"""
reputation_obj = self.get_Reputation(uid)
if reputation_obj is None:
return False
reputation_obj.nickname = new_nickname
return True
def delete(self, uid: str) -> bool:
"""Delete the User starting from the UID
Args:
uid (str): UID of the user
Returns:
bool: True if deleted
"""
result = False
if not self.is_exist(uid):
return result
for record in self.UID_REPUTATION_DB:
if record.uid == uid:
# If the user exist then remove and return True and do not go further
self.UID_REPUTATION_DB.remove(record)
result = True
self.Logs.debug(f'UID ({record.uid}) has been deleted')
return result
if not result:
self.Logs.critical(f'The UID {uid} was not deleted')
return result
def get_Reputation(self, uidornickname: str) -> Optional[MReputation]:
"""Get The User Object model
Args:
uidornickname (str): UID or Nickname
Returns:
UserModel|None: The UserModel Object | None
"""
for record in self.UID_REPUTATION_DB:
if record.uid == uidornickname:
return record
elif record.nickname == uidornickname:
return record
return None
def get_uid(self, uidornickname: str) -> Optional[str]:
"""Get the UID of the user starting from the UID or the Nickname
Args:
uidornickname (str): UID or Nickname
Returns:
str|None: Return the UID
"""
reputation_obj = self.get_Reputation(uidornickname)
if reputation_obj is None:
return None
return reputation_obj.uid
def get_nickname(self, uidornickname: str) -> Optional[str]:
"""Get the Nickname starting from UID or the nickname
Args:
uidornickname (str): UID or Nickname of the user
Returns:
str|None: the nickname
"""
reputation_obj = self.get_Reputation(uidornickname)
if reputation_obj is None:
return None
return reputation_obj.nickname
def is_exist(self, uidornickname: str) -> bool:
"""Check if the UID or the nickname exist in the reputation DB
Args:
uidornickname (str): The UID or the NICKNAME
Returns:
bool: True if exist
"""
reputation_obj = self.get_Reputation(uidornickname)
if isinstance(reputation_obj, MReputation):
return True
return False

70
core/classes/sasl.py Normal file
View File

@@ -0,0 +1,70 @@
from typing import Optional, Union, 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'):
self.Logs = loader.Logs # logger
def insert_sasl_client(self, psasl: 'MSasl') -> bool:
"""Insert a new Sasl authentication
Args:
new_user (UserModel): 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:
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

59
core/classes/settings.py Normal file
View File

@@ -0,0 +1,59 @@
'''This class should never be reloaded.
'''
from threading import Timer, Thread, RLock
from socket import socket
from typing import Any, Optional
from core.definition import MSModule
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[object] = {}
LOCK: RLock = RLock()
CONSOLE: bool = False
MAIN_SERVER_HOSTNAME: str = None
PROTOCTL_USER_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"""
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()

247
core/classes/user.py Normal file
View File

@@ -0,0 +1,247 @@
from re import sub
from typing import Any, Optional, TYPE_CHECKING
from datetime import datetime
if TYPE_CHECKING:
from core.loader import Loader
from core.definition import MUser
class User:
UID_DB: list['MUser'] = []
def __init__(self, loader: 'Loader'):
self.Logs = loader.Logs
self.Base = loader.Base
def insert(self, new_user: 'MUser') -> bool:
"""Insert a new User object
Args:
newUser (UserModel): New userModel object
Returns:
bool: True if inserted
"""
user_obj = self.get_user(new_user.uid)
if not user_obj is None:
# User already created return False
return False
self.UID_DB.append(new_user)
return True
def update_nickname(self, uid: str, new_nickname: str) -> bool:
"""Update the nickname starting from the UID
Args:
uid (str): UID of the user
new_nickname (str): New nickname
Returns:
bool: True if updated
"""
user_obj = self.get_user(uidornickname=uid)
if user_obj is None:
return False
user_obj.nickname = new_nickname
return True
def update_mode(self, uidornickname: str, modes: str) -> bool:
"""Updating user mode
Args:
uidornickname (str): The UID or Nickname of the user
modes (str): new modes to update
Returns:
bool: True if user mode has been updaed
"""
response = True
user_obj = self.get_user(uidornickname=uidornickname)
if user_obj is None:
return False
action = modes[0]
new_modes = modes[1:]
existing_umodes = user_obj.umodes
umodes = user_obj.umodes
if action == '+':
for nm in new_modes:
if nm not in existing_umodes:
umodes += nm
elif action == '-':
for nm in new_modes:
if nm in existing_umodes:
umodes = umodes.replace(nm, '')
else:
return False
liste_umodes = list(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)
user_obj.umodes = f"+{final_umodes}"
return response
def delete(self, uid: str) -> bool:
"""Delete the User starting from the UID
Args:
uid (str): UID of the user
Returns:
bool: True if deleted
"""
user_obj = self.get_user(uidornickname=uid)
if user_obj is None:
return False
self.UID_DB.remove(user_obj)
return True
def get_user(self, uidornickname: str) -> Optional['MUser']:
"""Get The User Object model
Args:
uidornickname (str): UID or Nickname
Returns:
UserModel|None: The UserModel Object | None
"""
for record in self.UID_DB:
if record.uid == uidornickname:
return record
elif record.nickname == uidornickname:
return record
return None
def get_uid(self, uidornickname:str) -> Optional[str]:
"""Get the UID of the user starting from the UID or the Nickname
Args:
uidornickname (str): UID or Nickname
Returns:
str|None: Return the UID
"""
user_obj = self.get_user(uidornickname=uidornickname)
if user_obj is None:
return None
return user_obj.uid
def get_nickname(self, uidornickname:str) -> Optional[str]:
"""Get the Nickname starting from UID or the nickname
Args:
uidornickname (str): UID or Nickname of the user
Returns:
str|None: the nickname
"""
user_obj = self.get_user(uidornickname=uidornickname)
if user_obj is None:
return None
return user_obj.nickname
def get_user_asdict(self, uidornickname: str) -> Optional[dict[str, Any]]:
"""Transform User Object to a dictionary
Args:
uidornickname (str): The UID or The nickname
Returns:
Union[dict[str, any], None]: User Object as a dictionary or None
"""
user_obj = self.get_user(uidornickname=uidornickname)
if user_obj is None:
return None
return user_obj.to_dict()
def is_exist(self, uidornikname: str) -> bool:
"""Check if the UID or the nickname exist in the USER DB
Args:
uidornickname (str): The UID or the NICKNAME
Returns:
bool: True if exist
"""
user_obj = self.get_user(uidornickname=uidornikname)
if user_obj is None:
return False
return True
def clean_uid(self, uid: str) -> Optional[str]:
"""Clean UID by removing @ / % / + / ~ / * / :
Args:
uid (str): The UID to clean
Returns:
str: Clean UID without any sign
"""
pattern = fr'[:|@|%|\+|~|\*]*'
parsed_UID = sub(pattern, '', uid)
if not parsed_UID:
return None
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,274 +0,0 @@
from dataclasses import dataclass, field
from datetime import datetime
from typing import Union
class User:
@dataclass
class UserDataClass:
uid: str
nickname: str
username: str
hostname: str
umodes: str
vhost: str
isWebirc: bool
connexion_datetime: datetime = field(default=datetime.now())
UID_DB:list[UserDataClass] = []
def __init__(self) -> None:
pass
def insert(self, user: UserDataClass) -> bool:
"""Insert new user
Args:
user (UserDataClass): The User dataclass
Returns:
bool: True if the record has been created
"""
exists = False
inserted = False
for record in self.UID_DB:
if record.uid == user.uid:
exists = True
print(f'{user.uid} already exist')
if not exists:
self.UID_DB.append(user)
print(f'New record with uid: {user.uid}')
inserted = True
return inserted
def update(self, uid: str, newnickname: str) -> bool:
"""Updating a single record with a new nickname
Args:
uid (str): the uid of the user
newnickname (str): the new nickname
Returns:
bool: True if the record has been updated
"""
status = False
for user in self.UID_DB:
if user.uid == uid:
user.nickname = newnickname
status = True
print(f'Updating record with uid: {uid}')
return status
def delete(self, uid: str) -> bool:
"""Delete a user based on his uid
Args:
uid (str): The UID of the user
Returns:
bool: True if the record has been deleted
"""
status = False
for user in self.UID_DB:
if user.uid == uid:
self.UID_DB.remove(user)
status = True
print(f'Removing record with uid: {uid}')
return status
def isexist(self, uidornickname:str) -> bool:
"""do the UID or Nickname exist ?
Args:
uidornickname (str): The UID or the Nickname
Returns:
bool: True if exist or False if don't exist
"""
result = False
for record in self.UID_DB:
if record.uid == uidornickname:
result = True
if record.nickname == uidornickname:
result = True
return result
def get_User(self, uidornickname) -> Union[UserDataClass, None]:
UserObject = None
for record in self.UID_DB:
if record.uid == uidornickname:
UserObject = record
elif record.nickname == uidornickname:
UserObject = record
return UserObject
def get_uid(self, uidornickname:str) -> Union[str, None]:
uid = None
for record in self.UID_DB:
if record.uid == uidornickname:
uid = record.uid
if record.nickname == uidornickname:
uid = record.uid
return uid
def get_nickname(self, uidornickname:str) -> Union[str, None]:
nickname = None
for record in self.UID_DB:
if record.nickname == uidornickname:
nickname = record.nickname
if record.uid == uidornickname:
nickname = record.nickname
return nickname
class Admin:
@dataclass
class AdminDataClass:
uid: str
nickname: str
username: str
hostname: str
umodes: str
vhost: str
level: int
connexion_datetime: datetime = field(default=datetime.now())
UID_ADMIN_DB:list[AdminDataClass] = []
def __init__(self) -> None:
pass
def insert(self, admin: AdminDataClass) -> bool:
"""Insert new user
Args:
user (UserDataClass): The User dataclass
Returns:
bool: True if the record has been created
"""
exists = False
inserted = False
for record in self.UID_ADMIN_DB:
if record.uid == admin.uid:
exists = True
print(f'{admin.uid} already exist')
if not exists:
self.UID_ADMIN_DB.append(admin)
print(f'New record with uid: {admin.uid}')
inserted = True
return inserted
def update(self, uid: str, newnickname: str) -> bool:
"""Updating a single record with a new nickname
Args:
uid (str): the uid of the user
newnickname (str): the new nickname
Returns:
bool: True if the record has been updated
"""
status = False
for admin in self.UID_ADMIN_DB:
if admin.uid == uid:
admin.nickname = newnickname
status = True
print(f'Updating record with uid: {uid}')
return status
def delete(self, uid: str) -> bool:
"""Delete a user based on his uid
Args:
uid (str): The UID of the user
Returns:
bool: True if the record has been deleted
"""
status = False
for admin in self.UID_ADMIN_DB:
if admin.uid == uid:
self.UID_ADMIN_DB.remove(admin)
status = True
print(f'Removing record with uid: {uid}')
return status
def isexist(self, uidornickname:str) -> bool:
"""do the UID or Nickname exist ?
Args:
uidornickname (str): The UID or the Nickname
Returns:
bool: True if exist or False if don't exist
"""
result = False
for record in self.UID_ADMIN_DB:
if record.uid == uidornickname:
result = True
if record.nickname == uidornickname:
result = True
return result
def get_Admin(self, uidornickname) -> Union[AdminDataClass, None]:
AdminObject = None
for record in self.UID_ADMIN_DB:
if record.uid == uidornickname:
AdminObject = record
elif record.nickname == uidornickname:
AdminObject = record
return AdminObject
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
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
return nickname
def get_level(self, uidornickname:str) -> int:
level = 0
for record in self.UID_ADMIN_DB:
if record.uid == uidornickname:
level = record.level
if record.nickname == uidornickname:
level = record.level
return level

363
core/definition.py Normal file
View File

@@ -0,0 +1,363 @@
from datetime import datetime
from json import dumps
from dataclasses import dataclass, field, asdict, fields
from typing import Literal, Any, Optional
from os import sep
@dataclass
class MainModel:
"""Parent Model contains important methods"""
def to_dict(self) -> dict[str, Any]:
"""Return the fields of a dataclass instance as a new dictionary mapping field names to field values."""
return asdict(self)
def to_json(self) -> str:
"""Return the object of a dataclass a json str."""
return dumps(self.to_dict())
def get_attributes(self) -> list[str]:
"""Return a list of attributes name"""
return [f.name for f in fields(self)]
@dataclass
class MClient(MainModel):
"""Model Client for registred nickname"""
uid: str = None
account: str = None
nickname: str = None
username: str = None
realname: str = None
hostname: str = None
umodes: str = None
vhost: str = None
fingerprint: str = None
isWebirc: bool = False
isWebsocket: bool = False
remote_ip: str = None
score_connexion: int = 0
geoip: str = None
connexion_datetime: datetime = field(default=datetime.now())
@dataclass
class MUser(MainModel):
"""Model User"""
uid: str = None
nickname: str = None
username: str = None
realname: str = None
hostname: str = None
umodes: str = None
vhost: str = None
fingerprint: str = None
isWebirc: bool = False
isWebsocket: bool = False
remote_ip: str = None
score_connexion: int = 0
geoip: str = None
connexion_datetime: datetime = field(default=datetime.now())
@dataclass
class MAdmin(MainModel):
"""Model Admin"""
uid: str = None
account: str = None
nickname: str = None
username: str = None
realname: str = None
hostname: str = None
umodes: str = None
vhost: str = None
fingerprint: str = None
isWebirc: bool = False
isWebsocket: bool = False
remote_ip: str = None
score_connexion: int = 0
geoip: str = None
connexion_datetime: datetime = field(default=datetime.now())
level: int = 0
@dataclass
class MReputation(MainModel):
"""Model Reputation"""
uid: str = None
nickname: str = None
username: str = None
realname: str = None
hostname: str = None
umodes: str = None
vhost: str = None
fingerprint: str = None
isWebirc: bool = False
isWebsocket: bool = False
remote_ip: str = None
score_connexion: int = 0
geoip: str = None
connexion_datetime: datetime = field(default=datetime.now())
secret_code: str = None
@dataclass
class MChannel(MainModel):
"""Model Channel"""
name: str = None
"""### Channel name
It include the #"""
uids: list[str] = field(default_factory=list[str])
"""### List of UID available in the channel
including their modes ~ @ % + *
Returns:
list: The list of UID's including theirs modes
"""
@dataclass
class ColorModel(MainModel):
white: str = "\x0300"
black: str = "\x0301"
blue: str = "\x0302"
green: str = "\x0303"
red: str = "\x0304"
yellow: str = "\x0306"
bold: str = "\x02"
nogc: str = "\x03"
underline: str = "\x1F"
@dataclass
class MConfig(MainModel):
"""Model Configuration"""
SERVEUR_IP: str = "127.0.0.1"
"""Server public IP (could be 127.0.0.1 localhost)"""
SERVEUR_HOSTNAME: str = "your.host.name"
"""IRC Server Hostname (your.hostname.extension)"""
SERVEUR_LINK: str = "your.link.url"
"""The link hostname (should be the same as your unrealircd link block)"""
SERVEUR_PORT: int = 6697
"""Server port as configured in your unrealircd link block"""
SERVEUR_PASSWORD: str = "YOUR.STRONG.PASSWORD"
"""Your link password"""
SERVEUR_ID: str = "Z01"
"""Service identification could be Z01 should be unique"""
SERVEUR_SSL: bool = True
"""Activate SSL connexion"""
SERVEUR_PROTOCOL: str = "unreal6"
"""Which server are you going to use. (default: unreal6)"""
SERVEUR_CHARSET: list[str] = field(default_factory=list[str])
"""0: utf-8 | 1: iso-8859-1"""
SERVICE_NAME: str = "Defender"
"""Service name (Ex. Defender)"""
SERVICE_NICKNAME: str = "Defender"
"""Nickname of the service (Ex. Defender)"""
SERVICE_REALNAME: str = "Defender IRC Service"
"""Realname of the service"""
SERVICE_USERNAME: str = "Security"
"""Username of the service"""
SERVICE_HOST: str = "Your.Service.Hostname"
"""The service hostname"""
SERVICE_INFO: str = "Defender IRC Service"
"""Swhois of the service"""
SERVICE_CHANLOG: str = "#services"
"""The channel used by the service (ex. #services)"""
SERVICE_SMODES: str = "+ioqBS"
"""The service mode (ex. +ioqBS)"""
SERVICE_CMODES: str = "ntsO"
"""The mode of the log channel (ex. ntsO)"""
SERVICE_UMODES: str = "o"
"""The mode of the service when joining chanlog (ex. o, the service will be operator in the chanlog)"""
SERVICE_PREFIX: str = "!"
"""The default prefix to communicate with the service"""
SERVICE_ID: str = field(init=False)
"""The service unique ID"""
OWNER: str = "admin"
"""The nickname of the admin of the service"""
PASSWORD: str = "password"
"""The password of the admin of the service"""
JSONRPC_URL: str = None
"""The RPC url, if local https://127.0.0.1:PORT/api should be fine"""
JSONRPC_PATH_TO_SOCKET_FILE: str = None
"""The full path of the socket file (/PATH/TO/YOUR/UNREALIRCD/SOCKET/FILE.socket)"""
JSONRPC_METHOD: str = None
"""3 methods are available; requests/socket/unixsocket"""
JSONRPC_USER: str = None
"""The RPC User defined in your unrealircd.conf"""
JSONRPC_PASSWORD: str = None
"""The RPC Password defined in your unrealircd.conf"""
SALON_JAIL: str = "#jail"
"""The JAIL channel (ex. #jail)"""
SALON_JAIL_MODES: str = "sS"
"""The jail channel modes (ex. sS)"""
SALON_LIBERER: str = "#welcome"
"""Channel where the nickname will be released"""
CLONE_CHANNEL: str = "clones"
"""Channel where clones are hosted and will log PRIVMSG"""
CLONE_CMODES: str = "+nts"
"""Clone channel modes (ex. +nts)"""
CLONE_UMODES: str = '+iwxz'
"""Clone User modes (ex. +iwxz)"""
CLONE_LOG_HOST_EXEMPT: list[str] = field(default_factory=list[str])
"""Hosts that clones will not log"""
CLONE_CHANNEL_PASSWORD: str = "clone_Password_1234"
"""Clone password channel"""
API_TIMEOUT: int = 60
"""Default api timeout in second. (default: 60)"""
PORTS_TO_SCAN: list[int] = field(default_factory=list[int])
"""List of ports to scan available for proxy_scan in the mod_defender module"""
WHITELISTED_IP: list[str] = field(default_factory=list[str])
"""List of remote IP to don't scan"""
GLINE_DURATION: str = "30"
"""Gline duration"""
DEBUG_LEVEL:Literal[10, 20, 30, 40, 50] = 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"
"""The name of the Logging instance"""
TABLE_CLIENT: str = "core_client"
"""Core Client table"""
TABLE_ADMIN: str = "core_admin"
"""Core Admin table"""
TABLE_COMMAND: str = "core_command"
"""Core command table"""
TABLE_LOG: str = "core_log"
"""Core log table"""
TABLE_MODULE: str = "core_module"
"""Core module table"""
TABLE_CONFIG: str = "core_config"
"""Core configuration table"""
TABLE_CHANNEL: str = "core_channel"
"""Core channel table"""
CURRENT_VERSION: str = None
"""Current version of Defender"""
LATEST_VERSION: str = None
"""The Latest version fetched from github"""
DB_NAME: str = "defender"
"""The database name"""
DB_PATH: str = f"db{sep}"
"""The database path"""
COLORS: ColorModel = field(default_factory=ColorModel)
"""Available colors in Defender"""
BATCH_SIZE: int = 400
"""The batch size used for privmsg and notice"""
DEFENDER_CONNEXION_DATETIME: datetime = field(default=datetime.now())
"""First Connexion datetime of the service"""
DEFENDER_INIT: int = 1
"""Init flag. When Defender is ready, this variable will be set to 0. (default: 1)"""
DEFENDER_RESTART: int = 0
"""Restart flag. When Defender should restart this variable should be set to 1 (default: 0)"""
DEFENDER_HEARTBEAT: bool = True
"""Activate the hearbeat pulse (default: True)"""
DEFENDER_HEARTBEAT_FREQUENCY: int = 2
"""Frequency in seconds between every pulse (default: 30 seconds)"""
OS_SEP: str = sep
"""The OS Separator. (default: os.sep)"""
HSID: str = None
"""Host Server ID. The Server ID of the server who is hosting Defender. (Default: None)"""
SSL_VERSION: str = None
"""If SSL is used. This variable will be filled out by the system. (Default: None)"""
def __post_init__(self):
# Initialiser SERVICE_ID après la création de l'objet
self.SERVICE_ID: str = f"{self.SERVEUR_ID}AAAAAB"
"""The service ID which is SERVEUR_ID and AAAAAB"""
self.SERVEUR_CHARSET: list = ["utf-8", "iso-8859-1"]
"""0: utf-8 | 1: iso-8859-1"""
@dataclass
class MCommand(MainModel):
module_name: str = None
command_name: str = None
description: str = None
command_level: int = 0
@dataclass
class MModule(MainModel):
module_name: str = None
class_name: str = None
class_instance: Optional[Any] = None
@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
auth_success: bool = False
level: int = 0

View File

@@ -1,48 +0,0 @@
{
"SERVEUR_IP": "0.0.0.0",
"SERVEUR_HOSTNAME": "your.host.name",
"SERVEUR_LINK": "your.link.to.server",
"SERVEUR_PORT": 7002,
"SERVEUR_PASSWORD": "link_password",
"SERVEUR_ID": "006",
"SERVEUR_SSL": true,
"SERVICE_NAME": "defender",
"SERVICE_NICKNAME": "BotNickname",
"SERVICE_REALNAME": "BotRealname",
"SERVICE_USERNAME": "BotUsername",
"SERVICE_HOST": "your.service.hostname",
"SERVICE_INFO": "Network IRC Service",
"SERVICE_CHANLOG": "#services",
"SERVICE_SMODES": "+ioqBS",
"SERVICE_CMODES": "ntsO",
"SERVICE_UMODES": "o",
"SERVICE_PREFIX": "!",
"OWNER": "admin",
"PASSWORD": "password",
"SALON_JAIL": "#jail",
"SALON_JAIL_MODES": "sS",
"SALON_LIBERER": "#welcome",
"API_TIMEOUT": 2,
"PORTS_TO_SCAN": [3028, 8080, 1080, 1085, 4145, 9050],
"WHITELISTED_IP": ["127.0.0.1"],
"GLINE_DURATION": "30",
"DEBUG_LEVEL": 20,
"CONFIG_COLOR": {
"blanche": "\\u0003\\u0030",
"noire": "\\u0003\\u0031",
"bleue": "\\u0003\\u0020",
"verte": "\\u0003\\u0033",
"rouge": "\\u0003\\u0034",
"jaune": "\\u0003\\u0036",
"gras": "\\u0002",
"nogc": "\\u0002\\u0003"
}
}

View File

@@ -1,64 +1,247 @@
from importlib.util import find_spec
from subprocess import check_call, run, CalledProcessError
from platform import python_version, python_version_tuple
from sys import exit
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.PYTHON_MIN_VERSION = '3.10'
self.venv_folder_name = '.pyenv'
self.cmd_venv_command = ['python3', '-m', 'venv', self.venv_folder_name]
self.module_to_install = ['sqlalchemy','psutil','requests']
self.set_configuration()
if not self.checkPythonVersion():
# Tester si c'est la bonne version de python
exit("Python Version Error")
else:
# Sinon tester les dependances python et les installer avec pip
self.checkDependencies()
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 checkPythonVersion(self) -> bool:
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 = python_version_tuple()
sys_major, sys_minor, sys_patch = self.config.python_current_version_tuple
# min python version required
python_required_version = self.PYTHON_MIN_VERSION.split('.')
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.PYTHON_MIN_VERSION} ##")
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.PYTHON_MIN_VERSION} ##")
print(f"## Your python version must be greather than or equal to {self.config.python_min_version} ##")
return False
print(f"===> Version of python : {python_version()} ==> OK")
print(f"> Version of python : {self.config.python_current_version} ==> OK")
return True
def run_subprocess(self, command:list) -> None:
def check_package(self, package_name) -> bool:
print(command)
try:
check_call(command)
print("La commande s'est terminée avec succès.")
except CalledProcessError as e:
print(f"La commande a échoué avec le code de retour :{e.returncode}")
print(f"Try to install dependencies ...")
exit(5)
# 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 checkDependencies(self) -> None:
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
@@ -67,38 +250,82 @@ class Install:
do_install = False
# Check if virtual env exist
if not os.path.exists(f'{self.venv_folder_name}'):
self.run_subprocess(self.cmd_venv_command)
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.module_to_install:
if find_spec(module) is None:
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")
check_call(['pip','cache','purge'])
print("===> Clean pip cache")
self.run_subprocess([self.config.venv_pip_executable, 'cache', 'purge'])
print("===> Verifier si pip est a jour")
check_call(['python', '-m', 'pip', 'install', '--upgrade', 'pip'])
print("===> Check if pip is up to date")
self.run_subprocess([self.config.venv_python_executable, '-m', 'pip', 'install', '--upgrade', 'pip'])
if find_spec('greenlet') is None:
check_call(['pip','install', '--only-binary', ':all:', 'greenlet'])
print('====> Module Greenlet installé')
if not self.check_package('greenlet'):
self.run_subprocess([self.config.venv_pip_executable, 'install', '--only-binary', ':all:', 'greenlet'])
print('====> Greenlet installed')
for module in self.module_to_install:
if find_spec(module) is None:
for module in self.config.venv_cmd_requirements:
if not self.check_package(module):
print("### Trying to install missing python packages ###")
check_call(['pip','install', module])
print(f"====> Module {module} installé")
self.run_subprocess([self.config.venv_pip_executable, 'install', module])
print(f"====> Module {module} installed!")
else:
print(f"==> {module} already installed")
print(f"#"*12)
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('Service file generated with current configuration')
print('Running IRC Service ...')
self.run_subprocess(self.config.service_cmd_daemon_reload)
self.run_subprocess(self.config.service_cmd_executable)
else:
with open(full_service_file_path, 'w+') as servicefile:
servicefile.write(contain)
servicefile.close()
print('Service file generated with current configuration')
print('Running IRC Service ...')
self.run_subprocess(self.config.service_cmd_daemon_reload)
self.run_subprocess(self.config.service_cmd_executable)
def print_final_message(self) -> None:
print(f"#"*24)
print("Installation complete ...")
print("You must change environment using the command below")
print(f"source {self.venv_folder_name}{os.sep}bin{os.sep}activate")
print(f"#"*12)
exit(1)
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

@@ -1,126 +0,0 @@
import json
from os import sep
from typing import Union
from dataclasses import dataclass, field
##########################################
# CONFIGURATION FILE #
##########################################
@dataclass
class ConfigDataModel:
SERVEUR_IP: str
SERVEUR_HOSTNAME: str # Le hostname du serveur IRC
SERVEUR_LINK: str # Host attendu par votre IRCd (ex. dans votre link block pour Unrealircd)
SERVEUR_PORT: int # Port du link
SERVEUR_PASSWORD: str # Mot de passe du link (Privilégiez argon2 sur Unrealircd)
SERVEUR_ID: str # SID (identification) du bot en tant que Services
SERVEUR_SSL: bool # Activer la connexion SSL
SERVICE_NAME: str # Le nom du service
SERVICE_NICKNAME: str # Nick du bot sur IRC
SERVICE_REALNAME: str # Realname du bot
SERVICE_USERNAME: str # Ident du bot
SERVICE_HOST: str # Host du bot
SERVICE_INFO: str # swhois du bot
SERVICE_CHANLOG: str # Salon des logs et autres messages issus du bot
SERVICE_SMODES: str # Mode du service
SERVICE_CMODES: str # Mode du salon (#ChanLog) que le bot appliquera à son entrée
SERVICE_UMODES: str # Mode que le bot pourra se donner à sa connexion au salon chanlog
SERVICE_PREFIX: str # Prefix pour envoyer les commandes au bot
SERVICE_ID: str = field(init=False) # L'identifiant du service
OWNER: str # Identifiant du compte admin
PASSWORD: str # Mot de passe du compte admin
SALON_JAIL: str # Salon pot de miel
SALON_JAIL_MODES: str # Mode du salon pot de miel
SALON_LIBERER: str # Le salon ou sera envoyé l'utilisateur clean
API_TIMEOUT: int # Timeout des api's
PORTS_TO_SCAN: list # Liste des ports a scanné pour une detection de proxy
WHITELISTED_IP: list # IP a ne pas scanner
GLINE_DURATION: str # La durée du gline
DEBUG_LEVEL: int # Le niveau des logs DEBUG 10 | INFO 20 | WARNING 30 | ERROR 40 | CRITICAL 50
CONFIG_COLOR: dict[str, str]
table_admin: str
table_commande: str
table_log: str
table_module: str
current_version: str
latest_version: str
db_name: str
db_path: str
def __post_init__(self):
# Initialiser SERVICE_ID après la création de l'objet
self.SERVICE_ID:str = f"{self.SERVEUR_ID}AAAAAB"
class Config:
def __init__(self):
self.ConfigObject: ConfigDataModel = self.__load_service_configuration()
return None
def __load_json_service_configuration(self):
conf_filename = f'core{sep}configuration.json'
with open(conf_filename, 'r') as configuration_data:
configuration:dict[str, Union[str, int, list, dict]] = json.load(configuration_data)
for key, value in configuration['CONFIG_COLOR'].items():
configuration['CONFIG_COLOR'][key] = str(value).encode('utf-8').decode('unicode_escape')
return configuration
def __load_service_configuration(self) -> ConfigDataModel:
import_config = self.__load_json_service_configuration()
ConfigObject: ConfigDataModel = ConfigDataModel(
SERVEUR_IP=import_config["SERVEUR_IP"],
SERVEUR_HOSTNAME=import_config["SERVEUR_HOSTNAME"],
SERVEUR_LINK=import_config["SERVEUR_LINK"],
SERVEUR_PORT=import_config["SERVEUR_PORT"],
SERVEUR_PASSWORD=import_config["SERVEUR_PASSWORD"],
SERVEUR_ID=import_config["SERVEUR_ID"],
SERVEUR_SSL=import_config["SERVEUR_SSL"],
SERVICE_NAME=import_config["SERVICE_NAME"],
SERVICE_NICKNAME=import_config["SERVICE_NICKNAME"],
SERVICE_REALNAME=import_config["SERVICE_REALNAME"],
SERVICE_USERNAME=import_config["SERVICE_USERNAME"],
SERVICE_HOST=import_config["SERVICE_HOST"],
SERVICE_INFO=import_config["SERVICE_INFO"],
SERVICE_CHANLOG=import_config["SERVICE_CHANLOG"],
SERVICE_SMODES=import_config["SERVICE_SMODES"],
SERVICE_CMODES=import_config["SERVICE_CMODES"],
SERVICE_UMODES=import_config["SERVICE_UMODES"],
SERVICE_PREFIX=import_config["SERVICE_PREFIX"],
OWNER=import_config["OWNER"],
PASSWORD=import_config["PASSWORD"],
SALON_JAIL=import_config["SALON_JAIL"],
SALON_JAIL_MODES=import_config["SALON_JAIL_MODES"],
SALON_LIBERER=import_config["SALON_LIBERER"],
API_TIMEOUT=import_config["API_TIMEOUT"],
PORTS_TO_SCAN=import_config["PORTS_TO_SCAN"],
WHITELISTED_IP=import_config["WHITELISTED_IP"],
GLINE_DURATION=import_config["GLINE_DURATION"],
DEBUG_LEVEL=import_config["DEBUG_LEVEL"],
CONFIG_COLOR=import_config["CONFIG_COLOR"],
table_admin='sys_admins',
table_commande='sys_commandes',
table_log='sys_logs',
table_module='sys_modules',
current_version='',
latest_version='',
db_name='defender',
db_path=f'db{sep}'
)
return ConfigObject

55
core/loader.py Normal file
View File

@@ -0,0 +1,55 @@
from logging import Logger
from core.classes import user, admin, client, channel, reputation, settings, sasl
import core.logs as logs
import core.definition as df
import core.utils as utils
import core.base as base_mod
import core.module as module_mod
import core.classes.commands as commands_mod
import core.classes.config as conf_mod
class Loader:
def __init__(self):
# Load Main Modules
self.Definition: df = df
self.ConfModule: conf_mod = conf_mod
self.BaseModule: base_mod = base_mod
self.CommandModule: commands_mod = commands_mod
self.LoggingModule: logs = logs
self.Utils: utils = utils
# Load Classes
self.ServiceLogging: logs.ServiceLogging = self.LoggingModule.ServiceLogging()
self.Logs: Logger = self.ServiceLogging.get_logger()
self.Settings: settings.Settings = settings.Settings()
self.Config: df.MConfig = self.ConfModule.Configuration(self).get_config_model()
self.Base: base_mod.Base = self.BaseModule.Base(self)
self.User: user.User = user.User(self)
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.Logs.debug("LOADER Success!")

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!

424
core/module.py Normal file
View File

@@ -0,0 +1,424 @@
'''
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 MModule
if TYPE_CHECKING:
from core.loader import Loader
from core.irc import Irc
class Module:
DB_MODULES: list[MModule] = []
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')
return [file.name.replace('.py', '') for file in base_path.rglob('mod_*.py')]
def get_module_information(self, module_name: str) -> tuple[str, str, str]:
# module_name : mod_defender
if not module_name.lower().startswith('mod_'):
return None, None, None
module_name = module_name.lower()
module_folder = module_name.split('_')[1].lower() # --> defender
class_name = module_name.split('_')[1].capitalize() # --> Defender
return module_folder, module_name, class_name
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
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
if not hasattr(create_instance_of_the_class, 'cmd'):
uplink.Protocol.send_priv_msg(
nick_from=self.__Config.SERVICE_NICKNAME,
msg=f"Module {module_name} ne contient pas de méthode cmd",
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=f"Module {module_name} chargé",
channel=self.__Config.SERVICE_CHANLOG
)
self.__Logs.debug(f"Module {class_name} has been loaded")
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:
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)
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}")
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:
mod_name (str): Module name ex mod_defender
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!")
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:
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}")
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:
return True
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

198
core/utils.py Normal file
View File

@@ -0,0 +1,198 @@
'''
Main utils library.
'''
import gc
import ssl
import socket
import sys
from pathlib import Path
from re import match, sub
from base64 import b64decode
from typing import Literal, Optional, Any, TYPE_CHECKING
from datetime import datetime, timedelta, timezone
from time import time
from random import choice
from hashlib import md5, sha3_512
if TYPE_CHECKING:
from core.irc import Irc
def convert_to_int(value: Any) -> Optional[int]:
"""Convert a value to int
Args:
value (Any): Value to convert to int if possible
Returns:
int: Return the int value or None if not possible
"""
try:
value_to_int = int(value)
return value_to_int
except ValueError:
return None
def get_unixtime() -> int:
"""Cette fonction retourne un UNIXTIME de type 12365456
Returns:
int: Current time in seconds since the Epoch (int)
"""
cet_offset = timezone(timedelta(hours=2))
now_cet = datetime.now(cet_offset)
unixtime_cet = int(now_cet.timestamp())
return int(time())
def get_sdatetime() -> str:
"""Retourne une date au format string (24-12-2023 20:50:59)
Returns:
str: Current datetime in this format %d-%m-%Y %H:%M:%S
"""
currentdate = datetime.now().strftime('%d-%m-%Y %H:%M:%S')
return currentdate
def get_datetime() -> datetime:
"""
Return the current datetime in a datetime object
"""
return datetime.now()
def 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)
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:
"""Retourn une chaîne aléatoire en fonction de la longueur spécifiée.
Returns:
str: The random string
"""
caracteres = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
randomize = ''.join(choice(caracteres) for _ in range(lenght))
return randomize
def hash_password(password: str, algorithm: Literal["md5, sha3_512"] = 'md5') -> str:
"""Return the crypted password following the selected algorithm
Args:
password (str): The plain text password
algorithm (str): The algorithm to use
Returns:
str: The crypted password, default md5
"""
match algorithm:
case 'md5':
password = md5(password.encode()).hexdigest()
return password
case 'sha3_512':
password = sha3_512(password.encode()).hexdigest()
return password
case _:
password = md5(password.encode()).hexdigest()
return password
def get_all_modules() -> list[str]:
"""Get list of all main modules
using this pattern mod_*.py
Returns:
list[str]: List of module names.
"""
base_path = Path('mods')
return [file.name.replace('.py', '') for file in base_path.rglob('mod_*.py')]
def clean_uid(uid: str) -> Optional[str]:
"""Clean UID by removing @ / % / + / ~ / * / :
Args:
uid (str): The UID to clean
Returns:
str: Clean UID without any sign
"""
if uid is None:
return None
pattern = fr'[:|@|%|\+|~|\*]*'
parsed_UID = sub(pattern, '', uid)
return parsed_UID
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,23 +1,21 @@
from core import installation
#############################################
# @Version : 1 #
# @Version : 6.2 #
# Requierements : #
# Python3.10 or higher #
# SQLAlchemy, requests, psutil #
# unrealircd-rpc-py #
# UnrealIRCD 6.2.2 or higher #
#############################################
#########################
# LANCEMENT DE DEFENDER #
#########################
try:
installation.Install()
from core.loader import Loader
from core.irc import Irc
ircInstance = Irc()
ircInstance = Irc(Loader())
ircInstance.init_irc(ircInstance)
except AssertionError as ae:

View File

@@ -1,275 +0,0 @@
from subprocess import check_call, run, CalledProcessError, PIPE
from platform import python_version, python_version_tuple, system
from sys import exit
import os, logging, shutil
try:
import pwd
except ModuleNotFoundError as err:
print(err)
class Install:
def __init__(self) -> None:
# Python required version
self.python_min_version = '3.10'
self.log_file = 'install.log'
self.ServiceName = 'Defender'
self.venv_name = '.pyenv'
self.venv_dependencies: list[str] = ['sqlalchemy','psutil','requests']
self.install_folder = os.getcwd()
self.osname = os.name
self.system_name = system()
self.cmd_linux_requirements: list[str] = ['apt', 'install', '-y', 'python3', 'python3-pip', 'python3-venv']
self.venv_pip_full_path = os.path.join(self.venv_name, f'bin{os.sep}pip')
self.venv_python_full_path = os.path.join(self.venv_name, f'bin{os.sep}python')
self.systemd_folder = '/etc/systemd/system/'
# Init log system
self.init_log_system()
# Exclude Windows OS
if self.osname == 'nt':
print('/!\\ Windows OS is not supported by this automatic installation /!\\')
self.Logs.critical('/!\\ Windows OS is not supported by this automatic install /!\\')
print(self.system_name)
exit(5)
if not self.is_root():
exit(5)
# Get the current user
self.system_username: str = input(f'What is the user ro run defender with ? [{os.getlogin()}] : ')
if str(self.system_username).strip() == '':
self.system_username = os.getlogin()
self.get_user_information(self.system_username)
self.Logs.debug(f'The user selected is: {self.system_username}')
self.Logs.debug(f'Operating system: {self.osname}')
# Install linux dependencies
self.install_linux_dependencies()
# Check python version
self.check_python_version()
# Create systemd service file
self.create_service_file()
# Check if Env Exist | install environment | Install python dependencies
self.check_venv()
# Create and start service
if self.osname != 'nt':
self.run_subprocess(['systemctl','daemon-reload'])
self.run_subprocess(['systemctl','start', self.ServiceName])
self.run_subprocess(['systemctl','status', self.ServiceName])
# Clean the Installation
self.clean_installation()
return None
def is_installed(self) -> bool:
is_installed = False
# Check logs folder
if os.path.exists('logs'):
is_installed = True
# Check db folder
if os.path.exists('db'):
is_installed = True
return is_installed
def is_root(self) -> bool:
if os.geteuid() != 0:
print('/!\\ user must run install.py as root /!\\')
self.Logs.critical('/!\\ user must run install.py as root /!\\')
return False
elif os.geteuid() == 0:
return True
def get_user_information(self, system_user: str) -> None:
try:
username: tuple = pwd.getpwnam(system_user)
self.system_uid = username.pw_uid
self.system_gid = username.pw_gid
return None
except KeyError as ke:
self.Logs.critical(f"This user [{system_user}] doesn't exist: {ke}")
print(f"This user [{system_user}] doesn't exist: {ke}")
exit(5)
def init_log_system(self) -> None:
# Init logs object
self.Logs = logging
self.Logs.basicConfig(level=logging.DEBUG,
filename=self.log_file,
encoding='UTF-8',
format='%(asctime)s - %(levelname)s - %(filename)s - %(lineno)d - %(funcName)s - %(message)s')
self.Logs.debug('#################### STARTING INSTALLATION ####################')
return None
def clean_installation(self) -> None:
# Chown the Python Env to non user privilege
self.run_subprocess(['chown','-R', f'{self.system_username}:{self.system_username}',
f'{os.path.join(self.install_folder, self.venv_name)}'
]
)
# Chown the installation log file
self.run_subprocess(['chown','-R', f'{self.system_username}:{self.system_username}',
f'{os.path.join(self.install_folder, self.log_file)}'
]
)
return None
def run_subprocess(self, command:list) -> None:
try:
run_command = check_call(command)
self.Logs.debug(f'{command} - {run_command}')
print(f'{command} - {run_command}')
except CalledProcessError as e:
print(f"Command failed :{e.returncode}")
self.Logs.critical(f"Command failed :{e.returncode}")
exit(5)
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
"""
self.Logs.debug(f'The current python version is: {python_version()}')
# Current system version
sys_major, sys_minor, sys_patch = python_version_tuple()
# min python version required
python_required_version = self.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.PYTHON_MIN_VERSION} ##")
self.Logs.critical(f'Your python version must be greather than or equal to {self.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.PYTHON_MIN_VERSION} ##")
self.Logs.critical(f'Your python version must be greather than or equal to {self.python_min_version}')
return False
print(f"===> Version of python : {python_version()} ==> OK")
self.Logs.debug(f'Version of python : {python_version()} ==> OK')
return True
def check_packages(self, package_name) -> bool:
try:
# Run a command in the virtual environment's Python to check if the package is installed
run([self.venv_python_full_path, '-c', f'import {package_name}'], check=True, stdout=PIPE, stderr=PIPE)
return True
except CalledProcessError:
return False
def check_venv(self) -> bool:
if os.path.exists(self.venv_name):
# Installer les dependances
self.install_dependencies()
return True
else:
self.run_subprocess(['python3', '-m', 'venv', self.venv_name])
self.Logs.debug(f'Python Virtual env installed {self.venv_name}')
print(f'Python Virtual env installed {self.venv_name}')
self.install_dependencies()
return False
def create_service_file(self) -> None:
if self.systemd_folder is None:
# If Windows, do not install systemd
return None
if os.path.exists(f'{self.systemd_folder}{os.sep}{self.ServiceName}.service'):
print(f'/!\\ Service already created in the system /!\\')
self.Logs.warning('/!\\ Service already created in the system /!\\')
print(f'The service file will be regenerated')
self.Logs.warning('The service file will be regenerated')
contain = f'''[Unit]
Description={self.ServiceName} IRC Service
[Service]
User={self.system_username}
ExecStart={os.path.join(self.install_folder, self.venv_python_full_path)} {os.path.join(self.install_folder, 'main.py')}
WorkingDirectory={self.install_folder}
SyslogIdentifier={self.ServiceName}
Restart=on-failure
[Install]
WantedBy=multi-user.target
'''
with open(f'{self.ServiceName}.service.generated', 'w+') as servicefile:
servicefile.write(contain)
servicefile.close()
print('Service file generated with current configuration')
self.Logs.debug('Service file generated with current configuration')
source = f'{self.install_folder}{os.sep}{self.ServiceName}.service.generated'
self.run_subprocess(['chown','-R', f'{self.system_username}:{self.system_username}', source])
destination = f'{self.systemd_folder}'
shutil.copy(source, destination)
os.rename(f'{self.systemd_folder}{os.sep}{self.ServiceName}.service.generated', f'{self.systemd_folder}{os.sep}{self.ServiceName}.service')
print(f'Service file moved to systemd folder {self.systemd_folder}')
self.Logs.debug(f'Service file moved to systemd folder {self.systemd_folder}')
def install_linux_dependencies(self) -> None:
self.run_subprocess(self.cmd_linux_requirements)
return None
def install_dependencies(self) -> None:
try:
self.run_subprocess([self.venv_pip_full_path, 'cache', 'purge'])
self.run_subprocess([self.venv_python_full_path, '-m', 'pip', 'install', '--upgrade', 'pip'])
if self.check_packages('greenlet') is None:
self.run_subprocess(
[self.venv_pip_full_path, 'install', '--only-binary', ':all:', 'greenlet']
)
for module in self.venv_dependencies:
if not self.check_packages(module):
### Trying to install missing python packages ###
self.run_subprocess([self.venv_pip_full_path, 'install', module])
else:
self.Logs.debug(f'{module} already installed')
print(f"==> {module} already installed")
except CalledProcessError as cpe:
self.Logs.critical(f'{cpe}')
Install()

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

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

@@ -0,0 +1,364 @@
from typing import TYPE_CHECKING, Optional, Any
import mods.clone.utils as utils
import mods.clone.threads as thds
import mods.clone.schemas as schemas
from mods.clone.clone_manager import CloneManager
if TYPE_CHECKING:
from core.irc import Irc
from faker import Faker
class Clone:
def __init__(self, irc_instance: 'Irc') -> None:
# Module name (Mandatory)
self.module_name = 'mod_' + str(self.__class__.__name__).lower()
# Add Irc Object to the module (Mandatory)
self.Irc = irc_instance
# Add Irc Protocol Object to the module (Mandatory)
self.Protocol = irc_instance.Protocol
# Add Global Configuration to the module (Mandatory)
self.Config = irc_instance.Config
# Add Base object to the module (Mandatory)
self.Base = irc_instance.Base
# Add logs object to the module (Mandatory)
self.Logs = irc_instance.Loader.Logs
# Add User object to the module (Mandatory)
self.User = irc_instance.User
# Add Channel object to the module (Mandatory)
self.Channel = irc_instance.Channel
# Add global definitions
self.Definition = irc_instance.Loader.Definition
# The Global Settings
self.Settings = irc_instance.Loader.Settings
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')
# 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
# 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
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.Schemas.ModConfModel()
# 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
"""
# Store Clones DB into the global Settings to retrieve it after the reload.
self.Settings.set_cache('UID_CLONE_DB', self.Clone.UID_CLONE_DB)
self.Channel.db_query_channel(action='del', module_name=self.module_name, channel_name=self.Config.CLONE_CHANNEL)
self.Protocol.send2socket(f":{self.Config.SERVICE_NICKNAME} MODE {self.Config.CLONE_CHANNEL} -nts")
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 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':
return self.Utils.handle_on_privmsg(self, cmd)
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]")
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")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} clone kill clone_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 #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
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 #channel")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} clone part clone_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]):
uid_sender = uplink.Irc.Utils.clean_uid(srvmsg[1])
senderObj = uplink.User.get_user(uid_sender)
if senderObj.hostname in uplink.Config.CLONE_LOG_HOST_EXEMPT:
return
if not senderObj is None:
senderMsg = ' '.join(srvmsg[4:])
clone_obj = uplink.Clone.get_clone(srvmsg[3])
if clone_obj is None:
return
if clone_obj.uid != uplink.Config.SERVICE_ID:
final_message = f"{senderObj.nickname}!{senderObj.username}@{senderObj.hostname} > {senderMsg.lstrip(':')}"
uplink.Protocol.send_priv_msg(
nick_from=clone_obj.uid,
msg=final_message,
channel=uplink.Config.CLONE_CHANNEL
)

1010
mods/command/mod_command.py Normal file

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,243 @@
from typing import TYPE_CHECKING, Literal, Optional
if TYPE_CHECKING:
from mods.command.mod_command import Command
def set_automode(uplink: 'Command', cmd: list[str], client: str) -> None:
command: str = str(cmd[0]).lower()
option: str = str(cmd[1]).lower()
allowed_modes: list[str] = uplink.Loader.Settings.PROTOCTL_PREFIX # ['q','a','o','h','v']
dnickname = uplink.Config.SERVICE_NICKNAME
service_id = uplink.Config.SERVICE_ID
fromuser = client
match option:
case 'set':
if len(cmd) < 5:
uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} {command.upper()} [nickname] [+/-mode] [#channel]")
uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"AutoModes available: {' / '.join(allowed_modes)}")
return None
nickname = str(cmd[2])
mode = str(cmd[3])
chan: str = str(cmd[4]).lower() if uplink.Channel.is_valid_channel(cmd[4]) else None
sign = mode[0] if mode.startswith( ('+', '-')) else None
clean_mode = mode[1:] if len(mode) > 0 else None
if sign is None:
uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg="You must provide the flag mode + or -")
return None
if clean_mode not in allowed_modes:
uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"You should use one of those modes {' / '.join(allowed_modes)}")
return None
if chan is None:
uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"You should use one of those modes {' / '.join(allowed_modes)}")
return None
db_data: dict[str, str] = {"nickname": nickname, "channel": chan}
db_query = uplink.Base.db_execute_query(query="SELECT id FROM command_automode WHERE nickname = :nickname and channel = :channel", params=db_data)
db_result = db_query.fetchone()
if db_result is not None:
if sign == '+':
db_data = {"updated_on": uplink.MainUtils.get_sdatetime(), "nickname": nickname, "channel": chan, "mode": mode}
db_result = uplink.Base.db_execute_query(query="UPDATE command_automode SET mode = :mode, updated_on = :updated_on WHERE nickname = :nickname and channel = :channel",
params=db_data)
if db_result.rowcount > 0:
uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Automode {mode} edited for {nickname} in {chan}")
elif sign == '-':
db_data = {"nickname": nickname, "channel": chan, "mode": f"+{clean_mode}"}
db_result = uplink.Base.db_execute_query(query="DELETE FROM command_automode WHERE nickname = :nickname and channel = :channel and mode = :mode",
params=db_data)
if db_result.rowcount > 0:
uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Automode {mode} deleted for {nickname} in {chan}")
else:
uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"The mode [{mode}] has not been found for {nickname} in channel {chan}")
return None
# Instert a new automode
if sign == '+':
db_data = {"created_on": uplink.MainUtils.get_sdatetime(), "updated_on": uplink.MainUtils.get_sdatetime(), "nickname": nickname, "channel": chan, "mode": mode}
db_query = uplink.Base.db_execute_query(
query="INSERT INTO command_automode (created_on, updated_on, nickname, channel, mode) VALUES (:created_on, :updated_on, :nickname, :channel, :mode)",
params=db_data
)
if db_query.rowcount > 0:
uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Automode {mode} applied to {nickname} in {chan}")
if uplink.Channel.is_user_present_in_channel(chan, uplink.User.get_uid(nickname)):
uplink.Protocol.send2socket(f":{service_id} MODE {chan} {mode} {nickname}")
else:
uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"AUTOMODE {mode} cannot be added to {nickname} in {chan} because it doesn't exist")
case 'list':
db_query = uplink.Base.db_execute_query("SELECT nickname, channel, mode FROM command_automode")
db_results = db_query.fetchall()
if not db_results:
uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,
msg="There is no automode to display.")
for db_result in db_results:
db_nickname, db_channel, db_mode = db_result
uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,
msg=f"Nickname: {db_nickname} | Channel: {db_channel} | Mode: {db_mode}")
case _:
uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} {command.upper()} SET [nickname] [+/-mode] [#channel]")
uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"/msg {dnickname} {command.upper()} LIST")
uplink.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"[AUTOMODES AVAILABLE] are {' / '.join(allowed_modes)}")
def set_deopall(uplink: 'Command', channel_name: str) -> None:
service_id = uplink.Config.SERVICE_ID
uplink.Protocol.send2socket(f":{service_id} SVSMODE {channel_name} -o")
return None
def set_devoiceall(uplink: 'Command', channel_name: str) -> None:
service_id = uplink.Config.SERVICE_ID
uplink.Protocol.send2socket(f":{service_id} SVSMODE {channel_name} -v")
return None
def set_mode_to_all(uplink: 'Command', channel_name: str, action: Literal['+', '-'], pmode: str) -> None:
chan_info = uplink.Channel.get_channel(channel_name)
service_id = uplink.Config.SERVICE_ID
dnickname = uplink.Config.SERVICE_NICKNAME
set_mode = pmode
mode:str = ''
users:str = ''
uids_split = [chan_info.uids[i:i + 6] for i in range(0, len(chan_info.uids), 6)]
uplink.Protocol.send2socket(f":{service_id} MODE {channel_name} {action}{set_mode} {dnickname}")
for uid in uids_split:
for i in range(0, len(uid)):
mode += set_mode
users += f'{uplink.User.get_nickname(uplink.MainUtils.clean_uid(uid[i]))} '
if i == len(uid) - 1:
uplink.Protocol.send2socket(f":{service_id} MODE {channel_name} {action}{mode} {users}")
mode = ''
users = ''
def set_operation(uplink: 'Command', cmd: list[str], channel_name: Optional[str], client: str, mode: str) -> None:
dnickname = uplink.Config.SERVICE_NICKNAME
service_id = uplink.Config.SERVICE_ID
if channel_name is None:
uplink.Protocol.send_notice(nick_from=dnickname, nick_to=client, msg=f" Right command : /msg {dnickname} {mode} [#SALON] [NICKNAME]")
return False
if len(cmd) == 1:
uplink.Protocol.send2socket(f":{dnickname} MODE {channel_name} {mode} {client}")
return None
# deop nickname
if len(cmd) == 2:
nickname = cmd[1]
uplink.Protocol.send2socket(f":{service_id} MODE {channel_name} {mode} {nickname}")
return None
nickname = cmd[2]
uplink.Protocol.send2socket(f":{service_id} MODE {channel_name} {mode} {nickname}")
return None
def set_ban(uplink: 'Command', cmd: list[str], action: Literal['+', '-'], client: str) -> None:
command = str(cmd[0])
dnickname = uplink.Config.SERVICE_NICKNAME
service_id = uplink.Config.SERVICE_ID
sentchannel = str(cmd[1]) if uplink.Channel.is_valid_channel(cmd[1]) else None
if sentchannel is None:
uplink.Protocol.send_notice(nick_from=dnickname, nick_to=client, msg=f" Right command : /msg {dnickname} {command.upper()} [#SALON] [NICKNAME]")
return None
nickname = cmd[2]
uplink.Protocol.send2socket(f":{service_id} MODE {sentchannel} {action}b {nickname}!*@*")
uplink.Logs.debug(f'{client} has banned {nickname} from {sentchannel}')
return None
def set_kick(uplink: 'Command', cmd: list[str], client: str) -> None:
command = str(cmd[0])
dnickname = uplink.Config.SERVICE_NICKNAME
service_id = uplink.Config.SERVICE_ID
sentchannel = str(cmd[1]) if uplink.Channel.is_valid_channel(cmd[1]) else None
if sentchannel is None:
uplink.Protocol.send_notice(nick_from=dnickname, nick_to=client, msg=f" Right command : /msg {dnickname} {command} [#SALON] [NICKNAME]")
return False
nickname = cmd[2]
final_reason = ' '.join(cmd[3:])
uplink.Protocol.send2socket(f":{service_id} KICK {sentchannel} {nickname} {final_reason}")
uplink.Logs.debug(f'{client} has kicked {nickname} from {sentchannel} : {final_reason}')
return None
def set_kickban(uplink: 'Command', cmd: list[str], client: str) -> None:
command = str(cmd[0])
dnickname = uplink.Config.SERVICE_NICKNAME
service_id = uplink.Config.SERVICE_ID
sentchannel = str(cmd[1]) if uplink.Channel.is_valid_channel(cmd[1]) else None
if sentchannel is None:
uplink.Protocol.send_notice(nick_from=dnickname, nick_to=client, msg=f" Right command : /msg {dnickname} {command} [#SALON] [NICKNAME]")
return False
nickname = cmd[2]
final_reason = ' '.join(cmd[3:])
uplink.Protocol.send2socket(f":{service_id} KICK {sentchannel} {nickname} {final_reason}")
uplink.Protocol.send2socket(f":{service_id} MODE {sentchannel} +b {nickname}!*@*")
uplink.Logs.debug(f'{client} has kicked and banned {nickname} from {sentchannel} : {final_reason}')
def set_assign_channel_to_service(uplink: 'Command', cmd: list[str], client: str) -> None:
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,975 @@
import traceback
import mods.defender.schemas as schemas
import mods.defender.utils as utils
import mods.defender.threads as thds
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.irc import Irc
class Defender:
def __init__(self, irc_instance: 'Irc') -> None:
# Module name (Mandatory)
self.module_name = 'mod_' + str(self.__class__.__name__).lower()
# Add Irc Object to the module (Mandatory)
self.Irc = irc_instance
# Add Loader Object to the module (Mandatory)
self.Loader = irc_instance.Loader
# Add server protocol Object to the module (Mandatory)
self.Protocol = irc_instance.Protocol
# Add Global Configuration to the module (Mandatory)
self.Config = irc_instance.Config
# Add Base object to the module (Mandatory)
self.Base = irc_instance.Base
# Add logs object to the module (Mandatory)
self.Logs = irc_instance.Loader.Logs
# Add User object to the module (Mandatory)
self.User = irc_instance.User
# Add Channel object to the module (Mandatory)
self.Channel = irc_instance.Channel
# Add Settings object to save objects when reloading modules (Mandatory)
self.Settings = irc_instance.Settings
# Add Reputation object to the module (Optional)
self.Reputation = irc_instance.Reputation
# Add module schemas
self.Schemas = schemas
# Add utils functions
self.Utils = utils
# Create module commands (Mandatory)
self.Irc.build_command(0, self.module_name, 'code', 'Display the code or key for access')
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, 'timer', 'Set or manage timers')
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')
# Init the module (Mandatory)
self.__init_module()
# Log the module
self.Logs.debug(f'-- Module {self.module_name} V2 loaded ...')
def __init_module(self) -> None:
# Create you own tables if needed (Mandatory)
self.__create_tables()
# Load module configuration (Mandatory)
self.__load_module_configuration()
# End of mandatory methods you can start your customization #
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:bool = True
self.freeipapi_isRunning:bool = True
self.cloudfilt_isRunning:bool = True
self.psutil_isRunning:bool = True
self.localscan_isRunning:bool = True
self.reputationTimer_isRunning:bool = True
self.autolimit_isRunning: bool = 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}")
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_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_module_configuration(self) -> None:
"""### Load Module Configuration
"""
# Variable qui va contenir les options de configuration du module Defender
self.ModConfig = self.Schemas.ModConfModel()
# Sync the configuration with core configuration (Mandatory)
self.Base.db_sync_core_config(self.module_name, self.ModConfig)
return None
def __update_configuration(self, param_key: str, param_value: str):
self.Base.db_update_core_config(self.module_name, self.ModConfig, param_key, param_value)
def __onload(self):
abuseipdb = self.Settings.get_cache('ABUSEIPDB')
freeipapi = self.Settings.get_cache('FREEIPAPI')
cloudfilt = self.Settings.get_cache('CLOUDFILT')
psutils = self.Settings.get_cache('PSUTIL')
localscan = self.Settings.get_cache('LOCALSCAN')
if abuseipdb:
self.Schemas.DB_ABUSEIPDB_USERS = abuseipdb
if freeipapi:
self.Schemas.DB_FREEIPAPI_USERS = freeipapi
if cloudfilt:
self.Schemas.DB_CLOUDFILT_USERS = cloudfilt
if psutils:
self.Schemas.DB_PSUTIL_USERS = psutils
if localscan:
self.Schemas.DB_LOCALSCAN_USERS = localscan
def unload(self) -> None:
"""Cette methode sera executée a chaque désactivation ou
rechargement de module
"""
self.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
return None
def insert_db_trusted(self, uid: str, nickname:str) -> None:
uid = self.User.get_uid(uid)
nickname = self.User.get_nickname(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.Base.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 run_db_action_timer(self, wait_for: float = 0) -> None:
query = f"SELECT param_key FROM {self.Config.TABLE_CONFIG}"
res = self.Base.db_execute_query(query)
service_id = self.Config.SERVICE_ID
dchanlog = self.Config.SERVICE_CHANLOG
for param in res.fetchall():
if param[0] == 'reputation':
self.Protocol.send_priv_msg(
nick_from=service_id,
msg=f" ===> {param[0]}",
channel=dchanlog
)
else:
self.Protocol.send_priv_msg(
nick_from=service_id,
msg=f"{param[0]}",
channel=dchanlog
)
return None
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}")
traceback.print_exc()
def hcmds(self, user:str, channel: any, cmd: list, fullcmd: list = []) -> None:
command = str(cmd[0]).lower()
fromuser = user
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 'timer':
try:
timer_sent = self.Base.int_if_possible(cmd[1])
timer_sent = int(timer_sent)
self.Base.create_timer(timer_sent, self.run_db_action_timer)
except TypeError as te:
self.Logs.error(f"Type Error -> {te}")
except ValueError as ve:
self.Logs.error(f"Value Error -> {ve}")
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 = self.User.get_nickname(fromuser)
jailed_UID = self.User.get_uid(fromuser)
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}")
self.User.get_user(jailed_UID).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}')
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 : {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':
for chan in self.Channel.UID_CHANNEL_DB:
if chan.name not in channel_to_dont_quit:
self.Protocol.send_join_chan(uidornickname=dnickname, channel=chan.name)
return None
if activation == 'off':
for chan in self.Channel.UID_CHANNEL_DB:
if chan.name not in channel_to_dont_quit:
self.Protocol.send_part_chan(uidornickname=dnickname, channel=chan.name)
self.join_saved_channels()
return None

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

@@ -0,0 +1,35 @@
from core.definition import MainModel, dataclass, MUser
@dataclass
class ModConfModel(MainModel):
reputation: int = 0
reputation_timer: int = 1
reputation_seuil: int = 26
reputation_score_after_release: int = 27
reputation_ban_all_chan: int = 0
reputation_sg: int = 1
local_scan: int = 0
psutil_scan: int = 0
abuseipdb_scan: int = 0
freeipapi_scan: int = 0
cloudfilt_scan: int = 0
flood: int = 0
flood_message: int = 5
flood_time: int = 1
flood_timer: int = 20
autolimit: int = 0
autolimit_amount: int = 3
autolimit_interval: int = 3
@dataclass
class FloodUser(MainModel):
uid: str = None
nbr_msg: int = 0
first_msg_time: int = 0
DB_FLOOD_USERS: list[FloodUser] = []
DB_ABUSEIPDB_USERS: list[MUser] = []
DB_FREEIPAPI_USERS: list[MUser] = []
DB_CLOUDFILT_USERS: list[MUser] = []
DB_PSUTIL_USERS: list[MUser] = []
DB_LOCALSCAN_USERS: list[MUser] = []

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

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

@@ -0,0 +1,710 @@
from calendar import c
import socket
import psutil
import requests
import mods.defender.threads as dthreads
from json import loads
from re import match
from typing import TYPE_CHECKING, Optional
from mods.defender.schemas import FloodUser
if TYPE_CHECKING:
from core.definition import MUser
from mods.defender.mod_defender import Defender
def handle_on_reputation(uplink: 'Defender', srvmsg: list[str]):
"""Handle reputation server message
>>> srvmsg = [':001', 'REPUTATION', '128.128.128.128', '0']
>>> srvmsg = [':001', 'REPUTATION', '128.128.128.128', '*0']
Args:
irc_instance (Irc): The Irc instance
srvmsg (list[str]): The Server MSG
"""
ip = srvmsg[2]
score = srvmsg[3]
if str(ip).find('*') != -1:
# If the reputation changed, we do not need to scan the IP
return
# Possibilité de déclancher les bans a ce niveau.
if not uplink.Base.is_valid_ip(ip):
return
def handle_on_mode(uplink: 'Defender', srvmsg: list[str]):
"""_summary_
>>> srvmsg = ['@unrealircd.org/...', ':001C0MF01', 'MODE', '#services', '+l', '1']
>>> srvmsg = ['...', ':001XSCU0Q', 'MODE', '#jail', '+b', '~security-group:unknown-users']
Args:
irc_instance (Irc): The Irc instance
srvmsg (list[str]): The Server MSG
confmodel (ModConfModel): The Module Configuration
"""
irc = uplink.Irc
gconfig = uplink.Config
p = uplink.Protocol
confmodel = uplink.ModConfig
channel = str(srvmsg[3])
mode = str(srvmsg[4])
group_to_check = str(srvmsg[5:])
group_to_unban = '~security-group:unknown-users'
if confmodel.autolimit == 1:
if mode == '+l' or mode == '-l':
chan = irc.Channel.get_channel(channel)
p.send2socket(f":{gconfig.SERVICE_ID} MODE {chan.name} +l {len(chan.uids) + confmodel.autolimit_amount}")
if gconfig.SALON_JAIL == channel:
if mode == '+b' and group_to_unban in group_to_check:
p.send2socket(f":{gconfig.SERVICE_ID} MODE {gconfig.SALON_JAIL} -b ~security-group:unknown-users")
p.send2socket(f":{gconfig.SERVICE_ID} MODE {gconfig.SALON_JAIL} -eee ~security-group:webirc-users ~security-group:known-users ~security-group:websocket-users")
def handle_on_privmsg(uplink: 'Defender', srvmsg: list[str]):
# ['@mtag....',':python', 'PRIVMSG', '#defender', ':zefzefzregreg', 'regg', 'aerg']
action_on_flood(uplink, srvmsg)
return None
def handle_on_sjoin(uplink: 'Defender', srvmsg: list[str]):
"""If Joining a new channel, it applies group bans.
>>> srvmsg = ['@msgid..', ':001', 'SJOIN', '1702138958', '#welcome', ':0015L1AHL']
Args:
irc_instance (Irc): The Irc instance
srvmsg (list[str]): The Server MSG
confmodel (ModConfModel): The Module Configuration
"""
irc = uplink.Irc
p = irc.Protocol
gconfig = uplink.Config
confmodel = uplink.ModConfig
parsed_chan = srvmsg[4] if irc.Channel.is_valid_channel(srvmsg[4]) else None
parsed_UID = uplink.Loader.Utils.clean_uid(srvmsg[5])
if parsed_chan is None or parsed_UID is None:
return
if confmodel.reputation == 1:
get_reputation = irc.Reputation.get_Reputation(parsed_UID)
if parsed_chan != gconfig.SALON_JAIL:
p.send2socket(f":{gconfig.SERVICE_ID} MODE {parsed_chan} +b ~security-group:unknown-users")
p.send2socket(f":{gconfig.SERVICE_ID} MODE {parsed_chan} +eee ~security-group:webirc-users ~security-group:known-users ~security-group:websocket-users")
if get_reputation is not None:
isWebirc = get_reputation.isWebirc
if not isWebirc:
if parsed_chan != gconfig.SALON_JAIL:
p.send_sapart(nick_to_sapart=get_reputation.nickname, channel_name=parsed_chan)
if confmodel.reputation_ban_all_chan == 1 and not isWebirc:
if parsed_chan != gconfig.SALON_JAIL:
p.send2socket(f":{gconfig.SERVICE_ID} MODE {parsed_chan} +b {get_reputation.nickname}!*@*")
p.send2socket(f":{gconfig.SERVICE_ID} KICK {parsed_chan} {get_reputation.nickname}")
irc.Logs.debug(f'SJOIN parsed_uid : {parsed_UID}')
def handle_on_slog(uplink: 'Defender', srvmsg: list[str]):
"""Handling SLOG messages
>>> srvmsg = ['@unrealircd...', ':001', 'SLOG', 'info', 'blacklist', 'BLACKLIST_HIT', ':[Blacklist]', 'IP', '162.x.x.x', 'matches', 'blacklist', 'dronebl', '(dnsbl.dronebl.org/reply=6)']
Args:
irc_instance (Irc): The Irc instance
srvmsg (list[str]): The Server MSG
confmodel (ModConfModel): The Module Configuration
"""
['@unrealircd...', ':001', 'SLOG', 'info', 'blacklist', 'BLACKLIST_HIT', ':[Blacklist]', 'IP', '162.x.x.x', 'matches', 'blacklist', 'dronebl', '(dnsbl.dronebl.org/reply=6)']
if not uplink.Base.is_valid_ip(srvmsg[8]):
return None
# if self.ModConfig.local_scan == 1 and not cmd[7] in self.Config.WHITELISTED_IP:
# self.localscan_remote_ip.append(cmd[7])
# if self.ModConfig.psutil_scan == 1 and not cmd[7] in self.Config.WHITELISTED_IP:
# self.psutil_remote_ip.append(cmd[7])
# if self.ModConfig.abuseipdb_scan == 1 and not cmd[7] in self.Config.WHITELISTED_IP:
# self.abuseipdb_remote_ip.append(cmd[7])
# if self.ModConfig.freeipapi_scan == 1 and not cmd[7] in self.Config.WHITELISTED_IP:
# self.freeipapi_remote_ip.append(cmd[7])
# if self.ModConfig.cloudfilt_scan == 1 and not cmd[7] in self.Config.WHITELISTED_IP:
# self.cloudfilt_remote_ip.append(cmd[7])
return None
def handle_on_nick(uplink: 'Defender', srvmsg: list[str]):
"""_summary_
>>> srvmsg = ['@unrealircd.org...', ':001MZQ0RB', 'NICK', 'newnickname', '1754663712']
Args:
irc_instance (Irc): The Irc instance
srvmsg (list[str]): The Server MSG
confmodel (ModConfModel): The Module Configuration
"""
uid = uplink.Loader.Utils.clean_uid(str(srvmsg[1]))
p = uplink.Protocol
confmodel = uplink.ModConfig
get_reputation = uplink.Reputation.get_Reputation(uid)
jail_salon = uplink.Config.SALON_JAIL
service_id = uplink.Config.SERVICE_ID
if get_reputation is None:
uplink.Logs.debug(f'This UID: {uid} is not listed in the reputation dataclass')
return None
# Update the new nickname
oldnick = get_reputation.nickname
newnickname = srvmsg[3]
get_reputation.nickname = newnickname
# If ban in all channel is ON then unban old nickname an ban the new nickname
if confmodel.reputation_ban_all_chan == 1:
for chan in uplink.Channel.UID_CHANNEL_DB:
if chan.name != jail_salon:
p.send2socket(f":{service_id} MODE {chan.name} -b {oldnick}!*@*")
p.send2socket(f":{service_id} MODE {chan.name} +b {newnickname}!*@*")
def handle_on_quit(uplink: 'Defender', srvmsg: list[str]):
"""_summary_
>>> srvmsg = ['@unrealircd.org...', ':001MZQ0RB', 'QUIT', ':Quit:', 'quit message']
Args:
uplink (Irc): The Defender Module instance
srvmsg (list[str]): The Server MSG
"""
p = uplink.Protocol
confmodel = uplink.ModConfig
ban_all_chan = uplink.Base.int_if_possible(confmodel.reputation_ban_all_chan)
final_UID = uplink.Loader.Utils.clean_uid(str(srvmsg[1]))
jail_salon = uplink.Config.SALON_JAIL
service_id = uplink.Config.SERVICE_ID
get_user_reputation = uplink.Reputation.get_Reputation(final_UID)
if get_user_reputation is not None:
final_nickname = get_user_reputation.nickname
for chan in uplink.Channel.UID_CHANNEL_DB:
if chan.name != jail_salon and ban_all_chan == 1:
p.send2socket(f":{service_id} MODE {chan.name} -b {final_nickname}!*@*")
uplink.Logs.debug(f"Mode -b {final_nickname} on channel {chan.name}")
uplink.Reputation.delete(final_UID)
uplink.Logs.debug(f"Client {get_user_reputation.nickname} has been removed from Reputation local DB")
def handle_on_uid(uplink: 'Defender', srvmsg: list[str]):
"""_summary_
>>> ['@s2s-md...', ':001', 'UID', 'nickname', '0', '1754675249', '...', '125-168-141-239.hostname.net', '001BAPN8M',
'0', '+iwx', '*', '32001BBE.25ACEFE7.429FE90D.IP', 'ZA2ic7w==', ':realname']
Args:
uplink (Defender): The Defender instance
srvmsg (list[str]): The Server MSG
"""
gconfig = uplink.Config
irc = uplink.Irc
confmodel = uplink.ModConfig
# If Init then do nothing
if gconfig.DEFENDER_INIT == 1:
return None
# Get User information
_User = irc.User.get_user(str(srvmsg[8]))
if _User is None:
irc.Logs.warning(f'This UID: [{srvmsg[8]}] is not available please check why')
return
# If user is not service or IrcOp then scan them
if not match(r'^.*[S|o?].*$', _User.umodes):
uplink.Schemas.DB_ABUSEIPDB_USERS.append(_User) if confmodel.abuseipdb_scan == 1 and _User.remote_ip not in gconfig.WHITELISTED_IP else None
uplink.Schemas.DB_FREEIPAPI_USERS.append(_User) if confmodel.freeipapi_scan == 1 and _User.remote_ip not in gconfig.WHITELISTED_IP else None
uplink.Schemas.DB_CLOUDFILT_USERS.append(_User) if confmodel.cloudfilt_scan == 1 and _User.remote_ip not in gconfig.WHITELISTED_IP else None
uplink.Schemas.DB_PSUTIL_USERS.append(_User) if confmodel.psutil_scan == 1 and _User.remote_ip not in gconfig.WHITELISTED_IP else None
uplink.Schemas.DB_LOCALSCAN_USERS.append(_User) if confmodel.local_scan == 1 and _User.remote_ip not in gconfig.WHITELISTED_IP else None
reputation_flag = confmodel.reputation
reputation_seuil = confmodel.reputation_seuil
if gconfig.DEFENDER_INIT == 0:
# Si le user n'es pas un service ni un IrcOP
if not match(r'^.*[S|o?].*$', _User.umodes):
if reputation_flag == 1 and _User.score_connexion <= reputation_seuil:
# currentDateTime = self.Base.get_datetime()
irc.Reputation.insert(
irc.Loader.Definition.MReputation(
**_User.to_dict(),
secret_code=irc.Utils.generate_random_string(8)
)
)
if irc.Reputation.is_exist(_User.uid):
if reputation_flag == 1 and _User.score_connexion <= reputation_seuil:
action_add_reputation_sanctions(uplink, _User.uid)
irc.Logs.info(f'[REPUTATION] Reputation system ON (Nickname: {_User.nickname}, uid: {_User.uid})')
####################
# ACTION FUNCTIONS #
####################
def action_on_flood(uplink: 'Defender', srvmsg: list[str]):
confmodel = uplink.ModConfig
if confmodel.flood == 0:
return None
irc = uplink.Irc
gconfig = uplink.Config
p = uplink.Protocol
flood_users = uplink.Schemas.DB_FLOOD_USERS
user_trigger = str(srvmsg[1]).replace(':','')
channel = srvmsg[3]
User = irc.User.get_user(user_trigger)
if User is None or not irc.Channel.is_valid_channel(channel_to_check=channel):
return
flood_time = confmodel.flood_time
flood_message = confmodel.flood_message
flood_timer = confmodel.flood_timer
service_id = gconfig.SERVICE_ID
dnickname = gconfig.SERVICE_NICKNAME
color_red = gconfig.COLORS.red
color_bold = gconfig.COLORS.bold
get_detected_uid = User.uid
get_detected_nickname = User.nickname
unixtime = irc.Utils.get_unixtime()
get_diff_secondes = 0
def get_flood_user(uid: str) -> Optional[FloodUser]:
for flood_user in flood_users:
if flood_user.uid == uid:
return flood_user
fu = get_flood_user(get_detected_uid)
if fu is None:
fu = FloodUser(get_detected_uid, 0, unixtime)
flood_users.append(fu)
fu.nbr_msg += 1
get_diff_secondes = unixtime - fu.first_msg_time
if get_diff_secondes > flood_time:
fu.first_msg_time = unixtime
fu.nbr_msg = 0
get_diff_secondes = unixtime - fu.first_msg_time
elif fu.nbr_msg > flood_message:
irc.Logs.info('system de flood detecté')
p.send_priv_msg(
nick_from=dnickname,
msg=f"{color_red} {color_bold} Flood detected. Apply the +m mode (Ô_o)",
channel=channel
)
p.send2socket(f":{service_id} MODE {channel} +m")
irc.Logs.info(f'FLOOD Détecté sur {get_detected_nickname} mode +m appliqué sur le salon {channel}')
fu.nbr_msg = 0
fu.first_msg_time = unixtime
irc.Base.create_timer(flood_timer, dthreads.timer_release_mode_mute, (uplink, 'mode-m', channel))
def action_add_reputation_sanctions(uplink: 'Defender', jailed_uid: str ):
irc = uplink.Irc
gconfig = uplink.Config
p = uplink.Protocol
confmodel = uplink.ModConfig
get_reputation = irc.Reputation.get_Reputation(jailed_uid)
if get_reputation is None:
irc.Logs.warning(f'UID {jailed_uid} has not been found')
return
salon_logs = gconfig.SERVICE_CHANLOG
salon_jail = gconfig.SALON_JAIL
code = get_reputation.secret_code
jailed_nickname = get_reputation.nickname
jailed_score = get_reputation.score_connexion
color_red = gconfig.COLORS.red
color_black = gconfig.COLORS.black
color_bold = gconfig.COLORS.bold
nogc = gconfig.COLORS.nogc
service_id = gconfig.SERVICE_ID
service_prefix = gconfig.SERVICE_PREFIX
reputation_ban_all_chan = confmodel.reputation_ban_all_chan
if not get_reputation.isWebirc:
# Si le user ne vient pas de webIrc
p.send_sajoin(nick_to_sajoin=jailed_nickname, channel_name=salon_jail)
p.send_priv_msg(nick_from=gconfig.SERVICE_NICKNAME,
msg=f" [{color_red} REPUTATION {nogc}] : Connexion de {jailed_nickname} ({jailed_score}) ==> {salon_jail}",
channel=salon_logs
)
p.send_notice(
nick_from=gconfig.SERVICE_NICKNAME,
nick_to=jailed_nickname,
msg=f"[{color_red} {jailed_nickname} {color_black}] : Merci de tapez la commande suivante {color_bold}{service_prefix}code {code}{color_bold}"
)
if reputation_ban_all_chan == 1:
for chan in irc.Channel.UID_CHANNEL_DB:
if chan.name != salon_jail:
p.send2socket(f":{service_id} MODE {chan.name} +b {jailed_nickname}!*@*")
p.send2socket(f":{service_id} KICK {chan.name} {jailed_nickname}")
irc.Logs.info(f"[REPUTATION] {jailed_nickname} jailed (UID: {jailed_uid}, score: {jailed_score})")
else:
irc.Logs.info(f"[REPUTATION] {jailed_nickname} skipped (trusted or WebIRC)")
irc.Reputation.delete(jailed_uid)
def action_apply_reputation_santions(uplink: 'Defender') -> None:
irc = uplink.Irc
gconfig = uplink.Config
p = uplink.Protocol
confmodel = uplink.ModConfig
reputation_flag = confmodel.reputation
reputation_timer = confmodel.reputation_timer
reputation_seuil = confmodel.reputation_seuil
ban_all_chan = confmodel.reputation_ban_all_chan
service_id = gconfig.SERVICE_ID
dchanlog = gconfig.SERVICE_CHANLOG
color_red = gconfig.COLORS.red
nogc = gconfig.COLORS.nogc
salon_jail = gconfig.SALON_JAIL
if reputation_flag == 0:
return None
elif reputation_timer == 0:
return None
uid_to_clean = []
for user in irc.Reputation.UID_REPUTATION_DB:
if not user.isWebirc: # Si il ne vient pas de WebIRC
if irc.User.get_user_uptime_in_minutes(user.uid) >= reputation_timer and int(user.score_connexion) <= int(reputation_seuil):
p.send_priv_msg(
nick_from=service_id,
msg=f"[{color_red} REPUTATION {nogc}] : Action sur {user.nickname} aprés {str(reputation_timer)} minutes d'inactivité",
channel=dchanlog
)
p.send2socket(f":{service_id} KILL {user.nickname} After {str(reputation_timer)} minutes of inactivity you should reconnect and type the password code")
p.send2socket(f":{gconfig.SERVEUR_LINK} REPUTATION {user.remote_ip} 0")
irc.Logs.info(f"Nickname: {user.nickname} KILLED after {str(reputation_timer)} minutes of inactivity")
uid_to_clean.append(user.uid)
for uid in uid_to_clean:
# Suppression des éléments dans {UID_DB} et {REPUTATION_DB}
for chan in irc.Channel.UID_CHANNEL_DB:
if chan.name != salon_jail and ban_all_chan == 1:
get_user_reputation = irc.Reputation.get_Reputation(uid)
p.send2socket(f":{service_id} MODE {chan.name} -b {get_user_reputation.nickname}!*@*")
# Lorsqu'un utilisateur quitte, il doit être supprimé de {UID_DB}.
irc.Channel.delete_user_from_all_channel(uid)
irc.Reputation.delete(uid)
irc.User.delete(uid)
def action_scan_client_with_cloudfilt(uplink: 'Defender', user_model: 'MUser') -> Optional[dict[str, str]]:
"""Analyse l'ip avec cloudfilt
Cette methode devra etre lancer toujours via un thread ou un timer.
Args:
uplink (Defender): Defender Instance
Returns:
dict[str, any] | None: les informations du provider
keys : 'countryCode', 'isProxy'
"""
remote_ip = user_model.remote_ip
username = user_model.username
hostname = user_model.hostname
nickname = user_model.nickname
p = uplink.Protocol
if remote_ip in uplink.Config.WHITELISTED_IP:
return None
if uplink.ModConfig.cloudfilt_scan == 0:
return None
if uplink.cloudfilt_key == '':
return None
service_id = uplink.Config.SERVICE_ID
service_chanlog = uplink.Config.SERVICE_CHANLOG
color_red = uplink.Config.COLORS.red
nogc = uplink.Config.COLORS.nogc
url = "https://developers18334.cloudfilt.com/"
data = {
'ip': remote_ip,
'key': uplink.cloudfilt_key
}
response = requests.post(url=url, data=data)
# Formatted output
decoded_response: dict = loads(response.text)
status_code = response.status_code
if status_code != 200:
uplink.Logs.warning(f'Error connecting to cloudfilt API | Code: {str(status_code)}')
return
result = {
'countryiso': decoded_response.get('countryiso', None),
'listed': decoded_response.get('listed', None),
'listed_by': decoded_response.get('listed_by', None),
'host': decoded_response.get('host', None)
}
# pseudo!ident@host
fullname = f'{nickname}!{username}@{hostname}'
p.send_priv_msg(
nick_from=service_id,
msg=f"[ {color_red}CLOUDFILT_SCAN{nogc} ] : Connexion de {fullname} ({remote_ip}) ==> Host: {str(result['host'])} | country: {str(result['countryiso'])} | listed: {str(result['listed'])} | listed by : {str(result['listed_by'])}",
channel=service_chanlog)
uplink.Logs.debug(f"[CLOUDFILT SCAN] ({fullname}) connected from ({result['countryiso']}), Listed: {result['listed']}, by: {result['listed_by']}")
if result['listed']:
p.send2socket(f":{service_id} GLINE +*@{remote_ip} {uplink.Config.GLINE_DURATION} Your connexion is listed as dangerous {str(result['listed'])} {str(result['listed_by'])} - detected by cloudfilt")
uplink.Logs.debug(f"[CLOUDFILT SCAN GLINE] Dangerous connection ({fullname}) from ({result['countryiso']}) Listed: {result['listed']}, by: {result['listed_by']}")
response.close()
return result
def action_scan_client_with_freeipapi(uplink: 'Defender', user_model: 'MUser') -> Optional[dict[str, str]]:
"""Analyse l'ip avec Freeipapi
Cette methode devra etre lancer toujours via un thread ou un timer.
Args:
uplink (Defender): The Defender object Instance
Returns:
dict[str, any] | None: les informations du provider
keys : 'countryCode', 'isProxy'
"""
p = uplink.Protocol
remote_ip = user_model.remote_ip
username = user_model.username
hostname = user_model.hostname
nickname = user_model.nickname
if remote_ip in uplink.Config.WHITELISTED_IP:
return None
if uplink.ModConfig.freeipapi_scan == 0:
return None
service_id = uplink.Config.SERVICE_ID
service_chanlog = uplink.Config.SERVICE_CHANLOG
color_red = uplink.Config.COLORS.red
nogc = uplink.Config.COLORS.nogc
url = f'https://freeipapi.com/api/json/{remote_ip}'
headers = {
'Accept': 'application/json',
}
response = requests.request(method='GET', url=url, headers=headers, timeout=uplink.timeout)
# Formatted output
decoded_response: dict = loads(response.text)
status_code = response.status_code
if status_code == 429:
uplink.Logs.warning('Too Many Requests - The rate limit for the API has been exceeded.')
return None
elif status_code != 200:
uplink.Logs.warning(f'status code = {str(status_code)}')
return None
result = {
'countryCode': decoded_response.get('countryCode', None),
'isProxy': decoded_response.get('isProxy', None)
}
# pseudo!ident@host
fullname = f'{nickname}!{username}@{hostname}'
p.send_priv_msg(
nick_from=service_id,
msg=f"[ {color_red}FREEIPAPI_SCAN{nogc} ] : Connexion de {fullname} ({remote_ip}) ==> Proxy: {str(result['isProxy'])} | Country : {str(result['countryCode'])}",
channel=service_chanlog)
uplink.Logs.debug(f"[FREEIPAPI SCAN] ({fullname}) connected from ({result['countryCode']}), Proxy: {result['isProxy']}")
if result['isProxy']:
p.send2socket(f":{service_id} GLINE +*@{remote_ip} {uplink.Config.GLINE_DURATION} This server do not allow proxy connexions {str(result['isProxy'])} - detected by freeipapi")
uplink.Logs.debug(f"[FREEIPAPI SCAN GLINE] Server do not allow proxy connexions {result['isProxy']}")
response.close()
return result
def action_scan_client_with_abuseipdb(uplink: 'Defender', user_model: 'MUser') -> Optional[dict[str, str]]:
"""Analyse l'ip avec AbuseIpDB
Cette methode devra etre lancer toujours via un thread ou un timer.
Args:
uplink (Defender): Defender instance object
user_model (MUser): l'objet User qui contient l'ip
Returns:
dict[str, str] | None: les informations du provider
"""
p = uplink.Protocol
remote_ip = user_model.remote_ip
username = user_model.username
hostname = user_model.hostname
nickname = user_model.nickname
if remote_ip in uplink.Config.WHITELISTED_IP:
return None
if uplink.ModConfig.abuseipdb_scan == 0:
return None
if uplink.abuseipdb_key == '':
return None
url = 'https://api.abuseipdb.com/api/v2/check'
querystring = {
'ipAddress': remote_ip,
'maxAgeInDays': '90'
}
headers = {
'Accept': 'application/json',
'Key': uplink.abuseipdb_key
}
response = requests.request(method='GET', url=url, headers=headers, params=querystring, timeout=uplink.timeout)
# Formatted output
decoded_response: dict[str, dict] = loads(response.text)
if 'data' not in decoded_response:
return None
result = {
'score': decoded_response.get('data', {}).get('abuseConfidenceScore', 0),
'country': decoded_response.get('data', {}).get('countryCode', None),
'isTor': decoded_response.get('data', {}).get('isTor', None),
'totalReports': decoded_response.get('data', {}).get('totalReports', 0)
}
service_id = uplink.Config.SERVICE_ID
service_chanlog = uplink.Config.SERVICE_CHANLOG
color_red = uplink.Config.COLORS.red
nogc = uplink.Config.COLORS.nogc
# pseudo!ident@host
fullname = f'{nickname}!{username}@{hostname}'
p.send_priv_msg(
nick_from=service_id,
msg=f"[ {color_red}ABUSEIPDB_SCAN{nogc} ] : Connexion de {fullname} ({remote_ip}) ==> Score: {str(result['score'])} | Country : {result['country']} | Tor : {str(result['isTor'])} | Total Reports : {str(result['totalReports'])}",
channel=service_chanlog
)
uplink.Logs.debug(f"[ABUSEIPDB SCAN] ({fullname}) connected from ({result['country']}), Score: {result['score']}, Tor: {result['isTor']}")
if result['isTor']:
p.send2socket(f":{service_id} GLINE +*@{remote_ip} {uplink.Config.GLINE_DURATION} This server do not allow Tor connexions {str(result['isTor'])} - Detected by Abuseipdb")
uplink.Logs.debug(f"[ABUSEIPDB SCAN GLINE] Server do not allow Tor connections Tor: {result['isTor']}, Score: {result['score']}")
elif result['score'] >= 95:
p.send2socket(f":{service_id} GLINE +*@{remote_ip} {uplink.Config.GLINE_DURATION} You were banned from this server because your abuse score is = {str(result['score'])} - Detected by Abuseipdb")
uplink.Logs.debug(f"[ABUSEIPDB SCAN GLINE] Server do not high risk connections Country: {result['country']}, Score: {result['score']}")
response.close()
return result
def action_scan_client_with_local_socket(uplink: 'Defender', user_model: 'MUser'):
"""local_scan
Args:
uplink (Defender): Defender instance object
user_model (MUser): l'objet User qui contient l'ip
"""
p = uplink.Protocol
remote_ip = user_model.remote_ip
username = user_model.username
hostname = user_model.hostname
nickname = user_model.nickname
fullname = f'{nickname}!{username}@{hostname}'
if remote_ip in uplink.Config.WHITELISTED_IP:
return None
for port in uplink.Config.PORTS_TO_SCAN:
try:
newSocket = ''
newSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM or socket.SOCK_NONBLOCK)
newSocket.settimeout(0.5)
connection = (remote_ip, uplink.Base.int_if_possible(port))
newSocket.connect(connection)
p.send_priv_msg(
nick_from=uplink.Config.SERVICE_NICKNAME,
msg=f"[ {uplink.Config.COLORS.red}PROXY_SCAN{uplink.Config.COLORS.nogc} ] {fullname} ({remote_ip}) : Port [{str(port)}] ouvert sur l'adresse ip [{remote_ip}]",
channel=uplink.Config.SERVICE_CHANLOG
)
# print(f"=======> Le port {str(port)} est ouvert !!")
uplink.Base.running_sockets.append(newSocket)
# print(newSocket)
newSocket.shutdown(socket.SHUT_RDWR)
newSocket.close()
except (socket.timeout, ConnectionRefusedError):
uplink.Logs.info(f"Le port {remote_ip}:{str(port)} est fermé")
except AttributeError as ae:
uplink.Logs.warning(f"AttributeError ({remote_ip}): {ae}")
except socket.gaierror as err:
uplink.Logs.warning(f"Address Info Error ({remote_ip}): {err}")
finally:
# newSocket.shutdown(socket.SHUT_RDWR)
newSocket.close()
uplink.Logs.info('=======> Fermeture de la socket')
def action_scan_client_with_psutil(uplink: 'Defender', user_model: 'MUser') -> list[int]:
"""psutil_scan for Linux (should be run on the same location as the unrealircd server)
Args:
userModel (UserModel): The User Model Object
Returns:
list[int]: list of ports
"""
p = uplink.Protocol
remote_ip = user_model.remote_ip
username = user_model.username
hostname = user_model.hostname
nickname = user_model.nickname
if remote_ip in uplink.Config.WHITELISTED_IP:
return None
try:
connections = psutil.net_connections(kind='inet')
fullname = f'{nickname}!{username}@{hostname}'
matching_ports = [conn.raddr.port for conn in connections if conn.raddr and conn.raddr.ip == remote_ip]
uplink.Logs.info(f"Connexion of {fullname} ({remote_ip}) using ports : {str(matching_ports)}")
if matching_ports:
p.send_priv_msg(
nick_from=uplink.Config.SERVICE_NICKNAME,
msg=f"[ {uplink.Config.COLORS.red}PSUTIL_SCAN{uplink.Config.COLORS.black} ] {fullname} ({remote_ip}) : is using ports {matching_ports}",
channel=uplink.Config.SERVICE_CHANLOG
)
return matching_ports
except psutil.AccessDenied as ad:
uplink.Logs.critical(f'psutil_scan: Permission error: {ad}')

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

@@ -0,0 +1,328 @@
import logging
import asyncio
import mods.jsonrpc.utils as utils
import mods.jsonrpc.threads as thds
from time import sleep
from types import SimpleNamespace
from typing import TYPE_CHECKING
from dataclasses import dataclass
from unrealircd_rpc_py.Live import LiveWebsocket, LiveUnixSocket
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 Main Utils (Mandatory)
self.MainUtils = ircInstance.Utils
# Add logs object to the module (Mandatory)
self.Logs = ircInstance.Loader.Logs
# Add User object to the module (Mandatory)
self.User = ircInstance.User
# Add Channel object to the module (Mandatory)
self.Channel = ircInstance.Channel
# Is RPC Active?
self.is_streaming = False
# Module Utils
self.Utils = utils
# Module threads
self.Threads = thds
# Run Garbage collector.
self.Base.create_timer(10, self.MainUtils.run_python_garbage_collector)
# Create module commands (Mandatory)
self.Irc.build_command(1, self.module_name, 'jsonrpc', 'Activate the JSON RPC Live connection [ON|OFF]')
self.Irc.build_command(1, self.module_name, 'jruser', 'Get Information about a user using JSON RPC')
self.Irc.build_command(1, self.module_name, 'jrinstances', 'Get number of instances')
# Init the module
self.__init_module()
# 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)
logging.getLogger('unrealircd-liverpc-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: LiveWebsocket = LiveWebsocket(
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'
)
if self.UnrealIrcdRpcLive.get_error.code != 0:
self.Logs.error(f"{self.UnrealIrcdRpcLive.get_error.message} ({self.UnrealIrcdRpcLive.get_error.code})")
self.Protocol.send_priv_msg(
nick_from=self.Config.SERVICE_NICKNAME,
msg=f"[{self.Config.COLORS.red}ERROR{self.Config.COLORS.nogc}] {self.UnrealIrcdRpcLive.get_error.message}",
channel=self.Config.SERVICE_CHANLOG
)
raise Exception(f"[LIVE-JSONRPC ERROR] {self.UnrealIrcdRpcLive.get_error.message}")
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
)
if self.Rpc.get_error.code != 0:
self.Logs.error(f"{self.Rpc.get_error.message} ({self.Rpc.get_error.code})")
self.Protocol.send_priv_msg(
nick_from=self.Config.SERVICE_NICKNAME,
msg=f"[{self.Config.COLORS.red}JSONRPC ERROR{self.Config.COLORS.nogc}] {self.Rpc.get_error.message}",
channel=self.Config.SERVICE_CHANLOG
)
raise Exception(f"[JSONRPC ERROR] {self.Rpc.get_error.message}")
if self.ModConfig.jsonrpc == 1:
self.Base.create_thread(func=self.Threads.thread_subscribe, func_args=(self, ), 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, response: SimpleNamespace) -> 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 self.UnrealIrcdRpcLive.get_error.code != 0:
self.Protocol.send_priv_msg(nick_from=dnickname,
msg=f"[{bold}{red}JSONRPC ERROR{nogc}{bold}] {self.UnrealIrcdRpcLive.get_error.message}",
channel=dchanlog)
return None
if hasattr(response, 'error'):
if response.error.code != 0:
self.Protocol.send_priv_msg(
nick_from=self.Config.SERVICE_NICKNAME,
msg=f"[{bold}{red}JSONRPC{nogc}{bold}] JSONRPC Event activated on {self.Config.JSONRPC_URL}",
channel=dchanlog)
return None
if hasattr(response, 'result'):
if isinstance(response.result, bool):
if response.result:
self.Protocol.send_priv_msg(
nick_from=self.Config.SERVICE_NICKNAME,
msg=f"[{bold}{green}JSONRPC{nogc}{bold}] JSONRPC Event activated on {self.Config.JSONRPC_URL}",
channel=dchanlog)
return None
level = response.result.level if hasattr(response.result, 'level') else ''
subsystem = response.result.subsystem if hasattr(response.result, 'subsystem') else ''
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 __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) -> 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 unload(self) -> 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.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])
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.get_error.code != 0:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f'{rpc.get_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(LiveWebsocket)}")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Nombre d'instance LiveUnixSocket: {self.MainUtils.get_number_gc_objects(LiveUnixSocket)}")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Nombre d'instance Loader: {self.MainUtils.get_number_gc_objects(Loader)}")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f"Nombre de toute les instances: {self.MainUtils.get_number_gc_objects()}")
except Exception as err:
self.Logs.error(f"Unknown Error: {err}")

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

@@ -0,0 +1,60 @@
import asyncio
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from mods.jsonrpc.mod_jsonrpc import Jsonrpc
def thread_subscribe(uplink: 'Jsonrpc') -> None:
response: dict[str, dict] = {}
snickname = uplink.Config.SERVICE_NICKNAME
schannel = uplink.Config.SERVICE_CHANLOG
if uplink.UnrealIrcdRpcLive.get_error.code == 0:
uplink.is_streaming = True
response = asyncio.run(uplink.UnrealIrcdRpcLive.subscribe(["all"]))
else:
uplink.Protocol.send_priv_msg(nick_from=snickname,
msg=f"[{uplink.Config.COLORS.red}JSONRPC ERROR{uplink.Config.COLORS.nogc}] {uplink.UnrealIrcdRpcLive.get_error.message}",
channel=schannel
)
if response is None:
return
code = response.get('error', {}).get('code', 0)
message = response.get('error', {}).get('message', None)
if code == 0:
uplink.Protocol.send_priv_msg(
nick_from=snickname,
msg=f"[{uplink.Config.COLORS.green}JSONRPC{uplink.Config.COLORS.nogc}] Stream is OFF",
channel=schannel
)
else:
uplink.Protocol.send_priv_msg(
nick_from=snickname,
msg=f"[{uplink.Config.COLORS.red}JSONRPC{uplink.Config.COLORS.nogc}] Stream has crashed! {code} - {message}",
channel=schannel
)
def thread_unsubscribe(uplink: 'Jsonrpc') -> None:
response: dict[str, dict] = asyncio.run(uplink.UnrealIrcdRpcLive.unsubscribe())
uplink.Logs.debug("[JSONRPC UNLOAD] Unsubscribe from the stream!")
uplink.is_streaming = False
uplink.update_configuration('jsonrpc', 0)
snickname = uplink.Config.SERVICE_NICKNAME
schannel = uplink.Config.SERVICE_CHANLOG
if response is None:
return None
code = response.get('error', {}).get('code', 0)
message = response.get('error', {}).get('message', None)
if code != 0:
uplink.Protocol.send_priv_msg(
nick_from=snickname,
msg=f"[{uplink.Config.COLORS.red}JSONRPC ERROR{uplink.Config.COLORS.nogc}] {message} ({code})",
channel=schannel
)

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,108 +0,0 @@
from core.irc import Irc
# Le module crée devra réspecter quelques conditions
# 1. Importer le module de configuration
# 2. Le nom de class devra toujours s'appeler comme le module exemple => nom de class Dktmb | nom du module mod_dktmb
# 3. la fonction __init__ devra toujours avoir les parametres suivant (self, irc:object)
# 1 . Créer la variable Irc dans le module
# 2 . Récuperer la configuration dans une variable
# 3 . Définir et enregistrer les nouvelles commandes
# 4. une fonction _hcmds(self, user:str, cmd: list) devra toujours etre crée.
class Test():
def __init__(self, ircInstance:Irc) -> None:
# Add Irc Object to the module
self.Irc = ircInstance
# 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.commands_level = {
0: ['test'],
1: ['test_level_1']
}
# Init the module
self.__init_module()
# Log the module
self.Logs.debug(f'Module {self.__class__.__name__} loaded ...')
def __init_module(self) -> None:
self.__set_commands(self.commands_level)
self.__create_tables()
return None
def __set_commands(self, commands:dict[int, list[str]]) -> None:
"""### Rajoute les commandes du module au programme principal
Args:
commands (list): Liste des commandes du module
"""
for level, com in commands.items():
for c in commands[level]:
if not c in self.Irc.commands:
self.Irc.commands_level[level].append(c)
self.Irc.commands.append(c)
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 unload(self) -> None:
return None
def cmd(self, data:list) -> None:
return None
def _hcmds(self, user:str, cmd: list, fullcmd: list = []) -> None:
command = str(cmd[0]).lower()
dnickname = self.Config.SERVICE_NICKNAME
fromuser = user
match command:
case 'test':
try:
self.Irc.send2socket(f":{dnickname} NOTICE {fromuser} : test command ready ...")
self.Logs.debug(f"Test logs ready")
except KeyError as ke:
self.Logs.error(f"Key Error : {ke}")

View File

@@ -1,441 +0,0 @@
from core.irc import Irc
import re
from dataclasses import dataclass, field
# 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
class VoteChannelModel:
channel_name: str
target_user: str
voter_users: list
vote_for: int
vote_against: int
VOTE_CHANNEL_DB:list[VoteChannelModel] = []
def __init__(self, ircInstance:Irc) -> None:
# Add Irc Object to the module
self.Irc = ircInstance
# 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.commands_level = {
0: ['vote_for', 'vote_against'],
1: ['activate', 'deactivate', 'submit', 'vote_stat', 'vote_verdict', 'vote_cancel']
}
# Init the module
self.__init_module()
# Log the module
self.Logs.debug(f'Module {self.__class__.__name__} loaded ...')
def __init_module(self) -> None:
self.__set_commands(self.commands_level)
self.__create_tables()
self.join_saved_channels()
return None
def __set_commands(self, commands:dict[int, list[str]]) -> None:
"""### Rajoute les commandes du module au programme principal
Args:
commands (list): Liste des commandes du module
"""
for level, com in commands.items():
for c in commands[level]:
if not c in self.Irc.commands:
self.Irc.commands_level[level].append(c)
self.Irc.commands.append(c)
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 votekick_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
datetime TEXT,
server_msg TEXT
)
'''
table_vote = '''CREATE TABLE IF NOT EXISTS votekick_channel (
id INTEGER PRIMARY KEY AUTOINCREMENT,
datetime TEXT,
channel TEXT
)
'''
self.Base.db_execute_query(table_logs)
self.Base.db_execute_query(table_vote)
return None
def unload(self) -> None:
try:
for chan in self.VOTE_CHANNEL_DB:
self.Irc.send2socket(f":{self.Config.SERVICE_NICKNAME} PART {chan.channel_name}")
self.VOTE_CHANNEL_DB = []
self.Logs.debug(f'Delete memory DB VOTE_CHANNEL_DB: {self.VOTE_CHANNEL_DB}')
return None
except UnboundLocalError as ne:
self.Logs.error(f'{ne}')
except NameError as ue:
self.Logs.error(f'{ue}')
except:
self.Logs.error('Error on the module')
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:
result = self.Base.db_execute_query("SELECT id, channel FROM votekick_channel")
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.Irc.send2socket(f":{self.Config.SERVEUR_ID} SJOIN {unixtime} {chan} + :{self.Config.SERVICE_ID}")
self.Irc.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
for chan in self.VOTE_CHANNEL_DB:
if chan.channel_name == channel:
target_user = self.User.get_nickname(chan.target_user)
if chan.vote_for > chan.vote_against:
self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :The user {self.Config.CONFIG_COLOR["gras"]}{target_user}{self.Config.CONFIG_COLOR["nogc"]} will be kicked from this channel')
self.Irc.send2socket(f":{dnickname} KICK {channel} {target_user} Following the vote, you are not welcome in {channel}")
self.Channel.delete_user_from_channel(channel, self.User.get_uid(target_user))
elif chan.vote_for <= chan.vote_against:
self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :This user will stay on this channel')
# Init the system
if self.init_vote_system(channel):
self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :System vote re initiated')
return None
def cmd(self, data:list) -> None:
cmd = list(data).copy()
match cmd[2]:
case 'SJOIN':
pass
case _:
pass
return None
def _hcmds(self, user:str, cmd: list, fullcmd: list = []) -> None:
# cmd is the command starting from the user command
# full cmd is sending the entire server response
command = str(cmd[0]).lower()
dnickname = self.Config.SERVICE_NICKNAME
fromuser = user
if len(fullcmd) >= 3:
fromchannel = str(fullcmd[2]).lower() if self.Base.Is_Channel(str(fullcmd[2]).lower()) else None
else:
fromchannel = None
if len(cmd) >= 2:
sentchannel = str(cmd[1]).lower() if self.Base.Is_Channel(str(cmd[1]).lower()) else None
else:
sentchannel = None
if not fromchannel is None:
channel = fromchannel
elif not sentchannel is None:
channel = sentchannel
else:
channel = None
match command:
case 'vote_cancel':
try:
if channel is None:
self.Logs.error(f"The channel is not known, defender can't cancel the vote")
self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} :You need to specify the channel => /msg {dnickname} vote_cancel #channel')
for vote in self.VOTE_CHANNEL_DB:
if vote.channel_name == channel:
self.init_vote_system(channel)
self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :Vote system re-initiated')
except IndexError as ke:
self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} :/msg {dnickname} vote_cancel #channel')
self.Logs.error(f'Index Error: {ke}')
case 'vote_for':
try:
# vote_for
channel = str(fullcmd[2]).lower()
for chan in self.VOTE_CHANNEL_DB:
if chan.channel_name == channel:
if fromuser in chan.voter_users:
self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :You already submitted a vote')
else:
chan.vote_for += 1
chan.voter_users.append(fromuser)
self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :Vote recorded, thank you')
except KeyError as ke:
self.Logs.error(f'Key Error: {ke}')
except IndexError as ie:
self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} :/msg {dnickname} vote_cancel #channel')
self.Logs.error(f'Index Error: {ie}')
case 'vote_against':
try:
# vote_against
channel = str(fullcmd[2]).lower()
for chan in self.VOTE_CHANNEL_DB:
if chan.channel_name == channel:
if fromuser in chan.voter_users:
self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :You already submitted a vote')
else:
chan.vote_against += 1
chan.voter_users.append(fromuser)
self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :Vote recorded, thank you')
except KeyError as ke:
self.Logs.error(f'Key Error: {ke}')
case 'vote_stat':
try:
# channel = str(fullcmd[2]).lower()
for chan in self.VOTE_CHANNEL_DB:
if chan.channel_name == channel:
self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :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))}')
except KeyError as ke:
self.Logs.error(f'Key Error: {ke}')
case 'vote_verdict':
try:
# channel = str(fullcmd[2]).lower()
for chan in self.VOTE_CHANNEL_DB:
if chan.channel_name == channel:
target_user = self.User.get_nickname(chan.target_user)
if chan.vote_for > chan.vote_against:
self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :The user {self.Config.CONFIG_COLOR["gras"]}{target_user}{self.Config.CONFIG_COLOR["nogc"]} will be kicked from this channel')
self.Irc.send2socket(f":{dnickname} KICK {channel} {target_user} Following the vote, you are not welcome in {channel}")
elif chan.vote_for <= chan.vote_against:
self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :This user will stay on this channel')
# Init the system
if self.init_vote_system(channel):
self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :System vote re initiated')
except KeyError as ke:
self.Logs.error(f'Key Error: {ke}')
case 'submit':
# submit nickname
try:
nickname_submitted = cmd[1]
# channel = str(fullcmd[2]).lower()
uid_submitted = self.User.get_uid(nickname_submitted)
user_submitted = self.User.get_User(nickname_submitted)
# check if there is an ongoing vote
if self.is_vote_ongoing(channel):
for vote in self.VOTE_CHANNEL_DB:
if vote.channel_name == channel:
ongoing_user = self.User.get_nickname(vote.target_user)
self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :There is an ongoing vote on {ongoing_user}')
return False
# check if the user exist
if user_submitted is None:
self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :This nickname <{nickname_submitted}> do not exist')
return False
uid_cleaned = self.Base.clean_uid(uid_submitted)
ChannelInfo = self.Channel.get_Channel(channel)
clean_uids_in_channel: list = []
for uid in ChannelInfo.uids:
clean_uids_in_channel.append(self.Base.clean_uid(uid))
if not uid_cleaned in clean_uids_in_channel:
self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :This nickname <{nickname_submitted}> is not available in this channel')
return False
# check if Ircop or Service or Bot
pattern = fr'[o|B|S]'
operator_user = re.findall(pattern, user_submitted.umodes)
if operator_user:
self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :You cant vote for this user ! he/she is protected')
return False
for chan in self.VOTE_CHANNEL_DB:
if chan.channel_name == channel:
chan.target_user = self.User.get_uid(nickname_submitted)
self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :{nickname_submitted} has been targeted for a vote')
self.Base.create_timer(60, self.timer_vote_verdict, (channel, ))
self.Irc.send2socket(f':{dnickname} PRIVMSG {channel} :This vote will end after 60 secondes')
except KeyError as ke:
self.Logs.error(f'Key Error: {ke}')
except TypeError as te:
self.Logs.error(te)
case 'activate':
try:
# activate #channel
# channel = str(cmd[1]).lower()
self.insert_vote_channel(
self.VoteChannelModel(
channel_name=channel,
target_user='',
voter_users=[],
vote_for=0,
vote_against=0
)
)
self.Irc.send2socket(f":{dnickname} JOIN {channel}")
self.Irc.send2socket(f":{dnickname} SAMODE {channel} +o {dnickname}")
self.Irc.send2socket(f":{dnickname} PRIVMSG {channel} :You can now use !submit <nickname> to decide if he will stay or not on this channel ")
except KeyError as ke:
self.Logs.error(f"Key Error : {ke}")
case 'deactivate':
try:
# deactivate #channel
# channel = str(cmd[1]).lower()
self.Irc.send2socket(f":{dnickname} SAMODE {channel} -o {dnickname}")
self.Irc.send2socket(f":{dnickname} PART {channel}")
for chan in self.VOTE_CHANNEL_DB:
if chan.channel_name == channel:
self.VOTE_CHANNEL_DB.remove(chan)
self.db_delete_vote_channel(chan.channel_name)
self.Logs.debug(f"Test logs ready")
except KeyError as ke:
self.Logs.error(f"Key Error : {ke}")

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

@@ -0,0 +1,157 @@
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.Loader.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}")

View File

@@ -0,0 +1,438 @@
"""
File : mod_votekick.py
Version : 1.0.0
Description : Manages votekick sessions for multiple channels.
Handles activation, ongoing vote checks, and cleanup.
Author : adator
Created : 2025-08-16
Last Updated: 2025-08-16
-----------------------------------------
"""
import re
import mods.votekick.schemas as schemas
import mods.votekick.utils as utils
from mods.votekick.votekick_manager import VotekickManager
import mods.votekick.threads as thds
from typing import TYPE_CHECKING, Any, Optional
if TYPE_CHECKING:
from core.irc import Irc
class Votekick:
def __init__(self, uplink: 'Irc') -> None:
# Module name (Mandatory)
self.module_name = 'mod_' + str(self.__class__.__name__).lower()
# Add Irc Object to the module
self.Irc = uplink
# Add Loader Object to the module (Mandatory)
self.Loader = uplink.Loader
# Add server protocol Object to the module (Mandatory)
self.Protocol = uplink.Protocol
# Add Global Configuration to the module
self.Config = uplink.Config
# Add Base object to the module
self.Base = uplink.Base
# Add logs object to the module
self.Logs = uplink.Logs
# Add User object to the module
self.User = uplink.User
# Add Channel object to the module
self.Channel = uplink.Channel
# Add Utils.
self.Utils = uplink.Utils
# Add Utils module
self.ModUtils = utils
# Add Schemas module
self.Schemas = schemas
# Add Threads module
self.Threads = thds
# Add VoteKick Manager
self.VoteKickManager = VotekickManager(self)
metadata = uplink.Loader.Settings.get_cache('VOTEKICK')
if metadata is not None:
self.VoteKickManager.VOTE_CHANNEL_DB = metadata
# self.VOTE_CHANNEL_DB = metadata
# Créer les nouvelles commandes du module
self.Irc.build_command(1, self.module_name, 'vote', 'The kick vote module')
# 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.ModUtils.join_saved_channels(self)
return None
def __create_tables(self) -> None:
"""Methode qui va créer la base de donnée si elle n'existe pas.
Une Session unique pour cette classe sera crée, qui sera utilisé dans cette classe / module
Returns:
None: Aucun retour n'es attendu
"""
table_logs = '''CREATE TABLE IF NOT EXISTS votekick_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
datetime TEXT,
server_msg TEXT
)
'''
table_vote = '''CREATE TABLE IF NOT EXISTS votekick_channel (
id INTEGER PRIMARY KEY AUTOINCREMENT,
datetime TEXT,
channel TEXT
)
'''
self.Base.db_execute_query(table_logs)
self.Base.db_execute_query(table_vote)
return None
def unload(self) -> None:
try:
# Cache the local DB with current votes.
self.Loader.Settings.set_cache('VOTEKICK', self.VoteKickManager.VOTE_CHANNEL_DB)
for chan in self.VoteKickManager.VOTE_CHANNEL_DB:
self.Protocol.send_part_chan(uidornickname=self.Config.SERVICE_ID, channel=chan.channel_name)
self.VoteKickManager.VOTE_CHANNEL_DB = []
self.Logs.debug(f'Delete memory DB VOTE_CHANNEL_DB: {self.VoteKickManager.VOTE_CHANNEL_DB}')
return None
except UnboundLocalError as ne:
self.Logs.error(f'{ne}')
except NameError as ue:
self.Logs.error(f'{ue}')
except Exception as err:
self.Logs.error(f'General Error: {err}')
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:
match command:
case 'PRIVMSG':
return None
case 'QUIT':
return None
case _:
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: Optional[list] = None) -> None:
# cmd is the command starting from the user command
# full cmd is sending the entire server response
command = str(cmd[0]).lower()
fullcmd = fullcmd
dnickname = self.Config.SERVICE_NICKNAME
fromuser = user
fromchannel = channel
match command:
case 'vote':
if len(cmd) == 1:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote activate #channel')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote deactivate #channel')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote +')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote -')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote cancel')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote status')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote submit nickname')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote verdict')
return None
option = str(cmd[1]).lower()
match option:
case 'activate':
try:
# vote activate #channel
if self.Admin.get_admin(fromuser) is None:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' :Your are not allowed to execute this command')
return None
sentchannel = str(cmd[2]).lower() if self.Channel.is_valid_channel(str(cmd[2]).lower()) else None
if sentchannel is None:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f" The correct command is {self.Config.SERVICE_PREFIX}{command} {option} #CHANNEL")
if self.VoteKickManager.activate_new_channel(sentchannel):
self.Channel.db_query_channel('add', self.module_name, sentchannel)
self.Protocol.send_join_chan(uidornickname=dnickname, channel=sentchannel)
self.Protocol.send2socket(f":{dnickname} SAMODE {sentchannel} +o {dnickname}")
self.Protocol.send_priv_msg(nick_from=dnickname,
msg="You can now use !submit <nickname> to decide if he will stay or not on this channel ",
channel=sentchannel
)
return None
except Exception as err:
self.Logs.error(f'{err}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} {command} {option} #channel')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' Exemple /msg {dnickname} {command} {option} #welcome')
case 'deactivate':
try:
# vote deactivate #channel
if self.Admin.get_admin(fromuser) is None:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f" Your are not allowed to execute this command")
return None
sentchannel = str(cmd[2]).lower() if self.Channel.is_valid_channel(str(cmd[2]).lower()) else None
if sentchannel is None:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f" The correct command is {self.Config.SERVICE_PREFIX}{command} {option} #CHANNEL")
self.Protocol.send2socket(f":{dnickname} SAMODE {sentchannel} -o {dnickname}")
self.Protocol.send_part_chan(uidornickname=dnickname, channel=sentchannel)
if self.VoteKickManager.drop_vote_channel_model(sentchannel):
self.Channel.db_query_channel('del', self.module_name, sentchannel)
return None
except Exception as err:
self.Logs.error(f'{err}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f" /msg {dnickname} {command} {option} #channel")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f" Exemple /msg {dnickname} {command} {option} #welcome")
case '+':
try:
# vote +
channel = fromchannel
if self.VoteKickManager.action_vote(channel, fromuser, '+'):
self.Protocol.send_priv_msg(nick_from=dnickname, msg="Vote recorded, thank you",channel=channel)
else:
self.Protocol.send_priv_msg(nick_from=dnickname, msg="You already submitted a vote", channel=channel)
except Exception as err:
self.Logs.error(f'{err}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} {command} {option}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' Exemple /msg {dnickname} {command} {option}')
case '-':
try:
# vote -
channel = fromchannel
if self.VoteKickManager.action_vote(channel, fromuser, '-'):
self.Protocol.send_priv_msg(nick_from=dnickname, msg="Vote recorded, thank you",channel=channel)
else:
self.Protocol.send_priv_msg(nick_from=dnickname, msg="You already submitted a vote", channel=channel)
except Exception as err:
self.Logs.error(f'{err}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} {command} {option}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' Exemple /msg {dnickname} {command} {option}')
case 'cancel':
try:
# vote cancel
if self.Admin.get_admin(fromuser) is None:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' Your are not allowed to execute this command')
return None
if channel is None:
self.Logs.error(f"The channel is not known, defender can't cancel the vote")
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' You need to specify the channel => /msg {dnickname} vote_cancel #channel')
for vote in self.VoteKickManager.VOTE_CHANNEL_DB:
if vote.channel_name == channel:
if self.VoteKickManager.init_vote_system(channel):
self.Protocol.send_priv_msg(nick_from=dnickname,
msg="Vote system re-initiated",
channel=channel
)
except Exception as err:
self.Logs.error(f'{err}')
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' Exemple /msg {dnickname} {command} {option}')
case 'status':
try:
# vote status
for chan in self.VoteKickManager.VOTE_CHANNEL_DB:
if chan.channel_name == channel:
self.Protocol.send_priv_msg(nick_from=dnickname,
msg=f"Channel: {chan.channel_name} | Target: {self.User.get_nickname(chan.target_user)} | For: {chan.vote_for} | Against: {chan.vote_against} | Number of voters: {str(len(chan.voter_users))}",
channel=channel
)
except Exception as err:
self.Logs.error(f'{err}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} {command} {option}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' Exemple /msg {dnickname} {command} {option}')
case 'submit':
try:
# vote submit nickname
if self.Admin.get_admin(fromuser) is None:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' Your are not allowed to execute this command')
return None
nickname_submitted = cmd[2]
uid_submitted = self.User.get_uid(nickname_submitted)
user_submitted = self.User.get_user(nickname_submitted)
ongoing_user = None
# check if there is an ongoing vote
if self.VoteKickManager.is_vote_ongoing(channel):
votec = self.VoteKickManager.get_vote_channel_model(channel)
if votec:
ongoing_user = self.User.get_nickname(votec.target_user)
self.Protocol.send_priv_msg(nick_from=dnickname,
msg=f"There is an ongoing vote on {ongoing_user}",
channel=channel
)
return None
# check if the user exist
if user_submitted is None:
self.Protocol.send_priv_msg(nick_from=dnickname,
msg=f"This nickname <{nickname_submitted}> do not exist",
channel=channel
)
return None
uid_cleaned = self.Loader.Utils.clean_uid(uid_submitted)
channel_obj = self.Channel.get_channel(channel)
if channel_obj is None:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' This channel [{channel}] do not exist in the Channel Object')
return None
clean_uids_in_channel: list = []
for uid in channel_obj.uids:
clean_uids_in_channel.append(self.Loader.Utils.clean_uid(uid))
if not uid_cleaned in clean_uids_in_channel:
self.Protocol.send_priv_msg(nick_from=dnickname,
msg=f"This nickname <{nickname_submitted}> is not available in this channel",
channel=channel
)
return None
# check if Ircop or Service or Bot
pattern = fr'[o|B|S]'
operator_user = re.findall(pattern, user_submitted.umodes)
if operator_user:
self.Protocol.send_priv_msg(nick_from=dnickname,
msg="You cant vote for this user ! he/she is protected",
channel=channel
)
return None
for chan in self.VoteKickManager.VOTE_CHANNEL_DB:
if chan.channel_name == channel:
chan.target_user = self.User.get_uid(nickname_submitted)
self.Protocol.send_priv_msg(nick_from=dnickname,
msg=f"{nickname_submitted} has been targeted for a vote",
channel=channel
)
self.Base.create_timer(60, self.Threads.timer_vote_verdict, (self, channel))
self.Protocol.send_priv_msg(nick_from=dnickname,
msg="This vote will end after 60 secondes",
channel=channel
)
except Exception as err:
self.Logs.error(f'{err}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} {command} {option} nickname')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' Exemple /msg {dnickname} {command} {option} adator')
case 'verdict':
try:
# vote verdict
if self.Admin.get_admin(fromuser) is None:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f'Your are not allowed to execute this command')
return None
votec = self.VoteKickManager.get_vote_channel_model(channel)
if votec:
target_user = self.User.get_nickname(votec.target_user)
if votec.vote_for >= votec.vote_against:
self.Protocol.send_priv_msg(nick_from=dnickname,
msg=f"User {self.Config.COLORS.bold}{target_user}{self.Config.COLORS.nogc} has {votec.vote_against} votes against and {votec.vote_for} votes for. For this reason, it\'ll be kicked from the channel",
channel=channel
)
self.Protocol.send2socket(f":{dnickname} KICK {channel} {target_user} Following the vote, you are not welcome in {channel}")
else:
self.Protocol.send_priv_msg(
nick_from=dnickname,
msg=f"User {self.Config.COLORS.bold}{target_user}{self.Config.COLORS.nogc} has {votec.vote_against} votes against and {votec.vote_for} votes for. For this reason, it\'ll remain in the channel",
channel=channel
)
if self.VoteKickManager.init_vote_system(channel):
self.Protocol.send_priv_msg(
nick_from=dnickname,
msg="System vote re initiated",
channel=channel
)
return None
except Exception as err:
self.Logs.error(f'{err}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} {command} {option}')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' Exemple /msg {dnickname} {command} {option}')
case _:
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote activate #channel')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote deactivate #channel')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote +')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote -')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote cancel')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote status')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote submit nickname')
self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser,msg=f' /msg {dnickname} vote verdict')
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.Utils
def activate_new_channel(self, channel_name: str) -> bool:
"""Activate a new channel in the votekick systeme
Args:
channel_name (str): The channel name you want to activate
Returns:
bool: True if it was activated
"""
votec = self.get_vote_channel_model(channel_name)
if votec is None:
self.VOTE_CHANNEL_DB.append(
VoteChannelModel(
channel_name=channel_name,
target_user='',
voter_users=[],
vote_for=0,
vote_against=0
)
)
self.Logs.debug(f"[VOTEKICK MANAGER] {channel_name} has been activated.")
return True
return False
def init_vote_system(self, channel_name: str) -> bool:
"""Initializes or resets the votekick system for a given channel.
This method clears the current target, voter list, and vote counts
in preparation for a new votekick session.
Args:
channel_name (str): The name of the channel for which the votekick system should be initialized.
Returns:
bool: True if the votekick system was successfully initialized, False if the channel is not found.
"""
votec = self.get_vote_channel_model(channel_name)
if votec is None:
self.Logs.debug(f"[VOTEKICK MANAGER] The channel ({channel_name}) is not active!")
return False
votec.target_user = ''
votec.voter_users = []
votec.vote_for = 0
votec.vote_against = 0
self.Logs.debug(f"[VOTEKICK MANAGER] The channel ({channel_name}) has been successfully initialized!")
return True
def get_vote_channel_model(self, channel_name: str) -> Optional[VoteChannelModel]:
"""Get Vote Channel Object model
Args:
channel_name (str): The channel name you want to activate
Returns:
(VoteChannelModel | None): The VoteChannelModel if exist
"""
for vote in self.VOTE_CHANNEL_DB:
if vote.channel_name.lower() == channel_name.lower():
self.Logs.debug(f"[VOTEKICK MANAGER] {channel_name} has been found in the VOTE_CHANNEL_DB")
return vote
return None
def drop_vote_channel_model(self, channel_name: str) -> bool:
"""Drop a channel from the votekick system.
Args:
channel_name (str): The channel name you want to drop
Returns:
bool: True if the channel has been droped.
"""
votec = self.get_vote_channel_model(channel_name)
if votec:
self.VOTE_CHANNEL_DB.remove(votec)
self.Logs.debug(f"[VOTEKICK MANAGER] {channel_name} has been removed from the VOTE_CHANNEL_DB")
return True
return False
def is_vote_ongoing(self, channel_name: str) -> bool:
"""Check if there is an angoing vote on the channel provided
Args:
channel_name (str): The channel name to check
Returns:
bool: True if there is an ongoing vote on the channel provided.
"""
votec = self.get_vote_channel_model(channel_name)
if votec is None:
self.Logs.debug(f"[VOTEKICK MANAGER] {channel_name} is not activated!")
return False
if votec.target_user:
self.Logs.debug(f'[VOTEKICK MANAGER] A vote is ongoing on {channel_name}')
return True
self.Logs.debug(f'[VOTEKICK MANAGER] {channel_name} is activated but there is no ongoing vote!')
return False
def action_vote(self, channel_name: str, nickname: str, action: Literal['+', '-']) -> bool:
"""
Registers a vote (for or against) in an active votekick session on a channel.
Args:
channel_name (str): The name of the channel where the votekick session is active.
nickname (str): The nickname of the user casting the vote.
action (Literal['+', '-']): The vote action. Use '+' to vote for kicking, '-' to vote against.
Returns:
bool: True if the vote was successfully registered, False otherwise.
This can fail if:
- The action is invalid (not '+' or '-')
- The user has already voted
- The channel has no active votekick session
"""
if action not in ['+', '-']:
self.Logs.debug(f"[VOTEKICK MANAGER] The action must be + or - while you have provided ({action})")
return False
votec = self.get_vote_channel_model(channel_name)
if votec:
client_obj = self.uplink.User.get_user(votec.target_user)
client_to_punish = votec.target_user if client_obj is None else client_obj.nickname
if nickname in votec.voter_users:
self.Logs.debug(f"[VOTEKICK MANAGER] This nickname ({nickname}) has already voted for ({client_to_punish})")
return False
else:
if action == '+':
votec.vote_for += 1
elif action == '-':
votec.vote_against += 1
votec.voter_users.append(nickname)
self.Logs.debug(f"[VOTEKICK MANAGER] The ({nickname}) has voted to ban ({client_to_punish})")
return True
else:
self.Logs.debug(f"[VOTEKICK MANAGER] This channel {channel_name} is not active!")
return False

View File

@@ -1,3 +1,9 @@
{
"version": "5.0.7"
"version": "6.2.2",
"requests": "2.32.3",
"psutil": "6.0.0",
"unrealircd_rpc_py": "2.0.5",
"sqlalchemy": "2.0.35",
"faker": "30.1.0"
}