From e5d673a8ddd44534ca550677843d77897d1e8c5f Mon Sep 17 00:00:00 2001 From: Slfhstd Date: Mon, 23 Feb 2026 23:31:15 +0000 Subject: [PATCH] converted to config.py --- Bot/main.py | 72 +++++++++++++++++--------------------------- Bot/utils/actions.py | 22 ++++++++++---- Bot/utils/tests.py | 32 ++++++++++++++++++++ README.md | 17 +++++++++++ config/config.json | 11 ------- config/config.py | 38 +++++++++++++++++++++++ 6 files changed, 131 insertions(+), 61 deletions(-) delete mode 100644 config/config.json create mode 100644 config/config.py diff --git a/Bot/main.py b/Bot/main.py index 28402e8..d560d11 100644 --- a/Bot/main.py +++ b/Bot/main.py @@ -9,7 +9,6 @@ import traceback import datetime as dt from pathlib import Path from logger import Logger -from jsonwrapper import AutoSaveDict from typing import ( Optional, Callable, @@ -24,53 +23,38 @@ from bot import ( Row, ) +# configuration is now a Python module instead of a JSON file +from config import config as cfg, TEMPLATE -def config_app(path: Path) -> AutoSaveDict: - config = { - 'client_id': '', - 'client_secret': '', - 'user_agent': '', - 'username': '', - 'password': '', - 'sub_name': '', - 'max_days': '', - 'max_posts': '', - 'sleep_minutes': '', - } - configuration: List[List[str]] = [] +# the old JSON-based interactive configuration helper has been removed. the +# values are now stored in ``config/config.py``. ``cfg`` above is a simple +# dict containing the settings; callers should treat numeric entries as +# integers. - if not os.path.exists(path): - for key, _ in config.items(): - config_name = ' '.join(key.split('_')).title() - user_inp = input(f"{config_name}: ") - configuration.append([key, user_inp]) - - for config_name, value in configuration: - config[config_name] = value - - config_handler = AutoSaveDict( - path, - **config - ) - return config_handler config_dir = Path(utils.BASE_DIR, 'config') config_dir.mkdir(parents=True, exist_ok=True) -config_file = Path(config_dir, 'config.json') -handler = config_app(config_file) -handler.init() +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. +if not config_path.exists(): + config_path.write_text(TEMPLATE) + print(f"Created new configuration template at {config_path!r}.\n" + "Please populate the values and restart the bot.") + sys.exit(0) + posts = Posts('deleted_posts', config_dir) logger = Logger(1) untracked_flairs = (utils.Flair.SOLVED, utils.Flair.ABANDONED) posts.init() reddit = praw.Reddit( - client_id=handler['client_id'], - client_secret=handler['client_secret'], - user_agent=handler['user_agent'], - username=handler['username'], - password=handler['password'], + client_id=cfg['client_id'], + client_secret=cfg['client_secret'], + user_agent=cfg['user_agent'], + username=cfg['username'], + password=cfg['password'], ) @@ -123,7 +107,7 @@ def notify_if_error(func: Callable[..., int]) -> Callable[..., int]: msg = f"Error with '{bot_name}':\n\n{full_error}\n\nPlease report to author ({author})" send_modmail( reddit, - handler['sub_name'], + cfg['sub_name'], f'An error has occured with {utils.BOT_NAME} msg', msg ) @@ -167,13 +151,13 @@ def main() -> int: posts_to_delete: Set[Row] = set() ignore_methods = ['Removed by mod',] - if utils.parse_cmd_line_args(sys.argv, logger, config_file, posts): + if utils.parse_cmd_line_args(sys.argv, logger, config_path, posts): return 0 saved_submission_ids = {row.post_id for row in posts.fetch_all()} - max_posts = handler['max_posts'] + max_posts = cfg.get('max_posts') limit = int(max_posts) if max_posts else None - sub_name = handler['sub_name'] + sub_name = cfg['sub_name'] for submission in reddit.subreddit(sub_name).new(limit=limit): try: @@ -185,7 +169,7 @@ def main() -> int: for stored_post in posts.fetch_all(): try: submission = reddit.submission(id=stored_post.post_id) - max_days = int(handler['max_days']) + max_days = int(cfg['max_days']) created = utils.string_to_dt(stored_post.record_created).date() flair = utils.get_flair(submission.link_flair_text) @@ -199,7 +183,7 @@ def main() -> int: if method not in ignore_methods: send_modmail( reddit, - handler['sub_name'], + cfg['sub_name'], "User's account has been deleted", utils.modmail_removal_notification(stored_post, 'Account has been deleted') ) @@ -213,7 +197,7 @@ def main() -> int: msg = utils.modmail_removal_notification(stored_post, method) send_modmail( reddit, - handler['sub_name'], + cfg['sub_name'], 'A post has been deleted', msg ) @@ -237,7 +221,7 @@ def main() -> int: logger.info(f"Total posts deleted: {len(posts_to_delete)}") # wait before the next cycle - sleep_minutes = int(handler['sleep_minutes']) if handler['sleep_minutes'] else 5 + sleep_minutes = int(cfg.get('sleep_minutes', 5)) time.sleep(sleep_minutes * 60) # end of while True diff --git a/Bot/utils/actions.py b/Bot/utils/actions.py index ab1ce5e..c477316 100644 --- a/Bot/utils/actions.py +++ b/Bot/utils/actions.py @@ -55,27 +55,37 @@ Ban Template; 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. + """ help_msg = """Command line help prompt Command: help Args: [] - Decription: Prints the help prompt + Description: Prints the help prompt Command: reset_config Args: [] - Decription: Reset the bot credentials + Description: Overwrite the Python configuration file with default values Command: reset_db Args: [] - Decription: Reset the database + Description: Reset the database """ if len(args) > 1: 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. try: - os.remove(config_file) - except FileNotFoundError: - logger.error("No configuration file found") + from config import TEMPLATE + config_file.write_text(TEMPLATE) + except Exception: + logger.error("Unable to reset configuration file") elif args[1] == 'reset_db': try: os.remove(posts.path) diff --git a/Bot/utils/tests.py b/Bot/utils/tests.py index 14213c0..95ef39b 100644 --- a/Bot/utils/tests.py +++ b/Bot/utils/tests.py @@ -1,11 +1,19 @@ import unittest import datetime as dt +from pathlib import Path from .actions import ( Flair, get_flair, string_to_dt, submission_is_older, + parse_cmd_line_args, ) +from logger import Logger + + +class DummyPosts: + def __init__(self, path): + self.path = path class TestActions(unittest.TestCase): @@ -46,3 +54,27 @@ class TestActions(unittest.TestCase): post_made = today - dt.timedelta(days=(max_days + 1)) result = submission_is_older(post_made.date(), max_days) self.assertTrue(result) + + def test_parse_cmd_line_args_reset_config_and_db(self) -> None: + tmp = Path(__file__).parent / "tmp_test" + tmp.mkdir(exist_ok=True) + cfg_file = tmp / "config.py" + # ensure file exists with junk content + cfg_file.write_text("not important") + db_file = tmp / "db.sqlite" + db_file.write_text("x") + posts = DummyPosts(db_file) + logger = Logger(1) + + # reset_config should rewrite the config file + result = parse_cmd_line_args(["prog", "reset_config"], logger, cfg_file, posts) + self.assertTrue(result) + self.assertTrue(cfg_file.exists()) + content = cfg_file.read_text() + self.assertIn("client_id", content) + + # reset_db should remove the database file + db_file.write_text("x") + result = parse_cmd_line_args(["prog", "reset_db"], logger, cfg_file, posts) + self.assertTrue(result) + self.assertFalse(db_file.exists()) diff --git a/README.md b/README.md index e69de29..5bf80ca 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,17 @@ +# DeletedPosts Bot + +Configuration used to live in a JSON file, but it has been migrated to a +Python module under ``config/config.py``. + +When you first run the application a template file will be created for you in +the ``config`` directory. Edit the values in the ``config`` dictionary and +restart the bot. + +You can also reset the configuration back to the default template by invoking +``reset_config`` on the command line: + +``` +python -m Bot.main reset_config +``` + +Other command line actions (``help`` and ``reset_db``) remain unchanged. diff --git a/config/config.json b/config/config.json deleted file mode 100644 index 997cb63..0000000 --- a/config/config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "client_id": "", - "client_secret": "", - "user_agent": "", - "username": "", - "password": "", - "sub_name": "", - "max_days": "180", - "max_posts": "180", - "sleep_minutes": "5" -} \ No newline at end of file diff --git a/config/config.py b/config/config.py new file mode 100644 index 0000000..43a3215 --- /dev/null +++ b/config/config.py @@ -0,0 +1,38 @@ +""" +Configuration for the DeletedPosts bot. + +This module exposes a single dictionary called ``config`` which holds all +of the parameters required to connect to Reddit and control the behaviour of +the bot. When you first run the program the file will be created for you +with empty values; you should edit it before starting the bot. You can also +reset the file to defaults by running the application with the +``reset_config`` command-line argument. + +Example usage:: + + from config import config + print(config['client_id']) + +""" + +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, +} + +# same data as a text template. both ``main.py`` and ``utils.actions`` import +# this so that the file can be created or reset without duplicating the +# literal configuration body. +TEMPLATE = f"""# configuration for DeletedPosts bot +# edit the values of the dictionary below and restart the bot + +config = {config!r} +"""