Created initial rule system & improved logging
This commit is contained in:
138
pointsbot/bot.py
138
pointsbot/bot.py
@@ -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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user