From 3673a10669f7ee5334dc8a536423580ae0a551c6 Mon Sep 17 00:00:00 2001 From: Elnath Date: Tue, 8 Jun 2021 00:52:05 +0200 Subject: [PATCH] Added GameFiles classes skeletons --- GameFiles/Game.py | 22 ++++++++ GameFiles/GamesFile.py | 121 +++++++++++++++++++++++++++++++++++++++++ GameFiles/__init__.py | 2 + SecretBot.py | 26 ++++++++- 4 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 GameFiles/Game.py create mode 100644 GameFiles/GamesFile.py create mode 100644 GameFiles/__init__.py diff --git a/GameFiles/Game.py b/GameFiles/Game.py new file mode 100644 index 0000000..1c374c4 --- /dev/null +++ b/GameFiles/Game.py @@ -0,0 +1,22 @@ +from typing import Dict, Callable + + +class Game: + """ + Game state for one guild + """ + def __init__(self, config_dict: Dict, save_function: Callable): + self.config = config_dict + self.save_function = save_function + + @staticmethod + def new_dict(): + return { + "game_started": False, + } + + def is_started(self): + return self.config["game_started"] + + async def start(self): + raise NotImplementedError("Start game") diff --git a/GameFiles/GamesFile.py b/GameFiles/GamesFile.py new file mode 100644 index 0000000..3ad1bdc --- /dev/null +++ b/GameFiles/GamesFile.py @@ -0,0 +1,121 @@ +import atexit +import json +import logging +from pathlib import Path +from typing import Union, Dict, Callable, Tuple +import discord +from .Game import Game + +logger = logging.getLogger(__name__) + +# Functions used to convert between configuration versions, in a dictionary of: old_version -> converter +# Each converter takes the old configuration and returns the converted configuration and the new version +config_version_converters: Dict[Union[None, str], Callable[[Dict], Tuple[Dict, str]]] = { +} + + +class GamesFile: + """ + Wrapper around the configuration file for all games of all guilds + """ + version = "1.0" + + @staticmethod + def empty_config() -> Dict: + """ + :return: An empty configuration, used to initialise the configuration file + """ + return { + "__version__": GamesFile.version, + } + + def __init__(self, games_file_path: Union[str, Path]): + self.file_obj = None # File descriptor to the on-disk config file + self.config = None # In-memory configuration + + # Loading configuration from disk + logger.debug("Loading games file") + games_file_path = Path(games_file_path).absolute() + if games_file_path.exists(): + if not games_file_path.is_file(): + raise ValueError(f"Games file {games_file_path} exists but is not a regular file") + + self.file_obj = games_file_path.open("r+") + + if games_file_path.stat().st_size == 0: # Config file is empty + logger.warning(f"games file {games_file_path} is empty, initialising an empty configuration") + self.config = self.empty_config() + self.save_to_file() # Initialise the file + else: # File is not empty + has_been_converted = self.reload_from_disk() + if has_been_converted: + self.save_to_file() + else: # File does not exist + self.file_obj = games_file_path.open("w+") + self.config = self.empty_config() + self.save_to_file() # Initialise the file + + # Verifying that attributes have been initialised properly + assert self.file_obj is not None and self.config is not None + logger.debug("Games file successfully initialised") + + atexit.register(self.save_to_file) + + def reload_from_disk(self) -> bool: + """ + Reload the configuration from disk + + :return: Whether the configuration had to be converted to a more recent version because the one on disk was an earlier version + :except json.JSONDecodeError: if file is not valid json + :except ValueError: if the version of the configuration in the file is not known or can not be converted to a more recent one + """ + logger.debug("Loading games file from disk") + self.file_obj.seek(0) # Moving to beginning of file + try: + self.config = json.load(self.file_obj) + except json.JSONDecodeError as e: + logger.critical(f"JSON Error when parsing games file: {e}") + raise e + # Checking configuration version and converting if needed + config_version = self.config["__version__"] + has_been_converted = False + if config_version != self.version: + logger.info(f"Games file is an older version, converting (file version: {config_version}, current: {self.version})...") + + # Performing the conversion + while config_version != self.version: + if config_version in config_version_converters: + logger.debug(f"Converting from {config_version}") + self.config, config_version = config_version_converters[config_version](self.config) + logger.debug(f"Converted to {config_version}") + else: + logger.critical(f"Impossible to find converter to convert from {config_version}") + raise ValueError(f"Configuration loading: impossible to convert to current version") + has_been_converted = True + + assert self.config["__version__"] == self.version + + logger.debug("Loaded games file from disk") + return has_been_converted + + def save_to_file(self, indent = 2) -> None: + """ + Save the configuration to disk + :param indent: Indentation to use for pretty-printing json + """ + logger.debug("Writing games file to disk") + self.file_obj.seek(0) + self.file_obj.truncate() + json.dump(self.config, self.file_obj, indent = indent) + self.file_obj.flush() + logger.debug("Written games file to disk") + + def __getitem__(self, guild: discord.Guild) -> Game: + """ + Get the game information for one guild + """ + guild_id_str = str(guild.id) + if guild_id_str not in self.config: + self.config[guild_id_str] = Game.new_dict() + self.save_to_file() + return Game(self.config[guild_id_str], self.save_to_file) diff --git a/GameFiles/__init__.py b/GameFiles/__init__.py new file mode 100644 index 0000000..f633827 --- /dev/null +++ b/GameFiles/__init__.py @@ -0,0 +1,2 @@ +from .Game import Game +from .GamesFile import GamesFile diff --git a/SecretBot.py b/SecretBot.py index 308e3d1..653f010 100755 --- a/SecretBot.py +++ b/SecretBot.py @@ -8,6 +8,8 @@ import discord import discord.utils from discord.ext import commands +from GameFiles import GamesFile +from GameFiles import Game import utils logger = logging.getLogger("SecretBot") @@ -15,7 +17,7 @@ logger = logging.getLogger("SecretBot") class SecretBot(commands.Cog): - def __init__(self): + def __init__(self, games_file: GamesFile): super().__init__() self.bot = commands.Bot( "!", @@ -24,6 +26,7 @@ class SecretBot(commands.Cog): allowed_mentions = discord.AllowedMentions.none(), # By default we do not allow mentions, as we will white-list them on each required message ) self.bot.add_cog(self) + self.games_file = games_file def run(self, token): self.bot.run(token) @@ -74,10 +77,28 @@ class SecretBot(commands.Cog): raise utils.CheckFailDoNotNotify return True + async def check_is_administrator_or_owner(self, ctx: commands.Context): + """ + Verify if the user has administrator rights or if it is the bot's owner. Notifies the user and raises utils.CheckFailDoNotNotify if not. + """ + if not (ctx.author.guild_permissions.administrator or await self.bot.is_owner(ctx.author)): + await ctx.reply(f":dragon_face: You have no power here!") + raise utils.CheckFailDoNotNotify + @commands.command(help = "See if I'm alive") async def ping(self, ctx: commands.Context): await ctx.reply(":ping_pong:", mention_author = True) + @commands.command("StartGame") + async def start_game(self, ctx: commands.Context): + game = self.games_file[ctx.guild] + if game.is_started(): + await ctx.reply(":x: a game is already running") + else: + await game.start() + + + if __name__ == '__main__': argparser = argparse.ArgumentParser(description = "Secret Hitler helper bot", formatter_class = argparse.ArgumentDefaultsHelpFormatter) @@ -108,5 +129,6 @@ if __name__ == '__main__': with token_file_path.open("r") as token_file: token = token_file.readline().strip() - bot = SecretBot() + games_file = GamesFile(ARGS.games_file) + bot = SecretBot(games_file) bot.run(token)