Files
whatsapp/scripts/backfill_bsuid.php
T
Lizandro GuarnizoandClaude Opus 5 26025a6399 Reconocer por BSUID a quien oculta su teléfono, y poder pedírselo
Meta no permite averiguar el teléfono a partir del BSUID: no hay endpoint de
consulta inversa, cada empresa debe llevar su propia equivalencia. Pero el
BSUID llega en TODOS los webhooks de mensaje, también en los que aún traen
teléfono, así que la equivalencia se puede ir guardando sola mientras la
persona todavía muestra su número.

- users.bsuid guarda esa equivalencia, y el webhook la anota en cada mensaje.
  Cuando alguien oculte su número, se le seguirá reconociendo y respondiendo
  a su teléfono de siempre.
- Va en columna aparte y no en phone_number porque el teléfono además cruza
  con el paciente y el turnero; mezclarlos rompería esos cruces.
- Para quien nunca escribió mostrando el número queda pedirle el contacto:
  pedirContacto() manda el botón request_contact_info, y el webhook atiende
  el mensaje `contacts` que llega si acepta.
- Al vincular puede aparecer un segundo registro de la misma persona. No se
  fusionan: una fusión mal hecha mezcla dos historias clínicas. Gana el que
  tiene el teléfono, el otro queda anotado en el log para revisarlo a mano.
- scripts/backfill_bsuid.php carga las equivalencias del histórico (6.423),
  con --simular para verlas sin escribir.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 20:07:52 -05:00

132 lines
5.0 KiB
PHP

<?php
/**
* scripts/backfill_bsuid.php
*
* Rellena users.bsuid a partir del histórico de webhook_logs.
*
* Meta manda el BSUID en todos los webhooks de mensaje, así que en el histórico
* están las equivalencias BSUID↔teléfono de la gente que escribió cuando aún
* mostraba su número. Cargarlas significa que, el día que oculten el teléfono,
* el bot siga sabiendo quiénes son en vez de tratarlos como desconocidos.
*
* Es idempotente: se puede correr las veces que haga falta.
*
* Uso:
* php scripts/backfill_bsuid.php --simular (no escribe, solo informa)
* php scripts/backfill_bsuid.php
*/
require_once __DIR__ . '/../config/config.php';
$simular = in_array('--simular', $argv, true);
echo $simular ? "Modo simulación: no se escribe nada.\n\n" : "Aplicando cambios.\n\n";
$db = Database::getInstance();
// --- 1. Recorrer el histórico y armar el mapa BSUID → teléfono ---
echo "Leyendo webhook_logs...\n";
$mapa = [];
$sinTelefono = [];
// Una sola pasada sin buffer: son más de 400.000 registros, así que ni caben
// en memoria de golpe ni conviene reconsultar la tabla por bloques (cada bloque
// volvía a recorrerla entera y tardaba una eternidad).
$conn = $db->getConnection();
$bufferPrevio = $conn->getAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY);
$conn->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
$leidos = 0;
try {
$stmt = $conn->query(
"SELECT request_body FROM webhook_logs
WHERE request_body LIKE '%user_id%' AND request_body LIKE '%\"messages\"%'"
);
while ($fila = $stmt->fetch(PDO::FETCH_ASSOC)) {
$leidos++;
if ($leidos % 50000 === 0) printf(" ...%d registros\n", $leidos);
$d = json_decode($fila['request_body'], true);
if (!$d) continue;
foreach ($d['entry'] ?? [] as $entry) {
foreach ($entry['changes'] ?? [] as $cambio) {
$v = $cambio['value'] ?? [];
// El teléfono puede venir en contacts[].wa_id o en messages[].from
foreach ($v['contacts'] ?? [] as $c) {
if (!empty($c['user_id']) && !empty($c['wa_id'])) {
$mapa[$c['user_id']] = $c['wa_id'];
}
}
foreach ($v['messages'] ?? [] as $m) {
$uid = $m['from_user_id'] ?? null;
if (!$uid) continue;
if (!empty($m['from'])) $mapa[$uid] = $m['from'];
else $sinTelefono[$uid] = true;
}
// Cuando alguien comparte su contacto, el teléfono llega aquí
foreach ($v['messages'] ?? [] as $m) {
if (($m['type'] ?? '') !== 'contacts') continue;
$uid = $m['from_user_id'] ?? null;
foreach ($m['contacts'] ?? [] as $c) {
foreach ($c['phones'] ?? [] as $t) {
$num = $t['wa_id'] ?? ($t['phone'] ?? null);
if ($uid && $num) $mapa[$uid] = preg_replace('/[^0-9]/', '', $num);
}
}
}
}
}
}
$stmt->closeCursor();
} finally {
// Dejar la conexión como estaba: las escrituras siguientes la necesitan con buffer
$conn->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, $bufferPrevio);
}
printf("\n registros revisados: %d\n", $leidos);
printf(" equivalencias encontradas: %d\n", count($mapa));
printf(" personas que llegaron sin teléfono: %d\n\n", count($sinTelefono));
// --- 2. Escribir el BSUID en el usuario que corresponde ---
$marcados = 0; $noExisten = 0; $yaEstaban = 0; $conflictos = 0;
foreach ($mapa as $bsuid => $telefono) {
$u = $db->fetch("SELECT id, bsuid FROM users WHERE phone_number = :t", ['t' => $telefono]);
if (!$u) { $noExisten++; continue; }
if ($u['bsuid'] === $bsuid) { $yaEstaban++; continue; }
if (!empty($u['bsuid'])) {
// El BSUID se regenera si la persona cambia de número: gana el más reciente
printf(" aviso: %s tenía %s y ahora %s\n", $telefono, $u['bsuid'], $bsuid);
$conflictos++;
}
if (!$simular) {
try {
$db->update('users', ['bsuid' => $bsuid], 'id = ?', [$u['id']]);
} catch (Exception $e) {
printf(" error en %s: %s\n", $telefono, $e->getMessage());
continue;
}
}
$marcados++;
}
printf("\n usuarios marcados con su BSUID: %d\n", $marcados);
printf(" ya lo tenían: %d\n", $yaEstaban);
printf(" sin usuario en la base: %d\n", $noExisten);
printf(" BSUID reemplazados (cambio de número): %d\n", $conflictos);
// --- 3. ¿A cuántos de los perdidos rescatamos? ---
$rescatables = array_intersect_key($mapa, $sinTelefono);
printf("\n de los que llegaron sin teléfono, quedan identificados: %d\n", count($rescatables));
foreach ($rescatables as $b => $t) {
printf(" %-24s → %s\n", $b, substr($t, 0, -4) . '****');
}
$pendientes = array_diff_key($sinTelefono, $mapa);
printf("\n siguen sin identificar: %d (hay que pedirles el contacto)\n", count($pendientes));
echo $simular ? "\nSimulación terminada, no se escribió nada.\n" : "\nListo.\n";