Added libs
This commit is contained in:
4
app/BKP/.gitignore
vendored
Normal file
4
app/BKP/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
__pycache__
|
||||
.venv
|
||||
*.log
|
||||
app.zip
|
||||
75
app/BKP/config.json
Normal file
75
app/BKP/config.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"app":{
|
||||
"id":"app",
|
||||
"checkUpdates": true,
|
||||
"repositories":["http://error/data"],
|
||||
"version": 0
|
||||
},
|
||||
"main":{
|
||||
"terminalWindow": true
|
||||
},
|
||||
"passwordManager":{
|
||||
"package": "lockbox",
|
||||
"client": "lockboxClient"
|
||||
},
|
||||
"updater":{
|
||||
"autoUpdate": true,
|
||||
"checkUpdates": true,
|
||||
"repositories":[
|
||||
"http://n0sys.duckdns.org:40404/libs"
|
||||
]
|
||||
},
|
||||
"packages":[
|
||||
{
|
||||
"id":"fspn",
|
||||
"checkUpdates": true,
|
||||
"repositories":["http://error/data"],
|
||||
"version": 0
|
||||
},
|
||||
{
|
||||
"id":"noSys",
|
||||
"checkUpdates": true
|
||||
},
|
||||
{
|
||||
"id":"fileTransfer",
|
||||
"checkUpdates": true
|
||||
},
|
||||
{
|
||||
"id":"api",
|
||||
"checkUpdates": true,
|
||||
"server": {
|
||||
"host": "127.0.0.1",
|
||||
"port": "5050"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id":"vueNoSys",
|
||||
"checkUpdates": true,
|
||||
"server": {
|
||||
"host": "127.0.0.1",
|
||||
"port": "3001"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id":"lockbox",
|
||||
"checkUpdates": true,
|
||||
"servers": {
|
||||
"api": {
|
||||
"port": "5001"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id":"rendezvous",
|
||||
"checkUpdates": true,
|
||||
"server": {
|
||||
"host": "0.0.0.0",
|
||||
"port": 40441
|
||||
}
|
||||
},
|
||||
{
|
||||
"id":"p2post",
|
||||
"checkUpdates": true
|
||||
}
|
||||
]
|
||||
}
|
||||
5
app/BKP/info.json
Normal file
5
app/BKP/info.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"id":"app",
|
||||
"version":0,
|
||||
"modules":[]
|
||||
}
|
||||
70
app/BKP/main.py
Normal file
70
app/BKP/main.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import os, sys
|
||||
import time
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
# TODO NOT SURE IF SYS PATH IS A GOOD PRACTICE
|
||||
root_dir = os.path.normpath(__file__.split("libs")[0])
|
||||
sys.path.append(root_dir)
|
||||
|
||||
from libs.app.utils import Utils, kargs_to_array, get_logger, is_http_running, config_root_logger
|
||||
|
||||
logger = None
|
||||
try:
|
||||
config_root_logger()
|
||||
logger = get_logger()
|
||||
utils = Utils()
|
||||
except Exception:
|
||||
logging.exception("ERROR")
|
||||
|
||||
from libs.app.updater import Updater
|
||||
|
||||
def main():
|
||||
log_ascii_art()
|
||||
logger.debug(f"Readable params: {kargs_to_array()}")
|
||||
|
||||
if not is_noSys_running():
|
||||
updater = Updater(utils)
|
||||
utils.configs.read_packages()
|
||||
|
||||
from libs.noSys.noSysCore import NoSysCore
|
||||
noSys = NoSysCore(updater)
|
||||
else:
|
||||
logger.debug(f"NoSys already running")
|
||||
# TODO open url frontend
|
||||
|
||||
logger.info(f"---------------- Main ended ----------------")
|
||||
|
||||
def is_noSys_running():
|
||||
try:
|
||||
api_config = utils.configs.packages["api"].config
|
||||
api_port = api_config["server"]["port"]
|
||||
api_health_check_url = f"http://localhost:{api_port}/api/health"
|
||||
logger.debug(f"Checking url: {api_health_check_url}")
|
||||
return is_http_running(api_health_check_url)
|
||||
except Exception:
|
||||
logger.exception("ERROR")
|
||||
|
||||
def log_ascii_art():
|
||||
logger.info("""
|
||||
_ __ ____
|
||||
/ |/ /__ / __/_ _____
|
||||
/ / _ \ _\ \/ // (_-<
|
||||
/_/|_/\___/ /___/\_, /___/
|
||||
/___/
|
||||
|
||||
Every man must have freedom, must have the scope to form, test, and act upon his own choices, for any sort of development of his own personality to take place. He must, in short, be free in order that he may be fully human.
|
||||
- Murray Rothbard
|
||||
|
||||
""")
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
time.sleep(30)
|
||||
except Exception:
|
||||
logger.exception("ERROR")
|
||||
for sec in range(30):
|
||||
logger.info(f"Closing in {30-sec}")
|
||||
time.sleep(1)
|
||||
|
||||
59
app/BKP/start.py
Normal file
59
app/BKP/start.py
Normal file
@@ -0,0 +1,59 @@
|
||||
import os, sys
|
||||
import logging
|
||||
import shutil
|
||||
import pathlib
|
||||
import urllib.request
|
||||
import time
|
||||
from pathlib import Path
|
||||
import zipfile
|
||||
|
||||
root_dir = pathlib.Path(__file__).parent.resolve()
|
||||
logger = logging.getLogger("start")
|
||||
|
||||
def start():
|
||||
global logger
|
||||
logger.debug(f"---------------- START ----------------")
|
||||
logger.debug(f"Root path: {root_dir}")
|
||||
|
||||
pathlib.Path(os.path.join(root_dir, "libs")).mkdir(parents=True, exist_ok=True)
|
||||
download_app()
|
||||
from libs.app.utils import Utils, get_logger, create_venv, download_file, kargs_to_array
|
||||
create_venv()
|
||||
args = kargs_to_array()
|
||||
# args.append("updateApp=False") # NEVER SET TRUE
|
||||
# args.append("updateLibs=True")
|
||||
utils = Utils()
|
||||
|
||||
main_file_path = os.path.join(root_dir, "libs","app", "main.py")
|
||||
pid = utils.new_python_process(main_file_path, args)
|
||||
logger.info(f"Main process running. PID {pid} - Args: {args}")
|
||||
logger.info(f"---------------- END ----------------")
|
||||
|
||||
def download_app():
|
||||
url = f"{default_repository}/app/app.zip"
|
||||
path = os.path.join(root_dir, "libs", "app.zip")
|
||||
if(not os.path.exists(path)):
|
||||
logger.debug(f"Downloading from URL: {url} to {path}")
|
||||
urllib.request.urlretrieve(url, path)
|
||||
logger.debug(f"Extracting all from {path}")
|
||||
with zipfile.ZipFile(path, 'r') as zip_ref:
|
||||
zip_ref.extractall(Path(path).parent.absolute())
|
||||
|
||||
|
||||
|
||||
default_repository = "http://n0sys.duckdns.org:30303/libs"
|
||||
|
||||
logger.addHandler(logging.StreamHandler())
|
||||
app_logs_path = os.path.join(root_dir, "logs", "app")
|
||||
shutil.rmtree(os.path.join(root_dir, "logs"), ignore_errors=True)
|
||||
pathlib.Path(app_logs_path).mkdir(parents=True, exist_ok=True)
|
||||
logger.addHandler(logging.FileHandler(os.path.join(app_logs_path,'start.log'), mode="a", encoding="utf-8"))
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
start()
|
||||
except Exception as e:
|
||||
logger.exception("ERROR")
|
||||
time.sleep(30)
|
||||
|
||||
124
app/BKP/updater.py
Normal file
124
app/BKP/updater.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import sys
|
||||
import subprocess
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import os
|
||||
from pathlib import Path
|
||||
import pathlib
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
import zipfile
|
||||
from threading import Thread
|
||||
|
||||
from libs.app.utils import Utils, Package, get_logger, root_dir
|
||||
|
||||
logger = get_logger("updater")
|
||||
|
||||
class Updater():
|
||||
def __init__(self, utils:Utils):
|
||||
self.utils = utils
|
||||
self.libs_path = os.path.join(root_dir, 'libs')
|
||||
self.start()
|
||||
|
||||
def start(self):
|
||||
logger.info(f"---------------- Updater Started ----------------")
|
||||
# Create better validation code
|
||||
if("updateApp" in self.utils.kargs):
|
||||
if(self.utils.kargs["updateApp"]):
|
||||
self.update_app()
|
||||
elif(self.utils.configs.data["updater"]["autoUpdate"]):
|
||||
self.update_app()
|
||||
|
||||
if("restart" in self.utils.flags and self.utils.flags["restart"]):
|
||||
self.utils.kargs["updateApp"] = False
|
||||
self.utils.restart_app()
|
||||
|
||||
if("updateLibs" in self.utils.kargs):
|
||||
if(self.utils.kargs["updateLibs"]):
|
||||
self.update_libs()
|
||||
elif(self.utils.configs.data["updater"]["checkUpdates"]):
|
||||
self.update_libs()
|
||||
|
||||
logger.info(f"---------------- Updater Ended ----------------")
|
||||
|
||||
def update_app(self):
|
||||
latest_version_repository = (self.utils.configs.data["app"]["version"],None)
|
||||
logger.info(f"App current version: {latest_version_repository[0]}")
|
||||
|
||||
logger.info(f"Updating app files")
|
||||
# TODO FIX THIS
|
||||
self.check_package_update(Package(config=self.utils.configs.data["app"]))
|
||||
|
||||
logger.info(f"App updated to version {latest_version_repository[0]}")
|
||||
self.utils.flags["restart"] = True
|
||||
|
||||
def update_libs(self):
|
||||
logger.info(f"Checking packages update")
|
||||
threads:list[Thread] = []
|
||||
for package in self.utils.configs.packages.values():
|
||||
# Default is check updates
|
||||
if(not "checkUpdates" in package.config or package.config["checkUpdates"]):
|
||||
thread = Thread(target=self.check_package_update, args=(package,))
|
||||
thread.start()
|
||||
threads.append(thread)
|
||||
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
def check_package_update(self, package:Package):
|
||||
logger.info(f"Updating package {package.config['id']}")
|
||||
package_id = package.config['id']
|
||||
|
||||
package_path = os.path.join(self.libs_path, package_id)
|
||||
if(package.info):
|
||||
latest_version_repository = (package.info["version"],None)
|
||||
else:
|
||||
logger.info(f"Creating package directory: {package_path}")
|
||||
Path(package_path).mkdir(parents=True, exist_ok=True)
|
||||
latest_version_repository = (-1,None)
|
||||
|
||||
repositories = self.utils.configs.data["updater"]["repositories"][:]
|
||||
if "repositories" in package.config:
|
||||
for repository in package.config["repositories"]:
|
||||
repositories.append(repository)
|
||||
|
||||
for repository in repositories:
|
||||
try:
|
||||
package_url = f"{repository}/{package_id}"
|
||||
info_url = f'{package_url}/info.json'
|
||||
logger.info(f"Reading remote info of {package_id} from repository {repository}")
|
||||
with urllib.request.urlopen(info_url, timeout=2) as data:
|
||||
remote_info = json.load(data)
|
||||
logger.debug(f"{package_id} remote info: {remote_info}")
|
||||
|
||||
# TODO Remove 'or' condition -> "or remote_info["version"]==0"
|
||||
if remote_info["version"] > latest_version_repository[0] or remote_info["version"]==0:
|
||||
latest_version_repository = (remote_info["version"], package_url)
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading remote info of {package_id} from repository {repository}: {e}")
|
||||
|
||||
if latest_version_repository[1]:
|
||||
package_zip = f"{latest_version_repository[1]}/{package_id}.zip"
|
||||
package_local_file = os.path.join(self.libs_path, f"{package_id}.zip")
|
||||
logger.debug(f"Downloading package from {package_zip} to local file {package_local_file}")
|
||||
self.download_package(package_zip, package_local_file)
|
||||
self.pip_install_requirements(package_path)
|
||||
|
||||
|
||||
def download_package(self, url, path):
|
||||
urllib.request.urlretrieve(url, path)
|
||||
logger.info(f"Extracting all from {path}")
|
||||
with zipfile.ZipFile(path, 'r') as zip_ref:
|
||||
zip_ref.extractall(Path(path).parent.absolute())
|
||||
|
||||
def pip_install_requirements(self, package_path):
|
||||
path = os.path.join(package_path, "requirements.txt")
|
||||
if(os.path.exists(path)):
|
||||
logger.info(f"Pip installing requirements in {path}")
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", path])
|
||||
else:
|
||||
logger.info(f"{package_path} no requirements.txt")
|
||||
|
||||
def check_module_requirements(self, package_path):
|
||||
pass
|
||||
256
app/BKP/utils.py
Normal file
256
app/BKP/utils.py
Normal file
@@ -0,0 +1,256 @@
|
||||
import os, sys
|
||||
import uuid
|
||||
import pathlib
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
root_dir = os.path.normpath(__file__.split("libs")[0])
|
||||
node_id = str(uuid.uuid4())
|
||||
|
||||
### LOGGER ###
|
||||
import logging, inspect
|
||||
|
||||
class CustomLoggingFormatter(logging.Formatter):
|
||||
grey = "\x1b[0;37m"
|
||||
green = "\x1b[1;32m"
|
||||
yellow = "\x1b[1;33m"
|
||||
red = "\x1b[1;31m"
|
||||
purple = "\x1b[1;35m"
|
||||
blue = "\x1b[1;34m"
|
||||
light_blue = "\x1b[1;36m"
|
||||
bold_red = "\x1b[31;1m"
|
||||
blink_red = "\x1b[5m\x1b[1;31m"
|
||||
reset = "\x1b[0m"
|
||||
prefix = light_blue + '%(asctime)s' + reset + ' |'
|
||||
colored_level = '%(levelname)-8s'
|
||||
message = '| %(message)s' + ''
|
||||
suffix = purple + ' (%(name)s %(filename)s:%(lineno)d)' + reset
|
||||
|
||||
FORMATS = {
|
||||
logging.DEBUG: prefix + grey + colored_level + reset + message + suffix,
|
||||
logging.INFO: prefix + blue + colored_level + reset + message + suffix,
|
||||
logging.WARNING: prefix + yellow + colored_level + reset + message + suffix,
|
||||
logging.ERROR: prefix + red + colored_level + reset + message + suffix,
|
||||
logging.CRITICAL: prefix + bold_red + colored_level + reset + message + suffix
|
||||
}
|
||||
|
||||
file_format = '%(asctime)s | %(levelname)-8s | %(message)s | %(name)s (%(filename)s:%(lineno)d)'
|
||||
|
||||
def format(self, record):
|
||||
log_fmt = self.FORMATS.get(record.levelno)
|
||||
formatter = logging.Formatter(log_fmt)
|
||||
return formatter.format(record)
|
||||
|
||||
# class FileFilter(logging.Filter):
|
||||
# def filter(self, record):
|
||||
# file_path = os.path.normpath(inspect.stack()[1].filename)
|
||||
# head = os.path.split(file_path)[0]
|
||||
# tail = os.path.split(file_path)[1]
|
||||
# if head == os.path.normpath(root_dir):
|
||||
# name = "app"
|
||||
# else:
|
||||
# after_lib = head.replace(os.path.join(root_dir, "libs"), "")
|
||||
# name = after_lib.split(os.sep)[1]
|
||||
# record.name = name
|
||||
# print(name)
|
||||
# return True
|
||||
|
||||
|
||||
def add_logging_console_handler(logger:logging.Logger, logging_level=logging.DEBUG):
|
||||
sh = logging.StreamHandler()
|
||||
sh.setLevel(logging_level)
|
||||
sh.setFormatter(CustomLoggingFormatter())
|
||||
logger.addHandler(sh)
|
||||
|
||||
def add_logging_file_handler(logger:logging.Logger, directory, file_name, logging_level=logging.DEBUG):
|
||||
file_path = os.path.join(root_dir, "logs", directory, f"{file_name}.log")
|
||||
pathlib.Path(file_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
fh = logging.FileHandler(file_path)
|
||||
fh.setLevel(logging_level)
|
||||
fh.setFormatter(logging.Formatter(CustomLoggingFormatter().file_format))
|
||||
logger.addHandler(fh)
|
||||
|
||||
def config_root_logger():
|
||||
logger = get_logger(name="root")
|
||||
# TODO Get lib that is calling logging
|
||||
# logger.addFilter(FileFilter())
|
||||
|
||||
def get_logger(name=""):
|
||||
file_path = os.path.normpath(inspect.stack()[1].filename)
|
||||
head = os.path.split(file_path)[0]
|
||||
tail = os.path.split(file_path)[1]
|
||||
if head == os.path.normpath(root_dir):
|
||||
lib = "root"
|
||||
else:
|
||||
after_lib = head.replace(os.path.join(root_dir, "libs"), "")
|
||||
lib = after_lib.split(os.sep)[1]
|
||||
|
||||
if name == "root":
|
||||
logger_name = name
|
||||
elif name:
|
||||
logger_name = f"{lib}.{name}"
|
||||
else:
|
||||
logger_name = lib
|
||||
|
||||
file_name = name if name else lib
|
||||
logger = logging.getLogger(logger_name)
|
||||
logger.propagate = False
|
||||
if not logger.hasHandlers():
|
||||
logger.setLevel(logging.DEBUG)
|
||||
add_logging_console_handler(logger=logger, logging_level=logging.DEBUG)
|
||||
add_logging_file_handler(logger=logger, directory=lib, file_name=file_name, logging_level=logging.DEBUG)
|
||||
|
||||
return logger
|
||||
|
||||
logger = get_logger()
|
||||
logger.debug(f"Root dir: {root_dir}")
|
||||
|
||||
### VENV ###
|
||||
import venv
|
||||
|
||||
def create_venv():
|
||||
venv_dir = os.path.join(root_dir, ".venv")
|
||||
if not os.path.exists(venv_dir):
|
||||
logger.debug(f"Creating python venv: {venv_dir}")
|
||||
venv.create(venv_dir, with_pip=True)
|
||||
|
||||
### DOWNLOADS ###
|
||||
import urllib.request
|
||||
|
||||
def download_file(url, path):
|
||||
head = os.path.split(path)[0]
|
||||
if not os.path.exists(head):
|
||||
logger.debug(f"Creating repository {head}")
|
||||
pathlib.Path(head).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logger.debug(f"Downloading from URL: {url} to {path}")
|
||||
urllib.request.urlretrieve(url, path)
|
||||
|
||||
### KARGS ###
|
||||
def read_kargs():
|
||||
kargs = {}
|
||||
params = sys.argv
|
||||
for param in params:
|
||||
if "=" in param:
|
||||
split = param.split("=")
|
||||
key = split[0]
|
||||
value = split[1]
|
||||
|
||||
if value == "true" or value == "True":
|
||||
value = True
|
||||
elif value == "false" or value == "False":
|
||||
value = False
|
||||
|
||||
kargs[key] = value
|
||||
kargs["rootDir"] = root_dir
|
||||
return kargs
|
||||
|
||||
def kargs_to_array(kargs={}):
|
||||
if not kargs:
|
||||
kargs = read_kargs()
|
||||
array = []
|
||||
for key, value in kargs.items():
|
||||
array.append(f"{key}={value}")
|
||||
return array
|
||||
|
||||
### HTTP UTILS ###
|
||||
def is_http_running(url):
|
||||
try:
|
||||
u:urllib.request.URLopener = urllib.request.urlopen(url)
|
||||
u.close()
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
### UTILS ###
|
||||
|
||||
class Utils:
|
||||
def __init__(self):
|
||||
self.kargs = read_kargs()
|
||||
self.configs = Config()
|
||||
self.flags = {}
|
||||
|
||||
# def get_env():
|
||||
# return os.environ.get('NOSYS_ENV', "PROD")
|
||||
|
||||
# def download_file(self, package, file, destination, repository=None):
|
||||
# if not repository:
|
||||
# repository = self.default_repository
|
||||
|
||||
# url = f"{repository}/{package}/{file}"
|
||||
# logger.debug(f"Downloading {file} from URL: {url}")
|
||||
# urllib.request.urlretrieve(url, destination)
|
||||
|
||||
# def download_main_file(self, ignoreIfExists=True):
|
||||
# main_path = os.path.join(self.root_dir, "libs","app", "main.py")
|
||||
# if (not os.path.exists(main_path)) or (os.path.exists(main_path) and not ignoreIfExists):
|
||||
# self.download_file(package="app", file="main.py", destination=main_path)
|
||||
|
||||
def is_terminal_visible(self):
|
||||
try:
|
||||
return self.configs.data["main"]["terminalWindow"]
|
||||
except Exception as e:
|
||||
return True
|
||||
|
||||
# def _create_log_dir(self):
|
||||
# logs_path = os.path.join(self.root_dir, "logs")
|
||||
# if not os.path.exists(logs_path):
|
||||
# logger.debug(f"Creating logs directory: {logs_path}")
|
||||
# pathlib.Path(logs_path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def new_python_process(self, file_path, args):
|
||||
python_executable = "python" if self.is_terminal_visible() else "pythonw"
|
||||
args = [f"{root_dir}/.venv/scripts/{python_executable}", file_path] + args
|
||||
logger.debug(f"Starting a new process: {args}")
|
||||
process = subprocess.Popen(args, cwd=root_dir, creationflags=subprocess.CREATE_NEW_CONSOLE)
|
||||
logger.debug(f"Process PID {process.pid} - {args}")
|
||||
return process.pid
|
||||
|
||||
def restart_app(self):
|
||||
args = [sys.executable, "start.py"] + kargs_to_array(self.kargs)
|
||||
subprocess.Popen(args, cwd=root_dir, creationflags=subprocess.CREATE_NEW_CONSOLE)
|
||||
logger.info(f"Restarting app: {args}")
|
||||
self.exit_app()
|
||||
|
||||
def exit_app(self, exit_code=1):
|
||||
os._exit(exit_code)
|
||||
# sys.exit(exit_code)
|
||||
|
||||
class Config:
|
||||
def __init__(self):
|
||||
self.data = None
|
||||
self.packages:dict[str, Package] = {}
|
||||
|
||||
self.read_config()
|
||||
|
||||
def read_config(self):
|
||||
config_path = os.path.join(root_dir, "libs", "app", "config.json")
|
||||
if(os.path.exists(config_path)):
|
||||
with open(config_path) as f:
|
||||
data = json.load(f)
|
||||
self.data = data
|
||||
self.read_packages()
|
||||
else:
|
||||
raise Exception(f"Config file {config_path} not exist")
|
||||
|
||||
def read_packages(self):
|
||||
for package in self.data["packages"]:
|
||||
try:
|
||||
with open(os.path.join(root_dir,'libs',package["id"],'info.json')) as f:
|
||||
self.packages[package["id"]] = Package(config=package, info=json.load(f))
|
||||
except Exception as e:
|
||||
self.packages[package["id"]] = Package(config=package, info={})
|
||||
logger.error(e)
|
||||
|
||||
class Package:
|
||||
def __init__(self, config={}, info={}):
|
||||
self.config = config
|
||||
self.info = info
|
||||
self.modules:dict[str, any] = {}
|
||||
|
||||
self.read_modules()
|
||||
|
||||
def read_modules(self):
|
||||
if "modules" in self.info:
|
||||
for module in self.info["modules"]:
|
||||
self.modules[module["id"]] = module
|
||||
Reference in New Issue
Block a user