From d9a360d031c8a6f35f149d6fe541df05aaddbc39 Mon Sep 17 00:00:00 2001 From: Michael Rausch Date: Wed, 8 Jan 2025 17:33:21 +1300 Subject: [PATCH] first commit --- .example.env | 5 + .gitignore | 37 ++++++++ Dockerfile | 21 ++++ main.py | 242 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 6 ++ 5 files changed, 311 insertions(+) create mode 100644 .example.env create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 main.py create mode 100644 requirements.txt diff --git a/.example.env b/.example.env new file mode 100644 index 0000000..f3e8f11 --- /dev/null +++ b/.example.env @@ -0,0 +1,5 @@ +REDDIT_CLIENT_ID= +REDDIT_CLIENT_SECRET= +REDDIT_USER_AGENT= +IFTTT_WEBHOOK_URL= +IFTTT_WEBHOOK_URL_NIGHT= \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b598f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Environment variables +.env + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual Environment +venv/ +ENV/ +env/ +.env/ + +# IDE specific files +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..db92bb0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +# Use Python 3.11 slim image as base +FROM python:3.11-slim + +# Set working directory +WORKDIR /app + +# Copy requirements first to leverage Docker cache +COPY requirements.txt . + +# Install dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code and .env +COPY main.py . +COPY .env . + +# Expose health check port +EXPOSE 5000 + +# Run the bot +CMD ["python", "main.py"] \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..dc832e8 --- /dev/null +++ b/main.py @@ -0,0 +1,242 @@ +# Standard library imports +import os +import time +from datetime import datetime +from zoneinfo import ZoneInfo + +# Third-party imports +import praw +import pyfiglet +import requests +from dotenv import load_dotenv +from flask import Flask, jsonify +from threading import Thread + +# Load environment variables +load_dotenv() + +# Constants +REDDIT_CLIENT_ID = os.getenv('REDDIT_CLIENT_ID') +REDDIT_CLIENT_SECRET = os.getenv('REDDIT_CLIENT_SECRET') +REDDIT_USER_AGENT = os.getenv('REDDIT_USER_AGENT') + +IFTTT_WEBHOOK_URL = os.getenv('IFTTT_WEBHOOK_URL') +IFTTT_WEBHOOK_URL_NIGHT = os.getenv('IFTTT_WEBHOOK_URL_NIGHT') + +INTERESTING_SCORE_THRESHOLD = 5 +VIRAL_MULTIPLIER = 2 +SCORE_INCREMENT = 5 +SUBMISSION_LIMIT = 5 +CHECK_INTERVAL = 300 # 5 minutes in seconds + +NIGHT_START_HOUR = 22 +NIGHT_END_HOUR = 7 +TIMEZONE = "Pacific/Auckland" + +MONITORED_SUBREDDITS = [ + ("NintendoSwitch2", "hour"), + ("nintendoswitch", "day"), +] + +# List to track posts we've already notified about +notified_posts = [] + +# Initialize Reddit API client +reddit = praw.Reddit( + client_id=REDDIT_CLIENT_ID, + client_secret=REDDIT_CLIENT_SECRET, + user_agent=REDDIT_USER_AGENT, +) + +# Keywords that indicate potentially interesting posts +interesting_words = [ + "youtube", + "Furukawa", + "President", + "Trailer", + "Announcement", + "Nintendo Direct", + "Pre-order Trailer" +] + +# Official Nintendo X (Twitter) accounts to monitor +nintendo_x_accounts = [ + "x.com/Nintendo", + "x.com/NintendoAmerica", + "x.com/NintendoUK", + "x.com/NintendoEU", + "x.com/NintendoFrance", + "x.com/NintendoGermany", + "x.com/NintendoJapan", + "x.com/NintendoCoLtd", +] + +app = Flask(__name__) + +@app.route('/health') +def health_check(): + """Health check endpoint that returns status and notified posts""" + return jsonify({ + 'status': 'healthy', + 'notified_posts': notified_posts, + 'monitored_subreddits': MONITORED_SUBREDDITS + }) + +def run_flask(): + """Run Flask server on port 5000""" + app.run(host='0.0.0.0', port=5000, debug=False, threaded=True) + +class CommonDataObject: + """ + A class to process and analyze Reddit submissions for interesting content. + Handles checking for keywords, X accounts, and viral metrics. + """ + def __init__(self, submission): + """Initialize with a Reddit submission and extract key data""" + self.submission = submission + self.body = submission.selftext.lower() + self.title = submission.title.lower() + self.updoots = submission.ups + self.url = submission.url.lower() + + def contains_nintendo_x_account(self): + """Check if submission contains reference to official Nintendo X accounts""" + for x_acct in nintendo_x_accounts: + if x_acct.lower() in self.body or x_acct.lower() in self.url: + return True + return False + + def contains_interesting_word(self): + """Check if submission contains any monitored keywords""" + for word in interesting_words: + if word.lower() in self.body or word.lower() in self.title: + return True + return False + + def is_viral(self, epoch_avg): + """Determine if post has gone viral based on upvote comparison""" + if epoch_avg > 0 and self.updoots > epoch_avg * VIRAL_MULTIPLIER: + return True + return False + + def likelihood_of_being_announcement(self, epoch_avg): + """ + Calculate a score indicating likelihood this is an important announcement + Returns tuple of (score, reason string) + """ + score = 0 + reason = "" + + if self.contains_nintendo_x_account(): + score += SCORE_INCREMENT + reason += "Contains Nintendo X account|" + + if self.contains_interesting_word(): + score += SCORE_INCREMENT + reason += "Contains interesting word|" + + if self.is_viral(epoch_avg): + score += SCORE_INCREMENT + reason += "Is viral|" + + if "youtube" in self.url or "youtube" in self.body: + score += SCORE_INCREMENT + reason += "Contains youtube|" + + return score, reason + +def print_splash(subreddit): + """Print ASCII art title and monitoring info""" + ascii_banner = pyfiglet.figlet_format("SwitchBot") + print(ascii_banner) + print(f"Monitoring subreddit: {subreddit}") + +def submission_is_interesting(submission, epoch_avg=0): + """ + Analyze a submission to determine if it's interesting + Returns tuple of (is_interesting, score, reason) + """ + cdo = CommonDataObject(submission) + score, reason = cdo.likelihood_of_being_announcement(epoch_avg) + + if score >= INTERESTING_SCORE_THRESHOLD: + return True, score, reason + + return False, score, reason + +def check_reddit(subreddit_name, time_filter): + """ + Check a subreddit for interesting posts and process them + Handles notification and IFTTT webhook calls for interesting content + """ + global notified_posts + + subreddit = reddit.subreddit(subreddit_name) + + print(".", end="", flush=True) + submissions = list(subreddit.top(limit=SUBMISSION_LIMIT, time_filter=time_filter)) + + if len(submissions) == 0: + return + + # Calculate average upvotes for this time period + epoch_avg = sum(submission.ups for submission in submissions) / len(submissions) + + # Check each submission for interesting content + for submission in submissions: + interesting, score, reason = submission_is_interesting(submission, epoch_avg) + + if interesting: + if submission.id in notified_posts: + continue + + notified_posts.append(submission.id) + + print("\n" + "=" * 50) + print("🎯 INTERESTING POST DETECTED! 🎯") + print("=" * 50) + print(f"📊 Score: {score}") + print(f"🔍 Reason: {reason}") + print("-" * 40) + print(f"📝 Title: {submission.title}") + print(f"🔗 URL: {submission.url}") + print(f"📄 Content:\n{submission.selftext}") + print(f"⬆️ Upvotes: {submission.ups}") + print("=" * 50 + "\n") + + # Send to IFTTT webhook + send_to_ifttt(submission, score, reason) + +def send_to_ifttt(submission, score, reason): + """Send interesting post data to IFTTT webhook for notifications""" + # Check if it's night time in NZDT (between 10pm and 7am) + current_time = datetime.now(ZoneInfo(TIMEZONE)) + is_night = current_time.hour >= NIGHT_START_HOUR or current_time.hour < NIGHT_END_HOUR + + webhook_url = IFTTT_WEBHOOK_URL_NIGHT if is_night else IFTTT_WEBHOOK_URL + + payload = { + "value1": f"Score: {score} - {submission.title}", + "value2": f"Reason: {reason}\nURL: {submission.url}", + "value3": submission.selftext[:1000] if submission.selftext else "No content" + } + + try: + requests.post(webhook_url, json=payload) + except Exception as e: + print(f"Failed to send to IFTTT: {e}") + +def boot(): + """Main entry point - starts monitoring loop and health check server""" + print_splash(str(MONITORED_SUBREDDITS)) + + # Start Flask server in a separate thread + Thread(target=run_flask, daemon=True).start() + + while True: + for subreddit in MONITORED_SUBREDDITS: + check_reddit(subreddit[0], subreddit[1]) + time.sleep(CHECK_INTERVAL) + +if __name__ == "__main__": + boot() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9630e0f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +praw +pyfiglet +python-dotenv +requests +flask +tzdata \ No newline at end of file