Freeze the app for user-friendly distribution & usage

This commit is contained in:
Collin Rapp
2020-05-10 11:26:09 -07:00
parent 176f3f6f0e
commit fb03e9fff5
10 changed files with 242 additions and 69 deletions

View File

@@ -1,3 +1,6 @@
import logging
import os
import os.path
import re
import praw
@@ -9,8 +12,13 @@ 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'))
# The pattern that determines whether a post is marked as solved
# Could also just use re.IGNORECASE flag
# Could also use re.IGNORECASE flag instead
SOLVED_PAT = re.compile('![Ss]olved')
MOD_SOLVED_PAT = re.compile('/[Ss]olved')
@@ -18,6 +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()
levels = cfg.levels
db = database.Database(cfg.database_path)
@@ -30,21 +45,26 @@ def run():
username=cfg.username,
password=cfg.password,
user_agent=USER_AGENT)
subreddit = reddit.subreddit(cfg.subreddit)
logging.info('Connected to Reddit as %s', reddit.user.me())
if not reddit.read_only:
logging.info('Has write access to Reddit')
else:
logging.info('Has read-only access to Reddit')
print_level(0, f'Connected to Reddit as {reddit.user.me()}')
print_level(1, f'Read-only? {reddit.read_only}')
print_level(0, f'Watching subreddit {subreddit.title}')
is_mod = bool(subreddit.moderator(redditor=reddit.user.me()))
print_level(1, f'Is mod? {is_mod}')
subreddit = reddit.subreddit(cfg.subreddit)
logging.info('Watching subreddit %s', subreddit.title)
if subreddit.moderator(redditor=reddit.user.me()):
logging.info('Is moderator for monitored subreddit')
else:
logging.warning('Is NOT moderator for monitored subreddit')
monitor_comments(subreddit, db, levels)
# Ignoring other potential exceptions for now, since we may not be able
# to recover from them as well as from this one
except prawcore.exceptions.RequestException as e:
print('Unable to connect; attempting again....')
log.error('Unable to connect; attempting again....')
except prawcore.exceptions.ServerError as e:
print('Lost connection to Reddit; attempting to reconnect....')
log.error('Lost connection to Reddit; attempting to reconnect....')
def monitor_comments(subreddit, db, levels):
@@ -55,57 +75,70 @@ def monitor_comments(subreddit, db, levels):
if comm is None:
continue
print_level(0, '\nFound comment')
print_level(1, f'Comment text: "{comm.body}"')
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):
print_level(1, 'Not a "![Ss]olved" comment')
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):
print_level(1, 'Mod comment')
logging.info('Comment was submitted by mod')
# print_level(1, 'Mod comment')
elif not is_first_solution(comm):
# Skip this "!solved" comment and wait for the next
print_level(1, 'Not the first solution')
# 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
print_level(1, 'This is the first solution found')
print_solution_info(comm)
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)
print_level(1, f'Added point for {solver.name}')
logging.info('Added point for user "%s"', solver.name)
# print_level(1, f'Added point for {solver.name}')
points = db.get_points(solver)
print_level(1, f'Total points for {solver.name}: {points}')
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)
try:
comm.reply(reply_body)
print_level(1, f'Replied to comment with: "{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:
print_level(1, 'Unable to reply to comment')
print_level(2, f'{e}')
logging.error('Unable to reply to comment: %s', e)
db.remove_point(solver)
print_level(1, f'Removed point awarded to {solver.name}')
print_level(1, 'Skipping comment')
logging.error('Removed point that was just awarded to user "%s"', solver.name)
logging.error('Skipping comment')
continue
# Check if (non-mod) user flair should be updated to new level
lvl = level_info.current
if lvl and lvl.points == points:
print_level(1, f'User reached level: {lvl.name}')
logging.info('User reached level: %s', lvl.name)
if not subreddit.moderator(redditor=solver):
print_level(2, 'Setting flair')
print_level(3, f'Flair text: {lvl.name}')
print_level(3, f'Flair template ID: {lvl.flair_template_id}')
# 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)
subreddit.flair.set(solver,
text=lvl.name,
flair_template_id=lvl.flair_template_id)
else:
print_level(2, 'Solver is mod; don\'t alter flair')
logging.info('Solver is mod; don\'t alter flair')
### Reddit Comment Functions ###
@@ -154,20 +187,40 @@ def find_solver(solved_comment):
return solved_comment.parent().author
### Debugging & Logging ###
### Print Functions ###
def print_separator_line():
print('#' * 80)
def print_welcome_message():
print_separator_line()
print('\n*** Welcome to PointsBot! ***\n')
print_separator_line()
print('\nThis bot will monitor the subreddit specified in the '
'configuration file as long as this program is running.')
print('\nAny Reddit activity that occurs while this program is not running '
'will be missed. You can work around this by using features '
'mentioned in the README.')
print('\nThe output from this program can be referenced if any issues are '
'to occur, and the relevant error message or crash report can be '
'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)
def print_solution_info(comm):
print_level(1, 'Submission solved')
print_level(2, 'Solution comment:')
print_level(3, f'Author: {comm.parent().author.name}')
print_level(3, f'Body: {comm.parent().body}')
print_level(2, '"Solved" comment:')
print_level(3, f'Author: {comm.author.name}')
print_level(3, f'Body: {comm.body}')
def log_solution_info(comm):
logging.info('Submission solved')
logging.debug('Solution comment:')
logging.debug('Author: %s', comm.parent().author.name)
logging.debug('Body: %s', comm.parent().body)
logging.debug('"Solved" comment:')
logging.debug('Author: %s', comm.author.name)
logging.debug('Body: %s', comm.body)

View File

@@ -115,19 +115,39 @@ def interactive_config(dest):
print('#' * 80 + '\nCONFIGURING THE BOT\n' + '#' * 80)
print('\nType a value for each field, then press enter.')
print('\nIf the field is specified as optional, leave blank to skip.\n')
print('\nIf a field is specified as optional, then you can skip it by just '
'pressing enter.\n')
configvals['core']['subreddit'] = input('subreddit? ')
configvals['core']['subreddit'] = input('name of subreddit to monitor? ')
print()
configvals['filepaths']['database'] = input('database filename? (optional) ')
print()
print('\n*** Bot account details ***\n')
configvals['credentials']['client_id'] = input('client_id? ')
configvals['credentials']['client_secret'] = input('client_secret? ')
configvals['credentials']['username'] = input('username? ')
configvals['credentials']['password'] = input('password? ')
configvals['credentials']['username'] = input('bot username? ')
configvals['credentials']['password'] = input('bot password? ')
add_another_level = True
while add_another_level:
print('\n*** Flair Levels ***\n')
print('These fields will determine the different levels that your '
'subreddit users can achieve by earning points.')
print('\nFor each level, you should provide...')
print("\t- Level name: the text that appears in the user's flair")
print('\t- Level points: the number of points needed to reach the level')
print('\t- Flair template ID: (optional) the flair template ID in your')
print('\t subreddit to be used for this level flair')
print('\nThese may be provided in any order; the bot will sort them later.')
print('\nDo not provide more than one level with the same number of points.')
print('\nNote that at the moment, providing a level points value of zero '
'will not set a default flair, because users must solve at least one '
'issue before the bot will keep track of their points and set their '
'flair for the first time.')
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 ***')
level = {}
level['name'] = input('\nLevel name? ')
level['points'] = int(input('Level points? '))
@@ -135,7 +155,7 @@ def interactive_config(dest):
configvals['levels'].append(level)
response = input('\nAdd another level? (y/n) ')
add_another_level = response.lower().startswith('y')
# add_another_level = response.lower().startswith('y')
with open(dest, 'w') as f:
toml.dump(configvals, f)