{"id":"bfzdk/rCord","name":"rCord","scope":"bfzdk","platform":"roblox","description":"Discord webhook wrapper for Roblox","version":"0.1.4","latest":"0.1.4","versions":["0.1.0","0.1.1","0.1.2","0.1.3","0.1.4"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"f43e20aac0255a6ede6bb70ddf1c5dd51f53c84730dc904faa340a299fdee082","likes":2,"downloads":0,"install":"forest install bfzdk/rCord","url":"https://forest.dev/p/roblox/bfzdk/rCord","files":"https://api.forest.dev/ai/package/roblox/bfzdk/rCord/files","readme":"# rCord\n\nA Roblox Luau library for sending Discord webhook messages. Provides a chainable builder API for constructing messages and embeds, built-in validation against Discord's limits, and automatic rate limit handling.\n\n## Features\n\n- Chainable message and embed builders\n- Full embed support (fields, images, thumbnails, author, footer, etc.)\n- Validation against all Discord character/count limits before sending\n- Automatic retry on rate limit (429) responses\n- `Secret` type support for safe URL storage\n- `allowed_mentions` control to prevent accidental pings\n\n---\n\n## Proxy Requirement\n\nDiscord blocks incoming requests from Roblox's servers, so you cannot call the Discord webhook API directly. You must route requests through a proxy server that forwards them to Discord on your behalf.\n\nSet your webhook URL to your proxy's endpoint instead of the Discord URL:\n\n```lua\n-- Proxy URL — use this instead\nlocal webhook = rCord.Webhook.new(\"https://your-proxy.example.com/api/webhooks/1234/abcd\")\n```\n\nThe proxy should forward the request to Discord verbatim, preserving the method, headers, and body. A common self-hosted option is [discord-proxy](https://webhook.lewisakura.moe/) by lewisakura. For a free setup using Cloudflare Workers, see this [DevForum tutorial](https://devforum.roblox.com/t/discord-webhook-proxy-w-cloudflare-free/3469604/2).\n\n---\n\n## Quick Start\n\n```lua\nlocal rCord = require(path.to.rCord)\n\nlocal webhook = rCord.Webhook.new(\"https://your-proxy.example.com/api/webhooks/...\")\n\n-- Simple message\nwebhook:send(\"Hello from Roblox!\")\n\n-- Message with an embed\nlocal embed = rCord.Embed.new()\n    :setTitle(\"Round Over\")\n    :setDescription(\"The round has ended.\")\n    :setColor(Color3.fromRGB(88, 101, 242))\n\nlocal message = rCord.Message.new()\n    :setUsername(\"Game Bot\")\n    :addEmbed(embed)\n\nlocal success, response = webhook:send(message)\nif not success then\n    warn(response.error)\nend\n```\n\n---\n\n## API Reference\n\n### `rCord`\n\nThe top-level table returned by the module.\n\n| Export | Type | Description |\n|---|---|---|\n| `Webhook` | class | Creates and sends webhook messages |\n| `Message` | class | Builds a message payload |\n| `Embed` | class | Builds an embed object |\n| `Flags` | table | Named constants for message flags |\n| `setDebug(value)` | function | Enables/disables debug logging to output |\n\n#### `rCord.Flags`\n\n| Constant | Value | Description |\n|---|---|---|\n| `SUPPRESS_EMBEDS` | `4` | Prevents Discord from generating link previews |\n| `SUPPRESS_NOTIFICATIONS` | `4096` | Sends the message silently (no ping sound) |\n\n---\n\n### `Webhook`\n\n#### `Webhook.new(url: string | Secret) -> Webhook`\n\nCreates a new webhook instance.\n\n```lua\n-- Plain string\nlocal webhook = rCord.Webhook.new(\"https://your-proxy.example.com/api/webhooks/...\")\n\n-- Roblox Secret (recommended — keeps URL out of output)\nlocal webhook = rCord.Webhook.new(HttpService:GetSecret(\"DISCORD_WEBHOOK\"))\n```\n\n#### `webhook:send(body, wait?, thread_id?) -> (boolean, ResponseData)`\n\nSends a message. `body` can be a plain string or a `Message` object.\n\nAutomatically retries once if Discord responds with a 429 rate limit, waiting the duration Discord specifies.\n\n| Parameter | Type | Description |\n|---|---|---|\n| `body` | `string \\| Message` | The message to send. A plain string is wrapped in a Message automatically. |\n| `wait` | `boolean?` | If `true`, Discord waits for the message to be created before responding. Required if you need the message ID back. Defaults to `false`. |\n| `thread_id` | `string?` | Sends the message into a specific thread inside the webhook's channel. |\n\n```lua\nlocal success, response = webhook:send(\"Hello!\", true)\n\nif success then\n    print(response.statusCode)   -- 200\n    print(response.body.id)      -- message ID (body is auto-decoded from JSON)\nelse\n    warn(response.error)\nend\n```\n\n### `Message`\n\nBuilds the payload sent to Discord. All setters return `self` for chaining.\n\n#### `Message.new() -> Message`\n\n```lua\nlocal message = rCord.Message.new()\n```\n\n#### Setters\n\n| Method | Parameter | Discord limit | Description |\n|---|---|---|---|\n| `:setContent(content)` | `string` | 2000 chars | The text content of the message |\n| `:setUsername(username)` | `string` | — | Overrides the webhook's display name |\n| `:setAvatarUrl(url)` | `string` | — | Overrides the webhook's avatar |\n| `:setTTS(tts)` | `boolean` | — | Sends as a text-to-speech message |\n| `:setThreadName(name)` | `string` | — | Creates a new thread with this name (forum/media channels) |\n| `:setAllowedMentions(body)` | `AllowedMentions` | — | Controls which mentions actually ping |\n| `:setFlags(flags)` | `number` | — | Bitfield of message flags (use `rCord.Flags`) |\n| `:addEmbed(embed)` | `Embed \\| EmbedType` | 10 embeds | Adds an embed to the message |\n\n#### `message:validate() -> (boolean, string?)`\n\nValidates the message against Discord's limits. Called automatically by `send` — you only need this if you want to check before sending.\n\n```lua\nlocal ok, err = message:validate()\nif not ok then\n    warn(err)  -- e.g. \"over 2000 characters\"\nend\n```\n\n#### `message:toJSON() -> table`\n\nReturns a plain table representation suitable for JSON encoding. Called internally by `send`.\n\n#### `AllowedMentions` type\n\n```lua\nwebhook:send(\n    rCord.Message.new()\n        :setContent(\"Hey @everyone!\")\n        :setAllowedMentions({ parse = {} })  -- parse = {} suppresses all pings\n)\n```\n\n| Field | Type | Description |\n|---|---|---|\n| `parse` | `{\"roles\" \\| \"users\" \\| \"everyone\"}?` | Which mention types to parse. Omit a type to suppress it. |\n| `roles` | `{string}?` | Allowlist of role IDs to ping (max 100) |\n| `users` | `{string}?` | Allowlist of user IDs to ping (max 100) |\n| `replied_user` | `boolean?` | Whether to ping the user being replied to |\n\n---\n\n### `Embed`\n\nBuilds a Discord embed. All setters return `self` for chaining.\n\n#### `Embed.new() -> Embed`\n\n```lua\nlocal embed = rCord.Embed.new()\n```\n\n#### Setters\n\n| Method | Parameter | Discord limit | Description |\n|---|---|---|---|\n| `:setTitle(title)` | `string` | 256 chars | Bold title at the top of the embed |\n| `:setDescription(description)` | `string` | 4096 chars | Main body text |\n| `:setUrl(url)` | `string` | — | Makes the title a hyperlink |\n| `:setTimestamp(timestamp)` | `string` | — | ISO 8601 timestamp shown in the footer |\n| `:setColor(color)` | `number \\| Color3` | — | Left-side accent colour. Accepts a `Color3` or a decimal integer |\n| `:setFooter(body)` | `EmbedFooter` | 2048 chars (text) | Footer text and optional icon |\n| `:setImage(body)` | `EmbedImage` | — | Large image at the bottom |\n| `:setThumbnail(body)` | `EmbedThumbnail` | — | Small image on the right |\n| `:setAuthor(body)` | `EmbedAuthor` | 256 chars (name) | Author line at the top |\n| `:setProvider(body)` | `EmbedProvider` | — | Provider info (usually set by Discord, not bots) |\n| `:setType(type)` | `\"rich\" \\| \"image\" \\| ...` | — | Embed type. Use `\"rich\"` for custom embeds |\n| `:addField(body)` | `EmbedField` | 25 fields, 256/1024 chars | Adds a name/value field |\n\n#### `:addField(body: EmbedField)`\n\n```lua\nembed:addField({\n    name = \"Score\",\n    value = \"1500\",\n    inline = true,\n})\n```\n\n| Field | Type | Limit | Description |\n|---|---|---|---|\n| `name` | `string` | 256 chars | Field label |\n| `value` | `string` | 1024 chars | Field content |\n| `inline` | `boolean?` | — | Whether to display side-by-side with adjacent inline fields |\n\n#### `:setColor(color: number | Color3)`\n\nBoth forms are accepted:\n\n```lua\nembed:setColor(0x5865F2)                     -- hex integer\nembed:setColor(Color3.fromRGB(88, 101, 242)) -- Color3\n```\n\n#### `:setFooter(body: EmbedFooter)`\n\n```lua\nembed:setFooter({ text = \"rCord\", icon_url = \"https://...\" })\n```\n\n#### `:setAuthor(body: EmbedAuthor)`\n\n```lua\nembed:setAuthor({ name = \"PlayerName\", icon_url = \"https://...\", url = \"https://...\" })\n```\n\n#### `:setImage(body: EmbedImage)` / `:setThumbnail(body: EmbedThumbnail)`\n\n```lua\nembed:setImage({ url = \"https://...\" })\nembed:setThumbnail({ url = \"https://...\", width = 64, height = 64 })\n```\n\n#### `embed:validate() -> (boolean, string?)`\n\nValidates all fields against Discord's character limits. Called automatically via `message:validate()`.\n\n#### `embed:getCharacters() -> number`\n\nReturns the total character count of the embed (title + description + footer text + author name + all field names and values). Discord's combined limit across all embeds in a message is 6000.\n\n---\n\n### `ResponseData`\n\nReturned as the second value from `webhook:send()`.\n\n| Field | Type | Description |\n|---|---|---|\n| `success` | `boolean` | Whether Discord accepted the message (`2xx` status) |\n| `statusCode` | `number` | HTTP status code |\n| `statusMessage` | `string` | HTTP status message |\n| `body` | `any?` | Decoded response body. Contains the created message object as a table when `wait = true`. `nil` when Discord returns no content (e.g. `wait = false`). Falls back to the raw string if the body is not valid JSON. |\n| `retry_after` | `number?` | Seconds Discord asked to wait (present on 429 responses) |\n| `error` | `string?` | Error description when the request or validation failed |\n\n---\n\n## Examples\n\n### Logging a player event\n\n```lua\nlocal rCord = require(path.to.rCord)\nlocal webhook = rCord.Webhook.new(HttpService:GetSecret(\"LOG_WEBHOOK\"))\n\nlocal function logPlayerJoin(player)\n    local embed = rCord.Embed.new()\n        :setAuthor({ name = player.Name })\n        :setTitle(\"Player Joined\")\n        :setColor(Color3.fromRGB(87, 242, 135))\n        :setTimestamp(DateTime.now():ToIsoDate())\n\n    webhook:send(rCord.Message.new():addEmbed(embed))\nend\n```\n\n### Silent notification with suppressed pings\n\n```lua\nlocal message = rCord.Message.new()\n    :setContent(\"Server restarting in 60 seconds.\")\n    :setAllowedMentions({ parse = {} })\n    :setFlags(rCord.Flags.SUPPRESS_NOTIFICATIONS)\n\nwebhook:send(message)\n```\n\n### Multiple inline fields\n\n```lua\nlocal embed = rCord.Embed.new()\n    :setTitle(\"Match Results\")\n    :addField({ name = \"Winner\", value = \"TeamA\", inline = true })\n    :addField({ name = \"Score\", value = \"5 - 2\", inline = true })\n    :addField({ name = \"Duration\", value = \"12m 34s\", inline = true })\n    :setColor(0xFEE75C)\n\nwebhook:send(rCord.Message.new():addEmbed(embed))\n```\n\n### Checking the response\n\n```lua\nlocal success, response = webhook:send(\"Test\", true)\n\nif not success then\n    warn(\"[rCord]\", response.error, response.statusCode)\n    return\nend\n\n-- body is already decoded — no JSONDecode needed\nprint(\"Message ID:\", response.body.id)\n```\n","readmeTruncated":false}