Created initial rule system & improved logging

This commit is contained in:
Collin Rapp
2020-05-11 22:14:26 -07:00
parent b72e7e244b
commit 2331085540

View File

@@ -3,6 +3,7 @@ import os
import os.path import os.path
import re import re
import sys import sys
from collections import namedtuple
import praw import praw
import prawcore import prawcore
@@ -13,15 +14,10 @@ from . import config, database, level, reply
USER_AGENT = 'PointsBot (by u/GlipGlorp7)' 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'))
# The pattern that determines whether a post is marked as solved # The pattern that determines whether a post is marked as solved
# Could also use re.IGNORECASE flag instead # Could also use re.IGNORECASE flag instead
SOLVED_PAT = re.compile('![Ss]olved') SOLVED_PATTERN = re.compile('![Ss]olved')
MOD_SOLVED_PAT = re.compile('/[Ss]olved') MOD_SOLVED_PATTERN = re.compile('/[Ss]olved')
### Main Function ### ### Main Function ###
@@ -34,7 +30,7 @@ def run():
file_handler = logging.FileHandler(cfg.log_path, 'w', 'utf-8') file_handler = logging.FileHandler(cfg.log_path, 'w', 'utf-8')
console_handler = logging.StreamHandler(sys.stderr) console_handler = logging.StreamHandler(sys.stderr)
console_handler.setLevel(logging.INFO) console_handler.setLevel(logging.INFO)
# logging.basicConfig(filename=cfg.log_path,
logging.basicConfig(handlers=[file_handler, console_handler], logging.basicConfig(handlers=[file_handler, console_handler],
level=logging.DEBUG, level=logging.DEBUG,
format='%(asctime)s %(levelname)s:%(module)s: %(message)s', format='%(asctime)s %(levelname)s:%(module)s: %(message)s',
@@ -54,7 +50,7 @@ def run():
if not reddit.read_only: if not reddit.read_only:
logging.info('Has write access to Reddit') logging.info('Has write access to Reddit')
else: else:
logging.info('Has read-only access to Reddit') logging.warning('Has read-only access to Reddit')
subreddit = reddit.subreddit(cfg.subreddit) subreddit = reddit.subreddit(cfg.subreddit)
logging.info('Watching subreddit %s', subreddit.title) logging.info('Watching subreddit %s', subreddit.title)
@@ -63,7 +59,7 @@ def run():
else: else:
logging.warning('Is NOT moderator for monitored subreddit') logging.warning('Is NOT moderator for monitored subreddit')
monitor_comments(subreddit, db, levels, cfg) monitor_comments(reddit, subreddit, db, levels, cfg)
# Ignoring other potential exceptions for now, since we may not be able # Ignoring other potential exceptions for now, since we may not be able
# to recover from them as well as from this one # to recover from them as well as from this one
except prawcore.exceptions.RequestException as e: except prawcore.exceptions.RequestException as e:
@@ -76,33 +72,40 @@ def run():
logging.error('Attempting to reconnect') logging.error('Attempting to reconnect')
def monitor_comments(subreddit, db, levels, cfg): def monitor_comments(reddit, subreddit, db, levels, cfg):
"""Monitor new comments in the subreddit, looking for confirmed solutions.""" """Monitor new comments in the subreddit, looking for confirmed solutions."""
# Passing pause_after=0 will bypass the internal exponential delay, but have # Passing pause_after=0 will bypass the internal exponential delay, but have
# to check if any comments are returned with each query # to check if any comments are returned with each query
for comm in subreddit.stream.comments(skip_existing=True, pause_after=0): for comm in subreddit.stream.comments(skip_existing=True, pause_after=0):
if comm is None: if comm is None:
continue continue
if comm.author == reddit.user.me():
logging.info('Received comment') logging.info('Comment was posted by this bot')
# TODO more debug info about comment, eg author continue
logging.debug('Comment author: "%s"', comm.author.name) if comm.author.name == reddit.user.me().name:
# logging.debug('Comment text: "%s"', comm.body) logging.info('Comment was posted by this bot name')
if not marks_as_solved(comm):
logging.info('Comment does not mark issue as solved')
continue continue
logging.info('Comment marks issues as solved') logging.info('Found comment')
# TODO more debug info about comment
logging.debug('Comment author: "%s"', comm.author.name)
logging.debug('Comment text: "%s"', comm.body)
if not marks_as_solved(comm):
# Skip this "!solved" comment
logging.info('Comment does not mark issue as solved')
continue
logging.info('Comment marks issue as solved')
if is_mod_comment(comm): if is_mod_comment(comm):
logging.info('Comment was submitted by mod') logging.info('Comment was submitted by mod')
elif not is_first_solution(comm): elif is_first_solution(comm):
logging.info('Comment is the first to mark the issue as solved')
else:
# Skip this "!solved" comment # Skip this "!solved" comment
logging.info('Comment is NOT the first to mark the issue as solved') logging.info('Comment is NOT the first to mark the issue as solved')
continue continue
logging.info('Comment is the first to mark the issue as solved')
log_solution_info(comm) log_solution_info(comm)
solver = find_solver(comm) solver = find_solver(comm)
@@ -122,7 +125,7 @@ def monitor_comments(subreddit, db, levels, cfg):
try: try:
comm.reply(reply_body) comm.reply(reply_body)
logging.info('Replied to the comment') logging.info('Replied to the comment')
# logging.debug('Reply body: %s', reply_body) logging.debug('Reply body: %s', reply_body)
except praw.exceptions.APIException as e: except praw.exceptions.APIException as e:
logging.error('Unable to reply to comment: %s', e) logging.error('Unable to reply to comment: %s', e)
db.remove_point(solver) db.remove_point(solver)
@@ -147,22 +150,85 @@ def monitor_comments(subreddit, db, levels, cfg):
### Reddit Comment Functions ### ### Reddit Comment Functions ###
SolutionResponseRule = namedtuple('SolutionResponseRule',
'description success_msg failure_msg check')
OP_RESPONSE_RULES = [
SolutionResponseRule(
'user "solved" pattern',
'Comment contains user "solved" pattern',
'Comment does not contain user "solved" pattern',
lambda c: SOLVED_PATTERN.search(c.body),
),
SolutionResponseRule(
'is a reply (not top-level)',
'Comment is a reply to another comment',
'Comment is a top-level comment',
lambda c: not c.is_root,
),
SolutionResponseRule(
'author is OP',
'Comment author is submission OP',
'Comment author is not submission OP',
lambda c: c.is_submitter,
),
SolutionResponseRule(
"OP can't solve own problem",
'Submission OP is different from solution author',
'Submission OP is marking own comment as solution',
lambda c: not c.is_root and not c.parent().is_submitter,
),
]
MOD_RESPONSE_RULES = [
SolutionResponseRule(
'contains mod "solved" pattern',
'Comment contains mod "solved" pattern',
'Comment does not contain mod "solved" pattern',
lambda c: MOD_SOLVED_PATTERN.search(c.body),
),
SolutionResponseRule(
'is a reply (not top-level)',
'Comment is a reply to another comment',
'Comment is a top-level comment',
lambda c: not c.is_root,
),
SolutionResponseRule(
'author is mod',
'Comment author is a mod',
'Comment author is not a mod',
# TODO Initialize rules in a function so that they can include other
# functions
lambda c: is_mod_comment(c),
),
]
# GENERAL_RESPONSE_RULES = []
def check_rules(rules, comment):
all_rules_passed = True
for rule in rules:
rule_passed = rule.check(comment)
logging.info(rule.success_msg if rule_passed else rule.failure_msg)
all_rules_passed = all_rules_passed and rule_passed
return all_rules_passed
def marks_as_solved(comment): def marks_as_solved(comment):
"""Return True if the comment meets the criteria for marking the submission """Return True if the comment meets the criteria for marking the submission
as solved, False otherwise. as solved, False otherwise.
""" """
op_resp_to_solver = (not comment.is_root # TODO should enforce that only one or the other can pass?
and comment.is_submitter op_rules_pass = check_rules(OP_RESPONSE_RULES, comment)
and not comment.parent().is_submitter if op_rules_pass:
and SOLVED_PAT.search(comment.body)) logging.info('OP marking submission as solved')
# Mod can only used MOD_SOLVED_PAT on any post, including their own mod_rules_pass = check_rules(MOD_RESPONSE_RULES, comment)
mod_resp_to_solver = (not comment.is_root if mod_rules_pass:
and comment.subreddit.moderator(redditor=comment.author) logging.info('Mod marking submission as solved')
and MOD_SOLVED_PAT.search(comment.body))
return op_resp_to_solver or mod_resp_to_solver return op_rules_pass or mod_rules_pass
def is_mod_comment(comment): def is_mod_comment(comment):
@@ -188,10 +254,11 @@ def is_first_solution(solved_comment):
def find_solver(solved_comment): def find_solver(solved_comment):
"""Determine the redditor responsible for solving the question.""" """Determine the redditor responsible for solving the question."""
# TODO plz make this better someday
return solved_comment.parent().author return solved_comment.parent().author
### Print Functions ### ### Print & Logging Functions ###
def print_separator_line(): def print_separator_line():
@@ -220,8 +287,5 @@ def log_solution_info(comm):
logging.debug('Solution comment:') logging.debug('Solution comment:')
logging.debug('Author: %s', comm.parent().author.name) logging.debug('Author: %s', comm.parent().author.name)
logging.debug('Body: %s', comm.parent().body) logging.debug('Body: %s', comm.parent().body)
logging.debug('Comment marking solution as solved:')
logging.debug('Author: %s', comm.author.name)
logging.debug('Body: %s', comm.body)