13 Commits

Author SHA1 Message Date
Imonlytryingtohelp dc8060270b 2.2.3 2026-04-04 15:52:06 +01:00
slfhstd 476eadd6c9 2.2.2 2026-03-29 20:27:19 +01:00
slfhstd 8fe3d1f030 2.2.1 2026-03-14 22:54:59 +00:00
slfhstd 6d2d04ea40 2.2.0 2026-03-12 22:56:57 +00:00
slfhstd f4faabe44d changes 2026-03-11 00:06:12 +00:00
slfhstd 87958ba09c 2.1.2 2026-03-10 23:52:19 +00:00
slfhstd 592521026a V2.1.1 2026-03-10 23:48:39 +00:00
slfhstd a2faff2853 changelog 2026-03-10 23:38:34 +00:00
slfhstd 12f62cfb24 V2.1.0 2026-03-10 23:36:40 +00:00
slfhstd ca98a3b466 guess 2026-03-10 22:40:17 +00:00
slfhstd af70310743 formatting 2026-03-10 22:39:11 +00:00
slfhstd 21cbad7111 formatting 2026-03-10 22:38:05 +00:00
slfhstd ca0f165257 formatting fixes 2026-03-10 22:36:57 +00:00
11 changed files with 661 additions and 121 deletions
+3
View File
@@ -1,2 +1,5 @@
.env
config/*
tests.py
message.md
docker/
+93 -1
View File
@@ -1,5 +1,97 @@
# Changelog
## [2.2.3] - 2026-04-04
### Fixed
- **Template Variable Substitution in post_tags Comments:**
- Fixed issue where `{author}` and `{{author}}` placeholders in `post_tags` comments were not being replaced with the actual post author's username.
- Template variables now properly substitute in automatic post_tags comments, matching behavior of trigger-based comments.
## [2.2.2] - 2026-03-29
### Added
- **Required Text Feature for Config Validation:**
- Added `required_text` section to `post_tags` for tag-specific validation rules.
- Each tag can now have its own list of required text strings to validate posts.
- If a post with a tag doesn't contain any of the required text strings, a custom message is prepended to the bot's comment.
- Supports searching in title, body, or both (`search_in` field).
- Properly handles quoted phrases (e.g., `"Tiny Takeover"`) by stripping quotes before comparison.
- Allows multiple different validation rules within a single shared post_tag entry.
## [2.2.1] - 2026-03-14
### Added
- **Post Backfill on Startup:**
- Added `backfill_recent_posts()` to automatically scan and comment on posts from the last 24 hours when bot starts up.
- Prevents missed posts when bot is offline for extended periods.
- Only processes posts created in the last 24 hours to avoid overwhelming backfill.
- **Duplicate Comment Detection:**
- Bot now checks if the exact comment has already been posted on a post before commenting again.
- Prevents duplicate comments on the same post.
### Changed
- Enhanced tag_post_watcher with better debug output and attempt tracking.
- Startup now prioritizes backfill thread for catching missed posts.
### Fixed
- Fixed issue where new submissions stream wasn't being processed (skip_existing=True issue).
- Improved stream error handling with attempt counter and better exception messages.
## [2.2.0] - 2026-03-12
### Added
- **Configurable Ignore Tags:**
- Added `ignore_tags` section to wiki config. Bot now ignores posts with matching tags in the title or flair (case-insensitive).
- Updated README.md and bot logic to support this feature.
- **Update Checker Integration:**
- Added `update_checker.py` module to check for bot updates and notify moderators via modmail.
- Update checker runs in a background thread and checks for updates hourly.
- **Bot Version & Name Constants:**
- Added `BOT_VERSION` and `BOT_NAME` constants at the top of `modreplybot.py` for easy version/name changes.
### Changed
- Integrated update checker startup in main bot entrypoint.
- Improved documentation for ignore_tags and update checker features.
### Fixed
- N/A
## [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 +127,4 @@
- All modmail-based notifications and config reloads.
- Legacy approval logic and unnecessary config options.
---
+7
View File
@@ -35,3 +35,10 @@ o846f5z
5jubgcs
5judtbe
5jugfpj
5jv66nh
5jvi6wk
5jvji2r
o9wezfi
o9wf1xl
o9x1jh0
o9x1lyh
+21
View File
@@ -49,3 +49,24 @@
1rqbf4n
1rqbf4n
1rqbf4n
1rqcduj
1rqcma5
1rqcm8p
1rqcr1v
1rqct6y
1rqcr0o
1rqct5q
1rqcvk4
1rqcvit
1rqd94l
1rqd966
1rqd97c
1rqd98i
1rqd99u
1rqd9aw
1rqd9cc
1rqd9ds
1rqd966
1rqd9aw
1rs5sz0
1rs5sxl
+1
View File
@@ -5,5 +5,6 @@ COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY config.py .
COPY modreplybot.py .
COPY update_checker.py .
ENV PYTHONUNBUFFERED=1
CMD ["python", "modreplybot.py"]
+67 -82
View File
@@ -15,34 +15,12 @@ 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
# ModReplyBot
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.
## 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
## Configuration
### 1. Wiki Page Configuration
Edit your subreddit wiki page (name set by `REDDIT_WIKI_PAGE` env variable) with YAML like:
```
triggers:
- trigger: help
comment: |
Thank you for your report!
@@ -57,83 +35,90 @@ triggers:
Welcome to the community!
status: log-only
post_tags:
post_tags:
- tag: Bedrock, Java
comment: |
__[Click here if your post says "Sorry, this post was removed by Reddits filters"](...)__
status: enabled
flair_id: 12345678-aaaa-bbbb-cccc-1234567890ab
```
- 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`.
# New: Ignore tags
ignore_tags:
- tag: Off-Topic
- tag: Meme
```
### 2. Environment Variables
Create a `.env` file (or set env variables directly) with:
- Triggers: Bot responds to mod comments and mod reports containing `!trigger` (e.g., `!help`) with the configured comment and actions.
```
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
```
- post_tags: Bot posts the comment automatically on new posts with matching tags in the title and can set flair.
- ignore_tags: Bot will ignore (not comment on) posts with these tags in the title. Tags are case-insensitive and match `[Tag]` in the post title or flair.
- Status options: `enabled`, `log-only`, `disabled`.
- Optional actions: `flair_id`, `stickied`, `lock_post`, `lock_comment`.
## Installation
### 2. Environment Variables
Create a `.env` file (or set env variables directly) with:
### Docker Compose (Recommended)
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 compose up -d
```
## Installation
* The DB folder is mounted for persistent database storage.
### Docker Compose (Recommended)
1. Copy `.env.example` to `.env` and fill in your values.
2. Run:
### Docker Run
1. Copy `.env.example` to `.env` and fill in your values.
2. Run:
```
docker compose up -d
```
```
docker run --env-file .env -v $(pwd)/DB:/app/DB slfhstd.uk/slfhstd/modreplybot:latest
```
* The DB folder is mounted for persistent database storage.
### Baremetal (Direct Python)
1. Install Python 3.11+
2. Install dependencies:
### Docker Run
1. Copy `.env.example` to `.env` and fill in your values.
2. Run:
```
pip install -r requirements.txt
```
```
docker run --env-file .env -v $(pwd)/DB:/app/DB slfhstd.uk/slfhstd/modreplybot:latest
```
3. Set environment variables or create a `.env` file.
4. Run:
### Baremetal (Direct Python)
1. Install Python 3.11+
2. Install dependencies:
```
python modreplybot.py
```
```
pip install -r requirements.txt
```
## 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.
3. Set environment variables or create a `.env` file.
4. Run:
## 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.
```
python modreplybot.py
```
## Moderator Guide
See `ModGuide.md` for a detailed guide to configuring triggers, auto-post tags, and wiki options.
## 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.
## License
MIT
## 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
Binary file not shown.
+3 -3
View File
@@ -1,10 +1,10 @@
services:
modreplybot:
image: slfhstd.uk/slfhstd/modreplybot:latest
image: slfhstd.uk/slfhstd/modreplybot:dev
container_name: modreplybot
env_file:
- .env
volumes:
- ./config:/app/config
- ./DB:/app/DB
- ./docker/config:/app/config
- ./docker/DB:/app/DB
restart: unless-stopped
+277 -12
View File
@@ -1,6 +1,10 @@
import os
import praw
from config import get_reddit, Config
from update_checker import start_update_checker
BOT_VERSION = "2.2.3" # Change this for new releases
BOT_NAME = "ModReplyBot" # Change this if bot name changes
import time
@@ -43,8 +47,18 @@ class ModReplyBot:
print(f"Chat message watcher error: {e}")
import time
time.sleep(30)
def comment_only(self, submission, comment_text):
def comment_only(self, submission, comment_text, matched_tag=None):
try:
# Replace template variables
comment_text = comment_text.replace("{{author}}", submission.author.name if submission.author else "unknown")
comment_text = comment_text.replace("{author}", submission.author.name if submission.author else "unknown")
# Check required text and prepend message if needed
if matched_tag and matched_tag in self.tag_required_text:
comment_text = self.check_required_text_and_prepend_message(
submission, comment_text, self.tag_required_text[matched_tag], matched_tag
)
comment = submission.reply(comment_text)
comment.mod.distinguish(sticky=True)
print(f"Commented (no approval) on: {submission.id}")
@@ -58,6 +72,16 @@ class ModReplyBot:
self.config_path = os.path.join(os.path.dirname(__file__), 'config', 'config.yaml')
self.triggers = []
self.comments = []
self.statuses = []
self.flair_ids = []
self.stickied = []
self.lock_post = []
self.lock_comment = []
self.trigger_required_text = []
self.tag_comments = {}
self.tag_statuses = {}
self.tag_flair_ids = {}
self.tag_required_text = {}
self.commented_posts = set()
self.commented_posts_file = os.path.join(os.path.dirname(__file__), 'DB', 'commented_posts.txt')
self.tagged_commented_posts_file = os.path.join(os.path.dirname(__file__), 'DB', 'tagged_commented_posts.txt')
@@ -73,6 +97,64 @@ class ModReplyBot:
if self.log_level == 'Debug' or (self.log_level == 'Default' and not debug_only):
print(message)
def check_required_text_and_prepend_message(self, submission, comment_text, required_text_list, matched_tag=None):
"""Check if post contains required text strings, prepend message if not."""
if not required_text_list:
return comment_text
# Get post content based on search_in
title = submission.title.lower()
body = getattr(submission, 'selftext', '').lower()
title_and_body = title + ' ' + body
print(f"[DEBUG] Checking required_text for tag '{matched_tag}', title='{title}', body='{body}'")
for req_entry in required_text_list:
req_tag = req_entry.get('tag', '').strip().lower()
print(f"[DEBUG] Checking req_entry with tag '{req_tag}'")
if req_tag and matched_tag and req_tag != matched_tag.lower():
print(f"[DEBUG] Skipping req_entry, tag mismatch")
continue # This required_text is for a different tag
text_list = req_entry.get('text', '').split(',')
def normalize_req_text(item):
item = item.strip()
# Remove surrounding quotes (single or double) for exact phrases
if (item.startswith('"') and item.endswith('"')) or (item.startswith("'") and item.endswith("'")):
item = item[1:-1]
return item.strip().lower()
text_list = [normalize_req_text(t) for t in text_list if t.strip()]
print(f"[DEBUG] Required text_list: {text_list}")
search_in = req_entry.get('search_in', 'title_and_body').lower()
if search_in == 'title':
content = title
elif search_in == 'body':
content = body
else: # title_and_body
content = title_and_body
print(f"[DEBUG] Searching in '{search_in}', content='{content}'")
# Check if any required text is present
found = False
for req_text in text_list:
if req_text in content:
print(f"[DEBUG] Found required text '{req_text}' in content")
found = True
break
if not found:
print(f"[DEBUG] Required text not found, prepending message")
# Prepend the message
message = req_entry.get('message', '').strip()
if message:
comment_text = message + '\n\n' + comment_text
else:
print(f"[DEBUG] Required text found, not prepending message")
return comment_text
def ensure_config_file(self):
import os
if not os.path.exists(self.config_path):
@@ -126,9 +208,11 @@ class ModReplyBot:
self.stickied = []
self.lock_post = []
self.lock_comment = []
self.trigger_required_text = []
self.tag_comments = {}
self.tag_statuses = {}
self.tag_flair_ids = {}
self.tag_required_text = {}
for entry in config.get('triggers', []):
self.triggers.append(entry.get('trigger', '').strip())
self.comments.append(entry.get('comment', '').strip())
@@ -141,16 +225,25 @@ class ModReplyBot:
lock_post_val = lock_post_val.lower() in ['true', '1', 'yes']
self.lock_post.append(bool(lock_post_val))
self.lock_comment.append(bool(entry.get('lock_comment', False)))
self.trigger_required_text.append(entry.get('required_text', []))
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()
flair_id = entry.get('flair_id', '').strip()
required_text = entry.get('required_text', [])
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
self.tag_flair_ids[tag] = flair_id
self.tag_required_text[tag] = required_text
# Parse ignore_tags
self.ignore_tags = set()
for entry in config.get('ignore_tags', []):
tag_val = entry.get('tag', '').strip().lower() if isinstance(entry, dict) else str(entry).strip().lower()
if tag_val:
self.ignore_tags.add(tag_val)
self._last_config_error_revision = None
return True
@@ -244,30 +337,127 @@ class ModReplyBot:
threading.Thread(target=mod_report_watcher, daemon=True).start()
threading.Thread(target=self.chat_message_watcher, daemon=True).start()
def tag_post_watcher():
while True:
def backfill_recent_posts():
"""Scan recent posts from last 24 hours that bot may have missed while offline."""
print("[BACKFILL] Starting backfill of recent posts (last 24 hours)...")
import time
current_time = time.time()
one_day_ago = current_time - (24 * 3600) # 24 hours in seconds
backfill_count = 0
try:
for submission in self.subreddit.stream.submissions(skip_existing=True):
flair = (submission.link_flair_text or '').strip().lower()
# Scan new posts, stopping when we hit posts older than 24 hours
for submission in self.subreddit.new(limit=1000):
if submission.created_utc < one_day_ago:
print(f"[BACKFILL] Reached posts older than 24 hours, stopping backfill.")
break
if submission.id in self.commented_posts:
continue # Already processed
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}")
print(f"[BACKFILL] Checking post {submission.id}: title='{title}', flair='{flair}', title_tags={title_tags_lower}")
# Ignore if any ignore_tag matches
ignore = False
if flair in self.ignore_tags:
ignore = True
else:
for tag in title_tags_lower:
if tag in self.ignore_tags:
ignore = True
break
if ignore:
print(f"[BACKFILL] Ignoring post {submission.id} due to ignore tag.")
continue
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, '')
# Check if bot has already commented this exact text on the post
try:
submission.comments.replace_more(limit=0)
bot_already_commented = False
for comment in submission.comments.list():
if comment.author and comment.author.name == self.reddit.user.me().name:
if comment.body == comment_text:
bot_already_commented = True
print(f"[BACKFILL] Bot already posted this comment on {submission.id}, skipping.")
break
if bot_already_commented:
self.save_commented_post(submission.id)
continue
except Exception as e:
print(f"[BACKFILL] Error checking existing comments on {submission.id}: {e}")
if flair_id:
try:
submission.flair.select(flair_id)
print(f"[BACKFILL] Set flair '{flair_id}' for post {submission.id}")
except Exception as e:
print(f"[BACKFILL] Error setting flair '{flair_id}' for post {submission.id}: {e}")
print(f"[BACKFILL] Auto-commenting on post {submission.id} with tag '{matched_tag}'")
self.comment_only(submission, comment_text, matched_tag)
backfill_count += 1
except Exception as e:
print(f"[BACKFILL] Error during backfill: {e}")
print(f"[BACKFILL] Backfill complete. Commented on {backfill_count} posts, switching to stream mode.")
def tag_post_watcher():
print("[TAG WATCH] Starting tag post watcher thread...")
attempt = 0
while True:
try:
attempt += 1
print(f"[TAG WATCH] Attempt {attempt}: Waiting for new submissions...")
for submission in self.subreddit.stream.submissions(skip_existing=True):
print(f"[TAG WATCH] GOT SUBMISSION: {submission.id}")
flair = (getattr(submission, 'link_flair_text', '') or '').strip().lower()
title = submission.title.strip()
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}")
# Ignore if any ignore_tag matches flair or title tag
ignore = False
if flair in self.ignore_tags:
ignore = True
else:
for tag in title_tags_lower:
if tag in self.ignore_tags:
ignore = True
break
if ignore:
print(f"[TAG WATCH] Ignoring post {submission.id} due to ignore tag.")
continue
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
# 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, '')
@@ -278,13 +468,79 @@ class ModReplyBot:
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)
self.comment_only(submission, comment_text, matched_tag)
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, matched_tag)
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()
# Start backfill thread first so it catches posts from when bot was offline
threading.Thread(target=backfill_recent_posts, daemon=True).start()
# Keep main thread alive
while True:
@@ -373,6 +629,13 @@ class ModReplyBot:
print(f"[DEBUG] Locked post {submission.id}")
except Exception as e:
print(f"[DEBUG] Error locking post {submission.id}: {e}")
# Check required text for triggers
if trigger_idx < len(self.trigger_required_text):
comment_text = self.check_required_text_and_prepend_message(
submission, comment_text, self.trigger_required_text[trigger_idx], self.triggers[trigger_idx]
)
if status == 'enabled':
print(f"[DEBUG] Submission object: {submission}, ID: {submission.id}, Type: {type(submission)}")
comment = submission.reply(comment_text)
@@ -424,4 +687,6 @@ class ModReplyBot:
if __name__ == "__main__":
bot = ModReplyBot()
# Start update checker thread
start_update_checker(bot.reddit, Config.SUBREDDIT, BOT_NAME, BOT_VERSION)
bot.run()
+66
View File
@@ -0,0 +1,66 @@
triggers:
- trigger: WC
status: enabled # enabled, disabled, log-only (Default: enabled)
flair_id: 924d06f8-4409-11eb-a08c-0ed3f1d75325
stickied: true
lock_comment: true
comment: |
__[We're looking for another moderator.](https://sh.reddit.com/r/MinecraftHelp/comments/1rktdzl/meta_were_still_looking_for_another_moderator/)__
Helpers, remember that ___all___ __top-level__ comments must be a genuine, good faith attempt to help OP. Comments breaking this [rule](https://sh.reddit.com/r/MinecraftHelp/wiki/rules/#wiki_5._commenting_rules) will be removed, and bans issued[.](https://reddit.com/u/{{author}}/)
__Links:__
[How to mark solved](https://www.reddit.com/r/MinecraftHelp/wiki/rules/#wiki_7._points_sytem_rules) || [How to delete your post](https://reddit.com/r/MinecraftHelp/wiki/faq#wiki_how_do_i_delete_a_post.2C_without_breaking_rule_7.3F) || [FAQ](https://reddit.com/r/MinecraftHelp/wiki/faq) || [Rules](https://reddit.com/r/MinecraftHelp/wiki/rules)
- trigger: Lock
status: enabled # enabled, disabled, log-only (Default: enabled)
stickied: true
lock_post: true
lock_comment: false
comment: |
# __POST LOCKED__
TESTING
- trigger: TEST2
status: enabled # enabled, disabled, log-only (Default: enabled)
comment: |
___TEST "___
post_tags:
- tag: Bedrock, Java, Launcher, Legacy
status: enabled # enabled, disabled, log-only (Default: enabled)
flair_id: 924d06f8-4409-11eb-a08c-0ed3f1d75325
comment: |
__[Click here if your post says "Sorry, this post was removed by Reddits filters"](https://sh.reddit.com/r/MinecraftHelp/wiki/faq/#wiki_why_are_my_posts.2Fcomments_not_showing_on_the_sub_straight_away.3F)[.](https://reddit.com/u/{{author}}/)__
__[We're looking for another moderator.](https://sh.reddit.com/r/MinecraftHelp/comments/1rktdzl/meta_were_still_looking_for_another_moderator/)__
Helpers, remember that ___all___ __top-level__ comments must be a genuine, good faith attempt to help OP. Comments breaking this [rule](https://sh.reddit.com/r/MinecraftHelp/wiki/rules/#wiki_5._commenting_rules) will be removed, and bans issued.
__Links:__
[How to mark solved](https://www.reddit.com/r/MinecraftHelp/wiki/rules/#wiki_7._points_sytem_rules) || [How to delete your post](https://reddit.com/r/MinecraftHelp/wiki/faq#wiki_how_do_i_delete_a_post.2C_without_breaking_rule_7.3F) || [FAQ](https://reddit.com/r/MinecraftHelp/wiki/faq) || [Rules](https://reddit.com/r/MinecraftHelp/wiki/rules)
required_text:
- text: Xbox, Nintendo, switch, Windows 10, PS4, PlayStation, Win10, PS5, Series X, Series S, android, iphone, ipad, ios, PC, computer, Gear VR, Fire, Samsung, Amazon Tablet, huawei, PSVR, surface pro, Google pixel, Chromebook, Chrome, x-box, Windows, win 10, play station, steam deck, steamdeck, steam-deck
tag: Bedrock
message: |
__It looks like you didn't mention your platform (e.g., Xbox, PlayStation, Switch, etc.). Please clarify for better help!__
search_in: title_and_body
- text: 26.1, 1.21.11, 1.21.10, 1.21.9, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4,1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.1, 1.18, 1.17.1, 1.17, 1.16.5, 1.16.4, 1.16.2, 1.16.1, 1.16, 1.15.2, 1.15.1, 1.15, 1.14.4, 1.14.3, 1.14.2, 1.14.1, 1.14, 1.13.2, 1.13.1, 1.13, 1.12.2, 1.12.1, 1.12, 1.11.2, 1.11.1, 1.11, 1.10.2, 1.10.1, 1.10, 1.9.4, 1.9.3, 1.9.2, 1.9.1, 1.9, 1.8.9, 1.8.8, 1.8.8, 1.8.7, 1.8.6, 1.8.5, 1.8.4, 1.8.3, 1.8.2, 1.8.1, 1.8, 1.7.10, 1.7.9, 1.7.8, 1.7.7, 1.7.6, 1.7.5, 1.7.4, 1.7.2, 1.6.4, 1.6.2, 1.6.1, 1.5.2, 1.5.1, 1.5, 1.4.7, 1.4.6, 1.4.5, 1.4.4, 1.4.2, 1.3.2, 1.3.1, 1.2.5, 1.2.4, 1.2.3, 1.2.2, 1.2.1, 1.1, 1.0, a1.2.6, a1.2.5, a1.2.4_01, a1.2.3_04, a1.2.3_02, a1.2.3_01, a1.2.3,a .2.2b, a1.2.2a, a1.2.1_01, a1.2.1, a1.2.0_02, a1.2.0_01, a1.2.0, a1.1.2_01, a1.1.2, a1.1.0, a1.0.17_04, a1.0.17_02, a1.0.16, a1.0.15, a1.0.14, a1.0.11, a1.0.5_01, a1.0.4, inf-20100618, c0.30_01c, c0.0.13a, c0.0.13a_03, c0.0.11a, rd-161348, rd-160052, rd-20090515, rd-132328, rd-132211, b1.8.1, b1.8, b1.7.3, b1.7.2, b1.7, b1.6.6, b1.6.5, b1.6.4, b1.6.3, b1.6.2, b1.6.1, b1.6, b1.6-tb3, b1.5_01, b1.5, b1.4_01, b1.4, b1.3_01, b1.3b, b1.2_02, b1.2_01, b1.2, b1.1_02, b1.1_01, b1.0.2, b1.0_01, b1.0, "beta 1", "alpha 1", "alpha v1", 20W45A, 20W46A, 20W48A, 20W49A, 20W51A, 21W05A, 21W05B, 21W06A, 21W07A, 21W08A, 21W08B, 21W10A, 21W11A, 21W13A, 21W14A, 21W15A, 21W16A, 21W17A, 21W18A, 21w19a, 21W20A, 25w02a, 25w09a, 25w09b, pre-classic, classic, indev, infdev, "april fool", 2.0, 15w14a, "love and hugs", 1.RV-Pre1, "trendy update", "3d shareware", "20w14", 22w13oneBlockAtATime, "one block at a time", "23w13a_or_b", "vote update", 24w14potato, "potato update", 25w14craftmine, "craftmine update", "combat test", "multiplayer test", "survival test", "halloween update", "adventure update", "pretty scary update", "redstone update", "horse update", "changed the world", "bountiful update", "combat update", "frostburn update", "exploration update", "world of color", "update aquatic", "aquatic update", "pillage update", "village and pillage", "village & pillage", "buzzy bees", "nether update", "cliffs update", "wild update", "tales update", "trails update", "bats and pots", "armored paws", "tricky trials", "bundles of bravery", "garden awakens", "spring drop", "spring to life", "summer drop", "Chase the Skies", "fall drop", "Copper Age", "Mounts of Mayhem", "Tiny Takeover"
tag: Java
message: |
__It looks like you didn't mention your version (e.g., 26.1, 1.21.11, 1.12.2 etc.). Please clarify for better help!__
search_in: title_and_body
- text: Java, Bedrock, Dungeons, Legends, Account
tag: Launcher
message: |
__It looks like you didn't mention the game you're trying to play (e.g., Bedrock, Java etc.). Please clarify for better help!__
search_in: title_and_body
ignore_tags:
- tag: PSA
+100
View File
@@ -0,0 +1,100 @@
import requests
import threading
import time
import os
from datetime import datetime
UPDATE_CHECK_INTERVAL = 3600 # Check every hour
UPDATE_COOLDOWN = 86400 # Don't send mail more than once per 24 hours
LAST_UPDATE_FILE = os.path.join(os.path.dirname(__file__), 'DB', '.last_update_check.txt')
def get_latest_version(bot_name):
"""Fetch latest version info from update server."""
try:
response = requests.get(f'https://updts.slfhstd.uk/api/version/{bot_name}', timeout=10)
if response.status_code == 200:
return response.json()
except Exception as e:
print(f"[UPDATE_CHECKER] Error fetching version: {e}")
return None
def send_update_modmail(reddit, subreddit_name, bot_name, current_version, available_version, changelog_url):
"""Send modmail to subreddit modteam about available update."""
try:
subject = f"🤖 {bot_name} Update Available (v{available_version})"
message = f"""Hello,
An update is available for {bot_name}!
**Current Version:** {current_version}
**Available Version:** {available_version}
Changelog: {changelog_url}
Please visit the update server for installation instructions.
---
This is an automated message from the Update Checker."""
data = {
"subject": subject,
"text": message,
"to": f"/r/{subreddit_name}",
}
reddit.post("api/compose/", data=data)
print(f"[UPDATE_CHECKER] Sent update notification to r/{subreddit_name}")
return True
except Exception as e:
print(f"[UPDATE_CHECKER] Error sending modmail: {e}")
return False
def should_send_update_mail():
"""Check if enough time has passed since last update mail."""
if not os.path.exists(LAST_UPDATE_FILE):
return True
try:
with open(LAST_UPDATE_FILE, 'r') as f:
last_check = float(f.read().strip())
return (time.time() - last_check) >= UPDATE_COOLDOWN
except:
return True
def mark_update_mailed():
"""Record when update mail was sent."""
os.makedirs(os.path.dirname(LAST_UPDATE_FILE), exist_ok=True)
with open(LAST_UPDATE_FILE, 'w') as f:
f.write(str(time.time()))
def update_checker_thread(reddit, subreddit_name, bot_name, current_version):
"""Background thread that checks for updates periodically."""
print(f"[UPDATE_CHECKER] Started for {bot_name} v{current_version}")
while True:
try:
latest = get_latest_version(bot_name)
if latest:
available_version = latest.get('version')
changelog_url = latest.get('changelog_url', '')
if available_version and available_version != current_version:
if should_send_update_mail():
sent = send_update_modmail(
reddit, subreddit_name, bot_name, current_version, available_version, changelog_url
)
if sent:
mark_update_mailed()
else:
print(f"[UPDATE_CHECKER] No version info received.")
except Exception as e:
print(f"[UPDATE_CHECKER] Error in update checker thread: {e}")
time.sleep(UPDATE_CHECK_INTERVAL)
def start_update_checker(reddit, subreddit_name, bot_name, current_version):
threading.Thread(
target=update_checker_thread,
args=(reddit, subreddit_name, bot_name, current_version),
daemon=True
).start()