mirror of
https://gitlab.com/crafty-controller/crafty-4.git
synced 2024-08-30 18:23:09 +00:00
Merge branch 'feature/discord-webhooks' into 'dev'
Webhook Functionality See merge request crafty-controller/crafty-4!594
This commit is contained in:
commit
4379ba408b
@ -2,6 +2,7 @@
|
|||||||
## --- [4.2.0] - 2023/TBD
|
## --- [4.2.0] - 2023/TBD
|
||||||
### New features
|
### New features
|
||||||
- Finish and Activate Arcadia notification backend ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/621) | [Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/626) | [Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/632))
|
- Finish and Activate Arcadia notification backend ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/621) | [Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/626) | [Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/632))
|
||||||
|
- Add initial Webhook Notification (Discord, Mattermost, Slack, Teams) ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/594))
|
||||||
### Bug fixes
|
### Bug fixes
|
||||||
- PWA: Removed the custom offline page in favour of browser default ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/607))
|
- PWA: Removed the custom offline page in favour of browser default ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/607))
|
||||||
- Fix hidden servers appearing visible on public mobile status page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/612))
|
- Fix hidden servers appearing visible on public mobile status page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/612))
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import queue
|
import queue
|
||||||
|
|
||||||
from app.classes.models.management import HelpersManagement
|
from app.classes.models.management import HelpersManagement, HelpersWebhooks
|
||||||
from app.classes.models.servers import HelperServers
|
from app.classes.models.servers import HelperServers
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -206,3 +206,30 @@ class ManagementController:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def set_master_server_dir(server_dir):
|
def set_master_server_dir(server_dir):
|
||||||
HelpersManagement.set_master_server_dir(server_dir)
|
HelpersManagement.set_master_server_dir(server_dir)
|
||||||
|
|
||||||
|
# **********************************************************************************
|
||||||
|
# Webhooks Methods
|
||||||
|
# **********************************************************************************
|
||||||
|
@staticmethod
|
||||||
|
def create_webhook(data):
|
||||||
|
return HelpersWebhooks.create_webhook(data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def modify_webhook(webhook_id, data):
|
||||||
|
HelpersWebhooks.modify_webhook(webhook_id, data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_webhook_by_id(webhook_id):
|
||||||
|
return HelpersWebhooks.get_webhook_by_id(webhook_id)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_webhooks_by_server(server_id, model=False):
|
||||||
|
return HelpersWebhooks.get_webhooks_by_server(server_id, model)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_webhook(webhook_id):
|
||||||
|
HelpersWebhooks.delete_webhook(webhook_id)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_webhook_by_server(server_id):
|
||||||
|
HelpersWebhooks.delete_webhooks_by_server(server_id)
|
||||||
|
@ -79,11 +79,15 @@ class HostStats(BaseModel):
|
|||||||
# **********************************************************************************
|
# **********************************************************************************
|
||||||
class Webhooks(BaseModel):
|
class Webhooks(BaseModel):
|
||||||
id = AutoField()
|
id = AutoField()
|
||||||
name = CharField(max_length=64, unique=True, index=True)
|
server_id = IntegerField(null=True)
|
||||||
method = CharField(default="POST")
|
name = CharField(default="Custom Webhook", max_length=64)
|
||||||
url = CharField(unique=True)
|
url = CharField(default="")
|
||||||
event = CharField(default="")
|
webhook_type = CharField(default="Custom")
|
||||||
send_data = BooleanField(default=True)
|
bot_name = CharField(default="Crafty Controller")
|
||||||
|
trigger = CharField(default="server_start,server_stop")
|
||||||
|
body = CharField(default="")
|
||||||
|
color = CharField(default="#005cd1")
|
||||||
|
enabled = BooleanField(default=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table_name = "webhooks"
|
table_name = "webhooks"
|
||||||
@ -501,3 +505,82 @@ class HelpersManagement:
|
|||||||
f"Not removing {dir_to_del} from excluded directories - "
|
f"Not removing {dir_to_del} from excluded directories - "
|
||||||
f"not in the excluded directory list for server ID {server_id}"
|
f"not in the excluded directory list for server ID {server_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# **********************************************************************************
|
||||||
|
# Webhooks Class
|
||||||
|
# **********************************************************************************
|
||||||
|
class HelpersWebhooks:
|
||||||
|
def __init__(self, database):
|
||||||
|
self.database = database
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_webhook(create_data) -> int:
|
||||||
|
"""Create a webhook in the database
|
||||||
|
|
||||||
|
Args:
|
||||||
|
server_id: ID of a server this webhook will be married to
|
||||||
|
name: The name of the webhook
|
||||||
|
url: URL to the webhook
|
||||||
|
webhook_type: The provider this webhook will be sent to
|
||||||
|
bot name: The name that will appear when the webhook is sent
|
||||||
|
triggers: Server actions that will trigger this webhook
|
||||||
|
body: The message body of the webhook
|
||||||
|
enabled: Should Crafty trigger the webhook
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The new webhooks's id
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PeeweeException: If the webhook already exists
|
||||||
|
"""
|
||||||
|
return Webhooks.insert(
|
||||||
|
{
|
||||||
|
Webhooks.server_id: create_data["server_id"],
|
||||||
|
Webhooks.name: create_data["name"],
|
||||||
|
Webhooks.webhook_type: create_data["webhook_type"],
|
||||||
|
Webhooks.url: create_data["url"],
|
||||||
|
Webhooks.bot_name: create_data["bot_name"],
|
||||||
|
Webhooks.body: create_data["body"],
|
||||||
|
Webhooks.color: create_data["color"],
|
||||||
|
Webhooks.trigger: create_data["trigger"],
|
||||||
|
Webhooks.enabled: create_data["enabled"],
|
||||||
|
}
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def modify_webhook(webhook_id, updata):
|
||||||
|
Webhooks.update(updata).where(Webhooks.id == webhook_id).execute()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_webhook_by_id(webhook_id):
|
||||||
|
return model_to_dict(Webhooks.get(Webhooks.id == webhook_id))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_webhooks_by_server(server_id, model):
|
||||||
|
if not model:
|
||||||
|
data = {}
|
||||||
|
for webhook in (
|
||||||
|
Webhooks.select().where(Webhooks.server_id == server_id).execute()
|
||||||
|
):
|
||||||
|
data[str(webhook.id)] = {
|
||||||
|
"webhook_type": webhook.webhook_type,
|
||||||
|
"name": webhook.name,
|
||||||
|
"url": webhook.url,
|
||||||
|
"bot_name": webhook.bot_name,
|
||||||
|
"trigger": webhook.trigger,
|
||||||
|
"body": webhook.body,
|
||||||
|
"color": webhook.color,
|
||||||
|
"enabled": webhook.enabled,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
data = Webhooks.select().where(Webhooks.server_id == server_id).execute()
|
||||||
|
return data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_webhook(webhook_id):
|
||||||
|
Webhooks.delete().where(Webhooks.id == webhook_id).execute()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_webhooks_by_server(server_id):
|
||||||
|
Webhooks.delete().where(Webhooks.server_id == server_id).execute()
|
||||||
|
@ -19,13 +19,13 @@ from zoneinfo import ZoneInfo
|
|||||||
from tzlocal import get_localzone
|
from tzlocal import get_localzone
|
||||||
from tzlocal.utils import ZoneInfoNotFoundError
|
from tzlocal.utils import ZoneInfoNotFoundError
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
from apscheduler.jobstores.base import JobLookupError
|
from apscheduler.jobstores.base import JobLookupError, ConflictingIdError
|
||||||
|
|
||||||
from app.classes.minecraft.stats import Stats
|
from app.classes.minecraft.stats import Stats
|
||||||
from app.classes.minecraft.mc_ping import ping, ping_bedrock
|
from app.classes.minecraft.mc_ping import ping, ping_bedrock
|
||||||
from app.classes.models.servers import HelperServers, Servers
|
from app.classes.models.servers import HelperServers, Servers
|
||||||
from app.classes.models.server_stats import HelperServerStats
|
from app.classes.models.server_stats import HelperServerStats
|
||||||
from app.classes.models.management import HelpersManagement
|
from app.classes.models.management import HelpersManagement, HelpersWebhooks
|
||||||
from app.classes.models.users import HelperUsers
|
from app.classes.models.users import HelperUsers
|
||||||
from app.classes.models.server_permissions import PermissionsServers
|
from app.classes.models.server_permissions import PermissionsServers
|
||||||
from app.classes.shared.console import Console
|
from app.classes.shared.console import Console
|
||||||
@ -33,6 +33,7 @@ from app.classes.shared.helpers import Helpers
|
|||||||
from app.classes.shared.file_helpers import FileHelpers
|
from app.classes.shared.file_helpers import FileHelpers
|
||||||
from app.classes.shared.null_writer import NullWriter
|
from app.classes.shared.null_writer import NullWriter
|
||||||
from app.classes.shared.websocket_manager import WebSocketManager
|
from app.classes.shared.websocket_manager import WebSocketManager
|
||||||
|
from app.classes.web.webhooks.webhook_factory import WebhookFactory
|
||||||
|
|
||||||
with redirect_stderr(NullWriter()):
|
with redirect_stderr(NullWriter()):
|
||||||
import psutil
|
import psutil
|
||||||
@ -165,6 +166,45 @@ class ServerInstance:
|
|||||||
self.stats_helper.server_crash_reset()
|
self.stats_helper.server_crash_reset()
|
||||||
self.stats_helper.set_update(False)
|
self.stats_helper.set_update(False)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def callback(called_func):
|
||||||
|
# Usage of @callback on method
|
||||||
|
# definition to run a webhook check
|
||||||
|
# on method completion
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
res = None
|
||||||
|
logger.debug("Checking for callbacks")
|
||||||
|
try:
|
||||||
|
res = called_func(*args, **kwargs)
|
||||||
|
finally:
|
||||||
|
events = WebhookFactory.get_monitored_events()
|
||||||
|
if called_func.__name__ in 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(","):
|
||||||
|
logger.info(
|
||||||
|
f"Found callback for event {called_func.__name__}"
|
||||||
|
f" for server {args[0].server_id}"
|
||||||
|
)
|
||||||
|
webhook = HelpersWebhooks.get_webhook_by_id(swebhook.id)
|
||||||
|
webhook_provider = WebhookFactory.create_provider(
|
||||||
|
webhook["webhook_type"]
|
||||||
|
)
|
||||||
|
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"],
|
||||||
|
color=webhook["color"],
|
||||||
|
)
|
||||||
|
return res
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
# **********************************************************************************
|
# **********************************************************************************
|
||||||
# Minecraft Server Management
|
# Minecraft Server Management
|
||||||
# **********************************************************************************
|
# **********************************************************************************
|
||||||
@ -262,13 +302,13 @@ class ServerInstance:
|
|||||||
seconds=30,
|
seconds=30,
|
||||||
id="save_stats_" + str(self.server_id),
|
id="save_stats_" + str(self.server_id),
|
||||||
)
|
)
|
||||||
except:
|
except ConflictingIdError:
|
||||||
self.server_scheduler.remove_job("save_" + str(self.server_id))
|
self.server_scheduler.remove_job("save_stats_" + str(self.server_id))
|
||||||
self.server_scheduler.add_job(
|
self.server_scheduler.add_job(
|
||||||
self.record_server_stats,
|
self.record_server_stats,
|
||||||
"interval",
|
"interval",
|
||||||
seconds=30,
|
seconds=30,
|
||||||
id="save_" + str(self.server_id),
|
id="save_stats_" + str(self.server_id),
|
||||||
)
|
)
|
||||||
|
|
||||||
def setup_server_run_command(self):
|
def setup_server_run_command(self):
|
||||||
@ -332,6 +372,7 @@ class ServerInstance:
|
|||||||
logger.critical(f"Unable to write/access {self.server_path}")
|
logger.critical(f"Unable to write/access {self.server_path}")
|
||||||
Console.critical(f"Unable to write/access {self.server_path}")
|
Console.critical(f"Unable to write/access {self.server_path}")
|
||||||
|
|
||||||
|
@callback
|
||||||
def start_server(self, user_id, forge_install=False):
|
def start_server(self, user_id, forge_install=False):
|
||||||
if not user_id:
|
if not user_id:
|
||||||
user_lang = self.helper.get_setting("language")
|
user_lang = self.helper.get_setting("language")
|
||||||
@ -775,6 +816,7 @@ class ServerInstance:
|
|||||||
if self.server_thread:
|
if self.server_thread:
|
||||||
self.server_thread.join()
|
self.server_thread.join()
|
||||||
|
|
||||||
|
@callback
|
||||||
def stop_server(self):
|
def stop_server(self):
|
||||||
running = self.check_running()
|
running = self.check_running()
|
||||||
if not running:
|
if not running:
|
||||||
@ -882,6 +924,7 @@ class ServerInstance:
|
|||||||
self.last_rc = poll
|
self.last_rc = poll
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@callback
|
||||||
def send_command(self, command):
|
def send_command(self, command):
|
||||||
if not self.check_running() and command.lower() != "start":
|
if not self.check_running() and command.lower() != "start":
|
||||||
logger.warning(f'Server not running, unable to send command "{command}"')
|
logger.warning(f'Server not running, unable to send command "{command}"')
|
||||||
@ -894,6 +937,7 @@ class ServerInstance:
|
|||||||
self.process.stdin.flush()
|
self.process.stdin.flush()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@callback
|
||||||
def crash_detected(self, name):
|
def crash_detected(self, name):
|
||||||
# clear the old scheduled watcher task
|
# clear the old scheduled watcher task
|
||||||
self.server_scheduler.remove_job(f"c_{self.server_id}")
|
self.server_scheduler.remove_job(f"c_{self.server_id}")
|
||||||
@ -914,6 +958,7 @@ class ServerInstance:
|
|||||||
f"The server {name} has crashed and will be restarted. "
|
f"The server {name} has crashed and will be restarted. "
|
||||||
f"Restarting server"
|
f"Restarting server"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.run_threaded_server(None)
|
self.run_threaded_server(None)
|
||||||
return True
|
return True
|
||||||
logger.critical(
|
logger.critical(
|
||||||
@ -926,6 +971,7 @@ class ServerInstance:
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@callback
|
||||||
def kill(self):
|
def kill(self):
|
||||||
logger.info(f"Terminating server {self.server_id} and all child processes")
|
logger.info(f"Terminating server {self.server_id} and all child processes")
|
||||||
try:
|
try:
|
||||||
@ -1014,6 +1060,7 @@ class ServerInstance:
|
|||||||
f.write("eula=true")
|
f.write("eula=true")
|
||||||
self.run_threaded_server(user_id)
|
self.run_threaded_server(user_id)
|
||||||
|
|
||||||
|
@callback
|
||||||
def backup_server(self):
|
def backup_server(self):
|
||||||
if self.settings["backup_path"] == "":
|
if self.settings["backup_path"] == "":
|
||||||
logger.critical("Backup path is None. Canceling Backup!")
|
logger.critical("Backup path is None. Canceling Backup!")
|
||||||
@ -1228,6 +1275,7 @@ class ServerInstance:
|
|||||||
if f["path"].endswith(".zip")
|
if f["path"].endswith(".zip")
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@callback
|
||||||
def jar_update(self):
|
def jar_update(self):
|
||||||
self.stats_helper.set_update(True)
|
self.stats_helper.set_update(True)
|
||||||
update_thread = threading.Thread(
|
update_thread = threading.Thread(
|
||||||
|
@ -25,6 +25,7 @@ from app.classes.controllers.roles_controller import RolesController
|
|||||||
from app.classes.shared.helpers import Helpers
|
from app.classes.shared.helpers import Helpers
|
||||||
from app.classes.shared.main_models import DatabaseShortcuts
|
from app.classes.shared.main_models import DatabaseShortcuts
|
||||||
from app.classes.web.base_handler import BaseHandler
|
from app.classes.web.base_handler import BaseHandler
|
||||||
|
from app.classes.web.webhooks.webhook_factory import WebhookFactory
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -344,7 +345,9 @@ class PanelHandler(BaseHandler):
|
|||||||
) as credits_default_local:
|
) as credits_default_local:
|
||||||
try:
|
try:
|
||||||
remote = requests.get(
|
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()
|
credits_dict: dict = remote.json()
|
||||||
if not credits_dict["staff"]:
|
if not credits_dict["staff"]:
|
||||||
@ -745,6 +748,22 @@ class PanelHandler(BaseHandler):
|
|||||||
page_data["history_stats"] = self.controller.servers.get_history_stats(
|
page_data["history_stats"] = self.controller.servers.get_history_stats(
|
||||||
server_id, hours=(days * 24)
|
server_id, hours=(days * 24)
|
||||||
)
|
)
|
||||||
|
if subpage == "webhooks":
|
||||||
|
if (
|
||||||
|
not page_data["permissions"]["Config"]
|
||||||
|
in page_data["user_permissions"]
|
||||||
|
):
|
||||||
|
if not superuser:
|
||||||
|
self.redirect(
|
||||||
|
"/panel/error?error=Unauthorized access to Webhooks Config"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
page_data[
|
||||||
|
"webhooks"
|
||||||
|
] = self.controller.management.get_webhooks_by_server(
|
||||||
|
server_id, model=True
|
||||||
|
)
|
||||||
|
page_data["triggers"] = WebhookFactory.get_monitored_events()
|
||||||
|
|
||||||
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)
|
||||||
@ -1012,6 +1031,110 @@ class PanelHandler(BaseHandler):
|
|||||||
|
|
||||||
template = "panel/panel_edit_user.html"
|
template = "panel/panel_edit_user.html"
|
||||||
|
|
||||||
|
elif page == "add_webhook":
|
||||||
|
server_id = self.get_argument("id", None)
|
||||||
|
if server_id is None:
|
||||||
|
return self.redirect("/panel/error?error=Invalid Server ID")
|
||||||
|
server_obj = self.controller.servers.get_server_instance_by_id(server_id)
|
||||||
|
page_data["backup_failed"] = server_obj.last_backup_status()
|
||||||
|
server_obj = None
|
||||||
|
page_data["active_link"] = "webhooks"
|
||||||
|
page_data["server_data"] = self.controller.servers.get_server_data_by_id(
|
||||||
|
server_id
|
||||||
|
)
|
||||||
|
page_data[
|
||||||
|
"user_permissions"
|
||||||
|
] = self.controller.server_perms.get_user_id_permissions_list(
|
||||||
|
exec_user["user_id"], server_id
|
||||||
|
)
|
||||||
|
page_data["permissions"] = {
|
||||||
|
"Commands": EnumPermissionsServer.COMMANDS,
|
||||||
|
"Terminal": EnumPermissionsServer.TERMINAL,
|
||||||
|
"Logs": EnumPermissionsServer.LOGS,
|
||||||
|
"Schedule": EnumPermissionsServer.SCHEDULE,
|
||||||
|
"Backup": EnumPermissionsServer.BACKUP,
|
||||||
|
"Files": EnumPermissionsServer.FILES,
|
||||||
|
"Config": EnumPermissionsServer.CONFIG,
|
||||||
|
"Players": EnumPermissionsServer.PLAYERS,
|
||||||
|
}
|
||||||
|
page_data["server_stats"] = self.controller.servers.get_server_stats_by_id(
|
||||||
|
server_id
|
||||||
|
)
|
||||||
|
page_data["server_stats"][
|
||||||
|
"server_type"
|
||||||
|
] = self.controller.servers.get_server_type_by_id(server_id)
|
||||||
|
page_data["new_webhook"] = True
|
||||||
|
page_data["webhook"] = {}
|
||||||
|
page_data["webhook"]["webhook_type"] = "Custom"
|
||||||
|
page_data["webhook"]["name"] = ""
|
||||||
|
page_data["webhook"]["url"] = ""
|
||||||
|
page_data["webhook"]["bot_name"] = "Crafty Controller"
|
||||||
|
page_data["webhook"]["trigger"] = []
|
||||||
|
page_data["webhook"]["body"] = ""
|
||||||
|
page_data["webhook"]["color"] = "#005cd1"
|
||||||
|
page_data["webhook"]["enabled"] = True
|
||||||
|
|
||||||
|
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:
|
||||||
|
self.redirect("/panel/error?error=Unauthorized access To Webhooks")
|
||||||
|
return
|
||||||
|
|
||||||
|
template = "panel/server_webhook_edit.html"
|
||||||
|
|
||||||
|
elif page == "webhook_edit":
|
||||||
|
server_id = self.get_argument("id", None)
|
||||||
|
webhook_id = self.get_argument("webhook_id", None)
|
||||||
|
if server_id is None:
|
||||||
|
return self.redirect("/panel/error?error=Invalid Server ID")
|
||||||
|
server_obj = self.controller.servers.get_server_instance_by_id(server_id)
|
||||||
|
page_data["backup_failed"] = server_obj.last_backup_status()
|
||||||
|
server_obj = None
|
||||||
|
page_data["active_link"] = "webhooks"
|
||||||
|
page_data["server_data"] = self.controller.servers.get_server_data_by_id(
|
||||||
|
server_id
|
||||||
|
)
|
||||||
|
page_data[
|
||||||
|
"user_permissions"
|
||||||
|
] = self.controller.server_perms.get_user_id_permissions_list(
|
||||||
|
exec_user["user_id"], server_id
|
||||||
|
)
|
||||||
|
page_data["permissions"] = {
|
||||||
|
"Commands": EnumPermissionsServer.COMMANDS,
|
||||||
|
"Terminal": EnumPermissionsServer.TERMINAL,
|
||||||
|
"Logs": EnumPermissionsServer.LOGS,
|
||||||
|
"Schedule": EnumPermissionsServer.SCHEDULE,
|
||||||
|
"Backup": EnumPermissionsServer.BACKUP,
|
||||||
|
"Files": EnumPermissionsServer.FILES,
|
||||||
|
"Config": EnumPermissionsServer.CONFIG,
|
||||||
|
"Players": EnumPermissionsServer.PLAYERS,
|
||||||
|
}
|
||||||
|
page_data["server_stats"] = self.controller.servers.get_server_stats_by_id(
|
||||||
|
server_id
|
||||||
|
)
|
||||||
|
page_data["server_stats"][
|
||||||
|
"server_type"
|
||||||
|
] = self.controller.servers.get_server_type_by_id(server_id)
|
||||||
|
page_data["new_webhook"] = False
|
||||||
|
page_data["webhook"] = self.controller.management.get_webhook_by_id(
|
||||||
|
webhook_id
|
||||||
|
)
|
||||||
|
page_data["webhook"]["trigger"] = str(
|
||||||
|
page_data["webhook"]["trigger"]
|
||||||
|
).split(",")
|
||||||
|
|
||||||
|
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:
|
||||||
|
self.redirect("/panel/error?error=Unauthorized access To Webhooks")
|
||||||
|
return
|
||||||
|
|
||||||
|
template = "panel/server_webhook_edit.html"
|
||||||
|
|
||||||
elif page == "add_schedule":
|
elif page == "add_schedule":
|
||||||
server_id = self.get_argument("id", None)
|
server_id = self.get_argument("id", None)
|
||||||
if server_id is None:
|
if server_id is None:
|
||||||
|
@ -50,6 +50,12 @@ from app.classes.web.routes.api.servers.server.tasks.task.children import (
|
|||||||
from app.classes.web.routes.api.servers.server.tasks.task.index import (
|
from app.classes.web.routes.api.servers.server.tasks.task.index import (
|
||||||
ApiServersServerTasksTaskIndexHandler,
|
ApiServersServerTasksTaskIndexHandler,
|
||||||
)
|
)
|
||||||
|
from app.classes.web.routes.api.servers.server.webhooks.index import (
|
||||||
|
ApiServersServerWebhooksIndexHandler,
|
||||||
|
)
|
||||||
|
from app.classes.web.routes.api.servers.server.webhooks.webhook.index import (
|
||||||
|
ApiServersServerWebhooksManagementIndexHandler,
|
||||||
|
)
|
||||||
from app.classes.web.routes.api.servers.server.users import ApiServersServerUsersHandler
|
from app.classes.web.routes.api.servers.server.users import ApiServersServerUsersHandler
|
||||||
from app.classes.web.routes.api.users.index import ApiUsersIndexHandler
|
from app.classes.web.routes.api.users.index import ApiUsersIndexHandler
|
||||||
from app.classes.web.routes.api.users.user.index import ApiUsersUserIndexHandler
|
from app.classes.web.routes.api.users.user.index import ApiUsersUserIndexHandler
|
||||||
@ -250,6 +256,16 @@ def api_handlers(handler_args):
|
|||||||
ApiServersServerHistoryHandler,
|
ApiServersServerHistoryHandler,
|
||||||
handler_args,
|
handler_args,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
r"/api/v2/servers/([0-9]+)/webhook/([0-9]+)/?",
|
||||||
|
ApiServersServerWebhooksManagementIndexHandler,
|
||||||
|
handler_args,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r"/api/v2/servers/([0-9]+)/webhook/?",
|
||||||
|
ApiServersServerWebhooksIndexHandler,
|
||||||
|
handler_args,
|
||||||
|
),
|
||||||
(
|
(
|
||||||
r"/api/v2/servers/([0-9]+)/action/([a-z_]+)/?",
|
r"/api/v2/servers/([0-9]+)/action/([a-z_]+)/?",
|
||||||
ApiServersServerActionHandler,
|
ApiServersServerActionHandler,
|
||||||
|
108
app/classes/web/routes/api/servers/server/webhooks/index.py
Normal file
108
app/classes/web/routes/api/servers/server/webhooks/index.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
# TODO: create and read
|
||||||
|
|
||||||
|
import json
|
||||||
|
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.webhooks.webhook_factory import WebhookFactory
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
new_webhook_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"webhook_type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": WebhookFactory.get_supported_providers(),
|
||||||
|
},
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"url": {"type": "string"},
|
||||||
|
"bot_name": {"type": "string"},
|
||||||
|
"trigger": {"type": "array"},
|
||||||
|
"body": {"type": "string"},
|
||||||
|
"color": {"type": "string", "default": "#005cd1"},
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"additionalProperties": False,
|
||||||
|
"minProperties": 7,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ApiServersServerWebhooksIndexHandler(BaseApiHandler):
|
||||||
|
def get(self, server_id: str):
|
||||||
|
auth_data = self.authenticate_user()
|
||||||
|
if not auth_data:
|
||||||
|
return
|
||||||
|
if (
|
||||||
|
EnumPermissionsServer.CONFIG
|
||||||
|
not in self.controller.server_perms.get_user_id_permissions_list(
|
||||||
|
auth_data[4]["user_id"], server_id
|
||||||
|
)
|
||||||
|
):
|
||||||
|
# if the user doesn't have Schedule permission, return an error
|
||||||
|
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||||
|
self.finish_json(
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"data": self.controller.management.get_webhooks_by_server(server_id),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def post(self, server_id: str):
|
||||||
|
auth_data = self.authenticate_user()
|
||||||
|
if not auth_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(self.request.body)
|
||||||
|
except json.decoder.JSONDecodeError as e:
|
||||||
|
return self.finish_json(
|
||||||
|
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
validate(data, new_webhook_schema)
|
||||||
|
except ValidationError as e:
|
||||||
|
return self.finish_json(
|
||||||
|
400,
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"error": "INVALID_JSON_SCHEMA",
|
||||||
|
"error_data": str(e),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
|
||||||
|
# if the user doesn't have access to the server, return an error
|
||||||
|
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||||
|
|
||||||
|
if (
|
||||||
|
EnumPermissionsServer.CONFIG
|
||||||
|
not in self.controller.server_perms.get_user_id_permissions_list(
|
||||||
|
auth_data[4]["user_id"], server_id
|
||||||
|
)
|
||||||
|
):
|
||||||
|
# if the user doesn't have Schedule permission, return an error
|
||||||
|
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||||
|
data["server_id"] = server_id
|
||||||
|
|
||||||
|
self.controller.management.add_to_audit_log(
|
||||||
|
auth_data[4]["user_id"],
|
||||||
|
f"Edited server {server_id}: added webhook",
|
||||||
|
server_id,
|
||||||
|
self.get_remote_ip(),
|
||||||
|
)
|
||||||
|
triggers = ""
|
||||||
|
for item in data["trigger"]:
|
||||||
|
string = item + ","
|
||||||
|
triggers += string
|
||||||
|
data["trigger"] = triggers
|
||||||
|
webhook_id = self.controller.management.create_webhook(data)
|
||||||
|
|
||||||
|
self.finish_json(200, {"status": "ok", "data": {"webhook_id": webhook_id}})
|
@ -0,0 +1,187 @@
|
|||||||
|
# TODO: read and delete
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from jsonschema import ValidationError, validate
|
||||||
|
from app.classes.models.server_permissions import EnumPermissionsServer
|
||||||
|
from app.classes.web.webhooks.webhook_factory import WebhookFactory
|
||||||
|
from app.classes.web.base_api_handler import BaseApiHandler
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
webhook_patch_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"webhook_type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": WebhookFactory.get_supported_providers(),
|
||||||
|
},
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"url": {"type": "string"},
|
||||||
|
"bot_name": {"type": "string"},
|
||||||
|
"trigger": {"type": "array"},
|
||||||
|
"body": {"type": "string"},
|
||||||
|
"color": {"type": "string", "default": "#005cd1"},
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"additionalProperties": False,
|
||||||
|
"minProperties": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ApiServersServerWebhooksManagementIndexHandler(BaseApiHandler):
|
||||||
|
def get(self, server_id: str, webhook_id: str):
|
||||||
|
auth_data = self.authenticate_user()
|
||||||
|
if not auth_data:
|
||||||
|
return
|
||||||
|
if (
|
||||||
|
EnumPermissionsServer.CONFIG
|
||||||
|
not in self.controller.server_perms.get_user_id_permissions_list(
|
||||||
|
auth_data[4]["user_id"], server_id
|
||||||
|
)
|
||||||
|
):
|
||||||
|
# if the user doesn't have Schedule permission, return an error
|
||||||
|
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||||
|
if (
|
||||||
|
not str(webhook_id)
|
||||||
|
in self.controller.management.get_webhooks_by_server(server_id).keys()
|
||||||
|
):
|
||||||
|
return self.finish_json(
|
||||||
|
400, {"status": "error", "error": "NO WEBHOOK FOUND"}
|
||||||
|
)
|
||||||
|
self.finish_json(
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"data": self.controller.management.get_webhook_by_id(webhook_id),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete(self, server_id: str, webhook_id: str):
|
||||||
|
auth_data = self.authenticate_user()
|
||||||
|
if not auth_data:
|
||||||
|
return
|
||||||
|
if (
|
||||||
|
EnumPermissionsServer.CONFIG
|
||||||
|
not in self.controller.server_perms.get_user_id_permissions_list(
|
||||||
|
auth_data[4]["user_id"], server_id
|
||||||
|
)
|
||||||
|
):
|
||||||
|
# if the user doesn't have Schedule permission, return an error
|
||||||
|
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.controller.management.delete_webhook(webhook_id)
|
||||||
|
except Exception:
|
||||||
|
return self.finish_json(
|
||||||
|
400, {"status": "error", "error": "NO WEBHOOK FOUND"}
|
||||||
|
)
|
||||||
|
self.controller.management.add_to_audit_log(
|
||||||
|
auth_data[4]["user_id"],
|
||||||
|
f"Edited server {server_id}: removed webhook",
|
||||||
|
server_id,
|
||||||
|
self.get_remote_ip(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.finish_json(200, {"status": "ok"})
|
||||||
|
|
||||||
|
def patch(self, server_id: str, webhook_id: str):
|
||||||
|
auth_data = self.authenticate_user()
|
||||||
|
if not auth_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(self.request.body)
|
||||||
|
except json.decoder.JSONDecodeError as e:
|
||||||
|
return self.finish_json(
|
||||||
|
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
validate(data, webhook_patch_schema)
|
||||||
|
except ValidationError as e:
|
||||||
|
return self.finish_json(
|
||||||
|
400,
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"error": "INVALID_JSON_SCHEMA",
|
||||||
|
"error_data": str(e),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
|
||||||
|
# if the user doesn't have access to the server, return an error
|
||||||
|
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||||
|
|
||||||
|
if (
|
||||||
|
EnumPermissionsServer.CONFIG
|
||||||
|
not in self.controller.server_perms.get_user_id_permissions_list(
|
||||||
|
auth_data[4]["user_id"], server_id
|
||||||
|
)
|
||||||
|
):
|
||||||
|
# if the user doesn't have Schedule permission, return an error
|
||||||
|
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||||
|
|
||||||
|
data["server_id"] = server_id
|
||||||
|
if "trigger" in data.keys():
|
||||||
|
triggers = ""
|
||||||
|
for item in data["trigger"]:
|
||||||
|
string = item + ","
|
||||||
|
triggers += string
|
||||||
|
data["trigger"] = triggers
|
||||||
|
self.controller.management.modify_webhook(webhook_id, data)
|
||||||
|
|
||||||
|
self.controller.management.add_to_audit_log(
|
||||||
|
auth_data[4]["user_id"],
|
||||||
|
f"Edited server {server_id}: updated webhook",
|
||||||
|
server_id,
|
||||||
|
self.get_remote_ip(),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.finish_json(200, {"status": "ok"})
|
||||||
|
|
||||||
|
def post(self, server_id: str, webhook_id: str):
|
||||||
|
auth_data = self.authenticate_user()
|
||||||
|
if not auth_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.controller.management.add_to_audit_log(
|
||||||
|
auth_data[4]["user_id"],
|
||||||
|
"Tested webhook",
|
||||||
|
server_id,
|
||||||
|
self.get_remote_ip(),
|
||||||
|
)
|
||||||
|
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
|
||||||
|
# if the user doesn't have access to the server, return an error
|
||||||
|
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||||
|
|
||||||
|
if (
|
||||||
|
EnumPermissionsServer.CONFIG
|
||||||
|
not in self.controller.server_perms.get_user_id_permissions_list(
|
||||||
|
auth_data[4]["user_id"], server_id
|
||||||
|
)
|
||||||
|
):
|
||||||
|
# if the user doesn't have Schedule permission, return an error
|
||||||
|
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||||
|
webhook = self.controller.management.get_webhook_by_id(webhook_id)
|
||||||
|
try:
|
||||||
|
webhook_provider = WebhookFactory.create_provider(webhook["webhook_type"])
|
||||||
|
webhook_provider.send(
|
||||||
|
server_name=self.controller.servers.get_server_data_by_id(server_id)[
|
||||||
|
"server_name"
|
||||||
|
],
|
||||||
|
title=f"Test Webhook: {webhook['name']}",
|
||||||
|
url=webhook["url"],
|
||||||
|
message=webhook["body"],
|
||||||
|
color=webhook["color"], # Prestigious purple!
|
||||||
|
bot_name="Crafty Webhooks Tester",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.finish_json(500, {"status": "error", "error": str(e)})
|
||||||
|
|
||||||
|
self.finish_json(200, {"status": "ok"})
|
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."""
|
82
app/classes/web/webhooks/discord_webhook.py
Normal file
82
app/classes/web/webhooks/discord_webhook.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
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, bot_name):
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
bot_name (str): Override for the Webhook's name set on creation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: A tuple containing the constructed payload (dict) incl headers (dict).
|
||||||
|
|
||||||
|
Note:
|
||||||
|
- Discord embed designer
|
||||||
|
- https://discohook.org/
|
||||||
|
"""
|
||||||
|
current_datetime = datetime.utcnow()
|
||||||
|
formatted_datetime = (
|
||||||
|
current_datetime.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convert the hex to an integer
|
||||||
|
sanitized_hex = color[1:] if color.startswith("#") else color
|
||||||
|
color_int = int(sanitized_hex, 16)
|
||||||
|
|
||||||
|
headers = {"Content-type": "application/json"}
|
||||||
|
payload = {
|
||||||
|
"username": bot_name,
|
||||||
|
"avatar_url": self.WEBHOOK_PFP_URL,
|
||||||
|
"embeds": [
|
||||||
|
{
|
||||||
|
"title": title,
|
||||||
|
"description": message,
|
||||||
|
"color": color_int,
|
||||||
|
"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 (str, optional): The color code for the embed's side stripe.
|
||||||
|
Defaults to a pretty blue if not provided.
|
||||||
|
bot_name (str): Override for the Webhook's name set on creation
|
||||||
|
|
||||||
|
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", "#005cd1") # Default to a color if not provided.
|
||||||
|
bot_name = kwargs.get("bot_name", self.WEBHOOK_USERNAME)
|
||||||
|
payload, headers = self._construct_discord_payload(
|
||||||
|
server_name, title, message, color, bot_name
|
||||||
|
)
|
||||||
|
return self._send_request(url, payload, headers)
|
74
app/classes/web/webhooks/mattermost_webhook.py
Normal file
74
app/classes/web/webhooks/mattermost_webhook.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
from app.classes.web.webhooks.base_webhook import WebhookProvider
|
||||||
|
|
||||||
|
|
||||||
|
class MattermostWebhook(WebhookProvider):
|
||||||
|
def _construct_mattermost_payload(self, server_name, title, message, bot_name):
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
bot_name (str): Override for the Webhook's name set on creation.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: A tuple containing the constructed payload (dict) incl headers (dict).
|
||||||
|
"""
|
||||||
|
formatted_text = (
|
||||||
|
f"-----\n\n"
|
||||||
|
f"#### {title}\n"
|
||||||
|
f"##### Server: ```{server_name}```\n\n"
|
||||||
|
f"```\n{message}\n```\n\n"
|
||||||
|
f"-----"
|
||||||
|
)
|
||||||
|
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
payload = {
|
||||||
|
"text": formatted_text,
|
||||||
|
"username": bot_name,
|
||||||
|
"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.
|
||||||
|
bot_name (str): Override for the Webhook's name set on creation, see note!
|
||||||
|
|
||||||
|
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:
|
||||||
|
- To set webhook username & pfp Mattermost needs to be configured to allow this!
|
||||||
|
- Mattermost's `config.json` setting is `"EnablePostUsernameOverride": true`
|
||||||
|
- Mattermost's `config.json` setting is `"EnablePostIconOverride": true`
|
||||||
|
"""
|
||||||
|
bot_name = kwargs.get("bot_name", self.WEBHOOK_USERNAME)
|
||||||
|
payload, headers = self._construct_mattermost_payload(
|
||||||
|
server_name, title, message, bot_name
|
||||||
|
)
|
||||||
|
return self._send_request(url, payload, headers)
|
98
app/classes/web/webhooks/slack_webhook.py
Normal file
98
app/classes/web/webhooks/slack_webhook.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
from app.classes.web.webhooks.base_webhook import WebhookProvider
|
||||||
|
|
||||||
|
|
||||||
|
class SlackWebhook(WebhookProvider):
|
||||||
|
def _construct_slack_payload(self, server_name, title, message, color, bot_name):
|
||||||
|
"""
|
||||||
|
Constructs the payload required for sending a Slack 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.
|
||||||
|
color (int): The color code for the side stripe in the Slack block.
|
||||||
|
bot_name (str): Override for the Webhook's name set on creation, (not working).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: A tuple containing the constructed payload (dict) incl headers (dict).
|
||||||
|
|
||||||
|
Note:
|
||||||
|
- Block Builder/designer
|
||||||
|
- https://app.slack.com/block-kit-builder/
|
||||||
|
"""
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
payload = {
|
||||||
|
"username": bot_name,
|
||||||
|
"attachments": [
|
||||||
|
{
|
||||||
|
"color": color,
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "section",
|
||||||
|
"text": {"type": "plain_text", "text": server_name},
|
||||||
|
},
|
||||||
|
{"type": "divider"},
|
||||||
|
{
|
||||||
|
"type": "section",
|
||||||
|
"text": {
|
||||||
|
"type": "mrkdwn",
|
||||||
|
"text": f"*{title}*\n{message}",
|
||||||
|
},
|
||||||
|
"accessory": {
|
||||||
|
"type": "image",
|
||||||
|
"image_url": self.WEBHOOK_PFP_URL,
|
||||||
|
"alt_text": "Crafty Controller Logo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "context",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "mrkdwn",
|
||||||
|
"text": (
|
||||||
|
f"*Crafty Controller "
|
||||||
|
f"v{self.CRAFTY_VERSION}*"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{"type": "divider"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload, headers
|
||||||
|
|
||||||
|
def send(self, server_name, title, url, message, **kwargs):
|
||||||
|
"""
|
||||||
|
Sends a Slack webhook notification using the given details.
|
||||||
|
|
||||||
|
The method constructs and dispatches a payload suitable for
|
||||||
|
Slack'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 (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).
|
||||||
|
|
||||||
|
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", "#005cd1") # Default to a color if not provided.
|
||||||
|
bot_name = kwargs.get("bot_name", self.WEBHOOK_USERNAME)
|
||||||
|
payload, headers = self._construct_slack_payload(
|
||||||
|
server_name, title, message, color, bot_name
|
||||||
|
)
|
||||||
|
return self._send_request(url, payload, headers)
|
124
app/classes/web/webhooks/teams_adaptive_webhook.py
Normal file
124
app/classes/web/webhooks/teams_adaptive_webhook.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from app.classes.web.webhooks.base_webhook import WebhookProvider
|
||||||
|
|
||||||
|
|
||||||
|
class TeamsWebhook(WebhookProvider):
|
||||||
|
def _construct_teams_payload(self, server_name, title, message):
|
||||||
|
"""
|
||||||
|
Constructs the payload required for sending a Teams Adaptive card notification.
|
||||||
|
|
||||||
|
This method prepares a payload for the Teams 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.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: A tuple containing the constructed payload (dict) incl headers (dict).
|
||||||
|
|
||||||
|
Note:
|
||||||
|
- Adaptive Card Designer
|
||||||
|
- https://www.adaptivecards.io/designer/
|
||||||
|
"""
|
||||||
|
current_datetime = datetime.utcnow()
|
||||||
|
formatted_datetime = current_datetime.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
headers = {"Content-type": "application/json"}
|
||||||
|
payload = {
|
||||||
|
"type": "message",
|
||||||
|
"attachments": [
|
||||||
|
{
|
||||||
|
"contentType": "application/vnd.microsoft.card.adaptive",
|
||||||
|
"content": {
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"type": "TextBlock",
|
||||||
|
"size": "Medium",
|
||||||
|
"weight": "Bolder",
|
||||||
|
"text": f"{title}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ColumnSet",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"type": "Column",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "Image",
|
||||||
|
"style": "Person",
|
||||||
|
"url": f"{self.WEBHOOK_PFP_URL}",
|
||||||
|
"size": "Small",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"width": "auto",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Column",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "TextBlock",
|
||||||
|
"weight": "Bolder",
|
||||||
|
"text": f"{server_name}",
|
||||||
|
"wrap": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "TextBlock",
|
||||||
|
"spacing": "None",
|
||||||
|
"text": "{{DATE("
|
||||||
|
+ f"{formatted_datetime}"
|
||||||
|
+ ",SHORT)}}",
|
||||||
|
"isSubtle": True,
|
||||||
|
"wrap": True,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"width": "stretch",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "TextBlock",
|
||||||
|
"text": f"{message}",
|
||||||
|
"wrap": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "TextBlock",
|
||||||
|
"text": f"Crafty Controller v{self.CRAFTY_VERSION}",
|
||||||
|
"wrap": True,
|
||||||
|
"separator": True,
|
||||||
|
"isSubtle": True,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||||
|
"version": "1.6",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload, headers
|
||||||
|
|
||||||
|
def send(self, server_name, title, url, message, **kwargs):
|
||||||
|
"""
|
||||||
|
Sends a Teams Adaptive card 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.
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
payload, headers = self._construct_teams_payload(server_name, title, message)
|
||||||
|
return self._send_request(url, payload, headers)
|
85
app/classes/web/webhooks/webhook_factory.py
Normal file
85
app/classes/web/webhooks/webhook_factory.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
from app.classes.web.webhooks.discord_webhook import DiscordWebhook
|
||||||
|
from app.classes.web.webhooks.mattermost_webhook import MattermostWebhook
|
||||||
|
from app.classes.web.webhooks.slack_webhook import SlackWebhook
|
||||||
|
from app.classes.web.webhooks.teams_adaptive_webhook import TeamsWebhook
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
"Slack": SlackWebhook,
|
||||||
|
"Teams": TeamsWebhook,
|
||||||
|
# "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 [
|
||||||
|
"start_server",
|
||||||
|
"stop_server",
|
||||||
|
"crash_detected",
|
||||||
|
"backup_server",
|
||||||
|
"jar_update",
|
||||||
|
"send_command",
|
||||||
|
"kill",
|
||||||
|
]
|
@ -31,6 +31,9 @@
|
|||||||
<a class="dropdown-item {% if data['active_link'] == 'admin_controls' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=admin_controls" role="tab" aria-selected="true"><i class="fas fa-users"></i> {{ translate('serverDetails', 'playerControls', data['lang']) }}</a>
|
<a class="dropdown-item {% if data['active_link'] == 'admin_controls' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=admin_controls" role="tab" aria-selected="true"><i class="fas fa-users"></i> {{ translate('serverDetails', 'playerControls', data['lang']) }}</a>
|
||||||
{% end %}
|
{% end %}
|
||||||
<a class="dropdown-item {% if data['active_link'] == 'metrics' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=metrics" role="tab" aria-selected="true"><i class="fa-solid fa-chart-line"></i> {{ translate('serverDetails', 'metrics', data['lang']) }}</a>
|
<a class="dropdown-item {% if data['active_link'] == 'metrics' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=metrics" role="tab" aria-selected="true"><i class="fa-solid fa-chart-line"></i> {{ translate('serverDetails', 'metrics', data['lang']) }}</a>
|
||||||
|
{% if data['permissions']['Config'] in data['user_permissions'] %}
|
||||||
|
<a class="dropdown-item {% if data['active_link'] == 'webhooks' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=webhooks" role="tab" aria-selected="true"><i class="fa-regular fa-bell"></i>{{ translate('webhooks', 'webhooks', data['lang']) }}</a>
|
||||||
|
{% end %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -53,4 +53,10 @@
|
|||||||
<a class="nav-link {% if data['active_link'] == 'metrics' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=metrics" role="tab" aria-selected="true">
|
<a class="nav-link {% if data['active_link'] == 'metrics' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=metrics" role="tab" aria-selected="true">
|
||||||
<i class="fa-solid fa-chart-line"></i>{{ translate('serverDetails', 'metrics', data['lang']) }}</a>
|
<i class="fa-solid fa-chart-line"></i>{{ translate('serverDetails', 'metrics', data['lang']) }}</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% if data['permissions']['Config'] in data['user_permissions'] %}
|
||||||
|
<li class="nav-item term-nav-item">
|
||||||
|
<a class="nav-link {% if data['active_link'] == 'webhooks' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=webhooks" role="tab" aria-selected="true">
|
||||||
|
<i class="fa-regular fa-bell"></i>{{ translate('webhooks', 'webhooks', data['lang']) }}</a>
|
||||||
|
</li>
|
||||||
|
{% end %}
|
||||||
</ul>
|
</ul>
|
278
app/frontend/templates/panel/server_webhook_edit.html
Normal file
278
app/frontend/templates/panel/server_webhook_edit.html
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
{% extends ../base.html %}
|
||||||
|
|
||||||
|
{% block meta %}
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
{% block title %}Crafty Controller - {{ translate('serverDetails', 'serverDetails', data['lang']) }}{% end %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/css/bootstrap-select.min.css">
|
||||||
|
<div class="content-wrapper">
|
||||||
|
|
||||||
|
<!-- Page Title Header Starts-->
|
||||||
|
<div class="row page-title-header">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="page-header">
|
||||||
|
<h4 class="page-title">
|
||||||
|
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
|
||||||
|
data['server_stats']['server_id']['server_name'] }}
|
||||||
|
<br />
|
||||||
|
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- Page Title Header Ends-->
|
||||||
|
|
||||||
|
{% include "parts/details_stats.html" %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<div class="col-sm-12 grid-margin">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body pt-0">
|
||||||
|
{% include "parts/server_controls_list.html" %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 col-sm-8">
|
||||||
|
{% if data['new_webhook'] == True %}
|
||||||
|
<form class="forms-sample" method="post" id="new_webhook_form"
|
||||||
|
action="/panel/new_webhook?id={{ data['server_stats']['server_id']['server_id'] }}">
|
||||||
|
{% else %}
|
||||||
|
<form class="forms-sample" method="post" id="webhook_form"
|
||||||
|
action="/panel/edit_webhook?id={{ data['server_stats']['server_id']['server_id'] }}&sch_id={{ data['webhook']['id'] }}">
|
||||||
|
{% end %}
|
||||||
|
<select class="form-select form-control form-control-lg select-css" id="webhook_type" name="webhook_type">
|
||||||
|
<option value="{{data['webhook']['webhook_type']}}">{{data['webhook']['webhook_type']}}</option>
|
||||||
|
{% for type in data['providers'] %}
|
||||||
|
{% if type != data['webhook']['webhook_type'] %}
|
||||||
|
<option value="{{type}}">{{type}}</option>
|
||||||
|
{%end%}
|
||||||
|
{% end %}
|
||||||
|
</select>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">{{ translate('webhooks', 'name' , data['lang']) }}</label>
|
||||||
|
<input type="input" class="form-control" name="name" id="name_input"
|
||||||
|
value="{{ data['webhook']['name']}}" maxlength="30" placeholder="Name" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="url">{{ translate('webhooks', 'url', data['lang']) }}</label>
|
||||||
|
<input type="input" class="form-control" name="url" id="url"
|
||||||
|
value="{{ data['webhook']['url']}}" placeholder="https://webhooks.craftycontrol.com/fakeurl" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="bot_name">{{ translate('webhooks', 'bot_name' , data['lang']) }}</label>
|
||||||
|
<input type="input" class="form-control" name="bot_name" id="bot_name_input"
|
||||||
|
value="{{ data['webhook']['bot_name']}}" maxlength="30" placeholder="Crafty Controller" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="trigger">{{ translate('webhooks', 'trigger', data['lang']) }}</label>
|
||||||
|
<select class="form-control selectpicker show-tick" name="trigger" id="trigger-select" data-icon-base="fas" data-tick-icon="fa-check" multiple data-style="custom-picker">
|
||||||
|
{% for trigger in data['triggers'] %}
|
||||||
|
{% if trigger in data["webhook"]["trigger"] %}
|
||||||
|
<option value="{{trigger}}" selected>{{translate('webhooks', trigger , data['lang'])}}</option>
|
||||||
|
{% else %}
|
||||||
|
<option value="{{trigger}}">{{translate('webhooks', trigger , data['lang'])}}</option>
|
||||||
|
{% end %}
|
||||||
|
{% end %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="body">{{ translate('webhooks', 'webhook_body', data['lang']) }}</label>
|
||||||
|
<textarea id="body-input" name="body" rows="4" cols="50">
|
||||||
|
{{ data["webhook"]["body"] }}
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="bot_name">{{ translate('webhooks', 'color' , data['lang']) }}</label>
|
||||||
|
<input type="color" class="form-control" name="color" id="color" value='{{data["webhook"]["color"]}}'>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-control custom-switch">
|
||||||
|
<input type="checkbox" class="custom-control-input" id="enabled" name="enabled" {% if data['webhook']['enabled'] %}checked{%end%}
|
||||||
|
value="1">
|
||||||
|
<label for="enabled" class="custom-control-label">{{ translate('webhooks', 'enabled', data['lang']) }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-success mr-2"><i class="fas fa-save"></i> {{
|
||||||
|
translate('serverConfig', 'save', data['lang']) }}</button>
|
||||||
|
<button type="reset"
|
||||||
|
onclick="location.href=`/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=webhooks`"
|
||||||
|
class="btn btn-light"><i class="fas fa-times"></i> {{ translate('serverConfig', 'cancel',
|
||||||
|
data['lang']) }}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
.custom-control-input:checked~.custom-control-label::before {
|
||||||
|
color: black !important;
|
||||||
|
background-color: blueviolet !important;
|
||||||
|
border-color: var(--outline) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-control-label::before {
|
||||||
|
background-color: white !important;
|
||||||
|
top: calc(-0.2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-switch .custom-control-label::after {
|
||||||
|
top: calc(-0.125rem + 1px);
|
||||||
|
}
|
||||||
|
#body-input {
|
||||||
|
background-color: var(--card-banner-bg);
|
||||||
|
outline-color: var(--outline);
|
||||||
|
color: var(--base-text);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!-- content-wrapper ends -->
|
||||||
|
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
<script>
|
||||||
|
$(function () {
|
||||||
|
$('.form-check-input').bootstrapToggle({
|
||||||
|
on: '',
|
||||||
|
off: ''
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
|
||||||
|
function getCookie(name) {
|
||||||
|
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
|
||||||
|
return r ? r[1] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function replacer(key, value) {
|
||||||
|
if (key != "start_time" && key != "cron_string" && key != "interval_type") {
|
||||||
|
if (typeof value == "boolean") {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
console.log(key)
|
||||||
|
if (key === "interval" && value === ""){
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (key === "command" && typeof(value === "integer")){
|
||||||
|
return value.toString();
|
||||||
|
}else {
|
||||||
|
return (isNaN(value) ? value : +value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (value === "" && key == "start_time"){
|
||||||
|
return "00:00";
|
||||||
|
}else{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverId = new URLSearchParams(document.location.search).get('id');
|
||||||
|
const webhookId = new URLSearchParams(document.location.search).get('webhook_id');
|
||||||
|
$(document).ready(function () {
|
||||||
|
console.log("ready!");
|
||||||
|
console.log('ready for JS!');
|
||||||
|
$('.selectpicker').selectpicker("refresh");
|
||||||
|
$("#new_webhook_form").on("submit", async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var token = getCookie("_xsrf")
|
||||||
|
let webhookForm = document.getElementById("new_webhook_form");
|
||||||
|
let select_val = JSON.stringify($('#trigger-select').val());
|
||||||
|
select_val = JSON.parse(select_val);
|
||||||
|
|
||||||
|
let formData = new FormData(webhookForm);
|
||||||
|
//Create an object from the form data entries
|
||||||
|
let formDataObject = Object.fromEntries(formData.entries());
|
||||||
|
//We need to make sure these are sent regardless of whether or not they're checked
|
||||||
|
formDataObject.enabled = $("#enabled").prop('checked');
|
||||||
|
formDataObject.trigger = select_val;
|
||||||
|
|
||||||
|
console.log(formDataObject);
|
||||||
|
// Format the plain form data as JSON
|
||||||
|
let formDataJsonString = JSON.stringify(formDataObject, replacer);
|
||||||
|
|
||||||
|
let res = await fetch(`/api/v2/servers/${serverId}/webhook/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-XSRFToken': token,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: formDataJsonString,
|
||||||
|
});
|
||||||
|
let responseData = await res.json();
|
||||||
|
if (responseData.status === "ok") {
|
||||||
|
window.location.href = `/panel/server_detail?id=${serverId}&subpage=webhooks`;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
bootbox.alert({
|
||||||
|
title: responseData.status,
|
||||||
|
message: responseData.error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#webhook_form").on("submit", async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var token = getCookie("_xsrf");
|
||||||
|
let webhookForm = document.getElementById("webhook_form");
|
||||||
|
let select_val = JSON.stringify($('#trigger-select').val());
|
||||||
|
select_val = JSON.parse(select_val);
|
||||||
|
|
||||||
|
let formData = new FormData(webhookForm);
|
||||||
|
//Create an object from the form data entries
|
||||||
|
let formDataObject = Object.fromEntries(formData.entries());
|
||||||
|
//We need to make sure these are sent regardless of whether or not they're checked
|
||||||
|
formDataObject.enabled = $("#enabled").prop('checked');
|
||||||
|
formDataObject.trigger = select_val;
|
||||||
|
if(formDataObject.webhook_type != "Discord"){
|
||||||
|
delete formDataObject.color
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(formDataObject);
|
||||||
|
// Format the plain form data as JSON
|
||||||
|
let formDataJsonString = JSON.stringify(formDataObject, replacer);
|
||||||
|
|
||||||
|
let res = await fetch(`/api/v2/servers/${serverId}/webhook/${webhookId}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'X-XSRFToken': token,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: formDataJsonString,
|
||||||
|
});
|
||||||
|
let responseData = await res.json();
|
||||||
|
if (responseData.status === "ok") {
|
||||||
|
window.location.href = `/panel/server_detail?id=${serverId}&subpage=webhooks`;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
bootbox.alert({
|
||||||
|
title: responseData.status,
|
||||||
|
message: responseData.error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
function hexToDiscordInt(hexColor) {
|
||||||
|
// Remove the hash at the start if it's there
|
||||||
|
const sanitizedHex = hexColor.startsWith('#') ? hexColor.slice(1) : hexColor;
|
||||||
|
|
||||||
|
// Convert the hex to an integer
|
||||||
|
return parseInt(sanitizedHex, 16);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/js/bootstrap-select.min.js"></script>
|
||||||
|
|
||||||
|
{% end %}
|
380
app/frontend/templates/panel/server_webhooks.html
Normal file
380
app/frontend/templates/panel/server_webhooks.html
Normal file
@ -0,0 +1,380 @@
|
|||||||
|
{% extends ../base.html %}
|
||||||
|
|
||||||
|
{% block meta %}
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
{% block title %}Crafty Controller - {{ translate('serverDetails', 'serverDetails', data['lang']) }}{% end %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="content-wrapper">
|
||||||
|
|
||||||
|
<!-- Page Title Header Starts-->
|
||||||
|
<div class="row page-title-header">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="page-header">
|
||||||
|
<h4 class="page-title">
|
||||||
|
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
|
||||||
|
data['server_stats']['server_id']['server_name'] }}
|
||||||
|
<br />
|
||||||
|
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- Page Title Header Ends-->
|
||||||
|
|
||||||
|
{% include "parts/details_stats.html" %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<div class="col-sm-12 grid-margin">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body pt-0">
|
||||||
|
|
||||||
|
<span class="d-none d-sm-block">
|
||||||
|
{% include "parts/server_controls_list.html" %}
|
||||||
|
</span>
|
||||||
|
<span class="d-block d-sm-none">
|
||||||
|
{% include "parts/m_server_controls_list.html" %}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 col-sm-12" style="overflow-x:auto;">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header header-sm d-flex justify-content-between align-items-center">
|
||||||
|
<h4 class="card-title"><i class="fa-regular fa-bell"></i> {{ translate('webhooks', 'webhooks', data['lang']) }} </h4>
|
||||||
|
{% if data['user_data']['hints'] %}
|
||||||
|
<span class="too_small" title="{{ translate('serverSchedules', 'cannotSee', data['lang']) }}" ,
|
||||||
|
data-content="{{ translate('serverSchedules', 'cannotSeeOnMobile', data['lang']) }}" ,
|
||||||
|
data-placement="bottom"></span>
|
||||||
|
{% end %}
|
||||||
|
<div><button
|
||||||
|
onclick="location.href=`/panel/add_webhook?id={{ data['server_stats']['server_id']['server_id'] }}`"
|
||||||
|
class="btn btn-info">{{ translate('webhooks', 'new', data['lang']) }}<i
|
||||||
|
class="fas fa-pencil-alt"></i></button></div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-hover d-none d-lg-block responsive-table" id="webhook_table" width="100%" style="table-layout:fixed;">
|
||||||
|
<thead>
|
||||||
|
<tr class="rounded">
|
||||||
|
<th style="width: 10%; min-width: 10px;">{{ translate('webhooks', 'name', data['lang']) }}
|
||||||
|
</th>
|
||||||
|
<th style="width: 20%; min-width: 50px;">{{ translate('webhooks', 'type', data['lang']) }}</th>
|
||||||
|
<th style="width: 50%; min-width: 50px;">{{ translate('webhooks', 'trigger', data['lang']) }}</th>
|
||||||
|
<th style="width: 10%; min-width: 50px;">{{ translate('webhooks', 'enabled',
|
||||||
|
data['lang']) }}</th>
|
||||||
|
<th style="width: 10%; min-width: 50px;">{{ translate('webhooks', 'edit', data['lang'])
|
||||||
|
}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for webhook in data['webhooks'] %}
|
||||||
|
<tr>
|
||||||
|
<td id="{{webhook.name}}" class="id">
|
||||||
|
<p>{{webhook.name}}</p>
|
||||||
|
</td>
|
||||||
|
<td id="{{webhook.webhook_type}}" class="type">
|
||||||
|
<p>{{webhook.webhook_type}}</p>
|
||||||
|
</td>
|
||||||
|
<td id="{{webhook.trigger}}" class="trigger" style="overflow: scroll; max-width: 30px;">
|
||||||
|
<ul>
|
||||||
|
{% for trigger in webhook.trigger.split(",") %}
|
||||||
|
{% if trigger in data["triggers"] %}
|
||||||
|
<li>{{translate('webhooks', trigger , data['lang'])}}</li>
|
||||||
|
{%end%}
|
||||||
|
{%end%}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td id="{{webhook.enabled}}" class="enabled">
|
||||||
|
<input style="width: 10px !important;" type="checkbox" class="webhook-enabled-toggle" data-webhook-id="{{webhook.id}}" data-webhook-enabled="{{ 'true' if webhook.enabled else 'false' }}">
|
||||||
|
</td>
|
||||||
|
<td id="webhook_edit" class="action">
|
||||||
|
<button onclick="window.location.href='/panel/webhook_edit?id={{ data['server_stats']['server_id']['server_id'] }}&webhook_id={{webhook.id}}'" class="btn btn-info">
|
||||||
|
<i class="fas fa-pencil-alt"></i>
|
||||||
|
</button>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<button data-webhook={{ webhook.id }} class="btn btn-danger del_button">
|
||||||
|
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button data-webhook={{ webhook.id }} data-toggle="tooltip" title="{{ translate('webhooks', 'run', data['lang']) }}" class="btn btn-outline-warning test-socket">
|
||||||
|
<i class="fa-solid fa-vial"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% end %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<table class="table table-hover d-block d-lg-none responsive-table" id="webhook_table_mini" width="100%" style="table-layout:fixed;">
|
||||||
|
<thead>
|
||||||
|
<tr class="rounded">
|
||||||
|
<th style="width: 33.33%; min-width: 10px;">Name
|
||||||
|
</th>
|
||||||
|
<th style="width: 33.33%; min-width: 50px;">{{ translate('webhooks', 'enabled',
|
||||||
|
data['lang']) }}</th>
|
||||||
|
<th style="width: 33.33%; min-width: 50px;">{{ translate('webhooks', 'edit', data['lang'])
|
||||||
|
}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for webhook in data['webhooks'] %}
|
||||||
|
<tr>
|
||||||
|
<td id="{{webhook.name}}" class="id">
|
||||||
|
<p>{{webhook.name}}</p>
|
||||||
|
</td>
|
||||||
|
<td id="{{webhook.enabled}}" class="enabled">
|
||||||
|
<input style="width: 10px !important;" type="checkbox" class="webhook-enabled-toggle" data-webhook-id="{{webhook.id}}" data-webhook-enabled="{{ 'true' if webhook.enabled else 'false' }}">
|
||||||
|
</td>
|
||||||
|
<td id="webhook_edit" class="action">
|
||||||
|
<button onclick="window.location.href='/panel/webhook_edit?id={{ data['server_stats']['server_id']['server_id'] }}&webhook_id={{webhook.id}}'" class="btn btn-info">
|
||||||
|
<i class="fas fa-pencil-alt"></i>
|
||||||
|
</button>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<button data-webhook={{ webhook.id }} class="btn btn-danger del_button">
|
||||||
|
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button data-webhook={{ webhook.id }} data-toggle="tooltip" title="{{ translate('webhooks', 'run', data['lang']) }}" class="btn btn-outline-warning test-socket">
|
||||||
|
<i class="fa-solid fa-vial"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% end %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
.popover-body {
|
||||||
|
color: white !important;
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-handle {
|
||||||
|
background-color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-on {
|
||||||
|
color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
height: 0px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||||
|
td::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbar for IE, Edge and Firefox */
|
||||||
|
td {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
/* IE and Edge */
|
||||||
|
scrollbar-width: none;
|
||||||
|
/* Firefox */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!-- content-wrapper ends -->
|
||||||
|
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
<script>
|
||||||
|
$(document).ready(function () {
|
||||||
|
console.log('ready for JS!')
|
||||||
|
$('#webhook_table').DataTable({
|
||||||
|
'order': [4, 'asc'],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
|
$(document).ready(function () {
|
||||||
|
console.log('ready for JS!')
|
||||||
|
$('#webhook_table_mini').DataTable({
|
||||||
|
'order': [2, 'asc']
|
||||||
|
}
|
||||||
|
);
|
||||||
|
document.getElementById('webhook_table_mini_wrapper').hidden = true;
|
||||||
|
});
|
||||||
|
$(document).ready(function () {
|
||||||
|
$('[data-toggle="popover"]').popover();
|
||||||
|
if ($(window).width() < 1000) {
|
||||||
|
$('.too_small').popover("show");
|
||||||
|
document.getElementById('webhook_table_wrapper').hidden = true;
|
||||||
|
document.getElementById('webhook_table_mini_wrapper').hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
$(window).ready(function () {
|
||||||
|
$('body').click(function () {
|
||||||
|
$('.too_small').popover("hide");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
$(window).resize(function () {
|
||||||
|
// This will execute whenever the window is resized
|
||||||
|
if ($(window).width() < 1000) {
|
||||||
|
$('.too_small').popover("show");
|
||||||
|
document.getElementById('webhook_table_wrapper').hidden = true;
|
||||||
|
document.getElementById('webhook_table_mini_wrapper').hidden = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$('.too_small').popover("hide");
|
||||||
|
document.getElementById('webhook_table_wrapper').hidden = false;
|
||||||
|
document.getElementById('webhook_table_mini_wrapper').hidden = true;
|
||||||
|
} // New width
|
||||||
|
});
|
||||||
|
|
||||||
|
function debounce(func, timeout = 300) {
|
||||||
|
let timer;
|
||||||
|
return (...args) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => { func.apply(this, args); }, timeout);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
$(() => {
|
||||||
|
$('.webhook-enabled-toggle').bootstrapToggle({
|
||||||
|
on: 'Yes',
|
||||||
|
off: 'No',
|
||||||
|
onstyle: 'success',
|
||||||
|
offstyle: 'danger',
|
||||||
|
})
|
||||||
|
$('.webhook-enabled-toggle').each(function () {
|
||||||
|
const enabled = JSON.parse(this.getAttribute('data-webhook-enabled'));
|
||||||
|
$(this).bootstrapToggle(enabled ? 'on' : 'off')
|
||||||
|
})
|
||||||
|
$('.webhook-enabled-toggle').change(function () {
|
||||||
|
const id = this.getAttribute('data-webhook-id');
|
||||||
|
const enabled = this.checked;
|
||||||
|
|
||||||
|
fetch(`/api/v2/servers/{{data['server_id']}}/webhook/${id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify({ enabled }),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
const serverId = new URLSearchParams(document.location.search).get('id')
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
|
||||||
|
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
|
||||||
|
function getCookie(name) {
|
||||||
|
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
|
||||||
|
return r ? r[1] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
console.log("ready!");
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$(".del_button").click(function () {
|
||||||
|
var webhook_id = $(this).data('webhook');
|
||||||
|
|
||||||
|
bootbox.confirm({
|
||||||
|
message: "{{ translate('webhooks', 'areYouSureDel', data['lang']) }}",
|
||||||
|
buttons: {
|
||||||
|
cancel: {
|
||||||
|
label: '<i class="fas fa-times"></i> {{ translate("serverSchedules", "cancel", data['lang']) }}'
|
||||||
|
},
|
||||||
|
confirm: {
|
||||||
|
className: 'btn-outline-danger',
|
||||||
|
label: '<i class="fas fa-check"></i> {{ translate("serverSchedules", "confirm", data['lang']) }}'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
callback: function (result) {
|
||||||
|
console.log(result);
|
||||||
|
if (result == true) {
|
||||||
|
del_hook(webhook_id, serverId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".test-socket").click(function () {
|
||||||
|
var webhook_id = $(this).data('webhook');
|
||||||
|
|
||||||
|
bootbox.confirm({
|
||||||
|
message: "{{ translate('webhooks', 'areYouSureRun', data['lang']) }}",
|
||||||
|
buttons: {
|
||||||
|
cancel: {
|
||||||
|
label: '<i class="fas fa-times"></i> {{ translate("serverSchedules", "cancel", data['lang']) }}'
|
||||||
|
},
|
||||||
|
confirm: {
|
||||||
|
className: 'btn-outline-danger',
|
||||||
|
label: '<i class="fas fa-check"></i> {{ translate("serverSchedules", "confirm", data['lang']) }}'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
callback: function (result) {
|
||||||
|
console.log(result);
|
||||||
|
if (result == true) {
|
||||||
|
test_hook(webhook_id, serverId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
async function test_hook(webhook_id, id) {
|
||||||
|
var token = getCookie("_xsrf")
|
||||||
|
|
||||||
|
let res = await fetch(`/api/v2/servers/${id}/webhook/${webhook_id}/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'token': token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let responseData = await res.json();
|
||||||
|
if (responseData.status === "ok") {
|
||||||
|
bootbox.alert("Webhook Sent!")
|
||||||
|
}else{
|
||||||
|
console.log(responseData);
|
||||||
|
bootbox.alert({
|
||||||
|
title: responseData.status,
|
||||||
|
message: responseData.error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function del_hook(webhook_id, id) {
|
||||||
|
var token = getCookie("_xsrf")
|
||||||
|
|
||||||
|
let res = await fetch(`/api/v2/servers/${id}/webhook/${webhook_id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'token': token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let responseData = await res.json();
|
||||||
|
if (responseData.status === "ok") {
|
||||||
|
window.location.reload();
|
||||||
|
}else{
|
||||||
|
bootbox.alert({
|
||||||
|
title: responseData.status,
|
||||||
|
message: responseData.error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% end %}
|
27
app/migrations/20230603_webhooks.py
Normal file
27
app/migrations/20230603_webhooks.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Generated by database migrator
|
||||||
|
import peewee
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(migrator, database, **kwargs):
|
||||||
|
migrator.drop_columns("webhooks", ["name", "method", "url", "event", "send_data"])
|
||||||
|
migrator.add_columns(
|
||||||
|
"webhooks",
|
||||||
|
server_id=peewee.IntegerField(null=True),
|
||||||
|
webhook_type=peewee.CharField(default="Custom"),
|
||||||
|
name=peewee.CharField(default="Custom Webhook", max_length=64),
|
||||||
|
url=peewee.CharField(default=""),
|
||||||
|
bot_name=peewee.CharField(default="Crafty Controller"),
|
||||||
|
trigger=peewee.CharField(default="server_start,server_stop"),
|
||||||
|
body=peewee.CharField(default=""),
|
||||||
|
color=peewee.CharField(default=""),
|
||||||
|
enabled=peewee.BooleanField(default=True),
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
Write your migrations here.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def rollback(migrator, database, **kwargs):
|
||||||
|
"""
|
||||||
|
Write your rollback migrations here.
|
||||||
|
"""
|
@ -627,5 +627,28 @@
|
|||||||
"uses": "Number of uses allowed (-1==No Limit)",
|
"uses": "Number of uses allowed (-1==No Limit)",
|
||||||
"manager": "Manager",
|
"manager": "Manager",
|
||||||
"selectManager": "Select Manager for User"
|
"selectManager": "Select Manager for User"
|
||||||
|
},
|
||||||
|
"webhooks": {
|
||||||
|
"webhooks": "Webhooks",
|
||||||
|
"name": "Name",
|
||||||
|
"type": "Webhook Type",
|
||||||
|
"trigger": "Trigger",
|
||||||
|
"enabled": "Enabled",
|
||||||
|
"url": "Webhook URL",
|
||||||
|
"bot_name": "Bot Name",
|
||||||
|
"webhook_body": "Webhook Body",
|
||||||
|
"color": "Select Color Accent",
|
||||||
|
"areYouSureDel": "Are you sure you want to delete this webhook?",
|
||||||
|
"areYouSureRun": "Are you sure you want to test this webhook?",
|
||||||
|
"edit": "Edit",
|
||||||
|
"run": "Test Run Webhook",
|
||||||
|
"new": "New Webhook",
|
||||||
|
"start_server": "Server Started",
|
||||||
|
"stop_server": "Server Stopped",
|
||||||
|
"crash_detected": "Server Crashed",
|
||||||
|
"jar_update": "Server Executable Updated",
|
||||||
|
"backup_server": "Server Backup Completed",
|
||||||
|
"send_command": "Server Command Received",
|
||||||
|
"kill": "Server Killed"
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user