Compare commits

15 Commits

Author SHA1 Message Date
slfhstd 5d61c3fea6 changelog 2026-03-11 21:06:04 +00:00
slfhstd a4bd8b9333 2.1 2026-03-11 21:01:49 +00:00
slfhstd ffaae146ac updated bot version 2026-03-11 20:51:11 +00:00
slfhstd b9ddeb5303 2026-03-11 18:25:51 +00:00
slfhstd 0e01f803b2 2026-03-11 18:21:06 +00:00
slfhstd e683449f17 2026-03-11 18:16:11 +00:00
slfhstd e47a28efae Further updates to fix permissions issues 2026-03-11 18:09:41 +00:00
slfhstd c605e00e95 docker build updates to improve security 2026-03-11 17:42:51 +00:00
slfhstd ea3eb899e7 updates to correct env file 2026-03-11 17:34:09 +00:00
slfhstd 2fa007761c gitignore 2026-03-11 17:33:29 +00:00
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
12 changed files with 909 additions and 33 deletions
+2
View File
@@ -0,0 +1,2 @@
modtestposts_config.yaml
prod.env
+253
View File
@@ -0,0 +1,253 @@
# Changelog - TestPostsBot
## Version 2.1.0 - Update Checker System
### New Features
#### 1. Update Checker System
- Bot now periodically checks for new versions from a centralized update server
- Automatically sends modmail notifications to subreddit modteam when updates are available
- Includes changelog URL in notification for easy reference
- 24-hour cooldown prevents spam (max 1 notification per day)
- Runs as a background daemon thread alongside main bot operations
- Tracks last update notification timestamp to file for persistence across restarts
#### 2. Version Management
- Bot version is now centralized at the top of `bot.py` in a dedicated section
- `BOT_VERSION` and `BOT_NAME` constants used throughout the bot
- Easy single-location reference for version updates
- Version automatically included in update checker calls and user agent string
#### 3. Infrastructure
- Created separate `update_server/` project for version management service
- Flask-based REST API serving version information via `/api/version/<bot_name>`
- `versions.json` file stores version details for all bots
- Docker support with Dockerfile for containerized deployment
- Includes comprehensive README with deployment options
### New Files
- **update_checker.py** - Background module that checks for and notifies about updates
### Modified Files
#### bot.py
- Added version constants section at top for easy reference
- Added import for `update_checker` module
- Added `start_update_checker()` call in `main()` function
- User agent now uses version constants: `{BOT_NAME}/{BOT_VERSION}`
#### requirements.txt
- Added `requests` library for HTTP requests to update server
#### docker-compose.yml
- Volume mounts for persistent `versions.json` and log files
### Configuration
Update checker timing is configurable in `update_checker.py`:
- `UPDATE_CHECK_INTERVAL = 60` seconds (check frequency)
- `UPDATE_COOLDOWN = 86400` seconds (24 hours between notifications)
### How It Works
1. On startup, bot creates background thread running update checker
2. Every hour, thread polls `https://updts.slfhstd.uk/api/version/TestPostsBot`
3. If version differs from current `BOT_VERSION`, and 24 hours have passed since last notification, sends modmail
4. Modmail includes current version, available version, and changelog URL
5. Last notification timestamp stored in `DB/.last_update_check.txt`
---
## Version 2.0.0 - Trigger-Based Redesign
### 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
@@ -0,0 +1 @@
1773261500.1906805
+103
View File
@@ -0,0 +1,103 @@
o9q8xhp
o9q9ibz
o9q9jvz
o9q9pcn
o9qawn5
o9qayrb
o9qb2tq
o9qdm6v
o9qdpsi
o9qdzec
o9qef38
o9qff1w
o9qfgmz
o9qfyl7
o9qgkgs
o9qh4nu
o9qh5uk
o9qhh0b
o9qiha2
o9qikd8
o9qijyj
o9qj7de
o9qja83
o9qjbc5
o9qkdi6
o9qkfdj
o9qkjp9
o9qm2qu
o9qm3gf
o9qm94g
o9qmr6t
o9qmt9p
o9qmylo
o9qnrak
o9qnsm6
o9qo16b
o9qot2n
o9qou0k
o9qp1vq
o9qpkrh
o9qpm3p
o9qpuks
o9qqe50
o9qqfrd
o9qr1ee
o9qrb06
o9qrcv4
o9qs54o
o9qs6i6
o9qsbua
o9qst9s
o9qvs45
o9qwlty
o9qxih7
o9qxjm9
o9qzqpn
o9qzsej
o9qzzma
o9r00bc
o9r093d
o9r111o
o9r6uje
o9r6w0g
o9r7s7b
o9r8j1q
o9r8k85
o9r8qtp
o9r9gzr
o9r9iud
o9r9vwe
o9r9xlc
o9raamy
o9raaid
o9racr4
o9raepg
o9rbepq
o9rd2ve
o9rd34l
o9rd3dx
o9rd3q8
o9rd44n
o9rd5ai
o9rd5ef
o9rd5n2
o9rd5i6
o9rd5vt
o9rd60r
o9rd5rs
o9rdgs4
o9rdqyg
5k3eari
5k3j00u
5k3nm4r
5k3p083
5k3pd9q
5k3pqsx
o9vy9h2
o9wez6a
o9wf1nr
o9wf3as
o9x1j7v
o9x1lp2
o9x1n77
+9
View File
@@ -1,8 +1,17 @@
# Dockerfile for Reddit Test Posts Bot # Dockerfile for Reddit Test Posts Bot
FROM python:3.11-slim FROM python:3.11-slim
# Create a non-root user for running the bot
WORKDIR /app WORKDIR /app
COPY requirements.txt . 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 .
COPY update_checker.py .
# Create DB directory
RUN mkdir -p /app/DB
ENV PYTHONUNBUFFERED=1
CMD ["python", "bot.py"] CMD ["python", "bot.py"]
+120 -14
View File
@@ -1,24 +1,130 @@
# 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 prod.env with your credentials, then:
docker-compose up
```
#### Security: Running as Non-Root User
By default, the container runs as a non-root user (UID 1000, GID 1000) for improved security. You can customize the user and group IDs by setting environment variables before running:
```bash
# Use specific user and group IDs
USER_ID=1001 GROUP_ID=1001 docker-compose up
# Use default (1000:1000)
docker-compose up
```
The user and group IDs can also be specified in a `.env` file:
```env
USER_ID=1001
GROUP_ID=1001
REDDIT_CLIENT_ID=your_client_id
REDDIT_CLIENT_SECRET=your_client_secret
REDDIT_USERNAME=bot_username
REDDIT_PASSWORD=bot_password
SUBREDDIT=your_subreddit
WIKI_PAGE=testpostsbot_config
```
### 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.
## Security
- **Non-Root Execution:** The Docker container runs as a non-root user (UID 1000, GID 1000) by default to minimize security risks. This can be customized via `USER_ID` and `GROUP_ID` environment variables.
- **Credentials:** Store Reddit API credentials in environment variables or `.env` files, never hardcode them.
- **Moderator-Only Commands:** All bot triggers and commands require the sender to be a moderator of the target subreddit.
- **DB Directory:** Processed message IDs are stored in a local `DB/` directory to prevent duplicate processing and maintain stateful operation.
+165 -13
View File
@@ -1,38 +1,161 @@
# 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
from update_checker import start_update_checker
# ==================== VERSION ====================
BOT_VERSION = "2.1.0"
BOT_NAME = "TestPostsBot"
# ==================================================
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')
REDDIT_USERNAME = os.environ.get('REDDIT_USERNAME') REDDIT_USERNAME = os.environ.get('REDDIT_USERNAME')
REDDIT_PASSWORD = os.environ.get('REDDIT_PASSWORD') REDDIT_PASSWORD = os.environ.get('REDDIT_PASSWORD')
REDDIT_USER_AGENT = os.environ.get('REDDIT_USER_AGENT', f'TestPostsBot/0.1 on {REDDIT_USERNAME}') REDDIT_USER_AGENT = os.environ.get('REDDIT_USER_AGENT', f'{BOT_NAME}/{BOT_VERSION} on {REDDIT_USERNAME}')
SUBREDDIT = os.environ.get('SUBREDDIT') SUBREDDIT = os.environ.get('SUBREDDIT')
WIKI_PAGE = os.environ.get('WIKI_PAGE', 'testpostsbot_config') 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}")
subreddit.submit(title, selftext=body) try:
time.sleep(2) # avoid rate limits 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(): 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 +164,42 @@ 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.")
# Start update checker in background thread
start_update_checker(reddit, SUBREDDIT, BOT_NAME, BOT_VERSION)
print("[STARTUP] Update checker 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 []
+5 -3
View File
@@ -1,6 +1,8 @@
services: services:
testpostbot: testpostsbot:
image: slfhstd.uk/slfhstd/testpostbot:dev image: slfhstd.uk/slfhstd/testpostsbot:dev
env_file: env_file:
- .env - prod.env
restart: unless-stopped restart: unless-stopped
volumes:
- ./DB:/app/DB
+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.
+2
View File
@@ -1 +1,3 @@
praw praw
PyYAML
requests
+105
View File
@@ -0,0 +1,105 @@
import requests
import threading
import time
import os
from datetime import datetime
UPDATE_CHECK_INTERVAL = 60 # Check every minute
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."""
# Build payload for the compose API
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:
version_info = get_latest_version(bot_name)
if version_info:
available_version = version_info.get('version')
changelog_url = version_info.get('changelog_url', '#')
if available_version != current_version and should_send_update_mail():
print(f"[UPDATE_CHECKER] Update found: {current_version} -> {available_version}")
if send_update_modmail(reddit, subreddit_name, bot_name, current_version, available_version, changelog_url):
mark_update_mailed()
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, bot_version):
"""Start the update checker in a background thread."""
thread = threading.Thread(
target=update_checker_thread,
args=(reddit, subreddit_name, bot_name, bot_version),
daemon=True
)
thread.start()