mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
perf: slim slack provider helper tests
This commit is contained in:
244
extensions/slack/src/monitor/provider-support.ts
Normal file
244
extensions/slack/src/monitor/provider-support.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import type { SlackChannelResolution } from "../resolve-channels.js";
|
||||
import type { SlackUserResolution } from "../resolve-users.js";
|
||||
import { formatUnknownError, waitForSlackSocketDisconnect } from "./reconnect-policy.js";
|
||||
|
||||
type SlackAppConstructor = typeof import("@slack/bolt").App;
|
||||
type SlackHttpReceiverConstructor = typeof import("@slack/bolt").HTTPReceiver;
|
||||
type SlackSocketModeReceiverConstructor = typeof import("@slack/bolt").SocketModeReceiver;
|
||||
|
||||
export type SlackBoltResolvedExports = {
|
||||
App: SlackAppConstructor;
|
||||
HTTPReceiver: SlackHttpReceiverConstructor;
|
||||
SocketModeReceiver: SlackSocketModeReceiverConstructor;
|
||||
};
|
||||
|
||||
type SlackSocketShutdownClient = {
|
||||
shuttingDown?: boolean;
|
||||
};
|
||||
type Constructor = abstract new (...args: never[]) => unknown;
|
||||
|
||||
function isConstructorFunction<
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Constructor guard preserves the requested concrete Slack constructor type.
|
||||
T extends Constructor,
|
||||
>(value: unknown): value is T {
|
||||
return typeof value === "function";
|
||||
}
|
||||
|
||||
function resolveSlackBoltModule(value: unknown): SlackBoltResolvedExports | null {
|
||||
if (!value || typeof value !== "object") {
|
||||
return null;
|
||||
}
|
||||
const app = Reflect.get(value, "App");
|
||||
const httpReceiver = Reflect.get(value, "HTTPReceiver");
|
||||
const socketModeReceiver = Reflect.get(value, "SocketModeReceiver");
|
||||
if (
|
||||
!isConstructorFunction<SlackAppConstructor>(app) ||
|
||||
!isConstructorFunction<SlackHttpReceiverConstructor>(httpReceiver) ||
|
||||
!isConstructorFunction<SlackSocketModeReceiverConstructor>(socketModeReceiver)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
App: app,
|
||||
HTTPReceiver: httpReceiver,
|
||||
SocketModeReceiver: socketModeReceiver,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveSlackBoltInterop(params: {
|
||||
defaultImport: unknown;
|
||||
namespaceImport: unknown;
|
||||
}): SlackBoltResolvedExports {
|
||||
const { defaultImport, namespaceImport } = params;
|
||||
const nestedDefault =
|
||||
defaultImport && typeof defaultImport === "object"
|
||||
? Reflect.get(defaultImport, "default")
|
||||
: undefined;
|
||||
const namespaceDefault =
|
||||
namespaceImport && typeof namespaceImport === "object"
|
||||
? Reflect.get(namespaceImport, "default")
|
||||
: undefined;
|
||||
const namespaceReceiver =
|
||||
namespaceImport && typeof namespaceImport === "object"
|
||||
? Reflect.get(namespaceImport, "HTTPReceiver")
|
||||
: undefined;
|
||||
const namespaceSocketModeReceiver =
|
||||
namespaceImport && typeof namespaceImport === "object"
|
||||
? Reflect.get(namespaceImport, "SocketModeReceiver")
|
||||
: undefined;
|
||||
const directModule =
|
||||
resolveSlackBoltModule(defaultImport) ??
|
||||
resolveSlackBoltModule(nestedDefault) ??
|
||||
resolveSlackBoltModule(namespaceDefault) ??
|
||||
resolveSlackBoltModule(namespaceImport);
|
||||
if (directModule) {
|
||||
return directModule;
|
||||
}
|
||||
if (
|
||||
isConstructorFunction<SlackAppConstructor>(defaultImport) &&
|
||||
isConstructorFunction<SlackHttpReceiverConstructor>(namespaceReceiver) &&
|
||||
isConstructorFunction<SlackSocketModeReceiverConstructor>(namespaceSocketModeReceiver)
|
||||
) {
|
||||
return {
|
||||
App: defaultImport,
|
||||
HTTPReceiver: namespaceReceiver,
|
||||
SocketModeReceiver: namespaceSocketModeReceiver,
|
||||
};
|
||||
}
|
||||
throw new TypeError("Unable to resolve @slack/bolt App/HTTPReceiver exports");
|
||||
}
|
||||
|
||||
export function publishSlackConnectedStatus(setStatus?: (next: Record<string, unknown>) => void) {
|
||||
if (!setStatus) {
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
setStatus({
|
||||
connected: true,
|
||||
lastConnectedAt: now,
|
||||
healthState: "healthy",
|
||||
lastError: null,
|
||||
});
|
||||
}
|
||||
|
||||
export function publishSlackDisconnectedStatus(
|
||||
setStatus?: (next: Record<string, unknown>) => void,
|
||||
error?: unknown,
|
||||
) {
|
||||
if (!setStatus) {
|
||||
return;
|
||||
}
|
||||
const at = Date.now();
|
||||
const message = error ? formatUnknownError(error) : undefined;
|
||||
setStatus({
|
||||
connected: false,
|
||||
healthState: "disconnected",
|
||||
lastDisconnect: message ? { at, error: message } : { at },
|
||||
lastError: message ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
export function createSlackBoltApp(params: {
|
||||
interop: SlackBoltResolvedExports;
|
||||
slackMode: "socket" | "http";
|
||||
botToken: string;
|
||||
appToken?: string;
|
||||
signingSecret?: string;
|
||||
slackWebhookPath: string;
|
||||
clientOptions: Record<string, unknown>;
|
||||
}) {
|
||||
const receiver =
|
||||
params.slackMode === "socket"
|
||||
? new params.interop.SocketModeReceiver({
|
||||
appToken: params.appToken ?? "",
|
||||
autoReconnectEnabled: false,
|
||||
installerOptions: {
|
||||
clientOptions: params.clientOptions,
|
||||
},
|
||||
})
|
||||
: new params.interop.HTTPReceiver({
|
||||
signingSecret: params.signingSecret ?? "",
|
||||
endpoints: params.slackWebhookPath,
|
||||
});
|
||||
const app = new params.interop.App({
|
||||
token: params.botToken,
|
||||
receiver,
|
||||
clientOptions: params.clientOptions,
|
||||
});
|
||||
return { app, receiver };
|
||||
}
|
||||
|
||||
export function createSlackSocketDisconnectWaiter(app: unknown, abortSignal?: AbortSignal) {
|
||||
const waiterAbortController = new AbortController();
|
||||
const relayAbort = () => waiterAbortController.abort();
|
||||
abortSignal?.addEventListener("abort", relayAbort, { once: true });
|
||||
return {
|
||||
promise: waitForSlackSocketDisconnect(app, waiterAbortController.signal),
|
||||
cancel: () => {
|
||||
waiterAbortController.abort();
|
||||
abortSignal?.removeEventListener("abort", relayAbort);
|
||||
},
|
||||
complete: () => {
|
||||
abortSignal?.removeEventListener("abort", relayAbort);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function startSlackSocketAndWaitForDisconnect(params: {
|
||||
app: { start: () => unknown };
|
||||
abortSignal?: AbortSignal;
|
||||
onStarted?: () => void;
|
||||
}) {
|
||||
const disconnectWaiter = createSlackSocketDisconnectWaiter(params.app, params.abortSignal);
|
||||
try {
|
||||
await Promise.resolve(params.app.start());
|
||||
if (params.abortSignal?.aborted) {
|
||||
disconnectWaiter.cancel();
|
||||
return null;
|
||||
}
|
||||
params.onStarted?.();
|
||||
const disconnect = await disconnectWaiter.promise;
|
||||
disconnectWaiter.complete();
|
||||
return disconnect;
|
||||
} catch (err) {
|
||||
disconnectWaiter.cancel();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveSlackSocketShutdownClient(
|
||||
app: unknown,
|
||||
): SlackSocketShutdownClient | undefined {
|
||||
if (!app || typeof app !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const receiver = Reflect.get(app, "receiver");
|
||||
if (!receiver || typeof receiver !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const client = Reflect.get(receiver, "client");
|
||||
if (!client || typeof client !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return client as SlackSocketShutdownClient;
|
||||
}
|
||||
|
||||
export async function gracefulStopSlackApp(app: { stop: () => unknown }) {
|
||||
const socketClient = resolveSlackSocketShutdownClient(app);
|
||||
if (socketClient) {
|
||||
socketClient.shuttingDown = true;
|
||||
}
|
||||
await Promise.resolve(app.stop()).catch(() => undefined);
|
||||
}
|
||||
|
||||
function formatSlackResolvedLabel(params: {
|
||||
input: string;
|
||||
id: string;
|
||||
name?: string;
|
||||
extra?: string[];
|
||||
}): string {
|
||||
const extras = params.extra?.filter(Boolean) ?? [];
|
||||
const suffix =
|
||||
extras.length > 0 ? ` (id:${params.id}, ${extras.join(", ")})` : ` (id:${params.id})`;
|
||||
return `${params.input}→${params.name ?? params.id}${suffix}`;
|
||||
}
|
||||
|
||||
export function formatSlackChannelResolved(entry: SlackChannelResolution): string {
|
||||
const id = entry.id ?? entry.input;
|
||||
return formatSlackResolvedLabel({
|
||||
input: entry.input,
|
||||
id,
|
||||
name: entry.name,
|
||||
extra: entry.archived ? ["archived"] : [],
|
||||
});
|
||||
}
|
||||
|
||||
export function formatSlackUserResolved(entry: SlackUserResolution): string {
|
||||
const id = entry.id ?? entry.input;
|
||||
return formatSlackResolvedLabel({
|
||||
input: entry.input,
|
||||
id,
|
||||
name: entry.name,
|
||||
extra: entry.note ? [entry.note] : [],
|
||||
});
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { __testing } from "./provider.js";
|
||||
import { formatSlackChannelResolved, formatSlackUserResolved } from "./provider-support.js";
|
||||
|
||||
describe("slack allowlist log formatting", () => {
|
||||
it("prints channel names alongside ids", () => {
|
||||
expect(
|
||||
__testing.formatSlackChannelResolved({
|
||||
formatSlackChannelResolved({
|
||||
input: "C0AQXEG6QFJ",
|
||||
resolved: true,
|
||||
id: "C0AQXEG6QFJ",
|
||||
@@ -15,7 +15,7 @@ describe("slack allowlist log formatting", () => {
|
||||
|
||||
it("prints user names alongside ids", () => {
|
||||
expect(
|
||||
__testing.formatSlackUserResolved({
|
||||
formatSlackUserResolved({
|
||||
input: "U090HHQ029J",
|
||||
resolved: true,
|
||||
id: "U090HHQ029J",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { isNonRecoverableSlackAuthError } from "./provider.js";
|
||||
import { isNonRecoverableSlackAuthError } from "./reconnect-policy.js";
|
||||
|
||||
describe("isNonRecoverableSlackAuthError", () => {
|
||||
it.each([
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { __testing } from "./provider.js";
|
||||
import { createSlackBoltApp, resolveSlackBoltInterop } from "./provider-support.js";
|
||||
|
||||
describe("resolveSlackBoltInterop", () => {
|
||||
function FakeApp() {}
|
||||
@@ -7,7 +7,7 @@ describe("resolveSlackBoltInterop", () => {
|
||||
function FakeSocketModeReceiver() {}
|
||||
|
||||
it("uses the default import when it already exposes named exports", () => {
|
||||
const resolved = __testing.resolveSlackBoltInterop({
|
||||
const resolved = resolveSlackBoltInterop({
|
||||
defaultImport: {
|
||||
App: FakeApp,
|
||||
HTTPReceiver: FakeHTTPReceiver,
|
||||
@@ -24,7 +24,7 @@ describe("resolveSlackBoltInterop", () => {
|
||||
});
|
||||
|
||||
it("uses nested default export when the default import is a wrapper object", () => {
|
||||
const resolved = __testing.resolveSlackBoltInterop({
|
||||
const resolved = resolveSlackBoltInterop({
|
||||
defaultImport: {
|
||||
default: {
|
||||
App: FakeApp,
|
||||
@@ -43,7 +43,7 @@ describe("resolveSlackBoltInterop", () => {
|
||||
});
|
||||
|
||||
it("uses the namespace receiver when the default import is the App constructor itself", () => {
|
||||
const resolved = __testing.resolveSlackBoltInterop({
|
||||
const resolved = resolveSlackBoltInterop({
|
||||
defaultImport: FakeApp,
|
||||
namespaceImport: {
|
||||
HTTPReceiver: FakeHTTPReceiver,
|
||||
@@ -59,7 +59,7 @@ describe("resolveSlackBoltInterop", () => {
|
||||
});
|
||||
|
||||
it("uses namespace.default when it exposes named exports", () => {
|
||||
const resolved = __testing.resolveSlackBoltInterop({
|
||||
const resolved = resolveSlackBoltInterop({
|
||||
defaultImport: undefined,
|
||||
namespaceImport: {
|
||||
default: {
|
||||
@@ -78,7 +78,7 @@ describe("resolveSlackBoltInterop", () => {
|
||||
});
|
||||
|
||||
it("falls back to the namespace import when it exposes named exports", () => {
|
||||
const resolved = __testing.resolveSlackBoltInterop({
|
||||
const resolved = resolveSlackBoltInterop({
|
||||
defaultImport: undefined,
|
||||
namespaceImport: {
|
||||
App: FakeApp,
|
||||
@@ -96,7 +96,7 @@ describe("resolveSlackBoltInterop", () => {
|
||||
|
||||
it("throws when the module cannot be resolved", () => {
|
||||
expect(() =>
|
||||
__testing.resolveSlackBoltInterop({
|
||||
resolveSlackBoltInterop({
|
||||
defaultImport: null,
|
||||
namespaceImport: {},
|
||||
}),
|
||||
@@ -131,7 +131,7 @@ describe("createSlackBoltApp", () => {
|
||||
|
||||
it("uses SocketModeReceiver with OpenClaw-owned reconnects and shared client options", () => {
|
||||
const clientOptions = { teamId: "T1" };
|
||||
const { app, receiver } = __testing.createSlackBoltApp({
|
||||
const { app, receiver } = createSlackBoltApp({
|
||||
interop: {
|
||||
App: FakeApp as never,
|
||||
HTTPReceiver: FakeHTTPReceiver as never,
|
||||
@@ -162,7 +162,7 @@ describe("createSlackBoltApp", () => {
|
||||
|
||||
it("uses HTTPReceiver for webhook mode", () => {
|
||||
const clientOptions = { teamId: "T1" };
|
||||
const { app, receiver } = __testing.createSlackBoltApp({
|
||||
const { app, receiver } = createSlackBoltApp({
|
||||
interop: {
|
||||
App: FakeApp as never,
|
||||
HTTPReceiver: FakeHTTPReceiver as never,
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { __testing } from "./provider.js";
|
||||
import {
|
||||
gracefulStopSlackApp,
|
||||
publishSlackConnectedStatus,
|
||||
publishSlackDisconnectedStatus,
|
||||
startSlackSocketAndWaitForDisconnect,
|
||||
} from "./provider-support.js";
|
||||
import { waitForSlackSocketDisconnect } from "./reconnect-policy.js";
|
||||
|
||||
class FakeEmitter {
|
||||
private listeners = new Map<string, Set<(...args: unknown[]) => void>>();
|
||||
@@ -29,7 +35,7 @@ describe("slack socket reconnect helpers", () => {
|
||||
it("marks socket mode healthy without seeding event liveness on connect", () => {
|
||||
const setStatus = vi.fn();
|
||||
|
||||
__testing.publishSlackConnectedStatus(setStatus);
|
||||
publishSlackConnectedStatus(setStatus);
|
||||
|
||||
expect(setStatus).toHaveBeenCalledTimes(1);
|
||||
expect(setStatus).toHaveBeenCalledWith(
|
||||
@@ -49,7 +55,7 @@ describe("slack socket reconnect helpers", () => {
|
||||
const setStatus = vi.fn();
|
||||
const err = new Error("dns down");
|
||||
|
||||
__testing.publishSlackDisconnectedStatus(setStatus, err);
|
||||
publishSlackDisconnectedStatus(setStatus, err);
|
||||
|
||||
expect(setStatus).toHaveBeenCalledTimes(1);
|
||||
expect(setStatus).toHaveBeenCalledWith({
|
||||
@@ -66,7 +72,7 @@ describe("slack socket reconnect helpers", () => {
|
||||
it("marks socket mode disconnected without error when the socket closes cleanly", () => {
|
||||
const setStatus = vi.fn();
|
||||
|
||||
__testing.publishSlackDisconnectedStatus(setStatus);
|
||||
publishSlackDisconnectedStatus(setStatus);
|
||||
|
||||
expect(setStatus).toHaveBeenCalledTimes(1);
|
||||
expect(setStatus).toHaveBeenCalledWith({
|
||||
@@ -83,7 +89,7 @@ describe("slack socket reconnect helpers", () => {
|
||||
const client = new FakeEmitter();
|
||||
const app = { receiver: { client } };
|
||||
|
||||
const waiter = __testing.waitForSlackSocketDisconnect(app as never);
|
||||
const waiter = waitForSlackSocketDisconnect(app as never);
|
||||
client.emit("disconnected");
|
||||
|
||||
await expect(waiter).resolves.toEqual({ event: "disconnect" });
|
||||
@@ -94,7 +100,7 @@ describe("slack socket reconnect helpers", () => {
|
||||
const app = { receiver: { client } };
|
||||
const err = new Error("dns down");
|
||||
|
||||
const waiter = __testing.waitForSlackSocketDisconnect(app as never);
|
||||
const waiter = waitForSlackSocketDisconnect(app as never);
|
||||
client.emit("error", err);
|
||||
|
||||
await expect(waiter).resolves.toEqual({ event: "error", error: err });
|
||||
@@ -111,7 +117,7 @@ describe("slack socket reconnect helpers", () => {
|
||||
const onStarted = vi.fn();
|
||||
|
||||
await expect(
|
||||
__testing.startSlackSocketAndWaitForDisconnect({
|
||||
startSlackSocketAndWaitForDisconnect({
|
||||
app: app as never,
|
||||
onStarted,
|
||||
}),
|
||||
@@ -130,7 +136,7 @@ describe("slack socket reconnect helpers", () => {
|
||||
const err = new Error("status sink failed");
|
||||
|
||||
await expect(
|
||||
__testing.startSlackSocketAndWaitForDisconnect({
|
||||
startSlackSocketAndWaitForDisconnect({
|
||||
app: app as never,
|
||||
onStarted: () => {
|
||||
throw err;
|
||||
@@ -148,7 +154,7 @@ describe("slack socket reconnect helpers", () => {
|
||||
const app = { receiver: { client } };
|
||||
const err = new Error("invalid_auth");
|
||||
|
||||
const waiter = __testing.waitForSlackSocketDisconnect(app as never);
|
||||
const waiter = waitForSlackSocketDisconnect(app as never);
|
||||
client.emit("unable_to_socket_mode_start", err);
|
||||
|
||||
await expect(waiter).resolves.toEqual({
|
||||
@@ -165,7 +171,7 @@ describe("slack socket reconnect helpers", () => {
|
||||
}),
|
||||
};
|
||||
|
||||
await __testing.gracefulStopSlackApp(app);
|
||||
await gracefulStopSlackApp(app);
|
||||
|
||||
expect(app.stop).toHaveBeenCalledTimes(1);
|
||||
expect(app.receiver.client.shuttingDown).toBe(true);
|
||||
|
||||
@@ -27,8 +27,8 @@ import { resolveSlackWebClientOptions } from "../client.js";
|
||||
import { isSlackExecApprovalClientEnabled } from "../exec-approvals.js";
|
||||
import { normalizeSlackWebhookPath, registerSlackHttpHandler } from "../http/index.js";
|
||||
import { SLACK_TEXT_LIMIT } from "../limits.js";
|
||||
import { resolveSlackChannelAllowlist, type SlackChannelResolution } from "../resolve-channels.js";
|
||||
import { resolveSlackUserAllowlist, type SlackUserResolution } from "../resolve-users.js";
|
||||
import { resolveSlackChannelAllowlist } from "../resolve-channels.js";
|
||||
import { resolveSlackUserAllowlist } from "../resolve-users.js";
|
||||
import { resolveSlackAppToken, resolveSlackBotToken } from "../token.js";
|
||||
import { normalizeAllowList } from "./allow-list.js";
|
||||
import { resolveSlackSlashCommandConfig } from "./commands.js";
|
||||
@@ -42,6 +42,19 @@ import {
|
||||
import { createSlackMonitorContext } from "./context.js";
|
||||
import { registerSlackMonitorEvents } from "./events.js";
|
||||
import { createSlackMessageHandler } from "./message-handler.js";
|
||||
import {
|
||||
createSlackBoltApp,
|
||||
createSlackSocketDisconnectWaiter,
|
||||
formatSlackChannelResolved,
|
||||
formatSlackUserResolved,
|
||||
gracefulStopSlackApp,
|
||||
publishSlackConnectedStatus,
|
||||
publishSlackDisconnectedStatus,
|
||||
resolveSlackBoltInterop,
|
||||
resolveSlackSocketShutdownClient,
|
||||
startSlackSocketAndWaitForDisconnect,
|
||||
type SlackBoltResolvedExports,
|
||||
} from "./provider-support.js";
|
||||
import {
|
||||
formatUnknownError,
|
||||
getSocketEmitter,
|
||||
@@ -53,90 +66,6 @@ import { resolveTextChunkLimit } from "./reply.runtime.js";
|
||||
import { registerSlackMonitorSlashCommands } from "./slash.js";
|
||||
import type { MonitorSlackOpts } from "./types.js";
|
||||
|
||||
type SlackAppConstructor = typeof import("@slack/bolt").App;
|
||||
type SlackHttpReceiverConstructor = typeof import("@slack/bolt").HTTPReceiver;
|
||||
type SlackSocketModeReceiverConstructor = typeof import("@slack/bolt").SocketModeReceiver;
|
||||
type SlackBoltResolvedExports = {
|
||||
App: SlackAppConstructor;
|
||||
HTTPReceiver: SlackHttpReceiverConstructor;
|
||||
SocketModeReceiver: SlackSocketModeReceiverConstructor;
|
||||
};
|
||||
type SlackSocketShutdownClient = {
|
||||
shuttingDown?: boolean;
|
||||
};
|
||||
type Constructor = abstract new (...args: never[]) => unknown;
|
||||
|
||||
function isConstructorFunction<
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Constructor guard preserves the requested concrete Slack constructor type.
|
||||
T extends Constructor,
|
||||
>(value: unknown): value is T {
|
||||
return typeof value === "function";
|
||||
}
|
||||
|
||||
function resolveSlackBoltModule(value: unknown): SlackBoltResolvedExports | null {
|
||||
if (!value || typeof value !== "object") {
|
||||
return null;
|
||||
}
|
||||
const app = Reflect.get(value, "App");
|
||||
const httpReceiver = Reflect.get(value, "HTTPReceiver");
|
||||
const socketModeReceiver = Reflect.get(value, "SocketModeReceiver");
|
||||
if (
|
||||
!isConstructorFunction<SlackAppConstructor>(app) ||
|
||||
!isConstructorFunction<SlackHttpReceiverConstructor>(httpReceiver) ||
|
||||
!isConstructorFunction<SlackSocketModeReceiverConstructor>(socketModeReceiver)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
App: app,
|
||||
HTTPReceiver: httpReceiver,
|
||||
SocketModeReceiver: socketModeReceiver,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSlackBoltInterop(params: {
|
||||
defaultImport: unknown;
|
||||
namespaceImport: unknown;
|
||||
}): SlackBoltResolvedExports {
|
||||
const { defaultImport, namespaceImport } = params;
|
||||
const nestedDefault =
|
||||
defaultImport && typeof defaultImport === "object"
|
||||
? Reflect.get(defaultImport, "default")
|
||||
: undefined;
|
||||
const namespaceDefault =
|
||||
namespaceImport && typeof namespaceImport === "object"
|
||||
? Reflect.get(namespaceImport, "default")
|
||||
: undefined;
|
||||
const namespaceReceiver =
|
||||
namespaceImport && typeof namespaceImport === "object"
|
||||
? Reflect.get(namespaceImport, "HTTPReceiver")
|
||||
: undefined;
|
||||
const namespaceSocketModeReceiver =
|
||||
namespaceImport && typeof namespaceImport === "object"
|
||||
? Reflect.get(namespaceImport, "SocketModeReceiver")
|
||||
: undefined;
|
||||
const directModule =
|
||||
resolveSlackBoltModule(defaultImport) ??
|
||||
resolveSlackBoltModule(nestedDefault) ??
|
||||
resolveSlackBoltModule(namespaceDefault) ??
|
||||
resolveSlackBoltModule(namespaceImport);
|
||||
if (directModule) {
|
||||
return directModule;
|
||||
}
|
||||
if (
|
||||
isConstructorFunction<SlackAppConstructor>(defaultImport) &&
|
||||
isConstructorFunction<SlackHttpReceiverConstructor>(namespaceReceiver) &&
|
||||
isConstructorFunction<SlackSocketModeReceiverConstructor>(namespaceSocketModeReceiver)
|
||||
) {
|
||||
return {
|
||||
App: defaultImport,
|
||||
HTTPReceiver: namespaceReceiver,
|
||||
SocketModeReceiver: namespaceSocketModeReceiver,
|
||||
};
|
||||
}
|
||||
throw new TypeError("Unable to resolve @slack/bolt App/HTTPReceiver exports");
|
||||
}
|
||||
|
||||
let slackBoltInterop: SlackBoltResolvedExports | undefined;
|
||||
|
||||
function getSlackBoltInterop(): SlackBoltResolvedExports {
|
||||
@@ -161,158 +90,6 @@ function parseApiAppIdFromAppToken(raw?: string) {
|
||||
return match?.[1]?.toUpperCase();
|
||||
}
|
||||
|
||||
function publishSlackConnectedStatus(setStatus?: (next: Record<string, unknown>) => void) {
|
||||
if (!setStatus) {
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
setStatus({
|
||||
connected: true,
|
||||
lastConnectedAt: now,
|
||||
healthState: "healthy",
|
||||
lastError: null,
|
||||
});
|
||||
}
|
||||
|
||||
function publishSlackDisconnectedStatus(
|
||||
setStatus?: (next: Record<string, unknown>) => void,
|
||||
error?: unknown,
|
||||
) {
|
||||
if (!setStatus) {
|
||||
return;
|
||||
}
|
||||
const at = Date.now();
|
||||
const message = error ? formatUnknownError(error) : undefined;
|
||||
setStatus({
|
||||
connected: false,
|
||||
healthState: "disconnected",
|
||||
lastDisconnect: message ? { at, error: message } : { at },
|
||||
lastError: message ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
function createSlackBoltApp(params: {
|
||||
interop: SlackBoltResolvedExports;
|
||||
slackMode: "socket" | "http";
|
||||
botToken: string;
|
||||
appToken?: string;
|
||||
signingSecret?: string;
|
||||
slackWebhookPath: string;
|
||||
clientOptions: Record<string, unknown>;
|
||||
}) {
|
||||
const receiver =
|
||||
params.slackMode === "socket"
|
||||
? new params.interop.SocketModeReceiver({
|
||||
appToken: params.appToken ?? "",
|
||||
autoReconnectEnabled: false,
|
||||
installerOptions: {
|
||||
clientOptions: params.clientOptions,
|
||||
},
|
||||
})
|
||||
: new params.interop.HTTPReceiver({
|
||||
signingSecret: params.signingSecret ?? "",
|
||||
endpoints: params.slackWebhookPath,
|
||||
});
|
||||
const app = new params.interop.App({
|
||||
token: params.botToken,
|
||||
receiver,
|
||||
clientOptions: params.clientOptions,
|
||||
});
|
||||
return { app, receiver };
|
||||
}
|
||||
|
||||
function createSlackSocketDisconnectWaiter(app: unknown, abortSignal?: AbortSignal) {
|
||||
const waiterAbortController = new AbortController();
|
||||
const relayAbort = () => waiterAbortController.abort();
|
||||
abortSignal?.addEventListener("abort", relayAbort, { once: true });
|
||||
return {
|
||||
promise: waitForSlackSocketDisconnect(app, waiterAbortController.signal),
|
||||
cancel: () => {
|
||||
waiterAbortController.abort();
|
||||
abortSignal?.removeEventListener("abort", relayAbort);
|
||||
},
|
||||
complete: () => {
|
||||
abortSignal?.removeEventListener("abort", relayAbort);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function startSlackSocketAndWaitForDisconnect(params: {
|
||||
app: { start: () => unknown };
|
||||
abortSignal?: AbortSignal;
|
||||
onStarted?: () => void;
|
||||
}) {
|
||||
const disconnectWaiter = createSlackSocketDisconnectWaiter(params.app, params.abortSignal);
|
||||
try {
|
||||
await Promise.resolve(params.app.start());
|
||||
if (params.abortSignal?.aborted) {
|
||||
disconnectWaiter.cancel();
|
||||
return null;
|
||||
}
|
||||
params.onStarted?.();
|
||||
const disconnect = await disconnectWaiter.promise;
|
||||
disconnectWaiter.complete();
|
||||
return disconnect;
|
||||
} catch (err) {
|
||||
disconnectWaiter.cancel();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSlackSocketShutdownClient(app: unknown): SlackSocketShutdownClient | undefined {
|
||||
if (!app || typeof app !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const receiver = Reflect.get(app, "receiver");
|
||||
if (!receiver || typeof receiver !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const client = Reflect.get(receiver, "client");
|
||||
if (!client || typeof client !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return client as SlackSocketShutdownClient;
|
||||
}
|
||||
|
||||
async function gracefulStopSlackApp(app: { stop: () => unknown }) {
|
||||
const socketClient = resolveSlackSocketShutdownClient(app);
|
||||
if (socketClient) {
|
||||
socketClient.shuttingDown = true;
|
||||
}
|
||||
await Promise.resolve(app.stop()).catch(() => undefined);
|
||||
}
|
||||
|
||||
function formatSlackResolvedLabel(params: {
|
||||
input: string;
|
||||
id: string;
|
||||
name?: string;
|
||||
extra?: string[];
|
||||
}): string {
|
||||
const extras = params.extra?.filter(Boolean) ?? [];
|
||||
const suffix =
|
||||
extras.length > 0 ? ` (id:${params.id}, ${extras.join(", ")})` : ` (id:${params.id})`;
|
||||
return `${params.input}→${params.name ?? params.id}${suffix}`;
|
||||
}
|
||||
|
||||
function formatSlackChannelResolved(entry: SlackChannelResolution): string {
|
||||
const id = entry.id ?? entry.input;
|
||||
return formatSlackResolvedLabel({
|
||||
input: entry.input,
|
||||
id,
|
||||
name: entry.name,
|
||||
extra: entry.archived ? ["archived"] : [],
|
||||
});
|
||||
}
|
||||
|
||||
function formatSlackUserResolved(entry: SlackUserResolution): string {
|
||||
const id = entry.id ?? entry.input;
|
||||
return formatSlackResolvedLabel({
|
||||
input: entry.input,
|
||||
id,
|
||||
name: entry.name,
|
||||
extra: entry.note ? [entry.note] : [],
|
||||
});
|
||||
}
|
||||
export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
const cfg = opts.config ?? loadConfig();
|
||||
const runtime: RuntimeEnv = opts.runtime ?? createNonExitingRuntime();
|
||||
@@ -428,7 +205,9 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
const slackHttpHandler =
|
||||
slackMode === "http" && receiver
|
||||
? async (req: IncomingMessage, res: ServerResponse) => {
|
||||
const httpReceiver = receiver as InstanceType<SlackHttpReceiverConstructor>;
|
||||
const httpReceiver = receiver as {
|
||||
requestListener: (req: IncomingMessage, res: ServerResponse) => unknown;
|
||||
};
|
||||
const guard = installRequestBodyLimitGuard(req, res, {
|
||||
maxBytes: SLACK_WEBHOOK_MAX_BODY_BYTES,
|
||||
timeoutMs: SLACK_WEBHOOK_BODY_TIMEOUT_MS,
|
||||
|
||||
Reference in New Issue
Block a user