good forst commit

This commit is contained in:
2026-06-09 02:02:40 +05:30
parent 801c1d7121
commit 249d759e6a
215 changed files with 15425 additions and 1240 deletions
+118 -20
View File
@@ -9,9 +9,12 @@ import { createForwardWorker } from './queues/forward.processor';
import { createIndexQueue } from './queues/index.queue';
import { createIndexWorker } from './queues/index.processor';
import { WhatsAppSessionPool } from './whatsapp/session-pool';
import { detectTags, isFlagged } from './whatsapp/tag-detector';
import { matchContentRules, TenantRuleRow } from './whatsapp/match-rules';
import { syncGroups } from './whatsapp/group-sync';
import { handleStarReaction } from './core/approval';
import { handleReaction } from './core/approval';
import { approveMessage } from './core/approve-message';
import { startOtpSenderLoop } from './whatsapp/otp-sender';
import { handleCommand } from './whatsapp/command-handler';
const logger = createLogger('tower-worker');
@@ -19,11 +22,6 @@ async function bootstrap() {
const env = validateEnv();
const prisma = new PrismaClient();
await prisma.$connect();
const adminJids = env.TOWER_ADMIN_JIDS
? env.TOWER_ADMIN_JIDS.split(',').map((j) => j.trim()).filter(Boolean)
: [];
const meiliClient = createMeiliClient(env.MEILI_URL, env.MEILI_MASTER_KEY);
await configureIndex(meiliClient).catch((err) =>
logger.warn({ err }, 'Failed to configure Meilisearch index — search may be degraded'),
@@ -34,7 +32,7 @@ async function bootstrap() {
const indexQueue = createIndexQueue(env.REDIS_URL);
const pool = new WhatsAppSessionPool();
const ingestWorker = createIngestWorker(env.REDIS_URL, prisma);
const ingestWorker = createIngestWorker(env.REDIS_URL, prisma, pool, forwardQueue, indexQueue);
const forwardWorker = createForwardWorker(env.REDIS_URL, pool);
const indexWorker = createIndexWorker(env.REDIS_URL, meiliClient);
@@ -54,8 +52,12 @@ async function bootstrap() {
account.id,
account.sessionPath,
async (msg, accountId) => {
const tags = detectTags(msg.content, msg.senderJid, adminJids);
if (!isFlagged(tags)) return;
logger.info({ groupJid: msg.sourceGroupJid, senderJid: msg.senderJid, content: msg.content?.slice(0, 80) }, 'Message received');
// Command handler intercepts STOP/START/PORTAL from non-bot members
await handleCommand(msg, accountId, prisma, pool).catch((err) =>
logger.error({ err }, 'Command handler error'),
);
const groupMap = groupMaps.get(accountId);
if (!groupMap) {
@@ -68,9 +70,76 @@ async function bootstrap() {
return;
}
// Resolve tenant from the group; drop messages from unclaimed groups
const group = await prisma.group.findUnique({
where: { id: sourceGroupId },
select: { tenantId: true },
});
if (!group || !group.tenantId) {
logger.info({ sourceGroupId }, 'Group not yet claimed — message dropped');
return;
}
// Check tenant state
const tenant = await prisma.tenant.findUnique({
where: { id: group.tenantId },
select: { isActive: true, isForwardingPaused: true },
});
if (!tenant || !tenant.isActive) {
logger.info({ tenantId: group.tenantId, sourceGroupId }, 'Tenant is inactive — message dropped');
return;
}
if (tenant.isForwardingPaused) {
logger.info({ tenantId: group.tenantId, sourceGroupId }, 'Forwarding paused for tenant — message dropped');
return;
}
// Load active rules for this tenant
const ruleRows: TenantRuleRow[] = await prisma.tenantRule.findMany({
where: { tenantId: group.tenantId, isActive: true },
select: { id: true, matchType: true, matchValue: true, action: true, priority: true },
orderBy: { priority: 'asc' },
});
const { tags, effectiveAction } = matchContentRules(msg.content, ruleRows);
logger.info({ tags, effectiveAction }, 'Rule match result');
// No matching rules — drop the message
if (tags.length === 0) return;
// SKIP action — silently drop
if (effectiveAction === 'SKIP') {
logger.info({ platformMsgId: msg.platformMsgId }, 'Message skipped by rule');
return;
}
// For AUTO_APPROVE, check if the sender is a group admin
let finalAction = effectiveAction;
if (effectiveAction === 'AUTO_APPROVE') {
try {
const sock = pool.get(accountId);
if (sock) {
const metadata = await sock.groupMetadata(msg.sourceGroupJid);
const isAdmin = metadata.participants?.some(
(p: any) => p.id === msg.senderJid && (p.admin === 'admin' || p.admin === 'superadmin'),
);
if (isAdmin) {
finalAction = 'AUTO_APPROVE';
} else {
logger.info({ senderJid: msg.senderJid }, 'Sender is not a group admin — downgrading AUTO_APPROVE to FLAG');
finalAction = 'FLAG';
}
}
} catch (err) {
logger.warn({ err, senderJid: msg.senderJid }, 'Failed to check group admin status — downgrading to FLAG');
finalAction = 'FLAG';
}
}
await ingestQueue.add(
'ingest',
{
tenantId: group.tenantId,
platformMsgId: msg.platformMsgId,
platform: 'whatsapp',
accountId,
@@ -79,14 +148,15 @@ async function bootstrap() {
senderName: msg.senderName,
content: msg.content,
tags,
effectiveAction: finalAction ?? undefined,
},
{ attempts: 3, backoff: { type: 'exponential', delay: 1000 } },
);
logger.info({ platformMsgId: msg.platformMsgId, tags }, 'Message enqueued');
logger.info({ platformMsgId: msg.platformMsgId, tags, effectiveAction: finalAction }, 'Message enqueued');
},
async (reaction) => {
const result = await handleStarReaction(reaction, adminJids, prisma);
const result = await handleReaction(reaction, prisma, pool, forwardQueue, indexQueue);
if (!result) return;
const { forwardJobs, indexDoc } = result;
@@ -110,13 +180,13 @@ async function bootstrap() {
},
async (groups, accountId) => {
logger.info({ count: Object.keys(groups).length, accountId }, 'Syncing groups');
const map = await syncGroups(groups, accountId, prisma);
const map = await syncGroups(groups, accountId, prisma, pool);
groupMaps.set(accountId, map);
},
async (qr, accountId) => {
await prisma.account.update({
where: { id: accountId },
data: { qrCode: qr },
data: { qrCode: qr, status: 'PAIRING' },
}).catch((err) => logger.error({ accountId, err }, 'Failed to store QR in DB'));
logger.info({ accountId }, 'QR code updated');
},
@@ -133,6 +203,12 @@ async function bootstrap() {
data: { status: 'DISCONNECTED' },
}).catch((err) => logger.error({ accountId, err }, 'Failed to update account status'));
logger.info({ accountId }, 'Account logged out — awaiting QR scan');
} else if (status === 'disconnected') {
await prisma.account.update({
where: { id: accountId },
data: { status: 'DISCONNECTED' },
}).catch((err) => logger.error({ accountId, err }, 'Failed to update account status'));
logger.info({ accountId }, 'Account disconnected');
}
},
);
@@ -141,14 +217,14 @@ async function bootstrap() {
}
}
// Load ACTIVE and DISCONNECTED accounts at startup (DISCONNECTED ones need re-auth)
// Load bot accounts at startup: ACTIVE, DISCONNECTED (need re-auth), PAIRING (mid-pairing)
const accounts = await prisma.account.findMany({
where: { status: { in: ['ACTIVE', 'DISCONNECTED'] }, platform: 'whatsapp' },
where: { isBot: true, status: { in: ['ACTIVE', 'DISCONNECTED', 'PAIRING'] }, platform: 'whatsapp' },
select: { id: true, sessionPath: true },
});
if (accounts.length === 0) {
logger.warn('No WhatsApp accounts found — add one via the dashboard');
logger.warn('No bot accounts found — pair one via /settings/bot');
}
for (const account of accounts) {
@@ -157,16 +233,18 @@ async function bootstrap() {
logger.info({ accountCount: accounts.length }, 'Tower worker ready');
// Poll every 30s for accounts added via the dashboard while worker is running
// Poll every 30s for accounts added via the dashboard while worker is running.
// Every 60s, also re-fetch groups for each active session so newly-added groups
// are detected without requiring a worker restart.
setInterval(async () => {
try {
const all = await prisma.account.findMany({
where: { status: { in: ['ACTIVE', 'DISCONNECTED'] }, platform: 'whatsapp' },
where: { isBot: true, status: { in: ['ACTIVE', 'DISCONNECTED', 'PAIRING'] }, platform: 'whatsapp' },
select: { id: true, sessionPath: true },
});
for (const account of all) {
if (!pool.get(account.id)) {
logger.info({ accountId: account.id }, 'New account detected — starting session');
logger.info({ accountId: account.id }, 'New bot account detected — starting session');
await startAccount(account);
}
}
@@ -175,6 +253,26 @@ async function bootstrap() {
}
}, 30_000);
setInterval(async () => {
try {
for (const [accountId, sock] of pool.getAll()) {
try {
const groups = await sock.groupFetchAllParticipating();
logger.info({ count: Object.keys(groups).length, accountId }, 'Periodic group re-sync');
const map = await syncGroups(groups, accountId, prisma, pool);
groupMaps.set(accountId, map);
} catch (err) {
logger.warn({ accountId, err }, 'Periodic group re-sync failed');
}
}
} catch (err) {
logger.error({ err }, 'Error in periodic group re-sync loop');
}
}, 60_000);
// OTP-sender loop: pick up unsent OtpChallenges and DM them via the bot
startOtpSenderLoop(prisma, pool, logger);
const shutdown = async () => {
logger.info('Shutting down...');
await pool.closeAll();