2026-03-08 23:27:43 +00:00
import os
2026-03-08 18:08:24 +00:00
import praw
from config import get_reddit , Config
import time
2026-03-10 18:37:11 +00:00
class ModReplyBot :
2026-03-08 18:08:24 +00:00
def __init__ ( self ) :
2026-03-08 21:32:51 +00:00
import os
2026-03-08 18:08:24 +00:00
self . reddit = get_reddit ( )
self . subreddit = self . reddit . subreddit ( Config . SUBREDDIT )
2026-03-08 21:32:51 +00:00
self . config_path = os . path . join ( os . path . dirname ( __file__ ) , ' config ' , ' config.yaml ' )
2026-03-08 18:08:24 +00:00
self . triggers = [ ]
self . comments = [ ]
2026-03-08 23:27:43 +00:00
self . commented_posts = set ( )
self . commented_posts_file = os . path . join ( os . path . dirname ( __file__ ) , ' DB ' , ' commented_posts.txt ' )
2026-03-08 21:32:51 +00:00
self . ensure_config_file ( )
2026-03-08 23:27:43 +00:00
self . load_commented_posts ( )
2026-03-10 18:37:11 +00:00
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 )
2026-03-08 18:08:24 +00:00
2026-03-08 21:32:51 +00:00
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 ) :
2026-03-10 18:37:11 +00:00
# Track last error revision to prevent modmail spam
if not hasattr ( self , ' _last_config_error_revision ' ) :
self . _last_config_error_revision = None
2026-03-08 18:08:24 +00:00
import yaml
try :
2026-03-08 23:27:43 +00:00
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 )
2026-03-10 18:37:11 +00:00
if not isinstance ( config , dict ) or ' triggers ' not in config :
raise ValueError ( " Wiki config missing required ' triggers ' key or is not a dict. " )
2026-03-08 18:08:24 +00:00
self . triggers = [ ]
self . comments = [ ]
2026-03-10 15:10:39 +00:00
self . statuses = [ ]
2026-03-08 23:27:43 +00:00
self . tag_comments = { }
2026-03-10 15:10:39 +00:00
self . tag_statuses = { }
2026-03-08 18:08:24 +00:00
for entry in config . get ( ' triggers ' , [ ] ) :
self . triggers . append ( entry . get ( ' trigger ' , ' ' ) . strip ( ) )
self . comments . append ( entry . get ( ' comment ' , ' ' ) . strip ( ) )
2026-03-10 15:10:39 +00:00
self . statuses . append ( entry . get ( ' status ' , ' enabled ' ) . strip ( ) . lower ( ) )
2026-03-08 23:27:43 +00:00
for entry in config . get ( ' post_tags ' , [ ] ) :
tags_str = entry . get ( ' tag ' , ' ' ) . strip ( )
comment = entry . get ( ' comment ' , ' ' ) . strip ( )
2026-03-10 15:10:39 +00:00
status = entry . get ( ' status ' , ' enabled ' ) . strip ( ) . lower ( )
2026-03-08 23:27:43 +00:00
tags = [ t . strip ( ) . lower ( ) for t in tags_str . split ( ' , ' ) if t . strip ( ) ]
for tag in tags :
self . tag_comments [ tag ] = comment
2026-03-10 15:10:39 +00:00
self . tag_statuses [ tag ] = status
2026-03-10 18:37:11 +00:00
# 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 \n Error details: { error_message } \n \n Please 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. " )
2026-03-08 18:08:24 +00:00
except Exception as e :
2026-03-10 18:37:11 +00:00
self . log ( f " Error sending modmail notification about wiki config error: { e } " )
2026-03-08 23:27:43 +00:00
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 ' )
2026-03-08 18:08:24 +00:00
def run ( self ) :
2026-03-08 23:27:43 +00:00
import threading
print ( " ModReplyBot started. Watching for comments and new posts... " )
2026-03-08 21:32:51 +00:00
try :
2026-03-10 18:37:11 +00:00
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 } " )
2026-03-08 21:32:51 +00:00
except Exception as e :
2026-03-10 18:37:11 +00:00
self . log ( f " Startup error: { e } " )
2026-03-08 21:32:51 +00:00
return
2026-03-08 23:27:43 +00:00
2026-03-10 18:37:11 +00:00
def mod_comment_watcher ( ) :
2026-03-08 23:27:43 +00:00
last_revision = None
while True :
try :
old_revision = last_revision
2026-03-10 18:37:11 +00:00
config_ok = self . fetch_yaml_config ( )
2026-03-08 23:27:43 +00:00
new_revision = getattr ( self , ' _wiki_revision_id ' , None )
if old_revision and new_revision and old_revision != new_revision :
2026-03-10 18:37:11 +00:00
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
2026-03-08 23:27:43 +00:00
last_revision = new_revision
for comment in self . subreddit . stream . comments ( skip_existing = True ) :
2026-03-10 18:37:11 +00:00
if comment . author and comment . author in self . subreddit . moderator ( ) :
self . handle_comment ( comment )
2026-03-08 23:27:43 +00:00
except Exception as e :
2026-03-10 18:37:11 +00:00
print ( f " Mod comment watcher error: { e } " )
2026-03-08 23:27:43 +00:00
2026-03-10 18:37:11 +00:00
def mod_report_watcher ( ) :
2026-03-08 23:27:43 +00:00
last_revision = None
while True :
try :
old_revision = last_revision
2026-03-10 18:37:11 +00:00
config_ok = self . fetch_yaml_config ( )
2026-03-08 23:27:43 +00:00
new_revision = getattr ( self , ' _wiki_revision_id ' , None )
if old_revision and new_revision and old_revision != new_revision :
2026-03-10 18:37:11 +00:00
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. " )
2026-03-08 23:27:43 +00:00
last_revision = new_revision
2026-03-10 18:37:11 +00:00
for submission in self . subreddit . mod . stream . reports ( ) :
self . handle_submission ( submission )
2026-03-08 23:27:43 +00:00
except Exception as e :
2026-03-10 18:37:11 +00:00
print ( f " Mod report watcher error: { e } " )
2026-03-08 23:27:43 +00:00
import time
2026-03-10 18:37:11 +00:00
time . sleep ( 30 )
2026-03-08 23:27:43 +00:00
2026-03-10 18:37:11 +00:00
threading . Thread ( target = mod_comment_watcher , daemon = True ) . start ( )
threading . Thread ( target = mod_report_watcher , daemon = True ) . start ( )
2026-03-08 23:27:43 +00:00
# Keep main thread alive
2026-03-08 18:08:24 +00:00
while True :
2026-03-08 23:27:43 +00:00
import time
time . sleep ( 60 )
def handle_submission ( self , submission ) :
2026-03-10 18:37:11 +00:00
# Respond to mod reports containing trigger phrases
print ( f " New mod report detected: { submission . id } | Title: { submission . title } " )
matched_trigger = None
2026-03-08 23:27:43 +00:00
matched_comment = None
2026-03-10 15:10:39 +00:00
matched_status = None
2026-03-10 18:37:11 +00:00
# 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 ( )
2026-03-08 21:32:51 +00:00
try :
2026-03-08 23:27:43 +00:00
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
2026-03-10 15:10:39 +00:00
if matched_status == ' enabled ' :
comment = submission . reply ( comment_text )
comment . mod . distinguish ( sticky = True )
2026-03-10 18:37:11 +00:00
print ( f " Commented on mod report { submission . id } for trigger [ { matched_trigger } ] (auto) " )
self . triggered_posts . add ( trigger_key )
2026-03-10 15:10:39 +00:00
elif matched_status == ' log-only ' :
2026-03-10 18:37:11 +00:00
print ( f " Log-only: Did not comment on mod report { submission . id } for trigger [ { matched_trigger } ] (auto) " )
self . triggered_posts . add ( trigger_key )
2026-03-10 15:10:39 +00:00
elif matched_status == ' disabled ' :
2026-03-10 18:37:11 +00:00
print ( f " Disabled: Did not comment/log for mod report { submission . id } for trigger [ { matched_trigger } ] (auto) " )
2026-03-10 15:10:39 +00:00
else :
2026-03-10 18:37:11 +00:00
print ( f " Unknown status ' { matched_status } ' for mod report { submission . id } for trigger [ { matched_trigger } ] (auto) " )
2026-03-08 21:32:51 +00:00
except Exception as e :
2026-03-10 18:37:11 +00:00
print ( f " Error commenting on mod report: { e } " )
2026-03-08 23:27:43 +00:00
else :
2026-03-10 18:37:11 +00:00
print ( f " No matching trigger found in mod report for post { submission . id } " )
2026-03-08 18:08:24 +00:00
2026-03-08 21:32:51 +00:00
def handle_comment ( self , comment ) :
comment_body = comment . body . lower ( )
for idx , trigger in enumerate ( self . triggers ) :
expected = f " ! { trigger . lower ( ) } "
2026-03-08 23:27:43 +00:00
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 ( ) :
2026-03-10 15:10:39 +00:00
status = self . statuses [ idx ] if idx < len ( self . statuses ) else ' enabled '
if status == ' disabled ' :
print ( f " Trigger ' { trigger } ' is disabled. Skipping. " )
continue
2026-03-08 21:32:51 +00:00
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 ( )
2026-03-10 15:10:39 +00:00
self . approve_and_comment ( submission , self . comments [ idx ] , status )
2026-03-08 21:32:51 +00:00
break
2026-03-08 18:08:24 +00:00
2026-03-10 15:10:39 +00:00
def approve_and_comment ( self , submission , comment_text , status = ' enabled ' ) :
2026-03-08 18:08:24 +00:00
try :
submission . mod . approve ( )
2026-03-10 15:10:39 +00:00
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 } " )
2026-03-08 18:08:24 +00:00
except Exception as e :
print ( f " Error approving/commenting: { e } " )
2026-03-08 23:27:43 +00:00
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 \n Bot 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 } " )
2026-03-08 18:08:24 +00:00
if __name__ == " __main__ " :
2026-03-10 18:37:11 +00:00
bot = ModReplyBot ( )
2026-03-08 18:08:24 +00:00
bot . run ( )