Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f4faabe44d | |||
| 87958ba09c | |||
| 592521026a | |||
| a2faff2853 | |||
| 12f62cfb24 | |||
| ca98a3b466 | |||
| af70310743 | |||
| 21cbad7111 | |||
| ca0f165257 |
@@ -1,2 +1,4 @@
|
||||
.env
|
||||
config/*
|
||||
tests.py
|
||||
message.md
|
||||
|
||||
+33
-1
@@ -1,5 +1,37 @@
|
||||
# Changelog
|
||||
|
||||
## [2.1.2] - 2026-03-10
|
||||
|
||||
### Fixed
|
||||
- Prevented AttributeError in modqueue watcher by checking for the 'title' attribute before accessing it. This ensures safe handling of Comment objects that do not have a 'title'.
|
||||
|
||||
## [2.1.1] - 2026-03-10
|
||||
|
||||
### Fixed
|
||||
- Prevented AttributeError in modqueue watcher and tag_post_watcher by checking for 'link_flair_text' only on Submission objects. This avoids errors when processing Comment objects.
|
||||
|
||||
### Changed
|
||||
- Updated logic in modqueue watcher and tag_post_watcher to safely handle both Submission and Comment objects.
|
||||
|
||||
## [2.1.0] - 2026-03-10
|
||||
|
||||
### Changed
|
||||
- Improved tag_post_watcher and modqueue_watcher logic: now runs modqueue watcher in a dedicated thread for reliable detection and commenting on filtered/removed posts.
|
||||
- Enhanced debug output for modqueue posts, including detailed attribute printing.
|
||||
- Updated .gitignore to include tests.py.
|
||||
- Updated DB/commented_posts.txt and DB/chat_wiki_requests.txt for persistent tracking.
|
||||
|
||||
### Fixed
|
||||
- Fixed issue where bot did not comment on posts in modqueue due to threading/loop logic.
|
||||
- Fixed detection of automod_filtered/removed posts in modqueue.
|
||||
|
||||
### Added
|
||||
- Added modqueue_watcher thread for independent modqueue processing.
|
||||
- Added debug print statements for modqueue post attributes and loop entry.
|
||||
|
||||
|
||||
|
||||
---
|
||||
## [V2 release]
|
||||
|
||||
### Major Features & Enhancements
|
||||
@@ -35,4 +67,4 @@
|
||||
- All modmail-based notifications and config reloads.
|
||||
- Legacy approval logic and unnecessary config options.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -35,3 +35,4 @@ o846f5z
|
||||
5jubgcs
|
||||
5judtbe
|
||||
5jugfpj
|
||||
5jv66nh
|
||||
|
||||
@@ -49,3 +49,22 @@
|
||||
1rqbf4n
|
||||
1rqbf4n
|
||||
1rqbf4n
|
||||
1rqcduj
|
||||
1rqcma5
|
||||
1rqcm8p
|
||||
1rqcr1v
|
||||
1rqct6y
|
||||
1rqcr0o
|
||||
1rqct5q
|
||||
1rqcvk4
|
||||
1rqcvit
|
||||
1rqd94l
|
||||
1rqd966
|
||||
1rqd97c
|
||||
1rqd98i
|
||||
1rqd99u
|
||||
1rqd9aw
|
||||
1rqd9cc
|
||||
1rqd9ds
|
||||
1rqd966
|
||||
1rqd9aw
|
||||
|
||||
@@ -15,125 +15,103 @@ ModReplyBot is a Reddit bot for moderators that automates post approval and stic
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
### 1. Wiki Page Configuration
|
||||
Edit your subreddit wiki page (name set by `REDDIT_WIKI_PAGE` env variable) with YAML like:
|
||||
|
||||
```
|
||||
```yaml
|
||||
triggers:
|
||||
- trigger: help
|
||||
comment: |
|
||||
Thank you for your report!
|
||||
This post is now approved.
|
||||
status: enabled
|
||||
flair_id: 12345678-aaaa-bbbb-cccc-1234567890ab
|
||||
stickied: true
|
||||
lock_post: false
|
||||
lock_comment: false
|
||||
- trigger: wc
|
||||
comment: |
|
||||
Welcome to the community!
|
||||
status: log-only
|
||||
|
||||
# ModReplyBot
|
||||
post_tags:
|
||||
- tag: Bedrock, Java
|
||||
comment: |
|
||||
__[Click here if your post says "Sorry, this post was removed by Reddit’s filters"](...)__
|
||||
status: enabled
|
||||
flair_id: 12345678-aaaa-bbbb-cccc-1234567890ab
|
||||
```
|
||||
|
||||
ModReplyBot is a Reddit bot for moderators that automates post actions and stickied comments based on subreddit wiki configuration. It responds to mod comments and mod reports containing trigger phrases, auto-comments on posts with configured tags, and now supports chat-based config reloads. All configuration is managed via a subreddit wiki page and environment variables.
|
||||
- Triggers: Bot responds to mod comments and mod reports containing `!trigger` (e.g., `!help`) with the configured comment and actions.
|
||||
- post_tags: Bot posts the comment automatically on new posts with matching tags in the title and can set flair.
|
||||
- Status options: `enabled`, `log-only`, `disabled`.
|
||||
- Optional actions: `flair_id`, `stickied`, `lock_post`, `lock_comment`.
|
||||
|
||||
## Features
|
||||
- Responds to moderator comments containing trigger phrases (starting with `!`)
|
||||
- Responds to moderator reports containing trigger phrases (starting with `!`)
|
||||
- Sets post flair, locks posts/comments, and posts stickied comments
|
||||
- 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
|
||||
- Chat-based config reload: send a chat message containing `reload-config` to the bot from a moderator account to reload the wiki config
|
||||
- Persistent database for auto-commented posts and processed chat requests (survives restarts and container recreations)
|
||||
- Docker and baremetal support
|
||||
### 2. Environment Variables
|
||||
Create a `.env` file (or set env variables directly) with:
|
||||
|
||||
## Configuration
|
||||
```
|
||||
REDDIT_CLIENT_ID=your_client_id
|
||||
REDDIT_CLIENT_SECRET=your_client_secret
|
||||
REDDIT_USERNAME=your_username
|
||||
REDDIT_PASSWORD=your_password
|
||||
REDDIT_USER_AGENT=modreplybot by /u/your_username
|
||||
REDDIT_SUBREDDIT=your_subreddit
|
||||
REDDIT_WIKI_PAGE=modreplybot-config
|
||||
LOG_LEVEL=Default
|
||||
```
|
||||
|
||||
### 1. Wiki Page Configuration
|
||||
Edit your subreddit wiki page (name set by `REDDIT_WIKI_PAGE` env variable) with YAML like:
|
||||
## Installation
|
||||
|
||||
```
|
||||
triggers:
|
||||
- trigger: help
|
||||
comment: |
|
||||
Thank you for your report!
|
||||
This post is now approved.
|
||||
status: enabled
|
||||
flair_id: 12345678-aaaa-bbbb-cccc-1234567890ab
|
||||
stickied: true
|
||||
lock_post: false
|
||||
lock_comment: false
|
||||
- trigger: wc
|
||||
comment: |
|
||||
Welcome to the community!
|
||||
status: log-only
|
||||
### Docker Compose (Recommended)
|
||||
1. Copy `.env.example` to `.env` and fill in your values.
|
||||
2. Run:
|
||||
|
||||
post_tags:
|
||||
- tag: Bedrock, Java
|
||||
comment: |
|
||||
__[Click here if your post says "Sorry, this post was removed by Reddit’s filters"](...)__
|
||||
status: enabled
|
||||
flair_id: 12345678-aaaa-bbbb-cccc-1234567890ab
|
||||
```
|
||||
```
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
- Triggers: Bot responds to mod comments and mod reports containing `!trigger` (e.g., `!help`) with the configured comment and actions.
|
||||
- post_tags: Bot posts the comment automatically on new posts with matching tags in the title and can set flair.
|
||||
- Status options: `enabled`, `log-only`, `disabled`.
|
||||
- Optional actions: `flair_id`, `stickied`, `lock_post`, `lock_comment`.
|
||||
* The DB folder is mounted for persistent database storage.
|
||||
|
||||
### 2. Environment Variables
|
||||
Create a `.env` file (or set env variables directly) with:
|
||||
### Docker Run
|
||||
1. Copy `.env.example` to `.env` and fill in your values.
|
||||
2. Run:
|
||||
|
||||
```
|
||||
REDDIT_CLIENT_ID=your_client_id
|
||||
REDDIT_CLIENT_SECRET=your_client_secret
|
||||
REDDIT_USERNAME=your_username
|
||||
REDDIT_PASSWORD=your_password
|
||||
REDDIT_USER_AGENT=modreplybot by /u/your_username
|
||||
REDDIT_SUBREDDIT=your_subreddit
|
||||
REDDIT_WIKI_PAGE=modreplybot-config
|
||||
LOG_LEVEL=Default
|
||||
```
|
||||
```
|
||||
docker run --env-file .env -v $(pwd)/DB:/app/DB slfhstd.uk/slfhstd/modreplybot:latest
|
||||
```
|
||||
|
||||
## Installation
|
||||
### Baremetal (Direct Python)
|
||||
1. Install Python 3.11+
|
||||
2. Install dependencies:
|
||||
|
||||
### Docker Compose (Recommended)
|
||||
1. Copy `.env.example` to `.env` and fill in your values.
|
||||
2. Run:
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
```
|
||||
docker compose up -d
|
||||
```
|
||||
3. Set environment variables or create a `.env` file.
|
||||
4. Run:
|
||||
|
||||
* The DB folder is mounted for persistent database storage.
|
||||
```
|
||||
python modreplybot.py
|
||||
```
|
||||
|
||||
### Docker Run
|
||||
1. Copy `.env.example` to `.env` and fill in your values.
|
||||
2. Run:
|
||||
## Chat-Based Config Reload
|
||||
- To reload the wiki config, send a chat message containing `reload-config` to the bot account from a moderator account.
|
||||
- The bot will reply to the chat message indicating whether the config is valid or not.
|
||||
- Chat message IDs are tracked in `/DB/chat_wiki_requests.txt` to prevent duplicate reloads after restarts.
|
||||
|
||||
```
|
||||
docker run --env-file .env -v $(pwd)/DB:/app/DB slfhstd.uk/slfhstd/modreplybot:latest
|
||||
```
|
||||
## 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 and mod reports for triggers.
|
||||
- Database is stored in `/DB/commented_posts.txt` and `/DB/chat_wiki_requests.txt` and survives container restarts.
|
||||
- If the wiki config is invalid, the bot will reply to the chat message with an error.
|
||||
|
||||
### Baremetal (Direct Python)
|
||||
1. Install Python 3.11+
|
||||
2. Install dependencies:
|
||||
## Moderator Guide
|
||||
See `ModGuide.md` for a detailed guide to configuring triggers, auto-post tags, and wiki options.
|
||||
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Set environment variables or create a `.env` file.
|
||||
4. Run:
|
||||
|
||||
```
|
||||
python modreplybot.py
|
||||
```
|
||||
|
||||
## Chat-Based Config Reload
|
||||
- To reload the wiki config, send a chat message containing `reload-config` to the bot account from a moderator account.
|
||||
- The bot will reply to the chat message indicating whether the config is valid or not.
|
||||
- Chat message IDs are tracked in `/DB/chat_wiki_requests.txt` to prevent duplicate reloads after restarts.
|
||||
|
||||
## 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 and mod reports for triggers.
|
||||
- Database is stored in `/DB/commented_posts.txt` and `/DB/chat_wiki_requests.txt` and survives container restarts.
|
||||
- If the wiki config is invalid, the bot will reply to the chat message with an error.
|
||||
|
||||
## Moderator Guide
|
||||
See `ModGuide.md` for a detailed guide to configuring triggers, auto-post tags, and wiki options.
|
||||
|
||||
## License
|
||||
MIT
|
||||
## License
|
||||
MIT
|
||||
|
||||
+78
-18
@@ -247,44 +247,104 @@ class ModReplyBot:
|
||||
def tag_post_watcher():
|
||||
while True:
|
||||
try:
|
||||
# Check new submissions
|
||||
for submission in self.subreddit.stream.submissions(skip_existing=True):
|
||||
flair = (submission.link_flair_text or '').strip().lower()
|
||||
flair = (getattr(submission, 'link_flair_text', '') or '').strip().lower()
|
||||
title = submission.title.strip()
|
||||
# Extract tags from title in square brackets
|
||||
import re
|
||||
title_tags = re.findall(r'\[(.*?)\]', title)
|
||||
title_tags_lower = [t.strip().lower() for t in title_tags]
|
||||
print(f"[TAG WATCH] Post {submission.id}: title='{title}', flair='{flair}', title_tags={title_tags_lower}")
|
||||
matched_tag = None
|
||||
# Check flair first
|
||||
if flair in self.tag_comments:
|
||||
matched_tag = flair
|
||||
else:
|
||||
# Check each tag in title
|
||||
for tag in title_tags_lower:
|
||||
if tag in self.tag_comments:
|
||||
matched_tag = tag
|
||||
break
|
||||
if matched_tag:
|
||||
# Only comment if not already actioned
|
||||
if submission.id not in self.commented_posts:
|
||||
status = self.tag_statuses.get(matched_tag, 'enabled')
|
||||
comment_text = self.tag_comments[matched_tag]
|
||||
flair_id = self.tag_flair_ids.get(matched_tag, '')
|
||||
if flair_id:
|
||||
try:
|
||||
submission.flair.select(flair_id)
|
||||
print(f"[TAG WATCH] Set flair '{flair_id}' for post {submission.id}")
|
||||
except Exception as e:
|
||||
print(f"[TAG WATCH] Error setting flair '{flair_id}' for post {submission.id}: {e}")
|
||||
print(f"Auto-commenting on post {submission.id} with tag '{matched_tag}'")
|
||||
self.comment_only(submission, comment_text)
|
||||
# Comment on all posts with matching tags
|
||||
if matched_tag and (submission.id not in self.commented_posts):
|
||||
status = self.tag_statuses.get(matched_tag, 'enabled')
|
||||
comment_text = self.tag_comments[matched_tag]
|
||||
flair_id = self.tag_flair_ids.get(matched_tag, '')
|
||||
if flair_id:
|
||||
try:
|
||||
submission.flair.select(flair_id)
|
||||
print(f"[TAG WATCH] Set flair '{flair_id}' for post {submission.id}")
|
||||
except Exception as e:
|
||||
print(f"[TAG WATCH] Error setting flair '{flair_id}' for post {submission.id}: {e}")
|
||||
print(f"Auto-commenting on post {submission.id} with tag '{matched_tag}'")
|
||||
self.comment_only(submission, comment_text)
|
||||
except Exception as e:
|
||||
print(f"Tag post watcher error: {e}")
|
||||
import time
|
||||
time.sleep(30)
|
||||
|
||||
def modqueue_watcher():
|
||||
while True:
|
||||
try:
|
||||
print("[MODQUEUE DEBUG] Entering modqueue loop...")
|
||||
modqueue_posts = list(self.subreddit.mod.modqueue(limit=100))
|
||||
print(f"[MODQUEUE DEBUG] Fetched {len(modqueue_posts)} posts from modqueue.")
|
||||
for submission in modqueue_posts:
|
||||
# Only access link_flair_text if it's a Submission
|
||||
if hasattr(submission, 'link_flair_text'):
|
||||
flair = (submission.link_flair_text or '').strip().lower()
|
||||
else:
|
||||
flair = ''
|
||||
# Only access title if it's a Submission
|
||||
if hasattr(submission, 'title'):
|
||||
title = submission.title.strip()
|
||||
else:
|
||||
title = ''
|
||||
import re
|
||||
title_tags = re.findall(r'\[(.*?)\]', title)
|
||||
title_tags_lower = [t.strip().lower() for t in title_tags]
|
||||
# Debug print all relevant attributes
|
||||
print(f"[MODQUEUE DEBUG] Post {submission.id}: title='{title}', flair='{flair}', title_tags={title_tags_lower}, author={submission.author}, removed_by_category={getattr(submission, 'removed_by_category', None)}, banned_by={getattr(submission, 'banned_by', None)}, mod_reason_title={getattr(submission, 'mod_reason_title', None)}, spam={getattr(submission, 'spam', None)}, removed={getattr(submission, 'removed', None)}")
|
||||
matched_tag = None
|
||||
if flair in self.tag_comments:
|
||||
matched_tag = flair
|
||||
else:
|
||||
for tag in title_tags_lower:
|
||||
if tag in self.tag_comments:
|
||||
matched_tag = tag
|
||||
break
|
||||
# Detect filtered/removed posts
|
||||
is_filtered = False
|
||||
if getattr(submission, 'removed_by_category', None):
|
||||
is_filtered = True
|
||||
if getattr(submission, 'banned_by', None):
|
||||
is_filtered = True
|
||||
if getattr(submission, 'mod_reason_title', None):
|
||||
is_filtered = True
|
||||
if getattr(submission, 'spam', None):
|
||||
is_filtered = True
|
||||
if getattr(submission, 'removed', None):
|
||||
is_filtered = True
|
||||
if is_filtered:
|
||||
print(f"[MODQUEUE WATCH] Post {submission.id} is filtered/removed.")
|
||||
# Comment only on filtered/removed posts with matching tags
|
||||
if matched_tag and is_filtered and (submission.id not in self.commented_posts):
|
||||
status = self.tag_statuses.get(matched_tag, 'enabled')
|
||||
comment_text = self.tag_comments[matched_tag]
|
||||
flair_id = self.tag_flair_ids.get(matched_tag, '')
|
||||
if flair_id:
|
||||
try:
|
||||
submission.flair.select(flair_id)
|
||||
print(f"[MODQUEUE WATCH] Set flair '{flair_id}' for post {submission.id}")
|
||||
except Exception as e:
|
||||
print(f"[MODQUEUE WATCH] Error setting flair '{flair_id}' for post {submission.id}: {e}")
|
||||
print(f"Auto-commenting on filtered/removed post {submission.id} with tag '{matched_tag}' from modqueue")
|
||||
self.comment_only(submission, comment_text)
|
||||
except Exception as e:
|
||||
print(f"Modqueue watcher error: {e}")
|
||||
import time
|
||||
time.sleep(30)
|
||||
|
||||
threading.Thread(target=tag_post_watcher, daemon=True).start()
|
||||
threading.Thread(target=modqueue_watcher, daemon=True).start()
|
||||
|
||||
# Keep main thread alive
|
||||
while True:
|
||||
|
||||
Reference in New Issue
Block a user