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:
Ben
2026-01-06 03:40:56 +00:00
parent b806ec8c83
commit 9d60f1a2fb
15 changed files with 2654 additions and 2 deletions

227
src/eve_mod/cli.py Normal file
View 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()