Changes to enable always on and trigger emthod. also updated to yaml config structure

This commit is contained in:
2026-03-11 16:36:10 +00:00
parent 827414d762
commit 1e78138992
7 changed files with 357 additions and 30 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
+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.
+113 -10
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,99 @@ 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}")
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')
if hasattr(message, 'body'):
message_body = message.body.lower()
# Check if sender is a moderator
author = getattr(message, 'author', None)
if author and author in subreddit.moderator():
# Load current config to check for triggers
config = fetch_config_from_wiki(reddit, subreddit_name, WIKI_PAGE)
posts_config = config.get('posts', [])
# 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:
print(f"[CHAT WATCH] Moderator '{author}' triggered '{trigger}'.")
# 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 chat message {message.id}.")
except Exception as e:
print(f"[CHAT WATCH] Error replying to chat 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}")
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 +119,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 []
+1 -1
View File
@@ -1,6 +1,6 @@
services:
testpostbot:
image: slfhstd.uk/slfhstd/testpostbot:dev
image: slfhstd.uk/slfhstd/testpostbot:latest
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