Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
527
extensions/slack/src/approval-handler.runtime.ts
Normal file
527
extensions/slack/src/approval-handler.runtime.ts
Normal file
@@ -0,0 +1,527 @@
|
||||
// Slack plugin module implements approval handler behavior.
|
||||
import type { App } from "@slack/bolt";
|
||||
import type { Block, KnownBlock } from "@slack/web-api";
|
||||
import type {
|
||||
ChannelApprovalCapabilityHandlerContext,
|
||||
ExecApprovalExpiredView,
|
||||
ExecApprovalPendingView,
|
||||
ExecApprovalResolvedView,
|
||||
ExpiredApprovalView,
|
||||
PendingApprovalView,
|
||||
PluginApprovalExpiredView,
|
||||
PluginApprovalPendingView,
|
||||
PluginApprovalResolvedView,
|
||||
ResolvedApprovalView,
|
||||
} from "openclaw/plugin-sdk/approval-handler-runtime";
|
||||
import { createChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-runtime";
|
||||
import { buildChannelApprovalNativeTargetKey } from "openclaw/plugin-sdk/approval-native-runtime";
|
||||
import { buildApprovalPresentationFromActionDescriptors } from "openclaw/plugin-sdk/approval-reply-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { logError } from "openclaw/plugin-sdk/logging-core";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import {
|
||||
isSlackAnyNativeApprovalClientEnabled,
|
||||
resolveSlackApprovalKind,
|
||||
shouldHandleSlackNativeApprovalRequest,
|
||||
} from "./approval-native-gates.js";
|
||||
import { normalizeSlackApproverId } from "./exec-approvals.js";
|
||||
import { resolveSlackReplyBlocks } from "./reply-blocks.js";
|
||||
import { sendMessageSlack } from "./send.js";
|
||||
import { truncateSlackText } from "./truncate.js";
|
||||
|
||||
type SlackBlock = Block | KnownBlock;
|
||||
type SlackPendingApproval = {
|
||||
channelId: string;
|
||||
messageTs: string;
|
||||
};
|
||||
type SlackPendingDelivery = {
|
||||
text: string;
|
||||
blocks: SlackBlock[];
|
||||
};
|
||||
type SlackMetadataItem = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
type SlackPluginApprovalView =
|
||||
| PluginApprovalPendingView
|
||||
| PluginApprovalResolvedView
|
||||
| PluginApprovalExpiredView;
|
||||
|
||||
const SLACK_CONTEXT_ELEMENTS_MAX = 10;
|
||||
const SLACK_CHAT_UPDATE_TEXT_LIMIT = 4000;
|
||||
const SLACK_TEXT_OBJECT_MAX = 3000;
|
||||
|
||||
type SlackExecApprovalConfig = NonNullable<
|
||||
NonNullable<NonNullable<OpenClawConfig["channels"]>["slack"]>["execApprovals"]
|
||||
>;
|
||||
|
||||
export type SlackApprovalHandlerContext = {
|
||||
app: App;
|
||||
config: SlackExecApprovalConfig;
|
||||
};
|
||||
|
||||
function resolveHandlerContext(params: ChannelApprovalCapabilityHandlerContext): {
|
||||
accountId: string;
|
||||
context: SlackApprovalHandlerContext;
|
||||
} | null {
|
||||
const context = params.context as SlackApprovalHandlerContext | undefined;
|
||||
const accountId = normalizeOptionalString(params.accountId) ?? "";
|
||||
if (!context?.app || !accountId) {
|
||||
return null;
|
||||
}
|
||||
return { accountId, context };
|
||||
}
|
||||
|
||||
function truncateSlackMrkdwn(text: string, maxChars: number): string {
|
||||
const limit = Math.max(0, Math.floor(maxChars));
|
||||
if (text.length <= limit) {
|
||||
return text;
|
||||
}
|
||||
if (limit <= 1) {
|
||||
return truncateUtf16Safe(text, limit);
|
||||
}
|
||||
return `${truncateUtf16Safe(text, limit - 1)}…`;
|
||||
}
|
||||
|
||||
function buildSlackCodeBlock(text: string): string {
|
||||
let fence = "```";
|
||||
while (text.includes(fence)) {
|
||||
fence += "`";
|
||||
}
|
||||
return `${fence}\n${text}\n${fence}`;
|
||||
}
|
||||
|
||||
function formatSlackApprover(resolvedBy?: string | null): string | null {
|
||||
const normalized = resolvedBy ? normalizeSlackApproverId(resolvedBy) : undefined;
|
||||
if (normalized) {
|
||||
return `<@${normalized}>`;
|
||||
}
|
||||
const trimmed = normalizeOptionalString(resolvedBy);
|
||||
return trimmed ? trimmed : null;
|
||||
}
|
||||
|
||||
function formatSlackMetadataLine(label: string, value: string): string {
|
||||
return `*${label}:* ${value}`;
|
||||
}
|
||||
|
||||
function buildSlackMetadataLines(metadata: readonly SlackMetadataItem[]): string[] {
|
||||
const lines: string[] = [];
|
||||
for (const item of metadata) {
|
||||
lines.push(formatSlackMetadataLine(item.label, item.value));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
function buildSlackMetadataContextElements(metadata: readonly SlackMetadataItem[]) {
|
||||
const lines = buildSlackMetadataLines(metadata);
|
||||
const visibleLineCount =
|
||||
lines.length > SLACK_CONTEXT_ELEMENTS_MAX ? SLACK_CONTEXT_ELEMENTS_MAX - 1 : lines.length;
|
||||
const elements: Array<{ type: "mrkdwn"; text: string }> = [];
|
||||
for (let index = 0; index < visibleLineCount; index += 1) {
|
||||
const line = lines[index];
|
||||
if (line === undefined) {
|
||||
continue;
|
||||
}
|
||||
elements.push({
|
||||
type: "mrkdwn",
|
||||
text: truncateSlackMrkdwn(line, SLACK_TEXT_OBJECT_MAX),
|
||||
});
|
||||
}
|
||||
if (lines.length > SLACK_CONTEXT_ELEMENTS_MAX) {
|
||||
elements.push({
|
||||
type: "mrkdwn",
|
||||
text: `…+${lines.length - visibleLineCount} more`,
|
||||
});
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
function buildSlackMetadataContextBlocks(metadata: readonly SlackMetadataItem[]): SlackBlock[] {
|
||||
const metadataElements = buildSlackMetadataContextElements(metadata);
|
||||
return metadataElements.length > 0
|
||||
? [
|
||||
{
|
||||
type: "context",
|
||||
elements: metadataElements,
|
||||
} satisfies SlackBlock,
|
||||
]
|
||||
: [];
|
||||
}
|
||||
|
||||
function resolveSlackApprovalDecisionLabel(
|
||||
decision: "allow-once" | "allow-always" | "deny",
|
||||
): string {
|
||||
return decision === "allow-once"
|
||||
? "Allowed once"
|
||||
: decision === "allow-always"
|
||||
? "Allowed always"
|
||||
: "Denied";
|
||||
}
|
||||
|
||||
function buildSlackPluginMetadata(view: SlackPluginApprovalView): SlackMetadataItem[] {
|
||||
return [{ label: "Approval ID", value: view.approvalId }, ...view.metadata];
|
||||
}
|
||||
|
||||
function resolveSlackPluginDescription(view: SlackPluginApprovalView): string {
|
||||
return normalizeOptionalString(view.description) ?? "A plugin action needs your approval.";
|
||||
}
|
||||
|
||||
function buildSlackPluginRequestBlocks(view: SlackPluginApprovalView): SlackBlock[] {
|
||||
return [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `*Request*\n${truncateSlackMrkdwn(view.title, 2600)}`,
|
||||
},
|
||||
},
|
||||
...buildSlackMetadataContextBlocks(buildSlackPluginMetadata(view)),
|
||||
];
|
||||
}
|
||||
|
||||
function buildSlackExecPendingApprovalText(view: ExecApprovalPendingView): string {
|
||||
const metadataLines = buildSlackMetadataLines(view.metadata);
|
||||
const lines = [
|
||||
"*Exec approval required*",
|
||||
"A command needs your approval.",
|
||||
"",
|
||||
"*Command*",
|
||||
buildSlackCodeBlock(view.commandText),
|
||||
...metadataLines,
|
||||
];
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function buildSlackPluginPendingApprovalText(view: PluginApprovalPendingView): string {
|
||||
const metadataLines = buildSlackMetadataLines(buildSlackPluginMetadata(view));
|
||||
const lines = [
|
||||
"*Plugin approval required*",
|
||||
resolveSlackPluginDescription(view),
|
||||
"",
|
||||
"*Request*",
|
||||
view.title,
|
||||
...metadataLines,
|
||||
];
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function buildSlackPendingApprovalText(view: PendingApprovalView): string {
|
||||
return view.approvalKind === "plugin"
|
||||
? buildSlackPluginPendingApprovalText(view)
|
||||
: buildSlackExecPendingApprovalText(view);
|
||||
}
|
||||
|
||||
function buildSlackExecPendingApprovalBlocks(view: ExecApprovalPendingView): SlackBlock[] {
|
||||
const interactiveBlocks =
|
||||
resolveSlackReplyBlocks({
|
||||
text: "",
|
||||
presentation: buildApprovalPresentationFromActionDescriptors(view.actions),
|
||||
}) ?? [];
|
||||
return [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: "*Exec approval required*\nA command needs your approval.",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `*Command*\n${buildSlackCodeBlock(truncateSlackMrkdwn(view.commandText, 2600))}`,
|
||||
},
|
||||
},
|
||||
...buildSlackMetadataContextBlocks(view.metadata),
|
||||
...interactiveBlocks,
|
||||
];
|
||||
}
|
||||
|
||||
function buildSlackPluginPendingApprovalBlocks(view: PluginApprovalPendingView): SlackBlock[] {
|
||||
const interactiveBlocks =
|
||||
resolveSlackReplyBlocks({
|
||||
text: "",
|
||||
presentation: buildApprovalPresentationFromActionDescriptors(view.actions),
|
||||
}) ?? [];
|
||||
return [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `*Plugin approval required*\n${truncateSlackMrkdwn(
|
||||
resolveSlackPluginDescription(view),
|
||||
2600,
|
||||
)}`,
|
||||
},
|
||||
},
|
||||
...buildSlackPluginRequestBlocks(view),
|
||||
...interactiveBlocks,
|
||||
];
|
||||
}
|
||||
|
||||
function buildSlackPendingApprovalBlocks(view: PendingApprovalView): SlackBlock[] {
|
||||
return view.approvalKind === "plugin"
|
||||
? buildSlackPluginPendingApprovalBlocks(view)
|
||||
: buildSlackExecPendingApprovalBlocks(view);
|
||||
}
|
||||
|
||||
function buildSlackExecResolvedText(view: ExecApprovalResolvedView): string {
|
||||
const resolvedBy = formatSlackApprover(view.resolvedBy);
|
||||
const lines = [
|
||||
`*Exec approval: ${resolveSlackApprovalDecisionLabel(view.decision)}*`,
|
||||
resolvedBy ? `Resolved by ${resolvedBy}.` : "Resolved.",
|
||||
"",
|
||||
"*Command*",
|
||||
buildSlackCodeBlock(view.commandText),
|
||||
];
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function buildSlackPluginResolvedText(view: PluginApprovalResolvedView): string {
|
||||
const resolvedBy = formatSlackApprover(view.resolvedBy);
|
||||
const metadataLines = buildSlackMetadataLines(buildSlackPluginMetadata(view));
|
||||
const lines = [
|
||||
`*Plugin approval: ${resolveSlackApprovalDecisionLabel(view.decision)}*`,
|
||||
resolvedBy ? `Resolved by ${resolvedBy}.` : "Resolved.",
|
||||
"",
|
||||
"*Request*",
|
||||
view.title,
|
||||
...metadataLines,
|
||||
];
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function buildSlackResolvedText(view: ResolvedApprovalView): string {
|
||||
return view.approvalKind === "plugin"
|
||||
? buildSlackPluginResolvedText(view)
|
||||
: buildSlackExecResolvedText(view);
|
||||
}
|
||||
|
||||
function buildSlackExecResolvedBlocks(view: ExecApprovalResolvedView): SlackBlock[] {
|
||||
const resolvedBy = formatSlackApprover(view.resolvedBy);
|
||||
return [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `*Exec approval: ${resolveSlackApprovalDecisionLabel(view.decision)}*\n${
|
||||
resolvedBy ? `Resolved by ${resolvedBy}.` : "Resolved."
|
||||
}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `*Command*\n${buildSlackCodeBlock(truncateSlackMrkdwn(view.commandText, 2600))}`,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function buildSlackPluginResolvedBlocks(view: PluginApprovalResolvedView): SlackBlock[] {
|
||||
const resolvedBy = formatSlackApprover(view.resolvedBy);
|
||||
return [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `*Plugin approval: ${resolveSlackApprovalDecisionLabel(view.decision)}*\n${
|
||||
resolvedBy ? `Resolved by ${resolvedBy}.` : "Resolved."
|
||||
}`,
|
||||
},
|
||||
},
|
||||
...buildSlackPluginRequestBlocks(view),
|
||||
];
|
||||
}
|
||||
|
||||
function buildSlackResolvedBlocks(view: ResolvedApprovalView): SlackBlock[] {
|
||||
return view.approvalKind === "plugin"
|
||||
? buildSlackPluginResolvedBlocks(view)
|
||||
: buildSlackExecResolvedBlocks(view);
|
||||
}
|
||||
|
||||
function buildSlackExecExpiredText(view: ExecApprovalExpiredView): string {
|
||||
return [
|
||||
"*Exec approval expired*",
|
||||
"This approval request expired before it was resolved.",
|
||||
"",
|
||||
"*Command*",
|
||||
buildSlackCodeBlock(view.commandText),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function buildSlackPluginExpiredText(view: PluginApprovalExpiredView): string {
|
||||
const metadataLines = buildSlackMetadataLines(buildSlackPluginMetadata(view));
|
||||
return [
|
||||
"*Plugin approval expired*",
|
||||
"This approval request expired before it was resolved.",
|
||||
"",
|
||||
"*Request*",
|
||||
view.title,
|
||||
...metadataLines,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function buildSlackExpiredText(view: ExpiredApprovalView): string {
|
||||
return view.approvalKind === "plugin"
|
||||
? buildSlackPluginExpiredText(view)
|
||||
: buildSlackExecExpiredText(view);
|
||||
}
|
||||
|
||||
function buildSlackExecExpiredBlocks(view: ExecApprovalExpiredView): SlackBlock[] {
|
||||
return [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: "*Exec approval expired*\nThis approval request expired before it was resolved.",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `*Command*\n${buildSlackCodeBlock(truncateSlackMrkdwn(view.commandText, 2600))}`,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function buildSlackPluginExpiredBlocks(view: PluginApprovalExpiredView): SlackBlock[] {
|
||||
return [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: "*Plugin approval expired*\nThis approval request expired before it was resolved.",
|
||||
},
|
||||
},
|
||||
...buildSlackPluginRequestBlocks(view),
|
||||
];
|
||||
}
|
||||
|
||||
function buildSlackExpiredBlocks(view: ExpiredApprovalView): SlackBlock[] {
|
||||
return view.approvalKind === "plugin"
|
||||
? buildSlackPluginExpiredBlocks(view)
|
||||
: buildSlackExecExpiredBlocks(view);
|
||||
}
|
||||
|
||||
async function updateMessage(params: {
|
||||
app: App;
|
||||
channelId: string;
|
||||
messageTs: string;
|
||||
text: string;
|
||||
blocks: SlackBlock[];
|
||||
}): Promise<void> {
|
||||
try {
|
||||
await params.app.client.chat.update({
|
||||
channel: params.channelId,
|
||||
ts: params.messageTs,
|
||||
text: truncateSlackText(params.text, SLACK_CHAT_UPDATE_TEXT_LIMIT),
|
||||
blocks: params.blocks,
|
||||
});
|
||||
} catch (err) {
|
||||
logError(`slack approvals: failed to update message: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const slackApprovalNativeRuntime = createChannelApprovalNativeRuntimeAdapter<
|
||||
SlackPendingDelivery,
|
||||
{ to: string; threadTs?: string },
|
||||
SlackPendingApproval,
|
||||
never,
|
||||
SlackPendingDelivery
|
||||
>({
|
||||
eventKinds: ["exec", "plugin"],
|
||||
availability: {
|
||||
isConfigured: (params) => {
|
||||
const resolved = resolveHandlerContext(params);
|
||||
return resolved
|
||||
? isSlackAnyNativeApprovalClientEnabled({
|
||||
cfg: params.cfg,
|
||||
accountId: resolved.accountId,
|
||||
})
|
||||
: false;
|
||||
},
|
||||
shouldHandle: (params) => {
|
||||
const resolved = resolveHandlerContext(params);
|
||||
if (!resolved) {
|
||||
return false;
|
||||
}
|
||||
return shouldHandleSlackNativeApprovalRequest({
|
||||
cfg: params.cfg,
|
||||
accountId: resolved.accountId,
|
||||
approvalKind: resolveSlackApprovalKind(params.request),
|
||||
request: params.request,
|
||||
});
|
||||
},
|
||||
},
|
||||
presentation: {
|
||||
buildPendingPayload: ({ view }) => ({
|
||||
text: buildSlackPendingApprovalText(view),
|
||||
blocks: buildSlackPendingApprovalBlocks(view),
|
||||
}),
|
||||
buildResolvedResult: ({ view }) => ({
|
||||
kind: "update",
|
||||
payload: {
|
||||
text: buildSlackResolvedText(view),
|
||||
blocks: buildSlackResolvedBlocks(view),
|
||||
},
|
||||
}),
|
||||
buildExpiredResult: ({ view }) => ({
|
||||
kind: "update",
|
||||
payload: {
|
||||
text: buildSlackExpiredText(view),
|
||||
blocks: buildSlackExpiredBlocks(view),
|
||||
},
|
||||
}),
|
||||
},
|
||||
transport: {
|
||||
prepareTarget: ({ plannedTarget }) => ({
|
||||
dedupeKey: buildChannelApprovalNativeTargetKey(plannedTarget.target),
|
||||
target: {
|
||||
to: plannedTarget.target.to,
|
||||
threadTs:
|
||||
plannedTarget.target.threadId != null ? String(plannedTarget.target.threadId) : undefined,
|
||||
},
|
||||
}),
|
||||
deliverPending: async ({ cfg, accountId, context, preparedTarget, pendingPayload }) => {
|
||||
const resolved = resolveHandlerContext({ cfg, accountId, context });
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
const message = await sendMessageSlack(preparedTarget.to, pendingPayload.text, {
|
||||
cfg,
|
||||
accountId: resolved.accountId,
|
||||
threadTs: preparedTarget.threadTs,
|
||||
blocks: pendingPayload.blocks,
|
||||
client: resolved.context.app.client,
|
||||
});
|
||||
return {
|
||||
channelId: message.channelId,
|
||||
messageTs: message.messageId,
|
||||
};
|
||||
},
|
||||
updateEntry: async ({ cfg, accountId, context, entry, payload }) => {
|
||||
const resolved = resolveHandlerContext({ cfg, accountId, context });
|
||||
if (!resolved) {
|
||||
return;
|
||||
}
|
||||
const nextPayload = payload;
|
||||
await updateMessage({
|
||||
app: resolved.context.app,
|
||||
channelId: entry.channelId,
|
||||
messageTs: entry.messageTs,
|
||||
text: nextPayload.text,
|
||||
blocks: nextPayload.blocks,
|
||||
});
|
||||
},
|
||||
},
|
||||
observe: {
|
||||
onDeliveryError: ({ error, request }) => {
|
||||
logError(`slack approvals: failed to deliver approval ${request.id}: ${String(error)}`);
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user