403Webshell
Server IP : 3.147.158.171  /  Your IP : 216.73.216.216
Web Server : Apache/2.4.67 (Amazon Linux) OpenSSL/3.5.5
System : Linux ip-172-31-2-178.us-east-2.compute.internal 6.1.172-216.329.amzn2023.x86_64 #1 SMP PREEMPT_DYNAMIC Wed May 20 06:31:34 UTC 2026 x86_64
User : ec2-user ( 1000)
PHP Version : 8.4.21
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : OFF
Directory :  /tsai/repo/api/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /tsai/repo/api//cron_common.py
"""
Common utilities and setup functions for cron scripts.

This module provides shared functionality for article and comment generation scripts,
reducing code duplication and ensuring consistent behavior.
"""

import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path
import requests
import sys
from typing import Optional, List, Tuple

from dotenv import load_dotenv

# Load environment variables FIRST, before any app imports
env_path = Path(__file__).parent / '.env'
load_dotenv(dotenv_path=env_path)

from app.services.wp import WpService
from app.services.drupal import DrupalService
from app.services.site_config_service import SiteConfigService
from app.services.chat import ChatService
from app.appTypes import Character, SiteConfig, WpUser


def parse_article_arguments(help_text: Optional[str] = None) -> Tuple[str, Optional[str], str, bool, bool, bool, bool, bool, Optional[str], Optional[str], Optional[int], Optional[str], Optional[str], bool]:
    """
    Parse command-line arguments for article creation.

    Args:
        help_text: Help text to display when no arguments provided (typically __doc__)

    Returns:
        Tuple of (site_id, character_name, character_description_override,
                  generate_featured_image, generate_secondary_image,
                  run_fact_check, research_current_events, include_previous_articles,
                  series_name, search_depth_override, chunks_per_source_override,
                  seed_image_url, seed_image_instructions, run_corrections)
    """
    if len(sys.argv) < 2:
        if help_text:
            print(help_text)
        else:
            print("Usage: python create_articles.py <site_id> [options]")
        sys.exit(1)

    site_id = sys.argv[1]

    # Parse --character <name>
    character_name = None
    if '--character' in sys.argv:
        idx = sys.argv.index('--character')
        if idx + 1 < len(sys.argv):
            character_name = sys.argv[idx + 1]
        else:
            print("Error: --character requires a character name")
            sys.exit(1)

    # Parse --character-description-file <path>
    character_description_override = ''
    if '--character-description-file' in sys.argv:
        idx = sys.argv.index('--character-description-file')
        if idx + 1 < len(sys.argv):
            description_file = sys.argv[idx + 1]
            try:
                with open(description_file, 'r', encoding='utf-8') as f:
                    character_description_override = f.read().strip()
                print(f"Loaded character description override from: {description_file}")
            except FileNotFoundError:
                print(f"Error: Character description file not found: {description_file}")
                sys.exit(1)
            except Exception as e:
                print(f"Error reading character description file: {e}")
                sys.exit(1)
        else:
            print("Error: --character-description-file requires a file path")
            sys.exit(1)

    # Parse article-specific boolean flags
    generate_featured_image = '--featured-image' in sys.argv
    generate_secondary_image = '--secondary-image' in sys.argv
    run_fact_check = '--fact-check' in sys.argv
    run_corrections = '--corrections' in sys.argv
    research_current_events = '--current-events' in sys.argv
    include_previous_articles = '--no-previous-articles' not in sys.argv

    # Parse --series <name>
    series_name = None
    if '--series' in sys.argv:
        idx = sys.argv.index('--series')
        if idx + 1 < len(sys.argv):
            series_name = sys.argv[idx + 1]
        else:
            print("Error: --series requires a series name")
            sys.exit(1)

    # Parse --search-depth <value>
    search_depth_override = None
    if '--search-depth' in sys.argv:
        idx = sys.argv.index('--search-depth')
        if idx + 1 < len(sys.argv):
            search_depth_override = sys.argv[idx + 1]
            if search_depth_override not in ('basic', 'advanced'):
                print("Error: --search-depth must be 'basic' or 'advanced'")
                sys.exit(1)
        else:
            print("Error: --search-depth requires a value (basic or advanced)")
            sys.exit(1)

    # Parse --chunks-per-source <value>
    chunks_per_source_override = None
    if '--chunks-per-source' in sys.argv:
        idx = sys.argv.index('--chunks-per-source')
        if idx + 1 < len(sys.argv):
            try:
                chunks_per_source_override = int(sys.argv[idx + 1])
                if chunks_per_source_override < 1 or chunks_per_source_override > 3:
                    print("Error: --chunks-per-source must be between 1 and 3")
                    sys.exit(1)
            except ValueError:
                print("Error: --chunks-per-source requires an integer value")
                sys.exit(1)
        else:
            print("Error: --chunks-per-source requires a value (1-3)")
            sys.exit(1)

    # Parse --seed-image-url <url>
    seed_image_url = None
    if '--seed-image-url' in sys.argv:
        idx = sys.argv.index('--seed-image-url')
        if idx + 1 < len(sys.argv):
            seed_image_url = sys.argv[idx + 1]
        else:
            print("Error: --seed-image-url requires a URL")
            sys.exit(1)

    # Parse --seed-image-instructions <text> or --seed-image-instructions-file <path>
    seed_image_instructions = None
    if '--seed-image-instructions' in sys.argv:
        idx = sys.argv.index('--seed-image-instructions')
        if idx + 1 < len(sys.argv):
            seed_image_instructions = sys.argv[idx + 1]
        else:
            print("Error: --seed-image-instructions requires a string")
            sys.exit(1)
    if '--seed-image-instructions-file' in sys.argv:
        idx = sys.argv.index('--seed-image-instructions-file')
        if idx + 1 < len(sys.argv):
            seed_instructions_file = sys.argv[idx + 1]
            try:
                with open(seed_instructions_file, 'r', encoding='utf-8') as f:
                    seed_image_instructions = f.read().strip()
                print(f"Loaded seed image instructions from: {seed_instructions_file}")
            except FileNotFoundError:
                print(f"Error: Seed image instructions file not found: {seed_instructions_file}")
                sys.exit(1)
            except Exception as e:
                print(f"Error reading seed image instructions file: {e}")
                sys.exit(1)
        else:
            print("Error: --seed-image-instructions-file requires a file path")
            sys.exit(1)

    return (site_id, character_name, character_description_override,
            generate_featured_image, generate_secondary_image,
            run_fact_check, research_current_events, include_previous_articles,
            series_name, search_depth_override, chunks_per_source_override,
            seed_image_url, seed_image_instructions, run_corrections)


def parse_comment_arguments(help_text: Optional[str] = None) -> Tuple[str, Optional[str], str]:
    """
    Parse command-line arguments for comment creation.

    Args:
        help_text: Help text to display when no arguments provided (typically __doc__)

    Returns:
        Tuple of (site_id, character_name, user_instructions)
    """
    if len(sys.argv) < 2:
        if help_text:
            print(help_text)
        else:
            print("Usage: python create_comments.py <site_id> [options]")
        sys.exit(1)

    site_id = sys.argv[1]

    # Parse --character <name>
    character_name = None
    if '--character' in sys.argv:
        idx = sys.argv.index('--character')
        if idx + 1 < len(sys.argv):
            character_name = sys.argv[idx + 1]
        else:
            print("Error: --character requires a character name")
            sys.exit(1)

    # Parse --instructions-file <path>
    user_instructions = ''
    if '--instructions-file' in sys.argv:
        idx = sys.argv.index('--instructions-file')
        if idx + 1 < len(sys.argv):
            instructions_file = sys.argv[idx + 1]
            try:
                with open(instructions_file, 'r', encoding='utf-8') as f:
                    user_instructions = f.read().strip()
                print(f"Loaded instructions from: {instructions_file}")
            except FileNotFoundError:
                print(f"Error: Instructions file not found: {instructions_file}")
                sys.exit(1)
            except Exception as e:
                print(f"Error reading instructions file: {e}")
                sys.exit(1)
        else:
            print("Error: --instructions-file requires a file path")
            sys.exit(1)

    return site_id, character_name, user_instructions


def check_internet_connectivity() -> None:
    """
    Internet connectivity check — disabled.
    On production servers, connectivity is guaranteed.
    """
    pass


def setup_logging(log_filename: str) -> None:
    """
    Configure logging with rotating file handler.

    Args:
        log_filename: Name of the log file (e.g., 'api.log')
    """
    log_path = f'logs/{log_filename}'
    handler = RotatingFileHandler(log_path, maxBytes=10*1024*1024, backupCount=5)
    logging.basicConfig(
        level=logging.INFO,
        format='%(levelname)s %(asctime)s %(filename)s ln %(lineno)s %(message)s',
        datefmt='%m/%d/%Y %I:%M:%S %p',
        handlers=[handler]
    )


def initialize_services() -> Tuple[WpService, DrupalService, SiteConfigService, ChatService]:
    """
    Initialize all required service instances.

    Returns:
        Tuple of (wp_service, drupal_service, site_config_service, chat_service)
    """
    wp_service = WpService()
    drupal_service = DrupalService()
    site_config_service = SiteConfigService()
    chat_service = ChatService()

    return wp_service, drupal_service, site_config_service, chat_service


def load_site_config(site_config_service: SiteConfigService, site_id: str) -> SiteConfig:
    """
    Load and validate site configuration.

    Args:
        site_config_service: Site configuration service instance
        site_id: Site slug identifier

    Returns:
        Site configuration object
    """
    site = site_config_service.get_site_by_slug(site_id)
    print(f'site config is {site}')

    if not site:
        logging.error(f"Invalid site slug: {site_id}")
        print(f"Invalid slug!")
        sys.exit(1)

    logging.info(f'site config is {site}')
    return site


def get_jwt_token(wp_service: WpService, site: SiteConfig, site_id: str) -> str:
    """
    Retrieve WordPress JWT token for the site.

    Args:
        wp_service: WordPress service instance
        site: Site configuration object
        site_id: Site slug identifier

    Returns:
        JWT token string
    """
    wp_token = wp_service.get_jwt_token(site)

    if not wp_token or not isinstance(wp_token, str):
        logging.error(f"Failed to get JWT token for site {site_id}: {wp_token}")
        print(f"Invalid token!")
        sys.exit(1)

    return wp_token


def fetch_and_filter_characters(
    drupal_service: DrupalService,
    wp_service: WpService,
    site: SiteConfig,
    site_id: str,
    wp_token: str
) -> List[Character]:
    """
    Fetch characters from Drupal, filter by site, and add WordPress user data.

    Args:
        drupal_service: Drupal service instance
        wp_service: WordPress service instance
        site: Site configuration object
        site_id: Site slug identifier
        wp_token: WordPress JWT token

    Returns:
        List of filtered characters with WordPress user data
    """
    try:
        # Fetch only characters owned by this site's Drupal user
        if not site.drupal_target_id:
            logging.error(f"No drupal_target_id configured for site {site_id}")
            print(f"Error: No drupal_target_id configured for site {site_id}")
            sys.exit(1)

        character_data = drupal_service.fetch_characters_by_uid(site.drupal_target_id)
        system_wide_data = drupal_service.fetch_system_wide_characters()
        character_data = character_data + system_wide_data
        logging.info(f'fetched characters for target_id {site.drupal_target_id} (incl. system-wide): {len(character_data)}')

        characters = drupal_service.create_characters(site, character_data)
        wp_users_data = wp_service.get_users(site, wp_token)

        # Ensure wp_users_data is not None
        if wp_users_data is None:
            wp_users_data = []
            logging.warning("No WordPress users returned, using empty list")

        filtered_characters = drupal_service.filter_characters(
            characters, site_id, wp_users_data
        )
        logging.info(f'filtered characters: {len(filtered_characters)}')

    except Exception as e:
        logging.error(f"Failed to fetch or filter characters: {e}", exc_info=True)
        print(f"Error fetching characters: {e}")
        sys.exit(1)

    # Add WordPress user data to each character
    for character in filtered_characters:
        wp_service.add_to_character(site, character, wp_users_data)

    return filtered_characters


def select_characters(
    filtered_characters: List[Character],
    drupal_service: DrupalService,
    site: SiteConfig,
    character_name: Optional[str] = None,
    field: str = 'commentsPerDay'
) -> List[Character]:
    """
    Select characters for content generation.

    If character_name is provided, only that character is selected.
    Otherwise, uses the calendar-schedule selection algorithm.

    Args:
        filtered_characters: List of all available characters
        drupal_service: Drupal service instance
        site: Site configuration object
        character_name: Optional specific character name to use
        field: Content type to schedule against ('commentsPerDay' or 'articlesPerDay'),
            used to pick the matching calendar schedule

    Returns:
        List of selected characters
    """
    if character_name:
        # Single character mode
        selected_character = None
        for character in filtered_characters:
            if character.name == character_name:
                selected_character = character
                break

        if selected_character:
            characters = [selected_character]
            print(f'SINGLE CHARACTER MODE: Using {character_name}')
            logging.info(f'Single character mode: {character_name}')
        else:
            print(f'Error: Character "{character_name}" not found in filtered characters')
            logging.error(f'Character "{character_name}" not found')
            available = [c.name for c in filtered_characters]
            print(f'Available characters: {", ".join(available)}')
            sys.exit(1)
    else:
        # Normal mode: use calendar-schedule selection
        characters = drupal_service.selectCharacters(filtered_characters, field=field)

    if not characters:
        logging.info('No characters selected.')
        print('No characters selected.')
        sys.exit(0)

    return characters


def load_character_articles(
    characters: List[Character],
    wp_service: WpService,
    site: SiteConfig,
    wp_token: str
) -> None:
    """
    Load each character's own articles (filtered by author).

    Modifies characters in-place by setting their articles attribute.

    Args:
        characters: List of characters to load articles for
        wp_service: WordPress service instance
        site: Site configuration object
        wp_token: WordPress JWT token
    """
    for character in characters:
        logging.info(f'Name: {character.name} IS POSTING!')
        logging.info(f'Description: {character.description}')

        # Load articles filtered by author using WordPress API
        if character.wp_user and isinstance(character.wp_user, WpUser):
            character.articles = wp_service.get_recent_articles(
                site, wp_token, 100, author_id=character.wp_user.id
            )
            if character.articles is None:
                character.articles = []
                logging.warning(f"No articles returned for author {character.wp_user.id}")
        else:
            character.articles = []

        logging.info(f"Character articles: {len(character.articles)}")


def filter_already_posted_today(
    characters: List[Character],
    wp_service: WpService,
    site: SiteConfig,
    wp_token: str
) -> List[Character]:
    """
    Filter out characters that have already posted an article today.

    Args:
        characters: List of selected characters
        wp_service: WordPress service instance
        site: Site configuration object
        wp_token: WordPress JWT token

    Returns:
        List of characters that have not yet posted today
    """
    filtered = []
    for character in characters:
        if character.wp_user and isinstance(character.wp_user, WpUser):
            if wp_service.has_author_posted_today(site, wp_token, character.wp_user.id):
                logging.info(f"Skipping {character.name}: already posted an article today")
                print(f"Skipping {character.name}: already posted an article today")
                continue
        filtered.append(character)

    if len(filtered) < len(characters):
        skipped = len(characters) - len(filtered)
        logging.info(f"Filtered out {skipped} character(s) that already posted today")

    return filtered


def filter_already_commented_today(
    characters: List[Character],
    wp_service: WpService,
    site: SiteConfig,
    wp_token: str
) -> List[Character]:
    """
    Filter out characters that have already posted a comment today.

    Args:
        characters: List of selected characters
        wp_service: WordPress service instance
        site: Site configuration object
        wp_token: WordPress JWT token

    Returns:
        List of characters that have not yet commented today
    """
    filtered = []
    for character in characters:
        if character.wp_user and isinstance(character.wp_user, WpUser):
            if wp_service.has_author_commented_today(site, wp_token, character.wp_user.id):
                logging.info(f"Skipping {character.name}: already commented today")
                print(f"Skipping {character.name}: already commented today")
                continue
        filtered.append(character)

    if len(filtered) < len(characters):
        skipped = len(characters) - len(filtered)
        logging.info(f"Filtered out {skipped} character(s) that already commented today")

    return filtered

Youez - 2016 - github.com/yon3zu
LinuXploit