Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save TheWhiteJZ/2a8e1f62cc8457ca7558522f655a62ec to your computer and use it in GitHub Desktop.

Select an option

Save TheWhiteJZ/2a8e1f62cc8457ca7558522f655a62ec to your computer and use it in GitHub Desktop.
Forward messages to Discord via webhook (stdlib only, no dependencies)
#!/usr/bin/env python3
"""
discord_notify.py — Send a message to a Discord channel via webhook.
No third-party dependencies required (uses stdlib only).
Usage
-----
1. Set your webhook URL in an environment variable:
export DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/..."
2. Call from Python:
from discord_notify import notify
notify("Hello from my project!")
3. Or run directly from the command line:
python discord_notify.py "Hello from the CLI"
python discord_notify.py # prompts for message
Environment variables
---------------------
DISCORD_WEBHOOK_URL – required: full Discord webhook URL
(can also be passed directly via the webhook_url param)
Rate limits
-----------
Discord allows ~30 messages/minute per webhook.
Embeds are supported via the `embeds` param (list of embed dicts).
See: https://discord.com/developers/docs/resources/webhook
"""
from __future__ import annotations
import json
import os
import sys
import urllib.error
import urllib.request
from typing import Any
def notify(
content: str = "",
*,
webhook_url: str | None = None,
username: str | None = None,
avatar_url: str | None = None,
embeds: list[dict[str, Any]] | None = None,
tts: bool = False,
) -> bool:
"""
Send a message to Discord via a webhook.
Parameters
----------
content : The plain-text message body (max 2000 chars).
webhook_url : Override the DISCORD_WEBHOOK_URL env var.
username : Override the webhook's default display name.
avatar_url : Override the webhook's default avatar.
embeds : List of Discord embed objects (max 10).
Example: [{"title": "Alert", "description": "...", "color": 0xFF0000}]
tts : Whether to send as a text-to-speech message.
Returns
-------
True on success, False on failure (errors are printed to stderr).
"""
url = webhook_url or os.getenv("DISCORD_WEBHOOK_URL", "")
if not url:
print(
"ERROR: No webhook URL provided. "
"Set DISCORD_WEBHOOK_URL or pass webhook_url=...",
file=sys.stderr,
)
return False
payload: dict[str, Any] = {"tts": tts}
if content:
payload["content"] = content[:2000] # Discord hard limit
if username:
payload["username"] = username
if avatar_url:
payload["avatar_url"] = avatar_url
if embeds:
payload["embeds"] = embeds[:10] # Discord hard limit
if not payload.get("content") and not payload.get("embeds"):
print("ERROR: Provide content and/or at least one embed.", file=sys.stderr)
return False
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
url,
data=data,
headers={
"Content-Type": "application/json",
"User-Agent": "discord-notify-gist/1.0",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=10) as resp:
# 204 No Content is the normal success response
if resp.status in (200, 204):
return True
print(f"WARNING: Unexpected status {resp.status}", file=sys.stderr)
return True
except urllib.error.HTTPError as exc:
if exc.code == 204:
return True # some versions raise on 204
body = exc.read().decode("utf-8", errors="replace")
print(f"ERROR: Discord webhook returned HTTP {exc.code}: {body}", file=sys.stderr)
return False
except OSError as exc:
print(f"ERROR: Network error sending Discord message: {exc}", file=sys.stderr)
return False
def notify_embed(
title: str,
description: str = "",
*,
color: int = 0x5865F2, # Discord blurple
fields: list[dict[str, Any]] | None = None,
footer: str | None = None,
url: str | None = None,
webhook_url: str | None = None,
username: str | None = None,
) -> bool:
"""
Convenience wrapper — sends a single rich embed.
Parameters
----------
title : Embed title (max 256 chars).
description : Embed body text (max 4096 chars).
color : Left-border color as an integer (e.g. 0xFF0000 for red).
fields : List of {"name": "...", "value": "...", "inline": True/False}.
footer : Footer text.
url : Makes the title a clickable hyperlink.
webhook_url : Override DISCORD_WEBHOOK_URL env var.
username : Override webhook display name.
"""
embed: dict[str, Any] = {"title": title[:256], "color": color}
if description:
embed["description"] = description[:4096]
if url:
embed["url"] = url
if fields:
embed["fields"] = fields[:25] # Discord hard limit
if footer:
embed["footer"] = {"text": footer[:2048]}
return notify(embeds=[embed], webhook_url=webhook_url, username=username)
# ---------------------------------------------------------------------------
# CLI entry-point
# ---------------------------------------------------------------------------
if __name__ == "__main__":
if len(sys.argv) > 1:
msg = " ".join(sys.argv[1:])
else:
msg = input("Message to send to Discord: ").strip()
if not msg:
print("No message provided.", file=sys.stderr)
sys.exit(1)
success = notify(msg)
sys.exit(0 if success else 1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment