|
|
|
|
@ -1,6 +1,5 @@
|
|
|
|
|
import type { Database } from 'bun:sqlite';
|
|
|
|
|
import { ResponseQueue } from './response-queue';
|
|
|
|
|
import { GroupSyncService } from './group-sync';
|
|
|
|
|
import { AllowedGroups } from './allowed-groups';
|
|
|
|
|
import { Metrics } from './metrics';
|
|
|
|
|
import { randomTokenBase64Url } from '../utils/crypto';
|
|
|
|
|
@ -45,7 +44,7 @@ export function buildJitAssigneePrompt(createdBy: string, groupId: string, unres
|
|
|
|
|
const list = unresolvedList.join(', ');
|
|
|
|
|
let groupCtx = '';
|
|
|
|
|
if (groupId && groupId.includes('@g.us')) {
|
|
|
|
|
const name = GroupSyncService.activeGroupsCache.get(groupId) || groupId;
|
|
|
|
|
const name = groupId;
|
|
|
|
|
groupCtx = ` (en el grupo ${name})`;
|
|
|
|
|
}
|
|
|
|
|
const msg = `No puedo asignar a ${list} aún${groupCtx}. Pídele que toque este enlace y diga 'activar': https://wa.me/${bot}`;
|
|
|
|
|
@ -102,7 +101,14 @@ export function maybeEnqueueOnboardingBundle(db: Database, params: {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Candidatos
|
|
|
|
|
let members = GroupSyncService.listActiveMemberIds(gid);
|
|
|
|
|
let members: string[] = [];
|
|
|
|
|
try {
|
|
|
|
|
const rows = db.prepare(`SELECT user_id FROM group_members WHERE group_id = ? AND is_active = 1`).all(gid) as Array<{ user_id: string }>;
|
|
|
|
|
for (const r of rows) {
|
|
|
|
|
const uid = String(r.user_id || '').trim();
|
|
|
|
|
if (/^\d+$/.test(uid) && uid.length < 14) members.push(uid);
|
|
|
|
|
}
|
|
|
|
|
} catch {}
|
|
|
|
|
const bot = String(process.env.CHATBOT_PHONE_NUMBER || '').trim();
|
|
|
|
|
const exclude = new Set<string>([params.createdBy, ...params.assignmentUserIds]);
|
|
|
|
|
members = members
|
|
|
|
|
@ -128,7 +134,12 @@ export function maybeEnqueueOnboardingBundle(db: Database, params: {
|
|
|
|
|
const delayEnv = Number(process.env.ONBOARDING_BUNDLE_DELAY_MS);
|
|
|
|
|
const delay2 = Number.isFinite(delayEnv) && delayEnv >= 0 ? Math.floor(delayEnv) : 5000 + Math.floor(Math.random() * 5001); // 5–10s
|
|
|
|
|
|
|
|
|
|
const groupLabel = GroupSyncService.activeGroupsCache.get(gid) || gid;
|
|
|
|
|
let groupLabel = gid;
|
|
|
|
|
try {
|
|
|
|
|
const row = db.prepare(`SELECT name FROM groups WHERE id = ?`).get(gid) as any;
|
|
|
|
|
const name = row?.name ? String(row.name).trim() : '';
|
|
|
|
|
if (name) groupLabel = name;
|
|
|
|
|
} catch {}
|
|
|
|
|
const codeStr = String(displayCode);
|
|
|
|
|
const desc = (params.description || '(sin descripción)').trim();
|
|
|
|
|
const shortDesc = desc.length > 100 ? (desc.slice(0, 100) + '…') : desc;
|
|
|
|
|
@ -196,3 +207,91 @@ Puedes interactuar con el bot escribiéndome por privado:
|
|
|
|
|
} catch {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function publishGroupCoveragePrompt(db: Database, groupId: string, ratio: number): void {
|
|
|
|
|
try {
|
|
|
|
|
const isTest = String(process.env.NODE_ENV || '').toLowerCase() === 'test';
|
|
|
|
|
const enabled =
|
|
|
|
|
isTest
|
|
|
|
|
? String(process.env.ONBOARDING_ENABLE_IN_TEST || '').toLowerCase() === 'true'
|
|
|
|
|
: (() => {
|
|
|
|
|
const v = process.env.ONBOARDING_PROMPTS_ENABLED;
|
|
|
|
|
return v == null ? true : ['true', '1', 'yes'].includes(String(v).toLowerCase());
|
|
|
|
|
})();
|
|
|
|
|
if (!enabled) {
|
|
|
|
|
try { Metrics.inc('onboarding_prompts_skipped_total', 1, { group_id: groupId, reason: 'disabled' }); } catch {}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Gating enforce
|
|
|
|
|
try {
|
|
|
|
|
const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase();
|
|
|
|
|
if (mode === 'enforce') {
|
|
|
|
|
try { (AllowedGroups as any).dbInstance = db; } catch {}
|
|
|
|
|
if (!AllowedGroups.isAllowed(groupId)) {
|
|
|
|
|
try { Metrics.inc('onboarding_prompts_skipped_total', 1, { group_id: groupId, reason: 'not_allowed' }); } catch {}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch {}
|
|
|
|
|
|
|
|
|
|
// Grace y cooldown
|
|
|
|
|
const rowG = db.prepare(`SELECT last_verified, onboarding_prompted_at FROM groups WHERE id = ?`).get(groupId) as any;
|
|
|
|
|
const nowMs = Date.now();
|
|
|
|
|
const graceRaw = Number(process.env.ONBOARDING_GRACE_SECONDS);
|
|
|
|
|
const graceSec = Number.isFinite(graceRaw) && graceRaw >= 0 ? Math.floor(graceRaw) : 90;
|
|
|
|
|
|
|
|
|
|
const lv = rowG?.last_verified ? String(rowG.last_verified) : null;
|
|
|
|
|
if (lv) {
|
|
|
|
|
const iso = lv.includes('T') ? lv : (lv.replace(' ', 'T') + 'Z');
|
|
|
|
|
const ms = Date.parse(iso);
|
|
|
|
|
if (Number.isFinite(ms)) {
|
|
|
|
|
const ageSec = Math.floor((nowMs - ms) / 1000);
|
|
|
|
|
if (ageSec < graceSec) {
|
|
|
|
|
try { Metrics.inc('onboarding_prompts_skipped_total', 1, { group_id: groupId, reason: 'grace_period' }); } catch {}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const cdRaw = Number(process.env.ONBOARDING_COOLDOWN_DAYS);
|
|
|
|
|
const cdDays = Number.isFinite(cdRaw) && cdRaw >= 0 ? Math.floor(cdRaw) : 7;
|
|
|
|
|
const promptedAt = rowG?.onboarding_prompted_at ? String(rowG.onboarding_prompted_at) : null;
|
|
|
|
|
if (promptedAt) {
|
|
|
|
|
const iso = promptedAt.includes('T') ? promptedAt : (promptedAt.replace(' ', 'T') + 'Z');
|
|
|
|
|
const ms = Date.parse(iso);
|
|
|
|
|
if (Number.isFinite(ms)) {
|
|
|
|
|
const diffMs = nowMs - ms;
|
|
|
|
|
if (diffMs < cdDays * 24 * 60 * 60 * 1000) {
|
|
|
|
|
try { Metrics.inc('onboarding_prompts_skipped_total', 1, { group_id: groupId, reason: 'cooldown_active' }); } catch {}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bot = String(process.env.CHATBOT_PHONE_NUMBER || '').trim();
|
|
|
|
|
if (!bot || !/^\d+$/.test(bot)) {
|
|
|
|
|
try { Metrics.inc('onboarding_prompts_skipped_total', 1, { group_id: groupId, reason: 'missing_bot_number' }); } catch {}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const msg = `Para poder asignarte tareas y acceder a la web, envía 'activar' al bot por privado. Solo hace falta una vez. Enlace: https://wa.me/${bot}`;
|
|
|
|
|
db.transaction(() => {
|
|
|
|
|
db.prepare(`
|
|
|
|
|
INSERT INTO response_queue (recipient, message, status, attempts, metadata, created_at, updated_at, next_attempt_at)
|
|
|
|
|
VALUES (?, ?, 'queued', 0, NULL, strftime('%Y-%m-%d %H:%M:%f','now'), strftime('%Y-%m-%d %H:%M:%f','now'), strftime('%Y-%m-%d %H:%M:%f','now'))
|
|
|
|
|
`).run(groupId, msg);
|
|
|
|
|
db.prepare(`
|
|
|
|
|
UPDATE groups
|
|
|
|
|
SET onboarding_prompted_at = strftime('%Y-%m-%d %H:%M:%f','now')
|
|
|
|
|
WHERE id = ?
|
|
|
|
|
`).run(groupId);
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
try { Metrics.inc('onboarding_prompts_sent_total', 1, { group_id: groupId, reason: 'coverage_below_threshold' }); } catch {}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
|
|
|
console.warn('⚠️ Onboarding prompt skipped due to internal error for', groupId, e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|