mirror of
https://gitlab.com/crafty-controller/crafty-4.git
synced 2025-12-05 01:10:15 +00:00
Merge branch 'dev' into bugfix/bedrock-build-update
This commit is contained in:
@@ -52,7 +52,7 @@ persistent=yes
|
|||||||
|
|
||||||
# Minimum Python version to use for version dependent checks. Will default to
|
# Minimum Python version to use for version dependent checks. Will default to
|
||||||
# the version used to run pylint.
|
# the version used to run pylint.
|
||||||
py-version=3.9
|
py-version=3.10
|
||||||
|
|
||||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||||
# user-friendly hints instead of false-positive error messages.
|
# user-friendly hints instead of false-positive error messages.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
## --- [4.5.6] - 2025/TBD
|
## --- [4.6.1] - 2025/TBD
|
||||||
### New features
|
### New features
|
||||||
TBD
|
- Jinja2 Dynamic Variables for Webhook Notifications ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/757))
|
||||||
### Bug fixes
|
### Bug fixes
|
||||||
- Change hour and minute intervals in APScheudler to fix incorrect triggers ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/910))
|
- Change hour and minute intervals in APScheudler to fix incorrect triggers ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/910))
|
||||||
- Use asyncio locks to limit upload handler race condition ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/907))
|
- Use asyncio locks to limit upload handler race condition ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/907))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[](https://craftycontrol.com)
|
[](https://craftycontrol.com)
|
||||||
# Crafty Controller 4.5.6
|
# Crafty Controller 4.6.1
|
||||||
> Python based Control Panel for your Minecraft Server
|
> Python based Control Panel for your Minecraft Server
|
||||||
|
|
||||||
## What is Crafty Controller?
|
## What is Crafty Controller?
|
||||||
@@ -66,8 +66,6 @@ The image is located at: `registry.gitlab.com/crafty-controller/crafty-4:latest`
|
|||||||
$ vim docker-compose.yml
|
$ vim docker-compose.yml
|
||||||
```
|
```
|
||||||
```yml
|
```yml
|
||||||
version: '3'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
crafty:
|
crafty:
|
||||||
container_name: crafty_container
|
container_name: crafty_container
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ MASTER_CONFIG = {
|
|||||||
"max_login_attempts": 3,
|
"max_login_attempts": 3,
|
||||||
"superMFA": False,
|
"superMFA": False,
|
||||||
"general_user_log_access": False,
|
"general_user_log_access": False,
|
||||||
|
"base_url": "127.0.0.1:8443",
|
||||||
}
|
}
|
||||||
|
|
||||||
CONFIG_CATEGORIES = {
|
CONFIG_CATEGORIES = {
|
||||||
@@ -80,6 +81,7 @@ CONFIG_CATEGORIES = {
|
|||||||
"disabled_language_files",
|
"disabled_language_files",
|
||||||
"big_bucket_repo",
|
"big_bucket_repo",
|
||||||
"enable_user_self_delete",
|
"enable_user_self_delete",
|
||||||
|
"base_url",
|
||||||
],
|
],
|
||||||
"security": [
|
"security": [
|
||||||
"allow_nsfw_profile_pictures",
|
"allow_nsfw_profile_pictures",
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ logger = logging.getLogger(__name__)
|
|||||||
class BackupManager:
|
class BackupManager:
|
||||||
|
|
||||||
SNAPSHOT_BACKUP_DATE_FORMAT_STRING = "%Y-%m-%d-%H-%M-%S"
|
SNAPSHOT_BACKUP_DATE_FORMAT_STRING = "%Y-%m-%d-%H-%M-%S"
|
||||||
|
SNAPSHOT_SUFFIX = ".manifest"
|
||||||
|
ARCHIVE_SUFFIX = ".zip"
|
||||||
|
|
||||||
def __init__(self, helper, file_helper, management_helper):
|
def __init__(self, helper, file_helper, management_helper):
|
||||||
self.helper = helper
|
self.helper = helper
|
||||||
@@ -94,13 +96,14 @@ class BackupManager:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def backup_starter(self, backup_config, server):
|
def backup_starter(self, backup_config, server) -> tuple:
|
||||||
"""Notify users of backup starting, and start the backup.
|
"""Notify users of backup starting, and start the backup.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
backup_config (_type_): _description_
|
backup_config (_type_): _description_
|
||||||
server (_type_): Server object to backup
|
server (_type_): Server object to backup
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Notify users of backup starting
|
# Notify users of backup starting
|
||||||
logger.info(f"Starting server {server.name} (ID {server.server_id}) backup")
|
logger.info(f"Starting server {server.name} (ID {server.server_id}) backup")
|
||||||
server_users = PermissionsServers.get_server_user_list(server.server_id)
|
server_users = PermissionsServers.get_server_user_list(server.server_id)
|
||||||
@@ -114,14 +117,37 @@ class BackupManager:
|
|||||||
).format(server.name),
|
).format(server.name),
|
||||||
)
|
)
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
|
size = False
|
||||||
# Start the backup
|
# Start the backup
|
||||||
if backup_config.get("backup_type", "zip_vault") == "zip_vault":
|
if backup_config.get("backup_type", "zip_vault") == "zip_vault":
|
||||||
self.zip_vault(backup_config, server)
|
backup_file_name = self.zip_vault(backup_config, server)
|
||||||
|
if (
|
||||||
|
backup_file_name
|
||||||
|
and Path(backup_file_name).suffix != self.ARCHIVE_SUFFIX
|
||||||
|
):
|
||||||
|
backup_file_name += self.ARCHIVE_SUFFIX
|
||||||
|
if backup_file_name:
|
||||||
|
size = (
|
||||||
|
Path(
|
||||||
|
backup_config["backup_location"],
|
||||||
|
backup_config["backup_id"],
|
||||||
|
backup_file_name,
|
||||||
|
)
|
||||||
|
.stat()
|
||||||
|
.st_size
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.snapshot_backup(backup_config, server)
|
backup_file_name = self.snapshot_backup(backup_config, server)
|
||||||
|
if (
|
||||||
|
backup_file_name
|
||||||
|
and Path(backup_file_name).suffix != self.SNAPSHOT_SUFFIX
|
||||||
|
):
|
||||||
|
backup_file_name += self.SNAPSHOT_SUFFIX
|
||||||
|
if backup_file_name:
|
||||||
|
return (backup_file_name, size)
|
||||||
|
return (False, "error")
|
||||||
|
|
||||||
def zip_vault(self, backup_config, server):
|
def zip_vault(self, backup_config, server) -> str | bool:
|
||||||
|
|
||||||
# Adjust the location to include the backup ID for destination.
|
# Adjust the location to include the backup ID for destination.
|
||||||
backup_location = os.path.join(
|
backup_location = os.path.join(
|
||||||
@@ -131,7 +157,7 @@ class BackupManager:
|
|||||||
# Check if the backup location even exists.
|
# Check if the backup location even exists.
|
||||||
if not backup_location:
|
if not backup_location:
|
||||||
Console.critical("No backup path found. Canceling")
|
Console.critical("No backup path found. Canceling")
|
||||||
return None
|
return False
|
||||||
|
|
||||||
self.helper.ensure_dir_exists(backup_location)
|
self.helper.ensure_dir_exists(backup_location)
|
||||||
|
|
||||||
@@ -195,8 +221,10 @@ class BackupManager:
|
|||||||
{"status": json.dumps({"status": "Standby", "message": ""})},
|
{"status": json.dumps({"status": "Standby", "message": ""})},
|
||||||
)
|
)
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
return Path(backup_filename).name
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.fail_backup(e, backup_config, server)
|
self.fail_backup(e, backup_config, server)
|
||||||
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def fail_backup(why: Exception, backup_config: dict, server) -> None:
|
def fail_backup(why: Exception, backup_config: dict, server) -> None:
|
||||||
@@ -234,8 +262,7 @@ class BackupManager:
|
|||||||
{"status": json.dumps({"status": "Failed", "message": f"{why}"})},
|
{"status": json.dumps({"status": "Failed", "message": f"{why}"})},
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
def list_backups(self, backup_config: dict, server_id) -> list:
|
||||||
def list_backups(backup_config: dict, server_id) -> list:
|
|
||||||
if not backup_config:
|
if not backup_config:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Error putting backup file list for server with ID: {server_id}"
|
f"Error putting backup file list for server with ID: {server_id}"
|
||||||
@@ -268,7 +295,7 @@ class BackupManager:
|
|||||||
"size": "",
|
"size": "",
|
||||||
}
|
}
|
||||||
for f in files
|
for f in files
|
||||||
if f["path"].endswith(".manifest")
|
if f["path"].endswith(self.SNAPSHOT_SUFFIX)
|
||||||
]
|
]
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -279,7 +306,7 @@ class BackupManager:
|
|||||||
"size": f["size"],
|
"size": f["size"],
|
||||||
}
|
}
|
||||||
for f in files
|
for f in files
|
||||||
if f["path"].endswith(".zip")
|
if f["path"].endswith(self.ARCHIVE_SUFFIX)
|
||||||
]
|
]
|
||||||
|
|
||||||
def remove_old_backups(self, backup_config, server):
|
def remove_old_backups(self, backup_config, server):
|
||||||
@@ -297,7 +324,7 @@ class BackupManager:
|
|||||||
logger.info(f"Removing old backup '{oldfile['path']}'")
|
logger.info(f"Removing old backup '{oldfile['path']}'")
|
||||||
os.remove(Helpers.get_os_understandable_path(oldfile_path))
|
os.remove(Helpers.get_os_understandable_path(oldfile_path))
|
||||||
|
|
||||||
def snapshot_backup(self, backup_config, server) -> None:
|
def snapshot_backup(self, backup_config, server) -> str | bool:
|
||||||
"""
|
"""
|
||||||
Creates snapshot style backup of server. No file will be saved more than once
|
Creates snapshot style backup of server. No file will be saved more than once
|
||||||
over all backups. Designed to enable encryption of files and s3 compatability in
|
over all backups. Designed to enable encryption of files and s3 compatability in
|
||||||
@@ -339,7 +366,7 @@ class BackupManager:
|
|||||||
manifest_file: io.TextIOWrapper = backup_manifest_path.open("w+")
|
manifest_file: io.TextIOWrapper = backup_manifest_path.open("w+")
|
||||||
except OSError as why:
|
except OSError as why:
|
||||||
self.fail_backup(why, backup_config, server)
|
self.fail_backup(why, backup_config, server)
|
||||||
return
|
return False
|
||||||
|
|
||||||
# Write manifest file version.
|
# Write manifest file version.
|
||||||
manifest_file.write("00\n")
|
manifest_file.write("00\n")
|
||||||
@@ -359,7 +386,7 @@ class BackupManager:
|
|||||||
manifest_file.close()
|
manifest_file.close()
|
||||||
backup_manifest_path.unlink(missing_ok=True)
|
backup_manifest_path.unlink(missing_ok=True)
|
||||||
self.fail_backup(why, backup_config, server)
|
self.fail_backup(why, backup_config, server)
|
||||||
return
|
return False
|
||||||
|
|
||||||
# Write saved file into manifest.
|
# Write saved file into manifest.
|
||||||
manifest_file.write(
|
manifest_file.write(
|
||||||
@@ -373,6 +400,13 @@ class BackupManager:
|
|||||||
backup_config["max_backups"], backup_repository_path
|
backup_config["max_backups"], backup_repository_path
|
||||||
)
|
)
|
||||||
|
|
||||||
|
HelpersManagement.update_backup_config(
|
||||||
|
backup_config["backup_id"],
|
||||||
|
{"status": json.dumps({"status": "Standby", "message": ""})},
|
||||||
|
)
|
||||||
|
|
||||||
|
return Path(backup_manifest_path).name
|
||||||
|
|
||||||
def snapshot_restore(
|
def snapshot_restore(
|
||||||
self, backup_config: {str}, backup_manifest_filename: str, server
|
self, backup_config: {str}, backup_manifest_filename: str, server
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import time
|
|||||||
import datetime
|
import datetime
|
||||||
import base64
|
import base64
|
||||||
import threading
|
import threading
|
||||||
import logging.config
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
import html
|
import html
|
||||||
import glob
|
import glob
|
||||||
@@ -54,31 +54,72 @@ def callback(called_func):
|
|||||||
res = None
|
res = None
|
||||||
logger.debug("Checking for callbacks")
|
logger.debug("Checking for callbacks")
|
||||||
try:
|
try:
|
||||||
res = called_func(*args, **kwargs)
|
res = called_func(*args, **kwargs) # Calls and runs the function
|
||||||
finally:
|
finally:
|
||||||
events = WebhookFactory.get_monitored_events()
|
event_type = called_func.__name__
|
||||||
if called_func.__name__ in events:
|
|
||||||
|
# For send_command, Retrieve command from args or kwargs
|
||||||
|
command = args[1] if len(args) > 1 else kwargs.get("command", "")
|
||||||
|
|
||||||
|
if event_type in WebhookFactory.get_monitored_events():
|
||||||
server_webhooks = HelpersWebhooks.get_webhooks_by_server(
|
server_webhooks = HelpersWebhooks.get_webhooks_by_server(
|
||||||
args[0].server_id, True
|
args[0].server_id, True
|
||||||
)
|
)
|
||||||
for swebhook in server_webhooks:
|
for swebhook in server_webhooks:
|
||||||
if called_func.__name__ in str(swebhook.trigger).split(","):
|
if event_type in str(swebhook.trigger).split(","):
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Found callback for event {called_func.__name__}"
|
f"Found callback for event {event_type}"
|
||||||
f" for server {args[0].server_id}"
|
f" for server {args[0].server_id}"
|
||||||
)
|
)
|
||||||
webhook = HelpersWebhooks.get_webhook_by_id(swebhook.id)
|
webhook = HelpersWebhooks.get_webhook_by_id(swebhook.id)
|
||||||
webhook_provider = WebhookFactory.create_provider(
|
webhook_provider = WebhookFactory.create_provider(
|
||||||
webhook["webhook_type"]
|
webhook["webhook_type"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Extract source context from kwargs if present
|
||||||
|
source_type = kwargs.get("source_type", "unknown")
|
||||||
|
source_id = kwargs.get("source_id", "")
|
||||||
|
source_name = kwargs.get("source_name", "")
|
||||||
|
backup_name = ""
|
||||||
|
backup_size = ""
|
||||||
|
backup_link = ""
|
||||||
|
backup_status = ""
|
||||||
|
backup_error = ""
|
||||||
|
|
||||||
|
if isinstance(res, dict):
|
||||||
|
backup_name = res.get("backup_name")
|
||||||
|
backup_size = str(res.get("backup_size"))
|
||||||
|
backup_link = res.get("backup_link")
|
||||||
|
backup_status = res.get("backup_status")
|
||||||
|
backup_error = res.get("backup_error")
|
||||||
|
|
||||||
|
event_data = {
|
||||||
|
"server_name": args[0].name,
|
||||||
|
"server_id": args[0].server_id,
|
||||||
|
"command": command,
|
||||||
|
"event_type": event_type,
|
||||||
|
"source_type": source_type,
|
||||||
|
"source_id": source_id,
|
||||||
|
"source_name": source_name,
|
||||||
|
"backup_name": backup_name,
|
||||||
|
"backup_size": backup_size,
|
||||||
|
"backup_link": backup_link,
|
||||||
|
"backup_status": backup_status,
|
||||||
|
"backup_error": backup_error,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add time variables to event_data
|
||||||
|
event_data = webhook_provider.add_time_variables(event_data)
|
||||||
|
|
||||||
if res is not False and swebhook.enabled:
|
if res is not False and swebhook.enabled:
|
||||||
webhook_provider.send(
|
webhook_provider.send(
|
||||||
bot_name=webhook["bot_name"],
|
|
||||||
server_name=args[0].name,
|
server_name=args[0].name,
|
||||||
title=webhook["name"],
|
title=webhook["name"],
|
||||||
url=webhook["url"],
|
url=webhook["url"],
|
||||||
message=webhook["body"],
|
message_template=webhook["body"],
|
||||||
|
event_data=event_data,
|
||||||
color=webhook["color"],
|
color=webhook["color"],
|
||||||
|
bot_name=webhook["bot_name"],
|
||||||
)
|
)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@@ -1205,7 +1246,7 @@ class ServerInstance:
|
|||||||
logger.info(f"Backup Thread started for server {self.settings['server_name']}.")
|
logger.info(f"Backup Thread started for server {self.settings['server_name']}.")
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def backup_server(self, backup_id):
|
def backup_server(self, backup_id) -> dict | bool:
|
||||||
logger.info(f"Starting server {self.name} (ID {self.server_id}) backup")
|
logger.info(f"Starting server {self.name} (ID {self.server_id}) backup")
|
||||||
server_users = PermissionsServers.get_server_user_list(self.server_id)
|
server_users = PermissionsServers.get_server_user_list(self.server_id)
|
||||||
# Alert the start of the backup to the authorized users.
|
# Alert the start of the backup to the authorized users.
|
||||||
@@ -1221,7 +1262,15 @@ class ServerInstance:
|
|||||||
|
|
||||||
# Get the backup config
|
# Get the backup config
|
||||||
if not backup_id:
|
if not backup_id:
|
||||||
return logger.error("No backup ID provided. Exiting backup")
|
logger.error("No backup ID provided. Exiting backup")
|
||||||
|
last_failed = self.last_backup_status()
|
||||||
|
if last_failed:
|
||||||
|
last_backup_status = "❌"
|
||||||
|
reason = "No backup ID provided"
|
||||||
|
return {
|
||||||
|
"backup_status": last_backup_status,
|
||||||
|
"backup_error": reason,
|
||||||
|
}
|
||||||
conf = HelpersManagement.get_backup_config(backup_id)
|
conf = HelpersManagement.get_backup_config(backup_id)
|
||||||
# Adjust the location to include the backup ID for destination.
|
# Adjust the location to include the backup ID for destination.
|
||||||
backup_location = os.path.join(conf["backup_location"], conf["backup_id"])
|
backup_location = os.path.join(conf["backup_location"], conf["backup_id"])
|
||||||
@@ -1229,7 +1278,16 @@ class ServerInstance:
|
|||||||
# Check if the backup location even exists.
|
# Check if the backup location even exists.
|
||||||
if not backup_location:
|
if not backup_location:
|
||||||
Console.critical("No backup path found. Canceling")
|
Console.critical("No backup path found. Canceling")
|
||||||
return None
|
backup_status = json.loads(
|
||||||
|
HelpersManagement.get_backup_config(backup_id)["status"]
|
||||||
|
)
|
||||||
|
if backup_status["status"] == "Failed":
|
||||||
|
last_backup_status = "❌"
|
||||||
|
reason = backup_status["message"]
|
||||||
|
return {
|
||||||
|
"backup_status": last_backup_status,
|
||||||
|
"backup_error": reason,
|
||||||
|
}
|
||||||
if conf["before"]:
|
if conf["before"]:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Found running server and send command option. Sending command"
|
"Found running server and send command option. Sending command"
|
||||||
@@ -1237,7 +1295,7 @@ class ServerInstance:
|
|||||||
self.send_command(conf["before"])
|
self.send_command(conf["before"])
|
||||||
# Pause to let command run
|
# Pause to let command run
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
self.backup_mgr.backup_starter(conf, self)
|
backup_name, backup_size = self.backup_mgr.backup_starter(conf, self)
|
||||||
if conf["after"]:
|
if conf["after"]:
|
||||||
self.send_command(conf["after"])
|
self.send_command(conf["after"])
|
||||||
if conf["shutdown"] and self.was_running:
|
if conf["shutdown"] and self.was_running:
|
||||||
@@ -1247,6 +1305,46 @@ class ServerInstance:
|
|||||||
self.run_threaded_server(HelperUsers.get_user_id_by_name("system"))
|
self.run_threaded_server(HelperUsers.get_user_id_by_name("system"))
|
||||||
self.set_backup_status()
|
self.set_backup_status()
|
||||||
|
|
||||||
|
# Return data for webhooks callback
|
||||||
|
base_url = f"{self.helper.get_setting('base_url')}"
|
||||||
|
size = backup_size
|
||||||
|
backup_status = json.loads(
|
||||||
|
HelpersManagement.get_backup_config(backup_id)["status"]
|
||||||
|
)
|
||||||
|
reason = backup_status["message"]
|
||||||
|
if not backup_name:
|
||||||
|
return {
|
||||||
|
"backup_status": "❌",
|
||||||
|
"backup_error": reason,
|
||||||
|
}
|
||||||
|
if backup_size:
|
||||||
|
size = self.helper.human_readable_file_size(backup_size)
|
||||||
|
url = (
|
||||||
|
f"https://{base_url}/api/v2/servers/{self.server_id}"
|
||||||
|
f"/backups/backup/{backup_id}/download/{html.escape(backup_name)}"
|
||||||
|
)
|
||||||
|
if conf["backup_type"] == "snapshot":
|
||||||
|
size = 0
|
||||||
|
url = (
|
||||||
|
f"https://{base_url}/panel/edit_backup?"
|
||||||
|
f"id={self.server_id}&backup_id={backup_id}"
|
||||||
|
)
|
||||||
|
backup_status = json.loads(
|
||||||
|
HelpersManagement.get_backup_config(backup_id)["status"]
|
||||||
|
)
|
||||||
|
last_backup_status = "✅"
|
||||||
|
reason = ""
|
||||||
|
if backup_status["status"] == "Failed":
|
||||||
|
last_backup_status = "❌"
|
||||||
|
reason = backup_status["message"]
|
||||||
|
return {
|
||||||
|
"backup_name": backup_name,
|
||||||
|
"backup_size": size,
|
||||||
|
"backup_link": url,
|
||||||
|
"backup_status": last_backup_status,
|
||||||
|
"backup_error": reason,
|
||||||
|
}
|
||||||
|
|
||||||
def set_backup_status(self):
|
def set_backup_status(self):
|
||||||
backups = HelpersManagement.get_backups_by_server(self.server_id, True)
|
backups = HelpersManagement.get_backups_by_server(self.server_id, True)
|
||||||
alert = False
|
alert = False
|
||||||
|
|||||||
@@ -701,7 +701,9 @@ class PanelHandler(BaseHandler):
|
|||||||
server_id, model=True
|
server_id, model=True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
page_data["triggers"] = WebhookFactory.get_monitored_events()
|
page_data["triggers"] = list(
|
||||||
|
WebhookFactory.get_monitored_events().keys()
|
||||||
|
)
|
||||||
|
|
||||||
def get_banned_players_html():
|
def get_banned_players_html():
|
||||||
banned_players = self.controller.servers.get_banned_players(server_id)
|
banned_players = self.controller.servers.get_banned_players(server_id)
|
||||||
@@ -991,7 +993,7 @@ class PanelHandler(BaseHandler):
|
|||||||
page_data["webhook"]["enabled"] = True
|
page_data["webhook"]["enabled"] = True
|
||||||
|
|
||||||
page_data["providers"] = WebhookFactory.get_supported_providers()
|
page_data["providers"] = WebhookFactory.get_supported_providers()
|
||||||
page_data["triggers"] = WebhookFactory.get_monitored_events()
|
page_data["triggers"] = list(WebhookFactory.get_monitored_events().keys())
|
||||||
|
|
||||||
if not EnumPermissionsServer.CONFIG in page_data["user_permissions"]:
|
if not EnumPermissionsServer.CONFIG in page_data["user_permissions"]:
|
||||||
if not superuser:
|
if not superuser:
|
||||||
@@ -1042,7 +1044,7 @@ class PanelHandler(BaseHandler):
|
|||||||
).split(",")
|
).split(",")
|
||||||
|
|
||||||
page_data["providers"] = WebhookFactory.get_supported_providers()
|
page_data["providers"] = WebhookFactory.get_supported_providers()
|
||||||
page_data["triggers"] = WebhookFactory.get_monitored_events()
|
page_data["triggers"] = list(WebhookFactory.get_monitored_events().keys())
|
||||||
|
|
||||||
if not EnumPermissionsServer.CONFIG in page_data["user_permissions"]:
|
if not EnumPermissionsServer.CONFIG in page_data["user_permissions"]:
|
||||||
if not superuser:
|
if not superuser:
|
||||||
|
|||||||
@@ -119,6 +119,13 @@ config_json_schema = {
|
|||||||
"error": "typeBool",
|
"error": "typeBool",
|
||||||
"fill": True,
|
"fill": True,
|
||||||
},
|
},
|
||||||
|
"base_url": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": (
|
||||||
|
r"^(?:(?:\d{1,3}\.){3}\d{1,3}"
|
||||||
|
r"|(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})(?::\d{1,5})?$"
|
||||||
|
),
|
||||||
|
},
|
||||||
"max_login_attempts": {"type": "integer", "error": "typeInt", "fill": True},
|
"max_login_attempts": {"type": "integer", "error": "typeInt", "fill": True},
|
||||||
"superMFA": {"type": "boolean", "error": "typeBool", "fill": True},
|
"superMFA": {"type": "boolean", "error": "typeBool", "fill": True},
|
||||||
"general_user_log_access": {
|
"general_user_log_access": {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
import logging
|
import logging
|
||||||
|
import datetime
|
||||||
|
import time
|
||||||
import requests
|
import requests
|
||||||
|
from jinja2 import Environment, BaseLoader
|
||||||
|
|
||||||
from app.classes.helpers.helpers import Helpers
|
from app.classes.helpers.helpers import Helpers
|
||||||
|
|
||||||
@@ -16,6 +19,12 @@ class WebhookProvider(ABC):
|
|||||||
ensuring that each provider will have a send method.
|
ensuring that each provider will have a send method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.jinja_env = Environment(
|
||||||
|
loader=BaseLoader(),
|
||||||
|
autoescape=True,
|
||||||
|
)
|
||||||
|
|
||||||
WEBHOOK_USERNAME = "Crafty Webhooks"
|
WEBHOOK_USERNAME = "Crafty Webhooks"
|
||||||
WEBHOOK_PFP_URL = (
|
WEBHOOK_PFP_URL = (
|
||||||
"https://gitlab.com/crafty-controller/crafty-4/-"
|
"https://gitlab.com/crafty-controller/crafty-4/-"
|
||||||
@@ -34,6 +43,55 @@ class WebhookProvider(ABC):
|
|||||||
logger.error(error)
|
logger.error(error)
|
||||||
raise RuntimeError(f"Failed to dispatch notification: {error}") from error
|
raise RuntimeError(f"Failed to dispatch notification: {error}") from error
|
||||||
|
|
||||||
|
def render_template(self, template_str, context):
|
||||||
|
"""
|
||||||
|
Renders a Jinja2 template with the provided context.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
template_str (str): The Jinja2 template string.
|
||||||
|
context (dict): A dictionary containing all the variables needed for
|
||||||
|
rendering the template.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The rendered message.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
template = self.jinja_env.from_string(template_str)
|
||||||
|
return template.render(context)
|
||||||
|
except Exception as error:
|
||||||
|
logger.error(f"Error rendering Jinja2 template: {error}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def add_time_variables(self, event_data):
|
||||||
|
"""
|
||||||
|
Adds various time format variables to the event_data dictionary.
|
||||||
|
|
||||||
|
Adds the following time-related variables to event_data:
|
||||||
|
- time_iso: ISO 8601 formatted datetime (UTC)
|
||||||
|
- time_unix: UNIX timestamp (seconds since epoch)
|
||||||
|
- time_day: Day of month (1-31)
|
||||||
|
- time_month: Month (1-12)
|
||||||
|
- time_year: Full year (e.g., 2025)
|
||||||
|
- time_formatted: Human-readable format (YYYY-MM-DD HH:MM:SS UTC)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event_data (dict): A dictionary containing event information.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: The event_data dictionary with time variables added.
|
||||||
|
"""
|
||||||
|
now_utc = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
unix_timestamp = int(time.time())
|
||||||
|
|
||||||
|
event_data["time_iso"] = now_utc.isoformat().replace("+00:00", "Z")
|
||||||
|
event_data["time_unix"] = unix_timestamp
|
||||||
|
event_data["time_day"] = now_utc.day
|
||||||
|
event_data["time_month"] = now_utc.month
|
||||||
|
event_data["time_year"] = now_utc.year
|
||||||
|
event_data["time_formatted"] = now_utc.strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||||
|
|
||||||
|
return event_data
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def send(self, server_name, title, url, message, **kwargs):
|
def send(self, server_name, title, url, message_template, event_data, **kwargs):
|
||||||
"""Abstract method that derived classes will implement for sending webhooks."""
|
"""Abstract method that derived classes will implement for sending webhooks."""
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class DiscordWebhook(WebhookProvider):
|
|||||||
|
|
||||||
return payload, headers
|
return payload, headers
|
||||||
|
|
||||||
def send(self, server_name, title, url, message, **kwargs):
|
def send(self, server_name, title, url, message_template, event_data, **kwargs):
|
||||||
"""
|
"""
|
||||||
Sends a Discord webhook notification using the given details.
|
Sends a Discord webhook notification using the given details.
|
||||||
|
|
||||||
@@ -74,6 +74,7 @@ class DiscordWebhook(WebhookProvider):
|
|||||||
Raises:
|
Raises:
|
||||||
Exception: If there's an error in dispatching the webhook.
|
Exception: If there's an error in dispatching the webhook.
|
||||||
"""
|
"""
|
||||||
|
message = self.render_template(message_template, event_data)
|
||||||
color = kwargs.get("color", "#005cd1") # Default to a color if not provided.
|
color = kwargs.get("color", "#005cd1") # Default to a color if not provided.
|
||||||
bot_name = kwargs.get("bot_name", self.WEBHOOK_USERNAME)
|
bot_name = kwargs.get("bot_name", self.WEBHOOK_USERNAME)
|
||||||
payload, headers = self._construct_discord_payload(
|
payload, headers = self._construct_discord_payload(
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class MattermostWebhook(WebhookProvider):
|
|||||||
|
|
||||||
return payload, headers
|
return payload, headers
|
||||||
|
|
||||||
def send(self, server_name, title, url, message, **kwargs):
|
def send(self, server_name, title, url, message_template, event_data, **kwargs):
|
||||||
"""
|
"""
|
||||||
Sends a Mattermost webhook notification using the given details.
|
Sends a Mattermost webhook notification using the given details.
|
||||||
|
|
||||||
@@ -52,7 +52,8 @@ class MattermostWebhook(WebhookProvider):
|
|||||||
server_name (str): The name of the server triggering the notification.
|
server_name (str): The name of the server triggering the notification.
|
||||||
title (str): The title for the notification message.
|
title (str): The title for the notification message.
|
||||||
url (str): The webhook URL to send the notification to.
|
url (str): The webhook URL to send the notification to.
|
||||||
message (str): The main content or body of the notification message.
|
message_template (str): The Jinja2 template for the message body.
|
||||||
|
event_data (dict): A dictionary containing variables for template rendering.
|
||||||
bot_name (str): Override for the Webhook's name set on creation, see note!
|
bot_name (str): Override for the Webhook's name set on creation, see note!
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -67,6 +68,7 @@ class MattermostWebhook(WebhookProvider):
|
|||||||
- Mattermost's `config.json` setting is `"EnablePostUsernameOverride": true`
|
- Mattermost's `config.json` setting is `"EnablePostUsernameOverride": true`
|
||||||
- Mattermost's `config.json` setting is `"EnablePostIconOverride": true`
|
- Mattermost's `config.json` setting is `"EnablePostIconOverride": true`
|
||||||
"""
|
"""
|
||||||
|
message = self.render_template(message_template, event_data)
|
||||||
bot_name = kwargs.get("bot_name", self.WEBHOOK_USERNAME)
|
bot_name = kwargs.get("bot_name", self.WEBHOOK_USERNAME)
|
||||||
payload, headers = self._construct_mattermost_payload(
|
payload, headers = self._construct_mattermost_payload(
|
||||||
server_name, title, message, bot_name
|
server_name, title, message, bot_name
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class SlackWebhook(WebhookProvider):
|
|||||||
|
|
||||||
return payload, headers
|
return payload, headers
|
||||||
|
|
||||||
def send(self, server_name, title, url, message, **kwargs):
|
def send(self, server_name, title, url, message_template, event_data, **kwargs):
|
||||||
"""
|
"""
|
||||||
Sends a Slack webhook notification using the given details.
|
Sends a Slack webhook notification using the given details.
|
||||||
|
|
||||||
@@ -78,7 +78,8 @@ class SlackWebhook(WebhookProvider):
|
|||||||
server_name (str): The name of the server triggering the notification.
|
server_name (str): The name of the server triggering the notification.
|
||||||
title (str): The title for the notification message.
|
title (str): The title for the notification message.
|
||||||
url (str): The webhook URL to send the notification to.
|
url (str): The webhook URL to send the notification to.
|
||||||
message (str): The main content or body of the notification message.
|
message_template (str): The Jinja2 template for the message body.
|
||||||
|
event_data (dict): A dictionary containing variables for template rendering.
|
||||||
color (str, optional): The color code for the blocks's colour accent.
|
color (str, optional): The color code for the blocks's colour accent.
|
||||||
Defaults to a pretty blue if not provided.
|
Defaults to a pretty blue if not provided.
|
||||||
bot_name (str): Override for the Webhook's name set on creation, (not working).
|
bot_name (str): Override for the Webhook's name set on creation, (not working).
|
||||||
@@ -90,6 +91,7 @@ class SlackWebhook(WebhookProvider):
|
|||||||
Raises:
|
Raises:
|
||||||
Exception: If there's an error in dispatching the webhook.
|
Exception: If there's an error in dispatching the webhook.
|
||||||
"""
|
"""
|
||||||
|
message = self.render_template(message_template, event_data)
|
||||||
color = kwargs.get("color", "#005cd1") # Default to a color if not provided.
|
color = kwargs.get("color", "#005cd1") # Default to a color if not provided.
|
||||||
bot_name = kwargs.get("bot_name", self.WEBHOOK_USERNAME)
|
bot_name = kwargs.get("bot_name", self.WEBHOOK_USERNAME)
|
||||||
payload, headers = self._construct_slack_payload(
|
payload, headers = self._construct_slack_payload(
|
||||||
|
|||||||
@@ -101,19 +101,19 @@ class TeamsWebhook(WebhookProvider):
|
|||||||
|
|
||||||
return payload, headers
|
return payload, headers
|
||||||
|
|
||||||
def send(self, server_name, title, url, message, **kwargs):
|
def send(self, server_name, title, url, message_template, event_data, **kwargs):
|
||||||
"""
|
"""
|
||||||
Sends a Teams Adaptive card notification using the given details.
|
Sends a Teams Adaptive card notification using the given details.
|
||||||
|
|
||||||
The method constructs and dispatches a payload suitable for
|
The method constructs and dispatches a payload suitable for
|
||||||
Discords's webhook system.
|
Teams's webhook system.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
server_name (str): The name of the server triggering the notification.
|
server_name (str): The name of the server triggering the notification.
|
||||||
title (str): The title for the notification message.
|
title (str): The title for the notification message.
|
||||||
url (str): The webhook URL to send the notification to.
|
url (str): The webhook URL to send the notification to.
|
||||||
message (str): The main content or body of the notification message.
|
message_template (str): The Jinja2 template for the message body.
|
||||||
Defaults to a pretty blue if not provided.
|
event_data (dict): A dictionary containing variables for template rendering.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: "Dispatch successful!" if the message is sent successfully, otherwise an
|
str: "Dispatch successful!" if the message is sent successfully, otherwise an
|
||||||
@@ -122,5 +122,6 @@ class TeamsWebhook(WebhookProvider):
|
|||||||
Raises:
|
Raises:
|
||||||
Exception: If there's an error in dispatching the webhook.
|
Exception: If there's an error in dispatching the webhook.
|
||||||
"""
|
"""
|
||||||
|
message = self.render_template(message_template, event_data)
|
||||||
payload, headers = self._construct_teams_payload(server_name, title, message)
|
payload, headers = self._construct_teams_payload(server_name, title, message)
|
||||||
return self._send_request(url, payload, headers)
|
return self._send_request(url, payload, headers)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class WebhookFactory:
|
|||||||
to manage the available providers.
|
to manage the available providers.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
- _registry (dict): A dictionary mapping provider names to their classes.
|
- _registry (dict): A dictionary mapping provider names to their classes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_registry = {
|
_registry = {
|
||||||
@@ -32,18 +32,18 @@ class WebhookFactory:
|
|||||||
provided arguments. If the provider is not recognized, a ValueError is raised.
|
provided arguments. If the provider is not recognized, a ValueError is raised.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
- provider_name (str): The name of the desired webhook provider.
|
- provider_name (str): The name of the desired webhook provider.
|
||||||
|
|
||||||
Additional arguments supported that we may use for if a provider
|
Additional arguments supported that we may use for if a provider
|
||||||
requires initialization:
|
requires initialization:
|
||||||
- *args: Positional arguments to pass to the provider's constructor.
|
- *args: Positional arguments to pass to the provider's constructor.
|
||||||
- **kwargs: Keyword arguments to pass to the provider's constructor.
|
- **kwargs: Keyword arguments to pass to the provider's constructor.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
WebhookProvider: An instance of the desired webhook provider.
|
WebhookProvider: An instance of the desired webhook provider.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If the specified provider name is not recognized.
|
ValueError: If the specified provider name is not recognized.
|
||||||
"""
|
"""
|
||||||
if provider_name not in cls._registry:
|
if provider_name not in cls._registry:
|
||||||
raise ValueError(f"Provider {provider_name} is not supported.")
|
raise ValueError(f"Provider {provider_name} is not supported.")
|
||||||
@@ -58,7 +58,7 @@ class WebhookFactory:
|
|||||||
currently registered in the factory's registry.
|
currently registered in the factory's registry.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[str]: A list of supported provider names.
|
List[str]: A list of supported provider names.
|
||||||
"""
|
"""
|
||||||
return list(cls._registry.keys())
|
return list(cls._registry.keys())
|
||||||
|
|
||||||
@@ -68,17 +68,45 @@ class WebhookFactory:
|
|||||||
Retrieves the list of supported events for monitoring.
|
Retrieves the list of supported events for monitoring.
|
||||||
|
|
||||||
This method provides a list of common server events that the webhook system can
|
This method provides a list of common server events that the webhook system can
|
||||||
monitor and notify about.
|
monitor and notify about. Along with the available `event_data` vars for use
|
||||||
|
on the frontend.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[str]: A list of supported monitored actions.
|
dict: A dictionary where each key is an event name and the value is a
|
||||||
|
dictionary containing a list of `variables` for that event.
|
||||||
|
These variables are intended for use in the frontend to show whats
|
||||||
|
available.
|
||||||
"""
|
"""
|
||||||
return [
|
# Common variables for all events
|
||||||
"start_server",
|
common_vars = [
|
||||||
"stop_server",
|
"server_name",
|
||||||
"crash_detected",
|
"server_id",
|
||||||
"backup_server",
|
"event_type",
|
||||||
"jar_update",
|
"source_type",
|
||||||
"send_command",
|
"source_id",
|
||||||
"kill",
|
"source_name",
|
||||||
|
"time_iso",
|
||||||
|
"time_unix",
|
||||||
|
"time_day",
|
||||||
|
"time_month",
|
||||||
|
"time_year",
|
||||||
|
"time_formatted",
|
||||||
]
|
]
|
||||||
|
return {
|
||||||
|
"start_server": {"variables": common_vars},
|
||||||
|
"stop_server": {"variables": common_vars},
|
||||||
|
"crash_detected": {"variables": common_vars},
|
||||||
|
"backup_server": {
|
||||||
|
"variables": common_vars
|
||||||
|
+ [
|
||||||
|
"backup_name",
|
||||||
|
"backup_link",
|
||||||
|
"backup_size",
|
||||||
|
"backup_status",
|
||||||
|
"backup_error",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"jar_update": {"variables": common_vars},
|
||||||
|
"send_command": {"variables": common_vars + ["command"]},
|
||||||
|
"kill": {"variables": common_vars + ["reason"]},
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"major": 4,
|
"major": 4,
|
||||||
"minor": 5,
|
"minor": 6,
|
||||||
"sub": 6
|
"sub": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.jinja2 {
|
||||||
|
background-color: var(--card-banner-bg);
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
outline: 1px solid var(--outline);
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: -10px;
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/css/bootstrap-select.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/css/bootstrap-select.min.css">
|
||||||
|
<link rel="stylesheet" href="/static/assets/css/partial/crafty-webhooks.css">
|
||||||
<div class="content-wrapper">
|
<div class="content-wrapper">
|
||||||
|
|
||||||
<!-- Page Title Header Starts-->
|
<!-- Page Title Header Starts-->
|
||||||
@@ -83,10 +84,11 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="body">{{ translate('webhooks', 'webhook_body', data['lang']) }}</label>
|
<label for="body">{{ translate('webhooks', 'webhook_body', data['lang']) }} <br><div class="jinja2"><small>{{translate("webhooks", "jinja2", data['lang'])}} <br>
|
||||||
<textarea id="body-input" name="body" rows="4" cols="50">
|
<a href="https://docs.craftycontrol.com/pages/user-guide/webhooks/#jinja2-dynamic-variables-461" target="_blank">{{translate("webhooks", "documentation", data['lang'])}}</a></small></div></label>
|
||||||
{{ data["webhook"]["body"] }}
|
<textarea id="body-input" name="body" rows="4" cols="50">
|
||||||
</textarea>
|
{{ data["webhook"]["body"] }}
|
||||||
|
</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="bot_name">{{ translate('webhooks', 'color' , data['lang']) }}</label>
|
<label for="bot_name">{{ translate('webhooks', 'color' , data['lang']) }}</label>
|
||||||
|
|||||||
@@ -790,9 +790,11 @@
|
|||||||
"bot_name": "Bot Name",
|
"bot_name": "Bot Name",
|
||||||
"color": "Select Color Accent",
|
"color": "Select Color Accent",
|
||||||
"crash_detected": "Server Crashed",
|
"crash_detected": "Server Crashed",
|
||||||
|
"documentation": "Please reference this documentation for information regarding dynamic variables.",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
"jar_update": "Server Executable Updated",
|
"jar_update": "Server Executable Updated",
|
||||||
|
"jinja2": "Crafty's webhook engine takes advantage of Jinja2 for dynamic message rendering.",
|
||||||
"kill": "Server Killed",
|
"kill": "Server Killed",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"new": "New Webhook",
|
"new": "New Webhook",
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
|
aiofiles==24.1.0
|
||||||
|
anyio==4.9.0
|
||||||
apscheduler==3.10.4
|
apscheduler==3.10.4
|
||||||
argon2-cffi==23.1.0
|
argon2-cffi==23.1.0
|
||||||
cached_property==1.5.2
|
cached_property==1.5.2
|
||||||
colorama==0.4.6
|
colorama==0.4.6
|
||||||
croniter==1.4.1
|
croniter==1.4.1
|
||||||
cryptography==44.0.1
|
cryptography==44.0.1
|
||||||
|
httpx==0.28.1
|
||||||
|
jinja2==3.1.6
|
||||||
|
jsonschema==4.19.1
|
||||||
libgravatar==1.0.4
|
libgravatar==1.0.4
|
||||||
nh3==0.2.14
|
nh3==0.2.14
|
||||||
|
orjson==3.9.15
|
||||||
packaging==23.2
|
packaging==23.2
|
||||||
peewee==3.13
|
peewee==3.13
|
||||||
|
pillow==10.4.0
|
||||||
|
prometheus-client==0.17.1
|
||||||
psutil==5.9.5
|
psutil==5.9.5
|
||||||
pyjwt==2.8.0
|
pyjwt==2.8.0
|
||||||
|
pyotp==2.9.0
|
||||||
PyYAML==6.0.1
|
PyYAML==6.0.1
|
||||||
requests==2.32.4
|
requests==2.32.4
|
||||||
termcolor==1.1
|
termcolor==1.1
|
||||||
tornado==6.5
|
tornado==6.5
|
||||||
tzlocal==5.1
|
tzlocal==5.1
|
||||||
jsonschema==4.19.1
|
|
||||||
orjson==3.9.15
|
|
||||||
prometheus-client==0.17.1
|
|
||||||
pyotp==2.9.0
|
|
||||||
pillow==10.4.0
|
|
||||||
httpx==0.28.1
|
|
||||||
aiofiles==24.1.0
|
|
||||||
anyio==4.9.0
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ sonar.organization=crafty-controller
|
|||||||
|
|
||||||
# This is the name and version displayed in the SonarCloud UI.
|
# This is the name and version displayed in the SonarCloud UI.
|
||||||
sonar.projectName=Crafty 4
|
sonar.projectName=Crafty 4
|
||||||
sonar.projectVersion=4.5.6
|
sonar.projectVersion=4.6.1
|
||||||
sonar.python.version=3.9, 3.10, 3.11
|
sonar.python.version=3.10, 3.11, 3.12
|
||||||
sonar.exclusions=app/migrations/**, app/frontend/static/assets/vendors/**
|
sonar.exclusions=app/migrations/**, app/frontend/static/assets/vendors/**
|
||||||
|
|
||||||
# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
|
# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
|
||||||
|
|||||||
Reference in New Issue
Block a user