@hardlydifficult/chat 1.1.63 → 1.1.64
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 +246 -216
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hardlydifficult/chat
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A unified API for Discord and Slack messaging with rich document support, threading, reactions, and bulk operations.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -13,32 +13,248 @@ npm install @hardlydifficult/chat
|
|
|
13
13
|
```typescript
|
|
14
14
|
import { createChatClient } from "@hardlydifficult/chat";
|
|
15
15
|
|
|
16
|
+
// Connect to Discord or Slack
|
|
16
17
|
const client = createChatClient({ type: "discord" });
|
|
17
|
-
|
|
18
|
-
console.log(client.me?.mention); // "<@123...>"
|
|
18
|
+
// or { type: "slack" }
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
await channel.postMessage("Hello!");
|
|
20
|
+
const channel = await client.connect("channel-id");
|
|
21
|
+
await channel.postMessage("Hello world!").addReactions(["👍", "👎"]);
|
|
22
|
+
```
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
## Core Concepts
|
|
25
|
+
|
|
26
|
+
### Message Operations
|
|
27
|
+
|
|
28
|
+
Messages returned from `postMessage()` support chainable reaction and management operations.
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
const msg = await channel
|
|
32
|
+
.postMessage("Vote: 1, 2, or 3")
|
|
33
|
+
.addReactions(["1️⃣", "2️⃣", "3️⃣"])
|
|
34
|
+
.onReaction((event) => console.log(`${event.user.username} voted ${event.emoji}`));
|
|
35
|
+
|
|
36
|
+
await msg.update("Final count in thread...");
|
|
37
|
+
await msg.delete({ cascadeReplies: false });
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
#### Reply Messages
|
|
41
|
+
|
|
42
|
+
Replies can be awaited like promises and support reactions before resolution.
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
const reply = await msg.reply("Counting votes...");
|
|
46
|
+
await reply.update("12 votes for pizza");
|
|
47
|
+
await reply.addReactions(["🎉"]);
|
|
48
|
+
await reply.waitForReactions();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Streaming Replies
|
|
52
|
+
|
|
53
|
+
Stream text into threads with automatic batching, chunking, and platform limit handling.
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
const stream = thread.stream(1000, abortSignal);
|
|
57
|
+
stream.append("Processing...\n");
|
|
58
|
+
stream.append("Result: 42\n");
|
|
59
|
+
await stream.stop();
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
#### Editable Stream
|
|
63
|
+
|
|
64
|
+
Updates a single message in-place instead of creating new messages.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
const editableStream = thread.editableStream(2000);
|
|
68
|
+
editableStream.append("Step 1...\n");
|
|
69
|
+
editableStream.append("Step 2...\n");
|
|
70
|
+
await editableStream.stop(); // posts one message, edits it twice
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Threads
|
|
74
|
+
|
|
75
|
+
Create and manage conversational threads anchored to messages.
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
const thread = await channel.createThread("Topic", "Session-1");
|
|
79
|
+
await thread.post("How can I help?");
|
|
80
|
+
thread.onReply(async (msg) => {
|
|
81
|
+
await thread.post(`You said: ${msg.content}`);
|
|
26
82
|
});
|
|
83
|
+
await thread.delete();
|
|
27
84
|
```
|
|
28
85
|
|
|
29
|
-
|
|
86
|
+
### Batching Messages
|
|
87
|
+
|
|
88
|
+
Group related messages with post-commit operations.
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
const batch = await channel.beginBatch({ key: "report" });
|
|
92
|
+
await batch.post("Line 1");
|
|
93
|
+
await batch.post("Line 2");
|
|
94
|
+
await batch.finish();
|
|
95
|
+
|
|
96
|
+
await batch.deleteAll();
|
|
97
|
+
await batch.keepLatest(5);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### With Batch Helper
|
|
101
|
+
|
|
102
|
+
Auto-finish batch even on errors.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
await channel.withBatch(async (batch) => {
|
|
106
|
+
await batch.post("First");
|
|
107
|
+
await batch.post("Second");
|
|
108
|
+
throw new Error("boom"); // batch.finish() called in finally
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Typing Indicators
|
|
113
|
+
|
|
114
|
+
Show typing indicators for long-running work.
|
|
30
115
|
|
|
31
116
|
```typescript
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
117
|
+
channel.beginTyping();
|
|
118
|
+
try {
|
|
119
|
+
await longRunningTask();
|
|
120
|
+
} finally {
|
|
121
|
+
channel.endTyping();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await channel.withTyping(() => processMessages());
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Message Cleanup
|
|
128
|
+
|
|
129
|
+
Convenience methods for bulk message management.
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// Keep newest 10, delete rest
|
|
133
|
+
await channel.pruneMessages({ keep: 10 });
|
|
134
|
+
|
|
135
|
+
// Fetch bot's recent messages
|
|
136
|
+
const botMessages = await channel.getRecentBotMessages(50);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Member Matching
|
|
140
|
+
|
|
141
|
+
Resolve users by mention, username, display name, or email.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
await channel.resolveMention("@nick"); // "<@U123>"
|
|
145
|
+
await channel.resolveMention("Nick Mancuso"); // "<@U123>"
|
|
146
|
+
await channel.resolveMention("nick@example.com"); // "<@U123>"
|
|
147
|
+
|
|
148
|
+
const member = await channel.findMember("nick");
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Message Tracker
|
|
152
|
+
|
|
153
|
+
Track messages by key for later editing.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
const tracker = createMessageTracker((content) => channel.postMessage(content));
|
|
157
|
+
tracker.post("status-worker-1", "🔴 Worker disconnected");
|
|
158
|
+
// Later:
|
|
159
|
+
tracker.edit("status-worker-1", "🟢 Worker reconnected");
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Command System
|
|
163
|
+
|
|
164
|
+
The built-in command framework supports auto-parsed arguments, typing indicators, and message cleanup.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { CommandRegistry, CommandDispatcher, setupJobLifecycle } from "@hardlydifficult/chat";
|
|
168
|
+
|
|
169
|
+
const registry = new CommandRegistry();
|
|
170
|
+
|
|
171
|
+
registry.register("tools", {
|
|
172
|
+
prefix: "merge",
|
|
173
|
+
description: "Merge pull requests",
|
|
174
|
+
args: { type: "rest", argName: "query" },
|
|
175
|
+
execute: async (ctx, args) => {
|
|
176
|
+
const { thread, abortController } = setupJobLifecycle({
|
|
177
|
+
originalMessage: ctx.incomingMessage,
|
|
178
|
+
thread: await ctx.startThread("Merge"),
|
|
179
|
+
abortController: new AbortController(),
|
|
180
|
+
ownerUsername: ctx.incomingMessage.author?.username!,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Use abortController.signal to support cancellation
|
|
184
|
+
const result = await mergePRs(args.query, abortController.signal);
|
|
185
|
+
await thread.post(result);
|
|
186
|
+
thread.complete();
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const dispatcher = new CommandDispatcher({
|
|
191
|
+
channel,
|
|
192
|
+
registry,
|
|
193
|
+
state: { inFlightCommands: new Set() },
|
|
194
|
+
});
|
|
195
|
+
channel.onMessage((msg) => dispatcher.handleMessage(msg));
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Platform Config
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
// Discord
|
|
202
|
+
createChatClient({
|
|
203
|
+
type: "discord",
|
|
204
|
+
token: process.env.DISCORD_TOKEN,
|
|
205
|
+
guildId: process.env.DISCORD_GUILD_ID,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Slack
|
|
209
|
+
createChatClient({
|
|
210
|
+
type: "slack",
|
|
211
|
+
token: process.env.SLACK_BOT_TOKEN,
|
|
212
|
+
appToken: process.env.SLACK_APP_TOKEN,
|
|
213
|
+
socketMode: true,
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Document Output
|
|
218
|
+
|
|
219
|
+
Convert structured documents to platform-native rich text.
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
import { Document, header, text, list, divider, context } from "@hardlydifficult/document-generator";
|
|
223
|
+
|
|
224
|
+
const doc = new Document()
|
|
225
|
+
.add(header("Status Report"))
|
|
226
|
+
.add(divider())
|
|
227
|
+
.add(text("All systems operational."))
|
|
228
|
+
.add(list(["API: ✅", "DB: ✅", "Cache: ✅"]))
|
|
229
|
+
.add(context("Generated at " + new Date().toISOString()));
|
|
230
|
+
|
|
231
|
+
await channel.postMessage(doc);
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Typing
|
|
35
235
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
236
|
+
All core types are exported for direct use.
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
import type { Member, Message, Thread, MessageBatch } from "@hardlydifficult/chat";
|
|
39
240
|
```
|
|
40
241
|
|
|
41
|
-
##
|
|
242
|
+
## Appendix
|
|
243
|
+
|
|
244
|
+
### Platform Differences
|
|
245
|
+
|
|
246
|
+
| Feature | Discord | Slack |
|
|
247
|
+
|------------------------|-----------------------------------|-----------------------------------|
|
|
248
|
+
| Typing indicators | ✅ Supported | ❌ No API support (no-op) |
|
|
249
|
+
| Message length limit | 2000 characters | 4000 characters |
|
|
250
|
+
| Thread creation | Explicit thread channel | Implicit via parent message ts |
|
|
251
|
+
| Bulk delete | ✅ Up to 100 messages at once | ❌ Must delete one-by-one |
|
|
252
|
+
| Emoji format | Plain Unicode or `:name:` | Colon-wrapped `:name:` |
|
|
253
|
+
| File uploads | As attachments | Via `filesUploadV2` API |
|
|
254
|
+
|
|
255
|
+
### Additional Features from Current README
|
|
256
|
+
|
|
257
|
+
#### Bot Identity
|
|
42
258
|
|
|
43
259
|
After `connect()`, `client.me` exposes the authenticated bot user:
|
|
44
260
|
|
|
@@ -51,7 +267,7 @@ console.log(client.me?.username); // "sprint-bot"
|
|
|
51
267
|
console.log(client.me?.mention); // "<@U09B00R2R96>"
|
|
52
268
|
```
|
|
53
269
|
|
|
54
|
-
|
|
270
|
+
#### Incoming Messages
|
|
55
271
|
|
|
56
272
|
Subscribe to new messages in a channel. The callback receives a full `Message` object — you can delete it, react to it, or reply in its thread.
|
|
57
273
|
|
|
@@ -75,26 +291,7 @@ unsubscribe();
|
|
|
75
291
|
|
|
76
292
|
Messages from the bot itself are automatically filtered out.
|
|
77
293
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
Content can be a string or a `Document` from `@hardlydifficult/document-generator`.
|
|
81
|
-
|
|
82
|
-
```typescript
|
|
83
|
-
// Simple text
|
|
84
|
-
channel.postMessage("Hello!");
|
|
85
|
-
|
|
86
|
-
// Rich document (auto-converted to Discord Embed / Slack Block Kit)
|
|
87
|
-
import { Document } from "@hardlydifficult/document-generator";
|
|
88
|
-
|
|
89
|
-
const report = new Document()
|
|
90
|
-
.header("Daily Report")
|
|
91
|
-
.text("Here are today's **highlights**:")
|
|
92
|
-
.list(["Feature A completed", "Bug B fixed", "99.9% uptime"]);
|
|
93
|
-
|
|
94
|
-
channel.postMessage(report);
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### Oversized Message Handling
|
|
294
|
+
#### Oversized Message Handling
|
|
98
295
|
|
|
99
296
|
Messages that exceed platform limits (Discord: 2000 chars, Slack: 4000 chars) are handled automatically:
|
|
100
297
|
|
|
@@ -103,7 +300,7 @@ Messages that exceed platform limits (Discord: 2000 chars, Slack: 4000 chars) ar
|
|
|
103
300
|
|
|
104
301
|
No caller changes needed — the library handles this transparently.
|
|
105
302
|
|
|
106
|
-
|
|
303
|
+
#### File Attachments
|
|
107
304
|
|
|
108
305
|
Send files as message attachments.
|
|
109
306
|
|
|
@@ -116,39 +313,15 @@ channel.postMessage("Here's the scan report", {
|
|
|
116
313
|
});
|
|
117
314
|
```
|
|
118
315
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
```typescript
|
|
122
|
-
const msg = await channel.postMessage("Hello");
|
|
123
|
-
|
|
124
|
-
await msg.update("Updated content");
|
|
125
|
-
msg.reply("Thread reply");
|
|
126
|
-
await msg.delete();
|
|
127
|
-
await msg.delete({ cascadeReplies: false }); // keep thread replies
|
|
128
|
-
```
|
|
316
|
+
#### Dismissable Messages
|
|
129
317
|
|
|
130
|
-
|
|
318
|
+
Post a message that the specified user can dismiss by clicking the trash reaction.
|
|
131
319
|
|
|
132
320
|
```typescript
|
|
133
|
-
|
|
134
|
-
.postMessage("Pick one")
|
|
135
|
-
.addReactions(["👍", "👎"])
|
|
136
|
-
.onReaction((event) => {
|
|
137
|
-
console.log(`${event.user.username} reacted with ${event.emoji}`);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
msg.offReaction(); // stop listening
|
|
141
|
-
|
|
142
|
-
// Remove the bot's own reactions
|
|
143
|
-
msg.removeReactions(["👍", "👎"]);
|
|
144
|
-
|
|
145
|
-
// Remove all reactions from the message (from all users)
|
|
146
|
-
msg.removeAllReactions();
|
|
321
|
+
await channel.postDismissable("Build complete!", user.id);
|
|
147
322
|
```
|
|
148
323
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
### Declarative Reactions
|
|
324
|
+
#### Declarative Reactions
|
|
152
325
|
|
|
153
326
|
`setReactions` manages the full reaction state on a message. It diffs against the previous `setReactions` call, removing stale emojis and adding new ones, and replaces any existing reaction handler.
|
|
154
327
|
|
|
@@ -162,37 +335,7 @@ msg.setReactions(["🟡"], (event) => handlePending(event));
|
|
|
162
335
|
msg.setReactions(["🟢"], (event) => handleMerged(event));
|
|
163
336
|
```
|
|
164
337
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
Post a message that the specified user can dismiss by clicking the trash reaction.
|
|
168
|
-
|
|
169
|
-
```typescript
|
|
170
|
-
await channel.postDismissable("Build complete!", user.id);
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
### Message Tracker
|
|
174
|
-
|
|
175
|
-
Track posted messages by key for later editing. Useful for status messages that should be updated in-place (e.g., "Worker disconnected" → "Worker reconnected").
|
|
176
|
-
|
|
177
|
-
```typescript
|
|
178
|
-
import { createMessageTracker } from "@hardlydifficult/chat";
|
|
179
|
-
|
|
180
|
-
const tracker = createMessageTracker((content) => channel.postMessage(content));
|
|
181
|
-
|
|
182
|
-
// Post and track by key
|
|
183
|
-
tracker.post("worker-1", "🔴 Worker disconnected: Server A");
|
|
184
|
-
|
|
185
|
-
// Later, edit the tracked message
|
|
186
|
-
const postedAt = tracker.getPostedAt("worker-1");
|
|
187
|
-
if (postedAt !== undefined) {
|
|
188
|
-
const downtime = Date.now() - postedAt.getTime();
|
|
189
|
-
tracker.edit("worker-1", `🟢 Worker reconnected: Server A (down for ${downtime}ms)`);
|
|
190
|
-
}
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
`post()` is fire-and-forget. `edit()` handles the race where the edit arrives before the original post completes — it chains on the stored promise.
|
|
194
|
-
|
|
195
|
-
### Message Batches
|
|
338
|
+
#### Message Batches
|
|
196
339
|
|
|
197
340
|
Group related posted messages so they can be retrieved and cleaned up together.
|
|
198
341
|
|
|
@@ -225,42 +368,7 @@ await channel.withBatch({ key: "sprint-update" }, async (batch) => {
|
|
|
225
368
|
});
|
|
226
369
|
```
|
|
227
370
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
- `id`, `key`, `author`, `createdAt`, `closedAt`, `isFinished`
|
|
231
|
-
- `messages` (tracked message refs)
|
|
232
|
-
- `post(content, options?)`
|
|
233
|
-
- `deleteAll(options?)`
|
|
234
|
-
- `keepLatest(n, options?)`
|
|
235
|
-
- `finish()`
|
|
236
|
-
|
|
237
|
-
### Threads
|
|
238
|
-
|
|
239
|
-
Create a thread, post messages, and listen for replies. The `Thread` object is the primary interface — all threading internals are hidden.
|
|
240
|
-
|
|
241
|
-
```typescript
|
|
242
|
-
// Create a thread (posts root message + starts thread)
|
|
243
|
-
const thread = await channel.createThread("Starting a session!", "Session");
|
|
244
|
-
|
|
245
|
-
// Post in the thread
|
|
246
|
-
const msg = await thread.post("How can I help?");
|
|
247
|
-
|
|
248
|
-
// Listen for replies
|
|
249
|
-
thread.onReply(async (msg) => {
|
|
250
|
-
await thread.post(`Got: ${msg.content}`);
|
|
251
|
-
// msg.reply() also posts in the same thread
|
|
252
|
-
await msg.reply("Thanks!");
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
// Post with file attachments
|
|
256
|
-
await thread.post("Here's the report", [
|
|
257
|
-
{ content: "# Report\n...", name: "report.md" },
|
|
258
|
-
]);
|
|
259
|
-
|
|
260
|
-
// Stop listening and clean up
|
|
261
|
-
thread.offReply();
|
|
262
|
-
await thread.delete();
|
|
263
|
-
```
|
|
371
|
+
#### Threads (Enhanced)
|
|
264
372
|
|
|
265
373
|
You can also create a thread from an existing message:
|
|
266
374
|
|
|
@@ -277,39 +385,7 @@ await thread.post("I'm back!");
|
|
|
277
385
|
thread.onReply(async (msg) => { /* ... */ });
|
|
278
386
|
```
|
|
279
387
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
```typescript
|
|
283
|
-
const msg = await channel.postMessage("Processing...");
|
|
284
|
-
await msg.reply("Done!", [{ content: resultData, name: "result.json" }]);
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
> **Note:** `channel.onMessage()` only fires for top-level channel messages, not thread replies. Use `thread.onReply()` to listen for thread messages.
|
|
288
|
-
|
|
289
|
-
> **Slack note:** Slack threads are implicit — `createThread()` posts a root message and uses its timestamp as the thread ID.
|
|
290
|
-
|
|
291
|
-
### Streaming Replies
|
|
292
|
-
|
|
293
|
-
Buffer text and flush it as thread replies at a regular interval. Useful for commands that produce output over time (e.g., streaming CLI output). Long text is automatically chunked to fit within platform message limits.
|
|
294
|
-
|
|
295
|
-
```typescript
|
|
296
|
-
const msg = await channel.postMessage("Running...");
|
|
297
|
-
const stream = msg.streamReply(2000); // flush every 2s
|
|
298
|
-
|
|
299
|
-
stream.append("output line 1\n");
|
|
300
|
-
stream.append("output line 2\n");
|
|
301
|
-
// ... text is batched and sent as replies every 2 seconds
|
|
302
|
-
|
|
303
|
-
await stream.stop(); // flushes remaining text and stops the timer
|
|
304
|
-
console.log(stream.content); // full accumulated text across all flushes
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
`flush()` sends buffered text immediately without waiting for the next interval:
|
|
308
|
-
|
|
309
|
-
```typescript
|
|
310
|
-
stream.append("important output");
|
|
311
|
-
await stream.flush();
|
|
312
|
-
```
|
|
388
|
+
#### Streaming Replies (Enhanced)
|
|
313
389
|
|
|
314
390
|
Both `streamReply()`, `thread.stream()`, and `thread.editableStream()` accept an optional `AbortSignal` to automatically stop the stream on cancellation. After abort, `append()` becomes a no-op and `stop()` is called automatically.
|
|
315
391
|
|
|
@@ -322,35 +398,7 @@ controller.abort(); // auto-stops, future appends are ignored
|
|
|
322
398
|
console.log(stream.content); // "working...\n" — only pre-abort text
|
|
323
399
|
```
|
|
324
400
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
Resolve fuzzy member queries directly to mention strings.
|
|
328
|
-
|
|
329
|
-
```typescript
|
|
330
|
-
const mention = await channel.resolveMention("Nick Mancuso");
|
|
331
|
-
await channel.postMessage(`Hey ${mention ?? "@nick"}, check this out!`);
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
You can still inspect members directly:
|
|
335
|
-
|
|
336
|
-
```typescript
|
|
337
|
-
const members = await channel.getMembers();
|
|
338
|
-
const member = await channel.findMember("@alice");
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
Each `Member` has `id`, `username`, `displayName`, `mention`, and (when available) `email`.
|
|
342
|
-
|
|
343
|
-
## Typing Indicator
|
|
344
|
-
|
|
345
|
-
Show a "typing" indicator while processing. `withTyping` sends the indicator immediately, refreshes it every 8 seconds, and cleans up automatically when the function completes.
|
|
346
|
-
|
|
347
|
-
```typescript
|
|
348
|
-
const result = await channel.withTyping(async () => {
|
|
349
|
-
// typing indicator stays active during this work
|
|
350
|
-
return await doExpensiveWork();
|
|
351
|
-
});
|
|
352
|
-
await channel.postMessage(result);
|
|
353
|
-
```
|
|
401
|
+
#### Typing Indicator (Enhanced)
|
|
354
402
|
|
|
355
403
|
For one-shot use, `sendTyping()` sends a single indicator without automatic refresh:
|
|
356
404
|
|
|
@@ -360,9 +408,7 @@ await channel.sendTyping();
|
|
|
360
408
|
|
|
361
409
|
> **Slack note:** Slack does not support bot typing indicators. Both methods are no-ops on Slack.
|
|
362
410
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
Delete messages and manage threads in bulk.
|
|
411
|
+
#### Bulk Operations (Enhanced)
|
|
366
412
|
|
|
367
413
|
```typescript
|
|
368
414
|
// Delete up to 100 recent messages
|
|
@@ -384,7 +430,7 @@ for (const thread of threads) {
|
|
|
384
430
|
|
|
385
431
|
> **Slack note:** Slack has no bulk delete API — messages are deleted one-by-one. Some may fail if the bot lacks permission to delete others' messages. `getThreads()` scans recent channel history for messages with replies.
|
|
386
432
|
|
|
387
|
-
|
|
433
|
+
#### Connection Resilience
|
|
388
434
|
|
|
389
435
|
Both platforms auto-reconnect via their underlying libraries (discord.js and @slack/bolt). Register callbacks for observability.
|
|
390
436
|
|
|
@@ -419,20 +465,4 @@ Both callbacks return an unsubscribe function.
|
|
|
419
465
|
2. Enable Socket Mode, generate App Token
|
|
420
466
|
3. Bot scopes: `chat:write`, `chat:write.public`, `reactions:write`, `reactions:read`, `channels:history`, `channels:read`, `files:write`, `users:read`
|
|
421
467
|
4. Subscribe to events: `reaction_added`, `message.channels`
|
|
422
|
-
5. Set `SLACK_BOT_TOKEN` and `SLACK_APP_TOKEN` env vars
|
|
423
|
-
|
|
424
|
-
## Platform Differences
|
|
425
|
-
|
|
426
|
-
| Feature | Discord | Slack |
|
|
427
|
-
| ----------------- | --------------------------------------------- | ------------------------------------------------ |
|
|
428
|
-
| Message limit | 2000 chars (auto-attaches as file if over) | 4000 chars (auto-uploads as file if over) |
|
|
429
|
-
| Incoming messages | Full support | Full support |
|
|
430
|
-
| Typing indicator | Full support | No-op (unsupported by Slack bot API) |
|
|
431
|
-
| File attachments | `AttachmentBuilder` | `filesUploadV2` |
|
|
432
|
-
| Thread creation | Creates named thread channel on message | Returns message timestamp (threads are implicit) |
|
|
433
|
-
| Thread replies | Messages arrive with `channelId = threadId` | Messages arrive with `thread_ts` on parent channel |
|
|
434
|
-
| Bulk delete | Native `bulkDelete` API (fast) | One-by-one deletion (slower, may partially fail) |
|
|
435
|
-
| Get threads | `fetchActive` + `fetchArchived` | Scans channel history for threaded messages |
|
|
436
|
-
| Delete thread | `ThreadChannel.delete()` | Deletes parent message and all replies |
|
|
437
|
-
| Get members | Guild members filtered by channel permissions | `conversations.members` + `users.info` |
|
|
438
|
-
| Auto-reconnect | Handled by discord.js | Handled by `@slack/bolt` Socket Mode |
|
|
468
|
+
5. Set `SLACK_BOT_TOKEN` and `SLACK_APP_TOKEN` env vars
|