initial
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
# Minecraft Update Bot - Environment Configuration
|
||||||
|
# Copy this file to .env and fill in your values
|
||||||
|
|
||||||
|
# Reddit App Credentials
|
||||||
|
# Create an app at https://www.reddit.com/prefs/apps (type: Script)
|
||||||
|
REDDIT_CLIENT_ID=YOUR_CLIENT_ID_HERE
|
||||||
|
REDDIT_CLIENT_SECRET=YOUR_CLIENT_SECRET_HERE
|
||||||
|
|
||||||
|
# Your Reddit Account
|
||||||
|
REDDIT_USERNAME=YOUR_USERNAME_HERE
|
||||||
|
REDDIT_PASSWORD=YOUR_PASSWORD_HERE
|
||||||
|
|
||||||
|
# Which subreddit to post to
|
||||||
|
SUBREDDIT=YOUR_SUBREDDIT_HERE
|
||||||
|
|
||||||
|
# Optional: customize the user agent
|
||||||
|
# REDDIT_USER_AGENT=MinecraftUpdateBot/1.0
|
||||||
|
|
||||||
|
# Optional: what types of releases to check for
|
||||||
|
# Options: release, snapshot, old_beta, old_alpha
|
||||||
|
# Multiple: RELEASE_TYPES=release,snapshot
|
||||||
|
RELEASE_TYPES=release
|
||||||
|
|
||||||
|
# Optional: how often to check for updates (in seconds)
|
||||||
|
# Default: 3600 (1 hour)
|
||||||
|
# For testing: 600 (10 minutes)
|
||||||
|
CHECK_INTERVAL=3600
|
||||||
|
|
||||||
|
# Wiki Configuration
|
||||||
|
# Post titles and bodies are loaded from the subreddit wiki page "minecraft_update_bot"
|
||||||
|
# Format: YAML with release_type sections containing title and body
|
||||||
|
# Example: See WIKI_CONFIG.md for complete documentation
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Project files
|
||||||
|
DB/
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
# Running with Docker Compose
|
||||||
|
|
||||||
|
This project includes Docker and Docker Compose configuration for easy deployment.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker installed
|
||||||
|
- Docker Compose installed
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Copy Environment File
|
||||||
|
|
||||||
|
```bash
|
||||||
|
copy .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Edit .env
|
||||||
|
|
||||||
|
Open `.env` and fill in your Reddit credentials:
|
||||||
|
|
||||||
|
```env
|
||||||
|
REDDIT_CLIENT_ID=your_client_id
|
||||||
|
REDDIT_CLIENT_SECRET=your_client_secret
|
||||||
|
REDDIT_USERNAME=your_username
|
||||||
|
REDDIT_PASSWORD=your_password
|
||||||
|
SUBREDDIT=your_subreddit
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. (Optional) Set Up Wiki Configuration
|
||||||
|
|
||||||
|
Create a wiki page on your subreddit named `minecraft_update_bot` with your post templates in YAML format. This allows different posts for releases, snapshots, etc.
|
||||||
|
|
||||||
|
See [WIKI_CONFIG.md](WIKI_CONFIG.md) for detailed instructions and examples.
|
||||||
|
|
||||||
|
### 4. Start the Bot
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
- Build the Docker image
|
||||||
|
- Start the bot in a container
|
||||||
|
- Persist data in a Docker volume named `minecraft-bot-db`
|
||||||
|
- Auto-restart the container if it crashes
|
||||||
|
|
||||||
|
### 5. View Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Watch logs in real-time
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Or check status
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Stop the Bot
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
All configuration is done via the `.env` file. The following variables are available:
|
||||||
|
|
||||||
|
| Variable | Required | Default | Description |
|
||||||
|
|----------|----------|---------|-------------|
|
||||||
|
| `REDDIT_CLIENT_ID` | ✓ | - | Your Reddit app client ID |
|
||||||
|
| `REDDIT_CLIENT_SECRET` | ✓ | - | Your Reddit app secret |
|
||||||
|
| `REDDIT_USERNAME` | ✓ | - | Your Reddit username |
|
||||||
|
| `REDDIT_PASSWORD` | ✓ | - | Your Reddit password |
|
||||||
|
| `SUBREDDIT` | ✓ | - | Subreddit to post to |
|
||||||
|
| `RELEASE_TYPES` | | `release` | Comma-separated: `release,snapshot` |
|
||||||
|
| `CHECK_INTERVAL` | | `3600` | Seconds between checks |
|
||||||
|
| `REDDIT_USER_AGENT` | | `MinecraftUpdateBot/1.0` | API user agent |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
**Check for both releases and snapshots:**
|
||||||
|
```env
|
||||||
|
RELEASE_TYPES=release,snapshot
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check every 10 minutes (for testing):**
|
||||||
|
```env
|
||||||
|
CHECK_INTERVAL=600
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Persistence
|
||||||
|
|
||||||
|
The bot's database (list of posted versions) is stored in a Docker volume called `minecraft-bot-db`. This persists between container restarts.
|
||||||
|
|
||||||
|
To view the data:
|
||||||
|
```bash
|
||||||
|
docker volume inspect minecraft-bot-db
|
||||||
|
```
|
||||||
|
|
||||||
|
To reset the database (post all versions again):
|
||||||
|
```bash
|
||||||
|
docker volume rm minecraft-bot-db
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To rebuild the image after code changes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose build --no-cache
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Container won't start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose logs
|
||||||
|
```
|
||||||
|
|
||||||
|
Check for configuration errors or missing credentials.
|
||||||
|
|
||||||
|
### Reddit authentication error
|
||||||
|
|
||||||
|
- Verify your credentials in `.env`
|
||||||
|
- Check that your Reddit app still exists at https://www.reddit.com/prefs/apps
|
||||||
|
- Make sure your account can post to the subreddit
|
||||||
|
|
||||||
|
### No posts being made
|
||||||
|
|
||||||
|
- Check the logs: `docker-compose logs -f`
|
||||||
|
- Verify the subreddit name in `.env`
|
||||||
|
- Check `docker exec minecraft-bot cat /app/DB/posted_versions.json` to see what's been posted
|
||||||
|
|
||||||
|
## Advanced Options
|
||||||
|
|
||||||
|
### Custom Post Template
|
||||||
|
|
||||||
|
Mount your config file to customize the post format:
|
||||||
|
|
||||||
|
Edit `docker-compose.yml`:
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- minecraft-bot-db:/app/DB
|
||||||
|
- ./config.py:/app/config.py # Uncomment this line
|
||||||
|
```
|
||||||
|
|
||||||
|
Then edit `config.py` locally to customize `POST_TEMPLATE`.
|
||||||
|
|
||||||
|
### Resource Limits
|
||||||
|
|
||||||
|
Uncomment and adjust in `docker-compose.yml`:
|
||||||
|
```yaml
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 256M
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Multiple Instances
|
||||||
|
|
||||||
|
Create separate directories with different `.env` files and run:
|
||||||
|
```bash
|
||||||
|
docker-compose -p instance1 up -d
|
||||||
|
docker-compose -p instance2 up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting Reddit Credentials
|
||||||
|
|
||||||
|
1. Go to https://www.reddit.com/prefs/apps
|
||||||
|
2. Click "Create an app" or "Create another app"
|
||||||
|
3. Choose "Script" application type
|
||||||
|
4. Fill in the form:
|
||||||
|
- **Name:** MinecraftUpdateBot
|
||||||
|
- **Redirect URI:** http://localhost:8080
|
||||||
|
5. Copy Client ID and Client Secret from the app page
|
||||||
|
6. Use your Reddit username and password
|
||||||
|
|
||||||
|
## Docker Commands Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start the bot
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Stop the bot
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Restart the bot
|
||||||
|
docker-compose restart
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Execute a command in the container
|
||||||
|
docker exec minecraft-bot python test_setup.py
|
||||||
|
|
||||||
|
# Remove everything (including volumes)
|
||||||
|
docker-compose down -v
|
||||||
|
```
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create DB directory
|
||||||
|
RUN mkdir -p DB
|
||||||
|
|
||||||
|
# Run the bot
|
||||||
|
CMD ["python", "main.py"]
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Quick Start Guide
|
||||||
|
|
||||||
|
## 1️⃣ Installation
|
||||||
|
```bash
|
||||||
|
cd d:\Git Repos\MinecraftUpdates
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2️⃣ Reddit Setup
|
||||||
|
1. Go to https://www.reddit.com/prefs/apps
|
||||||
|
2. Click "Create an app" (type: Script)
|
||||||
|
3. Get your credentials: Client ID, Client Secret
|
||||||
|
|
||||||
|
## 3️⃣ Configure
|
||||||
|
Edit `config.py`:
|
||||||
|
```python
|
||||||
|
REDDIT_CLIENT_ID = "your_client_id"
|
||||||
|
REDDIT_CLIENT_SECRET = "your_client_secret"
|
||||||
|
REDDIT_USERNAME = "your_username"
|
||||||
|
REDDIT_PASSWORD = "your_password"
|
||||||
|
SUBREDDIT = "your_subreddit"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4️⃣ Test
|
||||||
|
```bash
|
||||||
|
python test_setup.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Should show:
|
||||||
|
- ✓ Configuration checked
|
||||||
|
- ✓ Minecraft API working
|
||||||
|
- ✓ Reddit connection working
|
||||||
|
|
||||||
|
## 5️⃣ Run
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
See posts appear in your subreddit automatically!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Optional Config Changes
|
||||||
|
|
||||||
|
### Check for Snapshots Too
|
||||||
|
```python
|
||||||
|
RELEASE_TYPES = ["release", "snapshot"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check More Frequently (testing)
|
||||||
|
```python
|
||||||
|
CHECK_INTERVAL = 600 # 10 minutes instead of 1 hour
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize Post Format
|
||||||
|
Edit the `POST_TEMPLATE` in `config.py` - it uses `{version}` and `{release_date}` placeholders.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Overview
|
||||||
|
- `main.py` - The bot itself
|
||||||
|
- `config.py` - Settings (edit this!)
|
||||||
|
- `minecraft_checker.py` - Fetches Minecraft version data
|
||||||
|
- `test_setup.py` - Tests your configuration
|
||||||
|
- `DB/` - Stores tracking of posted versions (auto-created)
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
# Minecraft Update Bot 🎮
|
||||||
|
|
||||||
|
Automatically detects new Minecraft releases and posts them to a subreddit.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- ✅ Checks Minecraft launcher manifest for new releases
|
||||||
|
- ✅ Posts to Reddit whenever a new version is detected
|
||||||
|
- ✅ Tracks posted versions to avoid duplicates
|
||||||
|
- ✅ Runs continuously with configurable check interval
|
||||||
|
- ✅ Supports multiple release types (releases, snapshots, etc.)
|
||||||
|
- ✅ Docker & Docker Compose ready
|
||||||
|
- ✅ **Customizable post templates via subreddit wiki**
|
||||||
|
- ✅ **Different post formats for different release types**
|
||||||
|
- ✅ **Auto-update notifications** - Gets alerted when new bot versions are available
|
||||||
|
|
||||||
|
## Quick Start - Docker (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy and edit environment file
|
||||||
|
copy .env.example .env
|
||||||
|
# Edit .env with your Reddit credentials
|
||||||
|
|
||||||
|
# Start the bot
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
See [DOCKER.md](DOCKER.md) for detailed Docker setup instructions.
|
||||||
|
|
||||||
|
## Customizing Posts with Wiki Configuration
|
||||||
|
|
||||||
|
The bot loads post templates from your subreddit's wiki page `minecraft_update_bot`. This allows you to:
|
||||||
|
- Create different post formats for releases vs. snapshots
|
||||||
|
- Customize titles and bodies without restarting
|
||||||
|
- Support legacy version types with custom messages
|
||||||
|
|
||||||
|
**Quick Setup:**
|
||||||
|
1. Create a wiki page named `minecraft_update_bot` on your subreddit
|
||||||
|
2. Add YAML configuration with your templates (see example below)
|
||||||
|
3. Bot auto-loads every 30 minutes
|
||||||
|
|
||||||
|
**Simple Example:**
|
||||||
|
```yaml
|
||||||
|
release:
|
||||||
|
title: "Minecraft {version} Released!"
|
||||||
|
body: |
|
||||||
|
# Minecraft {version}
|
||||||
|
Available now! Get it at [minecraft.net](https://minecraft.net)
|
||||||
|
**Released:** {release_date}
|
||||||
|
|
||||||
|
snapshot:
|
||||||
|
title: "Minecraft {version} Snapshot"
|
||||||
|
body: |
|
||||||
|
# New Snapshot Available
|
||||||
|
Test {version} before the official release!
|
||||||
|
```
|
||||||
|
|
||||||
|
👉 See [WIKI_CONFIG.md](WIKI_CONFIG.md) for complete setup and examples.
|
||||||
|
|
||||||
|
## Manual Setup
|
||||||
|
|
||||||
|
### 1. Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create Reddit Application
|
||||||
|
|
||||||
|
1. Go to [https://www.reddit.com/prefs/apps](https://www.reddit.com/prefs/apps)
|
||||||
|
2. Click "Create an app" or "Create another app"
|
||||||
|
3. Fill in the form:
|
||||||
|
- **Name:** MinecraftUpdateBot
|
||||||
|
- **App type:** Script
|
||||||
|
- **Redirect URI:** http://localhost:8080
|
||||||
|
4. Copy the credentials
|
||||||
|
|
||||||
|
### 3. Configure the Bot
|
||||||
|
|
||||||
|
Edit `config.py` and fill in your credentials:
|
||||||
|
|
||||||
|
```python
|
||||||
|
REDDIT_CLIENT_ID = "YOUR_CLIENT_ID" # From app page
|
||||||
|
REDDIT_CLIENT_SECRET = "YOUR_CLIENT_SECRET" # From app page
|
||||||
|
REDDIT_USERNAME = "YOUR_USERNAME" # Your Reddit username
|
||||||
|
REDDIT_PASSWORD = "YOUR_PASSWORD" # Your Reddit password
|
||||||
|
SUBREDDIT = "YOUR_SUBREDDIT" # Subreddit to post to
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use environment variables (Docker-friendly):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
set REDDIT_CLIENT_ID=your_client_id
|
||||||
|
set REDDIT_CLIENT_SECRET=your_client_secret
|
||||||
|
set REDDIT_USERNAME=your_username
|
||||||
|
set REDDIT_PASSWORD=your_password
|
||||||
|
set SUBREDDIT=your_subreddit
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Customize (Optional)
|
||||||
|
|
||||||
|
In `config.py`:
|
||||||
|
- **RELEASE_TYPES:** Change `["release"]` to include snapshots or other types
|
||||||
|
- Options: `"release"`, `"snapshot"`, `"old_beta"`, `"old_alpha"`
|
||||||
|
- **CHECK_INTERVAL:** How often to check for updates (in seconds)
|
||||||
|
- Default: 3600 (1 hour)
|
||||||
|
- **POST_TEMPLATE:** Customize the post format
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```python
|
||||||
|
# Check for both releases and snapshots
|
||||||
|
RELEASE_TYPES = ["release", "snapshot"]
|
||||||
|
|
||||||
|
# Check every 30 minutes
|
||||||
|
CHECK_INTERVAL = 1800
|
||||||
|
|
||||||
|
# Check every 10 minutes (for testing)
|
||||||
|
CHECK_INTERVAL = 600
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Run the Bot
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see output like:
|
||||||
|
```
|
||||||
|
[BOT] Starting Minecraft Update Bot...
|
||||||
|
[BOT] ✓ Successfully connected to Reddit
|
||||||
|
[BOT] Started update checker (checking every 3600 seconds)
|
||||||
|
[BOT] ✓ Bot is running
|
||||||
|
```
|
||||||
|
|
||||||
|
## Automatic Update Notifications
|
||||||
|
|
||||||
|
The bot includes an update checker that periodically polls for new versions and notifies your subreddit's modteam via modmail when updates are available.
|
||||||
|
|
||||||
|
**How it works:**
|
||||||
|
- Checks `https://updts.slfhstd.uk` every hour for new versions
|
||||||
|
- Sends modmail to your subreddit's modteam if an update is found
|
||||||
|
- Limits notifications to once per 24 hours to avoid spam
|
||||||
|
- No configuration needed - it runs automatically!
|
||||||
|
|
||||||
|
**What you'll see:**
|
||||||
|
```
|
||||||
|
[UPDATE_CHECKER] Started for MinecraftUpdateBot v1.0
|
||||||
|
[UPDATE_CHECKER] Update found: 1.0 -> 1.1
|
||||||
|
[UPDATE_CHECKER] Sent update notification to r/your_subreddit
|
||||||
|
```
|
||||||
|
|
||||||
|
The modteam will receive a message with the new version number and changelog link.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. **Initialization:** Bot connects to Reddit and loads posted versions from `DB/posted_versions.json`
|
||||||
|
2. **Check Loop:** Every CHECK_INTERVAL seconds, it:
|
||||||
|
- Fetches the latest Minecraft versions from Mojang's launcher manifest
|
||||||
|
- Checks if any are new (not in `DB/posted_versions.json`)
|
||||||
|
- Posts to subreddit if new versions found
|
||||||
|
- Saves the posted version IDs
|
||||||
|
3. **Background:** Runs in background thread, checking continuously
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
Posted versions are stored in `DB/posted_versions.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"posted": [
|
||||||
|
"1.20.1",
|
||||||
|
"1.21",
|
||||||
|
"1.21.1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To reset (post all new versions again), delete or clear this file.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Connection Issues
|
||||||
|
- Check your credentials in `config.py` or environment variables
|
||||||
|
- Verify your Reddit app is configured correctly
|
||||||
|
- Make sure your Reddit account can post to the subreddit
|
||||||
|
|
||||||
|
### "No New Releases"
|
||||||
|
- The bot only posts once per version
|
||||||
|
- Check `DB/posted_versions.json` to see what's been posted
|
||||||
|
- Delete a version from the file to repost it
|
||||||
|
|
||||||
|
### Test with Snapshots
|
||||||
|
Change `RELEASE_TYPES = ["snapshot"]` and `CHECK_INTERVAL = 60` to test quickly.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `main.py` - Main bot script
|
||||||
|
- `config.py` - Configuration
|
||||||
|
- `minecraft_checker.py` - Fetches version data from Mojang
|
||||||
|
- `test_setup.py` - Validates your setup
|
||||||
|
- `Dockerfile` - Docker image definition
|
||||||
|
- `docker-compose.yml` - Docker Compose configuration
|
||||||
|
- `DB/` - Database folder (created automatically)
|
||||||
|
- `requirements.txt` - Python dependencies
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
+202
@@ -0,0 +1,202 @@
|
|||||||
|
# Wiki Configuration Setup
|
||||||
|
|
||||||
|
The Minecraft Update Bot uses a subreddit wiki page to store customizable post templates. This allows you to configure different post formats for different release types (releases, snapshots, etc.) without restarting the bot.
|
||||||
|
|
||||||
|
## Quick Setup
|
||||||
|
|
||||||
|
1. **Create the wiki page on your subreddit**
|
||||||
|
- Go to your subreddit settings
|
||||||
|
- Navigate to "Edit wiki page"
|
||||||
|
- Create a new page named `minecraft_update_bot`
|
||||||
|
|
||||||
|
2. **Add the configuration in YAML format**
|
||||||
|
|
||||||
|
Copy and paste the following into your wiki page:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
release:
|
||||||
|
title: "Minecraft {version} Released!"
|
||||||
|
body: |
|
||||||
|
# Minecraft {version} Released
|
||||||
|
|
||||||
|
A new version of Minecraft is now available!
|
||||||
|
|
||||||
|
**Version:** {version}
|
||||||
|
**Released:** {release_date}
|
||||||
|
|
||||||
|
Download it from [minecraft.net](https://minecraft.net) or use the Minecraft launcher.
|
||||||
|
|
||||||
|
snapshot:
|
||||||
|
title: "Minecraft {version} Snapshot Available"
|
||||||
|
body: |
|
||||||
|
# Minecraft {version} Snapshot
|
||||||
|
|
||||||
|
A new snapshot is available for testing!
|
||||||
|
|
||||||
|
**Version:** {version}
|
||||||
|
**Released:** {release_date}
|
||||||
|
|
||||||
|
Try it in the launcher with the development profiles.
|
||||||
|
|
||||||
|
default:
|
||||||
|
title: "Minecraft {version} ({type})"
|
||||||
|
body: |
|
||||||
|
# Minecraft {version}
|
||||||
|
|
||||||
|
A new {type} build has been released!
|
||||||
|
|
||||||
|
**Version:** {version}
|
||||||
|
**Released:** {release_date}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Save the wiki page**
|
||||||
|
- The bot will automatically load this config on startup
|
||||||
|
- Changes to the wiki are refreshed every 30 minutes
|
||||||
|
|
||||||
|
## Configuration Format
|
||||||
|
|
||||||
|
The wiki page must be valid YAML with this structure:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
release_type:
|
||||||
|
title: "Post title with {placeholders}"
|
||||||
|
body: |
|
||||||
|
Multi-line post body
|
||||||
|
with {placeholders}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Placeholders
|
||||||
|
|
||||||
|
In both title and body, you can use:
|
||||||
|
- `{version}` - The Minecraft version (e.g., "1.21")
|
||||||
|
- `{release_date}` - The formatted release date (e.g., "June 13, 2024")
|
||||||
|
- `{type}` - The release type (e.g., "release", "snapshot")
|
||||||
|
|
||||||
|
### Release Types
|
||||||
|
|
||||||
|
Create sections for each release type you want custom posts for:
|
||||||
|
- `release` - Final releases
|
||||||
|
- `snapshot` - Snapshots
|
||||||
|
- `old_beta` - Old beta versions
|
||||||
|
- `old_alpha` - Old alpha versions
|
||||||
|
- `default` - Fallback for any unconfigured type
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Example 1: Simple Setup (Releases Only)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
release:
|
||||||
|
title: "Minecraft {version} is Out!"
|
||||||
|
body: |
|
||||||
|
{version} is available now!
|
||||||
|
Get it at minecraft.net
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Different Templates per Type
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
release:
|
||||||
|
title: "🎉 Minecraft {version} Released"
|
||||||
|
body: |
|
||||||
|
# Minecraft {version}
|
||||||
|
|
||||||
|
The full release is here!
|
||||||
|
|
||||||
|
**Download:** [minecraft.net](https://minecraft.net)
|
||||||
|
**Date:** {release_date}
|
||||||
|
|
||||||
|
snapshot:
|
||||||
|
title: "📸 Minecraft {version} Snapshot"
|
||||||
|
body: |
|
||||||
|
# Snapshot Available
|
||||||
|
|
||||||
|
Test drive {version} before the official release!
|
||||||
|
|
||||||
|
**Date:** {release_date}
|
||||||
|
|
||||||
|
old_beta:
|
||||||
|
title: "[LEGACY] Minecraft {version} Beta"
|
||||||
|
body: |
|
||||||
|
Archive: Minecraft {version} beta
|
||||||
|
Released: {release_date}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Minimal Setup with Default
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
default:
|
||||||
|
title: "Minecraft {version}"
|
||||||
|
body: |
|
||||||
|
New {type}: {version}
|
||||||
|
{release_date}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Multiple Configurations:** Different posts for releases, snapshots, legacy versions, etc.
|
||||||
|
- **Auto-Refresh:** Wiki changes are loaded every 30 minutes automatically
|
||||||
|
- **Fallback:** If a release type isn't configured, it uses the `default` config
|
||||||
|
- **No Restart Required:** Changes take effect on the next check cycle
|
||||||
|
- **Flexible:** Use any text, formatting, and placeholders you want
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Configuration Not Loading
|
||||||
|
|
||||||
|
1. Check the bot logs:
|
||||||
|
```bash
|
||||||
|
docker-compose logs minecraft-bot
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Verify the wiki page name is exactly `minecraft_update_bot`
|
||||||
|
|
||||||
|
3. Ensure the YAML is valid (use a YAML validator if needed)
|
||||||
|
|
||||||
|
4. Check that the bot has permissions to read the wiki
|
||||||
|
|
||||||
|
### Posts Not Formatting Correctly
|
||||||
|
|
||||||
|
- Verify placeholder names are correct: `{version}`, `{release_date}`, `{type}`
|
||||||
|
- Check that the YAML syntax is correct (indentation matters!)
|
||||||
|
- Use the pipe `|` for multi-line bodies
|
||||||
|
|
||||||
|
### Reverting to Default
|
||||||
|
|
||||||
|
If the wiki page is empty or invalid, the bot will use embedded defaults:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
release:
|
||||||
|
title: "Minecraft {version} Released!"
|
||||||
|
body: "# Minecraft {version}\n\nA new version is available!\n\n**Version:** {version}\n**Released:** {release_date}\n\nGet it at [minecraft.net](https://minecraft.net)"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced: Testing Your Configuration
|
||||||
|
|
||||||
|
To test without posting to your subreddit:
|
||||||
|
|
||||||
|
1. View the bot logs:
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Watch for the line:
|
||||||
|
```
|
||||||
|
[WIKI_CONFIG] ✓ Loaded X configuration(s)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. This confirms your configs were loaded successfully
|
||||||
|
|
||||||
|
4. When a new version is posted, you'll see:
|
||||||
|
```
|
||||||
|
[BOT] ✓ Posted Minecraft X.X (release_type)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wiki Permissions
|
||||||
|
|
||||||
|
Make sure the bot account:
|
||||||
|
- Has **write** permission to edit the subreddit
|
||||||
|
- Can access the wiki pages
|
||||||
|
- Has the right to post to the subreddit
|
||||||
|
|
||||||
|
If you get permission errors, add the bot account as a moderator with wiki permissions.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
# Reddit Configuration
|
||||||
|
# Can be set via environment variables or directly here
|
||||||
|
REDDIT_CLIENT_ID = os.getenv("REDDIT_CLIENT_ID", "YOUR_CLIENT_ID")
|
||||||
|
REDDIT_CLIENT_SECRET = os.getenv("REDDIT_CLIENT_SECRET", "YOUR_CLIENT_SECRET")
|
||||||
|
REDDIT_USER_AGENT = os.getenv("REDDIT_USER_AGENT", "MinecraftUpdateBot/1.0")
|
||||||
|
REDDIT_USERNAME = os.getenv("REDDIT_USERNAME", "YOUR_USERNAME")
|
||||||
|
REDDIT_PASSWORD = os.getenv("REDDIT_PASSWORD", "YOUR_PASSWORD")
|
||||||
|
|
||||||
|
# Subreddit to post to
|
||||||
|
SUBREDDIT = os.getenv("SUBREDDIT", "YOUR_SUBREDDIT")
|
||||||
|
|
||||||
|
# Minecraft release types to check for
|
||||||
|
# Options: "release", "snapshot", "old_beta", "old_alpha"
|
||||||
|
# Can set via env as comma-separated: RELEASE_TYPES=release,snapshot
|
||||||
|
release_types_env = os.getenv("RELEASE_TYPES", "release")
|
||||||
|
RELEASE_TYPES = [t.strip() for t in release_types_env.split(",")]
|
||||||
|
|
||||||
|
# Post templates
|
||||||
|
# NOTE: Post titles and bodies are now fetched from the subreddit wiki page "minecraft_update_bot"
|
||||||
|
# See DOCKER.md or README.md for how to set up the wiki page with YAML configuration
|
||||||
|
#
|
||||||
|
# Wiki page format (YAML):
|
||||||
|
# release:
|
||||||
|
# title: "Minecraft {version} Released!"
|
||||||
|
# body: |
|
||||||
|
# # Minecraft {version} Released
|
||||||
|
# A new version is available!
|
||||||
|
# **Version:** {version}
|
||||||
|
# **Released:** {release_date}
|
||||||
|
#
|
||||||
|
# snapshot:
|
||||||
|
# title: "Minecraft {version} Snapshot"
|
||||||
|
# body: |
|
||||||
|
# # Minecraft {version} Snapshot
|
||||||
|
# Available for testing!
|
||||||
|
# **Version:** {version}
|
||||||
|
# **Released:** {release_date}
|
||||||
|
#
|
||||||
|
# Placeholders: {version}, {release_date}, {type}
|
||||||
|
|
||||||
|
|
||||||
|
# Check interval in seconds (default: 3600 = 1 hour)
|
||||||
|
# Can be set via environment variable: CHECK_INTERVAL=3600
|
||||||
|
CHECK_INTERVAL = int(os.getenv("CHECK_INTERVAL", "3600"))
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
services:
|
||||||
|
minecraft-bot:
|
||||||
|
build: .
|
||||||
|
image: minecraft-update-bot:latest
|
||||||
|
container_name: minecraft-bot
|
||||||
|
environment:
|
||||||
|
# Reddit Configuration
|
||||||
|
REDDIT_CLIENT_ID: ${REDDIT_CLIENT_ID}
|
||||||
|
REDDIT_CLIENT_SECRET: ${REDDIT_CLIENT_SECRET}
|
||||||
|
REDDIT_USER_AGENT: ${REDDIT_USER_AGENT:-MinecraftUpdateBot/1.0}
|
||||||
|
REDDIT_USERNAME: ${REDDIT_USERNAME}
|
||||||
|
REDDIT_PASSWORD: ${REDDIT_PASSWORD}
|
||||||
|
# Bot Configuration
|
||||||
|
SUBREDDIT: ${SUBREDDIT}
|
||||||
|
RELEASE_TYPES: ${RELEASE_TYPES:-release}
|
||||||
|
CHECK_INTERVAL: ${CHECK_INTERVAL:-3600}
|
||||||
|
volumes:
|
||||||
|
# Persist the database of posted versions
|
||||||
|
- minecraft-bot-db:/app/DB
|
||||||
|
# Optional: mount config.py for easy editing
|
||||||
|
# - ./config.py:/app/config.py
|
||||||
|
restart: unless-stopped
|
||||||
|
# Optional: resource limits
|
||||||
|
# deploy:
|
||||||
|
# resources:
|
||||||
|
# limits:
|
||||||
|
# cpus: '0.5'
|
||||||
|
# memory: 256M
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
minecraft-bot-db:
|
||||||
|
driver: local
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
"""
|
||||||
|
Minecraft Update Bot - Detects new Minecraft releases and posts to Reddit
|
||||||
|
"""
|
||||||
|
|
||||||
|
import praw
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
from datetime import datetime
|
||||||
|
import config
|
||||||
|
from minecraft_checker import get_latest_releases, parse_release_date
|
||||||
|
from wiki_config import WikiConfig
|
||||||
|
from update_checker import start_update_checker
|
||||||
|
|
||||||
|
|
||||||
|
# Bot version (increment when making changes)
|
||||||
|
BOT_VERSION = "1.0"
|
||||||
|
|
||||||
|
# Database file to track posted versions
|
||||||
|
DB_DIR = os.path.join(os.path.dirname(__file__), 'DB')
|
||||||
|
POSTED_VERSIONS_FILE = os.path.join(DB_DIR, 'posted_versions.json')
|
||||||
|
|
||||||
|
# Wiki configuration manager (initialized on startup)
|
||||||
|
wiki_config = WikiConfig()
|
||||||
|
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
"""Initialize the database directory and posted versions file."""
|
||||||
|
os.makedirs(DB_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
if not os.path.exists(POSTED_VERSIONS_FILE):
|
||||||
|
with open(POSTED_VERSIONS_FILE, 'w') as f:
|
||||||
|
json.dump({"posted": []}, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def load_posted_versions():
|
||||||
|
"""Load the list of already-posted version IDs."""
|
||||||
|
try:
|
||||||
|
with open(POSTED_VERSIONS_FILE, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return set(data.get("posted", []))
|
||||||
|
except:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
|
def save_posted_version(version_id):
|
||||||
|
"""Save a version ID as posted."""
|
||||||
|
try:
|
||||||
|
with open(POSTED_VERSIONS_FILE, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
except:
|
||||||
|
data = {"posted": []}
|
||||||
|
|
||||||
|
if version_id not in data.get("posted", []):
|
||||||
|
data.setdefault("posted", []).append(version_id)
|
||||||
|
|
||||||
|
with open(POSTED_VERSIONS_FILE, 'w') as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def make_reddit_connection():
|
||||||
|
"""Create a Reddit API connection using PRAW."""
|
||||||
|
try:
|
||||||
|
reddit = praw.Reddit(
|
||||||
|
client_id=config.REDDIT_CLIENT_ID,
|
||||||
|
client_secret=config.REDDIT_CLIENT_SECRET,
|
||||||
|
user_agent=config.REDDIT_USER_AGENT,
|
||||||
|
username=config.REDDIT_USERNAME,
|
||||||
|
password=config.REDDIT_PASSWORD
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test the connection
|
||||||
|
reddit.user.me()
|
||||||
|
print("[BOT] ✓ Successfully connected to Reddit")
|
||||||
|
return reddit
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[BOT] ✗ Failed to connect to Reddit: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def post_to_subreddit(reddit, version_info):
|
||||||
|
"""
|
||||||
|
Post a message about a new Minecraft release to the subreddit.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reddit: PRAW Reddit instance
|
||||||
|
version_info: Dict with keys: id, type, releaseTime
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
version_id = version_info["id"]
|
||||||
|
release_type = version_info["type"]
|
||||||
|
release_date = parse_release_date(version_info["releaseTime"])
|
||||||
|
|
||||||
|
# Get title and body from wiki config
|
||||||
|
title, body = wiki_config.format_post(release_type, version_id, release_date)
|
||||||
|
|
||||||
|
subreddit = reddit.subreddit(config.SUBREDDIT)
|
||||||
|
submission = subreddit.submit(title, selftext=body)
|
||||||
|
|
||||||
|
print(f"[BOT] ✓ Posted Minecraft {version_id} ({release_type})")
|
||||||
|
print(f" URL: {submission.url}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[BOT] ✗ Failed to post: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def check_for_updates(reddit):
|
||||||
|
"""
|
||||||
|
Check for new Minecraft releases and post if any are new.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reddit: PRAW Reddit instance
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
posted_versions = load_posted_versions()
|
||||||
|
latest_releases = get_latest_releases(config.RELEASE_TYPES)
|
||||||
|
|
||||||
|
for version_info in latest_releases:
|
||||||
|
version_id = version_info["id"]
|
||||||
|
|
||||||
|
if version_id not in posted_versions:
|
||||||
|
print(f"[BOT] New release found: {version_id}")
|
||||||
|
if post_to_subreddit(reddit, version_info):
|
||||||
|
save_posted_version(version_id)
|
||||||
|
else:
|
||||||
|
print(f"[BOT] Version {version_id} already posted, skipping")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[BOT] ✗ Error checking for updates: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def bot_thread(reddit):
|
||||||
|
"""
|
||||||
|
Background thread that periodically checks for Minecraft updates.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reddit: PRAW Reddit instance
|
||||||
|
"""
|
||||||
|
print(f"[BOT] Started update checker (checking every {config.CHECK_INTERVAL} seconds)")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
check_for_updates(reddit)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[BOT] ✗ Error in bot thread: {e}")
|
||||||
|
|
||||||
|
time.sleep(config.CHECK_INTERVAL)
|
||||||
|
|
||||||
|
|
||||||
|
def start_bot():
|
||||||
|
"""Start the Minecraft Update Bot."""
|
||||||
|
print("[BOT] Starting Minecraft Update Bot...")
|
||||||
|
|
||||||
|
# Initialize database
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
# Connect to Reddit
|
||||||
|
reddit = make_reddit_connection()
|
||||||
|
if not reddit:
|
||||||
|
print("[BOT] ✗ Cannot start bot without Reddit connection")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Initialize wiki configuration
|
||||||
|
wiki_config.init(reddit, config.SUBREDDIT)
|
||||||
|
wiki_config.fetch_from_wiki()
|
||||||
|
|
||||||
|
# Start update checker
|
||||||
|
start_update_checker(reddit, config.SUBREDDIT, "MinecraftUpdateBot", BOT_VERSION)
|
||||||
|
|
||||||
|
# Do an initial check
|
||||||
|
check_for_updates(reddit)
|
||||||
|
|
||||||
|
# Start background thread
|
||||||
|
thread = threading.Thread(target=bot_thread, args=(reddit,), daemon=True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
print("[BOT] ✓ Bot is running")
|
||||||
|
|
||||||
|
# Keep main thread alive
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n[BOT] Shutting down...")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
start_bot()
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
"""
|
||||||
|
Minecraft version checker - fetches latest releases from Mojang's launcher manifest
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
MANIFEST_URL = "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json"
|
||||||
|
TIMEOUT = 10
|
||||||
|
|
||||||
|
|
||||||
|
def get_minecraft_versions():
|
||||||
|
"""
|
||||||
|
Fetch all Minecraft versions from the official launcher manifest.
|
||||||
|
Returns dict with version info categorized by type.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = requests.get(MANIFEST_URL, timeout=TIMEOUT)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[MINECRAFT_CHECKER] Error fetching manifest: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_latest_releases(release_types=None):
|
||||||
|
"""
|
||||||
|
Get latest releases of specified types.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
release_types: List of types like ["release", "snapshot"]
|
||||||
|
If None, returns only the latest release
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of dicts with version info
|
||||||
|
"""
|
||||||
|
if release_types is None:
|
||||||
|
release_types = ["release"]
|
||||||
|
|
||||||
|
manifest = get_minecraft_versions()
|
||||||
|
if not manifest:
|
||||||
|
return []
|
||||||
|
|
||||||
|
latest_versions = []
|
||||||
|
|
||||||
|
# Get the latest version of each type
|
||||||
|
for release_type in release_types:
|
||||||
|
if release_type in manifest["latest"]:
|
||||||
|
version_id = manifest["latest"][release_type]
|
||||||
|
|
||||||
|
# Find the full version info
|
||||||
|
for version in manifest["versions"]:
|
||||||
|
if version["id"] == version_id:
|
||||||
|
latest_versions.append(version)
|
||||||
|
break
|
||||||
|
|
||||||
|
return latest_versions
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_versions_of_type(release_type):
|
||||||
|
"""
|
||||||
|
Get all versions of a specific type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
release_type: Type like "release", "snapshot", etc.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of version dicts
|
||||||
|
"""
|
||||||
|
manifest = get_minecraft_versions()
|
||||||
|
if not manifest:
|
||||||
|
return []
|
||||||
|
|
||||||
|
versions = []
|
||||||
|
for version in manifest["versions"]:
|
||||||
|
if version["type"] == release_type:
|
||||||
|
versions.append(version)
|
||||||
|
|
||||||
|
return versions
|
||||||
|
|
||||||
|
|
||||||
|
def parse_release_date(iso_date_str):
|
||||||
|
"""
|
||||||
|
Parse ISO format date string to readable format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
iso_date_str: Date in format "2024-06-13T11:27:32+00:00"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted date string like "June 13, 2024"
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Parse ISO format
|
||||||
|
dt = datetime.fromisoformat(iso_date_str.replace("+00:00", ""))
|
||||||
|
return dt.strftime("%B %d, %Y")
|
||||||
|
except:
|
||||||
|
return iso_date_str
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
praw==7.7.0
|
||||||
|
requests==2.31.0
|
||||||
|
PyYAML==6.0.1
|
||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
"""
|
||||||
|
Test script to verify bot setup before running
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import config
|
||||||
|
from minecraft_checker import get_latest_releases, parse_release_date
|
||||||
|
|
||||||
|
def check_config():
|
||||||
|
"""Verify configuration is filled in."""
|
||||||
|
print("[TEST] Checking configuration...")
|
||||||
|
|
||||||
|
required = [
|
||||||
|
"REDDIT_CLIENT_ID",
|
||||||
|
"REDDIT_CLIENT_SECRET",
|
||||||
|
"REDDIT_USERNAME",
|
||||||
|
"REDDIT_PASSWORD",
|
||||||
|
"SUBREDDIT"
|
||||||
|
]
|
||||||
|
|
||||||
|
missing = []
|
||||||
|
for key in required:
|
||||||
|
value = getattr(config, key, None)
|
||||||
|
if not value or value.startswith("YOUR_"):
|
||||||
|
missing.append(key)
|
||||||
|
else:
|
||||||
|
print(f" ✓ {key}: configured")
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
print(f"\n✗ Missing configuration:")
|
||||||
|
for key in missing:
|
||||||
|
print(f" - {key}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_minecraft_api():
|
||||||
|
"""Test fetching from Minecraft API."""
|
||||||
|
print("\n[TEST] Checking Minecraft API...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
latest = get_latest_releases()
|
||||||
|
if not latest:
|
||||||
|
print(" ✗ Failed to fetch latest releases")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f" ✓ Connected to Minecraft launcher manifest")
|
||||||
|
for version in latest:
|
||||||
|
release_date = parse_release_date(version["releaseTime"])
|
||||||
|
print(f" - {version['id']} ({version['type']}) - {release_date}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ✗ Error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def check_reddit_connection():
|
||||||
|
"""Test Reddit API connection."""
|
||||||
|
print("\n[TEST] Checking Reddit connection...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import praw
|
||||||
|
except ImportError:
|
||||||
|
print(" ✗ praw not installed. Run: pip install -r requirements.txt")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
reddit = praw.Reddit(
|
||||||
|
client_id=config.REDDIT_CLIENT_ID,
|
||||||
|
client_secret=config.REDDIT_CLIENT_SECRET,
|
||||||
|
user_agent=config.REDDIT_USER_AGENT,
|
||||||
|
username=config.REDDIT_USERNAME,
|
||||||
|
password=config.REDDIT_PASSWORD
|
||||||
|
)
|
||||||
|
|
||||||
|
user = reddit.user.me()
|
||||||
|
print(f" ✓ Connected as u/{user.name}")
|
||||||
|
|
||||||
|
# Check if we can access the subreddit
|
||||||
|
subreddit = reddit.subreddit(config.SUBREDDIT)
|
||||||
|
print(f" ✓ Can access r/{config.SUBREDDIT} (subscribers: {subreddit.subscribers:,})")
|
||||||
|
|
||||||
|
# Check if we can post
|
||||||
|
try:
|
||||||
|
subreddit.mod.modqueue()
|
||||||
|
print(f" ✓ Have moderator access to r/{config.SUBREDDIT}")
|
||||||
|
except:
|
||||||
|
print(f" ⚠ No moderator access (must be able to post normally)")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ✗ Reddit connection failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 50)
|
||||||
|
print("Minecraft Update Bot - Setup Test")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
config_ok = check_config()
|
||||||
|
api_ok = check_minecraft_api()
|
||||||
|
reddit_ok = check_reddit_connection()
|
||||||
|
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
if config_ok and api_ok and reddit_ok:
|
||||||
|
print("✓ All checks passed! Ready to run: python main.py")
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
print("✗ Some checks failed. See above for details.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
"""
|
||||||
|
Update Checker - Periodically checks for bot updates from update server
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
UPDATE_CHECK_INTERVAL = 3600 # Check every hour
|
||||||
|
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')
|
||||||
|
UPDATE_SERVER_URL = "https://updts.slfhstd.uk/api/version"
|
||||||
|
|
||||||
|
|
||||||
|
def get_latest_version(bot_name):
|
||||||
|
"""Fetch latest version info from update server."""
|
||||||
|
try:
|
||||||
|
response = requests.get(f'{UPDATE_SERVER_URL}/{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."""
|
||||||
|
|
||||||
|
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()
|
||||||
+207
@@ -0,0 +1,207 @@
|
|||||||
|
"""
|
||||||
|
Wiki Configuration Manager - Fetches post templates and titles from subreddit wiki
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import yaml
|
||||||
|
from typing import Dict, Optional, Tuple
|
||||||
|
|
||||||
|
# Cache TTL in seconds (refresh wiki config every 30 minutes)
|
||||||
|
WIKI_CACHE_TTL = 1800
|
||||||
|
WIKI_PAGE_NAME = "minecraft_update_bot"
|
||||||
|
|
||||||
|
|
||||||
|
class WikiConfig:
|
||||||
|
"""Manages post configurations loaded from subreddit wiki."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.configs: Dict[str, Dict[str, str]] = {}
|
||||||
|
self.default_config: Dict[str, str] = {"title": "", "body": ""}
|
||||||
|
self.last_updated = 0
|
||||||
|
self.reddit = None
|
||||||
|
self.subreddit_name = None
|
||||||
|
|
||||||
|
def init(self, reddit, subreddit_name: str):
|
||||||
|
"""Initialize the wiki config manager."""
|
||||||
|
self.reddit = reddit
|
||||||
|
self.subreddit_name = subreddit_name
|
||||||
|
|
||||||
|
def should_refresh(self) -> bool:
|
||||||
|
"""Check if cache has expired."""
|
||||||
|
return (time.time() - self.last_updated) >= WIKI_CACHE_TTL
|
||||||
|
|
||||||
|
def fetch_from_wiki(self) -> bool:
|
||||||
|
"""
|
||||||
|
Fetch post configurations from subreddit wiki.
|
||||||
|
|
||||||
|
Wiki page format (YAML):
|
||||||
|
```yaml
|
||||||
|
release:
|
||||||
|
title: "Minecraft {version} Released!"
|
||||||
|
body: |
|
||||||
|
# Minecraft {version} Released
|
||||||
|
A new version of Minecraft is available!
|
||||||
|
|
||||||
|
**Version:** {version}
|
||||||
|
**Date:** {release_date}
|
||||||
|
|
||||||
|
Get it now at [minecraft.net](https://minecraft.net)
|
||||||
|
|
||||||
|
snapshot:
|
||||||
|
title: "Minecraft {version} Snapshot Available"
|
||||||
|
body: |
|
||||||
|
# Minecraft {version} Snapshot
|
||||||
|
A new snapshot is available for testing!
|
||||||
|
|
||||||
|
**Version:** {version}
|
||||||
|
**Date:** {release_date}
|
||||||
|
|
||||||
|
Try it in the launcher!
|
||||||
|
|
||||||
|
default:
|
||||||
|
title: "Minecraft {version} ({type})"
|
||||||
|
body: |
|
||||||
|
New {type}: {version}
|
||||||
|
Released: {release_date}
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
subreddit = self.reddit.subreddit(self.subreddit_name)
|
||||||
|
wiki_page = subreddit.wiki[WIKI_PAGE_NAME]
|
||||||
|
content = wiki_page.content_md
|
||||||
|
|
||||||
|
self.configs = self._parse_yaml_content(content)
|
||||||
|
self.last_updated = time.time()
|
||||||
|
|
||||||
|
if self.configs:
|
||||||
|
print(f"[WIKI_CONFIG] ✓ Loaded {len(self.configs)} configuration(s)")
|
||||||
|
for release_type in self.configs:
|
||||||
|
print(f" - {release_type}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("[WIKI_CONFIG] ⚠ No configurations found in wiki")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WIKI_CONFIG] ✗ Error fetching wiki: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _parse_yaml_content(self, content: str) -> Dict[str, Dict[str, str]]:
|
||||||
|
"""
|
||||||
|
Parse YAML content into configuration dict.
|
||||||
|
|
||||||
|
Expected format:
|
||||||
|
```yaml
|
||||||
|
release:
|
||||||
|
title: "Minecraft {version} Released!"
|
||||||
|
body: |
|
||||||
|
Multi-line post body
|
||||||
|
with {placeholders}
|
||||||
|
|
||||||
|
snapshot:
|
||||||
|
title: "Minecraft {version} Snapshot"
|
||||||
|
body: |
|
||||||
|
Snapshot post body
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict mapping release_type -> {"title": str, "body": str}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = yaml.safe_load(content)
|
||||||
|
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
print("[WIKI_CONFIG] ✗ Wiki content is not valid YAML dictionary")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
configs = {}
|
||||||
|
for release_type, config in data.items():
|
||||||
|
if isinstance(config, dict):
|
||||||
|
if "title" in config and "body" in config:
|
||||||
|
configs[release_type.lower()] = {
|
||||||
|
"title": str(config["title"]),
|
||||||
|
"body": str(config["body"])
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
print(f"[WIKI_CONFIG] ⚠ Missing 'title' or 'body' for {release_type}")
|
||||||
|
|
||||||
|
return configs
|
||||||
|
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
print(f"[WIKI_CONFIG] ✗ YAML parsing error: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_config(self, release_type: str, refresh: bool = False) -> Tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Get title and body template for a release type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
release_type: Type like "release" or "snapshot"
|
||||||
|
refresh: Force refresh from wiki
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (title_template, body_template)
|
||||||
|
"""
|
||||||
|
# Refresh if needed
|
||||||
|
if refresh or self.should_refresh() or not self.configs:
|
||||||
|
self.fetch_from_wiki()
|
||||||
|
|
||||||
|
# Get config for this type, fall back to any available config
|
||||||
|
if release_type in self.configs:
|
||||||
|
config = self.configs[release_type]
|
||||||
|
return config["title"], config["body"]
|
||||||
|
|
||||||
|
# Try generic "default" config
|
||||||
|
if "default" in self.configs:
|
||||||
|
config = self.configs["default"]
|
||||||
|
return config["title"], config["body"]
|
||||||
|
|
||||||
|
# Fall back to hardcoded defaults
|
||||||
|
default_title = "Minecraft {version} Released!"
|
||||||
|
default_body = """# Minecraft {version}
|
||||||
|
|
||||||
|
A new version is available!
|
||||||
|
|
||||||
|
**Version:** {version}
|
||||||
|
**Released:** {release_date}
|
||||||
|
|
||||||
|
Get it at [minecraft.net](https://minecraft.net)"""
|
||||||
|
|
||||||
|
return default_title, default_body
|
||||||
|
|
||||||
|
def format_post(self, release_type: str, version: str, release_date: str) -> Tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Get formatted post title and body for a version.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
release_type: Type like "release" or "snapshot"
|
||||||
|
version: Version string like "1.21"
|
||||||
|
release_date: Formatted date string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (formatted_title, formatted_body)
|
||||||
|
"""
|
||||||
|
title_template, body_template = self.get_config(release_type)
|
||||||
|
|
||||||
|
# Format with available placeholders
|
||||||
|
format_dict = {
|
||||||
|
"version": version,
|
||||||
|
"release_date": release_date,
|
||||||
|
"type": release_type
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
formatted_title = title_template.format(**format_dict)
|
||||||
|
formatted_body = body_template.format(**format_dict)
|
||||||
|
return formatted_title, formatted_body
|
||||||
|
except KeyError as e:
|
||||||
|
print(f"[WIKI_CONFIG] ✗ Unknown placeholder in template: {e}")
|
||||||
|
# Fall back to defaults
|
||||||
|
formatted_title = f"Minecraft {version} ({release_type})"
|
||||||
|
formatted_body = f"New {release_type}: {version}\nReleased: {release_date}"
|
||||||
|
return formatted_title, formatted_body
|
||||||
Reference in New Issue
Block a user