-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add Discord Bot MCP server and tools #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughAdds a new discord-bot workspace implementing a Deco/Cloudflare Workers MCP server: project configuration and build files, TypeScript types and Zod schemas for Discord, a typed Discord REST client, signature verification and webhook handling, and multiple MCP tool implementations for messages, channels, guilds, roles, threads, and webhooks. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as MCP Client
participant Runtime as Deco Runtime
participant Tool as Private Tool
participant DClient as Discord Client
participant API as Discord API (api/v10)
participant Webhook as Webhook Handler
Client->>Runtime: Invoke MCP tool
Runtime->>Tool: Execute with input
Tool->>Tool: Validate input (Zod)
Tool->>DClient: createDiscordClient(botToken)
Tool->>DClient: Call method (e.g., sendMessage)
DClient->>API: HTTP request (auth headers, JSON)
API-->>DClient: Response
DClient->>Tool: Return parsed result
Tool->>Tool: Map & validate output (Zod)
Tool-->>Runtime: Return result
Runtime-->>Client: Response
note over Webhook, Runtime: Incoming Discord interaction flow
Webhook->>Webhook: extractSignatureHeaders & verifyDiscordSignature
Webhook->>Runtime: dispatch to appropriate handler (commands/components)
Webhook->>API: (async) follow-up via webhook or bot API
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Tip 📝 Customizable high-level summaries are now available in beta!You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.
Example instruction:
Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🚀 Preview Deployments Ready!Your changes have been deployed to preview environments: 📦
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
🧹 Nitpick comments (17)
discord-bot/server/lib/constants.ts (1)
3-24: Consider i18n support for error messages.All error messages are hardcoded in Portuguese. While this may be intentional for the target audience, consider:
- Adding an i18n/localization layer if the bot will serve multiple locales
- Documenting the locale choice in comments if Portuguese is the permanent default
discord-bot/server/tools/roles.ts (3)
48-55: Use typed interface instead ofanyfor request body.The body object is typed as
any, which bypasses TypeScript's type safety. Consider creating a typed interface for the role creation payload.Apply this pattern:
- const body: any = { + interface CreateRoleBody { + name?: string; + permissions?: string; + color?: number; + hoist: boolean; + mentionable: boolean; + } + + const body: CreateRoleBody = { hoist, mentionable, };
93-99: Use typed interface instead ofanyfor request body.Similar to the create tool, the edit body should be typed for better type safety.
175-184: Type the response mapping with explicit interface.The
.map()callback usesanyfor the role parameter. Consider adding an explicit type or interface for Discord role objects to improve type safety and catch potential field mismatches.discord-bot/server/tools/channels.ts (2)
50-68: Use typed interface instead ofanyfor request body.The body object is typed as
any. Consider creating a typed interface for the channel creation payload to leverage TypeScript's type checking.- const body: any = { + interface CreateChannelBody { + name: string; + type: number; + nsfw: boolean; + topic?: string; + parent_id?: string; + position?: number; + bitrate?: number; + user_limit?: number; + rate_limit_per_user?: number; + } + + const body: CreateChannelBody = { name, type, nsfw, };
104-109: Type the response mapping with explicit interface.The
.map()callback usesanyfor the channel parameter. Adding explicit typing would catch potential field mismatches at compile time.discord-bot/server/tools/webhooks.ts (3)
45-51: Use typed interface instead ofanyfor request body.Consistent with other tools, the webhook body should be typed for better type safety.
104-116: Use typed interface instead ofanyfor request body.The execute webhook body should use a typed interface to ensure all fields match the Discord API specification.
217-223: Type the response mapping with explicit interface.The
.map()callback usesanyfor the webhook parameter. Adding explicit typing would improve type safety.discord-bot/server/tools/guilds.ts (3)
56-62: Type the response mapping with explicit interface.The
.map()callback usesanyfor the guild parameter. Consider adding explicit typing for better type safety.
132-143: Type the response mapping with explicit interface.The nested
.map()usesanyfor the member parameter. Adding explicit types for Discord guild member objects would improve type safety and documentation.
172-178: Use typed interface instead ofanyfor request body.The ban body should use a typed interface to ensure the payload matches Discord's API specification.
discord-bot/server/tools/utils/discord-client.ts (1)
18-62: Consider extracting error handling to reduce duplication.The error handling logic on lines 47-52 is separate from
makeApiRequest, which likely has its own error handling. This creates two different error message formats and duplication. Consider refactoring to unify error handling.discord-bot/server/tools/messages.ts (2)
70-89: Consider usingSendMessageBodytype instead ofany.The request body is typed as
any, which reduces type safety. TheSendMessageBodyinterface is available inlib/types.ts(lines 549-563) and could be used here to ensure the body conforms to the expected structure.Apply this diff to improve type safety:
- const body: any = { + const body: Partial<SendMessageBody> = { tts, };Also add the import at the top:
import { sendMessageInputSchema, sendMessageOutputSchema, + type SendMessageBody, // ... other imports } from "../lib/types.ts";
223-233: Prefer typed response mapping overany.The message mapping uses
anytype for themsgparameter. Consider using theDiscordMessageinterface fromlib/types.tsfor better type safety and IDE support.Apply this diff:
+import type { DiscordMessage } from "../lib/types.ts"; return { - messages: messages.map((msg: any) => ({ + messages: (messages as DiscordMessage[]).map((msg) => ({ id: msg.id,This same pattern applies to lines 372 (users mapping) and 462 (pinned messages mapping).
discord-bot/server/tools/threads.ts (2)
53-64: Consider usingCreateThreadBodytype instead ofany.Similar to the message tools, the request body uses
anytype. TheCreateThreadBodyinterface is available inlib/types.ts(lines 585-596) and should be used for type safety.Apply this diff:
+import type { CreateThreadBody } from "../lib/types.ts"; - const body: any = { + const body: Partial<CreateThreadBody> = { name, };
158-163: Same type safety concern with response mapping.Similar to the messages tools, the thread mapping uses
anytype. Consider usingDiscordChannelinterface (since threads are a type of channel in Discord's API) for better type safety. This applies to both lines 158 and 201.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (19)
discord-bot/.gitignore(1 hunks)discord-bot/README.md(1 hunks)discord-bot/package.json(1 hunks)discord-bot/server/lib/constants.ts(1 hunks)discord-bot/server/lib/types.ts(1 hunks)discord-bot/server/main.ts(1 hunks)discord-bot/server/tools/channels.ts(1 hunks)discord-bot/server/tools/guilds.ts(1 hunks)discord-bot/server/tools/index.ts(1 hunks)discord-bot/server/tools/messages.ts(1 hunks)discord-bot/server/tools/roles.ts(1 hunks)discord-bot/server/tools/threads.ts(1 hunks)discord-bot/server/tools/utils/discord-client.ts(1 hunks)discord-bot/server/tools/webhooks.ts(1 hunks)discord-bot/shared/deco.gen.ts(1 hunks)discord-bot/tsconfig.json(1 hunks)discord-bot/vite.config.ts(1 hunks)discord-bot/wrangler.toml(1 hunks)package.json(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (11)
discord-bot/server/tools/webhooks.ts (3)
discord-bot/server/main.ts (1)
Env(33-38)discord-bot/server/lib/types.ts (8)
createWebhookInputSchema(1098-1102)createWebhookOutputSchema(1104-1110)executeWebhookInputSchema(1112-1132)executeWebhookOutputSchema(1134-1134)deleteWebhookInputSchema(1136-1139)deleteWebhookOutputSchema(1141-1144)listWebhooksInputSchema(1146-1155)listWebhooksOutputSchema(1157-1159)discord-bot/server/tools/utils/discord-client.ts (1)
createDiscordClient(67-257)
discord-bot/server/tools/guilds.ts (4)
discord-bot/server/main.ts (1)
Env(33-38)discord-bot/server/lib/types.ts (12)
listBotGuildsInputSchema(855-874)listBotGuildsOutputSchema(876-886)getGuildInputSchema(888-894)getGuildOutputSchema(896-903)getGuildMembersInputSchema(905-917)getGuildMembersOutputSchema(919-934)banMemberInputSchema(936-946)banMemberOutputSchema(948-951)getCurrentUserInputSchema(953-953)getCurrentUserOutputSchema(955-960)getUserInputSchema(962-964)getUserOutputSchema(966-966)discord-bot/server/tools/utils/discord-client.ts (1)
createDiscordClient(67-257)discord-bot/server/tools/index.ts (1)
guildTools(43-43)
discord-bot/server/tools/roles.ts (4)
discord-bot/server/main.ts (1)
Env(33-38)discord-bot/server/lib/types.ts (8)
createRoleInputSchema(972-982)createRoleOutputSchema(984-993)editRoleInputSchema(995-1003)editRoleOutputSchema(1005-1005)deleteRoleInputSchema(1007-1011)deleteRoleOutputSchema(1013-1016)getGuildRolesInputSchema(1018-1020)getGuildRolesOutputSchema(1022-1024)discord-bot/server/tools/utils/discord-client.ts (1)
createDiscordClient(67-257)discord-bot/server/tools/index.ts (1)
roleTools(44-44)
discord-bot/server/tools/threads.ts (4)
discord-bot/server/main.ts (1)
Env(33-38)discord-bot/server/lib/types.ts (10)
createThreadInputSchema(1030-1046)createThreadOutputSchema(1048-1053)joinThreadInputSchema(1055-1057)joinThreadOutputSchema(1059-1062)leaveThreadInputSchema(1064-1064)leaveThreadOutputSchema(1065-1065)getActiveThreadsInputSchema(1067-1069)getActiveThreadsOutputSchema(1071-1073)getArchivedThreadsInputSchema(1075-1090)getArchivedThreadsOutputSchema(1092-1092)discord-bot/server/tools/utils/discord-client.ts (1)
createDiscordClient(67-257)discord-bot/server/tools/index.ts (1)
threadTools(45-45)
discord-bot/server/tools/channels.ts (4)
discord-bot/server/main.ts (1)
Env(33-38)discord-bot/server/lib/types.ts (4)
createChannelInputSchema(814-834)createChannelOutputSchema(836-841)getGuildChannelsInputSchema(843-845)getGuildChannelsOutputSchema(847-849)discord-bot/server/tools/utils/discord-client.ts (1)
createDiscordClient(67-257)discord-bot/server/tools/index.ts (1)
channelTools(42-42)
discord-bot/server/tools/index.ts (6)
discord-bot/server/tools/messages.ts (1)
messageTools(484-496)discord-bot/server/tools/channels.ts (1)
channelTools(121-124)discord-bot/server/tools/guilds.ts (1)
guildTools(257-264)discord-bot/server/tools/roles.ts (1)
roleTools(196-201)discord-bot/server/tools/threads.ts (1)
threadTools(218-224)discord-bot/server/tools/webhooks.ts (1)
webhookTools(235-240)
discord-bot/shared/deco.gen.ts (1)
discord-bot/server/main.ts (2)
StateSchema(18-24)Env(33-38)
discord-bot/server/main.ts (1)
discord-bot/shared/deco.gen.ts (2)
StateSchema(21-21)Env(23-26)
discord-bot/server/tools/utils/discord-client.ts (1)
shared/tools/utils/api-client.ts (1)
makeApiRequest(27-39)
discord-bot/server/tools/messages.ts (3)
discord-bot/server/main.ts (1)
Env(33-38)discord-bot/server/tools/utils/discord-client.ts (1)
createDiscordClient(67-257)discord-bot/server/tools/index.ts (1)
messageTools(41-41)
discord-bot/vite.config.ts (1)
shared/deco-vite-plugin.ts (1)
deco(89-146)
🪛 LanguageTool
discord-bot/README.md
[locale-violation] ~3-~3: “server” é um estrangeirismo. É preferível dizer “servidor”.
Context: ...d Bot MCP MCP (Model Context Protocol) server para integração com Discord Bot API. #...
(PT_BARBARISMS_REPLACE_SERVER)
[typographical] ~53-~53: Símbolo sem par: “]” aparentemente está ausente
Context: ... ### Pré-requisitos 1. Criar um bot no [Discord Developer Portal](https://discor...
(UNPAIRED_BRACKETS)
[uncategorized] ~60-~60: Esta locução deve ser separada por vírgulas.
Context: ...o seu workspace Deco 2. Configure o Bot Token quando solicitado ### Permissões Neces...
(VERB_COMMA_CONJUNCTION)
🪛 markdownlint-cli2 (0.18.1)
discord-bot/README.md
93-93: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🔇 Additional comments (16)
discord-bot/.gitignore (1)
1-1: LGTM!Correctly ignores
.dev.vars, which is the standard file for local environment variables in Cloudflare Workers projects.package.json (1)
23-23: LGTM!The new workspace entry is correctly added and maintains the alphabetical ordering of the workspaces array.
discord-bot/shared/deco.gen.ts (1)
1-28: LGTM!This is a generated types file that provides the foundational type infrastructure for the MCP tooling. The
Mcp<T>type correctly wraps tool functions with schema validation and metadata, and the baseStateSchema,Env, andScopesexports align with the extended definitions inserver/main.ts.discord-bot/vite.config.ts (1)
1-29: LGTM!The Vite configuration correctly sets up the Cloudflare Workers environment with the Deco plugin. The environment variable definitions and global shims are appropriate for the Cloudflare Workers runtime, and the cache directory configuration is a reasonable optimization.
discord-bot/tsconfig.json (1)
1-43: LGTM!The TypeScript configuration is well-structured with appropriate strict type-checking rules, correct module resolution for bundlers, and path aliases that align with the project structure. The Cloudflare Workers types are correctly included.
discord-bot/server/main.ts (1)
1-62: LGTM! Clean entry point configuration.The main entry point is well-structured with proper StateSchema extension for botToken configuration, comprehensive Env type definition, and appropriate runtime wiring. The empty OAuth scopes array is correct for a bot token-based authentication model.
discord-bot/server/tools/index.ts (1)
1-46: LGTM! Well-organized tool aggregation.The central export pattern maintains clear domain separation while providing a convenient single entry point. The dual export strategy (aggregated + domain-specific) offers flexibility for different consumption patterns.
discord-bot/server/tools/utils/discord-client.ts (1)
44-59: Verification confirms code is correct.All PUT endpoints in the codebase—pinMessage (204 No Content), banMember (204 No Content), and joinThread (204 No Content)—return void on success. The special handling at lines 45-59 correctly checks the content-type header and returns parsed JSON only if present, otherwise returns void. This aligns with Discord API specifications and is an intentional, appropriate design choice.
discord-bot/server/tools/webhooks.ts (1)
126-139: Confirm that empty ID values in 204 No Content response are acceptable for your use cases.Verification confirms the fallback object with empty string IDs (
id="",channel_id="",author.id="") is returned to consumers whenwait=false(204 No Content). The schema allows empty strings sincesendMessageOutputSchemausesz.string()without constraints, so validation passes. However, ensure downstream consumers can handle these empty placeholder values without breaking or logging errors. If consumers require valid IDs, consider either not returning this fallback or documenting the limitation.discord-bot/server/tools/messages.ts (1)
209-211: LGTM! Good defensive limit clamping.The use of
Math.min(Math.max(limit, 1), 100)correctly ensures the limit stays within Discord API constraints (1-100 for messages). This pattern is consistently applied across pagination endpoints.discord-bot/server/tools/threads.ts (1)
47-49: Thread name validation is correct.The validation properly enforces Discord's 1-100 character limit for thread names. Note that the
!namecheck is technically redundant since the input schema already validatesnameas a required string, but it provides good defensive programming.discord-bot/server/lib/types.ts (5)
619-657: LGTM! Comprehensive embed schema.The
discordEmbedSchemacorrectly models Discord's embed structure with all supported fields (title, description, color, footer, image, thumbnail, author, fields). The nested object schemas properly validate the embed components.
659-678: Schema descriptions in Portuguese are intentional.The Zod schema descriptions are written in Portuguese (e.g., "ID do canal Discord onde enviar a mensagem"). This appears to be an intentional internationalization choice and is consistently applied throughout the file. Ensure this aligns with your target audience expectations.
760-761: Good schema reuse pattern.Reusing schemas where input/output shapes are identical (e.g.,
removeReactionInputSchema = addReactionInputSchema) is a good application of the DRY principle. This pattern is consistently applied throughout the file (also at lines 799-800, 1064-1065).
609-612: Well-organized type definition file.The clear separation between TypeScript interfaces (lines 1-608) and Zod schemas (lines 609-1159) with section markers makes this large file easy to navigate. The consistent naming convention (*InputSchema/*OutputSchema) enhances maintainability.
1-18: No issues found with discriminator field implementation.The Discord API v10 User object still includes the discriminator field, and it's always returned—migrated users receive discriminator = "0". The type definition correctly marks it as required (
discriminator: string;) since the API consistently returns this field. The application can handle the "0" value gracefully at runtime if needed, but the type definition is accurate for the current API specification.
| "dev": "deco dev --vite", | ||
| "configure": "deco configure", | ||
| "gen": "deco gen --output=shared/deco.gen.ts", | ||
| "deploy": "npm run build && deco deploy ./dist/server", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix the package manager inconsistency in the deploy script.
The deploy script uses npm run build, but the project uses Bun as its package manager (evident from the build script and bun.lock mentioned in the PR summary).
Apply this diff to fix the inconsistency:
- "deploy": "npm run build && deco deploy ./dist/server",
+ "deploy": "bun run build && deco deploy ./dist/server",🤖 Prompt for AI Agents
In discord-bot/package.json around line 11, the deploy script calls "npm run
build" while the project uses Bun; update the deploy script to invoke Bun
instead (for example replace the npm invocation with "bun run build" so the
script becomes bun run build && deco deploy ./dist/server) to ensure the package
manager is consistent.
| "@types/mime-db": "^1.43.6", | ||
| "deco-cli": "^0.26.0", | ||
| "typescript": "^5.7.2", | ||
| "vite": "7.2.0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify the Vite version exists and is compatible.
Vite is pinned to exact version 7.2.0, which seems unusually high. As of my knowledge cutoff, Vite's latest stable version is in the 5.x-6.x range. Please verify that this version exists and is the intended version.
🌐 Web query:
What is the latest version of Vite as of November 2025?
💡 Result:
The latest stable Vite version as of November 17, 2025 is v7.1.4. [1][2]
Sources:
[1] Vite official blog (announcing Vite 7).
[2] Vite package page on npm showing v7.1.4.
Vite version 7.2.0 exceeds the current latest stable release.
The latest stable Vite version as of November 17, 2025 is v7.1.4, but the package.json pins version 7.2.0. This version either doesn't exist yet or may be a pre-release. Verify this is intentional; otherwise, update to a released version or remove the exact pin to allow flexibility.
🤖 Prompt for AI Agents
In discord-bot/package.json around line 28, the Vite dependency is pinned to
"7.2.0" which exceeds the current stable release; update the version to a valid
released version (e.g., "7.1.4") or loosen the pin (e.g., "^7.1.4" or ">=7.1.4
<8.0.0") depending on whether you want exact reproducibility or minor upgrades,
then run npm install / pnpm install and verify the lockfile and build still
succeed.
| ``` | ||
| discord-bot/ | ||
| ├── server/ | ||
| │ ├── main.ts # Entry point do MCP | ||
| │ ├── lib/ | ||
| │ │ └── types.ts # Schemas Zod e tipos | ||
| │ └── tools/ | ||
| │ ├── index.ts # Exporta todas as tools | ||
| │ ├── messages.ts # Tools de mensagens | ||
| │ ├── channels.ts # Tools de canais | ||
| │ ├── guilds.ts # Tools de servidores | ||
| │ ├── roles.ts # Tools de roles | ||
| │ ├── threads.ts # Tools de threads | ||
| │ ├── webhooks.ts # Tools de webhooks | ||
| │ └── utils/ | ||
| │ └── discord-client.ts # Cliente HTTP Discord | ||
| └── shared/ | ||
| └── deco.gen.ts # Tipos gerados automaticamente | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add language specification to the fenced code block.
The fenced code block on line 93 is missing a language specification, which is flagged by markdownlint. Adding a language identifier improves syntax highlighting and documentation clarity.
Apply this diff to add the language specification:
-```
+```text
discord-bot/
├── server/
│ ├── main.ts # Entry point do MCP🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
93-93: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
In discord-bot/README.md around lines 93 to 111 the fenced code block lacks a
language specifier; update the opening fence from ``` to ```text so the block
starts with ```text to satisfy markdownlint and enable proper syntax
highlighting and documentation clarity.
| export const getArchivedThreadsInputSchema = z.object({ | ||
| channelId: z.string().describe("ID do canal Discord"), | ||
| type: z.enum(["public", "private"]).describe("Tipo de threads arquivadas"), | ||
| before: z | ||
| .string() | ||
| .optional() | ||
| .describe( | ||
| "Timestamp ISO8601 - retornar threads arquivadas antes desta data", | ||
| ), | ||
| limit: z | ||
| .number() | ||
| .min(1) | ||
| .max(100) | ||
| .optional() | ||
| .describe("Número máximo de threads a retornar"), | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify before parameter type for archived threads.
The before parameter (lines 1078-1083) is typed as a string with description "Timestamp ISO8601". However, Discord's API typically uses snowflake IDs (message/thread IDs) for pagination parameters, not ISO8601 timestamps. Verify that the Discord API documentation for archived threads actually accepts timestamps rather than snowflake IDs.
🌐 Web query:
Discord API v10 get archived threads before parameter type
💡 Result:
The before query accepts either an ISO8601 timestamp (string) or a Snowflake (ID). [1][2]
Sources:
- Discord docs (List Public/Private Archived Threads: before = ISO8601 timestamp). [1]
- discord-api-types v10 typings: before?: Snowflake | string. [2]
Update before parameter type to accept both ISO8601 timestamps and Snowflake IDs.
The before parameter (lines 1078-1083) is currently typed as string only. Discord API v10's before query accepts either an ISO8601 timestamp (string) or a Snowflake (ID). The schema should be updated to accept both types, and the description should clarify both accepted formats.
before: z
.union([z.string(), z.number()])
.optional()
.describe(
"ISO8601 timestamp ou Snowflake ID - retornar threads arquivadas antes desta data/ID",
),🤖 Prompt for AI Agents
In discord-bot/server/lib/types.ts around lines 1075 to 1090, the
getArchivedThreadsInputSchema's before field is currently only z.string();
change it to accept both ISO8601 timestamps and Snowflake IDs by using a union
of string and number (e.g., z.union([z.string(), z.number()]).optional()) and
update the .describe() text to mention both accepted formats in Portuguese
("ISO8601 timestamp ou Snowflake ID - retornar threads arquivadas antes desta
data/ID"), keeping the field optional and preserving existing validation for
other fields.
| botToken: string; | ||
| } | ||
|
|
||
| const DISCORD_API_URL = "https://discord.com/api/v10"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Remove duplicate DISCORD_API_URL constant.
This constant is already defined in discord-bot/server/lib/constants.ts. Duplicating it creates a maintenance burden and potential inconsistency.
Apply this diff:
+import { DISCORD_API_URL } from "../../lib/constants.ts";
+
export interface DiscordClientConfig {
botToken: string;
}
-const DISCORD_API_URL = "https://discord.com/api/v10";
-
/**
* Makes an authenticated request to the Discord API
*/📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const DISCORD_API_URL = "https://discord.com/api/v10"; | |
| import { DISCORD_API_URL } from "../../lib/constants.ts"; | |
| export interface DiscordClientConfig { | |
| botToken: string; | |
| } | |
| /** | |
| * Makes an authenticated request to the Discord API | |
| */ |
🤖 Prompt for AI Agents
In discord-bot/server/tools/utils/discord-client.ts around line 13, remove the
duplicated DISCORD_API_URL constant and instead import DISCORD_API_URL from
discord-bot/server/lib/constants.ts; update the top-of-file imports to include
that named import (use the correct relative path), delete the local const
declaration, and ensure all usages in this file reference the imported constant.
| #:schema node_modules/@decocms/runtime/config-schema.json | ||
| name = "discord-bot" | ||
| main = "server/main.ts" | ||
| compatibility_date = "2025-06-17" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix the future compatibility date.
The compatibility_date is set to "2025-06-17", which is in the past relative to today (November 17, 2025). Cloudflare Workers compatibility dates should reflect the API behavior you want to lock in, but shouldn't be set to dates before the code was written. Consider using a date closer to the initial development date or the current date.
Apply this diff to use the current date:
-compatibility_date = "2025-06-17"
+compatibility_date = "2025-11-17"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| compatibility_date = "2025-06-17" | |
| compatibility_date = "2025-11-17" |
🤖 Prompt for AI Agents
In discord-bot/wrangler.toml around line 4, the compatibility_date is set to
"2025-06-17" which is in the past; update the compatibility_date value to the
current development date (e.g., "2025-11-17") so the Workers runtime locks to
the intended API behavior and reflects a date not earlier than when the code was
written.
| [deco.integration] | ||
| description = "Discord Bot integration for sending messages, managing channels, roles, threads, webhooks, and server moderation. Full control of your Discord server through MCP tools." | ||
| icon = "https://support.discord.com/hc/user_images/PRywUXcqg0v5DD6s7C3LyQ.jpeg" | ||
| friendlyName = "Discord teste" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update the test-related friendly name for production.
The friendlyName is set to "Discord teste" (Portuguese for "Discord test"), which suggests this is a test configuration. For a production integration, this should have a proper production-ready name.
Apply this diff to use a production-appropriate name:
-friendlyName = "Discord teste"
+friendlyName = "Discord Bot"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| friendlyName = "Discord teste" | |
| friendlyName = "Discord Bot" |
🤖 Prompt for AI Agents
In discord-bot/wrangler.toml around line 16, the friendlyName is set to "Discord
teste" which indicates a test label; replace that value with a
production-appropriate name (for example "Discord", "Discord Bot", or "Discord
Integration") so the configuration reflects production usage and commit the
updated toml entry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (2)
discord-bot/server/tools/guilds.ts (2)
35-37: Redundant limit clamping (consider removing).The Zod schema
listBotGuildsInputSchemaalready enforces.min(1).max(200)validation on thelimitparameter. This manual clamping is redundant since invalid values would be rejected during schema validation before reaching this code.You may simplify to:
- const searchParams: Record<string, string> = { - limit: Math.min(Math.max(limit, 1), 200).toString(), - }; + const searchParams: Record<string, string> = { + limit: limit.toString(), + };Alternatively, if you prefer defensive programming to protect against future schema changes, the current approach is acceptable.
113-115: Redundant limit clamping (consider removing).Similar to
LIST_BOT_GUILDS, the Zod schemagetGuildMembersInputSchemaalready enforces.min(1).max(1000)validation. This manual clamping is redundant.- const searchParams: Record<string, string> = { - limit: Math.min(Math.max(limit, 1), 1000).toString(), - }; + const searchParams: Record<string, string> = { + limit: limit.toString(), + };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
discord-bot/server/tools/guilds.ts(1 hunks)discord-bot/shared/deco.gen.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
discord-bot/shared/deco.gen.ts (1)
discord-bot/server/main.ts (1)
Env(33-38)
discord-bot/server/tools/guilds.ts (5)
discord-bot/shared/deco.gen.ts (1)
Env(1925-2114)discord-bot/server/main.ts (1)
Env(33-38)discord-bot/server/lib/types.ts (12)
listBotGuildsInputSchema(855-874)listBotGuildsOutputSchema(876-886)getGuildInputSchema(888-894)getGuildOutputSchema(896-903)getGuildMembersInputSchema(905-917)getGuildMembersOutputSchema(919-934)banMemberInputSchema(936-946)banMemberOutputSchema(948-951)getCurrentUserInputSchema(953-953)getCurrentUserOutputSchema(955-960)getUserInputSchema(962-964)getUserOutputSchema(966-966)discord-bot/server/tools/utils/discord-client.ts (1)
createDiscordClient(67-257)discord-bot/server/tools/index.ts (1)
guildTools(43-43)
🪛 Biome (2.1.2)
discord-bot/shared/deco.gen.ts
[error] 412-414: An empty interface is equivalent to {}.
Safe fix: Use a type alias instead.
(lint/suspicious/noEmptyInterface)
[error] 1050-1054: An empty interface is equivalent to {}.
Safe fix: Use a type alias instead.
(lint/suspicious/noEmptyInterface)
[error] 1472-1474: An empty interface is equivalent to {}.
Safe fix: Use a type alias instead.
(lint/suspicious/noEmptyInterface)
🔇 Additional comments (4)
discord-bot/server/tools/guilds.ts (1)
22-254: LGTM! Clean implementation of guild tools.The overall structure is well-organized with:
- Consistent pattern across all tool factories
- Proper schema validation using Zod
- Good error handling and error message wrapping
- Clear JSDoc comments for each tool
- Proper mapping from Discord API responses to output schemas
discord-bot/shared/deco.gen.ts (3)
1-8: Generated file properly marked.The file header correctly indicates this is auto-generated by
json-schema-to-typescriptand should not be manually edited. This is the expected pattern for generated TypeScript definitions.
412-412: Empty interfaces in generated code (informational).Static analysis correctly identifies empty interfaces that could be type aliases. However, since this file is auto-generated by
json-schema-to-typescript, these patterns are expected and should not be manually changed. If needed, adjust the source JSON Schema or the code generation configuration instead.Also applies to: 1050-1050, 1470-1470
1905-2116: LGTM! Well-structured MCP type definitions.The generated MCP wiring provides:
- Strong TypeScript typing for all Discord operations via the
Mcp<T>generic type- Comprehensive
Envinterface mapping all 32+ Discord MCP actions with proper input/output types- Clear JSDoc descriptions for each operation
- Proper integration with Zod for runtime validation
The type structure correctly aligns with the tool implementations in
discord-bot/server/tools/guilds.tsand other tool modules.
| if (deleteMessageDays < 0 || deleteMessageDays > 7) { | ||
| throw new Error("deleteMessageDays must be between 0 and 7"); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Remove redundant validation logic.
The Zod schema banMemberInputSchema already validates that deleteMessageDays is between 0 and 7 using .min(0).max(7). This manual validation duplicates that logic and will never be reached with invalid values, since Zod validation occurs before the execute function runs.
- if (deleteMessageDays < 0 || deleteMessageDays > 7) {
- throw new Error("deleteMessageDays must be between 0 and 7");
- }
-
const client = createDiscordClient({ botToken: state.botToken });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (deleteMessageDays < 0 || deleteMessageDays > 7) { | |
| throw new Error("deleteMessageDays must be between 0 and 7"); | |
| } | |
| const client = createDiscordClient({ botToken: state.botToken }); |
🤖 Prompt for AI Agents
In discord-bot/server/tools/guilds.ts around lines 156 to 158, the manual check
that throws "deleteMessageDays must be between 0 and 7" is redundant because
banMemberInputSchema already enforces .min(0).max(7) and Zod validates inputs
before execute runs; remove the three-line manual validation block and any
related unreachable branches so the function relies on the schema validation
instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
♻️ Duplicate comments (3)
discord-bot/package.json (2)
11-11: Fix the package manager inconsistency in the deploy script.The deploy script uses
npm run build, but the project uses Bun as its package manager (evident from thebuildscript on line 13 and the PR summary mentioningbun.lock). This inconsistency was flagged in a previous review.Apply this diff to fix it:
- "deploy": "npm run build && deco deploy ./dist/server", + "deploy": "bun run build && deco deploy ./dist/server",
29-29: Vite version exceeds the current stable release.Vite is pinned to exact version
7.2.0, which exceeds the current stable release (7.1.4as of November 17, 2025, per previous review findings). Verify this version exists and is intentional; otherwise, update to a released version.Apply this diff to use the latest stable release:
- "vite": "7.2.0", + "vite": "^7.1.4",Alternatively, if you prefer exact reproducibility, use:
- "vite": "7.2.0", + "vite": "7.1.4",discord-bot/server/lib/types.ts (1)
1075-1090: Clarify and widenbeforefilter for archived threads.
getArchivedThreadsInputSchema.beforeis documented only as an ISO8601 timestamp, but Discord also accepts a Snowflake ID for pagination. Consider updating both the type and description so tools can pass either an ISO string or an ID:-export const getArchivedThreadsInputSchema = z.object({ +export const getArchivedThreadsInputSchema = z.object({ channelId: z.string().describe("ID do canal Discord"), type: z.enum(["public", "private"]).describe("Tipo de threads arquivadas"), - before: z - .string() - .optional() - .describe( - "Timestamp ISO8601 - retornar threads arquivadas antes desta data", - ), + before: z + .union([z.string(), z.number()]) + .optional() + .describe( + "ISO8601 timestamp ou Snowflake ID - retornar threads arquivadas antes desta data/ID", + ), limit: z .number() .min(1) .max(100) .optional() .describe("Número máximo de threads a retornar"), });This keeps the field optional while matching Discord’s documented behavior; ensure the HTTP client stringifies numeric values before sending.
🧹 Nitpick comments (3)
discord-bot/server/lib/types.ts (1)
427-432: Avoid duplicate message reference interfaces.
DiscordMessageReferenceandMessageReferenceare structurally identical. Maintaining two copies increases drift risk; a single exported type/interface (or a type alias) is enough:-export interface DiscordMessageReference { - message_id?: string; - channel_id?: string; - guild_id?: string; - fail_if_not_exists?: boolean; -} - -export interface MessageReference { - message_id?: string; - channel_id?: string; - guild_id?: string; - fail_if_not_exists?: boolean; -} +export interface DiscordMessageReference { + message_id?: string; + channel_id?: string; + guild_id?: string; + fail_if_not_exists?: boolean; +} + +export type MessageReference = DiscordMessageReference;This keeps both names available without duplicating the structure.
Also applies to: 542-547
discord-bot/server/lib/verification.ts (1)
31-82: Signature verification logic is solid; async modifier is optional.The Ed25519 verification flow (timestamp window,
timestamp + bodymessage, hex decoding, andnacl.sign.detached.verify) is correct and should interop with Discord’s webhook requirements. Since everything here is synchronous, you could drop theasynconverifyDiscordSignatureand the correspondingawaitat call sites to simplify the call graph, but this is purely optional.discord-bot/server/lib/webhook-handler.ts (1)
251-265: Model autocompletechoicesin the interaction data types to avoid@ts-ignore.
handleAutocompleterelies ondata.choices, butInteractionCallbackDatadoesn’t declare this property, forcing an@ts-ignore. To keep type safety, extend your types to cover autocomplete responses instead of bypassing the checker. For example:-export interface InteractionCallbackData { +export interface InteractionCallbackData { tts?: boolean; content?: string; embeds?: DiscordEmbed[]; allowed_mentions?: AllowedMentions; flags?: number; components?: DiscordMessageComponent[]; attachments?: DiscordMessageAttachment[]; + // Para respostas de autocomplete + // choices?: { name: string; value: string }[]; }Then you can remove the
// @ts-ignoreinhandleAutocomplete.Also applies to: 1242-1250
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (6)
discord-bot/package.json(1 hunks)discord-bot/scripts/register-commands.ts(1 hunks)discord-bot/server/lib/types.ts(1 hunks)discord-bot/server/lib/verification.ts(1 hunks)discord-bot/server/lib/webhook-handler.ts(1 hunks)discord-bot/server/main.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
discord-bot/server/lib/webhook-handler.ts (4)
discord-bot/server/main.ts (1)
Env(39-44)discord-bot/server/lib/verification.ts (2)
extractSignatureHeaders(87-94)verifyDiscordSignature(31-82)discord-bot/server/lib/types.ts (2)
DiscordInteraction(1217-1232)InteractionResponse(1237-1240)discord-bot/server/lib/constants.ts (1)
DISCORD_API_URL(1-1)
discord-bot/scripts/register-commands.ts (1)
discord-bot/server/lib/constants.ts (1)
DISCORD_API_URL(1-1)
discord-bot/server/main.ts (2)
discord-bot/shared/deco.gen.ts (2)
StateSchema(1923-1923)Env(1925-2114)discord-bot/server/lib/webhook-handler.ts (1)
handleDiscordWebhook(21-65)
🪛 GitHub Actions: Checks
discord-bot/server/lib/webhook-handler.ts
[error] 1-1: Formatting issues detected by oxfmt. Run 'bun run fmt' (without --check) to fix this file.
discord-bot/server/lib/verification.ts
[error] 1-1: Formatting issues detected by oxfmt. Run 'bun run fmt' (without --check) to fix this file.
discord-bot/scripts/register-commands.ts
[error] 1-1: Formatting issues detected by oxfmt. Run 'bun run fmt' (without --check) to fix this file.
🔇 Additional comments (1)
discord-bot/server/main.ts (1)
19-80: Runtime and webhook routing look consistent and well-scoped.StateSchema extension (botToken/discordPublicKey) and the custom
fetchhandler cleanly integrate Discord webhooks into the Deco runtime while preserving the ASSETS fallback and leaving scopes empty. No issues from a wiring/architecture standpoint.
| /** | ||
| * Script para registrar comandos slash no Discord | ||
| * | ||
| * Uso: | ||
| * bun run scripts/register-commands.ts | ||
| * | ||
| * Requisitos: | ||
| * - Defina as variáveis de ambiente: | ||
| * - DISCORD_APP_ID: ID da aplicação Discord | ||
| * - DISCORD_BOT_TOKEN: Token do bot | ||
| * - DISCORD_GUILD_ID (opcional): ID do servidor para comandos de teste | ||
| */ | ||
|
|
||
| const DISCORD_API_URL = "https://discord.com/api/v10"; | ||
|
|
||
| // ======================================== | ||
| // Configuração dos Comandos | ||
| // ======================================== | ||
|
|
||
| const commands = [ | ||
| { | ||
| name: "deco", | ||
| description: "Interagir com agente Deco via Discord", | ||
| options: [ | ||
| { | ||
| name: "message", | ||
| description: "Mensagem para enviar ao agente", | ||
| type: 3, // STRING | ||
| required: false, | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| name: "ping", | ||
| description: "Verificar se o bot está online", | ||
| }, | ||
| { | ||
| name: "help", | ||
| description: "Mostrar ajuda sobre comandos disponíveis", | ||
| }, | ||
| ]; | ||
|
|
||
| // ======================================== | ||
| // Funções de Registro | ||
| // ======================================== | ||
|
|
||
| async function registerGlobalCommands( | ||
| appId: string, | ||
| botToken: string | ||
| ): Promise<void> { | ||
| console.log("📝 Registrando comandos globais..."); | ||
| console.log(`Total: ${commands.length} comandos`); | ||
|
|
||
| const url = `${DISCORD_API_URL}/applications/${appId}/commands`; | ||
|
|
||
| try { | ||
| const response = await fetch(url, { | ||
| method: "PUT", | ||
| headers: { | ||
| "Authorization": `Bot ${botToken}`, | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify(commands), | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const error = await response.text(); | ||
| throw new Error( | ||
| `Falha ao registrar comandos: ${response.status} - ${error}` | ||
| ); | ||
| } | ||
|
|
||
| const result = await response.json(); | ||
| console.log("✅ Comandos globais registrados com sucesso!"); | ||
| console.log(`Registrados ${result.length} comandos`); | ||
|
|
||
| result.forEach((cmd: any) => { | ||
| console.log(` - /${cmd.name}: ${cmd.description}`); | ||
| }); | ||
|
|
||
| console.log( | ||
| "\n⚠️ Nota: Comandos globais podem demorar até 1 hora para propagar." | ||
| ); | ||
| } catch (error) { | ||
| console.error("❌ Erro ao registrar comandos globais:", error); | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| async function registerGuildCommands( | ||
| appId: string, | ||
| botToken: string, | ||
| guildId: string | ||
| ): Promise<void> { | ||
| console.log(`📝 Registrando comandos no servidor ${guildId}...`); | ||
| console.log(`Total: ${commands.length} comandos`); | ||
|
|
||
| const url = | ||
| `${DISCORD_API_URL}/applications/${appId}/guilds/${guildId}/commands`; | ||
|
|
||
| try { | ||
| const response = await fetch(url, { | ||
| method: "PUT", | ||
| headers: { | ||
| "Authorization": `Bot ${botToken}`, | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify(commands), | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const error = await response.text(); | ||
| throw new Error( | ||
| `Falha ao registrar comandos: ${response.status} - ${error}` | ||
| ); | ||
| } | ||
|
|
||
| const result = await response.json(); | ||
| console.log("✅ Comandos do servidor registrados com sucesso!"); | ||
| console.log(`Registrados ${result.length} comandos`); | ||
|
|
||
| result.forEach((cmd: any) => { | ||
| console.log(` - /${cmd.name}: ${cmd.description}`); | ||
| }); | ||
|
|
||
| console.log("\n✅ Comandos devem estar disponíveis imediatamente!"); | ||
| } catch (error) { | ||
| console.error("❌ Erro ao registrar comandos do servidor:", error); | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| async function listCommands( | ||
| appId: string, | ||
| botToken: string, | ||
| guildId?: string | ||
| ): Promise<void> { | ||
| const scope = guildId ? `servidor ${guildId}` : "globais"; | ||
| console.log(`📋 Listando comandos ${scope}...`); | ||
|
|
||
| const url = guildId | ||
| ? `${DISCORD_API_URL}/applications/${appId}/guilds/${guildId}/commands` | ||
| : `${DISCORD_API_URL}/applications/${appId}/commands`; | ||
|
|
||
| try { | ||
| const response = await fetch(url, { | ||
| headers: { | ||
| "Authorization": `Bot ${botToken}`, | ||
| }, | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const error = await response.text(); | ||
| throw new Error(`Falha ao listar comandos: ${response.status} - ${error}`); | ||
| } | ||
|
|
||
| const result = await response.json(); | ||
|
|
||
| if (result.length === 0) { | ||
| console.log(`ℹ️ Nenhum comando ${scope} encontrado.`); | ||
| return; | ||
| } | ||
|
|
||
| console.log(`\nComandos ${scope} (${result.length}):`); | ||
| result.forEach((cmd: any) => { | ||
| console.log(`\n 📌 /${cmd.name}`); | ||
| console.log(` ID: ${cmd.id}`); | ||
| console.log(` Descrição: ${cmd.description}`); | ||
| if (cmd.options && cmd.options.length > 0) { | ||
| console.log(` Opções:`); | ||
| cmd.options.forEach((opt: any) => { | ||
| const required = opt.required ? " (obrigatório)" : ""; | ||
| console.log(` - ${opt.name}: ${opt.description}${required}`); | ||
| }); | ||
| } | ||
| }); | ||
| } catch (error) { | ||
| console.error("❌ Erro ao listar comandos:", error); | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| async function deleteAllCommands( | ||
| appId: string, | ||
| botToken: string, | ||
| guildId?: string | ||
| ): Promise<void> { | ||
| const scope = guildId ? `servidor ${guildId}` : "globais"; | ||
| console.log(`🗑️ Deletando todos os comandos ${scope}...`); | ||
|
|
||
| const url = guildId | ||
| ? `${DISCORD_API_URL}/applications/${appId}/guilds/${guildId}/commands` | ||
| : `${DISCORD_API_URL}/applications/${appId}/commands`; | ||
|
|
||
| try { | ||
| // Primeiro, listar comandos existentes | ||
| const listResponse = await fetch(url, { | ||
| headers: { | ||
| "Authorization": `Bot ${botToken}`, | ||
| }, | ||
| }); | ||
|
|
||
| if (!listResponse.ok) { | ||
| throw new Error(`Falha ao listar comandos: ${listResponse.status}`); | ||
| } | ||
|
|
||
| const existingCommands = await listResponse.json(); | ||
|
|
||
| if (existingCommands.length === 0) { | ||
| console.log(`ℹ️ Nenhum comando ${scope} para deletar.`); | ||
| return; | ||
| } | ||
|
|
||
| console.log(`Encontrados ${existingCommands.length} comandos para deletar`); | ||
|
|
||
| // Deletar todos de uma vez enviando array vazio | ||
| const deleteResponse = await fetch(url, { | ||
| method: "PUT", | ||
| headers: { | ||
| "Authorization": `Bot ${botToken}`, | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify([]), | ||
| }); | ||
|
|
||
| if (!deleteResponse.ok) { | ||
| const error = await deleteResponse.text(); | ||
| throw new Error(`Falha ao deletar comandos: ${deleteResponse.status} - ${error}`); | ||
| } | ||
|
|
||
| console.log(`✅ Todos os comandos ${scope} foram deletados!`); | ||
| } catch (error) { | ||
| console.error("❌ Erro ao deletar comandos:", error); | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| // ======================================== | ||
| // CLI | ||
| // ======================================== | ||
|
|
||
| async function main() { | ||
| const appId = process.env.DISCORD_APP_ID; | ||
| const botToken = process.env.DISCORD_BOT_TOKEN; | ||
| const guildId = process.env.DISCORD_GUILD_ID; | ||
|
|
||
| if (!appId || !botToken) { | ||
| console.error("❌ Erro: Variáveis de ambiente não configuradas!"); | ||
| console.error("\nDefina as seguintes variáveis:"); | ||
| console.error(" - DISCORD_APP_ID: ID da aplicação Discord"); | ||
| console.error(" - DISCORD_BOT_TOKEN: Token do bot"); | ||
| console.error( | ||
| " - DISCORD_GUILD_ID (opcional): ID do servidor para comandos de teste" | ||
| ); | ||
| console.error("\nExemplo:"); | ||
| console.error( | ||
| " DISCORD_APP_ID=123456 DISCORD_BOT_TOKEN=abc123 bun run scripts/register-commands.ts" | ||
| ); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const action = process.argv[2] || "register"; | ||
|
|
||
| console.log("🤖 Discord Bot - Gerenciador de Comandos\n"); | ||
|
|
||
| try { | ||
| switch (action) { | ||
| case "register": | ||
| case "reg": | ||
| if (guildId) { | ||
| console.log("🎯 Modo: Registrar comandos no servidor (instantâneo)"); | ||
| await registerGuildCommands(appId, botToken, guildId); | ||
| } else { | ||
| console.log("🌍 Modo: Registrar comandos globais (demora até 1h)"); | ||
| await registerGlobalCommands(appId, botToken); | ||
| } | ||
| break; | ||
|
|
||
| case "list": | ||
| case "ls": | ||
| await listCommands(appId, botToken, guildId); | ||
| break; | ||
|
|
||
| case "delete": | ||
| case "del": | ||
| await deleteAllCommands(appId, botToken, guildId); | ||
| break; | ||
|
|
||
| case "help": | ||
| case "--help": | ||
| case "-h": | ||
| console.log("Uso: bun run scripts/register-commands.ts [ação]\n"); | ||
| console.log("Ações disponíveis:"); | ||
| console.log(" register, reg Registrar comandos (padrão)"); | ||
| console.log(" list, ls Listar comandos existentes"); | ||
| console.log(" delete, del Deletar todos os comandos"); | ||
| console.log(" help Mostrar esta ajuda"); | ||
| console.log("\nVariáveis de ambiente:"); | ||
| console.log(" DISCORD_APP_ID ID da aplicação (obrigatório)"); | ||
| console.log(" DISCORD_BOT_TOKEN Token do bot (obrigatório)"); | ||
| console.log(" DISCORD_GUILD_ID ID do servidor (opcional)"); | ||
| console.log("\nExemplos:"); | ||
| console.log(" # Registrar globalmente"); | ||
| console.log(" DISCORD_APP_ID=123 DISCORD_BOT_TOKEN=abc bun run scripts/register-commands.ts"); | ||
| console.log("\n # Registrar em servidor específico"); | ||
| console.log(" DISCORD_APP_ID=123 DISCORD_BOT_TOKEN=abc DISCORD_GUILD_ID=456 bun run scripts/register-commands.ts"); | ||
| console.log("\n # Listar comandos"); | ||
| console.log(" DISCORD_APP_ID=123 DISCORD_BOT_TOKEN=abc bun run scripts/register-commands.ts list"); | ||
| break; | ||
|
|
||
| default: | ||
| console.error(`❌ Ação desconhecida: ${action}`); | ||
| console.error("Use 'help' para ver as ações disponíveis."); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| console.log("\n✨ Concluído!"); | ||
| } catch (error) { | ||
| console.error("\n💥 Erro fatal:", error); | ||
| process.exit(1); | ||
| } | ||
| } | ||
|
|
||
| // Executar apenas se for o arquivo principal | ||
| if (import.meta.main) { | ||
| main(); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix formatting to satisfy oxfmt check.
GitHub Actions reports formatting issues in this script. Run the project formatter to keep CI green:
bun run fmt(and re-run checks). No functional issues spotted in the CLI logic itself.
🧰 Tools
🪛 GitHub Actions: Checks
[error] 1-1: Formatting issues detected by oxfmt. Run 'bun run fmt' (without --check) to fix this file.
🤖 Prompt for AI Agents
In discord-bot/scripts/register-commands.ts lines 1-328 the file fails the
repository formatter (oxfmt/CI) — run the project formatter (bun run fmt) to
automatically fix whitespace/formatting issues, review the changed diff, stage
and commit the formatted file, then push and re-run CI; if the formatter isn't
available ensure the repo's formatting tool is installed/configured (e.g., add
oxfmt/bun fmt to dev setup) and reformat the file before pushing so the GitHub
Actions check passes.
| export const listWebhooksInputSchema = z.object({ | ||
| channelId: z | ||
| .string() | ||
| .optional() | ||
| .describe("ID do canal Discord (se omitido, requer guildId)"), | ||
| guildId: z | ||
| .string() | ||
| .optional() | ||
| .describe("ID do servidor Discord (se omitido, requer channelId)"), | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Enforce “at least one of channelId or guildId” for listing webhooks.
listWebhooksInputSchema currently allows both channelId and guildId to be omitted, which will lead to an invalid Discord request. You can encode the “at least one provided” rule directly in the schema:
-export const listWebhooksInputSchema = z.object({
- channelId: z
- .string()
- .optional()
- .describe("ID do canal Discord (se omitido, requer guildId)"),
- guildId: z
- .string()
- .optional()
- .describe("ID do servidor Discord (se omitido, requer channelId)"),
-});
+export const listWebhooksInputSchema = z
+ .object({
+ channelId: z
+ .string()
+ .optional()
+ .describe("ID do canal Discord (se omitido, requer guildId)"),
+ guildId: z
+ .string()
+ .optional()
+ .describe("ID do servidor Discord (se omitido, requer channelId)"),
+ })
+ .refine(
+ (data) => !!data.channelId || !!data.guildId,
+ "Forneça pelo menos channelId ou guildId",
+ );This fails fast on invalid input instead of pushing an unusable request to Discord.
I'll verify the review comment by examining the actual codebase state and checking for usages of this schema.
</script>🏁 Script executed:
#!/bin/bash
# 1. Check the actual state of the schema in the file
echo "=== Checking listWebhooksInputSchema definition ==="
rg -n "listWebhooksInputSchema" discord-bot/server/lib/types.ts -A 10
# 2. Find all usages of listWebhooksInputSchema across the codebase
echo -e "\n=== Finding all usages of listWebhooksInputSchema ==="
rg -n "listWebhooksInputSchema" --type=ts -g '!node_modules' -C 2
# 3. Check if there's any validation logic in webhook-related handlers
echo -e "\n=== Searching for webhook request handlers ==="
rg -n "webhook" --type=ts -i -g '!node_modules' discord-bot/server -l | head -20Length of output: 1887
</script>
🏁 Script executed:
#!/bin/bash
# Check the execute function that uses listWebhooksInputSchema
echo "=== Checking the execute function in webhooks.ts ==="
rg -n "execute:" discord-bot/server/tools/webhooks.ts -A 30 | grep -A 30 "inputSchema: listWebhooksInputSchema"Length of output: 1177
Enforce "at least one of channelId or guildId" at schema level for listing webhooks.
The handler in discord-bot/server/tools/webhooks.ts (lines 202-204) already validates this requirement, but the schema itself should enforce it as well. Move the validation from the handler to the schema using .refine() for consistent, fail-fast validation:
-export const listWebhooksInputSchema = z.object({
- channelId: z
- .string()
- .optional()
- .describe("ID do canal Discord (se omitido, requer guildId)"),
- guildId: z
- .string()
- .optional()
- .describe("ID do servidor Discord (se omitido, requer channelId)"),
-});
+export const listWebhooksInputSchema = z
+ .object({
+ channelId: z
+ .string()
+ .optional()
+ .describe("ID do canal Discord (se omitido, requer guildId)"),
+ guildId: z
+ .string()
+ .optional()
+ .describe("ID do servidor Discord (se omitido, requer channelId)"),
+ })
+ .refine(
+ (data) => !!data.channelId || !!data.guildId,
+ "Forneça pelo menos channelId ou guildId",
+ );This makes the schema self-validating and clarifies the contract at the type level, while the handler validation provides an additional safety layer.
🤖 Prompt for AI Agents
In discord-bot/server/lib/types.ts around lines 1146 to 1155, the input schema
for listing webhooks currently allows both channelId and guildId to be optional
but doesn't enforce that at least one is provided; update the z.object to call
.refine(...) after the object definition to assert that either channelId or
guildId is present (e.g., refine(data => Boolean(data.channelId) ||
Boolean(data.guildId), { message: "Either channelId or guildId must be
provided", path: [...] })), so the schema fails fast and removes the need for
that check in the handler.
| /** | ||
| * Verificação de assinaturas de webhooks do Discord | ||
| * Usa o algoritmo Ed25519 para validar que as requisições vêm do Discord | ||
| */ | ||
| import nacl from "tweetnacl"; | ||
|
|
||
| /** | ||
| * Converte string hexadecimal para Uint8Array | ||
| */ | ||
| function hexToUint8Array(hex: string): Uint8Array { | ||
| if (hex.length % 2 !== 0) { | ||
| throw new Error("Hex string inválido: comprimento ímpar"); | ||
| } | ||
|
|
||
| const bytes = new Uint8Array(hex.length / 2); | ||
| for (let i = 0; i < hex.length; i += 2) { | ||
| bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); | ||
| } | ||
| return bytes; | ||
| } | ||
|
|
||
| /** | ||
| * Verifica a assinatura Ed25519 de uma requisição do Discord | ||
| * | ||
| * @param body - Corpo da requisição em formato de string (não parseado) | ||
| * @param signature - Header X-Signature-Ed25519 | ||
| * @param timestamp - Header X-Signature-Timestamp | ||
| * @param publicKey - Chave pública do Discord (obtida no Developer Portal) | ||
| * @returns true se a assinatura é válida, false caso contrário | ||
| */ | ||
| export async function verifyDiscordSignature( | ||
| body: string, | ||
| signature: string | null, | ||
| timestamp: string | null, | ||
| publicKey: string | ||
| ): Promise<boolean> { | ||
| if (!signature || !timestamp) { | ||
| console.error("Missing signature or timestamp headers"); | ||
| return false; | ||
| } | ||
|
|
||
| try { | ||
| // Validar timestamp (rejeitar requisições muito antigas - > 5 minutos) | ||
| const now = Math.floor(Date.now() / 1000); | ||
| const requestTime = parseInt(timestamp, 10); | ||
|
|
||
| if (isNaN(requestTime)) { | ||
| console.error("Invalid timestamp format"); | ||
| return false; | ||
| } | ||
|
|
||
| const timeDiff = Math.abs(now - requestTime); | ||
| if (timeDiff > 300) { // 5 minutos | ||
| console.warn(`Request timestamp too old: ${timeDiff}s difference`); | ||
| return false; | ||
| } | ||
|
|
||
| // Construir a mensagem que o Discord assinou | ||
| const message = timestamp + body; | ||
|
|
||
| // Converter de hexadecimal para bytes | ||
| const signatureBytes = hexToUint8Array(signature); | ||
| const publicKeyBytes = hexToUint8Array(publicKey); | ||
| const messageBytes = new TextEncoder().encode(message); | ||
|
|
||
| // Verificar assinatura usando Ed25519 | ||
| const isValid = nacl.sign.detached.verify( | ||
| messageBytes, | ||
| signatureBytes, | ||
| publicKeyBytes | ||
| ); | ||
|
|
||
| if (!isValid) { | ||
| console.error("Invalid signature"); | ||
| } | ||
|
|
||
| return isValid; | ||
| } catch (error) { | ||
| console.error("Error verifying Discord signature:", error); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Extrai os headers de assinatura de uma requisição | ||
| */ | ||
| export function extractSignatureHeaders( | ||
| request: Request | ||
| ): { signature: string | null; timestamp: string | null } { | ||
| return { | ||
| signature: request.headers.get("X-Signature-Ed25519"), | ||
| timestamp: request.headers.get("X-Signature-Timestamp"), | ||
| }; | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Run the formatter to fix oxfmt failures.
CI reports formatting issues for this file. Please run:
bun run fmtbefore merging so the oxfmt check passes.
🧰 Tools
🪛 GitHub Actions: Checks
[error] 1-1: Formatting issues detected by oxfmt. Run 'bun run fmt' (without --check) to fix this file.
🤖 Prompt for AI Agents
In discord-bot/server/lib/verification.ts lines 1-95, the file fails the
repository formatter (oxfmt) CI check; run the project formatter to fix
whitespace/formatting issues. Execute the project's formatting command (bun run
fmt) locally or via your preferred npm script, review changes, stage the updated
file, and push the commit so the CI oxfmt check passes.
| import nacl from "tweetnacl"; | ||
|
|
||
| /** | ||
| * Converte string hexadecimal para Uint8Array | ||
| */ | ||
| function hexToUint8Array(hex: string): Uint8Array { | ||
| if (hex.length % 2 !== 0) { | ||
| throw new Error("Hex string inválido: comprimento ímpar"); | ||
| } | ||
|
|
||
| const bytes = new Uint8Array(hex.length / 2); | ||
| for (let i = 0; i < hex.length; i += 2) { | ||
| bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); | ||
| } | ||
| return bytes; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Confirm tweetnacl import style matches your TypeScript config.
You’re importing tweetnacl as a default export (import nacl from "tweetnacl";), while many setups use import * as nacl from "tweetnacl";. This is fine if esModuleInterop / allowSyntheticDefaultImports are enabled in your tsconfig or Bun config; otherwise you may get a compile-time error.
If your config doesn’t enable synthetic default imports, switch to:
import * as nacl from "tweetnacl";🏁 Script executed:
#!/bin/bash
# Find TypeScript config files
fd -e json "tsconfig|bun\.config" --type f
# Check package.json for tweetnacl version and dependencies
if [ -f package.json ]; then
echo "=== package.json tweetnacl entry ==="
cat package.json | grep -A2 -B2 "tweetnacl"
fiLength of output: 289
🏁 Script executed:
#!/bin/bash
# Read tsconfig.json if present
if [ -f tsconfig.json ]; then
echo "=== tsconfig.json ==="
cat tsconfig.json
fi
# Check for bun.config.ts
if [ -f bun.config.ts ]; then
echo "=== bun.config.ts ==="
cat bun.config.ts
fiLength of output: 0
🏁 Script executed:
#!/bin/bash
# Check tweetnacl's package.json exports configuration
if [ -f node_modules/tweetnacl/package.json ]; then
echo "=== tweetnacl package.json (exports/main) ==="
cat node_modules/tweetnacl/package.json | grep -E '"(main|exports|types)"'
fi
# Check if @types/tweetnacl exists
if [ -d node_modules/@types/tweetnacl ]; then
echo "=== @types/tweetnacl exists ==="
ls -la node_modules/@types/tweetnacl/
fiLength of output: 0
---
🏁 Script executed:
```shell
#!/bin/bash
# Read discord-bot tsconfig.json
echo "=== discord-bot/tsconfig.json ==="
cat discord-bot/tsconfig.json
echo ""
echo "=== discord-bot/package.json (tweetnacl) ==="
cat discord-bot/package.json | grep -A3 -B3 "tweetnacl" || echo "tweetnacl not found in discord-bot/package.json"
Length of output: 1140
🏁 Script executed:
#!/bin/bash
# Verify the actual import in verification.ts
echo "=== discord-bot/server/lib/verification.ts (first 25 lines) ==="
head -25 discord-bot/server/lib/verification.tsLength of output: 750
🌐 Web query:
tweetnacl JavaScript import default export vs named import
💡 Result:
Short answer: tweetnacl (the npm package) is published as a CommonJS module that exports the library object (module.exports = nacl). In Node / CommonJS use require(...). In ESM you can either use the default-style import made available by many bundlers/transpilers (import nacl from 'tweetnacl') or the namespace form (import * as nacl from 'tweetnacl'); the namespace form always works with CommonJS-to-ESM interop. Examples:
-
CommonJS:
const nacl = require('tweetnacl'); (github.com) -
ESM (recommended safe):
import * as nacl from 'tweetnacl'; (github.com) -
ESM (default-like; may work depending on bundler/Node interop):
import nacl from 'tweetnacl'; (github.com)
Why choose import * as nacl: it avoids interop ambiguity between CommonJS module.exports and ESM default imports across different bundlers/runtimes. (github.com)
Citations:
- 1: https://github.com/dchest/tweetnacl-js?utm_source=openai
- 2: https://github.com/dchest/tweetnacl-js?utm_source=openai
- 3: https://github.com/dchest/tweetnacl-js?utm_source=openai
- 4: https://github.com/dchest/tweetnacl-js?utm_source=openai
Switch import to namespace form per tweetnacl's recommended practice.
tweetnacl is published as CommonJS and the recommended safe ESM form is import * as nacl from "tweetnacl". Your tsconfig.json uses moduleResolution: "bundler" (Vite) which provides automatic CommonJS interop, so the default import may work at runtime. However, your TypeScript config does not explicitly set esModuleInterop or allowSyntheticDefaultImports, and the namespace form avoids interop ambiguity across different bundlers/runtimes.
Update line 5 in discord-bot/server/lib/verification.ts:
import * as nacl from "tweetnacl";🤖 Prompt for AI Agents
In discord-bot/server/lib/verification.ts around lines 5 to 20, the file
currently imports tweetnacl using a default import which can cause CommonJS/ESM
interop issues; replace the default import with the namespace form by changing
the import statement to use "import * as nacl from 'tweetnacl'". Ensure no other
code relies on a default export and update any references if needed (they should
remain as nacl.*).
| /** | ||
| * Handler para webhooks do Discord | ||
| * Processa interações recebidas via webhook | ||
| */ | ||
| import type { Env } from "../main.ts"; | ||
| import { | ||
| type DiscordInteraction, | ||
| InteractionType, | ||
| InteractionResponseType, | ||
| type InteractionResponse, | ||
| } from "./types.ts"; | ||
| import { | ||
| verifyDiscordSignature, | ||
| extractSignatureHeaders, | ||
| } from "./verification.ts"; | ||
| import { DISCORD_API_URL } from "./constants.ts"; | ||
|
|
||
| /** | ||
| * Handler principal para requisições de webhook do Discord | ||
| */ | ||
| export async function handleDiscordWebhook( | ||
| request: Request, | ||
| env: Env | ||
| ): Promise<Response> { | ||
| // 1. Ler o corpo da requisição (precisa ser string para validação) | ||
| const body = await request.text(); | ||
|
|
||
| // 2. Extrair headers de assinatura | ||
| const { signature, timestamp } = extractSignatureHeaders(request); | ||
|
|
||
| // 3. Validar assinatura | ||
| const state = env.DECO_REQUEST_CONTEXT?.state; | ||
|
|
||
| // Suporte para dev local e produção | ||
| const publicKey = state?.discordPublicKey || (env as any).DISCORD_PUBLIC_KEY; | ||
|
|
||
| if (!publicKey) { | ||
| console.error("Discord Public Key not configured"); | ||
| return new Response("Server configuration error", { status: 500 }); | ||
| } | ||
|
|
||
| const isValid = await verifyDiscordSignature( | ||
| body, | ||
| signature, | ||
| timestamp, | ||
| publicKey | ||
| ); | ||
|
|
||
| if (!isValid) { | ||
| console.error("Invalid Discord signature"); | ||
| return new Response("Invalid signature", { status: 401 }); | ||
| } | ||
|
|
||
| // 4. Parsear a interação | ||
| let interaction: DiscordInteraction; | ||
| try { | ||
| interaction = JSON.parse(body) as DiscordInteraction; | ||
| } catch (error) { | ||
| console.error("Failed to parse interaction:", error); | ||
| return new Response("Invalid JSON", { status: 400 }); | ||
| } | ||
|
|
||
| // 5. Processar com base no tipo de interação | ||
| return await processInteraction(interaction, env); | ||
| } | ||
|
|
||
| /** | ||
| * Processa diferentes tipos de interação | ||
| */ | ||
| async function processInteraction( | ||
| interaction: DiscordInteraction, | ||
| env: Env | ||
| ): Promise<Response> { | ||
| switch (interaction.type) { | ||
| case InteractionType.PING: | ||
| // Discord está validando o endpoint | ||
| return respondToPing(); | ||
|
|
||
| case InteractionType.APPLICATION_COMMAND: | ||
| // Comando de aplicação (slash command) | ||
| return await handleApplicationCommand(interaction, env); | ||
|
|
||
| case InteractionType.MESSAGE_COMPONENT: | ||
| // Interação com componente (botão, select menu, etc) | ||
| return await handleMessageComponent(interaction, env); | ||
|
|
||
| case InteractionType.APPLICATION_COMMAND_AUTOCOMPLETE: | ||
| // Autocomplete de comando | ||
| return await handleAutocomplete(interaction, env); | ||
|
|
||
| case InteractionType.MODAL_SUBMIT: | ||
| // Submissão de modal | ||
| return await handleModalSubmit(interaction, env); | ||
|
|
||
| default: | ||
| console.warn(`Unknown interaction type: ${interaction.type}`); | ||
| return new Response("Unknown interaction type", { status: 400 }); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Responde ao PING do Discord (validação de endpoint) | ||
| */ | ||
| function respondToPing(): Response { | ||
| const response: InteractionResponse = { | ||
| type: InteractionResponseType.PONG, | ||
| }; | ||
|
|
||
| return new Response(JSON.stringify(response), { | ||
| status: 200, | ||
| headers: { "Content-Type": "application/json" }, | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Processa comandos de aplicação (slash commands) | ||
| */ | ||
| async function handleApplicationCommand( | ||
| interaction: DiscordInteraction, | ||
| env: Env | ||
| ): Promise<Response> { | ||
| // Responder imediatamente para evitar timeout (3 segundos) | ||
| const response: InteractionResponse = { | ||
| type: InteractionResponseType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, | ||
| }; | ||
|
|
||
| // Processar comando de forma assíncrona | ||
| // @ts-ignore - waitUntil existe em Cloudflare Workers | ||
| if (env.ctx?.waitUntil) { | ||
| // @ts-ignore | ||
| env.ctx.waitUntil(processCommandAsync(interaction, env)); | ||
| } else { | ||
| // Fallback para ambientes sem waitUntil | ||
| processCommandAsync(interaction, env).catch((error) => { | ||
| console.error("Error processing command:", error); | ||
| }); | ||
| } | ||
|
|
||
| return new Response(JSON.stringify(response), { | ||
| status: 200, | ||
| headers: { "Content-Type": "application/json" }, | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Processa o comando de forma assíncrona | ||
| */ | ||
| async function processCommandAsync( | ||
| interaction: DiscordInteraction, | ||
| env: Env | ||
| ): Promise<void> { | ||
| try { | ||
| const commandName = interaction.data?.name; | ||
| const options = interaction.data?.options || []; | ||
|
|
||
| // Extrair valor do primeiro argumento (se houver) | ||
| const firstOption = options[0]; | ||
| const userInput = firstOption?.value?.toString() || ""; | ||
|
|
||
| console.log(`Processing command: ${commandName}`, { | ||
| guildId: interaction.guild_id, | ||
| channelId: interaction.channel_id, | ||
| userId: interaction.member?.user?.id || interaction.user?.id, | ||
| input: userInput, | ||
| }); | ||
|
|
||
| // Aqui você pode: | ||
| // 1. Buscar configuração do comando em um KV ou D1 | ||
| // 2. Rotear para um agente Deco específico | ||
| // 3. Processar com IA | ||
| // 4. Integrar com outros serviços | ||
|
|
||
| // Por enquanto, vamos responder com uma mensagem simples | ||
| const responseMessage = `Comando **/${commandName}** recebido!\n\n` + | ||
| `📝 Input: ${userInput || "(nenhum)"}\n` + | ||
| `🆔 Guild: ${interaction.guild_id}\n` + | ||
| `📍 Channel: ${interaction.channel_id}`; | ||
|
|
||
| // Enviar resposta via followup | ||
| await sendFollowupMessage(interaction, env, { | ||
| content: responseMessage, | ||
| }); | ||
| } catch (error) { | ||
| console.error("Error in processCommandAsync:", error); | ||
|
|
||
| // Enviar mensagem de erro | ||
| await sendFollowupMessage(interaction, env, { | ||
| content: "❌ Ocorreu um erro ao processar o comando.", | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Envia uma mensagem de followup (após resposta inicial) | ||
| */ | ||
| async function sendFollowupMessage( | ||
| interaction: DiscordInteraction, | ||
| env: Env, | ||
| message: { | ||
| content?: string; | ||
| embeds?: any[]; | ||
| components?: any[]; | ||
| } | ||
| ): Promise<void> { | ||
| const url = `${DISCORD_API_URL}/webhooks/${interaction.application_id}/${interaction.token}`; | ||
|
|
||
| // Suporte para dev local e produção | ||
| const state = env.DECO_REQUEST_CONTEXT?.state; | ||
| const botToken = state?.botToken || (env as any).DISCORD_BOT_TOKEN; | ||
|
|
||
| const response = await fetch(url, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| Authorization: `Bot ${botToken}`, | ||
| }, | ||
| body: JSON.stringify(message), | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const errorText = await response.text(); | ||
| console.error("Failed to send followup message:", response.status, errorText); | ||
| throw new Error(`Failed to send followup: ${response.status}`); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Processa interações com componentes (botões, select menus) | ||
| */ | ||
| async function handleMessageComponent( | ||
| interaction: DiscordInteraction, | ||
| _env: Env | ||
| ): Promise<Response> { | ||
| // Responder ao componente | ||
| const response: InteractionResponse = { | ||
| type: InteractionResponseType.UPDATE_MESSAGE, | ||
| data: { | ||
| content: `Você clicou no componente: ${interaction.data?.custom_id}`, | ||
| }, | ||
| }; | ||
|
|
||
| return new Response(JSON.stringify(response), { | ||
| status: 200, | ||
| headers: { "Content-Type": "application/json" }, | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Processa autocomplete de comandos | ||
| */ | ||
| async function handleAutocomplete( | ||
| _interaction: DiscordInteraction, | ||
| _env: Env | ||
| ): Promise<Response> { | ||
| // Retornar opções de autocomplete | ||
| const response: InteractionResponse = { | ||
| type: InteractionResponseType.APPLICATION_COMMAND_AUTOCOMPLETE_RESULT, | ||
| data: { | ||
| // @ts-ignore - choices não está em InteractionCallbackData mas é válido | ||
| choices: [ | ||
| { name: "Opção 1", value: "opcao1" }, | ||
| { name: "Opção 2", value: "opcao2" }, | ||
| ], | ||
| }, | ||
| }; | ||
|
|
||
| return new Response(JSON.stringify(response), { | ||
| status: 200, | ||
| headers: { "Content-Type": "application/json" }, | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Processa submissão de modals | ||
| */ | ||
| async function handleModalSubmit( | ||
| _interaction: DiscordInteraction, | ||
| _env: Env | ||
| ): Promise<Response> { | ||
| // Processar dados do modal | ||
| const response: InteractionResponse = { | ||
| type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, | ||
| data: { | ||
| content: "Modal recebido com sucesso!", | ||
| }, | ||
| }; | ||
|
|
||
| return new Response(JSON.stringify(response), { | ||
| status: 200, | ||
| headers: { "Content-Type": "application/json" }, | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Edita a resposta original de uma interação | ||
| */ | ||
| export async function editOriginalResponse( | ||
| interaction: DiscordInteraction, | ||
| env: Env, | ||
| message: { | ||
| content?: string; | ||
| embeds?: any[]; | ||
| components?: any[]; | ||
| } | ||
| ): Promise<void> { | ||
| const url = `${DISCORD_API_URL}/webhooks/${interaction.application_id}/${interaction.token}/messages/@original`; | ||
|
|
||
| // Suporte para dev local e produção | ||
| const state = env.DECO_REQUEST_CONTEXT?.state; | ||
| const botToken = state?.botToken || (env as any).DISCORD_BOT_TOKEN; | ||
|
|
||
| const response = await fetch(url, { | ||
| method: "PATCH", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| Authorization: `Bot ${botToken}`, | ||
| }, | ||
| body: JSON.stringify(message), | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const errorText = await response.text(); | ||
| console.error("Failed to edit original response:", response.status, errorText); | ||
| throw new Error(`Failed to edit response: ${response.status}`); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Format file to satisfy oxfmt.
GitHub Actions indicates formatting issues here. Run:
bun run fmtso this file conforms to the formatter and CI passes.
🧰 Tools
🪛 GitHub Actions: Checks
[error] 1-1: Formatting issues detected by oxfmt. Run 'bun run fmt' (without --check) to fix this file.
🤖 Prompt for AI Agents
discord-bot/server/lib/webhook-handler.ts lines 1-327: the file fails the
repository formatter (oxfmt) check in CI; run the project's formatter and commit
the updated file to fix whitespace/formatting issues. Execute "bun run fmt" (or
the equivalent project formatting command), review the changes, run lint/tests
if needed, then add/commit the formatted file and push so CI will pass.
| async function sendFollowupMessage( | ||
| interaction: DiscordInteraction, | ||
| env: Env, | ||
| message: { | ||
| content?: string; | ||
| embeds?: any[]; | ||
| components?: any[]; | ||
| } | ||
| ): Promise<void> { | ||
| const url = `${DISCORD_API_URL}/webhooks/${interaction.application_id}/${interaction.token}`; | ||
|
|
||
| // Suporte para dev local e produção | ||
| const state = env.DECO_REQUEST_CONTEXT?.state; | ||
| const botToken = state?.botToken || (env as any).DISCORD_BOT_TOKEN; | ||
|
|
||
| const response = await fetch(url, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| Authorization: `Bot ${botToken}`, | ||
| }, | ||
| body: JSON.stringify(message), | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const errorText = await response.text(); | ||
| console.error("Failed to send followup message:", response.status, errorText); | ||
| throw new Error(`Failed to send followup: ${response.status}`); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fail fast when bot token is missing for followups/edits.
sendFollowupMessage and editOriginalResponse derive botToken from env.DECO_REQUEST_CONTEXT?.state.botToken || (env as any).DISCORD_BOT_TOKEN, but don’t guard against it being absent. In that case you’ll send Authorization: Bot undefined, get a 401 from Discord, and only see it as a generic “failed to send/edit” error.
Consider adding an explicit check:
- const state = env.DECO_REQUEST_CONTEXT?.state;
- const botToken = state?.botToken || (env as any).DISCORD_BOT_TOKEN;
+ const state = env.DECO_REQUEST_CONTEXT?.state;
+ const botToken = state?.botToken || (env as any).DISCORD_BOT_TOKEN;
+
+ if (!botToken) {
+ console.error("Discord bot token not configured for followup/edit");
+ throw new Error("Discord bot token not configured");
+ }This makes configuration issues obvious and avoids confusing downstream errors.
Also applies to: 297-325
🤖 Prompt for AI Agents
In discord-bot/server/lib/webhook-handler.ts around lines 196 to 225 (and
likewise for the editOriginalResponse block at 297-325), the function derives
botToken from env.DECO_REQUEST_CONTEXT?.state.botToken || (env as
any).DISCORD_BOT_TOKEN but does not validate it; add an explicit guard that
checks botToken is a non-empty string and if missing throw a clear, immediate
Error (or return a rejected Promise) like "Missing DISCORD_BOT_TOKEN /
state.botToken for followup/edit operations" before building headers or calling
fetch, so you fail fast with a meaningful message rather than sending an
Authorization: Bot undefined and getting a generic 401 from Discord.
- Introduced a new workspace for the Discord Bot, including essential configurations in package.json and bun.lock. - Created the Discord Bot server with tools for managing messages, channels, roles, threads, and webhooks. - Added TypeScript configuration, Vite setup, and comprehensive documentation for the Discord Bot features and tools. - Implemented a client for interacting with the Discord API, ensuring robust data handling and validation. - Included a README.md detailing installation, configuration, and usage instructions for the Discord Bot MCP server.
- Removed outdated comments and documentation from the guilds.ts file to streamline the codebase. - Renamed tools for fetching user information to include "Discord" in their identifiers for clarity. - Updated the Env interface to include new methods for managing Discord interactions, enhancing the bot's capabilities. - Added comprehensive TypeScript types and Zod schemas for input/output validation in the deco.gen.ts file, improving data integrity and consistency across the Discord bot functionalities.
636ba2f to
43a4606
Compare
Summary by CodeRabbit
New Features
Documentation
Chores
✏️ Tip: You can customize this high-level summary in your review settings.