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 RUN pip install --no-cache-dir -r requirements.txt
COPY bot.py . COPY bot.py .
COPY config.py . COPY config.py .
ENV PYTHONUNBUFFERED=1
CMD ["python", "bot.py"] CMD ["python", "bot.py"]
+88 -14
View File
@@ -1,24 +1,98 @@
# Reddit TestPostsBot # 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`. ## How It Works
2. Create a wiki page in your subreddit named `testpostsbot_config` with JSON like:
``` 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
"posts": [ 3. If the trigger is found and valid, the bot posts the configured posts to the subreddit
{"title": "Test Post 1", "body": "Body for post 1"}, 4. The bot replies to the moderator confirming success or failure
{"title": "Test Post 2", "body": "Body for post 2"}
] ## 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 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.
+152 -10
View File
@@ -1,10 +1,11 @@
# Reddit Test Posts Bot # 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 praw
import json
import time import time
import os 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_ID = os.environ.get('REDDIT_CLIENT_ID')
REDDIT_CLIENT_SECRET = os.environ.get('REDDIT_CLIENT_SECRET') 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): def is_moderator(reddit, subreddit_name):
"""Check if the bot is a moderator of the subreddit."""
subreddit = reddit.subreddit(subreddit_name) subreddit = reddit.subreddit(subreddit_name)
mods = [str(mod) for mod in subreddit.moderator()] mods = [str(mod) for mod in subreddit.moderator()]
return reddit.user.me().name in mods return reddit.user.me().name in mods
def make_posts(reddit, subreddit_name, posts): def make_posts(reddit, subreddit_name, posts):
"""Make the specified posts to the subreddit."""
subreddit = reddit.subreddit(subreddit_name) subreddit = reddit.subreddit(subreddit_name)
for post in posts: for post in posts:
title = post.get('title', 'Test Post') title = post.get('title', 'Test Post')
body = post.get('body', '') body = post.get('body', '')
print(f"Posting: {title}") print(f"[POSTING] Posting: {title}")
try:
subreddit.submit(title, selftext=body) subreddit.submit(title, selftext=body)
time.sleep(2) # avoid rate limits 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(): def main():
"""Main function - runs bot constantly."""
reddit = praw.Reddit( reddit = praw.Reddit(
client_id=REDDIT_CLIENT_ID, client_id=REDDIT_CLIENT_ID,
client_secret=REDDIT_CLIENT_SECRET, client_secret=REDDIT_CLIENT_SECRET,
@@ -41,13 +158,38 @@ def main():
user_agent=REDDIT_USER_AGENT user_agent=REDDIT_USER_AGENT
) )
config = fetch_config_from_wiki(reddit, SUBREDDIT, WIKI_PAGE) if not is_moderator(reddit, SUBREDDIT):
posts = config.get('posts', []) print(f"Bot is not a moderator of r/{SUBREDDIT}. Cannot continue.")
return
if is_moderator(reddit, SUBREDDIT): # Validate initial config
make_posts(reddit, SUBREDDIT, posts) if not validate_config_from_wiki(reddit, SUBREDDIT, WIKI_PAGE):
else: print(f"Initial wiki config is invalid. Please fix the config at r/{SUBREDDIT}/wiki/{WIKI_PAGE}")
print(f"Bot is not a moderator of r/{SUBREDDIT}. No posts made.") 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__': if __name__ == '__main__':
+70 -3
View File
@@ -1,14 +1,81 @@
# config.py # config.py
# Fetches config from subreddit wiki page # Fetches config from subreddit wiki page in YAML format
import json import yaml
def fetch_config_from_wiki(reddit, subreddit_name, wiki_page): 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) subreddit = reddit.subreddit(subreddit_name)
try: try:
wiki = subreddit.wiki[wiki_page] wiki = subreddit.wiki[wiki_page]
config_text = wiki.content_md 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 return config
except yaml.YAMLError as e:
print(f"Error parsing YAML config from wiki: {e}")
return {'posts': []}
except Exception as e: except Exception as e:
print(f"Error fetching config from wiki: {e}") print(f"Error fetching config from wiki: {e}")
return {'posts': []} 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: services:
testpostbot: testpostsbot:
image: slfhstd.uk/slfhstd/testpostbot:dev image: slfhstd.uk/slfhstd/testpostsbot:dev
env_file: env_file:
- .env - .env
restart: unless-stopped 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 praw
PyYAML