@theowlops/channelhub 1.0.0 → 1.1.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.
@@ -21,4 +21,6 @@ export declare class DiscordChannelAdapter extends BaseChannel {
21
21
  sendText(chatId: string, text: string, options?: SendOptions): Promise<SentMessageResult>;
22
22
  sendMedia(chatId: string, media: MediaPayload, options?: SendOptions): Promise<SentMessageResult>;
23
23
  addReaction(chatId: string, messageId: string, emoji: string): Promise<void>;
24
+ sendTyping(chatId: string): Promise<void>;
25
+ editText(chatId: string, messageId: string, text: string): Promise<SentMessageResult>;
24
26
  }
@@ -124,6 +124,19 @@ class DiscordChannelAdapter extends BaseChannel {
124
124
  const encoded = encodeURIComponent(emoji);
125
125
  await this.callApi("PUT", `/channels/${chatId}/messages/${messageId}/reactions/${encoded}/@me`);
126
126
  }
127
+ async sendTyping(chatId) {
128
+ await this.callApi("POST", `/channels/${chatId}/typing`, {});
129
+ }
130
+ async editText(chatId, messageId, text) {
131
+ const res = await this.callApi("PATCH", `/channels/${chatId}/messages/${messageId}`, {
132
+ content: text
133
+ });
134
+ return {
135
+ messageId: String(res.id || messageId),
136
+ chatId,
137
+ timestamp: Date.now()
138
+ };
139
+ }
127
140
  }
128
141
  export {
129
142
  DiscordChannelAdapter
@@ -17,4 +17,6 @@ export declare class SlackChannelAdapter extends BaseChannel {
17
17
  sendText(chatId: string, text: string, options?: SendOptions): Promise<SentMessageResult>;
18
18
  sendMedia(chatId: string, media: MediaPayload, options?: SendOptions): Promise<SentMessageResult>;
19
19
  addReaction(chatId: string, messageId: string, emoji: string): Promise<void>;
20
+ sendTyping(chatId: string): Promise<void>;
21
+ editText(chatId: string, messageId: string, text: string): Promise<SentMessageResult>;
20
22
  }
@@ -116,6 +116,19 @@ class SlackChannelAdapter extends BaseChannel {
116
116
  name: cleanName
117
117
  });
118
118
  }
119
+ async sendTyping(chatId) {}
120
+ async editText(chatId, messageId, text) {
121
+ const res = await this.callApi("chat.update", {
122
+ channel: chatId,
123
+ ts: messageId,
124
+ text
125
+ });
126
+ return {
127
+ messageId: String(res.ts || messageId),
128
+ chatId,
129
+ timestamp: Date.now()
130
+ };
131
+ }
119
132
  }
120
133
  export {
121
134
  SlackChannelAdapter
@@ -18,4 +18,6 @@ export declare class TelegramChannelAdapter extends BaseChannel {
18
18
  sendText(chatId: string, text: string, options?: SendOptions): Promise<SentMessageResult>;
19
19
  sendMedia(chatId: string, media: MediaPayload, options?: SendOptions): Promise<SentMessageResult>;
20
20
  addReaction(chatId: string, messageId: string, emoji: string): Promise<void>;
21
+ sendTyping(chatId: string): Promise<void>;
22
+ editText(chatId: string, messageId: string, text: string): Promise<SentMessageResult>;
21
23
  }
@@ -173,6 +173,24 @@ class TelegramChannelAdapter extends BaseChannel {
173
173
  reaction: [{ type: "emoji", emoji }]
174
174
  });
175
175
  }
176
+ async sendTyping(chatId) {
177
+ await this.callApi("sendChatAction", {
178
+ chat_id: chatId,
179
+ action: "typing"
180
+ });
181
+ }
182
+ async editText(chatId, messageId, text) {
183
+ const res = await this.callApi("editMessageText", {
184
+ chat_id: chatId,
185
+ message_id: Number(messageId),
186
+ text
187
+ });
188
+ return {
189
+ messageId: String(res.message_id || messageId),
190
+ chatId,
191
+ timestamp: Date.now()
192
+ };
193
+ }
176
194
  }
177
195
  export {
178
196
  TelegramChannelAdapter
@@ -5,18 +5,30 @@ export interface ZaloAdapterConfig {
5
5
  credentialsPath?: string;
6
6
  ownId?: string;
7
7
  defaultIsGroup?: boolean;
8
+ minDelayMs?: number;
9
+ maxDelayMs?: number;
10
+ cacheLimit?: number;
8
11
  }
12
+ export declare const EMOJI_TO_ZALO: Record<string, string>;
9
13
  export declare class ZaloChannelAdapter extends BaseChannel {
10
14
  readonly name: ChannelType;
11
15
  private api;
12
16
  private ownId?;
13
17
  private config;
18
+ private threadTypeCache;
19
+ private messageCache;
20
+ private sendQueue;
14
21
  constructor(config?: ZaloAdapterConfig);
15
22
  connect(): Promise<void>;
16
23
  disconnect(): Promise<void>;
17
24
  private setupEventListener;
25
+ private recordInbound;
26
+ private resolveThreadType;
27
+ private resolveQuote;
28
+ private enqueueSend;
18
29
  private normalizeMessage;
19
30
  sendText(chatId: string, text: string, options?: SendOptions): Promise<SentMessageResult>;
20
- sendMedia(chatId: string, media: MediaPayload, _options?: SendOptions): Promise<SentMessageResult>;
31
+ sendMedia(chatId: string, media: MediaPayload, options?: SendOptions): Promise<SentMessageResult>;
21
32
  addReaction(chatId: string, messageId: string, emoji: string): Promise<void>;
33
+ sendTyping(chatId: string): Promise<void>;
22
34
  }
@@ -37488,14 +37488,47 @@ class BaseChannel extends EventEmitter {
37488
37488
  }
37489
37489
 
37490
37490
  // src/channels/zalo/adapter.ts
37491
+ var EMOJI_TO_ZALO = {
37492
+ "❤️": "HEART",
37493
+ "\uD83D\uDC96": "HEART",
37494
+ "\uD83D\uDC4D": "LIKE",
37495
+ "\uD83D\uDE06": "HAHA",
37496
+ "\uD83D\uDE02": "TEARS_OF_JOY",
37497
+ "\uD83D\uDE2E": "WOW",
37498
+ "\uD83D\uDE2D": "CRY",
37499
+ "\uD83D\uDE21": "ANGRY",
37500
+ "\uD83D\uDE18": "KISS",
37501
+ "\uD83D\uDCA9": "SHIT",
37502
+ "\uD83C\uDF39": "ROSE",
37503
+ "\uD83D\uDC94": "BROKEN_HEART",
37504
+ "\uD83D\uDC4E": "DISLIKE",
37505
+ "\uD83D\uDE0D": "LOVE",
37506
+ "\uD83E\uDD14": "CONFUSED",
37507
+ "\uD83D\uDE09": "WINK",
37508
+ heart: "HEART",
37509
+ like: "LIKE",
37510
+ haha: "HAHA",
37511
+ wow: "WOW",
37512
+ cry: "CRY",
37513
+ angry: "ANGRY"
37514
+ };
37515
+
37491
37516
  class ZaloChannelAdapter extends BaseChannel {
37492
37517
  name = "zalo";
37493
37518
  api;
37494
37519
  ownId;
37495
37520
  config;
37521
+ threadTypeCache = new Map;
37522
+ messageCache = new Map;
37523
+ sendQueue = Promise.resolve();
37496
37524
  constructor(config = {}) {
37497
37525
  super();
37498
- this.config = config;
37526
+ this.config = {
37527
+ minDelayMs: 300,
37528
+ maxDelayMs: 800,
37529
+ cacheLimit: 1000,
37530
+ ...config
37531
+ };
37499
37532
  if (config.api) {
37500
37533
  this.api = config.api;
37501
37534
  }
@@ -37530,17 +37563,101 @@ class ZaloChannelAdapter extends BaseChannel {
37530
37563
  if (!this.api?.listener?.on)
37531
37564
  return;
37532
37565
  this.api.listener.on("message", (raw) => {
37566
+ this.recordInbound(raw);
37533
37567
  const unified = this.normalizeMessage(raw);
37534
37568
  if (unified) {
37535
37569
  this.emit("message", unified);
37536
37570
  }
37537
37571
  });
37572
+ const onSessionDrop = (err) => {
37573
+ this.emit("session:expired", {
37574
+ reason: "SESSION_EXPIRED_OR_DROPPED",
37575
+ raw: err,
37576
+ requiresQrScan: true
37577
+ });
37578
+ this.setConnected(false);
37579
+ };
37580
+ this.api.listener.on("closed", onSessionDrop);
37581
+ this.api.listener.on("error", (err) => {
37582
+ const msg = String(err?.message || err);
37583
+ if (msg.includes("1002") || msg.includes("session") || msg.includes("auth")) {
37584
+ onSessionDrop(err);
37585
+ }
37586
+ });
37538
37587
  if (this.api.listener.start) {
37539
37588
  try {
37540
37589
  this.api.listener.start();
37541
37590
  } catch {}
37542
37591
  }
37543
37592
  }
37593
+ recordInbound(raw) {
37594
+ if (!raw)
37595
+ return;
37596
+ const data = raw.data || raw;
37597
+ const isGroup = raw.type === 1 || raw.type === "group" || Boolean(raw.isGroup);
37598
+ const chatId = String(raw.threadId || data.idTo || data.threadId || "");
37599
+ const msgId = String(data.msgId || raw.msgId || "");
37600
+ const cliMsgId = data.cliMsgId ? String(data.cliMsgId) : undefined;
37601
+ const uidFrom = String(data.uidFrom || raw.senderId || "");
37602
+ const content = typeof data.content === "string" ? data.content : data.msg || "";
37603
+ if (chatId) {
37604
+ this.threadTypeCache.set(chatId, isGroup ? 1 : 0);
37605
+ }
37606
+ const cached = {
37607
+ msgId,
37608
+ cliMsgId,
37609
+ uidFrom,
37610
+ content,
37611
+ threadId: chatId,
37612
+ isGroup
37613
+ };
37614
+ const limit = this.config.cacheLimit || 1000;
37615
+ if (this.messageCache.size >= limit) {
37616
+ const firstKey = this.messageCache.keys().next().value;
37617
+ if (firstKey)
37618
+ this.messageCache.delete(firstKey);
37619
+ }
37620
+ if (msgId)
37621
+ this.messageCache.set(msgId, cached);
37622
+ if (cliMsgId)
37623
+ this.messageCache.set(cliMsgId, cached);
37624
+ }
37625
+ resolveThreadType(chatId) {
37626
+ if (this.threadTypeCache.has(chatId)) {
37627
+ return this.threadTypeCache.get(chatId);
37628
+ }
37629
+ return this.config.defaultIsGroup ?? true ? 1 : 0;
37630
+ }
37631
+ resolveQuote(replyToId) {
37632
+ if (!replyToId)
37633
+ return;
37634
+ const cached = this.messageCache.get(replyToId);
37635
+ if (cached) {
37636
+ return {
37637
+ msgId: cached.msgId,
37638
+ cliMsgId: cached.cliMsgId,
37639
+ uidFrom: cached.uidFrom,
37640
+ content: cached.content
37641
+ };
37642
+ }
37643
+ return replyToId;
37644
+ }
37645
+ async enqueueSend(operation) {
37646
+ const minDelay = this.config.minDelayMs ?? 300;
37647
+ const maxDelay = this.config.maxDelayMs ?? 800;
37648
+ const execute = async () => {
37649
+ if (maxDelay > 0) {
37650
+ const jitter = Math.floor(minDelay + Math.random() * Math.max(0, maxDelay - minDelay));
37651
+ if (jitter > 0) {
37652
+ await new Promise((r) => setTimeout(r, jitter));
37653
+ }
37654
+ }
37655
+ return await operation();
37656
+ };
37657
+ const next = this.sendQueue.then(execute, execute);
37658
+ this.sendQueue = next.catch(() => {});
37659
+ return next;
37660
+ }
37544
37661
  normalizeMessage(raw) {
37545
37662
  if (!raw)
37546
37663
  return null;
@@ -37574,41 +37691,60 @@ class ZaloChannelAdapter extends BaseChannel {
37574
37691
  async sendText(chatId, text, options) {
37575
37692
  if (!this.api)
37576
37693
  throw new Error("Zalo adapter is not connected.");
37577
- const isGroup = this.config.defaultIsGroup ?? true;
37578
- const threadType = isGroup ? 1 : 0;
37579
- const payload = { msg: text, quote: options?.replyToId };
37580
- const res = await this.api.sendMessage(payload, chatId, threadType);
37581
- const resMsgId = res?.message?.msgId || res?.msgId || `z-${Date.now()}`;
37582
- return {
37583
- messageId: String(resMsgId),
37584
- chatId,
37585
- timestamp: Date.now()
37586
- };
37694
+ const threadType = this.resolveThreadType(chatId);
37695
+ const quote = this.resolveQuote(options?.replyToId);
37696
+ const payload = { msg: text, quote };
37697
+ return await this.enqueueSend(async () => {
37698
+ const res = await this.api.sendMessage(payload, chatId, threadType);
37699
+ const resMsgId = res?.message?.msgId || res?.msgId || `z-${Date.now()}`;
37700
+ return {
37701
+ messageId: String(resMsgId),
37702
+ chatId,
37703
+ timestamp: Date.now()
37704
+ };
37705
+ });
37587
37706
  }
37588
- async sendMedia(chatId, media, _options) {
37707
+ async sendMedia(chatId, media, options) {
37589
37708
  if (!this.api)
37590
37709
  throw new Error("Zalo adapter is not connected.");
37591
- const isGroup = this.config.defaultIsGroup ?? true;
37592
- const threadType = isGroup ? 1 : 0;
37593
- let res;
37594
- if (media.type === "image") {
37595
- res = await this.api.sendMessage({ msg: media.caption || "", attachments: [media.source] }, chatId, threadType);
37596
- } else if (media.type === "video" && this.api.sendVideo) {
37597
- res = await this.api.sendVideo({ video: media.source, msg: media.caption || "" }, chatId, threadType);
37598
- } else {
37599
- res = await this.api.sendMessage({ msg: media.caption || "", attachments: [media.source] }, chatId, threadType);
37600
- }
37601
- const resMsgId = res?.message?.msgId || res?.msgId || `z-${Date.now()}`;
37602
- return {
37603
- messageId: String(resMsgId),
37604
- chatId,
37605
- timestamp: Date.now()
37606
- };
37710
+ const threadType = this.resolveThreadType(chatId);
37711
+ const quote = this.resolveQuote(options?.replyToId);
37712
+ return await this.enqueueSend(async () => {
37713
+ let res;
37714
+ if (media.type === "image") {
37715
+ res = await this.api.sendMessage({ msg: media.caption || "", attachments: [media.source], quote }, chatId, threadType);
37716
+ } else if (media.type === "video" && this.api.sendVideo) {
37717
+ res = await this.api.sendVideo({ video: media.source, msg: media.caption || "", quote }, chatId, threadType);
37718
+ } else {
37719
+ res = await this.api.sendMessage({ msg: media.caption || "", attachments: [media.source], quote }, chatId, threadType);
37720
+ }
37721
+ const resMsgId = res?.message?.msgId || res?.msgId || `z-${Date.now()}`;
37722
+ return {
37723
+ messageId: String(resMsgId),
37724
+ chatId,
37725
+ timestamp: Date.now()
37726
+ };
37727
+ });
37607
37728
  }
37608
37729
  async addReaction(chatId, messageId, emoji) {
37609
- if (this.api?.addReaction) {
37610
- await this.api.addReaction(chatId, messageId, emoji);
37611
- }
37730
+ if (!this.api?.addReaction)
37731
+ return;
37732
+ const threadType = this.resolveThreadType(chatId);
37733
+ const isGroup = threadType === 1;
37734
+ const reactionCode = EMOJI_TO_ZALO[emoji] || emoji;
37735
+ const cached = this.messageCache.get(messageId);
37736
+ const cliMsgId = cached?.cliMsgId || messageId;
37737
+ await this.enqueueSend(async () => {
37738
+ await this.api.addReaction(chatId, messageId, cliMsgId, reactionCode, threadType);
37739
+ });
37740
+ }
37741
+ async sendTyping(chatId) {
37742
+ if (!this.api?.sendTypingEvent)
37743
+ return;
37744
+ const threadType = this.resolveThreadType(chatId);
37745
+ try {
37746
+ await this.api.sendTypingEvent(chatId, true, threadType);
37747
+ } catch {}
37612
37748
  }
37613
37749
  }
37614
37750
  // src/personal/client.ts
@@ -38329,6 +38465,7 @@ function initOABot() {
38329
38465
  return new ZaloOABot;
38330
38466
  }
38331
38467
  export {
38468
+ EMOJI_TO_ZALO,
38332
38469
  ZaloChannelAdapter,
38333
38470
  ZaloOABot,
38334
38471
  ZaloPersonalBot,
@@ -1,9 +1,12 @@
1
1
  import type { IChannelAdapter, MediaPayload, SendOptions, SentMessageResult, UnifiedMessage } from "./types";
2
+ import { type StreamOptions } from "./stream";
2
3
  export interface MessageContext {
3
4
  message: UnifiedMessage;
4
5
  channel: IChannelAdapter;
5
6
  reply: (text: string, options?: SendOptions) => Promise<SentMessageResult>;
6
7
  replyMedia: (media: MediaPayload, options?: SendOptions) => Promise<SentMessageResult>;
7
8
  react: (emoji: string) => Promise<void>;
9
+ sendTyping: () => Promise<void>;
10
+ stream: (tokenStream: AsyncIterable<string>, options?: StreamOptions) => Promise<SentMessageResult[]>;
8
11
  }
9
12
  export declare function createMessageContext(message: UnifiedMessage, channel: IChannelAdapter): MessageContext;
@@ -3,3 +3,4 @@ export * from "./adapter";
3
3
  export * from "./bus";
4
4
  export * from "./context";
5
5
  export * from "./hub";
6
+ export * from "./stream";
@@ -28,6 +28,126 @@ class ChannelEventBus extends EventEmitter2 {
28
28
  return this.emit("status", status);
29
29
  }
30
30
  }
31
+ // src/core/stream.ts
32
+ class SmartStreamer {
33
+ adapter;
34
+ options;
35
+ constructor(adapter, options = {}) {
36
+ this.adapter = adapter;
37
+ this.options = {
38
+ editDebounceMs: 1000,
39
+ typingIntervalMs: 4000,
40
+ chunkMode: "sentence",
41
+ minSentenceLength: 60,
42
+ initialPlaceholder: "...",
43
+ ...options
44
+ };
45
+ }
46
+ async stream(chatId, tokenStream, sendOptions) {
47
+ let typingActive = true;
48
+ const triggerTyping = async () => {
49
+ if (this.adapter.sendTyping) {
50
+ try {
51
+ await this.adapter.sendTyping(chatId);
52
+ } catch {}
53
+ }
54
+ };
55
+ await triggerTyping();
56
+ const typingTimer = setInterval(() => {
57
+ if (typingActive)
58
+ triggerTyping();
59
+ }, this.options.typingIntervalMs);
60
+ try {
61
+ if (typeof this.adapter.editText === "function") {
62
+ return await this.streamWithEdit(chatId, tokenStream, sendOptions);
63
+ } else {
64
+ return await this.streamWithoutEdit(chatId, tokenStream, sendOptions);
65
+ }
66
+ } finally {
67
+ typingActive = false;
68
+ clearInterval(typingTimer);
69
+ }
70
+ }
71
+ async streamWithEdit(chatId, tokenStream, sendOptions) {
72
+ let accumulated = "";
73
+ let sentMsg = null;
74
+ let lastEditTime = 0;
75
+ let pendingEditTimeout = null;
76
+ const performEdit = async (text) => {
77
+ if (sentMsg && this.adapter.editText) {
78
+ await this.adapter.editText(chatId, sentMsg.messageId, text);
79
+ lastEditTime = Date.now();
80
+ }
81
+ };
82
+ for await (const chunk of tokenStream) {
83
+ accumulated += chunk;
84
+ if (!sentMsg) {
85
+ sentMsg = await this.adapter.sendText(chatId, accumulated.trim() || this.options.initialPlaceholder, sendOptions);
86
+ lastEditTime = Date.now();
87
+ continue;
88
+ }
89
+ const now = Date.now();
90
+ const elapsed = now - lastEditTime;
91
+ if (elapsed >= this.options.editDebounceMs) {
92
+ if (pendingEditTimeout) {
93
+ clearTimeout(pendingEditTimeout);
94
+ pendingEditTimeout = null;
95
+ }
96
+ await performEdit(accumulated);
97
+ } else if (!pendingEditTimeout) {
98
+ pendingEditTimeout = setTimeout(async () => {
99
+ pendingEditTimeout = null;
100
+ await performEdit(accumulated);
101
+ }, this.options.editDebounceMs - elapsed);
102
+ }
103
+ }
104
+ if (pendingEditTimeout) {
105
+ clearTimeout(pendingEditTimeout);
106
+ pendingEditTimeout = null;
107
+ }
108
+ if (sentMsg && accumulated) {
109
+ await performEdit(accumulated);
110
+ return [sentMsg];
111
+ } else if (!sentMsg && accumulated) {
112
+ const res = await this.adapter.sendText(chatId, accumulated, sendOptions);
113
+ return [res];
114
+ }
115
+ return sentMsg ? [sentMsg] : [];
116
+ }
117
+ async streamWithoutEdit(chatId, tokenStream, sendOptions) {
118
+ const results = [];
119
+ if (this.options.chunkMode === "accumulate") {
120
+ let accumulated = "";
121
+ for await (const chunk of tokenStream) {
122
+ accumulated += chunk;
123
+ }
124
+ if (accumulated.trim()) {
125
+ const res = await this.adapter.sendText(chatId, accumulated, sendOptions);
126
+ results.push(res);
127
+ }
128
+ return results;
129
+ }
130
+ let buffer = "";
131
+ const sentenceEndRegex = /[.?!;\n]\s*$/;
132
+ for await (const chunk of tokenStream) {
133
+ buffer += chunk;
134
+ if (buffer.length >= this.options.minSentenceLength && sentenceEndRegex.test(buffer.trimEnd())) {
135
+ const textToSend = buffer.trim();
136
+ if (textToSend) {
137
+ const res = await this.adapter.sendText(chatId, textToSend, sendOptions);
138
+ results.push(res);
139
+ buffer = "";
140
+ }
141
+ }
142
+ }
143
+ if (buffer.trim()) {
144
+ const res = await this.adapter.sendText(chatId, buffer.trim(), sendOptions);
145
+ results.push(res);
146
+ }
147
+ return results;
148
+ }
149
+ }
150
+
31
151
  // src/core/context.ts
32
152
  function createMessageContext(message, channel) {
33
153
  return {
@@ -45,6 +165,17 @@ function createMessageContext(message, channel) {
45
165
  if (channel.addReaction) {
46
166
  await channel.addReaction(message.chat.id, message.id, emoji);
47
167
  }
168
+ },
169
+ sendTyping: async () => {
170
+ if (channel.sendTyping) {
171
+ await channel.sendTyping(message.chat.id);
172
+ }
173
+ },
174
+ stream: async (tokenStream, options) => {
175
+ const streamer = new SmartStreamer(channel, options);
176
+ return await streamer.stream(message.chat.id, tokenStream, {
177
+ replyToId: message.id
178
+ });
48
179
  }
49
180
  };
50
181
  }
@@ -95,5 +226,6 @@ export {
95
226
  BaseChannel,
96
227
  ChannelEventBus,
97
228
  ChannelHub,
229
+ SmartStreamer,
98
230
  createMessageContext
99
231
  };
@@ -0,0 +1,16 @@
1
+ import type { IChannelAdapter, SendOptions, SentMessageResult } from "./types";
2
+ export interface StreamOptions {
3
+ editDebounceMs?: number;
4
+ typingIntervalMs?: number;
5
+ chunkMode?: "sentence" | "accumulate";
6
+ minSentenceLength?: number;
7
+ initialPlaceholder?: string;
8
+ }
9
+ export declare class SmartStreamer {
10
+ private adapter;
11
+ private options;
12
+ constructor(adapter: IChannelAdapter, options?: StreamOptions);
13
+ stream(chatId: string, tokenStream: AsyncIterable<string>, sendOptions?: SendOptions): Promise<SentMessageResult[]>;
14
+ private streamWithEdit;
15
+ private streamWithoutEdit;
16
+ }
@@ -57,6 +57,8 @@ export interface IChannelAdapter {
57
57
  sendText(chatId: string, text: string, options?: SendOptions): Promise<SentMessageResult>;
58
58
  sendMedia(chatId: string, media: MediaPayload, options?: SendOptions): Promise<SentMessageResult>;
59
59
  addReaction?(chatId: string, messageId: string, emoji: string): Promise<void>;
60
+ sendTyping?(chatId: string): Promise<void>;
61
+ editText?(chatId: string, messageId: string, text: string): Promise<SentMessageResult>;
60
62
  on(event: "message", handler: (msg: UnifiedMessage) => Promise<void> | void): this;
61
63
  on(event: "error", handler: (err: Error) => void): this;
62
64
  on(event: "status", handler: (status: ChannelStatus) => void): this;