La ejecución real falló con "MySQL server has gone away" tras escribir 4.522 de 5.663: leer los 228.887 registros tarda más de media hora, y para cuando empezaba a escribir el servidor ya había cerrado esa conexión. El singleton de Database no sabe reconectar. Ahora la lectura guarda el mapa en disco y termina; la segunda ejecución lo toma de la caché en un segundo y escribe con la conexión sana. Reintentar sale gratis, que era el otro problema: cada intento costaba media hora. También informa cuántos teléfonos tienen más de un BSUID. La simulación decía cero conflictos, pero no podía detectarlos: como no escribe, el bsuid del usuario siempre estaba vacío y la comparación nunca daba positivo. Al aplicar de verdad aparecieron. Ahora se cuentan al leer, sin depender de lo ya escrito. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
180 lines
7.3 KiB
PHP
180 lines
7.3 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.
|
|
*
|
|
* La lectura del histórico tarda más de media hora (son ~230.000 registros de
|
|
* un servidor remoto), y mantener la conexión abierta todo ese rato la mataba
|
|
* antes de empezar a escribir: "MySQL server has gone away". Por eso el mapa se
|
|
* guarda en disco y la escritura usa una conexión nueva. De paso, reintentar
|
|
* sale gratis: si el mapa ya está en caché, no se vuelve a leer la tabla.
|
|
*
|
|
* Uso:
|
|
* php scripts/backfill_bsuid.php --simular (no escribe, solo informa)
|
|
* php scripts/backfill_bsuid.php
|
|
* php scripts/backfill_bsuid.php --releer (ignora la caché y relee el histórico)
|
|
*/
|
|
|
|
require_once __DIR__ . '/../config/config.php';
|
|
|
|
$simular = in_array('--simular', $argv, true);
|
|
$releer = in_array('--releer', $argv, true);
|
|
echo $simular ? "Modo simulación: no se escribe nada.\n\n" : "Aplicando cambios.\n\n";
|
|
|
|
$cache = sys_get_temp_dir() . '/backfill_bsuid_mapa.json';
|
|
|
|
// --- 1. Recorrer el histórico y armar el mapa BSUID → teléfono ---
|
|
$mapa = [];
|
|
$sinTelefono = [];
|
|
|
|
if (!$releer && is_readable($cache)) {
|
|
$guardado = json_decode(file_get_contents($cache), true) ?: [];
|
|
$mapa = $guardado['mapa'] ?? [];
|
|
$sinTelefono = $guardado['sinTelefono'] ?? [];
|
|
printf("Mapa tomado de la caché (%s, del %s).\n", basename($cache), date('Y-m-d H:i', filemtime($cache)));
|
|
printf("Para releer el histórico: --releer\n\n");
|
|
}
|
|
|
|
if (!$mapa) {
|
|
echo "Leyendo webhook_logs (tarda; es una sola vez)...\n";
|
|
$db = Database::getInstance();
|
|
|
|
// 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
|
|
$conn->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, $bufferPrevio);
|
|
}
|
|
|
|
printf("\n registros revisados: %d\n", $leidos);
|
|
printf(" equivalencias encontradas: %d\n", count($mapa));
|
|
file_put_contents($cache, json_encode(['mapa' => $mapa, 'sinTelefono' => $sinTelefono]));
|
|
printf(" mapa guardado en %s\n", $cache);
|
|
|
|
// Se termina aquí a propósito. Tras media hora de lectura el servidor ya cerró
|
|
// esta conexión, y el singleton de Database no sabe reconectar: escribir ahora
|
|
// falla con "MySQL server has gone away" a mitad del recorrido, que es justo lo
|
|
// que pasó la primera vez. La segunda ejecución toma el mapa de la caché en un
|
|
// segundo y escribe con la conexión sana.
|
|
echo "\nMapa listo. Ejecute otra vez el comando para aplicarlo.\n";
|
|
exit(0);
|
|
}
|
|
|
|
printf(" equivalencias encontradas: %d\n", count($mapa));
|
|
printf(" personas que llegaron sin teléfono: %d\n", count($sinTelefono));
|
|
|
|
// --- 1b. ¿Hay teléfonos con más de un BSUID? ---
|
|
// Importa saberlo antes de escribir: si un mismo número aparece con dos
|
|
// identificadores es porque cambió de dueño, o porque la persona volvió a
|
|
// registrarse. Se queda el último visto, pero conviene que quede dicho.
|
|
$porTelefono = [];
|
|
foreach ($mapa as $b => $t) $porTelefono[$t][] = $b;
|
|
$ambiguos = array_filter($porTelefono, fn($bs) => count($bs) > 1);
|
|
printf(" teléfonos con más de un BSUID: %d\n\n", count($ambiguos));
|
|
foreach (array_slice($ambiguos, 0, 10, true) as $t => $bs) {
|
|
printf(" %s → %s\n", substr($t, 0, -4) . '****', implode(', ', $bs));
|
|
}
|
|
if (count($ambiguos) > 10) printf(" (y %d más)\n", count($ambiguos) - 10);
|
|
if ($ambiguos) echo "\n";
|
|
|
|
// --- 2. Escribir el BSUID en el usuario que corresponde ---
|
|
$db = Database::getInstance();
|
|
|
|
$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";
|