Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc8060270b | |||
| 476eadd6c9 | |||
| 8fe3d1f030 | |||
| 6d2d04ea40 | |||
| f4faabe44d | |||
| 87958ba09c | |||
| 592521026a | |||
| a2faff2853 | |||
| 12f62cfb24 | |||
| ca98a3b466 | |||
| af70310743 | |||
| 21cbad7111 | |||
| ca0f165257 | |||
| 00cebd44dc | |||
| 56acc187a9 |
@@ -1,2 +1,5 @@
|
|||||||
.env
|
.env
|
||||||
config/*
|
config/*
|
||||||
|
tests.py
|
||||||
|
message.md
|
||||||
|
docker/
|
||||||
|
|||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
|
||||||
|
# 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
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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 Reddit’s filters"](...)__
|
__[Click here if your post says "Sorry, this post was removed by Reddit’s 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 Reddit’s filters"](...)__
|
__[Click here if your post says "Sorry, this post was removed by Reddit’s filters"](...)__
|
||||||
status: enabled
|
status: enabled
|
||||||
|
flair_id: 12345678-aaaa-bbbb-cccc-1234567890ab
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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 Reddit’s filters"](...)__
|
__[Click here if your post says "Sorry, this post was removed by Reddit’s 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.
|
||||||
|
|||||||
Binary file not shown.
+3
-3
@@ -1,10 +1,10 @@
|
|||||||
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
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:/app/config
|
- ./docker/config:/app/config
|
||||||
- ./DB:/app/DB
|
- ./docker/DB:/app/DB
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
+469
-57
@@ -1,10 +1,70 @@
|
|||||||
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.3" # 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, 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}")
|
||||||
|
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()
|
||||||
@@ -12,8 +72,23 @@ class ModReplyBot:
|
|||||||
self.config_path = os.path.join(os.path.dirname(__file__), 'config', 'config.yaml')
|
self.config_path = os.path.join(os.path.dirname(__file__), 'config', 'config.yaml')
|
||||||
self.triggers = []
|
self.triggers = []
|
||||||
self.comments = []
|
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 = 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
|
||||||
@@ -22,6 +97,64 @@ class ModReplyBot:
|
|||||||
if self.log_level == 'Debug' or (self.log_level == 'Default' and not debug_only):
|
if self.log_level == 'Debug' or (self.log_level == 'Default' and not debug_only):
|
||||||
print(message)
|
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):
|
def ensure_config_file(self):
|
||||||
import os
|
import os
|
||||||
if not os.path.exists(self.config_path):
|
if not os.path.exists(self.config_path):
|
||||||
@@ -44,43 +177,75 @@ 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.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())
|
||||||
|
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)))
|
||||||
|
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
|
||||||
|
|
||||||
def notify_mods_config_error(self, error_message):
|
def notify_mods_config_error(self, error_message):
|
||||||
try:
|
try:
|
||||||
@@ -133,11 +298,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 +309,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 +335,212 @@ 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 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:
|
||||||
|
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, '')
|
||||||
|
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, 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
|
# Keep main thread alive
|
||||||
while True:
|
while True:
|
||||||
@@ -180,6 +552,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 +563,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 +579,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 +604,61 @@ 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}")
|
||||||
|
|
||||||
|
# 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':
|
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 +687,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()
|
||||||
|
|||||||
@@ -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 Reddit’s 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
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user