- Update report format to match Gemini-style table output - Add confidence threshold (85% default) to filter noise - Add --show-all flag to display all violations - Combine quote and explanation in single Description column - Much cleaner output: 12 high-confidence vs 51 total violations
267 lines
8.1 KiB
Python
267 lines
8.1 KiB
Python
"""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()
|