"""CLI entrypoint for EVE Forum Moderator.""" import asyncio from pathlib import Path import click from dotenv import load_dotenv from rich.console import Console from rich.progress import Progress, SpinnerColumn, TextColumn from .analyzer import Analyzer, AnalyzerError from .auth import AuthError, load_cookies from .discourse import DiscourseClient, DiscourseError, UserNotFoundError from .report import ReportGenerator console = Console() @click.group() @click.version_option(version="0.1.0") def cli() -> None: """EVE Forum Moderator - AI-assisted forum moderation tool.""" pass @cli.command() @click.argument("username") @click.option( "--days", "-d", default=30, help="Number of days to analyze (default: 30)", ) @click.option( "--cookies", "-c", type=click.Path(exists=False), default="cookies.env", help="Path to cookies file (default: cookies.env)", ) @click.option( "--output", "-o", type=click.Path(), default=None, help="Custom output path for markdown report", ) @click.option( "--verbose", "-v", is_flag=True, help="Show verbose output including detailed analysis", ) @click.option( "--max-posts", default=200, help="Maximum number of posts to fetch (default: 200)", ) @click.option( "--enrich/--no-enrich", default=True, help="Fetch full post content (slower but more accurate)", ) @click.option( "--debug", is_flag=True, help="Show debug output including sample post content", ) @click.option( "--show-all", is_flag=True, help="Show all violations regardless of confidence (default: 85%+ only)", ) def review( username: str, days: int, cookies: str, output: str | None, verbose: bool, max_posts: int, enrich: bool, debug: bool, show_all: bool, ) -> None: """ Review a user's forum posts for potential rule violations. USERNAME is the EVE Forum username to review. """ # Load environment variables (for LLM_PROXY_KEY) load_dotenv() try: # Load cookies with console.status("[bold blue]Loading authentication cookies..."): cookie_dict = load_cookies(Path(cookies)) console.print(f"[green]Loaded {len(cookie_dict)} cookies[/green]") # Run async operations asyncio.run( _review_user( username=username, days=days, cookies=cookie_dict, output=output, verbose=verbose, max_posts=max_posts, enrich=enrich, debug=debug, show_all=show_all, ) ) except AuthError as e: console.print(f"[red]Authentication Error:[/red] {e}") raise SystemExit(1) except AnalyzerError as e: console.print(f"[red]Analysis Error:[/red] {e}") raise SystemExit(1) except Exception as e: console.print(f"[red]Unexpected Error:[/red] {e}") if verbose: console.print_exception() raise SystemExit(1) async def _review_user( username: str, days: int, cookies: dict[str, str], output: str | None, verbose: bool, max_posts: int, enrich: bool, debug: bool = False, show_all: bool = False, ) -> None: """Async implementation of user review.""" try: async with DiscourseClient(cookies) as client: # Fetch user profile with console.status(f"[bold blue]Fetching profile for {username}..."): try: user = await client.get_user(username) console.print( f"[green]Found user:[/green] {user.username} " f"(Trust Level {user.trust_level}, {user.post_count} posts)" ) except UserNotFoundError: console.print(f"[red]User not found:[/red] {username}") raise SystemExit(1) # Fetch posts (using combined method to include hidden posts) with console.status(f"[bold blue]Fetching posts from last {days} days..."): posts = await client.get_user_posts_combined( username=username, days=days, max_posts=max_posts, ) console.print( f"[green]Found {len(posts)} posts (including hidden)[/green]" ) if not posts: console.print( "[yellow]No posts found in the specified time period.[/yellow]" ) return # Enrich posts with full content if enrich: with console.status("[bold blue]Fetching full post content..."): posts = await client.enrich_posts(posts) console.print("[green]Enriched posts with full content[/green]") # Debug: show sample post content if debug and posts: console.print("\n[bold yellow]DEBUG: Sample post content[/bold yellow]") for i, post in enumerate(posts[:3]): # Show first 3 posts console.print(f"\n[cyan]--- Post {i + 1} ---[/cyan]") console.print(f"[dim]Topic:[/dim] {post.topic_title}") console.print(f"[dim]URL:[/dim] {post.url}") console.print(f"[dim]Date:[/dim] {post.created_at}") console.print( f"[dim]Raw length:[/dim] {len(post.content_raw)} chars" ) console.print( f"[dim]Text length:[/dim] {len(post.content_text)} chars" ) content_preview = post.content_text[:500] if len(post.content_text) > 500: content_preview += "..." console.print(f"[dim]Content:[/dim]\n{content_preview}") console.print("\n") except DiscourseError as e: console.print(f"[red]Forum Error:[/red] {e}") raise SystemExit(1) # Analyze posts console.print("[bold blue]Analyzing posts for rule violations...[/bold blue]") try: analyzer = Analyzer() except AnalyzerError as e: console.print(f"[red]Analyzer Error:[/red] {e}") console.print( "[dim]Hint: Set LLM_PROXY_KEY environment variable or add it to .env[/dim]" ) raise SystemExit(1) with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=console, ) as progress: task = progress.add_task("Analyzing...", total=len(posts)) def update_progress(current: int, total: int) -> None: progress.update(task, completed=current) analyses = analyzer.analyze_all( posts=posts, progress_callback=update_progress, ) # Generate report confidence_threshold = 0.0 if show_all else 0.85 report_gen = ReportGenerator(confidence_threshold=confidence_threshold) # Print to terminal report_gen.print_summary(user, analyses, days) if verbose: report_gen.print_detailed(analyses) # Save to file if output: filepath = Path(output) filepath.parent.mkdir(parents=True, exist_ok=True) filepath.write_text(report_gen.generate_markdown(user, analyses, days)) else: filepath = report_gen.save_report(user, analyses, days) console.print(f"\n[green]Report saved to:[/green] {filepath}") @cli.command() def rules() -> None: """Display all forum moderation rules.""" from .rules import ALL_RULES, RuleCategory console.print("\n[bold]EVE Online Forum Moderation Rules[/bold]\n") current_category = None for rule in ALL_RULES: if rule.category != current_category: current_category = rule.category console.print(f"\n[bold cyan]{current_category.value.upper()}[/bold cyan]") console.print(f" [yellow]{rule.id}[/yellow] - [bold]{rule.name}[/bold]") console.print(f" [dim]{rule.description[:80]}...[/dim]") if __name__ == "__main__": cli()