Updates pending full V1 release

This commit is contained in:
2026-03-10 18:37:11 +00:00
parent 3e5a330929
commit 1dc4d009d7
7 changed files with 230 additions and 75 deletions
+1
View File
@@ -7,3 +7,4 @@ REDDIT_PASSWORD=your_password
REDDIT_USER_AGENT=modreplybot on /u/your_username
REDDIT_SUBREDDIT=your_subreddit
REDDIT_WIKI_PAGE=modreplybot-config
LOG_LEVEL=Default # Options: Default, Debug
+13
View File
@@ -12,3 +12,16 @@
1rpyvk2
1rpz5j5
1rpzgxq
1rq014r
1rpyv9d
1rpxb6z
1rpsp6z
1rpsgbi
1rps6ab
1rprg5y
1rpqfb7
1rpju64
1rpir3o
1rq2nve
1rq2nwm
1rq2nwm
+82
View File
@@ -0,0 +1,82 @@
# ModReplyBot Moderator Guide
## Wiki Configuration Page
All bot configuration is managed via your subreddit wiki page. The page name is set by the `REDDIT_WIKI_PAGE` environment variable (default: `modreplybot-config`).
**Always use the old.reddit.com wiki link for editing and referencing your config:**
```
https://old.reddit.com/r/<your_subreddit>/wiki/<wiki_page>
```
## Triggers
Triggers allow moderators to perform bot actions by commenting with a trigger phrase or by reporting a post with a trigger phrase in the report reason. Triggers must start with a `!` (ex: `!help`).
### Example Trigger Configuration
```
triggers:
- trigger: help
comment: |
Thank you for your report!
This post is now approved.
status: enabled
- trigger: wc
comment: |
Welcome to the community!
status: log-only
```
- **trigger**: The phrase (without the `!`) that mods use in comments or report reasons.
- **comment**: The text the bot will post as a stickied comment.
- **status**:
- `enabled`: Bot will approve the post and comment.
- `log-only`: Bot will approve the post but not comment.
- `disabled`: Bot will not act on this trigger.
## Auto-Post Tags
Auto-post tags allow the bot to comment automatically on new posts with specific tags in the title.
### Example Auto-Post Tag Configuration
```
post_tags:
- tag: Bedrock, Java
comment: |
__[Click here if your post says "Sorry, this post was removed by Reddits filters"](...)__
status: enabled
```
- **tag**: Comma-separated list of tags. The bot matches tags in post titles (case-insensitive).
- **comment**: The text the bot will post as a stickied comment.
- **status**:
- `enabled`: Bot will comment automatically.
- `log-only`: Bot will log but not comment.
- `disabled`: Bot will not act on this tag.
## Additional Notes
- The bot only comments once per trigger per post (even if triggered multiple times).
- The bot only auto-comments once per post for each tag.
- All bot actions are logged for transparency.
- If the wiki config is invalid, the bot will notify mods via modmail and pause until fixed.
## Troubleshooting
- Make sure your wiki config is valid YAML and includes both `triggers` and `post_tags` sections.
- Use old.reddit.com for wiki editing to avoid formatting issues.
- Check bot logs for errors and modmail for config issues.
## Example Wiki Config Excerpt
```
triggers:
- trigger: help
comment: |
Thank you for your report!
This post is now approved.
status: enabled
post_tags:
- tag: Bedrock, Java
comment: |
__[Click here if your post says "Sorry, this post was removed by Reddits filters"](...)__
status: enabled
```
+28 -12
View File
@@ -1,13 +1,15 @@
# ModReplyBot Reddit Bot
ModReplyBot is a Reddit bot for moderators that watches for mod comments containing triggers, approves posts, and leaves stickied comments. It also posts automatic comments based on post tags, and notifies mods when its configuration changes. All configuration is handled via a subreddit wiki page and environment variables.
# ModReplyBot
ModReplyBot is a Reddit bot for moderators that automates post approval and stickied comments based on subreddit wiki configuration. It responds to mod comments and mod reports containing trigger phrases, and auto-comments on posts with configured tags. All configuration is managed via a subreddit wiki page and environment variables.
## Features
- Responds only to moderator comments containing triggers (ignores non-mod comments)
- Responds to moderator comments containing trigger phrases (starting with `!`)
- Responds to moderator reports containing trigger phrases (starting with `!`)
- Approves posts and leaves stickied comments
- Posts automatic comments based on tags in post titles (e.g., [Bedrock], [Java])
- Posts automatic comments based on tags in post titles (e.g., `[Bedrock]`, `[Java]`)
- Triggers, tag comments, and bot config are managed via a subreddit wiki page
- Notifies mods via modmail when the config wiki page changes
- Notifies mods via modmail when the config wiki page changes or is invalid
- Persistent database for auto-commented posts (survives restarts and container recreations)
- Docker and baremetal support
@@ -22,23 +24,27 @@ triggers:
comment: |
Thank you for your report!
This post is now approved.
- trigger: question
status: enabled
- trigger: wc
comment: |
This post has been approved.
Your question will be answered soon.
Welcome to the community!
status: log-only
post_tags:
- tag: Bedrock, Java
comment: |
__[Click here if your post says "Sorry, this post was removed by Reddits filters"](...)__
status: enabled
```
- Triggers: Bot responds to mod comments containing !trigger (e.g., !help) with the configured comment.
- Triggers: Bot responds to mod comments and mod reports containing `!trigger` (e.g., `!help`) with the configured comment.
- post_tags: Bot posts the comment automatically on new posts with matching tags in the title.
- Status options: `enabled`, `log-only`, `disabled`.
### 2. Environment Variables
Create a `.env` file (or set env variables directly) with:
```
REDDIT_CLIENT_ID=your_client_id
REDDIT_CLIENT_SECRET=your_client_secret
@@ -46,9 +52,14 @@ REDDIT_USERNAME=your_username
REDDIT_PASSWORD=your_password
REDDIT_USER_AGENT=modreplybot by /u/your_username
REDDIT_SUBREDDIT=your_subreddit
REDDIT_WIKI_PAGE=modbot-config
REDDIT_WIKI_PAGE=modreplybot-config
LOG_LEVEL=Default
```
docker compose up -d
docker run --env-file .env -v $(pwd)/DB:/app/DB slfhstd.uk/slfhstd/modreplybot:latest
pip install -r requirements.txt
## Installation
### Docker Compose (Recommended)
@@ -59,7 +70,7 @@ REDDIT_WIKI_PAGE=modbot-config
docker compose up -d
```
- The DB folder is mounted for persistent database storage.
* The DB folder is mounted for persistent database storage.
### Docker Run
1. Copy `.env.example` to `.env` and fill in your values.
@@ -84,12 +95,17 @@ pip install -r requirements.txt
python modreplybot.py
```
## Troubleshooting
- Ensure your Reddit credentials are correct and have moderator permissions.
- The bot must be able to read the wiki page and approve posts.
- Check logs for errors.
- The bot only responds to mod comments for triggers.
- The bot only responds to mod comments and mod reports for triggers.
- Database is stored in DB/commented_posts.txt and survives container restarts.
- If the wiki config is invalid, the bot will notify mods via modmail and pause until fixed.
## Moderator Guide
See `ModGuide.md` for a detailed guide to configuring triggers, auto-post tags, and wiki options.
## License
MIT
+3 -2
View File
@@ -6,9 +6,10 @@ class Config:
CLIENT_SECRET = os.environ.get('REDDIT_CLIENT_SECRET')
USERNAME = os.environ.get('REDDIT_USERNAME')
PASSWORD = os.environ.get('REDDIT_PASSWORD')
USER_AGENT = os.environ.get('REDDIT_USER_AGENT', 'modbot by /u/your_username')
USER_AGENT = os.environ.get('REDDIT_USER_AGENT', 'modreplybot by /u/your_username')
SUBREDDIT = os.environ.get('REDDIT_SUBREDDIT')
WIKI_PAGE = os.environ.get('REDDIT_WIKI_PAGE', 'modbot-config')
WIKI_PAGE = os.environ.get('REDDIT_WIKI_PAGE', 'modreplybot-config')
LOG_LEVEL = os.environ.get('LOG_LEVEL', 'Default')
@staticmethod
def validate():
+1 -1
View File
@@ -1,5 +1,5 @@
services:
modbot:
modreplybot:
image: slfhstd.uk/slfhstd/modreplybot:latest
container_name: modreplybot
env_file:
+102 -60
View File
@@ -4,7 +4,7 @@ from config import get_reddit, Config
import time
class ModBot:
class ModReplyBot:
def __init__(self):
import os
self.reddit = get_reddit()
@@ -16,6 +16,11 @@ class ModBot:
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
@@ -36,6 +41,9 @@ class ModBot:
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
@@ -43,6 +51,8 @@ class ModBot:
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 = []
@@ -60,8 +70,31 @@ class ModBot:
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:
print(f"Error fetching YAML config from wiki: {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:
@@ -80,105 +113,114 @@ class ModBot:
import threading
print("ModReplyBot started. Watching for comments and new posts...")
try:
self.fetch_yaml_config()
print(f"Triggers loaded: {self.triggers}")
print(f"Tag comments loaded: {self.tag_comments}")
print(f"Reddit user: {self.reddit.user.me()}")
print(f"Subreddit: {self.subreddit.display_name}")
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:
print(f"Startup error: {e}")
self.log(f"Startup error: {e}")
return
def comment_watcher():
def mod_comment_watcher():
last_revision = None
while True:
try:
old_revision = last_revision
self.fetch_yaml_config()
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:
print("Wiki config changed, reloading triggers and tag comments.")
self.notify_mods_config_change(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):
self.handle_comment(comment)
if comment.author and comment.author in self.subreddit.moderator():
self.handle_comment(comment)
except Exception as e:
print(f"Comment watcher error: {e}")
# No sleep needed, stream blocks
print(f"Mod comment watcher error: {e}")
def submission_watcher():
seen_submissions = set()
def mod_report_watcher():
last_revision = None
while True:
try:
old_revision = last_revision
self.fetch_yaml_config()
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:
print("Wiki config changed, reloading triggers and tag comments.")
self.notify_mods_config_change(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
new_submissions = list(self.subreddit.new(limit=10))
found_submission = False
for submission in new_submissions:
if submission.id not in seen_submissions:
found_submission = True
seen_submissions.add(submission.id)
self.handle_submission(submission)
if not found_submission:
print("No submissions detected in new() this cycle.")
for submission in self.subreddit.mod.stream.reports():
self.handle_submission(submission)
except Exception as e:
print(f"Submission watcher error: {e}")
print(f"Mod report watcher error: {e}")
import time
time.sleep(5)
time.sleep(30)
threading.Thread(target=comment_watcher, daemon=True).start()
threading.Thread(target=submission_watcher, daemon=True).start()
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 new posts based on tag(s) in title
import re
title = submission.title
print(f"New post detected: {submission.id} | Title: {title}")
tag_matches = re.findall(r"\[(.+?)\]", title)
print(f"Tags found in title: {tag_matches}")
# 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_tag = None
matched_status = None
for tag in tag_matches:
tag_lower = tag.strip().lower()
if tag_lower in self.tag_comments:
matched_comment = self.tag_comments[tag_lower]
matched_tag = tag_lower
matched_status = self.tag_statuses.get(tag_lower, 'enabled')
break
if matched_comment:
if submission.id in self.commented_posts:
print(f"Already auto-commented on post {submission.id}, skipping.")
return
# 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 new post {submission.id} with tag [{matched_tag}] (auto)")
self.save_commented_post(submission.id)
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 post {submission.id} with tag [{matched_tag}] (auto)")
self.save_commented_post(submission.id)
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 post {submission.id} with tag [{matched_tag}] (auto)")
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 post {submission.id} with tag [{matched_tag}] (auto)")
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 new post: {e}")
print(f"Error commenting on mod report: {e}")
else:
print(f"No matching tag found for post {submission.id}")
print(f"No matching trigger found in mod report for post {submission.id}")
def handle_comment(self, comment):
comment_body = comment.body.lower()
@@ -234,5 +276,5 @@ class ModBot:
print(f"Error sending modmail notification: {e}")
if __name__ == "__main__":
bot = ModBot()
bot = ModReplyBot()
bot.run()