bwmarrin / discordgo

(Golang) Go bindings for Discord
BSD 3-Clause "New" or "Revised" License
5k stars 795 forks source link

I can't verify requests for Discord interactions URL properly #1526

Open preslavmihaylov opened 4 months ago

preslavmihaylov commented 4 months ago

I'm trying to write a golang handler, which reacts to discord interactions. I've set it up as the interactions URL in my discord developer portal.

This is the code I've written:

var secrets struct {
    DiscordPublicKey string
}

type UnknownDiscordInteraction struct {
    InteractionType discordgo.InteractionType `json:"type"`
}

func DiscordWebhook(w http.ResponseWriter, r *http.Request) {
    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Error reading request body", http.StatusInternalServerError)
        return
    }

    publicKeyBytes, err := hex.DecodeString(secrets.DiscordPublicKey)
    if err != nil {
        http.Error(w, "Error decoding hex string", http.StatusInternalServerError)
        return
    }

    if !discordgo.VerifyInteraction(r, publicKeyBytes) {
        http.Error(w, "invalid request signature", http.StatusUnauthorized)
        return
    }

    var interaction UnknownDiscordInteraction
    if err := json.Unmarshal(body, &interaction); err != nil {
        http.Error(w, "Error unmarshalling request body", http.StatusInternalServerError)
        return
    }

    if interaction.InteractionType == discordgo.InteractionPing {
        pongResp, err := json.Marshal(discordgo.InteractionResponse{
            Type: discordgo.InteractionResponsePong,
        })
        if err != nil {
            http.Error(w, "Error marshalling response", http.StatusInternalServerError)
            return
        }

        // set content-type header
        w.Header().Set("Content-Type", "application/json")
        w.Write(pongResp)
        w.WriteHeader(http.StatusOK)
        return
    }

    w.WriteHeader(http.StatusOK)
}

I can't save the endpoint in the developer dashboard, because any challenge request that discord sends I fail to verify, it returns that the request is invalid.

I am passing the DiscordPublicKey as a string, copied from my discord developer dashboard. Anyone has a clue what I'm doing wrong?

piesuke commented 1 month ago

@preslavmihaylov I'm facing the same problem.

Did you find a solution?

preslavmihaylov commented 1 month ago

No, I ended up writing a proxy in javascript which forwards discord requests to my go service to workaround this.

This is the JS code to handle that in case it's useful (deployed in Render):

import Fastify from "fastify";
import { Events } from "discord.js";

import { Client, GatewayIntentBits } from "discord.js";

const PROXY_ENDPOINT = process.env.PROXY_ENDPOINT || "";
const DISCORD_TOKEN = process.env.DISCORD_TOKEN || "";
const DISCORD_HANDLER_SECRET_TOKEN =
  process.env.DISCORD_HANDLER_SECRET_TOKEN || "";
// const DISCORD_APP_ID = process.env.DISCORD_APP_ID || "";
// const DISCORD_PUBLIC_KEY = process.env.DISCORD_PUBLIC_KEY || "";

const discordClient = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMembers,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent,
  ],
});

discordClient.once(Events.ClientReady, (readyClient) => {
  console.log(`Discord bot ready! Logged in as ${readyClient.user.tag}`);
});

discordClient.on(Events.MessageCreate, async (message) => {
  console.log(
    `Received message in ${message.channelId} from user ${message.author.id}`
  );
  console.log(message.toJSON());
  const response = await fetch(PROXY_ENDPOINT, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${DISCORD_HANDLER_SECRET_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(message.toJSON()),
  });
  const json = await response.json();
  console.log("Received response from proxy:", json);
});

discordClient.on(Events.InteractionCreate, async (interaction) => {
  if (interaction.isChatInputCommand()) {
    console.log("Received chat input command");
  }
  if (interaction.isMessageContextMenuCommand()) {
    console.log("Received message context menu command");
  }
  if (interaction.isModalSubmit()) {
    console.log("Received modal submission");
  }
});

discordClient.login(DISCORD_TOKEN);

const host = "RENDER" in process.env ? `0.0.0.0` : `localhost`;
const port = process.env.PORT || "4000";

const fastify = Fastify({
  logger: true,
});

// render healthcheck
fastify.head("/", async (_request, reply) => {
  reply.send();
});

fastify.get("/", async (_request, reply) => {
  reply.send();
});

fastify.listen({ host, port: parseInt(port) }, (err, address) => {
  if (err) {
    fastify.log.error(err);
    process.exit(1);
  }

  console.log(`Server is now listening on ${address}`);
});