diff --git a/Bot/main.py b/Bot/main.py index d560d11..8183dd5 100644 --- a/Bot/main.py +++ b/Bot/main.py @@ -17,14 +17,39 @@ from typing import ( Set, Any, ) + +import importlib +import importlib.util from bot import ( Datatype, Posts, Row, ) -# configuration is now a Python module instead of a JSON file -from config import config as cfg, TEMPLATE +# configuration will be loaded later once we have ensured the package is +# in place. this avoids import errors when the ``config`` directory is +# provided via a volume mount that is initially empty. + +cfg = None # type: ignore +TEMPLATE = None # type: ignore + +# fallback template used when the config module cannot yet be imported +DEFAULT_TEMPLATE = """# configuration for DeletedPosts bot +# edit the values of the dictionary below and restart the bot + +config = { + "client_id": "", + "client_secret": "", + "user_agent": "", + "username": "", + "password": "", + "sub_name": "", + # numeric settings are stored as integers here rather than strings + "max_days": 180, + "max_posts": 180, + "sleep_minutes": 5, +} +""" # the old JSON-based interactive configuration helper has been removed. the @@ -36,15 +61,35 @@ from config import config as cfg, TEMPLATE config_dir = Path(utils.BASE_DIR, 'config') config_dir.mkdir(parents=True, exist_ok=True) -config_path = Path(config_dir, 'config.py') -# ensure a configuration file exists - if not, write the template and exit so -# the user can edit it. +# make config a package so it can be imported later; if __init__.py is missing +# (for example a freshly mounted empty volume), create a minimal one. +init_file = config_dir / "__init__.py" +if not init_file.exists(): + init_file.write_text("# config package\n") + +config_path = config_dir / 'config.py' +# if the config file itself is missing, write the template and exit. we need to +# import the template name from the module, but since the file was just created +# we'll import once below after ensuring the package exists. if not config_path.exists(): - config_path.write_text(TEMPLATE) + # write fallback template if we haven't yet imported the module + fallback = TEMPLATE or DEFAULT_TEMPLATE + config_path.write_text(fallback) print(f"Created new configuration template at {config_path!r}.\n" "Please populate the values and restart the bot.") sys.exit(0) +# now that the package structure exists and config file is present, import it +import importlib +spec = importlib.util.spec_from_file_location("config", str(config_path)) +config_mod = importlib.util.module_from_spec(spec) +# insert into sys.modules so conventional imports work +sys.modules["config"] = config_mod +if spec.loader: + spec.loader.exec_module(config_mod) # type: ignore +cfg = config_mod.config +TEMPLATE = config_mod.TEMPLATE + posts = Posts('deleted_posts', config_dir) logger = Logger(1) untracked_flairs = (utils.Flair.SOLVED, utils.Flair.ABANDONED) diff --git a/Bot/utils/actions.py b/Bot/utils/actions.py index c477316..d3e557f 100644 --- a/Bot/utils/actions.py +++ b/Bot/utils/actions.py @@ -54,13 +54,34 @@ Ban Template; You can read [our rules](https://reddit.com/r/MinecraftHelp/wiki/rules) to see if you're eligible to appeal this ban.""" +# default template used when resetting the configuration. this mirrors +# the template defined in ``config/config.py``; keeping a copy here avoids +# depending on the module itself being importable (which can fail if the +# directory is newly mounted or otherwise not part of sys.path). +DEFAULT_TEMPLATE = """# configuration for DeletedPosts bot +# edit the values of the dictionary below and restart the bot + +config = { + "client_id": "", + "client_secret": "", + "user_agent": "", + "username": "", + "password": "", + "sub_name": "", + # numeric settings are stored as integers here rather than strings + "max_days": 180, + "max_posts": 180, + "sleep_minutes": 5, +} +""" + def parse_cmd_line_args(args: List[str], logger: Logger, config_file: Path, posts: Posts) -> bool: """Parse a very small set of operations from ``sys.argv``. ``config_file`` now refers to the Python configuration module path (typically ``.../config/config.py``). ``reset_config`` will overwrite the - file with the default template, which is imported from the configuration - module itself so that it does not need to be duplicated here. + file with a default template; the template is defined above to avoid + importing the configuration module directly. """ help_msg = """Command line help prompt Command: help @@ -79,11 +100,9 @@ def parse_cmd_line_args(args: List[str], logger: Logger, config_file: Path, post if args[1] == 'help': logger.info(help_msg) elif args[1] == 'reset_config': - # write the template text back to the configuration file. import - # TEMPLATE lazily in case the module has not yet been created. + # write the default template text back to the configuration file. try: - from config import TEMPLATE - config_file.write_text(TEMPLATE) + config_file.write_text(DEFAULT_TEMPLATE) except Exception: logger.error("Unable to reset configuration file") elif args[1] == 'reset_db': diff --git a/Dockerfile b/Dockerfile index fa81d91..ba932d1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,9 @@ WORKDIR /app # Copy application files COPY Bot ./Bot +COPY config ./config +# prepare configuration directory (volume may later overwrite it) RUN mkdir -p /app/config # Install dependencies diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..539502b --- /dev/null +++ b/config/__init__.py @@ -0,0 +1,4 @@ +# make ``config`` a package so it can be imported from anywhere in the +# project. re-export the important names for convenience. + +from .config import config, TEMPLATE