@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.
Files changed (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3203 -2
  3. package/WAProto/WAProtoCompile.js +291 -0
  4. package/WAProto/fetcher.js +69 -0
  5. package/WAProto/index.js +88 -0
  6. package/WAProto/parser.js +313 -0
  7. package/engine-requirements.js +10 -0
  8. package/lib/Builders/AIRich.js +590 -0
  9. package/lib/Builders/base.js +130 -0
  10. package/lib/Builders/builders.js +448 -0
  11. package/lib/Builders/index.js +3 -0
  12. package/lib/Defaults/index.js +225 -0
  13. package/lib/Defaults/phonenumber-mcc.json +223 -0
  14. package/lib/Signal/index.js +2 -0
  15. package/lib/Signal/libsignal.js +523 -0
  16. package/lib/Signal/lid-mapping.js +283 -0
  17. package/lib/Socket/Client/index.js +2 -0
  18. package/lib/Socket/Client/types.js +10 -0
  19. package/lib/Socket/Client/websocket.js +84 -0
  20. package/lib/Socket/business.js +152 -0
  21. package/lib/Socket/chats.js +855 -0
  22. package/lib/Socket/communities.js +147 -0
  23. package/lib/Socket/groups.js +294 -0
  24. package/lib/Socket/index.js +15 -0
  25. package/lib/Socket/messages-recv.js +904 -0
  26. package/lib/Socket/messages-send.js +693 -0
  27. package/lib/Socket/mex.js +34 -0
  28. package/lib/Socket/newsletter.js +752 -0
  29. package/lib/Socket/nexus-handler.js +55 -0
  30. package/lib/Socket/registration.js +155 -0
  31. package/lib/Socket/socket.js +798 -0
  32. package/lib/Socket/spam-report.js +65 -0
  33. package/lib/Socket/username.js +82 -0
  34. package/lib/Store/index.js +6 -0
  35. package/lib/Store/make-cache-manager-store.js +81 -0
  36. package/lib/Store/make-in-memory-store.js +425 -0
  37. package/lib/Store/make-ordered-dictionary.js +82 -0
  38. package/lib/Store/object-repository.js +31 -0
  39. package/lib/Store/reporting-info-store.js +38 -0
  40. package/lib/Types/Auth.js +2 -0
  41. package/lib/Types/Bussines.js +2 -0
  42. package/lib/Types/Call.js +2 -0
  43. package/lib/Types/Chat.js +8 -0
  44. package/lib/Types/Contact.js +2 -0
  45. package/lib/Types/Events.js +2 -0
  46. package/lib/Types/GroupMetadata.js +2 -0
  47. package/lib/Types/Label.js +25 -0
  48. package/lib/Types/LabelAssociation.js +7 -0
  49. package/lib/Types/Message.js +15 -0
  50. package/lib/Types/Newsletter.js +100 -0
  51. package/lib/Types/Product.js +2 -0
  52. package/lib/Types/Signal.js +2 -0
  53. package/lib/Types/Socket.js +14 -0
  54. package/lib/Types/State.js +26 -0
  55. package/lib/Types/USync.js +2 -0
  56. package/lib/Types/index.js +30 -0
  57. package/lib/Utils/auth-utils.js +142 -0
  58. package/lib/Utils/baileys-event-stream.js +33 -0
  59. package/lib/Utils/bot-capabilities.js +40 -0
  60. package/lib/Utils/bot-planning-replay.js +44 -0
  61. package/lib/Utils/browser-utils.js +26 -0
  62. package/lib/Utils/business.js +75 -0
  63. package/lib/Utils/chat-utils.js +316 -0
  64. package/lib/Utils/companion-reg-client-utils.js +20 -0
  65. package/lib/Utils/consumer-application.js +38 -0
  66. package/lib/Utils/crypto.js +52 -0
  67. package/lib/Utils/decode-wa-message.js +197 -0
  68. package/lib/Utils/event-buffer.js +273 -0
  69. package/lib/Utils/extras.js +186 -0
  70. package/lib/Utils/generics.js +195 -0
  71. package/lib/Utils/history.js +51 -0
  72. package/lib/Utils/identity-change-handler.js +19 -0
  73. package/lib/Utils/index.js +39 -0
  74. package/lib/Utils/key-store.js +3 -0
  75. package/lib/Utils/link-preview.js +51 -0
  76. package/lib/Utils/logger.js +2 -0
  77. package/lib/Utils/lt-hash.js +3 -0
  78. package/lib/Utils/make-mutex.js +16 -0
  79. package/lib/Utils/message-composer.js +59 -0
  80. package/lib/Utils/message-retry-manager.js +48 -0
  81. package/lib/Utils/messages-media.js +520 -0
  82. package/lib/Utils/messages.js +742 -0
  83. package/lib/Utils/meta-ai-msmsg.js +122 -0
  84. package/lib/Utils/meta-compositing.js +75 -0
  85. package/lib/Utils/modern-messages.js +235 -0
  86. package/lib/Utils/native-flow.js +55 -0
  87. package/lib/Utils/noise-handler.js +161 -0
  88. package/lib/Utils/pre-key-manager.js +76 -0
  89. package/lib/Utils/process-message.js +399 -0
  90. package/lib/Utils/reporting-utils.js +171 -0
  91. package/lib/Utils/signal.js +167 -0
  92. package/lib/Utils/spam-report-utils.js +142 -0
  93. package/lib/Utils/sync-action-utils.js +33 -0
  94. package/lib/Utils/tc-token-utils.js +102 -0
  95. package/lib/Utils/use-multi-file-auth-state.js +307 -0
  96. package/lib/Utils/validate-connection.js +181 -0
  97. package/lib/WABinary/constants.js +1321 -0
  98. package/lib/WABinary/decode.js +152 -0
  99. package/lib/WABinary/encode.js +140 -0
  100. package/lib/WABinary/generic-utils.js +179 -0
  101. package/lib/WABinary/index.js +6 -0
  102. package/lib/WABinary/jid-utils.js +235 -0
  103. package/lib/WABinary/types.js +4 -0
  104. package/lib/WAM/BinaryInfo.js +10 -0
  105. package/lib/WAM/constants.js +22853 -0
  106. package/lib/WAM/encode.js +150 -0
  107. package/lib/WAM/index.js +3 -0
  108. package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +30 -0
  109. package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +48 -0
  110. package/lib/WAUSync/Protocols/USyncContactProtocol.js +23 -0
  111. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +28 -0
  112. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +15 -0
  113. package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +24 -0
  114. package/lib/WAUSync/Protocols/USyncLIDProtocol.js +9 -0
  115. package/lib/WAUSync/Protocols/USyncPictureProtocol.js +21 -0
  116. package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +19 -0
  117. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +19 -0
  118. package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +22 -0
  119. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +16 -0
  120. package/lib/WAUSync/Protocols/index.js +12 -0
  121. package/lib/WAUSync/USyncQuery.js +100 -0
  122. package/lib/WAUSync/USyncUser.js +20 -0
  123. package/lib/WAUSync/index.js +3 -0
  124. package/lib/index.js +68 -0
  125. 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,3 @@
1
+ // ─── Key Store Helpers ─────────────────────────────────────────────────────────
2
+ // Single source of truth for blob key names — change here, changes everywhere
3
+ export const TC_TOKEN_INDEX_KEY = '__index'
@@ -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,2 @@
1
+ import P from 'pino';
2
+ export default P({ timestamp: () => `,"time":"${new Date().toJSON()}"` });
@@ -0,0 +1,3 @@
1
+ import { LTHashAntiTampering } from 'whatsapp-rust-bridge';
2
+
3
+ export const LT_HASH_ANTI_TAMPERING = new LTHashAntiTampering();
@@ -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
+ }