@hardlydifficult/chat 1.1.67 → 1.1.68
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 +249 -331
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hardlydifficult/chat
|
|
2
2
|
|
|
3
|
-
A unified
|
|
3
|
+
A TypeScript library for building chat bots with unified APIs for Discord and Slack, featuring threading, batching, streaming, commands, and cleanup.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -10,6 +10,26 @@ npm install @hardlydifficult/chat
|
|
|
10
10
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
|
+
```typescript
|
|
14
|
+
import { createChatClient } from '@hardlydifficult/chat';
|
|
15
|
+
|
|
16
|
+
// Create a Discord or Slack client based on platform
|
|
17
|
+
const client = createChatClient('discord', {
|
|
18
|
+
token: process.env.DISCORD_TOKEN!,
|
|
19
|
+
// or: 'slack', { token: process.env.SLACK_TOKEN! }
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Listen for messages and respond
|
|
23
|
+
client.onMessage((event) => {
|
|
24
|
+
event.channel.send('Hello, world!');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Start listening
|
|
28
|
+
await client.start();
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Alternative Quick Start (with Message Operations)
|
|
32
|
+
|
|
13
33
|
```typescript
|
|
14
34
|
import { createChatClient } from "@hardlydifficult/chat";
|
|
15
35
|
|
|
@@ -21,448 +41,346 @@ const channel = await client.connect("channel-id");
|
|
|
21
41
|
await channel.postMessage("Hello world!").addReactions(["👍", "👎"]);
|
|
22
42
|
```
|
|
23
43
|
|
|
24
|
-
## Core
|
|
44
|
+
## Core Abstractions
|
|
25
45
|
|
|
26
|
-
###
|
|
46
|
+
### Chat Clients
|
|
27
47
|
|
|
28
|
-
|
|
48
|
+
The library provides platform-specific chat clients through a unified interface.
|
|
29
49
|
|
|
30
50
|
```typescript
|
|
31
|
-
|
|
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
|
-
```
|
|
51
|
+
import { DiscordChatClient, SlackChatClient, createChatClient } from '@hardlydifficult/chat';
|
|
39
52
|
|
|
40
|
-
|
|
53
|
+
// Factory function for any supported platform
|
|
54
|
+
const client = createChatClient('discord', { token: '...' });
|
|
41
55
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const reply = await msg.reply("Counting votes...");
|
|
46
|
-
await reply.update("12 votes for pizza");
|
|
47
|
-
await reply.addReactions(["🎉"]);
|
|
48
|
-
await reply.waitForReactions();
|
|
56
|
+
// Or instantiate directly
|
|
57
|
+
const discordClient = new DiscordChatClient({ token: '...' });
|
|
58
|
+
const slackClient = new SlackChatClient({ token: '...' });
|
|
49
59
|
```
|
|
50
60
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
Each client supports:
|
|
62
|
+
- Message event handling via `onMessage()`
|
|
63
|
+
- Reaction event handling via `onReaction()`
|
|
64
|
+
- Platform-specific startup logic via `start()`
|
|
65
|
+
- Graceful shutdown via `stop()`
|
|
54
66
|
|
|
55
|
-
|
|
56
|
-
const stream = thread.stream(1000, abortSignal);
|
|
57
|
-
stream.append("Processing...\n");
|
|
58
|
-
stream.append("Result: 42\n");
|
|
59
|
-
await stream.stop();
|
|
60
|
-
```
|
|
67
|
+
### Channel and Message
|
|
61
68
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
Updates a single message in-place instead of creating new messages.
|
|
69
|
+
The `Channel` class abstracts messaging operations across platforms.
|
|
65
70
|
|
|
66
71
|
```typescript
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
await
|
|
72
|
+
// Send a message and get a ReplyMessage reference
|
|
73
|
+
const message = await channel.send('Hello');
|
|
74
|
+
await message.react('👍');
|
|
75
|
+
await message.reply('Thanks for the feedback!');
|
|
76
|
+
await message.delete(); // Only the bot's message
|
|
77
|
+
await channel.cleanup('all'); // Delete all bot messages in the channel
|
|
71
78
|
```
|
|
72
79
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
Create and manage conversational threads anchored to messages.
|
|
80
|
+
Messages are represented by the `Message` interface:
|
|
76
81
|
|
|
77
82
|
```typescript
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
interface Message {
|
|
84
|
+
id: string;
|
|
85
|
+
author: Member;
|
|
86
|
+
content: string;
|
|
87
|
+
timestamp: Date;
|
|
88
|
+
reactions: Reaction[];
|
|
89
|
+
thread?: Thread;
|
|
90
|
+
channel: Channel;
|
|
91
|
+
|
|
92
|
+
reply(content: string): Promise<ReplyMessage>;
|
|
93
|
+
update(content: string): Promise<void>;
|
|
94
|
+
delete(): Promise<void>;
|
|
95
|
+
react(emoji: string): Promise<void>;
|
|
96
|
+
clearReactions(): Promise<void>;
|
|
97
|
+
}
|
|
84
98
|
```
|
|
85
99
|
|
|
86
|
-
###
|
|
100
|
+
### Thread
|
|
87
101
|
|
|
88
|
-
|
|
102
|
+
Threads provide an isolated messaging context with its own lifecycle.
|
|
89
103
|
|
|
90
104
|
```typescript
|
|
91
|
-
|
|
92
|
-
await
|
|
93
|
-
await
|
|
94
|
-
await
|
|
95
|
-
|
|
96
|
-
await batch.deleteAll();
|
|
97
|
-
await batch.keepLatest(5);
|
|
105
|
+
// Create and use a thread
|
|
106
|
+
const thread = await channel.createThread('Discussions');
|
|
107
|
+
await thread.send('Let us discuss this here.');
|
|
108
|
+
await thread.sendStream('Streaming responses...');
|
|
109
|
+
await thread.cleanup('bot'); // Remove only bot messages
|
|
98
110
|
```
|
|
99
111
|
|
|
100
|
-
|
|
112
|
+
## Message Batching
|
|
101
113
|
|
|
102
|
-
|
|
114
|
+
Batching allows grouping multiple messages and managing them collectively.
|
|
103
115
|
|
|
104
116
|
```typescript
|
|
105
|
-
|
|
106
|
-
await batch.post("First");
|
|
107
|
-
await batch.post("Second");
|
|
108
|
-
throw new Error("boom"); // batch.finish() called in finally
|
|
109
|
-
});
|
|
110
|
-
```
|
|
117
|
+
import { MessageBatch } from '@hardlydifficult/chat';
|
|
111
118
|
|
|
112
|
-
|
|
119
|
+
// Create a batch
|
|
120
|
+
const batch = channel.batch();
|
|
121
|
+
batch.add(channel.send('First'));
|
|
122
|
+
batch.add(channel.send('Second'));
|
|
113
123
|
|
|
114
|
-
|
|
124
|
+
// Finish and keep only the latest
|
|
125
|
+
await batch.finish({ mode: 'keepLatest' });
|
|
115
126
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
try {
|
|
119
|
-
await longRunningTask();
|
|
120
|
-
} finally {
|
|
121
|
-
channel.endTyping();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
await channel.withTyping(() => processMessages());
|
|
127
|
+
// Or delete everything
|
|
128
|
+
await batch.delete();
|
|
125
129
|
```
|
|
126
130
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
The `MessageBatch` class supports:
|
|
132
|
+
- `add(promise)`: Add a pending message to the batch
|
|
133
|
+
- `finish(options)`: Finalize batch with modes: `'keepAll'`, `'keepLatest'`, `'deleteAll'`
|
|
134
|
+
- `delete()`: Delete all batched messages
|
|
135
|
+
- `query()`: Get list of posted messages
|
|
130
136
|
|
|
131
|
-
|
|
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
|
|
137
|
+
## Streaming Replies
|
|
140
138
|
|
|
141
|
-
|
|
139
|
+
The library supports real-time streaming with automatic message updates.
|
|
142
140
|
|
|
143
141
|
```typescript
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
142
|
+
// Use StreamingReply for automatic chunking
|
|
143
|
+
const stream = channel.stream({ mode: 'thread' });
|
|
144
|
+
stream.append('Hello, ');
|
|
145
|
+
await stream.flush(); // Force immediate delivery
|
|
146
|
+
stream.append('world!');
|
|
147
|
+
await stream.finish(); // Finalize with message deletion or edit
|
|
149
148
|
```
|
|
150
149
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
Track messages by key for later editing.
|
|
150
|
+
The `EditableStreamReply` class allows in-place editing:
|
|
154
151
|
|
|
155
152
|
```typescript
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
153
|
+
const stream = new EditableStreamReply(thread, { maxMessageLength: 2000 });
|
|
154
|
+
stream.append('Initial ');
|
|
155
|
+
await stream.flush();
|
|
156
|
+
stream.append('content.');
|
|
157
|
+
await stream.finish();
|
|
160
158
|
```
|
|
161
159
|
|
|
162
|
-
|
|
160
|
+
Both support:
|
|
161
|
+
- `append(text)`: Add text to buffer
|
|
162
|
+
- `flush()`: Send current buffer as message update
|
|
163
|
+
- `finish()`: Finalize and cleanup
|
|
164
|
+
- `abort()`: Cancel the stream mid-operation
|
|
165
|
+
|
|
166
|
+
## Commands
|
|
163
167
|
|
|
164
|
-
|
|
168
|
+
A regex-based command system with auto-parsing and context-aware routing.
|
|
165
169
|
|
|
166
170
|
```typescript
|
|
167
|
-
import {
|
|
171
|
+
import { CommandDispatcher, CommandRegistry, Command } from '@hardlydifficult/chat';
|
|
168
172
|
|
|
169
173
|
const registry = new CommandRegistry();
|
|
170
174
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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,
|
|
175
|
+
// Register a command with auto-parsed arguments
|
|
176
|
+
registry.register({
|
|
177
|
+
name: 'ping',
|
|
178
|
+
pattern: /^ping(\s+help)?$/i,
|
|
179
|
+
handler: async ({ context, match }) => {
|
|
180
|
+
if (match[1]) {
|
|
181
|
+
return 'Usage: `ping` or `ping help`';
|
|
182
|
+
}
|
|
183
|
+
return 'Pong!';
|
|
184
|
+
}
|
|
206
185
|
});
|
|
207
186
|
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
socketMode: true,
|
|
187
|
+
// Use dispatcher to route messages
|
|
188
|
+
const dispatcher = new CommandDispatcher(registry);
|
|
189
|
+
dispatcher.onMessage((event) => {
|
|
190
|
+
event.channel.typingIndicator.start();
|
|
191
|
+
return dispatcher.handle(event);
|
|
214
192
|
});
|
|
215
193
|
```
|
|
216
194
|
|
|
217
|
-
|
|
195
|
+
### Command Types
|
|
218
196
|
|
|
219
|
-
|
|
197
|
+
Commands are defined using:
|
|
220
198
|
|
|
221
199
|
```typescript
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
await channel.postMessage(doc);
|
|
200
|
+
interface Command {
|
|
201
|
+
name: string;
|
|
202
|
+
pattern: RegExp;
|
|
203
|
+
handler: (args: {
|
|
204
|
+
context: Context;
|
|
205
|
+
match: RegExpExecArray;
|
|
206
|
+
event: MessageEvent;
|
|
207
|
+
}) => Promise<string | void>;
|
|
208
|
+
}
|
|
232
209
|
```
|
|
233
210
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
All core types are exported for direct use.
|
|
211
|
+
Contexts include channel, user, and optional state:
|
|
237
212
|
|
|
238
213
|
```typescript
|
|
239
|
-
|
|
214
|
+
interface Context {
|
|
215
|
+
channel: Channel;
|
|
216
|
+
user: Member;
|
|
217
|
+
state?: Map<string, any>;
|
|
218
|
+
}
|
|
240
219
|
```
|
|
241
220
|
|
|
242
|
-
|
|
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
|
|
221
|
+
### Job Lifecycle
|
|
256
222
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
After `connect()`, `client.me` exposes the authenticated bot user:
|
|
223
|
+
Long-running commands support cancel/dismiss UI flows:
|
|
260
224
|
|
|
261
225
|
```typescript
|
|
262
|
-
|
|
263
|
-
await client.connect(channelId);
|
|
226
|
+
import { startJobLifecycle } from '@hardlydifficult/chat';
|
|
264
227
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
228
|
+
await startJobLifecycle(thread, {
|
|
229
|
+
onCancel: async () => {
|
|
230
|
+
// Handle user-initiated cancellation
|
|
231
|
+
},
|
|
232
|
+
onDismiss: async () => {
|
|
233
|
+
// Add dismiss emoji for user to remove later
|
|
234
|
+
}
|
|
235
|
+
});
|
|
268
236
|
```
|
|
269
237
|
|
|
270
|
-
|
|
238
|
+
## Member Matching
|
|
271
239
|
|
|
272
|
-
|
|
240
|
+
Resolve users by mention, ID, or fuzzy match:
|
|
273
241
|
|
|
274
242
|
```typescript
|
|
275
|
-
|
|
276
|
-
console.log(`${msg.author.username}: ${msg.content}`);
|
|
243
|
+
import { matchMember } from '@hardlydifficult/chat';
|
|
277
244
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
// Reply in the message's thread
|
|
285
|
-
msg.reply("Got it!");
|
|
286
|
-
});
|
|
245
|
+
// Match a member by mention or name
|
|
246
|
+
const member = await matchMember(
|
|
247
|
+
channel,
|
|
248
|
+
'@john', // or 'john', or '123456789'
|
|
249
|
+
{ fuzzyThreshold: 0.7 }
|
|
250
|
+
);
|
|
287
251
|
|
|
288
|
-
//
|
|
289
|
-
|
|
252
|
+
// Use aliases for more lenient matching
|
|
253
|
+
const aliases = { 'johnny': 'john' };
|
|
254
|
+
const member = await matchMember(channel, 'johnny', { aliases });
|
|
290
255
|
```
|
|
291
256
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
#### Oversized Message Handling
|
|
295
|
-
|
|
296
|
-
Messages that exceed platform limits (Discord: 2000 chars, Slack: 4000 chars) are handled automatically:
|
|
297
|
-
|
|
298
|
-
- **`postMessage`**: Sends the full content as a `message.txt` file attachment instead of failing
|
|
299
|
-
- **`update`**: Truncates with `…` (edits cannot attach files on either platform)
|
|
257
|
+
## Output Formatting
|
|
300
258
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
#### File Attachments
|
|
304
|
-
|
|
305
|
-
Send files as message attachments.
|
|
259
|
+
Convert abstract document blocks into platform-specific formats.
|
|
306
260
|
|
|
307
261
|
```typescript
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
262
|
+
import { formatDiscord, formatSlack } from '@hardlydifficult/chat';
|
|
263
|
+
|
|
264
|
+
const document = {
|
|
265
|
+
type: 'doc',
|
|
266
|
+
content: [
|
|
267
|
+
{ type: 'text', text: 'Hello ' },
|
|
268
|
+
{ type: 'bold', content: 'world' }
|
|
269
|
+
]
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const discordPayload = formatDiscord(document);
|
|
273
|
+
const slackBlocks = formatSlack(document);
|
|
314
274
|
```
|
|
315
275
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
```typescript
|
|
321
|
-
await channel.postDismissable("Build complete!", user.id);
|
|
322
|
-
```
|
|
276
|
+
Supported block types include:
|
|
277
|
+
- `text`, `bold`, `italic`, `code`, `pre`
|
|
278
|
+
- `header`, `divider`, `section`, `button`
|
|
279
|
+
- `mention` (platform-specific user/channel reference)
|
|
323
280
|
|
|
324
|
-
|
|
281
|
+
## Message Tracking
|
|
325
282
|
|
|
326
|
-
|
|
283
|
+
Track and update messages by key for efficient dynamic updates.
|
|
327
284
|
|
|
328
285
|
```typescript
|
|
329
|
-
|
|
286
|
+
import { MessageTracker } from '@hardlydifficult/chat';
|
|
330
287
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
288
|
+
const tracker = new MessageTracker(channel);
|
|
289
|
+
tracker.track('status', async () => channel.send('Updating...'));
|
|
290
|
+
tracker.edit('status', async (message) => {
|
|
291
|
+
await message.update('Updated!');
|
|
292
|
+
});
|
|
336
293
|
```
|
|
337
294
|
|
|
338
|
-
|
|
295
|
+
## File Operations
|
|
339
296
|
|
|
340
|
-
|
|
297
|
+
Post and update messages with file attachments.
|
|
341
298
|
|
|
342
299
|
```typescript
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
for (const member of members) {
|
|
346
|
-
const msg = await batch.post(summary(member));
|
|
347
|
-
await msg.reply(detail(member));
|
|
348
|
-
}
|
|
300
|
+
// Discord: attach files via buildMessagePayload
|
|
301
|
+
import { buildMessagePayload } from '@hardlydifficult/chat/discord';
|
|
349
302
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const recent = await channel.getBatches({
|
|
354
|
-
key: "sprint-update",
|
|
355
|
-
author: "me",
|
|
356
|
-
limit: 5,
|
|
303
|
+
const payload = buildMessagePayload({
|
|
304
|
+
content: 'Report',
|
|
305
|
+
files: [{ filename: 'report.pdf', data: buffer }]
|
|
357
306
|
});
|
|
358
307
|
|
|
359
|
-
await
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
For safer lifecycle handling, use `withBatch` (auto-finishes in `finally`):
|
|
308
|
+
await channel.send(payload);
|
|
363
309
|
|
|
364
|
-
|
|
365
|
-
await
|
|
366
|
-
|
|
367
|
-
|
|
310
|
+
// Slack: use built-in file support
|
|
311
|
+
await slackClient.postMessage({
|
|
312
|
+
channel: 'C123',
|
|
313
|
+
text: 'Report',
|
|
314
|
+
file: buffer,
|
|
315
|
+
filename: 'report.pdf'
|
|
368
316
|
});
|
|
369
317
|
```
|
|
370
318
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
You can also create a thread from an existing message:
|
|
374
|
-
|
|
375
|
-
```typescript
|
|
376
|
-
const msg = await channel.postMessage("Starting a discussion");
|
|
377
|
-
const thread = await msg.startThread("Discussion Thread", 1440); // auto-archive in minutes
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
Reconnect to an existing thread by ID (e.g., after a restart):
|
|
381
|
-
|
|
382
|
-
```typescript
|
|
383
|
-
const thread = channel.openThread(savedThreadId);
|
|
384
|
-
await thread.post("I'm back!");
|
|
385
|
-
thread.onReply(async (msg) => { /* ... */ });
|
|
386
|
-
```
|
|
319
|
+
## Setup
|
|
387
320
|
|
|
388
|
-
|
|
321
|
+
### Discord
|
|
389
322
|
|
|
390
|
-
|
|
323
|
+
1. Create a bot at [Discord Developer Portal](https://discord.com/developers/applications)
|
|
324
|
+
2. Enable **MESSAGE CONTENT INTENT** in bot settings
|
|
325
|
+
3. Invite with `bot` and `applications.commands` scopes
|
|
326
|
+
4. Set `DISCORD_TOKEN` environment variable
|
|
391
327
|
|
|
392
|
-
|
|
393
|
-
const controller = new AbortController();
|
|
394
|
-
const stream = thread.stream(2000, controller.signal);
|
|
328
|
+
### Slack
|
|
395
329
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
330
|
+
1. Create a Slack app at [Slack API](https://api.slack.com/apps)
|
|
331
|
+
2. Add **Incoming Webhooks** and **Bot User Token** scopes
|
|
332
|
+
3. Install to workspace and copy token
|
|
333
|
+
4. Set `SLACK_TOKEN` environment variable
|
|
400
334
|
|
|
401
|
-
|
|
335
|
+
## Platform Differences
|
|
402
336
|
|
|
403
|
-
|
|
337
|
+
| Feature | Discord | Slack |
|
|
338
|
+
|----------------------------|------------------------------------|------------------------------------|
|
|
339
|
+
| Message Length Limit | 2000 characters | 4000 characters |
|
|
340
|
+
| Thread Support | Text channel threads | Conversations with replies |
|
|
341
|
+
| File Uploads | Via `buildMessagePayload` | Via `chat.postMessage` |
|
|
342
|
+
| Typing Indicators | Supported via `typingIndicator` | Supported via `chat.scheduledMsg` |
|
|
343
|
+
| Message History | Requires **MESSAGE CONTENT INTENT**| Requires `history` scope |
|
|
344
|
+
| Mention Resolution | By ID or username#discriminator | By `@username` or user ID |
|
|
345
|
+
| Emoji Reactions | Unicode or custom emoji (with ID) | Custom emoji only by name |
|
|
404
346
|
|
|
405
|
-
|
|
406
|
-
await channel.sendTyping();
|
|
407
|
-
```
|
|
347
|
+
## Appendix
|
|
408
348
|
|
|
409
|
-
|
|
349
|
+
### Error Handling
|
|
410
350
|
|
|
411
|
-
|
|
351
|
+
Platform-specific error codes are mapped to user-friendly messages:
|
|
412
352
|
|
|
413
353
|
```typescript
|
|
414
|
-
|
|
415
|
-
const deletedCount = await channel.bulkDelete(50);
|
|
416
|
-
|
|
417
|
-
// List and filter recent messages
|
|
418
|
-
const botMessages = await channel.getMessages({ limit: 50, author: "me" });
|
|
419
|
-
const sameMessages = await channel.getRecentBotMessages(50);
|
|
420
|
-
|
|
421
|
-
// Keep latest 8 bot messages, delete older ones (opinionated cleanup helper)
|
|
422
|
-
await channel.pruneMessages({ author: "me", limit: 50, keep: 8 });
|
|
354
|
+
import { isRecoverableError, getErrorFriendlyMessage } from '@hardlydifficult/chat';
|
|
423
355
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
356
|
+
try {
|
|
357
|
+
await channel.send('Failed message');
|
|
358
|
+
} catch (error) {
|
|
359
|
+
if (isRecoverableError(error)) {
|
|
360
|
+
// Retry with backoff
|
|
361
|
+
} else {
|
|
362
|
+
console.log(getErrorFriendlyMessage(error));
|
|
363
|
+
}
|
|
428
364
|
}
|
|
429
365
|
```
|
|
430
366
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
Both platforms auto-reconnect via their underlying libraries (discord.js and @slack/bolt). Register callbacks for observability.
|
|
436
|
-
|
|
437
|
-
```typescript
|
|
438
|
-
const client = createChatClient({ type: "discord" });
|
|
439
|
-
|
|
440
|
-
client.onDisconnect((reason) => {
|
|
441
|
-
console.log("Disconnected:", reason);
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
client.onError((error) => {
|
|
445
|
-
console.error("Connection error:", error);
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
await client.disconnect(); // clean shutdown
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
Both callbacks return an unsubscribe function.
|
|
367
|
+
Recoverable errors include:
|
|
368
|
+
- Rate limits (`429 Too Many Requests`)
|
|
369
|
+
- Network timeouts
|
|
370
|
+
- Temporary unavailability
|
|
452
371
|
|
|
453
|
-
|
|
372
|
+
### Cleanup Modes
|
|
454
373
|
|
|
455
|
-
|
|
374
|
+
The `cleanup` method supports:
|
|
456
375
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
376
|
+
| Mode | Behavior |
|
|
377
|
+
|-----------|----------------------------------------------|
|
|
378
|
+
| `all` | Delete all messages (bot + users) |
|
|
379
|
+
| `bot` | Delete only bot-authored messages |
|
|
380
|
+
| `user` | Delete only user-authored messages |
|
|
381
|
+
| `since` | Delete messages newer than timestamp |
|
|
461
382
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
3. Bot scopes: `chat:write`, `chat:write.public`, `reactions:write`, `reactions:read`, `channels:history`, `channels:read`, `files:write`, `users:read`
|
|
467
|
-
4. Subscribe to events: `reaction_added`, `message.channels`
|
|
468
|
-
5. Set `SLACK_BOT_TOKEN` and `SLACK_APP_TOKEN` env vars
|
|
383
|
+
Example:
|
|
384
|
+
```typescript
|
|
385
|
+
await channel.cleanup({ mode: 'since', timestamp: new Date(Date.now() - 86400000) });
|
|
386
|
+
```
|