Added config options & basic single-file executable
This commit is contained in:
13
CHANGELOG.md
Normal file
13
CHANGELOG.md
Normal file
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
console=True )
|
||||
coll = COLLECT(exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='PointsBot')
|
||||
runtime_tmpdir=None,
|
||||
console=True )
|
||||
|
||||
116
README.md
116
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,8 +41,19 @@ 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)
|
||||
* [pipenv](https://pipenv.readthedocs.io/en/latest/)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
from . import bot
|
||||
|
||||
if __name__ == '__main__':
|
||||
bot.run()
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))')
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user