clearclaw 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channel/slack.d.ts +29 -0
- package/dist/channel/slack.js +402 -0
- package/dist/channel/telegram.d.ts +11 -2
- package/dist/channel/telegram.js +176 -32
- package/dist/config.d.ts +13 -3
- package/dist/config.js +18 -4
- package/dist/engine/claude-code.d.ts +2 -1
- package/dist/engine/claude-code.js +77 -8
- package/dist/files.d.ts +3 -0
- package/dist/files.js +33 -0
- package/dist/format.d.ts +4 -0
- package/dist/format.js +22 -18
- package/dist/index.js +10 -2
- package/dist/orchestrator.d.ts +2 -1
- package/dist/orchestrator.js +147 -102
- package/dist/types.d.ts +18 -3
- package/package.json +3 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { Channel, Button, ButtonResponse, SendMessageOpts } from "../types.js";
|
|
3
|
+
export declare class SlackChannel extends EventEmitter implements Channel {
|
|
4
|
+
name: string;
|
|
5
|
+
private app;
|
|
6
|
+
private botToken;
|
|
7
|
+
private botUserId;
|
|
8
|
+
private allowedUserIds;
|
|
9
|
+
private pendingButtonCallbacks;
|
|
10
|
+
private pendingModalCallbacks;
|
|
11
|
+
private typingMessageTs;
|
|
12
|
+
private userNameCache;
|
|
13
|
+
constructor(botToken: string, appToken: string, allowedUserIds: Set<string>);
|
|
14
|
+
connect(): Promise<void>;
|
|
15
|
+
disconnect(): Promise<void>;
|
|
16
|
+
ownsId(chatId: string): boolean;
|
|
17
|
+
sendMessage(chatId: string, text: string, opts?: SendMessageOpts): Promise<string[]>;
|
|
18
|
+
sendInteractive(chatId: string, text: string, buttons: Button[][]): Promise<ButtonResponse>;
|
|
19
|
+
editMessage(chatId: string, handle: string, text: string): Promise<void>;
|
|
20
|
+
deleteMessage(chatId: string, handle: string): Promise<void>;
|
|
21
|
+
pinMessage(chatId: string, handle: string): Promise<void>;
|
|
22
|
+
unpinAllMessages(chatId: string): Promise<void>;
|
|
23
|
+
updateStatus(chatId: string, text: string): Promise<void>;
|
|
24
|
+
setTyping(chatId: string, isTyping: boolean): Promise<void>;
|
|
25
|
+
private slackId;
|
|
26
|
+
/** Download a file from Slack using bot token auth. */
|
|
27
|
+
private downloadFile;
|
|
28
|
+
private resolveUserName;
|
|
29
|
+
}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { App, LogLevel } from "@slack/bolt";
|
|
3
|
+
import log from "../logger.js";
|
|
4
|
+
export class SlackChannel extends EventEmitter {
|
|
5
|
+
name = "slack";
|
|
6
|
+
app;
|
|
7
|
+
botToken;
|
|
8
|
+
botUserId;
|
|
9
|
+
allowedUserIds;
|
|
10
|
+
pendingButtonCallbacks = new Map(); // actionId → resolve
|
|
11
|
+
pendingModalCallbacks = new Map(); // modal callbackId → resolve
|
|
12
|
+
typingMessageTs = new Map(); // chatId → ts of bot's "typing…" placeholder
|
|
13
|
+
userNameCache = new Map();
|
|
14
|
+
constructor(botToken, appToken, allowedUserIds) {
|
|
15
|
+
super();
|
|
16
|
+
this.botToken = botToken;
|
|
17
|
+
this.allowedUserIds = allowedUserIds;
|
|
18
|
+
this.app = new App({
|
|
19
|
+
token: botToken, appToken, socketMode: true, logLevel: LogLevel.ERROR,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
async connect() {
|
|
23
|
+
this.app.event("message", async ({ event }) => {
|
|
24
|
+
const subtype = event.subtype;
|
|
25
|
+
// Suppress topic-change system messages from our setTopic calls
|
|
26
|
+
if (subtype === "channel_topic" || subtype === "group_topic") {
|
|
27
|
+
const ts = event.ts;
|
|
28
|
+
try {
|
|
29
|
+
await this.app.client.chat.delete({ channel: event.channel, ts });
|
|
30
|
+
}
|
|
31
|
+
catch { /* may lack permission */ }
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (subtype && subtype !== "bot_message" && subtype !== "file_share")
|
|
35
|
+
return;
|
|
36
|
+
// Bolt's message event is a union of many subtypes with different shapes;
|
|
37
|
+
// inline type is simpler than importing and narrowing the full union.
|
|
38
|
+
const msg = event;
|
|
39
|
+
const chatId = `slack:${msg.channel}`;
|
|
40
|
+
const isBotMessage = !!msg.bot_id || msg.user === this.botUserId;
|
|
41
|
+
if (isBotMessage)
|
|
42
|
+
return;
|
|
43
|
+
// Need either text or files to proceed
|
|
44
|
+
if (!msg.text && (!msg.files || msg.files.length === 0))
|
|
45
|
+
return;
|
|
46
|
+
const userId = msg.user;
|
|
47
|
+
if (!userId || !this.allowedUserIds.has(`slack:${userId}`)) {
|
|
48
|
+
if (userId)
|
|
49
|
+
log.info("[channel] ignored message from unauthorized user slack:%s", userId);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Download attached files in parallel
|
|
53
|
+
let attachments;
|
|
54
|
+
if (msg.files?.length) {
|
|
55
|
+
const downloadable = msg.files.filter((f) => f.url_private_download);
|
|
56
|
+
if (downloadable.length > 0) {
|
|
57
|
+
const results = await Promise.allSettled(downloadable.map(async (file) => {
|
|
58
|
+
const buffer = await this.downloadFile(file.url_private_download);
|
|
59
|
+
return { buffer, mimeType: file.mimetype ?? "application/octet-stream", filename: file.name };
|
|
60
|
+
}));
|
|
61
|
+
const succeeded = results
|
|
62
|
+
.filter((r) => r.status === "fulfilled")
|
|
63
|
+
.map((r) => r.value);
|
|
64
|
+
for (const r of results) {
|
|
65
|
+
if (r.status === "rejected") {
|
|
66
|
+
log.warn({ err: r.reason }, "[channel] failed to download Slack file");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (succeeded.length > 0)
|
|
70
|
+
attachments = succeeded;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const userName = await this.resolveUserName(userId);
|
|
74
|
+
this.emit("message", {
|
|
75
|
+
chatId,
|
|
76
|
+
user: { id: `slack:${userId}`, name: userName ?? userId },
|
|
77
|
+
text: msg.text ?? "",
|
|
78
|
+
...(attachments && { attachments }),
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
// Handle button presses from sendInteractive
|
|
82
|
+
this.app.action(/^ccb:/, async ({ action, body, ack }) => {
|
|
83
|
+
await ack();
|
|
84
|
+
const actionId = action.action_id;
|
|
85
|
+
const triggerId = body.trigger_id ?? "";
|
|
86
|
+
const resolve = this.pendingButtonCallbacks.get(actionId);
|
|
87
|
+
if (resolve)
|
|
88
|
+
resolve(triggerId);
|
|
89
|
+
});
|
|
90
|
+
// Handle /cc slash command — emit as /<subcommand> so orchestrator handles it
|
|
91
|
+
this.app.command("/cc", async ({ command, ack }) => {
|
|
92
|
+
await ack();
|
|
93
|
+
const subcommand = command.text.trim();
|
|
94
|
+
if (!subcommand)
|
|
95
|
+
return;
|
|
96
|
+
const chatId = `slack:${command.channel_id}`;
|
|
97
|
+
const userId = command.user_id;
|
|
98
|
+
if (!this.allowedUserIds.has(`slack:${userId}`))
|
|
99
|
+
return;
|
|
100
|
+
const userName = await this.resolveUserName(userId);
|
|
101
|
+
this.emit("message", {
|
|
102
|
+
chatId,
|
|
103
|
+
user: { id: `slack:${userId}`, name: userName ?? userId },
|
|
104
|
+
text: `/${subcommand}`,
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
// Handle modal submissions from "Deny + Note" feedback
|
|
108
|
+
this.app.view(/^ccv:/, async ({ view, ack }) => {
|
|
109
|
+
await ack();
|
|
110
|
+
const feedbackValue = view.state?.values?.feedback_block?.feedback_input?.value ?? "";
|
|
111
|
+
const resolve = this.pendingModalCallbacks.get(view.callback_id);
|
|
112
|
+
if (resolve) {
|
|
113
|
+
this.pendingModalCallbacks.delete(view.callback_id);
|
|
114
|
+
resolve(feedbackValue);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
await this.app.start();
|
|
118
|
+
try {
|
|
119
|
+
const auth = await this.app.client.auth.test();
|
|
120
|
+
this.botUserId = auth.user_id;
|
|
121
|
+
log.info("[channel] Slack bot: %s", this.botUserId);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
log.warn("[channel] Connected to Slack but failed to get bot user ID");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async disconnect() {
|
|
128
|
+
// Delete lingering typing placeholder messages
|
|
129
|
+
for (const [chatId, ts] of this.typingMessageTs) {
|
|
130
|
+
try {
|
|
131
|
+
await this.app.client.chat.delete({ channel: this.slackId(chatId), ts });
|
|
132
|
+
}
|
|
133
|
+
catch { /* already deleted or no permission */ }
|
|
134
|
+
}
|
|
135
|
+
this.typingMessageTs.clear();
|
|
136
|
+
await this.app.stop();
|
|
137
|
+
}
|
|
138
|
+
ownsId(chatId) { return chatId.startsWith("slack:"); }
|
|
139
|
+
async sendMessage(chatId, text, opts) {
|
|
140
|
+
const channel = this.slackId(chatId);
|
|
141
|
+
const chunks = splitMessage(text);
|
|
142
|
+
const handles = [];
|
|
143
|
+
// If a typing placeholder exists, edit the first chunk into it instead of posting new
|
|
144
|
+
const consumeTyping = opts?.consumeTyping !== false;
|
|
145
|
+
const typingTs = consumeTyping ? this.typingMessageTs.get(chatId) : undefined;
|
|
146
|
+
if (typingTs && chunks.length > 0) {
|
|
147
|
+
this.typingMessageTs.delete(chatId);
|
|
148
|
+
const firstChunk = chunks.shift();
|
|
149
|
+
const blocks = mrkdwnBlocks(firstChunk);
|
|
150
|
+
try {
|
|
151
|
+
await this.app.client.chat.update({
|
|
152
|
+
channel, ts: typingTs, text: firstChunk, blocks,
|
|
153
|
+
});
|
|
154
|
+
handles.push(typingTs);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Placeholder was deleted externally — fall back to posting
|
|
158
|
+
const result = await this.app.client.chat.postMessage({
|
|
159
|
+
channel, text: firstChunk, blocks,
|
|
160
|
+
});
|
|
161
|
+
if (result.ts)
|
|
162
|
+
handles.push(result.ts);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const chunk of chunks) {
|
|
166
|
+
const blocks = mrkdwnBlocks(chunk);
|
|
167
|
+
const result = await this.app.client.chat.postMessage({
|
|
168
|
+
channel, text: chunk, blocks,
|
|
169
|
+
});
|
|
170
|
+
if (result.ts)
|
|
171
|
+
handles.push(result.ts);
|
|
172
|
+
}
|
|
173
|
+
return handles;
|
|
174
|
+
}
|
|
175
|
+
async sendInteractive(chatId, text, buttons) {
|
|
176
|
+
const channel = this.slackId(chatId);
|
|
177
|
+
const callbackId = crypto.randomUUID().slice(0, 8);
|
|
178
|
+
// Pre-compute action IDs mapped to buttons
|
|
179
|
+
const actionEntries = [];
|
|
180
|
+
let idx = 0;
|
|
181
|
+
for (const row of buttons) {
|
|
182
|
+
for (const btn of row) {
|
|
183
|
+
actionEntries.push({ btn, actionId: `ccb:${callbackId}:${idx++}` });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const blocks = [
|
|
187
|
+
mrkdwnBlocks(splitMessage(text)[0])[0],
|
|
188
|
+
{
|
|
189
|
+
type: "actions",
|
|
190
|
+
elements: actionEntries.map(({ btn, actionId }) => ({
|
|
191
|
+
type: "button",
|
|
192
|
+
text: { type: "plain_text", text: btn.label },
|
|
193
|
+
action_id: actionId,
|
|
194
|
+
value: btn.value,
|
|
195
|
+
})),
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
const result = await this.app.client.chat.postMessage({
|
|
199
|
+
channel, text, blocks,
|
|
200
|
+
});
|
|
201
|
+
const cleanupAll = () => {
|
|
202
|
+
for (const { actionId } of actionEntries) {
|
|
203
|
+
this.pendingButtonCallbacks.delete(actionId);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
// Wait for button press (no timeout — matches CLI behavior)
|
|
207
|
+
const { button: pressed, triggerId } = await new Promise((resolve) => {
|
|
208
|
+
for (const entry of actionEntries) {
|
|
209
|
+
this.pendingButtonCallbacks.set(entry.actionId, (triggerId) => {
|
|
210
|
+
cleanupAll();
|
|
211
|
+
resolve({ button: entry.btn, triggerId });
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
// Update message to highlight the selected button (others become inert — no callbacks)
|
|
216
|
+
if (result.ts) {
|
|
217
|
+
const feedbackBlocks = [
|
|
218
|
+
blocks[0],
|
|
219
|
+
{
|
|
220
|
+
type: "actions",
|
|
221
|
+
elements: actionEntries.map(({ btn, actionId }) => ({
|
|
222
|
+
type: "button",
|
|
223
|
+
text: { type: "plain_text", text: btn === pressed ? `✅ ${btn.label}` : btn.label },
|
|
224
|
+
action_id: actionId,
|
|
225
|
+
value: btn.value,
|
|
226
|
+
})),
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
try {
|
|
230
|
+
await this.app.client.chat.update({
|
|
231
|
+
channel, ts: result.ts, text, blocks: feedbackBlocks,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
catch { /* best-effort */ }
|
|
235
|
+
}
|
|
236
|
+
if (pressed.requestText && triggerId) {
|
|
237
|
+
// Open a modal for feedback input (Slack equivalent of Telegram's force_reply)
|
|
238
|
+
const modalCallbackId = `ccv:${callbackId}`;
|
|
239
|
+
await this.app.client.views.open({
|
|
240
|
+
trigger_id: triggerId,
|
|
241
|
+
view: {
|
|
242
|
+
type: "modal",
|
|
243
|
+
callback_id: modalCallbackId,
|
|
244
|
+
title: { type: "plain_text", text: "Deny with note" },
|
|
245
|
+
submit: { type: "plain_text", text: "Submit" },
|
|
246
|
+
blocks: [
|
|
247
|
+
{
|
|
248
|
+
type: "input",
|
|
249
|
+
block_id: "feedback_block",
|
|
250
|
+
element: {
|
|
251
|
+
type: "plain_text_input",
|
|
252
|
+
action_id: "feedback_input",
|
|
253
|
+
multiline: true,
|
|
254
|
+
placeholder: { type: "plain_text", text: "Why are you denying this action?" },
|
|
255
|
+
},
|
|
256
|
+
label: { type: "plain_text", text: "Note" },
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
const followUpText = await new Promise((resolve) => {
|
|
262
|
+
this.pendingModalCallbacks.set(modalCallbackId, resolve);
|
|
263
|
+
});
|
|
264
|
+
if (followUpText && result.ts) {
|
|
265
|
+
try {
|
|
266
|
+
await this.app.client.chat.postMessage({
|
|
267
|
+
channel,
|
|
268
|
+
text: `Denied with note:\n> ${followUpText}`,
|
|
269
|
+
thread_ts: result.ts,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
catch { /* best-effort */ }
|
|
273
|
+
}
|
|
274
|
+
return { value: pressed.value, text: followUpText };
|
|
275
|
+
}
|
|
276
|
+
return { value: pressed.value };
|
|
277
|
+
}
|
|
278
|
+
async editMessage(chatId, handle, text) {
|
|
279
|
+
const chunk = splitMessage(text)[0];
|
|
280
|
+
await this.app.client.chat.update({
|
|
281
|
+
channel: this.slackId(chatId),
|
|
282
|
+
ts: handle,
|
|
283
|
+
text: chunk,
|
|
284
|
+
blocks: mrkdwnBlocks(chunk),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
async deleteMessage(chatId, handle) {
|
|
288
|
+
await this.app.client.chat.delete({
|
|
289
|
+
channel: this.slackId(chatId),
|
|
290
|
+
ts: handle,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
async pinMessage(chatId, handle) {
|
|
294
|
+
await this.app.client.pins.add({
|
|
295
|
+
channel: this.slackId(chatId),
|
|
296
|
+
timestamp: handle,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
async unpinAllMessages(chatId) {
|
|
300
|
+
const channel = this.slackId(chatId);
|
|
301
|
+
try {
|
|
302
|
+
const result = await this.app.client.pins.list({ channel });
|
|
303
|
+
if (result.items) {
|
|
304
|
+
for (const item of result.items) {
|
|
305
|
+
// The Slack API returns message.ts on pinned messages but the type defs omit it
|
|
306
|
+
const ts = item.message?.ts;
|
|
307
|
+
if (ts) {
|
|
308
|
+
await this.app.client.pins.remove({ channel, timestamp: ts });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch { /* may not have permission */ }
|
|
314
|
+
}
|
|
315
|
+
async updateStatus(chatId, text) {
|
|
316
|
+
await this.app.client.conversations.setTopic({
|
|
317
|
+
channel: this.slackId(chatId),
|
|
318
|
+
topic: text,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
async setTyping(chatId, isTyping) {
|
|
322
|
+
const channel = this.slackId(chatId);
|
|
323
|
+
if (isTyping) {
|
|
324
|
+
if (this.typingMessageTs.has(chatId))
|
|
325
|
+
return;
|
|
326
|
+
try {
|
|
327
|
+
const result = await this.app.client.chat.postMessage({
|
|
328
|
+
channel, text: "_typing…_",
|
|
329
|
+
});
|
|
330
|
+
if (result.ts)
|
|
331
|
+
this.typingMessageTs.set(chatId, result.ts);
|
|
332
|
+
}
|
|
333
|
+
catch { /* best-effort */ }
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
// Delete placeholder if it wasn't consumed by sendMessage
|
|
337
|
+
const ts = this.typingMessageTs.get(chatId);
|
|
338
|
+
if (ts) {
|
|
339
|
+
this.typingMessageTs.delete(chatId);
|
|
340
|
+
try {
|
|
341
|
+
await this.app.client.chat.delete({ channel, ts });
|
|
342
|
+
}
|
|
343
|
+
catch { /* already deleted */ }
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
slackId(chatId) {
|
|
348
|
+
return chatId.replace(/^slack:/, "");
|
|
349
|
+
}
|
|
350
|
+
/** Download a file from Slack using bot token auth. */
|
|
351
|
+
async downloadFile(url) {
|
|
352
|
+
const resp = await fetch(url, {
|
|
353
|
+
headers: { Authorization: `Bearer ${this.botToken}` },
|
|
354
|
+
});
|
|
355
|
+
if (!resp.ok)
|
|
356
|
+
throw new Error(`Slack file download failed: ${resp.status}`);
|
|
357
|
+
return Buffer.from(await resp.arrayBuffer());
|
|
358
|
+
}
|
|
359
|
+
async resolveUserName(userId) {
|
|
360
|
+
const cached = this.userNameCache.get(userId);
|
|
361
|
+
if (cached)
|
|
362
|
+
return cached;
|
|
363
|
+
try {
|
|
364
|
+
const result = await this.app.client.users.info({ user: userId });
|
|
365
|
+
const name = result.user?.real_name || result.user?.name;
|
|
366
|
+
if (name)
|
|
367
|
+
this.userNameCache.set(userId, name);
|
|
368
|
+
return name;
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function mrkdwnBlocks(text) {
|
|
376
|
+
return [{ type: "section", text: { type: "mrkdwn", text } }];
|
|
377
|
+
}
|
|
378
|
+
// Block Kit section.text has a 3000 char limit — the tightest Slack constraint.
|
|
379
|
+
// We split at this limit so each chunk fits both the text fallback and the block.
|
|
380
|
+
const MAX_TEXT = 3000;
|
|
381
|
+
function splitMessage(text) {
|
|
382
|
+
if (text.length <= MAX_TEXT)
|
|
383
|
+
return [text];
|
|
384
|
+
log.warn("[channel] splitting message (%d chars) into chunks", text.length);
|
|
385
|
+
const chunks = [];
|
|
386
|
+
let remaining = text;
|
|
387
|
+
while (remaining.length > MAX_TEXT) {
|
|
388
|
+
const newlineAt = remaining.lastIndexOf("\n", MAX_TEXT);
|
|
389
|
+
if (newlineAt > 0) {
|
|
390
|
+
chunks.push(remaining.slice(0, newlineAt));
|
|
391
|
+
remaining = remaining.slice(newlineAt + 1);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
chunks.push(remaining.slice(0, MAX_TEXT));
|
|
395
|
+
remaining = remaining.slice(MAX_TEXT);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (remaining)
|
|
399
|
+
chunks.push(remaining);
|
|
400
|
+
return chunks;
|
|
401
|
+
}
|
|
402
|
+
//# sourceMappingURL=slack.js.map
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import type { Channel, Button, ButtonResponse, SendMessageOpts
|
|
2
|
+
import type { Channel, Button, ButtonResponse, SendMessageOpts } from "../types.js";
|
|
3
3
|
export declare class TelegramChannel extends EventEmitter implements Channel {
|
|
4
4
|
name: string;
|
|
5
5
|
private bot;
|
|
6
|
+
private botToken;
|
|
6
7
|
private allowedUserIds;
|
|
7
8
|
private typingIntervals;
|
|
9
|
+
private statusHandles;
|
|
8
10
|
private pendingCallbacks;
|
|
9
11
|
private pendingTextResolvers;
|
|
10
12
|
constructor(botToken: string, allowedUserIds: Set<string>);
|
|
@@ -12,11 +14,18 @@ export declare class TelegramChannel extends EventEmitter implements Channel {
|
|
|
12
14
|
disconnect(): Promise<void>;
|
|
13
15
|
ownsId(chatId: string): boolean;
|
|
14
16
|
sendMessage(chatId: string, text: string, opts?: SendMessageOpts): Promise<string[]>;
|
|
15
|
-
sendInteractive(chatId: string, text: string, buttons: Button[][]
|
|
17
|
+
sendInteractive(chatId: string, text: string, buttons: Button[][]): Promise<ButtonResponse>;
|
|
16
18
|
editMessage(chatId: string, handle: string, text: string): Promise<void>;
|
|
17
19
|
deleteMessage(chatId: string, handle: string): Promise<void>;
|
|
18
20
|
pinMessage(chatId: string, handle: string): Promise<void>;
|
|
19
21
|
unpinAllMessages(chatId: string): Promise<void>;
|
|
22
|
+
updateStatus(chatId: string, text: string): Promise<void>;
|
|
20
23
|
setTyping(chatId: string, isTyping: boolean): Promise<void>;
|
|
24
|
+
/** Extract auth-checked sender info from a grammY context. Returns null if unauthorized. */
|
|
25
|
+
private extractSender;
|
|
26
|
+
/** Shared handler for photo/document messages: download, emit attachment or fallback. */
|
|
27
|
+
private handleMediaMessage;
|
|
28
|
+
/** Download a Telegram file by file_id, returning the raw buffer. */
|
|
29
|
+
private downloadFile;
|
|
21
30
|
private numericId;
|
|
22
31
|
}
|