@rubytech/create-maxy 1.0.474 → 1.0.476
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/package.json +1 -1
- package/payload/platform/plugins/whatsapp/PLUGIN.md +19 -1
- package/payload/platform/plugins/whatsapp/mcp/dist/index.js +133 -0
- package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -1
- package/payload/server/public/assets/ChatInput-sDYraTun.css +1 -0
- package/payload/server/public/assets/{admin-CW4uB1GJ.js → admin-DpmnCxNk.js} +60 -60
- package/payload/server/public/assets/{public-DEZaIlO-.js → public-BBxDqQvQ.js} +1 -1
- package/payload/server/public/index.html +3 -3
- package/payload/server/public/public.html +3 -3
- package/payload/server/server.js +413 -86
- package/payload/server/public/assets/ChatInput-BZayQkAS.css +0 -1
- /package/payload/server/public/assets/{ChatInput-DgjefIvo.js → ChatInput-DZ0j0Gdp.js} +0 -0
package/payload/server/server.js
CHANGED
|
@@ -5211,6 +5211,10 @@ var ADMIN_CORE_TOOLS = [
|
|
|
5211
5211
|
"mcp__whatsapp__whatsapp-send",
|
|
5212
5212
|
"mcp__whatsapp__whatsapp-send-document",
|
|
5213
5213
|
"mcp__whatsapp__whatsapp-config",
|
|
5214
|
+
"mcp__whatsapp__whatsapp-activity",
|
|
5215
|
+
"mcp__whatsapp__whatsapp-conversations",
|
|
5216
|
+
"mcp__whatsapp__whatsapp-messages",
|
|
5217
|
+
"mcp__whatsapp__whatsapp-group-info",
|
|
5214
5218
|
"mcp__admin__system-status",
|
|
5215
5219
|
"mcp__admin__brand-settings",
|
|
5216
5220
|
"mcp__admin__account-manage",
|
|
@@ -23724,7 +23728,8 @@ var WhatsAppAccountSchema = external_exports.object({
|
|
|
23724
23728
|
group: external_exports.enum(["always", "mentions", "never"]).optional().default("mentions")
|
|
23725
23729
|
}).strict().optional().describe("React to incoming messages with an emoji to acknowledge receipt before the agent responds."),
|
|
23726
23730
|
debounceMs: external_exports.number().int().nonnegative().optional().default(2e3).describe("Wait this many milliseconds after the last message before processing, to batch rapid multi-message input into a single agent turn. Default 2000ms catches most multi-message bursts. 0 means process each message immediately."),
|
|
23727
|
-
publicAgent: external_exports.string().optional().describe("Slug of the public agent that handles WhatsApp inbound from non-admin phones for this account. Overrides the top-level publicAgent when set.")
|
|
23731
|
+
publicAgent: external_exports.string().optional().describe("Slug of the public agent that handles WhatsApp inbound from non-admin phones for this account. Overrides the top-level publicAgent when set."),
|
|
23732
|
+
afterHoursMessage: external_exports.string().optional().describe("Auto-reply text sent to public senders when the business is closed (outside opening hours). If not set, messages received outside business hours are silently skipped with no reply.")
|
|
23728
23733
|
}).strict().superRefine((value, ctx) => {
|
|
23729
23734
|
if (value.dmPolicy !== "open") return;
|
|
23730
23735
|
const allow = (value.allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean);
|
|
@@ -23746,7 +23751,8 @@ var WhatsAppConfigSchema = external_exports.object({
|
|
|
23746
23751
|
mediaMaxMb: external_exports.number().int().positive().optional().default(50).describe("Maximum file size in MB for media the agent will download and process."),
|
|
23747
23752
|
textChunkLimit: external_exports.number().int().positive().optional().default(4e3).describe("Maximum characters per outbound message. Longer replies are split into multiple messages."),
|
|
23748
23753
|
debounceMs: external_exports.number().int().nonnegative().optional().default(2e3).describe("Wait this many milliseconds after the last message before processing, to batch rapid multi-message input into a single agent turn. Default 2000ms catches most multi-message bursts. 0 means process each message immediately."),
|
|
23749
|
-
publicAgent: external_exports.string().optional().describe("Slug of the public agent that handles WhatsApp inbound from non-admin phones. Per-account overrides take precedence.")
|
|
23754
|
+
publicAgent: external_exports.string().optional().describe("Slug of the public agent that handles WhatsApp inbound from non-admin phones. Per-account overrides take precedence."),
|
|
23755
|
+
afterHoursMessage: external_exports.string().optional().describe("Default auto-reply text sent to public senders when the business is closed (outside opening hours). Per-account overrides take precedence. If not set, messages received outside business hours are silently skipped.")
|
|
23750
23756
|
}).strict().superRefine((value, ctx) => {
|
|
23751
23757
|
if (value.dmPolicy !== "open") return;
|
|
23752
23758
|
const allow = (value.allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean);
|
|
@@ -24318,8 +24324,60 @@ function isDuplicateInbound(key) {
|
|
|
24318
24324
|
return true;
|
|
24319
24325
|
}
|
|
24320
24326
|
|
|
24327
|
+
// app/lib/whatsapp/activity.ts
|
|
24328
|
+
var TAG5 = "[whatsapp:activity]";
|
|
24329
|
+
var RECENT_EVENTS_MAX = 200;
|
|
24330
|
+
var counters = /* @__PURE__ */ new Map();
|
|
24331
|
+
var recentEvents = [];
|
|
24332
|
+
function recordActivity(event) {
|
|
24333
|
+
try {
|
|
24334
|
+
const now = Date.now();
|
|
24335
|
+
const { accountId, direction, jid, messageType } = event;
|
|
24336
|
+
let acct = counters.get(accountId);
|
|
24337
|
+
if (!acct) {
|
|
24338
|
+
acct = { inbound: 0, outbound: 0, firstEventAt: now, lastEventAt: now };
|
|
24339
|
+
counters.set(accountId, acct);
|
|
24340
|
+
}
|
|
24341
|
+
acct[direction]++;
|
|
24342
|
+
acct.lastEventAt = now;
|
|
24343
|
+
const entry = {
|
|
24344
|
+
channel: "whatsapp",
|
|
24345
|
+
accountId,
|
|
24346
|
+
direction,
|
|
24347
|
+
jid,
|
|
24348
|
+
timestamp: now,
|
|
24349
|
+
messageType
|
|
24350
|
+
};
|
|
24351
|
+
recentEvents.push(entry);
|
|
24352
|
+
if (recentEvents.length > RECENT_EVENTS_MAX) {
|
|
24353
|
+
recentEvents.splice(0, recentEvents.length - RECENT_EVENTS_MAX);
|
|
24354
|
+
}
|
|
24355
|
+
console.error(
|
|
24356
|
+
`${TAG5} channel-activity: direction=${direction} account=${accountId} jid=${jid}` + (messageType ? ` type=${messageType}` : "")
|
|
24357
|
+
);
|
|
24358
|
+
} catch (err) {
|
|
24359
|
+
console.error(`${TAG5} recording failed: ${String(err)}`);
|
|
24360
|
+
}
|
|
24361
|
+
}
|
|
24362
|
+
function getChannelActivity(accountId) {
|
|
24363
|
+
const accounts = [];
|
|
24364
|
+
for (const [id, acct] of counters) {
|
|
24365
|
+
if (accountId && id !== accountId) continue;
|
|
24366
|
+
accounts.push({
|
|
24367
|
+
accountId: id,
|
|
24368
|
+
inbound: acct.inbound,
|
|
24369
|
+
outbound: acct.outbound,
|
|
24370
|
+
total: acct.inbound + acct.outbound,
|
|
24371
|
+
firstEventAt: acct.firstEventAt,
|
|
24372
|
+
lastEventAt: acct.lastEventAt
|
|
24373
|
+
});
|
|
24374
|
+
}
|
|
24375
|
+
const events = accountId ? recentEvents.filter((e) => e.accountId === accountId) : [...recentEvents];
|
|
24376
|
+
return { accounts, recentEvents: events };
|
|
24377
|
+
}
|
|
24378
|
+
|
|
24321
24379
|
// app/lib/whatsapp/outbound/send.ts
|
|
24322
|
-
var
|
|
24380
|
+
var TAG6 = "[whatsapp:outbound]";
|
|
24323
24381
|
async function sendTextMessage(sock, to, text, opts) {
|
|
24324
24382
|
try {
|
|
24325
24383
|
const jid = to.includes("@") ? to : toWhatsappJid(to);
|
|
@@ -24331,11 +24389,14 @@ async function sendTextMessage(sock, to, text, opts) {
|
|
|
24331
24389
|
const messageId = result?.key?.id;
|
|
24332
24390
|
if (messageId) {
|
|
24333
24391
|
trackAgentSentMessage(messageId);
|
|
24334
|
-
console.error(`${
|
|
24392
|
+
console.error(`${TAG6} sent text to=${jid} id=${messageId}`);
|
|
24393
|
+
}
|
|
24394
|
+
if (opts?.accountId) {
|
|
24395
|
+
recordActivity({ accountId: opts.accountId, direction: "outbound", jid, messageType: "text" });
|
|
24335
24396
|
}
|
|
24336
24397
|
return { success: true, messageId: messageId ?? void 0 };
|
|
24337
24398
|
} catch (err) {
|
|
24338
|
-
console.error(`${
|
|
24399
|
+
console.error(`${TAG6} send failed to=${to}: ${String(err)}`);
|
|
24339
24400
|
return { success: false, error: String(err) };
|
|
24340
24401
|
}
|
|
24341
24402
|
}
|
|
@@ -24351,7 +24412,7 @@ async function sendReadReceipt(sock, chatJid, messageIds, participant) {
|
|
|
24351
24412
|
} catch {
|
|
24352
24413
|
}
|
|
24353
24414
|
}
|
|
24354
|
-
async function sendMediaMessage(sock, to, media) {
|
|
24415
|
+
async function sendMediaMessage(sock, to, media, opts) {
|
|
24355
24416
|
try {
|
|
24356
24417
|
const jid = to.includes("@") ? to : toWhatsappJid(to);
|
|
24357
24418
|
let payload;
|
|
@@ -24390,11 +24451,14 @@ async function sendMediaMessage(sock, to, media) {
|
|
|
24390
24451
|
const messageId = result?.key?.id;
|
|
24391
24452
|
if (messageId) {
|
|
24392
24453
|
trackAgentSentMessage(messageId);
|
|
24393
|
-
console.error(`${
|
|
24454
|
+
console.error(`${TAG6} sent ${media.type} to=${jid} id=${messageId}`);
|
|
24455
|
+
}
|
|
24456
|
+
if (opts?.accountId) {
|
|
24457
|
+
recordActivity({ accountId: opts.accountId, direction: "outbound", jid, messageType: media.type });
|
|
24394
24458
|
}
|
|
24395
24459
|
return { success: true, messageId: messageId ?? void 0 };
|
|
24396
24460
|
} catch (err) {
|
|
24397
|
-
console.error(`${
|
|
24461
|
+
console.error(`${TAG6} send media failed to=${to}: ${String(err)}`);
|
|
24398
24462
|
return { success: false, error: String(err) };
|
|
24399
24463
|
}
|
|
24400
24464
|
}
|
|
@@ -24408,7 +24472,7 @@ import {
|
|
|
24408
24472
|
downloadContentFromMessage,
|
|
24409
24473
|
normalizeMessageContent as normalizeMessageContent2
|
|
24410
24474
|
} from "@whiskeysockets/baileys";
|
|
24411
|
-
var
|
|
24475
|
+
var TAG7 = "[whatsapp:media]";
|
|
24412
24476
|
var MEDIA_DIR = "/tmp/maxy-media";
|
|
24413
24477
|
function mimeToExt(mimetype) {
|
|
24414
24478
|
const map2 = {
|
|
@@ -24464,25 +24528,25 @@ async function downloadInboundMedia(msg, sock, opts) {
|
|
|
24464
24528
|
}
|
|
24465
24529
|
);
|
|
24466
24530
|
if (!buffer || buffer.length === 0) {
|
|
24467
|
-
console.error(`${
|
|
24531
|
+
console.error(`${TAG7} primary download returned empty, trying direct fallback`);
|
|
24468
24532
|
const downloadable = getDownloadableContent(content);
|
|
24469
24533
|
if (downloadable) {
|
|
24470
24534
|
try {
|
|
24471
24535
|
const stream = await downloadContentFromMessage(downloadable.downloadable, downloadable.mediaType);
|
|
24472
24536
|
buffer = await streamToBuffer(stream);
|
|
24473
24537
|
} catch (fallbackErr) {
|
|
24474
|
-
console.error(`${
|
|
24538
|
+
console.error(`${TAG7} direct download fallback failed: ${String(fallbackErr)}`);
|
|
24475
24539
|
}
|
|
24476
24540
|
}
|
|
24477
24541
|
}
|
|
24478
24542
|
if (!buffer || buffer.length === 0) {
|
|
24479
|
-
console.error(`${
|
|
24543
|
+
console.error(`${TAG7} download failed: empty buffer for ${mimetype ?? "unknown"}`);
|
|
24480
24544
|
return void 0;
|
|
24481
24545
|
}
|
|
24482
24546
|
if (buffer.length > maxBytes) {
|
|
24483
24547
|
const sizeMB = (buffer.length / (1024 * 1024)).toFixed(1);
|
|
24484
24548
|
const limitMB = (maxBytes / (1024 * 1024)).toFixed(0);
|
|
24485
|
-
console.error(`${
|
|
24549
|
+
console.error(`${TAG7} media too large type=${mimetype ?? "unknown"} size=${sizeMB}MB limit=${limitMB}MB`);
|
|
24486
24550
|
return void 0;
|
|
24487
24551
|
}
|
|
24488
24552
|
await mkdir2(MEDIA_DIR, { recursive: true });
|
|
@@ -24491,20 +24555,20 @@ async function downloadInboundMedia(msg, sock, opts) {
|
|
|
24491
24555
|
const filePath = join6(MEDIA_DIR, filename);
|
|
24492
24556
|
await writeFile2(filePath, buffer);
|
|
24493
24557
|
const sizeKB = (buffer.length / 1024).toFixed(0);
|
|
24494
|
-
console.error(`${
|
|
24558
|
+
console.error(`${TAG7} media downloaded type=${mimetype ?? "unknown"} size=${sizeKB}KB path=${filePath}`);
|
|
24495
24559
|
return {
|
|
24496
24560
|
path: filePath,
|
|
24497
24561
|
mimetype: mimetype ?? "application/octet-stream",
|
|
24498
24562
|
size: buffer.length
|
|
24499
24563
|
};
|
|
24500
24564
|
} catch (err) {
|
|
24501
|
-
console.error(`${
|
|
24565
|
+
console.error(`${TAG7} media download failed type=${mimetype ?? "unknown"} error=${String(err)}`);
|
|
24502
24566
|
return void 0;
|
|
24503
24567
|
}
|
|
24504
24568
|
}
|
|
24505
24569
|
|
|
24506
24570
|
// app/lib/whatsapp/inbound/debounce.ts
|
|
24507
|
-
var
|
|
24571
|
+
var TAG8 = "[whatsapp:debounce]";
|
|
24508
24572
|
function createInboundDebouncer(opts) {
|
|
24509
24573
|
const { debounceMs, buildKey, onFlush, onError } = opts;
|
|
24510
24574
|
const pending = /* @__PURE__ */ new Map();
|
|
@@ -24516,7 +24580,7 @@ function createInboundDebouncer(opts) {
|
|
|
24516
24580
|
pending.delete(key);
|
|
24517
24581
|
const batchSize = batch.entries.length;
|
|
24518
24582
|
try {
|
|
24519
|
-
console.error(`${
|
|
24583
|
+
console.error(`${TAG8} debounce flush key=${key} batchSize=${batchSize}`);
|
|
24520
24584
|
const result = onFlush(batch.entries);
|
|
24521
24585
|
if (result && typeof result.catch === "function") {
|
|
24522
24586
|
result.catch(onError);
|
|
@@ -24573,17 +24637,153 @@ function createInboundDebouncer(opts) {
|
|
|
24573
24637
|
return { enqueue, flush, destroy };
|
|
24574
24638
|
}
|
|
24575
24639
|
|
|
24640
|
+
// app/lib/whatsapp/opening-hours.ts
|
|
24641
|
+
var TAG9 = "[whatsapp:hours]";
|
|
24642
|
+
async function isBusinessOpen(accountId) {
|
|
24643
|
+
try {
|
|
24644
|
+
const timezone = await resolveTimezone(accountId);
|
|
24645
|
+
const now = /* @__PURE__ */ new Date();
|
|
24646
|
+
const dayOfWeek = getDayOfWeek(now, timezone);
|
|
24647
|
+
const previousDayOfWeek = getPreviousDayOfWeek(now, timezone);
|
|
24648
|
+
const currentTime = getCurrentTime(now, timezone);
|
|
24649
|
+
const session = getSession();
|
|
24650
|
+
try {
|
|
24651
|
+
const result = await session.run(
|
|
24652
|
+
`MATCH (b:LocalBusiness {accountId: $accountId})
|
|
24653
|
+
OPTIONAL MATCH (b)-[:HAS_HOURS]->(hours:OpeningHoursSpecification)
|
|
24654
|
+
WHERE hours.dayOfWeek IN [$dayOfWeek, $previousDayOfWeek]
|
|
24655
|
+
RETURN hours.dayOfWeek AS dayOfWeek, hours.opens AS opens, hours.closes AS closes`,
|
|
24656
|
+
{ accountId, dayOfWeek, previousDayOfWeek }
|
|
24657
|
+
);
|
|
24658
|
+
if (result.records.length === 0) {
|
|
24659
|
+
console.error(`${TAG9} [${accountId}] business hours check: no opening hours configured, treating as open`);
|
|
24660
|
+
return { open: true, reason: "no opening hours configured" };
|
|
24661
|
+
}
|
|
24662
|
+
const specs = result.records.filter((r) => r.get("opens") != null && r.get("closes") != null).map((r) => ({
|
|
24663
|
+
day: String(r.get("dayOfWeek")).trim(),
|
|
24664
|
+
opens: String(r.get("opens")).trim(),
|
|
24665
|
+
closes: String(r.get("closes")).trim()
|
|
24666
|
+
}));
|
|
24667
|
+
if (specs.length === 0) {
|
|
24668
|
+
console.error(`${TAG9} [${accountId}] business hours check: no opening hours configured, treating as open`);
|
|
24669
|
+
return { open: true, reason: "no opening hours configured" };
|
|
24670
|
+
}
|
|
24671
|
+
for (const spec of specs) {
|
|
24672
|
+
if (!spec.opens || !spec.closes || spec.opens === spec.closes) continue;
|
|
24673
|
+
const isYesterday = spec.day === previousDayOfWeek;
|
|
24674
|
+
if (isYesterday) {
|
|
24675
|
+
if (spec.opens > spec.closes && currentTime < spec.closes) {
|
|
24676
|
+
const hoursStr = `${spec.opens}-${spec.closes} (${spec.day})`;
|
|
24677
|
+
console.error(
|
|
24678
|
+
`${TAG9} [${accountId}] business hours check: open (day=${dayOfWeek}, time=${currentTime}, hours=${hoursStr})`
|
|
24679
|
+
);
|
|
24680
|
+
return {
|
|
24681
|
+
open: true,
|
|
24682
|
+
reason: "within business hours",
|
|
24683
|
+
dayOfWeek,
|
|
24684
|
+
currentTime,
|
|
24685
|
+
matchedHours: hoursStr
|
|
24686
|
+
};
|
|
24687
|
+
}
|
|
24688
|
+
} else if (isTimeInRange(currentTime, spec.opens, spec.closes)) {
|
|
24689
|
+
const hoursStr = `${spec.opens}-${spec.closes}`;
|
|
24690
|
+
console.error(
|
|
24691
|
+
`${TAG9} [${accountId}] business hours check: open (day=${dayOfWeek}, time=${currentTime}, hours=${hoursStr})`
|
|
24692
|
+
);
|
|
24693
|
+
return {
|
|
24694
|
+
open: true,
|
|
24695
|
+
reason: "within business hours",
|
|
24696
|
+
dayOfWeek,
|
|
24697
|
+
currentTime,
|
|
24698
|
+
matchedHours: hoursStr
|
|
24699
|
+
};
|
|
24700
|
+
}
|
|
24701
|
+
}
|
|
24702
|
+
const todaySpecs = specs.filter((s) => s.day === dayOfWeek);
|
|
24703
|
+
const allHours = todaySpecs.map((s) => `${s.opens}-${s.closes}`).join(", ") || "none today";
|
|
24704
|
+
console.error(
|
|
24705
|
+
`${TAG9} [${accountId}] business hours check: closed (day=${dayOfWeek}, time=${currentTime}, hours=${allHours})`
|
|
24706
|
+
);
|
|
24707
|
+
return {
|
|
24708
|
+
open: false,
|
|
24709
|
+
reason: "outside business hours",
|
|
24710
|
+
dayOfWeek,
|
|
24711
|
+
currentTime,
|
|
24712
|
+
matchedHours: allHours
|
|
24713
|
+
};
|
|
24714
|
+
} finally {
|
|
24715
|
+
await session.close();
|
|
24716
|
+
}
|
|
24717
|
+
} catch (err) {
|
|
24718
|
+
console.error(
|
|
24719
|
+
`${TAG9} [${accountId}] business hours check failed, treating as open: ${err instanceof Error ? err.message : String(err)}`
|
|
24720
|
+
);
|
|
24721
|
+
return { open: true, reason: "hours check failed (treating as open)" };
|
|
24722
|
+
}
|
|
24723
|
+
}
|
|
24724
|
+
async function resolveTimezone(accountId) {
|
|
24725
|
+
try {
|
|
24726
|
+
const tz = await getUserTimezone(accountId);
|
|
24727
|
+
if (tz) {
|
|
24728
|
+
Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
24729
|
+
return tz;
|
|
24730
|
+
}
|
|
24731
|
+
} catch {
|
|
24732
|
+
}
|
|
24733
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
24734
|
+
}
|
|
24735
|
+
function getDayOfWeek(date5, timezone) {
|
|
24736
|
+
return new Intl.DateTimeFormat("en-US", { weekday: "long", timeZone: timezone }).format(date5);
|
|
24737
|
+
}
|
|
24738
|
+
function getPreviousDayOfWeek(date5, timezone) {
|
|
24739
|
+
const yesterday = new Date(date5.getTime() - 24 * 60 * 60 * 1e3);
|
|
24740
|
+
return getDayOfWeek(yesterday, timezone);
|
|
24741
|
+
}
|
|
24742
|
+
function getCurrentTime(date5, timezone) {
|
|
24743
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
24744
|
+
hour: "2-digit",
|
|
24745
|
+
minute: "2-digit",
|
|
24746
|
+
hour12: false,
|
|
24747
|
+
timeZone: timezone
|
|
24748
|
+
}).formatToParts(date5);
|
|
24749
|
+
const hour = parts.find((p) => p.type === "hour")?.value ?? "00";
|
|
24750
|
+
const minute = parts.find((p) => p.type === "minute")?.value ?? "00";
|
|
24751
|
+
return `${hour}:${minute}`;
|
|
24752
|
+
}
|
|
24753
|
+
function isTimeInRange(time3, opens, closes) {
|
|
24754
|
+
if (opens <= closes) {
|
|
24755
|
+
return time3 >= opens && time3 < closes;
|
|
24756
|
+
}
|
|
24757
|
+
return time3 >= opens || time3 < closes;
|
|
24758
|
+
}
|
|
24759
|
+
|
|
24576
24760
|
// app/lib/whatsapp/manager.ts
|
|
24577
|
-
var
|
|
24761
|
+
var TAG10 = "[whatsapp:manager]";
|
|
24578
24762
|
var MAX_RECONNECT_ATTEMPTS = 10;
|
|
24763
|
+
var MESSAGE_STORE_MAX = 500;
|
|
24764
|
+
var GROUP_META_TTL = 5 * 60 * 1e3;
|
|
24579
24765
|
var connections = /* @__PURE__ */ new Map();
|
|
24580
24766
|
var configDir = null;
|
|
24581
24767
|
var whatsAppConfig = {};
|
|
24582
24768
|
var onInboundMessage = null;
|
|
24583
24769
|
var initialized = false;
|
|
24770
|
+
var messageStore = /* @__PURE__ */ new Map();
|
|
24771
|
+
function storeMessage(storeKey, entry) {
|
|
24772
|
+
let entries = messageStore.get(storeKey);
|
|
24773
|
+
if (!entries) {
|
|
24774
|
+
entries = [];
|
|
24775
|
+
messageStore.set(storeKey, entries);
|
|
24776
|
+
}
|
|
24777
|
+
entries.push(entry);
|
|
24778
|
+
if (entries.length > MESSAGE_STORE_MAX) {
|
|
24779
|
+
const trimmed = entries.length - MESSAGE_STORE_MAX;
|
|
24780
|
+
entries.splice(0, trimmed);
|
|
24781
|
+
console.error(`${TAG10} message store trimmed for ${storeKey}: ${trimmed} oldest messages removed`);
|
|
24782
|
+
}
|
|
24783
|
+
}
|
|
24584
24784
|
async function init(opts) {
|
|
24585
24785
|
if (initialized) {
|
|
24586
|
-
console.error(`${
|
|
24786
|
+
console.error(`${TAG10} already initialized`);
|
|
24587
24787
|
return;
|
|
24588
24788
|
}
|
|
24589
24789
|
configDir = opts.configDir;
|
|
@@ -24591,20 +24791,20 @@ async function init(opts) {
|
|
|
24591
24791
|
loadConfig(opts.accountConfig);
|
|
24592
24792
|
const accountIds = listCredentialAccountIds(configDir);
|
|
24593
24793
|
if (accountIds.length === 0) {
|
|
24594
|
-
console.error(`${
|
|
24794
|
+
console.error(`${TAG10} init: no stored WhatsApp credentials found`);
|
|
24595
24795
|
initialized = true;
|
|
24596
24796
|
return;
|
|
24597
24797
|
}
|
|
24598
|
-
console.error(`${
|
|
24798
|
+
console.error(`${TAG10} init: found ${accountIds.length} credentialed account(s): ${accountIds.join(", ")}`);
|
|
24599
24799
|
initialized = true;
|
|
24600
24800
|
for (const accountId of accountIds) {
|
|
24601
24801
|
const accountCfg = whatsAppConfig.accounts?.[accountId];
|
|
24602
24802
|
if (accountCfg?.enabled === false) {
|
|
24603
|
-
console.error(`${
|
|
24803
|
+
console.error(`${TAG10} skipping disabled account=${accountId}`);
|
|
24604
24804
|
continue;
|
|
24605
24805
|
}
|
|
24606
24806
|
startConnection(accountId).catch((err) => {
|
|
24607
|
-
console.error(`${
|
|
24807
|
+
console.error(`${TAG10} failed to auto-start account=${accountId}: ${formatError(err)}`);
|
|
24608
24808
|
});
|
|
24609
24809
|
}
|
|
24610
24810
|
}
|
|
@@ -24613,7 +24813,7 @@ async function startConnection(accountId) {
|
|
|
24613
24813
|
const authDir = resolveAuthDir(configDir, accountId);
|
|
24614
24814
|
const hasAuth = await authExists(authDir);
|
|
24615
24815
|
if (!hasAuth) {
|
|
24616
|
-
console.error(`${
|
|
24816
|
+
console.error(`${TAG10} no credentials for account=${accountId}`);
|
|
24617
24817
|
return;
|
|
24618
24818
|
}
|
|
24619
24819
|
await stopConnection(accountId);
|
|
@@ -24628,7 +24828,8 @@ async function startConnection(accountId) {
|
|
|
24628
24828
|
reconnectAttempts: 0,
|
|
24629
24829
|
abortController: new AbortController(),
|
|
24630
24830
|
debouncer: null,
|
|
24631
|
-
lidMapping: null
|
|
24831
|
+
lidMapping: null,
|
|
24832
|
+
groupMetaCache: /* @__PURE__ */ new Map()
|
|
24632
24833
|
};
|
|
24633
24834
|
connections.set(accountId, conn);
|
|
24634
24835
|
await connectWithReconnect(conn);
|
|
@@ -24648,11 +24849,11 @@ async function stopConnection(accountId) {
|
|
|
24648
24849
|
conn.sock.ev.removeAllListeners("creds.update");
|
|
24649
24850
|
conn.sock.ws?.close?.();
|
|
24650
24851
|
} catch (err) {
|
|
24651
|
-
console.warn(`${
|
|
24852
|
+
console.warn(`${TAG10} socket cleanup error during stop account=${accountId}: ${String(err)}`);
|
|
24652
24853
|
}
|
|
24653
24854
|
}
|
|
24654
24855
|
connections.delete(accountId);
|
|
24655
|
-
console.error(`${
|
|
24856
|
+
console.error(`${TAG10} stopped account=${accountId}`);
|
|
24656
24857
|
}
|
|
24657
24858
|
function getStatus() {
|
|
24658
24859
|
return Array.from(connections.values()).map((conn) => ({
|
|
@@ -24686,18 +24887,19 @@ async function registerLoginSocket(accountId, sock, authDir) {
|
|
|
24686
24887
|
lastConnectedAt: Date.now(),
|
|
24687
24888
|
abortController: new AbortController(),
|
|
24688
24889
|
debouncer: null,
|
|
24689
|
-
lidMapping
|
|
24890
|
+
lidMapping,
|
|
24891
|
+
groupMetaCache: /* @__PURE__ */ new Map()
|
|
24690
24892
|
};
|
|
24691
24893
|
connections.set(accountId, conn);
|
|
24692
24894
|
try {
|
|
24693
24895
|
await sock.sendPresenceUpdate("available");
|
|
24694
|
-
console.error(`${
|
|
24896
|
+
console.error(`${TAG10} presence set to available account=${accountId}`);
|
|
24695
24897
|
} catch (err) {
|
|
24696
|
-
console.error(`${
|
|
24898
|
+
console.error(`${TAG10} presence update failed account=${accountId}: ${String(err)}`);
|
|
24697
24899
|
}
|
|
24698
24900
|
monitorInbound(conn);
|
|
24699
24901
|
watchForDisconnect(conn);
|
|
24700
|
-
console.error(`${
|
|
24902
|
+
console.error(`${TAG10} registered login socket for account=${accountId} phone=${selfId.e164 ?? "unknown"}`);
|
|
24701
24903
|
}
|
|
24702
24904
|
function reloadConfig(accountConfig) {
|
|
24703
24905
|
loadConfig(accountConfig);
|
|
@@ -24707,7 +24909,7 @@ async function shutdown() {
|
|
|
24707
24909
|
const ids = Array.from(connections.keys());
|
|
24708
24910
|
await Promise.all(ids.map((id) => stopConnection(id)));
|
|
24709
24911
|
initialized = false;
|
|
24710
|
-
console.error(`${
|
|
24912
|
+
console.error(`${TAG10} shutdown complete`);
|
|
24711
24913
|
}
|
|
24712
24914
|
function loadConfig(accountConfig) {
|
|
24713
24915
|
try {
|
|
@@ -24719,12 +24921,12 @@ function loadConfig(accountConfig) {
|
|
|
24719
24921
|
if (parsed.success) {
|
|
24720
24922
|
whatsAppConfig = parsed.data;
|
|
24721
24923
|
} else {
|
|
24722
|
-
console.error(`${
|
|
24924
|
+
console.error(`${TAG10} config validation failed: ${parsed.error.message}`);
|
|
24723
24925
|
whatsAppConfig = {};
|
|
24724
24926
|
}
|
|
24725
24927
|
}
|
|
24726
24928
|
} catch (err) {
|
|
24727
|
-
console.error(`${
|
|
24929
|
+
console.error(`${TAG10} config load error: ${String(err)}`);
|
|
24728
24930
|
whatsAppConfig = {};
|
|
24729
24931
|
}
|
|
24730
24932
|
}
|
|
@@ -24732,13 +24934,13 @@ async function connectWithReconnect(conn) {
|
|
|
24732
24934
|
const maxAttempts = MAX_RECONNECT_ATTEMPTS;
|
|
24733
24935
|
while (!conn.abortController.signal.aborted) {
|
|
24734
24936
|
try {
|
|
24735
|
-
console.error(`${
|
|
24937
|
+
console.error(`${TAG10} connecting account=${conn.accountId} attempt=${conn.reconnectAttempts}`);
|
|
24736
24938
|
const sock = await createWaSocket({
|
|
24737
24939
|
authDir: conn.authDir,
|
|
24738
24940
|
silent: true
|
|
24739
24941
|
});
|
|
24740
24942
|
conn.sock = sock;
|
|
24741
|
-
console.error(`${
|
|
24943
|
+
console.error(`${TAG10} socket created account=${conn.accountId} \u2014 waiting for connection`);
|
|
24742
24944
|
await waitForConnection(sock);
|
|
24743
24945
|
const selfId = readSelfId(conn.authDir);
|
|
24744
24946
|
conn.connected = true;
|
|
@@ -24748,12 +24950,12 @@ async function connectWithReconnect(conn) {
|
|
|
24748
24950
|
conn.reconnectAttempts = 0;
|
|
24749
24951
|
conn.lastError = void 0;
|
|
24750
24952
|
conn.lidMapping = sock.signalRepository?.lidMapping ?? null;
|
|
24751
|
-
console.error(`${
|
|
24953
|
+
console.error(`${TAG10} connected account=${conn.accountId} phone=${selfId.e164 ?? "unknown"}`);
|
|
24752
24954
|
try {
|
|
24753
24955
|
await sock.sendPresenceUpdate("available");
|
|
24754
|
-
console.error(`${
|
|
24956
|
+
console.error(`${TAG10} presence set to available account=${conn.accountId}`);
|
|
24755
24957
|
} catch (err) {
|
|
24756
|
-
console.error(`${
|
|
24958
|
+
console.error(`${TAG10} presence update failed account=${conn.accountId}: ${String(err)}`);
|
|
24757
24959
|
}
|
|
24758
24960
|
if (conn.debouncer) {
|
|
24759
24961
|
conn.debouncer.destroy();
|
|
@@ -24768,19 +24970,19 @@ async function connectWithReconnect(conn) {
|
|
|
24768
24970
|
conn.sock = null;
|
|
24769
24971
|
const classification = classifyDisconnect(err);
|
|
24770
24972
|
conn.lastError = classification.message;
|
|
24771
|
-
console.error(`${
|
|
24973
|
+
console.error(`${TAG10} disconnect account=${conn.accountId}: ${classification.kind} \u2014 ${classification.message}`);
|
|
24772
24974
|
if (!classification.shouldRetry) {
|
|
24773
|
-
console.error(`${
|
|
24975
|
+
console.error(`${TAG10} terminal disconnect for account=${conn.accountId}, stopping reconnection`);
|
|
24774
24976
|
return;
|
|
24775
24977
|
}
|
|
24776
24978
|
conn.reconnectAttempts++;
|
|
24777
24979
|
if (conn.reconnectAttempts > maxAttempts) {
|
|
24778
|
-
console.error(`${
|
|
24980
|
+
console.error(`${TAG10} max retries exceeded for account=${conn.accountId}`);
|
|
24779
24981
|
conn.lastError = `Max reconnect attempts (${maxAttempts}) exceeded`;
|
|
24780
24982
|
return;
|
|
24781
24983
|
}
|
|
24782
24984
|
const delay = computeBackoff(conn.reconnectAttempts);
|
|
24783
|
-
console.error(`${
|
|
24985
|
+
console.error(`${TAG10} reconnecting account=${conn.accountId} in ${delay}ms (attempt ${conn.reconnectAttempts}/${maxAttempts})`);
|
|
24784
24986
|
await new Promise((resolve19) => {
|
|
24785
24987
|
const timer = setTimeout(resolve19, delay);
|
|
24786
24988
|
conn.abortController.signal.addEventListener("abort", () => {
|
|
@@ -24810,20 +25012,51 @@ function watchForDisconnect(conn) {
|
|
|
24810
25012
|
conn.sock.ev.on("connection.update", (update) => {
|
|
24811
25013
|
if (update.connection === "close") {
|
|
24812
25014
|
if (connections.get(conn.accountId) !== conn) return;
|
|
24813
|
-
console.error(`${
|
|
25015
|
+
console.error(`${TAG10} socket disconnected for account=${conn.accountId}`);
|
|
24814
25016
|
conn.connected = false;
|
|
24815
25017
|
conn.sock = null;
|
|
24816
25018
|
connectWithReconnect(conn).catch((err) => {
|
|
24817
|
-
console.error(`${
|
|
25019
|
+
console.error(`${TAG10} reconnection failed for account=${conn.accountId}: ${formatError(err)}`);
|
|
24818
25020
|
});
|
|
24819
25021
|
}
|
|
24820
25022
|
});
|
|
24821
25023
|
}
|
|
25024
|
+
async function getGroupMeta(conn, jid) {
|
|
25025
|
+
const cached2 = conn.groupMetaCache.get(jid);
|
|
25026
|
+
if (cached2 && cached2.expires > Date.now()) return cached2;
|
|
25027
|
+
if (!conn.sock) return null;
|
|
25028
|
+
console.error(`${TAG10} group metadata cache miss for ${jid}, fetching from Baileys account=${conn.accountId}`);
|
|
25029
|
+
try {
|
|
25030
|
+
const meta3 = await conn.sock.groupMetadata(jid);
|
|
25031
|
+
const participants = await Promise.all(
|
|
25032
|
+
(meta3.participants ?? []).map(async (p) => {
|
|
25033
|
+
return await resolveJidToE164(p.id, conn.lidMapping) ?? p.id;
|
|
25034
|
+
})
|
|
25035
|
+
);
|
|
25036
|
+
const entry = {
|
|
25037
|
+
subject: meta3.subject,
|
|
25038
|
+
participants,
|
|
25039
|
+
expires: Date.now() + GROUP_META_TTL
|
|
25040
|
+
};
|
|
25041
|
+
conn.groupMetaCache.set(jid, entry);
|
|
25042
|
+
console.error(
|
|
25043
|
+
`${TAG10} group metadata cached for ${jid}: "${meta3.subject}", ${participants.length} participants, expires in ${GROUP_META_TTL}ms account=${conn.accountId}`
|
|
25044
|
+
);
|
|
25045
|
+
return entry;
|
|
25046
|
+
} catch (err) {
|
|
25047
|
+
console.error(
|
|
25048
|
+
`${TAG10} group metadata fetch failed for ${jid}: ${err instanceof Error ? err.message : String(err)}, caching empty entry for ${GROUP_META_TTL}ms account=${conn.accountId}`
|
|
25049
|
+
);
|
|
25050
|
+
const emptyEntry = { expires: Date.now() + GROUP_META_TTL };
|
|
25051
|
+
conn.groupMetaCache.set(jid, emptyEntry);
|
|
25052
|
+
return null;
|
|
25053
|
+
}
|
|
25054
|
+
}
|
|
24822
25055
|
function monitorInbound(conn) {
|
|
24823
25056
|
if (!conn.sock || !onInboundMessage) return;
|
|
24824
25057
|
const sock = conn.sock;
|
|
24825
25058
|
const debounceMs = whatsAppConfig.accounts?.[conn.accountId]?.debounceMs ?? whatsAppConfig.debounceMs ?? 0;
|
|
24826
|
-
console.error(`${
|
|
25059
|
+
console.error(`${TAG10} monitorInbound started account=${conn.accountId} debounceMs=${debounceMs}`);
|
|
24827
25060
|
conn.debouncer = createInboundDebouncer({
|
|
24828
25061
|
debounceMs,
|
|
24829
25062
|
buildKey: (payload) => {
|
|
@@ -24836,7 +25069,7 @@ function monitorInbound(conn) {
|
|
|
24836
25069
|
onInboundMessage(entries[0]);
|
|
24837
25070
|
return;
|
|
24838
25071
|
}
|
|
24839
|
-
console.error(`${
|
|
25072
|
+
console.error(`${TAG10} debounce: combining ${entries.length} messages account=${conn.accountId} from=${entries[0].senderPhone}`);
|
|
24840
25073
|
const last = entries[entries.length - 1];
|
|
24841
25074
|
const mediaEntry = entries.find((e) => e.mediaPath);
|
|
24842
25075
|
const combinedText = entries.map((e) => e.text).filter(Boolean).join("\n");
|
|
@@ -24849,16 +25082,60 @@ function monitorInbound(conn) {
|
|
|
24849
25082
|
});
|
|
24850
25083
|
},
|
|
24851
25084
|
onError: (err) => {
|
|
24852
|
-
console.error(`${
|
|
25085
|
+
console.error(`${TAG10} debounce flush error account=${conn.accountId}: ${String(err)}`);
|
|
24853
25086
|
}
|
|
24854
25087
|
});
|
|
24855
25088
|
sock.ev.on("messages.upsert", async (upsert) => {
|
|
24856
|
-
if (upsert.type !== "notify") return;
|
|
25089
|
+
if (upsert.type !== "notify" && upsert.type !== "append") return;
|
|
24857
25090
|
for (const msg of upsert.messages) {
|
|
24858
25091
|
try {
|
|
25092
|
+
const remoteJid = msg.key.remoteJid;
|
|
25093
|
+
if (!remoteJid || remoteJid === "status@broadcast") continue;
|
|
25094
|
+
if (!msg.message) continue;
|
|
25095
|
+
const extracted = extractMessage(msg);
|
|
25096
|
+
if (extracted.text) {
|
|
25097
|
+
const isGroup = isGroupJid(remoteJid);
|
|
25098
|
+
const senderJid = isGroup ? msg.key.participant ?? remoteJid : remoteJid;
|
|
25099
|
+
const senderPhone = await resolveJidToE164(senderJid, conn.lidMapping) ?? senderJid;
|
|
25100
|
+
const ts = msg.messageTimestamp ? Number(msg.messageTimestamp) : Math.floor(Date.now() / 1e3);
|
|
25101
|
+
const storeKey = `${conn.accountId}:${remoteJid}`;
|
|
25102
|
+
storeMessage(storeKey, {
|
|
25103
|
+
id: msg.key.id ?? `${remoteJid}-${ts}`,
|
|
25104
|
+
sender: senderPhone,
|
|
25105
|
+
senderName: msg.pushName ?? void 0,
|
|
25106
|
+
body: extracted.text,
|
|
25107
|
+
timestamp: ts,
|
|
25108
|
+
fromMe: Boolean(msg.key.fromMe),
|
|
25109
|
+
quoted: extracted.quotedMessage ? { id: extracted.quotedMessage.id, body: extracted.quotedMessage.text, sender: extracted.quotedMessage.sender } : void 0
|
|
25110
|
+
});
|
|
25111
|
+
const entries = messageStore.get(storeKey);
|
|
25112
|
+
console.error(
|
|
25113
|
+
`${TAG10} stored message ${msg.key.id ?? "?"} for ${remoteJid} (type: ${upsert.type}, store size: ${entries?.length ?? 0}/${MESSAGE_STORE_MAX}) account=${conn.accountId}`
|
|
25114
|
+
);
|
|
25115
|
+
recordActivity({
|
|
25116
|
+
accountId: conn.accountId,
|
|
25117
|
+
direction: "inbound",
|
|
25118
|
+
jid: remoteJid
|
|
25119
|
+
});
|
|
25120
|
+
}
|
|
25121
|
+
if (upsert.type === "append") {
|
|
25122
|
+
const sendReceipts = whatsAppConfig.accounts?.[conn.accountId]?.sendReadReceipts ?? whatsAppConfig.sendReadReceipts ?? true;
|
|
25123
|
+
if (sendReceipts && msg.key.id) {
|
|
25124
|
+
sendReadReceipt(
|
|
25125
|
+
sock,
|
|
25126
|
+
remoteJid,
|
|
25127
|
+
[msg.key.id],
|
|
25128
|
+
isGroupJid(remoteJid) ? msg.key.participant ?? void 0 : void 0
|
|
25129
|
+
);
|
|
25130
|
+
}
|
|
25131
|
+
console.error(
|
|
25132
|
+
`${TAG10} append-type message ${msg.key.id ?? "?"} stored, dispatch skipped account=${conn.accountId}`
|
|
25133
|
+
);
|
|
25134
|
+
continue;
|
|
25135
|
+
}
|
|
24859
25136
|
await handleInboundMessage(conn, msg);
|
|
24860
25137
|
} catch (err) {
|
|
24861
|
-
console.error(`${
|
|
25138
|
+
console.error(`${TAG10} inbound handler error account=${conn.accountId}: ${String(err)}`);
|
|
24862
25139
|
}
|
|
24863
25140
|
}
|
|
24864
25141
|
});
|
|
@@ -24868,35 +25145,35 @@ async function handleInboundMessage(conn, msg) {
|
|
|
24868
25145
|
const remoteJid = msg.key.remoteJid;
|
|
24869
25146
|
if (!remoteJid) return;
|
|
24870
25147
|
if (remoteJid === "status@broadcast") {
|
|
24871
|
-
console.error(`${
|
|
25148
|
+
console.error(`${TAG10} drop: status broadcast account=${conn.accountId}`);
|
|
24872
25149
|
return;
|
|
24873
25150
|
}
|
|
24874
25151
|
if (!msg.message) {
|
|
24875
|
-
console.error(`${
|
|
25152
|
+
console.error(`${TAG10} drop: empty message account=${conn.accountId} from=${remoteJid}`);
|
|
24876
25153
|
return;
|
|
24877
25154
|
}
|
|
24878
25155
|
const dedupKey = `${conn.accountId}:${remoteJid}:${msg.key.id}`;
|
|
24879
25156
|
if (isDuplicateInbound(dedupKey)) {
|
|
24880
|
-
console.error(`${
|
|
25157
|
+
console.error(`${TAG10} drop: duplicate account=${conn.accountId} key=${dedupKey}`);
|
|
24881
25158
|
return;
|
|
24882
25159
|
}
|
|
24883
25160
|
if (msg.key.fromMe) {
|
|
24884
25161
|
if (msg.key.id && isAgentSentMessage(msg.key.id)) {
|
|
24885
|
-
console.error(`${
|
|
25162
|
+
console.error(`${TAG10} drop: echo suppression account=${conn.accountId} msgId=${msg.key.id}`);
|
|
24886
25163
|
return;
|
|
24887
25164
|
}
|
|
24888
25165
|
const extracted2 = extractMessage(msg);
|
|
24889
25166
|
if (!extracted2.text) {
|
|
24890
|
-
console.error(`${
|
|
25167
|
+
console.error(`${TAG10} owner reply skipped \u2014 no text content account=${conn.accountId}`);
|
|
24891
25168
|
return;
|
|
24892
25169
|
}
|
|
24893
25170
|
const isGroup2 = isGroupJid(remoteJid);
|
|
24894
25171
|
const senderPhone2 = conn.selfPhone ?? "owner";
|
|
24895
|
-
console.error(`${
|
|
25172
|
+
console.error(`${TAG10} owner reply mirrored to session from=${senderPhone2} account=${conn.accountId}`);
|
|
24896
25173
|
const reply2 = async (text) => {
|
|
24897
25174
|
const currentSock = conn.sock;
|
|
24898
25175
|
if (!currentSock) throw new Error("WhatsApp disconnected \u2014 cannot reply");
|
|
24899
|
-
await sendTextMessage(currentSock, remoteJid, text);
|
|
25176
|
+
await sendTextMessage(currentSock, remoteJid, text, { accountId: conn.accountId });
|
|
24900
25177
|
};
|
|
24901
25178
|
onInboundMessage({
|
|
24902
25179
|
accountId: conn.accountId,
|
|
@@ -24913,7 +25190,7 @@ async function handleInboundMessage(conn, msg) {
|
|
|
24913
25190
|
}
|
|
24914
25191
|
const extracted = extractMessage(msg);
|
|
24915
25192
|
if (!extracted.text && !extracted.mediaType) {
|
|
24916
|
-
console.error(`${
|
|
25193
|
+
console.error(`${TAG10} drop: no text or media account=${conn.accountId} from=${remoteJid}`);
|
|
24917
25194
|
return;
|
|
24918
25195
|
}
|
|
24919
25196
|
let mediaResult;
|
|
@@ -24923,7 +25200,7 @@ async function handleInboundMessage(conn, msg) {
|
|
|
24923
25200
|
maxBytes: maxMb * 1024 * 1024
|
|
24924
25201
|
});
|
|
24925
25202
|
if (!mediaResult) {
|
|
24926
|
-
console.error(`${
|
|
25203
|
+
console.error(`${TAG10} media download returned undefined account=${conn.accountId} type=${extracted.mediaType} from=${remoteJid}`);
|
|
24927
25204
|
}
|
|
24928
25205
|
}
|
|
24929
25206
|
const isGroup = isGroupJid(remoteJid);
|
|
@@ -24951,9 +25228,14 @@ async function handleInboundMessage(conn, msg) {
|
|
|
24951
25228
|
});
|
|
24952
25229
|
}
|
|
24953
25230
|
console.error(
|
|
24954
|
-
`${
|
|
25231
|
+
`${TAG10} inbound account=${conn.accountId} from=${senderPhone} group=${isGroup} access=${accessResult.allowed ? "allowed" : "blocked"}(${accessResult.reason}) agent=${accessResult.agentType}` + (extracted.mediaType ? ` media=${extracted.mediaType}` : "") + (mediaResult ? ` mediaPath=${mediaResult.path}` : "") + (extracted.quotedMessage ? ` replyTo=${extracted.quotedMessage.id}` : "")
|
|
24955
25232
|
);
|
|
24956
25233
|
if (!accessResult.allowed) return;
|
|
25234
|
+
let groupSubject;
|
|
25235
|
+
if (isGroup) {
|
|
25236
|
+
const groupMeta = await getGroupMeta(conn, remoteJid);
|
|
25237
|
+
groupSubject = groupMeta?.subject;
|
|
25238
|
+
}
|
|
24957
25239
|
const sendReceipts = whatsAppConfig.accounts?.[conn.accountId]?.sendReadReceipts ?? whatsAppConfig.sendReadReceipts ?? true;
|
|
24958
25240
|
if (sendReceipts && msg.key.id) {
|
|
24959
25241
|
sendReadReceipt(conn.sock, remoteJid, [msg.key.id], isGroup ? senderJid : void 0);
|
|
@@ -24961,7 +25243,7 @@ async function handleInboundMessage(conn, msg) {
|
|
|
24961
25243
|
const reply = async (text) => {
|
|
24962
25244
|
const currentSock = conn.sock;
|
|
24963
25245
|
if (!currentSock) throw new Error("WhatsApp disconnected \u2014 cannot reply");
|
|
24964
|
-
await sendTextMessage(currentSock, remoteJid, text);
|
|
25246
|
+
await sendTextMessage(currentSock, remoteJid, text, { accountId: conn.accountId });
|
|
24965
25247
|
};
|
|
24966
25248
|
const sessionKey = accessResult.agentType === "admin" ? `whatsapp:${conn.accountId}` : void 0;
|
|
24967
25249
|
conn.lastMessageAt = Date.now();
|
|
@@ -24972,6 +25254,7 @@ async function handleInboundMessage(conn, msg) {
|
|
|
24972
25254
|
text: extracted.text,
|
|
24973
25255
|
isGroup,
|
|
24974
25256
|
groupJid: isGroup ? remoteJid : void 0,
|
|
25257
|
+
groupSubject,
|
|
24975
25258
|
reply,
|
|
24976
25259
|
sessionKey,
|
|
24977
25260
|
mediaPath: mediaResult?.path,
|
|
@@ -24979,6 +25262,24 @@ async function handleInboundMessage(conn, msg) {
|
|
|
24979
25262
|
mediaType: extracted.mediaType,
|
|
24980
25263
|
replyContext: extracted.quotedMessage
|
|
24981
25264
|
};
|
|
25265
|
+
if (accessResult.agentType === "public") {
|
|
25266
|
+
const hoursResult = await isBusinessOpen(conn.accountId);
|
|
25267
|
+
if (!hoursResult.open) {
|
|
25268
|
+
console.error(`${TAG10} [${conn.accountId}] dispatch skipped: business closed`);
|
|
25269
|
+
const afterHoursMessage = whatsAppConfig.accounts?.[conn.accountId]?.afterHoursMessage ?? whatsAppConfig.afterHoursMessage;
|
|
25270
|
+
if (afterHoursMessage) {
|
|
25271
|
+
try {
|
|
25272
|
+
await reply(afterHoursMessage);
|
|
25273
|
+
console.error(`${TAG10} [${conn.accountId}] after-hours auto-reply sent to ${remoteJid}`);
|
|
25274
|
+
} catch (err) {
|
|
25275
|
+
console.error(
|
|
25276
|
+
`${TAG10} [${conn.accountId}] after-hours auto-reply failed: ${err instanceof Error ? err.message : String(err)}`
|
|
25277
|
+
);
|
|
25278
|
+
}
|
|
25279
|
+
}
|
|
25280
|
+
return;
|
|
25281
|
+
}
|
|
25282
|
+
}
|
|
24982
25283
|
if (conn.debouncer) {
|
|
24983
25284
|
conn.debouncer.enqueue(payload);
|
|
24984
25285
|
} else {
|
|
@@ -25083,7 +25384,7 @@ async function POST13(req) {
|
|
|
25083
25384
|
{ status: 503 }
|
|
25084
25385
|
);
|
|
25085
25386
|
}
|
|
25086
|
-
const result = await sendTextMessage(sock, to, text);
|
|
25387
|
+
const result = await sendTextMessage(sock, to, text, { accountId });
|
|
25087
25388
|
return Response.json(result);
|
|
25088
25389
|
} catch (err) {
|
|
25089
25390
|
console.error(`[whatsapp:api] send error: ${String(err)}`);
|
|
@@ -25317,7 +25618,7 @@ async function POST14(req) {
|
|
|
25317
25618
|
import { readFile as readFile2, stat as stat2 } from "fs/promises";
|
|
25318
25619
|
import { realpathSync as realpathSync2 } from "fs";
|
|
25319
25620
|
import { resolve as resolve11, basename as basename2 } from "path";
|
|
25320
|
-
var
|
|
25621
|
+
var TAG11 = "[whatsapp:api]";
|
|
25321
25622
|
var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT || "";
|
|
25322
25623
|
async function POST15(req) {
|
|
25323
25624
|
try {
|
|
@@ -25340,16 +25641,16 @@ async function POST15(req) {
|
|
|
25340
25641
|
const accountResolved = realpathSync2(accountDir);
|
|
25341
25642
|
if (!resolvedPath.startsWith(accountResolved + "/")) {
|
|
25342
25643
|
const sanitised = filePath.replace(accountDir, "<account>/");
|
|
25343
|
-
console.error(`${
|
|
25644
|
+
console.error(`${TAG11} send-document REJECTED path=${sanitised} reason=outside_account_directory`);
|
|
25344
25645
|
return Response.json({ error: "Access denied: file is outside the account directory" }, { status: 403 });
|
|
25345
25646
|
}
|
|
25346
25647
|
} catch (err) {
|
|
25347
25648
|
const code = err.code;
|
|
25348
25649
|
if (code === "ENOENT") {
|
|
25349
|
-
console.error(`${
|
|
25650
|
+
console.error(`${TAG11} send-document ENOENT path=${filePath}`);
|
|
25350
25651
|
return Response.json({ error: `File not found: ${filePath}` }, { status: 404 });
|
|
25351
25652
|
}
|
|
25352
|
-
console.error(`${
|
|
25653
|
+
console.error(`${TAG11} send-document path error: ${String(err)}`);
|
|
25353
25654
|
return Response.json({ error: String(err) }, { status: 500 });
|
|
25354
25655
|
}
|
|
25355
25656
|
const fileStat = await stat2(resolvedPath);
|
|
@@ -25375,13 +25676,30 @@ async function POST15(req) {
|
|
|
25375
25676
|
mimetype,
|
|
25376
25677
|
filename,
|
|
25377
25678
|
caption
|
|
25378
|
-
});
|
|
25679
|
+
}, { accountId });
|
|
25680
|
+
console.error(
|
|
25681
|
+
`${TAG11} send-document to=${to} size=${fileStat.size} mime=${mimetype} ok=${result.success}` + (result.messageId ? ` id=${result.messageId}` : "")
|
|
25682
|
+
);
|
|
25683
|
+
return Response.json(result);
|
|
25684
|
+
} catch (err) {
|
|
25685
|
+
console.error(`${TAG11} send-document error: ${String(err)}`);
|
|
25686
|
+
return Response.json({ error: String(err) }, { status: 500 });
|
|
25687
|
+
}
|
|
25688
|
+
}
|
|
25689
|
+
|
|
25690
|
+
// app/api/whatsapp/activity/route.ts
|
|
25691
|
+
async function GET3(req) {
|
|
25692
|
+
try {
|
|
25693
|
+
const url2 = new URL(req.url);
|
|
25694
|
+
const accountId = url2.searchParams.get("accountId") ?? void 0;
|
|
25695
|
+
const result = getChannelActivity(accountId);
|
|
25696
|
+
const total = result.accounts.reduce((sum, a) => sum + a.total, 0);
|
|
25379
25697
|
console.error(
|
|
25380
|
-
|
|
25698
|
+
`[whatsapp:api] activity accounts=${result.accounts.length} total=${total} recentEvents=${result.recentEvents.length}` + (accountId ? ` filter=${accountId}` : "")
|
|
25381
25699
|
);
|
|
25382
25700
|
return Response.json(result);
|
|
25383
25701
|
} catch (err) {
|
|
25384
|
-
console.error(
|
|
25702
|
+
console.error(`[whatsapp:api] activity error: ${String(err)}`);
|
|
25385
25703
|
return Response.json({ error: String(err) }, { status: 500 });
|
|
25386
25704
|
}
|
|
25387
25705
|
}
|
|
@@ -25441,7 +25759,7 @@ async function waitForAuthPage(timeoutMs = 2e4) {
|
|
|
25441
25759
|
}
|
|
25442
25760
|
return false;
|
|
25443
25761
|
}
|
|
25444
|
-
async function
|
|
25762
|
+
async function GET4() {
|
|
25445
25763
|
return Response.json({ authenticated: checkAuthStatus() });
|
|
25446
25764
|
}
|
|
25447
25765
|
async function POST17(req) {
|
|
@@ -25591,11 +25909,19 @@ async function POST19(req) {
|
|
|
25591
25909
|
} catch (err) {
|
|
25592
25910
|
console.error(`[session] onboarding query failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
25593
25911
|
}
|
|
25912
|
+
let businessName;
|
|
25913
|
+
try {
|
|
25914
|
+
const branding = await fetchBranding(accountId);
|
|
25915
|
+
businessName = branding?.name || void 0;
|
|
25916
|
+
} catch {
|
|
25917
|
+
}
|
|
25918
|
+
console.log(`[session] businessName=${businessName ?? "\u2013"}`);
|
|
25594
25919
|
return Response.json({
|
|
25595
25920
|
session_key: sessionKey,
|
|
25596
25921
|
agent_id: "admin",
|
|
25597
25922
|
thinkingView,
|
|
25598
|
-
onboardingComplete
|
|
25923
|
+
onboardingComplete,
|
|
25924
|
+
businessName
|
|
25599
25925
|
});
|
|
25600
25926
|
}
|
|
25601
25927
|
|
|
@@ -25887,7 +26213,7 @@ async function POST21(req) {
|
|
|
25887
26213
|
import { existsSync as existsSync14, readdirSync as readdirSync3, readFileSync as readFileSync14, statSync as statSync3 } from "fs";
|
|
25888
26214
|
import { resolve as resolve12, basename as basename3 } from "path";
|
|
25889
26215
|
var TAIL_BYTES = 8192;
|
|
25890
|
-
async function
|
|
26216
|
+
async function GET5(request) {
|
|
25891
26217
|
const { searchParams } = new URL(request.url);
|
|
25892
26218
|
const fileParam = searchParams.get("file");
|
|
25893
26219
|
const typeParam = searchParams.get("type");
|
|
@@ -25961,7 +26287,7 @@ async function GET4(request) {
|
|
|
25961
26287
|
|
|
25962
26288
|
// app/api/admin/claude-info/route.ts
|
|
25963
26289
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
25964
|
-
async function
|
|
26290
|
+
async function GET6() {
|
|
25965
26291
|
let version2 = "unknown";
|
|
25966
26292
|
try {
|
|
25967
26293
|
const raw2 = execFileSync3("claude", ["--version"], { encoding: "utf-8", timeout: 5e3 });
|
|
@@ -25985,7 +26311,7 @@ async function GET5() {
|
|
|
25985
26311
|
import { readFile as readFile3, readdir } from "fs/promises";
|
|
25986
26312
|
import { existsSync as existsSync15 } from "fs";
|
|
25987
26313
|
import { resolve as resolve13 } from "path";
|
|
25988
|
-
async function
|
|
26314
|
+
async function GET7(req, attachmentId) {
|
|
25989
26315
|
const sessionKey = new URL(req.url).searchParams.get("session_key") ?? "";
|
|
25990
26316
|
if (!validateSession(sessionKey, "admin")) {
|
|
25991
26317
|
return new Response("Unauthorized", { status: 401 });
|
|
@@ -26069,7 +26395,7 @@ async function PATCH(req) {
|
|
|
26069
26395
|
// app/api/admin/agents/route.ts
|
|
26070
26396
|
import { resolve as resolve15 } from "path";
|
|
26071
26397
|
import { readdirSync as readdirSync4, readFileSync as readFileSync16, existsSync as existsSync16 } from "fs";
|
|
26072
|
-
async function
|
|
26398
|
+
async function GET8() {
|
|
26073
26399
|
const account = resolveAccount();
|
|
26074
26400
|
if (!account) {
|
|
26075
26401
|
return Response.json({ agents: [] });
|
|
@@ -26172,7 +26498,7 @@ function isNewer(latest, installed) {
|
|
|
26172
26498
|
function invalidateVersionCache() {
|
|
26173
26499
|
latestCache = null;
|
|
26174
26500
|
}
|
|
26175
|
-
async function
|
|
26501
|
+
async function GET9() {
|
|
26176
26502
|
const installed = readInstalled();
|
|
26177
26503
|
const latest = await fetchLatest();
|
|
26178
26504
|
const updateAvailable = installed !== "unknown" && latest !== null && isNewer(latest, installed);
|
|
@@ -26252,7 +26578,7 @@ async function POST22(req) {
|
|
|
26252
26578
|
}
|
|
26253
26579
|
|
|
26254
26580
|
// app/api/admin/sessions/route.ts
|
|
26255
|
-
async function
|
|
26581
|
+
async function GET10(req) {
|
|
26256
26582
|
const url2 = new URL(req.url);
|
|
26257
26583
|
const sessionKey = url2.searchParams.get("session_key");
|
|
26258
26584
|
if (!sessionKey) {
|
|
@@ -26303,7 +26629,7 @@ async function DELETE2(req, { params }) {
|
|
|
26303
26629
|
}
|
|
26304
26630
|
|
|
26305
26631
|
// app/api/admin/sessions/[id]/messages/route.ts
|
|
26306
|
-
async function
|
|
26632
|
+
async function GET11(req, { params }) {
|
|
26307
26633
|
const { id: conversationId } = await params;
|
|
26308
26634
|
const url2 = new URL(req.url);
|
|
26309
26635
|
const sessionKey = url2.searchParams.get("session_key");
|
|
@@ -26686,33 +27012,34 @@ app.post("/api/whatsapp/reconnect", (c) => POST12(c.req.raw));
|
|
|
26686
27012
|
app.post("/api/whatsapp/send", (c) => POST13(c.req.raw));
|
|
26687
27013
|
app.post("/api/whatsapp/config", (c) => POST14(c.req.raw));
|
|
26688
27014
|
app.post("/api/whatsapp/send-document", (c) => POST15(c.req.raw));
|
|
26689
|
-
app.get("/api/
|
|
27015
|
+
app.get("/api/whatsapp/activity", (c) => GET3(c.req.raw));
|
|
27016
|
+
app.get("/api/onboarding/claude-auth", () => GET4());
|
|
26690
27017
|
app.post("/api/onboarding/claude-auth", (c) => POST17(c.req.raw));
|
|
26691
27018
|
app.post("/api/onboarding/set-pin", (c) => POST18(c.req.raw));
|
|
26692
27019
|
app.delete("/api/onboarding/set-pin", (c) => DELETE(c.req.raw));
|
|
26693
27020
|
app.post("/api/admin/session", (c) => POST19(c.req.raw));
|
|
26694
27021
|
app.post("/api/admin/chat", (c) => POST20(c.req.raw));
|
|
26695
27022
|
app.post("/api/admin/compact", (c) => POST21(c.req.raw));
|
|
26696
|
-
app.get("/api/admin/logs", (c) =>
|
|
26697
|
-
app.get("/api/admin/claude-info", () =>
|
|
27023
|
+
app.get("/api/admin/logs", (c) => GET5(c.req.raw));
|
|
27024
|
+
app.get("/api/admin/claude-info", () => GET6());
|
|
26698
27025
|
app.patch("/api/admin/account", (c) => PATCH(c.req.raw));
|
|
26699
27026
|
app.get(
|
|
26700
27027
|
"/api/admin/attachment/:attachmentId",
|
|
26701
|
-
(c) =>
|
|
27028
|
+
(c) => GET7(c.req.raw, c.req.param("attachmentId"))
|
|
26702
27029
|
);
|
|
26703
27030
|
app.post("/api/admin/file-attach", (c) => POST16(c.req.raw));
|
|
26704
|
-
app.get("/api/admin/version", () =>
|
|
27031
|
+
app.get("/api/admin/version", () => GET9());
|
|
26705
27032
|
app.post("/api/admin/version/upgrade", (c) => POST22(c.req.raw));
|
|
26706
|
-
app.get("/api/admin/agents", () =>
|
|
27033
|
+
app.get("/api/admin/agents", () => GET8());
|
|
26707
27034
|
app.post("/api/admin/browser/launch", () => POST23());
|
|
26708
|
-
app.get("/api/admin/sessions", (c) =>
|
|
27035
|
+
app.get("/api/admin/sessions", (c) => GET10(c.req.raw));
|
|
26709
27036
|
app.delete(
|
|
26710
27037
|
"/api/admin/sessions/:id",
|
|
26711
27038
|
(c) => DELETE2(c.req.raw, { params: Promise.resolve({ id: c.req.param("id") }) })
|
|
26712
27039
|
);
|
|
26713
27040
|
app.get(
|
|
26714
27041
|
"/api/admin/sessions/:id/messages",
|
|
26715
|
-
(c) =>
|
|
27042
|
+
(c) => GET11(c.req.raw, { params: Promise.resolve({ id: c.req.param("id") }) })
|
|
26716
27043
|
);
|
|
26717
27044
|
var SAFE_SLUG_RE = /^[a-z][a-z0-9-]{2,49}$/;
|
|
26718
27045
|
var SAFE_FILENAME_RE = /^[a-z0-9_][a-z0-9_.-]{0,99}$/i;
|