@theowlops/channelhub 1.0.0 → 1.0.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.
@@ -5,18 +5,29 @@ 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>;
22
33
  }
@@ -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,52 @@ 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
+ });
37612
37740
  }
37613
37741
  }
37614
37742
  // src/personal/client.ts
@@ -38329,6 +38457,7 @@ function initOABot() {
38329
38457
  return new ZaloOABot;
38330
38458
  }
38331
38459
  export {
38460
+ EMOJI_TO_ZALO,
38332
38461
  ZaloChannelAdapter,
38333
38462
  ZaloOABot,
38334
38463
  ZaloPersonalBot,
package/dist/index.cjs CHANGED
@@ -37627,14 +37627,47 @@ class ChannelHub {
37627
37627
  }
37628
37628
  }
37629
37629
  // src/channels/zalo/adapter.ts
37630
+ var EMOJI_TO_ZALO = {
37631
+ "❤️": "HEART",
37632
+ "\uD83D\uDC96": "HEART",
37633
+ "\uD83D\uDC4D": "LIKE",
37634
+ "\uD83D\uDE06": "HAHA",
37635
+ "\uD83D\uDE02": "TEARS_OF_JOY",
37636
+ "\uD83D\uDE2E": "WOW",
37637
+ "\uD83D\uDE2D": "CRY",
37638
+ "\uD83D\uDE21": "ANGRY",
37639
+ "\uD83D\uDE18": "KISS",
37640
+ "\uD83D\uDCA9": "SHIT",
37641
+ "\uD83C\uDF39": "ROSE",
37642
+ "\uD83D\uDC94": "BROKEN_HEART",
37643
+ "\uD83D\uDC4E": "DISLIKE",
37644
+ "\uD83D\uDE0D": "LOVE",
37645
+ "\uD83E\uDD14": "CONFUSED",
37646
+ "\uD83D\uDE09": "WINK",
37647
+ heart: "HEART",
37648
+ like: "LIKE",
37649
+ haha: "HAHA",
37650
+ wow: "WOW",
37651
+ cry: "CRY",
37652
+ angry: "ANGRY"
37653
+ };
37654
+
37630
37655
  class ZaloChannelAdapter extends BaseChannel {
37631
37656
  name = "zalo";
37632
37657
  api;
37633
37658
  ownId;
37634
37659
  config;
37660
+ threadTypeCache = new Map;
37661
+ messageCache = new Map;
37662
+ sendQueue = Promise.resolve();
37635
37663
  constructor(config = {}) {
37636
37664
  super();
37637
- this.config = config;
37665
+ this.config = {
37666
+ minDelayMs: 300,
37667
+ maxDelayMs: 800,
37668
+ cacheLimit: 1000,
37669
+ ...config
37670
+ };
37638
37671
  if (config.api) {
37639
37672
  this.api = config.api;
37640
37673
  }
@@ -37669,17 +37702,101 @@ class ZaloChannelAdapter extends BaseChannel {
37669
37702
  if (!this.api?.listener?.on)
37670
37703
  return;
37671
37704
  this.api.listener.on("message", (raw) => {
37705
+ this.recordInbound(raw);
37672
37706
  const unified = this.normalizeMessage(raw);
37673
37707
  if (unified) {
37674
37708
  this.emit("message", unified);
37675
37709
  }
37676
37710
  });
37711
+ const onSessionDrop = (err) => {
37712
+ this.emit("session:expired", {
37713
+ reason: "SESSION_EXPIRED_OR_DROPPED",
37714
+ raw: err,
37715
+ requiresQrScan: true
37716
+ });
37717
+ this.setConnected(false);
37718
+ };
37719
+ this.api.listener.on("closed", onSessionDrop);
37720
+ this.api.listener.on("error", (err) => {
37721
+ const msg = String(err?.message || err);
37722
+ if (msg.includes("1002") || msg.includes("session") || msg.includes("auth")) {
37723
+ onSessionDrop(err);
37724
+ }
37725
+ });
37677
37726
  if (this.api.listener.start) {
37678
37727
  try {
37679
37728
  this.api.listener.start();
37680
37729
  } catch {}
37681
37730
  }
37682
37731
  }
37732
+ recordInbound(raw) {
37733
+ if (!raw)
37734
+ return;
37735
+ const data = raw.data || raw;
37736
+ const isGroup = raw.type === 1 || raw.type === "group" || Boolean(raw.isGroup);
37737
+ const chatId = String(raw.threadId || data.idTo || data.threadId || "");
37738
+ const msgId = String(data.msgId || raw.msgId || "");
37739
+ const cliMsgId = data.cliMsgId ? String(data.cliMsgId) : undefined;
37740
+ const uidFrom = String(data.uidFrom || raw.senderId || "");
37741
+ const content = typeof data.content === "string" ? data.content : data.msg || "";
37742
+ if (chatId) {
37743
+ this.threadTypeCache.set(chatId, isGroup ? 1 : 0);
37744
+ }
37745
+ const cached = {
37746
+ msgId,
37747
+ cliMsgId,
37748
+ uidFrom,
37749
+ content,
37750
+ threadId: chatId,
37751
+ isGroup
37752
+ };
37753
+ const limit = this.config.cacheLimit || 1000;
37754
+ if (this.messageCache.size >= limit) {
37755
+ const firstKey = this.messageCache.keys().next().value;
37756
+ if (firstKey)
37757
+ this.messageCache.delete(firstKey);
37758
+ }
37759
+ if (msgId)
37760
+ this.messageCache.set(msgId, cached);
37761
+ if (cliMsgId)
37762
+ this.messageCache.set(cliMsgId, cached);
37763
+ }
37764
+ resolveThreadType(chatId) {
37765
+ if (this.threadTypeCache.has(chatId)) {
37766
+ return this.threadTypeCache.get(chatId);
37767
+ }
37768
+ return this.config.defaultIsGroup ?? true ? 1 : 0;
37769
+ }
37770
+ resolveQuote(replyToId) {
37771
+ if (!replyToId)
37772
+ return;
37773
+ const cached = this.messageCache.get(replyToId);
37774
+ if (cached) {
37775
+ return {
37776
+ msgId: cached.msgId,
37777
+ cliMsgId: cached.cliMsgId,
37778
+ uidFrom: cached.uidFrom,
37779
+ content: cached.content
37780
+ };
37781
+ }
37782
+ return replyToId;
37783
+ }
37784
+ async enqueueSend(operation) {
37785
+ const minDelay = this.config.minDelayMs ?? 300;
37786
+ const maxDelay = this.config.maxDelayMs ?? 800;
37787
+ const execute = async () => {
37788
+ if (maxDelay > 0) {
37789
+ const jitter = Math.floor(minDelay + Math.random() * Math.max(0, maxDelay - minDelay));
37790
+ if (jitter > 0) {
37791
+ await new Promise((r) => setTimeout(r, jitter));
37792
+ }
37793
+ }
37794
+ return await operation();
37795
+ };
37796
+ const next = this.sendQueue.then(execute, execute);
37797
+ this.sendQueue = next.catch(() => {});
37798
+ return next;
37799
+ }
37683
37800
  normalizeMessage(raw) {
37684
37801
  if (!raw)
37685
37802
  return null;
@@ -37713,41 +37830,52 @@ class ZaloChannelAdapter extends BaseChannel {
37713
37830
  async sendText(chatId, text, options) {
37714
37831
  if (!this.api)
37715
37832
  throw new Error("Zalo adapter is not connected.");
37716
- const isGroup = this.config.defaultIsGroup ?? true;
37717
- const threadType = isGroup ? 1 : 0;
37718
- const payload = { msg: text, quote: options?.replyToId };
37719
- const res = await this.api.sendMessage(payload, chatId, threadType);
37720
- const resMsgId = res?.message?.msgId || res?.msgId || `z-${Date.now()}`;
37721
- return {
37722
- messageId: String(resMsgId),
37723
- chatId,
37724
- timestamp: Date.now()
37725
- };
37833
+ const threadType = this.resolveThreadType(chatId);
37834
+ const quote = this.resolveQuote(options?.replyToId);
37835
+ const payload = { msg: text, quote };
37836
+ return await this.enqueueSend(async () => {
37837
+ const res = await this.api.sendMessage(payload, chatId, threadType);
37838
+ const resMsgId = res?.message?.msgId || res?.msgId || `z-${Date.now()}`;
37839
+ return {
37840
+ messageId: String(resMsgId),
37841
+ chatId,
37842
+ timestamp: Date.now()
37843
+ };
37844
+ });
37726
37845
  }
37727
- async sendMedia(chatId, media, _options) {
37846
+ async sendMedia(chatId, media, options) {
37728
37847
  if (!this.api)
37729
37848
  throw new Error("Zalo adapter is not connected.");
37730
- const isGroup = this.config.defaultIsGroup ?? true;
37731
- const threadType = isGroup ? 1 : 0;
37732
- let res;
37733
- if (media.type === "image") {
37734
- res = await this.api.sendMessage({ msg: media.caption || "", attachments: [media.source] }, chatId, threadType);
37735
- } else if (media.type === "video" && this.api.sendVideo) {
37736
- res = await this.api.sendVideo({ video: media.source, msg: media.caption || "" }, chatId, threadType);
37737
- } else {
37738
- res = await this.api.sendMessage({ msg: media.caption || "", attachments: [media.source] }, chatId, threadType);
37739
- }
37740
- const resMsgId = res?.message?.msgId || res?.msgId || `z-${Date.now()}`;
37741
- return {
37742
- messageId: String(resMsgId),
37743
- chatId,
37744
- timestamp: Date.now()
37745
- };
37849
+ const threadType = this.resolveThreadType(chatId);
37850
+ const quote = this.resolveQuote(options?.replyToId);
37851
+ return await this.enqueueSend(async () => {
37852
+ let res;
37853
+ if (media.type === "image") {
37854
+ res = await this.api.sendMessage({ msg: media.caption || "", attachments: [media.source], quote }, chatId, threadType);
37855
+ } else if (media.type === "video" && this.api.sendVideo) {
37856
+ res = await this.api.sendVideo({ video: media.source, msg: media.caption || "", quote }, chatId, threadType);
37857
+ } else {
37858
+ res = await this.api.sendMessage({ msg: media.caption || "", attachments: [media.source], quote }, chatId, threadType);
37859
+ }
37860
+ const resMsgId = res?.message?.msgId || res?.msgId || `z-${Date.now()}`;
37861
+ return {
37862
+ messageId: String(resMsgId),
37863
+ chatId,
37864
+ timestamp: Date.now()
37865
+ };
37866
+ });
37746
37867
  }
37747
37868
  async addReaction(chatId, messageId, emoji) {
37748
- if (this.api?.addReaction) {
37749
- await this.api.addReaction(chatId, messageId, emoji);
37750
- }
37869
+ if (!this.api?.addReaction)
37870
+ return;
37871
+ const threadType = this.resolveThreadType(chatId);
37872
+ const isGroup = threadType === 1;
37873
+ const reactionCode = EMOJI_TO_ZALO[emoji] || emoji;
37874
+ const cached = this.messageCache.get(messageId);
37875
+ const cliMsgId = cached?.cliMsgId || messageId;
37876
+ await this.enqueueSend(async () => {
37877
+ await this.api.addReaction(chatId, messageId, cliMsgId, reactionCode, threadType);
37878
+ });
37751
37879
  }
37752
37880
  }
37753
37881
  // src/personal/client.ts
package/dist/index.js CHANGED
@@ -37563,14 +37563,47 @@ class ChannelHub {
37563
37563
  }
37564
37564
  }
37565
37565
  // src/channels/zalo/adapter.ts
37566
+ var EMOJI_TO_ZALO = {
37567
+ "❤️": "HEART",
37568
+ "\uD83D\uDC96": "HEART",
37569
+ "\uD83D\uDC4D": "LIKE",
37570
+ "\uD83D\uDE06": "HAHA",
37571
+ "\uD83D\uDE02": "TEARS_OF_JOY",
37572
+ "\uD83D\uDE2E": "WOW",
37573
+ "\uD83D\uDE2D": "CRY",
37574
+ "\uD83D\uDE21": "ANGRY",
37575
+ "\uD83D\uDE18": "KISS",
37576
+ "\uD83D\uDCA9": "SHIT",
37577
+ "\uD83C\uDF39": "ROSE",
37578
+ "\uD83D\uDC94": "BROKEN_HEART",
37579
+ "\uD83D\uDC4E": "DISLIKE",
37580
+ "\uD83D\uDE0D": "LOVE",
37581
+ "\uD83E\uDD14": "CONFUSED",
37582
+ "\uD83D\uDE09": "WINK",
37583
+ heart: "HEART",
37584
+ like: "LIKE",
37585
+ haha: "HAHA",
37586
+ wow: "WOW",
37587
+ cry: "CRY",
37588
+ angry: "ANGRY"
37589
+ };
37590
+
37566
37591
  class ZaloChannelAdapter extends BaseChannel {
37567
37592
  name = "zalo";
37568
37593
  api;
37569
37594
  ownId;
37570
37595
  config;
37596
+ threadTypeCache = new Map;
37597
+ messageCache = new Map;
37598
+ sendQueue = Promise.resolve();
37571
37599
  constructor(config = {}) {
37572
37600
  super();
37573
- this.config = config;
37601
+ this.config = {
37602
+ minDelayMs: 300,
37603
+ maxDelayMs: 800,
37604
+ cacheLimit: 1000,
37605
+ ...config
37606
+ };
37574
37607
  if (config.api) {
37575
37608
  this.api = config.api;
37576
37609
  }
@@ -37605,17 +37638,101 @@ class ZaloChannelAdapter extends BaseChannel {
37605
37638
  if (!this.api?.listener?.on)
37606
37639
  return;
37607
37640
  this.api.listener.on("message", (raw) => {
37641
+ this.recordInbound(raw);
37608
37642
  const unified = this.normalizeMessage(raw);
37609
37643
  if (unified) {
37610
37644
  this.emit("message", unified);
37611
37645
  }
37612
37646
  });
37647
+ const onSessionDrop = (err) => {
37648
+ this.emit("session:expired", {
37649
+ reason: "SESSION_EXPIRED_OR_DROPPED",
37650
+ raw: err,
37651
+ requiresQrScan: true
37652
+ });
37653
+ this.setConnected(false);
37654
+ };
37655
+ this.api.listener.on("closed", onSessionDrop);
37656
+ this.api.listener.on("error", (err) => {
37657
+ const msg = String(err?.message || err);
37658
+ if (msg.includes("1002") || msg.includes("session") || msg.includes("auth")) {
37659
+ onSessionDrop(err);
37660
+ }
37661
+ });
37613
37662
  if (this.api.listener.start) {
37614
37663
  try {
37615
37664
  this.api.listener.start();
37616
37665
  } catch {}
37617
37666
  }
37618
37667
  }
37668
+ recordInbound(raw) {
37669
+ if (!raw)
37670
+ return;
37671
+ const data = raw.data || raw;
37672
+ const isGroup = raw.type === 1 || raw.type === "group" || Boolean(raw.isGroup);
37673
+ const chatId = String(raw.threadId || data.idTo || data.threadId || "");
37674
+ const msgId = String(data.msgId || raw.msgId || "");
37675
+ const cliMsgId = data.cliMsgId ? String(data.cliMsgId) : undefined;
37676
+ const uidFrom = String(data.uidFrom || raw.senderId || "");
37677
+ const content = typeof data.content === "string" ? data.content : data.msg || "";
37678
+ if (chatId) {
37679
+ this.threadTypeCache.set(chatId, isGroup ? 1 : 0);
37680
+ }
37681
+ const cached = {
37682
+ msgId,
37683
+ cliMsgId,
37684
+ uidFrom,
37685
+ content,
37686
+ threadId: chatId,
37687
+ isGroup
37688
+ };
37689
+ const limit = this.config.cacheLimit || 1000;
37690
+ if (this.messageCache.size >= limit) {
37691
+ const firstKey = this.messageCache.keys().next().value;
37692
+ if (firstKey)
37693
+ this.messageCache.delete(firstKey);
37694
+ }
37695
+ if (msgId)
37696
+ this.messageCache.set(msgId, cached);
37697
+ if (cliMsgId)
37698
+ this.messageCache.set(cliMsgId, cached);
37699
+ }
37700
+ resolveThreadType(chatId) {
37701
+ if (this.threadTypeCache.has(chatId)) {
37702
+ return this.threadTypeCache.get(chatId);
37703
+ }
37704
+ return this.config.defaultIsGroup ?? true ? 1 : 0;
37705
+ }
37706
+ resolveQuote(replyToId) {
37707
+ if (!replyToId)
37708
+ return;
37709
+ const cached = this.messageCache.get(replyToId);
37710
+ if (cached) {
37711
+ return {
37712
+ msgId: cached.msgId,
37713
+ cliMsgId: cached.cliMsgId,
37714
+ uidFrom: cached.uidFrom,
37715
+ content: cached.content
37716
+ };
37717
+ }
37718
+ return replyToId;
37719
+ }
37720
+ async enqueueSend(operation) {
37721
+ const minDelay = this.config.minDelayMs ?? 300;
37722
+ const maxDelay = this.config.maxDelayMs ?? 800;
37723
+ const execute = async () => {
37724
+ if (maxDelay > 0) {
37725
+ const jitter = Math.floor(minDelay + Math.random() * Math.max(0, maxDelay - minDelay));
37726
+ if (jitter > 0) {
37727
+ await new Promise((r) => setTimeout(r, jitter));
37728
+ }
37729
+ }
37730
+ return await operation();
37731
+ };
37732
+ const next = this.sendQueue.then(execute, execute);
37733
+ this.sendQueue = next.catch(() => {});
37734
+ return next;
37735
+ }
37619
37736
  normalizeMessage(raw) {
37620
37737
  if (!raw)
37621
37738
  return null;
@@ -37649,41 +37766,52 @@ class ZaloChannelAdapter extends BaseChannel {
37649
37766
  async sendText(chatId, text, options) {
37650
37767
  if (!this.api)
37651
37768
  throw new Error("Zalo adapter is not connected.");
37652
- const isGroup = this.config.defaultIsGroup ?? true;
37653
- const threadType = isGroup ? 1 : 0;
37654
- const payload = { msg: text, quote: options?.replyToId };
37655
- const res = await this.api.sendMessage(payload, chatId, threadType);
37656
- const resMsgId = res?.message?.msgId || res?.msgId || `z-${Date.now()}`;
37657
- return {
37658
- messageId: String(resMsgId),
37659
- chatId,
37660
- timestamp: Date.now()
37661
- };
37769
+ const threadType = this.resolveThreadType(chatId);
37770
+ const quote = this.resolveQuote(options?.replyToId);
37771
+ const payload = { msg: text, quote };
37772
+ return await this.enqueueSend(async () => {
37773
+ const res = await this.api.sendMessage(payload, chatId, threadType);
37774
+ const resMsgId = res?.message?.msgId || res?.msgId || `z-${Date.now()}`;
37775
+ return {
37776
+ messageId: String(resMsgId),
37777
+ chatId,
37778
+ timestamp: Date.now()
37779
+ };
37780
+ });
37662
37781
  }
37663
- async sendMedia(chatId, media, _options) {
37782
+ async sendMedia(chatId, media, options) {
37664
37783
  if (!this.api)
37665
37784
  throw new Error("Zalo adapter is not connected.");
37666
- const isGroup = this.config.defaultIsGroup ?? true;
37667
- const threadType = isGroup ? 1 : 0;
37668
- let res;
37669
- if (media.type === "image") {
37670
- res = await this.api.sendMessage({ msg: media.caption || "", attachments: [media.source] }, chatId, threadType);
37671
- } else if (media.type === "video" && this.api.sendVideo) {
37672
- res = await this.api.sendVideo({ video: media.source, msg: media.caption || "" }, chatId, threadType);
37673
- } else {
37674
- res = await this.api.sendMessage({ msg: media.caption || "", attachments: [media.source] }, chatId, threadType);
37675
- }
37676
- const resMsgId = res?.message?.msgId || res?.msgId || `z-${Date.now()}`;
37677
- return {
37678
- messageId: String(resMsgId),
37679
- chatId,
37680
- timestamp: Date.now()
37681
- };
37785
+ const threadType = this.resolveThreadType(chatId);
37786
+ const quote = this.resolveQuote(options?.replyToId);
37787
+ return await this.enqueueSend(async () => {
37788
+ let res;
37789
+ if (media.type === "image") {
37790
+ res = await this.api.sendMessage({ msg: media.caption || "", attachments: [media.source], quote }, chatId, threadType);
37791
+ } else if (media.type === "video" && this.api.sendVideo) {
37792
+ res = await this.api.sendVideo({ video: media.source, msg: media.caption || "", quote }, chatId, threadType);
37793
+ } else {
37794
+ res = await this.api.sendMessage({ msg: media.caption || "", attachments: [media.source], quote }, chatId, threadType);
37795
+ }
37796
+ const resMsgId = res?.message?.msgId || res?.msgId || `z-${Date.now()}`;
37797
+ return {
37798
+ messageId: String(resMsgId),
37799
+ chatId,
37800
+ timestamp: Date.now()
37801
+ };
37802
+ });
37682
37803
  }
37683
37804
  async addReaction(chatId, messageId, emoji) {
37684
- if (this.api?.addReaction) {
37685
- await this.api.addReaction(chatId, messageId, emoji);
37686
- }
37805
+ if (!this.api?.addReaction)
37806
+ return;
37807
+ const threadType = this.resolveThreadType(chatId);
37808
+ const isGroup = threadType === 1;
37809
+ const reactionCode = EMOJI_TO_ZALO[emoji] || emoji;
37810
+ const cached = this.messageCache.get(messageId);
37811
+ const cliMsgId = cached?.cliMsgId || messageId;
37812
+ await this.enqueueSend(async () => {
37813
+ await this.api.addReaction(chatId, messageId, cliMsgId, reactionCode, threadType);
37814
+ });
37687
37815
  }
37688
37816
  }
37689
37817
  // src/personal/client.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theowlops/channelhub",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "ChannelHub SDK - Unified multi-channel messaging toolkit (Zalo, Telegram, Discord, Slack) for AI agents, standalone bots, and enterprise automation.",
5
5
  "module": "dist/index.js",
6
6
  "main": "dist/index.cjs",