chore: remove documentation files from PR
Documentation files removed as requested - implementation only. https://claude.ai/code/session_01SoNUGBEWbJwWWws3F2fuzh
This commit is contained in:
@@ -1,582 +0,0 @@
|
||||
# WhatsApp Device Migration - LID/PN (Identificadores)
|
||||
|
||||
## 📋 Índice
|
||||
1. [O que é LID/PN](#o-que-é-lidpn)
|
||||
2. [Por que existe](#por-que-existe)
|
||||
3. [Como funciona](#como-funciona)
|
||||
4. [Quando usa LID vs PN](#quando-usa-lid-vs-pn)
|
||||
5. [Fluxo de Migração](#fluxo-de-migração)
|
||||
6. [Como aparece nos logs](#como-aparece-nos-logs)
|
||||
7. [Implementação técnica](#implementação-técnica)
|
||||
|
||||
---
|
||||
|
||||
## O que é LID/PN
|
||||
|
||||
### PN (Phone Number)
|
||||
- **Formato**: `12345@s.whatsapp.net` ou `12345:5@s.whatsapp.net`
|
||||
- **Significado**: Identificador baseado no número de telefone
|
||||
- **Exemplo**: Se seu número é +55 11 98765-4321, o PN seria `5511987654321@s.whatsapp.net`
|
||||
- **Com device**: `5511987654321:5@s.whatsapp.net` (device 5 = WhatsApp Web/Desktop)
|
||||
|
||||
### LID (Long-lived Identifier)
|
||||
- **Formato**: `aaaaa@lid` ou `aaaaa:5@lid`
|
||||
- **Significado**: Identificador de longa duração desvinculado do número de telefone
|
||||
- **Exemplo**: `a1b2c3d4e5@lid`
|
||||
- **Com device**: `a1b2c3d4e5:5@lid` (device 5 = WhatsApp Web/Desktop)
|
||||
|
||||
### Device Numbers
|
||||
- **0**: Dispositivo principal (celular)
|
||||
- **1-98**: Dispositivos companheiros (Web, Desktop, etc)
|
||||
- **99**: Dispositivo "hosted" (companion device especial)
|
||||
|
||||
---
|
||||
|
||||
## Por que existe
|
||||
|
||||
### Problema Original (Apenas PN)
|
||||
Quando o WhatsApp usava apenas números de telefone como identificadores:
|
||||
- **Troca de número**: Usuário perde todo histórico de conversas criptografadas
|
||||
- **Portabilidade**: Dificulta migração entre operadoras
|
||||
- **Privacidade**: Número de telefone exposto em todos os metadados
|
||||
- **Sessões Signal**: Precisavam ser recriadas ao trocar número
|
||||
|
||||
### Solução (LID)
|
||||
Com LID, o WhatsApp pode:
|
||||
- **Manter identidade**: Mesmo trocando de número, mantém o mesmo LID
|
||||
- **Privacidade**: Não expõe número de telefone em todas comunicações
|
||||
- **Migração suave**: Transição transparente PN → LID
|
||||
- **Sessões preservadas**: Criptografia mantida mesmo com mudança de número
|
||||
|
||||
---
|
||||
|
||||
## Como funciona
|
||||
|
||||
### Arquitetura de Armazenamento
|
||||
|
||||
```
|
||||
Database (SignalDataTypeMap['lid-mapping']):
|
||||
├─ Forward Mapping: pn:{phoneUser} → {lidUser}
|
||||
│ Exemplo: "pn:5511987654321" → "a1b2c3d4e5"
|
||||
│
|
||||
├─ Reverse Mapping: {lidUser}_reverse → {phoneUser}
|
||||
│ Exemplo: "a1b2c3d4e5_reverse" → "5511987654321"
|
||||
│
|
||||
└─ Device List: device-list:{userId} → [devices...]
|
||||
Exemplo: "device-list:5511987654321" → ["0", "1", "5", "99"]
|
||||
```
|
||||
|
||||
### LRU Cache (In-Memory)
|
||||
```javascript
|
||||
Cache Configuration:
|
||||
- TTL: 3 dias (259.200.000 ms)
|
||||
- Max Size: 50.000 entradas
|
||||
- Auto Purge: Ativado
|
||||
- Update Age on Get: Sim (renova TTL ao acessar)
|
||||
```
|
||||
|
||||
### Estrutura de Dados
|
||||
|
||||
```typescript
|
||||
type LIDMapping = {
|
||||
pn: string // "12345@s.whatsapp.net"
|
||||
lid: string // "aaaaa@lid"
|
||||
}
|
||||
|
||||
// Armazenamento
|
||||
{
|
||||
'lid-mapping': {
|
||||
'pn:12345': 'aaaaa', // Forward
|
||||
'aaaaa_reverse': '12345', // Reverse
|
||||
},
|
||||
'device-list': {
|
||||
'12345': ['0', '1', '5', '99'] // Devices conhecidos
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quando usa LID vs PN
|
||||
|
||||
### Usa PN (Phone Number)
|
||||
1. **Primeiro contato**: Quando nunca conversou com o usuário
|
||||
2. **Antes da migração**: Sistema ainda não recebeu o LID do servidor
|
||||
3. **Fallback**: Se lookup do LID falhar
|
||||
4. **Compatibilidade**: Dispositivos antigos que não suportam LID
|
||||
|
||||
### Usa LID (Long-lived ID)
|
||||
1. **Após migração bem-sucedida**: Quando `migrateSession()` completou
|
||||
2. **LID disponível**: Quando existe mapeamento em cache/database
|
||||
3. **Usuário interagiu**: Depois que o usuário enviou/recebeu mensagem
|
||||
4. **Prioridade**: LID sempre tem prioridade sobre PN quando disponível
|
||||
|
||||
### Decisão no Envio de Mensagem
|
||||
|
||||
```typescript
|
||||
// Arquivo: messages-send.ts:346-370
|
||||
|
||||
const isLidUser = requestedLidUsers.has(user) // Usuário tem LID?
|
||||
|
||||
const finalJid = isLidUser
|
||||
? jidEncode(user, item.server, item.device) // USA LID
|
||||
: jidEncode(item.user, item.server, item.device) // USA PN
|
||||
|
||||
// Exemplo:
|
||||
// isLidUser = true: "a1b2c3d4e5:5@lid"
|
||||
// isLidUser = false: "5511987654321:5@s.whatsapp.net"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fluxo de Migração
|
||||
|
||||
### 1. Login/Conexão (Trigger Inicial)
|
||||
|
||||
```
|
||||
socket.ts:1462-1487
|
||||
|
||||
1. Cliente conecta ao WhatsApp
|
||||
└─> Envia credenciais
|
||||
|
||||
2. Servidor responde com SUCCESS
|
||||
└─> <success lid="a1b2c3d4e5" /> ← SERVIDOR ENVIA O LID
|
||||
|
||||
3. Sistema armazena mapeamento próprio
|
||||
└─> await lidMapping.storeLIDPNMappings([{
|
||||
lid: "a1b2c3d4e5@lid",
|
||||
pn: "5511987654321@s.whatsapp.net"
|
||||
}])
|
||||
|
||||
4. Cria device list
|
||||
└─> device-list[5511987654321] = ["0", "1", "5", "99"]
|
||||
|
||||
5. TRIGGER MIGRAÇÃO EM MASSA
|
||||
└─> await signalRepository.migrateSession(
|
||||
"5511987654321@s.whatsapp.net", // FROM (PN)
|
||||
"a1b2c3d4e5@lid" // TO (LID)
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Migração de Sessão Signal
|
||||
|
||||
```
|
||||
libsignal.ts:484-635
|
||||
|
||||
migrateSession(fromPN, toLID):
|
||||
├─ Validação
|
||||
│ └─ Apenas suporta PN → LID (não LID → PN)
|
||||
│
|
||||
├─ Busca devices do device-list
|
||||
│ └─ ["0", "1", "5", "99"]
|
||||
│
|
||||
├─ Filtra devices com sessão Signal existente
|
||||
│ └─ [0, 5] (apenas esses têm sessão ativa)
|
||||
│
|
||||
├─ Cria operações de migração para cada device:
|
||||
│ ├─ Device 0:
|
||||
│ │ FROM: 5511987654321:0@s.whatsapp.net
|
||||
│ │ TO: a1b2c3d4e5:0@lid
|
||||
│ │
|
||||
│ └─ Device 5:
|
||||
│ FROM: 5511987654321:5@s.whatsapp.net
|
||||
│ TO: a1b2c3d4e5:5@lid
|
||||
│
|
||||
├─ Executa transação atômica:
|
||||
│ ├─ Copia sessão: session[a1b2c3d4e5:0@lid] = session[5511987654321:0@s.whatsapp.net]
|
||||
│ ├─ Deleta antiga: session[5511987654321:0@s.whatsapp.net] = null
|
||||
│ ├─ Copia sessão: session[a1b2c3d4e5:5@lid] = session[5511987654321:5@s.whatsapp.net]
|
||||
│ └─ Deleta antiga: session[5511987654321:5@s.whatsapp.net] = null
|
||||
│
|
||||
├─ Cacheia migrações completadas (TTL: 7 dias)
|
||||
│ └─ migratedSessionCache.add("5511987654321:0")
|
||||
│
|
||||
└─ Retorna: { migrated: 2, skipped: 0, total: 2 }
|
||||
```
|
||||
|
||||
### 3. Descoberta de Devices no Envio
|
||||
|
||||
```
|
||||
messages-send.ts:303-403
|
||||
|
||||
Ao enviar mensagem para usuário:
|
||||
├─ Executa USyncQuery
|
||||
│ └─> .withDeviceProtocol()
|
||||
│ └─> .withLIDProtocol()
|
||||
│
|
||||
├─ Servidor responde com:
|
||||
│ ├─ Device list: [0, 1, 5, 99]
|
||||
│ ├─ LID: "a1b2c3d4e5"
|
||||
│ └─ Identity keys de cada device
|
||||
│
|
||||
├─ Sistema armazena:
|
||||
│ ├─ device-list[user] = [devices...]
|
||||
│ └─> Permite migração em massa futura
|
||||
│
|
||||
└─ Prioriza uso de LID se disponível
|
||||
```
|
||||
|
||||
### 4. USync Protocol (Busca de LID)
|
||||
|
||||
```
|
||||
socket.ts:470-493
|
||||
|
||||
pnFromLIDUSync(jids: string[]):
|
||||
├─ Cria USyncQuery
|
||||
│ └─> .withLIDProtocol()
|
||||
│ └─> .withContext('background')
|
||||
│
|
||||
├─ Adiciona usuários
|
||||
│ └─> .withUser(new USyncUser().withId(jid))
|
||||
│
|
||||
├─ Executa query no servidor WhatsApp
|
||||
│ └─> const results = await executeUSyncQuery(usyncQuery)
|
||||
│
|
||||
└─ Retorna mapeamentos:
|
||||
[{
|
||||
pn: "5511987654321@s.whatsapp.net",
|
||||
lid: "a1b2c3d4e5@lid"
|
||||
}]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Como aparece nos logs
|
||||
|
||||
### Cenário 1: Primeiro Contato (Apenas PN)
|
||||
|
||||
```
|
||||
[INFO] Sending message to: 5511987654321@s.whatsapp.net
|
||||
[DEBUG] No LID mapping found, using PN
|
||||
[DEBUG] Querying devices for: 5511987654321@s.whatsapp.net
|
||||
[DEBUG] USyncQuery: withDeviceProtocol, withLIDProtocol
|
||||
[INFO] Devices found: [0, 1, 5]
|
||||
[INFO] LID received from server: a1b2c3d4e5
|
||||
[DEBUG] Storing LID mapping: 5511987654321 → a1b2c3d4e5
|
||||
[DEBUG] Storing device list: [0, 1, 5]
|
||||
[INFO] Message encrypted for PN: 5511987654321:0@s.whatsapp.net
|
||||
```
|
||||
|
||||
### Cenário 2: Após Interação (Usuário envia mensagem)
|
||||
|
||||
```
|
||||
[INFO] Message received from: 5511987654321@s.whatsapp.net
|
||||
[DEBUG] Checking for LID mapping...
|
||||
[DEBUG] LID found in cache: a1b2c3d4e5@lid
|
||||
[INFO] Triggering session migration...
|
||||
[DEBUG] migrateSession(5511987654321@s.whatsapp.net → a1b2c3d4e5@lid)
|
||||
[DEBUG] Found devices in device-list: [0, 1, 5]
|
||||
[DEBUG] Migrating device 0: session copied and old deleted
|
||||
[DEBUG] Migrating device 5: session copied and old deleted
|
||||
[INFO] Migration completed: { migrated: 2, skipped: 0 }
|
||||
[DEBUG] Cached migration for 7 days
|
||||
```
|
||||
|
||||
### Cenário 3: Envio Subsequente (Usa LID)
|
||||
|
||||
```
|
||||
[INFO] Sending message to: 5511987654321@s.whatsapp.net
|
||||
[DEBUG] Checking for LID mapping...
|
||||
[DEBUG] LID found in cache: a1b2c3d4e5@lid (cache hit)
|
||||
[DEBUG] Using LID for encryption
|
||||
[INFO] Message encrypted for LID: a1b2c3d4e5:0@lid
|
||||
[INFO] Message encrypted for LID: a1b2c3d4e5:5@lid
|
||||
[DEBUG] Encryption used migrated session (no re-establishment needed)
|
||||
```
|
||||
|
||||
### Cenário 4: Cache Miss (Busca no Database)
|
||||
|
||||
```
|
||||
[INFO] Sending message to: 5511988888888@s.whatsapp.net
|
||||
[DEBUG] Cache miss for PN: 5511988888888
|
||||
[DEBUG] Fetching from database: lid-mapping:pn:5511988888888
|
||||
[DEBUG] Database hit: found LID mapping → b2c3d4e5f6
|
||||
[DEBUG] Updating cache (TTL: 3 days)
|
||||
[INFO] Using LID: b2c3d4e5f6:0@lid
|
||||
```
|
||||
|
||||
### Cenário 5: USync Fallback (Não está no DB)
|
||||
|
||||
```
|
||||
[INFO] Sending message to: 5511977777777@s.whatsapp.net
|
||||
[DEBUG] Cache miss for PN: 5511977777777
|
||||
[DEBUG] Database miss: no mapping found
|
||||
[DEBUG] Triggering USync fetch...
|
||||
[DEBUG] USyncQuery with LIDProtocol for: 5511977777777
|
||||
[INFO] USync response received: LID = c3d4e5f6g7
|
||||
[DEBUG] Storing LID mapping in database
|
||||
[DEBUG] Updating cache
|
||||
[INFO] Using LID: c3d4e5f6g7:0@lid
|
||||
```
|
||||
|
||||
### Exemplo de Log Real Completo
|
||||
|
||||
```
|
||||
2026-02-09 10:15:32 [socket.ts:1462] Connection success, lid=a1b2c3d4e5
|
||||
2026-02-09 10:15:32 [lid-mapping.ts:245] storeLIDPNMappings: 1 mapping(s)
|
||||
2026-02-09 10:15:32 [lid-mapping.ts:267] Phase 1: Validated 1, cache miss 1
|
||||
2026-02-09 10:15:32 [lid-mapping.ts:305] Phase 3: Stored 1 new mapping(s)
|
||||
2026-02-09 10:15:32 [socket.ts:1475] Stored device-list for user: 5511987654321
|
||||
2026-02-09 10:15:32 [libsignal.ts:491] migrateSession: PN → LID
|
||||
2026-02-09 10:15:32 [libsignal.ts:522] Found 2 devices: [0, 5]
|
||||
2026-02-09 10:15:32 [libsignal.ts:589] Migrated device 0: a1b2c3d4e5:0@lid
|
||||
2026-02-09 10:15:32 [libsignal.ts:589] Migrated device 5: a1b2c3d4e5:5@lid
|
||||
2026-02-09 10:15:32 [libsignal.ts:627] Migration result: {migrated:2, skipped:0}
|
||||
|
||||
... usuário interage ...
|
||||
|
||||
2026-02-09 10:20:15 [messages-send.ts:346] Preparing message for 5511987654321
|
||||
2026-02-09 10:20:15 [lid-mapping.ts:158] getLIDForPN: 5511987654321
|
||||
2026-02-09 10:20:15 [lid-mapping.ts:164] Cache hit: a1b2c3d4e5
|
||||
2026-02-09 10:20:15 [messages-send.ts:370] Using LID: a1b2c3d4e5:0@lid
|
||||
2026-02-09 10:20:15 [messages-send.ts:370] Using LID: a1b2c3d4e5:5@lid
|
||||
2026-02-09 10:20:15 [libsignal.ts:180] Encrypting with session: a1b2c3d4e5:0@lid
|
||||
2026-02-09 10:20:15 [libsignal.ts:180] Encrypting with session: a1b2c3d4e5:5@lid
|
||||
2026-02-09 10:20:15 [socket.ts:892] Message sent successfully
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementação Técnica
|
||||
|
||||
### Arquivos Principais
|
||||
|
||||
```
|
||||
src/
|
||||
├── Signal/
|
||||
│ ├── lid-mapping.ts (1.166 linhas)
|
||||
│ │ └─> LIDMappingStore: Cache LRU + Database + USync
|
||||
│ │
|
||||
│ └── libsignal.ts
|
||||
│ └─> migrateSession(): Migração PN → LID
|
||||
│
|
||||
├── Socket/
|
||||
│ ├── socket.ts
|
||||
│ │ ├─> Inicialização: Armazena LID próprio
|
||||
│ │ └─> Trigger migração no login
|
||||
│ │
|
||||
│ └── messages-send.ts
|
||||
│ ├─> Device query com USyncQuery
|
||||
│ └─> Decisão LID vs PN
|
||||
│
|
||||
├── Utils/
|
||||
│ └── sync-action-utils.ts
|
||||
│ └─> LID mapping events
|
||||
│
|
||||
└── Types/
|
||||
└── Auth.ts
|
||||
└─> type LIDMapping = { pn, lid }
|
||||
```
|
||||
|
||||
### LIDMappingStore - Operações Principais
|
||||
|
||||
```typescript
|
||||
class LIDMappingStore {
|
||||
// 1. ARMAZENAR MAPEAMENTOS
|
||||
async storeLIDPNMappings(mappings: LIDMapping[]): Promise<Result> {
|
||||
// 3 fases: Validate → Fetch → Store
|
||||
// Batch size: 100 (configurável)
|
||||
// Retry: 3 tentativas com backoff exponencial
|
||||
}
|
||||
|
||||
// 2. BUSCAR LID PARA PN (com device)
|
||||
async getLIDForPN(pn: string): Promise<string | undefined> {
|
||||
// Input: "5511987654321:5@s.whatsapp.net"
|
||||
// Output: "a1b2c3d4e5:5@lid"
|
||||
// Flow: Cache → Database → USync (fallback)
|
||||
// Request coalescing: Deduplica requests concorrentes
|
||||
}
|
||||
|
||||
// 3. BUSCAR PN PARA LID (reverse)
|
||||
async getPNForLID(lid: string): Promise<string | undefined> {
|
||||
// Input: "a1b2c3d4e5:5@lid"
|
||||
// Output: "5511987654321:5@s.whatsapp.net"
|
||||
// Flow: Cache → Database → USync (fallback)
|
||||
}
|
||||
|
||||
// 4. OPERAÇÕES EM LOTE
|
||||
async getLIDsForPNs(pns: string[]): Promise<Map<string, string>> {
|
||||
// Processa múltiplos PNs em paralelo
|
||||
// Batch size configurable
|
||||
}
|
||||
|
||||
async getPNsForLIDs(lids: string[]): Promise<Map<string, string>> {
|
||||
// Processa múltiplos LIDs em paralelo
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Request Coalescing (Deduplicação)
|
||||
|
||||
```typescript
|
||||
// Problema: 10 chamadas simultâneas para o mesmo PN
|
||||
const promises = Array(10).fill(null).map(() =>
|
||||
lidMapping.getLIDForPN("5511987654321@s.whatsapp.net")
|
||||
)
|
||||
|
||||
// Sem coalescing: 10 queries ao database ❌
|
||||
// Com coalescing: 1 query compartilhada ✅
|
||||
|
||||
// Implementação:
|
||||
pendingRequests.set(key, dbPromise)
|
||||
// Requests subsequentes reutilizam a mesma Promise
|
||||
return pendingRequests.get(key)
|
||||
```
|
||||
|
||||
### Configuração de Ambiente
|
||||
|
||||
```bash
|
||||
# LID Cache TTL (Tempo de vida)
|
||||
BAILEYS_LID_CACHE_TTL_MS=259200000 # 3 dias (min: 60s, max: 30 dias)
|
||||
|
||||
# Tamanho máximo do cache
|
||||
BAILEYS_LID_MAX_CACHE_SIZE=50000 # 50k entradas (min: 100, max: 1M)
|
||||
|
||||
# Batch size para operações em lote
|
||||
BAILEYS_LID_BATCH_SIZE=100 # (min: 1, max: 1000)
|
||||
|
||||
# Retry attempts
|
||||
BAILEYS_LID_RETRY_ATTEMPTS=3 # (min: 1, max: 10)
|
||||
BAILEYS_LID_RETRY_DELAY_MS=1000 # 1s base + exponential backoff
|
||||
|
||||
# Features
|
||||
BAILEYS_LID_CACHE_AUTO_PURGE=true # Auto cleanup de cache
|
||||
BAILEYS_LID_UPDATE_AGE_ON_GET=true # Renova TTL ao acessar
|
||||
BAILEYS_LID_METRICS=false # Estatísticas detalhadas
|
||||
BAILEYS_LID_DEBUG=false # Logs de debug
|
||||
```
|
||||
|
||||
### Estatísticas (Metrics)
|
||||
|
||||
```typescript
|
||||
getStats() {
|
||||
return {
|
||||
cacheSize: 15234, // Entradas no cache
|
||||
cacheHits: 45678, // Acertos no cache
|
||||
cacheMisses: 1234, // Falhas de cache
|
||||
cacheHitRate: 0.973, // 97.3% hit rate
|
||||
databaseHits: 892, // Buscas no database
|
||||
databaseMisses: 342, // Não encontrado no DB
|
||||
usyncFetches: 342, // Fallback para USync
|
||||
usyncFailures: 5, // Falhas no USync
|
||||
mappingsStored: 16789, // Total armazenado
|
||||
invalidMappings: 23, // Rejeitados (inválidos)
|
||||
operationsInProgress: 3 // Operações ativas (UAF protection)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Device Migration Cache
|
||||
|
||||
```typescript
|
||||
// Cache de migrações completadas (evita re-processar)
|
||||
migratedSessionCache = new Map<string, number>()
|
||||
// Key: "5511987654321:0" (PN + device)
|
||||
// Value: timestamp da migração
|
||||
|
||||
// TTL: 7 dias
|
||||
const MIGRATION_CACHE_TTL = 7 * 24 * 60 * 60 * 1000
|
||||
|
||||
// Cleanup automático de entradas expiradas
|
||||
setInterval(() => {
|
||||
const now = Date.now()
|
||||
for (const [key, timestamp] of migratedSessionCache.entries()) {
|
||||
if (now - timestamp > MIGRATION_CACHE_TTL) {
|
||||
migratedSessionCache.delete(key)
|
||||
}
|
||||
}
|
||||
}, 60 * 60 * 1000) // Executa a cada 1 hora
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resumo Executivo
|
||||
|
||||
### O Fluxo Completo em Etapas
|
||||
|
||||
```
|
||||
1. LOGIN
|
||||
└─> Servidor envia LID do usuário próprio
|
||||
└─> Sistema armazena: PN ↔ LID (self mapping)
|
||||
└─> Trigger migração de sessões Signal: PN → LID
|
||||
|
||||
2. PRIMEIRO ENVIO PARA CONTATO
|
||||
└─> Usa PN (não tem LID ainda)
|
||||
└─> USyncQuery descobre devices + LID
|
||||
└─> Armazena LID mapping + device list
|
||||
|
||||
3. USUÁRIO INTERAGE (envia mensagem)
|
||||
└─> Sistema detecta PN no from
|
||||
└─> Busca LID (cache/database/usync)
|
||||
└─> Trigger migração de sessão para esse contato
|
||||
|
||||
4. ENVIOS SUBSEQUENTES
|
||||
└─> Usa LID (já migrado)
|
||||
└─> Cache hit (rápido)
|
||||
└─> Encriptação usa sessão LID
|
||||
```
|
||||
|
||||
### Por que você vê PN e LID nos logs?
|
||||
|
||||
```
|
||||
Momento 1: Primeira interação
|
||||
├─ Sistema ainda usa PN
|
||||
├─ Descobre LID via USync
|
||||
└─ Logs mostram: "using PN" → "LID discovered"
|
||||
|
||||
Momento 2: Migração acontece
|
||||
├─> Sessões Signal copiadas PN → LID
|
||||
└─> Logs mostram: "migrated 2 devices"
|
||||
|
||||
Momento 3: Próximas mensagens
|
||||
├─> Sistema usa LID
|
||||
└─> Logs mostram: "using LID" (cache hit)
|
||||
```
|
||||
|
||||
### Benefícios da Implementação
|
||||
|
||||
✅ **Performance**
|
||||
- Cache LRU com 97%+ hit rate
|
||||
- Request coalescing evita duplicação
|
||||
- Batch operations reduzem roundtrips
|
||||
|
||||
✅ **Confiabilidade**
|
||||
- Retry com exponential backoff
|
||||
- Graceful degradation (fallback para PN)
|
||||
- Transaction support para atomicidade
|
||||
|
||||
✅ **Segurança**
|
||||
- Sessões Signal preservadas
|
||||
- Identity keys validados
|
||||
- Detecção de device reinstall
|
||||
|
||||
✅ **Escalabilidade**
|
||||
- Cache configurável (até 1M entradas)
|
||||
- Batch size ajustável
|
||||
- Auto purge de dados antigos
|
||||
|
||||
✅ **Observabilidade**
|
||||
- Métricas detalhadas
|
||||
- Debug logging configurável
|
||||
- Statistics para monitoramento
|
||||
|
||||
---
|
||||
|
||||
## Conclusão
|
||||
|
||||
O sistema de **Device Migration com LID/PN** é uma implementação **production-grade** que permite ao WhatsApp migrar usuários de identificadores baseados em número de telefone (PN) para identificadores de longa duração (LID) de forma **transparente**, **eficiente** e **segura**.
|
||||
|
||||
A transição acontece automaticamente quando:
|
||||
1. ✅ Usuário loga (recebe LID do servidor)
|
||||
2. ✅ Envia mensagem (descobre LID do destinatário)
|
||||
3. ✅ Recebe mensagem (descobre LID do remetente)
|
||||
|
||||
Após migração, **todas as comunicações usam LID**, preservando:
|
||||
- 🔐 Sessões criptográficas Signal
|
||||
- 🆔 Identidade mesmo com troca de número
|
||||
- 🚀 Performance com cache inteligente
|
||||
- 🔄 Compatibilidade com sistemas antigos (fallback PN)
|
||||
|
||||
**Nos logs, você verá**:
|
||||
- **PN** → Primeira interação ou fallback
|
||||
- **LID** → Após descoberta e migração (estado ideal)
|
||||
@@ -1,341 +0,0 @@
|
||||
# Implementação: Merge Automático LID/PN
|
||||
|
||||
## 📋 Resumo
|
||||
|
||||
Esta implementação resolve o problema de duplicação de conversas causado por identificadores LID (Long-lived ID) e PN (Phone Number) do WhatsApp, implementando merge automático na API (Opção A+).
|
||||
|
||||
## 🎯 Objetivos Alcançados
|
||||
|
||||
✅ **Consolidação de eventos**: `lid-mapping.update` agora é bufferável
|
||||
✅ **Merge automático**: API detecta e notifica mudanças LID→PN
|
||||
✅ **Backward compatible**: ZPRO antigo continua funcionando
|
||||
✅ **Zero duplicação**: Consumidores recebem notificação para unificar chats
|
||||
|
||||
## 📝 Mudanças Implementadas
|
||||
|
||||
### 1. **Event Buffer** (`src/Utils/event-buffer.ts`)
|
||||
|
||||
#### Adicionado `lid-mapping.update` ao array de eventos bufferáveis:
|
||||
```typescript
|
||||
const BUFFERABLE_EVENT = [
|
||||
// ... eventos existentes
|
||||
'lid-mapping.update' // ← NOVO
|
||||
] as const
|
||||
```
|
||||
|
||||
**Benefício**: Eventos LID mapping agora são consolidados em batches junto com mensagens, reduzindo 50% de eventos separados.
|
||||
|
||||
#### Adicionado lógica de append para `lid-mapping.update`:
|
||||
```typescript
|
||||
case 'lid-mapping.update':
|
||||
const lidMappings = eventData as BaileysEventMap['lid-mapping.update']
|
||||
for (const mapping of lidMappings) {
|
||||
const key = `${mapping.lid}-${mapping.pn}`
|
||||
if (!data.lidMappings[key]) {
|
||||
data.lidMappings[key] = mapping
|
||||
}
|
||||
}
|
||||
break
|
||||
```
|
||||
|
||||
#### Adicionado consolidação em `consolidateEvents()`:
|
||||
```typescript
|
||||
const lidMappingList = Object.values(data.lidMappings)
|
||||
if (lidMappingList.length) {
|
||||
map['lid-mapping.update'] = lidMappingList
|
||||
}
|
||||
```
|
||||
|
||||
#### Adicionado inicialização em `makeBufferData()`:
|
||||
```typescript
|
||||
return {
|
||||
// ... campos existentes
|
||||
lidMappings: {} // ← NOVO
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. **Tipos** (`src/Types/Events.ts`)
|
||||
|
||||
#### Adicionado `lidMappings` ao `BufferedEventData`:
|
||||
```typescript
|
||||
export type BufferedEventData = {
|
||||
// ... campos existentes
|
||||
lidMappings: { [key: string]: LIDMapping } // ← NOVO
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. **Tipos de Chat** (`src/Types/Chat.ts`)
|
||||
|
||||
#### Estendido `ChatUpdate` com campos de merge:
|
||||
```typescript
|
||||
export type ChatUpdate = Partial<
|
||||
Chat & {
|
||||
// ... campos existentes
|
||||
/** indicates if this chat was merged from LID to PN */
|
||||
merged?: boolean
|
||||
/** previous chat ID before merge (LID format) */
|
||||
previousId?: string
|
||||
/** timestamp when the merge occurred */
|
||||
mergedAt?: number
|
||||
}
|
||||
>
|
||||
```
|
||||
|
||||
**Nota**: Campos são **opcionais** para manter backward compatibility.
|
||||
|
||||
---
|
||||
|
||||
### 4. **Socket de Chats** (`src/Socket/chats.ts`)
|
||||
|
||||
#### Implementado merge automático no handler `lid-mapping.update`:
|
||||
|
||||
```typescript
|
||||
ev.on('lid-mapping.update', async (mappings) => {
|
||||
try {
|
||||
// 1. Armazenar mapeamentos (lógica existente)
|
||||
const result = await signalRepository.lidMapping.storeLIDPNMappings(mappings)
|
||||
|
||||
// 2. NOVO: Notificar consumidores sobre merge
|
||||
for (const mapping of mappings) {
|
||||
const lidUser = jidNormalizedUser(mapping.lid)
|
||||
const pnUser = jidNormalizedUser(mapping.pn)
|
||||
|
||||
if (lidUser && pnUser && lidUser !== pnUser) {
|
||||
// Emite evento de chat update com metadados de merge
|
||||
ev.emit('chats.update', [{
|
||||
id: pnUser,
|
||||
merged: true,
|
||||
previousId: lidUser,
|
||||
mergedAt: Date.now()
|
||||
}])
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn({ count: mappings.length, error }, 'Failed to store LID-PN mappings')
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Comportamento**:
|
||||
1. API detecta novo mapeamento LID→PN
|
||||
2. Armazena mapeamento internamente
|
||||
3. Emite evento `chats.update` com campos `merged`, `previousId`, `mergedAt`
|
||||
4. ZPRO recebe notificação e pode unificar chats localmente
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Fluxo de Funcionamento
|
||||
|
||||
### Antes (Problema):
|
||||
```
|
||||
1. Mensagem chega com LID
|
||||
└─> ZPRO cria chat: "123@lid"
|
||||
|
||||
2. Cliente interage, descobre PN
|
||||
└─> API emite lid-mapping.update (evento SEPARADO)
|
||||
└─> ZPRO recebe mensagem com PN
|
||||
└─> ZPRO cria OUTRO chat: "5511999@s.whatsapp.net"
|
||||
|
||||
RESULTADO: 2 CONVERSAS DUPLICADAS ❌
|
||||
```
|
||||
|
||||
### Depois (Solução):
|
||||
```
|
||||
1. Mensagem chega com LID
|
||||
└─> ZPRO cria chat: "123@lid"
|
||||
|
||||
2. Cliente interage, descobre PN
|
||||
└─> API detecta LID→PN mapping
|
||||
└─> API emite em BATCH consolidado:
|
||||
├─ messages.upsert (mensagens)
|
||||
├─ lid-mapping.update (mapeamento)
|
||||
└─ chats.update ({ merged: true, previousId: "123@lid" })
|
||||
|
||||
3. ZPRO recebe batch
|
||||
└─> Detecta campo "merged: true"
|
||||
└─> Renomeia chat de "123@lid" para "5511999@s.whatsapp.net"
|
||||
└─> Unifica mensagens
|
||||
|
||||
RESULTADO: 1 CONVERSA UNIFICADA ✅
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 Como ZPRO Deve Implementar
|
||||
|
||||
### Código Exemplo:
|
||||
|
||||
```typescript
|
||||
// ZPRO precisa apenas escutar chats.update
|
||||
sock.ev.on('chats.update', async (chats) => {
|
||||
for (const chat of chats) {
|
||||
// Detectar merge
|
||||
if (chat.merged && chat.previousId) {
|
||||
console.log(`Chat merged: ${chat.previousId} → ${chat.id}`)
|
||||
|
||||
// 1. Renomear chat no banco de dados
|
||||
await database.chats.update(
|
||||
{ id: chat.previousId },
|
||||
{ id: chat.id }
|
||||
)
|
||||
|
||||
// 2. Atualizar mensagens (opcional, se armazenadas separadamente)
|
||||
await database.messages.updateMany(
|
||||
{ chatId: chat.previousId },
|
||||
{ chatId: chat.id }
|
||||
)
|
||||
|
||||
// 3. Atualizar UI
|
||||
updateChatInUI(chat.previousId, chat.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Backward Compatibility**:
|
||||
```typescript
|
||||
// ZPRO ANTIGO (sem suporte a merge)
|
||||
sock.ev.on('chats.update', async (chats) => {
|
||||
for (const chat of chats) {
|
||||
updateChat(chat.id, chat.unreadCount)
|
||||
// Ignora campos "merged", "previousId" automaticamente ✅
|
||||
}
|
||||
})
|
||||
// Funciona sem quebrar! Apenas não aproveita o merge.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Performance
|
||||
|
||||
### Impacto de Performance (120 instâncias, 1200 msgs/dia):
|
||||
|
||||
| Métrica | Antes | Depois | Melhoria |
|
||||
|---------|-------|--------|----------|
|
||||
| **Eventos emitidos** | 2 separados | 1 batch | 50% ↓ |
|
||||
| **Latência buffer** | N/A | +30-100ms | Imperceptível |
|
||||
| **Processamento ZPRO** | 2 rodadas | 1 rodada | 50% ↓ |
|
||||
| **Memória API** | N/A | +7MB/instância | Desprezível |
|
||||
| **CPU** | N/A | <1% | Desprezível |
|
||||
|
||||
**Conclusão**: Zero impacto negativo, melhorias significativas em consolidação.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist de Testes
|
||||
|
||||
### Testes na API (Baileys):
|
||||
- [x] `lid-mapping.update` é bufferável
|
||||
- [x] Eventos consolidam em batch
|
||||
- [x] `chats.update` contém campos `merged`, `previousId`, `mergedAt`
|
||||
- [ ] Teste unitário de consolidação de buffer
|
||||
- [ ] Teste de integração com 10-120 instâncias
|
||||
- [ ] Benchmark de performance
|
||||
|
||||
### Testes no ZPRO:
|
||||
- [ ] ZPRO recebe campo `merged: true`
|
||||
- [ ] ZPRO renomeia chat corretamente
|
||||
- [ ] ZPRO unifica mensagens
|
||||
- [ ] Backward compatibility (ZPRO antigo não quebra)
|
||||
- [ ] UI atualiza após merge
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Deployment
|
||||
|
||||
### Fase 1: Deploy da API ✅
|
||||
```bash
|
||||
# Implementação completa
|
||||
git checkout claude/lid-pn-auto-merge-implementation-c96aba
|
||||
git push origin claude/lid-pn-auto-merge-implementation-c96aba
|
||||
|
||||
# Após review e merge
|
||||
# Deploy em produção
|
||||
```
|
||||
|
||||
### Fase 2: Atualização do ZPRO (Opcional)
|
||||
```bash
|
||||
# ZPRO pode atualizar quando quiser para aproveitar merge
|
||||
# Não há pressa - backward compatible!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Referências
|
||||
|
||||
- [DEVICE_MIGRATION_LID_PN.md](./DEVICE_MIGRATION_LID_PN.md) - Documentação completa sobre LID/PN
|
||||
- [Análise de Performance](./DEVICE_MIGRATION_LID_PN.md#-teste-de-escala-10-a-120-instâncias) - Detalhes de performance
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Notas Importantes
|
||||
|
||||
### 1. **Por que não fazer merge de mensagens na API?**
|
||||
|
||||
**Resposta**: Merge de mensagens seria muito complexo e custoso:
|
||||
- API não tem acesso direto ao banco de mensagens do ZPRO
|
||||
- Cada consumidor pode ter estrutura de dados diferente
|
||||
- Melhor deixar consumidor decidir como mergear
|
||||
|
||||
### 2. **O que acontece se ZPRO não implementar o merge?**
|
||||
|
||||
**Resposta**: Continua funcionando, apenas com duplicação:
|
||||
- ZPRO recebe eventos normalmente
|
||||
- Campos `merged`, `previousId` são ignorados
|
||||
- Sistema não quebra (backward compatible)
|
||||
- Quando ZPRO implementar, merge será automático
|
||||
|
||||
### 3. **Quantos eventos ZPRO recebe?**
|
||||
|
||||
**Antes**:
|
||||
```
|
||||
1. messages.upsert (buffered)
|
||||
2. lid-mapping.update (IMMEDIATE - não bufferável)
|
||||
Total: 2 eventos em momentos diferentes
|
||||
```
|
||||
|
||||
**Depois**:
|
||||
```
|
||||
1. Batch consolidado:
|
||||
- messages.upsert
|
||||
- lid-mapping.update
|
||||
- chats.update (com merged: true)
|
||||
Total: 1 evento batch
|
||||
```
|
||||
|
||||
### 4. **Campos sem underscore**
|
||||
|
||||
Conforme solicitado, **TODOS os campos não usam `_` prefix**:
|
||||
- ✅ `merged` (não `_merged`)
|
||||
- ✅ `previousId` (não `_previousId`)
|
||||
- ✅ `mergedAt` (não `_mergedAt`)
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### Problema: ZPRO continua duplicando chats
|
||||
|
||||
**Solução**: ZPRO precisa implementar handler de `merged: true`
|
||||
|
||||
### Problema: Performance degradou
|
||||
|
||||
**Solução**: Verificar configuração de buffer:
|
||||
```bash
|
||||
BAILEYS_BUFFER_TIMEOUT_MS=30000 # 30s padrão
|
||||
BAILEYS_BUFFER_MAX_SIZE=5000 # Limite de eventos
|
||||
```
|
||||
|
||||
### Problema: Eventos não consolidam
|
||||
|
||||
**Solução**: Verificar se `lid-mapping.update` está em BUFFERABLE_EVENT
|
||||
|
||||
---
|
||||
|
||||
**Implementado por**: Claude (Opção A+)
|
||||
**Data**: 2026-02-09
|
||||
**Branch**: `claude/lid-pn-auto-merge-implementation-c96aba`
|
||||
Reference in New Issue
Block a user