@opentag/slack 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +2 -1
- package/dist/dispatcher-events.d.ts +7 -0
- package/dist/dispatcher-events.d.ts.map +1 -0
- package/dist/events.d.ts +107 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +860 -16
- package/dist/index.js.map +1 -1
- package/dist/ingress.d.ts +43 -0
- package/dist/ingress.d.ts.map +1 -0
- package/dist/normalize.d.ts +3 -0
- package/dist/normalize.d.ts.map +1 -1
- package/dist/render.d.ts +72 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/socket-mode.d.ts +33 -0
- package/dist/socket-mode.d.ts.map +1 -0
- package/package.json +11 -3
package/dist/index.js
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
|
+
// src/events.ts
|
|
2
|
+
import { parseThreadActionCommand } from "@opentag/core";
|
|
3
|
+
|
|
1
4
|
// src/normalize.ts
|
|
2
5
|
import { commandFromRawText } from "@opentag/core";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
var LEADING_MENTION_RUN = /^(?:<@[^>]+>\s*)+/;
|
|
7
|
+
var MENTION_TOKEN = /<@([^>]+)>/g;
|
|
8
|
+
function stripSlackAppMention(text2, botUserId) {
|
|
9
|
+
const run = text2.match(LEADING_MENTION_RUN);
|
|
10
|
+
if (!run) return null;
|
|
11
|
+
const leadingRun = run[0];
|
|
12
|
+
if (botUserId) {
|
|
13
|
+
const mentionedIds = leadingRun.match(MENTION_TOKEN) ?? [];
|
|
14
|
+
const botIsMentioned = mentionedIds.some((token) => {
|
|
15
|
+
const id = token.slice(2, -1).split("|")[0] ?? "";
|
|
16
|
+
return id.toLowerCase() === botUserId.toLowerCase();
|
|
17
|
+
});
|
|
18
|
+
if (!botIsMentioned) return null;
|
|
10
19
|
}
|
|
11
|
-
|
|
20
|
+
const stripped = text2.slice(leadingRun.length).trim();
|
|
21
|
+
return stripped.length > 0 ? stripped : null;
|
|
12
22
|
}
|
|
13
23
|
function encodeSlackThreadKey(input) {
|
|
14
24
|
return `${input.teamId}|${input.channelId}|${input.threadTs}`;
|
|
@@ -20,18 +30,27 @@ function parseSlackThreadKey(threadKey) {
|
|
|
20
30
|
}
|
|
21
31
|
return { teamId, channelId, threadTs };
|
|
22
32
|
}
|
|
23
|
-
|
|
33
|
+
var UNKNOWN_WRITE_VERB_PATTERN = /\b(add|append|apply|change|commit|create|delete|edit|fix|modify|open\s+a?\s*pr|pull\s+request|remove|update|write)\b/i;
|
|
34
|
+
var REPO_WRITE_TARGET_PATTERN = /\b(repo|repository|code|file|files|branch|commit|diff|patch|readme|pr|pull\s+request|package\.json|pnpm|npm|test|build)\b|(?:^|\s)[./\w-]+\.(?:cjs|css|gitignore|go|html|js|json|jsx|lock|md|mjs|py|rb|rs|sh|toml|ts|tsx|txt|yaml|yml)\b|(?:^|[\s`'"(])(?:[./\w-]+\/)?(?:Dockerfile|Makefile|Procfile|Rakefile|Gemfile|Brewfile|Justfile|Taskfile|\.dockerignore|\.env(?:\.[\w-]+)?|\.gitignore|\.npmrc)(?=$|[\s`'",.):])/i;
|
|
35
|
+
function commandLooksRepoWriteCapable(command) {
|
|
36
|
+
return UNKNOWN_WRITE_VERB_PATTERN.test(command.rawText) && REPO_WRITE_TARGET_PATTERN.test(command.rawText);
|
|
37
|
+
}
|
|
38
|
+
function permissionsForCommand(command) {
|
|
24
39
|
const permissions = [
|
|
25
40
|
{
|
|
26
41
|
scope: "chat:postMessage",
|
|
27
42
|
reason: "reply in the originating Slack thread"
|
|
28
43
|
},
|
|
44
|
+
{
|
|
45
|
+
scope: "reactions:write",
|
|
46
|
+
reason: "mark the originating Slack message as received without posting a thread reply"
|
|
47
|
+
},
|
|
29
48
|
{
|
|
30
49
|
scope: "runner:local",
|
|
31
50
|
reason: "execute the run on a paired local daemon"
|
|
32
51
|
}
|
|
33
52
|
];
|
|
34
|
-
if (intent === "fix" || intent === "run") {
|
|
53
|
+
if (command.intent === "fix" || command.intent === "run" || command.intent === "unknown" && commandLooksRepoWriteCapable(command)) {
|
|
35
54
|
permissions.push(
|
|
36
55
|
{
|
|
37
56
|
scope: "repo:read",
|
|
@@ -49,11 +68,50 @@ function permissionsForIntent(intent) {
|
|
|
49
68
|
}
|
|
50
69
|
return permissions;
|
|
51
70
|
}
|
|
71
|
+
function contextPointersForCommand(command) {
|
|
72
|
+
const context = [];
|
|
73
|
+
for (const reference of command.parsed?.references ?? []) {
|
|
74
|
+
if (reference.kind === "url") {
|
|
75
|
+
context.push({
|
|
76
|
+
kind: "url",
|
|
77
|
+
uri: reference.uri,
|
|
78
|
+
visibility: "organization",
|
|
79
|
+
title: reference.title ?? "Command URL reference"
|
|
80
|
+
});
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (reference.kind === "file" || reference.kind === "path") {
|
|
84
|
+
context.push({
|
|
85
|
+
kind: "file",
|
|
86
|
+
uri: reference.uri,
|
|
87
|
+
...reference.line ? { line: reference.line } : {},
|
|
88
|
+
...reference.startLine ? { startLine: reference.startLine } : {},
|
|
89
|
+
...reference.endLine ? { endLine: reference.endLine } : {},
|
|
90
|
+
visibility: "organization",
|
|
91
|
+
title: referenceTitle(reference)
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return context;
|
|
96
|
+
}
|
|
97
|
+
function referenceTitle(reference) {
|
|
98
|
+
return reference.title ?? "Command file reference";
|
|
99
|
+
}
|
|
100
|
+
function commandMetadata(command) {
|
|
101
|
+
if (!command.parsed) return {};
|
|
102
|
+
return {
|
|
103
|
+
commandParser: command.parsed.version,
|
|
104
|
+
commandDiagnostics: command.parsed.diagnostics,
|
|
105
|
+
...command.parsed.approval ? { approval: command.parsed.approval } : {},
|
|
106
|
+
...command.parsed.network ? { network: command.parsed.network } : {}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
52
109
|
function normalizeSlackAppMention(input) {
|
|
53
110
|
const rawText = stripSlackAppMention(input.text, input.botUserId);
|
|
54
111
|
if (!rawText) return null;
|
|
55
112
|
const command = commandFromRawText(rawText);
|
|
56
113
|
const replyThreadTs = input.threadTs ?? input.ts;
|
|
114
|
+
const agentId = input.agentId ?? "opentag";
|
|
57
115
|
return {
|
|
58
116
|
id: `evt_slack_app_mention_${input.eventId}`,
|
|
59
117
|
source: "slack",
|
|
@@ -67,12 +125,14 @@ function normalizeSlackAppMention(input) {
|
|
|
67
125
|
},
|
|
68
126
|
target: {
|
|
69
127
|
mention: input.botUserId ? `<@${input.botUserId}>` : "<@app>",
|
|
70
|
-
agentId
|
|
128
|
+
agentId,
|
|
129
|
+
...command.parsed?.executorHint ? { executorHint: command.parsed.executorHint } : {}
|
|
71
130
|
},
|
|
72
131
|
command,
|
|
73
132
|
context: [
|
|
74
133
|
{
|
|
75
|
-
|
|
134
|
+
provider: "slack",
|
|
135
|
+
kind: "message",
|
|
76
136
|
uri: `slack://team/${input.teamId}/channel/${input.channelId}/message/${input.ts}`,
|
|
77
137
|
visibility: "organization",
|
|
78
138
|
title: "Slack message"
|
|
@@ -82,9 +142,10 @@ function normalizeSlackAppMention(input) {
|
|
|
82
142
|
uri: input.text,
|
|
83
143
|
visibility: "organization",
|
|
84
144
|
title: "Slack message text"
|
|
85
|
-
}
|
|
145
|
+
},
|
|
146
|
+
...contextPointersForCommand(command)
|
|
86
147
|
],
|
|
87
|
-
permissions:
|
|
148
|
+
permissions: permissionsForCommand(command),
|
|
88
149
|
callback: {
|
|
89
150
|
provider: "slack",
|
|
90
151
|
uri: input.callbackUri ?? "https://slack.com/api/chat.postMessage",
|
|
@@ -98,16 +159,799 @@ function normalizeSlackAppMention(input) {
|
|
|
98
159
|
teamId: input.teamId,
|
|
99
160
|
channelId: input.channelId,
|
|
100
161
|
messageTs: input.ts,
|
|
101
|
-
|
|
162
|
+
...input.appId ? { slackAppId: input.appId } : {},
|
|
163
|
+
...input.botUserId ? { slackBotUserId: input.botUserId } : {},
|
|
164
|
+
...commandMetadata(command),
|
|
165
|
+
repoProvider: input.binding.repoProvider ?? "github",
|
|
102
166
|
owner: input.binding.owner,
|
|
103
167
|
repo: input.binding.repo
|
|
104
168
|
}
|
|
105
169
|
};
|
|
106
170
|
}
|
|
171
|
+
|
|
172
|
+
// src/render.ts
|
|
173
|
+
import { suggestedActionCandidatesFromResult } from "@opentag/core";
|
|
174
|
+
var MAX_SLACK_SUGGESTED_ACTION_CANDIDATES = 20;
|
|
175
|
+
function buildSlackSuggestedActionButtonValue(input) {
|
|
176
|
+
return JSON.stringify(input);
|
|
177
|
+
}
|
|
178
|
+
function parseSlackSuggestedActionButtonValue(value) {
|
|
179
|
+
try {
|
|
180
|
+
const parsed = JSON.parse(value);
|
|
181
|
+
if (parsed.version !== 1 || typeof parsed.command !== "string" || parsed.command.trim().length === 0 || typeof parsed.proposalId !== "string" || parsed.proposalId.length === 0 || typeof parsed.intentId !== "string" || parsed.intentId.length === 0) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
version: 1,
|
|
186
|
+
command: parsed.command.trim(),
|
|
187
|
+
proposalId: parsed.proposalId,
|
|
188
|
+
intentId: parsed.intentId
|
|
189
|
+
};
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function escapeSlackText(text2) {
|
|
195
|
+
return text2.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
196
|
+
}
|
|
197
|
+
function markdownToSlackMrkdwn(text2) {
|
|
198
|
+
const links = [];
|
|
199
|
+
const withoutLinks = text2.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, label, url) => {
|
|
200
|
+
const token = `\0SLACK_LINK_${links.length}\0`;
|
|
201
|
+
links.push(`<${url}|${escapeSlackText(label)}>`);
|
|
202
|
+
return token;
|
|
203
|
+
});
|
|
204
|
+
const converted = escapeSlackText(withoutLinks).replace(/\*\*(.+?)\*\*/g, "*$1*").replace(/__(.+?)__/g, "*$1*");
|
|
205
|
+
return links.reduce((output, link, index) => output.replace(`\0SLACK_LINK_${index}\0`, link), converted);
|
|
206
|
+
}
|
|
207
|
+
function renderSlackAcknowledgement(runId) {
|
|
208
|
+
void runId;
|
|
209
|
+
return "Working on it.";
|
|
210
|
+
}
|
|
211
|
+
function slackSourceReceiptReactionName(state) {
|
|
212
|
+
if (state === "received") return "eyes";
|
|
213
|
+
return "eyes";
|
|
214
|
+
}
|
|
215
|
+
function createSlackReactionPayload(input) {
|
|
216
|
+
return {
|
|
217
|
+
channel: input.channelId,
|
|
218
|
+
timestamp: input.messageTs,
|
|
219
|
+
name: input.name
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function nextActionSummary(result) {
|
|
223
|
+
if (!result.nextAction) return void 0;
|
|
224
|
+
if (typeof result.nextAction === "string") return result.nextAction;
|
|
225
|
+
return result.nextAction.summary;
|
|
226
|
+
}
|
|
227
|
+
function stringParam(params, key) {
|
|
228
|
+
const value = params?.[key];
|
|
229
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
230
|
+
}
|
|
231
|
+
function stringArrayParam(params, key) {
|
|
232
|
+
const value = params?.[key];
|
|
233
|
+
if (!Array.isArray(value)) return [];
|
|
234
|
+
return value.filter((item) => typeof item === "string" && item.length > 0);
|
|
235
|
+
}
|
|
236
|
+
function renderVerificationParams(params) {
|
|
237
|
+
const value = params?.["verification"];
|
|
238
|
+
if (!Array.isArray(value)) return [];
|
|
239
|
+
return value.map((item) => {
|
|
240
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return void 0;
|
|
241
|
+
const command = item["command"];
|
|
242
|
+
const outcome = item["outcome"];
|
|
243
|
+
return typeof command === "string" && typeof outcome === "string" ? ` - \`${command}\`: ${outcome}` : void 0;
|
|
244
|
+
}).filter((line) => Boolean(line));
|
|
245
|
+
}
|
|
246
|
+
function renderSuggestedActionDetails(params, action) {
|
|
247
|
+
if (action !== "create_pull_request") return [];
|
|
248
|
+
const lines = [];
|
|
249
|
+
const title = stringParam(params, "title");
|
|
250
|
+
const head = stringParam(params, "head") ?? stringParam(params, "branch");
|
|
251
|
+
const base = stringParam(params, "base") ?? stringParam(params, "baseBranch");
|
|
252
|
+
const changedFiles = stringArrayParam(params, "changedFiles");
|
|
253
|
+
const risks = stringArrayParam(params, "risks");
|
|
254
|
+
const verification = renderVerificationParams(params);
|
|
255
|
+
if (title) lines.push(` Title: ${markdownToSlackMrkdwn(title)}`);
|
|
256
|
+
if (head || base) lines.push(` Branch: \`${head ?? "unknown"}\` -> \`${base ?? "main"}\``);
|
|
257
|
+
if (changedFiles.length > 0) lines.push(` Changed files: ${changedFiles.map((file) => `\`${file}\``).join(", ")}`);
|
|
258
|
+
if (risks.length > 0) {
|
|
259
|
+
lines.push(" Risks:");
|
|
260
|
+
for (const risk of risks) {
|
|
261
|
+
lines.push(` - ${markdownToSlackMrkdwn(risk)}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (verification.length > 0) {
|
|
265
|
+
lines.push(" Verification:");
|
|
266
|
+
lines.push(...verification);
|
|
267
|
+
}
|
|
268
|
+
return lines;
|
|
269
|
+
}
|
|
270
|
+
function truncateSlackText(text2, maxLength) {
|
|
271
|
+
const normalized = text2.replace(/\s+/g, " ").trim();
|
|
272
|
+
if (normalized.length <= maxLength) return normalized;
|
|
273
|
+
return `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}\u2026`;
|
|
274
|
+
}
|
|
275
|
+
function firstMarkdownSection(text2, heading) {
|
|
276
|
+
const pattern = new RegExp(`\\*\\*${heading}:\\*\\*\\s*([\\s\\S]*?)(?=\\n\\s*\\n\\*\\*[^*]+:\\*\\*|\\n\\s*\\n[A-Z][^\\n]{0,60}:|$)`, "i");
|
|
277
|
+
const match = text2.match(pattern);
|
|
278
|
+
return match?.[1]?.trim();
|
|
279
|
+
}
|
|
280
|
+
function compactSlackSummary(summary) {
|
|
281
|
+
const whatChanged = firstMarkdownSection(summary, "What changed");
|
|
282
|
+
const firstParagraph = summary.split(/\n\s*\n/).map((part) => part.trim()).find(Boolean);
|
|
283
|
+
const selected = whatChanged ?? firstParagraph ?? summary;
|
|
284
|
+
return truncateSlackText(selected.replace(/^\*\*[^*]+:\*\*\s*/i, ""), 360);
|
|
285
|
+
}
|
|
286
|
+
function compactNextAction(nextAction) {
|
|
287
|
+
return truncateSlackText(nextAction, 180);
|
|
288
|
+
}
|
|
289
|
+
function renderSuggestedActionCandidateLines(candidate) {
|
|
290
|
+
const lines = [`${candidate.index}. *${markdownToSlackMrkdwn(candidate.intent.summary)}*`];
|
|
291
|
+
const details = renderSuggestedActionDetails(candidate.intent.params, candidate.intent.action).filter((line) => line.trim().startsWith("Branch:") || line.trim().startsWith("Changed files:")).map((line) => line.replace(/^\s+/, ""));
|
|
292
|
+
lines.push(...details);
|
|
293
|
+
if (candidate.proposalPreconditions?.length) {
|
|
294
|
+
lines.push(`Preconditions: ${candidate.proposalPreconditions.length} check(s) in the audit log.`);
|
|
295
|
+
}
|
|
296
|
+
return lines;
|
|
297
|
+
}
|
|
298
|
+
function renderSuggestedActionsMarkdown(result) {
|
|
299
|
+
const candidates = suggestedActionCandidatesFromResult(result);
|
|
300
|
+
if (candidates.length === 0) return [];
|
|
301
|
+
const lines = ["*Suggested actions*"];
|
|
302
|
+
const visibleCandidates = candidates.slice(0, MAX_SLACK_SUGGESTED_ACTION_CANDIDATES);
|
|
303
|
+
for (const candidate of visibleCandidates) {
|
|
304
|
+
lines.push("", ...renderSuggestedActionCandidateLines(candidate));
|
|
305
|
+
}
|
|
306
|
+
const remainingCount = candidates.length - visibleCandidates.length;
|
|
307
|
+
if (remainingCount > 0) {
|
|
308
|
+
lines.push("", `Showing first ${visibleCandidates.length} of ${candidates.length} actions. Reply with an action number for the rest.`);
|
|
309
|
+
}
|
|
310
|
+
lines.push("", "Use the buttons below, or reply `apply 1`, `approve 1`, or `reject 1`.");
|
|
311
|
+
return lines;
|
|
312
|
+
}
|
|
313
|
+
function createSuggestedActionButtons(candidate) {
|
|
314
|
+
return [
|
|
315
|
+
{
|
|
316
|
+
type: "button",
|
|
317
|
+
text: { type: "plain_text", text: `Apply ${candidate.index}`, emoji: true },
|
|
318
|
+
action_id: `opentag:apply:${candidate.index}`,
|
|
319
|
+
value: buildSlackSuggestedActionButtonValue({
|
|
320
|
+
version: 1,
|
|
321
|
+
command: `apply ${candidate.index}`,
|
|
322
|
+
proposalId: candidate.proposalId,
|
|
323
|
+
intentId: candidate.intent.intentId
|
|
324
|
+
}),
|
|
325
|
+
style: "primary"
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
type: "button",
|
|
329
|
+
text: { type: "plain_text", text: "Approve", emoji: true },
|
|
330
|
+
action_id: `opentag:approve:${candidate.index}`,
|
|
331
|
+
value: buildSlackSuggestedActionButtonValue({
|
|
332
|
+
version: 1,
|
|
333
|
+
command: `approve ${candidate.index}`,
|
|
334
|
+
proposalId: candidate.proposalId,
|
|
335
|
+
intentId: candidate.intent.intentId
|
|
336
|
+
})
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
type: "button",
|
|
340
|
+
text: { type: "plain_text", text: "Reject", emoji: true },
|
|
341
|
+
action_id: `opentag:reject:${candidate.index}`,
|
|
342
|
+
value: buildSlackSuggestedActionButtonValue({
|
|
343
|
+
version: 1,
|
|
344
|
+
command: `reject ${candidate.index}`,
|
|
345
|
+
proposalId: candidate.proposalId,
|
|
346
|
+
intentId: candidate.intent.intentId
|
|
347
|
+
}),
|
|
348
|
+
style: "danger"
|
|
349
|
+
}
|
|
350
|
+
];
|
|
351
|
+
}
|
|
352
|
+
function renderSlackFinalResult(result) {
|
|
353
|
+
const lines = [`*Finished: ${result.conclusion}.*`, markdownToSlackMrkdwn(compactSlackSummary(result.summary))];
|
|
354
|
+
if (result.verification?.length) {
|
|
355
|
+
lines.push(
|
|
356
|
+
`Verified: ${result.verification.slice(0, 3).map((check) => `\`${markdownToSlackMrkdwn(check.command)}\` ${markdownToSlackMrkdwn(check.outcome)}`).join(", ")}`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
const nextAction = nextActionSummary(result);
|
|
360
|
+
if (nextAction && !result.suggestedChanges?.length) {
|
|
361
|
+
lines.push(`Next: ${markdownToSlackMrkdwn(compactNextAction(nextAction))}`);
|
|
362
|
+
}
|
|
363
|
+
const suggestedActions = renderSuggestedActionsMarkdown(result);
|
|
364
|
+
if (suggestedActions.length > 0) {
|
|
365
|
+
lines.push("", ...suggestedActions);
|
|
366
|
+
}
|
|
367
|
+
return lines.join("\n");
|
|
368
|
+
}
|
|
369
|
+
function createSlackFinalResultBlocks(result) {
|
|
370
|
+
const blocks = [
|
|
371
|
+
{
|
|
372
|
+
type: "section",
|
|
373
|
+
text: {
|
|
374
|
+
type: "mrkdwn",
|
|
375
|
+
text: `*Finished: ${result.conclusion}.*
|
|
376
|
+
${markdownToSlackMrkdwn(compactSlackSummary(result.summary))}`
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
];
|
|
380
|
+
if (result.verification?.length) {
|
|
381
|
+
blocks.push({
|
|
382
|
+
type: "section",
|
|
383
|
+
text: {
|
|
384
|
+
type: "mrkdwn",
|
|
385
|
+
text: `Verified: ${markdownToSlackMrkdwn(
|
|
386
|
+
result.verification.slice(0, 3).map((check) => `\`${check.command}\` ${check.outcome}`).join(", ")
|
|
387
|
+
)}`
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
const nextAction = nextActionSummary(result);
|
|
392
|
+
const suggestedActionCandidates = suggestedActionCandidatesFromResult(result);
|
|
393
|
+
if (nextAction && suggestedActionCandidates.length === 0) {
|
|
394
|
+
blocks.push({
|
|
395
|
+
type: "section",
|
|
396
|
+
text: {
|
|
397
|
+
type: "mrkdwn",
|
|
398
|
+
text: `Next: ${markdownToSlackMrkdwn(compactNextAction(nextAction))}`
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
if (suggestedActionCandidates.length > 0) {
|
|
403
|
+
blocks.push({ type: "divider" });
|
|
404
|
+
blocks.push({
|
|
405
|
+
type: "section",
|
|
406
|
+
text: {
|
|
407
|
+
type: "mrkdwn",
|
|
408
|
+
text: "*Suggested actions*\nChoose an action in this thread. Details stay in the OpenTag audit log."
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
const visibleCandidates = suggestedActionCandidates.slice(0, MAX_SLACK_SUGGESTED_ACTION_CANDIDATES);
|
|
412
|
+
for (const candidate of visibleCandidates) {
|
|
413
|
+
blocks.push({
|
|
414
|
+
type: "section",
|
|
415
|
+
text: {
|
|
416
|
+
type: "mrkdwn",
|
|
417
|
+
text: renderSuggestedActionCandidateLines(candidate).join("\n")
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
blocks.push({
|
|
421
|
+
type: "actions",
|
|
422
|
+
block_id: `opentag_actions_${candidate.index}`,
|
|
423
|
+
elements: createSuggestedActionButtons(candidate)
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
const remainingCount = suggestedActionCandidates.length - visibleCandidates.length;
|
|
427
|
+
if (remainingCount > 0) {
|
|
428
|
+
blocks.push({
|
|
429
|
+
type: "section",
|
|
430
|
+
text: {
|
|
431
|
+
type: "mrkdwn",
|
|
432
|
+
text: `Showing first ${visibleCandidates.length} of ${suggestedActionCandidates.length} actions. Reply with an action number for the rest.`
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return blocks;
|
|
438
|
+
}
|
|
439
|
+
function createSlackPostMessagePayload(input) {
|
|
440
|
+
return {
|
|
441
|
+
channel: input.channelId,
|
|
442
|
+
text: markdownToSlackMrkdwn(input.text),
|
|
443
|
+
thread_ts: input.threadTs,
|
|
444
|
+
...input.blocks?.length ? { blocks: input.blocks } : {}
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function createSlackUpdateMessagePayload(input) {
|
|
448
|
+
return {
|
|
449
|
+
channel: input.channelId,
|
|
450
|
+
text: markdownToSlackMrkdwn(input.text),
|
|
451
|
+
ts: input.messageTs,
|
|
452
|
+
...input.blocks?.length ? { blocks: input.blocks } : {}
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/events.ts
|
|
457
|
+
function json(body, status = 200) {
|
|
458
|
+
return { kind: "json", status, body };
|
|
459
|
+
}
|
|
460
|
+
function text(body, status = 200) {
|
|
461
|
+
return { kind: "text", status, body };
|
|
462
|
+
}
|
|
463
|
+
function createSlackEventProcessor(input) {
|
|
464
|
+
async function processBlockActions(payload, slackApp) {
|
|
465
|
+
const action = payload.actions?.find((candidate) => {
|
|
466
|
+
if (candidate.action_id?.startsWith("opentag:")) return true;
|
|
467
|
+
return typeof candidate.value === "string" && parseSlackSuggestedActionButtonValue(candidate.value) !== null;
|
|
468
|
+
});
|
|
469
|
+
if (!action) {
|
|
470
|
+
return json({ ok: true });
|
|
471
|
+
}
|
|
472
|
+
const parsedValue = typeof action.value === "string" ? parseSlackSuggestedActionButtonValue(action.value) : null;
|
|
473
|
+
const rawText = parsedValue?.command ?? (typeof action.value === "string" && parseThreadActionCommand(action.value) ? action.value.trim() : void 0);
|
|
474
|
+
if (!rawText || !parseThreadActionCommand(rawText)) {
|
|
475
|
+
return json({ error: "invalid_interactive_action" }, 400);
|
|
476
|
+
}
|
|
477
|
+
if (!input.submitThreadAction) {
|
|
478
|
+
return json({ ok: true });
|
|
479
|
+
}
|
|
480
|
+
const teamId = payload.team?.id;
|
|
481
|
+
const userId = payload.user?.id;
|
|
482
|
+
const channelId = payload.channel?.id ?? payload.container?.channel_id;
|
|
483
|
+
const messageTs = payload.message?.ts ?? payload.container?.message_ts;
|
|
484
|
+
const threadTs = payload.message?.thread_ts ?? payload.container?.thread_ts ?? messageTs;
|
|
485
|
+
if (!teamId || !userId || !channelId || !messageTs || !threadTs) {
|
|
486
|
+
return json({ error: "invalid_interactive_payload" }, 400);
|
|
487
|
+
}
|
|
488
|
+
const binding = await input.resolveChannelBinding({
|
|
489
|
+
teamId,
|
|
490
|
+
channelId
|
|
491
|
+
});
|
|
492
|
+
if (!binding) {
|
|
493
|
+
return json({ ok: true, ignored: "unbound_channel" });
|
|
494
|
+
}
|
|
495
|
+
await input.submitThreadAction({
|
|
496
|
+
id: `approval_slack_block_${payload.trigger_id ?? `${action.action_id ?? "action"}_${action.action_ts ?? messageTs}`}`,
|
|
497
|
+
rawText,
|
|
498
|
+
actor: {
|
|
499
|
+
provider: "slack",
|
|
500
|
+
providerUserId: userId,
|
|
501
|
+
handle: payload.user?.username ?? payload.user?.name ?? userId,
|
|
502
|
+
organizationId: teamId
|
|
503
|
+
},
|
|
504
|
+
callback: {
|
|
505
|
+
provider: "slack",
|
|
506
|
+
uri: slackApp.callbackUri ?? "https://slack.com/api/chat.postMessage",
|
|
507
|
+
threadKey: encodeSlackThreadKey({
|
|
508
|
+
teamId,
|
|
509
|
+
channelId,
|
|
510
|
+
threadTs
|
|
511
|
+
})
|
|
512
|
+
},
|
|
513
|
+
metadata: {
|
|
514
|
+
source: "slack_button",
|
|
515
|
+
teamId,
|
|
516
|
+
channelId,
|
|
517
|
+
messageTs,
|
|
518
|
+
...payload.api_app_id ? { slackAppId: payload.api_app_id } : {},
|
|
519
|
+
...action.action_id ? { actionId: action.action_id } : {},
|
|
520
|
+
...action.block_id ? { blockId: action.block_id } : {},
|
|
521
|
+
...action.action_ts ? { actionTs: action.action_ts } : {},
|
|
522
|
+
...parsedValue ? { proposalId: parsedValue.proposalId, intentId: parsedValue.intentId } : {},
|
|
523
|
+
repoProvider: binding.repoProvider ?? "github",
|
|
524
|
+
owner: binding.owner,
|
|
525
|
+
repo: binding.repo
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
return json({ ok: true });
|
|
529
|
+
}
|
|
530
|
+
return {
|
|
531
|
+
async process(payload, slackApp) {
|
|
532
|
+
if (payload.type === "block_actions") {
|
|
533
|
+
return processBlockActions(payload, slackApp);
|
|
534
|
+
}
|
|
535
|
+
if (payload.type === "url_verification") {
|
|
536
|
+
return text(payload.challenge ?? "");
|
|
537
|
+
}
|
|
538
|
+
if (payload.type !== "event_callback" || !payload.event || !["app_mention", "message"].includes(payload.event.type)) {
|
|
539
|
+
return json({ ok: true });
|
|
540
|
+
}
|
|
541
|
+
if (payload.event.type === "message" && (payload.event.subtype || payload.event.bot_id)) {
|
|
542
|
+
return json({ ok: true });
|
|
543
|
+
}
|
|
544
|
+
if (!payload.team_id || !payload.event.channel || !payload.event.user || !payload.event.text || !payload.event.ts || !payload.event_id) {
|
|
545
|
+
return json({ error: "invalid_event_payload" }, 400);
|
|
546
|
+
}
|
|
547
|
+
const rawThreadActionText = payload.event.type === "app_mention" ? stripSlackAppMention(payload.event.text, payload.authorizations?.[0]?.user_id) : payload.event.text.trim();
|
|
548
|
+
if (payload.event.type === "message" && (!rawThreadActionText || !parseThreadActionCommand(rawThreadActionText))) {
|
|
549
|
+
return json({ ok: true });
|
|
550
|
+
}
|
|
551
|
+
const binding = await input.resolveChannelBinding({
|
|
552
|
+
teamId: payload.team_id,
|
|
553
|
+
channelId: payload.event.channel
|
|
554
|
+
});
|
|
555
|
+
if (!binding) {
|
|
556
|
+
return json({ ok: true, ignored: "unbound_channel" });
|
|
557
|
+
}
|
|
558
|
+
if (rawThreadActionText && parseThreadActionCommand(rawThreadActionText) && input.submitThreadAction) {
|
|
559
|
+
await input.submitThreadAction({
|
|
560
|
+
id: `approval_slack_${payload.event_id}`,
|
|
561
|
+
rawText: rawThreadActionText,
|
|
562
|
+
actor: {
|
|
563
|
+
provider: "slack",
|
|
564
|
+
providerUserId: payload.event.user,
|
|
565
|
+
handle: payload.event.user,
|
|
566
|
+
organizationId: payload.team_id
|
|
567
|
+
},
|
|
568
|
+
callback: {
|
|
569
|
+
provider: "slack",
|
|
570
|
+
uri: slackApp.callbackUri ?? "https://slack.com/api/chat.postMessage",
|
|
571
|
+
threadKey: encodeSlackThreadKey({
|
|
572
|
+
teamId: payload.team_id,
|
|
573
|
+
channelId: payload.event.channel,
|
|
574
|
+
threadTs: payload.event.thread_ts ?? payload.event.ts
|
|
575
|
+
})
|
|
576
|
+
},
|
|
577
|
+
metadata: {
|
|
578
|
+
teamId: payload.team_id,
|
|
579
|
+
channelId: payload.event.channel,
|
|
580
|
+
messageTs: payload.event.ts,
|
|
581
|
+
...payload.api_app_id ? { slackAppId: payload.api_app_id } : {},
|
|
582
|
+
...payload.authorizations?.[0]?.user_id ? { slackBotUserId: payload.authorizations[0].user_id } : {},
|
|
583
|
+
repoProvider: binding.repoProvider ?? "github",
|
|
584
|
+
owner: binding.owner,
|
|
585
|
+
repo: binding.repo
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
return json({ ok: true });
|
|
589
|
+
}
|
|
590
|
+
if (payload.event.type !== "app_mention") {
|
|
591
|
+
return json({ ok: true });
|
|
592
|
+
}
|
|
593
|
+
const event = normalizeSlackAppMention({
|
|
594
|
+
teamId: payload.team_id,
|
|
595
|
+
channelId: payload.event.channel,
|
|
596
|
+
userId: payload.event.user,
|
|
597
|
+
text: payload.event.text,
|
|
598
|
+
ts: payload.event.ts,
|
|
599
|
+
eventId: payload.event_id,
|
|
600
|
+
eventTime: payload.event_time ?? Math.floor(Date.parse(input.now()) / 1e3),
|
|
601
|
+
agentId: slackApp.agentId,
|
|
602
|
+
binding,
|
|
603
|
+
...payload.api_app_id ? { appId: payload.api_app_id } : {},
|
|
604
|
+
...payload.event.thread_ts ? { threadTs: payload.event.thread_ts } : {},
|
|
605
|
+
...payload.authorizations?.[0]?.user_id ? { botUserId: payload.authorizations[0].user_id } : {},
|
|
606
|
+
...slackApp.callbackUri ? { callbackUri: slackApp.callbackUri } : {}
|
|
607
|
+
});
|
|
608
|
+
if (!event) {
|
|
609
|
+
return json({ ok: true, ignored: "empty_command" });
|
|
610
|
+
}
|
|
611
|
+
await input.createRun(event);
|
|
612
|
+
return json({ ok: true });
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// src/ingress.ts
|
|
618
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
619
|
+
import { serve } from "@hono/node-server";
|
|
620
|
+
import { Hono } from "hono";
|
|
621
|
+
|
|
622
|
+
// src/dispatcher-events.ts
|
|
623
|
+
import { randomUUID } from "crypto";
|
|
624
|
+
import { createOpenTagClient } from "@opentag/client";
|
|
625
|
+
function createSlackDispatcherEventProcessorInput(config) {
|
|
626
|
+
const dispatcherClient = createOpenTagClient({
|
|
627
|
+
dispatcherUrl: config.dispatcherUrl,
|
|
628
|
+
...config.dispatcherToken ? { pairingToken: config.dispatcherToken } : {}
|
|
629
|
+
});
|
|
630
|
+
return {
|
|
631
|
+
async resolveChannelBinding(input) {
|
|
632
|
+
try {
|
|
633
|
+
const { binding } = await dispatcherClient.getChannelBinding({
|
|
634
|
+
provider: "slack",
|
|
635
|
+
accountId: input.teamId,
|
|
636
|
+
conversationId: input.channelId
|
|
637
|
+
});
|
|
638
|
+
return {
|
|
639
|
+
teamId: binding.accountId,
|
|
640
|
+
channelId: binding.conversationId,
|
|
641
|
+
repoProvider: binding.repoProvider,
|
|
642
|
+
owner: binding.owner,
|
|
643
|
+
repo: binding.repo
|
|
644
|
+
};
|
|
645
|
+
} catch (error) {
|
|
646
|
+
if (error instanceof Error && error.message.includes("channel_binding_not_found")) {
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
throw error;
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
async createRun(event) {
|
|
653
|
+
const runId = `run_${randomUUID()}`;
|
|
654
|
+
const created = await dispatcherClient.createRun({ runId, event });
|
|
655
|
+
return created.outcome === "run_created" ? { runId: created.run.id } : { runId };
|
|
656
|
+
},
|
|
657
|
+
async submitThreadAction(action) {
|
|
658
|
+
await dispatcherClient.submitThreadAction(action);
|
|
659
|
+
},
|
|
660
|
+
now: () => (/* @__PURE__ */ new Date()).toISOString()
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// src/ingress.ts
|
|
665
|
+
function computeSlackSignature(input) {
|
|
666
|
+
const base = `v0:${input.timestamp}:${input.rawBody}`;
|
|
667
|
+
const digest = createHmac("sha256", input.signingSecret).update(base).digest("hex");
|
|
668
|
+
return `v0=${digest}`;
|
|
669
|
+
}
|
|
670
|
+
function verifySlackSignature(input) {
|
|
671
|
+
const expected = computeSlackSignature(input);
|
|
672
|
+
const expectedBuffer = Buffer.from(expected);
|
|
673
|
+
const actualBuffer = Buffer.from(input.signature);
|
|
674
|
+
return expectedBuffer.length === actualBuffer.length && timingSafeEqual(expectedBuffer, actualBuffer);
|
|
675
|
+
}
|
|
676
|
+
function verifySlackTimestamp(input) {
|
|
677
|
+
const timestampSeconds = Number(input.timestamp);
|
|
678
|
+
if (!Number.isFinite(timestampSeconds)) return false;
|
|
679
|
+
const toleranceSeconds = input.toleranceSeconds ?? 300;
|
|
680
|
+
const ageSeconds = Math.abs(Math.floor(input.nowMs / 1e3) - timestampSeconds);
|
|
681
|
+
return ageSeconds <= toleranceSeconds;
|
|
682
|
+
}
|
|
683
|
+
function createSlackEventsApp(input) {
|
|
684
|
+
const app = new Hono();
|
|
685
|
+
const processor = createSlackEventProcessor(input);
|
|
686
|
+
function parseSlackPayload(rawBody, contentType) {
|
|
687
|
+
try {
|
|
688
|
+
if (contentType?.includes("application/x-www-form-urlencoded") || rawBody.startsWith("payload=")) {
|
|
689
|
+
const interactivePayload = new URLSearchParams(rawBody).get("payload");
|
|
690
|
+
if (!interactivePayload) return null;
|
|
691
|
+
return JSON.parse(interactivePayload);
|
|
692
|
+
}
|
|
693
|
+
return JSON.parse(rawBody);
|
|
694
|
+
} catch {
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function resolveSlackApp(inputValue) {
|
|
699
|
+
const candidates = inputValue.apiAppId ? input.slackApps.filter((candidate) => !candidate.appId || candidate.appId === inputValue.apiAppId) : input.slackApps;
|
|
700
|
+
if (candidates.length === 0) {
|
|
701
|
+
return { error: "unknown_slack_app" };
|
|
702
|
+
}
|
|
703
|
+
const slackApp = candidates.find(
|
|
704
|
+
(candidate) => verifySlackSignature({
|
|
705
|
+
signingSecret: candidate.signingSecret,
|
|
706
|
+
timestamp: inputValue.timestamp,
|
|
707
|
+
rawBody: inputValue.rawBody,
|
|
708
|
+
signature: inputValue.signature
|
|
709
|
+
})
|
|
710
|
+
);
|
|
711
|
+
return slackApp ? { slackApp } : { error: "invalid_signature" };
|
|
712
|
+
}
|
|
713
|
+
app.post("/slack/events", async (c) => {
|
|
714
|
+
const timestamp = c.req.header("x-slack-request-timestamp");
|
|
715
|
+
const signature = c.req.header("x-slack-signature");
|
|
716
|
+
if (!timestamp || !signature) {
|
|
717
|
+
return c.json({ error: "missing_signature_headers" }, 401);
|
|
718
|
+
}
|
|
719
|
+
if (!verifySlackTimestamp({ timestamp, nowMs: input.clock?.() ?? Date.now() })) {
|
|
720
|
+
return c.json({ error: "stale_signature_timestamp" }, 401);
|
|
721
|
+
}
|
|
722
|
+
const rawBody = await c.req.text();
|
|
723
|
+
const payload = parseSlackPayload(rawBody, c.req.header("content-type"));
|
|
724
|
+
if (!payload) {
|
|
725
|
+
return c.json({ error: "invalid_json" }, 400);
|
|
726
|
+
}
|
|
727
|
+
const resolvedSlackApp = resolveSlackApp({
|
|
728
|
+
rawBody,
|
|
729
|
+
signature,
|
|
730
|
+
timestamp,
|
|
731
|
+
...payload.api_app_id ? { apiAppId: payload.api_app_id } : {}
|
|
732
|
+
});
|
|
733
|
+
if ("error" in resolvedSlackApp) {
|
|
734
|
+
return c.json({ error: resolvedSlackApp.error }, 401);
|
|
735
|
+
}
|
|
736
|
+
const result = await processor.process(payload, resolvedSlackApp.slackApp);
|
|
737
|
+
if (result.kind === "text") {
|
|
738
|
+
return c.text(result.body, result.status);
|
|
739
|
+
}
|
|
740
|
+
return c.json(result.body, result.status);
|
|
741
|
+
});
|
|
742
|
+
return app;
|
|
743
|
+
}
|
|
744
|
+
function startSlackIngress(config) {
|
|
745
|
+
const port = config.port ?? 3040;
|
|
746
|
+
const server = serve({
|
|
747
|
+
fetch: createSlackEventsApp({
|
|
748
|
+
slackApps: [
|
|
749
|
+
{
|
|
750
|
+
signingSecret: config.signingSecret,
|
|
751
|
+
agentId: config.agentId ?? "opentag",
|
|
752
|
+
...config.appId ? { appId: config.appId } : {},
|
|
753
|
+
...config.callbackUri ? { callbackUri: config.callbackUri } : {}
|
|
754
|
+
}
|
|
755
|
+
],
|
|
756
|
+
...createSlackDispatcherEventProcessorInput(config)
|
|
757
|
+
}).fetch,
|
|
758
|
+
port
|
|
759
|
+
});
|
|
760
|
+
return {
|
|
761
|
+
url: `http://localhost:${port}`,
|
|
762
|
+
server,
|
|
763
|
+
close() {
|
|
764
|
+
return new Promise((resolve, reject) => {
|
|
765
|
+
server.close((error) => {
|
|
766
|
+
if (error) {
|
|
767
|
+
reject(error);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
resolve();
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// src/socket-mode.ts
|
|
778
|
+
import WebSocket from "ws";
|
|
779
|
+
var SLACK_CONNECTIONS_OPEN_URL = "https://slack.com/api/apps.connections.open";
|
|
780
|
+
var DEFAULT_RECONNECT_DELAY_MS = 1e3;
|
|
781
|
+
var TERMINAL_SLACK_ERROR_CODES = [
|
|
782
|
+
"invalid_auth",
|
|
783
|
+
"not_authed",
|
|
784
|
+
"account_inactive",
|
|
785
|
+
"token_revoked",
|
|
786
|
+
"token_expired",
|
|
787
|
+
"not_allowed_token_type",
|
|
788
|
+
"no_permission",
|
|
789
|
+
"missing_scope",
|
|
790
|
+
"ekm_access_denied"
|
|
791
|
+
];
|
|
792
|
+
function isTerminalSlackAuthError(error) {
|
|
793
|
+
if (!(error instanceof Error)) return false;
|
|
794
|
+
return TERMINAL_SLACK_ERROR_CODES.some((code) => error.message.includes(code));
|
|
795
|
+
}
|
|
796
|
+
function rawDataToString(data) {
|
|
797
|
+
if (typeof data === "string") return data;
|
|
798
|
+
if (Buffer.isBuffer(data)) return data.toString("utf8");
|
|
799
|
+
if (Array.isArray(data)) return Buffer.concat(data).toString("utf8");
|
|
800
|
+
if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf8");
|
|
801
|
+
return Buffer.from(data).toString("utf8");
|
|
802
|
+
}
|
|
803
|
+
async function openSlackSocketUrl(input) {
|
|
804
|
+
const response = await input.fetchImpl(SLACK_CONNECTIONS_OPEN_URL, {
|
|
805
|
+
method: "POST",
|
|
806
|
+
headers: {
|
|
807
|
+
authorization: `Bearer ${input.appToken}`
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
const body = await response.json().catch(() => ({}));
|
|
811
|
+
if (!response.ok || !body.ok || !body.url) {
|
|
812
|
+
const reason = body.error ?? `http_${response.status}`;
|
|
813
|
+
throw new Error(`Slack Socket Mode connection failed: ${reason}`);
|
|
814
|
+
}
|
|
815
|
+
return body.url;
|
|
816
|
+
}
|
|
817
|
+
function parseSocketEnvelope(data) {
|
|
818
|
+
try {
|
|
819
|
+
return JSON.parse(rawDataToString(data));
|
|
820
|
+
} catch {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
async function handleSocketMessage(input) {
|
|
825
|
+
const envelope = parseSocketEnvelope(input.data);
|
|
826
|
+
if (!envelope?.envelope_id) {
|
|
827
|
+
input.logError("[slack] ignored Socket Mode envelope without envelope_id");
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
input.socket.send(JSON.stringify({ envelope_id: envelope.envelope_id }));
|
|
831
|
+
if (!envelope.payload) {
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
if (input.slackApp.appId && envelope.payload.api_app_id && envelope.payload.api_app_id !== input.slackApp.appId) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (envelope.type !== "events_api" && envelope.payload.type !== "block_actions") {
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
await input.processor.process(envelope.payload, input.slackApp);
|
|
841
|
+
}
|
|
842
|
+
function wait(ms) {
|
|
843
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
844
|
+
}
|
|
845
|
+
function startSlackSocketModeApp(input, dependencies = {}) {
|
|
846
|
+
const fetchImpl = dependencies.fetchImpl ?? fetch;
|
|
847
|
+
const createWebSocket = dependencies.createWebSocket ?? ((url) => new WebSocket(url));
|
|
848
|
+
const reconnectDelayMs = dependencies.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS;
|
|
849
|
+
const log = dependencies.log ?? ((message) => console.log(message));
|
|
850
|
+
const logError = dependencies.logError ?? ((message, error) => error ? console.error(message, error) : console.error(message));
|
|
851
|
+
const processor = createSlackEventProcessor(input);
|
|
852
|
+
let closed = false;
|
|
853
|
+
let activeSocket;
|
|
854
|
+
async function runOneConnection(socketUrl) {
|
|
855
|
+
await new Promise((resolve) => {
|
|
856
|
+
const socket = createWebSocket(socketUrl);
|
|
857
|
+
activeSocket = socket;
|
|
858
|
+
let settled = false;
|
|
859
|
+
const finish = () => {
|
|
860
|
+
if (settled) return;
|
|
861
|
+
settled = true;
|
|
862
|
+
if (activeSocket === socket) activeSocket = void 0;
|
|
863
|
+
resolve();
|
|
864
|
+
};
|
|
865
|
+
socket.once("open", () => {
|
|
866
|
+
log("[slack] Socket Mode connected");
|
|
867
|
+
});
|
|
868
|
+
socket.on("message", (data) => {
|
|
869
|
+
void handleSocketMessage({
|
|
870
|
+
data,
|
|
871
|
+
socket,
|
|
872
|
+
processor,
|
|
873
|
+
slackApp: input.slackApp,
|
|
874
|
+
logError
|
|
875
|
+
}).catch((error) => {
|
|
876
|
+
logError("[slack] failed to handle Socket Mode event:", error);
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
socket.once("close", finish);
|
|
880
|
+
socket.once("error", (error) => {
|
|
881
|
+
if (!closed) {
|
|
882
|
+
logError("[slack] Socket Mode connection error:", error);
|
|
883
|
+
}
|
|
884
|
+
socket.close();
|
|
885
|
+
finish();
|
|
886
|
+
});
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
const startPromise = (async () => {
|
|
890
|
+
while (!closed) {
|
|
891
|
+
try {
|
|
892
|
+
const socketUrl = await openSlackSocketUrl({ appToken: input.appToken, fetchImpl });
|
|
893
|
+
await runOneConnection(socketUrl);
|
|
894
|
+
} catch (error) {
|
|
895
|
+
if (isTerminalSlackAuthError(error)) {
|
|
896
|
+
if (!closed) {
|
|
897
|
+
logError("[slack] terminal Socket Mode auth/config error, aborting:", error);
|
|
898
|
+
}
|
|
899
|
+
throw error;
|
|
900
|
+
}
|
|
901
|
+
if (!closed) {
|
|
902
|
+
logError("[slack] failed to open Socket Mode connection, retrying:", error);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (!closed) {
|
|
906
|
+
await wait(reconnectDelayMs);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
})();
|
|
910
|
+
return {
|
|
911
|
+
startPromise,
|
|
912
|
+
async close() {
|
|
913
|
+
closed = true;
|
|
914
|
+
activeSocket?.close();
|
|
915
|
+
await startPromise.catch(() => void 0);
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
function startSlackSocketModeIngress(config, dependencies = {}) {
|
|
920
|
+
return startSlackSocketModeApp(
|
|
921
|
+
{
|
|
922
|
+
appToken: config.appToken,
|
|
923
|
+
slackApp: {
|
|
924
|
+
agentId: config.agentId ?? "opentag",
|
|
925
|
+
...config.appId ? { appId: config.appId } : {},
|
|
926
|
+
...config.callbackUri ? { callbackUri: config.callbackUri } : {}
|
|
927
|
+
},
|
|
928
|
+
...createSlackDispatcherEventProcessorInput(config)
|
|
929
|
+
},
|
|
930
|
+
dependencies
|
|
931
|
+
);
|
|
932
|
+
}
|
|
107
933
|
export {
|
|
934
|
+
buildSlackSuggestedActionButtonValue,
|
|
935
|
+
computeSlackSignature,
|
|
936
|
+
createSlackEventProcessor,
|
|
937
|
+
createSlackEventsApp,
|
|
938
|
+
createSlackFinalResultBlocks,
|
|
939
|
+
createSlackPostMessagePayload,
|
|
940
|
+
createSlackReactionPayload,
|
|
941
|
+
createSlackUpdateMessagePayload,
|
|
108
942
|
encodeSlackThreadKey,
|
|
943
|
+
markdownToSlackMrkdwn,
|
|
109
944
|
normalizeSlackAppMention,
|
|
945
|
+
parseSlackSuggestedActionButtonValue,
|
|
110
946
|
parseSlackThreadKey,
|
|
111
|
-
|
|
947
|
+
renderSlackAcknowledgement,
|
|
948
|
+
renderSlackFinalResult,
|
|
949
|
+
slackSourceReceiptReactionName,
|
|
950
|
+
startSlackIngress,
|
|
951
|
+
startSlackSocketModeApp,
|
|
952
|
+
startSlackSocketModeIngress,
|
|
953
|
+
stripSlackAppMention,
|
|
954
|
+
verifySlackSignature,
|
|
955
|
+
verifySlackTimestamp
|
|
112
956
|
};
|
|
113
957
|
//# sourceMappingURL=index.js.map
|