diff --git a/bun.lockb b/bun.lockb index 69d9750..81e47a2 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 7f8a562..b9bfb12 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,10 @@ "dependencies": { "@nuxtjs/eslint-config": "^12.0.0", "chart.js": "^4.4.0", + "discord-api-types": "^0.37.63", "eslint": "^8.50.0", "node-html-parser": "^6.1.10", + "tweetnacl": "^1.0.3", "typescript": "^5.2.2", "validator": "^13.11.0", "vue-chartjs": "^5.2.0", diff --git a/server/api/discord.ts b/server/api/discord.ts new file mode 100644 index 0000000..3e10398 --- /dev/null +++ b/server/api/discord.ts @@ -0,0 +1,100 @@ +import nacl from 'tweetnacl' +import type { + APIApplicationCommandInteraction, + APIBaseInteraction, + APIInteractionResponseChannelMessageWithSource, + APIPingInteraction, + APIInteractionResponseCallbackData +} from 'discord-api-types/v10' +import { InteractionResponseType, InteractionType, MessageFlags } from 'discord-api-types/v10' +import { SupabaseClient } from '@supabase/supabase-js' +import { Database } from '~/types/supabase' +import { serverSupabaseServiceRole } from '#supabase/server' + +export default defineEventHandler(async (event) => { + const body: APIBaseInteraction = await readBody(event) + const signature = event.headers.get('x-signature-ed25519') as string + const timestamp = event.headers.get('x-signature-timestamp') as string + + const rawBody = await readRawBody(event) + + const client = serverSupabaseServiceRole(event) + + if (!rawBody) { + throw createError({ + statusCode: 400, + statusMessage: 'no body' + }) + } + + if (!verifySecurity(signature, timestamp, rawBody, useRuntimeConfig().discordPublicKey || 'nope')) { + throw createError({ + statusCode: 401, + statusMessage: 'invalid request signature' + }) + } + + // Ping + if (body.type === InteractionType.Ping) { + return { + type: InteractionResponseType.Pong + } + } + + if (body.type !== InteractionType.ApplicationCommand) { + throw createError({ + statusCode: 400, + statusMessage: 'invalid interaction type' + }) + } + + // handle commands here. they're here somewhere! + switch ((body as unknown as APIApplicationCommandInteraction).data.name) { + // /profile + case 'profile': { + const user = await findDiscordUser('my Cool ID', client) + + if (!user) { + return discordFailureResponse('You must link your account first!') + } + + return { + type: InteractionResponseType.ChannelMessageWithSource, + data: { + content: 'Hello, world!', + flags: MessageFlags.Ephemeral + } as APIInteractionResponseCallbackData + } as APIInteractionResponseChannelMessageWithSource + } + } +}) + +function findDiscordUser(userId: String, supabaseClient: SupabaseClient) { + return supabaseClient.from('integrations').select('*') + .eq('platform', 'discord') + .eq('data->>id', userId) + .single() +} + +function discordFailureResponse(msg: string): APIInteractionResponseChannelMessageWithSource { + return { + type: InteractionResponseType.ChannelMessageWithSource, + data: { + content: 'Failed! ' + msg, + flags: MessageFlags.Ephemeral + } as APIInteractionResponseCallbackData + } as APIInteractionResponseChannelMessageWithSource +} + +/** + * Required per Discord's guidelines. + */ +function verifySecurity(signature: string, timestamp: string, body: string, publicKey: string) { + const msg = timestamp.trim() + body.trim() + + return nacl.sign.detached.verify( + Buffer.from(msg), + Buffer.from(signature.trim(), 'hex'), + Buffer.from(publicKey.trim(), 'hex') + ) +}