From 5ade7f0249726e25ec5847ec742a4de22c6b90fc Mon Sep 17 00:00:00 2001 From: wally-hc Date: Wed, 26 Aug 2026 23:59:52 +0100 Subject: [PATCH 1/3] Allow slack channel managers to ping @channel and @here --- .env.example | 3 +++ AGENTS.md | 3 ++- README.md | 3 +++ lib/perms.js | 26 +++++++++++++++++++++++++- slack.manifest.yaml | 1 + 5 files changed, 34 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index d73e56c..7309659 100644 --- a/.env.example +++ b/.env.example @@ -28,6 +28,9 @@ HACKCLUB_CDN_KEY=sk_cdn_your_key_here SLACK_BROWSER_TOKEN=xoxc-your-browser-token SLACK_COOKIE=xoxd-d-cookie-value +SLACK_ORG_TOKEN=xoxb-your-org-wide-token +CHANNEL_MANAGER_ROLE_ID=Rl0X + # shroud bot ID SHROUD_ID=U07K4TS9HQE diff --git a/AGENTS.md b/AGENTS.md index 19ec2fb..3549821 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,8 +36,9 @@ Prometheus is a Slack bot built with `@slack/bolt` in Socket Mode. It runs via ` - `isWorkspaceAdmin` — Slack API check (`users.info`) - `isChannelManager` — `appointed_managers` with `role = 'manager'` - `isChannelModerator` — any role in `appointed_managers` (manager or moderator) +- `isSlackChannelManager` — Slack's native Channel Manager role (Enterprise Grid), via `admin.roles.listAssignments`. No-ops (`false`) unless `SLACK_ORG_TOKEN`/`CHANNEL_MANAGER_ROLE_ID` are set — those only work if the app is installed org-wide with `admin.roles:read`, which is unrequested in `slack.manifest.yaml` until an Org Owner/Admin approves it - `canManage` — globalAdmin OR channelManager (for delete/destroy/welcome) -- `canBan` — globalAdmin OR channelModerator (for ban/unban/@here) +- `canBan` — globalAdmin OR channelModerator OR isSlackChannelManager (for ban/unban/@here/@channel) - `canAnchor` — `canManage` OR workspaceAdmin - `SUPERADMINS` grants access to the `/pro admin` command; it does not automatically insert rows into `global_admins` diff --git a/README.md b/README.md index 658255a..2818526 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Prometheus is a Slack bot that lets community members take responsibility for ke - **Workspace admin**: inherited from Slack - **Channel manager**: appointed per-channel; can delete, destroy, set welcome messages - **Channel moderator**: appointed per-channel; can timeout, @here, @channel +- **Slack Channel Manager**: Slack's own native per-channel role (Enterprise Grid); can @here, @channel. Requires an org-wide app install with `admin.roles:read` — inert without `SLACK_ORG_TOKEN`/`CHANNEL_MANAGER_ROLE_ID` ## Web API @@ -90,6 +91,8 @@ Other statuses: `400` malformed request, `401` missing or invalid key, `403` no | `HACKCLUB_CDN_KEY` | No | CDN API key for archiving deleted thread archives to the HC CDN | | `SLACK_BROWSER_TOKEN` | No | Browser token (xoxc) for Slack's undocumented moderation APIs (eg thread hiding) | | `SLACK_COOKIE` | No | Session cookie (`d=` value) paired with `SLACK_BROWSER_TOKEN` | +| `SLACK_ORG_TOKEN` | No | Org-wide app install token with `admin.roles:read`, for native Slack Channel Manager support | +| `CHANNEL_MANAGER_ROLE_ID` | No | `role_id` of Slack's built-in Channel Manager role, paired with `SLACK_ORG_TOKEN` | | `SLACK_CLIENT_ID` | No | Slack app client ID used by Sign in with Slack | | `SLACK_CLIENT_SECRET` | No | Slack app client secret used by Sign in with Slack | | `BETTER_AUTH_SECRET` | No | Random secret of at least 32 characters used by Better Auth | diff --git a/lib/perms.js b/lib/perms.js index 34217ff..13caba1 100644 --- a/lib/perms.js +++ b/lib/perms.js @@ -1,3 +1,4 @@ +import { WebClient } from "@slack/web-api"; import { isGlobalAdmin, hasChannelRole as dbHasChannelRole, @@ -6,6 +7,27 @@ import { export { isGlobalAdmin }; +const ORG_TOKEN = process.env.SLACK_ORG_TOKEN; +const CHANNEL_MANAGER_ROLE_ID = process.env.CHANNEL_MANAGER_ROLE_ID; +export const nativeChannelManagersAvailable = Boolean(ORG_TOKEN && CHANNEL_MANAGER_ROLE_ID); +console.log( + `[perms] native Slack channel managers: ${nativeChannelManagersAvailable ? "available" : "not configured"}`, +); +const orgClient = ORG_TOKEN ? new WebClient(ORG_TOKEN) : null; + +export const isSlackChannelManager = async (userId, channelId) => { + if (!nativeChannelManagersAvailable) return false; + try { + const r = await orgClient.admin.roles.listAssignments({ + role_ids: [CHANNEL_MANAGER_ROLE_ID], + entity_ids: [channelId], + }); + return (r.role_assignments || []).some((a) => a.user_id === userId); + } catch { + return false; + } +}; + export const isWorkspaceAdmin = async (client, userId) => { try { const r = await client.users.info({ user: userId }); @@ -25,7 +47,9 @@ export const isChannelManager = (_client, userId, channelId) => // timeout, untimeout, @here, @channel export const canBan = async (client, userId, channelId) => - (await isGlobalAdmin(userId)) || (await dbHasChannelRole(userId, channelId)); + (await isGlobalAdmin(userId)) || + (await dbHasChannelRole(userId, channelId)) || + (await isSlackChannelManager(userId, channelId)); // delete, destroy thread, welcome export const canManage = async (client, userId, channelId) => diff --git a/slack.manifest.yaml b/slack.manifest.yaml index aec557f..73208b8 100644 --- a/slack.manifest.yaml +++ b/slack.manifest.yaml @@ -55,6 +55,7 @@ oauth_config: - mpim:read - mpim:write - pins:write + - admin.roles:read user: - channels:history - channels:write From 392d9d925cd0d73fc172c138786fdea53d6f6058 Mon Sep 17 00:00:00 2001 From: wally-hc Date: Thu, 27 Aug 2026 00:31:50 +0100 Subject: [PATCH 2/3] Fix readme table alignment --- README.md | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 2818526..8129de6 100644 --- a/README.md +++ b/README.md @@ -77,27 +77,27 @@ Other statuses: `400` malformed request, `401` missing or invalid key, `403` no 4. Create an app-level token with `connections:write` (for Socket Mode!). 5. Fill out your `.env`, check the `.env.example` for reference. Here's a bit more detailed rundown of what to expect -| Variable | Required | Purpose | -| ---------------------- | -------- | -------------------------------------------------------------------------------------------------- | -| `SLACK_BOT_TOKEN` | Yes | Bot User OAuth Token (xoxb) for posting messages | -| `SLACK_USER_TOKEN` | Yes | User OAuth Token (`xoxp`) from a **workspace admin** — required for all moderation actions | -| `SLACK_APP_TOKEN` | Yes | App-Level Token (xapp) with `connections:write` for Socket Mode | -| `SLACK_SIGNING_SECRET` | Yes | Signing secret from app settings | -| `SUPERADMINS` | Yes | Comma-separated Slack user IDs seeded as global admins (e.g. `U12345678,U87654321`) | -| `DATABASE_URL` | Yes | PostgreSQL connection URL | -| `DATABASE_POOL_SIZE` | No | Maximum PostgreSQL connections per bot instance (defaults to 4) | -| `LOG_CHANNEL` | API | Channel ID for **private** audit logs which includes full message content and CDN transcripts | -| `PUBLIC_LOG_CHANNEL` | No | Channel ID for **public** audit logs which are redacted, shows only who did what in which channel | -| `HACKCLUB_CDN_KEY` | No | CDN API key for archiving deleted thread archives to the HC CDN | -| `SLACK_BROWSER_TOKEN` | No | Browser token (xoxc) for Slack's undocumented moderation APIs (eg thread hiding) | -| `SLACK_COOKIE` | No | Session cookie (`d=` value) paired with `SLACK_BROWSER_TOKEN` | -| `SLACK_ORG_TOKEN` | No | Org-wide app install token with `admin.roles:read`, for native Slack Channel Manager support | -| `CHANNEL_MANAGER_ROLE_ID` | No | `role_id` of Slack's built-in Channel Manager role, paired with `SLACK_ORG_TOKEN` | -| `SLACK_CLIENT_ID` | No | Slack app client ID used by Sign in with Slack | -| `SLACK_CLIENT_SECRET` | No | Slack app client secret used by Sign in with Slack | -| `BETTER_AUTH_SECRET` | No | Random secret of at least 32 characters used by Better Auth | -| `DASHBOARD_BASE_URL` | No | Public HTTPS origin the dashboard and API are served from (e.g. `https://prometheus.hackclub.com`) | -| `PORT` | No | Port the dashboard and API listen on (defaults to 3000) | +| Variable | Required | Purpose | +| ------------------------- | -------- | -------------------------------------------------------------------------------------------------- | +| `SLACK_BOT_TOKEN` | Yes | Bot User OAuth Token (xoxb) for posting messages | +| `SLACK_USER_TOKEN` | Yes | User OAuth Token (`xoxp`) from a **workspace admin** — required for all moderation actions | +| `SLACK_APP_TOKEN` | Yes | App-Level Token (xapp) with `connections:write` for Socket Mode | +| `SLACK_SIGNING_SECRET` | Yes | Signing secret from app settings | +| `SUPERADMINS` | Yes | Comma-separated Slack user IDs seeded as global admins (e.g. `U12345678,U87654321`) | +| `DATABASE_URL` | Yes | PostgreSQL connection URL | +| `DATABASE_POOL_SIZE` | No | Maximum PostgreSQL connections per bot instance (defaults to 4) | +| `LOG_CHANNEL` | API | Channel ID for **private** audit logs which includes full message content and CDN transcripts | +| `PUBLIC_LOG_CHANNEL` | No | Channel ID for **public** audit logs which are redacted, shows only who did what in which channel | +| `HACKCLUB_CDN_KEY` | No | CDN API key for archiving deleted thread archives to the HC CDN | +| `SLACK_BROWSER_TOKEN` | No | Browser token (xoxc) for Slack's undocumented moderation APIs (eg thread hiding) | +| `SLACK_COOKIE` | No | Session cookie (`d=` value) paired with `SLACK_BROWSER_TOKEN` | +| `SLACK_ORG_TOKEN` | No | Org-wide app install token with `admin.roles:read`, for native Slack Channel Manager support | +| `CHANNEL_MANAGER_ROLE_ID` | No | `role_id` of Slack's built-in Channel Manager role, paired with `SLACK_ORG_TOKEN` | +| `SLACK_CLIENT_ID` | No | Slack app client ID used by Sign in with Slack | +| `SLACK_CLIENT_SECRET` | No | Slack app client secret used by Sign in with Slack | +| `BETTER_AUTH_SECRET` | No | Random secret of at least 32 characters used by Better Auth | +| `DASHBOARD_BASE_URL` | No | Public HTTPS origin the dashboard and API are served from (e.g. `https://prometheus.hackclub.com`) | +| `PORT` | No | Port the dashboard and API listen on (defaults to 3000) | 6. Apply database migrations and run it: From 8f055eedeec9015cf662b811feb42e9b05c491c0 Mon Sep 17 00:00:00 2001 From: Echo Date: Wed, 26 Aug 2026 23:12:12 -0400 Subject: [PATCH 3/3] use admin api for channel managers --- .env.example | 3 --- AGENTS.md | 2 +- README.md | 42 ++++++++++++++++++++---------------------- lib/moderation.js | 12 ++++++++++++ lib/perms.js | 24 +++--------------------- slack.manifest.yaml | 1 - 6 files changed, 36 insertions(+), 48 deletions(-) diff --git a/.env.example b/.env.example index 7309659..d73e56c 100644 --- a/.env.example +++ b/.env.example @@ -28,9 +28,6 @@ HACKCLUB_CDN_KEY=sk_cdn_your_key_here SLACK_BROWSER_TOKEN=xoxc-your-browser-token SLACK_COOKIE=xoxd-d-cookie-value -SLACK_ORG_TOKEN=xoxb-your-org-wide-token -CHANNEL_MANAGER_ROLE_ID=Rl0X - # shroud bot ID SHROUD_ID=U07K4TS9HQE diff --git a/AGENTS.md b/AGENTS.md index d5e1189..ec27a03 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,7 @@ Prometheus is a Slack bot built with `@slack/bolt` in Socket Mode. It runs via ` - `isWorkspaceAdmin` — Slack API check (`users.info`) - `isChannelManager` — `appointed_managers` with `role = 'manager'` - `isChannelModerator` — any role in `appointed_managers` (manager or moderator) -- `isSlackChannelManager` — Slack's native Channel Manager role (Enterprise Grid), via `admin.roles.listAssignments`. No-ops (`false`) unless `SLACK_ORG_TOKEN`/`CHANNEL_MANAGER_ROLE_ID` are set — those only work if the app is installed org-wide with `admin.roles:read`, which is unrequested in `slack.manifest.yaml` until an Org Owner/Admin approves it +- `isSlackChannelManager` — Slack's native Channel Manager role (Enterprise Grid), via `getChannelManagers` in `lib/moderation.js` (undocumented `admin.roles.entity.listAssignments`). Uses the same enterprise moderation creds (`SLACK_BROWSER_TOKEN`/`SLACK_COOKIE`); no-ops (`false`) when those aren't configured - `canManage` — globalAdmin OR channelManager (for delete/destroy/welcome) - `canBan` — globalAdmin OR channelModerator OR isSlackChannelManager (for ban/unban/@here/@channel) - `canAnchor` — `canManage` OR workspaceAdmin diff --git a/README.md b/README.md index 8129de6..cd394bc 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Prometheus is a Slack bot that lets community members take responsibility for ke - **Workspace admin**: inherited from Slack - **Channel manager**: appointed per-channel; can delete, destroy, set welcome messages - **Channel moderator**: appointed per-channel; can timeout, @here, @channel -- **Slack Channel Manager**: Slack's own native per-channel role (Enterprise Grid); can @here, @channel. Requires an org-wide app install with `admin.roles:read` — inert without `SLACK_ORG_TOKEN`/`CHANNEL_MANAGER_ROLE_ID` +- **Slack Channel Manager**: Slack's own native per-channel role (Enterprise Grid); can @here, @channel. Read via the enterprise moderation creds — inert without `SLACK_BROWSER_TOKEN`/`SLACK_COOKIE` ## Web API @@ -77,27 +77,25 @@ Other statuses: `400` malformed request, `401` missing or invalid key, `403` no 4. Create an app-level token with `connections:write` (for Socket Mode!). 5. Fill out your `.env`, check the `.env.example` for reference. Here's a bit more detailed rundown of what to expect -| Variable | Required | Purpose | -| ------------------------- | -------- | -------------------------------------------------------------------------------------------------- | -| `SLACK_BOT_TOKEN` | Yes | Bot User OAuth Token (xoxb) for posting messages | -| `SLACK_USER_TOKEN` | Yes | User OAuth Token (`xoxp`) from a **workspace admin** — required for all moderation actions | -| `SLACK_APP_TOKEN` | Yes | App-Level Token (xapp) with `connections:write` for Socket Mode | -| `SLACK_SIGNING_SECRET` | Yes | Signing secret from app settings | -| `SUPERADMINS` | Yes | Comma-separated Slack user IDs seeded as global admins (e.g. `U12345678,U87654321`) | -| `DATABASE_URL` | Yes | PostgreSQL connection URL | -| `DATABASE_POOL_SIZE` | No | Maximum PostgreSQL connections per bot instance (defaults to 4) | -| `LOG_CHANNEL` | API | Channel ID for **private** audit logs which includes full message content and CDN transcripts | -| `PUBLIC_LOG_CHANNEL` | No | Channel ID for **public** audit logs which are redacted, shows only who did what in which channel | -| `HACKCLUB_CDN_KEY` | No | CDN API key for archiving deleted thread archives to the HC CDN | -| `SLACK_BROWSER_TOKEN` | No | Browser token (xoxc) for Slack's undocumented moderation APIs (eg thread hiding) | -| `SLACK_COOKIE` | No | Session cookie (`d=` value) paired with `SLACK_BROWSER_TOKEN` | -| `SLACK_ORG_TOKEN` | No | Org-wide app install token with `admin.roles:read`, for native Slack Channel Manager support | -| `CHANNEL_MANAGER_ROLE_ID` | No | `role_id` of Slack's built-in Channel Manager role, paired with `SLACK_ORG_TOKEN` | -| `SLACK_CLIENT_ID` | No | Slack app client ID used by Sign in with Slack | -| `SLACK_CLIENT_SECRET` | No | Slack app client secret used by Sign in with Slack | -| `BETTER_AUTH_SECRET` | No | Random secret of at least 32 characters used by Better Auth | -| `DASHBOARD_BASE_URL` | No | Public HTTPS origin the dashboard and API are served from (e.g. `https://prometheus.hackclub.com`) | -| `PORT` | No | Port the dashboard and API listen on (defaults to 3000) | +| Variable | Required | Purpose | +| ---------------------- | -------- | -------------------------------------------------------------------------------------------------- | +| `SLACK_BOT_TOKEN` | Yes | Bot User OAuth Token (xoxb) for posting messages | +| `SLACK_USER_TOKEN` | Yes | User OAuth Token (`xoxp`) from a **workspace admin** — required for all moderation actions | +| `SLACK_APP_TOKEN` | Yes | App-Level Token (xapp) with `connections:write` for Socket Mode | +| `SLACK_SIGNING_SECRET` | Yes | Signing secret from app settings | +| `SUPERADMINS` | Yes | Comma-separated Slack user IDs seeded as global admins (e.g. `U12345678,U87654321`) | +| `DATABASE_URL` | Yes | PostgreSQL connection URL | +| `DATABASE_POOL_SIZE` | No | Maximum PostgreSQL connections per bot instance (defaults to 4) | +| `LOG_CHANNEL` | API | Channel ID for **private** audit logs which includes full message content and CDN transcripts | +| `PUBLIC_LOG_CHANNEL` | No | Channel ID for **public** audit logs which are redacted, shows only who did what in which channel | +| `HACKCLUB_CDN_KEY` | No | CDN API key for archiving deleted thread archives to the HC CDN | +| `SLACK_BROWSER_TOKEN` | No | Browser token (xoxc) for Slack's undocumented moderation APIs (eg thread hiding) | +| `SLACK_COOKIE` | No | Session cookie (`d=` value) paired with `SLACK_BROWSER_TOKEN` | +| `SLACK_CLIENT_ID` | No | Slack app client ID used by Sign in with Slack | +| `SLACK_CLIENT_SECRET` | No | Slack app client secret used by Sign in with Slack | +| `BETTER_AUTH_SECRET` | No | Random secret of at least 32 characters used by Better Auth | +| `DASHBOARD_BASE_URL` | No | Public HTTPS origin the dashboard and API are served from (e.g. `https://prometheus.hackclub.com`) | +| `PORT` | No | Port the dashboard and API listen on (defaults to 3000) | 6. Apply database migrations and run it: diff --git a/lib/moderation.js b/lib/moderation.js index 7f7f628..d508a46 100644 --- a/lib/moderation.js +++ b/lib/moderation.js @@ -68,3 +68,15 @@ export async function deleteAttachment(channel, ts, attachment) { attachment, }); } + +export async function getChannelManagers(channelId) { + if (!areWeEnterprise) return []; + try { + const json = await moderationAPI("admin.roles.entity.listAssignments", { + entity_id: channelId, + }); + return json.role_assignments?.[0]?.users || []; + } catch { + return []; + } +} diff --git a/lib/perms.js b/lib/perms.js index 13caba1..812f395 100644 --- a/lib/perms.js +++ b/lib/perms.js @@ -1,32 +1,14 @@ -import { WebClient } from "@slack/web-api"; import { isGlobalAdmin, hasChannelRole as dbHasChannelRole, isAppointedManager as dbIsAppointedManager, } from "./db.js"; +import { getChannelManagers } from "./moderation.js"; export { isGlobalAdmin }; -const ORG_TOKEN = process.env.SLACK_ORG_TOKEN; -const CHANNEL_MANAGER_ROLE_ID = process.env.CHANNEL_MANAGER_ROLE_ID; -export const nativeChannelManagersAvailable = Boolean(ORG_TOKEN && CHANNEL_MANAGER_ROLE_ID); -console.log( - `[perms] native Slack channel managers: ${nativeChannelManagersAvailable ? "available" : "not configured"}`, -); -const orgClient = ORG_TOKEN ? new WebClient(ORG_TOKEN) : null; - -export const isSlackChannelManager = async (userId, channelId) => { - if (!nativeChannelManagersAvailable) return false; - try { - const r = await orgClient.admin.roles.listAssignments({ - role_ids: [CHANNEL_MANAGER_ROLE_ID], - entity_ids: [channelId], - }); - return (r.role_assignments || []).some((a) => a.user_id === userId); - } catch { - return false; - } -}; +export const isSlackChannelManager = async (userId, channelId) => + (await getChannelManagers(channelId)).includes(userId); export const isWorkspaceAdmin = async (client, userId) => { try { diff --git a/slack.manifest.yaml b/slack.manifest.yaml index 73208b8..aec557f 100644 --- a/slack.manifest.yaml +++ b/slack.manifest.yaml @@ -55,7 +55,6 @@ oauth_config: - mpim:read - mpim:write - pins:write - - admin.roles:read user: - channels:history - channels:write