fix(whatsapp): complete history synchronization
This commit is contained in:
@@ -9,10 +9,12 @@ import path from 'node:path'
|
||||
import https from 'node:https'
|
||||
import { Boom } from '@hapi/boom'
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent'
|
||||
import { ProxyAgent as UndiciProxyAgent } from 'undici'
|
||||
import makeWASocket, {
|
||||
Browsers,
|
||||
BufferJSON,
|
||||
DisconnectReason,
|
||||
downloadAndProcessHistorySyncNotification,
|
||||
downloadMediaMessage,
|
||||
fetchLatestBaileysVersion,
|
||||
getContentType,
|
||||
@@ -308,6 +310,61 @@ function upsertMessage(accountId, raw) {
|
||||
return message
|
||||
}
|
||||
|
||||
async function replayStoredHistoryNotifications(
|
||||
accountId, account, store, logger, processedIds = new Set(), downloadOptions = {},
|
||||
) {
|
||||
const replayed = new Set(account.replayedHistoryNotificationIds || [])
|
||||
const candidates = []
|
||||
for (const messages of store.messages.values()) {
|
||||
for (const item of messages || []) {
|
||||
const notification = item.raw?.message?.protocolMessage?.historySyncNotification
|
||||
if (!notification || !item.id || replayed.has(item.id) || processedIds.has(item.id)) continue
|
||||
candidates.push({ id: item.id, notification })
|
||||
}
|
||||
}
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const data = await downloadAndProcessHistorySyncNotification(
|
||||
candidate.notification, downloadOptions, logger,
|
||||
)
|
||||
for (const chat of data.chats || []) {
|
||||
const id = String(chat.id || '')
|
||||
if (id) store.chats.set(id, { ...(store.chats.get(id) || {}), ...jsonSafe(chat), id })
|
||||
}
|
||||
for (const contact of data.contacts || []) {
|
||||
const id = String(contact.id || '')
|
||||
if (id) store.contacts.set(id, { ...(store.contacts.get(id) || {}), ...jsonSafe(contact), id })
|
||||
}
|
||||
for (const message of data.messages || []) upsertMessage(accountId, message)
|
||||
replayed.add(candidate.id)
|
||||
account.replayedHistoryNotificationIds = [...replayed].slice(-100)
|
||||
account.historySync = {
|
||||
status: 'processing',
|
||||
syncType: data.syncType ?? candidate.notification.syncType ?? null,
|
||||
progress: data.progress ?? candidate.notification.progress ?? null,
|
||||
chats: data.chats?.length || 0,
|
||||
contacts: data.contacts?.length || 0,
|
||||
messages: data.messages?.length || 0,
|
||||
recovered: true,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
await saveAccounts()
|
||||
scheduleStoreSave(accountId)
|
||||
void deliverWebhook(account, 'history.synced', {
|
||||
chats: data.chats?.length || 0,
|
||||
contacts: data.contacts?.length || 0,
|
||||
messages: data.messages?.length || 0,
|
||||
progress: data.progress ?? candidate.notification.progress ?? null,
|
||||
syncType: data.syncType ?? candidate.notification.syncType ?? null,
|
||||
recovered: true,
|
||||
isLatest: true,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.warn({ err: error, messageId: candidate.id }, 'stored history notification replay failed')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function storedMessage(accountId, jid, messageId) {
|
||||
const store = stores.get(accountId)
|
||||
return (store?.messages.get(jid) || []).find((item) => item.id === messageId)
|
||||
@@ -672,10 +729,13 @@ async function connectAccount(accountId) {
|
||||
const authPath = path.join(authDir, accountId)
|
||||
await fs.mkdir(authPath, { recursive: true, mode: 0o700 })
|
||||
const { state, saveCreds } = await useMultiFileAuthState(authPath)
|
||||
const { version } = await fetchLatestBaileysVersion()
|
||||
const logger = log.child({ accountId })
|
||||
const proxyUrl = accountProxyUrl(account)
|
||||
const proxyAgent = proxyUrl ? new HttpsProxyAgent(proxyUrl) : undefined
|
||||
const fetchDispatcher = proxyUrl ? new UndiciProxyAgent(proxyUrl) : undefined
|
||||
const { version } = await fetchLatestBaileysVersion(
|
||||
fetchDispatcher ? { dispatcher: fetchDispatcher } : {},
|
||||
)
|
||||
const sock = makeWASocket({
|
||||
version,
|
||||
logger,
|
||||
@@ -685,7 +745,11 @@ async function connectAccount(accountId) {
|
||||
},
|
||||
browser: Browsers.ubuntu('Lingxi Crossborder'),
|
||||
...(proxyAgent ? { agent: proxyAgent, fetchAgent: proxyAgent } : {}),
|
||||
...(fetchDispatcher ? { options: { dispatcher: fetchDispatcher } } : {}),
|
||||
syncFullHistory: true,
|
||||
// Baileys 7 excludes FULL history notifications by default even when
|
||||
// syncFullHistory is enabled. Accept every server-provided history phase.
|
||||
shouldSyncHistoryMessage: () => true,
|
||||
markOnlineOnConnect: false,
|
||||
generateHighQualityLinkPreview: true,
|
||||
getMessage: async (key) => {
|
||||
@@ -694,7 +758,7 @@ async function connectAccount(accountId) {
|
||||
},
|
||||
cachedGroupMetadata: async (jid) => store.groups.get(jid),
|
||||
})
|
||||
sessions.set(accountId, { sock, connecting: true })
|
||||
sessions.set(accountId, { sock, connecting: true, fetchDispatcher })
|
||||
account.status = state.creds.registered ? 'connecting' : 'waiting_pairing'
|
||||
account.updatedAt = new Date().toISOString()
|
||||
await saveAccounts()
|
||||
@@ -722,6 +786,18 @@ async function connectAccount(accountId) {
|
||||
const session = sessions.get(accountId)
|
||||
if (session) session.connecting = false
|
||||
await saveAccounts()
|
||||
const processedIds = new Set(
|
||||
(state.creds.processedHistoryMessages || []).map((item) => String(item?.key?.id || '')),
|
||||
)
|
||||
void replayStoredHistoryNotifications(
|
||||
accountId,
|
||||
account,
|
||||
store,
|
||||
logger,
|
||||
processedIds,
|
||||
fetchDispatcher ? { dispatcher: fetchDispatcher } : {},
|
||||
)
|
||||
.catch((error) => logger.warn({ err: error }, 'stored history recovery failed'))
|
||||
for (const job of outboundJobs.values()) {
|
||||
if (job.accountId === accountId && ['queued', 'retrying', 'sending'].includes(job.status)) {
|
||||
scheduleOutboundJob(job.id)
|
||||
@@ -733,7 +809,9 @@ async function connectAccount(accountId) {
|
||||
if (sessions.get(accountId)?.sock !== sock) return
|
||||
const statusCode = new Boom(update.lastDisconnect?.error).output?.statusCode
|
||||
const loggedOut = statusCode === DisconnectReason.loggedOut
|
||||
const session = sessions.get(accountId)
|
||||
sessions.delete(accountId)
|
||||
if (session?.fetchDispatcher) void session.fetchDispatcher.close().catch(() => undefined)
|
||||
account.status = loggedOut ? 'logged_out' : 'disconnected'
|
||||
account.connectionState = loggedOut ? 'logged_out' : 'reconnecting'
|
||||
account.lastError = update.lastDisconnect?.error?.message || `连接关闭 (${statusCode || 'unknown'})`
|
||||
@@ -753,7 +831,9 @@ async function connectAccount(accountId) {
|
||||
}
|
||||
}
|
||||
})
|
||||
sock.ev.on('messaging-history.set', ({ chats = [], contacts = [], messages = [] }) => {
|
||||
sock.ev.on('messaging-history.set', ({
|
||||
chats = [], contacts = [], messages = [], isLatest, progress, syncType, chunkOrder,
|
||||
}) => {
|
||||
for (const chat of chats) {
|
||||
const id = String(chat.id || '')
|
||||
if (id) store.chats.set(id, { ...(store.chats.get(id) || {}), ...jsonSafe(chat), id })
|
||||
@@ -763,13 +843,40 @@ async function connectAccount(accountId) {
|
||||
if (id) store.contacts.set(id, { ...(store.contacts.get(id) || {}), ...jsonSafe(contact), id })
|
||||
}
|
||||
for (const item of messages) upsertMessage(accountId, item)
|
||||
account.historySync = {
|
||||
status: isLatest || progress === 100 ? 'processing' : 'syncing',
|
||||
syncType: syncType ?? null,
|
||||
progress: progress ?? null,
|
||||
chunkOrder: chunkOrder ?? null,
|
||||
chats: chats.length,
|
||||
contacts: contacts.length,
|
||||
messages: messages.length,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
void saveAccounts()
|
||||
scheduleStoreSave(accountId)
|
||||
void deliverWebhook(account, 'history.synced', {
|
||||
chats: chats.length,
|
||||
contacts: contacts.length,
|
||||
messages: messages.length,
|
||||
isLatest: Boolean(isLatest),
|
||||
progress: progress ?? null,
|
||||
syncType: syncType ?? null,
|
||||
chunkOrder: chunkOrder ?? null,
|
||||
})
|
||||
})
|
||||
sock.ev.on('messaging-history.status', ({ syncType, status, explicit }) => {
|
||||
account.historySync = {
|
||||
...(account.historySync || {}),
|
||||
status,
|
||||
syncType,
|
||||
explicit: Boolean(explicit),
|
||||
updatedAt: new Date().toISOString(),
|
||||
...(status === 'complete' ? { completedAt: new Date().toISOString() } : {}),
|
||||
}
|
||||
void saveAccounts()
|
||||
void deliverWebhook(account, 'history.status', account.historySync)
|
||||
})
|
||||
sock.ev.on('chats.upsert', (items) => {
|
||||
for (const chat of items || []) {
|
||||
const id = String(chat.id || '')
|
||||
|
||||
Reference in New Issue
Block a user