38 Commits

Author SHA1 Message Date
adator
c404cc3234 Merge pull request #53 from adator85/dev
Dev
2024-10-02 00:06:54 +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
f7b49c151f Merge pull request #51 from adator85/dev
V5.3.4
2024-09-29 22:43:28 +02:00
adator
1ee9b7e3ff Merge pull request #50 from adator85/dev
Dev
2024-09-26 01:00:39 +02:00
adator
4d0087623c Merge pull request #49 from adator85/dev
unsubscribe before unload
2024-09-22 23:54:22 +02:00
adator
dc20f5ec3c Merge pull request #48 from adator85/dev
Dev
2024-09-22 23:20:37 +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
857cbfc85d Merge pull request #46 from adator85/dev
Send the ready msg to channel log
2024-09-22 16:33:49 +02:00
adator
3518589e9c Merge pull request #45 from adator85/dev
Fix ConfModel error
2024-09-22 16:31:35 +02:00
adator
e14c97de03 Merge pull request #44 from adator85/dev
V5.3.0
2024-09-22 16:20:45 +02:00
adator
69360be3ad Merge pull request #43 from adator85/dev
Update info command
2024-09-21 20:28:29 +02:00
adator
bfa90c6bd5 Merge pull request #42 from adator85/dev
V5.2.9
2024-09-21 20:22:42 +02:00
adator
5c8378a0e7 Merge pull request #41 from adator85/dev
V5.2.9
2024-09-21 16:43:14 +02:00
adator
e3b212ea88 Merge pull request #40 from adator85/dev
finetune clone connection
2024-09-20 23:13:33 +02:00
adator
0c2a350d38 Merge pull request #39 from adator85/dev
Dev
2024-09-20 21:12:59 +02:00
adator
1cea8d0601 Merge pull request #38 from adator85/dev
V5.2.1
2024-09-15 03:09:22 +02:00
adator
652b400d5e Merge pull request #37 from adator85/dev
update mode clone
2024-09-15 02:50:27 +02:00
adator
2f8b965b59 Merge pull request #36 from adator85/dev
Dev
2024-09-15 02:04:32 +02:00
adator
3c043cefd8 Merge pull request #35 from adator85/dev
V5.1.8
2024-09-08 00:42:57 +02:00
adator
59a75cecd8 Merge pull request #34 from adator85/dev
V5.1.7
2024-09-03 00:21:32 +02:00
adator
71053437a7 Merge pull request #33 from adator85/dev
V5.1.6
2024-09-01 22:15:54 +02:00
adator
7796d05206 Merge pull request #32 from adator85/dev
Update vote kick commands
2024-09-01 18:55:57 +02:00
adator
5f2567f9e5 Merge pull request #31 from adator85/dev
mod_command update
2024-09-01 17:30:05 +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
a02f2f9a26 Merge pull request #29 from adator85/dev
update mod_clone module
2024-09-01 15:54:50 +02:00
adator
d73adb6f0b Merge pull request #28 from adator85/dev
update readme
2024-09-01 15:35:59 +02:00
adator
b812e64992 Merge pull request #27 from adator85/dev
Dev
2024-09-01 15:24:18 +02:00
adator
9bd1f68df2 Merge pull request #26 from adator85/dev
Dev
2024-09-01 14:59:38 +02:00
adator
f44b08bf36 Merge pull request #25 from adator85/dev
fix Installation
2024-08-29 01:36:38 +02:00
adator
1a19e1613a Merge pull request #24 from adator85/dev
Fix Bug installation
2024-08-29 01:31:19 +02:00
adator
cdc15b7b47 Merge pull request #23 from adator85/dev
Dev
2024-08-29 01:16:55 +02:00
adator
31fe9f62ec Merge pull request #22 from adator85/dev
Dev
2024-08-24 01:39:11 +02:00
adator
f0853e3afb Merge pull request #21 from adator85/dev
New Installation file created for unix system
2024-08-22 01:02:00 +02:00
adator
6dade09257 Merge pull request #20 from adator85/dev
README Update
2024-08-21 00:50:31 +02:00
adator
9533b010b2 Merge pull request #19 from adator85/dev
V5.0.4 - Delete a user when a user has been kicked
2024-08-20 02:24:37 +02:00
adator
824db73590 Merge pull request #18 from adator85/dev
Delete channel mode information
2024-08-20 02:14:31 +02:00
adator
96bf4b6f80 Merge pull request #17 from adator85/dev
Fix channel update
2024-08-20 02:08:09 +02:00
adator
922336363e Merge pull request #16 from adator85/dev
Dev
2024-08-20 01:56:04 +02:00
6 changed files with 78 additions and 234 deletions

View File

@@ -48,6 +48,7 @@ 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']
return None
@@ -80,14 +81,6 @@ 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')
@@ -236,19 +229,6 @@ class Base:
return False
def db_update_module(self, user_cmd: str, module_name: str) -> None:
"""Modifie la date et le user qui a rechargé le module
Args:
user_cmd (str): le user qui a rechargé le module
module_name (str): le module a rechargé
"""
update_cmd_query = f"UPDATE {self.Config.table_module} SET datetime = :datetime, user = :user WHERE module_name = :module_name"
mes_donnees = {'datetime': self.get_datetime(), 'user': user_cmd, 'module_name': module_name}
self.db_execute_query(update_cmd_query, mes_donnees)
return False
def db_delete_module(self, module_name:str) -> None:
"""Supprime les modules de la base de données
@@ -611,7 +591,7 @@ class Base:
)
'''
table_core_channel = f'''CREATE TABLE IF NOT EXISTS {self.Config.table_channel} (
table_core_channel = '''CREATE TABLE IF NOT EXISTS core_channel (
id INTEGER PRIMARY KEY AUTOINCREMENT,
datetime TEXT,
module_name TEXT,

View File

@@ -1,8 +1,7 @@
import os
import json
from sys import exit, prefix
from sys import exit
from dataclasses import dataclass
from subprocess import check_call, run, CalledProcessError, PIPE, check_output
from subprocess import check_call, run, CalledProcessError, PIPE
from platform import python_version, python_version_tuple
class Install:
@@ -25,23 +24,13 @@ class Install:
venv_pip_executable: str
venv_python_executable: str
@dataclass
class Package:
name: str = None
version: str = None
DB_PACKAGES: list[Package] = []
def __init__(self) -> None:
self.set_configuration()
if self.skip_install:
self.check_packages_version()
return None
self.check_packages_version()
# Sinon tester les dependances python et les installer avec pip
if self.do_install():
@@ -86,13 +75,11 @@ class Install:
if not os.path.exists(os.path.join(self.config.defender_install_folder, 'core', 'configuration.json')):
# If configuration file do not exist
exit("/!\\ Configuration file (core/configuration.json) doesn't exist! please create it /!\\")
exit("/!\\ Configuration file (configuration.json) doesn't exist /!\\")
# 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'
#print('/!\\ Skip installation /!\\')
self.skip_install = True
return False
@@ -104,7 +91,7 @@ class Install:
def is_root(self) -> bool:
if os.geteuid() != 0:
print('> User without privileges ==> OK')
print('User without privileges ==> PASS')
return False
elif os.geteuid() == 0:
print('/!\\ Do not use root to install Defender /!\\')
@@ -136,75 +123,6 @@ class Install:
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

View File

@@ -95,15 +95,13 @@ class Irc:
return None
except ssl.SSLEOFError as soe:
self.Base.logs.critical(f"SSLEOFError: {soe} - {soc.fileno()}")
self.Base.logs.critical(f"SSLEOFError __create_socket: {soe} - {soc.fileno()}")
except ssl.SSLError as se:
self.Base.logs.critical(f"SSLError: {se} - {soc.fileno()}")
self.Base.logs.critical(f"SSLError __create_socket: {se} - {soc.fileno()}")
except OSError as oe:
self.Base.logs.critical(f"OSError: {oe} - {soc.fileno()}")
if 'connection refused' in str(oe).lower():
sys.exit(oe)
self.Base.logs.critical(f"OSError __create_socket: {oe} - {soc.fileno()}")
except AttributeError as ae:
self.Base.logs.critical(f"AttributeError: {ae} - {soc.fileno()}")
self.Base.logs.critical(f"AttributeError __create_socket: {ae} - {soc.fileno()}")
def __ssl_context(self) -> ssl.SSLContext:
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
@@ -313,7 +311,7 @@ class Irc:
def send_response(self, responses:list[bytes]) -> None:
try:
# print(responses)
# print(data)
for data in responses:
response = data.decode(self.CHARSET[0]).split()
self.cmd(response)
@@ -490,8 +488,7 @@ class Irc:
self.Base.db_record_module(fromuser, module_name)
self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :Module {module_name} chargé")
# self.Base.logs.info(self.loaded_classes)
self.Base.logs.info(f"Module {class_name} has been loaded")
self.Base.logs.info(self.loaded_classes)
return True
except ModuleNotFoundError as moduleNotFound:
@@ -535,56 +532,6 @@ class Irc:
self.Base.logs.error(f"General Error: {err}")
return False
def reload_module(self, from_user: str, mod_name: str) -> bool:
try:
module_name = mod_name.lower() # ==> mod_defender
class_name = module_name.split('_')[1].capitalize() # ==> Defender
if 'mods.' + module_name in sys.modules:
self.Base.logs.info('Unload the module ...')
self.loaded_classes[class_name].unload()
self.Base.logs.info('Module Already Loaded ... reloading the module ...')
the_module = sys.modules['mods.' + module_name]
importlib.reload(the_module)
# Supprimer la class déja instancier
if class_name in self.loaded_classes:
# Supprimer les commandes déclarer dans la classe
for level, command in self.loaded_classes[class_name].commands_level.items():
# Supprimer la commande de la variable commands
for c in self.loaded_classes[class_name].commands_level[level]:
self.commands.remove(c)
self.commands_level[level].remove(c)
del self.loaded_classes[class_name]
my_class = getattr(the_module, class_name, None)
new_instance = my_class(self.ircObject)
self.loaded_classes[class_name] = new_instance
self.Base.db_update_module(from_user, mod_name)
self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :Module {module_name} rechargé")
return False
else:
self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :Module {module_name} n'est pas chargé !")
except TypeError as te:
self.Base.logs.error(f"A TypeError raised: {te}")
self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :A TypeError raised: {te}")
self.Base.db_delete_module(module_name)
except AttributeError as ae:
self.Base.logs.error(f"Missing Attribute: {ae}")
self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :Missing Attribute: {ae}")
self.Base.db_delete_module(module_name)
except KeyError as ke:
self.Base.logs.error(f"Key Error: {ke}")
self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :Key Error: {ke}")
self.Base.db_delete_module(module_name)
except Exception as e:
self.Base.logs.error(f"Something went wrong with a module you want to reload: {e}")
self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :Something went wrong with the module: {e}")
self.Base.db_delete_module(module_name)
def insert_db_admin(self, uid:str, level:int) -> None:
if self.User.get_User(uid) is None:
@@ -1100,7 +1047,7 @@ class Irc:
ping_response = current_unixtime - recieved_unixtime
self.send2socket(f'PONG :{recieved_unixtime}')
self.send2socket(f':{dnickname} NOTICE {user_trigger} :\x01PING {ping_response} secs\x01')
self.send2socket(f':{dnickname} NOTICE {user_trigger} :\x01PING {recieved_unixtime} secs\x01')
return False
if not arg[0].lower() in self.commands:
@@ -1416,14 +1363,8 @@ class Irc:
self.send2socket(f':{dnickname} NOTICE {fromuser} : ***************** FIN DES COMMANDES *****************')
case 'load':
try:
# Load a module ex: .load mod_defender
mod_name = str(cmd[1])
self.load_module(fromuser, mod_name)
except KeyError as ke:
self.Base.logs.error(f"Key Error: {ke} - list recieved: {cmd}")
except Exception as err:
self.Base.logs.error(f"General Error: {ke} - list recieved: {cmd}")
self.load_module(fromuser, str(cmd[1]))
case 'unload':
# unload mod_defender
@@ -1434,10 +1375,50 @@ class Irc:
self.Base.logs.error(f"General Error: {err}")
case 'reload':
# reload mod_defender
# reload mod_dktmb
try:
module_name = str(cmd[1]).lower() # ==> mod_defender
self.reload_module(from_user=fromuser, mod_name=module_name)
module_name = str(cmd[1]).lower() # ==> mod_defender
class_name = module_name.split('_')[1].capitalize() # ==> Defender
if 'mods.' + module_name in sys.modules:
self.Base.logs.info('Unload the module ...')
self.loaded_classes[class_name].unload()
self.Base.logs.info('Module Already Loaded ... reloading the module ...')
the_module = sys.modules['mods.' + module_name]
importlib.reload(the_module)
# Supprimer la class déja instancier
if class_name in self.loaded_classes:
# Supprimer les commandes déclarer dans la classe
for level, command in self.loaded_classes[class_name].commands_level.items():
# Supprimer la commande de la variable commands
for c in self.loaded_classes[class_name].commands_level[level]:
self.commands.remove(c)
self.commands_level[level].remove(c)
del self.loaded_classes[class_name]
my_class = getattr(the_module, class_name, None)
new_instance = my_class(self.ircObject)
self.loaded_classes[class_name] = new_instance
self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :Module {module_name} rechargé")
return False
else:
self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :Module {module_name} n'est pas chargé !")
except TypeError as te:
self.Base.logs.error(f"A TypeError raised: {te}")
self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :A TypeError raised: {te}")
self.Base.db_delete_module(module_name)
except AttributeError as ae:
self.Base.logs.error(f"Missing Attribute: {ae}")
self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :Missing Attribute: {ae}")
self.Base.db_delete_module(module_name)
except KeyError as ke:
self.Base.logs.error(f"Key Error: {ke}")
self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :Key Error: {ke}")
self.Base.db_delete_module(module_name)
except Exception as e:
self.Base.logs.error(f"Something went wrong with a module you want to reload: {e}")
self.send2socket(f":{self.Config.SERVICE_NICKNAME} PRIVMSG {self.Config.SERVICE_CHANLOG} :Something went wrong with the module: {e}")
@@ -1488,24 +1469,24 @@ class Irc:
self.Base.logs.debug(self.loaded_classes)
all_modules = self.Base.get_all_modules()
loaded = False
results = self.Base.db_execute_query(f'SELECT datetime, user, module_name FROM {self.Config.table_module}')
results = self.Base.db_execute_query(f'SELECT module_name FROM {self.Config.table_module}')
results = results.fetchall()
found = False
for module in all_modules:
for loaded_mod in results:
if module == loaded_mod[2]:
loaded_datetime = loaded_mod[0]
loaded_user = loaded_mod[1]
loaded = True
if module == loaded_mod[0]:
found = True
if loaded:
self.send2socket(f":{dnickname} NOTICE {fromuser} :{module} - {self.Config.COLORS.green}Loaded{self.Config.COLORS.nogc} by {loaded_user} on {loaded_datetime}")
loaded = False
if found:
self.send2socket(f":{dnickname} NOTICE {fromuser} :{module} - {self.Config.COLORS.green}Loaded{self.Config.COLORS.nogc}")
else:
self.send2socket(f":{dnickname} NOTICE {fromuser} :{module} - {self.Config.COLORS.red}Not Loaded{self.Config.COLORS.nogc}")
found = False
case 'show_timers':
if self.Base.running_timers:

View File

@@ -150,7 +150,6 @@ class Command():
try:
message = ' '.join(cmd[3:])
self.Irc.send2socket(f":{dnickname} NOTICE {self.user_to_notice} :[{red}ERROR MSG{nogc}] {message}")
self.Base.logs.error(f"{cmd[1]} - {message}")
except KeyError as ke:
self.Base.logs.error(ke)
except Exception as err:
@@ -213,7 +212,6 @@ class Command():
dnickname = self.Config.SERVICE_NICKNAME
service_id = self.Config.SERVICE_ID
dchanlog = self.Config.SERVICE_CHANLOG
self.user_to_notice = user
fromuser = user
fromchannel = channel

View File

@@ -10,7 +10,8 @@ class Jsonrpc():
class ModConfModel:
"""The Model containing the module parameters
"""
jsonrpc: int = 0
param_exemple1: str
param_exemple2: int
def __init__(self, ircInstance:Irc) -> None:
@@ -58,22 +59,10 @@ class Jsonrpc():
self.__load_module_configuration()
# End of mandatory methods you can start your customization #
# self.UnrealIrcdRpcLive: Live = Live(
# req_method='unixsocket',
# path_to_socket_file=self.Config.JSONRPC_PATH_TO_SOCKET_FILE,
# callback_object_instance=self,
# callback_method_name='callback_sent_to_irc'
# )
self.UnrealIrcdRpcLive: Live = Live(
req_method='websocket',
url=self.Config.JSONRPC_URL,
username=self.Config.JSONRPC_USER,
password=self.Config.JSONRPC_PASSWORD,
callback_object_instance=self,
callback_method_name='callback_sent_to_irc',
debug_level=10
)
self.UnrealIrcdRpcLive: Live = Live(path_to_socket_file=self.Config.JSONRPC_PATH_TO_SOCKET_FILE,
callback_object_instance=self,
callback_method_name='callback_sent_to_irc'
)
self.Rpc: Loader = Loader(
req_method=self.Config.JSONRPC_METHOD,
@@ -90,9 +79,6 @@ class Jsonrpc():
if self.UnrealIrcdRpcLive.Error.code != 0:
self.Irc.sendPrivMsg(f"[{self.Config.COLORS.red}ERROR{self.Config.COLORS.nogc}] {self.UnrealIrcdRpcLive.Error.message}", self.Config.SERVICE_CHANLOG)
if self.ModConfig.jsonrpc == 1:
self.Base.create_thread(self.thread_start_jsonrpc, run_once=True)
return None
def __set_commands(self, commands:dict[int, list[str]]) -> None:
@@ -165,10 +151,10 @@ class Jsonrpc():
"""
try:
# Build the default configuration model (Mandatory)
self.ModConfig = self.ModConfModel(jsonrpc=0)
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)
#self.Base.db_sync_core_config(self.module_name, self.ModConfig)
return None
@@ -197,7 +183,6 @@ class Jsonrpc():
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
@@ -214,19 +199,10 @@ class Jsonrpc():
match option:
case 'on':
for thread in self.Base.running_threads:
if thread.getName() == 'thread_start_jsonrpc':
if thread.is_alive():
self.Irc.sendPrivMsg(f"Thread {thread.getName()} is running", dchannel)
else:
self.Irc.sendPrivMsg(f"Thread {thread.getName()} is not running, wait untill the process will be cleaned up", dchannel)
self.Base.create_thread(self.thread_start_jsonrpc, run_once=True)
self.__update_configuration('jsonrpc', 1)
case 'off':
self.UnrealIrcdRpcLive.unsubscribe()
self.__update_configuration('jsonrpc', 0)
except IndexError as ie:
self.Logs.error(ie)
@@ -276,9 +252,6 @@ class Jsonrpc():
self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} :CLIENT PORT : {UserInfo.client_port}')
self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} :SERVER PORT : {UserInfo.server_port}')
self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} :CERTFP : {UserInfo.tls.certfp}')
self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} :CIPHER : {UserInfo.tls.cipher}')
self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} :IDLE SINCE : {UserInfo.idle_since}')
self.Irc.send2socket(f':{dnickname} NOTICE {fromuser} :CONNECTED SINCE : {UserInfo.connected_since}')

View File

@@ -1,9 +1,3 @@
{
"version": "5.4.0",
"requests": "2.32.3",
"psutil": "6.0.0",
"unrealircd_rpc_py": "1.0.6",
"sqlalchemy": "2.0.35",
"faker": "30.1.0"
"version": "5.3.5"
}