From cf3e2c8c78fc6bc012c708ca8b146a576c4ab9c8 Mon Sep 17 00:00:00 2001 From: Slfhstd Date: Wed, 11 Mar 2026 23:24:42 +0000 Subject: [PATCH] initial --- .env.example | 32 +++++++ .gitignore | 30 ++++++ DOCKER.md | 204 +++++++++++++++++++++++++++++++++++++++++ Dockerfile | 16 ++++ QUICKSTART.md | 65 +++++++++++++ README.md | 212 +++++++++++++++++++++++++++++++++++++++++++ WIKI_CONFIG.md | 202 +++++++++++++++++++++++++++++++++++++++++ config.py | 46 ++++++++++ docker-compose.yml | 32 +++++++ main.py | 194 +++++++++++++++++++++++++++++++++++++++ minecraft_checker.py | 98 ++++++++++++++++++++ requirements.txt | 3 + test_setup.py | 118 ++++++++++++++++++++++++ update_checker.py | 109 ++++++++++++++++++++++ wiki_config.py | 207 ++++++++++++++++++++++++++++++++++++++++++ 15 files changed, 1568 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 DOCKER.md create mode 100644 Dockerfile create mode 100644 QUICKSTART.md create mode 100644 README.md create mode 100644 WIKI_CONFIG.md create mode 100644 config.py create mode 100644 docker-compose.yml create mode 100644 main.py create mode 100644 minecraft_checker.py create mode 100644 requirements.txt create mode 100644 test_setup.py create mode 100644 update_checker.py create mode 100644 wiki_config.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9772c69 --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee4197b --- /dev/null +++ b/.gitignore @@ -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 diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..4219f19 --- /dev/null +++ b/DOCKER.md @@ -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 +``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bebeeff --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..3c5c0e1 --- /dev/null +++ b/QUICKSTART.md @@ -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) diff --git a/README.md b/README.md new file mode 100644 index 0000000..67e0630 --- /dev/null +++ b/README.md @@ -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 diff --git a/WIKI_CONFIG.md b/WIKI_CONFIG.md new file mode 100644 index 0000000..a4df1f2 --- /dev/null +++ b/WIKI_CONFIG.md @@ -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. diff --git a/config.py b/config.py new file mode 100644 index 0000000..a9348c1 --- /dev/null +++ b/config.py @@ -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")) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..67aa5c6 --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/main.py b/main.py new file mode 100644 index 0000000..ee82ef2 --- /dev/null +++ b/main.py @@ -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() diff --git a/minecraft_checker.py b/minecraft_checker.py new file mode 100644 index 0000000..8e88891 --- /dev/null +++ b/minecraft_checker.py @@ -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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a34395a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +praw==7.7.0 +requests==2.31.0 +PyYAML==6.0.1 diff --git a/test_setup.py b/test_setup.py new file mode 100644 index 0000000..4e88e1f --- /dev/null +++ b/test_setup.py @@ -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()) diff --git a/update_checker.py b/update_checker.py new file mode 100644 index 0000000..e1b8b49 --- /dev/null +++ b/update_checker.py @@ -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() diff --git a/wiki_config.py b/wiki_config.py new file mode 100644 index 0000000..e5e72b0 --- /dev/null +++ b/wiki_config.py @@ -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