mirror of
https://gitlab.com/crafty-controller/crafty-4.git
synced 2025-01-18 17:15:13 +01:00
Merge branch 'dev' into bugfix/reaction-of-reaction
This commit is contained in:
commit
66e74b6c04
@ -2,6 +2,7 @@
|
||||
## --- [4.2.0] - 2023/TBD
|
||||
### 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))
|
||||
- Add initial Webhook Notification (Discord, Mattermost, Slack, Teams) ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/594))
|
||||
### 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))
|
||||
- Fix hidden servers appearing visible on public mobile status page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/612))
|
||||
@ -23,6 +24,7 @@
|
||||
- Make files hover cursor pointer ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/627))
|
||||
- Use `Jar` class naming for jar refresh to make room for steamCMD naming in the future ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/630))
|
||||
- Improve ui visibility of Build Wizard selection tabs ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/633))
|
||||
- Add additional logging for server bootstrap & moves unnecessary logging to `debug` for improved log clarity ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/635))
|
||||
### Lang
|
||||
TBD
|
||||
<br><br>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import queue
|
||||
|
||||
from app.classes.models.management import HelpersManagement
|
||||
from app.classes.models.management import HelpersManagement, HelpersWebhooks
|
||||
from app.classes.models.servers import HelperServers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -206,3 +206,30 @@ class ManagementController:
|
||||
@staticmethod
|
||||
def 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)
|
||||
|
@ -226,7 +226,7 @@ class Stats:
|
||||
def get_server_players(self, server_id):
|
||||
server = HelperServers.get_server_data_by_id(server_id)
|
||||
|
||||
logger.info(f"Getting players for server {server}")
|
||||
logger.debug(f"Getting players for server {server['server_name']}")
|
||||
|
||||
internal_ip = server["server_ip"]
|
||||
server_port = server["server_port"]
|
||||
|
@ -79,11 +79,15 @@ class HostStats(BaseModel):
|
||||
# **********************************************************************************
|
||||
class Webhooks(BaseModel):
|
||||
id = AutoField()
|
||||
name = CharField(max_length=64, unique=True, index=True)
|
||||
method = CharField(default="POST")
|
||||
url = CharField(unique=True)
|
||||
event = CharField(default="")
|
||||
send_data = BooleanField(default=True)
|
||||
server_id = IntegerField(null=True)
|
||||
name = CharField(default="Custom Webhook", max_length=64)
|
||||
url = CharField(default="")
|
||||
webhook_type = CharField(default="Custom")
|
||||
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:
|
||||
table_name = "webhooks"
|
||||
@ -501,3 +505,82 @@ class HelpersManagement:
|
||||
f"Not removing {dir_to_del} from excluded directories - "
|
||||
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.utils import ZoneInfoNotFoundError
|
||||
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.mc_ping import ping, ping_bedrock
|
||||
from app.classes.models.servers import HelperServers, Servers
|
||||
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.server_permissions import PermissionsServers
|
||||
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.null_writer import NullWriter
|
||||
from app.classes.shared.websocket_manager import WebSocketManager
|
||||
from app.classes.web.webhooks.webhook_factory import WebhookFactory
|
||||
|
||||
with redirect_stderr(NullWriter()):
|
||||
import psutil
|
||||
@ -165,6 +166,45 @@ class ServerInstance:
|
||||
self.stats_helper.server_crash_reset()
|
||||
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
|
||||
# **********************************************************************************
|
||||
@ -262,13 +302,13 @@ class ServerInstance:
|
||||
seconds=30,
|
||||
id="save_stats_" + str(self.server_id),
|
||||
)
|
||||
except:
|
||||
self.server_scheduler.remove_job("save_" + str(self.server_id))
|
||||
except ConflictingIdError:
|
||||
self.server_scheduler.remove_job("save_stats_" + str(self.server_id))
|
||||
self.server_scheduler.add_job(
|
||||
self.record_server_stats,
|
||||
"interval",
|
||||
seconds=30,
|
||||
id="save_" + str(self.server_id),
|
||||
id="save_stats_" + str(self.server_id),
|
||||
)
|
||||
|
||||
def setup_server_run_command(self):
|
||||
@ -332,6 +372,7 @@ class ServerInstance:
|
||||
logger.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):
|
||||
if not user_id:
|
||||
user_lang = self.helper.get_setting("language")
|
||||
@ -775,6 +816,7 @@ class ServerInstance:
|
||||
if self.server_thread:
|
||||
self.server_thread.join()
|
||||
|
||||
@callback
|
||||
def stop_server(self):
|
||||
running = self.check_running()
|
||||
if not running:
|
||||
@ -792,6 +834,7 @@ class ServerInstance:
|
||||
f"Assuming it was never started."
|
||||
)
|
||||
if self.settings["stop_command"]:
|
||||
logger.info(f"Stop command requested for {self.settings['server_name']}.")
|
||||
self.send_command(self.settings["stop_command"])
|
||||
self.write_player_cache()
|
||||
else:
|
||||
@ -861,6 +904,9 @@ class ServerInstance:
|
||||
if not self.check_running():
|
||||
self.run_threaded_server(user_id)
|
||||
else:
|
||||
logger.info(
|
||||
f"Restart command detected. Sending stop command to {self.server_id}."
|
||||
)
|
||||
self.stop_threaded_server()
|
||||
time.sleep(2)
|
||||
self.run_threaded_server(user_id)
|
||||
@ -882,6 +928,7 @@ class ServerInstance:
|
||||
self.last_rc = poll
|
||||
return False
|
||||
|
||||
@callback
|
||||
def send_command(self, command):
|
||||
if not self.check_running() and command.lower() != "start":
|
||||
logger.warning(f'Server not running, unable to send command "{command}"')
|
||||
@ -894,6 +941,7 @@ class ServerInstance:
|
||||
self.process.stdin.flush()
|
||||
return True
|
||||
|
||||
@callback
|
||||
def crash_detected(self, name):
|
||||
# clear the old scheduled watcher task
|
||||
self.server_scheduler.remove_job(f"c_{self.server_id}")
|
||||
@ -914,6 +962,7 @@ class ServerInstance:
|
||||
f"The server {name} has crashed and will be restarted. "
|
||||
f"Restarting server"
|
||||
)
|
||||
|
||||
self.run_threaded_server(None)
|
||||
return True
|
||||
logger.critical(
|
||||
@ -926,6 +975,7 @@ class ServerInstance:
|
||||
)
|
||||
return False
|
||||
|
||||
@callback
|
||||
def kill(self):
|
||||
logger.info(f"Terminating server {self.server_id} and all child processes")
|
||||
try:
|
||||
@ -1014,6 +1064,7 @@ class ServerInstance:
|
||||
f.write("eula=true")
|
||||
self.run_threaded_server(user_id)
|
||||
|
||||
@callback
|
||||
def backup_server(self):
|
||||
if self.settings["backup_path"] == "":
|
||||
logger.critical("Backup path is None. Canceling Backup!")
|
||||
@ -1228,6 +1279,7 @@ class ServerInstance:
|
||||
if f["path"].endswith(".zip")
|
||||
]
|
||||
|
||||
@callback
|
||||
def jar_update(self):
|
||||
self.stats_helper.set_update(True)
|
||||
update_thread = threading.Thread(
|
||||
@ -1631,7 +1683,7 @@ class ServerInstance:
|
||||
def get_server_players(self):
|
||||
server = HelperServers.get_server_data_by_id(self.server_id)
|
||||
|
||||
logger.info(f"Getting players for server {server}")
|
||||
logger.debug(f"Getting players for server {server['server_name']}")
|
||||
|
||||
internal_ip = server["server_ip"]
|
||||
server_port = server["server_port"]
|
||||
|
@ -25,6 +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.webhooks.webhook_factory import WebhookFactory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -344,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"]:
|
||||
@ -745,6 +748,22 @@ class PanelHandler(BaseHandler):
|
||||
page_data["history_stats"] = self.controller.servers.get_history_stats(
|
||||
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():
|
||||
banned_players = self.controller.servers.get_banned_players(server_id)
|
||||
@ -1012,6 +1031,110 @@ class PanelHandler(BaseHandler):
|
||||
|
||||
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":
|
||||
server_id = self.get_argument("id", 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 (
|
||||
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.users.index import ApiUsersIndexHandler
|
||||
from app.classes.web.routes.api.users.user.index import ApiUsersUserIndexHandler
|
||||
@ -250,6 +256,16 @@ def api_handlers(handler_args):
|
||||
ApiServersServerHistoryHandler,
|
||||
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_]+)/?",
|
||||
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>
|
||||
{% 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>
|
||||
{% 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>
|
@ -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">
|
||||
<i class="fa-solid fa-chart-line"></i>{{ translate('serverDetails', 'metrics', data['lang']) }}</a>
|
||||
</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>
|
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)",
|
||||
"manager": "Manager",
|
||||
"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…
x
Reference in New Issue
Block a user