@zervonex/baileys 0.0.0-stage → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +3203 -2
- package/WAProto/WAProtoCompile.js +291 -0
- package/WAProto/fetcher.js +69 -0
- package/WAProto/index.js +88 -0
- package/WAProto/parser.js +313 -0
- package/engine-requirements.js +10 -0
- package/lib/Builders/AIRich.js +590 -0
- package/lib/Builders/base.js +130 -0
- package/lib/Builders/builders.js +448 -0
- package/lib/Builders/index.js +3 -0
- package/lib/Defaults/index.js +225 -0
- package/lib/Defaults/phonenumber-mcc.json +223 -0
- package/lib/Signal/index.js +2 -0
- package/lib/Signal/libsignal.js +523 -0
- package/lib/Signal/lid-mapping.js +283 -0
- package/lib/Socket/Client/index.js +2 -0
- package/lib/Socket/Client/types.js +10 -0
- package/lib/Socket/Client/websocket.js +84 -0
- package/lib/Socket/business.js +152 -0
- package/lib/Socket/chats.js +855 -0
- package/lib/Socket/communities.js +147 -0
- package/lib/Socket/groups.js +294 -0
- package/lib/Socket/index.js +15 -0
- package/lib/Socket/messages-recv.js +904 -0
- package/lib/Socket/messages-send.js +693 -0
- package/lib/Socket/mex.js +34 -0
- package/lib/Socket/newsletter.js +752 -0
- package/lib/Socket/nexus-handler.js +55 -0
- package/lib/Socket/registration.js +155 -0
- package/lib/Socket/socket.js +798 -0
- package/lib/Socket/spam-report.js +65 -0
- package/lib/Socket/username.js +82 -0
- package/lib/Store/index.js +6 -0
- package/lib/Store/make-cache-manager-store.js +81 -0
- package/lib/Store/make-in-memory-store.js +425 -0
- package/lib/Store/make-ordered-dictionary.js +82 -0
- package/lib/Store/object-repository.js +31 -0
- package/lib/Store/reporting-info-store.js +38 -0
- package/lib/Types/Auth.js +2 -0
- package/lib/Types/Bussines.js +2 -0
- package/lib/Types/Call.js +2 -0
- package/lib/Types/Chat.js +8 -0
- package/lib/Types/Contact.js +2 -0
- package/lib/Types/Events.js +2 -0
- package/lib/Types/GroupMetadata.js +2 -0
- package/lib/Types/Label.js +25 -0
- package/lib/Types/LabelAssociation.js +7 -0
- package/lib/Types/Message.js +15 -0
- package/lib/Types/Newsletter.js +100 -0
- package/lib/Types/Product.js +2 -0
- package/lib/Types/Signal.js +2 -0
- package/lib/Types/Socket.js +14 -0
- package/lib/Types/State.js +26 -0
- package/lib/Types/USync.js +2 -0
- package/lib/Types/index.js +30 -0
- package/lib/Utils/auth-utils.js +142 -0
- package/lib/Utils/baileys-event-stream.js +33 -0
- package/lib/Utils/bot-capabilities.js +40 -0
- package/lib/Utils/bot-planning-replay.js +44 -0
- package/lib/Utils/browser-utils.js +26 -0
- package/lib/Utils/business.js +75 -0
- package/lib/Utils/chat-utils.js +316 -0
- package/lib/Utils/companion-reg-client-utils.js +20 -0
- package/lib/Utils/consumer-application.js +38 -0
- package/lib/Utils/crypto.js +52 -0
- package/lib/Utils/decode-wa-message.js +197 -0
- package/lib/Utils/event-buffer.js +273 -0
- package/lib/Utils/extras.js +186 -0
- package/lib/Utils/generics.js +195 -0
- package/lib/Utils/history.js +51 -0
- package/lib/Utils/identity-change-handler.js +19 -0
- package/lib/Utils/index.js +39 -0
- package/lib/Utils/key-store.js +3 -0
- package/lib/Utils/link-preview.js +51 -0
- package/lib/Utils/logger.js +2 -0
- package/lib/Utils/lt-hash.js +3 -0
- package/lib/Utils/make-mutex.js +16 -0
- package/lib/Utils/message-composer.js +59 -0
- package/lib/Utils/message-retry-manager.js +48 -0
- package/lib/Utils/messages-media.js +520 -0
- package/lib/Utils/messages.js +742 -0
- package/lib/Utils/meta-ai-msmsg.js +122 -0
- package/lib/Utils/meta-compositing.js +75 -0
- package/lib/Utils/modern-messages.js +235 -0
- package/lib/Utils/native-flow.js +55 -0
- package/lib/Utils/noise-handler.js +161 -0
- package/lib/Utils/pre-key-manager.js +76 -0
- package/lib/Utils/process-message.js +399 -0
- package/lib/Utils/reporting-utils.js +171 -0
- package/lib/Utils/signal.js +167 -0
- package/lib/Utils/spam-report-utils.js +142 -0
- package/lib/Utils/sync-action-utils.js +33 -0
- package/lib/Utils/tc-token-utils.js +102 -0
- package/lib/Utils/use-multi-file-auth-state.js +307 -0
- package/lib/Utils/validate-connection.js +181 -0
- package/lib/WABinary/constants.js +1321 -0
- package/lib/WABinary/decode.js +152 -0
- package/lib/WABinary/encode.js +140 -0
- package/lib/WABinary/generic-utils.js +179 -0
- package/lib/WABinary/index.js +6 -0
- package/lib/WABinary/jid-utils.js +235 -0
- package/lib/WABinary/types.js +4 -0
- package/lib/WAM/BinaryInfo.js +10 -0
- package/lib/WAM/constants.js +22853 -0
- package/lib/WAM/encode.js +150 -0
- package/lib/WAM/index.js +3 -0
- package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +30 -0
- package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +48 -0
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +23 -0
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +28 -0
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +15 -0
- package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +24 -0
- package/lib/WAUSync/Protocols/USyncLIDProtocol.js +9 -0
- package/lib/WAUSync/Protocols/USyncPictureProtocol.js +21 -0
- package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +19 -0
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +19 -0
- package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +22 -0
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +16 -0
- package/lib/WAUSync/Protocols/index.js +12 -0
- package/lib/WAUSync/USyncQuery.js +100 -0
- package/lib/WAUSync/USyncUser.js +20 -0
- package/lib/WAUSync/index.js +3 -0
- package/lib/index.js +68 -0
- package/package.json +216 -5
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { trimUndefined } from './generics.js'
|
|
2
|
+
import { createCipheriv, createDecipheriv, randomBytes, randomUUID } from 'crypto'
|
|
3
|
+
import { proto } from '../../WAProto/index.js'
|
|
4
|
+
|
|
5
|
+
export const SCHEDULED_MSG_META_TYPE = 'scheduled_message'
|
|
6
|
+
export const SCHEDULED_MSG_MAX_PER_CHAT = 30
|
|
7
|
+
export const SCHEDULED_MSG_MAX_MEDIA = 1
|
|
8
|
+
export const SCHEDULED_MSG_RESOURCE_LIMIT_NACK_CODE = 419
|
|
9
|
+
export const SCHEDULED_MSG_REVEAL_KEY_BYTES = 32
|
|
10
|
+
export const SCHEDULED_MSG_REVEAL_KEY_IV_BYTES = 12
|
|
11
|
+
export const SCHEDULED_MSG_REVEAL_KEY_RETENTION_DAYS = 30
|
|
12
|
+
export const SCHEDULED_MSG_TAG_BYTES = 16
|
|
13
|
+
export const SCHEDULED_MSG_WINDOW = Object.freeze({ chat: { minSeconds: 600, maxSeconds: 1209600 }, newsletter: { minSeconds: 600, maxSeconds: 2592000 } })
|
|
14
|
+
export const generateRevealKey = () => randomBytes(SCHEDULED_MSG_REVEAL_KEY_BYTES)
|
|
15
|
+
export const generateRevealKeyId = () => randomUUID()
|
|
16
|
+
export const encryptWithRevealKey = (plaintext, revealKey) => {
|
|
17
|
+
const key = Buffer.from(revealKey)
|
|
18
|
+
if (key.length !== SCHEDULED_MSG_REVEAL_KEY_BYTES) throw new TypeError(`reveal key must be ${SCHEDULED_MSG_REVEAL_KEY_BYTES} bytes`)
|
|
19
|
+
const encIv = randomBytes(SCHEDULED_MSG_REVEAL_KEY_IV_BYTES)
|
|
20
|
+
const cipher = createCipheriv('aes-256-gcm', key, encIv)
|
|
21
|
+
const body = Buffer.concat([cipher.update(Buffer.from(plaintext)), cipher.final()])
|
|
22
|
+
return { encIv, encPayload: Buffer.concat([body, cipher.getAuthTag()]) }
|
|
23
|
+
}
|
|
24
|
+
export const decryptWithRevealKey = (encPayload, encIv, revealKey) => {
|
|
25
|
+
const key = Buffer.from(revealKey)
|
|
26
|
+
const payload = Buffer.from(encPayload)
|
|
27
|
+
if (payload.length < SCHEDULED_MSG_TAG_BYTES) throw new TypeError('encrypted payload is too short to carry an auth tag')
|
|
28
|
+
const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(encIv))
|
|
29
|
+
decipher.setAuthTag(payload.subarray(payload.length - SCHEDULED_MSG_TAG_BYTES))
|
|
30
|
+
return Buffer.concat([decipher.update(payload.subarray(0, payload.length - SCHEDULED_MSG_TAG_BYTES)), decipher.final()])
|
|
31
|
+
}
|
|
32
|
+
export const buildConditionalRevealMessage = ({ encPayload, encIv, revealKeyId }) => ({ conditionalRevealMessage: { conditionalRevealMessageType: proto.Message.ConditionalRevealMessage.ConditionalRevealMessageType.SCHEDULED_MESSAGE, encPayload: Buffer.from(encPayload), encIv: Buffer.from(encIv), revealKeyId } })
|
|
33
|
+
export const encodeScheduledMessage = (message, revealKey = generateRevealKey()) => {
|
|
34
|
+
const plaintext = proto.Message.encode(proto.Message.fromObject(message)).finish()
|
|
35
|
+
const { encIv, encPayload } = encryptWithRevealKey(plaintext, revealKey)
|
|
36
|
+
const revealKeyId = generateRevealKeyId()
|
|
37
|
+
return { revealKey, revealKeyId, encIv, encPayload, message: buildConditionalRevealMessage({ encPayload, encIv, revealKeyId }) }
|
|
38
|
+
}
|
|
39
|
+
export const decodeScheduledMessage = (conditionalRevealMessage, revealKey) => {
|
|
40
|
+
const inner = conditionalRevealMessage?.conditionalRevealMessage ?? conditionalRevealMessage
|
|
41
|
+
if (!inner?.encPayload || !inner?.encIv) throw new TypeError('conditionalRevealMessage is missing encPayload or encIv')
|
|
42
|
+
return proto.Message.decode(decryptWithRevealKey(inner.encPayload, inner.encIv, revealKey))
|
|
43
|
+
}
|
|
44
|
+
export const buildScheduledMsgMetaNode = ({ kind = 'schedule', scheduledTimestampS, revealKeyId, revealKey }) => {
|
|
45
|
+
if (!revealKeyId) throw new TypeError('revealKeyId is required')
|
|
46
|
+
if (kind === 'schedule' && !revealKey) throw new TypeError('scheduling requires the reveal key')
|
|
47
|
+
const attrs = { type: SCHEDULED_MSG_META_TYPE }
|
|
48
|
+
if (kind === 'schedule') {
|
|
49
|
+
if (!Number.isFinite(Number(scheduledTimestampS))) throw new TypeError('scheduledTimestampS must be a unix timestamp in seconds')
|
|
50
|
+
attrs.st = String(Math.floor(Number(scheduledTimestampS)))
|
|
51
|
+
}
|
|
52
|
+
return { tag: 'meta', attrs, content: [{ tag: 'key', attrs: { rkid: revealKeyId }, content: kind === 'schedule' ? Buffer.from(revealKey) : undefined }] }
|
|
53
|
+
}
|
|
54
|
+
export const buildUnscheduleProtocolMessage = (key) => ({ protocolMessage: { key, type: proto.Message.ProtocolMessage.Type.MESSAGE_UNSCHEDULE } })
|
|
55
|
+
export const isScheduledTimeValid = (scheduledTimestampS, nowSeconds = Math.floor(Date.now() / 1000), window = SCHEDULED_MSG_WINDOW.chat) => {
|
|
56
|
+
const rounded = Math.floor(nowSeconds / 60) * 60
|
|
57
|
+
return scheduledTimestampS - rounded >= window.minSeconds && scheduledTimestampS - nowSeconds <= window.maxSeconds
|
|
58
|
+
}
|
|
59
|
+
export const SplitPaymentStatus = Object.freeze({ PENDING: 0, PAID: 1 })
|
|
60
|
+
export const ReminderFrequency = Object.freeze({ REMINDER_FREQUENCY_UNKNOWN: 0, WEEKLY: 1, BI_WEEKLY: 2, MONTHLY: 3, QUARTERLY: 4 })
|
|
61
|
+
export const ReminderStatus = Object.freeze({ REMINDER_STATUS_UNKNOWN: 0, ACTIVE: 1, CANCELLED_BY_CREATOR: 2, STOPPED_BY_RECEIVER: 3, EXPIRED: 4, PAID: 5 })
|
|
62
|
+
export const MONEY_OFFSET = 1000
|
|
63
|
+
const drop = (object) => { for (const key of Object.keys(object)) { if (object[key] === undefined || object[key] === null) delete object[key] } return object }
|
|
64
|
+
export const money = (amount, currencyCode, offset = MONEY_OFFSET) => {
|
|
65
|
+
const value = Number(amount)
|
|
66
|
+
if (!Number.isFinite(value) || value < 0) throw new TypeError('money needs a non-negative amount')
|
|
67
|
+
if (typeof currencyCode !== 'string' || currencyCode.length !== 3) throw new TypeError('money needs a three letter currency code, like IDR or USD')
|
|
68
|
+
if (!Number.isInteger(offset) || offset <= 0) throw new TypeError('money offset must be a positive integer')
|
|
69
|
+
return { value: Math.round(value * offset), offset, currencyCode: currencyCode.toUpperCase() }
|
|
70
|
+
}
|
|
71
|
+
export const readMoney = (value) => { if (!value?.offset) return undefined; return { amount: Number(value.value) / Number(value.offset), currencyCode: value.currencyCode } }
|
|
72
|
+
export const buildSplitPayment = ({ splitId, total, currency, description, requesterJid, participants = [], createdAt } = {}) => {
|
|
73
|
+
if (!splitId) throw new TypeError('buildSplitPayment needs a splitId')
|
|
74
|
+
if (!participants.length) throw new TypeError('buildSplitPayment needs at least one participant')
|
|
75
|
+
return drop({ splitId: String(splitId), totalAmount: total === undefined ? undefined : money(total, currency), description, requesterJid, participants: participants.map((p, i) => { if (!p?.jid) throw new TypeError(`participant ${i} needs a jid`); return drop({ jid: p.jid, amount: p.amount === undefined ? undefined : money(p.amount, p.currency ?? currency), status: p.status ?? SplitPaymentStatus.PENDING }) }), createdAtMs: createdAt === undefined ? Date.now() : Number(createdAt) })
|
|
76
|
+
}
|
|
77
|
+
export const buildSplitPaymentUpdate = ({ splitId, participantJid } = {}) => { if (!splitId || !participantJid) throw new TypeError('buildSplitPaymentUpdate needs both splitId and participantJid'); return { splitId: String(splitId), participantJid } }
|
|
78
|
+
export const buildPaymentReminder = ({ reminderId, instanceId, description, frequency = ReminderFrequency.MONTHLY, status = ReminderStatus.ACTIVE, amount, currency, payeeVpa, payeeJid, payerJid } = {}) => {
|
|
79
|
+
if (!reminderId) throw new TypeError('buildPaymentReminder needs a reminderId')
|
|
80
|
+
return drop({ reminderId: String(reminderId), instanceId: instanceId === undefined ? undefined : String(instanceId), description, frequency, status, amount: amount === undefined ? undefined : money(amount, currency), payeeVpa, payeeJid, payerJid })
|
|
81
|
+
}
|
|
82
|
+
export const readSplitPayment = (msg) => {
|
|
83
|
+
const split = (msg?.message ?? msg)?.splitPaymentMessage
|
|
84
|
+
if (!split) return null
|
|
85
|
+
return drop({ splitId: split.splitId, total: readMoney(split.totalAmount), description: split.description, requesterJid: split.requesterJid, createdAtMs: split.createdAtMs === undefined ? undefined : Number(split.createdAtMs), participants: (split.participants ?? []).map(p => drop({ jid: p.jid, amount: readMoney(p.amount), status: p.status })) })
|
|
86
|
+
}
|
|
87
|
+
export const readPaymentReminder = (msg) => {
|
|
88
|
+
const reminder = (msg?.message ?? msg)?.paymentReminderMessage
|
|
89
|
+
if (!reminder) return null
|
|
90
|
+
return drop({ reminderId: reminder.reminderId, instanceId: reminder.instanceId, description: reminder.description, frequency: reminder.frequency, status: reminder.status, amount: readMoney(reminder.amount), payeeVpa: reminder.payeeVpa, payeeJid: reminder.payeeJid, payerJid: reminder.payerJid })
|
|
91
|
+
}
|
|
92
|
+
export const StatusLinkType = Object.freeze({ RASTERIZED_LINK_PREVIEW: 1, RASTERIZED_LINK_TRUNCATED: 2, RASTERIZED_LINK_FULL_URL: 3 })
|
|
93
|
+
export const STICKER_DEFAULT_AREA = Object.freeze({ x: 0.25, y: 0.4, width: 0.5, height: 0.2 })
|
|
94
|
+
const fraction = (value, name) => { const n = Number(value); if (!Number.isFinite(n) || n < 0 || n > 1) throw new TypeError(`${name} must be between 0 and 1`); return n }
|
|
95
|
+
export const stickerArea = ({ x, y, width, height } = STICKER_DEFAULT_AREA) => {
|
|
96
|
+
const left = fraction(x, 'x'), top = fraction(y, 'y'), right = fraction(left + Number(width), 'x + width'), bottom = fraction(top + Number(height), 'y + height')
|
|
97
|
+
if (right <= left || bottom <= top) throw new TypeError('a sticker area needs a positive width and height')
|
|
98
|
+
return [{ x: left, y: top }, { x: right, y: top }, { x: right, y: bottom }, { x: left, y: bottom }]
|
|
99
|
+
}
|
|
100
|
+
const annotation = (area, fields) => ({ polygonVertices: Array.isArray(area) ? area : stickerArea(area), ...fields })
|
|
101
|
+
export const locationSticker = ({ latitude, longitude, name, area, skipConfirmation } = {}) => {
|
|
102
|
+
const degreesLatitude = Number(latitude), degreesLongitude = Number(longitude)
|
|
103
|
+
if (!Number.isFinite(degreesLatitude) || !Number.isFinite(degreesLongitude)) throw new TypeError('locationSticker needs a numeric latitude and longitude')
|
|
104
|
+
return annotation(area, { location: { degreesLatitude, degreesLongitude, ...(name ? { name } : {}) }, ...(skipConfirmation === undefined ? {} : { shouldSkipConfirmation: !!skipConfirmation }) })
|
|
105
|
+
}
|
|
106
|
+
export const channelSticker = ({ jid, name, serverMessageId = 0, accessibilityText, area } = {}) => {
|
|
107
|
+
if (typeof jid !== 'string' || !jid.endsWith('@newsletter')) throw new TypeError('channelSticker needs the channel jid ending in @newsletter')
|
|
108
|
+
return annotation(area, { newsletter: { newsletterJid: jid, serverMessageId: Number(serverMessageId), ...(name ? { newsletterName: name } : {}), ...(accessibilityText ? { accessibilityText } : {}) } })
|
|
109
|
+
}
|
|
110
|
+
export const linkSticker = ({ url, title, area, linkType = StatusLinkType.RASTERIZED_LINK_PREVIEW } = {}) => {
|
|
111
|
+
if (typeof url !== 'string' || !url) throw new TypeError('linkSticker needs a url')
|
|
112
|
+
return annotation(area, { tapAction: { tapUrl: url, ...(title ? { title } : {}) }, statusLinkType: linkType })
|
|
113
|
+
}
|
|
114
|
+
export const musicSticker = ({ songId, title, author, mediaId, artworkDirectPath, artworkSha256, artworkEncSha256, artworkMediaKey, artistAttribution, countryBlocklist, isExplicit, startTimeMs = 0, derivedStartTimeMs, durationMs, area } = {}) => {
|
|
115
|
+
if (!songId && !mediaId) throw new TypeError('musicSticker needs at least a songId or a musicContentMediaId')
|
|
116
|
+
const bytes = v => v === undefined ? undefined : Buffer.isBuffer(v) ? v : Buffer.from(v, 'base64')
|
|
117
|
+
const em = { ...(songId ? { songId: String(songId) } : {}), ...(mediaId ? { musicContentMediaId: String(mediaId) } : {}), ...(title ? { title } : {}), ...(author ? { author } : {}), ...(artistAttribution ? { artistAttribution } : {}), ...(artworkDirectPath ? { artworkDirectPath } : {}), ...(isExplicit === undefined ? {} : { isExplicit: !!isExplicit }), musicSongStartTimeInMs: Number(startTimeMs), ...(derivedStartTimeMs === undefined ? {} : { derivedContentStartTimeInMs: Number(derivedStartTimeMs) }), ...(durationMs === undefined ? {} : { overlapDurationInMs: Number(durationMs) }) }
|
|
118
|
+
for (const [k, v] of [['artworkSha256', artworkSha256], ['artworkEncSha256', artworkEncSha256], ['artworkMediaKey', artworkMediaKey], ['countryBlocklist', countryBlocklist]]) { const b = bytes(v); if (b) em[k] = b }
|
|
119
|
+
return annotation(area, { embeddedContent: { embeddedMusic: em } })
|
|
120
|
+
}
|
|
121
|
+
export const messageSticker = ({ stanzaId, message, area } = {}) => {
|
|
122
|
+
if (!message) throw new TypeError('messageSticker needs the message it embeds')
|
|
123
|
+
return annotation(area, { embeddedContent: { embeddedMessage: { ...(stanzaId ? { stanzaId } : {}), message } } })
|
|
124
|
+
}
|
|
125
|
+
const STICKER_ACTIONS = ['location', 'newsletter', 'embeddedAction', 'tapAction']
|
|
126
|
+
export const normalizeStickers = (stickers) => {
|
|
127
|
+
const list = Array.isArray(stickers) ? stickers : [stickers]
|
|
128
|
+
return list.filter(Boolean).map((sticker, i) => {
|
|
129
|
+
const v = sticker.polygonVertices
|
|
130
|
+
if (!Array.isArray(v) || v.length !== 4) throw new TypeError(`sticker ${i} needs four polygonVertices`)
|
|
131
|
+
const actions = STICKER_ACTIONS.filter(n => sticker[n] !== undefined && sticker[n] !== null)
|
|
132
|
+
if (actions.length > 1) throw new TypeError(`sticker ${i} sets ${actions.join(' and ')}, but only one action is allowed per sticker`)
|
|
133
|
+
return proto.InteractiveAnnotation.fromObject(sticker)
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
export const readStickers = (message) => {
|
|
137
|
+
const media = message?.imageMessage ?? message?.videoMessage ?? message
|
|
138
|
+
const annotations = media?.interactiveAnnotations
|
|
139
|
+
if (!Array.isArray(annotations) || !annotations.length) return []
|
|
140
|
+
return annotations.map(item => {
|
|
141
|
+
const [topLeft, , bottomRight] = item.polygonVertices ?? []
|
|
142
|
+
const music = item.embeddedContent?.embeddedMusic
|
|
143
|
+
return { kind: item.location ? 'location' : item.newsletter ? 'channel' : item.tapAction ? 'link' : music ? 'music' : item.embeddedContent?.embeddedMessage ? 'message' : 'unknown', area: topLeft && bottomRight ? { x: topLeft.x, y: topLeft.y, width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y } : undefined, location: item.location ?? undefined, channel: item.newsletter ?? undefined, link: item.tapAction ? { url: item.tapAction.tapUrl, title: item.tapAction.title, linkType: item.statusLinkType } : undefined, music: music ? { songId: music.songId, title: music.title, author: music.author } : undefined, message: item.embeddedContent?.embeddedMessage ?? undefined, annotation: item }
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
export const MusicMessageStyle = Object.freeze({ UNKNOWN: 0, VINYL: 1 })
|
|
147
|
+
export const MUSIC_ALLOWED_HOSTS = Object.freeze(['.whatsapp.net', '.whatsapp.com', '.fbcdn.net', '.facebook.com', '.instagram.com', '.cdninstagram.com'])
|
|
148
|
+
export const isMusicHostAllowed = (value) => { let host; try { host = new URL(String(value)).hostname.toLowerCase() } catch { return false } return MUSIC_ALLOWED_HOSTS.some(s => host.endsWith(s) || host === s.slice(1)) }
|
|
149
|
+
const assertMusicHost = (value, label) => { if (value === undefined) return undefined; if (!isMusicHostAllowed(value)) throw new TypeError(`${label} must be hosted on ${MUSIC_ALLOWED_HOSTS.join(', ')}`); return value }
|
|
150
|
+
const embeddedMusicOf = (options) => musicSticker(options).embeddedContent.embeddedMusic
|
|
151
|
+
export const buildMusicMessage = ({ songUri, artworkUri, style = MusicMessageStyle.VINYL, contextInfo, ...music } = {}) => trimUndefined({ embeddedMusic: embeddedMusicOf(music), songUri: assertMusicHost(songUri, 'songUri'), artworkUri: assertMusicHost(artworkUri, 'artworkUri'), style, contextInfo })
|
|
152
|
+
export const readMusicMessage = (msg) => {
|
|
153
|
+
const music = (msg?.message ?? msg)?.musicMessage
|
|
154
|
+
if (!music) return null
|
|
155
|
+
const em = music.embeddedMusic ?? {}
|
|
156
|
+
return { songId: em.songId, mediaId: em.musicContentMediaId, title: em.title, author: em.author, artistAttribution: em.artistAttribution, isExplicit: em.isExplicit, startTimeMs: em.musicSongStartTimeInMs, durationMs: em.overlapDurationInMs, songUri: music.songUri, artworkUri: music.artworkUri, style: music.style, embeddedMusic: em }
|
|
157
|
+
}
|
|
158
|
+
export function buildAckStanza(node, errorCode, meId) {
|
|
159
|
+
const { tag, attrs } = node
|
|
160
|
+
const stanza = { tag: 'ack', attrs: { id: attrs.id, to: attrs.from, class: tag } }
|
|
161
|
+
if (errorCode) stanza.attrs.error = errorCode.toString()
|
|
162
|
+
if (attrs.participant) stanza.attrs.participant = attrs.participant
|
|
163
|
+
if (attrs.recipient) stanza.attrs.recipient = attrs.recipient
|
|
164
|
+
if (attrs.type) stanza.attrs.type = attrs.type
|
|
165
|
+
if (tag === 'message' && meId) stanza.attrs.from = meId
|
|
166
|
+
return stanza
|
|
167
|
+
}
|
|
168
|
+
export const pickSenderKeyRecipients = (devices, senderKeyMap, { force = false, skip } = {}) => {
|
|
169
|
+
const recipients = []
|
|
170
|
+
for (const device of devices) {
|
|
171
|
+
const deviceJid = device?.jid
|
|
172
|
+
if (!deviceJid || (!force && senderKeyMap[deviceJid]) || (skip && skip(device))) continue
|
|
173
|
+
recipients.push(deviceJid)
|
|
174
|
+
}
|
|
175
|
+
return recipients
|
|
176
|
+
}
|
|
177
|
+
export const deliveredSenderKeyJids = nodes => { const delivered = new Set(); for (const node of nodes || []) { const jid = node?.attrs?.jid; if (jid) delivered.add(jid) } return delivered }
|
|
178
|
+
export const senderKeyResetSummary = (stored, jid) => { const devices = Object.keys(stored?.[jid] || {}); return { jid, cleared: devices.length, devices } }
|
|
179
|
+
export const commitSenderKeyDelivery = (senderKeyMap, recipients, nodes) => {
|
|
180
|
+
const delivered = deliveredSenderKeyJids(nodes), marked = [], skipped = []
|
|
181
|
+
for (const jid of recipients) { if (delivered.has(jid)) { senderKeyMap[jid] = true; marked.push(jid) } else { delete senderKeyMap[jid]; skipped.push(jid) } }
|
|
182
|
+
return { marked, skipped }
|
|
183
|
+
}
|
|
184
|
+
export const GROUP_ADDRESSING_MODES = Object.freeze(['pn', 'lid'])
|
|
185
|
+
export const isUsableGroupMetadata = metadata => { if (!metadata || typeof metadata !== 'object') return false; if (!Array.isArray(metadata.participants)) return false; return GROUP_ADDRESSING_MODES.includes(metadata.addressingMode) }
|
|
186
|
+
export const describeUnusableGroupMetadata = metadata => { if (!metadata || typeof metadata !== 'object') return 'nothing cached'; if (!Array.isArray(metadata.participants)) return 'no participants'; if (metadata.addressingMode === undefined || metadata.addressingMode === null) return 'no addressingMode'; return `addressingMode ${JSON.stringify(metadata.addressingMode)} is not pn or lid` }
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { Boom } from '@hapi/boom'
|
|
2
|
+
import { createHash, randomBytes } from 'crypto'
|
|
3
|
+
import { createRequire } from 'module'
|
|
4
|
+
import { proto, fetchProtoBundle } from '../../WAProto/index.js'
|
|
5
|
+
import { DisconnectReason } from '../Types/index.js'
|
|
6
|
+
import { getAllBinaryNodeChildren, jidDecode } from '../WABinary/index.js'
|
|
7
|
+
import { sha256 } from './crypto.js'
|
|
8
|
+
|
|
9
|
+
export const BufferJSON = {
|
|
10
|
+
replacer: (k, value) => {
|
|
11
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array || value?.type === 'Buffer') return { type: 'Buffer', data: Buffer.from(value?.data || value).toString('base64') }
|
|
12
|
+
return value
|
|
13
|
+
},
|
|
14
|
+
reviver: (_, value) => {
|
|
15
|
+
if (typeof value === 'object' && value !== null && value.type === 'Buffer' && typeof value.data === 'string') return Buffer.from(value.data, 'base64')
|
|
16
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
17
|
+
const keys = Object.keys(value)
|
|
18
|
+
if (keys.length > 0 && keys.every(k => !isNaN(parseInt(k, 10)))) {
|
|
19
|
+
const values = Object.values(value)
|
|
20
|
+
if (values.every(v => typeof v === 'number')) return Buffer.from(values)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return value
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const getKeyAuthor = (key, meId = 'me') => (key?.fromMe ? meId : key?.participantAlt || key?.remoteJidAlt || key?.participant || key?.remoteJid) || ''
|
|
28
|
+
export const isStringNullOrEmpty = value => value == null || value === ''
|
|
29
|
+
|
|
30
|
+
export const writeRandomPadMax16 = msg => { const pad = randomBytes(1); const padLength = (pad[0] & 0x0f) + 1; return Buffer.concat([msg, Buffer.alloc(padLength, padLength)]) }
|
|
31
|
+
export const unpadRandomMax16 = e => { const t = new Uint8Array(e); if (!t.length) throw new Error('unpadPkcs7 given empty bytes'); const r = t[t.length - 1]; if (r > t.length) throw new Error(`unpad given ${t.length} bytes, but pad is ${r}`); return new Uint8Array(t.buffer, t.byteOffset, t.length - r) }
|
|
32
|
+
|
|
33
|
+
export const generateParticipantHashV2 = participants => { participants.sort(); return '2:' + sha256(Buffer.from(participants.join(''))).toString('base64').slice(0, 6) }
|
|
34
|
+
|
|
35
|
+
export const encodeWAMessage = message => {
|
|
36
|
+
try { return writeRandomPadMax16(proto.Message.encode(message?.$type === proto.Message ? message : proto.Message.fromObject(message)).finish()) }
|
|
37
|
+
catch (error) {
|
|
38
|
+
try { return writeRandomPadMax16(proto.Message.encode(message).finish()) }
|
|
39
|
+
catch (e) { console.warn('Message encoding failed:', error.message, e.message); throw new Error(`Failed to encode message: ${error.message}`) }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const generateRegistrationId = () => Uint16Array.from(randomBytes(2))[0] & 16383
|
|
44
|
+
|
|
45
|
+
export const encodeBigEndian = (e, t = 4) => { let r = e; const a = new Uint8Array(t); for (let i = t - 1; i >= 0; i--) { a[i] = 255 & r; r >>>= 8 }; return a }
|
|
46
|
+
|
|
47
|
+
export const toNumber = t => typeof t === 'object' && t ? ('toNumber' in t ? t.toNumber() : t.low) : t || 0
|
|
48
|
+
export const unixTimestampSeconds = (date = new Date()) => Math.floor(date.getTime() / 1000)
|
|
49
|
+
|
|
50
|
+
export const debouncedTimeout = (intervalMs = 1000, task) => {
|
|
51
|
+
let timeout
|
|
52
|
+
return {
|
|
53
|
+
start: (newIntervalMs, newTask) => { task = newTask || task; intervalMs = newIntervalMs || intervalMs; if (timeout) clearTimeout(timeout); timeout = setTimeout(() => task?.(), intervalMs) },
|
|
54
|
+
cancel: () => { if (timeout) clearTimeout(timeout); timeout = undefined },
|
|
55
|
+
setTask: newTask => (task = newTask),
|
|
56
|
+
setInterval: newInterval => (intervalMs = newInterval)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const delay = ms => delayCancellable(ms).delay
|
|
61
|
+
export const delayCancellable = ms => {
|
|
62
|
+
const stack = new Error().stack
|
|
63
|
+
let timeout, reject
|
|
64
|
+
const delay = new Promise((resolve, _reject) => { timeout = setTimeout(resolve, ms); reject = _reject })
|
|
65
|
+
return { delay, cancel: () => { clearTimeout(timeout); reject(new Boom('Cancelled', { statusCode: 500, data: { stack } })) } }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const promiseTimeout = async (ms, promise) => {
|
|
69
|
+
if (!ms) return new Promise(promise)
|
|
70
|
+
const stack = new Error().stack
|
|
71
|
+
const { delay, cancel } = delayCancellable(ms)
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
delay.then(() => reject(new Boom('Timed Out', { statusCode: DisconnectReason.timedOut, data: { stack } }))).catch(reject)
|
|
74
|
+
promise(resolve, reject)
|
|
75
|
+
}).finally(cancel)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const generateMessageIDV2 = userId => {
|
|
79
|
+
const data = Buffer.alloc(8 + 20 + 16)
|
|
80
|
+
data.writeBigUInt64BE(BigInt(Math.floor(Date.now() / 1000)))
|
|
81
|
+
if (userId) { const id = jidDecode(userId); if (id?.user) { data.write(id.user, 8); data.write('@c.us', 8 + id.user.length) } }
|
|
82
|
+
randomBytes(16).copy(data, 28)
|
|
83
|
+
return 'ZERVONEX' + createHash('sha256').update(data).digest().toString('hex').toUpperCase().substring(0, 18)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const generateMessageID = () => 'ZERVONEX' + randomBytes(18).toString('hex').toUpperCase()
|
|
87
|
+
|
|
88
|
+
export const bindWaitForEvent = (ev, event) => async (check, timeoutMs) => {
|
|
89
|
+
let listener, closeListener
|
|
90
|
+
await promiseTimeout(timeoutMs, (resolve, reject) => {
|
|
91
|
+
closeListener = ({ connection, lastDisconnect }) => { if (connection === 'close') reject(lastDisconnect?.error || new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })) }
|
|
92
|
+
ev.on('connection.update', closeListener)
|
|
93
|
+
listener = async update => { if (await check(update)) resolve() }
|
|
94
|
+
ev.on(event, listener)
|
|
95
|
+
}).finally(() => { ev.off(event, listener); ev.off('connection.update', closeListener) })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const bindWaitForConnectionUpdate = ev => bindWaitForEvent(ev, 'connection.update')
|
|
99
|
+
|
|
100
|
+
export const fetchLatestWaWebVersion = async () => {
|
|
101
|
+
const { version } = await fetchProtoBundle()
|
|
102
|
+
return { version: [2, 3000, parseInt(version)], isLatest: true }
|
|
103
|
+
}
|
|
104
|
+
export const fetchLatestBaileysVersion = fetchLatestWaWebVersion
|
|
105
|
+
|
|
106
|
+
export const generateMdTagPrefix = () => { const b = randomBytes(4); return `${b.readUInt16BE()}.${b.readUInt16BE(2)}-` }
|
|
107
|
+
|
|
108
|
+
const STATUS_MAP = { sender: proto.WebMessageInfo.Status.SERVER_ACK, played: proto.WebMessageInfo.Status.PLAYED, read: proto.WebMessageInfo.Status.READ, 'read-self': proto.WebMessageInfo.Status.READ }
|
|
109
|
+
export const getStatusFromReceiptType = type => typeof type === 'undefined' ? proto.WebMessageInfo.Status.DELIVERY_ACK : STATUS_MAP[type]
|
|
110
|
+
|
|
111
|
+
const CODE_MAP = { conflict: DisconnectReason.connectionReplaced }
|
|
112
|
+
export const getErrorCodeFromStreamError = node => { const [reasonNode] = getAllBinaryNodeChildren(node); let reason = reasonNode?.tag || 'unknown'; const statusCode = +(node.attrs.code || CODE_MAP[reason] || DisconnectReason.badSession); if (statusCode === DisconnectReason.restartRequired) reason = 'restart required'; return { reason, statusCode } }
|
|
113
|
+
|
|
114
|
+
export const getCallStatusFromNode = ({ tag, attrs }) => {
|
|
115
|
+
switch (tag) {
|
|
116
|
+
case 'offer': case 'offer_notice': return 'offer'
|
|
117
|
+
case 'terminate': return attrs.reason === 'timeout' ? 'timeout' : 'terminate'
|
|
118
|
+
case 'reject': return 'reject'
|
|
119
|
+
case 'accept': return 'accept'
|
|
120
|
+
default: return 'ringing'
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const UNEXPECTED_SERVER_CODE_TEXT = 'Unexpected server response: '
|
|
125
|
+
export const getCodeFromWSError = error => {
|
|
126
|
+
if (error?.message?.includes(UNEXPECTED_SERVER_CODE_TEXT)) { const code = +error.message.slice(UNEXPECTED_SERVER_CODE_TEXT.length); if (!Number.isNaN(code) && code >= 400) return code }
|
|
127
|
+
if (error?.code?.startsWith('E') || error?.message?.includes('timed out')) return 408
|
|
128
|
+
return 500
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const isWABusinessPlatform = platform => platform === 'smbi' || platform === 'smba'
|
|
132
|
+
export const trimUndefined = obj => { for (const key in obj) if (typeof obj[key] === 'undefined') delete obj[key]; return obj }
|
|
133
|
+
|
|
134
|
+
const CROCKFORD_CHARACTERS = '123456789ABCDEFGHJKLMNPQRSTVWXYZ'
|
|
135
|
+
export const bytesToCrockford = buffer => {
|
|
136
|
+
let value = 0, bitCount = 0
|
|
137
|
+
const crockford = []
|
|
138
|
+
for (const element of buffer) { value = (value << 8) | (element & 0xff); bitCount += 8; while (bitCount >= 5) { crockford.push(CROCKFORD_CHARACTERS.charAt((value >>> (bitCount - 5)) & 31)); bitCount -= 5 } }
|
|
139
|
+
if (bitCount > 0) crockford.push(CROCKFORD_CHARACTERS.charAt((value << (5 - bitCount)) & 31))
|
|
140
|
+
return crockford.join('')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const encodeNewsletterMessage = message => proto.Message.encode(message).finish()
|
|
144
|
+
|
|
145
|
+
const _require = createRequire(import.meta.url)
|
|
146
|
+
const Prism = _require('prismjs')
|
|
147
|
+
const loadPrismLanguages = _require('prismjs/components/index')
|
|
148
|
+
let _prismReady = false
|
|
149
|
+
const ensurePrism = () => { if (_prismReady) return; _prismReady = true; try { loadPrismLanguages() } catch { } }
|
|
150
|
+
|
|
151
|
+
const PRISM_TYPE_MAP = {
|
|
152
|
+
keyword: 1, 'keyword control': 1, builtin: 1, important: 1, bold: 1,
|
|
153
|
+
function: 2, 'function-definition': 2, method: 2, 'method-definition': 2,
|
|
154
|
+
string: 3, 'template-string': 3, 'string interpolation': 3, char: 3, regex: 3,
|
|
155
|
+
number: 4, constant: 4, boolean: 4, unit: 4,
|
|
156
|
+
comment: 5, 'block-comment': 5, prolog: 5, doctype: 5
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const flattenPrism = (tokens, out = []) => {
|
|
160
|
+
for (const t of tokens) {
|
|
161
|
+
if (typeof t === 'string') { if (t) out.push({ codeContent: t, highlightType: 0 }); continue }
|
|
162
|
+
const type = PRISM_TYPE_MAP[t.type] ?? 0
|
|
163
|
+
const content = Array.isArray(t.content) ? flattenPrism(t.content, []).map(x => x.codeContent).join('') : String(t.content)
|
|
164
|
+
if (content) out.push({ codeContent: content, highlightType: type })
|
|
165
|
+
}
|
|
166
|
+
return out
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const TYPE_NAMES = ['DEFAULT', 'KEYWORD', 'METHOD', 'STR', 'NUMBER', 'COMMENT']
|
|
170
|
+
export const tokenizeCode = (code, language = 'javascript') => {
|
|
171
|
+
ensurePrism()
|
|
172
|
+
const grammar = Prism.languages[language.toLowerCase()] ?? Prism.languages.javascript
|
|
173
|
+
const raw = flattenPrism(Prism.tokenize(code, grammar))
|
|
174
|
+
const merged = []
|
|
175
|
+
for (const t of raw) { const prev = merged[merged.length - 1]; if (prev && prev.highlightType === t.highlightType) prev.codeContent += t.codeContent; else merged.push({ ...t }) }
|
|
176
|
+
return { codeBlocks: merged, unified_codeBlock: merged.map(t => ({ content: t.codeContent, type: TYPE_NAMES[t.highlightType] ?? 'DEFAULT' })) }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const waitAllPromises = async value => {
|
|
180
|
+
if (value instanceof Promise) return waitAllPromises(await value)
|
|
181
|
+
if (Array.isArray(value)) return Promise.all(value.map(waitAllPromises))
|
|
182
|
+
if (value && typeof value === 'object' && value.constructor === Object) { const keys = Object.keys(value); const vals = await Promise.all(keys.map(k => waitAllPromises(value[k]))); return Object.fromEntries(keys.map((k, i) => [k, vals[i]])) }
|
|
183
|
+
return value
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export const extractIE = (text, extraEntities = []) => {
|
|
187
|
+
const entities = [...extraEntities]
|
|
188
|
+
let out = text
|
|
189
|
+
out = out.replace(/\[\[([^:]+):([^\]]+)\]\]/g, (_, key, url) => { entities.push({ __typename: 'GenAITextInlineEntity', key, metadata: { __typename: 'GenAILatexItem', latex_expression: '.', font_height: 83.333, padding: 15, latex_image: { __typename: 'GenAIMediaItem', mime_type: 'image/png', url, url_fallback: url, width: 400, height: 400, expiration_timestamp_ms: Date.now() + 86400000 } } }); return `{{${key}}}.{{/${key}}}` })
|
|
190
|
+
out = out.replace(/\[([^\]]+)\]\(>([^)]+)\)/g, (_, display, deeplink) => { const key = `dl_${entities.length}`; entities.push({ __typename: 'GenAITextInlineEntity', key, metadata: { __typename: 'GenAIDeeplinkInlineEntityMetadata', deeplink, display_text: display } }); return `{{${key}}}${display}{{/${key}}}` })
|
|
191
|
+
out = out.replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, (_, display, url) => { const key = `lnk_${entities.length}`; entities.push({ __typename: 'GenAITextInlineEntity', key, metadata: { __typename: 'GenAIHyperlinkInlineEntityMetadata', url, display_text: display } }); return `{{${key}}}${display}{{/${key}}}` })
|
|
192
|
+
return { text: out, inline_entities: entities }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export const generateVerificationMetadata = () => ({ proofs: [{ version: 1, useCase: 1, signature: randomBytes(32), certificateChain: [randomBytes(64)], certificateChainSki: [] }] })
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { promisify } from 'util'
|
|
2
|
+
import { inflate } from 'zlib'
|
|
3
|
+
import { proto } from '../../WAProto/index.js'
|
|
4
|
+
import { WAMessageStubType } from '../Types/index.js'
|
|
5
|
+
import { isJidUser } from '../WABinary/index.js'
|
|
6
|
+
import { toNumber } from './generics.js'
|
|
7
|
+
import { normalizeMessageContent } from './messages.js'
|
|
8
|
+
import { downloadContentFromMessage } from './messages-media.js'
|
|
9
|
+
|
|
10
|
+
const inflatePromise = promisify(inflate)
|
|
11
|
+
|
|
12
|
+
export const downloadHistory = async (msg, options) => {
|
|
13
|
+
const stream = await downloadContentFromMessage(msg, 'md-msg-hist', { options })
|
|
14
|
+
const chunks = []
|
|
15
|
+
for await (const chunk of stream) chunks.push(chunk)
|
|
16
|
+
return proto.HistorySync.decode(await inflatePromise(Buffer.concat(chunks)))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const processHistoryMessage = item => {
|
|
20
|
+
const messages = [], contacts = [], chats = []
|
|
21
|
+
switch (item.syncType) {
|
|
22
|
+
case proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP:
|
|
23
|
+
case proto.HistorySync.HistorySyncType.RECENT:
|
|
24
|
+
case proto.HistorySync.HistorySyncType.FULL:
|
|
25
|
+
case proto.HistorySync.HistorySyncType.ON_DEMAND:
|
|
26
|
+
for (const chat of (item.conversations ?? [])) {
|
|
27
|
+
contacts.push({ id: chat.id, name: chat.name || undefined, lid: chat.lidJid || undefined, phoneNumber: chat.pnJid || undefined, jid: isJidUser(chat.id) ? chat.id : undefined })
|
|
28
|
+
const msgs = chat.messages || []
|
|
29
|
+
delete chat.messages; delete chat.archived; delete chat.muteEndTime; delete chat.pinned
|
|
30
|
+
for (const item of msgs) {
|
|
31
|
+
const { message } = item
|
|
32
|
+
messages.push(message)
|
|
33
|
+
if (!chat.messages?.length) chat.messages = [{ message }]
|
|
34
|
+
if (!message.key.fromMe && !chat.lastMessageRecvTimestamp) chat.lastMessageRecvTimestamp = toNumber(message.messageTimestamp)
|
|
35
|
+
if ((message.messageStubType === WAMessageStubType.BIZ_PRIVACY_MODE_TO_BSP || message.messageStubType === WAMessageStubType.BIZ_PRIVACY_MODE_TO_FB) && message.messageStubParameters?.[0]) contacts.push({ id: message.key.participant || message.key.remoteJid, verifiedName: message.messageStubParameters[0] })
|
|
36
|
+
}
|
|
37
|
+
if (isJidUser(chat.id) && chat.readOnly && chat.archived) delete chat.readOnly
|
|
38
|
+
chats.push({ ...chat })
|
|
39
|
+
}
|
|
40
|
+
break
|
|
41
|
+
case proto.HistorySync.HistorySyncType.PUSH_NAME:
|
|
42
|
+
for (const c of (item.pushnames ?? [])) contacts.push({ id: c.id, notify: c.pushname })
|
|
43
|
+
for (const c of (item.inlineContacts ?? [])) { if (c.id) contacts.push({ id: c.id, name: c.fullName || c.firstName || undefined, notify: c.pushName || undefined, lid: c.lidJid || undefined, phoneNumber: c.pnJid || undefined }) }
|
|
44
|
+
break
|
|
45
|
+
}
|
|
46
|
+
return { chats, contacts, messages, syncType: item.syncType, progress: item.progress }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const downloadAndProcessHistorySyncNotification = async (msg, options) => processHistoryMessage(await downloadHistory(msg, options))
|
|
50
|
+
|
|
51
|
+
export const getHistoryMsg = message => message ? normalizeMessageContent(message)?.protocolMessage?.historySyncNotification : undefined
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { areJidsSameUser, getBinaryNodeChild, jidDecode } from '../WABinary/index.js'
|
|
2
|
+
import { isStringNullOrEmpty } from './generics.js'
|
|
3
|
+
|
|
4
|
+
export async function handleIdentityChange(node, ctx) {
|
|
5
|
+
const from = node.attrs.from
|
|
6
|
+
if (!from) return { action: 'invalid_notification' }
|
|
7
|
+
if (!getBinaryNodeChild(node, 'identity')) return { action: 'no_identity_node' }
|
|
8
|
+
ctx.logger.info({ jid: from }, 'identity changed')
|
|
9
|
+
const decoded = jidDecode(from)
|
|
10
|
+
if (decoded?.device && decoded.device !== 0) { ctx.logger.debug({ jid: from, device: decoded.device }, 'ignoring identity change from companion device'); return { action: 'skipped_companion_device', device: decoded.device } }
|
|
11
|
+
if (ctx.meId && (areJidsSameUser(from, ctx.meId) || (ctx.meLid && areJidsSameUser(from, ctx.meLid)))) { ctx.logger.info({ jid: from }, 'self primary identity changed'); return { action: 'skipped_self_primary' } }
|
|
12
|
+
if (ctx.debounceCache.get(from)) { ctx.logger.debug({ jid: from }, 'skipping identity assert (debounced)'); return { action: 'debounced' } }
|
|
13
|
+
ctx.debounceCache.set(from, true)
|
|
14
|
+
if (!(await ctx.validateSession(from)).exists) { ctx.logger.debug({ jid: from }, 'no old session, skipping session refresh'); return { action: 'skipped_no_session' } }
|
|
15
|
+
ctx.logger.debug({ jid: from }, 'old session exists, will refresh session')
|
|
16
|
+
if (!isStringNullOrEmpty(node.attrs.offline)) { ctx.logger.debug({ jid: from }, 'skipping session refresh during offline processing'); return { action: 'skipped_offline' } }
|
|
17
|
+
try { await ctx.assertSessions([from], true); return { action: 'session_refreshed' } }
|
|
18
|
+
catch (error) { ctx.logger.warn({ error, jid: from }, 'failed to assert sessions after identity change'); return { action: 'session_refresh_failed', error } }
|
|
19
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export * from './generics.js'
|
|
2
|
+
export * from './decode-wa-message.js'
|
|
3
|
+
export * from './messages.js'
|
|
4
|
+
export * from './messages-media.js'
|
|
5
|
+
export * from './validate-connection.js'
|
|
6
|
+
export * from './crypto.js'
|
|
7
|
+
export * from './signal.js'
|
|
8
|
+
export * from './make-mutex.js'
|
|
9
|
+
export * from './logger.js'
|
|
10
|
+
export * from './noise-handler.js'
|
|
11
|
+
export * from './history.js'
|
|
12
|
+
export * from './chat-utils.js'
|
|
13
|
+
export * from './lt-hash.js'
|
|
14
|
+
export * from './auth-utils.js'
|
|
15
|
+
export * from './baileys-event-stream.js'
|
|
16
|
+
export * from './use-multi-file-auth-state.js'
|
|
17
|
+
export * from './link-preview.js'
|
|
18
|
+
export * from './event-buffer.js'
|
|
19
|
+
export * from './process-message.js'
|
|
20
|
+
export * from './message-retry-manager.js'
|
|
21
|
+
export * from './browser-utils.js'
|
|
22
|
+
export * from './identity-change-handler.js'
|
|
23
|
+
export * from './key-store.js'
|
|
24
|
+
export * from './reporting-utils.js'
|
|
25
|
+
export * from './companion-reg-client-utils.js'
|
|
26
|
+
export * from './tc-token-utils.js'
|
|
27
|
+
export * from './meta-ai-msmsg.js'
|
|
28
|
+
export * from './spam-report-utils.js'
|
|
29
|
+
export * from './modern-messages.js'
|
|
30
|
+
export * from './extras.js'
|
|
31
|
+
export * from './native-flow.js'
|
|
32
|
+
export * from './consumer-application.js'
|
|
33
|
+
export * from './bot-capabilities.js'
|
|
34
|
+
export * from './meta-compositing.js'
|
|
35
|
+
export * from './bot-planning-replay.js'
|
|
36
|
+
export * from './message-composer.js'
|
|
37
|
+
export * from './business.js'
|
|
38
|
+
export * from './pre-key-manager.js'
|
|
39
|
+
export * from './sync-action-utils.js'
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { unfurl } from 'unfurl.js'
|
|
2
|
+
import { prepareWAMessageMedia } from './messages.js'
|
|
3
|
+
import { extractImageThumb, getHttpStream } from './messages-media.js'
|
|
4
|
+
|
|
5
|
+
const THUMBNAIL_WIDTH = 192
|
|
6
|
+
const TIMEOUT = 8_000
|
|
7
|
+
const MAX_INFLIGHT = 1000
|
|
8
|
+
const _inflight = new Map()
|
|
9
|
+
|
|
10
|
+
const _normalize = text => { const t = text.trim(); return /^https?:\/\//i.test(t) ? t : `https://${t}` }
|
|
11
|
+
const _extractImage = data => data?.open_graph?.images?.[0]?.url ?? data?.twitter_card?.images?.[0]?.url ?? data?.oEmbed?.thumbnails?.[0]?.url ?? data?.favicon ?? null
|
|
12
|
+
const _previewType = image => image ? 5 : 0
|
|
13
|
+
const _compressedThumb = async (url, opts) => (await extractImageThumb(await getHttpStream(url, opts.fetchOpts), opts.thumbnailWidth ?? THUMBNAIL_WIDTH)).buffer
|
|
14
|
+
|
|
15
|
+
const _resolveThumbnail = async (image, opts) => {
|
|
16
|
+
if (!image) return {}
|
|
17
|
+
if (opts.uploadImage) {
|
|
18
|
+
try {
|
|
19
|
+
const { imageMessage } = await prepareWAMessageMedia({ image: { url: image } }, { upload: opts.uploadImage, mediaTypeOverride: 'thumbnail-link', options: opts.fetchOpts })
|
|
20
|
+
const jpeg = imageMessage?.jpegThumbnail ? Buffer.from(imageMessage.jpegThumbnail) : await _compressedThumb(image, opts).catch(() => undefined)
|
|
21
|
+
return { jpegThumbnail: jpeg, highQualityThumbnail: imageMessage ?? undefined }
|
|
22
|
+
} catch { }
|
|
23
|
+
}
|
|
24
|
+
try { return { jpegThumbnail: await _compressedThumb(image, opts) } } catch { return {} }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const _fetchMeta = async (url, opts) => {
|
|
28
|
+
try {
|
|
29
|
+
const data = await unfurl(url, { timeout: opts.fetchOpts?.timeout ?? TIMEOUT })
|
|
30
|
+
const title = (data?.open_graph?.title !== data?.title ? data?.open_graph?.title : data?.title) ?? data?.oEmbed?.title
|
|
31
|
+
if (!title) return undefined
|
|
32
|
+
return { url: data?.open_graph?.url ?? url, title, description: data?.open_graph?.description ?? data?.description ?? data?.oEmbed?.author_name ?? '', image: _extractImage(data) }
|
|
33
|
+
} catch (err) { opts.logger?.warn({ err: err?.message || err, url }, 'unfurl failed'); return undefined }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const _buildResult = async (meta, text, opts) => ({
|
|
37
|
+
'canonical-url': meta.url, 'matched-text': text,
|
|
38
|
+
title: meta.title, description: meta.description,
|
|
39
|
+
originalThumbnailUrl: meta.image, previewType: _previewType(meta.image),
|
|
40
|
+
...await _resolveThumbnail(meta.image, opts)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export const getUrlInfo = (text, opts = {}) => {
|
|
44
|
+
const url = _normalize(text)
|
|
45
|
+
if (_inflight.has(url)) return _inflight.get(url)
|
|
46
|
+
if (_inflight.size >= MAX_INFLIGHT) return Promise.resolve(undefined)
|
|
47
|
+
const o = { fetchOpts: { timeout: TIMEOUT, ...opts.fetchOpts }, thumbnailWidth: opts.thumbnailWidth ?? THUMBNAIL_WIDTH, uploadImage: opts.uploadImage, logger: opts.logger }
|
|
48
|
+
const promise = (async () => { const meta = await _fetchMeta(url, o); return meta ? _buildResult(meta, text, o) : undefined })().finally(() => _inflight.delete(url))
|
|
49
|
+
_inflight.set(url, promise)
|
|
50
|
+
return promise
|
|
51
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const makeMutex = () => {
|
|
2
|
+
let task = Promise.resolve()
|
|
3
|
+
return {
|
|
4
|
+
mutex(code) {
|
|
5
|
+
task = task.catch(() => { }).then(code)
|
|
6
|
+
return task
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const makeKeyedMutex = () => {
|
|
12
|
+
const map = {}
|
|
13
|
+
return {
|
|
14
|
+
mutex(key, code) { if (!map[key]) map[key] = makeMutex(); return map[key].mutex(code) }
|
|
15
|
+
}
|
|
16
|
+
}
|