Updates pending full V1 release
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -12,3 +12,16 @@
|
||||
1rpyvk2
|
||||
1rpz5j5
|
||||
1rpzgxq
|
||||
1rq014r
|
||||
1rpyv9d
|
||||
1rpxb6z
|
||||
1rpsp6z
|
||||
1rpsgbi
|
||||
1rps6ab
|
||||
1rprg5y
|
||||
1rpqfb7
|
||||
1rpju64
|
||||
1rpir3o
|
||||
1rq2nve
|
||||
1rq2nwm
|
||||
1rq2nwm
|
||||
|
||||
+82
@@ -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 Reddit’s 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 Reddit’s filters"](...)__
|
||||
status: enabled
|
||||
```
|
||||
@@ -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 Reddit’s 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
|
||||
|
||||
@@ -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
@@ -1,5 +1,5 @@
|
||||
services:
|
||||
modbot:
|
||||
modreplybot:
|
||||
image: slfhstd.uk/slfhstd/modreplybot:latest
|
||||
container_name: modreplybot
|
||||
env_file:
|
||||
|
||||
+102
-60
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user