@cf-agents/discord
Advanced tools
| # @cf-agents/discord | ||
| ## 0.1.0 | ||
| ### Minor Changes | ||
| - 0f96a03: Added support for 'Single Channel Mode', allowing a default channel/chat ID to be configured at tool initialization. |
+110
| /** | ||
| * Verify a Discord webhook signature using Ed25519. | ||
| */ | ||
| export async function verifyDiscordSignature( | ||
| payload: string, | ||
| signature: string, | ||
| timestamp: string, | ||
| publicKey: string | ||
| ): Promise<boolean> { | ||
| try { | ||
| const encoder = new TextEncoder(); | ||
| const data = encoder.encode(timestamp + payload); | ||
| const signatureBytes = new Uint8Array( | ||
| signature.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)) | ||
| ); | ||
| const publicKeyBytes = new Uint8Array( | ||
| publicKey.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)) | ||
| ); | ||
| const key = await crypto.subtle.importKey( | ||
| "raw", | ||
| publicKeyBytes, | ||
| { name: "Ed25519", namedCurve: "Ed25519" }, | ||
| false, | ||
| ["verify"] | ||
| ); | ||
| return await crypto.subtle.verify( | ||
| "Ed25519", | ||
| key, | ||
| signatureBytes, | ||
| data | ||
| ); | ||
| } catch (e) { | ||
| console.error("Discord signature verification failed:", e); | ||
| return false; | ||
| } | ||
| } | ||
| export type DiscordInteractionResponse = { | ||
| message: string; | ||
| userId: string; | ||
| channelId: string; | ||
| reply: (text: string) => Promise<void>; | ||
| }; | ||
| /** | ||
| * Standard handler for Discord interactions (slash commands or outgoing webhooks) | ||
| */ | ||
| export async function handleDiscordWebhook( | ||
| request: Request, | ||
| env: { DISCORD_PUBLIC_KEY: string; DISCORD_TOKEN: string } | ||
| ): Promise<DiscordInteractionResponse | Response | null> { | ||
| if (request.method !== "POST") return null; | ||
| const signature = request.headers.get("X-Signature-Ed25519"); | ||
| const timestamp = request.headers.get("X-Signature-Timestamp"); | ||
| const body = await request.text(); | ||
| if (!signature || !timestamp || !env.DISCORD_PUBLIC_KEY) { | ||
| return new Response("Missing signature headers", { status: 401 }); | ||
| } | ||
| const isValid = await verifyDiscordSignature( | ||
| body, | ||
| signature, | ||
| timestamp, | ||
| env.DISCORD_PUBLIC_KEY | ||
| ); | ||
| if (!isValid) { | ||
| return new Response("Invalid request signature", { status: 401 }); | ||
| } | ||
| const interaction = JSON.parse(body); | ||
| // Handle PING from Discord | ||
| if (interaction.type === 1) { | ||
| return new Response(JSON.stringify({ type: 1 }), { | ||
| headers: { "Content-Type": "application/json" }, | ||
| }); | ||
| } | ||
| // Handle Application Command (Slash Command) or Message Component | ||
| if (interaction.type === 2 || interaction.type === 3) { | ||
| const userId = interaction.member?.user?.id || interaction.user?.id; | ||
| const channelId = interaction.channel_id; | ||
| const message = interaction.data?.options?.[0]?.value || interaction.data?.custom_id || ""; | ||
| return { | ||
| message, | ||
| userId, | ||
| channelId, | ||
| reply: async (text: string) => { | ||
| const url = `https://discord.com/api/v10/interactions/${interaction.id}/${interaction.token}/callback`; | ||
| await fetch(url, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| type: 4, // CHANNEL_MESSAGE_WITH_SOURCE | ||
| data: { content: text }, | ||
| }), | ||
| }); | ||
| }, | ||
| }; | ||
| } | ||
| return null; | ||
| } |
+1
-1
| { | ||
| "name": "@cf-agents/discord", | ||
| "version": "0.0.1", | ||
| "version": "0.1.0", | ||
| "main": "src/index.ts", | ||
@@ -5,0 +5,0 @@ "types": "src/index.ts", |
+22
-4
@@ -20,6 +20,4 @@ # @cf-agents/discord | ||
| ## Usage | ||
| Initialize the tools with your Discord Bot Token and an optional default channel. | ||
| Initialize the tools with your Discord Bot Token. | ||
| ```typescript | ||
@@ -31,4 +29,24 @@ import { createDiscordTools } from "@cf-agents/discord"; | ||
| token: "YOUR_BOT_TOKEN" | ||
| }) | ||
| }), | ||
| config: { | ||
| channelId: "OPTIONAL_DEFAULT_CHANNEL_ID" | ||
| } | ||
| }); | ||
| ```## Two-Way Messaging (Webhooks) | ||
| Handle incoming Discord interactions (slash commands) to trigger your agent. | ||
| ```typescript | ||
| import { handleDiscordWebhook } from "@cf-agents/discord"; | ||
| // In your fetch handler | ||
| const result = await handleDiscordWebhook(request, { | ||
| DISCORD_PUBLIC_KEY: env.DISCORD_PUBLIC_KEY, | ||
| DISCORD_TOKEN: env.DISCORD_TOKEN | ||
| }); | ||
| if (result && "message" in result) { | ||
| // Process with agent and reply | ||
| await result.reply("Processing your request..."); | ||
| } | ||
| ``` |
+1
-0
| export * from "./types"; | ||
| export * from "./tools"; | ||
| export * from "./webhooks"; |
+17
-4
@@ -7,2 +7,5 @@ import { tool } from "ai"; | ||
| getToken: () => Promise<DiscordToken>; | ||
| config?: { | ||
| channelId?: string; | ||
| }; | ||
| } | ||
@@ -27,8 +30,13 @@ | ||
| parameters: z.object({ | ||
| channelId: z.string().describe("The ID of the channel to send to"), | ||
| channelId: z.string().optional().describe("The ID of the channel to send to. Optional if a channel is pre-configured."), | ||
| content: z.string().describe("The message content"), | ||
| }), | ||
| execute: async ({ channelId, content }) => { | ||
| const targetChannelId = channelId ?? context.config?.channelId; | ||
| if (!targetChannelId) { | ||
| throw new Error("No channelId provided and no default channel configured."); | ||
| } | ||
| const response = await getAuthenticatedFetch( | ||
| `${DISCORD_API_BASE}/channels/${channelId}/messages`, | ||
| `${DISCORD_API_BASE}/channels/${targetChannelId}/messages`, | ||
| { | ||
@@ -52,9 +60,14 @@ method: "POST", | ||
| parameters: z.object({ | ||
| channelId: z.string().describe("The ID of the channel to read from"), | ||
| channelId: z.string().optional().describe("The ID of the channel to read from. Optional if a channel is pre-configured."), | ||
| limit: z.number().optional().default(10).describe("Number of messages to retrieve (max 100)"), | ||
| }), | ||
| execute: async ({ channelId, limit }) => { | ||
| const targetChannelId = channelId ?? context.config?.channelId; | ||
| if (!targetChannelId) { | ||
| throw new Error("No channelId provided and no default channel configured."); | ||
| } | ||
| const params = new URLSearchParams({ limit: String(limit) }); | ||
| const response = await getAuthenticatedFetch( | ||
| `${DISCORD_API_BASE}/channels/${channelId}/messages?${params}` | ||
| `${DISCORD_API_BASE}/channels/${targetChannelId}/messages?${params}` | ||
| ); | ||
@@ -61,0 +74,0 @@ |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
10478
83.15%9
28.57%256
72.97%51
54.55%2
100%