12 Commits

Author SHA1 Message Date
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
slfhstd 00cebd44dc added changelog 2026-03-10 22:34:21 +00:00
slfhstd 56acc187a9 V2 release 2026-03-10 22:29:35 +00:00
10 changed files with 649 additions and 101 deletions
+2
View File
@@ -1,2 +1,4 @@
.env .env
config/* config/*
tests.py
message.md
+91
View File
@@ -0,0 +1,91 @@
# Changelog
## [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
- **Chat-Based Config Reload:**
- Bot reloads wiki config when a moderator sends a chat message containing `reload-config`.
- Bot replies to chat messages indicating config validity.
- Chat message IDs are tracked in `/DB/chat_wiki_requests.txt` for persistence across restarts.
- **Configurable Actions for Triggers and Tags:**
- Added support for optional `flair_id`, `stickied`, `lock_post`, and `lock_comment` in both triggers and post_tags.
- Bot can set flair, sticky comments, lock posts/comments as specified in wiki config.
- **Persistent Tracking:**
- Auto-commented posts and processed chat requests are tracked in `/DB/commented_posts.txt` and `/DB/chat_wiki_requests.txt`.
### Code & Documentation Updates
- **modreplybot.py:**
- Refactored to remove all modmail notification code.
- All config reloads and error notifications are now handled via chat messages.
- Improved error handling and logging.
- Updated logic for flair/tag actions and chat message processing.
- **README.md & ModGuide.md:**
- Updated to reflect chat-based config reload, new config options, and persistent tracking.
- Added detailed examples for triggers and post_tags with new fields.
- **DB Folder:**
- Added `/DB/chat_wiki_requests.txt` for persistent chat request tracking.
- Updated `/DB/commented_posts.txt` for improved tracking.
### Other Changes
- **Removed:**
- All modmail-based notifications and config reloads.
- Legacy approval logic and unnecessary config options.
+44
View File
@@ -0,0 +1,44 @@
3cmp3o4
3fx1qfn
3g7v9uc
3jeabq9
3wskhjw
3x39w4t
3yuu0aa
3zzls88
3zzlt5d
3zzlvdb
3zzm08q
402nk2v
4abxjnc
4abxt0q
4abxurh
4dl3vu7
4lq8osg
4n5ukvi
4n5ullm
4uvaecx
4v59c9t
4z0edd4
51digym
52yjb3r
52yjj84
o8428zh
o844qn1
o844sou
o846cxy
o846f5z
5hmwzis
5ju7six
5ju8grz
5ju8ve3
5jubgcs
5judtbe
5jugfpj
5jv66nh
5jvi6wk
5jvji2r
o9wezfi
o9wf1xl
o9x1jh0
o9x1lyh
+72 -27
View File
@@ -1,27 +1,72 @@
1roi8zs 1rq7d74
1roi5cl 1rq7d8m
1roi245 1rq7jgx
1roi088 1rq7jif
1rlvzus 1rq7t4b
1rlvufz 1rq7t2q
1rlvqyo 1rq7yfy
1rlvfof 1rq7yes
1rlpiwm 1rq857k
1rlpivb 1rq856a
1rpysqt 1rq7p24
1rpyvk2 1rq8h7x
1rpz5j5 1rq8h62
1rpzgxq 1rq8h62
1rq014r 1rq856a
1rpyv9d 1rq8q3r
1rpxb6z 1rq8q2l
1rpsp6z 1rq8q2l
1rpsgbi 1rq8q2l
1rps6ab 1rq8xxd
1rprg5y 1rq8xwd
1rpqfb7 1rq94gj
1rpju64 1rq94ex
1rpir3o 1rq97u4
1rq2nve 1rq97st
1rq2nwm 1rq9dmi
1rq2nwm 1rq9dlb
1rq9lze
1rq9lxo
1rq9pgz
1rq9pfg
1rq9ubo
1rq9uaq
1rq9z7v
1rq9z6p
1rqa30g
1rqa2yy
1rqa6th
1rqa6rz
1rqaawn
1rqaerv
1rqaeq6
1rqaeq6
1rqaeq6
1rqaerv
1rqb476
1rqbf64
1rqb45z
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 RUN pip install --no-cache-dir -r requirements.txt
COPY config.py . COPY config.py .
COPY modreplybot.py . COPY modreplybot.py .
COPY update_checker.py .
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
CMD ["python", "modreplybot.py"] CMD ["python", "modreplybot.py"]
+26 -5
View File
@@ -1,3 +1,4 @@
# ModReplyBot Moderator Guide # ModReplyBot Moderator Guide
## Wiki Configuration Page ## Wiki Configuration Page
@@ -12,7 +13,7 @@ https://old.reddit.com/r/<your_subreddit>/wiki/<wiki_page>
## Triggers ## 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`). 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: `!test`).
### Example Trigger Configuration ### Example Trigger Configuration
``` ```
@@ -22,6 +23,10 @@ triggers:
Thank you for your report! Thank you for your report!
This post is now approved. This post is now approved.
status: enabled status: enabled
flair_id: 12345678-aaaa-bbbb-cccc-1234567890ab
stickied: true
lock_post: false
lock_comment: false
- trigger: wc - trigger: wc
comment: | comment: |
Welcome to the community! Welcome to the community!
@@ -31,9 +36,13 @@ triggers:
- **trigger**: The phrase (without the `!`) that mods use in comments or report reasons. - **trigger**: The phrase (without the `!`) that mods use in comments or report reasons.
- **comment**: The text the bot will post as a stickied comment. - **comment**: The text the bot will post as a stickied comment.
- **status**: - **status**:
- `enabled`: Bot will approve the post and comment. - `enabled`: Bot will comment and perform actions.
- `log-only`: Bot will approve the post but not comment. - `log-only`: Bot will log but not comment.
- `disabled`: Bot will not act on this trigger. - `disabled`: Bot will not act on this trigger.
- **flair_id**: Optional. Set post flair by ID.
- **stickied**: Optional. Sticky the bot's comment.
- **lock_post**: Optional. Lock the post.
- **lock_comment**: Optional. Lock the bot's comment.
## Auto-Post Tags ## Auto-Post Tags
@@ -46,6 +55,7 @@ post_tags:
comment: | comment: |
__[Click here if your post says "Sorry, this post was removed by Reddits filters"](...)__ __[Click here if your post says "Sorry, this post was removed by Reddits filters"](...)__
status: enabled status: enabled
flair_id: 12345678-aaaa-bbbb-cccc-1234567890ab
``` ```
- **tag**: Comma-separated list of tags. The bot matches tags in post titles (case-insensitive). - **tag**: Comma-separated list of tags. The bot matches tags in post titles (case-insensitive).
@@ -54,17 +64,23 @@ post_tags:
- `enabled`: Bot will comment automatically. - `enabled`: Bot will comment automatically.
- `log-only`: Bot will log but not comment. - `log-only`: Bot will log but not comment.
- `disabled`: Bot will not act on this tag. - `disabled`: Bot will not act on this tag.
- **flair_id**: Optional. Set post flair by ID.
## 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.
## Additional Notes ## Additional Notes
- The bot only comments once per trigger per post (even if triggered multiple times). - 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. - The bot only auto-comments once per post for each tag.
- All bot actions are logged for transparency. - All bot actions are logged for transparency.
- If the wiki config is invalid, the bot will notify mods via modmail and pause until fixed. - If the wiki config is invalid, the bot will reply to the chat message with an error.
## Troubleshooting ## Troubleshooting
- Make sure your wiki config is valid YAML and includes both `triggers` and `post_tags` sections. - 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. - Use old.reddit.com for wiki editing to avoid formatting issues.
- Check bot logs for errors and modmail for config issues. - Check bot logs for errors and chat replies for config issues.
## Example Wiki Config Excerpt ## Example Wiki Config Excerpt
``` ```
@@ -74,9 +90,14 @@ triggers:
Thank you for your report! Thank you for your report!
This post is now approved. This post is now approved.
status: enabled status: enabled
flair_id: 12345678-aaaa-bbbb-cccc-1234567890ab
stickied: true
lock_post: false
lock_comment: false
post_tags: post_tags:
- tag: Bedrock, Java - tag: Bedrock, Java
comment: | comment: |
__[Click here if your post says "Sorry, this post was removed by Reddits filters"](...)__ __[Click here if your post says "Sorry, this post was removed by Reddits filters"](...)__
status: enabled status: enabled
flair_id: 12345678-aaaa-bbbb-cccc-1234567890ab
``` ```
+23 -10
View File
@@ -15,16 +15,21 @@ ModReplyBot is a Reddit bot for moderators that automates post approval and stic
## Configuration ## Configuration
### 1. Wiki Page Configuration ### 1. Wiki Page Configuration
Edit your subreddit wiki page (name set by `REDDIT_WIKI_PAGE` env variable) with YAML like: Edit your subreddit wiki page (name set by `REDDIT_WIKI_PAGE` env variable) with YAML like:
``` ```yaml
triggers: triggers:
- trigger: help - trigger: help
comment: | comment: |
Thank you for your report! Thank you for your report!
This post is now approved. This post is now approved.
status: enabled status: enabled
flair_id: 12345678-aaaa-bbbb-cccc-1234567890ab
stickied: true
lock_post: false
lock_comment: false
- trigger: wc - trigger: wc
comment: | comment: |
Welcome to the community! Welcome to the community!
@@ -35,16 +40,24 @@ post_tags:
comment: | comment: |
__[Click here if your post says "Sorry, this post was removed by Reddits filters"](...)__ __[Click here if your post says "Sorry, this post was removed by Reddits filters"](...)__
status: enabled status: enabled
flair_id: 12345678-aaaa-bbbb-cccc-1234567890ab
# New: Ignore tags
ignore_tags:
- tag: Off-Topic
- tag: Meme
``` ```
- Triggers: Bot responds to mod comments and mod reports 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 and actions.
- post_tags: Bot posts the comment automatically on new posts with matching tags in the title.
- 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`. - Status options: `enabled`, `log-only`, `disabled`.
- Optional actions: `flair_id`, `stickied`, `lock_post`, `lock_comment`.
### 2. Environment Variables ### 2. Environment Variables
Create a `.env` file (or set env variables directly) with: Create a `.env` file (or set env variables directly) with:
``` ```
REDDIT_CLIENT_ID=your_client_id REDDIT_CLIENT_ID=your_client_id
REDDIT_CLIENT_SECRET=your_client_secret REDDIT_CLIENT_SECRET=your_client_secret
@@ -56,10 +69,6 @@ REDDIT_WIKI_PAGE=modreplybot-config
LOG_LEVEL=Default 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 ## Installation
### Docker Compose (Recommended) ### Docker Compose (Recommended)
@@ -95,14 +104,18 @@ pip install -r requirements.txt
python modreplybot.py 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 ## Troubleshooting
- Ensure your Reddit credentials are correct and have moderator permissions. - Ensure your Reddit credentials are correct and have moderator permissions.
- The bot must be able to read the wiki page and approve posts. - The bot must be able to read the wiki page and approve posts.
- Check logs for errors. - Check logs for errors.
- The bot only responds to mod comments and mod reports 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. - 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 notify mods via modmail and pause until fixed. - If the wiki config is invalid, the bot will reply to the chat message with an error.
## Moderator Guide ## Moderator Guide
See `ModGuide.md` for a detailed guide to configuring triggers, auto-post tags, and wiki options. See `ModGuide.md` for a detailed guide to configuring triggers, auto-post tags, and wiki options.
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
modreplybot: modreplybot:
image: slfhstd.uk/slfhstd/modreplybot:latest image: slfhstd.uk/slfhstd/modreplybot:dev
container_name: modreplybot container_name: modreplybot
env_file: env_file:
- .env - .env
+288 -57
View File
@@ -1,10 +1,60 @@
import os import os
import praw import praw
from config import get_reddit, Config from config import get_reddit, Config
from update_checker import start_update_checker
BOT_VERSION = "2.2.0" # Change this for new releases
BOT_NAME = "ModReplyBot" # Change this if bot name changes
import time import time
class ModReplyBot: class ModReplyBot:
def chat_message_watcher(self):
chat_requests_file = os.path.join(os.path.dirname(__file__), 'DB', 'chat_wiki_requests.txt')
processed_message_ids = set()
# Load processed IDs from file
if os.path.exists(chat_requests_file):
with open(chat_requests_file, 'r', encoding='utf-8') as f:
for line in f:
processed_message_ids.add(line.strip())
while True:
try:
for message in self.reddit.inbox.stream():
if not hasattr(message, 'id') or message.id in processed_message_ids:
continue
processed_message_ids.add(message.id)
# Save processed ID to file
with open(chat_requests_file, 'a', encoding='utf-8') as f:
f.write(message.id + '\n')
if hasattr(message, 'body') and 'reload-config' in message.body.lower():
# Check if sender is a moderator
author = getattr(message, 'author', None)
if author and author in self.subreddit.moderator():
print(f"[CHAT WATCH] Moderator '{author}' requested config reload.")
result = self.fetch_yaml_config()
if result:
print("[CHAT WATCH] Wiki config reloaded successfully.")
reply_text = "Config reloaded successfully. Config is valid."
else:
print("[CHAT WATCH] Wiki config reload failed.")
reply_text = "Config reload failed. Config is invalid."
try:
message.reply(reply_text)
print(f"[CHAT WATCH] Replied to chat message {message.id}.")
except Exception as e:
print(f"[CHAT WATCH] Error replying to chat message {message.id}: {e}")
except Exception as e:
print(f"Chat message watcher error: {e}")
import time
time.sleep(30)
def comment_only(self, submission, comment_text):
try:
comment = submission.reply(comment_text)
comment.mod.distinguish(sticky=True)
print(f"Commented (no approval) on: {submission.id}")
self.save_commented_post(submission.id)
except Exception as e:
print(f"Error commenting (no approval): {e}")
def __init__(self): def __init__(self):
import os import os
self.reddit = get_reddit() self.reddit = get_reddit()
@@ -14,6 +64,11 @@ class ModReplyBot:
self.comments = [] self.comments = []
self.commented_posts = set() self.commented_posts = set()
self.commented_posts_file = os.path.join(os.path.dirname(__file__), 'DB', 'commented_posts.txt') 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')
self.tagged_commented_posts = set()
self._wiki_config_cache = None
self._wiki_config_cache_time = 0
self._wiki_config_cache_ttl = 300 # seconds (5 minutes)
self.ensure_config_file() self.ensure_config_file()
self.load_commented_posts() self.load_commented_posts()
self.log_level = Config.LOG_LEVEL self.log_level = Config.LOG_LEVEL
@@ -44,43 +99,70 @@ class ModReplyBot:
# Track last error revision to prevent modmail spam # Track last error revision to prevent modmail spam
if not hasattr(self, '_last_config_error_revision'): if not hasattr(self, '_last_config_error_revision'):
self._last_config_error_revision = None self._last_config_error_revision = None
import yaml import yaml, time
try: now = time.time()
wiki_page = Config.WIKI_PAGE # Use cache if not expired
wiki = self.subreddit.wiki[wiki_page] if self._wiki_config_cache and (now - self._wiki_config_cache_time < self._wiki_config_cache_ttl):
wiki_content = wiki.content_md config = self._wiki_config_cache
self._wiki_revision_id = getattr(wiki, 'revision_id', None) self._wiki_revision_id = getattr(self, '_wiki_revision_id', None)
config = yaml.safe_load(wiki_content) else:
if not isinstance(config, dict) or 'triggers' not in config: try:
raise ValueError("Wiki config missing required 'triggers' key or is not a dict.") wiki_page = Config.WIKI_PAGE
self.triggers = [] wiki = self.subreddit.wiki[wiki_page]
self.comments = [] wiki_content = wiki.content_md
self.statuses = [] self._wiki_revision_id = getattr(wiki, 'revision_id', None)
self.tag_comments = {} config = yaml.safe_load(wiki_content)
self.tag_statuses = {} self._wiki_config_cache = config
for entry in config.get('triggers', []): self._wiki_config_cache_time = now
self.triggers.append(entry.get('trigger', '').strip()) except Exception as e:
self.comments.append(entry.get('comment', '').strip()) self.log(f"Error fetching YAML config from wiki: {e}")
self.statuses.append(entry.get('status', 'enabled').strip().lower()) revision = getattr(self, '_wiki_revision_id', None)
for entry in config.get('post_tags', []): if revision != self._last_config_error_revision:
tags_str = entry.get('tag', '').strip() self._last_config_error_revision = revision
comment = entry.get('comment', '').strip() return False
status = entry.get('status', 'enabled').strip().lower() if not isinstance(config, dict) or 'triggers' not in config:
tags = [t.strip().lower() for t in tags_str.split(',') if t.strip()] self.log("Wiki config missing required 'triggers' key or is not a dict.")
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:
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 return False
self.triggers = []
self.comments = []
self.statuses = []
self.flair_ids = []
self.stickied = []
self.lock_post = []
self.lock_comment = []
self.tag_comments = {}
self.tag_statuses = {}
self.tag_flair_ids = {}
for entry in config.get('triggers', []):
self.triggers.append(entry.get('trigger', '').strip())
self.comments.append(entry.get('comment', '').strip())
self.statuses.append(entry.get('status', 'enabled').strip().lower())
self.flair_ids.append(entry.get('flair_id', '').strip())
self.stickied.append(bool(entry.get('stickied', False)))
# Parse lock_post as a proper boolean
lock_post_val = entry.get('lock_post', False)
if isinstance(lock_post_val, str):
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)))
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()
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
# 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
def notify_mods_config_error(self, error_message): def notify_mods_config_error(self, error_message):
try: try:
@@ -133,11 +215,10 @@ class ModReplyBot:
config_ok = self.fetch_yaml_config() config_ok = self.fetch_yaml_config()
new_revision = getattr(self, '_wiki_revision_id', None) new_revision = getattr(self, '_wiki_revision_id', None)
if old_revision and new_revision and old_revision != new_revision: if old_revision and new_revision and old_revision != new_revision:
if config_ok: if config_ok:
self.log("Wiki config changed, reloading triggers and tag comments.") self.log("Wiki config changed, reloading triggers and tag comments.")
self.notify_mods_config_change(new_revision) else:
else: self.log("Wiki config error detected, not reloading bot.")
self.log("Wiki config error detected, not reloading bot.")
self.log_level = Config.LOG_LEVEL self.log_level = Config.LOG_LEVEL
last_revision = new_revision last_revision = new_revision
for comment in self.subreddit.stream.comments(skip_existing=True): for comment in self.subreddit.stream.comments(skip_existing=True):
@@ -145,6 +226,8 @@ class ModReplyBot:
self.handle_comment(comment) self.handle_comment(comment)
except Exception as e: except Exception as e:
print(f"Mod comment watcher error: {e}") print(f"Mod comment watcher error: {e}")
import time
time.sleep(5)
def mod_report_watcher(): def mod_report_watcher():
last_revision = None last_revision = None
@@ -169,6 +252,121 @@ class ModReplyBot:
threading.Thread(target=mod_comment_watcher, daemon=True).start() threading.Thread(target=mod_comment_watcher, daemon=True).start()
threading.Thread(target=mod_report_watcher, daemon=True).start() 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:
try:
# Check new submissions
for submission in self.subreddit.stream.submissions(skip_existing=True):
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, '')
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 # Keep main thread alive
while True: while True:
@@ -180,6 +378,7 @@ class ModReplyBot:
matched_trigger = None matched_trigger = None
matched_comment = None matched_comment = None
matched_status = None matched_status = None
matched_idx = None
# Check report reasons for triggers # Check report reasons for triggers
if hasattr(submission, 'mod_reports') and submission.mod_reports: if hasattr(submission, 'mod_reports') and submission.mod_reports:
for report_tuple in submission.mod_reports: for report_tuple in submission.mod_reports:
@@ -190,6 +389,7 @@ class ModReplyBot:
matched_trigger = trigger matched_trigger = trigger
matched_comment = self.comments[idx] matched_comment = self.comments[idx]
matched_status = self.statuses[idx] if idx < len(self.statuses) else 'enabled' matched_status = self.statuses[idx] if idx < len(self.statuses) else 'enabled'
matched_idx = idx
break break
if matched_trigger: if matched_trigger:
break break
@@ -205,18 +405,8 @@ class ModReplyBot:
try: try:
footer = "^I ^am ^a ^bot ^and ^this ^comment ^was ^made ^automatically. ^Message ^the ^Mod ^team ^if ^I'm ^not ^working ^correctly." 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 comment_text = matched_comment.replace("{{author}}", submission.author.name if submission.author else "unknown") + "\n\n" + footer
if matched_status == 'enabled': self.approve_and_comment(submission, comment_text, matched_status, matched_idx)
comment = submission.reply(comment_text) self.triggered_posts.add(trigger_key)
comment.mod.distinguish(sticky=True)
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 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 mod report {submission.id} for trigger [{matched_trigger}] (auto)")
else:
print(f"Unknown status '{matched_status}' for mod report {submission.id} for trigger [{matched_trigger}] (auto)")
except Exception as e: except Exception as e:
print(f"Error commenting on mod report: {e}") print(f"Error commenting on mod report: {e}")
else: else:
@@ -240,15 +430,54 @@ class ModReplyBot:
print(f"Error removing comment: {e}") print(f"Error removing comment: {e}")
submission = comment.submission submission = comment.submission
self.fetch_yaml_config() self.fetch_yaml_config()
self.approve_and_comment(submission, self.comments[idx], status) self.approve_and_comment(submission, self.comments[idx], status, idx)
break break
def approve_and_comment(self, submission, comment_text, status='enabled'): def approve_and_comment(self, submission, comment_text, status='enabled', trigger_idx=None):
try: try:
submission.mod.approve() print(f"[DEBUG] approve_and_comment called with trigger_idx={trigger_idx}")
if trigger_idx is not None:
print(f"[DEBUG] Config for trigger_idx={trigger_idx}: lock_post={self.lock_post[trigger_idx]}, stickied={self.stickied[trigger_idx]}, lock_comment={self.lock_comment[trigger_idx]}, flair_id={self.flair_ids[trigger_idx]}")
# Approve post if configured for this trigger
# Set flair, stickied, lock_post, lock_comment if configured for this trigger
if trigger_idx is not None:
flair_id = self.flair_ids[trigger_idx]
if flair_id:
try:
submission.flair.select(flair_id)
print(f"[DEBUG] Set flair '{flair_id}' for post {submission.id}")
except Exception as e:
print(f"[DEBUG] Error setting flair '{flair_id}' for post {submission.id}: {e}")
print(f"[DEBUG] lock_post[{trigger_idx}] = {self.lock_post[trigger_idx]}")
if self.lock_post[trigger_idx]:
try:
submission.mod.lock()
print(f"[DEBUG] Locked post {submission.id}")
except Exception as e:
print(f"[DEBUG] Error locking post {submission.id}: {e}")
if status == 'enabled': if status == 'enabled':
print(f"[DEBUG] Submission object: {submission}, ID: {submission.id}, Type: {type(submission)}")
comment = submission.reply(comment_text) comment = submission.reply(comment_text)
comment.mod.distinguish(sticky=True) print(f"[DEBUG] Comment object: {comment}, ID: {comment.id}, Type: {type(comment)}")
# Always sticky if stickied is True, match tag logic
if trigger_idx is not None and self.stickied[trigger_idx]:
try:
result = comment.mod.distinguish(sticky=True)
print(f"[DEBUG] Stickied bot comment {comment.id} on post {submission.id}, result: {result}")
except Exception as e:
print(f"[DEBUG] Error stickying comment {comment.id} on post {submission.id}: {e}")
else:
try:
result = comment.mod.distinguish()
print(f"[DEBUG] Distinguished bot comment {comment.id} on post {submission.id}, result: {result}")
except Exception as e:
print(f"[DEBUG] Error distinguishing comment {comment.id} on post {submission.id}: {e}")
if trigger_idx is not None and self.lock_comment[trigger_idx]:
try:
comment.mod.lock()
print(f"[DEBUG] Locked bot comment {comment.id} on post {submission.id}")
except Exception as e:
print(f"[DEBUG] Error locking comment {comment.id} on post {submission.id}: {e}")
print(f"Approved and commented on: {submission.id}") print(f"Approved and commented on: {submission.id}")
self.save_commented_post(submission.id) self.save_commented_post(submission.id)
elif status == 'log-only': elif status == 'log-only':
@@ -277,4 +506,6 @@ class ModReplyBot:
if __name__ == "__main__": if __name__ == "__main__":
bot = ModReplyBot() bot = ModReplyBot()
# Start update checker thread
start_update_checker(bot.reddit, Config.SUBREDDIT, BOT_NAME, BOT_VERSION)
bot.run() bot.run()
+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()