From bdbd343f0c69a257b0324e3c10d2b84ec34e470b Mon Sep 17 00:00:00 2001 From: Collin Rapp Date: Sun, 10 May 2020 15:25:20 -0700 Subject: [PATCH] Added config options & basic single-file executable --- CHANGELOG.md | 13 +++++ PointsBot.py | 3 +- PointsBot.spec | 14 ++--- README.md | 118 +++++++++++++++++++++++++++++------------- freeze.cmd | 7 +-- pointsbot/__main__.py | 4 -- pointsbot/bot.py | 38 ++++++-------- pointsbot/config.py | 85 ++++++++++++++++++++---------- pointsbot/reply.py | 27 +++++++--- 9 files changed, 197 insertions(+), 112 deletions(-) create mode 100644 CHANGELOG.md delete mode 100644 pointsbot/__main__.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f1b2c89 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +## 2020/05/10 + +Features: +1. Adding basic initial logging to a file + +Fixes: N/A + +Miscellaneous: +1. Moved feedback & scoreboard links for bot reply into configuration +2. Changed program entry point to `PointsBot.py` +3. Added ability to freeze the app as a simple executable diff --git a/PointsBot.py b/PointsBot.py index ff559cd..f4ccf9e 100644 --- a/PointsBot.py +++ b/PointsBot.py @@ -1,3 +1,4 @@ -"""Dummy file used for either runnning or freezing the bot.""" +"""Entry point used for either runnning or freezing the bot.""" import pointsbot + pointsbot.run() diff --git a/PointsBot.spec b/PointsBot.spec index b8edd57..cb9c1ba 100644 --- a/PointsBot.spec +++ b/PointsBot.spec @@ -19,19 +19,15 @@ pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE(pyz, a.scripts, + a.binaries, + a.zipfiles, + a.datas, [], - exclude_binaries=True, name='PointsBot', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, + upx_exclude=[], + runtime_tmpdir=None, console=True ) -coll = COLLECT(exe, - a.binaries, - a.zipfiles, - a.datas, - strip=False, - upx=True, - upx_exclude=[], - name='PointsBot') diff --git a/README.md b/README.md index ec5ac35..59e076e 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ * [Description](#description) * [Installation](#installation) +* [Prerequisites & Configuration](#prerequisites--configuration) * [Usage](#usage) -* [Setup](#setup) * [Terms of Use for a bot for Reddit](#terms-of-use-for-a-bot-for-reddit) * [License](#license) @@ -41,10 +41,21 @@ awarded for each submission. ## Installation -Requirements: +### Basic Installation +These are the instructions for simply using the bot without needing to edit the +code. These instructions are best suited for users with less technical +experience. + +Go the the [releases page](https://github.com/cur33/PointsBot/releases) for this +project, then download and unzip the latest release. Make sure you pick the +release best suited for your machine & operating system. + +### Advanced Installation + +Requirements: * [Python 3](https://www.python.org/downloads/) (specifically version 3.7 or greater) - * pip (should be installed automatically with Python) +* pip (should be installed automatically with Python) * [pipenv](https://pipenv.readthedocs.io/en/latest/) * After installying Python & pip, install by running `pip install pipenv` * For other installation options, @@ -60,39 +71,7 @@ To uninstall (i.e. delete the project's virtual environment and the installed python packages), navigate to the project root directory and instead run `pipenv --rm`. -## Usage - -Once you have followed the instructions in the [Setup](#setup) section below, -the simplest way to run the bot is to navigate to the project root directory and -run: - -```bash -pipenv run python -m pointsbot -``` - -## Setup - -### Configuration file - -The bot stores any necessary data, including configuration, under the -`.pointsbot` directory in your home directory. - -If this is your first time running the bot, you will need to copy -`pointsbot.sample.toml` to a new file called `.pointsbot\pointsbot.toml`, -located in the `.pointsbot` directory mentioned above. Any instances of -the word "REDACTED" should be replaced with the appropriate values; other values -should work as-is, but can be changed as needed. - -This is because the config file can contain sensitive information, and -maintaining only sample versions of these files helps developers to avoid -accidentally uploading that sensitive information to a public (or even private) -code repository. - -More information on the specific config options can be found in the comments in -the sample config file. - -You shouldn't have to worry about it, but if you need it, information on the -TOML syntax used for the file can be found on [Github](https://github.com/toml-lang/toml). +## Prerequisites & Configuration ### Bot account @@ -130,6 +109,73 @@ Some of the bot's behaviors, e.g. altering redditor flairs, require moderator permissions. It should require just the "Flair" and "Posts" permissions and perhaps the "Access" permission, so you don't need to grant it full permissions. +### Configuration file + +The bot will store its configuration file in a `.pointsbot` directory under the +home directory for your user on your machine. By default, this directory is also +where it will store its database and log files, but you can alter this behavior +by specifying other locations for those in the configuration process. + +If this is your first time running the bot, then it will fail to detect a +configuration file and will prompt you for the necessary fields. This includes +the credentials for the Reddit account and Reddit app which you created for the +bot in the [Prerequisites & Configuration](#prerequisites--configuration) step +above. The bot will create the configuration file and save this information for +future use. + +At the moment, there is not a good way to edit the existing configuration unless +you want to edit the configuration file yourself (see next paragraph). This is +the recommended process for editing the configuration until this feature is +added: +1. Go to your home directory and open the `.pointsbot` directory. +2. Either rename the `pointsbot.toml` file to something else, or move it to a + different directory. +3. Open that file in a text editor (if you haven't installed one, you can use + the default text editor for your operating system, like Notepad on Windows). +4. Run the bot and walk through the configuration process again, copying and + pasting any unchanged values from the original configuration file. +5. Once you are finished, you can either keep the old configuration file for a + while just in case, or you can delete it. + +If you'd prefer, you can instead create and edit the configuration file +yourself. You will need to copy `pointsbot.sample.toml` to a new file called +`pointsbot.toml` located in the `.pointsbot` directory mentioned above. Any +instances of the word "REDACTED" should be replaced with the appropriate values; +other values should work as-is, but can be changed as needed. + +This is because the config file can contain sensitive information, and +maintaining only sample versions of these files in this repository helps +developers to avoid accidentally uploading that sensitive information to a +public (or even private) code repository. + +More information on the specific config options can be found in the comments in +the sample config file. + +You shouldn't have to worry about it, but if you need it, information on the +TOML syntax used for the file can be found on +[TOML's Github page](https://github.com/toml-lang/toml). + +## Usage + +### Basic Usage + +Follow these instructions if you downloaded the bot from the releases page in +the [Installation](#installation) step above. + +In the unzipped folder, double-click on the `PointsBot.exe` file. It will open a +command prompt that will ask you for any additional information it may require. +You will *not* need any knowledge of the command prompt for your operating +system to interact with the bot. + +### Advanced Usage + +The simplest way to run the bot is to navigate to the project root directory and +run: + +```bash +pipenv run python PointsBot.py +``` + ## Terms of use for a bot for Reddit Since this is an open-source, unmonetized program, it should be considered diff --git a/freeze.cmd b/freeze.cmd index f62f5b9..d7ed219 100644 --- a/freeze.cmd +++ b/freeze.cmd @@ -1,13 +1,14 @@ @echo off +REM The below is an alternative to using a custom hook for praw REM FOR /F "tokens=* USEBACKQ" %%F IN (`pipenv --venv`) DO ( REM SET pipenvdir=%%F REM ) - REM --add-data "%pipenvdir%\Lib\site-packages\praw\praw.ini;site-packages\praw\praw.ini" ^ -REM --noconfirm ^ +REM using the --noconfirm option sometimes causes issues when rebuilding +REM (ie when it tries to delete the previous dist directory) pyinstaller ^ - --onedir ^ + --onefile ^ --additional-hooks-dir .\pyinstaller-hooks\ ^ PointsBot.py diff --git a/pointsbot/__main__.py b/pointsbot/__main__.py deleted file mode 100644 index 7bf51e4..0000000 --- a/pointsbot/__main__.py +++ /dev/null @@ -1,4 +0,0 @@ -from . import bot - -if __name__ == '__main__': - bot.run() diff --git a/pointsbot/bot.py b/pointsbot/bot.py index f3f4a0b..187ed68 100644 --- a/pointsbot/bot.py +++ b/pointsbot/bot.py @@ -13,9 +13,9 @@ from . import config, database, level, reply USER_AGENT = 'PointsBot (by u/GlipGlorp7)' # TODO put this in config -LOG_FILEPATH = os.path.abspath(os.path.join(os.path.expanduser('~'), - '.pointsbot', - 'pointsbot.log.txt')) +# LOG_FILEPATH = os.path.abspath(os.path.join(os.path.expanduser('~'), + # '.pointsbot', + # 'pointsbot.log')) # The pattern that determines whether a post is marked as solved # Could also use re.IGNORECASE flag instead @@ -26,14 +26,13 @@ MOD_SOLVED_PAT = re.compile('/[Ss]olved') def run(): - logging.basicConfig(filename=LOG_FILEPATH, - level=logging.DEBUG, - format='%(asctime)s %(module)s:%(levelname)s: %(message)s', - datefmt='%Y-%m-%d %H:%M:%S') - print_welcome_message() cfg = config.load() + logging.basicConfig(filename=cfg.log_path, + level=logging.DEBUG, + format='%(asctime)s %(levelname)s:%(module)s: %(message)s', + datefmt='%Y-%m-%d %H:%M:%S') levels = cfg.levels db = database.Database(cfg.database_path) @@ -78,46 +77,43 @@ def monitor_comments(subreddit, db, levels): logging.info('Received comment') # TODO more debug info about comment, eg author logging.debug('Comment text: "%s"', comm.body) - # print_level(0, '\nFound comment') - # print_level(1, f'Comment text: "{comm.body}"') if not marks_as_solved(comm): logging.info('Comment does not mark issue as solved') - # print_level(1, 'Not a "![Ss]olved" comment') continue logging.info('Comment marks issues as solved') if is_mod_comment(comm): logging.info('Comment was submitted by mod') - # print_level(1, 'Mod comment') elif not is_first_solution(comm): # Skip this "!solved" comment logging.info('Comment is NOT the first to mark the issue as solved') - # print_level(1, 'Not the first solution') continue logging.info('Comment is the first to mark the issue as solved') - # print_level(1, 'This is the first solution found') log_solution_info(comm) solver = find_solver(comm) db.add_point(solver) logging.info('Added point for user "%s"', solver.name) - # print_level(1, f'Added point for {solver.name}') points = db.get_points(solver) logging.info('Total points for user "%s": %d', solver.name, points) - # print_level(1, f'Total points for {solver.name}: {points}') level_info = level.user_level_info(points, levels) # Reply to the comment marking the submission as solved - reply_body = reply.make(solver, points, level_info) + reply_body = reply.make( + solver, + points, + level_info, + feedback_url=cfg.feedback_url, + scoreboard_url=cfg.scoreboard_url + ) try: comm.reply(reply_body) logging.info('Replied to the comment') logging.debug('Reply body: %s', reply_body) - # print_level(1, f'Replied to comment with: "{reply_body}"') except praw.exceptions.APIException as e: logging.error('Unable to reply to comment: %s', e) db.remove_point(solver) @@ -130,7 +126,6 @@ def monitor_comments(subreddit, db, levels): if lvl and lvl.points == points: logging.info('User reached level: %s', lvl.name) if not subreddit.moderator(redditor=solver): - # print_level(2, 'Setting flair') logging.info('User is not mod; setting flair') logging.info('Flair text: %s', lvl.name) logging.info('Flair template ID: %s', lvl.flair_template_id) @@ -208,10 +203,7 @@ def print_welcome_message(): 'sent to the developer by reporting an issue on the Github page.') print('\nFuture updates will hopefully resolve these issues, but for the ' "moment, this is what we've got to work with! :)\n") - - -def print_level(num_levels, string): - print('\t' * num_levels + string) + print_separator_line() def log_solution_info(comm): diff --git a/pointsbot/config.py b/pointsbot/config.py index e8dac1d..51fbe09 100644 --- a/pointsbot/config.py +++ b/pointsbot/config.py @@ -13,30 +13,57 @@ DATADIR = os.path.join(os.path.expanduser('~'), '.pointsbot') CONFIGPATH = os.path.join(DATADIR, 'pointsbot.toml') # Path to the sample config file +# Unused for now SAMPLEPATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'pointsbot.sample.toml')) +### Primary Functions ### + + +def load(filepath=CONFIGPATH): + # Prompt user for config values if file doesn't exist + if not os.path.exists(filepath): + datadir = os.path.dirname(filepath) + if not os.path.exists(datadir): + os.makedirs(datadir) + interactive_config(filepath) + + return Config.from_toml(filepath) + + ### Classes ### class Config: # Default config vals - DEFAULT_DBNAME = 'pointsbot.db' + DEFAULT_DB_NAME = 'pointsbot.db' + DEFAULT_LOG_NAME = 'pointsbot.log' def __init__(self, filepath, subreddit, client_id, client_secret, username, - password, levels, database_path=None): + password, levels, database_path=None, log_path=None, + feedback_url=None, scoreboard_url=None): self._filepath = filepath self._dirname = os.path.dirname(filepath) + if not log_path: + log_path = os.path.join(self._dirname, self.DEFAULT_LOG_NAME) + elif os.path.isdir(log_path): + log_path = os.path.join(log_path, self.DEFAULT_LOG_NAME) + self.log_path = log_path + + # TODO init logging here so it can be used immediately? + if not database_path: - database_path = os.path.join(self._dirname, self.DEFAULT_DBNAME) + database_path = os.path.join(self._dirname, self.DEFAULT_DB_NAME) elif os.path.isdir(database_path): - database_path = os.path.join(database_path, self.DEFAULT_DBNAME) + database_path = os.path.join(database_path, self.DEFAULT_DB_NAME) self.database_path = database_path self.subreddit = subreddit + self.feedback_url = feedback_url + self.scoreboard_url = scoreboard_url self.client_id = client_id self.client_secret = client_secret @@ -62,6 +89,10 @@ class Config: if dbpath: dbpath = os.path.abspath(os.path.expandvars(os.path.expanduser(dbpath))) + logpath = obj['filepaths']['log'] + if logpath: + logpath = os.path.abspath(os.path.expandvars(os.path.expanduser(logpath))) + return cls( filepath, obj['core']['subreddit'], @@ -71,12 +102,15 @@ class Config: obj['credentials']['password'], levels, database_path=dbpath, + log_path=logpath, + feedback_url=obj['links']['feedback'], + scoreboard_url=obj['links']['scoreboard'], ) def save(self): obj = deepcopy(vars(self)) - orig_levels = obj['levels'] - obj['levels'] = [] + orig_levels, obj['levels'] = obj['levels'], [] + # obj['levels'] = [] for level in orig_levels: obj['levels'].append({ 'name': level.name, @@ -88,20 +122,6 @@ class Config: toml.dump(obj, f) -### Functions ### - - -def load(filepath=CONFIGPATH): - # Prompt user for config values if file doesn't exist - if not os.path.exists(filepath): - datadir = os.path.dirname(filepath) - if not os.path.exists(datadir): - os.makedirs(datadir) - interactive_config(filepath) - - return Config.from_toml(filepath) - - ### Interactive Config Editing ### @@ -113,14 +133,25 @@ def interactive_config(dest): 'levels': [], } - print('#' * 80 + '\nCONFIGURING THE BOT\n' + '#' * 80) - print('\nType a value for each field, then press enter.') + print('\n' + ('#' * 80) + '\nCONFIGURING THE BOT\n' + ('#' * 80) + '\n') + print('Type a value for each field, then press enter.') print('\nIf a field is specified as optional, then you can skip it by just ' - 'pressing enter.\n') + 'pressing enter.') + print("\nIt is recommended that you skip any fields that you aren't sure " + 'about') + print('\n*** Core Configuration ***\n') configvals['core']['subreddit'] = input('name of subreddit to monitor? ') print() - configvals['filepaths']['database'] = input('database filename? (optional) ') + configvals['filepaths']['database'] = input('database filepath? (optional) ') + configvals['filepaths']['log'] = input('log filepath? (optional) ') + + print('\n*** Website Links ***\n') + print('These values should only be provided if you have valid URLs for ' + 'websites that provide these services for your subreddit.\n') + configvals['links']['feedback'] = input('feedback webpage URL? (optional) ') + configvals['links']['scoreboard'] = input('scoreboard webpage URL? (optional) ') + print('\n*** Bot account details ***\n') configvals['credentials']['client_id'] = input('client_id? ') configvals['credentials']['client_secret'] = input('client_secret? ') @@ -144,18 +175,16 @@ def interactive_config(dest): print('\nFor any more questions, please refer to the README on the Github ' 'page.') - # add_another_level = True response = 'y' while response.lower().startswith('y'): - print('\n*** Adding a level ***') + print('\n*** Adding a level ***\n') level = {} - level['name'] = input('\nLevel name? ') + level['name'] = input('Level name? ') level['points'] = int(input('Level points? ')) level['flair_template_id'] = input('Flair template ID? (optional) ') configvals['levels'].append(level) response = input('\nAdd another level? (y/n) ') - # add_another_level = response.lower().startswith('y') with open(dest, 'w') as f: toml.dump(configvals, f) diff --git a/pointsbot/reply.py b/pointsbot/reply.py index 2dc6711..6b47a80 100644 --- a/pointsbot/reply.py +++ b/pointsbot/reply.py @@ -8,14 +8,14 @@ FILLED_SYMBOL = '\u25AE' # A small filled box character EMPTY_SYMBOL = '\u25AF' # A same-sized empty box character # Number of "excess" points should be greater than max level points -EXCESS_POINTS = 100 # TODO move this to level? +EXCESS_POINTS = 100 # TODO move this to level and/or config? EXCESS_SYMBOL = '\u2605' # A star character EXCESS_SYMBOL_TITLE = 'a star' # Used in comment body ### Main Functions ### -def make(redditor, points, level_info): +def make(redditor, points, level_info, feedback_url=None, scoreboard_url=None): paras = [header()] if points == 1: @@ -45,7 +45,7 @@ def make(redditor, points, level_info): paras.append(points_status(redditor, points, level_info)) paras.append(divider()) - paras.append(footer()) + paras.append(footer(feedback_url=feedback_url, scoreboard_url=scoreboard_url)) return '\n\n'.join(paras) @@ -130,10 +130,21 @@ def divider(): return '***' -def footer(): - return ('^(Bot maintained by GlipGlorp7 ' - '| [Scoreboard](https://points.minecrafthelp.co.uk) ' - '| [Feedback](https://forms.gle/m94aGjFQwGopqQ836) ' - '| [Source Code](https://github.com/cur33/PointsBot))') +def footer(feedback_url=None, scoreboard_url=None): + footer_sections = ['Bot maintained by GlipGlorp7'] + if scoreboard_url: + # https://points.minecrafthelp.co.uk + footer_sections.append(f'[Scoreboard]({scoreboard_url})') + if feedback_url: + # https://forms.gle/m94aGjFQwGopqQ836 + footer_sections.append(f'[Feedback]({feedback_url})') + footer_sections.append('[Source Code](https://github.com/cur33/PointsBot)') + + return '^(' + ' | '.join(footer_sections) + ')' + + # return ('^(Bot maintained by GlipGlorp7 ' + # '| [Scoreboard](https://points.minecrafthelp.co.uk) ' + # '| [Feedback](https://forms.gle/m94aGjFQwGopqQ836) ' + # '| [Source Code](https://github.com/cur33/PointsBot))')