Created
May 27, 2026 18:09
-
-
Save TheWhiteJZ/2a8e1f62cc8457ca7558522f655a62ec to your computer and use it in GitHub Desktop.
Forward messages to Discord via webhook (stdlib only, no dependencies)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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