diff --git a/CHANGELOG.md b/CHANGELOG.md
index dfd883a7..15bd4c39 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,13 @@
# Changelog
## --- [4.2.2] - 2023/TBD
### New features
-TBD
+- Loading Screen for Crafty during startup ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/668))
### Refactor
- Remove deprecated API V1 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/670))
+- Tidy up main.py to be more comprehensive ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/668))
### Bug fixes
- Remove webhook `custom` option from webook provider list as it's not currently an option ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/664))
+- Bump cryptography for CVE-2023-49083 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/680))
### Tweaks
- Homogenize Panel logos/branding ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/666))
- Retain previous tab when revisiting server details page (#272)([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/667))
@@ -15,7 +17,7 @@ TBD
- Fix Unban button failing to pardon users ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/671))
- Fix stack in API error handling ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/674))
### Lang
-TBD
+- pl_PL Minor fixes ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/675))
## --- [4.2.1] - 2023/11/01
diff --git a/app/classes/controllers/servers_controller.py b/app/classes/controllers/servers_controller.py
index 99151a32..88923194 100644
--- a/app/classes/controllers/servers_controller.py
+++ b/app/classes/controllers/servers_controller.py
@@ -22,6 +22,7 @@ from app.classes.models.server_permissions import (
PermissionsServers,
EnumPermissionsServer,
)
+from app.classes.shared.websocket_manager import WebSocketManager
logger = logging.getLogger(__name__)
@@ -36,6 +37,7 @@ class ServersController(metaclass=Singleton):
self.management_helper = management_helper
self.servers_list = []
self.stats = Stats(self.helper, self)
+ self.web_sock = WebSocketManager()
self.server_subpage = {}
# **********************************************************************************
@@ -170,8 +172,15 @@ class ServersController(metaclass=Singleton):
def init_all_servers(self):
servers = self.get_all_defined_servers()
self.failed_servers = []
-
for server in servers:
+ self.web_sock.broadcast_to_admins(
+ "update",
+ {"section": "server", "server": server["server_name"]},
+ )
+ self.web_sock.broadcast_to_non_admins(
+ "update",
+ {"section": "init"},
+ )
server_id = server.get("server_id")
# if we have already initialized this server, let's skip it.
diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py
index 62ce8819..7524cf90 100644
--- a/app/classes/shared/helpers.py
+++ b/app/classes/shared/helpers.py
@@ -80,6 +80,7 @@ class Helpers:
self.translation = Translation(self)
self.update_available = False
self.ignored_names = ["crafty_managed.txt", "db_stats"]
+ self.crafty_starting = False
@staticmethod
def auto_installer_fix(ex):
diff --git a/app/classes/shared/main_controller.py b/app/classes/shared/main_controller.py
index 27104b62..c8ac3b3c 100644
--- a/app/classes/shared/main_controller.py
+++ b/app/classes/shared/main_controller.py
@@ -85,7 +85,7 @@ class Controller:
encoding="utf-8",
) as f:
self.auth_tracker = json.load(f)
- except:
+ except (FileNotFoundError, json.JSONDecodeError):
self.auth_tracker = {}
def log_attempt(self, remote_ip, username):
diff --git a/app/classes/shared/websocket_manager.py b/app/classes/shared/websocket_manager.py
index f48adef8..7cda296d 100644
--- a/app/classes/shared/websocket_manager.py
+++ b/app/classes/shared/websocket_manager.py
@@ -37,7 +37,15 @@ class WebSocketManager(metaclass=Singleton):
def broadcast_to_admins(self, event_type: str, data):
def filter_fn(client):
- if client.get_user_id in HelperUsers.get_super_user_list():
+ if str(client.get_user_id()) in str(HelperUsers.get_super_user_list()):
+ return True
+ return False
+
+ self.broadcast_with_fn(filter_fn, event_type, data)
+
+ def broadcast_to_non_admins(self, event_type: str, data):
+ def filter_fn(client):
+ if str(client.get_user_id()) not in str(HelperUsers.get_super_user_list()):
return True
return False
diff --git a/app/classes/web/panel_handler.py b/app/classes/web/panel_handler.py
index e8643aa7..8ac827c3 100644
--- a/app/classes/web/panel_handler.py
+++ b/app/classes/web/panel_handler.py
@@ -210,6 +210,8 @@ class PanelHandler(BaseHandler):
error = self.get_argument("error", "WTF Error!")
template = "panel/denied.html"
+ if self.helper.crafty_starting:
+ page = "loading"
now = time.time()
formatted_time = str(
@@ -243,9 +245,13 @@ class PanelHandler(BaseHandler):
for r in exec_user["roles"]:
role = self.controller.roles.get_role(r)
exec_user_role.add(role["role_name"])
- defined_servers = self.controller.servers.get_authorized_servers(
- exec_user["user_id"]
- )
+ # get_auth_servers will throw an exception if run while Crafty is starting
+ if not self.helper.crafty_starting:
+ defined_servers = self.controller.servers.get_authorized_servers(
+ exec_user["user_id"]
+ )
+ else:
+ defined_servers = []
user_order = self.controller.users.get_user_by_id(exec_user["user_id"])
user_order = user_order["server_order"].split(",")
@@ -1615,7 +1621,8 @@ class PanelHandler(BaseHandler):
logs_thread.start()
self.redirect("/panel/dashboard")
return
-
+ if self.helper.crafty_starting:
+ template = "panel/loading.html"
self.render(
template,
data=page_data,
diff --git a/app/frontend/templates/panel/loading.html b/app/frontend/templates/panel/loading.html
new file mode 100644
index 00000000..a56a75ee
--- /dev/null
+++ b/app/frontend/templates/panel/loading.html
@@ -0,0 +1,73 @@
+{% extends ../base.html %}
+
+{% block meta %}
+{% end %}
+
+{% block title %}Crafty Controller Starting{% end %}
+
+{% block content %}
+
+
+
+
+
+{% end %}
diff --git a/app/translations/en_EN.json b/app/translations/en_EN.json
index 6db96dfc..ab4c54ce 100644
--- a/app/translations/en_EN.json
+++ b/app/translations/en_EN.json
@@ -590,6 +590,15 @@
"newServer": "Create New Server",
"servers": "Servers"
},
+ "startup": {
+ "starting": "Crafty Is Starting...",
+ "serverInit": "Initializing Servers",
+ "server": "Initializing ",
+ "internet": "Checking for internet connection",
+ "tasks": "Starting Tasks Scheduler",
+ "internals": "Configuring and starting Crafty's internal componenets",
+ "almost": "Finishing up. Hang on tight..."
+ },
"userConfig": {
"apiKey": "API Keys",
"auth": "Authorized? ",
@@ -655,4 +664,4 @@
"webhook_body": "Webhook Body",
"webhooks": "Webhooks"
}
-}
+}
\ No newline at end of file
diff --git a/app/translations/pl_PL.json b/app/translations/pl_PL.json
index 828ec0b1..2c60c6df 100644
--- a/app/translations/pl_PL.json
+++ b/app/translations/pl_PL.json
@@ -326,8 +326,8 @@
"bePatientDeleteFiles": "Poczekaj, aż usuniemy twój serwer i jego pliki. Strona za chwilę się zamknie.",
"bePatientUpdate": "Poczekaj kiedy my aktualizujemy twój serwer. Pobieranie zależy od prędkości twojego internetu.
Strona się odświeży za chwile.",
"cancel": "Anuluj",
- "crashTime": "Crash wyszedł poza limit czasu",
- "crashTimeDesc": "How long should we wait before we consider your server as crashed?",
+ "crashTime": "Crash serwera wyszedł poza limit czasu",
+ "crashTimeDesc": "Jak długo powinniśmy poczekać zanim uznać serwer za zcrashowany?",
"deleteFilesQuestion": "Usuń pliki serwera z maszyny?",
"deleteFilesQuestionMessage": "Czy chcesz aby Crafty usunął wszystkie pliki tego serwera?
To zawiera backupy.",
"deleteServer": "Usuń serwer",
@@ -403,7 +403,7 @@
"filterList": "Filtrowane słowa",
"logs": "Logi",
"metrics": "Statystyki",
- "playerControls": "Player Management",
+ "playerControls": "Zarządzanie użytkownikami",
"reset": "Resetuj Scrolla",
"schedule": "Harmonogram",
"serverDetails": "Detale serwera",
@@ -421,7 +421,7 @@
"deleteItemQuestion": "Czy jesteś pewien że chcesz usunąć \" + name + \"?",
"deleteItemQuestionMessage": "Usuwasz \\\"\" + path + \"\\\"!
Ta akcja jest nieodwracalna i zostanie usunięta na zawsze!",
"download": "Pobierz",
- "editingFile": "Edytuję plik",
+ "editingFile": "Edytuj plik",
"error": "Error while getting files",
"fileReadError": "Error odczytu pliku",
"files": "Pliki",
@@ -432,7 +432,7 @@
"rename": "Zmień nazwę",
"renameItemQuestion": "Jaka ma być nowa nazwa?",
"save": "Zapisz",
- "size": "Włącz zmienianie rozmiaru edytora",
+ "size": "Włącz rozszerzanie i zmniejszanie edytora",
"stayHere": "NIE WYCHODŹ Z TEJ STRONY!",
"unsupportedLanguage": "Uwaga: To nie jest wspierany typ pliku",
"unzip": "Rozpakuj",
@@ -545,7 +545,7 @@
"buildServer": "Zbuduj serwer!",
"clickRoot": "Kilknij tutaj aby zaznaczyć główną ścieżkę",
"close": "Zamknij",
- "defaultPort": "25565 podstawowy",
+ "defaultPort": "Domyślnie 25565",
"downloading": "Pobieranie serwera...",
"explainRoot": "Proszę, kliknij przycisk poniżej aby zaznaczyć główną ścieżkę w tym archiwum",
"importServer": "Importuj egzystujący serwer",
@@ -640,12 +640,12 @@
"edit": "Edytuj",
"enabled": "Włączony",
"jar_update": "Plik startowy zaktualizowany",
- "kill": "Serwer zatrzymany",
+ "kill": "Serwer został zabity",
"name": "Nazwa",
"new": "Nowy Webhook",
"newWebhook": "Nowy Webhook",
"no-webhook": "Nie posiadasz aktualnie żadnych Webhooków dla tego serwera. Aby dodać webhook kliknij na",
- "run": "Włącz Webhook",
+ "run": "Przetestuj Webhook",
"send_command": "Komenda serwera otrzymana!",
"start_server": "Serwer włączony",
"stop_server": "Serwer wyłączony",
diff --git a/main.py b/main.py
index 4cd78814..cac130c1 100644
--- a/main.py
+++ b/main.py
@@ -20,6 +20,18 @@ from app.classes.shared.websocket_manager import WebSocketManager
console = Console()
helper = Helpers()
+# Get the path our application is running on.
+if getattr(sys, "frozen", False):
+ APPLICATION_PATH = os.path.dirname(sys.executable)
+ RUNNING_MODE = "Frozen/executable"
+else:
+ try:
+ app_full_path = os.path.realpath(__file__)
+ APPLICATION_PATH = os.path.dirname(app_full_path)
+ RUNNING_MODE = "Non-interactive (e.g. 'python main.py')"
+ except NameError:
+ APPLICATION_PATH = os.getcwd()
+ RUNNING_MODE = "Interactive"
if helper.check_root():
Console.critical(
"Root detected. Root/Admin access denied. "
@@ -51,7 +63,178 @@ except ModuleNotFoundError as err:
helper.auto_installer_fix(err)
+def internet_check():
+ """
+ This checks to see if the Crafty host is connected to the
+ internet. This will show a warning in the console if no interwebs.
+ """
+ print()
+ logger.info("Checking Internet. This may take a minute.")
+ Console.info("Checking Internet. This may take a minute.")
+
+ if not helper.check_internet():
+ logger.warning(
+ "We have detected the machine running Crafty has no "
+ "connection to the internet. Client connections to "
+ "the server may be limited."
+ )
+ Console.warning(
+ "We have detected the machine running Crafty has no "
+ "connection to the internet. Client connections to "
+ "the server may be limited."
+ )
+
+
+def controller_setup():
+ """
+ Method sets up the software controllers.
+ This also sets the application path as well as the
+ master server dir (if not set).
+
+ This also clears the support logs status.
+ """
+ if not controller.check_system_user():
+ controller.add_system_user()
+
+ master_server_dir = controller.management.get_master_server_dir()
+ if master_server_dir == "":
+ logger.debug("Could not find master server path. Setting default")
+ controller.set_master_server_dir(
+ os.path.join(controller.project_root, "servers")
+ )
+ else:
+ helper.servers_dir = master_server_dir
+
+ Console.debug(f"Execution Mode: {RUNNING_MODE}")
+ Console.debug(f"Application path : '{APPLICATION_PATH}'")
+
+ controller.clear_support_status()
+
+
+def tasks_starter():
+ """
+ Method starts stats recording, app scheduler, and
+ serverjars/steamCMD cache refreshers
+ """
+ # start stats logging
+ tasks_manager.start_stats_recording()
+
+ # once the controller is up and stats are logging, we can kick off
+ # the scheduler officially
+ tasks_manager.start_scheduler()
+
+ # refresh our cache and schedule for every 12 hoursour cache refresh
+ # for serverjars.com
+ tasks_manager.serverjar_cache_refresher()
+
+
+def signal_handler(signum, _frame):
+ """
+ Method handles sigterm and shuts the app down.
+ """
+ if not args.daemon:
+ print() # for newline after prompt
+ signame = signal.Signals(signum).name
+ logger.info(f"Recieved signal {signame} [{signum}], stopping Crafty...")
+ Console.info(f"Recieved signal {signame} [{signum}], stopping Crafty...")
+ tasks_manager._main_graceful_exit()
+ crafty_prompt.universal_exit()
+
+
+def do_cleanup():
+ """
+ Checks Crafty's temporary directory and clears it out on boot.
+ """
+ try:
+ logger.info("Removing old temp dirs")
+ FileHelpers.del_dirs(os.path.join(controller.project_root, "temp"))
+ except:
+ logger.info("Did not find old temp dir.")
+ os.mkdir(os.path.join(controller.project_root, "temp"))
+
+
+def do_version_check():
+ """
+ Checks for remote version differences.
+
+ Prints in terminal with differences if true.
+
+ Also sets helper variable to update available when pages
+ are served.
+ """
+
+ # Check if new version available
+ remote_ver = helper.check_remote_version()
+ if remote_ver:
+ notice = f"""
+ A new version of Crafty is available!
+ {'/' * 37}
+ New version available: {remote_ver}
+ Current version: {pkg_version.parse(helper.get_version_string())}
+ {'/' * 37}
+ """
+ Console.yellow(notice)
+
+ crafty_prompt.prompt = f"Crafty Controller v{helper.get_version_string()} > "
+
+
+def setup_starter():
+ """
+ This method starts our setup threads.
+ (tasks scheduler, internet checks, controller setups)
+
+ Once our threads complete we will set our startup
+ variable to false and send a reload to any clients waiting.
+
+
+ """
+ if not args.daemon:
+ time.sleep(0.01) # Wait for the prompt to start
+ print() # Make a newline after the prompt so logs are on an empty line
+ else:
+ time.sleep(0.01) # Wait for the daemon info message
+
+ Console.info("Setting up Crafty's internal components...")
+ # Start the setup threads
+ web_sock.broadcast("update", {"section": "tasks"})
+ time.sleep(2)
+ tasks_starter_thread.start()
+ web_sock.broadcast("update", {"section": "internet"})
+ time.sleep(2)
+ internet_check_thread.start()
+ web_sock.broadcast(
+ "update",
+ {"section": "internals"},
+ )
+ time.sleep(2)
+ controller_setup_thread.start()
+
+ # Wait for the setup threads to finish
+ web_sock.broadcast(
+ "update",
+ {"section": "almost"},
+ )
+ tasks_starter_thread.join()
+ internet_check_thread.join()
+ controller_setup_thread.join()
+ helper.crafty_starting = False
+ web_sock.broadcast("send_start_reload", "")
+ do_version_check()
+ Console.info("Crafty has fully started and is now ready for use!")
+
+ do_cleanup()
+
+ if not args.daemon:
+ # Put the prompt under the cursor
+ crafty_prompt.print_prompt()
+
+
def do_intro():
+ """
+ Runs the Crafty Controller Terminal Intro with information about the software
+ This method checks for a "settings file" or config.json. If it does not find
+ one it will create one.
+ """
logger.info("***** Crafty Controller Started *****")
version = helper.get_version_string()
@@ -72,6 +255,11 @@ def do_intro():
def setup_logging(debug=True):
+ """
+ This method sets up our logging for Crafty. It takes
+ one optional (defaulted to True) parameter which
+ determines whether or not the logging level is "debug" or verbose.
+ """
logging_config_file = os.path.join(os.path.curdir, "app", "config", "logging.json")
if not helper.check_file_exists(
os.path.join(os.path.curdir, "logs", "auth_tracker.log")
@@ -117,11 +305,11 @@ if __name__ == "__main__":
)
args = parser.parse_args()
-
helper.ensure_logging_setup()
-
+ helper.crafty_starting = True
+ # Init WebSocket Manager Here
+ web_sock = WebSocketManager()
setup_logging(debug=args.verbose)
-
if args.verbose:
Console.level = "debug"
@@ -133,23 +321,27 @@ if __name__ == "__main__":
# print our pretty start message
do_intro()
-
# our session file, helps prevent multiple controller agents on the same machine.
helper.create_session_file(ignore=args.ignore)
-
# start the database
database = peewee.SqliteDatabase(
helper.db_path, pragmas={"journal_mode": "wal", "cache_size": -1024 * 10}
)
database_proxy.initialize(database)
-
migration_manager = MigrationManager(database, helper)
migration_manager.up() # Automatically runs migrations
- # do our installer stuff
+ # init classes
+ # now the tables are created, we can load the tasks_manager and server controller
user_helper = HelperUsers(database, helper)
management_helper = HelpersManagement(database, helper)
installer = DatabaseBuilder(database, helper, user_helper, management_helper)
+ file_helper = FileHelpers(helper)
+ import_helper = ImportHelpers(helper, file_helper)
+ controller = Controller(database, helper, file_helper, import_helper)
+ controller.set_project_root(APPLICATION_PATH)
+ tasks_manager = TasksManager(helper, controller, file_helper)
+ import3 = Import3(helper, controller)
FRESH_INSTALL = installer.is_fresh_install()
if FRESH_INSTALL:
@@ -171,153 +363,45 @@ if __name__ == "__main__":
helper.set_setting("reset_secrets_on_next_boot", False)
else:
Console.info("No flag found. Secrets are staying")
- file_helper = FileHelpers(helper)
- import_helper = ImportHelpers(helper, file_helper)
- # Init WebSocket Manager Here
- WebSocketManager()
- # now the tables are created, we can load the tasks_manager and server controller
- controller = Controller(database, helper, file_helper, import_helper)
+
+ # Check to see if client config.json version is different than the
+ # Master config.json in helpers.py
Console.info("Checking for remote changes to config.json")
controller.get_config_diff()
Console.info("Remote change complete.")
- import3 = Import3(helper, controller)
- tasks_manager = TasksManager(helper, controller, file_helper)
+ # startup the web server
tasks_manager.start_webserver()
- def signal_handler(signum, _frame):
- if not args.daemon:
- print() # for newline after prompt
- signame = signal.Signals(signum).name
- logger.info(f"Recieved signal {signame} [{signum}], stopping Crafty...")
- Console.info(f"Recieved signal {signame} [{signum}], stopping Crafty...")
- tasks_manager._main_graceful_exit()
- crafty_prompt.universal_exit()
-
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
# init servers
logger.info("Initializing all servers defined")
Console.info("Initializing all servers defined")
+ web_sock.broadcast(
+ "update",
+ {"section": "serverInit"},
+ )
controller.servers.init_all_servers()
- def tasks_starter():
- # start stats logging
- tasks_manager.start_stats_recording()
-
- # once the controller is up and stats are logging, we can kick off
- # the scheduler officially
- tasks_manager.start_scheduler()
-
- # refresh our cache and schedule for every 12 hoursour cache refresh
- # for serverjars.com
- tasks_manager.serverjar_cache_refresher()
-
+ # start up our tasks handler in tasks.py
tasks_starter_thread = Thread(target=tasks_starter, name="tasks_starter")
- def internet_check():
- print()
- logger.info("Checking Internet. This may take a minute.")
- Console.info("Checking Internet. This may take a minute.")
-
- if not helper.check_internet():
- logger.warning(
- "We have detected the machine running Crafty has no "
- "connection to the internet. Client connections to "
- "the server may be limited."
- )
- Console.warning(
- "We have detected the machine running Crafty has no "
- "connection to the internet. Client connections to "
- "the server may be limited."
- )
-
+ # check to see if instance has internet
internet_check_thread = Thread(target=internet_check, name="internet_check")
- def controller_setup():
- if not controller.check_system_user():
- controller.add_system_user()
-
- if getattr(sys, "frozen", False):
- application_path = os.path.dirname(sys.executable)
- running_mode = "Frozen/executable"
- else:
- try:
- app_full_path = os.path.realpath(__file__)
- application_path = os.path.dirname(app_full_path)
- running_mode = "Non-interactive (e.g. 'python main.py')"
- except NameError:
- application_path = os.getcwd()
- running_mode = "Interactive"
-
- controller.set_project_root(application_path)
- master_server_dir = controller.management.get_master_server_dir()
- if master_server_dir == "":
- logger.debug("Could not find master server path. Setting default")
- controller.set_master_server_dir(
- os.path.join(controller.project_root, "servers")
- )
- else:
- helper.servers_dir = master_server_dir
-
- Console.debug(f"Execution Mode: {running_mode}")
- Console.debug(f"Application path : '{application_path}'")
-
- controller.clear_support_status()
-
+ # start the Crafty console.
crafty_prompt = MainPrompt(
helper, tasks_manager, migration_manager, controller, import3
)
+ # set up all controllers
controller_setup_thread = Thread(target=controller_setup, name="controller_setup")
- def setup_starter():
- if not args.daemon:
- time.sleep(0.01) # Wait for the prompt to start
- print() # Make a newline after the prompt so logs are on an empty line
- else:
- time.sleep(0.01) # Wait for the daemon info message
+ setup_starter_thread = Thread(target=setup_starter, name="setup_starter")
- Console.info("Setting up Crafty's internal components...")
-
- # Start the setup threads
- tasks_starter_thread.start()
- internet_check_thread.start()
- controller_setup_thread.start()
-
- # Wait for the setup threads to finish
- tasks_starter_thread.join()
- internet_check_thread.join()
- controller_setup_thread.join()
-
- Console.info("Crafty has fully started and is now ready for use!")
-
- # Check if new version available
- remote_ver = helper.check_remote_version()
- if remote_ver:
- notice = f"""
- A new version of Crafty is available!
- {'/' * 37}
- New version available: {remote_ver}
- Current version: {pkg_version.parse(helper.get_version_string())}
- {'/' * 37}
- """
- Console.yellow(notice)
-
- crafty_prompt.prompt = f"Crafty Controller v{helper.get_version_string()} > "
- try:
- logger.info("Removing old temp dirs")
- FileHelpers.del_dirs(os.path.join(controller.project_root, "temp"))
- except:
- logger.info("Did not find old temp dir.")
- os.mkdir(os.path.join(controller.project_root, "temp"))
-
- if not args.daemon:
- # Put the prompt under the cursor
- crafty_prompt.print_prompt()
-
- Thread(target=setup_starter, name="setup_starter").start()
+ setup_starter_thread.start()
if not args.daemon:
# Start the Crafty prompt
diff --git a/requirements.txt b/requirements.txt
index 1e9feb0f..7b1adcfc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,13 +4,13 @@ argon2-cffi==23.1.0
cached_property==1.5.2
colorama==0.4.6
croniter==1.4.1
-cryptography==41.0.4
+cryptography==41.0.7
libgravatar==1.0.4
nh3==0.2.14
packaging==23.2
peewee==3.13
psutil==5.9.5
-pyOpenSSL==23.2.0
+pyOpenSSL==23.3.0
pyjwt==2.8.0
PyYAML==6.0.1
requests==2.31.0