mirror of
https://gitlab.com/crafty-controller/crafty-4.git
synced 2024-08-30 18:23:09 +00:00
Refactor Webhooks to WebhookFactory
To save having a massive class lets go modular
This commit is contained in:
parent
1ea0d692d0
commit
409d7618dd
@ -25,7 +25,7 @@ from app.classes.controllers.roles_controller import RolesController
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.shared.main_models import DatabaseShortcuts
|
||||
from app.classes.web.base_handler import BaseHandler
|
||||
from app.classes.web.webhook_handler import WebhookHandler
|
||||
from app.classes.web.webhooks.webhook_factory import WebhookFactory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -345,7 +345,9 @@ class PanelHandler(BaseHandler):
|
||||
) as credits_default_local:
|
||||
try:
|
||||
remote = requests.get(
|
||||
"https://craftycontrol.com/credits-v2", allow_redirects=True
|
||||
"https://craftycontrol.com/credits-v2",
|
||||
allow_redirects=True,
|
||||
timeout=10,
|
||||
)
|
||||
credits_dict: dict = remote.json()
|
||||
if not credits_dict["staff"]:
|
||||
@ -1070,8 +1072,8 @@ class PanelHandler(BaseHandler):
|
||||
page_data["webhook"]["body"] = ""
|
||||
page_data["webhook"]["enabled"] = True
|
||||
|
||||
page_data["providers"] = WebhookHandler.get_providers()
|
||||
page_data["triggers"] = WebhookHandler.get_monitored_actions()
|
||||
page_data["providers"] = WebhookFactory.get_supported_providers()
|
||||
page_data["triggers"] = WebhookFactory.get_monitored_events()
|
||||
|
||||
if not EnumPermissionsServer.CONFIG in page_data["user_permissions"]:
|
||||
if not superuser:
|
||||
@ -1121,8 +1123,8 @@ class PanelHandler(BaseHandler):
|
||||
page_data["webhook"]["trigger"]
|
||||
).split(",")
|
||||
|
||||
page_data["providers"] = WebhookHandler.get_providers()
|
||||
page_data["triggers"] = WebhookHandler.get_monitored_actions()
|
||||
page_data["providers"] = WebhookFactory.get_supported_providers
|
||||
page_data["triggers"] = WebhookFactory.get_monitored_events
|
||||
|
||||
if not EnumPermissionsServer.CONFIG in page_data["user_permissions"]:
|
||||
if not superuser:
|
||||
|
@ -6,14 +6,17 @@ import logging
|
||||
from jsonschema import ValidationError, validate
|
||||
from app.classes.models.server_permissions import EnumPermissionsServer
|
||||
from app.classes.web.base_api_handler import BaseApiHandler
|
||||
from app.classes.web.webhook_handler import WebhookHandler
|
||||
from app.classes.web.webhooks.webhook_factory import WebhookFactory
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
new_webhook_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"webhook_type": {"type": "string", "enum": WebhookHandler.get_providers()},
|
||||
"webhook_type": {
|
||||
"type": "string",
|
||||
"enum": WebhookFactory.get_supported_providers(),
|
||||
},
|
||||
"name": {"type": "string"},
|
||||
"url": {"type": "string"},
|
||||
"bot_name": {"type": "string"},
|
||||
|
@ -3,11 +3,9 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from croniter import croniter
|
||||
from jsonschema import ValidationError, validate
|
||||
from app.classes.models.server_permissions import EnumPermissionsServer
|
||||
from app.classes.web.webhook_handler import WebhookHandler
|
||||
|
||||
from app.classes.web.webhooks.webhook_factory import WebhookFactory
|
||||
from app.classes.web.base_api_handler import BaseApiHandler
|
||||
|
||||
|
||||
@ -16,7 +14,10 @@ logger = logging.getLogger(__name__)
|
||||
webhook_patch_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"webhook_type": {"type": "string", "enum": WebhookHandler.get_providers()},
|
||||
"webhook_type": {
|
||||
"type": "string",
|
||||
"enum": WebhookFactory.get_supported_providers(),
|
||||
},
|
||||
"name": {"type": "string"},
|
||||
"url": {"type": "string"},
|
||||
"bot_name": {"type": "string"},
|
||||
@ -168,8 +169,13 @@ class ApiServersServerWebhooksManagementIndexHandler(BaseApiHandler):
|
||||
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||
webhook = self.controller.management.get_webhook_by_id(webhook_id)
|
||||
try:
|
||||
WebhookHandler.send_discord_webhook(
|
||||
webhook["bot_name"], webhook["url"], webhook["body"], 880808
|
||||
webhook_provider = WebhookFactory.create_provider(webhook["webhook_type"])
|
||||
webhook_provider.send(
|
||||
server_name=server_id, # TODO get actual server name
|
||||
title="Tickle Test Webhook",
|
||||
url=webhook["url"],
|
||||
message=webhook["body"],
|
||||
color=4915409, # Prestigious purple!
|
||||
)
|
||||
except Exception as e:
|
||||
self.finish_json(500, {"status": "error", "error": str(e)})
|
||||
|
@ -1,151 +0,0 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
import requests
|
||||
|
||||
from app.classes.shared.helpers import Helpers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
helper = Helpers()
|
||||
|
||||
|
||||
class WebhookHandler:
|
||||
WEBHOOK_USERNAME = "Crafty Webhooks"
|
||||
WEBHOOK_PFP_URL = (
|
||||
"https://gitlab.com/crafty-controller/crafty-4/-"
|
||||
+ "/raw/master/app/frontend/static/assets/images/"
|
||||
+ "Crafty_4-0.png"
|
||||
)
|
||||
CRAFTY_VERSION = helper.get_version_string()
|
||||
|
||||
@staticmethod
|
||||
def get_providers():
|
||||
return [
|
||||
"Discord",
|
||||
"Mattermost",
|
||||
"Signal",
|
||||
"Slack",
|
||||
"SMTP",
|
||||
"Splunk",
|
||||
"Teams",
|
||||
"Telegram",
|
||||
"Custom",
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_monitored_actions():
|
||||
return ["server_start", "server_stop", "server_crash", "server_backup"]
|
||||
|
||||
@staticmethod
|
||||
def send_discord_webhook(server_name, title, url, message, color):
|
||||
"""
|
||||
Sends a message to a Discord channel via a webhook.
|
||||
|
||||
This method prepares a payload for the Discord webhook API using
|
||||
the message content, Crafty Controller version, and the current UTC datetime.
|
||||
It dispatches this payload to the specified webhook URL.
|
||||
|
||||
Parameters:
|
||||
- server_name (str): Name of the server, used as 'author' in the Discord embed.
|
||||
- title (str): Title of the message in the Discord embed.
|
||||
- url (str): URL of the Discord webhook.
|
||||
- message (str): Main content of the message in the Discord embed.
|
||||
- color (int): Color code for the side stripe in the Discord message.
|
||||
|
||||
Returns:
|
||||
str: "Dispatch successful!" if the message is sent successfully, otherwise an
|
||||
exception is raised.
|
||||
|
||||
Raises:
|
||||
Exception: If there's an error in dispatching the webhook.
|
||||
|
||||
Note:
|
||||
- Docs: https://discord.com/developers/docs/resources/webhook#execute-webhook
|
||||
- Webhook request times out after 10 seconds to prevent indefinite hanging.
|
||||
"""
|
||||
|
||||
# Get the current UTC datetime
|
||||
current_datetime = datetime.utcnow()
|
||||
|
||||
# Format the datetime to discord's required UTC string format
|
||||
# "YYYY-MM-DDTHH:MM:SS.MSSZ"
|
||||
formatted_datetime = (
|
||||
current_datetime.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
||||
)
|
||||
|
||||
# Prepare webhook payload
|
||||
headers = {"Content-type": "application/json"}
|
||||
payload = {
|
||||
"username": WebhookHandler.WEBHOOK_USERNAME,
|
||||
"avatar_url": WebhookHandler.WEBHOOK_PFP_URL,
|
||||
"embeds": [
|
||||
{
|
||||
"title": title,
|
||||
"description": message,
|
||||
"color": color,
|
||||
"author": {"name": server_name},
|
||||
"footer": {
|
||||
"text": f"Crafty Controller v.{WebhookHandler.CRAFTY_VERSION}"
|
||||
},
|
||||
"timestamp": formatted_datetime,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
# Dispatch webhook
|
||||
try:
|
||||
response = requests.post(url, json=payload, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
return "Dispatch successful"
|
||||
except requests.RequestException as error:
|
||||
logger.error(error)
|
||||
raise RuntimeError(f"Failed to dispatch notification: {error}") from error
|
||||
|
||||
@staticmethod
|
||||
def send_mattermost_webhook(server_name, title, url, message):
|
||||
"""
|
||||
Sends a message to a Mattermost channel via an incoming webhook.
|
||||
|
||||
Parameters:
|
||||
- server_name (str): Name of the server, used as 'author' in the Discord embed.
|
||||
- title (str): Title of the message in the Discord embed.
|
||||
- url (str): URL of the Discord webhook.
|
||||
- message (str): Main content of the message in the Discord embed.
|
||||
|
||||
Returns:
|
||||
str: "Dispatch successful!" if the message is sent successfully, otherwise an
|
||||
exception is raised.
|
||||
|
||||
Raises:
|
||||
Exception: If there's an error in dispatching the webhook.
|
||||
|
||||
Note:
|
||||
- Docs: https://developers.mattermost.com/integrate/webhooks/incoming
|
||||
- Webhook request times out after 10 seconds to prevent indefinite hanging.
|
||||
"""
|
||||
# Format the text for Mattermost
|
||||
formatted_text = f"#### {title} \n *Server: {server_name}* \n\n {message}"
|
||||
|
||||
# Prepare webhook payload
|
||||
headers = {"Content-Type": "application/json"}
|
||||
payload = {
|
||||
"text": formatted_text,
|
||||
"username": WebhookHandler.WEBHOOK_USERNAME,
|
||||
"icon_url": WebhookHandler.WEBHOOK_PFP_URL,
|
||||
"props": {
|
||||
"card": (
|
||||
f"[Crafty Controller "
|
||||
f"v.{WebhookHandler.CRAFTY_VERSION}](https://craftycontrol.com)"
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
# Dispatch webhook
|
||||
try:
|
||||
response = requests.post(url, json=payload, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
return "Dispatch successful"
|
||||
except requests.RequestException as error:
|
||||
logger.error(error)
|
||||
raise RuntimeError(
|
||||
f"Failed to dispatch notification to Mattermost: {error}"
|
||||
) from error
|
39
app/classes/web/webhooks/base_webhook.py
Normal file
39
app/classes/web/webhooks/base_webhook.py
Normal file
@ -0,0 +1,39 @@
|
||||
from abc import ABC, abstractmethod
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from app.classes.shared.helpers import Helpers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
helper = Helpers()
|
||||
|
||||
|
||||
class WebhookProvider(ABC):
|
||||
"""
|
||||
Base class for all webhook providers.
|
||||
|
||||
Provides a common interface for all webhook provider implementations,
|
||||
ensuring that each provider will have a send method.
|
||||
"""
|
||||
|
||||
WEBHOOK_USERNAME = "Crafty Webhooks"
|
||||
WEBHOOK_PFP_URL = (
|
||||
"https://gitlab.com/crafty-controller/crafty-4/-"
|
||||
+ "/raw/master/app/frontend/static/assets/images/"
|
||||
+ "Crafty_4-0.png"
|
||||
)
|
||||
CRAFTY_VERSION = helper.get_version_string()
|
||||
|
||||
def _send_request(self, url, payload, headers=None):
|
||||
"""Send a POST request to the given URL with the provided payload."""
|
||||
try:
|
||||
response = requests.post(url, json=payload, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
return "Dispatch successful"
|
||||
except requests.RequestException as error:
|
||||
logger.error(error)
|
||||
raise RuntimeError(f"Failed to dispatch notification: {error}") from error
|
||||
|
||||
@abstractmethod
|
||||
def send(self, server_name, title, url, message, **kwargs):
|
||||
"""Abstract method that derived classes will implement for sending webhooks."""
|
71
app/classes/web/webhooks/discord_webhook.py
Normal file
71
app/classes/web/webhooks/discord_webhook.py
Normal file
@ -0,0 +1,71 @@
|
||||
from datetime import datetime
|
||||
from app.classes.web.webhooks.base_webhook import WebhookProvider
|
||||
|
||||
|
||||
class DiscordWebhook(WebhookProvider):
|
||||
def _construct_discord_payload(self, server_name, title, message, color):
|
||||
"""
|
||||
Constructs the payload required for sending a Discord webhook notification.
|
||||
|
||||
This method prepares a payload for the Discord webhook API using the provided
|
||||
message content, the Crafty Controller version, and the current UTC datetime.
|
||||
|
||||
Parameters:
|
||||
server_name (str): The name of the server triggering the notification.
|
||||
title (str): The title for the notification message.
|
||||
message (str): The main content of the notification message.
|
||||
color (int): The color code for the side stripe in the Discord embed message.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing the constructed payload (dict) incl headers (dict).
|
||||
"""
|
||||
current_datetime = datetime.utcnow()
|
||||
formatted_datetime = (
|
||||
current_datetime.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
||||
)
|
||||
|
||||
headers = {"Content-type": "application/json"}
|
||||
payload = {
|
||||
"username": self.WEBHOOK_USERNAME,
|
||||
"avatar_url": self.WEBHOOK_PFP_URL,
|
||||
"embeds": [
|
||||
{
|
||||
"title": title,
|
||||
"description": message,
|
||||
"color": color,
|
||||
"author": {"name": server_name},
|
||||
"footer": {"text": f"Crafty Controller v.{self.CRAFTY_VERSION}"},
|
||||
"timestamp": formatted_datetime,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
return payload, headers
|
||||
|
||||
def send(self, server_name, title, url, message, **kwargs):
|
||||
"""
|
||||
Sends a Discord webhook notification using the given details.
|
||||
|
||||
The method constructs and dispatches a payload suitable for
|
||||
Discords'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.
|
||||
color (int, optional): The color code for the embed's side stripe.
|
||||
Defaults to a pretty blue if not provided.
|
||||
|
||||
Returns:
|
||||
str: "Dispatch successful!" if the message is sent successfully, otherwise an
|
||||
exception is raised.
|
||||
|
||||
Raises:
|
||||
Exception: If there's an error in dispatching the webhook.
|
||||
"""
|
||||
color = kwargs.get("color", 23761) # Default to a color if not provided.
|
||||
payload, headers = self._construct_discord_payload(
|
||||
server_name, title, message, color
|
||||
)
|
||||
return self._send_request(url, payload, headers)
|
60
app/classes/web/webhooks/mattermost_webhook.py
Normal file
60
app/classes/web/webhooks/mattermost_webhook.py
Normal file
@ -0,0 +1,60 @@
|
||||
from app.classes.web.webhooks.base_webhook import WebhookProvider
|
||||
|
||||
|
||||
class MattermostWebhook(WebhookProvider):
|
||||
def _construct_mattermost_payload(self, server_name, title, message):
|
||||
"""
|
||||
Constructs the payload required for sending a Mattermost webhook notification.
|
||||
|
||||
The method formats the given information into a Markdown-styled message for MM,
|
||||
including an information card containing the Crafty version.
|
||||
|
||||
Parameters:
|
||||
server_name (str): The name of the server triggering the notification.
|
||||
title (str): The title for the notification message.
|
||||
message (str): The main content of the notification message.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing the constructed payload (dict) incl headers (dict).
|
||||
"""
|
||||
formatted_text = f"#### {title} \n *Server: {server_name}* \n\n {message}"
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
payload = {
|
||||
"text": formatted_text,
|
||||
"username": self.WEBHOOK_USERNAME,
|
||||
"icon_url": self.WEBHOOK_PFP_URL,
|
||||
"props": {
|
||||
"card": (
|
||||
f"[Crafty Controller "
|
||||
f"v.{self.CRAFTY_VERSION}](https://craftycontrol.com)"
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
return payload, headers
|
||||
|
||||
def send(self, server_name, title, url, message, **kwargs):
|
||||
"""
|
||||
Sends a Mattermost webhook notification using the given details.
|
||||
|
||||
The method constructs and dispatches a payload suitable for
|
||||
Mattermost'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.
|
||||
|
||||
Returns:
|
||||
str: "Dispatch successful!" if the message is sent successfully, otherwise an
|
||||
exception is raised.
|
||||
|
||||
Raises:
|
||||
Exception: If there's an error in dispatching the webhook.
|
||||
"""
|
||||
payload, headers = self._construct_mattermost_payload(
|
||||
server_name, title, message
|
||||
)
|
||||
return self._send_request(url, payload, headers)
|
79
app/classes/web/webhooks/webhook_factory.py
Normal file
79
app/classes/web/webhooks/webhook_factory.py
Normal file
@ -0,0 +1,79 @@
|
||||
from app.classes.web.webhooks.discord_webhook import DiscordWebhook
|
||||
from app.classes.web.webhooks.mattermost_webhook import MattermostWebhook
|
||||
|
||||
|
||||
class WebhookFactory:
|
||||
"""
|
||||
A factory class responsible for the creation and management of webhook providers.
|
||||
|
||||
This class provides methods to instantiate specific webhook providers based on
|
||||
their name and to retrieve a list of supported providers. It uses a registry pattern
|
||||
to manage the available providers.
|
||||
|
||||
Attributes:
|
||||
- _registry (dict): A dictionary mapping provider names to their classes.
|
||||
"""
|
||||
|
||||
_registry = {
|
||||
"Discord": DiscordWebhook,
|
||||
"Mattermost": MattermostWebhook,
|
||||
# "Signal",
|
||||
# "Slack",
|
||||
# "SMTP",
|
||||
# "Splunk",
|
||||
# "Teams",
|
||||
# "Telegram",
|
||||
# "Custom",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def create_provider(cls, provider_name, *args, **kwargs):
|
||||
"""
|
||||
Creates and returns an instance of the specified webhook provider.
|
||||
|
||||
This method looks up the provider in the registry, then instantiates it w/ the
|
||||
provided arguments. If the provider is not recognized, a ValueError is raised.
|
||||
|
||||
Arguments:
|
||||
- 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.
|
||||
|
||||
Returns:
|
||||
WebhookProvider: An instance of the desired webhook provider.
|
||||
|
||||
Raises:
|
||||
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.")
|
||||
return cls._registry[provider_name](*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_supported_providers(cls):
|
||||
"""
|
||||
Retrieves the names of all supported webhook providers.
|
||||
|
||||
This method returns a list containing the names of all providers
|
||||
currently registered in the factory's registry.
|
||||
|
||||
Returns:
|
||||
List[str]: A list of supported provider names.
|
||||
"""
|
||||
return list(cls._registry.keys())
|
||||
|
||||
@staticmethod
|
||||
def get_monitored_events():
|
||||
"""
|
||||
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.
|
||||
|
||||
Returns:
|
||||
List[str]: A list of supported monitored actions.
|
||||
"""
|
||||
return ["server_start", "server_stop", "server_crash", "server_backup"]
|
Loading…
Reference in New Issue
Block a user