281 lines
13 KiB
Python
281 lines
13 KiB
Python
import os
|
|
import praw
|
|
from config import get_reddit, Config
|
|
|
|
import time
|
|
|
|
class ModReplyBot:
|
|
def __init__(self):
|
|
import os
|
|
self.reddit = get_reddit()
|
|
self.subreddit = self.reddit.subreddit(Config.SUBREDDIT)
|
|
self.config_path = os.path.join(os.path.dirname(__file__), 'config', 'config.yaml')
|
|
self.triggers = []
|
|
self.comments = []
|
|
self.commented_posts = set()
|
|
self.commented_posts_file = os.path.join(os.path.dirname(__file__), 'DB', 'commented_posts.txt')
|
|
self.ensure_config_file()
|
|
self.load_commented_posts()
|
|
self.log_level = Config.LOG_LEVEL
|
|
|
|
def log(self, message, debug_only=False):
|
|
if self.log_level == 'Debug' or (self.log_level == 'Default' and not debug_only):
|
|
print(message)
|
|
|
|
def ensure_config_file(self):
|
|
import os
|
|
if not os.path.exists(self.config_path):
|
|
default_yaml = (
|
|
'triggers:\n'
|
|
' - trigger: help\n'
|
|
' comment: |\n'
|
|
' Thank you for your report!\n'
|
|
' This post is now approved.\n'
|
|
' - trigger: question\n'
|
|
' comment: |\n'
|
|
' This post has been approved.\n'
|
|
' Your question will be answered soon.\n'
|
|
)
|
|
os.makedirs(os.path.dirname(self.config_path), exist_ok=True)
|
|
with open(self.config_path, 'w', encoding='utf-8') as f:
|
|
f.write(default_yaml)
|
|
|
|
def fetch_yaml_config(self):
|
|
# Track last error revision to prevent modmail spam
|
|
if not hasattr(self, '_last_config_error_revision'):
|
|
self._last_config_error_revision = None
|
|
import yaml
|
|
try:
|
|
wiki_page = Config.WIKI_PAGE
|
|
wiki = self.subreddit.wiki[wiki_page]
|
|
wiki_content = wiki.content_md
|
|
self._wiki_revision_id = getattr(wiki, 'revision_id', None)
|
|
config = yaml.safe_load(wiki_content)
|
|
if not isinstance(config, dict) or 'triggers' not in config:
|
|
raise ValueError("Wiki config missing required 'triggers' key or is not a dict.")
|
|
self.triggers = []
|
|
self.comments = []
|
|
self.statuses = []
|
|
self.tag_comments = {}
|
|
self.tag_statuses = {}
|
|
for entry in config.get('triggers', []):
|
|
self.triggers.append(entry.get('trigger', '').strip())
|
|
self.comments.append(entry.get('comment', '').strip())
|
|
self.statuses.append(entry.get('status', 'enabled').strip().lower())
|
|
for entry in config.get('post_tags', []):
|
|
tags_str = entry.get('tag', '').strip()
|
|
comment = entry.get('comment', '').strip()
|
|
status = entry.get('status', 'enabled').strip().lower()
|
|
tags = [t.strip().lower() for t in tags_str.split(',') if t.strip()]
|
|
for tag in tags:
|
|
self.tag_comments[tag] = comment
|
|
self.tag_statuses[tag] = status
|
|
# Reset error revision tracker on successful config
|
|
self._last_config_error_revision = None
|
|
return True
|
|
except Exception as e:
|
|
self.log(f"Error fetching YAML config from wiki: {e}")
|
|
# Only send modmail if revision is new
|
|
revision = getattr(self, '_wiki_revision_id', None)
|
|
if revision != self._last_config_error_revision:
|
|
self.notify_mods_config_error(str(e))
|
|
self._last_config_error_revision = revision
|
|
return False
|
|
|
|
def notify_mods_config_error(self, error_message):
|
|
try:
|
|
subject = "ModReplyBot wiki config error"
|
|
body = f"The bot detected an error in the wiki config page and will not reload until it is fixed.\n\nError details: {error_message}\n\nPlease check the wiki config for formatting or missing information."
|
|
data = {
|
|
"subject": subject,
|
|
"text": body,
|
|
"to": f"/r/{Config.SUBREDDIT}",
|
|
}
|
|
self.reddit.post("api/compose/", data=data)
|
|
self.log("Sent modmail notification about wiki config error.")
|
|
except Exception as e:
|
|
self.log(f"Error sending modmail notification about wiki config error: {e}")
|
|
|
|
def load_commented_posts(self):
|
|
try:
|
|
with open(self.commented_posts_file, 'r', encoding='utf-8') as f:
|
|
for line in f:
|
|
self.commented_posts.add(line.strip())
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
def save_commented_post(self, post_id):
|
|
self.commented_posts.add(post_id)
|
|
with open(self.commented_posts_file, 'a', encoding='utf-8') as f:
|
|
f.write(post_id + '\n')
|
|
|
|
def run(self):
|
|
import threading
|
|
print("ModReplyBot started. Watching for comments and new posts...")
|
|
try:
|
|
config_ok = self.fetch_yaml_config()
|
|
if not config_ok:
|
|
self.log("Bot startup aborted due to wiki config error.")
|
|
return
|
|
self.log(f"Triggers loaded: {self.triggers}")
|
|
self.log(f"Tag comments loaded: {self.tag_comments}")
|
|
self.log(f"Reddit user: {self.reddit.user.me()}")
|
|
self.log(f"Subreddit: {self.subreddit.display_name}")
|
|
except Exception as e:
|
|
self.log(f"Startup error: {e}")
|
|
return
|
|
|
|
def mod_comment_watcher():
|
|
last_revision = None
|
|
while True:
|
|
try:
|
|
old_revision = last_revision
|
|
config_ok = self.fetch_yaml_config()
|
|
new_revision = getattr(self, '_wiki_revision_id', None)
|
|
if old_revision and new_revision and old_revision != new_revision:
|
|
if config_ok:
|
|
self.log("Wiki config changed, reloading triggers and tag comments.")
|
|
self.notify_mods_config_change(new_revision)
|
|
else:
|
|
self.log("Wiki config error detected, not reloading bot.")
|
|
self.log_level = Config.LOG_LEVEL
|
|
last_revision = new_revision
|
|
for comment in self.subreddit.stream.comments(skip_existing=True):
|
|
if comment.author and comment.author in self.subreddit.moderator():
|
|
self.handle_comment(comment)
|
|
except Exception as e:
|
|
print(f"Mod comment watcher error: {e}")
|
|
|
|
def mod_report_watcher():
|
|
last_revision = None
|
|
while True:
|
|
try:
|
|
old_revision = last_revision
|
|
config_ok = self.fetch_yaml_config()
|
|
new_revision = getattr(self, '_wiki_revision_id', None)
|
|
if old_revision and new_revision and old_revision != new_revision:
|
|
if config_ok:
|
|
self.log("Wiki config changed, reloading triggers and tag comments.")
|
|
self.notify_mods_config_change(new_revision)
|
|
else:
|
|
self.log("Wiki config error detected, not reloading bot.")
|
|
last_revision = new_revision
|
|
for submission in self.subreddit.mod.stream.reports():
|
|
self.handle_submission(submission)
|
|
except Exception as e:
|
|
print(f"Mod report watcher error: {e}")
|
|
import time
|
|
time.sleep(30)
|
|
|
|
threading.Thread(target=mod_comment_watcher, daemon=True).start()
|
|
threading.Thread(target=mod_report_watcher, daemon=True).start()
|
|
|
|
# Keep main thread alive
|
|
while True:
|
|
import time
|
|
time.sleep(60)
|
|
def handle_submission(self, submission):
|
|
# Respond to mod reports containing trigger phrases
|
|
print(f"New mod report detected: {submission.id} | Title: {submission.title}")
|
|
matched_trigger = None
|
|
matched_comment = None
|
|
matched_status = None
|
|
# Check report reasons for triggers
|
|
if hasattr(submission, 'mod_reports') and submission.mod_reports:
|
|
for report_tuple in submission.mod_reports:
|
|
report_reason = report_tuple[0].strip().lower()
|
|
for idx, trigger in enumerate(self.triggers):
|
|
expected = f"!{trigger.lower()}"
|
|
if expected in report_reason:
|
|
matched_trigger = trigger
|
|
matched_comment = self.comments[idx]
|
|
matched_status = self.statuses[idx] if idx < len(self.statuses) else 'enabled'
|
|
break
|
|
if matched_trigger:
|
|
break
|
|
if matched_trigger:
|
|
# Only skip if this exact trigger was already actioned for this post
|
|
trigger_key = f"{submission.id}:{matched_trigger}"
|
|
if hasattr(self, 'triggered_posts'):
|
|
if trigger_key in self.triggered_posts:
|
|
print(f"Already actioned trigger [{matched_trigger}] on post {submission.id}, skipping.")
|
|
return
|
|
else:
|
|
self.triggered_posts = set()
|
|
try:
|
|
footer = "^I ^am ^a ^bot ^and ^this ^comment ^was ^made ^automatically. ^Message ^the ^Mod ^team ^if ^I'm ^not ^working ^correctly."
|
|
comment_text = matched_comment.replace("{{author}}", submission.author.name if submission.author else "unknown") + "\n\n" + footer
|
|
if matched_status == 'enabled':
|
|
comment = submission.reply(comment_text)
|
|
comment.mod.distinguish(sticky=True)
|
|
print(f"Commented on mod report {submission.id} for trigger [{matched_trigger}] (auto)")
|
|
self.triggered_posts.add(trigger_key)
|
|
elif matched_status == 'log-only':
|
|
print(f"Log-only: Did not comment on mod report {submission.id} for trigger [{matched_trigger}] (auto)")
|
|
self.triggered_posts.add(trigger_key)
|
|
elif matched_status == 'disabled':
|
|
print(f"Disabled: Did not comment/log for mod report {submission.id} for trigger [{matched_trigger}] (auto)")
|
|
else:
|
|
print(f"Unknown status '{matched_status}' for mod report {submission.id} for trigger [{matched_trigger}] (auto)")
|
|
except Exception as e:
|
|
print(f"Error commenting on mod report: {e}")
|
|
else:
|
|
print(f"No matching trigger found in mod report for post {submission.id}")
|
|
|
|
def handle_comment(self, comment):
|
|
comment_body = comment.body.lower()
|
|
for idx, trigger in enumerate(self.triggers):
|
|
expected = f"!{trigger.lower()}"
|
|
words = [w.strip() for w in comment_body.split()]
|
|
# Only respond if author is a moderator
|
|
if expected in words and comment.author and comment.author in self.subreddit.moderator():
|
|
status = self.statuses[idx] if idx < len(self.statuses) else 'enabled'
|
|
if status == 'disabled':
|
|
print(f"Trigger '{trigger}' is disabled. Skipping.")
|
|
continue
|
|
try:
|
|
comment.mod.remove()
|
|
print(f"Removed triggering comment: {comment.id}")
|
|
except Exception as e:
|
|
print(f"Error removing comment: {e}")
|
|
submission = comment.submission
|
|
self.fetch_yaml_config()
|
|
self.approve_and_comment(submission, self.comments[idx], status)
|
|
break
|
|
|
|
def approve_and_comment(self, submission, comment_text, status='enabled'):
|
|
try:
|
|
submission.mod.approve()
|
|
if status == 'enabled':
|
|
comment = submission.reply(comment_text)
|
|
comment.mod.distinguish(sticky=True)
|
|
print(f"Approved and commented on: {submission.id}")
|
|
self.save_commented_post(submission.id)
|
|
elif status == 'log-only':
|
|
print(f"Log-only: Approved but did not comment on: {submission.id}")
|
|
self.save_commented_post(submission.id)
|
|
elif status == 'disabled':
|
|
print(f"Disabled: Did not approve/comment/log for: {submission.id}")
|
|
else:
|
|
print(f"Unknown status '{status}' for submission {submission.id}")
|
|
except Exception as e:
|
|
print(f"Error approving/commenting: {e}")
|
|
|
|
def notify_mods_config_change(self, revision_id):
|
|
try:
|
|
subject = "ModReplyBot config wiki changed"
|
|
body = f"The config wiki page was updated (revision: {revision_id}).\n\nBot restarted and is running successfully."
|
|
data = {
|
|
"subject": subject,
|
|
"text": body,
|
|
"to": f"/r/{Config.SUBREDDIT}",
|
|
}
|
|
self.reddit.post("api/compose/", data=data)
|
|
print("Sent modmail notification about config change.")
|
|
except Exception as e:
|
|
print(f"Error sending modmail notification: {e}")
|
|
|
|
if __name__ == "__main__":
|
|
bot = ModReplyBot()
|
|
bot.run()
|