@flink-app/slack-plugin
v2.0.0-alpha.61
Published
Flink plugin for bi-directional Slack messaging with auto-discovered SlackHandler files
Readme
@flink-app/slack-plugin
Bi-directional Slack messaging plugin for Flink. Receive and respond to Slack messages using auto-discovered handler files — the same pattern as @flink-app/inbound-email-plugin.
Features
- Receive channel messages, DMs, mentions, and thread replies
- Send messages to channels and threads
- Upload and download files with typed
SlackFilemetadata - Add emoji reactions
- Retrieve thread and channel history for context
- Auto-discover
SlackHandlerfiles via Flink compiler plugin - Socket Mode (no public URL needed) and HTTP mode
- Bot message filtering to prevent infinite loops
- Automatic
<@BOTID>mention stripping requestContextintegration for user/permission injection
Installation
pnpm add @flink-app/slack-pluginSetup
1. Configure the compiler plugin
In your flink.config.js:
const { compilerPlugin } = require("@flink-app/slack-plugin/compiler");
module.exports = {
compilerPlugins: [compilerPlugin()],
};This tells the Flink compiler to scan src/slack-handlers/ for handler files.
2. Register the plugin
import { slackPlugin } from "@flink-app/slack-plugin";
const app = new FlinkApp<AppCtx>({
plugins: [
slackPlugin<AppCtx>({
connection: {
mode: "socket",
token: process.env.SLACK_BOT_TOKEN!,
appToken: process.env.SLACK_APP_TOKEN!,
signingSecret: process.env.SLACK_SIGNING_SECRET!,
},
// Optional: resolve your app's user from a Slack user ID
resolveUser: async (msg, ctx) => {
return ctx.repos.userRepo.findOne({ slackUserId: msg.userId });
},
}),
],
});3. Extend your app context type
Import SlackPluginCtx and pass it as the generic argument to FlinkContext. This gives you full type-safety on ctx.plugins.slack throughout your app.
// src/Ctx.ts
import { FlinkContext } from "@flink-app/flink";
import { SlackPluginCtx } from "@flink-app/slack-plugin";
export interface Ctx extends FlinkContext<SlackPluginCtx> {
repos: {
// your repos here
};
}ctx.plugins.slack is then fully typed — send messages, manage connections, and access the raw WebClient.
Note: Import
SlackPluginCtx(notSlackApi) when extending yourCtx.SlackApiis the inner API type and is not intended for this purpose.
4. Create handler files
Create handlers in src/slack-handlers/. Each file exports a default handler function and an optional Route.
Handler Examples
Respond to @mentions
// src/slack-handlers/HandleMention.ts
import { SlackHandler, SlackRouteProps } from "@flink-app/slack-plugin";
export const Route: SlackRouteProps = { isMention: true };
const handler: SlackHandler<AppCtx> = async ({ ctx, message, slack }) => {
const thread = await slack.getThread(message);
await slack.reply(message, `You said: "${message.text}". Thread has ${thread.messages.length} messages.`);
};
export default handler;Handle DMs
// src/slack-handlers/HandleDM.ts
import { SlackHandler, SlackRouteProps } from "@flink-app/slack-plugin";
export const Route: SlackRouteProps = { isDM: true };
const handler: SlackHandler<AppCtx> = async ({ message, slack }) => {
await slack.reply(message, `Got your DM: "${message.text}"`);
};
export default handler;Match by pattern
// src/slack-handlers/HandleHelp.ts
import { SlackHandler, SlackRouteProps } from "@flink-app/slack-plugin";
export const Route: SlackRouteProps = { pattern: /^help$/i };
const handler: SlackHandler<AppCtx> = async ({ message, slack }) => {
await slack.reply(message, "Here's what I can do…");
};
export default handler;Match by channel
// src/slack-handlers/HandleSupport.ts
import { SlackHandler, SlackRouteProps } from "@flink-app/slack-plugin";
export const Route: SlackRouteProps = { channel: "support" };
const handler: SlackHandler<AppCtx> = async ({ ctx, message, slack }) => {
await slack.reply(message, "A support agent will be with you shortly.");
};
export default handler;Catch-all (no Route)
// src/slack-handlers/Fallback.ts
import { SlackHandler } from "@flink-app/slack-plugin";
const handler: SlackHandler<AppCtx> = async ({ message, slack }) => {
console.log(`Unrouted message from ${message.userId}: ${message.text}`);
};
export default handler;Route Properties
| Property | Type | Description |
|----------------|-------------------------------------------------|--------------------------------------------------|
| channel | string \| RegExp \| (msg) => boolean | Match by channel ID |
| pattern | string \| RegExp \| (msg) => boolean | Match on message text |
| isDM | boolean | Only match direct messages |
| isMention | boolean | Only match @mentions |
| isThread | boolean | Only match thread replies |
| connectionId | string \| string[] | Only match messages from specific connection(s) |
An empty or omitted Route matches all messages (catch-all).
Plugin Context
The plugin exposes ctx.plugins.slack with these methods:
send(opts)
Send a message to any channel.
await ctx.plugins.slack.send({
channel: "general",
text: "Hello from Flink!",
threadTs: "1234567890.123456", // optional: reply in thread
blocks: [], // optional: Block Kit blocks
});reply(message, text, opts?)
Reply to a message. Continues the thread if the message is already in one, otherwise starts a new thread.
await slack.reply(message, "Got it!");getThread(message)
Fetch all messages in a thread.
const thread = await slack.getThread(message);
// thread.messages: SlackThreadMessage[]getChannelHistory(opts)
Fetch recent messages from a channel.
const history = await slack.getChannelHistory({
channelId: message.channelId,
limit: 10,
});withConnection(id)
Get a scoped context bound to a specific connection. Useful for proactive sending from jobs or HTTP handlers.
const acmeSlack = ctx.plugins.slack.withConnection("acme");
await acmeSlack.send({ channel: "general", text: "Hello!" });addConnection(id, connection)
Add a new connection at runtime.
await ctx.plugins.slack.addConnection("acme", {
mode: "socket",
token: "xoxb-...",
appToken: "xapp-...",
signingSecret: "...",
});removeConnection(id)
Stop and remove a connection.
await ctx.plugins.slack.removeConnection("acme");listConnections()
List active connection IDs.
const ids = ctx.plugins.slack.listConnections(); // ["default", "acme"]sendFile(opts)
Upload a file to a channel or thread.
await ctx.plugins.slack.sendFile({
channel: message.channelId,
file: Buffer.from("hello world"),
filename: "hello.txt",
title: "My File", // optional
initialComment: "Here you go", // optional: message alongside the file
threadTs: message.threadTs, // optional: upload into thread
});downloadFile(file)
Download a file attached to an incoming message. Uses the bot token for authentication since Slack file URLs are private.
if (message.files?.length) {
const content = await slack.downloadFile(message.files[0]);
// content is a Buffer
}react(message, emoji)
Add an emoji reaction to a message.
await slack.react(message, "thumbsup");client
Raw @slack/web-api WebClient for anything not covered by the helpers. Returns the default connection's client.
await ctx.plugins.slack.client.reactions.add({
channel: message.channelId,
timestamp: message.ts,
name: "thumbsup",
});Connection Modes
Socket Mode (recommended for development)
No public URL required. Uses WebSocket via an app-level token.
connection: {
mode: "socket",
token: "xoxb-...",
appToken: "xapp-...",
signingSecret: "...",
}HTTP Mode
Receives events via HTTP. Requires a publicly accessible URL configured in your Slack app.
connection: {
mode: "http",
token: "xoxb-...",
signingSecret: "...",
port: 3100, // default
}Plugin Options
| Option | Type | Default | Description |
|---------------------|--------------------------------------------------------------|---------|-----------------------------------------------------------------|
| connection | SlackConnectionOptions | — | Single connection (stored as "default"). |
| connections | Record<string, SlackConnectionOptions> | — | Multiple named connections at startup. |
| loadConnections | (ctx) => Promise<Record<string, SlackConnectionOptions>> | — | Load connections dynamically at init (e.g. from database). |
| stripMention | boolean | true | Remove <@BOTID> prefix from text. |
| resolveUser | (msg, ctx) => Promise<any> | — | Resolve app user from Slack message. |
| resolvePermissions| (user, ctx) => Promise<string[]> | — | Resolve permissions for resolved user. |
| onUnhandled | (msg, ctx) => Promise<void> | — | Called when no handler matches. |
You can use connection, connections, loadConnections, or any combination. All are optional — you can start with zero connections and add them dynamically at runtime.
Multi-Tenant / Dynamic Connections
For SaaS apps where each tenant connects their own Slack workspace, the plugin supports multiple concurrent connections. Handlers work identically — the slack argument is automatically scoped to the connection that received the message.
Multiple connections at startup
slackPlugin<AppCtx>({
connections: {
acme: {
mode: "socket",
token: process.env.ACME_SLACK_TOKEN!,
appToken: process.env.ACME_SLACK_APP_TOKEN!,
signingSecret: process.env.ACME_SIGNING_SECRET!,
},
beta: {
mode: "socket",
token: process.env.BETA_SLACK_TOKEN!,
appToken: process.env.BETA_SLACK_APP_TOKEN!,
signingSecret: process.env.BETA_SIGNING_SECRET!,
},
},
});Load connections from database
slackPlugin<AppCtx>({
loadConnections: async (ctx) => {
const tenants = await ctx.repos.tenantRepo.findAll({ slackEnabled: true });
return Object.fromEntries(
tenants.map(t => [t.id, {
mode: "socket" as const,
token: t.slackToken,
appToken: t.slackAppToken,
signingSecret: t.signingSecret,
}])
);
},
});Add/remove connections at runtime
// Add a new tenant's Slack connection
await ctx.plugins.slack.addConnection("acme", {
mode: "socket",
token: "xoxb-...",
appToken: "xapp-...",
signingSecret: "...",
});
// Remove it
await ctx.plugins.slack.removeConnection("acme");
// List active connections
const ids = ctx.plugins.slack.listConnections(); // ["acme", "beta"]Proactive sending to a specific connection
From jobs, HTTP handlers, or anywhere with access to the plugin context:
await ctx.plugins.slack.withConnection("acme").send({
channel: "general",
text: "Hello from Flink!",
});Route filtering by connection
Handlers can declare which connections they listen on:
export const Route: SlackRouteProps = {
isMention: true,
connectionId: "acme", // only this connection
// connectionId: ["acme", "beta"], // specific set
// omit connectionId → listen on ALL connections (default)
};Accessing connection info in handlers
Every SlackMessage includes a connectionId field identifying which connection received it:
const handler: SlackHandler<AppCtx> = async ({ message, slack }) => {
console.log(`Message from connection: ${message.connectionId}`);
await slack.reply(message, "Got it!"); // uses the correct connection automatically
};CLI
Send a test message from the command line:
flink-slack send --token xoxb-... --channel general "Hello from CLI"Or using an environment variable:
export SLACK_BOT_TOKEN=xoxb-...
flink-slack send --channel general "Hello from CLI"Slack App Setup
Create a Slack app at api.slack.com/apps and configure the following. See the Slack Bolt getting started guide for detailed walkthrough.
1. Bot Token Scopes
Under OAuth & Permissions → Scopes, add:
| Scope | Purpose |
|---|---|
| chat:write | Send messages |
| files:read | Download files from messages |
| files:write | Upload files |
| reactions:write | Add emoji reactions |
| channels:history | Read channel messages |
| groups:history | Read private channel messages |
| im:history | Read DMs |
| mpim:history | Read group DMs |
2. Event Subscriptions
Under Event Subscriptions → Subscribe to bot events, add:
message.channels— channel messagesmessage.groups— private channel messagesmessage.im— DMsapp_mention— @mentions
3. Socket Mode (recommended)
Under Settings → Socket Mode, toggle it on and generate an app-level token with the connections:write scope. This gives you the xapp-... token used in the appToken config field. No public URL needed.
See Slack Socket Mode docs for details.
4. Install to workspace
Under Install App, click Install to Workspace. Copy the Bot User OAuth Token (xoxb-...) and the Signing Secret (from Basic Information).
Multi-tenant setup
For multi-tenant apps, each tenant installs the Slack app to their own workspace via OAuth v2. After the OAuth flow completes, store each tenant's token, appToken, and signingSecret, then call addConnection() to start receiving messages. See the Slack distributing apps guide for the full OAuth flow.
