import logging import datetime from peewee import ( ForeignKeyField, CharField, IntegerField, DateTimeField, FloatField, TextField, AutoField, BooleanField, ) from playhouse.shortcuts import model_to_dict from app.classes.models.base_model import BaseModel from app.classes.models.users import HelperUsers from app.classes.models.servers import Servers from app.classes.models.server_permissions import PermissionsServers from app.classes.shared.main_models import DatabaseShortcuts from app.classes.shared.websocket_manager import WebSocketManager logger = logging.getLogger(__name__) # ********************************************************************************** # Audit_Log Class # ********************************************************************************** class AuditLog(BaseModel): audit_id = AutoField() created = DateTimeField(default=datetime.datetime.now) user_name = CharField(default="") user_id = IntegerField(default=0, index=True) source_ip = CharField(default="127.0.0.1") server_id = ForeignKeyField( Servers, backref="audit_server", null=True ) # When auditing global events, use server ID null log_msg = TextField(default="") class Meta: table_name = "audit_log" # ********************************************************************************** # Crafty Settings Class # ********************************************************************************** class CraftySettings(BaseModel): id = AutoField() secret_api_key = CharField(default="") cookie_secret = CharField(default="") login_photo = CharField(default="login_1.jpg") login_opacity = IntegerField(default=100) master_server_dir = CharField(default="") class Meta: table_name = "crafty_settings" # ********************************************************************************** # Host_Stats Class # ********************************************************************************** class HostStats(BaseModel): time = DateTimeField(default=datetime.datetime.now, index=True) boot_time = CharField(default="") cpu_usage = FloatField(default=0) cpu_cores = IntegerField(default=0) cpu_cur_freq = FloatField(default=0) cpu_max_freq = FloatField(default=0) mem_percent = FloatField(default=0) mem_usage = CharField(default="") mem_total = CharField(default="") disk_json = TextField(default="") class Meta: table_name = "host_stats" # ********************************************************************************** # Webhooks Class # ********************************************************************************** class Webhooks(BaseModel): id = AutoField() server_id = ForeignKeyField(Servers, backref="webhook_server", 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" # ********************************************************************************** # Schedules Class # ********************************************************************************** class Schedules(BaseModel): schedule_id = IntegerField(unique=True, primary_key=True) server_id = ForeignKeyField(Servers, backref="schedule_server") enabled = BooleanField() action = CharField() interval = IntegerField() interval_type = CharField() start_time = CharField(null=True) command = CharField(null=True) name = CharField() one_time = BooleanField(default=False) cron_string = CharField(default="") parent = IntegerField(null=True) delay = IntegerField(default=0) next_run = CharField(default="") class Meta: table_name = "schedules" # ********************************************************************************** # Backups Class # ********************************************************************************** class Backups(BaseModel): excluded_dirs = CharField(null=True) max_backups = IntegerField() server_id = ForeignKeyField(Servers, backref="backups_server") compress = BooleanField(default=False) shutdown = BooleanField(default=False) before = CharField(default="") after = CharField(default="") class Meta: table_name = "backups" class HelpersManagement: def __init__(self, database, helper): self.database = database self.helper = helper # ********************************************************************************** # Host_Stats Methods # ********************************************************************************** @staticmethod def get_latest_hosts_stats(): # pylint: disable=no-member query = HostStats.select().order_by(HostStats.id.desc()).get() return model_to_dict(query) # ********************************************************************************** # Audit_Log Methods # ********************************************************************************** @staticmethod def get_activity_log(): query = AuditLog.select() return DatabaseShortcuts.return_db_rows(query) def add_to_audit_log(self, user_id, log_msg, server_id=None, source_ip=None): logger.debug(f"Adding to audit log User:{user_id} - Message: {log_msg} ") user_data = HelperUsers.get_user(user_id) audit_msg = f"{str(user_data['username']).capitalize()} {log_msg}" server_users = PermissionsServers.get_server_user_list(server_id) for user in server_users: try: WebSocketManager().broadcast_user(user, "notification", audit_msg) except Exception as e: logger.error(f"Error broadcasting to user {user} - {e}") AuditLog.insert( { AuditLog.user_name: user_data["username"], AuditLog.user_id: user_id, AuditLog.server_id: server_id, AuditLog.log_msg: audit_msg, AuditLog.source_ip: source_ip, } ).execute() # deletes records when there's more than 300 ordered = AuditLog.select().order_by(+AuditLog.created) for item in ordered: if not self.helper.get_setting("max_audit_entries"): max_entries = 300 else: max_entries = self.helper.get_setting("max_audit_entries") if AuditLog.select().count() > max_entries: AuditLog.delete().where(AuditLog.audit_id == item.audit_id).execute() else: return def add_to_audit_log_raw(self, user_name, user_id, server_id, log_msg, source_ip): AuditLog.insert( { AuditLog.user_name: user_name, AuditLog.user_id: user_id, AuditLog.server_id: server_id, AuditLog.log_msg: log_msg, AuditLog.source_ip: source_ip, } ).execute() # deletes records when there's more than 300 ordered = AuditLog.select().order_by(+AuditLog.created) for item in ordered: # configurable through app/config/config.json if not self.helper.get_setting("max_audit_entries"): max_entries = 300 else: max_entries = self.helper.get_setting("max_audit_entries") if AuditLog.select().count() > max_entries: AuditLog.delete().where(AuditLog.audit_id == item.audit_id).execute() else: return @staticmethod def create_crafty_row(): CraftySettings.insert( { CraftySettings.secret_api_key: "", CraftySettings.cookie_secret: "", CraftySettings.login_photo: "login_1.jpg", CraftySettings.login_opacity: 100, } ).execute() @staticmethod def set_secret_api_key(key): CraftySettings.update({CraftySettings.secret_api_key: key}).where( CraftySettings.id == 1 ).execute() @staticmethod def get_secret_api_key(): settings = CraftySettings.select(CraftySettings.secret_api_key).where( CraftySettings.id == 1 ) return settings[0].secret_api_key @staticmethod def get_cookie_secret(): settings = CraftySettings.select(CraftySettings.cookie_secret).where( CraftySettings.id == 1 ) return settings[0].cookie_secret @staticmethod def set_cookie_secret(key): CraftySettings.update({CraftySettings.cookie_secret: key}).where( CraftySettings.id == 1 ).execute() # ********************************************************************************** # Config Methods # ********************************************************************************** @staticmethod def get_login_image(): settings = CraftySettings.select(CraftySettings.login_photo).where( CraftySettings.id == 1 ) return settings[0].login_photo @staticmethod def set_login_image(photo): CraftySettings.update({CraftySettings.login_photo: photo}).where( CraftySettings.id == 1 ).execute() @staticmethod def get_login_opacity(): settings = CraftySettings.select(CraftySettings.login_opacity).where( CraftySettings.id == 1 ) return settings[0].login_opacity @staticmethod def set_login_opacity(opacity): CraftySettings.update({CraftySettings.login_opacity: opacity}).where( CraftySettings.id == 1 ).execute() @staticmethod def get_master_server_dir(): settings = CraftySettings.select(CraftySettings.master_server_dir).where( CraftySettings.id == 1 ) return settings[0].master_server_dir @staticmethod def set_master_server_dir(server_dir): CraftySettings.update({CraftySettings.master_server_dir: server_dir}).where( CraftySettings.id == 1 ).execute() # ********************************************************************************** # Schedules Methods # ********************************************************************************** @staticmethod def create_scheduled_task( server_id, action, interval, interval_type, start_time, command, name, enabled=True, one_time=False, cron_string="* * * * *", parent=None, delay=0, ): sch_id = Schedules.insert( { Schedules.server_id: server_id, Schedules.action: action, Schedules.enabled: enabled, Schedules.interval: interval, Schedules.interval_type: interval_type, Schedules.start_time: start_time, Schedules.command: command, Schedules.name: name, Schedules.one_time: one_time, Schedules.cron_string: cron_string, Schedules.parent: parent, Schedules.delay: delay, Schedules.next_run: "", } ).execute() return sch_id @staticmethod def delete_scheduled_task(schedule_id): return Schedules.delete().where(Schedules.schedule_id == schedule_id).execute() @staticmethod def update_scheduled_task(schedule_id, updates): Schedules.update(updates).where(Schedules.schedule_id == schedule_id).execute() @staticmethod def delete_scheduled_task_by_server(server_id): Schedules.delete().where(Schedules.server_id == server_id).execute() @staticmethod def get_scheduled_task(schedule_id): return model_to_dict(Schedules.get(Schedules.schedule_id == schedule_id)) @staticmethod def get_scheduled_task_model(schedule_id): return Schedules.select().where(Schedules.schedule_id == schedule_id).get() @staticmethod def get_schedules_by_server(server_id): return Schedules.select().where(Schedules.server_id == server_id).execute() @staticmethod def get_child_schedules_by_server(schedule_id, server_id): return ( Schedules.select() .where(Schedules.server_id == server_id, Schedules.parent == schedule_id) .execute() ) @staticmethod def get_child_schedules(schedule_id): return Schedules.select().where(Schedules.parent == schedule_id) @staticmethod def get_schedules_all(): return Schedules.select().execute() @staticmethod def get_schedules_enabled(): return ( Schedules.select() .where(Schedules.enabled == True) # pylint: disable=singleton-comparison .execute() ) # ********************************************************************************** # Backups Methods # ********************************************************************************** @staticmethod def get_backup_config(server_id): try: row = ( Backups.select().where(Backups.server_id == server_id).join(Servers)[0] ) conf = { "backup_path": row.server_id.backup_path, "excluded_dirs": row.excluded_dirs, "max_backups": row.max_backups, "server_id": row.server_id_id, "compress": row.compress, "shutdown": row.shutdown, "before": row.before, "after": row.after, } except IndexError: conf = { "backup_path": None, "excluded_dirs": None, "max_backups": 0, "server_id": server_id, "compress": False, "shutdown": False, "before": "", "after": "", } return conf @staticmethod def remove_backup_config(server_id): Backups.delete().where(Backups.server_id == server_id).execute() def set_backup_config( self, server_id: int, backup_path: str = None, max_backups: int = None, excluded_dirs: list = None, compress: bool = False, shutdown: bool = False, before: str = "", after: str = "", ): logger.debug(f"Updating server {server_id} backup config with {locals()}") if Backups.select().where(Backups.server_id == server_id).exists(): new_row = False conf = {} else: conf = { "excluded_dirs": None, "max_backups": 0, "server_id": server_id, "compress": False, "shutdown": False, "before": "", "after": "", } new_row = True if max_backups is not None: conf["max_backups"] = max_backups if excluded_dirs is not None: dirs_to_exclude = ",".join(excluded_dirs) conf["excluded_dirs"] = dirs_to_exclude conf["compress"] = compress conf["shutdown"] = shutdown conf["before"] = before conf["after"] = after if not new_row: with self.database.atomic(): if backup_path is not None: server_rows = ( Servers.update(backup_path=backup_path) .where(Servers.server_id == server_id) .execute() ) else: server_rows = 0 backup_rows = ( Backups.update(conf).where(Backups.server_id == server_id).execute() ) logger.debug( f"Updating existing backup record. " f"{server_rows}+{backup_rows} rows affected" ) else: with self.database.atomic(): conf["server_id"] = server_id if backup_path is not None: Servers.update(backup_path=backup_path).where( Servers.server_id == server_id ) Backups.create(**conf) logger.debug("Creating new backup record.") @staticmethod def get_excluded_backup_dirs(server_id: int): excluded_dirs = HelpersManagement.get_backup_config(server_id)["excluded_dirs"] if excluded_dirs is not None and excluded_dirs != "": dir_list = excluded_dirs.split(",") else: dir_list = [] return dir_list def add_excluded_backup_dir(self, server_id: int, dir_to_add: str): dir_list = self.get_excluded_backup_dirs(server_id) if dir_to_add not in dir_list: dir_list.append(dir_to_add) excluded_dirs = ",".join(dir_list) self.set_backup_config(server_id=server_id, excluded_dirs=excluded_dirs) else: logger.debug( f"Not adding {dir_to_add} to excluded directories - " f"already in the excluded directory list for server ID {server_id}" ) def del_excluded_backup_dir(self, server_id: int, dir_to_del: str): dir_list = self.get_excluded_backup_dirs(server_id) if dir_to_del in dir_list: dir_list.remove(dir_to_del) excluded_dirs = ",".join(dir_list) self.set_backup_config(server_id=server_id, excluded_dirs=excluded_dirs) else: logger.debug( 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()