Compare commits

5 Commits

Author SHA1 Message Date
slfhstd 4f9243ff59 changelog added 2026-03-11 17:29:08 +00:00
slfhstd 36fd5430bc updates 2026-03-11 17:26:02 +00:00
slfhstd 95da1522b6 gitignore 2026-03-11 16:40:55 +00:00
slfhstd b3a2788555 gitignore 2026-03-11 16:39:55 +00:00
slfhstd 1e78138992 Changes to enable always on and trigger emthod. also updated to yaml config structure 2026-03-11 16:36:10 +00:00
10 changed files with 594 additions and 31 deletions
+8
View File
@@ -0,0 +1,8 @@
# Example environment variables for TestPostsBot
REDDIT_CLIENT_ID=vzdZ6l330b_l7UWPoZ7tcQ
REDDIT_CLIENT_SECRET=Zh7gVG1glv8BNxlGFBHU1UAJnDjo9w
REDDIT_USERNAME=MinecraftHelpModBot
REDDIT_PASSWORD=t%FdW9$&CAdhQo6@Zz9v
REDDIT_USER_AGENT=TestPostsBot/0.1 on {REDDIT_USERNAME}
SUBREDDIT=MinecraftHelpModCopy
WIKI_PAGE=testpostsbot_config
+2
View File
@@ -0,0 +1,2 @@
modtestposts_config.yaml
.env
+194
View File
@@ -0,0 +1,194 @@
# Changelog - TestPostsBot V2
## Overview
Complete refactor of TestPostsBot to be a continuously running, trigger-based posting bot that responds to moderator commands via chat messages.
## Major Features Added
### 1. Trigger-Based Posting System
- Bot now runs continuously and listens for chat messages instead of executing once and exiting
- Moderators can trigger posts by sending chat messages containing configured trigger keywords
- Each trigger can post one or multiple posts in sequence
- Posts are made with 2-second delays between submissions to respect rate limiting
### 2. YAML Configuration Format
- **Changed from:** JSON configuration format
- **Changed to:** YAML configuration format for better readability and maintainability
- New config structure supports nested trigger/post relationships
- Example YAML config provided in `example_config.yaml`
### 3. Chat Message Handler
- Added chat message watcher that runs as a background thread
- Listens for messages sent to the bot account by moderators
- Implements special `reload-config` command for validating wiki config without making posts
- Validates that sender is a moderator before processing commands
- Tracks processed message IDs to prevent duplicate processing
### 4. Configuration Validation
- New `validate_config_from_wiki()` function validates YAML format and required structure
- Validates that config has a `posts` key with proper trigger/post structure
- Prevents bot from running with invalid configuration
- `reload-config` command provides feedback on config validity
### 5. Enhanced Logging
- Added prefixed logging for different operations: `[STARTUP]`, `[POSTING]`, `[CHAT WATCH]`
- Detailed debug output showing:
- Messages received and who sent them
- Moderator status verification
- Trigger matching and posting status
- Config validation results
- Error messages with full tracebacks
## File Changes
### New Files
- **example_config.yaml** - Comprehensive example showing trigger and post configuration with multiple scenarios
- **.gitignore** - Added to exclude common files
- **example.env** - Environment configuration template
### Modified Files
#### bot.py
**Old Behavior:**
- Ran once, fetched posts from hardcoded config, made posts, and exited
- No continuous operation
- No trigger system
**New Behavior:**
- Runs continuously in infinite loop
- Spawns chat message watcher as background daemon thread
- Loads triggers from wiki config dynamically
- Only posts when a moderator sends a matching trigger
- Implements `reload-config` special command
- Validates config on startup
- Graceful shutdown on KeyboardInterrupt
**Key Functions:**
- `chat_message_watcher()` - Monitors inbox stream for moderator messages
- `make_posts()` - Posts to subreddit with rate limit delays
- `main()` - Continuous operation loop with thread management
#### config.py
**Old Behavior:**
- Fetched JSON config from wiki
- Simple error handling with fallback empty dict
**New Behavior:**
- Uses `yaml.safe_load()` instead of `json.loads()`
- `fetch_config_from_wiki()` - Fetches and parses YAML config
- `validate_config_from_wiki()` - Validates config format and required keys
- `get_trigger_posts()` - Retrieves posts associated with specific trigger
- Better error messages for YAML parsing failures
#### requirements.txt
**Added:**
- `PyYAML` - Required for YAML config parsing
#### README.md
**Complete Rewrite:**
- Added comprehensive documentation for trigger-based system
- Documented new YAML config format with examples
- Explained how moderators trigger posts
- Added setup instructions for environment variables
- Documented `reload-config` command
- Added Docker and standalone running instructions
- Clarified that only moderators can trigger posts
#### docker-compose.yml
**Updated:**
- Environment variables now leverage .env file
- Updated service configuration for continuous operation
#### Dockerfile
**Updated:**
- Adjusted for continuous operation mode
- Ensures proper signal handling for graceful shutdown
### Configuration Examples
**Old Format (JSON):**
```json
{
"posts": [
{"title": "Test Post 1", "body": "Body for post 1"},
{"title": "Test Post 2", "body": "Body for post 2"}
]
}
```
**New Format (YAML):**
```yaml
posts:
- trigger: "test"
posts:
- title: "Test Post 1"
body: "Body for post 1"
- title: "Test Post 2"
body: "Body for post 2"
- trigger: "weekly-thread"
posts:
- title: "Weekly Thread 1"
body: "Content"
- title: "Weekly Thread 2"
body: "Content"
```
## Operational Changes
### Before (V1)
- Bot runs, posts hardcoded posts, exits
- Single execution cycle
- Config loaded once at startup
- No way to trigger posts without restarting bot
### After (V2)
- Bot runs continuously
- Moderators send chat messages to trigger posts
- Config is fetched fresh for each trigger (allows live updates)
- Special `reload-config` command validates configuration
- Background thread handles message monitoring
- Main thread keeps bot alive indefinitely
## Chat Commands
### Trigger Posts
Send chat message containing trigger keyword (e.g., "modtestposts", "weekly-thread")
- Bot fetches configured posts for that trigger
- Posts them to the subreddit in sequence
- Replies with confirmation of posts made
### Reload Config
Send chat message containing "reload-config"
- Bot validates wiki config YAML format
- Replies with success/failure status
- Useful for verifying config before using triggers
## Technical Improvements
1. **Concurrency:** Uses threading for background message monitoring while keeping main thread alive
2. **Deduplication:** Tracks processed message IDs in `DB/chat_wiki_requests.txt` to prevent duplicate processing
3. **Recovery:** Graceful error handling with continue on failures in message stream
4. **Validation:** Comprehensive config validation before any operations
5. **Logging:** Detailed logging for debugging and monitoring
## Compatibility Notes
- Requires `praw` and `PyYAML` packages
- PRAW version should support `inbox.stream()` method
- Reddit bot account must be moderator of target subreddit
- Reddit bot account must be added to chat conversations where triggers will be sent
## Upgrade Path from V1
1. Update wiki config from JSON to YAML format
2. Restructure config to use triggers (see `example_config.yaml`)
3. Redeploy bot with updated code
4. Send "reload-config" to verify new config works
5. Use trigger keywords to post instead of restarting bot
---
**Version:** 2.0
**Date:** March 11, 2026
**Status:** Production Ready
+1
View File
@@ -5,4 +5,5 @@ COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY bot.py .
COPY config.py .
ENV PYTHONUNBUFFERED=1
CMD ["python", "bot.py"]
+88 -14
View File
@@ -1,24 +1,98 @@
# Reddit TestPostsBot
## Usage
A Reddit bot that makes test posts to your subreddit when triggered via private messages by moderators.
1. Fill in your Reddit API credentials and subreddit name in `bot.py`.
2. Create a wiki page in your subreddit named `testpostsbot_config` with JSON like:
## How It Works
```
{
"posts": [
{"title": "Test Post 1", "body": "Body for post 1"},
{"title": "Test Post 2", "body": "Body for post 2"}
]
}
1. The bot runs continuously and listens for private messages sent to its account
2. When a moderator sends a message containing a trigger keyword, the bot checks the wiki config
3. If the trigger is found and valid, the bot posts the configured posts to the subreddit
4. The bot replies to the moderator confirming success or failure
## Setup
### Environment Variables
Set these variables via environment or directly in `bot.py`:
- `REDDIT_CLIENT_ID`: Your Reddit app's client ID
- `REDDIT_CLIENT_SECRET`: Your Reddit app's client secret
- `REDDIT_USERNAME`: The bot account's username
- `REDDIT_PASSWORD`: The bot account's password
- `REDDIT_USER_AGENT`: (Optional) Custom user agent string
- `SUBREDDIT`: The subreddit to post to (without the /r/)
- `WIKI_PAGE`: (Optional) Wiki page for config (default: `testpostsbot_config`)
### Wiki Configuration
Create a wiki page in your subreddit named `testpostsbot_config` (or set `WIKI_PAGE` env var) with YAML formatted triggers and posts:
```yaml
posts:
- trigger: "summer-schedule"
posts:
- title: "Summer Announcement Post"
body: "This is the summer announcement."
- title: "Summer Rules Update"
body: "New summer rules are now in effect."
- trigger: "test"
posts:
- title: "Test Post"
body: "This is a test post."
- trigger: "weekly-thread"
posts:
- title: "Weekly Discussion Thread"
body: |
This is the weekly discussion thread.
Feel free to discuss anything related to the subreddit.
```
3. Build and run with Docker:
Each trigger can have one or multiple posts. Posts are made in order with a 2-second delay between each to avoid rate limiting.
```
### Triggering Posts
To trigger posts, send a private message to the bot account containing the trigger keyword. For example:
- Message: "Can you run summer-schedule?" → Posts the summer schedule posts
- Message: "Trigger: test" → Posts the test post
- Message: "Please post weekly-thread" → Posts the weekly discussion thread
Only moderators of the subreddit can trigger posts.
## Running the Bot
### Docker
```bash
docker build -t testpostsbot .
docker run --env REDDIT_CLIENT_ID=... --env REDDIT_CLIENT_SECRET=... --env REDDIT_USERNAME=... --env REDDIT_PASSWORD=... --env SUBREDDIT=... testpostsbot
docker run \
--env REDDIT_CLIENT_ID=your_client_id \
--env REDDIT_CLIENT_SECRET=your_client_secret \
--env REDDIT_USERNAME=bot_username \
--env REDDIT_PASSWORD=bot_password \
--env SUBREDDIT=your_subreddit \
testpostsbot
```
Or edit the variables directly in `bot.py` for quick testing.
### Docker Compose
```bash
# Edit docker-compose.yml with your credentials
docker-compose up
```
### Standalone
```bash
pip install -r requirements.txt
python bot.py
```
## Configuration Notes
- The wiki config is validated on startup. If the YAML is malformed, the bot will not start.
- The config is fetched fresh for each trigger, so you can update the wiki while the bot is running.
- Only the first matching trigger per message is processed.
- All processed messages are tracked in `DB/chat_wiki_requests.txt` to avoid duplicate processing.
+154 -12
View File
@@ -1,10 +1,11 @@
# Reddit Test Posts Bot
# This script reads config from a subreddit wiki page and makes test posts if the bot is a moderator.
# This script reads config from a subreddit wiki page and makes test posts
# when a moderator sends a chat message containing a configured trigger.
import praw
import json
import time
import os
from config import fetch_config_from_wiki
import threading
from config import fetch_config_from_wiki, validate_config_from_wiki, get_trigger_posts
REDDIT_CLIENT_ID = os.environ.get('REDDIT_CLIENT_ID')
REDDIT_CLIENT_SECRET = os.environ.get('REDDIT_CLIENT_SECRET')
@@ -17,22 +18,138 @@ WIKI_PAGE = os.environ.get('WIKI_PAGE', 'testpostsbot_config')
def is_moderator(reddit, subreddit_name):
"""Check if the bot is a moderator of the subreddit."""
subreddit = reddit.subreddit(subreddit_name)
mods = [str(mod) for mod in subreddit.moderator()]
return reddit.user.me().name in mods
def make_posts(reddit, subreddit_name, posts):
"""Make the specified posts to the subreddit."""
subreddit = reddit.subreddit(subreddit_name)
for post in posts:
title = post.get('title', 'Test Post')
body = post.get('body', '')
print(f"Posting: {title}")
subreddit.submit(title, selftext=body)
time.sleep(2) # avoid rate limits
print(f"[POSTING] Posting: {title}")
try:
subreddit.submit(title, selftext=body)
time.sleep(2) # avoid rate limits
except Exception as e:
print(f"[POSTING] Error posting '{title}': {e}")
def chat_message_watcher(reddit, subreddit_name):
"""
Watches for chat messages from moderators containing trigger keywords.
When a trigger is found, posts the configured posts for that trigger.
"""
chat_requests_file = os.path.join(os.path.dirname(__file__), 'DB', 'chat_wiki_requests.txt')
os.makedirs(os.path.dirname(chat_requests_file), exist_ok=True)
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())
subreddit = reddit.subreddit(subreddit_name)
while True:
try:
for message in 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')
# Check if message has body and author
if not hasattr(message, 'body'):
continue
author = getattr(message, 'author', None)
if not author:
continue
# Check if sender is a moderator
try:
is_mod = author in subreddit.moderator()
except Exception as e:
print(f"[CHAT WATCH] Error checking if {author} is mod: {e}")
continue
if not is_mod:
continue
message_body_lower = message.body.lower()
print(f"[CHAT WATCH] Moderator '{author}' sent message: {message.body[:100]}")
# Handle special 'reload-config' command
if 'reload-config' in message_body_lower:
print(f"[CHAT WATCH] Reload-config command detected.")
result = validate_config_from_wiki(reddit, subreddit_name, WIKI_PAGE)
if result:
print("[CHAT WATCH] Wiki config validated successfully.")
reply_text = "Config validated successfully. Config is valid YAML."
else:
print("[CHAT WATCH] Wiki config validation failed.")
reply_text = "Config validation failed. Check the wiki config YAML formatting."
try:
message.reply(reply_text)
print(f"[CHAT WATCH] Replied to message {message.id}.")
except Exception as e:
print(f"[CHAT WATCH] Error replying to message {message.id}: {e}")
continue
# Load current config to check for triggers
config = fetch_config_from_wiki(reddit, subreddit_name, WIKI_PAGE)
posts_config = config.get('posts', [])
trigger_found = False
# Check if message contains any trigger
for post_config in posts_config:
if not isinstance(post_config, dict):
continue
trigger = post_config.get('trigger', '').lower()
if trigger and trigger in message_body_lower:
trigger_found = True
print(f"[CHAT WATCH] Matched trigger '{trigger}' in message.")
# Get posts for this trigger
trigger_posts = get_trigger_posts(reddit, subreddit_name, WIKI_PAGE, trigger)
if trigger_posts:
print(f"[CHAT WATCH] Found {len(trigger_posts)} post(s) for trigger '{trigger}'.")
make_posts(reddit, subreddit_name, trigger_posts)
reply_text = f"Successfully posted {len(trigger_posts)} post(s) for trigger '{trigger}'."
else:
print(f"[CHAT WATCH] No posts found for trigger '{trigger}'.")
reply_text = f"No posts configured for trigger '{trigger}'."
try:
message.reply(reply_text)
print(f"[CHAT WATCH] Replied to message {message.id}.")
except Exception as e:
print(f"[CHAT WATCH] Error replying to message {message.id}: {e}")
# Only process the first matching trigger per message
break
except Exception as e:
print(f"[CHAT WATCH] Chat message watcher error: {e}")
import traceback
traceback.print_exc()
time.sleep(30)
def main():
"""Main function - runs bot constantly."""
reddit = praw.Reddit(
client_id=REDDIT_CLIENT_ID,
client_secret=REDDIT_CLIENT_SECRET,
@@ -41,13 +158,38 @@ def main():
user_agent=REDDIT_USER_AGENT
)
config = fetch_config_from_wiki(reddit, SUBREDDIT, WIKI_PAGE)
posts = config.get('posts', [])
if not is_moderator(reddit, SUBREDDIT):
print(f"Bot is not a moderator of r/{SUBREDDIT}. Cannot continue.")
return
if is_moderator(reddit, SUBREDDIT):
make_posts(reddit, SUBREDDIT, posts)
else:
print(f"Bot is not a moderator of r/{SUBREDDIT}. No posts made.")
# Validate initial config
if not validate_config_from_wiki(reddit, SUBREDDIT, WIKI_PAGE):
print(f"Initial wiki config is invalid. Please fix the config at r/{SUBREDDIT}/wiki/{WIKI_PAGE}")
return
print(f"[STARTUP] TestPostsBot started for r/{SUBREDDIT}")
print(f"[STARTUP] Config page: r/{SUBREDDIT}/wiki/{WIKI_PAGE}")
print(f"[STARTUP] Bot username: {reddit.user.me()}")
# Start chat message watcher in background thread
chat_thread = threading.Thread(
target=chat_message_watcher,
args=(reddit, SUBREDDIT),
daemon=True
)
chat_thread.start()
print("[STARTUP] Chat message watcher started.")
# Keep main thread alive
try:
while True:
time.sleep(60)
except KeyboardInterrupt:
print("[SHUTDOWN] Bot shutting down...")
if __name__ == '__main__':
main()
if __name__ == '__main__':
+70 -3
View File
@@ -1,14 +1,81 @@
# config.py
# Fetches config from subreddit wiki page
import json
# Fetches config from subreddit wiki page in YAML format
import yaml
def fetch_config_from_wiki(reddit, subreddit_name, wiki_page):
"""
Fetches config from the wiki page as YAML.
Returns the parsed config dict, or empty dict if error.
"""
subreddit = reddit.subreddit(subreddit_name)
try:
wiki = subreddit.wiki[wiki_page]
config_text = wiki.content_md
config = json.loads(config_text)
config = yaml.safe_load(config_text)
# If config is None or empty, return empty dict
if not config:
config = {'posts': []}
return config
except yaml.YAMLError as e:
print(f"Error parsing YAML config from wiki: {e}")
return {'posts': []}
except Exception as e:
print(f"Error fetching config from wiki: {e}")
return {'posts': []}
def validate_config_from_wiki(reddit, subreddit_name, wiki_page):
"""
Validates the config from the wiki page.
Returns True if the config is valid YAML with 'posts' key, False otherwise.
"""
subreddit = reddit.subreddit(subreddit_name)
try:
wiki = subreddit.wiki[wiki_page]
config_text = wiki.content_md
config = yaml.safe_load(config_text)
# Validate required structure
if not isinstance(config, dict) or 'posts' not in config:
print("Wiki config missing required 'posts' key or is not a dict.")
return False
return True
except yaml.YAMLError as e:
print(f"Error parsing YAML config from wiki: {e}")
return False
except Exception as e:
print(f"Error fetching config from wiki: {e}")
return False
def get_trigger_posts(reddit, subreddit_name, wiki_page, trigger_name):
"""
Gets the posts associated with a specific trigger.
Returns a list of post dicts, or empty list if trigger not found.
"""
config = fetch_config_from_wiki(reddit, subreddit_name, wiki_page)
posts_config = config.get('posts', [])
if not isinstance(posts_config, list):
print("Config 'posts' is not a list.")
return []
for post_config in posts_config:
if not isinstance(post_config, dict):
continue
if post_config.get('trigger', '').lower() == trigger_name.lower():
# Get the posts for this trigger
trigger_posts = post_config.get('posts', [])
if not isinstance(trigger_posts, list):
print(f"Trigger '{trigger_name}' posts is not a list.")
return []
return trigger_posts
# Trigger not found
print(f"Trigger '{trigger_name}' not found in config.")
return []
+2 -2
View File
@@ -1,6 +1,6 @@
services:
testpostbot:
image: slfhstd.uk/slfhstd/testpostbot:dev
testpostsbot:
image: slfhstd.uk/slfhstd/testpostsbot:dev
env_file:
- .env
restart: unless-stopped
+74
View File
@@ -0,0 +1,74 @@
# Example TestPostsBot Wiki Configuration
# Copy this content to your subreddit wiki page (e.g., r/yoursubreddit/wiki/testpostsbot_config)
# Each trigger can post one or multiple posts when activated via moderator private message
posts:
# Simple trigger with a single post
- trigger: "test"
posts:
- title: "Test Post"
body: "This is a test post to verify the bot is working correctly."
# Trigger with multiple posts
- trigger: "weekly-thread"
posts:
- title: "Weekly Discussion Thread"
body: |
This is the weekly discussion thread for this week.
Feel free to discuss anything related to the subreddit!
Some guidelines:
- Be respectful
- Stay on topic
- Report rule violations
- title: "Weekly Off-Topic Thread"
body: |
This is the weekly off-topic discussion thread.
Talk about anything that's not related to the subreddit here!
# Seasonal trigger with announcement posts
- trigger: "summer-schedule"
posts:
- title: "Summer 2026 Schedule Announcement"
body: |
The summer schedule for 2026 is now in effect!
Key dates:
- June 21: Summer begins
- July 4: Holiday special event
- September 21: Summer ends
We hope to see everyone participating!
- title: "Summer Rules Update"
body: |
We've updated our rules for the summer season.
Please review the updated sidebar for all changes.
Violations of the new rules may result in temporary bans.
- title: "Summer Event Sign-ups Now Open"
body: |
Sign up for our summer events by replying to this post!
Available events:
- Community game day
- AMAs with community members
- Photo contest
See the sidebar for full details.
# Maintenance trigger
- trigger: "maintenance"
posts:
- title: "Subreddit Maintenance Notice"
body: |
The subreddit will be undergoing maintenance in the next 24 hours.
During this time:
- Post submissions may be temporarily disabled
- Automod rules may be updated
- Stylesheet changes will be applied
We apologize for any inconvenience this may cause.
+1
View File
@@ -1 +1,2 @@
praw
PyYAML