Metadata-Version: 2.4
Name: dismessage
Version: 0.6.1
Summary: Pixel-perfect Discord message & friend request renderer
Author: F² Cyanic
License: MIT
Keywords: discord,screenshot,fake,message,render,image,twemoji,gg sans,reactions,replies
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 & friend request renderer. Uses Discord's **actual** HTML+CSS (ripped from a live page export) so the output is indistinguishable from a real screenshot. Only the avatar, username, text, and badges are swapped in at render time.

**It's literally Discord's UI code with different data plugged in.** 🐱

## Install

```bash
pip install dismessage[all]
python -m playwright install chromium
```

That's it. Now you can render fake Discord messages that look so real your friends will question reality.

## Quick start

### Render a conversation with reactions, replies, and mentions

```python
import dismessage
from dismessage import Author, Message, Reaction, FriendRequest, render_png, render_friend_request_png, set_discord_token, shutdown_browser
from datetime import datetime, timezone, timedelta

# === AUTHORS ===
vyse = Author(
    id="123",
    name="14313",              
    display_name="vyse",      
    avatar_url="https://cdn.discordapp.com/avatars/1530079894346661958/499a35785e6d6449ad818d13bc5313db.png?size=256",
    clan_tag="meow",
    clan_badge_url="https://cdn.discordapp.com/clan-badges/1524825517344292977/e08bd1473bf781ca021c4dbf74bff1bf.png?size=16",  
)

cyanic = Author(
    id="789",
    name="cy.xn",
    display_name="F² Cyanic",
    avatar_url="https://cdn.discordapp.com/avatars/1014527069360758854/3d9d1d32c81a492223affef48f821e04.png?size=256",
    avatar_decoration_url="https://cdn.discordapp.com/avatar-decoration-presets/a_c3cffc19e9784f7d0b005eecdf1b566e.png",
    clan_tag="snek",
    clan_badge_url="https://cdn.discordapp.com/clan-badges/267624335836053506/fd3549e1a1a692afda2a7b4cf6b6cad6.png?size=16",
)

# === MESSAGES ===
now = datetime.now(timezone.utc)

msg1 = Message(
    author=vyse,
    content="im a skiddie",
    timestamp=now,
    reactions=[
        Reaction(emoji="💀", count=4, reacted_by_me=True),
    ],
)

msg2 = Message(
    author=cyanic,
    content="WHAT",
    timestamp=now + timedelta(seconds=10),
    reply_to=msg1,   
    mentioned=False,
)

msg3 = Message(
    author=vyse,
    content="i vibecoded jalapeno",
    timestamp=now + timedelta(seconds=20),
    reply_to=msg2,
    mentioned=True,
)

# === RENDER ===
render_png(
    [msg1, msg2, msg3],
    "output.png",
    quality=3.0,
)

# === CLEANUP ===
shutdown_browser()  # closes the persistent Chromium
```

### Fake a friend request

```python
from dismessage import Author, FriendRequest, render_friend_request_png

req = FriendRequest(
    author=Author(
        id="123",
        name="foo",           # shows as grey subtext
        display_name="Foo",   # shows as the main name
        avatar_url="https://cdn.discordapp.com/avatars/123/abc.webp?size=32",
    ),
    count=1,
)

render_friend_request_png(req, "request.png")
```

### Fetch and render a REAL Discord message

```python
from dismessage import fetch_messages, render_png

messages = fetch_messages(
    token="YOUR_BOT_TOKEN",
    channel_id=1532447895683596358,
    message_id=1532447918504804482,
    context_before=2,
    context_after=2,
)
render_png(messages, "real.png")
```

The fetcher auto-resolves the author's avatar, profile effect (avatar decoration), guild tag, and verified-app badge from `GET /users/{id}`.

### Auto-resolve mentions

```python
import dismessage

# Call this once before rendering — enables <@id> and <#id> auto-resolution
dismessage.set_discord_token("YOUR_BOT_TOKEN")

# Now <@123> automatically calls GET /users/123 → global_name → @DisplayName (blue)
# And <#456> automatically calls GET /channels/456 → name → #general (blue)
msg = Message(author=author, content="hey <@123> check <#456> @everyone")
render_png([msg], "out.png")
```

## What it supports

### Messages
- **1:1 visual fidelity** — uses Discord's real DOM + CSS, not a reimplementation
- **Avatar decorations** (profile effects) — the overlay PNGs (cat ears, etc.)
- **Guild tags** (clan tags) — the chip next to the username with badge image + tag text
- **Bot badges** — "✓ APP" for verified bots, "APP" for unverified
- **Role colors** — username color from the highest role
- **Twemoji support** — all unicode emoji rendered as high-res SVG images from the [jdecked/twemoji](https://github.com/jdecked/twemoji) CDN (the exact fork Discord uses)
- **gg sans font** — all 12 weights/styles bundled (regular, medium, semibold, bold, extrabold + italics + mono)
- **Full Discord markdown**:
  - `**bold**`, `*italic*`, `__underline__`, `~~strikethrough~~`
  - `` `inline code` `` and ` ```code blocks``` `
  - `||spoilers||` (dark background, hidden text)
  - `# H1`, `## H2`, `### H3` headers
  - `-# subtext` (Discord's small grey text)
  - `> blockquote`
  - `@everyone`, `@here` (blue automatically)
  - `<@user>`, `<#channel>`, `<@&role>` mentions (auto-resolved to names via Discord API, blue)
  - `<:name:id>` custom emoji (animated + static)
  - Bare URLs auto-linked
- **Reactions** — add unlimited reactions to any message:
  - `Reaction(emoji="👍", count=3, reacted_by_me=True)` → blue (you reacted)
  - `Reaction(emoji="🔥", count=2, reacted_by_me=False)` → grey
  - Supports unicode emoji AND custom emoji (`<:name:id>`)
- **Replies** — `reply_to=Message(...)` renders the slim reply reference header with avatar, username, clan tag, and text preview
- **Mention highlight** — `mentioned=True` adds the yellow background + yellow bar on the left (like when someone mentions you)
- **Message grouping** — consecutive messages from the same author within 5 min are grouped
- **Quality control** — `quality=3.0` for 3x resolution, `quality=1.0` for low-RAM servers
- **Custom crop area** — `crop_x`, `crop_y`, `crop_width`, `crop_height` params (supports string aliases like `"box_x - 16"`)
- **Persistent Chromium** — browser launches once and stays open, so renders are ~0.4s instead of ~3s

### Friend requests
- The "Received — 1" header + card with avatar, display name, username subtext, Accept/Ignore buttons
- **No guild tag, no profile effect** — matches real Discord's friend request card

## API reference

### `Author`

| Field                    | Type             | Description                                            |
| ------------------------ | ---------------- | ------------------------------------------------------ |
| `id`                     | `str`            | User ID (used for default avatar fallback).            |
| `name`                   | `str`            | Raw username (e.g. `"cy.xn"`). Shows in reply previews & friend requests. |
| `display_name`           | `Optional[str]`  | Display name (e.g. `"F² Cyanic"`). Overrides `name` for display. |
| `color`                  | `Optional[str]`  | CSS color for the username (role color).               |
| `avatar_url`             | `Optional[str]`  | Avatar image URL.                                      |
| `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.                                  |
| `bot`                    | `bool`           | Show the "APP" badge.                                  |
| `verified_app`           | `bool`           | Add the checkmark to the APP badge.                    |

### `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).              |
| `reactions`             | `Optional[list]`     | List of `Reaction` objects.                              |
| `mentioned`             | `bool`               | If True, shows yellow highlight bar + background.        |

### `Reaction`

| Field           | Type   | Description                                          |
| --------------- | ------ | ---------------------------------------------------- |
| `emoji`         | `str`  | Unicode emoji (e.g. `"👍"`) or custom (`"<:name:123>"`). |
| `count`         | `int`  | Number of reactions. Default: 1.                     |
| `reacted_by_me` | `bool` | If True, reaction is blue. Default: False.           |

### `FriendRequest`

| Field    | Type      | Description                                              |
| -------- | --------- | -------------------------------------------------------- |
| `author` | `Author`  | The user who sent the request.                           |
| `count`  | `int`     | Number in the "Received — N" header. Default: 1.        |
| `subtitle` | `str`   | Custom subtitle. Defaults to the author's username.       |

### Functions

| Function | Description |
| -------- | ----------- |
| `render_html(messages, output_path=None)` | Render messages to standalone HTML. |
| `render_png(messages, output_path, *, quality=2.0, lite=False, persistent=True, crop_x=None, crop_y=None, crop_width=None, crop_height=None)` | Render messages to PNG via Chromium. |
| `render_friend_request_html(request, output_path=None)` | Render friend request card to HTML. |
| `render_friend_request_png(request, output_path, *, quality=2.0, ...)` | Render friend request card to PNG. |
| `fetch_messages(token, channel_id, message_id, ...)` | Fetch a real Discord message + context. |
| `render_markdown(text)` | Convert Discord markdown to HTML. |
| `set_discord_token(token)` | Set bot token for mention auto-resolution. |
| `shutdown_browser()` | Close the persistent Chromium instance. |

### Crop params

Crop params accept numbers (int/float) or string aliases:
```python
render_png(messages, "out.png",
    crop_x="box_x - 16",         # auto-detect element's X, minus 16px
    crop_y="box_y",               # auto-detect Y, no offset
    crop_width="box_width + 32",  # auto-detect width, plus 32px
    crop_height=200,              # literal number
)
```
If all crop params are `None` (default), auto-detects the message area.

## Optional dependencies

| Install command | What you get |
| --------------- | ------------ |
| `pip install dismessage` | HTML output only (zero deps) |
| `pip install dismessage[render]` | + PNG output (Playwright) |
| `pip install dismessage[fetch]` | + `fetch_messages()` (httpx) |
| `pip install dismessage[lite]` | + Pillow lite renderer |
| `pip install dismessage[all]` | Everything |

## FAQ

**Q: Why is the payload so big?**
A: It contains Discord's actual CSS (~4 MB uncompressed, ~800 KB gzip-compressed) + gg sans fonts (~640 KB). This is the price of pixel-perfect rendering — we use Discord's real stylesheets instead of trying to reimplement them. The payload is loaded once at import time and cached in memory.

**Q: Does this violate Discord's ToS?**
A: Probably not — it's a rendering library, not a self-bot. But don't use it to deceive people or impersonate others. Be normal.

**Q: Can I use this without Chromium?**
A: For HTML output, yes (zero dependencies). For PNG output, you need a browser engine — there's no way around it for Discord's complex CSS. The persistent browser makes it fast (~0.4s per render after warmup).

**Q: Will Discord break this?**
A: If Discord changes their CSS class names (which they do occasionally), the templates will need updating. Just re-export the page and rebuild the payload.

## License

MIT. Go wild.

---

<p align="center">
  <sub>built with blood, sweat, and a lot of <code>print()</code> debugging by <a href="#">F² Cyanic</a></sub><br>
  <sub>powered by Discord's actual CSS (we stole nothing, we just copied what your browser already downloaded)</sub><br>
  <sub>⚠️ WARNING: may cause confusion, arguments, and people asking "wait is that real?"</sub><br>
  <sub>🐱 meow</sub>
</p>
