clawdbot 2026.1.4-1 → 2026.1.5-1
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/CHANGELOG.md +32 -6
- package/README.md +26 -1
- package/dist/agents/pi-embedded-runner.js +2 -0
- package/dist/agents/pi-embedded-subscribe.js +18 -3
- package/dist/agents/pi-tools.js +45 -6
- package/dist/agents/tools/browser-tool.js +38 -89
- package/dist/agents/tools/cron-tool.js +8 -8
- package/dist/agents/workspace.js +8 -1
- package/dist/auto-reply/command-detection.js +26 -0
- package/dist/auto-reply/reply/agent-runner.js +15 -8
- package/dist/auto-reply/reply/commands.js +36 -25
- package/dist/auto-reply/reply/directive-handling.js +4 -2
- package/dist/auto-reply/reply/directives.js +12 -0
- package/dist/auto-reply/reply/session-updates.js +2 -4
- package/dist/auto-reply/reply.js +26 -4
- package/dist/browser/config.js +22 -4
- package/dist/browser/profiles-service.js +3 -1
- package/dist/browser/profiles.js +14 -3
- package/dist/canvas-host/a2ui/.bundle.hash +2 -0
- package/dist/cli/gateway-cli.js +2 -2
- package/dist/cli/profile.js +81 -0
- package/dist/cli/program.js +10 -1
- package/dist/cli/run-main.js +33 -0
- package/dist/commands/configure.js +5 -0
- package/dist/commands/onboard-providers.js +1 -1
- package/dist/commands/setup.js +4 -1
- package/dist/config/defaults.js +56 -0
- package/dist/config/io.js +47 -6
- package/dist/config/paths.js +2 -2
- package/dist/config/port-defaults.js +32 -0
- package/dist/config/sessions.js +3 -2
- package/dist/config/validation.js +2 -2
- package/dist/config/zod-schema.js +16 -0
- package/dist/discord/monitor.js +75 -266
- package/dist/entry.js +16 -0
- package/dist/gateway/call.js +8 -1
- package/dist/gateway/server-methods/chat.js +1 -1
- package/dist/gateway/server.js +14 -3
- package/dist/index.js +2 -2
- package/dist/infra/control-ui-assets.js +118 -0
- package/dist/infra/dotenv.js +15 -0
- package/dist/infra/shell-env.js +79 -0
- package/dist/infra/system-events.js +50 -23
- package/dist/macos/relay.js +8 -2
- package/dist/telegram/bot.js +24 -1
- package/dist/utils.js +8 -2
- package/dist/web/auto-reply.js +18 -21
- package/dist/web/inbound.js +5 -1
- package/dist/web/qr-image.js +4 -4
- package/dist/web/session.js +2 -3
- package/docs/agent.md +0 -2
- package/docs/assets/markdown.css +4 -1
- package/docs/audio.md +0 -2
- package/docs/clawd.md +0 -2
- package/docs/configuration.md +62 -3
- package/docs/docs.json +9 -1
- package/docs/faq.md +32 -7
- package/docs/gateway.md +28 -0
- package/docs/images.md +0 -2
- package/docs/index.md +2 -4
- package/docs/mac/icon.md +1 -1
- package/docs/nix.md +57 -11
- package/docs/onboarding.md +0 -2
- package/docs/refactor/webagent-session.md +0 -2
- package/docs/research/memory.md +1 -1
- package/docs/skills.md +0 -2
- package/docs/templates/AGENTS.md +2 -2
- package/docs/tools.md +15 -0
- package/docs/whatsapp.md +2 -0
- package/package.json +9 -16
- package/dist/control-ui/assets/index-BFID3yAA.css +0 -1
- package/dist/control-ui/assets/index-CE_axlTS.js +0 -2235
- package/dist/control-ui/assets/index-CE_axlTS.js.map +0 -1
- package/dist/control-ui/index.html +0 -15
- package/dist/daemon/constants.js +0 -10
- package/dist/daemon/launchd.js +0 -276
- package/dist/daemon/legacy.js +0 -63
- package/dist/daemon/program-args.js +0 -76
- package/dist/daemon/schtasks.js +0 -257
- package/dist/daemon/service.js +0 -60
- package/dist/daemon/systemd.js +0 -266
- package/dist/imessage/client.js +0 -165
- package/dist/imessage/index.js +0 -3
- package/dist/imessage/monitor.js +0 -272
- package/dist/imessage/probe.js +0 -26
- package/dist/imessage/send.js +0 -83
- package/dist/imessage/targets.js +0 -176
- package/dist/signal/client.js +0 -134
- package/dist/signal/daemon.js +0 -69
- package/dist/signal/index.js +0 -3
- package/dist/signal/monitor.js +0 -336
- package/dist/signal/probe.js +0 -46
- package/dist/signal/send.js +0 -91
- package/dist/slack/actions.js +0 -97
- package/dist/slack/index.js +0 -5
- package/dist/slack/monitor.js +0 -1029
- package/dist/slack/probe.js +0 -47
- package/dist/slack/send.js +0 -131
- package/dist/slack/token.js +0 -10
- package/dist/tui/commands.js +0 -74
- package/dist/tui/components/assistant-message.js +0 -16
- package/dist/tui/components/chat-log.js +0 -92
- package/dist/tui/components/custom-editor.js +0 -53
- package/dist/tui/components/selectors.js +0 -8
- package/dist/tui/components/tool-execution.js +0 -111
- package/dist/tui/components/user-message.js +0 -17
- package/dist/tui/gateway-chat.js +0 -140
- package/dist/tui/layout.js +0 -41
- package/dist/tui/message-list.js +0 -57
- package/dist/tui/theme/theme.js +0 -80
- package/dist/tui/theme.js +0 -25
- package/dist/tui/tui.js +0 -708
- package/dist/wizard/clack-prompter.js +0 -56
- package/dist/wizard/onboarding.js +0 -452
- package/dist/wizard/prompts.js +0 -6
- package/dist/wizard/session.js +0 -203
package/dist/slack/monitor.js
DELETED
|
@@ -1,1029 +0,0 @@
|
|
|
1
|
-
import bolt from "@slack/bolt";
|
|
2
|
-
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
|
|
3
|
-
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
|
4
|
-
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
|
5
|
-
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
|
6
|
-
import { loadConfig } from "../config/config.js";
|
|
7
|
-
import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
|
|
8
|
-
import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
|
|
9
|
-
import { enqueueSystemEvent } from "../infra/system-events.js";
|
|
10
|
-
import { getChildLogger } from "../logging.js";
|
|
11
|
-
import { detectMime } from "../media/mime.js";
|
|
12
|
-
import { saveMediaBuffer } from "../media/store.js";
|
|
13
|
-
import { sendMessageSlack } from "./send.js";
|
|
14
|
-
import { resolveSlackAppToken, resolveSlackBotToken } from "./token.js";
|
|
15
|
-
function normalizeSlackSlug(raw) {
|
|
16
|
-
const trimmed = raw?.trim().toLowerCase() ?? "";
|
|
17
|
-
if (!trimmed)
|
|
18
|
-
return "";
|
|
19
|
-
const dashed = trimmed.replace(/\s+/g, "-");
|
|
20
|
-
const cleaned = dashed.replace(/[^a-z0-9#@._+-]+/g, "-");
|
|
21
|
-
return cleaned.replace(/-{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, "");
|
|
22
|
-
}
|
|
23
|
-
function normalizeAllowList(list) {
|
|
24
|
-
return (list ?? []).map((entry) => String(entry).trim()).filter(Boolean);
|
|
25
|
-
}
|
|
26
|
-
function normalizeAllowListLower(list) {
|
|
27
|
-
return normalizeAllowList(list).map((entry) => entry.toLowerCase());
|
|
28
|
-
}
|
|
29
|
-
function allowListMatches(params) {
|
|
30
|
-
const allowList = params.allowList;
|
|
31
|
-
if (allowList.length === 0)
|
|
32
|
-
return false;
|
|
33
|
-
if (allowList.includes("*"))
|
|
34
|
-
return true;
|
|
35
|
-
const id = params.id?.toLowerCase();
|
|
36
|
-
const name = params.name?.toLowerCase();
|
|
37
|
-
const slug = normalizeSlackSlug(name);
|
|
38
|
-
const candidates = [
|
|
39
|
-
id,
|
|
40
|
-
id ? `slack:${id}` : undefined,
|
|
41
|
-
id ? `user:${id}` : undefined,
|
|
42
|
-
name,
|
|
43
|
-
name ? `slack:${name}` : undefined,
|
|
44
|
-
slug,
|
|
45
|
-
].filter(Boolean);
|
|
46
|
-
return candidates.some((value) => allowList.includes(value));
|
|
47
|
-
}
|
|
48
|
-
function resolveSlackSlashCommandConfig(raw) {
|
|
49
|
-
return {
|
|
50
|
-
enabled: raw?.enabled === true,
|
|
51
|
-
name: raw?.name?.trim() || "clawd",
|
|
52
|
-
sessionPrefix: raw?.sessionPrefix?.trim() || "slack:slash",
|
|
53
|
-
ephemeral: raw?.ephemeral !== false,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
function shouldEmitSlackReactionNotification(params) {
|
|
57
|
-
const { mode, botId, messageAuthorId, userId, userName, allowlist } = params;
|
|
58
|
-
const effectiveMode = mode ?? "own";
|
|
59
|
-
if (effectiveMode === "off")
|
|
60
|
-
return false;
|
|
61
|
-
if (effectiveMode === "own") {
|
|
62
|
-
if (!botId || !messageAuthorId)
|
|
63
|
-
return false;
|
|
64
|
-
return messageAuthorId === botId;
|
|
65
|
-
}
|
|
66
|
-
if (effectiveMode === "allowlist") {
|
|
67
|
-
if (!Array.isArray(allowlist) || allowlist.length === 0)
|
|
68
|
-
return false;
|
|
69
|
-
const users = normalizeAllowListLower(allowlist);
|
|
70
|
-
return allowListMatches({
|
|
71
|
-
allowList: users,
|
|
72
|
-
id: userId,
|
|
73
|
-
name: userName ?? undefined,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
function resolveSlackChannelLabel(params) {
|
|
79
|
-
const channelName = params.channelName?.trim();
|
|
80
|
-
if (channelName) {
|
|
81
|
-
const slug = normalizeSlackSlug(channelName);
|
|
82
|
-
return `#${slug || channelName}`;
|
|
83
|
-
}
|
|
84
|
-
const channelId = params.channelId?.trim();
|
|
85
|
-
return channelId ? `#${channelId}` : "unknown channel";
|
|
86
|
-
}
|
|
87
|
-
function resolveSlackChannelConfig(params) {
|
|
88
|
-
const { channelId, channelName, channels } = params;
|
|
89
|
-
const entries = channels ?? {};
|
|
90
|
-
const keys = Object.keys(entries);
|
|
91
|
-
const normalizedName = channelName ? normalizeSlackSlug(channelName) : "";
|
|
92
|
-
const directName = channelName ? channelName.trim() : "";
|
|
93
|
-
const candidates = [
|
|
94
|
-
channelId,
|
|
95
|
-
channelName ? `#${directName}` : "",
|
|
96
|
-
directName,
|
|
97
|
-
normalizedName,
|
|
98
|
-
].filter(Boolean);
|
|
99
|
-
let matched;
|
|
100
|
-
for (const candidate of candidates) {
|
|
101
|
-
if (candidate && entries[candidate]) {
|
|
102
|
-
matched = entries[candidate];
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
const fallback = entries["*"];
|
|
107
|
-
if (keys.length === 0) {
|
|
108
|
-
return { allowed: true, requireMention: true };
|
|
109
|
-
}
|
|
110
|
-
if (!matched && !fallback) {
|
|
111
|
-
return { allowed: false, requireMention: true };
|
|
112
|
-
}
|
|
113
|
-
const resolved = matched ?? fallback ?? {};
|
|
114
|
-
const allowed = resolved.allow ?? true;
|
|
115
|
-
const requireMention = resolved.requireMention ?? fallback?.requireMention ?? true;
|
|
116
|
-
return { allowed, requireMention };
|
|
117
|
-
}
|
|
118
|
-
async function resolveSlackMedia(params) {
|
|
119
|
-
const files = params.files ?? [];
|
|
120
|
-
for (const file of files) {
|
|
121
|
-
const url = file.url_private_download ?? file.url_private;
|
|
122
|
-
if (!url)
|
|
123
|
-
continue;
|
|
124
|
-
try {
|
|
125
|
-
const res = await fetch(url, {
|
|
126
|
-
headers: { Authorization: `Bearer ${params.token}` },
|
|
127
|
-
});
|
|
128
|
-
if (!res.ok)
|
|
129
|
-
continue;
|
|
130
|
-
const buffer = Buffer.from(await res.arrayBuffer());
|
|
131
|
-
if (buffer.byteLength > params.maxBytes)
|
|
132
|
-
continue;
|
|
133
|
-
const contentType = await detectMime({
|
|
134
|
-
buffer,
|
|
135
|
-
headerMime: res.headers.get("content-type"),
|
|
136
|
-
filePath: file.name,
|
|
137
|
-
});
|
|
138
|
-
const saved = await saveMediaBuffer(buffer, contentType ?? file.mimetype, "inbound", params.maxBytes);
|
|
139
|
-
return {
|
|
140
|
-
path: saved.path,
|
|
141
|
-
contentType: saved.contentType,
|
|
142
|
-
placeholder: file.name ? `[Slack file: ${file.name}]` : "[Slack file]",
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
// Ignore download failures and fall through to the next file.
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
export async function monitorSlackProvider(opts = {}) {
|
|
152
|
-
const cfg = loadConfig();
|
|
153
|
-
const botToken = resolveSlackBotToken(opts.botToken ??
|
|
154
|
-
process.env.SLACK_BOT_TOKEN ??
|
|
155
|
-
cfg.slack?.botToken ??
|
|
156
|
-
undefined);
|
|
157
|
-
const appToken = resolveSlackAppToken(opts.appToken ??
|
|
158
|
-
process.env.SLACK_APP_TOKEN ??
|
|
159
|
-
cfg.slack?.appToken ??
|
|
160
|
-
undefined);
|
|
161
|
-
if (!botToken || !appToken) {
|
|
162
|
-
throw new Error("SLACK_BOT_TOKEN and SLACK_APP_TOKEN (or slack.botToken/slack.appToken) are required for Slack socket mode");
|
|
163
|
-
}
|
|
164
|
-
const runtime = opts.runtime ?? {
|
|
165
|
-
log: console.log,
|
|
166
|
-
error: console.error,
|
|
167
|
-
exit: (code) => {
|
|
168
|
-
throw new Error(`exit ${code}`);
|
|
169
|
-
},
|
|
170
|
-
};
|
|
171
|
-
const dmConfig = cfg.slack?.dm;
|
|
172
|
-
const allowFrom = normalizeAllowList(dmConfig?.allowFrom);
|
|
173
|
-
const groupDmEnabled = dmConfig?.groupEnabled ?? false;
|
|
174
|
-
const groupDmChannels = normalizeAllowList(dmConfig?.groupChannels);
|
|
175
|
-
const channelsConfig = cfg.slack?.channels;
|
|
176
|
-
const dmEnabled = dmConfig?.enabled ?? true;
|
|
177
|
-
const reactionMode = cfg.slack?.reactionNotifications ?? "own";
|
|
178
|
-
const reactionAllowlist = cfg.slack?.reactionAllowlist ?? [];
|
|
179
|
-
const slashCommand = resolveSlackSlashCommandConfig(opts.slashCommand ?? cfg.slack?.slashCommand);
|
|
180
|
-
const textLimit = resolveTextChunkLimit(cfg, "slack");
|
|
181
|
-
const mediaMaxBytes = (opts.mediaMaxMb ?? cfg.slack?.mediaMaxMb ?? 20) * 1024 * 1024;
|
|
182
|
-
const logger = getChildLogger({ module: "slack-auto-reply" });
|
|
183
|
-
const channelCache = new Map();
|
|
184
|
-
const userCache = new Map();
|
|
185
|
-
const seenMessages = new Map();
|
|
186
|
-
const markMessageSeen = (channelId, ts) => {
|
|
187
|
-
if (!channelId || !ts)
|
|
188
|
-
return false;
|
|
189
|
-
const key = `${channelId}:${ts}`;
|
|
190
|
-
if (seenMessages.has(key))
|
|
191
|
-
return true;
|
|
192
|
-
seenMessages.set(key, Date.now());
|
|
193
|
-
if (seenMessages.size > 500) {
|
|
194
|
-
const cutoff = Date.now() - 60_000;
|
|
195
|
-
for (const [entry, seenAt] of seenMessages) {
|
|
196
|
-
if (seenAt < cutoff || seenMessages.size > 450) {
|
|
197
|
-
seenMessages.delete(entry);
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return false;
|
|
205
|
-
};
|
|
206
|
-
const { App } = bolt;
|
|
207
|
-
const app = new App({
|
|
208
|
-
token: botToken,
|
|
209
|
-
appToken,
|
|
210
|
-
socketMode: true,
|
|
211
|
-
});
|
|
212
|
-
let botUserId = "";
|
|
213
|
-
try {
|
|
214
|
-
const auth = await app.client.auth.test({ token: botToken });
|
|
215
|
-
botUserId = auth.user_id ?? "";
|
|
216
|
-
}
|
|
217
|
-
catch (err) {
|
|
218
|
-
runtime.error?.(danger(`slack auth failed: ${String(err)}`));
|
|
219
|
-
}
|
|
220
|
-
const resolveChannelName = async (channelId) => {
|
|
221
|
-
const cached = channelCache.get(channelId);
|
|
222
|
-
if (cached)
|
|
223
|
-
return cached;
|
|
224
|
-
try {
|
|
225
|
-
const info = await app.client.conversations.info({
|
|
226
|
-
token: botToken,
|
|
227
|
-
channel: channelId,
|
|
228
|
-
});
|
|
229
|
-
const name = info.channel && "name" in info.channel ? info.channel.name : undefined;
|
|
230
|
-
const channel = info.channel ?? undefined;
|
|
231
|
-
const type = channel?.is_im
|
|
232
|
-
? "im"
|
|
233
|
-
: channel?.is_mpim
|
|
234
|
-
? "mpim"
|
|
235
|
-
: channel?.is_channel
|
|
236
|
-
? "channel"
|
|
237
|
-
: channel?.is_group
|
|
238
|
-
? "group"
|
|
239
|
-
: undefined;
|
|
240
|
-
const entry = { name, type };
|
|
241
|
-
channelCache.set(channelId, entry);
|
|
242
|
-
return entry;
|
|
243
|
-
}
|
|
244
|
-
catch {
|
|
245
|
-
return {};
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
const resolveUserName = async (userId) => {
|
|
249
|
-
const cached = userCache.get(userId);
|
|
250
|
-
if (cached)
|
|
251
|
-
return cached;
|
|
252
|
-
try {
|
|
253
|
-
const info = await app.client.users.info({
|
|
254
|
-
token: botToken,
|
|
255
|
-
user: userId,
|
|
256
|
-
});
|
|
257
|
-
const profile = info.user?.profile;
|
|
258
|
-
const name = profile?.display_name ||
|
|
259
|
-
profile?.real_name ||
|
|
260
|
-
info.user?.name ||
|
|
261
|
-
undefined;
|
|
262
|
-
const entry = { name };
|
|
263
|
-
userCache.set(userId, entry);
|
|
264
|
-
return entry;
|
|
265
|
-
}
|
|
266
|
-
catch {
|
|
267
|
-
return {};
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
const isChannelAllowed = (params) => {
|
|
271
|
-
const channelType = params.channelType;
|
|
272
|
-
const isDirectMessage = channelType === "im";
|
|
273
|
-
const isGroupDm = channelType === "mpim";
|
|
274
|
-
const isRoom = channelType === "channel" || channelType === "group";
|
|
275
|
-
if (isDirectMessage && !dmEnabled)
|
|
276
|
-
return false;
|
|
277
|
-
if (isGroupDm && !groupDmEnabled)
|
|
278
|
-
return false;
|
|
279
|
-
if (isGroupDm && groupDmChannels.length > 0) {
|
|
280
|
-
const allowList = normalizeAllowListLower(groupDmChannels);
|
|
281
|
-
const candidates = [
|
|
282
|
-
params.channelId,
|
|
283
|
-
params.channelName ? `#${params.channelName}` : undefined,
|
|
284
|
-
params.channelName,
|
|
285
|
-
params.channelName ? normalizeSlackSlug(params.channelName) : undefined,
|
|
286
|
-
]
|
|
287
|
-
.filter((value) => Boolean(value))
|
|
288
|
-
.map((value) => value.toLowerCase());
|
|
289
|
-
const permitted = allowList.includes("*") ||
|
|
290
|
-
candidates.some((candidate) => allowList.includes(candidate));
|
|
291
|
-
if (!permitted)
|
|
292
|
-
return false;
|
|
293
|
-
}
|
|
294
|
-
if (isRoom && params.channelId) {
|
|
295
|
-
const channelConfig = resolveSlackChannelConfig({
|
|
296
|
-
channelId: params.channelId,
|
|
297
|
-
channelName: params.channelName,
|
|
298
|
-
channels: channelsConfig,
|
|
299
|
-
});
|
|
300
|
-
if (channelConfig?.allowed === false)
|
|
301
|
-
return false;
|
|
302
|
-
}
|
|
303
|
-
return true;
|
|
304
|
-
};
|
|
305
|
-
const handleSlackMessage = async (message, opts) => {
|
|
306
|
-
if (opts.source === "message" && message.type !== "message")
|
|
307
|
-
return;
|
|
308
|
-
if (message.bot_id)
|
|
309
|
-
return;
|
|
310
|
-
if (opts.source === "message" &&
|
|
311
|
-
message.subtype &&
|
|
312
|
-
message.subtype !== "file_share") {
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
if (!message.user)
|
|
316
|
-
return;
|
|
317
|
-
if (markMessageSeen(message.channel, message.ts))
|
|
318
|
-
return;
|
|
319
|
-
let channelInfo = {};
|
|
320
|
-
let channelType = message.channel_type;
|
|
321
|
-
if (!channelType || channelType !== "im") {
|
|
322
|
-
channelInfo = await resolveChannelName(message.channel);
|
|
323
|
-
channelType = channelType ?? channelInfo.type;
|
|
324
|
-
}
|
|
325
|
-
const channelName = channelInfo?.name;
|
|
326
|
-
const resolvedChannelType = channelType;
|
|
327
|
-
const isDirectMessage = resolvedChannelType === "im";
|
|
328
|
-
const isGroupDm = resolvedChannelType === "mpim";
|
|
329
|
-
const isRoom = resolvedChannelType === "channel" || resolvedChannelType === "group";
|
|
330
|
-
if (!isChannelAllowed({
|
|
331
|
-
channelId: message.channel,
|
|
332
|
-
channelName,
|
|
333
|
-
channelType: resolvedChannelType,
|
|
334
|
-
})) {
|
|
335
|
-
logVerbose("slack: drop message (channel not allowed)");
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
if (isDirectMessage && allowFrom.length > 0) {
|
|
339
|
-
const permitted = allowListMatches({
|
|
340
|
-
allowList: normalizeAllowListLower(allowFrom),
|
|
341
|
-
id: message.user,
|
|
342
|
-
});
|
|
343
|
-
if (!permitted) {
|
|
344
|
-
logVerbose(`Blocked unauthorized slack sender ${message.user} (not in allowFrom)`);
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
const channelConfig = isRoom
|
|
349
|
-
? resolveSlackChannelConfig({
|
|
350
|
-
channelId: message.channel,
|
|
351
|
-
channelName,
|
|
352
|
-
channels: channelsConfig,
|
|
353
|
-
})
|
|
354
|
-
: null;
|
|
355
|
-
const wasMentioned = opts.wasMentioned ??
|
|
356
|
-
(!isDirectMessage &&
|
|
357
|
-
Boolean(botUserId && message.text?.includes(`<@${botUserId}>`)));
|
|
358
|
-
if (isRoom && channelConfig?.requireMention && !wasMentioned) {
|
|
359
|
-
logger.info({ channel: message.channel, reason: "no-mention" }, "skipping room message");
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
const media = await resolveSlackMedia({
|
|
363
|
-
files: message.files,
|
|
364
|
-
token: botToken,
|
|
365
|
-
maxBytes: mediaMaxBytes,
|
|
366
|
-
});
|
|
367
|
-
const rawBody = (message.text ?? "").trim() || media?.placeholder || "";
|
|
368
|
-
if (!rawBody)
|
|
369
|
-
return;
|
|
370
|
-
const sender = await resolveUserName(message.user);
|
|
371
|
-
const senderName = sender?.name ?? message.user;
|
|
372
|
-
const roomLabel = channelName ? `#${channelName}` : `#${message.channel}`;
|
|
373
|
-
const preview = rawBody.replace(/\s+/g, " ").slice(0, 160);
|
|
374
|
-
const inboundLabel = isDirectMessage
|
|
375
|
-
? `Slack DM from ${senderName}`
|
|
376
|
-
: `Slack message in ${roomLabel} from ${senderName}`;
|
|
377
|
-
enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
|
|
378
|
-
contextKey: `slack:message:${message.channel}:${message.ts ?? "unknown"}`,
|
|
379
|
-
});
|
|
380
|
-
const textWithId = `${rawBody}\n[slack message id: ${message.ts} channel: ${message.channel}]`;
|
|
381
|
-
const body = formatAgentEnvelope({
|
|
382
|
-
surface: "Slack",
|
|
383
|
-
from: senderName,
|
|
384
|
-
timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined,
|
|
385
|
-
body: textWithId,
|
|
386
|
-
});
|
|
387
|
-
const isRoomish = isRoom || isGroupDm;
|
|
388
|
-
const ctxPayload = {
|
|
389
|
-
Body: body,
|
|
390
|
-
From: isDirectMessage
|
|
391
|
-
? `slack:${message.user}`
|
|
392
|
-
: isRoom
|
|
393
|
-
? `slack:channel:${message.channel}`
|
|
394
|
-
: `slack:group:${message.channel}`,
|
|
395
|
-
To: isDirectMessage
|
|
396
|
-
? `user:${message.user}`
|
|
397
|
-
: `channel:${message.channel}`,
|
|
398
|
-
ChatType: isDirectMessage ? "direct" : isRoom ? "room" : "group",
|
|
399
|
-
GroupSubject: isRoomish ? roomLabel : undefined,
|
|
400
|
-
SenderName: senderName,
|
|
401
|
-
Surface: "slack",
|
|
402
|
-
MessageSid: message.ts,
|
|
403
|
-
ReplyToId: message.thread_ts ?? message.ts,
|
|
404
|
-
Timestamp: message.ts ? Math.round(Number(message.ts) * 1000) : undefined,
|
|
405
|
-
WasMentioned: isRoomish ? wasMentioned : undefined,
|
|
406
|
-
MediaPath: media?.path,
|
|
407
|
-
MediaType: media?.contentType,
|
|
408
|
-
MediaUrl: media?.path,
|
|
409
|
-
};
|
|
410
|
-
const replyTarget = ctxPayload.To ?? undefined;
|
|
411
|
-
if (!replyTarget) {
|
|
412
|
-
runtime.error?.(danger("slack: missing reply target"));
|
|
413
|
-
return;
|
|
414
|
-
}
|
|
415
|
-
if (isDirectMessage) {
|
|
416
|
-
const sessionCfg = cfg.session;
|
|
417
|
-
const mainKey = (sessionCfg?.mainKey ?? "main").trim() || "main";
|
|
418
|
-
const storePath = resolveStorePath(sessionCfg?.store);
|
|
419
|
-
await updateLastRoute({
|
|
420
|
-
storePath,
|
|
421
|
-
sessionKey: mainKey,
|
|
422
|
-
channel: "slack",
|
|
423
|
-
to: `user:${message.user}`,
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
if (shouldLogVerbose()) {
|
|
427
|
-
logVerbose(`slack inbound: channel=${message.channel} from=${ctxPayload.From} preview="${preview}"`);
|
|
428
|
-
}
|
|
429
|
-
let blockSendChain = Promise.resolve();
|
|
430
|
-
const sendBlockReply = (payload) => {
|
|
431
|
-
if (!payload?.text &&
|
|
432
|
-
!payload?.mediaUrl &&
|
|
433
|
-
!(payload?.mediaUrls?.length ?? 0)) {
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
blockSendChain = blockSendChain
|
|
437
|
-
.then(async () => {
|
|
438
|
-
await deliverReplies({
|
|
439
|
-
replies: [payload],
|
|
440
|
-
target: replyTarget,
|
|
441
|
-
token: botToken,
|
|
442
|
-
runtime,
|
|
443
|
-
textLimit,
|
|
444
|
-
});
|
|
445
|
-
})
|
|
446
|
-
.catch((err) => {
|
|
447
|
-
runtime.error?.(danger(`slack block reply failed: ${String(err)}`));
|
|
448
|
-
});
|
|
449
|
-
};
|
|
450
|
-
const replyResult = await getReplyFromConfig(ctxPayload, {
|
|
451
|
-
onBlockReply: sendBlockReply,
|
|
452
|
-
}, cfg);
|
|
453
|
-
const replies = replyResult
|
|
454
|
-
? Array.isArray(replyResult)
|
|
455
|
-
? replyResult
|
|
456
|
-
: [replyResult]
|
|
457
|
-
: [];
|
|
458
|
-
await blockSendChain;
|
|
459
|
-
if (replies.length === 0)
|
|
460
|
-
return;
|
|
461
|
-
await deliverReplies({
|
|
462
|
-
replies,
|
|
463
|
-
target: replyTarget,
|
|
464
|
-
token: botToken,
|
|
465
|
-
runtime,
|
|
466
|
-
textLimit,
|
|
467
|
-
});
|
|
468
|
-
if (shouldLogVerbose()) {
|
|
469
|
-
logVerbose(`slack: delivered ${replies.length} reply${replies.length === 1 ? "" : "ies"} to ${replyTarget}`);
|
|
470
|
-
}
|
|
471
|
-
};
|
|
472
|
-
app.event("message", async ({ event }) => {
|
|
473
|
-
try {
|
|
474
|
-
const message = event;
|
|
475
|
-
if (message.subtype === "message_changed") {
|
|
476
|
-
const changed = event;
|
|
477
|
-
const channelId = changed.channel;
|
|
478
|
-
const channelInfo = channelId
|
|
479
|
-
? await resolveChannelName(channelId)
|
|
480
|
-
: {};
|
|
481
|
-
const channelType = channelInfo?.type;
|
|
482
|
-
if (!isChannelAllowed({
|
|
483
|
-
channelId,
|
|
484
|
-
channelName: channelInfo?.name,
|
|
485
|
-
channelType,
|
|
486
|
-
})) {
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
const messageId = changed.message?.ts ?? changed.previous_message?.ts;
|
|
490
|
-
const label = resolveSlackChannelLabel({
|
|
491
|
-
channelId,
|
|
492
|
-
channelName: channelInfo?.name,
|
|
493
|
-
});
|
|
494
|
-
enqueueSystemEvent(`Slack message edited in ${label}.`, {
|
|
495
|
-
contextKey: `slack:message:changed:${channelId ?? "unknown"}:${messageId ?? changed.event_ts ?? "unknown"}`,
|
|
496
|
-
});
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
if (message.subtype === "message_deleted") {
|
|
500
|
-
const deleted = event;
|
|
501
|
-
const channelId = deleted.channel;
|
|
502
|
-
const channelInfo = channelId
|
|
503
|
-
? await resolveChannelName(channelId)
|
|
504
|
-
: {};
|
|
505
|
-
const channelType = channelInfo?.type;
|
|
506
|
-
if (!isChannelAllowed({
|
|
507
|
-
channelId,
|
|
508
|
-
channelName: channelInfo?.name,
|
|
509
|
-
channelType,
|
|
510
|
-
})) {
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
const label = resolveSlackChannelLabel({
|
|
514
|
-
channelId,
|
|
515
|
-
channelName: channelInfo?.name,
|
|
516
|
-
});
|
|
517
|
-
enqueueSystemEvent(`Slack message deleted in ${label}.`, {
|
|
518
|
-
contextKey: `slack:message:deleted:${channelId ?? "unknown"}:${deleted.deleted_ts ?? deleted.event_ts ?? "unknown"}`,
|
|
519
|
-
});
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
if (message.subtype === "thread_broadcast") {
|
|
523
|
-
const thread = event;
|
|
524
|
-
const channelId = thread.channel;
|
|
525
|
-
const channelInfo = channelId
|
|
526
|
-
? await resolveChannelName(channelId)
|
|
527
|
-
: {};
|
|
528
|
-
const channelType = channelInfo?.type;
|
|
529
|
-
if (!isChannelAllowed({
|
|
530
|
-
channelId,
|
|
531
|
-
channelName: channelInfo?.name,
|
|
532
|
-
channelType,
|
|
533
|
-
})) {
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
const label = resolveSlackChannelLabel({
|
|
537
|
-
channelId,
|
|
538
|
-
channelName: channelInfo?.name,
|
|
539
|
-
});
|
|
540
|
-
const messageId = thread.message?.ts ?? thread.event_ts;
|
|
541
|
-
enqueueSystemEvent(`Slack thread reply broadcast in ${label}.`, {
|
|
542
|
-
contextKey: `slack:thread:broadcast:${channelId ?? "unknown"}:${messageId ?? "unknown"}`,
|
|
543
|
-
});
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
await handleSlackMessage(message, { source: "message" });
|
|
547
|
-
}
|
|
548
|
-
catch (err) {
|
|
549
|
-
runtime.error?.(danger(`slack handler failed: ${String(err)}`));
|
|
550
|
-
}
|
|
551
|
-
});
|
|
552
|
-
app.event("app_mention", async ({ event }) => {
|
|
553
|
-
try {
|
|
554
|
-
const mention = event;
|
|
555
|
-
await handleSlackMessage(mention, {
|
|
556
|
-
source: "app_mention",
|
|
557
|
-
wasMentioned: true,
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
catch (err) {
|
|
561
|
-
runtime.error?.(danger(`slack mention handler failed: ${String(err)}`));
|
|
562
|
-
}
|
|
563
|
-
});
|
|
564
|
-
const handleReactionEvent = async (event, action) => {
|
|
565
|
-
try {
|
|
566
|
-
const item = event.item;
|
|
567
|
-
if (!event.user)
|
|
568
|
-
return;
|
|
569
|
-
if (!item?.channel || !item?.ts)
|
|
570
|
-
return;
|
|
571
|
-
if (item.type && item.type !== "message")
|
|
572
|
-
return;
|
|
573
|
-
if (botUserId && event.user === botUserId)
|
|
574
|
-
return;
|
|
575
|
-
const channelInfo = await resolveChannelName(item.channel);
|
|
576
|
-
const channelType = channelInfo?.type;
|
|
577
|
-
const isDirectMessage = channelType === "im";
|
|
578
|
-
const isGroupDm = channelType === "mpim";
|
|
579
|
-
const isRoom = channelType === "channel" || channelType === "group";
|
|
580
|
-
const channelName = channelInfo?.name;
|
|
581
|
-
if (isDirectMessage && !dmEnabled)
|
|
582
|
-
return;
|
|
583
|
-
if (isGroupDm && !groupDmEnabled)
|
|
584
|
-
return;
|
|
585
|
-
if (isGroupDm && groupDmChannels.length > 0) {
|
|
586
|
-
const allowList = normalizeAllowListLower(groupDmChannels);
|
|
587
|
-
const candidates = [
|
|
588
|
-
item.channel,
|
|
589
|
-
channelName ? `#${channelName}` : undefined,
|
|
590
|
-
channelName,
|
|
591
|
-
channelName ? normalizeSlackSlug(channelName) : undefined,
|
|
592
|
-
]
|
|
593
|
-
.filter((value) => Boolean(value))
|
|
594
|
-
.map((value) => value.toLowerCase());
|
|
595
|
-
const permitted = allowList.includes("*") ||
|
|
596
|
-
candidates.some((candidate) => allowList.includes(candidate));
|
|
597
|
-
if (!permitted)
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
if (isRoom) {
|
|
601
|
-
const channelConfig = resolveSlackChannelConfig({
|
|
602
|
-
channelId: item.channel,
|
|
603
|
-
channelName,
|
|
604
|
-
channels: channelsConfig,
|
|
605
|
-
});
|
|
606
|
-
if (channelConfig?.allowed === false)
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
const actor = await resolveUserName(event.user);
|
|
610
|
-
const shouldNotify = shouldEmitSlackReactionNotification({
|
|
611
|
-
mode: reactionMode,
|
|
612
|
-
botId: botUserId,
|
|
613
|
-
messageAuthorId: event.item_user ?? undefined,
|
|
614
|
-
userId: event.user,
|
|
615
|
-
userName: actor?.name ?? undefined,
|
|
616
|
-
allowlist: reactionAllowlist,
|
|
617
|
-
});
|
|
618
|
-
if (!shouldNotify)
|
|
619
|
-
return;
|
|
620
|
-
const emojiLabel = event.reaction ?? "emoji";
|
|
621
|
-
const actorLabel = actor?.name ?? event.user;
|
|
622
|
-
const channelLabel = channelName
|
|
623
|
-
? `#${normalizeSlackSlug(channelName) || channelName}`
|
|
624
|
-
: `#${item.channel}`;
|
|
625
|
-
const authorInfo = event.item_user
|
|
626
|
-
? await resolveUserName(event.item_user)
|
|
627
|
-
: undefined;
|
|
628
|
-
const authorLabel = authorInfo?.name ?? event.item_user;
|
|
629
|
-
const baseText = `Slack reaction ${action}: :${emojiLabel}: by ${actorLabel} in ${channelLabel} msg ${item.ts}`;
|
|
630
|
-
const text = authorLabel ? `${baseText} from ${authorLabel}` : baseText;
|
|
631
|
-
enqueueSystemEvent(text, {
|
|
632
|
-
contextKey: `slack:reaction:${action}:${item.channel}:${item.ts}:${event.user}:${emojiLabel}`,
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
catch (err) {
|
|
636
|
-
runtime.error?.(danger(`slack reaction handler failed: ${String(err)}`));
|
|
637
|
-
}
|
|
638
|
-
};
|
|
639
|
-
app.event("reaction_added", async ({ event }) => {
|
|
640
|
-
await handleReactionEvent(event, "added");
|
|
641
|
-
});
|
|
642
|
-
app.event("reaction_removed", async ({ event }) => {
|
|
643
|
-
await handleReactionEvent(event, "removed");
|
|
644
|
-
});
|
|
645
|
-
app.event("member_joined_channel", async ({ event }) => {
|
|
646
|
-
try {
|
|
647
|
-
const payload = event;
|
|
648
|
-
const channelId = payload.channel;
|
|
649
|
-
const channelInfo = channelId
|
|
650
|
-
? await resolveChannelName(channelId)
|
|
651
|
-
: {};
|
|
652
|
-
const channelType = payload.channel_type ?? channelInfo?.type;
|
|
653
|
-
if (!isChannelAllowed({
|
|
654
|
-
channelId,
|
|
655
|
-
channelName: channelInfo?.name,
|
|
656
|
-
channelType,
|
|
657
|
-
})) {
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
const userInfo = payload.user
|
|
661
|
-
? await resolveUserName(payload.user)
|
|
662
|
-
: {};
|
|
663
|
-
const userLabel = userInfo?.name ?? payload.user ?? "someone";
|
|
664
|
-
const label = resolveSlackChannelLabel({
|
|
665
|
-
channelId,
|
|
666
|
-
channelName: channelInfo?.name,
|
|
667
|
-
});
|
|
668
|
-
enqueueSystemEvent(`Slack: ${userLabel} joined ${label}.`, {
|
|
669
|
-
contextKey: `slack:member:joined:${channelId ?? "unknown"}:${payload.user ?? "unknown"}`,
|
|
670
|
-
});
|
|
671
|
-
}
|
|
672
|
-
catch (err) {
|
|
673
|
-
runtime.error?.(danger(`slack join handler failed: ${String(err)}`));
|
|
674
|
-
}
|
|
675
|
-
});
|
|
676
|
-
app.event("member_left_channel", async ({ event }) => {
|
|
677
|
-
try {
|
|
678
|
-
const payload = event;
|
|
679
|
-
const channelId = payload.channel;
|
|
680
|
-
const channelInfo = channelId
|
|
681
|
-
? await resolveChannelName(channelId)
|
|
682
|
-
: {};
|
|
683
|
-
const channelType = payload.channel_type ?? channelInfo?.type;
|
|
684
|
-
if (!isChannelAllowed({
|
|
685
|
-
channelId,
|
|
686
|
-
channelName: channelInfo?.name,
|
|
687
|
-
channelType,
|
|
688
|
-
})) {
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
|
-
const userInfo = payload.user
|
|
692
|
-
? await resolveUserName(payload.user)
|
|
693
|
-
: {};
|
|
694
|
-
const userLabel = userInfo?.name ?? payload.user ?? "someone";
|
|
695
|
-
const label = resolveSlackChannelLabel({
|
|
696
|
-
channelId,
|
|
697
|
-
channelName: channelInfo?.name,
|
|
698
|
-
});
|
|
699
|
-
enqueueSystemEvent(`Slack: ${userLabel} left ${label}.`, {
|
|
700
|
-
contextKey: `slack:member:left:${channelId ?? "unknown"}:${payload.user ?? "unknown"}`,
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
catch (err) {
|
|
704
|
-
runtime.error?.(danger(`slack leave handler failed: ${String(err)}`));
|
|
705
|
-
}
|
|
706
|
-
});
|
|
707
|
-
app.event("channel_created", async ({ event }) => {
|
|
708
|
-
try {
|
|
709
|
-
const payload = event;
|
|
710
|
-
const channelId = payload.channel?.id;
|
|
711
|
-
const channelName = payload.channel?.name;
|
|
712
|
-
if (!isChannelAllowed({
|
|
713
|
-
channelId,
|
|
714
|
-
channelName,
|
|
715
|
-
channelType: "channel",
|
|
716
|
-
})) {
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
|
-
const label = resolveSlackChannelLabel({ channelId, channelName });
|
|
720
|
-
enqueueSystemEvent(`Slack channel created: ${label}.`, {
|
|
721
|
-
contextKey: `slack:channel:created:${channelId ?? channelName ?? "unknown"}`,
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
catch (err) {
|
|
725
|
-
runtime.error?.(danger(`slack channel created handler failed: ${String(err)}`));
|
|
726
|
-
}
|
|
727
|
-
});
|
|
728
|
-
app.event("channel_rename", async ({ event }) => {
|
|
729
|
-
try {
|
|
730
|
-
const payload = event;
|
|
731
|
-
const channelId = payload.channel?.id;
|
|
732
|
-
const channelName = payload.channel?.name_normalized ?? payload.channel?.name;
|
|
733
|
-
if (!isChannelAllowed({
|
|
734
|
-
channelId,
|
|
735
|
-
channelName,
|
|
736
|
-
channelType: "channel",
|
|
737
|
-
})) {
|
|
738
|
-
return;
|
|
739
|
-
}
|
|
740
|
-
const label = resolveSlackChannelLabel({ channelId, channelName });
|
|
741
|
-
enqueueSystemEvent(`Slack channel renamed: ${label}.`, {
|
|
742
|
-
contextKey: `slack:channel:renamed:${channelId ?? channelName ?? "unknown"}`,
|
|
743
|
-
});
|
|
744
|
-
}
|
|
745
|
-
catch (err) {
|
|
746
|
-
runtime.error?.(danger(`slack channel rename handler failed: ${String(err)}`));
|
|
747
|
-
}
|
|
748
|
-
});
|
|
749
|
-
app.event("pin_added", async ({ event }) => {
|
|
750
|
-
try {
|
|
751
|
-
const payload = event;
|
|
752
|
-
const channelId = payload.channel_id;
|
|
753
|
-
const channelInfo = channelId
|
|
754
|
-
? await resolveChannelName(channelId)
|
|
755
|
-
: {};
|
|
756
|
-
if (!isChannelAllowed({
|
|
757
|
-
channelId,
|
|
758
|
-
channelName: channelInfo?.name,
|
|
759
|
-
channelType: channelInfo?.type,
|
|
760
|
-
})) {
|
|
761
|
-
return;
|
|
762
|
-
}
|
|
763
|
-
const label = resolveSlackChannelLabel({
|
|
764
|
-
channelId,
|
|
765
|
-
channelName: channelInfo?.name,
|
|
766
|
-
});
|
|
767
|
-
const userInfo = payload.user
|
|
768
|
-
? await resolveUserName(payload.user)
|
|
769
|
-
: {};
|
|
770
|
-
const userLabel = userInfo?.name ?? payload.user ?? "someone";
|
|
771
|
-
const itemType = payload.item?.type ?? "item";
|
|
772
|
-
const messageId = payload.item?.message?.ts ?? payload.event_ts;
|
|
773
|
-
enqueueSystemEvent(`Slack: ${userLabel} pinned a ${itemType} in ${label}.`, {
|
|
774
|
-
contextKey: `slack:pin:added:${channelId ?? "unknown"}:${messageId ?? "unknown"}`,
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
catch (err) {
|
|
778
|
-
runtime.error?.(danger(`slack pin added handler failed: ${String(err)}`));
|
|
779
|
-
}
|
|
780
|
-
});
|
|
781
|
-
app.event("pin_removed", async ({ event }) => {
|
|
782
|
-
try {
|
|
783
|
-
const payload = event;
|
|
784
|
-
const channelId = payload.channel_id;
|
|
785
|
-
const channelInfo = channelId
|
|
786
|
-
? await resolveChannelName(channelId)
|
|
787
|
-
: {};
|
|
788
|
-
if (!isChannelAllowed({
|
|
789
|
-
channelId,
|
|
790
|
-
channelName: channelInfo?.name,
|
|
791
|
-
channelType: channelInfo?.type,
|
|
792
|
-
})) {
|
|
793
|
-
return;
|
|
794
|
-
}
|
|
795
|
-
const label = resolveSlackChannelLabel({
|
|
796
|
-
channelId,
|
|
797
|
-
channelName: channelInfo?.name,
|
|
798
|
-
});
|
|
799
|
-
const userInfo = payload.user
|
|
800
|
-
? await resolveUserName(payload.user)
|
|
801
|
-
: {};
|
|
802
|
-
const userLabel = userInfo?.name ?? payload.user ?? "someone";
|
|
803
|
-
const itemType = payload.item?.type ?? "item";
|
|
804
|
-
const messageId = payload.item?.message?.ts ?? payload.event_ts;
|
|
805
|
-
enqueueSystemEvent(`Slack: ${userLabel} unpinned a ${itemType} in ${label}.`, {
|
|
806
|
-
contextKey: `slack:pin:removed:${channelId ?? "unknown"}:${messageId ?? "unknown"}`,
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
catch (err) {
|
|
810
|
-
runtime.error?.(danger(`slack pin removed handler failed: ${String(err)}`));
|
|
811
|
-
}
|
|
812
|
-
});
|
|
813
|
-
if (slashCommand.enabled) {
|
|
814
|
-
app.command(slashCommand.name, async ({ command, ack, respond }) => {
|
|
815
|
-
try {
|
|
816
|
-
const prompt = command.text?.trim();
|
|
817
|
-
if (!prompt) {
|
|
818
|
-
await ack({
|
|
819
|
-
text: "Message required.",
|
|
820
|
-
response_type: "ephemeral",
|
|
821
|
-
});
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
|
-
await ack();
|
|
825
|
-
if (botUserId && command.user_id === botUserId)
|
|
826
|
-
return;
|
|
827
|
-
const channelInfo = await resolveChannelName(command.channel_id);
|
|
828
|
-
const channelType = channelInfo?.type ??
|
|
829
|
-
(command.channel_name === "directmessage" ? "im" : undefined);
|
|
830
|
-
const isDirectMessage = channelType === "im";
|
|
831
|
-
const isGroupDm = channelType === "mpim";
|
|
832
|
-
const isRoom = channelType === "channel" || channelType === "group";
|
|
833
|
-
if (isDirectMessage && !dmEnabled) {
|
|
834
|
-
await respond({
|
|
835
|
-
text: "Slack DMs are disabled.",
|
|
836
|
-
response_type: "ephemeral",
|
|
837
|
-
});
|
|
838
|
-
return;
|
|
839
|
-
}
|
|
840
|
-
if (isGroupDm && !groupDmEnabled) {
|
|
841
|
-
await respond({
|
|
842
|
-
text: "Slack group DMs are disabled.",
|
|
843
|
-
response_type: "ephemeral",
|
|
844
|
-
});
|
|
845
|
-
return;
|
|
846
|
-
}
|
|
847
|
-
if (isGroupDm && groupDmChannels.length > 0) {
|
|
848
|
-
const allowList = normalizeAllowListLower(groupDmChannels);
|
|
849
|
-
const channelName = channelInfo?.name;
|
|
850
|
-
const candidates = [
|
|
851
|
-
command.channel_id,
|
|
852
|
-
channelName ? `#${channelName}` : undefined,
|
|
853
|
-
channelName,
|
|
854
|
-
channelName ? normalizeSlackSlug(channelName) : undefined,
|
|
855
|
-
]
|
|
856
|
-
.filter((value) => Boolean(value))
|
|
857
|
-
.map((value) => value.toLowerCase());
|
|
858
|
-
const permitted = allowList.includes("*") ||
|
|
859
|
-
candidates.some((candidate) => allowList.includes(candidate));
|
|
860
|
-
if (!permitted) {
|
|
861
|
-
await respond({
|
|
862
|
-
text: "This group DM is not allowed.",
|
|
863
|
-
response_type: "ephemeral",
|
|
864
|
-
});
|
|
865
|
-
return;
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
if (isDirectMessage && allowFrom.length > 0) {
|
|
869
|
-
const sender = await resolveUserName(command.user_id);
|
|
870
|
-
const permitted = allowListMatches({
|
|
871
|
-
allowList: normalizeAllowListLower(allowFrom),
|
|
872
|
-
id: command.user_id,
|
|
873
|
-
name: sender?.name ?? undefined,
|
|
874
|
-
});
|
|
875
|
-
if (!permitted) {
|
|
876
|
-
await respond({
|
|
877
|
-
text: "You are not authorized to use this command.",
|
|
878
|
-
response_type: "ephemeral",
|
|
879
|
-
});
|
|
880
|
-
return;
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
if (isRoom) {
|
|
884
|
-
const channelConfig = resolveSlackChannelConfig({
|
|
885
|
-
channelId: command.channel_id,
|
|
886
|
-
channelName: channelInfo?.name,
|
|
887
|
-
channels: channelsConfig,
|
|
888
|
-
});
|
|
889
|
-
if (channelConfig?.allowed === false) {
|
|
890
|
-
await respond({
|
|
891
|
-
text: "This channel is not allowed.",
|
|
892
|
-
response_type: "ephemeral",
|
|
893
|
-
});
|
|
894
|
-
return;
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
const sender = await resolveUserName(command.user_id);
|
|
898
|
-
const senderName = sender?.name ?? command.user_name ?? command.user_id;
|
|
899
|
-
const channelName = channelInfo?.name;
|
|
900
|
-
const roomLabel = channelName
|
|
901
|
-
? `#${channelName}`
|
|
902
|
-
: `#${command.channel_id}`;
|
|
903
|
-
const isRoomish = isRoom || isGroupDm;
|
|
904
|
-
const ctxPayload = {
|
|
905
|
-
Body: prompt,
|
|
906
|
-
From: isDirectMessage
|
|
907
|
-
? `slack:${command.user_id}`
|
|
908
|
-
: isRoom
|
|
909
|
-
? `slack:channel:${command.channel_id}`
|
|
910
|
-
: `slack:group:${command.channel_id}`,
|
|
911
|
-
To: `slash:${command.user_id}`,
|
|
912
|
-
ChatType: isDirectMessage ? "direct" : isRoom ? "room" : "group",
|
|
913
|
-
GroupSubject: isRoomish ? roomLabel : undefined,
|
|
914
|
-
SenderName: senderName,
|
|
915
|
-
Surface: "slack",
|
|
916
|
-
WasMentioned: true,
|
|
917
|
-
MessageSid: command.trigger_id,
|
|
918
|
-
Timestamp: Date.now(),
|
|
919
|
-
SessionKey: `${slashCommand.sessionPrefix}:${command.user_id}`,
|
|
920
|
-
};
|
|
921
|
-
const replyResult = await getReplyFromConfig(ctxPayload, undefined, cfg);
|
|
922
|
-
const replies = replyResult
|
|
923
|
-
? Array.isArray(replyResult)
|
|
924
|
-
? replyResult
|
|
925
|
-
: [replyResult]
|
|
926
|
-
: [];
|
|
927
|
-
await deliverSlackSlashReplies({
|
|
928
|
-
replies,
|
|
929
|
-
respond,
|
|
930
|
-
ephemeral: slashCommand.ephemeral,
|
|
931
|
-
textLimit,
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
catch (err) {
|
|
935
|
-
runtime.error?.(danger(`slack slash handler failed: ${String(err)}`));
|
|
936
|
-
await respond({
|
|
937
|
-
text: "Sorry, something went wrong handling that command.",
|
|
938
|
-
response_type: "ephemeral",
|
|
939
|
-
});
|
|
940
|
-
}
|
|
941
|
-
});
|
|
942
|
-
}
|
|
943
|
-
const stopOnAbort = () => {
|
|
944
|
-
if (opts.abortSignal?.aborted)
|
|
945
|
-
void app.stop();
|
|
946
|
-
};
|
|
947
|
-
opts.abortSignal?.addEventListener("abort", stopOnAbort, { once: true });
|
|
948
|
-
try {
|
|
949
|
-
await app.start();
|
|
950
|
-
runtime.log?.("slack socket mode connected");
|
|
951
|
-
if (opts.abortSignal?.aborted)
|
|
952
|
-
return;
|
|
953
|
-
await new Promise((resolve) => {
|
|
954
|
-
opts.abortSignal?.addEventListener("abort", () => resolve(), {
|
|
955
|
-
once: true,
|
|
956
|
-
});
|
|
957
|
-
});
|
|
958
|
-
}
|
|
959
|
-
finally {
|
|
960
|
-
opts.abortSignal?.removeEventListener("abort", stopOnAbort);
|
|
961
|
-
await app.stop().catch(() => undefined);
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
async function deliverReplies(params) {
|
|
965
|
-
const chunkLimit = Math.min(params.textLimit, 4000);
|
|
966
|
-
for (const payload of params.replies) {
|
|
967
|
-
const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
|
968
|
-
const text = payload.text ?? "";
|
|
969
|
-
if (!text && mediaList.length === 0)
|
|
970
|
-
continue;
|
|
971
|
-
if (mediaList.length === 0) {
|
|
972
|
-
for (const chunk of chunkText(text, chunkLimit)) {
|
|
973
|
-
const threadTs = undefined;
|
|
974
|
-
const trimmed = chunk.trim();
|
|
975
|
-
if (!trimmed || trimmed === SILENT_REPLY_TOKEN)
|
|
976
|
-
continue;
|
|
977
|
-
await sendMessageSlack(params.target, trimmed, {
|
|
978
|
-
token: params.token,
|
|
979
|
-
threadTs,
|
|
980
|
-
});
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
else {
|
|
984
|
-
let first = true;
|
|
985
|
-
for (const mediaUrl of mediaList) {
|
|
986
|
-
const caption = first ? text : "";
|
|
987
|
-
first = false;
|
|
988
|
-
const threadTs = undefined;
|
|
989
|
-
await sendMessageSlack(params.target, caption, {
|
|
990
|
-
token: params.token,
|
|
991
|
-
mediaUrl,
|
|
992
|
-
threadTs,
|
|
993
|
-
});
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
params.runtime.log?.(`delivered reply to ${params.target}`);
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
async function deliverSlackSlashReplies(params) {
|
|
1000
|
-
const messages = [];
|
|
1001
|
-
const chunkLimit = Math.min(params.textLimit, 4000);
|
|
1002
|
-
for (const payload of params.replies) {
|
|
1003
|
-
const textRaw = payload.text?.trim() ?? "";
|
|
1004
|
-
const text = textRaw && textRaw !== SILENT_REPLY_TOKEN ? textRaw : undefined;
|
|
1005
|
-
const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
|
1006
|
-
const combined = [
|
|
1007
|
-
text ?? "",
|
|
1008
|
-
...mediaList.map((url) => url.trim()).filter(Boolean),
|
|
1009
|
-
]
|
|
1010
|
-
.filter(Boolean)
|
|
1011
|
-
.join("\n");
|
|
1012
|
-
if (!combined)
|
|
1013
|
-
continue;
|
|
1014
|
-
for (const chunk of chunkText(combined, chunkLimit)) {
|
|
1015
|
-
messages.push(chunk);
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
if (messages.length === 0) {
|
|
1019
|
-
await params.respond({
|
|
1020
|
-
text: "No response was generated for that command.",
|
|
1021
|
-
response_type: "ephemeral",
|
|
1022
|
-
});
|
|
1023
|
-
return;
|
|
1024
|
-
}
|
|
1025
|
-
const responseType = params.ephemeral ? "ephemeral" : "in_channel";
|
|
1026
|
-
for (const message of messages) {
|
|
1027
|
-
await params.respond({ text: message, response_type: responseType });
|
|
1028
|
-
}
|
|
1029
|
-
}
|