@aarekaz/switchboard-slack 0.3.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/README.md +324 -0
- package/dist/index.d.ts +194 -0
- package/dist/index.js +779 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
// src/register.ts
|
|
2
|
+
import { registry } from "@aarekaz/switchboard-core";
|
|
3
|
+
|
|
4
|
+
// src/adapter.ts
|
|
5
|
+
import bolt from "@slack/bolt";
|
|
6
|
+
import { LRUCache } from "lru-cache";
|
|
7
|
+
import {
|
|
8
|
+
ok,
|
|
9
|
+
err,
|
|
10
|
+
ConnectionError,
|
|
11
|
+
MessageSendError,
|
|
12
|
+
MessageEditError,
|
|
13
|
+
MessageDeleteError,
|
|
14
|
+
ReactionError
|
|
15
|
+
} from "@aarekaz/switchboard-core";
|
|
16
|
+
|
|
17
|
+
// src/normalizers.ts
|
|
18
|
+
function normalizeMessage(slackMessage) {
|
|
19
|
+
const attachments = [];
|
|
20
|
+
if (slackMessage.files && Array.isArray(slackMessage.files)) {
|
|
21
|
+
for (const file of slackMessage.files) {
|
|
22
|
+
attachments.push({
|
|
23
|
+
id: file.id,
|
|
24
|
+
filename: file.name || "unknown",
|
|
25
|
+
url: file.url_private || file.permalink || "",
|
|
26
|
+
mimeType: file.mimetype || "application/octet-stream",
|
|
27
|
+
size: file.size || 0
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
id: slackMessage.ts || slackMessage.message_ts || slackMessage.event_ts,
|
|
33
|
+
channelId: slackMessage.channel || slackMessage.channel_id || "",
|
|
34
|
+
userId: slackMessage.user || slackMessage.bot_id || "unknown",
|
|
35
|
+
text: extractPlainText(slackMessage),
|
|
36
|
+
timestamp: new Date(parseFloat(slackMessage.ts || slackMessage.event_ts || "0") * 1e3),
|
|
37
|
+
threadId: slackMessage.thread_ts,
|
|
38
|
+
attachments: attachments.length > 0 ? attachments : void 0,
|
|
39
|
+
platform: "slack",
|
|
40
|
+
_raw: slackMessage
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function extractPlainText(message) {
|
|
44
|
+
if (message.blocks && Array.isArray(message.blocks)) {
|
|
45
|
+
const texts = [];
|
|
46
|
+
for (const block of message.blocks) {
|
|
47
|
+
if (block.type === "section" && block.text) {
|
|
48
|
+
texts.push(block.text.text || "");
|
|
49
|
+
} else if (block.type === "context" && block.elements) {
|
|
50
|
+
for (const element of block.elements) {
|
|
51
|
+
if (element.text) {
|
|
52
|
+
texts.push(element.text);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (texts.length > 0) {
|
|
58
|
+
return texts.join("\\n");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return message.text || "";
|
|
62
|
+
}
|
|
63
|
+
function normalizeEmoji(slackEmoji) {
|
|
64
|
+
if (!/^:.+:$/.test(slackEmoji)) {
|
|
65
|
+
return slackEmoji;
|
|
66
|
+
}
|
|
67
|
+
return slackEmoji.replace(/^:|:$/g, "");
|
|
68
|
+
}
|
|
69
|
+
var EMOJI_MAP = {
|
|
70
|
+
"\u{1F44D}": "thumbsup",
|
|
71
|
+
"\u{1F44E}": "thumbsdown",
|
|
72
|
+
"\u2764\uFE0F": "heart",
|
|
73
|
+
"\u{1F602}": "joy",
|
|
74
|
+
"\u{1F60A}": "blush",
|
|
75
|
+
"\u{1F60D}": "heart_eyes",
|
|
76
|
+
"\u{1F389}": "tada",
|
|
77
|
+
"\u{1F525}": "fire",
|
|
78
|
+
"\u2705": "white_check_mark",
|
|
79
|
+
"\u274C": "x",
|
|
80
|
+
"\u2B50": "star",
|
|
81
|
+
"\u{1F4AF}": "100",
|
|
82
|
+
"\u{1F680}": "rocket",
|
|
83
|
+
"\u{1F440}": "eyes",
|
|
84
|
+
"\u{1F914}": "thinking_face",
|
|
85
|
+
"\u{1F62D}": "sob",
|
|
86
|
+
"\u{1F631}": "scream",
|
|
87
|
+
"\u{1F64F}": "pray",
|
|
88
|
+
"\u{1F4AA}": "muscle",
|
|
89
|
+
"\u{1F44F}": "clap",
|
|
90
|
+
"\u{1F3AF}": "dart",
|
|
91
|
+
"\u2728": "sparkles",
|
|
92
|
+
"\u{1F91D}": "handshake",
|
|
93
|
+
"\u{1F4A1}": "bulb",
|
|
94
|
+
"\u{1F41B}": "bug",
|
|
95
|
+
"\u26A1": "zap",
|
|
96
|
+
"\u{1F527}": "wrench",
|
|
97
|
+
"\u{1F4DD}": "memo",
|
|
98
|
+
"\u{1F3A8}": "art",
|
|
99
|
+
"\u267B\uFE0F": "recycle",
|
|
100
|
+
"\u{1F512}": "lock",
|
|
101
|
+
"\u{1F513}": "unlock",
|
|
102
|
+
"\u270F\uFE0F": "pencil2",
|
|
103
|
+
"\u{1F5D1}\uFE0F": "wastebasket"
|
|
104
|
+
};
|
|
105
|
+
function toSlackEmoji(emoji) {
|
|
106
|
+
if (/^:.+:$/.test(emoji)) {
|
|
107
|
+
return emoji.slice(1, -1);
|
|
108
|
+
}
|
|
109
|
+
if (EMOJI_MAP[emoji]) {
|
|
110
|
+
return EMOJI_MAP[emoji];
|
|
111
|
+
}
|
|
112
|
+
if (/^[a-z0-9_+-]+$/i.test(emoji)) {
|
|
113
|
+
return emoji;
|
|
114
|
+
}
|
|
115
|
+
const stripped = emoji.replace(/[\uFE00-\uFE0F]/g, "");
|
|
116
|
+
if (EMOJI_MAP[stripped]) {
|
|
117
|
+
return EMOJI_MAP[stripped];
|
|
118
|
+
}
|
|
119
|
+
return emoji;
|
|
120
|
+
}
|
|
121
|
+
function normalizeMessageEvent(event) {
|
|
122
|
+
return {
|
|
123
|
+
type: "message",
|
|
124
|
+
message: normalizeMessage(event)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function normalizeReactionEvent(event, action) {
|
|
128
|
+
return {
|
|
129
|
+
type: "reaction",
|
|
130
|
+
messageId: event.item.ts,
|
|
131
|
+
userId: event.user,
|
|
132
|
+
emoji: normalizeEmoji(event.reaction),
|
|
133
|
+
action
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/adapter.ts
|
|
138
|
+
var { App } = bolt;
|
|
139
|
+
var SlackAdapter = class {
|
|
140
|
+
name = "slack-adapter";
|
|
141
|
+
platform = "slack";
|
|
142
|
+
app = null;
|
|
143
|
+
eventHandlers = /* @__PURE__ */ new Set();
|
|
144
|
+
config;
|
|
145
|
+
messageCache;
|
|
146
|
+
cacheHits = 0;
|
|
147
|
+
cacheMisses = 0;
|
|
148
|
+
constructor(config = {}) {
|
|
149
|
+
this.config = {
|
|
150
|
+
cacheSize: config.cacheSize || 1e3,
|
|
151
|
+
cacheTTL: config.cacheTTL || 1e3 * 60 * 60,
|
|
152
|
+
// 1 hour default
|
|
153
|
+
...config
|
|
154
|
+
};
|
|
155
|
+
this.messageCache = new LRUCache({
|
|
156
|
+
max: this.config.cacheSize,
|
|
157
|
+
ttl: this.config.cacheTTL
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Connect to Slack
|
|
162
|
+
*/
|
|
163
|
+
async connect(credentials) {
|
|
164
|
+
const slackCreds = credentials;
|
|
165
|
+
if (!slackCreds.botToken) {
|
|
166
|
+
throw new ConnectionError(
|
|
167
|
+
"slack",
|
|
168
|
+
new Error("Slack bot token is required")
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const useSocketMode = !!(slackCreds.appToken || this.config.socketMode);
|
|
173
|
+
const appOptions = {
|
|
174
|
+
token: slackCreds.botToken
|
|
175
|
+
};
|
|
176
|
+
if (useSocketMode) {
|
|
177
|
+
if (!slackCreds.appToken) {
|
|
178
|
+
throw new ConnectionError(
|
|
179
|
+
"slack",
|
|
180
|
+
new Error("App token (appToken) is required for Socket Mode")
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
appOptions.socketMode = true;
|
|
184
|
+
appOptions.appToken = slackCreds.appToken;
|
|
185
|
+
} else if (slackCreds.signingSecret) {
|
|
186
|
+
appOptions.signingSecret = slackCreds.signingSecret;
|
|
187
|
+
if (this.config.port) {
|
|
188
|
+
appOptions.port = this.config.port;
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
throw new ConnectionError(
|
|
192
|
+
"slack",
|
|
193
|
+
new Error(
|
|
194
|
+
"Either appToken (for Socket Mode) or signingSecret (for Events API) is required"
|
|
195
|
+
)
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
this.app = new App(appOptions);
|
|
199
|
+
this.setupEventListeners();
|
|
200
|
+
await this.app.start();
|
|
201
|
+
if (process.env.NODE_ENV !== "production") {
|
|
202
|
+
console.log(
|
|
203
|
+
`\u2705 Slack adapter connected (${useSocketMode ? "Socket Mode" : "Events API"})`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
throw new ConnectionError("slack", error);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Disconnect from Slack
|
|
212
|
+
*/
|
|
213
|
+
async disconnect() {
|
|
214
|
+
if (this.app) {
|
|
215
|
+
await this.app.stop();
|
|
216
|
+
this.app = null;
|
|
217
|
+
}
|
|
218
|
+
this.messageCache.clear();
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Check if connected
|
|
222
|
+
*/
|
|
223
|
+
isConnected() {
|
|
224
|
+
return this.app !== null;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Send a message to a channel
|
|
228
|
+
*/
|
|
229
|
+
async sendMessage(channelId, text, options) {
|
|
230
|
+
if (!this.app) {
|
|
231
|
+
return err(new ConnectionError("slack", new Error("Not connected")));
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
const slackOptions = options?.slack;
|
|
235
|
+
const result = await this.app.client.chat.postMessage({
|
|
236
|
+
channel: channelId,
|
|
237
|
+
text,
|
|
238
|
+
thread_ts: options?.threadId || slackOptions?.thread_ts,
|
|
239
|
+
blocks: slackOptions?.blocks,
|
|
240
|
+
unfurl_links: slackOptions?.unfurl_links,
|
|
241
|
+
unfurl_media: slackOptions?.unfurl_media,
|
|
242
|
+
metadata: slackOptions?.metadata
|
|
243
|
+
});
|
|
244
|
+
if (!result.ok || !result.message) {
|
|
245
|
+
return err(
|
|
246
|
+
new MessageSendError(
|
|
247
|
+
"slack",
|
|
248
|
+
channelId,
|
|
249
|
+
new Error(result.error || "Failed to send message")
|
|
250
|
+
)
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
const unifiedMessage = normalizeMessage(result.message);
|
|
254
|
+
if (!unifiedMessage.channelId) {
|
|
255
|
+
unifiedMessage.channelId = channelId;
|
|
256
|
+
}
|
|
257
|
+
this.cacheMessage(unifiedMessage);
|
|
258
|
+
return ok(unifiedMessage);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
return err(
|
|
261
|
+
new MessageSendError(
|
|
262
|
+
"slack",
|
|
263
|
+
channelId,
|
|
264
|
+
error instanceof Error ? error : new Error(String(error))
|
|
265
|
+
)
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Edit a message
|
|
271
|
+
*/
|
|
272
|
+
async editMessage(messageRef, newText) {
|
|
273
|
+
if (!this.app) {
|
|
274
|
+
return err(new ConnectionError("slack", new Error("Not connected")));
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
const messageId = typeof messageRef === "string" ? messageRef : messageRef.id;
|
|
278
|
+
let channelId;
|
|
279
|
+
if (typeof messageRef === "string") {
|
|
280
|
+
const context = this.messageCache.get(messageRef);
|
|
281
|
+
if (!context) {
|
|
282
|
+
this.cacheMisses++;
|
|
283
|
+
this.logCacheStats();
|
|
284
|
+
return err(
|
|
285
|
+
new MessageEditError(
|
|
286
|
+
"slack",
|
|
287
|
+
messageRef,
|
|
288
|
+
new Error(
|
|
289
|
+
'Cannot edit message: channel context not found.\\n\\nThis happens when:\\n1. The message is older than 1 hour (cache expired)\\n2. The bot restarted since the message was sent\\n3. The message was sent by another bot instance\\n\\nSolution: Pass the full message object instead:\\n bot.editMessage(message, "text") // \u2705 Works reliably\\n bot.editMessage(message.id, "text") // \u274C May fail on Slack'
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
this.cacheHits++;
|
|
295
|
+
this.logCacheStats();
|
|
296
|
+
channelId = context.channelId;
|
|
297
|
+
} else {
|
|
298
|
+
channelId = messageRef.channelId;
|
|
299
|
+
}
|
|
300
|
+
const result = await this.app.client.chat.update({
|
|
301
|
+
channel: channelId,
|
|
302
|
+
ts: messageId,
|
|
303
|
+
text: newText
|
|
304
|
+
});
|
|
305
|
+
if (!result.ok || !result.message) {
|
|
306
|
+
return err(
|
|
307
|
+
new MessageEditError(
|
|
308
|
+
"slack",
|
|
309
|
+
messageId,
|
|
310
|
+
new Error(result.error || "Failed to edit message")
|
|
311
|
+
)
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
return ok(normalizeMessage(result.message));
|
|
315
|
+
} catch (error) {
|
|
316
|
+
const messageId = typeof messageRef === "string" ? messageRef : messageRef.id;
|
|
317
|
+
return err(
|
|
318
|
+
new MessageEditError(
|
|
319
|
+
"slack",
|
|
320
|
+
messageId,
|
|
321
|
+
error instanceof Error ? error : new Error(String(error))
|
|
322
|
+
)
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Delete a message
|
|
328
|
+
*/
|
|
329
|
+
async deleteMessage(messageRef) {
|
|
330
|
+
if (!this.app) {
|
|
331
|
+
return err(new ConnectionError("slack", new Error("Not connected")));
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
const messageId = typeof messageRef === "string" ? messageRef : messageRef.id;
|
|
335
|
+
let channelId;
|
|
336
|
+
if (typeof messageRef === "string") {
|
|
337
|
+
const context = this.messageCache.get(messageRef);
|
|
338
|
+
if (!context) {
|
|
339
|
+
this.cacheMisses++;
|
|
340
|
+
this.logCacheStats();
|
|
341
|
+
return err(
|
|
342
|
+
new MessageDeleteError(
|
|
343
|
+
"slack",
|
|
344
|
+
messageRef,
|
|
345
|
+
new Error(
|
|
346
|
+
"Cannot delete message: channel context not found.\\n\\nSolution: Pass the full message object instead:\\n bot.deleteMessage(message) // \u2705 Works reliably\\n bot.deleteMessage(message.id) // \u274C May fail on Slack"
|
|
347
|
+
)
|
|
348
|
+
)
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
this.cacheHits++;
|
|
352
|
+
this.logCacheStats();
|
|
353
|
+
channelId = context.channelId;
|
|
354
|
+
} else {
|
|
355
|
+
channelId = messageRef.channelId;
|
|
356
|
+
}
|
|
357
|
+
const result = await this.app.client.chat.delete({
|
|
358
|
+
channel: channelId,
|
|
359
|
+
ts: messageId
|
|
360
|
+
});
|
|
361
|
+
if (!result.ok) {
|
|
362
|
+
return err(
|
|
363
|
+
new MessageDeleteError(
|
|
364
|
+
"slack",
|
|
365
|
+
messageId,
|
|
366
|
+
new Error(result.error || "Failed to delete message")
|
|
367
|
+
)
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
this.messageCache.delete(messageId);
|
|
371
|
+
return ok(void 0);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
const messageId = typeof messageRef === "string" ? messageRef : messageRef.id;
|
|
374
|
+
return err(
|
|
375
|
+
new MessageDeleteError(
|
|
376
|
+
"slack",
|
|
377
|
+
messageId,
|
|
378
|
+
error instanceof Error ? error : new Error(String(error))
|
|
379
|
+
)
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Add a reaction to a message
|
|
385
|
+
*/
|
|
386
|
+
async addReaction(messageRef, emoji) {
|
|
387
|
+
if (!this.app) {
|
|
388
|
+
return err(new ConnectionError("slack", new Error("Not connected")));
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
const messageId = typeof messageRef === "string" ? messageRef : messageRef.id;
|
|
392
|
+
let channelId;
|
|
393
|
+
if (typeof messageRef === "string") {
|
|
394
|
+
const context = this.messageCache.get(messageRef);
|
|
395
|
+
if (!context) {
|
|
396
|
+
this.cacheMisses++;
|
|
397
|
+
this.logCacheStats();
|
|
398
|
+
return err(
|
|
399
|
+
new ReactionError(
|
|
400
|
+
"slack",
|
|
401
|
+
messageRef,
|
|
402
|
+
emoji,
|
|
403
|
+
new Error(
|
|
404
|
+
"Cannot add reaction: channel context not found.\\n\\nSolution: Pass the full message object instead:\\n bot.addReaction(message, emoji) // \u2705 Works reliably\\n bot.addReaction(message.id, emoji) // \u274C May fail on Slack"
|
|
405
|
+
)
|
|
406
|
+
)
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
this.cacheHits++;
|
|
410
|
+
this.logCacheStats();
|
|
411
|
+
channelId = context.channelId;
|
|
412
|
+
} else {
|
|
413
|
+
channelId = messageRef.channelId;
|
|
414
|
+
}
|
|
415
|
+
const slackEmoji = toSlackEmoji(emoji);
|
|
416
|
+
const result = await this.app.client.reactions.add({
|
|
417
|
+
channel: channelId,
|
|
418
|
+
timestamp: messageId,
|
|
419
|
+
name: slackEmoji.replace(/^:|:$/g, "")
|
|
420
|
+
// Slack API wants emoji without colons
|
|
421
|
+
});
|
|
422
|
+
if (!result.ok) {
|
|
423
|
+
return err(
|
|
424
|
+
new ReactionError(
|
|
425
|
+
"slack",
|
|
426
|
+
messageId,
|
|
427
|
+
emoji,
|
|
428
|
+
new Error(result.error || "Failed to add reaction")
|
|
429
|
+
)
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
return ok(void 0);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
const messageId = typeof messageRef === "string" ? messageRef : messageRef.id;
|
|
435
|
+
return err(
|
|
436
|
+
new ReactionError(
|
|
437
|
+
"slack",
|
|
438
|
+
messageId,
|
|
439
|
+
emoji,
|
|
440
|
+
error instanceof Error ? error : new Error(String(error))
|
|
441
|
+
)
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Remove a reaction from a message
|
|
447
|
+
*/
|
|
448
|
+
async removeReaction(messageRef, emoji) {
|
|
449
|
+
if (!this.app) {
|
|
450
|
+
return err(new ConnectionError("slack", new Error("Not connected")));
|
|
451
|
+
}
|
|
452
|
+
try {
|
|
453
|
+
const messageId = typeof messageRef === "string" ? messageRef : messageRef.id;
|
|
454
|
+
let channelId;
|
|
455
|
+
if (typeof messageRef === "string") {
|
|
456
|
+
const context = this.messageCache.get(messageRef);
|
|
457
|
+
if (!context) {
|
|
458
|
+
this.cacheMisses++;
|
|
459
|
+
this.logCacheStats();
|
|
460
|
+
return err(
|
|
461
|
+
new ReactionError(
|
|
462
|
+
"slack",
|
|
463
|
+
messageRef,
|
|
464
|
+
emoji,
|
|
465
|
+
new Error(
|
|
466
|
+
"Cannot remove reaction: channel context not found.\\n\\nSolution: Pass the full message object instead:\\n bot.removeReaction(message, emoji) // \u2705 Works reliably\\n bot.removeReaction(message.id, emoji) // \u274C May fail on Slack"
|
|
467
|
+
)
|
|
468
|
+
)
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
this.cacheHits++;
|
|
472
|
+
this.logCacheStats();
|
|
473
|
+
channelId = context.channelId;
|
|
474
|
+
} else {
|
|
475
|
+
channelId = messageRef.channelId;
|
|
476
|
+
}
|
|
477
|
+
const slackEmoji = toSlackEmoji(emoji);
|
|
478
|
+
const result = await this.app.client.reactions.remove({
|
|
479
|
+
channel: channelId,
|
|
480
|
+
timestamp: messageId,
|
|
481
|
+
name: slackEmoji.replace(/^:|:$/g, "")
|
|
482
|
+
// Slack API wants emoji without colons
|
|
483
|
+
});
|
|
484
|
+
if (!result.ok) {
|
|
485
|
+
return err(
|
|
486
|
+
new ReactionError(
|
|
487
|
+
"slack",
|
|
488
|
+
messageId,
|
|
489
|
+
emoji,
|
|
490
|
+
new Error(result.error || "Failed to remove reaction")
|
|
491
|
+
)
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
return ok(void 0);
|
|
495
|
+
} catch (error) {
|
|
496
|
+
const messageId = typeof messageRef === "string" ? messageRef : messageRef.id;
|
|
497
|
+
return err(
|
|
498
|
+
new ReactionError(
|
|
499
|
+
"slack",
|
|
500
|
+
messageId,
|
|
501
|
+
emoji,
|
|
502
|
+
error instanceof Error ? error : new Error(String(error))
|
|
503
|
+
)
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Create a thread (reply to a message)
|
|
509
|
+
*/
|
|
510
|
+
async createThread(messageRef, text) {
|
|
511
|
+
if (!this.app) {
|
|
512
|
+
return err(new ConnectionError("slack", new Error("Not connected")));
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
const messageId = typeof messageRef === "string" ? messageRef : messageRef.id;
|
|
516
|
+
let channelId;
|
|
517
|
+
if (typeof messageRef === "string") {
|
|
518
|
+
const context = this.messageCache.get(messageRef);
|
|
519
|
+
if (!context) {
|
|
520
|
+
this.cacheMisses++;
|
|
521
|
+
this.logCacheStats();
|
|
522
|
+
return err(
|
|
523
|
+
new MessageSendError(
|
|
524
|
+
"slack",
|
|
525
|
+
"unknown",
|
|
526
|
+
new Error(
|
|
527
|
+
"Cannot create thread: channel context not found.\\n\\nSolution: Pass the full message object instead:\\n bot.createThread(message, text) // \u2705 Works reliably\\n bot.createThread(message.id, text) // \u274C May fail on Slack"
|
|
528
|
+
)
|
|
529
|
+
)
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
this.cacheHits++;
|
|
533
|
+
this.logCacheStats();
|
|
534
|
+
channelId = context.channelId;
|
|
535
|
+
} else {
|
|
536
|
+
channelId = messageRef.channelId;
|
|
537
|
+
}
|
|
538
|
+
const result = await this.app.client.chat.postMessage({
|
|
539
|
+
channel: channelId,
|
|
540
|
+
text,
|
|
541
|
+
thread_ts: messageId
|
|
542
|
+
// This creates/replies in a thread
|
|
543
|
+
});
|
|
544
|
+
if (!result.ok || !result.message) {
|
|
545
|
+
return err(
|
|
546
|
+
new MessageSendError(
|
|
547
|
+
"slack",
|
|
548
|
+
channelId,
|
|
549
|
+
new Error(result.error || "Failed to create thread")
|
|
550
|
+
)
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
const unifiedMessage = normalizeMessage(result.message);
|
|
554
|
+
this.cacheMessage(unifiedMessage);
|
|
555
|
+
return ok(unifiedMessage);
|
|
556
|
+
} catch (error) {
|
|
557
|
+
const channelId = typeof messageRef === "string" ? "unknown" : messageRef.channelId;
|
|
558
|
+
return err(
|
|
559
|
+
new MessageSendError(
|
|
560
|
+
"slack",
|
|
561
|
+
channelId,
|
|
562
|
+
error instanceof Error ? error : new Error(String(error))
|
|
563
|
+
)
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Upload a file to a channel
|
|
569
|
+
*/
|
|
570
|
+
async uploadFile(channelId, file, options) {
|
|
571
|
+
if (!this.app) {
|
|
572
|
+
return err(new ConnectionError("slack", new Error("Not connected")));
|
|
573
|
+
}
|
|
574
|
+
try {
|
|
575
|
+
return err(
|
|
576
|
+
new MessageSendError(
|
|
577
|
+
"slack",
|
|
578
|
+
channelId,
|
|
579
|
+
new Error("File upload not yet implemented for Slack adapter")
|
|
580
|
+
)
|
|
581
|
+
);
|
|
582
|
+
} catch (error) {
|
|
583
|
+
return err(
|
|
584
|
+
new MessageSendError(
|
|
585
|
+
"slack",
|
|
586
|
+
channelId,
|
|
587
|
+
error instanceof Error ? error : new Error(String(error))
|
|
588
|
+
)
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Subscribe to platform events
|
|
594
|
+
*/
|
|
595
|
+
onEvent(handler) {
|
|
596
|
+
this.eventHandlers.add(handler);
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Get list of channels
|
|
600
|
+
*/
|
|
601
|
+
async getChannels() {
|
|
602
|
+
if (!this.app) {
|
|
603
|
+
return err(new ConnectionError("slack", new Error("Not connected")));
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
const result = await this.app.client.conversations.list({
|
|
607
|
+
types: "public_channel,private_channel"
|
|
608
|
+
});
|
|
609
|
+
if (!result.ok || !result.channels) {
|
|
610
|
+
return err(
|
|
611
|
+
new Error(result.error || "Failed to fetch channels")
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
const channels = result.channels.map((channel) => ({
|
|
615
|
+
id: channel.id,
|
|
616
|
+
name: channel.name || "unknown",
|
|
617
|
+
type: "text",
|
|
618
|
+
isPrivate: channel.is_private || false,
|
|
619
|
+
topic: channel.topic?.value
|
|
620
|
+
}));
|
|
621
|
+
return ok(channels);
|
|
622
|
+
} catch (error) {
|
|
623
|
+
return err(error instanceof Error ? error : new Error(String(error)));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Get list of users
|
|
628
|
+
*/
|
|
629
|
+
async getUsers(channelId) {
|
|
630
|
+
if (!this.app) {
|
|
631
|
+
return err(new ConnectionError("slack", new Error("Not connected")));
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
if (channelId) {
|
|
635
|
+
const result = await this.app.client.conversations.members({
|
|
636
|
+
channel: channelId
|
|
637
|
+
});
|
|
638
|
+
if (!result.ok || !result.members) {
|
|
639
|
+
return err(
|
|
640
|
+
new Error(result.error || "Failed to fetch channel members")
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
const users = [];
|
|
644
|
+
for (const userId of result.members) {
|
|
645
|
+
const userInfo = await this.app.client.users.info({ user: userId });
|
|
646
|
+
if (userInfo.ok && userInfo.user) {
|
|
647
|
+
users.push({
|
|
648
|
+
id: userInfo.user.id,
|
|
649
|
+
username: userInfo.user.name || "unknown",
|
|
650
|
+
displayName: userInfo.user.real_name || userInfo.user.profile?.display_name,
|
|
651
|
+
isBot: userInfo.user.is_bot || false,
|
|
652
|
+
avatarUrl: userInfo.user.profile?.image_512
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return ok(users);
|
|
657
|
+
} else {
|
|
658
|
+
const result = await this.app.client.users.list();
|
|
659
|
+
if (!result.ok || !result.members) {
|
|
660
|
+
return err(
|
|
661
|
+
new Error(result.error || "Failed to fetch users")
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
const users = result.members.map((user) => ({
|
|
665
|
+
id: user.id,
|
|
666
|
+
username: user.name || "unknown",
|
|
667
|
+
displayName: user.real_name || user.profile?.display_name,
|
|
668
|
+
isBot: user.is_bot || false,
|
|
669
|
+
avatarUrl: user.profile?.image_512
|
|
670
|
+
}));
|
|
671
|
+
return ok(users);
|
|
672
|
+
}
|
|
673
|
+
} catch (error) {
|
|
674
|
+
return err(error instanceof Error ? error : new Error(String(error)));
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Normalize platform message to UnifiedMessage
|
|
679
|
+
*/
|
|
680
|
+
normalizeMessage(platformMessage) {
|
|
681
|
+
return normalizeMessage(platformMessage);
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Normalize platform event to UnifiedEvent
|
|
685
|
+
*/
|
|
686
|
+
normalizeEvent(platformEvent) {
|
|
687
|
+
const event = platformEvent;
|
|
688
|
+
if (event.type === "message" || event.message) {
|
|
689
|
+
return normalizeMessageEvent(event);
|
|
690
|
+
}
|
|
691
|
+
if (event.type === "reaction_added") {
|
|
692
|
+
return normalizeReactionEvent(event, "added");
|
|
693
|
+
}
|
|
694
|
+
if (event.type === "reaction_removed") {
|
|
695
|
+
return normalizeReactionEvent(event, "removed");
|
|
696
|
+
}
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Set up event listeners for Slack
|
|
701
|
+
*/
|
|
702
|
+
setupEventListeners() {
|
|
703
|
+
if (!this.app) return;
|
|
704
|
+
this.app.message(async ({ message }) => {
|
|
705
|
+
if ("subtype" in message && message.subtype !== void 0) {
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const unifiedMessage = normalizeMessage(message);
|
|
709
|
+
this.cacheMessage(unifiedMessage);
|
|
710
|
+
const event = {
|
|
711
|
+
type: "message",
|
|
712
|
+
message: unifiedMessage
|
|
713
|
+
};
|
|
714
|
+
this.eventHandlers.forEach((handler) => {
|
|
715
|
+
try {
|
|
716
|
+
handler(event);
|
|
717
|
+
} catch (error) {
|
|
718
|
+
console.error("[Switchboard] Error in message handler:", error);
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
this.app.event("reaction_added", async ({ event }) => {
|
|
723
|
+
const reactionEvent = normalizeReactionEvent(event, "added");
|
|
724
|
+
this.eventHandlers.forEach((handler) => {
|
|
725
|
+
try {
|
|
726
|
+
handler(reactionEvent);
|
|
727
|
+
} catch (error) {
|
|
728
|
+
console.error("[Switchboard] Error in reaction handler:", error);
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
this.app.event("reaction_removed", async ({ event }) => {
|
|
733
|
+
const reactionEvent = normalizeReactionEvent(event, "removed");
|
|
734
|
+
this.eventHandlers.forEach((handler) => {
|
|
735
|
+
try {
|
|
736
|
+
handler(reactionEvent);
|
|
737
|
+
} catch (error) {
|
|
738
|
+
console.error("[Switchboard] Error in reaction handler:", error);
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Cache a message's context
|
|
745
|
+
*/
|
|
746
|
+
cacheMessage(message) {
|
|
747
|
+
this.messageCache.set(message.id, {
|
|
748
|
+
channelId: message.channelId,
|
|
749
|
+
threadId: message.threadId,
|
|
750
|
+
timestamp: message.timestamp
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Log cache statistics (every 1000 operations)
|
|
755
|
+
*/
|
|
756
|
+
logCacheStats() {
|
|
757
|
+
const total = this.cacheHits + this.cacheMisses;
|
|
758
|
+
if (total > 0 && total % 1e3 === 0) {
|
|
759
|
+
const hitRate = (this.cacheHits / total * 100).toFixed(1);
|
|
760
|
+
console.log(`[Switchboard] Slack cache hit rate: ${hitRate}% (${this.cacheHits}/${total})`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
// src/register.ts
|
|
766
|
+
var slackAdapter = new SlackAdapter();
|
|
767
|
+
registry.register("slack", slackAdapter);
|
|
768
|
+
if (process.env.NODE_ENV !== "production") {
|
|
769
|
+
console.log("[Switchboard] Slack adapter registered");
|
|
770
|
+
}
|
|
771
|
+
export {
|
|
772
|
+
SlackAdapter,
|
|
773
|
+
normalizeEmoji,
|
|
774
|
+
normalizeMessage,
|
|
775
|
+
normalizeMessageEvent,
|
|
776
|
+
normalizeReactionEvent,
|
|
777
|
+
toSlackEmoji
|
|
778
|
+
};
|
|
779
|
+
//# sourceMappingURL=index.js.map
|