feat: implement EVE Forum Moderator CLI tool
- Add structured forum rules from EVE moderation policy - Implement cookie-based Discourse authentication - Add async Discourse API client with rate limiting - Implement Claude-powered rule violation analyzer - Add rich terminal and Discourse-compatible markdown reports - Create Click CLI with review and rules commands
This commit is contained in:
227
src/eve_mod/cli.py
Normal file
227
src/eve_mod/cli.py
Normal file
@@ -0,0 +1,227 @@
|
||||
"""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)",
|
||||
)
|
||||
def review(
|
||||
username: str,
|
||||
days: int,
|
||||
cookies: str,
|
||||
output: str | None,
|
||||
verbose: bool,
|
||||
max_posts: int,
|
||||
enrich: 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,
|
||||
)
|
||||
)
|
||||
|
||||
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,
|
||||
) -> 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
|
||||
with console.status(f"[bold blue]Fetching posts from last {days} days..."):
|
||||
posts = await client.get_user_posts(
|
||||
username=username,
|
||||
days=days,
|
||||
max_posts=max_posts,
|
||||
)
|
||||
console.print(f"[green]Found {len(posts)} posts[/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]")
|
||||
|
||||
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
|
||||
report_gen = ReportGenerator()
|
||||
|
||||
# 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()
|
||||
Reference in New Issue
Block a user