Bright ideas and techniques for building with Convex.
Profile image
Sarah Shader
a year ago

Discord Bot Webhooks with Convex

A prompt and response from a Discord bot powered by Convex

Convex provides client libraries for storing and interacting with data, but sometimes we want a third-party app like Discord to interact with data in Convex. This is often done via a webhook over HTTP, which Convex recently added support for! I used this functionality to build a simple Discord bot that posts custom responses to certain key phrases.

I store my prompts and responses using Convex, and can edit them from a web app using the ConvexReactClient (shown in orange). When I trigger my Discord bot, Discord sends an HTTP request via a webhook to a configured URL (my Convex deployment), which I handle using an httpAction (shown in blue).

Diagram

The full code for my app is in https://github.com/sshader/discord-bot, but I'll focus on the webhook specifically.

After setting up my Discord bot and registering a command, I now needed to figure out how to respond to the webhook requests. I can define an endpoint that just returns a successful response to test that everything is hooked up correctly:

// https://github.com/sshader/discord-bot/blob/stack/convex/http.ts#L9
const http = httpRouter();
http.route({
  path: '/discord',
  method: 'POST',
  handler: httpAction(async ({}, request) => {
    return new Response(null, { status: 200 })
  })
});

When I try updating my Discord bot settings, I can see in the Convex dashboard that my endpoint is getting run.

Screenshot

Screenshot

Discord requires that you (1) acknowledge a “ping” request and (2) verify the signature of the request and reject it if it’s invalid before it will send your endpoint any webhook events. I used the discord-interactions npm package to verify the signature.

// https://github.com/sshader/discord-bot/blob/stack/convex/http.ts#L14
httpAction(async ({}, request) => {
  const bodyText = await request.text();

  // Check signature -- uses discord-interactions package
  const isValidSignature = verifyKey(
    bodyText,
    request.headers.get('X-Signature-Ed25519'),
    request.headers.get('X-Signature-Timestamp'),
    process.env.DISCORD_PUBLIC_KEY
  )
  if (!isValidSignature) {
    return new Response("invalid request signature", { status: 401 });
  }
	
  const body = JSON.parse(bodyText);
  // Handle ping
  if (body.type === InteractionType.PING) {
    return new Response(
      JSON.stringify({ type: InteractionResponseType.PONG }),
      { status: 200 }
    )
  }
});

In addition to the Convex docs it was helpful to look at Cloudflare workers, which have similar JS handlers using the fetch standard. Viewing my endpoint logs in the dashboard helped me catch an issue where I initially did these steps in the reverse order, and so wasn’t verifying signatures on the “ping” requests.

The last part is to query my Convex data and respond to Discord -- I have a Convex query that returns a stores response for a given prompt which I'm using here:

// https://github.com/sshader/discord-bot/blob/stack/convex/http.ts#L37
const data = body.data;
if (
  body.type === InteractionType.APPLICATION_COMMAND &&
  data.name === "ask_convex"
) {
  const prompt = data.options[0].value;
  // See https://github.com/sshader/discord-bot/blob/stack/convex/botResponses.ts#L2
  const botResponse = await runQuery("bot_responses:getBotResponse", { prompt });

  return new Response(
    JSON.stringify({
      type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
      data: { content: botResponse },
    }),
    { headers: { "content-type": "application/json" }, status: 200 }
  );
} 

Here’s how it looks all together!

Screenshot of the final app

Build in minutes, scale forever.

Convex is the backend application platform with everything you need to build your project. Cloud functions, a database, file storage, scheduling, search, and realtime updates fit together seamlessly.

Get started