@diegoaltoworks/chatter 0.29.0 → 0.30.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/channels/whatsapp/inbound.d.ts +131 -0
- package/dist/channels/whatsapp/inbound.d.ts.map +1 -0
- package/dist/channels/whatsapp/inbound.test.d.ts +2 -0
- package/dist/channels/whatsapp/inbound.test.d.ts.map +1 -0
- package/dist/channels/whatsapp/index.d.ts +1 -0
- package/dist/channels/whatsapp/index.d.ts.map +1 -1
- package/dist/channels/whatsapp/index.js +332 -0
- package/dist/channels/whatsapp/index.mjs +325 -0
- package/package.json +1 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WhatsApp inbound: turns a raw Baileys message into a reply through
|
|
3
|
+
* chatter's own pipeline (`prepareChat` + `answerOnce`), gated by
|
|
4
|
+
* `./channels`' reply gates and loop guard. `./channel` hands every raw
|
|
5
|
+
* message to `config.onMessage` uninterpreted — this module is that
|
|
6
|
+
* interpretation.
|
|
7
|
+
*
|
|
8
|
+
* Message-shape parsing (text/caption extraction, contextInfo mentions and
|
|
9
|
+
* quoted-reply resolution) is genericized from the reference implementation:
|
|
10
|
+
* text AND contextInfo live in a DIFFERENT place per message shape — plain
|
|
11
|
+
* text carries both on `extendedTextMessage`, but a photo/video/document
|
|
12
|
+
* carries its caption on its own message type and its contextInfo there too.
|
|
13
|
+
* Missing that meant mentions in photo captions were invisible and group
|
|
14
|
+
* photo requests were silently ignored.
|
|
15
|
+
*
|
|
16
|
+
* `createWhatsAppInboundHandler` needs `ServerDependencies`
|
|
17
|
+
* (`client`/`store`/`prompts`), which only exists once `createServer` builds
|
|
18
|
+
* it — after `channels` (and therefore `onMessage`) must already be
|
|
19
|
+
* configured. Wire it up from `ChatterConfig.customRoutes`, which runs
|
|
20
|
+
* before channels start and receives the same deps:
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* let handleInbound: ((event: WhatsAppMessageEvent) => Promise<void>) | undefined;
|
|
24
|
+
* const whatsapp = createWhatsAppChannel({
|
|
25
|
+
* sessionSecret: process.env.WA_SESSION_SECRET as string,
|
|
26
|
+
* onMessage: (event) => handleInbound?.(event),
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* await createServer({
|
|
30
|
+
* ...,
|
|
31
|
+
* channels: [whatsapp],
|
|
32
|
+
* customRoutes: async (app, deps) => {
|
|
33
|
+
* handleInbound = createWhatsAppInboundHandler({
|
|
34
|
+
* client: deps.client,
|
|
35
|
+
* store: deps.store,
|
|
36
|
+
* prompts: deps.prompts,
|
|
37
|
+
* answerFn: deps.config.answerFn,
|
|
38
|
+
* bucketsFor: deps.config.bucketsFor,
|
|
39
|
+
* registry: new Map(),
|
|
40
|
+
* });
|
|
41
|
+
* },
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
import type { WAMessage, WASocket } from "@whiskeysockets/baileys";
|
|
46
|
+
import type OpenAI from "openai";
|
|
47
|
+
import { type AnswerFn } from "../../core/answer";
|
|
48
|
+
import { type BucketsFor } from "../../core/buckets";
|
|
49
|
+
import type { PromptLoader } from "../../core/prompts";
|
|
50
|
+
import type { VectorStore } from "../../core/retrieval";
|
|
51
|
+
import { type SessionIdentityRegistry } from "../gates";
|
|
52
|
+
import type { WhatsAppMessageEvent } from "./channel";
|
|
53
|
+
export declare function isGroupJid(jid: string): boolean;
|
|
54
|
+
/** "447700900123:17@s.whatsapp.net" (device-suffixed) and "447700900123@s.whatsapp.net" match. */
|
|
55
|
+
export declare function jidsMatch(a: string | undefined, b: string | undefined): boolean;
|
|
56
|
+
export declare function jidToPhoneNumber(jid: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the sender's real phone number. Modern WhatsApp identifies group
|
|
59
|
+
* senders (and some DM chats) by LID (anonymized `"12345@lid"`), which
|
|
60
|
+
* carries no relationship to the phone number — treating it as one silently
|
|
61
|
+
* keys per-person state on garbage. Prefer the `*Pn` fields Baileys attaches
|
|
62
|
+
* when available, then fall back to the socket's own LID mapping store.
|
|
63
|
+
*
|
|
64
|
+
* The returned string is a stable identity key, not guaranteed to be a
|
|
65
|
+
* dialable phone number: an unresolved LID returns an explicit `"lid:<id>"`
|
|
66
|
+
* marker rather than a phone-shaped fabrication (`jidToPhoneNumber("12345@lid")`
|
|
67
|
+
* would otherwise yield `"+12345"`, indistinguishable from a real number) —
|
|
68
|
+
* callers keying security or spend decisions off `sender` (`bucketsFor`,
|
|
69
|
+
* usage metering) must not mistake a LID numeral for a phone number.
|
|
70
|
+
*/
|
|
71
|
+
export declare function senderPhoneFor(sock: WASocket, message: WAMessage, chatJid: string): Promise<string>;
|
|
72
|
+
export interface WaMessageContext {
|
|
73
|
+
mentionedJids: string[];
|
|
74
|
+
quotedParticipantJid?: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* contextInfo (mentions, quoted message) lives in a DIFFERENT place per
|
|
78
|
+
* message shape: a plain text message carries it on `extendedTextMessage`,
|
|
79
|
+
* but a photo/video/document message carries it on its own type.
|
|
80
|
+
*/
|
|
81
|
+
export declare function messageContext(message: WAMessage): WaMessageContext;
|
|
82
|
+
/** Text/caption across every shape that can carry one — plain text, a reply, or a photo/video/document caption. */
|
|
83
|
+
export declare function extractText(message: WAMessage): string;
|
|
84
|
+
export interface WhatsAppInboundConfig {
|
|
85
|
+
client: OpenAI;
|
|
86
|
+
store: VectorStore;
|
|
87
|
+
prompts: PromptLoader;
|
|
88
|
+
answerFn?: AnswerFn;
|
|
89
|
+
bucketsFor?: BucketsFor;
|
|
90
|
+
model?: string;
|
|
91
|
+
/**
|
|
92
|
+
* Own identities per session — share ONE registry across every session
|
|
93
|
+
* this process runs so a message from one session's own number never
|
|
94
|
+
* looks like a stranger to another (the loop-guard case a single
|
|
95
|
+
* session's `fromMe` flag can't catch). Kept in sync from every message,
|
|
96
|
+
* not just at connection open, because a session's LID identity can
|
|
97
|
+
* populate late.
|
|
98
|
+
*/
|
|
99
|
+
registry: SessionIdentityRegistry;
|
|
100
|
+
/** Group chats eligible for a reply. Empty (default) = every group. Has no effect on DMs, which always reply regardless. */
|
|
101
|
+
allowedChats?: string[];
|
|
102
|
+
muteRegex?: RegExp;
|
|
103
|
+
unmuteRegex?: RegExp;
|
|
104
|
+
/** Neutral, overridable acknowledgements — this module ships no bot personality; unset = silent mute/unmute. */
|
|
105
|
+
muteReply?: string;
|
|
106
|
+
unmuteReply?: string;
|
|
107
|
+
dmRateLimit?: {
|
|
108
|
+
max: number;
|
|
109
|
+
windowMs: number;
|
|
110
|
+
};
|
|
111
|
+
groupRateLimit?: {
|
|
112
|
+
max: number;
|
|
113
|
+
windowMs: number;
|
|
114
|
+
};
|
|
115
|
+
/** Extra system-prompt section describing the delivery channel; passed through to `prepareChat`. */
|
|
116
|
+
channelHint?: string;
|
|
117
|
+
/**
|
|
118
|
+
* Optional persona layer for a sender/text pair — the resolution
|
|
119
|
+
* mechanism (registry, windowed rolls, prompt files) lives in its own
|
|
120
|
+
* module; this only plugs the result into `prepareChat`'s `personaLayer`.
|
|
121
|
+
* A throw/rejection is treated as "no persona" for that turn.
|
|
122
|
+
*/
|
|
123
|
+
personaResolver?: (ctx: {
|
|
124
|
+
senderPhone: string;
|
|
125
|
+
text: string;
|
|
126
|
+
}) => string | undefined | Promise<string | undefined>;
|
|
127
|
+
now?: () => number;
|
|
128
|
+
}
|
|
129
|
+
/** Builds the `onMessage` handler for `createWhatsAppChannel` (see `./channel`) — see the module docstring for how to wire `ServerDependencies` into it. */
|
|
130
|
+
export declare function createWhatsAppInboundHandler(config: WhatsAppInboundConfig): (event: WhatsAppMessageEvent) => Promise<void>;
|
|
131
|
+
//# sourceMappingURL=inbound.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inbound.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/inbound.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,OAAO,EAAE,KAAK,QAAQ,EAAc,MAAM,mBAAmB,CAAC;AAC9D,OAAO,EAAE,KAAK,UAAU,EAAkB,MAAM,oBAAoB,CAAC;AAErE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAKL,KAAK,uBAAuB,EAE7B,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAItD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE/C;AAED,kGAAkG;AAClG,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAI/E;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGpD;AAeD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,QAAQ,EACd,OAAO,EAAE,SAAS,EAClB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,CA+BjB;AAID,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,GAAG,gBAAgB,CAWnE;AAED,mHAAmH;AACnH,wBAAgB,WAAW,CAAC,OAAO,EAAE,SAAS,GAAG,MAAM,CAWtD;AAID,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,YAAY,CAAC;IACtB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,QAAQ,EAAE,uBAAuB,CAAC;IAClC,4HAA4H;IAC5H,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gHAAgH;IAChH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,cAAc,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,oGAAoG;IACpG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE;QACtB,WAAW,EAAE,MAAM,CAAC;QACpB,IAAI,EAAE,MAAM,CAAC;KACd,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACvD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAqBD,4JAA4J;AAC5J,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,qBAAqB,GAC5B,CAAC,KAAK,EAAE,oBAAoB,KAAK,OAAO,CAAC,IAAI,CAAC,CA0GhD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inbound.test.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/inbound.test.ts"],"names":[],"mappings":""}
|
|
@@ -29,5 +29,6 @@ export type { Baileys } from "./baileys";
|
|
|
29
29
|
export { loadBaileys } from "./baileys";
|
|
30
30
|
export { acquireSessionLease, createWhatsAppChannel, type LeaseGatedConnectDeps, reconnectDelayMs, senderNameFor, shutdownWaSessions, type WaSessionHandle, type WhatsAppChannelConfig, type WhatsAppMessageEvent, } from "./channel";
|
|
31
31
|
export { decrypt, encrypt } from "./crypto";
|
|
32
|
+
export { createWhatsAppInboundHandler, extractText, isGroupJid, jidsMatch, jidToPhoneNumber, messageContext, senderPhoneFor, type WaMessageContext, type WhatsAppInboundConfig, } from "./inbound";
|
|
32
33
|
export { canAcquireLease, createTursoWaLeaseStore, LEASE_HEARTBEAT_MS, LEASE_STALE_MS, LEASE_WAIT_MS, type WaLeaseRow, type WaLeaseStore, } from "./lease";
|
|
33
34
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC1E,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,YAAY,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,qBAAqB,EAC1B,gBAAgB,EAChB,aAAa,EACb,kBAAkB,EAClB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,GAC1B,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EACL,eAAe,EACf,uBAAuB,EACvB,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,KAAK,UAAU,EACf,KAAK,YAAY,GAClB,MAAM,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAC1E,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,YAAY,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,KAAK,qBAAqB,EAC1B,gBAAgB,EAChB,aAAa,EACb,kBAAkB,EAClB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,GAC1B,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EACL,4BAA4B,EAC5B,WAAW,EACX,UAAU,EACV,SAAS,EACT,gBAAgB,EAChB,cAAc,EACd,cAAc,EACd,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,GAC3B,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,eAAe,EACf,uBAAuB,EACvB,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,KAAK,UAAU,EACf,KAAK,YAAY,GAClB,MAAM,SAAS,CAAC"}
|
|
@@ -37,11 +37,18 @@ __export(index_exports, {
|
|
|
37
37
|
canAcquireLease: () => canAcquireLease,
|
|
38
38
|
createTursoWaLeaseStore: () => createTursoWaLeaseStore,
|
|
39
39
|
createWhatsAppChannel: () => createWhatsAppChannel,
|
|
40
|
+
createWhatsAppInboundHandler: () => createWhatsAppInboundHandler,
|
|
40
41
|
decrypt: () => decrypt,
|
|
41
42
|
encrypt: () => encrypt,
|
|
43
|
+
extractText: () => extractText,
|
|
44
|
+
isGroupJid: () => isGroupJid,
|
|
45
|
+
jidToPhoneNumber: () => jidToPhoneNumber,
|
|
46
|
+
jidsMatch: () => jidsMatch,
|
|
42
47
|
loadBaileys: () => loadBaileys,
|
|
48
|
+
messageContext: () => messageContext,
|
|
43
49
|
reconnectDelayMs: () => reconnectDelayMs,
|
|
44
50
|
senderNameFor: () => senderNameFor,
|
|
51
|
+
senderPhoneFor: () => senderPhoneFor,
|
|
45
52
|
shutdownWaSessions: () => shutdownWaSessions,
|
|
46
53
|
useTursoAuthState: () => useTursoAuthState
|
|
47
54
|
});
|
|
@@ -459,6 +466,324 @@ function createWhatsAppChannel(config) {
|
|
|
459
466
|
}
|
|
460
467
|
};
|
|
461
468
|
}
|
|
469
|
+
|
|
470
|
+
// src/core/guardrails.ts
|
|
471
|
+
function detectLeakage(text) {
|
|
472
|
+
const flags = [
|
|
473
|
+
/system prompt/i,
|
|
474
|
+
/hidden instruction/i,
|
|
475
|
+
/here (are|is) (my|the) rules/i,
|
|
476
|
+
/BEGIN SYSTEM PROMPT/i,
|
|
477
|
+
/tool instructions/i
|
|
478
|
+
];
|
|
479
|
+
return flags.some((rx) => rx.test(text));
|
|
480
|
+
}
|
|
481
|
+
function scrubOutput(text) {
|
|
482
|
+
return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
|
|
483
|
+
}
|
|
484
|
+
function guardOutput(text) {
|
|
485
|
+
if (detectLeakage(text)) {
|
|
486
|
+
return "Sorry, I can't share internal instructions. How else can I help?";
|
|
487
|
+
}
|
|
488
|
+
return scrubOutput(text);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/core/llm.ts
|
|
492
|
+
var DEFAULT_MODEL = "gpt-4o";
|
|
493
|
+
async function completeOnce({
|
|
494
|
+
client,
|
|
495
|
+
system,
|
|
496
|
+
messages,
|
|
497
|
+
temperature = 0.2,
|
|
498
|
+
model = DEFAULT_MODEL
|
|
499
|
+
}) {
|
|
500
|
+
const res = await client.chat.completions.create({
|
|
501
|
+
model,
|
|
502
|
+
temperature,
|
|
503
|
+
messages: [{ role: "system", content: system }, ...messages]
|
|
504
|
+
});
|
|
505
|
+
const text = res.choices[0]?.message?.content ?? "";
|
|
506
|
+
return {
|
|
507
|
+
content: guardOutput(text),
|
|
508
|
+
usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// src/core/answer.ts
|
|
513
|
+
var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
|
|
514
|
+
async function runAnswerFn(answerFn, { system, messages, mode, sender }) {
|
|
515
|
+
const result = await answerFn({ system, messages, mode, sender });
|
|
516
|
+
const raw = typeof result === "string" ? result : result?.content;
|
|
517
|
+
const usage = typeof result === "string" ? void 0 : result?.usage;
|
|
518
|
+
return {
|
|
519
|
+
content: guardOutput(typeof raw === "string" ? raw : ""),
|
|
520
|
+
usage: usage ?? noUsage()
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
async function answerOnce({
|
|
524
|
+
answerFn,
|
|
525
|
+
client,
|
|
526
|
+
system,
|
|
527
|
+
messages,
|
|
528
|
+
mode,
|
|
529
|
+
sender,
|
|
530
|
+
temperature,
|
|
531
|
+
model
|
|
532
|
+
}) {
|
|
533
|
+
if (answerFn) {
|
|
534
|
+
return runAnswerFn(answerFn, { system, messages, mode, sender });
|
|
535
|
+
}
|
|
536
|
+
return completeOnce({ client, system, messages, temperature, model });
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/core/buckets.ts
|
|
540
|
+
function defaultBuckets(mode) {
|
|
541
|
+
return ["base", mode];
|
|
542
|
+
}
|
|
543
|
+
function isIdentified(sender) {
|
|
544
|
+
return typeof sender === "string" && sender.trim().length > 0;
|
|
545
|
+
}
|
|
546
|
+
function normalize(buckets) {
|
|
547
|
+
const out = [];
|
|
548
|
+
for (const raw of buckets) {
|
|
549
|
+
if (typeof raw !== "string") continue;
|
|
550
|
+
const bucket = raw.trim();
|
|
551
|
+
if (bucket && !out.includes(bucket)) out.push(bucket);
|
|
552
|
+
}
|
|
553
|
+
return out;
|
|
554
|
+
}
|
|
555
|
+
async function resolveBuckets({
|
|
556
|
+
mode,
|
|
557
|
+
sender,
|
|
558
|
+
bucketsFor
|
|
559
|
+
}) {
|
|
560
|
+
if (!bucketsFor) return void 0;
|
|
561
|
+
const identified = isIdentified(sender);
|
|
562
|
+
const requested = await bucketsFor(identified ? { mode, sender } : { mode });
|
|
563
|
+
if (!Array.isArray(requested)) return void 0;
|
|
564
|
+
const cleaned = normalize(requested);
|
|
565
|
+
if (identified) return cleaned;
|
|
566
|
+
const ceiling = new Set(defaultBuckets(mode));
|
|
567
|
+
return cleaned.filter((bucket) => ceiling.has(bucket));
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// src/core/pipeline.ts
|
|
571
|
+
var MODE_SETTINGS = {
|
|
572
|
+
public: { topK: 6, label: "Context" },
|
|
573
|
+
private: { topK: 8, label: "Internal Context" }
|
|
574
|
+
};
|
|
575
|
+
async function prepareChat({
|
|
576
|
+
store,
|
|
577
|
+
prompts,
|
|
578
|
+
mode,
|
|
579
|
+
messages,
|
|
580
|
+
personaLayer,
|
|
581
|
+
channelHint,
|
|
582
|
+
buckets
|
|
583
|
+
}) {
|
|
584
|
+
const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
|
|
585
|
+
if (!lastUserMsg) {
|
|
586
|
+
throw new Error("no user message found in conversation");
|
|
587
|
+
}
|
|
588
|
+
const { topK, label } = MODE_SETTINGS[mode];
|
|
589
|
+
const ctx = await store.query(lastUserMsg.content, topK, buckets ?? defaultBuckets(mode));
|
|
590
|
+
const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
|
|
591
|
+
const hint = channelHint?.trim();
|
|
592
|
+
const system = [
|
|
593
|
+
prompts.baseSystemRules,
|
|
594
|
+
persona,
|
|
595
|
+
...hint ? [hint] : [],
|
|
596
|
+
`${label}:
|
|
597
|
+
${ctx.join("\n\n")}`
|
|
598
|
+
].join("\n\n");
|
|
599
|
+
return { system, messages };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// src/channels/gates.ts
|
|
603
|
+
function matches(re, text) {
|
|
604
|
+
return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
|
|
605
|
+
}
|
|
606
|
+
function decideChannelAction(msg, config) {
|
|
607
|
+
if (msg.fromBot || !msg.text.trim()) return "ignore";
|
|
608
|
+
if (!msg.isDirectMessage) {
|
|
609
|
+
if (config.allowedChats.length > 0 && !config.allowedChats.includes(msg.chatId)) {
|
|
610
|
+
return "ignore";
|
|
611
|
+
}
|
|
612
|
+
if (config.muteRegex && matches(config.muteRegex, msg.text)) return "mute";
|
|
613
|
+
if (config.unmuteRegex && matches(config.unmuteRegex, msg.text)) return "unmute";
|
|
614
|
+
if (config.mutedChats.has(msg.chatId)) return "ignore";
|
|
615
|
+
return msg.mentionsBot || msg.isReplyToBot ? "reply" : "ignore";
|
|
616
|
+
}
|
|
617
|
+
return "reply";
|
|
618
|
+
}
|
|
619
|
+
function createSlidingWindowRateLimiter(maxPerWindow, windowMs, now = Date.now) {
|
|
620
|
+
const log = /* @__PURE__ */ new Map();
|
|
621
|
+
return function allow(key) {
|
|
622
|
+
const cutoff = now() - windowMs;
|
|
623
|
+
const entries = (log.get(key) ?? []).filter((t) => t > cutoff);
|
|
624
|
+
if (entries.length >= maxPerWindow) {
|
|
625
|
+
if (entries.length > 0) log.set(key, entries);
|
|
626
|
+
else log.delete(key);
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
entries.push(now());
|
|
630
|
+
log.set(key, entries);
|
|
631
|
+
return true;
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
function underReplyRateLimit(msg, limiters) {
|
|
635
|
+
return msg.isDirectMessage ? limiters.dm(msg.chatId) : limiters.group(msg.chatId);
|
|
636
|
+
}
|
|
637
|
+
function isFromAnySession(id, registry) {
|
|
638
|
+
if (!id) return false;
|
|
639
|
+
for (const ids of registry.values()) {
|
|
640
|
+
if (ids.includes(id)) return true;
|
|
641
|
+
}
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
function isEffectivelyFromSelf(msg, registry) {
|
|
645
|
+
return msg.fromBot || isFromAnySession(msg.senderId, registry);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// src/channels/whatsapp/inbound.ts
|
|
649
|
+
function isGroupJid(jid) {
|
|
650
|
+
return jid.endsWith("@g.us");
|
|
651
|
+
}
|
|
652
|
+
function jidsMatch(a, b) {
|
|
653
|
+
if (!a || !b) return false;
|
|
654
|
+
const bare = (jid) => jid.split("@")[0]?.split(":")[0] ?? jid;
|
|
655
|
+
return bare(a) === bare(b);
|
|
656
|
+
}
|
|
657
|
+
function jidToPhoneNumber(jid) {
|
|
658
|
+
const bare = jid.split("@")[0]?.split(":")[0] ?? "";
|
|
659
|
+
return bare.startsWith("+") ? bare : `+${bare}`;
|
|
660
|
+
}
|
|
661
|
+
function normalizeJid(jid) {
|
|
662
|
+
const [user = "", domain] = jid.split("@");
|
|
663
|
+
const bareUser = user.split(":")[0] ?? user;
|
|
664
|
+
return domain ? `${bareUser}@${domain}` : bareUser;
|
|
665
|
+
}
|
|
666
|
+
async function senderPhoneFor(sock, message, chatJid) {
|
|
667
|
+
const key = message.key;
|
|
668
|
+
let jid = [key.participantPn, key.senderPn, key.participant, key.remoteJidAlt, chatJid].find(
|
|
669
|
+
(candidate) => typeof candidate === "string" && candidate.length > 0
|
|
670
|
+
) ?? chatJid;
|
|
671
|
+
if (jid.endsWith("@lid")) {
|
|
672
|
+
try {
|
|
673
|
+
const mapped = await sock.signalRepository?.lidMapping?.getPNForLID?.(jid);
|
|
674
|
+
if (mapped) jid = mapped;
|
|
675
|
+
} catch {
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return jid.endsWith("@lid") ? `lid:${jid.split("@")[0]?.split(":")[0] ?? jid}` : jidToPhoneNumber(jid);
|
|
679
|
+
}
|
|
680
|
+
function messageContext(message) {
|
|
681
|
+
const content = message.message;
|
|
682
|
+
const contextInfo = content?.extendedTextMessage?.contextInfo ?? content?.imageMessage?.contextInfo ?? content?.videoMessage?.contextInfo ?? content?.documentMessage?.contextInfo;
|
|
683
|
+
return {
|
|
684
|
+
mentionedJids: contextInfo?.mentionedJid ?? [],
|
|
685
|
+
quotedParticipantJid: contextInfo?.participant ?? void 0
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
function extractText(message) {
|
|
689
|
+
const content = message.message;
|
|
690
|
+
if (!content) return "";
|
|
691
|
+
return content.conversation ?? content.extendedTextMessage?.text ?? content.imageMessage?.caption ?? content.videoMessage?.caption ?? content.documentMessage?.caption ?? "";
|
|
692
|
+
}
|
|
693
|
+
var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
|
|
694
|
+
var DEFAULT_DM_RATE_LIMIT = { max: 20, windowMs: 60 * 60 * 1e3 };
|
|
695
|
+
async function resolvePersonaLayer(config, ctx) {
|
|
696
|
+
if (!config.personaResolver) return void 0;
|
|
697
|
+
try {
|
|
698
|
+
return await config.personaResolver(ctx);
|
|
699
|
+
} catch {
|
|
700
|
+
return void 0;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function createWhatsAppInboundHandler(config) {
|
|
704
|
+
const mutedChats = /* @__PURE__ */ new Set();
|
|
705
|
+
const now = config.now ?? Date.now;
|
|
706
|
+
const group = config.groupRateLimit ?? DEFAULT_GROUP_RATE_LIMIT;
|
|
707
|
+
const dm = config.dmRateLimit ?? DEFAULT_DM_RATE_LIMIT;
|
|
708
|
+
const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
|
|
709
|
+
const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
|
|
710
|
+
return async function handleWhatsAppMessage(event) {
|
|
711
|
+
const { sessionId, sock, message } = event;
|
|
712
|
+
try {
|
|
713
|
+
const chatId = message.key.remoteJid ?? "";
|
|
714
|
+
if (!chatId || chatId === "status@broadcast") return;
|
|
715
|
+
const text = extractText(message);
|
|
716
|
+
const context = messageContext(message);
|
|
717
|
+
const senderId = message.key.participant ?? chatId;
|
|
718
|
+
const ownIds = [sock.user?.id, sock.user?.lid].filter((id) => Boolean(id)).map(normalizeJid);
|
|
719
|
+
config.registry.set(sessionId, ownIds);
|
|
720
|
+
const fromBot = isEffectivelyFromSelf(
|
|
721
|
+
{ fromBot: message.key.fromMe ?? false, senderId: normalizeJid(senderId) },
|
|
722
|
+
config.registry
|
|
723
|
+
);
|
|
724
|
+
const msg = {
|
|
725
|
+
chatId,
|
|
726
|
+
senderId,
|
|
727
|
+
text,
|
|
728
|
+
isDirectMessage: !isGroupJid(chatId),
|
|
729
|
+
mentionsBot: context.mentionedJids.some((jid) => ownIds.some((own) => jidsMatch(jid, own))),
|
|
730
|
+
isReplyToBot: ownIds.some((own) => jidsMatch(context.quotedParticipantJid, own)),
|
|
731
|
+
fromBot
|
|
732
|
+
};
|
|
733
|
+
const action = decideChannelAction(msg, {
|
|
734
|
+
allowedChats: config.allowedChats ?? [],
|
|
735
|
+
mutedChats,
|
|
736
|
+
muteRegex: config.muteRegex,
|
|
737
|
+
unmuteRegex: config.unmuteRegex
|
|
738
|
+
});
|
|
739
|
+
if (action === "mute") {
|
|
740
|
+
mutedChats.add(chatId);
|
|
741
|
+
if (config.muteReply) await sock.sendMessage(chatId, { text: config.muteReply });
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (action === "unmute") {
|
|
745
|
+
mutedChats.delete(chatId);
|
|
746
|
+
if (config.unmuteReply) await sock.sendMessage(chatId, { text: config.unmuteReply });
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (action !== "reply") return;
|
|
750
|
+
if (!underReplyRateLimit(msg, { dm: underDmRateLimit, group: underGroupRateLimit })) {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const senderPhone = await senderPhoneFor(sock, message, chatId);
|
|
754
|
+
const personaLayer = await resolvePersonaLayer(config, { senderPhone, text });
|
|
755
|
+
const buckets = await resolveBuckets({
|
|
756
|
+
mode: "public",
|
|
757
|
+
sender: senderPhone,
|
|
758
|
+
bucketsFor: config.bucketsFor
|
|
759
|
+
});
|
|
760
|
+
const messages = [{ role: "user", content: text }];
|
|
761
|
+
const { system } = await prepareChat({
|
|
762
|
+
store: config.store,
|
|
763
|
+
prompts: config.prompts,
|
|
764
|
+
mode: "public",
|
|
765
|
+
messages,
|
|
766
|
+
personaLayer,
|
|
767
|
+
channelHint: config.channelHint,
|
|
768
|
+
buckets
|
|
769
|
+
});
|
|
770
|
+
const { content } = await answerOnce({
|
|
771
|
+
answerFn: config.answerFn,
|
|
772
|
+
client: config.client,
|
|
773
|
+
system,
|
|
774
|
+
messages,
|
|
775
|
+
mode: "public",
|
|
776
|
+
sender: senderPhone,
|
|
777
|
+
model: config.model
|
|
778
|
+
});
|
|
779
|
+
if (content) {
|
|
780
|
+
await sock.sendMessage(chatId, { text: content }, { quoted: message });
|
|
781
|
+
}
|
|
782
|
+
} catch (error) {
|
|
783
|
+
console.warn(`WhatsApp[${sessionId}]: inbound message handling failed:`, error);
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
}
|
|
462
787
|
// Annotate the CommonJS export names for ESM import in node:
|
|
463
788
|
0 && (module.exports = {
|
|
464
789
|
LEASE_HEARTBEAT_MS,
|
|
@@ -468,11 +793,18 @@ function createWhatsAppChannel(config) {
|
|
|
468
793
|
canAcquireLease,
|
|
469
794
|
createTursoWaLeaseStore,
|
|
470
795
|
createWhatsAppChannel,
|
|
796
|
+
createWhatsAppInboundHandler,
|
|
471
797
|
decrypt,
|
|
472
798
|
encrypt,
|
|
799
|
+
extractText,
|
|
800
|
+
isGroupJid,
|
|
801
|
+
jidToPhoneNumber,
|
|
802
|
+
jidsMatch,
|
|
473
803
|
loadBaileys,
|
|
804
|
+
messageContext,
|
|
474
805
|
reconnectDelayMs,
|
|
475
806
|
senderNameFor,
|
|
807
|
+
senderPhoneFor,
|
|
476
808
|
shutdownWaSessions,
|
|
477
809
|
useTursoAuthState
|
|
478
810
|
});
|
|
@@ -410,6 +410,324 @@ function createWhatsAppChannel(config) {
|
|
|
410
410
|
}
|
|
411
411
|
};
|
|
412
412
|
}
|
|
413
|
+
|
|
414
|
+
// src/core/guardrails.ts
|
|
415
|
+
function detectLeakage(text) {
|
|
416
|
+
const flags = [
|
|
417
|
+
/system prompt/i,
|
|
418
|
+
/hidden instruction/i,
|
|
419
|
+
/here (are|is) (my|the) rules/i,
|
|
420
|
+
/BEGIN SYSTEM PROMPT/i,
|
|
421
|
+
/tool instructions/i
|
|
422
|
+
];
|
|
423
|
+
return flags.some((rx) => rx.test(text));
|
|
424
|
+
}
|
|
425
|
+
function scrubOutput(text) {
|
|
426
|
+
return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
|
|
427
|
+
}
|
|
428
|
+
function guardOutput(text) {
|
|
429
|
+
if (detectLeakage(text)) {
|
|
430
|
+
return "Sorry, I can't share internal instructions. How else can I help?";
|
|
431
|
+
}
|
|
432
|
+
return scrubOutput(text);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// src/core/llm.ts
|
|
436
|
+
var DEFAULT_MODEL = "gpt-4o";
|
|
437
|
+
async function completeOnce({
|
|
438
|
+
client,
|
|
439
|
+
system,
|
|
440
|
+
messages,
|
|
441
|
+
temperature = 0.2,
|
|
442
|
+
model = DEFAULT_MODEL
|
|
443
|
+
}) {
|
|
444
|
+
const res = await client.chat.completions.create({
|
|
445
|
+
model,
|
|
446
|
+
temperature,
|
|
447
|
+
messages: [{ role: "system", content: system }, ...messages]
|
|
448
|
+
});
|
|
449
|
+
const text = res.choices[0]?.message?.content ?? "";
|
|
450
|
+
return {
|
|
451
|
+
content: guardOutput(text),
|
|
452
|
+
usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/core/answer.ts
|
|
457
|
+
var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
|
|
458
|
+
async function runAnswerFn(answerFn, { system, messages, mode, sender }) {
|
|
459
|
+
const result = await answerFn({ system, messages, mode, sender });
|
|
460
|
+
const raw = typeof result === "string" ? result : result?.content;
|
|
461
|
+
const usage = typeof result === "string" ? void 0 : result?.usage;
|
|
462
|
+
return {
|
|
463
|
+
content: guardOutput(typeof raw === "string" ? raw : ""),
|
|
464
|
+
usage: usage ?? noUsage()
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
async function answerOnce({
|
|
468
|
+
answerFn,
|
|
469
|
+
client,
|
|
470
|
+
system,
|
|
471
|
+
messages,
|
|
472
|
+
mode,
|
|
473
|
+
sender,
|
|
474
|
+
temperature,
|
|
475
|
+
model
|
|
476
|
+
}) {
|
|
477
|
+
if (answerFn) {
|
|
478
|
+
return runAnswerFn(answerFn, { system, messages, mode, sender });
|
|
479
|
+
}
|
|
480
|
+
return completeOnce({ client, system, messages, temperature, model });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/core/buckets.ts
|
|
484
|
+
function defaultBuckets(mode) {
|
|
485
|
+
return ["base", mode];
|
|
486
|
+
}
|
|
487
|
+
function isIdentified(sender) {
|
|
488
|
+
return typeof sender === "string" && sender.trim().length > 0;
|
|
489
|
+
}
|
|
490
|
+
function normalize(buckets) {
|
|
491
|
+
const out = [];
|
|
492
|
+
for (const raw of buckets) {
|
|
493
|
+
if (typeof raw !== "string") continue;
|
|
494
|
+
const bucket = raw.trim();
|
|
495
|
+
if (bucket && !out.includes(bucket)) out.push(bucket);
|
|
496
|
+
}
|
|
497
|
+
return out;
|
|
498
|
+
}
|
|
499
|
+
async function resolveBuckets({
|
|
500
|
+
mode,
|
|
501
|
+
sender,
|
|
502
|
+
bucketsFor
|
|
503
|
+
}) {
|
|
504
|
+
if (!bucketsFor) return void 0;
|
|
505
|
+
const identified = isIdentified(sender);
|
|
506
|
+
const requested = await bucketsFor(identified ? { mode, sender } : { mode });
|
|
507
|
+
if (!Array.isArray(requested)) return void 0;
|
|
508
|
+
const cleaned = normalize(requested);
|
|
509
|
+
if (identified) return cleaned;
|
|
510
|
+
const ceiling = new Set(defaultBuckets(mode));
|
|
511
|
+
return cleaned.filter((bucket) => ceiling.has(bucket));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/core/pipeline.ts
|
|
515
|
+
var MODE_SETTINGS = {
|
|
516
|
+
public: { topK: 6, label: "Context" },
|
|
517
|
+
private: { topK: 8, label: "Internal Context" }
|
|
518
|
+
};
|
|
519
|
+
async function prepareChat({
|
|
520
|
+
store,
|
|
521
|
+
prompts,
|
|
522
|
+
mode,
|
|
523
|
+
messages,
|
|
524
|
+
personaLayer,
|
|
525
|
+
channelHint,
|
|
526
|
+
buckets
|
|
527
|
+
}) {
|
|
528
|
+
const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
|
|
529
|
+
if (!lastUserMsg) {
|
|
530
|
+
throw new Error("no user message found in conversation");
|
|
531
|
+
}
|
|
532
|
+
const { topK, label } = MODE_SETTINGS[mode];
|
|
533
|
+
const ctx = await store.query(lastUserMsg.content, topK, buckets ?? defaultBuckets(mode));
|
|
534
|
+
const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
|
|
535
|
+
const hint = channelHint?.trim();
|
|
536
|
+
const system = [
|
|
537
|
+
prompts.baseSystemRules,
|
|
538
|
+
persona,
|
|
539
|
+
...hint ? [hint] : [],
|
|
540
|
+
`${label}:
|
|
541
|
+
${ctx.join("\n\n")}`
|
|
542
|
+
].join("\n\n");
|
|
543
|
+
return { system, messages };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/channels/gates.ts
|
|
547
|
+
function matches(re, text) {
|
|
548
|
+
return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
|
|
549
|
+
}
|
|
550
|
+
function decideChannelAction(msg, config) {
|
|
551
|
+
if (msg.fromBot || !msg.text.trim()) return "ignore";
|
|
552
|
+
if (!msg.isDirectMessage) {
|
|
553
|
+
if (config.allowedChats.length > 0 && !config.allowedChats.includes(msg.chatId)) {
|
|
554
|
+
return "ignore";
|
|
555
|
+
}
|
|
556
|
+
if (config.muteRegex && matches(config.muteRegex, msg.text)) return "mute";
|
|
557
|
+
if (config.unmuteRegex && matches(config.unmuteRegex, msg.text)) return "unmute";
|
|
558
|
+
if (config.mutedChats.has(msg.chatId)) return "ignore";
|
|
559
|
+
return msg.mentionsBot || msg.isReplyToBot ? "reply" : "ignore";
|
|
560
|
+
}
|
|
561
|
+
return "reply";
|
|
562
|
+
}
|
|
563
|
+
function createSlidingWindowRateLimiter(maxPerWindow, windowMs, now = Date.now) {
|
|
564
|
+
const log = /* @__PURE__ */ new Map();
|
|
565
|
+
return function allow(key) {
|
|
566
|
+
const cutoff = now() - windowMs;
|
|
567
|
+
const entries = (log.get(key) ?? []).filter((t) => t > cutoff);
|
|
568
|
+
if (entries.length >= maxPerWindow) {
|
|
569
|
+
if (entries.length > 0) log.set(key, entries);
|
|
570
|
+
else log.delete(key);
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
entries.push(now());
|
|
574
|
+
log.set(key, entries);
|
|
575
|
+
return true;
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function underReplyRateLimit(msg, limiters) {
|
|
579
|
+
return msg.isDirectMessage ? limiters.dm(msg.chatId) : limiters.group(msg.chatId);
|
|
580
|
+
}
|
|
581
|
+
function isFromAnySession(id, registry) {
|
|
582
|
+
if (!id) return false;
|
|
583
|
+
for (const ids of registry.values()) {
|
|
584
|
+
if (ids.includes(id)) return true;
|
|
585
|
+
}
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
function isEffectivelyFromSelf(msg, registry) {
|
|
589
|
+
return msg.fromBot || isFromAnySession(msg.senderId, registry);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/channels/whatsapp/inbound.ts
|
|
593
|
+
function isGroupJid(jid) {
|
|
594
|
+
return jid.endsWith("@g.us");
|
|
595
|
+
}
|
|
596
|
+
function jidsMatch(a, b) {
|
|
597
|
+
if (!a || !b) return false;
|
|
598
|
+
const bare = (jid) => jid.split("@")[0]?.split(":")[0] ?? jid;
|
|
599
|
+
return bare(a) === bare(b);
|
|
600
|
+
}
|
|
601
|
+
function jidToPhoneNumber(jid) {
|
|
602
|
+
const bare = jid.split("@")[0]?.split(":")[0] ?? "";
|
|
603
|
+
return bare.startsWith("+") ? bare : `+${bare}`;
|
|
604
|
+
}
|
|
605
|
+
function normalizeJid(jid) {
|
|
606
|
+
const [user = "", domain] = jid.split("@");
|
|
607
|
+
const bareUser = user.split(":")[0] ?? user;
|
|
608
|
+
return domain ? `${bareUser}@${domain}` : bareUser;
|
|
609
|
+
}
|
|
610
|
+
async function senderPhoneFor(sock, message, chatJid) {
|
|
611
|
+
const key = message.key;
|
|
612
|
+
let jid = [key.participantPn, key.senderPn, key.participant, key.remoteJidAlt, chatJid].find(
|
|
613
|
+
(candidate) => typeof candidate === "string" && candidate.length > 0
|
|
614
|
+
) ?? chatJid;
|
|
615
|
+
if (jid.endsWith("@lid")) {
|
|
616
|
+
try {
|
|
617
|
+
const mapped = await sock.signalRepository?.lidMapping?.getPNForLID?.(jid);
|
|
618
|
+
if (mapped) jid = mapped;
|
|
619
|
+
} catch {
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return jid.endsWith("@lid") ? `lid:${jid.split("@")[0]?.split(":")[0] ?? jid}` : jidToPhoneNumber(jid);
|
|
623
|
+
}
|
|
624
|
+
function messageContext(message) {
|
|
625
|
+
const content = message.message;
|
|
626
|
+
const contextInfo = content?.extendedTextMessage?.contextInfo ?? content?.imageMessage?.contextInfo ?? content?.videoMessage?.contextInfo ?? content?.documentMessage?.contextInfo;
|
|
627
|
+
return {
|
|
628
|
+
mentionedJids: contextInfo?.mentionedJid ?? [],
|
|
629
|
+
quotedParticipantJid: contextInfo?.participant ?? void 0
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
function extractText(message) {
|
|
633
|
+
const content = message.message;
|
|
634
|
+
if (!content) return "";
|
|
635
|
+
return content.conversation ?? content.extendedTextMessage?.text ?? content.imageMessage?.caption ?? content.videoMessage?.caption ?? content.documentMessage?.caption ?? "";
|
|
636
|
+
}
|
|
637
|
+
var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
|
|
638
|
+
var DEFAULT_DM_RATE_LIMIT = { max: 20, windowMs: 60 * 60 * 1e3 };
|
|
639
|
+
async function resolvePersonaLayer(config, ctx) {
|
|
640
|
+
if (!config.personaResolver) return void 0;
|
|
641
|
+
try {
|
|
642
|
+
return await config.personaResolver(ctx);
|
|
643
|
+
} catch {
|
|
644
|
+
return void 0;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function createWhatsAppInboundHandler(config) {
|
|
648
|
+
const mutedChats = /* @__PURE__ */ new Set();
|
|
649
|
+
const now = config.now ?? Date.now;
|
|
650
|
+
const group = config.groupRateLimit ?? DEFAULT_GROUP_RATE_LIMIT;
|
|
651
|
+
const dm = config.dmRateLimit ?? DEFAULT_DM_RATE_LIMIT;
|
|
652
|
+
const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
|
|
653
|
+
const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
|
|
654
|
+
return async function handleWhatsAppMessage(event) {
|
|
655
|
+
const { sessionId, sock, message } = event;
|
|
656
|
+
try {
|
|
657
|
+
const chatId = message.key.remoteJid ?? "";
|
|
658
|
+
if (!chatId || chatId === "status@broadcast") return;
|
|
659
|
+
const text = extractText(message);
|
|
660
|
+
const context = messageContext(message);
|
|
661
|
+
const senderId = message.key.participant ?? chatId;
|
|
662
|
+
const ownIds = [sock.user?.id, sock.user?.lid].filter((id) => Boolean(id)).map(normalizeJid);
|
|
663
|
+
config.registry.set(sessionId, ownIds);
|
|
664
|
+
const fromBot = isEffectivelyFromSelf(
|
|
665
|
+
{ fromBot: message.key.fromMe ?? false, senderId: normalizeJid(senderId) },
|
|
666
|
+
config.registry
|
|
667
|
+
);
|
|
668
|
+
const msg = {
|
|
669
|
+
chatId,
|
|
670
|
+
senderId,
|
|
671
|
+
text,
|
|
672
|
+
isDirectMessage: !isGroupJid(chatId),
|
|
673
|
+
mentionsBot: context.mentionedJids.some((jid) => ownIds.some((own) => jidsMatch(jid, own))),
|
|
674
|
+
isReplyToBot: ownIds.some((own) => jidsMatch(context.quotedParticipantJid, own)),
|
|
675
|
+
fromBot
|
|
676
|
+
};
|
|
677
|
+
const action = decideChannelAction(msg, {
|
|
678
|
+
allowedChats: config.allowedChats ?? [],
|
|
679
|
+
mutedChats,
|
|
680
|
+
muteRegex: config.muteRegex,
|
|
681
|
+
unmuteRegex: config.unmuteRegex
|
|
682
|
+
});
|
|
683
|
+
if (action === "mute") {
|
|
684
|
+
mutedChats.add(chatId);
|
|
685
|
+
if (config.muteReply) await sock.sendMessage(chatId, { text: config.muteReply });
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (action === "unmute") {
|
|
689
|
+
mutedChats.delete(chatId);
|
|
690
|
+
if (config.unmuteReply) await sock.sendMessage(chatId, { text: config.unmuteReply });
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (action !== "reply") return;
|
|
694
|
+
if (!underReplyRateLimit(msg, { dm: underDmRateLimit, group: underGroupRateLimit })) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const senderPhone = await senderPhoneFor(sock, message, chatId);
|
|
698
|
+
const personaLayer = await resolvePersonaLayer(config, { senderPhone, text });
|
|
699
|
+
const buckets = await resolveBuckets({
|
|
700
|
+
mode: "public",
|
|
701
|
+
sender: senderPhone,
|
|
702
|
+
bucketsFor: config.bucketsFor
|
|
703
|
+
});
|
|
704
|
+
const messages = [{ role: "user", content: text }];
|
|
705
|
+
const { system } = await prepareChat({
|
|
706
|
+
store: config.store,
|
|
707
|
+
prompts: config.prompts,
|
|
708
|
+
mode: "public",
|
|
709
|
+
messages,
|
|
710
|
+
personaLayer,
|
|
711
|
+
channelHint: config.channelHint,
|
|
712
|
+
buckets
|
|
713
|
+
});
|
|
714
|
+
const { content } = await answerOnce({
|
|
715
|
+
answerFn: config.answerFn,
|
|
716
|
+
client: config.client,
|
|
717
|
+
system,
|
|
718
|
+
messages,
|
|
719
|
+
mode: "public",
|
|
720
|
+
sender: senderPhone,
|
|
721
|
+
model: config.model
|
|
722
|
+
});
|
|
723
|
+
if (content) {
|
|
724
|
+
await sock.sendMessage(chatId, { text: content }, { quoted: message });
|
|
725
|
+
}
|
|
726
|
+
} catch (error) {
|
|
727
|
+
console.warn(`WhatsApp[${sessionId}]: inbound message handling failed:`, error);
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
}
|
|
413
731
|
export {
|
|
414
732
|
LEASE_HEARTBEAT_MS,
|
|
415
733
|
LEASE_STALE_MS,
|
|
@@ -418,11 +736,18 @@ export {
|
|
|
418
736
|
canAcquireLease,
|
|
419
737
|
createTursoWaLeaseStore,
|
|
420
738
|
createWhatsAppChannel,
|
|
739
|
+
createWhatsAppInboundHandler,
|
|
421
740
|
decrypt,
|
|
422
741
|
encrypt,
|
|
742
|
+
extractText,
|
|
743
|
+
isGroupJid,
|
|
744
|
+
jidToPhoneNumber,
|
|
745
|
+
jidsMatch,
|
|
423
746
|
loadBaileys,
|
|
747
|
+
messageContext,
|
|
424
748
|
reconnectDelayMs,
|
|
425
749
|
senderNameFor,
|
|
750
|
+
senderPhoneFor,
|
|
426
751
|
shutdownWaSessions,
|
|
427
752
|
useTursoAuthState
|
|
428
753
|
};
|
package/package.json
CHANGED