Compare commits
15 Commits
827414d762
..
v2.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d61c3fea6 | |||
| a4bd8b9333 | |||
| ffaae146ac | |||
| b9ddeb5303 | |||
| 0e01f803b2 | |||
| e683449f17 | |||
| e47a28efae | |||
| c605e00e95 | |||
| ea3eb899e7 | |||
| 2fa007761c | |||
| 4f9243ff59 | |||
| 36fd5430bc | |||
| 95da1522b6 | |||
| b3a2788555 | |||
| 1e78138992 |
@@ -0,0 +1,2 @@
|
|||||||
|
modtestposts_config.yaml
|
||||||
|
prod.env
|
||||||
+253
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1773261500.1906805
|
||||||
@@ -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
|
||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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__':
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 +1,3 @@
|
|||||||
praw
|
praw
|
||||||
|
PyYAML
|
||||||
|
requests
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user