365 lines
13 KiB
Python
365 lines
13 KiB
Python
"""
|
|
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 bedrock_checker import get_latest_bedrock_release
|
|
from wiki_config import WikiConfig
|
|
from update_checker import start_update_checker
|
|
|
|
|
|
# Bot version (increment when making changes)
|
|
BOT_VERSION = "1.0.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 (Java & Bedrock) and post if any are new.
|
|
|
|
Args:
|
|
reddit: PRAW Reddit instance
|
|
"""
|
|
try:
|
|
posted_versions = load_posted_versions()
|
|
|
|
# Check Java Edition releases
|
|
java_releases = get_latest_releases(config.RELEASE_TYPES)
|
|
for version_info in java_releases:
|
|
version_id = version_info["id"]
|
|
|
|
if version_id not in posted_versions:
|
|
print(f"[BOT] New Java release found: {version_id}")
|
|
if post_to_subreddit(reddit, version_info):
|
|
save_posted_version(version_id)
|
|
else:
|
|
print(f"[BOT] Java version {version_id} already posted, skipping")
|
|
|
|
# Check Bedrock Edition releases if enabled
|
|
if config.CHECK_BEDROCK:
|
|
print(f"[BOT] Checking Bedrock Edition releases...")
|
|
bedrock_release = get_latest_bedrock_release()
|
|
if bedrock_release:
|
|
bedrock_id = f"bedrock-{bedrock_release['version']}"
|
|
source = bedrock_release.get('source', 'unknown')
|
|
|
|
if bedrock_id not in posted_versions:
|
|
print(f"[BOT] New Bedrock release found: {bedrock_release['version']} (from {source})")
|
|
if post_to_subreddit(reddit, bedrock_release):
|
|
save_posted_version(bedrock_id)
|
|
else:
|
|
print(f"[BOT] Bedrock version {bedrock_release['version']} already posted, skipping")
|
|
else:
|
|
print(f"[BOT] ⚠ No Bedrock release data available")
|
|
else:
|
|
print(f"[BOT] Bedrock Edition checking is disabled (REDDIT_CHECK_BEDROCK={config.CHECK_BEDROCK})")
|
|
|
|
except Exception as e:
|
|
print(f"[BOT] ✗ Error checking for updates: {e}")
|
|
|
|
|
|
def repost_latest_versions(reddit):
|
|
"""
|
|
Repost the latest Minecraft releases (Java & Bedrock).
|
|
|
|
This bypasses the "already posted" check, allowing moderators to
|
|
manually repost the latest versions via the "repost-latest" chat command.
|
|
|
|
Args:
|
|
reddit: PRAW Reddit instance
|
|
"""
|
|
try:
|
|
posted_count = 0
|
|
|
|
# Repost latest Java Edition releases
|
|
java_releases = get_latest_releases(config.RELEASE_TYPES)
|
|
for version_info in java_releases:
|
|
version_id = version_info["id"]
|
|
print(f"[BOT] Reposting Java release: {version_id}")
|
|
if post_to_subreddit(reddit, version_info):
|
|
posted_count += 1
|
|
|
|
# Repost latest Bedrock Edition release if enabled
|
|
if config.CHECK_BEDROCK:
|
|
bedrock_release = get_latest_bedrock_release()
|
|
if bedrock_release:
|
|
version_id = bedrock_release["id"]
|
|
print(f"[BOT] Reposting Bedrock release: {version_id}")
|
|
if post_to_subreddit(reddit, bedrock_release):
|
|
posted_count += 1
|
|
else:
|
|
print(f"[BOT] ⚠ No Bedrock release data available for repost")
|
|
|
|
print(f"[BOT] ✓ Reposted {posted_count} version(s)")
|
|
return posted_count > 0
|
|
|
|
except Exception as e:
|
|
print(f"[BOT] ✗ Error reposting latest versions: {e}")
|
|
return False
|
|
|
|
|
|
def chat_message_watcher(reddit):
|
|
"""
|
|
Background thread that watches for chat messages with reload commands.
|
|
|
|
Monitors Reddit chat messages for moderators sending "reload-config"
|
|
to trigger a reload of the wiki configuration without restarting the bot.
|
|
|
|
Args:
|
|
reddit: PRAW Reddit instance
|
|
"""
|
|
chat_requests_file = os.path.join(DB_DIR, 'chat_wiki_requests.txt')
|
|
processed_message_ids = set()
|
|
subreddit = reddit.subreddit(config.SUBREDDIT)
|
|
|
|
print("[CHAT] Chat message watcher started")
|
|
|
|
# Load previously processed message IDs
|
|
if os.path.exists(chat_requests_file):
|
|
try:
|
|
with open(chat_requests_file, 'r', encoding='utf-8') as f:
|
|
for line in f:
|
|
processed_message_ids.add(line.strip())
|
|
except Exception as e:
|
|
print(f"[CHAT] Error loading processed message IDs: {e}")
|
|
|
|
while True:
|
|
try:
|
|
for message in reddit.inbox.stream():
|
|
# Skip if no ID or already processed
|
|
if not hasattr(message, 'id') or message.id in processed_message_ids:
|
|
continue
|
|
|
|
# Mark as processed
|
|
processed_message_ids.add(message.id)
|
|
try:
|
|
with open(chat_requests_file, 'a', encoding='utf-8') as f:
|
|
f.write(message.id + '\n')
|
|
except Exception as e:
|
|
print(f"[CHAT] Error saving message ID: {e}")
|
|
|
|
# Check for reload-config command
|
|
if hasattr(message, 'body') and 'reload-config' in message.body.lower():
|
|
author = getattr(message, 'author', None)
|
|
|
|
# Verify sender is a moderator
|
|
try:
|
|
if author and author in subreddit.moderator():
|
|
print(f"[CHAT] Moderator '{author}' requested config reload")
|
|
|
|
# Reload wiki configuration
|
|
try:
|
|
wiki_config.fetch_from_wiki()
|
|
print("[CHAT] Wiki config reloaded successfully")
|
|
reply_text = "✓ Wiki config reloaded successfully!"
|
|
except Exception as e:
|
|
print(f"[CHAT] Failed to reload wiki config: {e}")
|
|
reply_text = f"✗ Failed to reload wiki config: {e}"
|
|
|
|
# Reply to the message
|
|
try:
|
|
message.reply(reply_text)
|
|
print(f"[CHAT] Replied to message {message.id}")
|
|
except Exception as e:
|
|
print(f"[CHAT] Error replying to message {message.id}: {e}")
|
|
else:
|
|
print(f"[CHAT] Non-moderator '{author}' attempted reload-config (ignored)")
|
|
except Exception as e:
|
|
print(f"[CHAT] Error checking moderator status: {e}")
|
|
|
|
# Check for repost-latest command
|
|
elif hasattr(message, 'body') and 'repost-latest' in message.body.lower():
|
|
author = getattr(message, 'author', None)
|
|
|
|
# Verify sender is a moderator
|
|
try:
|
|
if author and author in subreddit.moderator():
|
|
print(f"[CHAT] Moderator '{author}' requested repost of latest versions")
|
|
|
|
# Repost latest versions
|
|
try:
|
|
if repost_latest_versions(reddit):
|
|
reply_text = "✓ Latest versions reposted successfully!"
|
|
else:
|
|
reply_text = "✗ Failed to repost latest versions."
|
|
except Exception as e:
|
|
print(f"[CHAT] Error reposting latest versions: {e}")
|
|
reply_text = f"✗ Error reposting latest versions: {e}"
|
|
|
|
# Reply to the message
|
|
try:
|
|
message.reply(reply_text)
|
|
print(f"[CHAT] Replied to message {message.id}")
|
|
except Exception as e:
|
|
print(f"[CHAT] Error replying to message {message.id}: {e}")
|
|
else:
|
|
print(f"[CHAT] Non-moderator '{author}' attempted repost-latest (ignored)")
|
|
except Exception as e:
|
|
print(f"[CHAT] Error checking moderator status: {e}")
|
|
|
|
except Exception as e:
|
|
print(f"[CHAT] Error in chat message watcher: {e}")
|
|
|
|
time.sleep(30)
|
|
|
|
|
|
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 for update checking
|
|
thread = threading.Thread(target=bot_thread, args=(reddit,), daemon=True)
|
|
thread.start()
|
|
|
|
# Start background thread for chat commands
|
|
chat_thread = threading.Thread(target=chat_message_watcher, args=(reddit,), daemon=True)
|
|
chat_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()
|