2 Commits

Author SHA1 Message Date
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
6 changed files with 283 additions and 7 deletions
+1
View File
@@ -2,3 +2,4 @@
config/*
tests.py
message.md
docker/
+32
View File
@@ -1,6 +1,38 @@
# Changelog
## [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
Binary file not shown.
+2 -2
View File
@@ -5,6 +5,6 @@ services:
env_file:
- .env
volumes:
- ./config:/app/config
- ./DB:/app/DB
- ./docker/config:/app/config
- ./docker/DB:/app/DB
restart: unless-stopped
+182 -5
View File
@@ -3,7 +3,7 @@ import praw
from config import get_reddit, Config
from update_checker import start_update_checker
BOT_VERSION = "2.2.0" # Change this for new releases
BOT_VERSION = "2.2.2" # Change this for new releases
BOT_NAME = "ModReplyBot" # Change this if bot name changes
import time
@@ -47,8 +47,14 @@ 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:
# 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}")
@@ -62,6 +68,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')
@@ -77,6 +93,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):
@@ -130,9 +204,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())
@@ -145,16 +221,19 @@ 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', []):
@@ -254,11 +333,99 @@ class ModReplyBot:
threading.Thread(target=mod_report_watcher, daemon=True).start()
threading.Thread(target=self.chat_message_watcher, daemon=True).start()
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:
# 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()
import re
title_tags = re.findall(r'\[(.*?)\]', title)
title_tags_lower = [t.strip().lower() for t in title_tags]
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
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
if matched_tag:
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:
# Check new submissions
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
@@ -297,7 +464,7 @@ 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
@@ -359,7 +526,7 @@ class ModReplyBot:
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)
self.comment_only(submission, comment_text, matched_tag)
except Exception as e:
print(f"Modqueue watcher error: {e}")
import time
@@ -367,6 +534,9 @@ class ModReplyBot:
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:
@@ -455,6 +625,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)
+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