from abc import ABC import asyncio from typing import Any import re import os import random import time import logging import discord from mcstatus import JavaServer from libs import Configs, Presence SHELL_SCRIPT_PATH = 'scripts' class Client(discord.Bot, ABC): """< discord.Bot > The bot class """ def __init__(self): self.__config = Configs() self.logger = self.__setup_logger(self.__config.log_level) super(Client, self).__init__( # default presence activity=discord.Game('Beep Boop! Loading...'), status=discord.Status.idle, # debug debug_guilds=self.config.debug_guilds, ) self.is_server_starting = False self.last_start = time.time() self.mc_server = JavaServer( self.__config.server_address, self.__config.server_port ) self.presence_manager = Presence(self) # extensions from extensions import Admin, StartStop, Status for module in [Admin, StartStop, Status]: self.add_cog(module(self)) def run(self, *args: Any, **kwargs: Any) -> None: """< function > Starts the bot and automatically gets the configured token. """ super(Client, self).run(self.__config.auth_token) @property def config(self) -> Configs: """< property > The default config should not be changeable even on runtime. This ensures its read-only. :return: Butter default config """ return self.__config @property def color(self) -> int: """< property > Depending on the setting set in ButterConfig either the bot's default color gets returned or a random always bright color. :return: hex-Color """ colors: list[int] = [0, 255, random.randint(0, 255)] random.shuffle(colors) return int('0x%02x%02x%02x' % tuple(colors), 16) def __setup_logger(self, level: int = logging.INFO) -> logging.Logger: """< function > Basic logging abilities """ path_list = re.split('/| \\\\', self.__config.log_path) for i, folder in enumerate(path_list): # filtering empty strings (e.g. for using source folder) if not folder: continue try: # creates folders os.mkdir(os.path.join(*path_list[:i + 1])) except FileExistsError: continue logger = logging.getLogger('discord') logger.setLevel(level) formatter = logging.Formatter(fmt='[%(asctime)s] - %(levelname)s: %(name)s: %(message)s') file_handler = logging.FileHandler(filename=f'{os.path.join(*path_list, "") or ""}discord.log', encoding='utf-8', mode='w') file_handler.setFormatter(formatter) logger.addHandler(file_handler) console_handler = logging.StreamHandler() console_handler.setFormatter(formatter) logger.addHandler(console_handler) return logger async def on_ready(self) -> None: """< coroutine > Logs when the bot is online. """ self.logger.info('Bot successfully started') async def execute_shell(self, file_name: str, retry=True) -> int: """< coroutine > Runs a bash script in executer and returns the process code. Logs errors if process returncode isn't 0. :param file_name: file name :param retry: recursion stopper in case granting permissions fails. :return: process returncode """ async def __grant_permission() -> int: self.logger.info(f'Granting permissions to {file_name}...') process_chmod = await asyncio.create_subprocess_shell( cmd=f'chmod +x {os.path.join(os.getcwd(), SHELL_SCRIPT_PATH, file_name)}' ) await process_chmod.communicate() return process_chmod.returncode process = await asyncio.create_subprocess_exec( program=os.path.join(os.getcwd(), SHELL_SCRIPT_PATH, file_name), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() self.logger.info(f'Executed script {file_name} with exit code {process.returncode}') if process.returncode == 0: self.logger.info(f'stdout:\n{stdout.decode()}') # bash returncode 126: permission error elif process.returncode == 126 and retry: # retrying once self.logger.warning(f'Missing permissions for {file_name}') return await self.execute_shell(file_name, retry=False) else: self.logger.error(f'stderr:\n{stderr.decode()}') return process.returncode