Ctrl K
ComunidadGuías…

bot.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""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()