mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-07 22:09:57 +00:00
refactor: split slack/discord/session maintenance helpers
This commit is contained in:
41
src/slack/monitor/channel-type.ts
Normal file
41
src/slack/monitor/channel-type.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { SlackMessageEvent } from "../types.js";
|
||||
|
||||
export function inferSlackChannelType(
|
||||
channelId?: string | null,
|
||||
): SlackMessageEvent["channel_type"] | undefined {
|
||||
const trimmed = channelId?.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
if (trimmed.startsWith("D")) {
|
||||
return "im";
|
||||
}
|
||||
if (trimmed.startsWith("C")) {
|
||||
return "channel";
|
||||
}
|
||||
if (trimmed.startsWith("G")) {
|
||||
return "group";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function normalizeSlackChannelType(
|
||||
channelType?: string | null,
|
||||
channelId?: string | null,
|
||||
): SlackMessageEvent["channel_type"] {
|
||||
const normalized = channelType?.trim().toLowerCase();
|
||||
const inferred = inferSlackChannelType(channelId);
|
||||
if (
|
||||
normalized === "im" ||
|
||||
normalized === "mpim" ||
|
||||
normalized === "channel" ||
|
||||
normalized === "group"
|
||||
) {
|
||||
// D-prefix channel IDs are always DMs — override a contradicting channel_type.
|
||||
if (inferred === "im" && normalized !== "im") {
|
||||
return "im";
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
return inferred ?? "channel";
|
||||
}
|
||||
@@ -12,47 +12,10 @@ import type { SlackMessageEvent } from "../types.js";
|
||||
import { normalizeAllowList, normalizeAllowListLower, normalizeSlackSlug } from "./allow-list.js";
|
||||
import type { SlackChannelConfigEntries } from "./channel-config.js";
|
||||
import { resolveSlackChannelConfig } from "./channel-config.js";
|
||||
import { normalizeSlackChannelType } from "./channel-type.js";
|
||||
import { isSlackChannelAllowedByPolicy } from "./policy.js";
|
||||
|
||||
export function inferSlackChannelType(
|
||||
channelId?: string | null,
|
||||
): SlackMessageEvent["channel_type"] | undefined {
|
||||
const trimmed = channelId?.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
if (trimmed.startsWith("D")) {
|
||||
return "im";
|
||||
}
|
||||
if (trimmed.startsWith("C")) {
|
||||
return "channel";
|
||||
}
|
||||
if (trimmed.startsWith("G")) {
|
||||
return "group";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function normalizeSlackChannelType(
|
||||
channelType?: string | null,
|
||||
channelId?: string | null,
|
||||
): SlackMessageEvent["channel_type"] {
|
||||
const normalized = channelType?.trim().toLowerCase();
|
||||
const inferred = inferSlackChannelType(channelId);
|
||||
if (
|
||||
normalized === "im" ||
|
||||
normalized === "mpim" ||
|
||||
normalized === "channel" ||
|
||||
normalized === "group"
|
||||
) {
|
||||
// D-prefix channel IDs are always DMs — override a contradicting channel_type.
|
||||
if (inferred === "im" && normalized !== "im") {
|
||||
return "im";
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
return inferred ?? "channel";
|
||||
}
|
||||
export { inferSlackChannelType, normalizeSlackChannelType } from "./channel-type.js";
|
||||
|
||||
export type SlackMonitorContext = {
|
||||
cfg: OpenClawConfig;
|
||||
|
||||
259
src/slack/monitor/events/interactions.modal.ts
Normal file
259
src/slack/monitor/events/interactions.modal.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
import { enqueueSystemEvent } from "../../../infra/system-events.js";
|
||||
import { parseSlackModalPrivateMetadata } from "../../modal-metadata.js";
|
||||
import { authorizeSlackSystemEventSender } from "../auth.js";
|
||||
import type { SlackMonitorContext } from "../context.js";
|
||||
|
||||
export type ModalInputSummary = {
|
||||
blockId: string;
|
||||
actionId: string;
|
||||
actionType?: string;
|
||||
inputKind?: "text" | "number" | "email" | "url" | "rich_text";
|
||||
value?: string;
|
||||
selectedValues?: string[];
|
||||
selectedUsers?: string[];
|
||||
selectedChannels?: string[];
|
||||
selectedConversations?: string[];
|
||||
selectedLabels?: string[];
|
||||
selectedDate?: string;
|
||||
selectedTime?: string;
|
||||
selectedDateTime?: number;
|
||||
inputValue?: string;
|
||||
inputNumber?: number;
|
||||
inputEmail?: string;
|
||||
inputUrl?: string;
|
||||
richTextValue?: unknown;
|
||||
richTextPreview?: string;
|
||||
};
|
||||
|
||||
export type SlackModalBody = {
|
||||
user?: { id?: string };
|
||||
team?: { id?: string };
|
||||
view?: {
|
||||
id?: string;
|
||||
callback_id?: string;
|
||||
private_metadata?: string;
|
||||
root_view_id?: string;
|
||||
previous_view_id?: string;
|
||||
external_id?: string;
|
||||
hash?: string;
|
||||
state?: { values?: unknown };
|
||||
};
|
||||
is_cleared?: boolean;
|
||||
};
|
||||
|
||||
type SlackModalEventBase = {
|
||||
callbackId: string;
|
||||
userId: string;
|
||||
expectedUserId?: string;
|
||||
viewId?: string;
|
||||
sessionRouting: ReturnType<typeof resolveModalSessionRouting>;
|
||||
payload: {
|
||||
actionId: string;
|
||||
callbackId: string;
|
||||
viewId?: string;
|
||||
userId: string;
|
||||
teamId?: string;
|
||||
rootViewId?: string;
|
||||
previousViewId?: string;
|
||||
externalId?: string;
|
||||
viewHash?: string;
|
||||
isStackedView?: boolean;
|
||||
privateMetadata?: string;
|
||||
routedChannelId?: string;
|
||||
routedChannelType?: string;
|
||||
inputs: ModalInputSummary[];
|
||||
};
|
||||
};
|
||||
|
||||
export type SlackModalInteractionKind = "view_submission" | "view_closed";
|
||||
export type SlackModalEventHandlerArgs = { ack: () => Promise<void>; body: unknown };
|
||||
export type RegisterSlackModalHandler = (
|
||||
matcher: RegExp,
|
||||
handler: (args: SlackModalEventHandlerArgs) => Promise<void>,
|
||||
) => void;
|
||||
|
||||
type SlackInteractionContextPrefix = "slack:interaction:view" | "slack:interaction:view-closed";
|
||||
|
||||
function resolveModalSessionRouting(params: {
|
||||
ctx: SlackMonitorContext;
|
||||
metadata: ReturnType<typeof parseSlackModalPrivateMetadata>;
|
||||
}): { sessionKey: string; channelId?: string; channelType?: string } {
|
||||
const metadata = params.metadata;
|
||||
if (metadata.sessionKey) {
|
||||
return {
|
||||
sessionKey: metadata.sessionKey,
|
||||
channelId: metadata.channelId,
|
||||
channelType: metadata.channelType,
|
||||
};
|
||||
}
|
||||
if (metadata.channelId) {
|
||||
return {
|
||||
sessionKey: params.ctx.resolveSlackSystemEventSessionKey({
|
||||
channelId: metadata.channelId,
|
||||
channelType: metadata.channelType,
|
||||
}),
|
||||
channelId: metadata.channelId,
|
||||
channelType: metadata.channelType,
|
||||
};
|
||||
}
|
||||
return {
|
||||
sessionKey: params.ctx.resolveSlackSystemEventSessionKey({}),
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeSlackViewLifecycleContext(view: {
|
||||
root_view_id?: string;
|
||||
previous_view_id?: string;
|
||||
external_id?: string;
|
||||
hash?: string;
|
||||
}): {
|
||||
rootViewId?: string;
|
||||
previousViewId?: string;
|
||||
externalId?: string;
|
||||
viewHash?: string;
|
||||
isStackedView?: boolean;
|
||||
} {
|
||||
const rootViewId = view.root_view_id;
|
||||
const previousViewId = view.previous_view_id;
|
||||
const externalId = view.external_id;
|
||||
const viewHash = view.hash;
|
||||
return {
|
||||
rootViewId,
|
||||
previousViewId,
|
||||
externalId,
|
||||
viewHash,
|
||||
isStackedView: Boolean(previousViewId),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSlackModalEventBase(params: {
|
||||
ctx: SlackMonitorContext;
|
||||
body: SlackModalBody;
|
||||
summarizeViewState: (values: unknown) => ModalInputSummary[];
|
||||
}): SlackModalEventBase {
|
||||
const metadata = parseSlackModalPrivateMetadata(params.body.view?.private_metadata);
|
||||
const callbackId = params.body.view?.callback_id ?? "unknown";
|
||||
const userId = params.body.user?.id ?? "unknown";
|
||||
const viewId = params.body.view?.id;
|
||||
const inputs = params.summarizeViewState(params.body.view?.state?.values);
|
||||
const sessionRouting = resolveModalSessionRouting({
|
||||
ctx: params.ctx,
|
||||
metadata,
|
||||
});
|
||||
return {
|
||||
callbackId,
|
||||
userId,
|
||||
expectedUserId: metadata.userId,
|
||||
viewId,
|
||||
sessionRouting,
|
||||
payload: {
|
||||
actionId: `view:${callbackId}`,
|
||||
callbackId,
|
||||
viewId,
|
||||
userId,
|
||||
teamId: params.body.team?.id,
|
||||
...summarizeSlackViewLifecycleContext({
|
||||
root_view_id: params.body.view?.root_view_id,
|
||||
previous_view_id: params.body.view?.previous_view_id,
|
||||
external_id: params.body.view?.external_id,
|
||||
hash: params.body.view?.hash,
|
||||
}),
|
||||
privateMetadata: params.body.view?.private_metadata,
|
||||
routedChannelId: sessionRouting.channelId,
|
||||
routedChannelType: sessionRouting.channelType,
|
||||
inputs,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function emitSlackModalLifecycleEvent(params: {
|
||||
ctx: SlackMonitorContext;
|
||||
body: SlackModalBody;
|
||||
interactionType: SlackModalInteractionKind;
|
||||
contextPrefix: SlackInteractionContextPrefix;
|
||||
summarizeViewState: (values: unknown) => ModalInputSummary[];
|
||||
formatSystemEvent: (payload: Record<string, unknown>) => string;
|
||||
}): Promise<void> {
|
||||
const { callbackId, userId, expectedUserId, viewId, sessionRouting, payload } =
|
||||
resolveSlackModalEventBase({
|
||||
ctx: params.ctx,
|
||||
body: params.body,
|
||||
summarizeViewState: params.summarizeViewState,
|
||||
});
|
||||
const isViewClosed = params.interactionType === "view_closed";
|
||||
const isCleared = params.body.is_cleared === true;
|
||||
const eventPayload = isViewClosed
|
||||
? {
|
||||
interactionType: params.interactionType,
|
||||
...payload,
|
||||
isCleared,
|
||||
}
|
||||
: {
|
||||
interactionType: params.interactionType,
|
||||
...payload,
|
||||
};
|
||||
|
||||
if (isViewClosed) {
|
||||
params.ctx.runtime.log?.(
|
||||
`slack:interaction view_closed callback=${callbackId} user=${userId} cleared=${isCleared}`,
|
||||
);
|
||||
} else {
|
||||
params.ctx.runtime.log?.(
|
||||
`slack:interaction view_submission callback=${callbackId} user=${userId} inputs=${payload.inputs.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!expectedUserId) {
|
||||
params.ctx.runtime.log?.(
|
||||
`slack:interaction drop modal callback=${callbackId} user=${userId} reason=missing-expected-user`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const auth = await authorizeSlackSystemEventSender({
|
||||
ctx: params.ctx,
|
||||
senderId: userId,
|
||||
channelId: sessionRouting.channelId,
|
||||
channelType: sessionRouting.channelType,
|
||||
expectedSenderId: expectedUserId,
|
||||
});
|
||||
if (!auth.allowed) {
|
||||
params.ctx.runtime.log?.(
|
||||
`slack:interaction drop modal callback=${callbackId} user=${userId} reason=${auth.reason ?? "unauthorized"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
enqueueSystemEvent(params.formatSystemEvent(eventPayload), {
|
||||
sessionKey: sessionRouting.sessionKey,
|
||||
contextKey: [params.contextPrefix, callbackId, viewId, userId].filter(Boolean).join(":"),
|
||||
});
|
||||
}
|
||||
|
||||
export function registerModalLifecycleHandler(params: {
|
||||
register: RegisterSlackModalHandler;
|
||||
matcher: RegExp;
|
||||
ctx: SlackMonitorContext;
|
||||
interactionType: SlackModalInteractionKind;
|
||||
contextPrefix: SlackInteractionContextPrefix;
|
||||
summarizeViewState: (values: unknown) => ModalInputSummary[];
|
||||
formatSystemEvent: (payload: Record<string, unknown>) => string;
|
||||
}) {
|
||||
params.register(params.matcher, async ({ ack, body }: SlackModalEventHandlerArgs) => {
|
||||
await ack();
|
||||
if (params.ctx.shouldDropMismatchedSlackEvent?.(body)) {
|
||||
params.ctx.runtime.log?.(
|
||||
`slack:interaction drop ${params.interactionType} payload (mismatched app/team)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await emitSlackModalLifecycleEvent({
|
||||
ctx: params.ctx,
|
||||
body: body as SlackModalBody,
|
||||
interactionType: params.interactionType,
|
||||
contextPrefix: params.contextPrefix,
|
||||
summarizeViewState: params.summarizeViewState,
|
||||
formatSystemEvent: params.formatSystemEvent,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
import type { SlackActionMiddlewareArgs } from "@slack/bolt";
|
||||
import type { Block, KnownBlock } from "@slack/web-api";
|
||||
import { enqueueSystemEvent } from "../../../infra/system-events.js";
|
||||
import { parseSlackModalPrivateMetadata } from "../../modal-metadata.js";
|
||||
import { authorizeSlackSystemEventSender } from "../auth.js";
|
||||
import type { SlackMonitorContext } from "../context.js";
|
||||
import { escapeSlackMrkdwn } from "../mrkdwn.js";
|
||||
import {
|
||||
registerModalLifecycleHandler,
|
||||
type ModalInputSummary,
|
||||
type RegisterSlackModalHandler,
|
||||
} from "./interactions.modal.js";
|
||||
|
||||
// Prefix for OpenClaw-generated action IDs to scope our handler
|
||||
const OPENCLAW_ACTION_PREFIX = "openclaw:";
|
||||
@@ -68,58 +72,6 @@ type InteractionSummary = InteractionSelectionFields & {
|
||||
threadTs?: string;
|
||||
};
|
||||
|
||||
type ModalInputSummary = InteractionSelectionFields & {
|
||||
blockId: string;
|
||||
actionId: string;
|
||||
};
|
||||
|
||||
type SlackModalBody = {
|
||||
user?: { id?: string };
|
||||
team?: { id?: string };
|
||||
view?: {
|
||||
id?: string;
|
||||
callback_id?: string;
|
||||
private_metadata?: string;
|
||||
root_view_id?: string;
|
||||
previous_view_id?: string;
|
||||
external_id?: string;
|
||||
hash?: string;
|
||||
state?: { values?: unknown };
|
||||
};
|
||||
is_cleared?: boolean;
|
||||
};
|
||||
|
||||
type SlackModalEventBase = {
|
||||
callbackId: string;
|
||||
userId: string;
|
||||
expectedUserId?: string;
|
||||
viewId?: string;
|
||||
sessionRouting: ReturnType<typeof resolveModalSessionRouting>;
|
||||
payload: {
|
||||
actionId: string;
|
||||
callbackId: string;
|
||||
viewId?: string;
|
||||
userId: string;
|
||||
teamId?: string;
|
||||
rootViewId?: string;
|
||||
previousViewId?: string;
|
||||
externalId?: string;
|
||||
viewHash?: string;
|
||||
isStackedView?: boolean;
|
||||
privateMetadata?: string;
|
||||
routedChannelId?: string;
|
||||
routedChannelType?: string;
|
||||
inputs: ModalInputSummary[];
|
||||
};
|
||||
};
|
||||
|
||||
type SlackModalInteractionKind = "view_submission" | "view_closed";
|
||||
type SlackModalEventHandlerArgs = { ack: () => Promise<void>; body: unknown };
|
||||
type RegisterSlackModalHandler = (
|
||||
matcher: RegExp,
|
||||
handler: (args: SlackModalEventHandlerArgs) => Promise<void>,
|
||||
) => void;
|
||||
|
||||
function truncateInteractionString(
|
||||
value: string,
|
||||
max = SLACK_INTERACTION_STRING_MAX_CHARS,
|
||||
@@ -518,182 +470,6 @@ function summarizeViewState(values: unknown): ModalInputSummary[] {
|
||||
return entries;
|
||||
}
|
||||
|
||||
function resolveModalSessionRouting(params: {
|
||||
ctx: SlackMonitorContext;
|
||||
metadata: ReturnType<typeof parseSlackModalPrivateMetadata>;
|
||||
}): { sessionKey: string; channelId?: string; channelType?: string } {
|
||||
const metadata = params.metadata;
|
||||
if (metadata.sessionKey) {
|
||||
return {
|
||||
sessionKey: metadata.sessionKey,
|
||||
channelId: metadata.channelId,
|
||||
channelType: metadata.channelType,
|
||||
};
|
||||
}
|
||||
if (metadata.channelId) {
|
||||
return {
|
||||
sessionKey: params.ctx.resolveSlackSystemEventSessionKey({
|
||||
channelId: metadata.channelId,
|
||||
channelType: metadata.channelType,
|
||||
}),
|
||||
channelId: metadata.channelId,
|
||||
channelType: metadata.channelType,
|
||||
};
|
||||
}
|
||||
return {
|
||||
sessionKey: params.ctx.resolveSlackSystemEventSessionKey({}),
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeSlackViewLifecycleContext(view: {
|
||||
root_view_id?: string;
|
||||
previous_view_id?: string;
|
||||
external_id?: string;
|
||||
hash?: string;
|
||||
}): {
|
||||
rootViewId?: string;
|
||||
previousViewId?: string;
|
||||
externalId?: string;
|
||||
viewHash?: string;
|
||||
isStackedView?: boolean;
|
||||
} {
|
||||
const rootViewId = view.root_view_id;
|
||||
const previousViewId = view.previous_view_id;
|
||||
const externalId = view.external_id;
|
||||
const viewHash = view.hash;
|
||||
return {
|
||||
rootViewId,
|
||||
previousViewId,
|
||||
externalId,
|
||||
viewHash,
|
||||
isStackedView: Boolean(previousViewId),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSlackModalEventBase(params: {
|
||||
ctx: SlackMonitorContext;
|
||||
body: SlackModalBody;
|
||||
}): SlackModalEventBase {
|
||||
const metadata = parseSlackModalPrivateMetadata(params.body.view?.private_metadata);
|
||||
const callbackId = params.body.view?.callback_id ?? "unknown";
|
||||
const userId = params.body.user?.id ?? "unknown";
|
||||
const viewId = params.body.view?.id;
|
||||
const inputs = summarizeViewState(params.body.view?.state?.values);
|
||||
const sessionRouting = resolveModalSessionRouting({
|
||||
ctx: params.ctx,
|
||||
metadata,
|
||||
});
|
||||
return {
|
||||
callbackId,
|
||||
userId,
|
||||
expectedUserId: metadata.userId,
|
||||
viewId,
|
||||
sessionRouting,
|
||||
payload: {
|
||||
actionId: `view:${callbackId}`,
|
||||
callbackId,
|
||||
viewId,
|
||||
userId,
|
||||
teamId: params.body.team?.id,
|
||||
...summarizeSlackViewLifecycleContext({
|
||||
root_view_id: params.body.view?.root_view_id,
|
||||
previous_view_id: params.body.view?.previous_view_id,
|
||||
external_id: params.body.view?.external_id,
|
||||
hash: params.body.view?.hash,
|
||||
}),
|
||||
privateMetadata: params.body.view?.private_metadata,
|
||||
routedChannelId: sessionRouting.channelId,
|
||||
routedChannelType: sessionRouting.channelType,
|
||||
inputs,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function emitSlackModalLifecycleEvent(params: {
|
||||
ctx: SlackMonitorContext;
|
||||
body: SlackModalBody;
|
||||
interactionType: SlackModalInteractionKind;
|
||||
contextPrefix: "slack:interaction:view" | "slack:interaction:view-closed";
|
||||
}): Promise<void> {
|
||||
const { callbackId, userId, expectedUserId, viewId, sessionRouting, payload } =
|
||||
resolveSlackModalEventBase({
|
||||
ctx: params.ctx,
|
||||
body: params.body,
|
||||
});
|
||||
const isViewClosed = params.interactionType === "view_closed";
|
||||
const isCleared = params.body.is_cleared === true;
|
||||
const eventPayload = isViewClosed
|
||||
? {
|
||||
interactionType: params.interactionType,
|
||||
...payload,
|
||||
isCleared,
|
||||
}
|
||||
: {
|
||||
interactionType: params.interactionType,
|
||||
...payload,
|
||||
};
|
||||
|
||||
if (isViewClosed) {
|
||||
params.ctx.runtime.log?.(
|
||||
`slack:interaction view_closed callback=${callbackId} user=${userId} cleared=${isCleared}`,
|
||||
);
|
||||
} else {
|
||||
params.ctx.runtime.log?.(
|
||||
`slack:interaction view_submission callback=${callbackId} user=${userId} inputs=${payload.inputs.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!expectedUserId) {
|
||||
params.ctx.runtime.log?.(
|
||||
`slack:interaction drop modal callback=${callbackId} user=${userId} reason=missing-expected-user`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const auth = await authorizeSlackSystemEventSender({
|
||||
ctx: params.ctx,
|
||||
senderId: userId,
|
||||
channelId: sessionRouting.channelId,
|
||||
channelType: sessionRouting.channelType,
|
||||
expectedSenderId: expectedUserId,
|
||||
});
|
||||
if (!auth.allowed) {
|
||||
params.ctx.runtime.log?.(
|
||||
`slack:interaction drop modal callback=${callbackId} user=${userId} reason=${auth.reason ?? "unauthorized"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
enqueueSystemEvent(formatSlackInteractionSystemEvent(eventPayload), {
|
||||
sessionKey: sessionRouting.sessionKey,
|
||||
contextKey: [params.contextPrefix, callbackId, viewId, userId].filter(Boolean).join(":"),
|
||||
});
|
||||
}
|
||||
|
||||
function registerModalLifecycleHandler(params: {
|
||||
register: RegisterSlackModalHandler;
|
||||
matcher: RegExp;
|
||||
ctx: SlackMonitorContext;
|
||||
interactionType: SlackModalInteractionKind;
|
||||
contextPrefix: "slack:interaction:view" | "slack:interaction:view-closed";
|
||||
}) {
|
||||
params.register(params.matcher, async ({ ack, body }: SlackModalEventHandlerArgs) => {
|
||||
await ack();
|
||||
if (params.ctx.shouldDropMismatchedSlackEvent?.(body)) {
|
||||
params.ctx.runtime.log?.(
|
||||
`slack:interaction drop ${params.interactionType} payload (mismatched app/team)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await emitSlackModalLifecycleEvent({
|
||||
ctx: params.ctx,
|
||||
body: body as SlackModalBody,
|
||||
interactionType: params.interactionType,
|
||||
contextPrefix: params.contextPrefix,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContext }) {
|
||||
const { ctx } = params;
|
||||
if (typeof ctx.app.action !== "function") {
|
||||
@@ -891,6 +667,8 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex
|
||||
ctx,
|
||||
interactionType: "view_submission",
|
||||
contextPrefix: "slack:interaction:view",
|
||||
summarizeViewState,
|
||||
formatSystemEvent: formatSlackInteractionSystemEvent,
|
||||
});
|
||||
|
||||
const viewClosed = (
|
||||
@@ -909,5 +687,7 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex
|
||||
ctx,
|
||||
interactionType: "view_closed",
|
||||
contextPrefix: "slack:interaction:view-closed",
|
||||
summarizeViewState,
|
||||
formatSystemEvent: formatSlackInteractionSystemEvent,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ vi.mock("../../../pairing/pairing-store.js", () => ({
|
||||
}));
|
||||
|
||||
type MessageHandler = (args: { event: Record<string, unknown>; body: unknown }) => Promise<void>;
|
||||
type AppMentionHandler = MessageHandler;
|
||||
|
||||
type MessageCase = {
|
||||
overrides?: SlackSystemEventTestOverrides;
|
||||
@@ -37,6 +38,19 @@ function createMessageHandlers(overrides?: SlackSystemEventTestOverrides) {
|
||||
};
|
||||
}
|
||||
|
||||
function createAppMentionHandlers(overrides?: SlackSystemEventTestOverrides) {
|
||||
const harness = createSlackSystemEventTestHarness(overrides);
|
||||
const handleSlackMessage = vi.fn(async () => {});
|
||||
registerSlackMessageEvents({
|
||||
ctx: harness.ctx,
|
||||
handleSlackMessage,
|
||||
});
|
||||
return {
|
||||
handler: harness.getHandler("app_mention") as AppMentionHandler | null,
|
||||
handleSlackMessage,
|
||||
};
|
||||
}
|
||||
|
||||
function makeChangedEvent(overrides?: { channel?: string; user?: string }) {
|
||||
const user = overrides?.user ?? "U1";
|
||||
return {
|
||||
@@ -214,4 +228,42 @@ describe("registerSlackMessageEvents", () => {
|
||||
expect(handleSlackMessage).not.toHaveBeenCalled();
|
||||
expect(messageQueueMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("skips app_mention events for DM channel ids even with contradictory channel_type", async () => {
|
||||
const { handler, handleSlackMessage } = createAppMentionHandlers({ dmPolicy: "open" });
|
||||
expect(handler).toBeTruthy();
|
||||
|
||||
await handler!({
|
||||
event: {
|
||||
type: "app_mention",
|
||||
channel: "D123",
|
||||
channel_type: "channel",
|
||||
user: "U1",
|
||||
text: "<@U_BOT> hello",
|
||||
ts: "123.456",
|
||||
},
|
||||
body: {},
|
||||
});
|
||||
|
||||
expect(handleSlackMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes app_mention events from channels to the message handler", async () => {
|
||||
const { handler, handleSlackMessage } = createAppMentionHandlers({ dmPolicy: "open" });
|
||||
expect(handler).toBeTruthy();
|
||||
|
||||
await handler!({
|
||||
event: {
|
||||
type: "app_mention",
|
||||
channel: "C123",
|
||||
channel_type: "channel",
|
||||
user: "U1",
|
||||
text: "<@U_BOT> hello",
|
||||
ts: "123.789",
|
||||
},
|
||||
body: {},
|
||||
});
|
||||
|
||||
expect(handleSlackMessage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { SlackEventMiddlewareArgs } from "@slack/bolt";
|
||||
import { danger } from "../../../globals.js";
|
||||
import { enqueueSystemEvent } from "../../../infra/system-events.js";
|
||||
import type { SlackAppMentionEvent, SlackMessageEvent } from "../../types.js";
|
||||
import { normalizeSlackChannelType } from "../channel-type.js";
|
||||
import type { SlackMonitorContext } from "../context.js";
|
||||
import type { SlackMessageHandler } from "../message-handler.js";
|
||||
import { resolveSlackMessageSubtypeHandler } from "./message-subtype-handlers.js";
|
||||
@@ -66,7 +67,7 @@ export function registerSlackMessageEvents(params: {
|
||||
|
||||
// Skip app_mention for DMs - they're already handled by message.im event
|
||||
// This prevents duplicate processing when both message and app_mention fire for DMs
|
||||
const channelType = mention.channel_type;
|
||||
const channelType = normalizeSlackChannelType(mention.channel_type, mention.channel);
|
||||
if (channelType === "im" || channelType === "mpim") {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user