Metadata-Version: 2.4
Name: dismessage
Version: 0.4.2
Summary: Pixel-perfect Discord message & friend request renderer
Author: F² Cyanic
License: MIT
Keywords: discord,screenshot,fake,message,render,image
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: render
Requires-Dist: playwright>=1.40; extra == "render"
Provides-Extra: fetch
Requires-Dist: httpx>=0.25; extra == "fetch"
Provides-Extra: lite
Requires-Dist: Pillow>=10.0; extra == "lite"
Requires-Dist: httpx>=0.25; extra == "lite"
Provides-Extra: all
Requires-Dist: playwright>=1.40; extra == "all"
Requires-Dist: httpx>=0.25; extra == "all"
Requires-Dist: Pillow>=10.0; extra == "all"
Dynamic: license-file

# DisMessage

> Pixel-perfect 1:1 Discord message renderer for Python. Render fake Discord conversations to standalone HTML or PNG — including avatars, avatar decorations ("profile effects"), guild tags, role colors, custom emoji, the Verified App badge, and the full Discord markdown set.

DisMessage was built by reverse-engineering Discord's actual DOM structure and CSS class names (extracted from a live page export), so the output is indistinguishable from a real Discord screenshot at a glance.

## Features

- **Faithful 1:1 layout** — uses the exact CSS class names Discord ships (`cozy_c19a55`, `markup__75297`, `clanTagChiplet_c19a55`, etc.) so the rendering matches Discord down to the pixel.
- **Avatar decorations** ("profile effects") — overlay PNGs that sit on top of the avatar, sized correctly using Discord's `--decoration-to-avatar-ratio` variable.
- **Guild tags** (clan tags) — the small chip next to the username with the clan badge image and tag text.
- **Verified App badge** — the blue "✓ APP" chip that appears on verified bots. Plain "BOT" chip for unverified bots.
- **Role colors** — username color picked from the highest role with a color.
- **Full Discord markdown**:
  - `**bold**`, `*italic*`, `__underline__`, `~~strikethrough~~`
  - `` `inline code` `` and ` ```code blocks``` `
  - `||spoilers||` (rendered with hover-to-reveal CSS)
  - `# H1`, `## H2`, `### H3` headers
  - `-# subtext` (Discord's small grey text)
  - `> blockquote`
  - `<@user>`, `<#channel>`, `<@&role>` mentions
  - `<:name:id>` custom emoji (animated and static)
  - Bare URLs auto-linked
- **Message grouping** — consecutive messages from the same author within 5 minutes are automatically grouped (no repeated avatar/header), matching Discord's behavior.
- **Reply references** — pass `reply_to=...` on a `Message` to render the slim "replying to X" header.
- **Themes** — `dark` (default), `light`, `darker`.
- **Two output formats**:
  - `render_html()` — standalone `.html` file (no external dependencies, opens in any browser).
  - `render_png()` — `.png` image rendered via headless Chromium (Playwright). Great for posting in Discord.
- **Optional Discord fetcher** — `fetch_messages()` calls the Discord REST API to pull a real message and auto-resolves its author's avatar, avatar decoration, clan tag, and verified-app flag.

## Installation

```bash
pip install dismessage httpx playwright
python -m playwright install chromium
```

(`httpx` is only needed if you use `fetch_messages()`. `playwright` is only needed for PNG output. HTML output works with zero dependencies.)

## Quick start

### Render a fake conversation

```python
from datetime import datetime, timezone
from dismessage import Author, Message, render_png

alice = Author(
    id="123", name="Alice",
    avatar_url="https://cdn.discordapp.com/avatars/123/abc.webp?size=80",
    avatar_decoration_url="https://cdn.discordapp.com/avatar-decoration-presets/DEF.png?size=80&pas=true",
    clan_tag="myguild",
    clan_badge_url="https://cdn.discordapp.com/clan-badges/456/badge.png?size=16",
)
bob = Author(
    id="456", name="BobBot",
    avatar_url="https://cdn.discordapp.com/avatars/456/xyz.webp?size=80",
    bot=True, verified_app=True,
)

now = datetime.now(timezone.utc)
messages = [
    Message(author=alice, content="hello **world** -# subtext", timestamp=now),
    Message(author=bob,   content="hi `code` and ||spoiler||", timestamp=now),
]

render_png(messages, "out.png", theme="dark")
```

### Fetch and render a real Discord message

```python
from dismessage import fetch_messages, render_png

messages = fetch_messages(
    token="YOUR_BOT_TOKEN",        # or a user token (against ToS)
    channel_id=1532447895683596358,
    message_id=1532447918504804482,
    context_before=2,
    context_after=2,
    guild_id=1532447895683596358,  # required to resolve clan tags
)
render_png(messages, "real_msg.png", theme="dark")
```

### Use it in a discord.py bot

See `bot.py` for a complete example. The gist:

```python
import discord
from dismessage import Author, Message, render_png

@bot.tree.command()
async def fake(interaction: discord.Interaction, user: discord.User, message: str):
    author = Author(
        id=str(user.id),
        name=user.global_name or user.name,
        avatar_url=user.avatar.url if user.avatar else None,
        bot=user.bot,
    )
    msg = Message(author=author, content=message)
    render_png([msg], "/tmp/fake.png", theme="dark")
    await interaction.response.send_message(file=discord.File("/tmp/fake.png"))
```

## API reference

### `Author`

| Field                    | Type             | Description                                            |
| ------------------------ | ---------------- | ------------------------------------------------------ |
| `id`                     | `str`            | User ID (used for default avatar fallback).            |
| `name`                   | `str`            | Global display name.                                   |
| `display_name`           | `Optional[str]`  | Guild nickname (overrides `name` if set).              |
| `color`                  | `Optional[str]`  | CSS color for the username (role color).               |
| `avatar_url`             | `Optional[str]`  | Avatar image URL (40×40).                              |
| `avatar_decoration_url`  | `Optional[str]`  | Avatar decoration ("profile effect") PNG URL.          |
| `clan_tag`               | `Optional[str]`  | Guild tag text (e.g. `"meow"`).                        |
| `clan_badge_url`         | `Optional[str]`  | Clan badge image URL (shown left of the tag text).     |
| `bot`                    | `bool`           | Show the gray "BOT" chip.                              |
| `verified_app`           | `bool`           | Show the blue "✓ APP" chip (overrides `bot`).          |

### `Message`

| Field                   | Type                 | Description                                              |
| ----------------------- | -------------------- | -------------------------------------------------------- |
| `author`                | `Author`             | The message author.                                      |
| `content`               | `str`                | Discord markdown content.                                |
| `timestamp`             | `Optional[datetime]` | Message timestamp (shown as "7:00 PM").                  |
| `grouped_with_previous` | `Optional[bool]`     | Force grouping on/off. `None` = auto-detect.             |
| `reply_to`              | `Optional[Message]`  | Reference message (renders the slim reply header).       |
| `accessories_html`      | `str`                | Extra HTML to inject (embeds, attachments).              |

### `render_html(messages, output_path=None, *, theme="dark", width=400, group_window_seconds=300)`

Render to standalone HTML. If `output_path` is `None`, returns the HTML as a string.

### `render_png(messages, output_path, *, theme="dark", width=400, group_window_seconds=300, device_scale_factor=2.0)`

Render to a PNG via headless Chromium. Requires Playwright + Chromium installed.

### `fetch_messages(token, channel_id, message_id, *, context_before=0, context_after=0, guild_id=None, resolve_clan_tag=True, resolve_avatar_decoration=True)`

Fetch a real Discord message (optionally with surrounding context) and return a list of `Message` objects. The fetcher auto-resolves:
- Author avatar (from `avatar` hash, with `a_` prefix → GIF)
- Avatar decoration (via `GET /users/{id}/profile`)
- Clan tag + badge (via `GET /users/{id}/profile?with_mutual_guilds=true`)
- Bot flag (from `user.bot`)
- Verified app flag (from `user.public_flags` bit 16)

### `render_markdown(text)`

Convert Discord markdown to HTML. Useful if you want to render just the content without the full message chrome.

## Themes

Three built-in themes matching Discord's presets:

- `"dark"` — `#313338` background, light text (Discord's default)
- `"darker"` — `#1e1f22` background (Discord's "Darker" theme)
- `"light"` — `#FFFFFF` background, dark text

To customize, monkey-patch `dismessage.THEMES["dark"]` with your own CSS variable map:

```python
import dismessage
dismessage.THEMES["dark"]["--background-primary"] = "#1a1a1a"
```

## Why does this exist?

For bots that need to render a "fake Discord screenshot" — for moderation logs, joke commands, /fake-message generators, etc. The existing options are all web-based SaaS products with rate limits and watermarks. DisMessage is a pure-Python library you can run locally.

## License

MIT.

## Contributing

PRs welcome. The repo includes a `bot.py` example that wires DisMessage into discord.py — feel free to use it as a starting point.
