diff --git a/.pylintrc b/.pylintrc index e4e3f119..c0aae8e6 100644 --- a/.pylintrc +++ b/.pylintrc @@ -52,7 +52,7 @@ persistent=yes # Minimum Python version to use for version dependent checks. Will default to # 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 # user-friendly hints instead of false-positive error messages. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f0a6d6f..eb9bc75b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog -## --- [4.5.6] - 2025/TBD +## --- [4.6.1] - 2025/TBD ### New features -TBD +- Jinja2 Dynamic Variables for Webhook Notifications ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/757)) ### 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)) - Use asyncio locks to limit upload handler race condition ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/907)) diff --git a/README.md b/README.md index 636fa6df..349150b2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![Crafty Logo](app/frontend/static/assets/images/logo_long.svg)](https://craftycontrol.com) -# Crafty Controller 4.5.6 +# Crafty Controller 4.6.1 > Python based Control Panel for your Minecraft Server ## 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 ``` ```yml -version: '3' - services: crafty: container_name: crafty_container diff --git a/app/classes/helpers/helpers.py b/app/classes/helpers/helpers.py index 78ef3a3a..e90e8468 100644 --- a/app/classes/helpers/helpers.py +++ b/app/classes/helpers/helpers.py @@ -69,6 +69,7 @@ MASTER_CONFIG = { "max_login_attempts": 3, "superMFA": False, "general_user_log_access": False, + "base_url": "127.0.0.1:8443", } CONFIG_CATEGORIES = { @@ -80,6 +81,7 @@ CONFIG_CATEGORIES = { "disabled_language_files", "big_bucket_repo", "enable_user_self_delete", + "base_url", ], "security": [ "allow_nsfw_profile_pictures", diff --git a/app/classes/shared/backup_mgr.py b/app/classes/shared/backup_mgr.py index ffa8ffdc..3563b34f 100644 --- a/app/classes/shared/backup_mgr.py +++ b/app/classes/shared/backup_mgr.py @@ -27,6 +27,8 @@ logger = logging.getLogger(__name__) class BackupManager: 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): 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. Args: backup_config (_type_): _description_ server (_type_): Server object to backup """ + # Notify users of backup starting logger.info(f"Starting server {server.name} (ID {server.server_id}) backup") server_users = PermissionsServers.get_server_user_list(server.server_id) @@ -114,14 +117,37 @@ class BackupManager: ).format(server.name), ) time.sleep(3) - + size = False # Start the backup 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: - 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. backup_location = os.path.join( @@ -131,7 +157,7 @@ class BackupManager: # Check if the backup location even exists. if not backup_location: Console.critical("No backup path found. Canceling") - return None + return False self.helper.ensure_dir_exists(backup_location) @@ -195,8 +221,10 @@ class BackupManager: {"status": json.dumps({"status": "Standby", "message": ""})}, ) time.sleep(5) + return Path(backup_filename).name except Exception as e: self.fail_backup(e, backup_config, server) + return False @staticmethod def fail_backup(why: Exception, backup_config: dict, server) -> None: @@ -234,8 +262,7 @@ class BackupManager: {"status": json.dumps({"status": "Failed", "message": f"{why}"})}, ) - @staticmethod - def list_backups(backup_config: dict, server_id) -> list: + def list_backups(self, backup_config: dict, server_id) -> list: if not backup_config: logger.info( f"Error putting backup file list for server with ID: {server_id}" @@ -268,7 +295,7 @@ class BackupManager: "size": "", } for f in files - if f["path"].endswith(".manifest") + if f["path"].endswith(self.SNAPSHOT_SUFFIX) ] return [ { @@ -279,7 +306,7 @@ class BackupManager: "size": f["size"], } for f in files - if f["path"].endswith(".zip") + if f["path"].endswith(self.ARCHIVE_SUFFIX) ] def remove_old_backups(self, backup_config, server): @@ -297,7 +324,7 @@ class BackupManager: logger.info(f"Removing old backup '{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 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+") except OSError as why: self.fail_backup(why, backup_config, server) - return + return False # Write manifest file version. manifest_file.write("00\n") @@ -359,7 +386,7 @@ class BackupManager: manifest_file.close() backup_manifest_path.unlink(missing_ok=True) self.fail_backup(why, backup_config, server) - return + return False # Write saved file into manifest. manifest_file.write( @@ -373,6 +400,13 @@ class BackupManager: 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( self, backup_config: {str}, backup_manifest_filename: str, server ) -> None: diff --git a/app/classes/shared/server.py b/app/classes/shared/server.py index b5dc31d9..419839c0 100644 --- a/app/classes/shared/server.py +++ b/app/classes/shared/server.py @@ -7,7 +7,7 @@ import time import datetime import base64 import threading -import logging.config +import logging import subprocess import html import glob @@ -54,31 +54,72 @@ def callback(called_func): res = None logger.debug("Checking for callbacks") try: - res = called_func(*args, **kwargs) + res = called_func(*args, **kwargs) # Calls and runs the function finally: - events = WebhookFactory.get_monitored_events() - if called_func.__name__ in events: + event_type = called_func.__name__ + + # 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( args[0].server_id, True ) for swebhook in server_webhooks: - if called_func.__name__ in str(swebhook.trigger).split(","): + if event_type in str(swebhook.trigger).split(","): logger.info( - f"Found callback for event {called_func.__name__}" + f"Found callback for event {event_type}" f" for server {args[0].server_id}" ) webhook = HelpersWebhooks.get_webhook_by_id(swebhook.id) webhook_provider = WebhookFactory.create_provider( 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: webhook_provider.send( - bot_name=webhook["bot_name"], server_name=args[0].name, title=webhook["name"], url=webhook["url"], - message=webhook["body"], + message_template=webhook["body"], + event_data=event_data, color=webhook["color"], + bot_name=webhook["bot_name"], ) return res @@ -1205,7 +1246,7 @@ class ServerInstance: logger.info(f"Backup Thread started for server {self.settings['server_name']}.") @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") server_users = PermissionsServers.get_server_user_list(self.server_id) # Alert the start of the backup to the authorized users. @@ -1221,7 +1262,15 @@ class ServerInstance: # Get the backup config 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) # Adjust the location to include the backup ID for destination. backup_location = os.path.join(conf["backup_location"], conf["backup_id"]) @@ -1229,7 +1278,16 @@ class ServerInstance: # Check if the backup location even exists. if not backup_location: 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"]: logger.debug( "Found running server and send command option. Sending command" @@ -1237,7 +1295,7 @@ class ServerInstance: self.send_command(conf["before"]) # Pause to let command run time.sleep(5) - self.backup_mgr.backup_starter(conf, self) + backup_name, backup_size = self.backup_mgr.backup_starter(conf, self) if conf["after"]: self.send_command(conf["after"]) 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.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): backups = HelpersManagement.get_backups_by_server(self.server_id, True) alert = False diff --git a/app/classes/web/panel_handler.py b/app/classes/web/panel_handler.py index 91a451e9..17c1529c 100644 --- a/app/classes/web/panel_handler.py +++ b/app/classes/web/panel_handler.py @@ -701,7 +701,9 @@ class PanelHandler(BaseHandler): 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(): banned_players = self.controller.servers.get_banned_players(server_id) @@ -991,7 +993,7 @@ class PanelHandler(BaseHandler): page_data["webhook"]["enabled"] = True 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 superuser: @@ -1042,7 +1044,7 @@ class PanelHandler(BaseHandler): ).split(",") 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 superuser: diff --git a/app/classes/web/routes/api/crafty/config/index.py b/app/classes/web/routes/api/crafty/config/index.py index d6ed25cb..c82e8998 100644 --- a/app/classes/web/routes/api/crafty/config/index.py +++ b/app/classes/web/routes/api/crafty/config/index.py @@ -119,6 +119,13 @@ config_json_schema = { "error": "typeBool", "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}, "superMFA": {"type": "boolean", "error": "typeBool", "fill": True}, "general_user_log_access": { diff --git a/app/classes/web/webhooks/base_webhook.py b/app/classes/web/webhooks/base_webhook.py index 28bcdac7..e031903e 100644 --- a/app/classes/web/webhooks/base_webhook.py +++ b/app/classes/web/webhooks/base_webhook.py @@ -1,6 +1,9 @@ from abc import ABC, abstractmethod import logging +import datetime +import time import requests +from jinja2 import Environment, BaseLoader from app.classes.helpers.helpers import Helpers @@ -16,6 +19,12 @@ class WebhookProvider(ABC): 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_PFP_URL = ( "https://gitlab.com/crafty-controller/crafty-4/-" @@ -34,6 +43,55 @@ class WebhookProvider(ABC): logger.error(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 - 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.""" diff --git a/app/classes/web/webhooks/discord_webhook.py b/app/classes/web/webhooks/discord_webhook.py index eebe38aa..607e9bc3 100644 --- a/app/classes/web/webhooks/discord_webhook.py +++ b/app/classes/web/webhooks/discord_webhook.py @@ -51,7 +51,7 @@ class DiscordWebhook(WebhookProvider): 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. @@ -74,6 +74,7 @@ class DiscordWebhook(WebhookProvider): Raises: 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. bot_name = kwargs.get("bot_name", self.WEBHOOK_USERNAME) payload, headers = self._construct_discord_payload( diff --git a/app/classes/web/webhooks/mattermost_webhook.py b/app/classes/web/webhooks/mattermost_webhook.py index 3dc97c05..8fb6e796 100644 --- a/app/classes/web/webhooks/mattermost_webhook.py +++ b/app/classes/web/webhooks/mattermost_webhook.py @@ -41,7 +41,7 @@ class MattermostWebhook(WebhookProvider): 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. @@ -52,7 +52,8 @@ class MattermostWebhook(WebhookProvider): server_name (str): The name of the server triggering the notification. title (str): The title for the notification message. 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! Returns: @@ -67,6 +68,7 @@ class MattermostWebhook(WebhookProvider): - Mattermost's `config.json` setting is `"EnablePostUsernameOverride": 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) payload, headers = self._construct_mattermost_payload( server_name, title, message, bot_name diff --git a/app/classes/web/webhooks/slack_webhook.py b/app/classes/web/webhooks/slack_webhook.py index cd7c71bf..960a25e1 100644 --- a/app/classes/web/webhooks/slack_webhook.py +++ b/app/classes/web/webhooks/slack_webhook.py @@ -67,7 +67,7 @@ class SlackWebhook(WebhookProvider): 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. @@ -78,7 +78,8 @@ class SlackWebhook(WebhookProvider): server_name (str): The name of the server triggering the notification. title (str): The title for the notification message. 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. Defaults to a pretty blue if not provided. bot_name (str): Override for the Webhook's name set on creation, (not working). @@ -90,6 +91,7 @@ class SlackWebhook(WebhookProvider): Raises: 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. bot_name = kwargs.get("bot_name", self.WEBHOOK_USERNAME) payload, headers = self._construct_slack_payload( diff --git a/app/classes/web/webhooks/teams_adaptive_webhook.py b/app/classes/web/webhooks/teams_adaptive_webhook.py index 6342de65..37a4dc04 100644 --- a/app/classes/web/webhooks/teams_adaptive_webhook.py +++ b/app/classes/web/webhooks/teams_adaptive_webhook.py @@ -101,19 +101,19 @@ class TeamsWebhook(WebhookProvider): 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. The method constructs and dispatches a payload suitable for - Discords's webhook system. + Teams's webhook system. Parameters: server_name (str): The name of the server triggering the notification. title (str): The title for the notification message. url (str): The webhook URL to send the notification to. - message (str): The main content or body of the notification message. - Defaults to a pretty blue if not provided. + message_template (str): The Jinja2 template for the message body. + event_data (dict): A dictionary containing variables for template rendering. Returns: str: "Dispatch successful!" if the message is sent successfully, otherwise an @@ -122,5 +122,6 @@ class TeamsWebhook(WebhookProvider): Raises: 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) return self._send_request(url, payload, headers) diff --git a/app/classes/web/webhooks/webhook_factory.py b/app/classes/web/webhooks/webhook_factory.py index 9fe2c752..6e873323 100644 --- a/app/classes/web/webhooks/webhook_factory.py +++ b/app/classes/web/webhooks/webhook_factory.py @@ -13,7 +13,7 @@ class WebhookFactory: to manage the available providers. Attributes: - - _registry (dict): A dictionary mapping provider names to their classes. + - _registry (dict): A dictionary mapping provider names to their classes. """ _registry = { @@ -32,18 +32,18 @@ class WebhookFactory: provided arguments. If the provider is not recognized, a ValueError is raised. 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 requires initialization: - - *args: Positional arguments to pass to the provider's constructor. - - **kwargs: Keyword 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. Returns: - WebhookProvider: An instance of the desired webhook provider. + WebhookProvider: An instance of the desired webhook provider. 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: raise ValueError(f"Provider {provider_name} is not supported.") @@ -58,7 +58,7 @@ class WebhookFactory: currently registered in the factory's registry. Returns: - List[str]: A list of supported provider names. + List[str]: A list of supported provider names. """ return list(cls._registry.keys()) @@ -68,17 +68,45 @@ class WebhookFactory: Retrieves the list of supported events for monitoring. 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: - 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 [ - "start_server", - "stop_server", - "crash_detected", - "backup_server", - "jar_update", - "send_command", - "kill", + # Common variables for all events + common_vars = [ + "server_name", + "server_id", + "event_type", + "source_type", + "source_id", + "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"]}, + } diff --git a/app/config/version.json b/app/config/version.json index bb1da134..4c45077b 100644 --- a/app/config/version.json +++ b/app/config/version.json @@ -1,5 +1,5 @@ { "major": 4, - "minor": 5, - "sub": 6 + "minor": 6, + "sub": 1 } diff --git a/app/frontend/static/assets/css/partial/crafty-webhooks.css b/app/frontend/static/assets/css/partial/crafty-webhooks.css new file mode 100644 index 00000000..c21cfd7f --- /dev/null +++ b/app/frontend/static/assets/css/partial/crafty-webhooks.css @@ -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; +} \ No newline at end of file diff --git a/app/frontend/templates/panel/server_webhook_edit.html b/app/frontend/templates/panel/server_webhook_edit.html index c0e351cf..a82eefcd 100644 --- a/app/frontend/templates/panel/server_webhook_edit.html +++ b/app/frontend/templates/panel/server_webhook_edit.html @@ -7,6 +7,7 @@ {% block content %} +
@@ -83,10 +84,11 @@
- - + +
diff --git a/app/translations/en_EN.json b/app/translations/en_EN.json index 6c29f479..7cd22112 100644 --- a/app/translations/en_EN.json +++ b/app/translations/en_EN.json @@ -790,9 +790,11 @@ "bot_name": "Bot Name", "color": "Select Color Accent", "crash_detected": "Server Crashed", + "documentation": "Please reference this documentation for information regarding dynamic variables.", "edit": "Edit", "enabled": "Enabled", "jar_update": "Server Executable Updated", + "jinja2": "Crafty's webhook engine takes advantage of Jinja2 for dynamic message rendering.", "kill": "Server Killed", "name": "Name", "new": "New Webhook", diff --git a/requirements.txt b/requirements.txt index d59a884d..59905ff5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,26 +1,26 @@ - +aiofiles==24.1.0 +anyio==4.9.0 apscheduler==3.10.4 argon2-cffi==23.1.0 cached_property==1.5.2 colorama==0.4.6 croniter==1.4.1 cryptography==44.0.1 +httpx==0.28.1 +jinja2==3.1.6 +jsonschema==4.19.1 libgravatar==1.0.4 nh3==0.2.14 +orjson==3.9.15 packaging==23.2 peewee==3.13 +pillow==10.4.0 +prometheus-client==0.17.1 psutil==5.9.5 pyjwt==2.8.0 +pyotp==2.9.0 PyYAML==6.0.1 requests==2.32.4 termcolor==1.1 tornado==6.5 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 diff --git a/sonar-project.properties b/sonar-project.properties index 06f48266..7c73fc67 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,8 +3,8 @@ sonar.organization=crafty-controller # This is the name and version displayed in the SonarCloud UI. sonar.projectName=Crafty 4 -sonar.projectVersion=4.5.6 -sonar.python.version=3.9, 3.10, 3.11 +sonar.projectVersion=4.6.1 +sonar.python.version=3.10, 3.11, 3.12 sonar.exclusions=app/migrations/**, app/frontend/static/assets/vendors/** # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.