converted to config.py
This commit is contained in:
72
Bot/main.py
72
Bot/main.py
@@ -9,7 +9,6 @@ import traceback
|
|||||||
import datetime as dt
|
import datetime as dt
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from logger import Logger
|
from logger import Logger
|
||||||
from jsonwrapper import AutoSaveDict
|
|
||||||
from typing import (
|
from typing import (
|
||||||
Optional,
|
Optional,
|
||||||
Callable,
|
Callable,
|
||||||
@@ -24,53 +23,38 @@ from bot import (
|
|||||||
Row,
|
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 = Path(utils.BASE_DIR, 'config')
|
||||||
config_dir.mkdir(parents=True, exist_ok=True)
|
config_dir.mkdir(parents=True, exist_ok=True)
|
||||||
config_file = Path(config_dir, 'config.json')
|
config_path = Path(config_dir, 'config.py')
|
||||||
handler = config_app(config_file)
|
# ensure a configuration file exists - if not, write the template and exit so
|
||||||
handler.init()
|
# 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)
|
posts = Posts('deleted_posts', config_dir)
|
||||||
logger = Logger(1)
|
logger = Logger(1)
|
||||||
untracked_flairs = (utils.Flair.SOLVED, utils.Flair.ABANDONED)
|
untracked_flairs = (utils.Flair.SOLVED, utils.Flair.ABANDONED)
|
||||||
posts.init()
|
posts.init()
|
||||||
reddit = praw.Reddit(
|
reddit = praw.Reddit(
|
||||||
client_id=handler['client_id'],
|
client_id=cfg['client_id'],
|
||||||
client_secret=handler['client_secret'],
|
client_secret=cfg['client_secret'],
|
||||||
user_agent=handler['user_agent'],
|
user_agent=cfg['user_agent'],
|
||||||
username=handler['username'],
|
username=cfg['username'],
|
||||||
password=handler['password'],
|
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})"
|
msg = f"Error with '{bot_name}':\n\n{full_error}\n\nPlease report to author ({author})"
|
||||||
send_modmail(
|
send_modmail(
|
||||||
reddit,
|
reddit,
|
||||||
handler['sub_name'],
|
cfg['sub_name'],
|
||||||
f'An error has occured with {utils.BOT_NAME} msg',
|
f'An error has occured with {utils.BOT_NAME} msg',
|
||||||
msg
|
msg
|
||||||
)
|
)
|
||||||
@@ -167,13 +151,13 @@ def main() -> int:
|
|||||||
posts_to_delete: Set[Row] = set()
|
posts_to_delete: Set[Row] = set()
|
||||||
ignore_methods = ['Removed by mod',]
|
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
|
return 0
|
||||||
|
|
||||||
saved_submission_ids = {row.post_id for row in posts.fetch_all()}
|
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
|
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):
|
for submission in reddit.subreddit(sub_name).new(limit=limit):
|
||||||
try:
|
try:
|
||||||
@@ -185,7 +169,7 @@ def main() -> int:
|
|||||||
for stored_post in posts.fetch_all():
|
for stored_post in posts.fetch_all():
|
||||||
try:
|
try:
|
||||||
submission = reddit.submission(id=stored_post.post_id)
|
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()
|
created = utils.string_to_dt(stored_post.record_created).date()
|
||||||
flair = utils.get_flair(submission.link_flair_text)
|
flair = utils.get_flair(submission.link_flair_text)
|
||||||
|
|
||||||
@@ -199,7 +183,7 @@ def main() -> int:
|
|||||||
if method not in ignore_methods:
|
if method not in ignore_methods:
|
||||||
send_modmail(
|
send_modmail(
|
||||||
reddit,
|
reddit,
|
||||||
handler['sub_name'],
|
cfg['sub_name'],
|
||||||
"User's account has been deleted",
|
"User's account has been deleted",
|
||||||
utils.modmail_removal_notification(stored_post, '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)
|
msg = utils.modmail_removal_notification(stored_post, method)
|
||||||
send_modmail(
|
send_modmail(
|
||||||
reddit,
|
reddit,
|
||||||
handler['sub_name'],
|
cfg['sub_name'],
|
||||||
'A post has been deleted',
|
'A post has been deleted',
|
||||||
msg
|
msg
|
||||||
)
|
)
|
||||||
@@ -237,7 +221,7 @@ def main() -> int:
|
|||||||
logger.info(f"Total posts deleted: {len(posts_to_delete)}")
|
logger.info(f"Total posts deleted: {len(posts_to_delete)}")
|
||||||
|
|
||||||
# wait before the next cycle
|
# 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)
|
time.sleep(sleep_minutes * 60)
|
||||||
|
|
||||||
# end of while True
|
# end of while True
|
||||||
|
|||||||
@@ -55,27 +55,37 @@ Ban Template;
|
|||||||
|
|
||||||
|
|
||||||
def parse_cmd_line_args(args: List[str], logger: Logger, config_file: Path, posts: Posts) -> bool:
|
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
|
help_msg = """Command line help prompt
|
||||||
Command: help
|
Command: help
|
||||||
Args: []
|
Args: []
|
||||||
Decription: Prints the help prompt
|
Description: Prints the help prompt
|
||||||
|
|
||||||
Command: reset_config
|
Command: reset_config
|
||||||
Args: []
|
Args: []
|
||||||
Decription: Reset the bot credentials
|
Description: Overwrite the Python configuration file with default values
|
||||||
|
|
||||||
Command: reset_db
|
Command: reset_db
|
||||||
Args: []
|
Args: []
|
||||||
Decription: Reset the database
|
Description: Reset the database
|
||||||
"""
|
"""
|
||||||
if len(args) > 1:
|
if len(args) > 1:
|
||||||
if args[1] == 'help':
|
if args[1] == 'help':
|
||||||
logger.info(help_msg)
|
logger.info(help_msg)
|
||||||
elif args[1] == 'reset_config':
|
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:
|
try:
|
||||||
os.remove(config_file)
|
from config import TEMPLATE
|
||||||
except FileNotFoundError:
|
config_file.write_text(TEMPLATE)
|
||||||
logger.error("No configuration file found")
|
except Exception:
|
||||||
|
logger.error("Unable to reset configuration file")
|
||||||
elif args[1] == 'reset_db':
|
elif args[1] == 'reset_db':
|
||||||
try:
|
try:
|
||||||
os.remove(posts.path)
|
os.remove(posts.path)
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import unittest
|
import unittest
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
from pathlib import Path
|
||||||
from .actions import (
|
from .actions import (
|
||||||
Flair,
|
Flair,
|
||||||
get_flair,
|
get_flair,
|
||||||
string_to_dt,
|
string_to_dt,
|
||||||
submission_is_older,
|
submission_is_older,
|
||||||
|
parse_cmd_line_args,
|
||||||
)
|
)
|
||||||
|
from logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
class DummyPosts:
|
||||||
|
def __init__(self, path):
|
||||||
|
self.path = path
|
||||||
|
|
||||||
|
|
||||||
class TestActions(unittest.TestCase):
|
class TestActions(unittest.TestCase):
|
||||||
@@ -46,3 +54,27 @@ class TestActions(unittest.TestCase):
|
|||||||
post_made = today - dt.timedelta(days=(max_days + 1))
|
post_made = today - dt.timedelta(days=(max_days + 1))
|
||||||
result = submission_is_older(post_made.date(), max_days)
|
result = submission_is_older(post_made.date(), max_days)
|
||||||
self.assertTrue(result)
|
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())
|
||||||
|
|||||||
17
README.md
17
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.
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"client_id": "",
|
|
||||||
"client_secret": "",
|
|
||||||
"user_agent": "",
|
|
||||||
"username": "",
|
|
||||||
"password": "",
|
|
||||||
"sub_name": "",
|
|
||||||
"max_days": "180",
|
|
||||||
"max_posts": "180",
|
|
||||||
"sleep_minutes": "5"
|
|
||||||
}
|
|
||||||
38
config/config.py
Normal file
38
config/config.py
Normal file
@@ -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}
|
||||||
|
"""
|
||||||
Reference in New Issue
Block a user