Skip to content

Commit

Permalink
324 complete slackbot connector (#326)
Browse files Browse the repository at this point in the history
  • Loading branch information
mlhaufe authored Aug 18, 2024
1 parent 9545cdd commit 5278cf6
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 81 deletions.
1 change: 1 addition & 0 deletions .devcontainer/compose.extend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ services:
command: /bin/sh -c "while sleep 1000; do :; done"
ports:
- 3000:3000
- 443:3000
networks:
- app-network
# puts the Dev Container on the same network as the database, so that it can access it on localhost
Expand Down
4 changes: 2 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "npm install --verbose --no-progress",
// "postCreateCommand": "",
// https://www.kenmuse.com/blog/avoiding-dubious-ownership-in-dev-containers/
"postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
"postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder} && npm install --verbose --no-progress",
// Configure tool-specific properties.
"customizations": {
"vscode": {
Expand Down
7 changes: 1 addition & 6 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,4 @@ services:
restart: unless-stopped
volumes:
- ${WEBAPP_STORAGE_HOME}/site/wwwroot:/home/site/wwwroot
command: node /home/site/wwwroot/server/index.mjs
environment:
NUXT_HOST: 0.0.0.0
NUXT_PORT: 3000
ports:
- 443:3000
command: node /home/site/wwwroot/server/index.mjs
13 changes: 13 additions & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ export default defineNuxtConfig({
authPrimaryUserFlow: '',
sessionPassword: '',
origin: '',
port: '',
slackAppToken: '',
slackSigningSecret: '',
slackClientId: '',
slackClientSecret: '',
slackBotToken: '',

// The public keys which are available both client-side and server-side
public: {}
Expand Down Expand Up @@ -93,6 +99,13 @@ export default defineNuxtConfig({
}
},
},
routeRules: {
'/api/slack': {
security: {
xssValidator: false
}
}
},
nitro: {
esbuild: {
options: {
Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@final-hill/cathedral",
"version": "0.12.2",
"version": "0.13.0",
"description": "Requirements management system",
"keywords": [],
"private": true,
Expand Down Expand Up @@ -68,4 +68,4 @@
"typescript": "^5.5.4",
"vue-tsc": "^2.0.28"
}
}
}
136 changes: 71 additions & 65 deletions server/api/slack-bot/index.post.ts → server/api/slack/index.post.ts
Original file line number Diff line number Diff line change
@@ -1,79 +1,87 @@
import { z } from "zod"
import crypto from 'crypto'
import { WebClient } from "@slack/web-api"
import crypto from 'crypto'
import { z } from "zod"

// https://api.slack.com/events
const eventSchema = z.object({
type: z.string(),
user: z.string(),
text: z.string(),
ts: z.string(),
channel: z.string(),
event_ts: z.string(),
})

const urlVerificationSchema = z.object({
token: z.string(),
challenge: z.string(),
type: z.literal("url_verification"),
type: z.literal("url_verification")
})

const appMentionSchema = z.object({
type: z.literal("app_mention"),
user: z.string(),
bot_id: z.string().optional(),
text: z.string(),
ts: z.string(),
channel: z.string(),
event_ts: z.string(),
})

const messageSchema = z.object({
type: z.literal("message"),
bot_id: z.string().optional(),
user: z.string(),
text: z.string(),
ts: z.string(),
channel: z.string(),
event_ts: z.string()
})

const eventCallbackSchema = z.object({
token: z.string(),
team_id: z.string(),
api_app_id: z.string(),
event: appMentionSchema,
type: z.literal("event_callback"),
event: z.union([appMentionSchema, messageSchema]),
type: z.literal('event_callback'),
event_id: z.string(),
event_time: z.number(),
authed_users: z.array(z.string()),
})
authorizations: z.array(z.object({
enterprise_id: z.string().nullable(),
team_id: z.string(),
user_id: z.string(),
is_bot: z.boolean(),
is_enterprise_install: z.boolean()
})),
is_ext_shared_channel: z.boolean(),
event_context: z.string()
});

const bodySchema = z.union([urlVerificationSchema, eventCallbackSchema])

const slack = new WebClient(process.env.SLACK_BOT_TOKEN)

async function sendResponse(slackEvent: NonNullable<typeof eventSchema._type>) {
const { channel, ts } = slackEvent
const config = useRuntimeConfig(),
slack = new WebClient(config.slackBotToken)

async function sendResponse(slackEvent: typeof eventCallbackSchema._type.event) {
try {
const thread = await slack.conversations.replies({
channel,
ts,
inclusive: true,
})
// const thread = await slack.conversations.replies({
// channel,
// ts,
// // inclusive: true,
// })

// const prompts = await generatePromptFromThread(thread)
// const llmResponse = await getLlmResponse(prompts)

await slack.chat.postMessage({
channel,
text: 'Your message has been received, but I am unable to respond at this time.',
thread_ts: ts,
const result = await slack.chat.postMessage({
channel: slackEvent.channel,
text: 'Message received. I do not have a response yet.',
thread_ts: slackEvent.ts,
})
} catch (error) {
await slack.chat.postMessage({
channel,
thread_ts: ts,
text: `@${process.env.SLACK_ADMIN_MEMBER_ID} ` +
`There was an error processing the message. ` +
`Error: ${error instanceof Error ? error.message : JSON.stringify(error)}`
throw createError({
statusCode: 500,
statusMessage: 'Internal Server Error',
message: 'Failed to send response to Slack',
data: { error }
})
}
}

// https://api.slack.com/authentication/verifying-requests-from-slack#validating-a-request
function isValidSlackRequest(headers: Headers, rawBody: string) {
const signingSecret = process.env.SLACK_SIGNING_SECRET!,
const signingSecret = config.slackSigningSecret,
timestamp = headers.get('X-Slack-Request-Timestamp')!

// Prevent replay attacks by checking the timestamp
Expand All @@ -100,53 +108,51 @@ function isValidSlackRequest(headers: Headers, rawBody: string) {
return computedSignature === slackSignature
}

/**
* Manages communications from the Slack bot
*/
export default defineEventHandler(async (event) => {
const rawBody = (await readRawBody(event))!

const body = await readValidatedBody(event, (b) => bodySchema.safeParse(b)),
rawBody = (await readRawBody(event))!,
headers = event.headers

if (!process.env.SLACK_BOT_TOKEN)
if (!body.success)
throw createError({
statusCode: 500,
statusMessage: 'Internal Server Error',
message: 'Slack bot token not found'
statusCode: 400,
statusMessage: 'Bad Request',
message: 'Invalid request body'
})

console.log('SLACKBOT API BODY:', JSON.stringify(body.data))
/*
if (!body.success)
throw createError({
statusCode: 400,
statusMessage: 'Bad Request: Invalid body parameters',
message: JSON.stringify(body.error.errors)
})
*/

if (!isValidSlackRequest(headers, rawBody))
throw createError({
statusCode: 403,
statusMessage: 'Forbidden',
message: 'Invalid Slack request signature'
})

const requestType = body.data!.type
const requestType = body.data?.type

if (!body.data)
return {}

switch (requestType) {
case 'url_verification':
return { challenge: body.data!.challenge };
return { challenge: body.data.challenge }
case 'event_callback':
const eventType = body.data!.event!.type
if (eventType === 'app_mention')
return await sendResponse(body.data!.event!)

throw createError({
statusCode: 400,
statusMessage: 'Bad Request: Invalid event type',
message: `Unhandled event type: ${eventType}`
})
const eventType = body.data.event.type
switch (eventType) {
case 'app_mention':
case 'message':
// prevent infinite loop by ignoring messages from bots
// Including messages from our own bot
if (body.data.event.bot_id)
return {}
return await sendResponse(body.data.event)
default:
throw createError({
statusCode: 400,
statusMessage: 'Bad Request: Invalid event type',
message: `Unhandled event type: ${eventType}`
})
}
default:
throw createError({
statusCode: 400,
Expand Down
2 changes: 1 addition & 1 deletion server/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getServerSession } from '#auth'
export default eventHandler(async (event) => {
const url = event.node.req.url!

if (url.startsWith('/api/slack-bot') || url.startsWith("/api/auth") || !url.startsWith("/api"))
if (url.startsWith('/api/slack') || url.startsWith("/api/auth") || !url.startsWith("/api"))
return

const session = await getServerSession(event)
Expand Down

0 comments on commit 5278cf6

Please sign in to comment.