feat: promote slack to a dedicated native notification channel

This commit is contained in:
coo1estguy
2026-09-07 17:02:39 +05:30
parent 9c9634fef8
commit 5fca3ee5d2
15 changed files with 1517 additions and 26 deletions
+1 -1
View File
@@ -295,7 +295,7 @@ First-party providers ship for Resend and Firebase Cloud Messaging. Anything els
| | |
| :------------------ | :------------------------------------------------------------------------------------- |
| **Channels** | `email`, `sms`, `push`, `webhook`, `telegram`, `discord`, `whatsapp` |
| **Channels** | `email`, `sms`, `push`, `webhook`, `telegram`, `discord`, `whatsapp`, `slack` |
| **Targeting** | A user, a list of users, a segment, or a topic |
| **Priorities** | `low`, `normal`, `high`, `critical` — separate stream lanes |
| **Scheduling** | Future sends with `sendAt`, quiet-hours deferral, cancellation |
+1
View File
@@ -0,0 +1 @@
ALTER TYPE "public"."channel" ADD VALUE 'slack';
File diff suppressed because it is too large Load Diff
+7
View File
@@ -29,6 +29,13 @@
"when": 1788779801992,
"tag": "0003_skinny_daimon_hellstrom",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1788780502388,
"tag": "0004_pretty_bruce_banner",
"breakpoints": true
}
]
}
+11 -11
View File
@@ -90,17 +90,17 @@ Because triple braces `{{{body}}}` preserve unescaped HTML, your agent in Claude
**Campaigns & Sending**
| Tool | What it does |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `send_campaign` | Send a template to a list of recipients across `email`, `sms`, `push`, `webhook`, `telegram`, `discord`, or `whatsapp` with campaign tagging |
| `send_notification` | Send to existing users, a segment, or a topic with `sendAt` scheduling and priority lanes |
| `list_campaigns` | Recent campaign labels with size and last activity (supports `search`, `channel`, date range) |
| `get_campaign_stats` | Sent, delivered, failed, opened, clicked, bounced, complained, unsubscribed, with rates |
| `list_segments` | Every segment tag in use — the valid audiences to broadcast to |
| `list_scheduled` | Sends queued for the future, including quiet-hours deferrals |
| `get_delivery_logs` | Per-message detail with filters for template, channel, status, campaign, or user search |
| `get_notification` | Status and delivery attempts for a specific task |
| `cancel_notification` | Cancel a pending scheduled or quiet-hours deferred notification |
| Tool | What it does |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `send_campaign` | Send a template to a list of recipients across `email`, `sms`, `push`, `webhook`, `telegram`, `discord`, `whatsapp`, or `slack` with campaign tagging |
| `send_notification` | Send to existing users, a segment, or a topic with `sendAt` scheduling and priority lanes |
| `list_campaigns` | Recent campaign labels with size and last activity (supports `search`, `channel`, date range) |
| `get_campaign_stats` | Sent, delivered, failed, opened, clicked, bounced, complained, unsubscribed, with rates |
| `list_segments` | Every segment tag in use — the valid audiences to broadcast to |
| `list_scheduled` | Sends queued for the future, including quiet-hours deferrals |
| `get_delivery_logs` | Per-message detail with filters for template, channel, status, campaign, or user search |
| `get_notification` | Status and delivery attempts for a specific task |
| `cancel_notification` | Cancel a pending scheduled or quiet-hours deferred notification |
**Templates** — `list_templates`, `get_template`, `upsert_template`, `delete_template`, `preview_template`, `render_template`
+12 -6
View File
@@ -12,6 +12,7 @@ const CHANNELS = [
"telegram",
"discord",
"whatsapp",
"slack",
] as const;
const CONTACT_CHANNELS = [
"email",
@@ -21,6 +22,7 @@ const CONTACT_CHANNELS = [
"telegram",
"discord",
"whatsapp",
"slack",
] as const;
const PRIORITIES = ["low", "normal", "high", "critical"] as const;
@@ -227,10 +229,10 @@ export function registerTools(server: McpServer, api: NotifkitApi): void {
title: "Send a campaign to a list of addresses",
description:
"Send one template to a list of recipient destinations (email, SMS/phone, push tokens, webhook URLs, " +
"Telegram chat ids, or Discord webhook URLs) given directly — the tool creates or updates a user " +
"Telegram chat ids, Discord webhook URLs, WhatsApp phone numbers, or Slack channel/user IDs/webhook URLs) given directly — the tool creates or updates a user " +
"record for each address, then sends, and tags every message with a campaign label so the results " +
"can be read back later with get_campaign_stats. Supports 'email', 'sms', 'push', 'webhook', " +
"'telegram', and 'discord' channels. Use this when the person supplies the recipients themselves " +
"'telegram', 'discord', 'whatsapp', and 'slack' channels. Use this when the person supplies the recipients themselves " +
"(a pasted list, a spreadsheet column). Always report the returned campaign label back to the user " +
"— it is how they ask about results later. Suppressed addresses (previous unsubscribes, complaints, " +
"hard bounces) are dropped automatically at send time, so the delivered count is normally lower than " +
@@ -249,7 +251,7 @@ export function registerTools(server: McpServer, api: NotifkitApi): void {
.enum(CONTACT_CHANNELS)
.optional()
.describe(
"Channel to send on ('email', 'sms', 'push', 'webhook', 'telegram', or 'discord'). Defaults to 'email'.",
"Channel to send on ('email', 'sms', 'push', 'webhook', 'telegram', 'discord', 'whatsapp', or 'slack'). Defaults to 'email'.",
),
recipients: z
.array(z.string().min(1))
@@ -257,8 +259,8 @@ export function registerTools(server: McpServer, api: NotifkitApi): void {
.max(10000)
.optional()
.describe(
"Recipient destination addresses: email addresses for email, E.164 phone numbers for sms, " +
"device tokens for push, URLs for webhook, chat ids for telegram, or webhook URLs for discord.",
"Recipient destination addresses: email addresses for email, E.164 phone numbers for sms/whatsapp, " +
"device tokens for push, URLs for webhook, chat ids for telegram, webhook URLs for discord, or channel/user IDs for slack.",
),
emails: z
.array(z.string().email())
@@ -311,6 +313,10 @@ export function registerTools(server: McpServer, api: NotifkitApi): void {
userRecord = { id: `telegram:${target}` };
} else if (channel === "discord") {
userRecord = { id: `discord:${target}` };
} else if (channel === "whatsapp") {
userRecord = { id: `whatsapp:${target}`, phone: target };
} else if (channel === "slack") {
userRecord = { id: `slack:${target}` };
} else {
userRecord = { id: `webhook:${target}` };
}
@@ -854,7 +860,7 @@ export function registerTools(server: McpServer, api: NotifkitApi): void {
.optional()
.describe(
"Only list users who have a registered contact for this channel " +
"(email, sms, push, webhook, telegram, discord).",
"(email, sms, push, webhook, telegram, discord, whatsapp, slack).",
),
},
annotations: { title: "List users", readOnlyHint: true },
+2 -2
View File
@@ -1,7 +1,7 @@
# @notifkit/provider-slack
Slack transport for [notifkit](https://github.com/devkitshq/notifkit). Registers
against the `webhook` channel and posts through either a Slack bot token
against the `slack` channel and posts through either a Slack bot token
(`chat.postMessage`) or a plain Incoming Webhook URL — whichever a recipient's
stored contact target resolves to.
@@ -35,7 +35,7 @@ shape:
| `appId` | Purely descriptive — shows up in log lines so you can tell multiple Slack app installations apart. Doesn't affect sending. |
| `clientId` / `clientSecret` | Accepted, but **not** used to authenticate sends — see below. |
A recipient's `webhook` contact target (or a template's own `channel`
A recipient's `slack` contact target (or a template's own `channel`
override, or the transport's `webhookUrl` fallback) decides how the message
is actually delivered:
+1 -1
View File
@@ -58,7 +58,7 @@ export interface SlackTransportOptions {
}
export class SlackTransport implements Transport {
readonly channel = "webhook" as const;
readonly channel = "slack" as const;
readonly limits?: { limit: number; windowSeconds: number };
private readonly botToken?: string;
+1
View File
@@ -9,6 +9,7 @@ export const NotificationChannelSchema = z.enum([
"telegram",
"discord",
"whatsapp",
"slack",
]);
export type NotificationChannel = z.infer<typeof NotificationChannelSchema>;
@@ -10,6 +10,7 @@ export const RecipientProfileSchema = z.object({
pushToken: z.string().optional(),
telegram: z.string().optional(),
discord: z.string().url().optional(),
slack: z.string().optional(),
locale: z.string().default("en"),
timezone: z.string().default("UTC"),
preferences: z.object({
+1
View File
@@ -31,6 +31,7 @@ export const ContactChannelSchema = z.enum([
"telegram",
"discord",
"whatsapp",
"slack",
]);
export type ContactChannel = z.infer<typeof ContactChannelSchema>;
+1
View File
@@ -26,6 +26,7 @@ export const channelEnum = pgEnum("channel", [
"telegram",
"discord",
"whatsapp",
"slack",
]);
export const projects = pgTable("projects", {
+5 -3
View File
@@ -172,9 +172,11 @@ export class AiWorker extends BaseWorker {
? pending.recipient.telegram
: pending.channel === "discord"
? pending.recipient.discord
: pending.channel === "push"
? (pending.recipient.pushTokens?.[0] ?? pending.recipient.pushToken)
: undefined;
: pending.channel === "slack"
? pending.recipient.slack
: pending.channel === "push"
? (pending.recipient.pushTokens?.[0] ?? pending.recipient.pushToken)
: undefined;
const resolvedDestination =
destination ?? (pending.channel === "push" ? undefined : pending.recipientId);
+1
View File
@@ -431,6 +431,7 @@ export class EnricherWorker extends BaseWorker {
webhook: channel === "webhook" ? destination : undefined,
telegram: channel === "telegram" ? destination : undefined,
discord: channel === "discord" ? destination : undefined,
slack: channel === "slack" ? destination : undefined,
locale: profile.language ?? "en",
timezone: profile.timezone ?? "UTC",
preferences: {
+2 -2
View File
@@ -1487,8 +1487,8 @@ describe("SlackTransport (Slack Provider)", () => {
vi.unstubAllGlobals();
});
it("rides the existing webhook channel rather than adding one of its own", () => {
expect(new SlackTransport().channel).toBe("webhook");
it("exposes the dedicated slack channel", () => {
expect(new SlackTransport().channel).toBe("slack");
});
it("constructs without options and defaults to a conservative rate ceiling", () => {