""" 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}") # Get the latest message timestamp at startup last_seen_timestamp = None try: # Fetch the most recent message (limit=1) latest = list(reddit.inbox.all(limit=1)) if latest and hasattr(latest[0], 'created_utc'): last_seen_timestamp = latest[0].created_utc print(f"[CHAT] Last seen message timestamp at startup: {last_seen_timestamp}") except Exception as e: print(f"[CHAT] Error fetching latest message timestamp: {e}") while True: try: for message in reddit.inbox.stream(): # Skip if no ID, already processed, or message is older than startup if (not hasattr(message, 'id') or message.id in processed_message_ids or (last_seen_timestamp is not None and hasattr(message, 'created_utc') and message.created_utc <= last_seen_timestamp)): 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}: {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()