diff --git a/package.json b/package.json index c2e7ce6..66bb67f 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "type": "module", "scripts": { "generate": "kubb generate", + "check": "npm run typecheck && npm run lint && npm run fmt:check", "test": "node --test", "typecheck": "tsc", "lint": "oxlint", diff --git a/src/server.ts b/src/server.ts index 96e869e..83ca933 100644 --- a/src/server.ts +++ b/src/server.ts @@ -52,8 +52,9 @@ export function buildServer(deps: ServerDeps, opts?: { logger?: FastifyServerOpt name: { type: "string" }, email: { type: "string" }, reason: { type: "string" }, + livesHere: { type: "string" }, }, - required: ["name", "email", "reason"], + required: ["name", "email", "reason", "livesHere"], additionalProperties: false, } as const; @@ -67,10 +68,10 @@ export function buildServer(deps: ServerDeps, opts?: { logger?: FastifyServerOpt return reply.code(401).send({ error: "unauthorized" }); } - const { name, email, reason } = request.body; + const { name, email, reason, livesHere } = request.body; request.log.info({ name, channel: deps.inviteChannel }, "invites/request: posting request"); - const msg = formatInviteRequest({ name, email, reason }); + const msg = formatInviteRequest({ name, email, reason, livesHere }); try { await deps.slackClient.sendMessage({ msg, channel: deps.inviteChannel }); } catch (err) { @@ -153,16 +154,19 @@ function formatInviteRequest({ name, email, reason, + livesHere, }: { name: string; email: string; reason: string; + livesHere: string; }): string { return [ "New Slack invite request", `Name: ${name}`, `Email: ${email}`, `What brings you to devICT: ${reason}`, + `Do you live in the Wichita, KS region?: ${livesHere}`, "", "Approve by replying `@bot !invite` in this thread.", ].join("\n"); diff --git a/test/server.test.ts b/test/server.test.ts index 38d8b69..2800dbd 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -3,7 +3,6 @@ import assert from "node:assert/strict"; import { createHmac } from "node:crypto"; import { buildServer, type ServerDeps } from "../src/server.ts"; import type { AppConfig } from "../src/config.ts"; -import type { EventsClient } from "../src/events/events.ts"; import { verifySlackSignature } from "../src/slack/signature.ts"; import type { SlackClient } from "../src/slack/slack.ts"; @@ -137,9 +136,22 @@ test("POST /slack/webhook dispatches a message command", async () => { }); }, }; - const app = buildServer({ eventsClient: fakes().eventsClient, slackClient, inviteWebhookToken: config.SLACK_INVITE_WEBHOOK_TOKEN, inviteChannel: config.SLACK_INVITE_CHANNEL }, { logger: false }); + const app = buildServer( + { + eventsClient: fakes().eventsClient, + slackClient, + inviteWebhookToken: config.SLACK_INVITE_WEBHOOK_TOKEN, + inviteChannel: config.SLACK_INVITE_CHANNEL, + }, + { logger: false }, + ); - const event = { type: "app_mention", text: "<@U0> !events", channel: "C1", ts: "1700000000.000100" }; + const event = { + type: "app_mention", + text: "<@U0> !events", + channel: "C1", + ts: "1700000000.000100", + }; const body = JSON.stringify({ type: "event_callback", event }); const ts = Math.floor(Date.now() / 1000); const res = await app.inject({ @@ -208,7 +220,12 @@ test("POST /invites/request posts the request to the invite channel and returns }, }; const app = buildServer( - { eventsClient: fakes().eventsClient, slackClient, inviteWebhookToken: config.SLACK_INVITE_WEBHOOK_TOKEN, inviteChannel: config.SLACK_INVITE_CHANNEL }, + { + eventsClient: fakes().eventsClient, + slackClient, + inviteWebhookToken: config.SLACK_INVITE_WEBHOOK_TOKEN, + inviteChannel: config.SLACK_INVITE_CHANNEL, + }, { logger: false }, ); @@ -251,7 +268,12 @@ test("POST /invites/request returns 500 when sendMessage fails", async () => { }, }; const app = buildServer( - { eventsClient: fakes().eventsClient, slackClient, inviteWebhookToken: config.SLACK_INVITE_WEBHOOK_TOKEN, inviteChannel: config.SLACK_INVITE_CHANNEL }, + { + eventsClient: fakes().eventsClient, + slackClient, + inviteWebhookToken: config.SLACK_INVITE_WEBHOOK_TOKEN, + inviteChannel: config.SLACK_INVITE_CHANNEL, + }, { logger: false }, ); @@ -268,14 +290,22 @@ test("POST /invites/request returns 500 when sendMessage fails", async () => { await app.close(); }); -function signEvent(event: Record): { body: string; ts: number; headers: Record } { +function signEvent(event: Record): { + body: string; + ts: number; + headers: Record; +} { const body = JSON.stringify({ type: "event_callback", event }); const ts = Math.floor(Date.now() / 1000); - return { body, ts, headers: { - "content-type": "application/json", - "x-slack-request-timestamp": String(ts), - "x-slack-signature": sign(config.SLACK_SIGNING_SECRET, ts, body), - } }; + return { + body, + ts, + headers: { + "content-type": "application/json", + "x-slack-request-timestamp": String(ts), + "x-slack-signature": sign(config.SLACK_SIGNING_SECRET, ts, body), + }, + }; } test("!invite in invite channel thread invites the user from the parent message", async () => { @@ -290,7 +320,9 @@ test("!invite in invite channel thread invites the user from the parent message" }, async getThreadParent() { // Slack auto-linkifies emails in message text to . - return { text: "Name: Jane Doe\nEmail: \nWhat brings you: community" }; + return { + text: "Name: Jane Doe\nEmail: \nWhat brings you: community", + }; }, verifySignature({ timestamp, rawBody, signature }) { return verifySlackSignature({ @@ -302,11 +334,22 @@ test("!invite in invite channel thread invites the user from the parent message" }, }; const app = buildServer( - { eventsClient: fakes().eventsClient, slackClient, inviteWebhookToken: config.SLACK_INVITE_WEBHOOK_TOKEN, inviteChannel: config.SLACK_INVITE_CHANNEL }, + { + eventsClient: fakes().eventsClient, + slackClient, + inviteWebhookToken: config.SLACK_INVITE_WEBHOOK_TOKEN, + inviteChannel: config.SLACK_INVITE_CHANNEL, + }, { logger: false }, ); - const event = { type: "app_mention", text: "<@U0> !invite", channel: config.SLACK_INVITE_CHANNEL, ts: "1700000000.000200", thread_ts: "1700000000.000100" }; + const event = { + type: "app_mention", + text: "<@U0> !invite", + channel: config.SLACK_INVITE_CHANNEL, + ts: "1700000000.000200", + thread_ts: "1700000000.000100", + }; const { body, headers } = signEvent(event); const res = await app.inject({ method: "POST", url: "/slack/webhook", headers, payload: body }); @@ -338,11 +381,22 @@ test("!invite outside the invite channel is ignored", async () => { }, }; const app = buildServer( - { eventsClient: fakes().eventsClient, slackClient, inviteWebhookToken: config.SLACK_INVITE_WEBHOOK_TOKEN, inviteChannel: config.SLACK_INVITE_CHANNEL }, + { + eventsClient: fakes().eventsClient, + slackClient, + inviteWebhookToken: config.SLACK_INVITE_WEBHOOK_TOKEN, + inviteChannel: config.SLACK_INVITE_CHANNEL, + }, { logger: false }, ); - const event = { type: "app_mention", text: "<@U0> !invite", channel: "C-elsewhere", ts: "1700000000.000200", thread_ts: "1700000000.000100" }; + const event = { + type: "app_mention", + text: "<@U0> !invite", + channel: "C-elsewhere", + ts: "1700000000.000200", + thread_ts: "1700000000.000100", + }; const { body, headers } = signEvent(event); const res = await app.inject({ method: "POST", url: "/slack/webhook", headers, payload: body }); @@ -371,12 +425,22 @@ test("!invite without a thread is ignored", async () => { }, }; const app = buildServer( - { eventsClient: fakes().eventsClient, slackClient, inviteWebhookToken: config.SLACK_INVITE_WEBHOOK_TOKEN, inviteChannel: config.SLACK_INVITE_CHANNEL }, + { + eventsClient: fakes().eventsClient, + slackClient, + inviteWebhookToken: config.SLACK_INVITE_WEBHOOK_TOKEN, + inviteChannel: config.SLACK_INVITE_CHANNEL, + }, { logger: false }, ); // no thread_ts: top-level mention, not a reply - const event = { type: "app_mention", text: "<@U0> !invite", channel: config.SLACK_INVITE_CHANNEL, ts: "1700000000.000200" }; + const event = { + type: "app_mention", + text: "<@U0> !invite", + channel: config.SLACK_INVITE_CHANNEL, + ts: "1700000000.000200", + }; const { body, headers } = signEvent(event); const res = await app.inject({ method: "POST", url: "/slack/webhook", headers, payload: body }); @@ -408,11 +472,22 @@ test("!invite replies with error when getThreadParent fails", async () => { }, }; const app = buildServer( - { eventsClient: fakes().eventsClient, slackClient, inviteWebhookToken: config.SLACK_INVITE_WEBHOOK_TOKEN, inviteChannel: config.SLACK_INVITE_CHANNEL }, + { + eventsClient: fakes().eventsClient, + slackClient, + inviteWebhookToken: config.SLACK_INVITE_WEBHOOK_TOKEN, + inviteChannel: config.SLACK_INVITE_CHANNEL, + }, { logger: false }, ); - const event = { type: "app_mention", text: "<@U0> !invite", channel: config.SLACK_INVITE_CHANNEL, ts: "1700000000.000200", thread_ts: "1700000000.000100" }; + const event = { + type: "app_mention", + text: "<@U0> !invite", + channel: config.SLACK_INVITE_CHANNEL, + ts: "1700000000.000200", + thread_ts: "1700000000.000100", + }; const { body, headers } = signEvent(event); const res = await app.inject({ method: "POST", url: "/slack/webhook", headers, payload: body }); @@ -446,11 +521,22 @@ test("!invite replies with error when no email found in parent message", async ( }, }; const app = buildServer( - { eventsClient: fakes().eventsClient, slackClient, inviteWebhookToken: config.SLACK_INVITE_WEBHOOK_TOKEN, inviteChannel: config.SLACK_INVITE_CHANNEL }, + { + eventsClient: fakes().eventsClient, + slackClient, + inviteWebhookToken: config.SLACK_INVITE_WEBHOOK_TOKEN, + inviteChannel: config.SLACK_INVITE_CHANNEL, + }, { logger: false }, ); - const event = { type: "app_mention", text: "<@U0> !invite", channel: config.SLACK_INVITE_CHANNEL, ts: "1700000000.000200", thread_ts: "1700000000.000100" }; + const event = { + type: "app_mention", + text: "<@U0> !invite", + channel: config.SLACK_INVITE_CHANNEL, + ts: "1700000000.000200", + thread_ts: "1700000000.000100", + }; const { body, headers } = signEvent(event); const res = await app.inject({ method: "POST", url: "/slack/webhook", headers, payload: body }); @@ -482,11 +568,22 @@ test("!invite replies with error when inviteUser fails", async () => { }, }; const app = buildServer( - { eventsClient: fakes().eventsClient, slackClient, inviteWebhookToken: config.SLACK_INVITE_WEBHOOK_TOKEN, inviteChannel: config.SLACK_INVITE_CHANNEL }, + { + eventsClient: fakes().eventsClient, + slackClient, + inviteWebhookToken: config.SLACK_INVITE_WEBHOOK_TOKEN, + inviteChannel: config.SLACK_INVITE_CHANNEL, + }, { logger: false }, ); - const event = { type: "app_mention", text: "<@U0> !invite", channel: config.SLACK_INVITE_CHANNEL, ts: "1700000000.000200", thread_ts: "1700000000.000100" }; + const event = { + type: "app_mention", + text: "<@U0> !invite", + channel: config.SLACK_INVITE_CHANNEL, + ts: "1700000000.000200", + thread_ts: "1700000000.000100", + }; const { body, headers } = signEvent(event); const res = await app.inject({ method: "POST", url: "/slack/webhook", headers, payload: body });