Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Two Next.js sites share one Postgres database and four internal packages. Club m
| --- | --- | --- |
| `sites/mainweb` | `web` | Public club site + authenticated portal (App Hosting) |
| `sites/hacklytics2027` | `hacklytics2027` | Hacklytics 2027 marketing site, static export (Firebase Hosting) |
| `apps/dsgt-slack` | `@query/dsgt-slack` | `@dsgt` Slack bot (HTTP webhook on mainweb in production) |
| `packages/api` | `@query/api` | tRPC routers, middleware, pricing |
| `packages/auth` | `@query/auth` | NextAuth (Google, GitHub, email codes) |
| `packages/db` | `@query/db` | Drizzle schema, client, membership rules |
Expand Down
13 changes: 13 additions & 0 deletions apphosting.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ env:
secret: projects/672446353769/secrets/NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
- variable: STRIPE_WEBHOOK_SECRET
secret: projects/672446353769/secrets/STRIPE_WEBHOOK_SECRET
# Slack Events API for @dsgt (HTTP, not Socket Mode). Create these secrets
# once before the deploy that references them, or App Hosting cannot start:
# gcloud secrets create SLACK_BOT_TOKEN --project=dsgt-website
# gcloud secrets create SLACK_SIGNING_SECRET --project=dsgt-website
# firebase apphosting:secrets:grantaccess SLACK_BOT_TOKEN --backend query
# firebase apphosting:secrets:grantaccess SLACK_SIGNING_SECRET --backend query
# Steps and Request URL: apps/dsgt-slack/README.md
- variable: SLACK_BOT_TOKEN
secret: projects/672446353769/secrets/SLACK_BOT_TOKEN
- variable: SLACK_SIGNING_SECRET
secret: projects/672446353769/secrets/SLACK_SIGNING_SECRET
- variable: SLACK_SOCKET_MODE
value: "false"
- variable: NODE_ENV
value: production
# Consumer Gmail, which caps around 500 recipients a day — shared between
Expand Down
22 changes: 22 additions & 0 deletions apps/dsgt-slack/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copy to .env and fill in values from https://api.slack.com/apps
# Never commit .env or real tokens.

# Bot User OAuth Token (xoxb-...) from OAuth & Permissions after install
SLACK_BOT_TOKEN=xoxb-your-bot-token

# Signing Secret from Basic Information
SLACK_SIGNING_SECRET=your-signing-secret

# App-level token (xapp-...) from Basic Information → App-Level Tokens
# Required only for local Socket Mode. Create a token with the connections:write
# scope. Not used in production (HTTP Events API on Firebase App Hosting).
SLACK_APP_TOKEN=xapp-your-app-token

# Local default is Socket Mode. Production App Hosting sets this to false.
# SLACK_APP_TOKEN is not required when this is false.
SLACK_SOCKET_MODE=true

# Used only when running this package's local HTTP receiver
# (SLACK_SOCKET_MODE=false). Production Request URL is the App Hosting
# origin (AUTH_URL, host datasciencegt.org) plus /api/webhooks/slack.
PORT=3000
209 changes: 209 additions & 0 deletions apps/dsgt-slack/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
# dsgt Slack bot

Bolt for JavaScript app for **Data Science @ Georgia Tech (DS@GT)**. In Slack it
appears as **@dsgt**. This is a custom Slack app owned by the club, not a
third-party Slack integration.

The bot answers `@dsgt` mentions, direct messages, and the `/dsgt` slash
command. It can confirm that it is online (`ping`) and tell people how to join:
the first event is **August 26 (8/26)**, further details will be announced
there, and the club site is datasciencegt.org.

Production traffic is **HTTP Events API** on the existing Firebase App Hosting
backend (`sites/mainweb` / Cloud Run). There is no second Cloud Run service and
no Socket Mode process in production.

## Grok Bot (the LLM / MCP teammate)

This Firebase app is **not** Grok Bot. It is a small FAQ process: help, ping,
and the August 26 join answer. It cannot use your Grok Bot computer, Google
connectors, or MCP tools.

If club Slack should talk to **your Grok Bot** (the Yodo pattern: people DM
`@dsgt`, and the teammate uses Slack, Drive, Slides, Forms, Gmail), do that in
the Grok Bot app, not here:

1. Open Grok Bot and open the teammate you want as `@dsgt`.
2. Connect **Slack** to the **DS@GT club workspace** (not Campus Leads).
3. When Slack asks to install the app, click **Allow** as a workspace admin.
4. Add Google Workspace connectors there (Drive / Slides / Forms). MCP for
those products lives on Grok Bot.
5. Name the Slack bot user `dsgt` so it matches the club identity.

You can keep this FAQ webhook as a fallback, or skip installing it if Grok Bot
is the only `@dsgt` in the workspace. Do not run both against the same bot
tokens or they will fight over events.

## Create the Slack app from the manifest

1. Open [https://api.slack.com/apps](https://api.slack.com/apps) and sign in to
the workspace that should host the bot (typically the DS@GT Slack).
2. Click **Create New App** → **From an app manifest**.
3. Select the workspace, then paste the contents of
[`manifest.yaml`](./manifest.yaml) (YAML is accepted).
4. Confirm the summary. The app name and bot user display name should both be
`dsgt`. Socket Mode should be **off**. The manifest does **not** declare
Event Subscriptions, Interactivity, or `/dsgt` — Slack rejects those without
a Request URL or Socket Mode (`invalid_manifest`). You can add them in the
app settings after `{AUTH_URL}/api/webhooks/slack` is live.
5. Click **Create**.

You now have a real Slack app whose bot user is **dsgt**.

## Install into a workspace and collect tokens

1. In the app settings sidebar, open **OAuth & Permissions** and click
**Install to Workspace**. Approve the requested bot scopes.
2. Copy the **Bot User OAuth Token** (`xoxb-...`). You will put this in GCP
Secret Manager as `SLACK_BOT_TOKEN` (and in `apps/dsgt-slack/.env` only if
you run the bot locally).
3. Open **Basic Information** and copy the **Signing Secret**. That is
`SLACK_SIGNING_SECRET`.

Do **not** commit `.env`, tokens, or signing secrets. `SLACK_APP_TOKEN` is not
required in production.

## Production Request URL (Firebase App Hosting)

Slack must POST to the **Firebase App Hosting / Cloud Run** URL for `sites/mainweb`
(workspace `web`). That origin is `{AUTH_URL}` — the same public
origin as `AUTH_URL` / `NEXTAUTH_URL` in [`apphosting.yaml`](../../apphosting.yaml).

**Use this Request URL everywhere** (Event Subscriptions, Slash Commands
`/dsgt`, Interactivity & Shortcuts). Take the `AUTH_URL` / `NEXTAUTH_URL`
value from [`apphosting.yaml`](../../apphosting.yaml) (the App Hosting origin
for the club site, host `datasciencegt.org`) and append the path:

```text
{AUTH_URL}/api/webhooks/slack
```

Set those three URLs in the Slack app settings **after** the App Hosting
deploy that includes this route is live (Event Subscriptions, Slash Commands
`/dsgt`, Interactivity). They are not in `manifest.yaml` because Slack will
not create the app with those features unless a Request URL or Socket Mode is
set. Slack will POST a `url_verification` challenge; the Next.js route
answers it.

The Next.js route is
`sites/mainweb/app/(portal)/api/webhooks/stripe`’s neighbor:
`sites/mainweb/app/(portal)/api/webhooks/slack/route.ts`. `proxy.ts` already
leaves `/api/webhooks/*` alone, same as Stripe.

### App Hosting vs Firebase Hosting

| Surface | What it is | Slack |
| --- | --- | --- |
| **Firebase App Hosting** | Next.js on Cloud Run, `apphosting.yaml`, `sites/mainweb` | **Yes — this is the API** |
| **Firebase Hosting** site `dsgt-website` | Static `sites/mainweb/out/` | **No.** That site cannot run the webhook |

If Event Subscriptions shows a verification failure, the Request URL is almost
certainly pointing at Hosting `out/` (or the wrong host), not App Hosting.

After the App Hosting deploy that includes this route is live, open **Event
Subscriptions** and click **Retry** / save so Slack can complete the
`url_verification` challenge.

Reinstall the app if Slack asks you to after changing URLs or scopes.

## Secret Manager (one-time)

GCP project: `dsgt-website` (number `672446353769`). App Hosting backend name:
`query`. [`apphosting.yaml`](../../apphosting.yaml) maps:

| Process env | Secret |
| --- | --- |
| `SLACK_BOT_TOKEN` | `projects/672446353769/secrets/SLACK_BOT_TOKEN` |
| `SLACK_SIGNING_SECRET` | `projects/672446353769/secrets/SLACK_SIGNING_SECRET` |
| `SLACK_SOCKET_MODE` | literal `false` (not a secret) |

`SLACK_APP_TOKEN` is **not** wired. Create the secrets before the deploy that
references them, or App Hosting will fail to start:

```bash
gcloud config set project dsgt-website

# Create empty secrets, then add a version from stdin (do not put tokens in the
# shell history file if you can avoid it — prefer a local file you delete).
gcloud secrets create SLACK_BOT_TOKEN --project=dsgt-website
gcloud secrets create SLACK_SIGNING_SECRET --project=dsgt-website

printf '%s' 'xoxb-your-bot-token' | gcloud secrets versions add SLACK_BOT_TOKEN --data-file=-
printf '%s' 'your-signing-secret' | gcloud secrets versions add SLACK_SIGNING_SECRET --data-file=-

firebase apphosting:secrets:grantaccess SLACK_BOT_TOKEN --backend query
firebase apphosting:secrets:grantaccess SLACK_SIGNING_SECRET --backend query
```

Replace the `printf` placeholders with the real values from the Slack app
settings. Never commit those values.

## Run locally (Socket Mode)

Socket Mode is a **local-only** option so you can develop without a public URL.
Production stays on HTTP.

1. In the Slack app settings, turn **Socket Mode** on temporarily (or use a
separate development app). Generate an App-Level Token with
`connections:write` (`xapp-...`).
2. From the monorepo root:

```bash
cp apps/dsgt-slack/.env.example apps/dsgt-slack/.env
# edit apps/dsgt-slack/.env with SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET,
# SLACK_APP_TOKEN, and SLACK_SOCKET_MODE=true

pnpm install
pnpm --filter @query/dsgt-slack dev
```

`pnpm dev` at the repo root also starts this package. If `.env` is missing, the
task stays idle so the rest of the monorepo can still boot.

You can also hit the Next.js webhook locally while `web` is running
(`http://localhost:3001/api/webhooks/slack`) if you tunnel that URL to Slack.
That path is what production uses.

### Invite @dsgt to a channel

In the channel:

```text
/invite @dsgt
```

Then mention it (`@dsgt ping`) or run `/dsgt help`. Direct messages work from
the app's **Messages** tab without an invite.

### Quick checks after install

| Action | Expected reply |
| ----------------------------------------- | ----------------------------------------------- |
| `@dsgt ping` or `/dsgt ping` | Health check confirming the bot is online |
| `/dsgt help` | List of topics |
| “How do I join?” in a DM, or `/dsgt join` | August 26 first-event FAQ and datasciencegt.org |

## Scripts

| Script | Command |
| --- | --- |
| Dev (watch) | `pnpm --filter @query/dsgt-slack dev` |
| Start | `pnpm --filter @query/dsgt-slack start` |
| Lint | `pnpm --filter @query/dsgt-slack lint` |
| Typecheck | `pnpm --filter @query/dsgt-slack typecheck` |
| Test | `pnpm --filter @query/dsgt-slack test` |

Repo-wide `pnpm lint`, `pnpm typecheck`, `pnpm build`, and `pnpm test` include
this package (`pnpm test` runs the join-FAQ unit test along with the existing
vitest targets).

## Environment

See [`.env.example`](./.env.example). Do not commit `.env` or real tokens.

| Variable | Local Socket Mode | Production (App Hosting) |
| --- | --- | --- |
| `SLACK_BOT_TOKEN` | required | Secret Manager |
| `SLACK_SIGNING_SECRET` | required | Secret Manager |
| `SLACK_APP_TOKEN` | required | not used |
| `SLACK_SOCKET_MODE` | `true` (default) | `false` |
12 changes: 12 additions & 0 deletions apps/dsgt-slack/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { config } from "@query/eslint-config/base";

/** @type {import("eslint").Linter.Config[]} */
export default [
...config,
{
files: ["src/index.ts"],
rules: {
"no-console": "off",
},
},
];
29 changes: 29 additions & 0 deletions apps/dsgt-slack/manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
_metadata:
major_version: 1
minor_version: 1
display_information:
name: dsgt
description: Official Slack bot for Data Science @ Georgia Tech (DS@GT).
background_color: "#003057"
features:
app_home:
home_tab_enabled: false
messages_tab_enabled: true
messages_tab_read_only_enabled: false
bot_user:
display_name: dsgt
always_online: true
oauth_config:
scopes:
bot:
- app_mentions:read
- chat:write
- chat:write.public
- commands
- im:history
- im:read
- im:write
settings:
org_deploy_enabled: false
socket_mode_enabled: false
token_rotation_enabled: false
36 changes: 36 additions & 0 deletions apps/dsgt-slack/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@query/dsgt-slack",
"version": "0.0.0",
"private": true,
"type": "module",
"exports": {
".": "./src/replies.ts",
"./replies": "./src/replies.ts",
"./http": "./src/http.ts"
},
"scripts": {
"dev": "tsx watch src/index.ts",
"start": "tsx src/index.ts",
"build": "tsc --noEmit",
"lint": "eslint . --max-warnings 0",
"typecheck": "tsc --noEmit",
"test": "vitest run --config vitest.config.ts"
},
"dependencies": {
"@slack/bolt": "^5.0.0",
"@slack/web-api": "^7.13.0",
"dotenv": "^16.6.1",
"undici": "^6.27.0",
"zod": "3.25.53"
},
"devDependencies": {
"@query/eslint-config": "workspace:*",
"@query/tsconfig": "workspace:*",
"@types/express": "^5.0.0",
"@types/node": "^22.15.32",
"eslint": "10.1.0",
"tsx": "^4.21.0",
"typescript": "6.0.2",
"vitest": "^4.1.8"
}
}
Loading
Loading