@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.
- package/dist/channels/zalo/adapter.d.ts +12 -1
- package/dist/channels/zalo/index.js +160 -31
- package/dist/index.cjs +159 -31
- package/dist/index.js +159 -31
- package/package.json +1 -1
|
@@ -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,
|
|
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 =
|
|
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
|
|
37578
|
-
const
|
|
37579
|
-
const payload = { msg: text, quote
|
|
37580
|
-
|
|
37581
|
-
|
|
37582
|
-
|
|
37583
|
-
|
|
37584
|
-
|
|
37585
|
-
|
|
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,
|
|
37707
|
+
async sendMedia(chatId, media, options) {
|
|
37589
37708
|
if (!this.api)
|
|
37590
37709
|
throw new Error("Zalo adapter is not connected.");
|
|
37591
|
-
const
|
|
37592
|
-
const
|
|
37593
|
-
|
|
37594
|
-
|
|
37595
|
-
|
|
37596
|
-
|
|
37597
|
-
|
|
37598
|
-
|
|
37599
|
-
|
|
37600
|
-
|
|
37601
|
-
|
|
37602
|
-
|
|
37603
|
-
|
|
37604
|
-
|
|
37605
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
37717
|
-
const
|
|
37718
|
-
const payload = { msg: text, quote
|
|
37719
|
-
|
|
37720
|
-
|
|
37721
|
-
|
|
37722
|
-
|
|
37723
|
-
|
|
37724
|
-
|
|
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,
|
|
37846
|
+
async sendMedia(chatId, media, options) {
|
|
37728
37847
|
if (!this.api)
|
|
37729
37848
|
throw new Error("Zalo adapter is not connected.");
|
|
37730
|
-
const
|
|
37731
|
-
const
|
|
37732
|
-
|
|
37733
|
-
|
|
37734
|
-
|
|
37735
|
-
|
|
37736
|
-
|
|
37737
|
-
|
|
37738
|
-
|
|
37739
|
-
|
|
37740
|
-
|
|
37741
|
-
|
|
37742
|
-
|
|
37743
|
-
|
|
37744
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
37653
|
-
const
|
|
37654
|
-
const payload = { msg: text, quote
|
|
37655
|
-
|
|
37656
|
-
|
|
37657
|
-
|
|
37658
|
-
|
|
37659
|
-
|
|
37660
|
-
|
|
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,
|
|
37782
|
+
async sendMedia(chatId, media, options) {
|
|
37664
37783
|
if (!this.api)
|
|
37665
37784
|
throw new Error("Zalo adapter is not connected.");
|
|
37666
|
-
const
|
|
37667
|
-
const
|
|
37668
|
-
|
|
37669
|
-
|
|
37670
|
-
|
|
37671
|
-
|
|
37672
|
-
|
|
37673
|
-
|
|
37674
|
-
|
|
37675
|
-
|
|
37676
|
-
|
|
37677
|
-
|
|
37678
|
-
|
|
37679
|
-
|
|
37680
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|