"""
GhostWipe HTML report generator.
Builds a single self-contained HTML file (no external resources) that lets a
moderator browse every message GhostWipe deleted for a departing member,
switching between channels, searching content, and inspecting attachments.
"""
from __future__ import annotations
import html
import json
from typing import Any, Dict, List
TEMPLATE = r"""
__PAGE_TITLE__
"""
def _badge_class(reason: str) -> str:
return {
"left": "badge-left",
"kicked": "badge-kicked",
"banned": "badge-banned",
}.get(reason, "badge-left")
def _badge_label(reason: str) -> str:
return {
"left": "Left",
"kicked": "Kicked",
"banned": "Banned",
}.get(reason, reason.title())
def generate_report_html(
*,
guild_name: str,
member_name: str,
member_id: int,
avatar_url: str,
reason: str,
event_time_str: str,
moderator: str = None,
mod_reason: str = None,
dry_run: bool = False,
reveal_content: bool = True,
channels: List[Dict[str, Any]],
version: str = "1.0.0",
generated_at: str = "",
) -> str:
"""Build the standalone HTML report string.
``channels`` is a list of dicts shaped like:
{
"id": str, "name": str, "type": "text"|"thread"|"voice",
"skipped": bool, "skip_reason": str | None,
"message_count": int,
"messages": [
{"id": str, "timestamp": str, "content": str,
"attachments": [{"filename", "url", "size", "is_image"}],
"stickers": [str]},
...
],
}
"""
total_deleted = sum(c["message_count"] for c in channels if not c.get("skipped"))
channels_affected = sum(1 for c in channels if not c.get("skipped") and c["message_count"] > 0)
channels_skipped = sum(1 for c in channels if c.get("skipped"))
if not reveal_content:
for ch in channels:
for m in ch.get("messages", []):
m["redacted"] = True
meta_bits = [f"Server: {guild_name}"]
if moderator:
meta_bits.append(f"By: {moderator}")
if mod_reason:
meta_bits.append(f"Reason: {mod_reason}")
meta_line = " • ".join(meta_bits)
payload = {
"guild_name": guild_name,
"member_name": member_name,
"member_id": str(member_id),
"reason": reason,
"dry_run": dry_run,
"meta_line": meta_line,
"totals": {
"messages_deleted": total_deleted,
"channels_affected": channels_affected,
"channels_skipped": channels_skipped,
},
"channels": channels,
}
out = TEMPLATE
out = out.replace("__PAGE_TITLE__", html.escape(f"GhostWipe Report — {member_name}"))
out = out.replace("__AVATAR_URL__", html.escape(avatar_url or "", quote=True))
out = out.replace("__USERNAME__", html.escape(member_name))
out = out.replace("__REASON_BADGE_CLASS__", _badge_class(reason))
out = out.replace("__REASON_LABEL__", _badge_label(reason))
out = out.replace("__USER_ID__", str(member_id))
out = out.replace("__GHOSTWIPE_VERSION__", html.escape(version))
out = out.replace("__GENERATED_AT__", html.escape(generated_at or event_time_str))
out = out.replace("__REPORT_DATA_JSON__", json.dumps(payload))
return out