bot.py
Rama master · réplica de lectura del repositorio original.
"""Entrypoint for the Reddit monitor bot.
Wiring + main loop only; the implementation lives in the reddit_scraper package.
"""
import logging
import time
from concurrent.futures import ThreadPoolExecutor
from colorama import Fore, Style, init
from dotenv import load_dotenv
from reddit_scraper import config, credentials, health
from reddit_scraper.monitor import RedditMonitor
# Re-exported for the test suite / external callers.
from reddit_scraper.sources import fetch_posts_json, fetch_thread_comments_json # noqa: F401
# Initialize colorama and logging
init(autoreset=True)
class ColoredFormatter(logging.Formatter):
def __init__(self, fmt, datefmt=None, style='%'):
super().__init__(fmt, datefmt, style)
self.colors = {
logging.DEBUG: Fore.CYAN,
logging.INFO: Fore.GREEN,
logging.WARNING: Fore.YELLOW,
logging.ERROR: Fore.RED,
logging.CRITICAL: Fore.RED + Style.BRIGHT,
}
def format(self, record):
color = self.colors.get(record.levelno, Fore.WHITE)
record.msg = f"{color}{record.msg}{Style.RESET_ALL}"
return super().format(record)
_formatter = ColoredFormatter('%(asctime)s - %(levelname)s - %(message)s')
_handler = logging.StreamHandler()
_handler.setFormatter(_formatter)
logging.basicConfig(level=logging.INFO, handlers=[_handler])
# Load environment variables from .env file
load_dotenv()
def main():
credentials.detect_auth_capability()
reddit = credentials.authenticate_reddit() # Authenticate Reddit once (None if no creds)
# Initial config load
cfg = config.read_config()
if cfg is None:
exit(1)
subreddits_to_search = cfg.get('subreddits_to_search', [])
config.apply_source_order_from_config(cfg)
last_config_mtime = config.get_config_mtime()
last_creds_mtime = config.get_credentials_mtime()
# Track last run time for each monitor by ID
last_run_times = {}
loop_time = 0
while True:
# Check if config file has been modified
current_mtime = config.get_config_mtime()
if current_mtime and last_config_mtime and current_mtime > last_config_mtime:
logging.info("Configuration file changed, reloading...")
new_config = config.read_config()
if new_config is not None:
cfg = new_config
subreddits_to_search = cfg.get('subreddits_to_search', [])
config.apply_source_order_from_config(cfg)
last_config_mtime = current_mtime
logging.info("Configuration reloaded successfully.")
else:
logging.warning("Failed to reload configuration, using previous settings.")
# Reload credentials when credentials.json changes (e.g. a Sylvia key or Reddit
# app entered via the UI) so new keys take effect without restarting the bot.
current_creds_mtime = config.get_credentials_mtime()
if current_creds_mtime != last_creds_mtime:
logging.info("Credentials changed, reloading...")
credentials.detect_auth_capability() # re-bridges the Sylvia key into sources
reddit = credentials.authenticate_reddit() # pick up new/changed Reddit app creds
last_creds_mtime = current_creds_mtime
# Filter to only enabled monitors
enabled_monitors = [m for m in subreddits_to_search if m.get('enabled', True)]
# Determine which monitors are due to run based on their refresh interval
current_time = time.time()
monitors_to_run = []
for monitor in enabled_monitors:
monitor_id = monitor.get('id', monitor.get('subreddit', 'unknown'))
# Use cooldown_minutes as the per-monitor refresh interval (default 10 min)
refresh_interval = monitor.get('cooldown_minutes', 10) * 60 # seconds
last_run = last_run_times.get(monitor_id, 0)
time_since_last_run = current_time - last_run
if time_since_last_run >= refresh_interval:
monitors_to_run.append(monitor)
last_run_times[monitor_id] = current_time
logging.info(
f"Running monitor: {monitor.get('name', monitor.get('subreddit'))} "
f"(interval: {monitor.get('cooldown_minutes', 10)} min)"
)
else:
time_remaining = int((refresh_interval - time_since_last_run) / 60)
logging.debug(
f"Skipping {monitor.get('name', monitor.get('subreddit'))} - {time_remaining} min until next run"
)
if monitors_to_run:
with ThreadPoolExecutor() as executor:
futures = [executor.submit(RedditMonitor(reddit, **params).run) for params in monitors_to_run]
for future in futures:
try:
future.result()
except Exception as e:
error_message = f"Error during subreddit search: {e}"
logging.error(error_message)
RedditMonitor(reddit, subreddit='error', keywords=[]).send_error_notification(error_message)
else:
logging.debug("No monitors due to run this cycle")
# Report health to Uptime Kuma (up only if a Reddit fetch succeeded recently)
health.send_kuma_heartbeat()
# Base cycle interval - check every 2 minutes (monitors have their own schedules)
logging.info(f"Cycle {loop_time} complete. Sleeping for 2 minutes before next check...")
loop_time += 1
time.sleep(120) # Check every 2 minutes for lower CPU usage
if __name__ == "__main__":
main()