-
Star
(131)
You must be signed in to star a gist -
Fork
(33)
You must be signed in to fork a gist
-
-
Save Mo45/cb0813cb8a6ebcd6524f6a36d4f8862c to your computer and use it in GitHub Desktop.
| <?php | |
| /** | |
| * discord.msg.send.php v0.8 | |
| * Kirill Krasin © 2025 | |
| * https://github.com/Mo45 | |
| * | |
| * For revisions and comments vist: https://gist.github.com/Mo45/cb0813cb8a6ebcd6524f6a36d4f8862c | |
| * | |
| * Sends a message to Discord via Webhook with file support and rate limit handling | |
| * | |
| * This function handles both simple messages and complex embeds with attachments, | |
| * automatically retrying on rate limits and validating file requirements. | |
| * | |
| * Create new webhook in your Discord channel settings and copy&paste URL into $webhookUrl. | |
| * | |
| * You can use Markdown in your message. More about formatting: https://discordapp.com/developers/docs/reference#message-formatting | |
| * | |
| * @param string $webhookUrl Full Discord webhook URL (from channel settings) | |
| * @param array $payload Message data array (content, embeds, username, etc.) | |
| * @param string|null $filePath Absolute path to file for attachment (optional) | |
| * @param int $maxRetries Maximum retry attempts on rate limits (default: 3) | |
| * @return bool True on success, false on failure (errors logged) | |
| */ | |
| function sendDiscordWebhook(string $webhookUrl, array $payload, ?string $filePath = null, int $maxRetries = 3): bool | |
| { | |
| // ================================================================= | |
| // FILE VALIDATION CHECKS | |
| // ================================================================= | |
| // Verify file existence before processing | |
| if ($filePath && !file_exists($filePath)) { | |
| error_log("File not found: $filePath"); | |
| return false; | |
| } | |
| // Enforce Discord's file size limit (8MB standard servers, 50MB boosted) | |
| if ($filePath && filesize($filePath) > 8 * 1024 * 1024) { | |
| error_log("File size exceeds Discord limit: $filePath"); | |
| return false; | |
| } | |
| $retryCount = 0; | |
| // ================================================================= | |
| // RATE LIMIT RETRY LOOP | |
| // ================================================================= | |
| while ($retryCount <= $maxRetries) { | |
| $ch = curl_init($webhookUrl); | |
| // ============================================================= | |
| // CURL BASE CONFIGURATION | |
| // ============================================================= | |
| $options = [ | |
| CURLOPT_FOLLOWLOCATION => true, // Follow redirects (301/302) | |
| CURLOPT_HEADER => true, // Include headers in response | |
| CURLOPT_RETURNTRANSFER => true, // Return transfer instead of output | |
| CURLOPT_TIMEOUT => 120, // Maximum execution time (seconds) | |
| CURLOPT_SSL_VERIFYPEER => true, // Verify SSL certificate | |
| CURLOPT_POST => true, // Always use POST method | |
| ]; | |
| // ============================================================= | |
| // PAYLOAD PREPARATION: FILE ATTACHMENT MODE | |
| // ============================================================= | |
| if ($filePath) { | |
| // Generate unique boundary for multipart separation | |
| $boundary = '----DiscordWebhookBoundary' . uniqid(); | |
| // Encode metadata payload to JSON | |
| $payloadJson = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); | |
| // ========================================================= | |
| // MULTIPART REQUEST BODY CONSTRUCTION | |
| // ========================================================= | |
| // Part 1: JSON metadata payload | |
| $body = "--$boundary\r\n"; | |
| $body .= "Content-Disposition: form-data; name=\"payload_json\"\r\n"; | |
| $body .= "Content-Type: application/json\r\n\r\n"; | |
| $body .= $payloadJson . "\r\n"; | |
| // Part 2: File attachment | |
| $body .= "--$boundary\r\n"; | |
| $body .= "Content-Disposition: form-data; name=\"file\"; filename=\"" . basename($filePath) . "\"\r\n"; | |
| $body .= "Content-Type: " . mime_content_type($filePath) . "\r\n\r\n"; | |
| $body .= file_get_contents($filePath) . "\r\n"; | |
| // End boundary marker | |
| $body .= "--$boundary--\r\n"; | |
| // ========================================================= | |
| // CURL CONFIG FOR FILE UPLOAD | |
| // ========================================================= | |
| $options[CURLOPT_HTTPHEADER] = [ | |
| "Content-Type: multipart/form-data; boundary=$boundary", | |
| "Content-Length: " . strlen($body) | |
| ]; | |
| $options[CURLOPT_POSTFIELDS] = $body; | |
| } | |
| // ============================================================= | |
| // PAYLOAD PREPARATION: JSON-ONLY MODE | |
| // ============================================================= | |
| else { | |
| // Standard JSON request configuration | |
| $options[CURLOPT_HTTPHEADER] = ['Content-Type: application/json']; | |
| $options[CURLOPT_POSTFIELDS] = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); | |
| } | |
| // Apply all configured options | |
| curl_setopt_array($ch, $options); | |
| // ============================================================= | |
| // EXECUTE CURL REQUEST | |
| // ============================================================= | |
| $response = curl_exec($ch); | |
| // ============================================================= | |
| // CURL ERROR HANDLING | |
| // ============================================================= | |
| if ($response === false) { | |
| $error = curl_error($ch); | |
| $errno = curl_errno($ch); | |
| curl_close($ch); | |
| error_log("cURL error ($errno): $error"); | |
| return false; | |
| } | |
| // ============================================================= | |
| // RESPONSE PROCESSING | |
| // ============================================================= | |
| $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); | |
| $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); | |
| $responseHeaders = substr($response, 0, $headerSize); | |
| $responseBody = substr($response, $headerSize); | |
| curl_close($ch); | |
| // ============================================================= | |
| // SUCCESSFUL DELIVERY (2xx STATUS) | |
| // ============================================================= | |
| if ($httpCode >= 200 && $httpCode < 300) { | |
| return true; | |
| } | |
| // ============================================================= | |
| // RATE LIMIT HANDLING (429 TOO MANY REQUESTS) | |
| // ============================================================= | |
| if ($httpCode === 429) { | |
| $retryAfter = 1; // Default cooldown (in seconds) | |
| // Parse Retry-After header (seconds to wait) | |
| if (preg_match('/retry-after: (\d+)/i', $responseHeaders, $matches)) { | |
| $retryAfter = (int)$matches[1]; | |
| } | |
| // Fallback to Discord's rate-limit header | |
| elseif (preg_match('/x-ratelimit-reset-after: (\d+\.?\d*)/i', $responseHeaders, $matches)) { | |
| $retryAfter = (float)$matches[1]; | |
| } | |
| error_log("Rate limit hit. Retrying after {$retryAfter} seconds"); | |
| sleep(ceil($retryAfter) + 1); // Add safety buffer | |
| $retryCount++; | |
| continue; | |
| } | |
| // ============================================================= | |
| // ERROR LOGGING AND ANALYSIS | |
| // ============================================================= | |
| $errorInfo = [ | |
| 'http_code' => $httpCode, | |
| 'response' => $responseBody, | |
| 'headers' => $responseHeaders, | |
| 'file' => $filePath ? basename($filePath) : 'none' | |
| ]; | |
| error_log("Discord API error: " . print_r($errorInfo, true)); | |
| // ============================================================= | |
| // FATAL ERROR HANDLING (4xx CLIENT ERRORS) | |
| // ============================================================= | |
| // Don't retry on permanent client errors (except 429) | |
| if ($httpCode >= 400 && $httpCode < 500 && $httpCode !== 429) { | |
| return false; | |
| } | |
| $retryCount++; | |
| } | |
| // ================================================================= | |
| // FINAL FAILURE AFTER RETRIES | |
| // ================================================================= | |
| error_log("Failed after $maxRetries attempts"); | |
| return false; | |
| } | |
| // ===================================================================== | |
| // PASTE YOUR WEBHOOK URL HERE | |
| // ===================================================================== | |
| $webhookUrl = "YOUR_WEBHOOK_URL"; | |
| // ===================================================================== | |
| // MESSAGE CONFIGURATION | |
| // ===================================================================== | |
| // Base message parameters | |
| $message = [ | |
| // Text content | |
| 'content' => "This is message with mention, use userID <@12341234123412341>", | |
| // Custom username | |
| 'username' => "php-msg-bot", | |
| // Text-to-speech | |
| 'tts' => false, | |
| // Mention permissions | |
| 'allowed_mentions' => [ | |
| 'parse' => ['users'] // Allow @user mentions | |
| ] | |
| ]; | |
| // ===================================================================== | |
| // EMBED CONSTRUCTION | |
| // ===================================================================== | |
| $embed = [ | |
| 'title' => "Annotated Discord Webhook", // Embed title | |
| 'type' => "rich", // Embed type | |
| 'description' => "Embed with detailed documentation", // Main text | |
| 'url' => "https://gist.github.com/Mo45/cb0813cb8a6ebcd6524f6a36d4f8862c", //URL of title link | |
| 'timestamp' => date(DATE_ATOM), // ISO 8601 timestamp | |
| 'color' => hexdec("3366ff"), // Left border color | |
| 'footer' => [ // Footer section | |
| 'text' => "GitHub.com/Mo45", | |
| 'icon_url' => "https://avatars.githubusercontent.com/u/4648775" | |
| ], | |
| 'author' => [ // Author section | |
| 'name' => "Mo45", | |
| 'url' => "https://github.com/Mo45" | |
| ], | |
| 'fields' => [ // Data fields | |
| [ | |
| 'name' => "New Feature", | |
| 'value' => "File upload", | |
| 'inline' => true | |
| ], | |
| [ | |
| 'name' => "Status", | |
| 'value' => "Completed", | |
| 'inline' => true | |
| ] | |
| ] | |
| ]; | |
| // Attach embed to message | |
| $message['embeds'] = [$embed]; | |
| // ===================================================================== | |
| // FILE ATTACHMENT (OPTIONAL) | |
| // ===================================================================== | |
| // Uncomment to enable file upload: | |
| // $filePath = __DIR__ . '/test.jpg'; | |
| // ===================================================================== | |
| // EXECUTION | |
| // ===================================================================== | |
| $result = sendDiscordWebhook( | |
| webhookUrl: $webhookUrl, | |
| payload: $message, | |
| filePath: $filePath ?? null, // Pass null if no file | |
| maxRetries: 5 | |
| ); | |
| if ($result) { | |
| echo "Message delivered successfully!"; | |
| } else { | |
| echo "Delivery failed. Check error logs for details."; | |
| } |
Did you ever figure out how to make it work on submit?
Did you ever figure out how to make it work on submit?
you can using this endpoint API : /channels/ {channel_id {/messages}
I just created a package for this.
https://packagist.org/packages/atakde/discord-webhook-php
https://github.com/atakde/discord-webhook-php
I just created a package for this. https://packagist.org/packages/atakde/discord-webhook-php https://github.com/atakde/discord-webhook-php
How do you do embeds? Your documentation does not cover them.
I just created a package for this. https://packagist.org/packages/atakde/discord-webhook-php https://github.com/atakde/discord-webhook-php
How do you do embeds? Your documentation does not cover them.
You are right, actually we don't have documentation. I will prepare :)
You can use like that;
$messageFactory = new MessageFactory();
$message= $messageFactory->create('embed');https://github.com/atakde/discord-webhook-php/blob/master/src/Message/EmbedMessage.php
I just created a package for this. https://packagist.org/packages/atakde/discord-webhook-php https://github.com/atakde/discord-webhook-php
How do you do embeds? Your documentation does not cover them.
You are right, actually we don't have documentation. I will prepare :)
You can use like that;
$messageFactory = new MessageFactory(); $message= $messageFactory->create('embed');https://github.com/atakde/discord-webhook-php/blob/master/src/Message/EmbedMessage.php
I tried to use it with fields and it just wouldn't work. I ended up going with my own solution.
I just created a package for this. https://packagist.org/packages/atakde/discord-webhook-php https://github.com/atakde/discord-webhook-php
How do you do embeds? Your documentation does not cover them.
You are right, actually we don't have documentation. I will prepare :)
You can use like that;$messageFactory = new MessageFactory(); $message= $messageFactory->create('embed');https://github.com/atakde/discord-webhook-php/blob/master/src/Message/EmbedMessage.php
I tried to use it with fields and it just wouldn't work. I ended up going with my own solution.
$embedMessage = $messageFactory->create('embed');
$embedMessage->setTitle("Title");
$embedMessage->setAuthorUrl("https://doodleipsum.com/700?i=f8b1abea359b643310916a38aa0b0562");
$embedMessage->setAuthorIcon("https://doodleipsum.com/700?i=f8b1abea359b643310916a38aa0b0562");
$embedMessage->setFields([
[
'name' => 'Field 1',
'value' => 'Value 1',
'inline' => true
],
[
'name' => 'Field 2',
'value' => 'Value 2',
'inline' => false
]
]);
$webhook = new DiscordWebhook($embedMessage);
$webhook->setWebhookUrl("YOUR_WEBHOOK_URL");;
$res = $webhook->send();I tested in my end and it is working. Feel free to mail me the issue. This is the example that I tested.
works well thanks <3
can confirm this works , thanks
Updated code. Key features in this version (v0.8):
- File Validation
- Checks file existence before processing & enforces Discord's 8MB file size limit
- Rate Limit Handling
- Automatic retry loop with configurable attempts & Discord's Retry-After header parsing
- Multipart Request
- Proper CRLF line endings (RFC-compliant)
- Combines JSON metadata + binary file
- And other cool stuff like SSL verification, error handling, ISO 8601 timestamp
Find this code useful? Feel free to donate.
Yep, Discord accepts most if not all file types.