chore: format everything

This commit is contained in:
canove
2025-05-06 12:10:19 -03:00
parent 04afa20244
commit fa706d0b50
76 changed files with 8241 additions and 7142 deletions

View File

@@ -1,6 +1,7 @@
lib
coverage
*.lock
*.json
src/WABinary/index.ts
WAProto
WASignalGroup

View File

@@ -30,8 +30,9 @@
"changelog:update": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
"example": "node --inspect -r ts-node/register Example/example.ts",
"gen:protobuf": "sh WAProto/GenerateStatics.sh",
"format": "prettier --write \"src/**/*.{ts,js,json,md}\"",
"lint": "eslint src --ext .js,.ts",
"lint:fix": "yarn lint --fix",
"lint:fix": "yarn format && yarn lint --fix",
"prepack": "tsc",
"prepare": "tsc",
"preinstall": "node ./engine-requirements.js",

View File

@@ -17,14 +17,12 @@ export const WA_DEFAULT_EPHEMERAL = 7 * 24 * 60 * 60
export const NOISE_MODE = 'Noise_XX_25519_AESGCM_SHA256\0\0\0\0'
export const DICT_VERSION = 2
export const KEY_BUNDLE_TYPE = Buffer.from([5])
export const NOISE_WA_HEADER = Buffer.from(
[ 87, 65, 6, DICT_VERSION ]
) // last is "DICT_VERSION"
export const NOISE_WA_HEADER = Buffer.from([87, 65, 6, DICT_VERSION]) // last is "DICT_VERSION"
/** from: https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url */
export const URL_REGEX = /https:\/\/(?![^:@\/\s]+:[^:@\/\s]+@)[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(:\d+)?(\/[^\s]*)?/g
export const WA_CERT_DETAILS = {
SERIAL: 0,
SERIAL: 0
}
export const PROCESSABLE_HISTORY_TYPES = [
@@ -32,7 +30,7 @@ export const PROCESSABLE_HISTORY_TYPES = [
proto.Message.HistorySyncNotification.HistorySyncType.PUSH_NAME,
proto.Message.HistorySyncNotification.HistorySyncType.RECENT,
proto.Message.HistorySyncNotification.HistorySyncType.FULL,
proto.Message.HistorySyncNotification.HistorySyncType.ON_DEMAND,
proto.Message.HistorySyncNotification.HistorySyncType.ON_DEMAND
]
export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
@@ -60,7 +58,7 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
options: {},
appStateMacVerification: {
patch: false,
snapshot: false,
snapshot: false
},
countryCode: 'US',
getMessage: async () => undefined,
@@ -77,19 +75,19 @@ export const MEDIA_PATH_MAP: { [T in MediaType]?: string } = {
'thumbnail-link': '/mms/image',
'product-catalog-image': '/product/image',
'md-app-state': '',
'md-msg-hist': '/mms/md-app-state',
'md-msg-hist': '/mms/md-app-state'
}
export const MEDIA_HKDF_KEY_MAPPING = {
'audio': 'Audio',
'document': 'Document',
'gif': 'Video',
'image': 'Image',
'ppic': '',
'product': 'Image',
'ptt': 'Audio',
'sticker': 'Image',
'video': 'Video',
audio: 'Audio',
document: 'Document',
gif: 'Video',
image: 'Image',
ppic: '',
product: 'Image',
ptt: 'Audio',
sticker: 'Image',
video: 'Video',
'thumbnail-document': 'Document Thumbnail',
'thumbnail-image': 'Image Thumbnail',
'thumbnail-video': 'Video Thumbnail',
@@ -98,7 +96,7 @@ export const MEDIA_HKDF_KEY_MAPPING = {
'md-app-state': 'App State',
'product-catalog-image': '',
'payment-bg-image': 'Payment Background',
'ptv': 'Video'
ptv: 'Video'
}
export const MEDIA_KEYS = Object.keys(MEDIA_PATH_MAP) as MediaType[]
@@ -111,5 +109,5 @@ export const DEFAULT_CACHE_TTLS = {
SIGNAL_STORE: 5 * 60, // 5 minutes
MSG_RETRY: 60 * 60, // 1 hour
CALL_OFFER: 5 * 60, // 5 minutes
USER_DEVICES: 5 * 60, // 5 minutes
USER_DEVICES: 5 * 60 // 5 minutes
}

View File

@@ -1,5 +1,11 @@
import * as libsignal from 'libsignal'
import { GroupCipher, GroupSessionBuilder, SenderKeyDistributionMessage, SenderKeyName, SenderKeyRecord } from '../../WASignalGroup'
import {
GroupCipher,
GroupSessionBuilder,
SenderKeyDistributionMessage,
SenderKeyName,
SenderKeyRecord
} from '../../WASignalGroup'
import { SignalAuthState } from '../Types'
import { SignalRepository } from '../Types/Signal'
import { generateSignalPubKey } from '../Utils'
@@ -18,7 +24,13 @@ export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository
const builder = new GroupSessionBuilder(storage)
const senderName = jidToSignalSenderKeyName(item.groupId!, authorJid)
const senderMsg = new SenderKeyDistributionMessage(null, null, null, null, item.axolotlSenderKeyDistributionMessage)
const senderMsg = new SenderKeyDistributionMessage(
null,
null,
null,
null,
item.axolotlSenderKeyDistributionMessage
)
const { [senderName]: senderKey } = await auth.keys.get('sender-key', [senderName])
if (!senderKey) {
await storage.storeSenderKey(senderName, new SenderKeyRecord())
@@ -64,7 +76,7 @@ export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository
return {
ciphertext,
senderKeyDistributionMessage: senderKeyDistributionMessage.serialize(),
senderKeyDistributionMessage: senderKeyDistributionMessage.serialize()
}
},
async injectE2ESession({ jid, session }) {
@@ -73,7 +85,7 @@ export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository
},
jidToSignalProtocolAddress(jid) {
return jidToSignalProtocolAddress(jid).toString()
},
}
}
}
@@ -95,7 +107,7 @@ function signalStorage({ creds, keys }: SignalAuthState) {
}
},
storeSession: async (id, session) => {
await keys.set({ 'session': { [id]: session.serialize() } })
await keys.set({ session: { [id]: session.serialize() } })
},
isTrustedIdentity: () => {
return true
@@ -127,14 +139,12 @@ function signalStorage({ creds, keys }: SignalAuthState) {
storeSenderKey: async (keyId, key) => {
await keys.set({ 'sender-key': { [keyId]: key.serialize() } })
},
getOurRegistrationId: () => (
creds.registrationId
),
getOurRegistrationId: () => creds.registrationId,
getOurIdentity: () => {
const { signedIdentityKey } = creds
return {
privKey: Buffer.from(signedIdentityKey.private),
pubKey: generateSignalPubKey(signedIdentityKey.public),
pubKey: generateSignalPubKey(signedIdentityKey.public)
}
}
}

View File

@@ -8,12 +8,15 @@ export abstract class AbstractSocketClient extends EventEmitter {
abstract get isClosing(): boolean
abstract get isConnecting(): boolean
constructor(public url: URL, public config: SocketConfig) {
constructor(
public url: URL,
public config: SocketConfig
) {
super()
this.setMaxListeners(0)
}
abstract connect(): Promise<void>
abstract close(): Promise<void>
abstract send(str: Uint8Array | string, cb?: (err?: Error) => void): boolean;
abstract send(str: Uint8Array | string, cb?: (err?: Error) => void): boolean
}

View File

@@ -3,7 +3,6 @@ import { DEFAULT_ORIGIN } from '../../Defaults'
import { AbstractSocketClient } from './types'
export class WebSocketClient extends AbstractSocketClient {
protected socket: WebSocket | null = null
get isOpen(): boolean {
@@ -29,7 +28,7 @@ export class WebSocketClient extends AbstractSocketClient {
headers: this.config.options?.headers as {},
handshakeTimeout: this.config.connectTimeoutMs,
timeout: this.config.connectTimeoutMs,
agent: this.config.agent,
agent: this.config.agent
})
this.socket.setMaxListeners(0)

View File

@@ -1,16 +1,19 @@
import { GetCatalogOptions, ProductCreate, ProductUpdate, SocketConfig } from '../Types'
import { parseCatalogNode, parseCollectionsNode, parseOrderDetailsNode, parseProductNode, toProductNode, uploadingNecessaryImagesOfProduct } from '../Utils/business'
import {
parseCatalogNode,
parseCollectionsNode,
parseOrderDetailsNode,
parseProductNode,
toProductNode,
uploadingNecessaryImagesOfProduct
} from '../Utils/business'
import { BinaryNode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary'
import { getBinaryNodeChild } from '../WABinary/generic-utils'
import { makeMessagesRecvSocket } from './messages-recv'
export const makeBusinessSocket = (config: SocketConfig) => {
const sock = makeMessagesRecvSocket(config)
const {
authState,
query,
waUploadToServer
} = sock
const { authState, query, waUploadToServer } = sock
const getCatalog = async ({ jid, limit, cursor }: GetCatalogOptions) => {
jid = jid || authState.creds.me?.id
@@ -31,7 +34,7 @@ export const makeBusinessSocket = (config: SocketConfig) => {
tag: 'height',
attrs: {},
content: Buffer.from('100')
},
}
]
if (cursor) {
@@ -54,7 +57,7 @@ export const makeBusinessSocket = (config: SocketConfig) => {
tag: 'product_catalog',
attrs: {
jid,
'allow_shop_source': 'true'
allow_shop_source: 'true'
},
content: queryParamNodes
}
@@ -72,13 +75,13 @@ export const makeBusinessSocket = (config: SocketConfig) => {
to: S_WHATSAPP_NET,
type: 'get',
xmlns: 'w:biz:catalog',
'smax_id': '35'
smax_id: '35'
},
content: [
{
tag: 'collections',
attrs: {
'biz_jid': jid,
biz_jid: jid
},
content: [
{
@@ -116,7 +119,7 @@ export const makeBusinessSocket = (config: SocketConfig) => {
to: S_WHATSAPP_NET,
type: 'get',
xmlns: 'fb:thrift_iq',
'smax_id': '5'
smax_id: '5'
},
content: [
{
@@ -245,8 +248,7 @@ export const makeBusinessSocket = (config: SocketConfig) => {
{
tag: 'product_catalog_delete',
attrs: { v: '1' },
content: productIds.map(
id => ({
content: productIds.map(id => ({
tag: 'product',
attrs: {},
content: [
@@ -256,8 +258,7 @@ export const makeBusinessSocket = (config: SocketConfig) => {
content: Buffer.from(id)
}
]
})
)
}))
}
]
})

View File

@@ -2,12 +2,52 @@ import NodeCache from '@cacheable/node-cache'
import { Boom } from '@hapi/boom'
import { proto } from '../../WAProto'
import { DEFAULT_CACHE_TTLS, PROCESSABLE_HISTORY_TYPES } from '../Defaults'
import { ALL_WA_PATCH_NAMES, BotListInfo, ChatModification, ChatMutation, LTHashState, MessageUpsertType, PresenceData, SocketConfig, WABusinessHoursConfig, WABusinessProfile, WAMediaUpload, WAMessage, WAPatchCreate, WAPatchName, WAPresence, WAPrivacyCallValue, WAPrivacyGroupAddValue, WAPrivacyMessagesValue, WAPrivacyOnlineValue, WAPrivacyValue, WAReadReceiptsValue } from '../Types'
import {
ALL_WA_PATCH_NAMES,
BotListInfo,
ChatModification,
ChatMutation,
LTHashState,
MessageUpsertType,
PresenceData,
SocketConfig,
WABusinessHoursConfig,
WABusinessProfile,
WAMediaUpload,
WAMessage,
WAPatchCreate,
WAPatchName,
WAPresence,
WAPrivacyCallValue,
WAPrivacyGroupAddValue,
WAPrivacyMessagesValue,
WAPrivacyOnlineValue,
WAPrivacyValue,
WAReadReceiptsValue
} from '../Types'
import { LabelActionBody } from '../Types/Label'
import { chatModificationToAppPatch, ChatMutationMap, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch, extractSyncdPatches, generateProfilePicture, getHistoryMsg, newLTHashState, processSyncAction } from '../Utils'
import {
chatModificationToAppPatch,
ChatMutationMap,
decodePatches,
decodeSyncdSnapshot,
encodeSyncdPatch,
extractSyncdPatches,
generateProfilePicture,
getHistoryMsg,
newLTHashState,
processSyncAction
} from '../Utils'
import { makeMutex } from '../Utils/make-mutex'
import processMessage from '../Utils/process-message'
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary'
import {
BinaryNode,
getBinaryNodeChild,
getBinaryNodeChildren,
jidNormalizedUser,
reduceBinaryNodeToDictionary,
S_WHATSAPP_NET
} from '../WABinary'
import { USyncQuery, USyncUser } from '../WAUSync'
import { makeUSyncSocket } from './usync'
const MAX_SYNC_ATTEMPTS = 2
@@ -19,18 +59,10 @@ export const makeChatsSocket = (config: SocketConfig) => {
fireInitQueries,
appStateMacVerification,
shouldIgnoreJid,
shouldSyncHistoryMessage,
shouldSyncHistoryMessage
} = config
const sock = makeUSyncSocket(config)
const {
ev,
ws,
authState,
generateMessageTag,
sendNode,
query,
onUnexpectedError,
} = sock
const { ev, ws, authState, generateMessageTag, sendNode, query, onUnexpectedError } = sock
let privacySettings: { [_: string]: string } | undefined
let needToFlushWithAppStateSync = false
@@ -38,7 +70,9 @@ export const makeChatsSocket = (config: SocketConfig) => {
/** this mutex ensures that the notifications (receipts, messages etc.) are processed in order */
const processingMutex = makeMutex()
const placeholderResendCache = config.placeholderResendCache || new NodeCache({
const placeholderResendCache =
config.placeholderResendCache ||
new NodeCache({
stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
useClones: false
})
@@ -62,9 +96,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
to: S_WHATSAPP_NET,
type: 'get'
},
content: [
{ tag: 'privacy', attrs: {} }
]
content: [{ tag: 'privacy', attrs: {} }]
})
privacySettings = reduceBinaryNodeToDictionary(content?.[0] as BinaryNode, 'category')
}
@@ -81,7 +113,8 @@ export const makeChatsSocket = (config: SocketConfig) => {
to: S_WHATSAPP_NET,
type: 'set'
},
content: [{
content: [
{
tag: 'privacy',
attrs: {},
content: [
@@ -90,7 +123,8 @@ export const makeChatsSocket = (config: SocketConfig) => {
attrs: { name, value }
}
]
}]
}
]
})
}
@@ -134,12 +168,14 @@ export const makeChatsSocket = (config: SocketConfig) => {
to: S_WHATSAPP_NET,
type: 'set'
},
content: [{
content: [
{
tag: 'disappearing_mode',
attrs: {
duration: duration.toString()
}
}]
}
]
})
}
@@ -151,12 +187,14 @@ export const makeChatsSocket = (config: SocketConfig) => {
to: S_WHATSAPP_NET,
type: 'get'
},
content: [{
content: [
{
tag: 'bot',
attrs: {
v: '2'
}
}]
}
]
})
const botNode = getBinaryNodeChild(resp, 'bot')
@@ -177,9 +215,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
}
const onWhatsApp = async (...jids: string[]) => {
const usyncQuery = new USyncQuery()
.withContactProtocol()
.withLIDProtocol()
const usyncQuery = new USyncQuery().withContactProtocol().withLIDProtocol()
for (const jid of jids) {
const phone = `+${jid.replace('+', '').split('@')[0].split(':')[0]}`
@@ -189,13 +225,12 @@ export const makeChatsSocket = (config: SocketConfig) => {
const results = await sock.executeUSyncQuery(usyncQuery)
if (results) {
return results.list.filter((a) => !!a.contact).map(({ contact, id, lid }) => ({ jid: id, exists: contact, lid }))
return results.list.filter(a => !!a.contact).map(({ contact, id, lid }) => ({ jid: id, exists: contact, lid }))
}
}
const fetchStatus = async (...jids: string[]) => {
const usyncQuery = new USyncQuery()
.withStatusProtocol()
const usyncQuery = new USyncQuery().withStatusProtocol()
for (const jid of jids) {
usyncQuery.withUser(new USyncUser().withId(jid))
@@ -208,8 +243,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
}
const fetchDisappearingDuration = async (...jids: string[]) => {
const usyncQuery = new USyncQuery()
.withDisappearingModeProtocol()
const usyncQuery = new USyncQuery().withDisappearingModeProtocol()
for (const jid of jids) {
usyncQuery.withUser(new USyncUser().withId(jid))
@@ -225,7 +259,9 @@ export const makeChatsSocket = (config: SocketConfig) => {
const updateProfilePicture = async (jid: string, content: WAMediaUpload) => {
let targetJid
if (!jid) {
throw new Boom('Illegal no-jid profile update. Please specify either your ID or the ID of the chat you wish to update')
throw new Boom(
'Illegal no-jid profile update. Please specify either your ID or the ID of the chat you wish to update'
)
}
if (jidNormalizedUser(jid) !== jidNormalizedUser(authState.creds.me!.id)) {
@@ -255,7 +291,9 @@ export const makeChatsSocket = (config: SocketConfig) => {
const removeProfilePicture = async (jid: string) => {
let targetJid
if (!jid) {
throw new Boom('Illegal no-jid profile update. Please specify either your ID or the ID of the chat you wish to update')
throw new Boom(
'Illegal no-jid profile update. Please specify either your ID or the ID of the chat you wish to update'
)
}
if (jidNormalizedUser(jid) !== jidNormalizedUser(authState.creds.me!.id)) {
@@ -307,8 +345,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
})
const listNode = getBinaryNodeChild(result, 'list')
return getBinaryNodeChildren(listNode, 'item')
.map(n => n.attrs.jid)
return getBinaryNodeChildren(listNode, 'item').map(n => n.attrs.jid)
}
const updateBlockStatus = async (jid: string, action: 'block' | 'unblock') => {
@@ -339,14 +376,18 @@ export const makeChatsSocket = (config: SocketConfig) => {
xmlns: 'w:biz',
type: 'get'
},
content: [{
content: [
{
tag: 'business_profile',
attrs: { v: '244' },
content: [{
content: [
{
tag: 'profile',
attrs: { jid }
}]
}]
}
]
}
]
})
const profileNode = getBinaryNodeChild(results, 'business_profile')
@@ -369,9 +410,9 @@ export const makeChatsSocket = (config: SocketConfig) => {
website: websiteStr ? [websiteStr] : [],
email: email?.content?.toString(),
category: category?.content?.toString(),
'business_hours': {
business_hours: {
timezone: businessHours?.attrs?.timezone,
'business_config': businessHoursConfig?.map(({ attrs }) => attrs as unknown as WABusinessHoursConfig)
business_config: businessHoursConfig?.map(({ attrs }) => attrs as unknown as WABusinessHoursConfig)
}
}
}
@@ -385,14 +426,14 @@ export const makeChatsSocket = (config: SocketConfig) => {
to: S_WHATSAPP_NET,
type: 'set',
xmlns: 'urn:xmpp:whatsapp:dirty',
id: generateMessageTag(),
id: generateMessageTag()
},
content: [
{
tag: 'clean',
attrs: {
type,
...(fromTimestamp ? { timestamp: fromTimestamp.toString() } : null),
...(fromTimestamp ? { timestamp: fromTimestamp.toString() } : null)
}
}
]
@@ -413,14 +454,14 @@ export const makeChatsSocket = (config: SocketConfig) => {
}
}
const resyncAppState = ev.createBufferedFunction(async(collections: readonly WAPatchName[], isInitialSync: boolean) => {
const resyncAppState = ev.createBufferedFunction(
async (collections: readonly WAPatchName[], isInitialSync: boolean) => {
// we use this to determine which events to fire
// otherwise when we resync from scratch -- all notifications will fire
const initialVersionMap: { [T in WAPatchName]?: number } = {}
const globalMutationMap: ChatMutationMap = {}
await authState.keys.transaction(
async() => {
await authState.keys.transaction(async () => {
const collectionsToHandle = new Set<string>(collections)
// in case something goes wrong -- ensure we don't enter a loop that cannot be exited from
const attemptsMap: { [T in WAPatchName]?: number } = {}
@@ -453,7 +494,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
name,
version: state.version.toString(),
// return snapshot if being synced from scratch
'return_snapshot': (!state.version).toString()
return_snapshot: (!state.version).toString()
}
})
}
@@ -519,15 +560,17 @@ export const makeChatsSocket = (config: SocketConfig) => {
if (hasMorePatches) {
logger.info(`${name} has more patches...`)
} else { // collection is done with sync
} else {
// collection is done with sync
collectionsToHandle.delete(name)
}
} catch (error) {
// if retry attempts overshoot
// or key not found
const isIrrecoverableError = attemptsMap[name]! >= MAX_SYNC_ATTEMPTS
|| error.output?.statusCode === 404
|| error.name === 'TypeError'
const isIrrecoverableError =
attemptsMap[name]! >= MAX_SYNC_ATTEMPTS ||
error.output?.statusCode === 404 ||
error.name === 'TypeError'
logger.info(
{ name, error: error.stack },
`failed to sync state from version${isIrrecoverableError ? '' : ', removing and trying from scratch'}`
@@ -543,14 +586,14 @@ export const makeChatsSocket = (config: SocketConfig) => {
}
}
}
}
)
})
const { onMutation } = newAppStateChunkHandler(isInitialSync)
for (const key in globalMutationMap) {
onMutation(globalMutationMap[key])
}
})
}
)
/**
* fetch the profile picture of a user/group
@@ -559,7 +602,8 @@ export const makeChatsSocket = (config: SocketConfig) => {
*/
const profilePictureUrl = async (jid: string, type: 'preview' | 'image' = 'preview', timeoutMs?: number) => {
jid = jidNormalizedUser(jid)
const result = await query({
const result = await query(
{
tag: 'iq',
attrs: {
target: jid,
@@ -567,10 +611,10 @@ export const makeChatsSocket = (config: SocketConfig) => {
type: 'get',
xmlns: 'w:profile:picture'
},
content: [
{ tag: 'picture', attrs: { type, query: 'url' } }
]
}, timeoutMs)
content: [{ tag: 'picture', attrs: { type, query: 'url' } }]
},
timeoutMs
)
const child = getBinaryNodeChild(result, 'picture')
return child?.attrs?.url
}
@@ -597,7 +641,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
tag: 'chatstate',
attrs: {
from: me.id,
to: toJid!,
to: toJid!
},
content: [
{
@@ -613,7 +657,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
* @param toJid the jid to subscribe to
* @param tcToken token for subscription, use if present
*/
const presenceSubscribe = (toJid: string, tcToken?: Buffer) => (
const presenceSubscribe = (toJid: string, tcToken?: Buffer) =>
sendNode({
tag: 'presence',
attrs: {
@@ -631,7 +675,6 @@ export const makeChatsSocket = (config: SocketConfig) => {
]
: undefined
})
)
const handlePresenceUpdate = ({ tag, attrs, content }: BinaryNode) => {
let presence: PresenceData | undefined
@@ -676,12 +719,10 @@ export const makeChatsSocket = (config: SocketConfig) => {
}
let initial: LTHashState
let encodeResult: { patch: proto.ISyncdPatch, state: LTHashState }
let encodeResult: { patch: proto.ISyncdPatch; state: LTHashState }
await processingMutex.mutex(
async() => {
await authState.keys.transaction(
async() => {
await processingMutex.mutex(async () => {
await authState.keys.transaction(async () => {
logger.debug({ patch: patchCreate }, 'applying app patch')
await resyncAppState([name], false)
@@ -689,12 +730,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
const { [name]: currentSyncVersion } = await authState.keys.get('app-state-sync-version', [name])
initial = currentSyncVersion || newLTHashState()
encodeResult = await encodeSyncdPatch(
patchCreate,
myAppStateKeyId,
initial,
getAppStateSyncKey,
)
encodeResult = await encodeSyncdPatch(patchCreate, myAppStateKeyId, initial, getAppStateSyncKey)
const { patch, state } = encodeResult
const node: BinaryNode = {
@@ -714,7 +750,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
attrs: {
name,
version: (state.version - 1).toString(),
'return_snapshot': 'false'
return_snapshot: 'false'
},
content: [
{
@@ -731,21 +767,19 @@ export const makeChatsSocket = (config: SocketConfig) => {
await query(node)
await authState.keys.set({ 'app-state-sync-version': { [name]: state } })
}
)
}
)
})
})
if (config.emitOwnEvents) {
const { onMutation } = newAppStateChunkHandler(false)
const { mutationMap } = await decodePatches(
name,
[{ ...encodeResult!.patch, version: { version: encodeResult!.state.version }, }],
[{ ...encodeResult!.patch, version: { version: encodeResult!.state.version } }],
initial!,
getAppStateSyncKey,
config.options,
undefined,
logger,
logger
)
for (const key in mutationMap) {
onMutation(mutationMap[key])
@@ -760,22 +794,25 @@ export const makeChatsSocket = (config: SocketConfig) => {
attrs: {
to: S_WHATSAPP_NET,
xmlns: 'w',
type: 'get',
type: 'get'
},
content: [
{ tag: 'props', attrs: {
{
tag: 'props',
attrs: {
protocol: '2',
hash: authState?.creds?.lastPropHash || ''
} }
}
}
]
})
const propsNode = getBinaryNodeChild(resultNode, 'props')
let props: { [_: string]: string } = {}
if (propsNode) {
if(propsNode.attrs?.hash) { // on some clients, the hash is returning as undefined
if (propsNode.attrs?.hash) {
// on some clients, the hash is returning as undefined
authState.creds.lastPropHash = propsNode?.attrs?.hash
ev.emit('creds.update', authState.creds)
}
@@ -801,70 +838,88 @@ export const makeChatsSocket = (config: SocketConfig) => {
/**
* Star or Unstar a message
*/
const star = (jid: string, messages: { id: string, fromMe?: boolean }[], star: boolean) => {
return chatModify({
const star = (jid: string, messages: { id: string; fromMe?: boolean }[], star: boolean) => {
return chatModify(
{
star: {
messages,
star
}
}, jid)
},
jid
)
}
/**
* Adds label
*/
const addLabel = (jid: string, labels: LabelActionBody) => {
return chatModify({
return chatModify(
{
addLabel: {
...labels
}
}, jid)
},
jid
)
}
/**
* Adds label for the chats
*/
const addChatLabel = (jid: string, labelId: string) => {
return chatModify({
return chatModify(
{
addChatLabel: {
labelId
}
}, jid)
},
jid
)
}
/**
* Removes label for the chat
*/
const removeChatLabel = (jid: string, labelId: string) => {
return chatModify({
return chatModify(
{
removeChatLabel: {
labelId
}
}, jid)
},
jid
)
}
/**
* Adds label for the message
*/
const addMessageLabel = (jid: string, messageId: string, labelId: string) => {
return chatModify({
return chatModify(
{
addMessageLabel: {
messageId,
labelId
}
}, jid)
},
jid
)
}
/**
* Removes label for the message
*/
const removeMessageLabel = (jid: string, messageId: string, labelId: string) => {
return chatModify({
return chatModify(
{
removeMessageLabel: {
messageId,
labelId
}
}, jid)
},
jid
)
}
/**
@@ -872,18 +927,14 @@ export const makeChatsSocket = (config: SocketConfig) => {
* help ensure parity with WA Web
* */
const executeInitQueries = async () => {
await Promise.all([
fetchProps(),
fetchBlocklist(),
fetchPrivacySettings(),
])
await Promise.all([fetchProps(), fetchBlocklist(), fetchPrivacySettings()])
}
const upsertMessage = ev.createBufferedFunction(async (msg: WAMessage, type: MessageUpsertType) => {
ev.emit('messages.upsert', { messages: [msg], type })
if (!!msg.pushName) {
let jid = msg.key.fromMe ? authState.creds.me!.id : (msg.key.participant || msg.key.remoteJid)
let jid = msg.key.fromMe ? authState.creds.me!.id : msg.key.participant || msg.key.remoteJid
jid = jidNormalizedUser(jid!)
if (!msg.key.fromMe) {
@@ -898,10 +949,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
const historyMsg = getHistoryMsg(msg.message!)
const shouldProcessHistoryMsg = historyMsg
? (
shouldSyncHistoryMessage(historyMsg)
&& PROCESSABLE_HISTORY_TYPES.includes(historyMsg.syncType!)
)
? shouldSyncHistoryMessage(historyMsg) && PROCESSABLE_HISTORY_TYPES.includes(historyMsg.syncType!)
: false
if (historyMsg && !authState.creds.myAppStateKeyId) {
@@ -911,32 +959,23 @@ export const makeChatsSocket = (config: SocketConfig) => {
await Promise.all([
(async () => {
if(
historyMsg
&& authState.creds.myAppStateKeyId
) {
if (historyMsg && authState.creds.myAppStateKeyId) {
pendingAppStateSync = false
await doAppStateSync()
}
})(),
processMessage(
msg,
{
processMessage(msg, {
shouldProcessHistoryMsg,
placeholderResendCache,
ev,
creds: authState.creds,
keyStore: authState.keys,
logger,
options: config.options,
}
)
options: config.options
})
])
if(
msg.message?.protocolMessage?.appStateSyncKeyShare
&& pendingAppStateSync
) {
if (msg.message?.protocolMessage?.appStateSyncKeyShare && pendingAppStateSync) {
await doAppStateSync()
pendingAppStateSync = false
}
@@ -988,23 +1027,21 @@ export const makeChatsSocket = (config: SocketConfig) => {
ev.on('connection.update', ({ connection, receivedPendingNotifications }) => {
if (connection === 'open') {
if (fireInitQueries) {
executeInitQueries()
.catch(
error => onUnexpectedError(error, 'init queries')
executeInitQueries().catch(error => onUnexpectedError(error, 'init queries'))
}
sendPresenceUpdate(markOnlineOnConnect ? 'available' : 'unavailable').catch(error =>
onUnexpectedError(error, 'presence update requests')
)
}
sendPresenceUpdate(markOnlineOnConnect ? 'available' : 'unavailable')
.catch(
error => onUnexpectedError(error, 'presence update requests')
)
}
if(receivedPendingNotifications && // if we don't have the app state key
if (
receivedPendingNotifications && // if we don't have the app state key
// we keep buffering events until we finally have
// the key and can sync the messages
// todo scrutinize
!authState.creds?.myAppStateKeyId) {
!authState.creds?.myAppStateKeyId
) {
ev.buffer()
needToFlushWithAppStateSync = true
}

View File

@@ -1,42 +1,50 @@
import { proto } from '../../WAProto'
import { GroupMetadata, GroupParticipant, ParticipantAction, SocketConfig, WAMessageKey, WAMessageStubType } from '../Types'
import {
GroupMetadata,
GroupParticipant,
ParticipantAction,
SocketConfig,
WAMessageKey,
WAMessageStubType
} from '../Types'
import { generateMessageIDV2, unixTimestampSeconds } from '../Utils'
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, getBinaryNodeChildString, jidEncode, jidNormalizedUser } from '../WABinary'
import {
BinaryNode,
getBinaryNodeChild,
getBinaryNodeChildren,
getBinaryNodeChildString,
jidEncode,
jidNormalizedUser
} from '../WABinary'
import { makeChatsSocket } from './chats'
export const makeGroupsSocket = (config: SocketConfig) => {
const sock = makeChatsSocket(config)
const { authState, ev, query, upsertMessage } = sock
const groupQuery = async(jid: string, type: 'get' | 'set', content: BinaryNode[]) => (
const groupQuery = async (jid: string, type: 'get' | 'set', content: BinaryNode[]) =>
query({
tag: 'iq',
attrs: {
type,
xmlns: 'w:g2',
to: jid,
to: jid
},
content
})
)
const groupMetadata = async (jid: string) => {
const result = await groupQuery(
jid,
'get',
[ { tag: 'query', attrs: { request: 'interactive' } } ]
)
const result = await groupQuery(jid, 'get', [{ tag: 'query', attrs: { request: 'interactive' } }])
return extractGroupMetadata(result)
}
const groupFetchAllParticipating = async () => {
const result = await query({
tag: 'iq',
attrs: {
to: '@g.us',
xmlns: 'w:g2',
type: 'get',
type: 'get'
},
content: [
{
@@ -83,10 +91,7 @@ export const makeGroupsSocket = (config: SocketConfig) => {
groupMetadata,
groupCreate: async (subject: string, participants: string[]) => {
const key = generateMessageIDV2()
const result = await groupQuery(
'@g.us',
'set',
[
const result = await groupQuery('@g.us', 'set', [
{
tag: 'create',
attrs: {
@@ -98,58 +103,41 @@ export const makeGroupsSocket = (config: SocketConfig) => {
attrs: { jid }
}))
}
]
)
])
return extractGroupMetadata(result)
},
groupLeave: async (id: string) => {
await groupQuery(
'@g.us',
'set',
[
await groupQuery('@g.us', 'set', [
{
tag: 'leave',
attrs: {},
content: [
{ tag: 'group', attrs: { id } }
]
content: [{ tag: 'group', attrs: { id } }]
}
]
)
])
},
groupUpdateSubject: async (jid: string, subject: string) => {
await groupQuery(
jid,
'set',
[
await groupQuery(jid, 'set', [
{
tag: 'subject',
attrs: {},
content: Buffer.from(subject, 'utf-8')
}
]
)
])
},
groupRequestParticipantsList: async (jid: string) => {
const result = await groupQuery(
jid,
'get',
[
const result = await groupQuery(jid, 'get', [
{
tag: 'membership_approval_requests',
attrs: {}
}
]
)
])
const node = getBinaryNodeChild(result, 'membership_approval_requests')
const participants = getBinaryNodeChildren(node, 'membership_approval_request')
return participants.map(v => v.attrs)
},
groupRequestParticipantsUpdate: async (jid: string, participants: string[], action: 'approve' | 'reject') => {
const result = await groupQuery(
jid,
'set',
[{
const result = await groupQuery(jid, 'set', [
{
tag: 'membership_requests_action',
attrs: {},
content: [
@@ -162,8 +150,8 @@ export const makeGroupsSocket = (config: SocketConfig) => {
}))
}
]
}]
)
}
])
const node = getBinaryNodeChild(result, 'membership_requests_action')
const nodeAction = getBinaryNodeChild(node, action)
const participantsAffected = getBinaryNodeChildren(nodeAction, 'participant')
@@ -171,15 +159,8 @@ export const makeGroupsSocket = (config: SocketConfig) => {
return { status: p.attrs.error || '200', jid: p.attrs.jid }
})
},
groupParticipantsUpdate: async(
jid: string,
participants: string[],
action: ParticipantAction
) => {
const result = await groupQuery(
jid,
'set',
[
groupParticipantsUpdate: async (jid: string, participants: string[], action: ParticipantAction) => {
const result = await groupQuery(jid, 'set', [
{
tag: action,
attrs: {},
@@ -188,8 +169,7 @@ export const makeGroupsSocket = (config: SocketConfig) => {
attrs: { jid }
}))
}
]
)
])
const node = getBinaryNodeChild(result, action)
const participantsAffected = getBinaryNodeChildren(node, 'participant')
return participantsAffected.map(p => {
@@ -200,22 +180,16 @@ export const makeGroupsSocket = (config: SocketConfig) => {
const metadata = await groupMetadata(jid)
const prev = metadata.descId ?? null
await groupQuery(
jid,
'set',
[
await groupQuery(jid, 'set', [
{
tag: 'description',
attrs: {
...(description ? { id: generateMessageIDV2() } : { delete: 'true' }),
...(prev ? { prev } : {})
},
content: description ? [
{ tag: 'body', attrs: {}, content: Buffer.from(description, 'utf-8') }
] : undefined
content: description ? [{ tag: 'body', attrs: {}, content: Buffer.from(description, 'utf-8') }] : undefined
}
]
)
])
},
groupInviteCode: async (jid: string) => {
const result = await groupQuery(jid, 'get', [{ tag: 'invite', attrs: {} }])
@@ -240,7 +214,9 @@ export const makeGroupsSocket = (config: SocketConfig) => {
* @returns true if successful
*/
groupRevokeInviteV4: async (groupJid: string, invitedJid: string) => {
const result = await groupQuery(groupJid, 'set', [{ tag: 'revoke', attrs: {}, content: [{ tag: 'participant', attrs: { jid: invitedJid } }] }])
const result = await groupQuery(groupJid, 'set', [
{ tag: 'revoke', attrs: {}, content: [{ tag: 'participant', attrs: { jid: invitedJid } }] }
])
return !!result
},
@@ -249,16 +225,19 @@ export const makeGroupsSocket = (config: SocketConfig) => {
* @param key the key of the invite message, or optionally only provide the jid of the person who sent the invite
* @param inviteMessage the message to accept
*/
groupAcceptInviteV4: ev.createBufferedFunction(async(key: string | WAMessageKey, inviteMessage: proto.Message.IGroupInviteMessage) => {
groupAcceptInviteV4: ev.createBufferedFunction(
async (key: string | WAMessageKey, inviteMessage: proto.Message.IGroupInviteMessage) => {
key = typeof key === 'string' ? { remoteJid: key } : key
const results = await groupQuery(inviteMessage.groupJid!, 'set', [{
const results = await groupQuery(inviteMessage.groupJid!, 'set', [
{
tag: 'accept',
attrs: {
code: inviteMessage.inviteCode!,
expiration: inviteMessage.inviteExpiration!.toString(),
admin: key.remoteJid!
}
}])
}
])
// if we have the full message key
// update the invite message to be expired
@@ -286,12 +265,10 @@ export const makeGroupsSocket = (config: SocketConfig) => {
remoteJid: inviteMessage.groupJid,
id: generateMessageIDV2(sock.user?.id),
fromMe: false,
participant: key.remoteJid,
participant: key.remoteJid
},
messageStubType: WAMessageStubType.GROUP_PARTICIPANT_ADD,
messageStubParameters: [
authState.creds.me!.id
],
messageStubParameters: [authState.creds.me!.id],
participant: key.remoteJid,
messageTimestamp: unixTimestampSeconds()
},
@@ -299,15 +276,16 @@ export const makeGroupsSocket = (config: SocketConfig) => {
)
return results.attrs.from
}),
}
),
groupGetInviteInfo: async (code: string) => {
const results = await groupQuery('@g.us', 'get', [{ tag: 'invite', attrs: { code } }])
return extractGroupMetadata(results)
},
groupToggleEphemeral: async (jid: string, ephemeralExpiration: number) => {
const content: BinaryNode = ephemeralExpiration ?
{ tag: 'ephemeral', attrs: { expiration: ephemeralExpiration.toString() } } :
{ tag: 'not_ephemeral', attrs: { } }
const content: BinaryNode = ephemeralExpiration
? { tag: 'ephemeral', attrs: { expiration: ephemeralExpiration.toString() } }
: { tag: 'not_ephemeral', attrs: {} }
await groupQuery(jid, 'set', [content])
},
groupSettingUpdate: async (jid: string, setting: 'announcement' | 'not_announcement' | 'locked' | 'unlocked') => {
@@ -317,13 +295,14 @@ export const makeGroupsSocket = (config: SocketConfig) => {
await groupQuery(jid, 'set', [{ tag: 'member_add_mode', attrs: {}, content: mode }])
},
groupJoinApprovalMode: async (jid: string, mode: 'on' | 'off') => {
await groupQuery(jid, 'set', [ { tag: 'membership_approval_mode', attrs: { }, content: [ { tag: 'group_join', attrs: { state: mode } } ] } ])
await groupQuery(jid, 'set', [
{ tag: 'membership_approval_mode', attrs: {}, content: [{ tag: 'group_join', attrs: { state: mode } }] }
])
},
groupFetchAllParticipating
}
}
export const extractGroupMetadata = (result: BinaryNode) => {
const group = getBinaryNodeChild(result, 'group')!
const descChild = getBinaryNodeChild(group, 'description')
@@ -355,14 +334,12 @@ export const extractGroupMetadata = (result: BinaryNode) => {
isCommunityAnnounce: !!getBinaryNodeChild(group, 'default_sub_group'),
joinApprovalMode: !!getBinaryNodeChild(group, 'membership_approval_mode'),
memberAddMode,
participants: getBinaryNodeChildren(group, 'participant').map(
({ attrs }) => {
participants: getBinaryNodeChildren(group, 'participant').map(({ attrs }) => {
return {
id: attrs.jid,
admin: (attrs.type || null) as GroupParticipant['admin'],
admin: (attrs.type || null) as GroupParticipant['admin']
}
}
),
}),
ephemeralDuration: eph ? +eph : undefined
}
return metadata

View File

@@ -3,11 +3,10 @@ import { UserFacingSocketConfig } from '../Types'
import { makeBusinessSocket } from './business'
// export the last socket layer
const makeWASocket = (config: UserFacingSocketConfig) => (
const makeWASocket = (config: UserFacingSocketConfig) =>
makeBusinessSocket({
...DEFAULT_CONNECTION_CONFIG,
...config
})
)
export default makeWASocket

View File

@@ -1,11 +1,20 @@
import NodeCache from '@cacheable/node-cache'
import { Boom } from '@hapi/boom'
import { randomBytes } from 'crypto'
import Long = require('long');
import Long = require('long')
import { proto } from '../../WAProto'
import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT } from '../Defaults'
import { MessageReceiptType, MessageRelayOptions, MessageUserReceipt, SocketConfig, WACallEvent, WAMessageKey, WAMessageStatus, WAMessageStubType, WAPatchName } from '../Types'
import {
MessageReceiptType,
MessageRelayOptions,
MessageUserReceipt,
SocketConfig,
WACallEvent,
WAMessageKey,
WAMessageStatus,
WAMessageStubType,
WAPatchName
} from '../Types'
import {
aesDecryptCTR,
aesEncryptGCM,
@@ -21,7 +30,8 @@ import {
getCallStatusFromNode,
getHistoryMsg,
getNextPreKeys,
getStatusFromReceiptType, hkdf,
getStatusFromReceiptType,
hkdf,
MISSING_KEYS_ERROR_TEXT,
NACK_REASONS,
NO_MESSAGE_FOUND_ERROR_TEXT,
@@ -37,7 +47,8 @@ import {
getBinaryNodeChild,
getBinaryNodeChildBuffer,
getBinaryNodeChildren,
isJidGroup, isJidStatusBroadcast,
isJidGroup,
isJidStatusBroadcast,
isJidUser,
jidDecode,
jidNormalizedUser,
@@ -47,13 +58,7 @@ import { extractGroupMetadata } from './groups'
import { makeMessagesSocket } from './messages-send'
export const makeMessagesRecvSocket = (config: SocketConfig) => {
const {
logger,
retryRequestDelayMs,
maxMsgRetryCount,
getMessage,
shouldIgnoreJid
} = config
const { logger, retryRequestDelayMs, maxMsgRetryCount, getMessage, shouldIgnoreJid } = config
const sock = makeMessagesSocket(config)
const {
ev,
@@ -70,22 +75,28 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
relayMessage,
sendReceipt,
uploadPreKeys,
sendPeerDataOperationMessage,
sendPeerDataOperationMessage
} = sock
/** this mutex ensures that each retryRequest will wait for the previous one to finish */
const retryMutex = makeMutex()
const msgRetryCache = config.msgRetryCounterCache || new NodeCache({
const msgRetryCache =
config.msgRetryCounterCache ||
new NodeCache({
stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
useClones: false
})
const callOfferCache = config.callOfferCache || new NodeCache({
const callOfferCache =
config.callOfferCache ||
new NodeCache({
stdTTL: DEFAULT_CACHE_TTLS.CALL_OFFER, // 5 mins
useClones: false
})
const placeholderResendCache = config.placeholderResendCache || new NodeCache({
const placeholderResendCache =
config.placeholderResendCache ||
new NodeCache({
stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, // 1 hour
useClones: false
})
@@ -114,7 +125,10 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
stanza.attrs.recipient = attrs.recipient
}
if(!!attrs.type && (tag !== 'message' || getBinaryNodeChild({ tag, attrs, content }, 'unavailable') || errorCode !== 0)) {
if (
!!attrs.type &&
(tag !== 'message' || getBinaryNodeChild({ tag, attrs, content }, 'unavailable') || errorCode !== 0)
) {
stanza.attrs.type = attrs.type
}
@@ -127,22 +141,24 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
}
const rejectCall = async (callId: string, callFrom: string) => {
const stanza: BinaryNode = ({
const stanza: BinaryNode = {
tag: 'call',
attrs: {
from: authState.creds.me!.id,
to: callFrom,
to: callFrom
},
content: [{
content: [
{
tag: 'reject',
attrs: {
'call-id': callId,
'call-creator': callFrom,
count: '0',
count: '0'
},
content: undefined,
}],
})
content: undefined
}
]
}
await query(stanza)
}
@@ -171,8 +187,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
}
const deviceIdentity = encodeSignedDeviceIdentity(account!, true)
await authState.keys.transaction(
async() => {
await authState.keys.transaction(async () => {
const receipt: BinaryNode = {
tag: 'receipt',
attrs: {
@@ -231,8 +246,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
await sendNode(receipt)
logger.info({ msgAttrs: node.attrs, retryCount }, 'sent retry receipt')
}
)
})
}
const handleEncryptNotification = async (node: BinaryNode) => {
@@ -258,11 +272,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
}
}
const handleGroupNotification = (
participant: string,
child: BinaryNode,
msg: Partial<proto.IWebMessageInfo>
) => {
const handleGroupNotification = (participant: string, child: BinaryNode, msg: Partial<proto.IWebMessageInfo>) => {
const participantJid = getBinaryNodeChild(child, 'participant')?.attrs?.jid || participant
switch (child?.tag) {
case 'create':
@@ -272,15 +282,19 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
msg.messageStubParameters = [metadata.subject]
msg.key = { participant: metadata.owner }
ev.emit('chats.upsert', [{
ev.emit('chats.upsert', [
{
id: metadata.id,
name: metadata.subject,
conversationTimestamp: metadata.creation,
}])
ev.emit('groups.upsert', [{
conversationTimestamp: metadata.creation
}
])
ev.emit('groups.upsert', [
{
...metadata,
author: participant
}])
}
])
break
case 'ephemeral':
case 'not_ephemeral':
@@ -329,12 +343,12 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
case 'announcement':
case 'not_announcement':
msg.messageStubType = WAMessageStubType.GROUP_CHANGE_ANNOUNCE
msg.messageStubParameters = [ (child.tag === 'announcement') ? 'on' : 'off' ]
msg.messageStubParameters = [child.tag === 'announcement' ? 'on' : 'off']
break
case 'locked':
case 'unlocked':
msg.messageStubType = WAMessageStubType.GROUP_CHANGE_RESTRICT
msg.messageStubParameters = [ (child.tag === 'locked') ? 'on' : 'off' ]
msg.messageStubParameters = [child.tag === 'locked' ? 'on' : 'off']
break
case 'invite':
msg.messageStubType = WAMessageStubType.GROUP_CHANGE_INVITE_LINK
@@ -420,10 +434,12 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const setPicture = getBinaryNodeChild(node, 'set')
const delPicture = getBinaryNodeChild(node, 'delete')
ev.emit('contacts.update', [{
id: jidNormalizedUser(node?.attrs?.from) || ((setPicture || delPicture)?.attrs?.hash) || '',
ev.emit('contacts.update', [
{
id: jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || '',
imgUrl: setPicture ? 'changed' : 'removed'
}])
}
])
if (isJidGroup(from)) {
const node = setPicture || delPicture
@@ -435,7 +451,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
result.participant = node?.attrs.author
result.key = {
...result.key || {},
...(result.key || {}),
participant: setPicture?.attrs.author
}
}
@@ -453,8 +469,8 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
...authState.creds.accountSettings,
defaultDisappearingMode: {
ephemeralExpiration: newDuration,
ephemeralSettingTimestamp: timestamp,
},
ephemeralSettingTimestamp: timestamp
}
}
})
} else if (child.tag === 'blocklist') {
@@ -462,7 +478,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
for (const { attrs } of blocklists) {
const blocklist = [attrs.jid]
const type = (attrs.action === 'block') ? 'add' : 'remove'
const type = attrs.action === 'block' ? 'add' : 'remove'
ev.emit('blocklist.update', { blocklist, type })
}
}
@@ -471,17 +487,28 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
case 'link_code_companion_reg':
const linkCodeCompanionReg = getBinaryNodeChild(node, 'link_code_companion_reg')
const ref = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'link_code_pairing_ref'))
const primaryIdentityPublicKey = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'primary_identity_pub'))
const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeCompanionReg, 'link_code_pairing_wrapped_primary_ephemeral_pub'))
const primaryIdentityPublicKey = toRequiredBuffer(
getBinaryNodeChildBuffer(linkCodeCompanionReg, 'primary_identity_pub')
)
const primaryEphemeralPublicKeyWrapped = toRequiredBuffer(
getBinaryNodeChildBuffer(linkCodeCompanionReg, 'link_code_pairing_wrapped_primary_ephemeral_pub')
)
const codePairingPublicKey = await decipherLinkPublicKey(primaryEphemeralPublicKeyWrapped)
const companionSharedKey = Curve.sharedKey(authState.creds.pairingEphemeralKeyPair.private, codePairingPublicKey)
const companionSharedKey = Curve.sharedKey(
authState.creds.pairingEphemeralKeyPair.private,
codePairingPublicKey
)
const random = randomBytes(32)
const linkCodeSalt = randomBytes(32)
const linkCodePairingExpanded = await hkdf(companionSharedKey, 32, {
salt: linkCodeSalt,
info: 'link_code_pairing_key_bundle_encryption_key'
})
const encryptPayload = Buffer.concat([Buffer.from(authState.creds.signedIdentityKey.public), primaryIdentityPublicKey, random])
const encryptPayload = Buffer.concat([
Buffer.from(authState.creds.signedIdentityKey.public),
primaryIdentityPublicKey,
random
])
const encryptIv = randomBytes(12)
const encrypted = aesEncryptGCM(encryptPayload, linkCodePairingExpanded, encryptIv, Buffer.alloc(0))
const encryptedPayload = Buffer.concat([linkCodeSalt, encryptIv, encrypted])
@@ -501,7 +528,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
tag: 'link_code_companion_reg',
attrs: {
jid: authState.creds.me!.id,
stage: 'companion_finish',
stage: 'companion_finish'
},
content: [
{
@@ -561,11 +588,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
msgRetryCache.set(key, newValue)
}
const sendMessagesAgain = async(
key: proto.IMessageKey,
ids: string[],
retryNode: BinaryNode
) => {
const sendMessagesAgain = async (key: proto.IMessageKey, ids: string[], retryNode: BinaryNode) => {
// todo: implement a cache to store the last 256 sent messages (copy whatsmeow)
const msgs = await Promise.all(ids.map(id => getMessage({ ...key, id })))
const remoteJid = key.remoteJid!
@@ -606,7 +629,10 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const handleReceipt = async (node: BinaryNode) => {
const { attrs, content } = node
const isLid = attrs.from.includes('lid')
const isNodeFromMe = areJidsSameUser(attrs.participant || attrs.from, isLid ? authState.creds.me?.lid : authState.creds.me?.id)
const isNodeFromMe = areJidsSameUser(
attrs.participant || attrs.from,
isLid ? authState.creds.me?.lid : authState.creds.me?.id
)
const remoteJid = !isNodeFromMe || isJidGroup(attrs.from) ? attrs.from : attrs.recipient
const fromMe = !attrs.recipient || (attrs.type === 'retry' && isNodeFromMe)
@@ -631,21 +657,18 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
try {
await Promise.all([
processingMutex.mutex(
async() => {
processingMutex.mutex(async () => {
const status = getStatusFromReceiptType(attrs.type)
if (
typeof status !== 'undefined' &&
(
// basically, we only want to know when a message from us has been delivered to/read by the other person
// or another device of ours has read some messages
status >= proto.WebMessageInfo.Status.SERVER_ACK ||
!isNodeFromMe
)
(status >= proto.WebMessageInfo.Status.SERVER_ACK || !isNodeFromMe)
) {
if (isJidGroup(remoteJid) || isJidStatusBroadcast(remoteJid)) {
if (attrs.participant) {
const updateKey: keyof MessageUserReceipt = status === proto.WebMessageInfo.Status.DELIVERY_ACK ? 'receiptTimestamp' : 'readTimestamp'
const updateKey: keyof MessageUserReceipt =
status === proto.WebMessageInfo.Status.DELIVERY_ACK ? 'receiptTimestamp' : 'readTimestamp'
ev.emit(
'message-receipt.update',
ids.map(id => ({
@@ -687,8 +710,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
logger.info({ attrs, key }, 'will not send message again, as sent too many times')
}
}
}
)
})
])
} finally {
await sendMessageAck(node)
@@ -705,8 +727,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
try {
await Promise.all([
processingMutex.mutex(
async() => {
processingMutex.mutex(async () => {
const msg = await processNotification(node)
if (msg) {
const fromMe = areJidsSameUser(node.attrs.participant || remoteJid, authState.creds.me!.id)
@@ -723,8 +744,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const fullMsg = proto.WebMessageInfo.fromObject(msg)
await upsertMessage(fullMsg, 'append')
}
}
)
})
])
} finally {
await sendMessageAck(node)
@@ -764,27 +784,27 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
}
}
const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(
node,
authState.creds.me!.id,
authState.creds.me!.lid || '',
signalRepository,
logger,
)
const {
fullMessage: msg,
category,
author,
decrypt
} = decryptMessageNode(node, authState.creds.me!.id, authState.creds.me!.lid || '', signalRepository, logger)
if (response && msg?.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
msg.messageStubParameters = [NO_MESSAGE_FOUND_ERROR_TEXT, response]
}
if(msg.message?.protocolMessage?.type === proto.Message.ProtocolMessage.Type.SHARE_PHONE_NUMBER && node.attrs.sender_pn) {
if (
msg.message?.protocolMessage?.type === proto.Message.ProtocolMessage.Type.SHARE_PHONE_NUMBER &&
node.attrs.sender_pn
) {
ev.emit('chats.phoneNumberShare', { lid: node.attrs.from, jid: node.attrs.sender_pn })
}
try {
await Promise.all([
processingMutex.mutex(
async() => {
processingMutex.mutex(async () => {
await decrypt()
// message failed to decrypt
if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT) {
@@ -792,8 +812,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
return sendMessageAck(node, NACK_REASONS.ParsingError)
}
retryMutex.mutex(
async() => {
retryMutex.mutex(async () => {
if (ws.isOpen) {
if (getBinaryNodeChild(node, 'unavailable')) {
return
@@ -807,15 +826,16 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
} else {
logger.debug({ node }, 'connection closed, ignoring retry req')
}
}
)
})
} else {
// no type in the receipt => message delivered
let type: MessageReceiptType = undefined
let participant = msg.key.participant
if(category === 'peer') { // special peer message
if (category === 'peer') {
// special peer message
type = 'peer_msg'
} else if(msg.key.fromMe) { // message was sent by us from a different device
} else if (msg.key.fromMe) {
// message was sent by us from a different device
type = 'sender'
// need to specially handle this case
if (isJidUser(msg.key.remoteJid!)) {
@@ -840,8 +860,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
await sendMessageAck(node)
await upsertMessage(msg, node.attrs.offline ? 'append' : 'notify')
}
)
})
])
} catch (error) {
logger.error({ error, node }, 'error in handling message')
@@ -891,9 +910,11 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
}
const pdoMessage = {
placeholderMessageResendRequest: [{
placeholderMessageResendRequest: [
{
messageKey
}],
}
],
peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND
}
@@ -919,7 +940,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
id: callId,
date: new Date(+attrs.t * 1000),
offline: !!attrs.offline,
status,
status
}
if (status === 'offer') {
@@ -968,20 +989,15 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
// device could not display the message
if (attrs.error) {
logger.warn({ attrs }, 'received error in ack')
ev.emit(
'messages.update',
[
ev.emit('messages.update', [
{
key,
update: {
status: WAMessageStatus.ERROR,
messageStubParameters: [
attrs.error
]
messageStubParameters: [attrs.error]
}
}
]
)
])
}
}
@@ -997,8 +1013,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
ev.flush()
function execTask() {
return exec(node, false)
.catch(err => onUnexpectedError(err, identifier))
return exec(node, false).catch(err => onUnexpectedError(err, identifier))
}
}
@@ -1035,10 +1050,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const nodeProcessor = nodeProcessorMap.get(type)
if (!nodeProcessor) {
onUnexpectedError(
new Error(`unknown offline node type: ${type}`),
'processing offline node'
)
onUnexpectedError(new Error(`unknown offline node type: ${type}`), 'processing offline node')
continue
}
@@ -1056,7 +1068,12 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const offlineNodeProcessor = makeOfflineNodeProcessor()
const processNode = (type: MessageType, node: BinaryNode, identifier: string, exec: (node: BinaryNode) => Promise<void>) => {
const processNode = (
type: MessageType,
node: BinaryNode,
identifier: string,
exec: (node: BinaryNode) => Promise<void>
) => {
const isOffline = !!node.attrs.offline
if (isOffline) {
@@ -1083,8 +1100,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
processNode('notification', node, 'handling notification', handleNotification)
})
ws.on('CB:ack,class:message', (node: BinaryNode) => {
handleBadAck(node)
.catch(error => onUnexpectedError(error, 'handling bad ack'))
handleBadAck(node).catch(error => onUnexpectedError(error, 'handling bad ack'))
})
ev.on('call', ([call]) => {
@@ -1096,11 +1112,13 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
id: call.id,
fromMe: false
},
messageTimestamp: unixTimestampSeconds(call.date),
messageTimestamp: unixTimestampSeconds(call.date)
}
if (call.status === 'timeout') {
if (call.isGroup) {
msg.messageStubType = call.isVideo ? WAMessageStubType.CALL_MISSED_GROUP_VIDEO : WAMessageStubType.CALL_MISSED_GROUP_VOICE
msg.messageStubType = call.isVideo
? WAMessageStubType.CALL_MISSED_GROUP_VIDEO
: WAMessageStubType.CALL_MISSED_GROUP_VOICE
} else {
msg.messageStubType = call.isVideo ? WAMessageStubType.CALL_MISSED_VIDEO : WAMessageStubType.CALL_MISSED_VOICE
}
@@ -1126,6 +1144,6 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
sendRetryRequest,
rejectCall,
fetchMessageHistory,
requestPlaceholderResend,
requestPlaceholderResend
}
}

View File

@@ -1,12 +1,49 @@
import NodeCache from '@cacheable/node-cache'
import { Boom } from '@hapi/boom'
import { proto } from '../../WAProto'
import { DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults'
import { AnyMessageContent, MediaConnInfo, MessageReceiptType, MessageRelayOptions, MiscMessageGenerationOptions, SocketConfig, WAMessageKey } from '../Types'
import { aggregateMessageKeysNotFromMe, assertMediaContent, bindWaitForEvent, decryptMediaRetryData, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2, generateWAMessage, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, normalizeMessageContent, parseAndInjectE2ESessions, unixTimestampSeconds } from '../Utils'
import {
AnyMessageContent,
MediaConnInfo,
MessageReceiptType,
MessageRelayOptions,
MiscMessageGenerationOptions,
SocketConfig,
WAMessageKey
} from '../Types'
import {
aggregateMessageKeysNotFromMe,
assertMediaContent,
bindWaitForEvent,
decryptMediaRetryData,
encodeSignedDeviceIdentity,
encodeWAMessage,
encryptMediaRetryRequest,
extractDeviceJids,
generateMessageIDV2,
generateWAMessage,
getStatusCodeForMediaRetry,
getUrlFromDirectPath,
getWAUploadToServer,
normalizeMessageContent,
parseAndInjectE2ESessions,
unixTimestampSeconds
} from '../Utils'
import { getUrlInfo } from '../Utils/link-preview'
import { areJidsSameUser, BinaryNode, BinaryNodeAttributes, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, isJidUser, jidDecode, jidEncode, jidNormalizedUser, JidWithDevice, S_WHATSAPP_NET } from '../WABinary'
import {
areJidsSameUser,
BinaryNode,
BinaryNodeAttributes,
getBinaryNodeChild,
getBinaryNodeChildren,
isJidGroup,
isJidUser,
jidDecode,
jidEncode,
jidNormalizedUser,
JidWithDevice,
S_WHATSAPP_NET
} from '../WABinary'
import { USyncQuery, USyncUser } from '../WAUSync'
import { makeGroupsSocket } from './groups'
@@ -17,7 +54,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
generateHighQualityLinkPreview,
options: axiosOptions,
patchMessageBeforeSending,
cachedGroupMetadata,
cachedGroupMetadata
} = config
const sock = makeGroupsSocket(config)
const {
@@ -30,10 +67,12 @@ export const makeMessagesSocket = (config: SocketConfig) => {
fetchPrivacySettings,
sendNode,
groupMetadata,
groupToggleEphemeral,
groupToggleEphemeral
} = sock
const userDevicesCache = config.userDevicesCache || new NodeCache({
const userDevicesCache =
config.userDevicesCache ||
new NodeCache({
stdTTL: DEFAULT_CACHE_TTLS.USER_DEVICES, // 5 minutes
useClones: false
})
@@ -41,25 +80,23 @@ export const makeMessagesSocket = (config: SocketConfig) => {
let mediaConn: Promise<MediaConnInfo>
const refreshMediaConn = async (forceGet = false) => {
const media = await mediaConn
if(!media || forceGet || (new Date().getTime() - media.fetchDate.getTime()) > media.ttl * 1000) {
if (!media || forceGet || new Date().getTime() - media.fetchDate.getTime() > media.ttl * 1000) {
mediaConn = (async () => {
const result = await query({
tag: 'iq',
attrs: {
type: 'set',
xmlns: 'w:m',
to: S_WHATSAPP_NET,
to: S_WHATSAPP_NET
},
content: [{ tag: 'media_conn', attrs: {} }]
})
const mediaConnNode = getBinaryNodeChild(result, 'media_conn')
const node: MediaConnInfo = {
hosts: getBinaryNodeChildren(mediaConnNode, 'host').map(
({ attrs }) => ({
hosts: getBinaryNodeChildren(mediaConnNode, 'host').map(({ attrs }) => ({
hostname: attrs.hostname,
maxContentLengthBytes: +attrs.maxContentLengthBytes,
})
),
maxContentLengthBytes: +attrs.maxContentLengthBytes
})),
auth: mediaConnNode!.attrs.auth,
ttl: +mediaConnNode!.attrs.ttl,
fetchDate: new Date()
@@ -76,12 +113,17 @@ export const makeMessagesSocket = (config: SocketConfig) => {
* generic send receipt function
* used for receipts of phone call, read, delivery etc.
* */
const sendReceipt = async(jid: string, participant: string | undefined, messageIds: string[], type: MessageReceiptType) => {
const sendReceipt = async (
jid: string,
participant: string | undefined,
messageIds: string[],
type: MessageReceiptType
) => {
const node: BinaryNode = {
tag: 'receipt',
attrs: {
id: messageIds[0],
},
id: messageIds[0]
}
}
const isReadReceipt = type === 'read' || type === 'read-self'
if (isReadReceipt) {
@@ -168,9 +210,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
return deviceResults
}
const query = new USyncQuery()
.withContext('message')
.withDeviceProtocol()
const query = new USyncQuery().withContext('message').withDeviceProtocol()
for (const jid of toFetch) {
query.withUser(new USyncUser().withId(jid))
@@ -203,14 +243,10 @@ export const makeMessagesSocket = (config: SocketConfig) => {
if (force) {
jidsRequiringFetch = jids
} else {
const addrs = jids.map(jid => (
signalRepository
.jidToSignalProtocolAddress(jid)
))
const addrs = jids.map(jid => signalRepository.jidToSignalProtocolAddress(jid))
const sessions = await authState.keys.get('session', addrs)
for (const jid of jids) {
const signalId = signalRepository
.jidToSignalProtocolAddress(jid)
const signalId = signalRepository.jidToSignalProtocolAddress(jid)
if (!sessions[signalId]) {
jidsRequiringFetch.push(jid)
}
@@ -224,18 +260,16 @@ export const makeMessagesSocket = (config: SocketConfig) => {
attrs: {
xmlns: 'encrypt',
type: 'get',
to: S_WHATSAPP_NET,
to: S_WHATSAPP_NET
},
content: [
{
tag: 'key',
attrs: {},
content: jidsRequiringFetch.map(
jid => ({
content: jidsRequiringFetch.map(jid => ({
tag: 'user',
attrs: { jid },
})
)
attrs: { jid }
}))
}
]
})
@@ -268,18 +302,14 @@ export const makeMessagesSocket = (config: SocketConfig) => {
additionalAttributes: {
category: 'peer',
// eslint-disable-next-line camelcase
push_priority: 'high_force',
},
push_priority: 'high_force'
}
})
return msgId
}
const createParticipantNodes = async(
jids: string[],
message: proto.IMessage,
extraAttrs?: BinaryNode['attrs']
) => {
const createParticipantNodes = async (jids: string[], message: proto.IMessage, extraAttrs?: BinaryNode['attrs']) => {
let patched = await patchMessageBeforeSending(message, jids)
if (!Array.isArray(patched)) {
patched = jids ? jids.map(jid => ({ recipientJid: jid, ...patched })) : [patched]
@@ -288,16 +318,14 @@ export const makeMessagesSocket = (config: SocketConfig) => {
let shouldIncludeDeviceIdentity = false
const nodes = await Promise.all(
patched.map(
async patchedMessageWithJid => {
patched.map(async patchedMessageWithJid => {
const { recipientJid: jid, ...patchedMessage } = patchedMessageWithJid
if (!jid) {
return {} as BinaryNode
}
const bytes = encodeWAMessage(patchedMessage)
const { type, ciphertext } = await signalRepository
.encryptMessage({ jid, data: bytes })
const { type, ciphertext } = await signalRepository.encryptMessage({ jid, data: bytes })
if (type === 'pkmsg') {
shouldIncludeDeviceIdentity = true
}
@@ -305,19 +333,20 @@ export const makeMessagesSocket = (config: SocketConfig) => {
const node: BinaryNode = {
tag: 'to',
attrs: { jid },
content: [{
content: [
{
tag: 'enc',
attrs: {
v: '2',
type,
...extraAttrs || {}
...(extraAttrs || {})
},
content: ciphertext
}]
}
]
}
return node
}
)
})
)
return { nodes, shouldIncludeDeviceIdentity }
}
@@ -325,7 +354,15 @@ export const makeMessagesSocket = (config: SocketConfig) => {
const relayMessage = async (
jid: string,
message: proto.IMessage,
{ messageId: msgId, participant, additionalAttributes, additionalNodes, useUserDevicesCache, useCachedGroupMetadata, statusJidList }: MessageRelayOptions
{
messageId: msgId,
participant,
additionalAttributes,
additionalNodes,
useUserDevicesCache,
useCachedGroupMetadata,
statusJidList
}: MessageRelayOptions
) => {
const meId = authState.creds.me!.id
@@ -342,7 +379,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
useCachedGroupMetadata = useCachedGroupMetadata !== false && !isStatus
const participants: BinaryNode[] = []
const destinationJid = (!isStatus) ? jidEncode(user, isLid ? 'lid' : isGroup ? 'g.us' : 's.whatsapp.net') : statusJid
const destinationJid = !isStatus ? jidEncode(user, isLid ? 'lid' : isGroup ? 'g.us' : 's.whatsapp.net') : statusJid
const binaryNodeContent: BinaryNode[] = []
const devices: JidWithDevice[] = []
@@ -360,15 +397,14 @@ export const makeMessagesSocket = (config: SocketConfig) => {
// only send to the specific device that asked for a retry
// otherwise the message is sent out to every device that should be a recipient
if (!isGroup && !isStatus) {
additionalAttributes = { ...additionalAttributes, 'device_fanout': 'false' }
additionalAttributes = { ...additionalAttributes, device_fanout: 'false' }
}
const { user, device } = jidDecode(participant.jid)!
devices.push({ user, device })
}
await authState.keys.transaction(
async() => {
await authState.keys.transaction(async () => {
const mediaType = getMediaType(message)
if (mediaType) {
extraAttrs['mediatype'] = mediaType
@@ -401,7 +437,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
])
if (!participant) {
const participantsList = (groupData && !isStatus) ? groupData.participants.map(p => p.id) : []
const participantsList = groupData && !isStatus ? groupData.participants.map(p => p.id) : []
if (isStatus && statusJidList) {
participantsList.push(...statusJidList)
}
@@ -425,13 +461,11 @@ export const makeMessagesSocket = (config: SocketConfig) => {
const bytes = encodeWAMessage(patched)
const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage(
{
const { ciphertext, senderKeyDistributionMessage } = await signalRepository.encryptGroupMessage({
group: destinationJid,
data: bytes,
meId,
}
)
meId
})
const senderKeyJids: string[] = []
// ensure a connection is established with every device
@@ -491,7 +525,11 @@ export const makeMessagesSocket = (config: SocketConfig) => {
const otherJids: string[] = []
for (const { user, device } of devices) {
const isMe = user === meUser
const jid = jidEncode(isMe && isLid ? authState.creds?.me?.lid!.split(':')[0] || user : user, isLid ? 'lid' : 's.whatsapp.net', device)
const jid = jidEncode(
isMe && isLid ? authState.creds?.me?.lid!.split(':')[0] || user : user,
isLid ? 'lid' : 's.whatsapp.net',
device
)
if (isMe) {
meJids.push(jid)
} else {
@@ -558,7 +596,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
}
if (shouldIncludeDeviceIdentity) {
(stanza.content as BinaryNode[]).push({
;(stanza.content as BinaryNode[]).push({
tag: 'device-identity',
attrs: {},
content: encodeSignedDeviceIdentity(authState.creds.account!, true)
@@ -568,19 +606,17 @@ export const makeMessagesSocket = (config: SocketConfig) => {
}
if (additionalNodes && additionalNodes.length > 0) {
(stanza.content as BinaryNode[]).push(...additionalNodes)
;(stanza.content as BinaryNode[]).push(...additionalNodes)
}
logger.debug({ msgId }, `sending message to ${participants.length} devices`)
await sendNode(stanza)
}
)
})
return msgId
}
const getMessageType = (message: proto.IMessage) => {
if (message.pollCreationMessage || message.pollCreationMessageV2 || message.pollCreationMessageV3) {
return 'poll'
@@ -636,16 +672,14 @@ export const makeMessagesSocket = (config: SocketConfig) => {
{
tag: 'tokens',
attrs: {},
content: jids.map(
jid => ({
content: jids.map(jid => ({
tag: 'token',
attrs: {
jid: jidNormalizedUser(jid),
t,
type: 'trusted_contact'
}
})
)
}))
}
]
})
@@ -678,10 +712,9 @@ export const makeMessagesSocket = (config: SocketConfig) => {
const node = await encryptMediaRetryRequest(message.key, mediaKey, meId)
let error: Error | undefined = undefined
await Promise.all(
[
await Promise.all([
sendNode(node),
waitForMsgMediaUpdate(async(update) => {
waitForMsgMediaUpdate(async update => {
const result = update.find(c => c.key.id === message.key.id)
if (result) {
if (result.error) {
@@ -691,10 +724,10 @@ export const makeMessagesSocket = (config: SocketConfig) => {
const media = await decryptMediaRetryData(result.media!, mediaKey, result.key.id!)
if (media.result !== proto.MediaRetryNotification.ResultType.SUCCESS) {
const resultStr = proto.MediaRetryNotification.ResultType[media.result!]
throw new Boom(
`Media re-upload failed by device (${resultStr})`,
{ data: media, statusCode: getStatusCodeForMediaRetry(media.result!) || 404 }
)
throw new Boom(`Media re-upload failed by device (${resultStr})`, {
data: media,
statusCode: getStatusCodeForMediaRetry(media.result!) || 404
})
}
content.directPath = media.directPath
@@ -709,24 +742,17 @@ export const makeMessagesSocket = (config: SocketConfig) => {
return true
}
})
]
)
])
if (error) {
throw error
}
ev.emit('messages.update', [
{ key: message.key, update: { message: message.message } }
])
ev.emit('messages.update', [{ key: message.key, update: { message: message.message } }])
return message
},
sendMessage: async(
jid: string,
content: AnyMessageContent,
options: MiscMessageGenerationOptions = { }
) => {
sendMessage: async (jid: string, content: AnyMessageContent, options: MiscMessageGenerationOptions = {}) => {
const userJid = authState.creds.me!.id
if (
typeof content === 'object' &&
@@ -735,40 +761,35 @@ export const makeMessagesSocket = (config: SocketConfig) => {
isJidGroup(jid)
) {
const { disappearingMessagesInChat } = content
const value = typeof disappearingMessagesInChat === 'boolean' ?
(disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) :
disappearingMessagesInChat
const value =
typeof disappearingMessagesInChat === 'boolean'
? disappearingMessagesInChat
? WA_DEFAULT_EPHEMERAL
: 0
: disappearingMessagesInChat
await groupToggleEphemeral(jid, value)
} else {
const fullMsg = await generateWAMessage(
jid,
content,
{
const fullMsg = await generateWAMessage(jid, content, {
logger,
userJid,
getUrlInfo: text => getUrlInfo(
text,
{
getUrlInfo: text =>
getUrlInfo(text, {
thumbnailWidth: linkPreviewImageThumbnailWidth,
fetchOpts: {
timeout: 3_000,
...axiosOptions || { }
...(axiosOptions || {})
},
logger,
uploadImage: generateHighQualityLinkPreview
? waUploadToServer
: undefined
},
),
uploadImage: generateHighQualityLinkPreview ? waUploadToServer : undefined
}),
//TODO: CACHE
getProfilePicUrl: sock.profilePictureUrl,
upload: waUploadToServer,
mediaCache: config.mediaCache,
options: config.options,
messageId: generateMessageIDV2(sock.user?.id),
...options,
}
)
...options
})
const isDeleteMsg = 'delete' in content && !!content.delete
const isEditMsg = 'edit' in content && !!content.edit
const isPinMsg = 'pin' in content && !!content.pin
@@ -792,20 +813,26 @@ export const makeMessagesSocket = (config: SocketConfig) => {
tag: 'meta',
attrs: {
polltype: 'creation'
},
}
} as BinaryNode)
}
if ('cachedGroupMetadata' in options) {
console.warn('cachedGroupMetadata in sendMessage are deprecated, now cachedGroupMetadata is part of the socket config.')
console.warn(
'cachedGroupMetadata in sendMessage are deprecated, now cachedGroupMetadata is part of the socket config.'
)
}
await relayMessage(jid, fullMsg.message!, { messageId: fullMsg.key.id!, useCachedGroupMetadata: options.useCachedGroupMetadata, additionalAttributes, statusJidList: options.statusJidList, additionalNodes })
await relayMessage(jid, fullMsg.message!, {
messageId: fullMsg.key.id!,
useCachedGroupMetadata: options.useCachedGroupMetadata,
additionalAttributes,
statusJidList: options.statusJidList,
additionalNodes
})
if (config.emitOwnEvents) {
process.nextTick(() => {
processingMutex.mutex(() => (
upsertMessage(fullMsg, 'append')
))
processingMutex.mutex(() => upsertMessage(fullMsg, 'append'))
})
}

View File

@@ -28,7 +28,7 @@ import {
getPlatformId,
makeEventBuffer,
makeNoiseHandler,
promiseTimeout,
promiseTimeout
} from '../Utils'
import {
assertNodeErrorFree,
@@ -61,16 +61,17 @@ export const makeSocket = (config: SocketConfig) => {
defaultQueryTimeoutMs,
transactionOpts,
qrTimeout,
makeSignalRepository,
makeSignalRepository
} = config
if (printQRInTerminal) {
console.warn('⚠️ The printQRInTerminal option has been deprecated. You will no longer receive QR codes in the terminal automatically. Please listen to the connection.update event yourself and handle the QR your way. You can remove this message by removing this opttion. This message will be removed in a future version.')
console.warn(
'⚠️ The printQRInTerminal option has been deprecated. You will no longer receive QR codes in the terminal automatically. Please listen to the connection.update event yourself and handle the QR your way. You can remove this message by removing this opttion. This message will be removed in a future version.'
)
}
const url = typeof waWebSocketUrl === 'string' ? new URL(waWebSocketUrl) : waWebSocketUrl
if (config.mobile || url.protocol === 'tcp:') {
throw new Boom('Mobile API is not supported anymore', { statusCode: DisconnectReason.loggedOut })
}
@@ -116,17 +117,14 @@ export const makeSocket = (config: SocketConfig) => {
}
const bytes = noise.encodeFrame(data)
await promiseTimeout<void>(
connectTimeoutMs,
async(resolve, reject) => {
await promiseTimeout<void>(connectTimeoutMs, async (resolve, reject) => {
try {
await sendPromise.call(ws, bytes)
resolve()
} catch (error) {
reject(error)
}
}
)
})
}
/** send a binary node */
@@ -141,10 +139,7 @@ export const makeSocket = (config: SocketConfig) => {
/** log & process any unexpected errors */
const onUnexpectedError = (err: Error | Boom, msg: string) => {
logger.error(
{ err },
`unexpected error in '${msg}'`
)
logger.error({ err }, `unexpected error in '${msg}'`)
}
/** await the next incoming message */
@@ -164,8 +159,7 @@ export const makeSocket = (config: SocketConfig) => {
ws.on('frame', onOpen)
ws.on('close', onClose)
ws.on('error', onClose)
})
.finally(() => {
}).finally(() => {
ws.off('frame', onOpen)
ws.off('close', onClose)
ws.off('error', onClose)
@@ -187,8 +181,7 @@ export const makeSocket = (config: SocketConfig) => {
let onRecv: (json) => void
let onErr: (err) => void
try {
const result = await promiseTimeout<T>(timeoutMs,
(resolve, reject) => {
const result = await promiseTimeout<T>(timeoutMs, (resolve, reject) => {
onRecv = resolve
onErr = err => {
reject(err || new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }))
@@ -197,8 +190,7 @@ export const makeSocket = (config: SocketConfig) => {
ws.on(`TAG:${msgId}`, onRecv)
ws.on('close', onErr) // if the socket closes, you'll never receive the message
ws.off('error', onErr)
},
)
})
return result as any
} finally {
@@ -216,10 +208,7 @@ export const makeSocket = (config: SocketConfig) => {
const msgId = node.attrs.id
const [result] = await Promise.all([
waitForMessage(msgId, timeoutMs),
sendNode(node)
])
const [result] = await Promise.all([waitForMessage(msgId, timeoutMs), sendNode(node)])
if ('tag' in result) {
assertNodeErrorFree(result)
@@ -255,15 +244,13 @@ export const makeSocket = (config: SocketConfig) => {
logger.info({ node }, 'logging in...')
}
const payloadEnc = noise.encrypt(
proto.ClientPayload.encode(node).finish()
)
const payloadEnc = noise.encrypt(proto.ClientPayload.encode(node).finish())
await sendRawMessage(
proto.HandshakeMessage.encode({
clientFinish: {
static: keyEnc,
payload: payloadEnc,
},
payload: payloadEnc
}
}).finish()
)
noise.finishInit()
@@ -279,9 +266,7 @@ export const makeSocket = (config: SocketConfig) => {
type: 'get',
to: S_WHATSAPP_NET
},
content: [
{ tag: 'count', attrs: {} }
]
content: [{ tag: 'count', attrs: {} }]
})
const countChild = getBinaryNodeChild(result, 'count')
return +countChild!.attrs.value
@@ -289,8 +274,7 @@ export const makeSocket = (config: SocketConfig) => {
/** generates and uploads a set of pre-keys to the server */
const uploadPreKeys = async (count = INITIAL_PREKEY_COUNT) => {
await keys.transaction(
async() => {
await keys.transaction(async () => {
logger.info({ count }, 'uploading pre-keys')
const { update, node } = await getNextPreKeysNode({ creds, keys }, count)
@@ -298,8 +282,7 @@ export const makeSocket = (config: SocketConfig) => {
ev.emit('creds.update', update)
logger.info({ count }, 'uploaded pre-keys')
}
)
})
}
const uploadPreKeysToServerIfRequired = async () => {
@@ -356,10 +339,7 @@ export const makeSocket = (config: SocketConfig) => {
}
closed = true
logger.info(
{ trace: error?.stack },
error ? 'connection errored' : 'connection closed'
)
logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed')
clearInterval(keepAliveReq)
clearTimeout(qrTimer)
@@ -402,16 +382,15 @@ export const makeSocket = (config: SocketConfig) => {
ws.on('open', onOpen)
ws.on('close', onClose)
ws.on('error', onClose)
})
.finally(() => {
}).finally(() => {
ws.off('open', onOpen)
ws.off('close', onClose)
ws.off('error', onClose)
})
}
const startKeepAliveRequest = () => (
keepAliveReq = setInterval(() => {
const startKeepAliveRequest = () =>
(keepAliveReq = setInterval(() => {
if (!lastDateRecv) {
lastDateRecv = new Date()
}
@@ -425,40 +404,33 @@ export const makeSocket = (config: SocketConfig) => {
end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }))
} else if (ws.isOpen) {
// if its all good, send a keep alive request
query(
{
query({
tag: 'iq',
attrs: {
id: generateMessageTag(),
to: S_WHATSAPP_NET,
type: 'get',
xmlns: 'w:p',
xmlns: 'w:p'
},
content: [{ tag: 'ping', attrs: {} }]
}
)
.catch(err => {
}).catch(err => {
logger.error({ trace: err.stack }, 'error in sending keep alive')
})
} else {
logger.warn('keep alive called when WS not open')
}
}, keepAliveIntervalMs)
)
}, keepAliveIntervalMs))
/** i have no idea why this exists. pls enlighten me */
const sendPassiveIq = (tag: 'passive' | 'active') => (
const sendPassiveIq = (tag: 'passive' | 'active') =>
query({
tag: 'iq',
attrs: {
to: S_WHATSAPP_NET,
xmlns: 'passive',
type: 'set',
type: 'set'
},
content: [
{ tag, attrs: {} }
]
content: [{ tag, attrs: {} }]
})
)
/** logout & invalidate connection */
const logout = async (msg?: string) => {
@@ -583,7 +555,9 @@ export const makeSocket = (config: SocketConfig) => {
ws.on('error', mapWebSocketError(end))
ws.on('close', () => end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })))
// the server terminated the connection
ws.on('CB:xmlstreamend', () => end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed })))
ws.on('CB:xmlstreamend', () =>
end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed }))
)
// QR gen
ws.on('CB:iq,type:set,pair-device', async (stanza: BinaryNode) => {
const iq: BinaryNode = {
@@ -591,7 +565,7 @@ export const makeSocket = (config: SocketConfig) => {
attrs: {
to: S_WHATSAPP_NET,
type: 'result',
id: stanza.attrs.id,
id: stanza.attrs.id
}
}
await sendNode(iq)
@@ -729,8 +703,7 @@ export const makeSocket = (config: SocketConfig) => {
sendNode({
tag: 'presence',
attrs: { name: name! }
})
.catch(err => {
}).catch(err => {
logger.warn({ trace: err.stack }, 'error in sending presence update on name change')
})
}
@@ -738,7 +711,6 @@ export const makeSocket = (config: SocketConfig) => {
Object.assign(creds, update)
})
return {
type: 'md' as 'md',
ws,
@@ -762,7 +734,7 @@ export const makeSocket = (config: SocketConfig) => {
requestPairingCode,
/** Waits for the connection to WA to reach a state */
waitForConnectionUpdate: bindWaitForConnectionUpdate(ev),
sendWAMBuffer,
sendWAMBuffer
}
}
@@ -772,11 +744,6 @@ export const makeSocket = (config: SocketConfig) => {
* */
function mapWebSocketError(handler: (err: Error) => void) {
return (error: Error) => {
handler(
new Boom(
`WebSocket Error (${error?.message})`,
{ statusCode: getCodeFromWSError(error), data: error }
)
)
handler(new Boom(`WebSocket Error (${error?.message})`, { statusCode: getCodeFromWSError(error), data: error }))
}
}

View File

@@ -7,10 +7,7 @@ import { makeSocket } from './socket'
export const makeUSyncSocket = (config: SocketConfig) => {
const sock = makeSocket(config)
const {
generateMessageTag,
query,
} = sock
const { generateMessageTag, query } = sock
const executeUSyncQuery = async (usyncQuery: USyncQuery) => {
if (usyncQuery.protocols.length === 0) {
@@ -21,15 +18,13 @@ export const makeUSyncSocket = (config: SocketConfig) => {
// variable below has only validated users
const validUsers = usyncQuery.users
const userNodes = validUsers.map((user) => {
const userNodes = validUsers.map(user => {
return {
tag: 'user',
attrs: {
jid: !user.phone ? user.id : undefined,
jid: !user.phone ? user.id : undefined
},
content: usyncQuery.protocols
.map((a) => a.getUserElement(user))
.filter(a => a !== null)
content: usyncQuery.protocols.map(a => a.getUserElement(user)).filter(a => a !== null)
} as BinaryNode
})
@@ -42,14 +37,14 @@ export const makeUSyncSocket = (config: SocketConfig) => {
const queryNode: BinaryNode = {
tag: 'query',
attrs: {},
content: usyncQuery.protocols.map((a) => a.getQueryElement())
content: usyncQuery.protocols.map(a => a.getQueryElement())
}
const iq = {
tag: 'iq',
attrs: {
to: S_WHATSAPP_NET,
type: 'get',
xmlns: 'usync',
xmlns: 'usync'
},
content: [
{
@@ -59,14 +54,11 @@ export const makeUSyncSocket = (config: SocketConfig) => {
mode: usyncQuery.mode,
sid: generateMessageTag(),
last: 'true',
index: '0',
index: '0'
},
content: [
queryNode,
listNode
]
content: [queryNode, listNode]
}
],
]
}
const result = await query(iq)
@@ -76,6 +68,6 @@ export const makeUSyncSocket = (config: SocketConfig) => {
return {
...sock,
executeUSyncQuery,
executeUSyncQuery
}
}

View File

@@ -4,7 +4,6 @@ import { processSyncAction } from '../Utils/chat-utils'
import logger from '../Utils/logger'
describe('App State Sync Tests', () => {
const me: Contact = { id: randomJid() }
// case when initial sync is off
it('should return archive=false event', () => {
@@ -129,7 +128,7 @@ describe('App State Sync Tests', () => {
}
}
}
],
]
]
const ctx: InitialAppStateSyncOptions = {
@@ -152,7 +151,7 @@ describe('App State Sync Tests', () => {
const index = ['archive', jid]
const now = unixTimestampSeconds()
const CASES: { settings: AccountSettings, mutations: ChatMutation[] }[] = [
const CASES: { settings: AccountSettings; mutations: ChatMutation[] }[] = [
{
settings: { unarchiveChats: true },
mutations: [
@@ -169,7 +168,7 @@ describe('App State Sync Tests', () => {
}
}
}
],
]
},
{
settings: { unarchiveChats: false },
@@ -187,7 +186,7 @@ describe('App State Sync Tests', () => {
}
}
}
],
]
}
]

View File

@@ -5,7 +5,6 @@ import logger from '../Utils/logger'
import { randomJid } from './utils'
describe('Event Buffer Tests', () => {
let ev: ReturnType<typeof makeEventBuffer>
beforeEach(() => {
const _logger = logger.child({})
@@ -93,7 +92,8 @@ describe('Event Buffer Tests', () => {
ev.on('chats.update', () => fail('not should have emitted'))
ev.buffer()
ev.emit('chats.update', [{
ev.emit('chats.update', [
{
id: chatId,
archived: true,
conditional(buff) {
@@ -101,8 +101,10 @@ describe('Event Buffer Tests', () => {
return true
}
}
}])
ev.emit('chats.update', [{
}
])
ev.emit('chats.update', [
{
id: chatId2,
archived: true,
conditional(buff) {
@@ -110,24 +112,29 @@ describe('Event Buffer Tests', () => {
return true
}
}
}])
}
])
ev.flush()
ev.buffer()
ev.emit('chats.upsert', [{
ev.emit('chats.upsert', [
{
id: chatId,
conversationTimestamp: 123,
unreadCount: 1,
muteEndTime: 123
}])
}
])
ev.emit('messaging-history.set', {
chats: [{
chats: [
{
id: chatId2,
conversationTimestamp: 123,
unreadCount: 1,
muteEndTime: 123
}],
}
],
contacts: [],
messages: [],
isLatest: false
@@ -152,7 +159,8 @@ describe('Event Buffer Tests', () => {
ev.on('chats.update', () => fail('not should have emitted'))
ev.buffer()
ev.emit('chats.update', [{
ev.emit('chats.update', [
{
id: chatId,
archived: true,
conditional(buff) {
@@ -160,13 +168,16 @@ describe('Event Buffer Tests', () => {
return false
}
}
}])
ev.emit('chats.upsert', [{
}
])
ev.emit('chats.upsert', [
{
id: chatId,
conversationTimestamp: 123,
unreadCount: 1,
muteEndTime: 123
}])
}
])
ev.flush()

View File

@@ -5,24 +5,18 @@ import { makeMockSignalKeyStore } from './utils'
logger.level = 'trace'
describe('Key Store w Transaction Tests', () => {
const rawStore = makeMockSignalKeyStore()
const store = addTransactionCapability(
rawStore,
logger,
{
const store = addTransactionCapability(rawStore, logger, {
maxCommitRetries: 1,
delayBetweenTriesMs: 10
}
)
})
it('should use transaction cache when mutated', async () => {
const key = '123'
const value = new Uint8Array(1)
const ogGet = rawStore.get
await store.transaction(
async() => {
await store.set({ 'session': { [key]: value } })
await store.transaction(async () => {
await store.set({ session: { [key]: value } })
rawStore.get = () => {
throw new Error('should not have been called')
@@ -30,8 +24,7 @@ describe('Key Store w Transaction Tests', () => {
const { [key]: stored } = await store.get('session', [key])
expect(stored).toEqual(new Uint8Array(1))
}
)
})
rawStore.get = ogGet
})
@@ -39,15 +32,11 @@ describe('Key Store w Transaction Tests', () => {
it('should not commit a failed transaction', async () => {
const key = 'abcd'
await expect(
store.transaction(
async() => {
await store.set({ 'session': { [key]: new Uint8Array(1) } })
store.transaction(async () => {
await store.set({ session: { [key]: new Uint8Array(1) } })
throw new Error('fail')
}
)
).rejects.toThrowError(
'fail'
)
})
).rejects.toThrowError('fail')
const { [key]: stored } = await store.get('session', [key])
expect(stored).toBeUndefined()
@@ -61,10 +50,9 @@ describe('Key Store w Transaction Tests', () => {
promiseResolve = resolve
})
store.transaction(
async() => {
store.transaction(async () => {
await store.set({
'session': {
session: {
'1': new Uint8Array(1)
}
})
@@ -72,17 +60,14 @@ describe('Key Store w Transaction Tests', () => {
await delay(5)
// reolve the promise to let the other transaction continue
promiseResolve()
}
)
})
await store.transaction(
async() => {
await store.transaction(async () => {
await promise
await delay(5)
expect(store.isInTransaction()).toBe(true)
}
)
})
expect(store.isInTransaction()).toBe(false)
// ensure that the transaction were committed

View File

@@ -3,7 +3,6 @@ import { SignalAuthState, SignalDataTypeMap } from '../Types'
import { Curve, generateRegistrationId, generateSignalPubKey, signedKeyPair } from '../Utils'
describe('Signal Tests', () => {
it('should correctly encrypt/decrypt 1 message', async () => {
const user1 = makeUser()
const user2 = makeUser()
@@ -12,13 +11,9 @@ describe('Signal Tests', () => {
await prepareForSendingMessage(user1, user2)
const result = await user1.repository.encryptMessage(
{ jid: user2.jid, data: msg }
)
const result = await user1.repository.encryptMessage({ jid: user2.jid, data: msg })
const dec = await user2.repository.decryptMessage(
{ jid: user1.jid, ...result }
)
const dec = await user2.repository.decryptMessage({ jid: user1.jid, ...result })
expect(dec).toEqual(msg)
})
@@ -32,13 +27,9 @@ describe('Signal Tests', () => {
for (let preKeyId = 2; preKeyId <= 3; preKeyId++) {
await prepareForSendingMessage(user1, user2, preKeyId)
const result = await user1.repository.encryptMessage(
{ jid: user2.jid, data: msg }
)
const result = await user1.repository.encryptMessage({ jid: user2.jid, data: msg })
const dec = await user2.repository.decryptMessage(
{ jid: user1.jid, ...result }
)
const dec = await user2.repository.decryptMessage({ jid: user1.jid, ...result })
expect(dec).toEqual(msg)
}
@@ -53,13 +44,9 @@ describe('Signal Tests', () => {
await prepareForSendingMessage(user1, user2)
for (let i = 0; i < 10; i++) {
const result = await user1.repository.encryptMessage(
{ jid: user2.jid, data: msg }
)
const result = await user1.repository.encryptMessage({ jid: user2.jid, data: msg })
const dec = await user2.repository.decryptMessage(
{ jid: user1.jid, ...result }
)
const dec = await user2.repository.decryptMessage({ jid: user1.jid, ...result })
expect(dec).toEqual(msg)
}
@@ -72,36 +59,30 @@ describe('Signal Tests', () => {
const msg = Buffer.from('hello there!')
const sender = participants[0]
const enc = await sender.repository.encryptGroupMessage(
{
const enc = await sender.repository.encryptGroupMessage({
group: groupId,
meId: sender.jid,
data: msg
}
)
})
for (const participant of participants) {
if (participant === sender) {
continue
}
await participant.repository.processSenderKeyDistributionMessage(
{
await participant.repository.processSenderKeyDistributionMessage({
item: {
groupId,
axolotlSenderKeyDistributionMessage: enc.senderKeyDistributionMessage
},
authorJid: sender.jid
}
)
})
const dec = await participant.repository.decryptGroupMessage(
{
const dec = await participant.repository.decryptGroupMessage({
group: groupId,
authorJid: sender.jid,
msg: enc.ciphertext
}
)
})
expect(dec).toEqual(msg)
}
})
@@ -116,14 +97,9 @@ function makeUser() {
return { store, jid, repository }
}
async function prepareForSendingMessage(
sender: User,
receiver: User,
preKeyId = 2
) {
async function prepareForSendingMessage(sender: User, receiver: User, preKeyId = 2) {
const preKey = Curve.generateKeyPair()
await sender.repository.injectE2ESession(
{
await sender.repository.injectE2ESession({
jid: receiver.jid,
session: {
registrationId: receiver.store.creds.registrationId,
@@ -131,15 +107,14 @@ async function prepareForSendingMessage(
signedPreKey: {
keyId: receiver.store.creds.signedPreKey.keyId,
publicKey: generateSignalPubKey(receiver.store.creds.signedPreKey.keyPair.public),
signature: receiver.store.creds.signedPreKey.signature,
signature: receiver.store.creds.signedPreKey.signature
},
preKey: {
keyId: preKeyId,
publicKey: generateSignalPubKey(preKey.public),
publicKey: generateSignalPubKey(preKey.public)
}
}
}
)
})
await receiver.store.keys.set({
'pre-key': {
@@ -156,7 +131,7 @@ function makeTestAuthState(): SignalAuthState {
creds: {
signedIdentityKey: identityKey,
registrationId: generateRegistrationId(),
signedPreKey: signedKeyPair(identityKey, 1),
signedPreKey: signedKeyPair(identityKey, 1)
},
keys: {
get(type, ids) {
@@ -176,7 +151,7 @@ function makeTestAuthState(): SignalAuthState {
store[getUniqueId(type, id)] = data[type][id]
}
}
},
}
}
}

View File

@@ -31,11 +31,10 @@ const TEST_VECTORS: TestVector[] = [
)
),
plaintext: readFileSync('./Media/icon.png')
},
}
]
describe('Media Download Tests', () => {
it('should download a full encrypted media correctly', async () => {
for (const { type, message, plaintext } of TEST_VECTORS) {
const readPipe = await downloadContentFromMessage(message, type)

View File

@@ -2,7 +2,6 @@ import { WAMessageContent } from '../Types'
import { normalizeMessageContent } from '../Utils'
describe('Messages Tests', () => {
it('should correctly unwrap messages', () => {
const CONTENT = { imageMessage: {} }
expectRightContent(CONTENT)
@@ -29,9 +28,7 @@ describe('Messages Tests', () => {
})
function expectRightContent(content: WAMessageContent) {
expect(
normalizeMessageContent(content)
).toHaveProperty('imageMessage')
expect(normalizeMessageContent(content)).toHaveProperty('imageMessage')
}
})
})

View File

@@ -27,7 +27,7 @@ export function makeMockSignalKeyStore(): SignalKeyStore {
store[getUniqueId(type, id)] = data[type][id]
}
}
},
}
}
function getUniqueId(type: string, id: string) {

View File

@@ -2,7 +2,7 @@ import type { proto } from '../../WAProto'
import type { Contact } from './Contact'
import type { MinimalMessage } from './Message'
export type KeyPair = { public: Uint8Array, private: Uint8Array }
export type KeyPair = { public: Uint8Array; private: Uint8Array }
export type SignedKeyPair = {
keyPair: KeyPair
signature: Uint8Array
@@ -67,7 +67,7 @@ export type AuthenticationCreds = SignalCreds & {
export type SignalDataTypeMap = {
'pre-key': KeyPair
'session': Uint8Array
session: Uint8Array
'sender-key': Uint8Array
'sender-key-memory': { [jid: string]: boolean }
'app-state-sync-key': proto.Message.IAppStateSyncKeyData

View File

@@ -1,4 +1,3 @@
export type WACallUpdateType = 'offer' | 'ringing' | 'timeout' | 'reject' | 'accept' | 'terminate'
export type WACallEvent = {

View File

@@ -22,9 +22,15 @@ export type WAPrivacyMessagesValue = 'all' | 'contacts'
/** set of statuses visible to other people; see updatePresence() in WhatsAppWeb.Send */
export type WAPresence = 'unavailable' | 'available' | 'composing' | 'recording' | 'paused'
export const ALL_WA_PATCH_NAMES = ['critical_block', 'critical_unblock_low', 'regular_high', 'regular_low', 'regular'] as const
export const ALL_WA_PATCH_NAMES = [
'critical_block',
'critical_unblock_low',
'regular_high',
'regular_low',
'regular'
] as const
export type WAPatchName = typeof ALL_WA_PATCH_NAMES[number]
export type WAPatchName = (typeof ALL_WA_PATCH_NAMES)[number]
export interface PresenceData {
lastKnownPresence: WAPresence
@@ -54,7 +60,8 @@ export type Chat = proto.IConversation & {
lastMessageRecvTimestamp?: number
}
export type ChatUpdate = Partial<Chat & {
export type ChatUpdate = Partial<
Chat & {
/**
* if specified in the update,
* the EV buffer will check if the condition gets fulfilled before applying the update
@@ -65,7 +72,8 @@ export type ChatUpdate = Partial<Chat & {
* undefined if the condition is not yet fulfilled
* */
conditional: (bufferedData: BufferedEventData) => boolean | undefined
}>
}
>
/**
* the last messages in a chat, sorted reverse-chronologically. That is, the latest message should be first in the chat
@@ -74,7 +82,7 @@ export type ChatUpdate = Partial<Chat & {
export type LastMessageList = MinimalMessage[] | proto.SyncActionValue.ISyncActionMessageRange
export type ChatModification =
{
| {
archive: boolean
lastMessages: LastMessageList
}
@@ -86,12 +94,13 @@ export type ChatModification =
}
| {
clear: boolean
} | {
deleteForMe: { deleteMedia: boolean, key: WAMessageKey, timestamp: number }
}
| {
deleteForMe: { deleteMedia: boolean; key: WAMessageKey; timestamp: number }
}
| {
star: {
messages: { id: string, fromMe?: boolean }[]
messages: { id: string; fromMe?: boolean }[]
star: boolean
}
}
@@ -99,7 +108,7 @@ export type ChatModification =
markRead: boolean
lastMessages: LastMessageList
}
| { delete: true, lastMessages: LastMessageList }
| { delete: true; lastMessages: LastMessageList }
// Label
| { addLabel: LabelActionBody }
// Label assosiation

View File

@@ -29,42 +29,48 @@ export type BaileysEventMap = {
'chats.upsert': Chat[]
/** update the given chats */
'chats.update': ChatUpdate[]
'chats.phoneNumberShare': {lid: string, jid: string}
'chats.phoneNumberShare': { lid: string; jid: string }
/** delete chats with given ID */
'chats.delete': string[]
/** presence of contact in a chat updated */
'presence.update': { id: string, presences: { [participant: string]: PresenceData } }
'presence.update': { id: string; presences: { [participant: string]: PresenceData } }
'contacts.upsert': Contact[]
'contacts.update': Partial<Contact>[]
'messages.delete': { keys: WAMessageKey[] } | { jid: string, all: true }
'messages.delete': { keys: WAMessageKey[] } | { jid: string; all: true }
'messages.update': WAMessageUpdate[]
'messages.media-update': { key: WAMessageKey, media?: { ciphertext: Uint8Array, iv: Uint8Array }, error?: Boom }[]
'messages.media-update': { key: WAMessageKey; media?: { ciphertext: Uint8Array; iv: Uint8Array }; error?: Boom }[]
/**
* add/update the given messages. If they were received while the connection was online,
* the update will have type: "notify"
* if requestId is provided, then the messages was received from the phone due to it being unavailable
* */
'messages.upsert': { messages: WAMessage[], type: MessageUpsertType, requestId?: string }
'messages.upsert': { messages: WAMessage[]; type: MessageUpsertType; requestId?: string }
/** message was reacted to. If reaction was removed -- then "reaction.text" will be falsey */
'messages.reaction': { key: WAMessageKey, reaction: proto.IReaction }[]
'messages.reaction': { key: WAMessageKey; reaction: proto.IReaction }[]
'message-receipt.update': MessageUserReceiptUpdate[]
'groups.upsert': GroupMetadata[]
'groups.update': Partial<GroupMetadata>[]
/** apply an action to participants in a group */
'group-participants.update': { id: string, author: string, participants: string[], action: ParticipantAction }
'group.join-request': { id: string, author: string, participant: string, action: RequestJoinAction, method: RequestJoinMethod }
'group-participants.update': { id: string; author: string; participants: string[]; action: ParticipantAction }
'group.join-request': {
id: string
author: string
participant: string
action: RequestJoinAction
method: RequestJoinMethod
}
'blocklist.set': { blocklist: string[] }
'blocklist.update': { blocklist: string[], type: 'add' | 'remove' }
'blocklist.update': { blocklist: string[]; type: 'add' | 'remove' }
/** Receive an update on a call, including when the call was received, rejected, accepted */
'call': WACallEvent[]
call: WACallEvent[]
'labels.edit': Label
'labels.association': { association: LabelAssociation, type: 'add' | 'remove' }
'labels.association': { association: LabelAssociation; type: 'add' | 'remove' }
}
export type BufferedEventData = {
@@ -83,11 +89,11 @@ export type BufferedEventData = {
chatDeletes: Set<string>
contactUpserts: { [jid: string]: Contact }
contactUpdates: { [jid: string]: Partial<Contact> }
messageUpserts: { [key: string]: { type: MessageUpsertType, message: WAMessage } }
messageUpserts: { [key: string]: { type: MessageUpsertType; message: WAMessage } }
messageUpdates: { [key: string]: WAMessageUpdate }
messageDeletes: { [key: string]: WAMessageKey }
messageReactions: { [key: string]: { key: WAMessageKey, reactions: proto.IReaction[] } }
messageReceipts: { [key: string]: { key: WAMessageKey, userReceipt: proto.IUserReceipt[] } }
messageReactions: { [key: string]: { key: WAMessageKey; reactions: proto.IReaction[] } }
messageReceipts: { [key: string]: { key: WAMessageKey; userReceipt: proto.IUserReceipt[] } }
groupUpdates: { [jid: string]: Partial<GroupMetadata> }
}

View File

@@ -1,6 +1,10 @@
import { Contact } from './Contact'
export type GroupParticipant = (Contact & { isAdmin?: boolean, isSuperAdmin?: boolean, admin?: 'admin' | 'superadmin' | null })
export type GroupParticipant = Contact & {
isAdmin?: boolean
isSuperAdmin?: boolean
admin?: 'admin' | 'superadmin' | null
}
export type ParticipantAction = 'add' | 'remove' | 'promote' | 'demote' | 'modify'
@@ -46,7 +50,6 @@ export interface GroupMetadata {
author?: string
}
export interface WAGroupCreateResponse {
status: number
gid?: string

View File

@@ -44,5 +44,5 @@ export enum LabelColor {
Color17,
Color18,
Color19,
Color20,
Color20
}

View File

@@ -17,7 +17,12 @@ export type WAMessageKey = proto.IMessageKey
export type WATextMessage = proto.Message.IExtendedTextMessage
export type WAContextInfo = proto.IContextInfo
export type WALocationMessage = proto.Message.ILocationMessage
export type WAGenericMediaMessage = proto.Message.IVideoMessage | proto.Message.IImageMessage | proto.Message.IAudioMessage | proto.Message.IDocumentMessage | proto.Message.IStickerMessage
export type WAGenericMediaMessage =
| proto.Message.IVideoMessage
| proto.Message.IImageMessage
| proto.Message.IAudioMessage
| proto.Message.IDocumentMessage
| proto.Message.IStickerMessage
export const WAMessageStubType = proto.WebMessageInfo.StubType
export const WAMessageStatus = proto.WebMessageInfo.Status
import { ILogger } from '../Utils/logger'
@@ -27,14 +32,22 @@ export type WAMediaUpload = Buffer | WAMediaPayloadStream | WAMediaPayloadURL
/** Set of message types that are supported by the library */
export type MessageType = keyof proto.Message
export type DownloadableMessage = { mediaKey?: Uint8Array | null, directPath?: string | null, url?: string | null }
export type DownloadableMessage = { mediaKey?: Uint8Array | null; directPath?: string | null; url?: string | null }
export type MessageReceiptType = 'read' | 'read-self' | 'hist_sync' | 'peer_msg' | 'sender' | 'inactive' | 'played' | undefined
export type MessageReceiptType =
| 'read'
| 'read-self'
| 'hist_sync'
| 'peer_msg'
| 'sender'
| 'inactive'
| 'played'
| undefined
export type MediaConnInfo = {
auth: string
ttl: number
hosts: { hostname: string, maxContentLengthBytes: number }[]
hosts: { hostname: string; maxContentLengthBytes: number }[]
fetchDate: Date
}
@@ -88,11 +101,13 @@ type RequestPhoneNumber = {
export type MediaType = keyof typeof MEDIA_HKDF_KEY_MAPPING
export type AnyMediaMessageContent = (
({
| ({
image: WAMediaUpload
caption?: string
jpegThumbnail?: string
} & Mentionable & Contextable & WithDimensions)
} & Mentionable &
Contextable &
WithDimensions)
| ({
video: WAMediaUpload
caption?: string
@@ -100,7 +115,9 @@ export type AnyMediaMessageContent = (
jpegThumbnail?: string
/** if set to true, will send as a `video note` */
ptv?: boolean
} & Mentionable & Contextable & WithDimensions)
} & Mentionable &
Contextable &
WithDimensions)
| {
audio: WAMediaUpload
/** if set to true, will send as a `voice note` */
@@ -111,13 +128,14 @@ export type AnyMediaMessageContent = (
| ({
sticker: WAMediaUpload
isAnimated?: boolean
} & WithDimensions) | ({
} & WithDimensions)
| ({
document: WAMediaUpload
mimetype: string
fileName?: string
caption?: string
} & Contextable))
& { mimetype?: string } & Editable
} & Contextable)
) & { mimetype?: string } & Editable
export type ButtonReplyInfo = {
displayText: string
@@ -138,15 +156,18 @@ export type WASendableProduct = Omit<proto.Message.ProductMessage.IProductSnapsh
}
export type AnyRegularMessageContent = (
({
| ({
text: string
linkPreview?: WAUrlInfo | null
}
& Mentionable & Contextable & Editable)
} & Mentionable &
Contextable &
Editable)
| AnyMediaMessageContent
| ({
poll: PollMessageOptions
} & Mentionable & Contextable & Editable)
} & Mentionable &
Contextable &
Editable)
| {
contacts: {
displayName?: string
@@ -180,16 +201,23 @@ export type AnyRegularMessageContent = (
businessOwnerJid?: string
body?: string
footer?: string
} | SharePhoneNumber | RequestPhoneNumber
) & ViewOnce
}
| SharePhoneNumber
| RequestPhoneNumber
) &
ViewOnce
export type AnyMessageContent = AnyRegularMessageContent | {
export type AnyMessageContent =
| AnyRegularMessageContent
| {
forward: WAMessage
force?: boolean
} | {
}
| {
/** Delete your message or anyone's message in a group (admin required) */
delete: WAMessageKey
} | {
}
| {
disappearingMessagesInChat: boolean | number
}
@@ -204,7 +232,7 @@ type MinimalRelayOptions = {
export type MessageRelayOptions = MinimalRelayOptions & {
/** only send to a specific participant; used when a message decryption fails for a single user */
participant?: { jid: string, count: number }
participant?: { jid: string; count: number }
/** additional attributes to add to the WA binary node */
additionalAttributes?: { [_: string]: string }
additionalNodes?: BinaryNode[]
@@ -236,7 +264,10 @@ export type MessageGenerationOptionsFromContent = MiscMessageGenerationOptions &
userJid: string
}
export type WAMediaUploadFunction = (readStream: Readable, opts: { fileEncSha256B64: string, mediaType: MediaType, timeoutMs?: number }) => Promise<{ mediaUrl: string, directPath: string }>
export type WAMediaUploadFunction = (
readStream: Readable,
opts: { fileEncSha256B64: string; mediaType: MediaType; timeoutMs?: number }
) => Promise<{ mediaUrl: string; directPath: string }>
export type MediaGenerationOptions = {
logger?: ILogger
@@ -268,11 +299,11 @@ export type MessageUpsertType = 'append' | 'notify'
export type MessageUserReceipt = proto.IUserReceipt
export type WAMessageUpdate = { update: Partial<WAMessage>, key: proto.IMessageKey }
export type WAMessageUpdate = { update: Partial<WAMessage>; key: proto.IMessageKey }
export type WAMessageCursor = { before: WAMessageKey | undefined } | { after: WAMessageKey | undefined }
export type MessageUserReceiptUpdate = { key: proto.IMessageKey, receipt: MessageUserReceipt }
export type MessageUserReceiptUpdate = { key: proto.IMessageKey; receipt: MessageUserReceipt }
export type MediaDecryptionKeyInfo = {
iv: Buffer

View File

@@ -2,7 +2,7 @@ import { WAMediaUpload } from './Message'
export type CatalogResult = {
data: {
paging: { cursors: { before: string, after: string } }
paging: { cursors: { before: string; after: string } }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any[]
}

View File

@@ -51,9 +51,7 @@ type E2ESessionOpts = {
export type SignalRepository = {
decryptGroupMessage(opts: DecryptGroupSignalOpts): Promise<Uint8Array>
processSenderKeyDistributionMessage(
opts: ProcessSenderKeyDistributionMessageOpts
): Promise<void>
processSenderKeyDistributionMessage(opts: ProcessSenderKeyDistributionMessageOpts): Promise<void>
decryptMessage(opts: DecryptSignalProtoOpts): Promise<Uint8Array>
encryptMessage(opts: EncryptMessageOpts): Promise<{
type: 'pkmsg' | 'msg'

View File

@@ -1,4 +1,3 @@
import { AxiosRequestConfig } from 'axios'
import type { Agent } from 'https'
import type { URL } from 'url'
@@ -108,8 +107,11 @@ export type SocketConfig = {
* */
patchMessageBeforeSending: (
msg: proto.IMessage,
recipientJids?: string[],
) => Promise<PatchedMessageWithRecipientJID[] | PatchedMessageWithRecipientJID> | PatchedMessageWithRecipientJID[] | PatchedMessageWithRecipientJID
recipientJids?: string[]
) =>
| Promise<PatchedMessageWithRecipientJID[] | PatchedMessageWithRecipientJID>
| PatchedMessageWithRecipientJID[]
| PatchedMessageWithRecipientJID
/** verify app state MACs */
appStateMacVerification: {

View File

@@ -63,4 +63,4 @@ export type WABusinessProfile = {
address?: string
}
export type CurveKeyPair = { private: Uint8Array, public: Uint8Array }
export type CurveKeyPair = { private: Uint8Array; public: Uint8Array }

View File

@@ -1,7 +1,15 @@
import NodeCache from '@cacheable/node-cache'
import { randomBytes } from 'crypto'
import { DEFAULT_CACHE_TTLS } from '../Defaults'
import type { AuthenticationCreds, CacheStore, SignalDataSet, SignalDataTypeMap, SignalKeyStore, SignalKeyStoreWithTransaction, TransactionCapabilityOptions } from '../Types'
import type {
AuthenticationCreds,
CacheStore,
SignalDataSet,
SignalDataTypeMap,
SignalKeyStore,
SignalKeyStoreWithTransaction,
TransactionCapabilityOptions
} from '../Types'
import { Curve, signedKeyPair } from './crypto'
import { delay, generateRegistrationId } from './generics'
import { ILogger } from './logger'
@@ -17,10 +25,12 @@ export function makeCacheableSignalKeyStore(
logger?: ILogger,
_cache?: CacheStore
): SignalKeyStore {
const cache = _cache || new NodeCache({
const cache =
_cache ||
new NodeCache({
stdTTL: DEFAULT_CACHE_TTLS.SIGNAL_STORE, // 5 minutes
useClones: false,
deleteOnExpire: true,
deleteOnExpire: true
})
function getUniqueId(type: string, id: string) {
@@ -98,31 +108,24 @@ export const addTransactionCapability = (
get: async (type, ids) => {
if (isInTransaction()) {
const dict = transactionCache[type]
const idsRequiringFetch = dict
? ids.filter(item => typeof dict[item] === 'undefined')
: ids
const idsRequiringFetch = dict ? ids.filter(item => typeof dict[item] === 'undefined') : ids
// only fetch if there are any items to fetch
if (idsRequiringFetch.length) {
dbQueriesInTransaction += 1
const result = await state.get(type, idsRequiringFetch)
transactionCache[type] ||= {}
Object.assign(
transactionCache[type]!,
result
)
Object.assign(transactionCache[type]!, result)
}
return ids.reduce(
(dict, id) => {
return ids.reduce((dict, id) => {
const value = transactionCache[type]?.[id]
if (value) {
dict[id] = value
}
return dict
}, { }
)
}, {})
} else {
return state.get(type, ids)
}
@@ -211,6 +214,6 @@ export const initAuthCreds = (): AuthenticationCreds => {
registered: false,
pairingCode: undefined,
lastPropHash: undefined,
routingInfo: undefined,
routingInfo: undefined
}
}

View File

@@ -20,11 +20,9 @@ export const captureEventStream = (ev: BaileysEventEmitter, filename: string) =>
const content = JSON.stringify({ timestamp: Date.now(), event: args[0], data: args[1] }) + '\n'
const result = oldEmit.apply(ev, args)
writeMutex.mutex(
async() => {
writeMutex.mutex(async () => {
await writeFile(filename, content, { flag: 'a' })
}
)
})
return result
}
@@ -52,7 +50,7 @@ export const readAndEmitEventStream = (filename: string, delayIntervalMs = 0) =>
if (line) {
const { event, data } = JSON.parse(line)
ev.emit(event, data)
delayIntervalMs && await delay(delayIntervalMs)
delayIntervalMs && (await delay(delayIntervalMs))
}
}

View File

@@ -1,6 +1,16 @@
import { Boom } from '@hapi/boom'
import { createHash } from 'crypto'
import { CatalogCollection, CatalogStatus, OrderDetails, OrderProduct, Product, ProductCreate, ProductUpdate, WAMediaUpload, WAMediaUploadFunction } from '../Types'
import {
CatalogCollection,
CatalogStatus,
OrderDetails,
OrderProduct,
Product,
ProductCreate,
ProductUpdate,
WAMediaUpload,
WAMediaUploadFunction
} from '../Types'
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, getBinaryNodeChildString } from '../WABinary'
import { getStream, getUrlFromDirectPath, toReadable } from './messages-media'
@@ -11,16 +21,13 @@ export const parseCatalogNode = (node: BinaryNode) => {
return {
products,
nextPageCursor: paging
? getBinaryNodeChildString(paging, 'after')
: undefined
nextPageCursor: paging ? getBinaryNodeChildString(paging, 'after') : undefined
}
}
export const parseCollectionsNode = (node: BinaryNode) => {
const collectionsNode = getBinaryNodeChild(node, 'collections')
const collections = getBinaryNodeChildren(collectionsNode, 'collection').map<CatalogCollection>(
collectionNode => {
const collections = getBinaryNodeChildren(collectionsNode, 'collection').map<CatalogCollection>(collectionNode => {
const id = getBinaryNodeChildString(collectionNode, 'id')!
const name = getBinaryNodeChildString(collectionNode, 'name')!
@@ -31,8 +38,7 @@ export const parseCollectionsNode = (node: BinaryNode) => {
products,
status: parseStatusInfo(collectionNode)
}
}
)
})
return {
collections
@@ -41,8 +47,7 @@ export const parseCollectionsNode = (node: BinaryNode) => {
export const parseOrderDetailsNode = (node: BinaryNode) => {
const orderNode = getBinaryNodeChild(node, 'order')
const products = getBinaryNodeChildren(orderNode, 'product').map<OrderProduct>(
productNode => {
const products = getBinaryNodeChildren(orderNode, 'product').map<OrderProduct>(productNode => {
const imageNode = getBinaryNodeChild(productNode, 'image')!
return {
id: getBinaryNodeChildString(productNode, 'id')!,
@@ -52,15 +57,14 @@ export const parseOrderDetailsNode = (node: BinaryNode) => {
currency: getBinaryNodeChildString(productNode, 'currency')!,
quantity: +getBinaryNodeChildString(productNode, 'quantity')!
}
}
)
})
const priceNode = getBinaryNodeChild(orderNode, 'price')
const orderDetails: OrderDetails = {
price: {
total: +getBinaryNodeChildString(priceNode, 'total')!,
currency: getBinaryNodeChildString(priceNode, 'currency')!,
currency: getBinaryNodeChildString(priceNode, 'currency')!
},
products
}
@@ -108,8 +112,7 @@ export const toProductNode = (productId: string | undefined, product: ProductCre
content.push({
tag: 'media',
attrs: {},
content: product.images.map(
img => {
content: product.images.map(img => {
if (!('url' in img)) {
throw new Boom('Expected img for product to already be uploaded', { statusCode: 400 })
}
@@ -125,8 +128,7 @@ export const toProductNode = (productId: string | undefined, product: ProductCre
}
]
}
}
)
})
})
}
@@ -164,7 +166,6 @@ export const toProductNode = (productId: string | undefined, product: ProductCre
}
}
if (typeof product.isHidden !== 'undefined') {
attrs['is_hidden'] = product.isHidden.toString()
}
@@ -188,7 +189,7 @@ export const parseProductNode = (productNode: BinaryNode) => {
id,
imageUrls: parseImageUrls(mediaNode),
reviewStatus: {
whatsapp: getBinaryNodeChildString(statusInfoNode, 'status')!,
whatsapp: getBinaryNodeChildString(statusInfoNode, 'status')!
},
availability: 'in stock',
name: getBinaryNodeChildString(productNode, 'name')!,
@@ -197,7 +198,7 @@ export const parseProductNode = (productNode: BinaryNode) => {
description: getBinaryNodeChildString(productNode, 'description')!,
price: +getBinaryNodeChildString(productNode, 'price')!,
currency: getBinaryNodeChildString(productNode, 'currency')!,
isHidden,
isHidden
}
return product
@@ -206,10 +207,16 @@ export const parseProductNode = (productNode: BinaryNode) => {
/**
* Uploads images not already uploaded to WA's servers
*/
export async function uploadingNecessaryImagesOfProduct<T extends ProductUpdate | ProductCreate>(product: T, waUploadToServer: WAMediaUploadFunction, timeoutMs = 30_000) {
export async function uploadingNecessaryImagesOfProduct<T extends ProductUpdate | ProductCreate>(
product: T,
waUploadToServer: WAMediaUploadFunction,
timeoutMs = 30_000
) {
product = {
...product,
images: product.images ? await uploadingNecessaryImages(product.images, waUploadToServer, timeoutMs) : product.images
images: product.images
? await uploadingNecessaryImages(product.images, waUploadToServer, timeoutMs)
: product.images
}
return product
}
@@ -223,9 +230,7 @@ export const uploadingNecessaryImages = async(
timeoutMs = 30_000
) => {
const results = await Promise.all(
images.map<Promise<{ url: string }>>(
async img => {
images.map<Promise<{ url: string }>>(async img => {
if ('url' in img) {
const url = img.url.toString()
if (url.includes('.whatsapp.net')) {
@@ -243,17 +248,13 @@ export const uploadingNecessaryImages = async(
const sha = hasher.digest('base64')
const { directPath } = await waUploadToServer(
toReadable(Buffer.concat(contentBlocks)),
{
const { directPath } = await waUploadToServer(toReadable(Buffer.concat(contentBlocks)), {
mediaType: 'product-catalog-image',
fileEncSha256B64: sha,
timeoutMs
}
)
})
return { url: getUrlFromDirectPath(directPath) }
}
)
})
)
return results
}
@@ -270,6 +271,6 @@ const parseStatusInfo = (mediaNode: BinaryNode): CatalogStatus => {
const node = getBinaryNodeChild(mediaNode, 'status_info')
return {
status: getBinaryNodeChildString(node, 'status')!,
canAppeal: getBinaryNodeChildString(node, 'can_appeal') === 'true',
canAppeal: getBinaryNodeChildString(node, 'can_appeal') === 'true'
}
}

View File

@@ -1,14 +1,26 @@
import { Boom } from '@hapi/boom'
import { AxiosRequestConfig } from 'axios'
import { proto } from '../../WAProto'
import { BaileysEventEmitter, Chat, ChatModification, ChatMutation, ChatUpdate, Contact, InitialAppStateSyncOptions, LastMessageList, LTHashState, WAPatchCreate, WAPatchName } from '../Types'
import {
BaileysEventEmitter,
Chat,
ChatModification,
ChatMutation,
ChatUpdate,
Contact,
InitialAppStateSyncOptions,
LastMessageList,
LTHashState,
WAPatchCreate,
WAPatchName
} from '../Types'
import { ChatLabelAssociation, LabelAssociationType, MessageLabelAssociation } from '../Types/LabelAssociation'
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, jidNormalizedUser } from '../WABinary'
import { aesDecrypt, aesEncrypt, hkdf, hmacSign } from './crypto'
import { toNumber } from './generics'
import { ILogger } from './logger'
import { LT_HASH_ANTI_TAMPERING } from './lt-hash'
import { downloadContentFromMessage, } from './messages-media'
import { downloadContentFromMessage } from './messages-media'
type FetchAppStateSyncKey = (keyId: string) => Promise<proto.Message.IAppStateSyncKeyData | null | undefined>
@@ -25,7 +37,12 @@ const mutationKeys = async(keydata: Uint8Array) => {
}
}
const generateMac = (operation: proto.SyncdMutation.SyncdOperation, data: Buffer, keyId: Uint8Array | string, key: Buffer) => {
const generateMac = (
operation: proto.SyncdMutation.SyncdOperation,
data: Buffer,
keyId: Uint8Array | string,
key: Buffer
) => {
const getKeyData = () => {
let r: number
switch (operation) {
@@ -58,7 +75,7 @@ const to64BitNetworkOrder = (e: number) => {
return buff
}
type Mac = { indexMac: Uint8Array, valueMac: Uint8Array, operation: proto.SyncdMutation.SyncdOperation }
type Mac = { indexMac: Uint8Array; valueMac: Uint8Array; operation: proto.SyncdMutation.SyncdOperation }
const makeLtHashGenerator = ({ indexValueMap, hash }: Pick<LTHashState, 'hash' | 'indexValueMap'>) => {
indexValueMap = { ...indexValueMap }
@@ -100,21 +117,18 @@ const makeLtHashGenerator = ({ indexValueMap, hash }: Pick<LTHashState, 'hash' |
}
const generateSnapshotMac = (lthash: Uint8Array, version: number, name: WAPatchName, key: Buffer) => {
const total = Buffer.concat([
lthash,
to64BitNetworkOrder(version),
Buffer.from(name, 'utf-8')
])
const total = Buffer.concat([lthash, to64BitNetworkOrder(version), Buffer.from(name, 'utf-8')])
return hmacSign(total, key, 'sha256')
}
const generatePatchMac = (snapshotMac: Uint8Array, valueMacs: Uint8Array[], version: number, type: WAPatchName, key: Buffer) => {
const total = Buffer.concat([
snapshotMac,
...valueMacs,
to64BitNetworkOrder(version),
Buffer.from(type, 'utf-8')
])
const generatePatchMac = (
snapshotMac: Uint8Array,
valueMacs: Uint8Array[],
version: number,
type: WAPatchName,
key: Buffer
) => {
const total = Buffer.concat([snapshotMac, ...valueMacs, to64BitNetworkOrder(version), Buffer.from(type, 'utf-8')])
return hmacSign(total, key)
}
@@ -200,7 +214,8 @@ export const decodeSyncdMutations = async(
// if it's a syncdmutation, get the operation property
// otherwise, if it's only a record -- it'll be a SET mutation
const operation = 'operation' in msgMutation ? msgMutation.operation : proto.SyncdMutation.SyncdOperation.SET
const record = ('record' in msgMutation && !!msgMutation.record) ? msgMutation.record : msgMutation as proto.ISyncdRecord
const record =
'record' in msgMutation && !!msgMutation.record ? msgMutation.record : (msgMutation as proto.ISyncdRecord)
const key = await getKey(record.keyId!.id!)
const content = Buffer.from(record.value!.blob!)
@@ -239,7 +254,10 @@ export const decodeSyncdMutations = async(
const base64Key = Buffer.from(keyId).toString('base64')
const keyEnc = await getAppStateSyncKey(base64Key)
if (!keyEnc) {
throw new Boom(`failed to find key "${base64Key}" to decode mutation`, { statusCode: 404, data: { msgMutations } })
throw new Boom(`failed to find key "${base64Key}" to decode mutation`, {
statusCode: 404,
data: { msgMutations }
})
}
return mutationKeys(keyEnc.keyData!)
@@ -264,7 +282,13 @@ export const decodeSyncdPatch = async(
const mainKey = await mutationKeys(mainKeyObj.keyData!)
const mutationmacs = msg.mutations!.map(mutation => mutation.record!.value!.blob!.slice(-32))
const patchMac = generatePatchMac(msg.snapshotMac!, mutationmacs, toNumber(msg.version!.version), name, mainKey.patchMacKey)
const patchMac = generatePatchMac(
msg.snapshotMac!,
mutationmacs,
toNumber(msg.version!.version),
name,
mainKey.patchMacKey
)
if (Buffer.compare(patchMac, msg.patchMac!) !== 0) {
throw new Boom('Invalid patch mac')
}
@@ -274,17 +298,15 @@ export const decodeSyncdPatch = async(
return result
}
export const extractSyncdPatches = async(
result: BinaryNode,
options: AxiosRequestConfig<{}>
) => {
export const extractSyncdPatches = async (result: BinaryNode, options: AxiosRequestConfig<{}>) => {
const syncNode = getBinaryNodeChild(result, 'sync')
const collectionNodes = getBinaryNodeChildren(syncNode, 'collection')
const final = {} as { [T in WAPatchName]: { patches: proto.ISyncdPatch[], hasMorePatches: boolean, snapshot?: proto.ISyncdSnapshot } }
const final = {} as {
[T in WAPatchName]: { patches: proto.ISyncdPatch[]; hasMorePatches: boolean; snapshot?: proto.ISyncdSnapshot }
}
await Promise.all(
collectionNodes.map(
async collectionNode => {
collectionNodes.map(async collectionNode => {
const patchesNode = getBinaryNodeChild(collectionNode, 'patches')
const patches = getBinaryNodeChildren(patchesNode || collectionNode, 'patch')
@@ -301,9 +323,7 @@ export const extractSyncdPatches = async(
snapshotNode.content = Buffer.from(Object.values(snapshotNode.content))
}
const blobRef = proto.ExternalBlobReference.decode(
snapshotNode.content as Buffer
)
const blobRef = proto.ExternalBlobReference.decode(snapshotNode.content as Buffer)
const data = await downloadExternalBlob(blobRef, options)
snapshot = proto.SyncdSnapshot.decode(data)
}
@@ -324,18 +344,13 @@ export const extractSyncdPatches = async(
}
final[name] = { patches: syncds, hasMorePatches, snapshot }
}
)
})
)
return final
}
export const downloadExternalBlob = async(
blob: proto.IExternalBlobReference,
options: AxiosRequestConfig<{}>
) => {
export const downloadExternalBlob = async (blob: proto.IExternalBlobReference, options: AxiosRequestConfig<{}>) => {
const stream = await downloadContentFromMessage(blob, 'md-app-state', { options })
const bufferArray: Buffer[] = []
for await (const chunk of stream) {
@@ -345,10 +360,7 @@ export const downloadExternalBlob = async(
return Buffer.concat(bufferArray)
}
export const downloadExternalPatch = async(
blob: proto.IExternalBlobReference,
options: AxiosRequestConfig<{}>
) => {
export const downloadExternalPatch = async (blob: proto.IExternalBlobReference, options: AxiosRequestConfig<{}>) => {
const buffer = await downloadExternalBlob(blob, options)
const syncData = proto.SyncdMutations.decode(buffer)
return syncData
@@ -365,15 +377,14 @@ export const decodeSyncdSnapshot = async(
newState.version = toNumber(snapshot.version!.version)
const mutationMap: ChatMutationMap = {}
const areMutationsRequired = typeof minimumVersionNumber === 'undefined'
|| newState.version > minimumVersionNumber
const areMutationsRequired = typeof minimumVersionNumber === 'undefined' || newState.version > minimumVersionNumber
const { hash, indexValueMap } = await decodeSyncdMutations(
snapshot.records!,
newState,
getAppStateSyncKey,
areMutationsRequired
? (mutation) => {
? mutation => {
const index = mutation.syncAction.index?.toString()
mutationMap[index!] = mutation
}
@@ -444,7 +455,7 @@ export const decodePatches = async(
const index = mutation.syncAction.index?.toString()
mutationMap[index!] = mutation
}
: (() => { }),
: () => {},
true
)
@@ -472,10 +483,7 @@ export const decodePatches = async(
return { state: newState, mutationMap }
}
export const chatModificationToAppPatch = (
mod: ChatModification,
jid: string
) => {
export const chatModificationToAppPatch = (mod: ChatModification, jid: string) => {
const OP = proto.SyncdMutation.SyncdOperation
const getMessageRange = (lastMessages: LastMessageList) => {
let messageRange: proto.SyncActionValue.ISyncActionMessageRange
@@ -483,8 +491,8 @@ export const chatModificationToAppPatch = (
const lastMsg = lastMessages[lastMessages.length - 1]
messageRange = {
lastMessageTimestamp: lastMsg?.messageTimestamp,
messages: lastMessages?.length ? lastMessages.map(
m => {
messages: lastMessages?.length
? lastMessages.map(m => {
if (!m.key?.id || !m.key?.remoteJid) {
throw new Boom('Incomplete key', { statusCode: 400, data: m })
}
@@ -502,8 +510,8 @@ export const chatModificationToAppPatch = (
}
return m
}
) : undefined
})
: undefined
}
} else {
messageRange = lastMessages
@@ -605,7 +613,7 @@ export const chatModificationToAppPatch = (
patch = {
syncAction: {
deleteChatAction: {
messageRange: getMessageRange(mod.lastMessages),
messageRange: getMessageRange(mod.lastMessages)
}
},
index: ['deleteChat', jid, '1'],
@@ -623,7 +631,7 @@ export const chatModificationToAppPatch = (
index: ['setting_pushName'],
type: 'critical_block',
apiVersion: 1,
operation: OP.SET,
operation: OP.SET
}
} else if ('addLabel' in mod) {
patch = {
@@ -638,56 +646,49 @@ export const chatModificationToAppPatch = (
index: ['label_edit', mod.addLabel.id],
type: 'regular',
apiVersion: 3,
operation: OP.SET,
operation: OP.SET
}
} else if ('addChatLabel' in mod) {
patch = {
syncAction: {
labelAssociationAction: {
labeled: true,
labeled: true
}
},
index: [LabelAssociationType.Chat, mod.addChatLabel.labelId, jid],
type: 'regular',
apiVersion: 3,
operation: OP.SET,
operation: OP.SET
}
} else if ('removeChatLabel' in mod) {
patch = {
syncAction: {
labelAssociationAction: {
labeled: false,
labeled: false
}
},
index: [LabelAssociationType.Chat, mod.removeChatLabel.labelId, jid],
type: 'regular',
apiVersion: 3,
operation: OP.SET,
operation: OP.SET
}
} else if ('addMessageLabel' in mod) {
patch = {
syncAction: {
labelAssociationAction: {
labeled: true,
labeled: true
}
},
index: [
LabelAssociationType.Message,
mod.addMessageLabel.labelId,
jid,
mod.addMessageLabel.messageId,
'0',
'0'
],
index: [LabelAssociationType.Message, mod.addMessageLabel.labelId, jid, mod.addMessageLabel.messageId, '0', '0'],
type: 'regular',
apiVersion: 3,
operation: OP.SET,
operation: OP.SET
}
} else if ('removeMessageLabel' in mod) {
patch = {
syncAction: {
labelAssociationAction: {
labeled: false,
labeled: false
}
},
index: [
@@ -700,7 +701,7 @@ export const chatModificationToAppPatch = (
],
type: 'regular',
apiVersion: 3,
operation: OP.SET,
operation: OP.SET
}
} else {
throw new Boom('not supported')
@@ -716,7 +717,7 @@ export const processSyncAction = (
ev: BaileysEventEmitter,
me: Contact,
initialSyncOpts?: InitialAppStateSyncOptions,
logger?: ILogger,
logger?: ILogger
) => {
const isInitialSync = !!initialSyncOpts
const accountSettings = initialSyncOpts?.accountSettings
@@ -729,18 +730,13 @@ export const processSyncAction = (
} = syncAction
if (action?.muteAction) {
ev.emit(
'chats.update',
[
ev.emit('chats.update', [
{
id,
muteEndTime: action.muteAction?.muted
? toNumber(action.muteAction.muteEndTimestamp)
: null,
muteEndTime: action.muteAction?.muted ? toNumber(action.muteAction.muteEndTimestamp) : null,
conditional: getChatUpdateConditional(id, undefined)
}
]
)
])
} else if (action?.archiveChatAction || type === 'archive' || type === 'unarchive') {
// okay so we've to do some annoying computation here
// when we're initially syncing the app state
@@ -753,9 +749,7 @@ export const processSyncAction = (
// 2. if the account unarchiveChats setting is false -- then it doesn't matter,
// it'll always take an app state action to mark in unarchived -- which we'll get anyway
const archiveAction = action?.archiveChatAction
const isArchived = archiveAction
? archiveAction.archived
: type === 'archive'
const isArchived = archiveAction ? archiveAction.archived : type === 'archive'
// // basically we don't need to fire an "archive" update if the chat is being marked unarchvied
// // this only applies for the initial sync
// if(isInitialSync && !isArchived) {
@@ -765,11 +759,13 @@ export const processSyncAction = (
const msgRange = !accountSettings?.unarchiveChats ? undefined : archiveAction?.messageRange
// logger?.debug({ chat: id, syncAction }, 'message range archive')
ev.emit('chats.update', [{
ev.emit('chats.update', [
{
id,
archived: isArchived,
conditional: getChatUpdateConditional(id, msgRange)
}])
}
])
} else if (action?.markChatAsReadAction) {
const markReadAction = action.markChatAsReadAction
// basically we don't need to fire an "read" update if the chat is being marked as read
@@ -777,11 +773,13 @@ export const processSyncAction = (
// this only applies for the initial sync
const isNullUpdate = isInitialSync && markReadAction.read
ev.emit('chats.update', [{
ev.emit('chats.update', [
{
id,
unreadCount: isNullUpdate ? null : !!markReadAction?.read ? 0 : -1,
conditional: getChatUpdateConditional(id, markReadAction?.messageRange)
}])
}
])
} else if (action?.deleteMessageForMeAction || type === 'deleteMessageForMe') {
ev.emit('messages.delete', {
keys: [
@@ -800,11 +798,13 @@ export const processSyncAction = (
ev.emit('creds.update', { me: { ...me, name } })
}
} else if (action?.pinAction) {
ev.emit('chats.update', [{
ev.emit('chats.update', [
{
id,
pinned: action.pinAction?.pinned ? toNumber(action.timestamp) : null,
conditional: getChatUpdateConditional(id, undefined)
}])
}
])
} else if (action?.unarchiveChatsSetting) {
const unarchiveChats = !!action.unarchiveChatsSetting.unarchiveChats
ev.emit('creds.update', { accountSettings: { unarchiveChats } })
@@ -841,29 +841,31 @@ export const processSyncAction = (
})
} else if (action?.labelAssociationAction) {
ev.emit('labels.association', {
type: action.labelAssociationAction.labeled
? 'add'
: 'remove',
association: type === LabelAssociationType.Chat
? {
type: action.labelAssociationAction.labeled ? 'add' : 'remove',
association:
type === LabelAssociationType.Chat
? ({
type: LabelAssociationType.Chat,
chatId: syncAction.index[2],
labelId: syncAction.index[1]
} as ChatLabelAssociation
: {
} as ChatLabelAssociation)
: ({
type: LabelAssociationType.Message,
chatId: syncAction.index[2],
messageId: syncAction.index[3],
labelId: syncAction.index[1]
} as MessageLabelAssociation
} as MessageLabelAssociation)
})
} else {
logger?.debug({ syncAction, id }, 'unprocessable update')
}
function getChatUpdateConditional(id: string, msgRange: proto.SyncActionValue.ISyncActionMessageRange | null | undefined): ChatUpdate['conditional'] {
function getChatUpdateConditional(
id: string,
msgRange: proto.SyncActionValue.ISyncActionMessageRange | null | undefined
): ChatUpdate['conditional'] {
return isInitialSync
? (data) => {
? data => {
const chat = data.historySets.chats[id] || data.chatUpserts[id]
if (chat) {
return msgRange ? isValidPatchBasedOnMessageRange(chat, msgRange) : true
@@ -872,7 +874,10 @@ export const processSyncAction = (
: undefined
}
function isValidPatchBasedOnMessageRange(chat: Chat, msgRange: proto.SyncActionValue.ISyncActionMessageRange | null | undefined) {
function isValidPatchBasedOnMessageRange(
chat: Chat,
msgRange: proto.SyncActionValue.ISyncActionMessageRange | null | undefined
) {
const lastMsgTimestamp = Number(msgRange?.lastMessageTimestamp || msgRange?.lastSystemMessageTimestamp || 0)
const chatLastMsgTimestamp = Number(chat?.lastMessageRecvTimestamp || 0)
return lastMsgTimestamp >= chatLastMsgTimestamp

View File

@@ -7,11 +7,8 @@ import { KeyPair } from '../Types'
const { subtle } = globalThis.crypto
/** prefix version byte to the pub keys, required for some curve crypto functions */
export const generateSignalPubKey = (pubKey: Uint8Array | Buffer) => (
pubKey.length === 33
? pubKey
: Buffer.concat([ KEY_BUNDLE_TYPE, pubKey ])
)
export const generateSignalPubKey = (pubKey: Uint8Array | Buffer) =>
pubKey.length === 33 ? pubKey : Buffer.concat([KEY_BUNDLE_TYPE, pubKey])
export const Curve = {
generateKeyPair: (): KeyPair => {
@@ -26,9 +23,7 @@ export const Curve = {
const shared = libsignal.curve.calculateAgreement(generateSignalPubKey(publicKey), privateKey)
return Buffer.from(shared)
},
sign: (privateKey: Uint8Array, buf: Uint8Array) => (
libsignal.curve.calculateSignature(privateKey, buf)
),
sign: (privateKey: Uint8Array, buf: Uint8Array) => libsignal.curve.calculateSignature(privateKey, buf),
verify: (pubKey: Uint8Array, message: Uint8Array, signature: Uint8Array) => {
try {
libsignal.curve.verifySignature(generateSignalPubKey(pubKey), message, signature)
@@ -111,7 +106,11 @@ export function aesEncrypWithIV(buffer: Buffer, key: Buffer, IV: Buffer) {
}
// sign HMAC using SHA 256
export function hmacSign(buffer: Buffer | Uint8Array, key: Buffer | Uint8Array, variant: 'sha256' | 'sha512' = 'sha256') {
export function hmacSign(
buffer: Buffer | Uint8Array,
key: Buffer | Uint8Array,
variant: 'sha256' | 'sha512' = 'sha256'
) {
return createHmac(variant, key).update(buffer).digest()
}
@@ -127,27 +126,17 @@ export function md5(buffer: Buffer) {
export async function hkdf(
buffer: Uint8Array | Buffer,
expandedLength: number,
info: { salt?: Buffer, info?: string }
info: { salt?: Buffer; info?: string }
): Promise<Buffer> {
// Ensure we have a Uint8Array for the key material
const inputKeyMaterial = buffer instanceof Uint8Array
? buffer
: new Uint8Array(buffer)
const inputKeyMaterial = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer)
// Set default values if not provided
const salt = info.salt ? new Uint8Array(info.salt) : new Uint8Array(0)
const infoBytes = info.info
? new TextEncoder().encode(info.info)
: new Uint8Array(0)
const infoBytes = info.info ? new TextEncoder().encode(info.info) : new Uint8Array(0)
// Import the input key material
const importedKey = await subtle.importKey(
'raw',
inputKeyMaterial,
{ name: 'HKDF' },
false,
['deriveBits']
)
const importedKey = await subtle.importKey('raw', inputKeyMaterial, { name: 'HKDF' }, false, ['deriveBits'])
// Derive bits using HKDF
const derivedBits = await subtle.deriveBits(
@@ -164,7 +153,6 @@ export async function hkdf(
return Buffer.from(derivedBits)
}
export async function derivePairingCodeKey(pairingCode: string, salt: Buffer): Promise<Buffer> {
// Convert inputs to formats Web Crypto API can work with
const encoder = new TextEncoder()
@@ -172,13 +160,7 @@ export async function derivePairingCodeKey(pairingCode: string, salt: Buffer): P
const saltBuffer = salt instanceof Uint8Array ? salt : new Uint8Array(salt)
// Import the pairing code as key material
const keyMaterial = await subtle.importKey(
'raw',
pairingCodeBuffer,
{ name: 'PBKDF2' },
false,
['deriveBits']
)
const keyMaterial = await subtle.importKey('raw', pairingCodeBuffer, { name: 'PBKDF2' }, false, ['deriveBits'])
// Derive bits using PBKDF2 with the same parameters
// 2 << 16 = 131,072 iterations

View File

@@ -1,7 +1,17 @@
import { Boom } from '@hapi/boom'
import { proto } from '../../WAProto'
import { SignalRepository, WAMessageKey } from '../Types'
import { areJidsSameUser, BinaryNode, isJidBroadcast, isJidGroup, isJidMetaIa, isJidNewsletter, isJidStatusBroadcast, isJidUser, isLidUser } from '../WABinary'
import {
areJidsSameUser,
BinaryNode,
isJidBroadcast,
isJidGroup,
isJidMetaIa,
isJidNewsletter,
isJidStatusBroadcast,
isJidUser,
isLidUser
} from '../WABinary'
import { unpadRandomMax16 } from './generics'
import { ILogger } from './logger'
@@ -24,17 +34,20 @@ export const NACK_REASONS = {
DBOperationFailed: 552
}
type MessageType = 'chat' | 'peer_broadcast' | 'other_broadcast' | 'group' | 'direct_peer_status' | 'other_status' | 'newsletter'
type MessageType =
| 'chat'
| 'peer_broadcast'
| 'other_broadcast'
| 'group'
| 'direct_peer_status'
| 'other_status'
| 'newsletter'
/**
* Decode the received node as a message.
* @note this will only parse the message, not decrypt it
*/
export function decodeMessageNode(
stanza: BinaryNode,
meId: string,
meLid: string
) {
export function decodeMessageNode(stanza: BinaryNode, meId: string, meLid: string) {
let msgType: MessageType
let chatId: string
let author: string
@@ -178,7 +191,9 @@ export const decryptMessageNode = (
throw new Error(`Unknown e2e type: ${e2eType}`)
}
let msg: proto.IMessage = proto.Message.decode(e2eType !== 'plaintext' ? unpadRandomMax16(msgBuffer) : msgBuffer)
let msg: proto.IMessage = proto.Message.decode(
e2eType !== 'plaintext' ? unpadRandomMax16(msgBuffer) : msgBuffer
)
msg = msg.deviceSentMessage?.message || msg
if (msg.senderKeyDistributionMessage) {
//eslint-disable-next-line max-depth
@@ -198,10 +213,7 @@ export const decryptMessageNode = (
fullMessage.message = msg
}
} catch (err) {
logger.error(
{ key: fullMessage.key, err },
'failed to decrypt message'
)
logger.error({ key: fullMessage.key, err }, 'failed to decrypt message')
fullMessage.messageStubType = proto.WebMessageInfo.StubType.CIPHERTEXT
fullMessage.messageStubParameters = [err.message]
}

View File

@@ -1,6 +1,16 @@
import EventEmitter from 'events'
import { proto } from '../../WAProto'
import { BaileysEvent, BaileysEventEmitter, BaileysEventMap, BufferedEventData, Chat, ChatUpdate, Contact, WAMessage, WAMessageStatus } from '../Types'
import {
BaileysEvent,
BaileysEventEmitter,
BaileysEventMap,
BufferedEventData,
Chat,
ChatUpdate,
Contact,
WAMessage,
WAMessageStatus
} from '../Types'
import { trimUndefined } from './generics'
import { ILogger } from './logger'
import { updateMessageWithReaction, updateMessageWithReceipt } from './messages'
@@ -18,10 +28,10 @@ const BUFFERABLE_EVENT = [
'messages.delete',
'messages.reaction',
'message-receipt.update',
'groups.update',
'groups.update'
] as const
type BufferableEvent = typeof BUFFERABLE_EVENT[number]
type BufferableEvent = (typeof BUFFERABLE_EVENT)[number]
/**
* A map that contains a list of all events that have been triggered
@@ -36,14 +46,14 @@ const BUFFERABLE_EVENT_SET = new Set<BaileysEvent>(BUFFERABLE_EVENT)
type BaileysBufferableEventEmitter = BaileysEventEmitter & {
/** Use to process events in a batch */
process(handler: (events: BaileysEventData) => void | Promise<void>): (() => void)
process(handler: (events: BaileysEventData) => void | Promise<void>): () => void
/**
* starts buffering events, call flush() to release them
* */
buffer(): void
/** buffers all events till the promise completes */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
createBufferedFunction<A extends any[], T>(work: (...args: A) => Promise<T>): ((...args: A) => Promise<T>)
createBufferedFunction<A extends any[], T>(work: (...args: A) => Promise<T>): (...args: A) => Promise<T>
/**
* flushes all buffered events
* @param force if true, will flush all data regardless of any pending buffers
@@ -112,10 +122,7 @@ export const makeEventBuffer = (logger: ILogger): BaileysBufferableEventEmitter
data = newData
logger.trace(
{ conditionalChatUpdatesLeft },
'released buffered events'
)
logger.trace({ conditionalChatUpdatesLeft }, 'released buffered events')
return true
}
@@ -157,7 +164,7 @@ export const makeEventBuffer = (logger: ILogger): BaileysBufferableEventEmitter
},
on: (...args) => ev.on(...args),
off: (...args) => ev.off(...args),
removeAllListeners: (...args) => ev.removeAllListeners(...args),
removeAllListeners: (...args) => ev.removeAllListeners(...args)
}
}
@@ -306,7 +313,6 @@ function append<E extends BufferableEvent>(
if (data.chatUpserts[chatId]) {
delete data.chatUpserts[chatId]
}
if (data.historySets.chats[chatId]) {
@@ -382,9 +388,7 @@ function append<E extends BufferableEvent>(
} else {
data.messageUpserts[key] = {
message,
type: type === 'notify' || data.messageUpserts[key]?.type === 'notify'
? 'notify'
: type
type: type === 'notify' || data.messageUpserts[key]?.type === 'notify' ? 'notify' : type
}
}
}
@@ -419,7 +423,6 @@ function append<E extends BufferableEvent>(
const keyStr = stringifyMessageKey(key)
if (!data.messageDeletes[keyStr]) {
data.messageDeletes[keyStr] = key
}
if (data.messageUpserts[keyStr]) {
@@ -443,8 +446,7 @@ function append<E extends BufferableEvent>(
if (existing) {
updateMessageWithReaction(existing.message, reaction)
} else {
data.messageReactions[keyStr] = data.messageReactions[keyStr]
|| { key, reactions: [] }
data.messageReactions[keyStr] = data.messageReactions[keyStr] || { key, reactions: [] }
updateMessageWithReaction(data.messageReactions[keyStr], reaction)
}
}
@@ -458,8 +460,7 @@ function append<E extends BufferableEvent>(
if (existing) {
updateMessageWithReceipt(existing.message, receipt)
} else {
data.messageReceipts[keyStr] = data.messageReceipts[keyStr]
|| { key, userReceipt: [] }
data.messageReceipts[keyStr] = data.messageReceipts[keyStr] || { key, userReceipt: [] }
updateMessageWithReceipt(data.messageReceipts[keyStr], receipt)
}
}
@@ -472,7 +473,6 @@ function append<E extends BufferableEvent>(
const groupUpdate = data.groupUpdates[id] || {}
if (!data.groupUpdates[id]) {
data.groupUpdates[id] = Object.assign(groupUpdate, update)
}
}
@@ -504,10 +504,10 @@ function append<E extends BufferableEvent>(
const chatId = message.key.remoteJid!
const chat = data.chatUpdates[chatId] || data.chatUpserts[chatId]
if (
isRealMessage(message, '')
&& shouldIncrementChatUnread(message)
&& typeof chat?.unreadCount === 'number'
&& chat.unreadCount > 0
isRealMessage(message, '') &&
shouldIncrementChatUnread(message) &&
typeof chat?.unreadCount === 'number' &&
chat.unreadCount > 0
) {
logger.debug({ chatId: chat.id }, 'decrementing chat counter')
chat.unreadCount -= 1
@@ -567,15 +567,15 @@ function consolidateEvents(data: BufferedEventData) {
map['messages.delete'] = { keys: messageDeleteList }
}
const messageReactionList = Object.values(data.messageReactions).flatMap(
({ key, reactions }) => reactions.flatMap(reaction => ({ key, reaction }))
const messageReactionList = Object.values(data.messageReactions).flatMap(({ key, reactions }) =>
reactions.flatMap(reaction => ({ key, reaction }))
)
if (messageReactionList.length) {
map['messages.reaction'] = messageReactionList
}
const messageReceiptList = Object.values(data.messageReceipts).flatMap(
({ key, userReceipt }) => userReceipt.flatMap(receipt => ({ key, receipt }))
const messageReceiptList = Object.values(data.messageReceipts).flatMap(({ key, userReceipt }) =>
userReceipt.flatMap(receipt => ({ key, receipt }))
)
if (messageReceiptList.length) {
map['message-receipt.update'] = messageReceiptList
@@ -600,8 +600,10 @@ function consolidateEvents(data: BufferedEventData) {
}
function concatChats<C extends Partial<Chat>>(a: C, b: Partial<Chat>) {
if(b.unreadCount === null && // neutralize unread counter
a.unreadCount! < 0) {
if (
b.unreadCount === null && // neutralize unread counter
a.unreadCount! < 0
) {
a.unreadCount = undefined
b.unreadCount = undefined
}

View File

@@ -4,26 +4,34 @@ import { createHash, randomBytes } from 'crypto'
import { platform, release } from 'os'
import { proto } from '../../WAProto'
import { version as baileysVersion } from '../Defaults/baileys-version.json'
import { BaileysEventEmitter, BaileysEventMap, BrowsersMap, ConnectionState, DisconnectReason, WACallUpdateType, WAVersion } from '../Types'
import {
BaileysEventEmitter,
BaileysEventMap,
BrowsersMap,
ConnectionState,
DisconnectReason,
WACallUpdateType,
WAVersion
} from '../Types'
import { BinaryNode, getAllBinaryNodeChildren, jidDecode } from '../WABinary'
const PLATFORM_MAP = {
'aix': 'AIX',
'darwin': 'Mac OS',
'win32': 'Windows',
'android': 'Android',
'freebsd': 'FreeBSD',
'openbsd': 'OpenBSD',
'sunos': 'Solaris'
aix: 'AIX',
darwin: 'Mac OS',
win32: 'Windows',
android: 'Android',
freebsd: 'FreeBSD',
openbsd: 'OpenBSD',
sunos: 'Solaris'
}
export const Browsers: BrowsersMap = {
ubuntu: (browser) => ['Ubuntu', browser, '22.04.4'],
macOS: (browser) => ['Mac OS', browser, '14.4.1'],
baileys: (browser) => ['Baileys', browser, '6.5.0'],
windows: (browser) => ['Windows', browser, '10.0.22631'],
ubuntu: browser => ['Ubuntu', browser, '22.04.4'],
macOS: browser => ['Mac OS', browser, '14.4.1'],
baileys: browser => ['Baileys', browser, '6.5.0'],
windows: browser => ['Windows', browser, '10.0.22631'],
/** The appropriate browser based on your OS & release */
appropriate: (browser) => [ PLATFORM_MAP[platform()] || 'Ubuntu', browser, release() ]
appropriate: browser => [PLATFORM_MAP[platform()] || 'Ubuntu', browser, release()]
}
export const getPlatformId = (browser: string) => {
@@ -52,12 +60,8 @@ export const BufferJSON = {
}
}
export const getKeyAuthor = (
key: proto.IMessageKey | undefined | null,
meId = 'me'
) => (
export const getKeyAuthor = (key: proto.IMessageKey | undefined | null, meId = 'me') =>
(key?.fromMe ? meId : key?.participant || key?.remoteJid) || ''
)
export const writeRandomPadMax16 = (msg: Uint8Array) => {
const pad = randomBytes(1)
@@ -83,11 +87,7 @@ export const unpadRandomMax16 = (e: Uint8Array | Buffer) => {
return new Uint8Array(t.buffer, t.byteOffset, t.length - r)
}
export const encodeWAMessage = (message: proto.IMessage) => (
writeRandomPadMax16(
proto.Message.encode(message).finish()
)
)
export const encodeWAMessage = (message: proto.IMessage) => writeRandomPadMax16(proto.Message.encode(message).finish())
export const generateRegistrationId = (): number => {
return Uint16Array.from(randomBytes(2))[0] & 16383
@@ -104,7 +104,8 @@ export const encodeBigEndian = (e: number, t = 4) => {
return a
}
export const toNumber = (t: Long | number | null | undefined): number => ((typeof t === 'object' && t) ? ('toNumber' in t ? t.toNumber() : (t as Long).low) : t || 0)
export const toNumber = (t: Long | number | null | undefined): number =>
typeof t === 'object' && t ? ('toNumber' in t ? t.toNumber() : (t as Long).low) : t || 0
/** unix timestamp of a date in seconds */
export const unixTimestampSeconds = (date: Date = new Date()) => Math.floor(date.getTime() / 1000)
@@ -124,8 +125,8 @@ export const debouncedTimeout = (intervalMs = 1000, task?: () => void) => {
timeout && clearTimeout(timeout)
timeout = undefined
},
setTask: (newTask: () => void) => task = newTask,
setInterval: (newInterval: number) => intervalMs = newInterval
setTask: (newTask: () => void) => (task = newTask),
setInterval: (newInterval: number) => (intervalMs = newInterval)
}
}
@@ -154,7 +155,10 @@ export const delayCancellable = (ms: number) => {
return { delay, cancel }
}
export async function promiseTimeout<T>(ms: number | undefined, promise: (resolve: (v: T) => void, reject: (error) => void) => void) {
export async function promiseTimeout<T>(
ms: number | undefined,
promise: (resolve: (v: T) => void, reject: (error) => void) => void
) {
if (!ms) {
return new Promise(promise)
}
@@ -164,19 +168,20 @@ export async function promiseTimeout<T>(ms: number | undefined, promise: (resolv
const { delay, cancel } = delayCancellable(ms)
const p = new Promise((resolve, reject) => {
delay
.then(() => reject(
.then(() =>
reject(
new Boom('Timed Out', {
statusCode: DisconnectReason.timedOut,
data: {
stack
}
})
))
)
)
.catch(err => reject(err))
promise(resolve, reject)
})
.finally (cancel)
}).finally(cancel)
return p as Promise<T>
}
@@ -208,34 +213,27 @@ export function bindWaitForEvent<T extends keyof BaileysEventMap>(ev: BaileysEve
return async (check: (u: BaileysEventMap[T]) => Promise<boolean | undefined>, timeoutMs?: number) => {
let listener: (item: BaileysEventMap[T]) => void
let closeListener: (state: Partial<ConnectionState>) => void
await (
promiseTimeout<void>(
timeoutMs,
(resolve, reject) => {
await promiseTimeout<void>(timeoutMs, (resolve, reject) => {
closeListener = ({ connection, lastDisconnect }) => {
if (connection === 'close') {
reject(
lastDisconnect?.error
|| new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
lastDisconnect?.error || new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
)
}
}
ev.on('connection.update', closeListener)
listener = async(update) => {
listener = async update => {
if (await check(update)) {
resolve()
}
}
ev.on(event, listener)
}
)
.finally(() => {
}).finally(() => {
ev.off(event, listener)
ev.off('connection.update', closeListener)
})
)
}
}
@@ -248,13 +246,10 @@ export const bindWaitForConnectionUpdate = (ev: BaileysEventEmitter) => bindWait
export const fetchLatestBaileysVersion = async (options: AxiosRequestConfig<{}> = {}) => {
const URL = 'https://raw.githubusercontent.com/WhiskeySockets/Baileys/master/src/Defaults/baileys-version.json'
try {
const result = await axios.get<{ version: WAVersion }>(
URL,
{
const result = await axios.get<{ version: WAVersion }>(URL, {
...options,
responseType: 'json'
}
)
})
return {
version: result.data.version,
isLatest: true
@@ -274,13 +269,10 @@ export const fetchLatestBaileysVersion = async(options: AxiosRequestConfig<{}> =
*/
export const fetchLatestWaWebVersion = async (options: AxiosRequestConfig<{}>) => {
try {
const { data } = await axios.get(
'https://web.whatsapp.com/sw.js',
{
const { data } = await axios.get('https://web.whatsapp.com/sw.js', {
...options,
responseType: 'json'
}
)
})
const regex = /\\?"client_revision\\?":\s*(\d+)/
const match = data.match(regex)
@@ -317,9 +309,9 @@ export const generateMdTagPrefix = () => {
}
const STATUS_MAP: { [_: string]: proto.WebMessageInfo.Status } = {
'sender': proto.WebMessageInfo.Status.SERVER_ACK,
'played': proto.WebMessageInfo.Status.PLAYED,
'read': proto.WebMessageInfo.Status.READ,
sender: proto.WebMessageInfo.Status.SERVER_ACK,
played: proto.WebMessageInfo.Status.PLAYED,
read: proto.WebMessageInfo.Status.READ,
'read-self': proto.WebMessageInfo.Status.READ
}
/**
@@ -399,9 +391,10 @@ export const getCodeFromWSError = (error: Error) => {
}
} else if (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(error as any)?.code?.startsWith('E')
|| error?.message?.includes('timed out')
) { // handle ETIMEOUT, ENOTFOUND etc
(error as any)?.code?.startsWith('E') ||
error?.message?.includes('timed out')
) {
// handle ETIMEOUT, ENOTFOUND etc
statusCode = 408
}

View File

@@ -10,10 +10,7 @@ import { downloadContentFromMessage } from './messages-media'
const inflatePromise = promisify(inflate)
export const downloadHistory = async(
msg: proto.Message.IHistorySyncNotification,
options: AxiosRequestConfig<{}>
) => {
export const downloadHistory = async (msg: proto.Message.IHistorySyncNotification, options: AxiosRequestConfig<{}>) => {
const stream = await downloadContentFromMessage(msg, 'md-msg-hist', { options })
const bufferArray: Buffer[] = []
for await (const chunk of stream) {
@@ -62,14 +59,13 @@ export const processHistoryMessage = (item: proto.IHistorySync) => {
}
if (
(message.messageStubType === WAMessageStubType.BIZ_PRIVACY_MODE_TO_BSP
|| message.messageStubType === WAMessageStubType.BIZ_PRIVACY_MODE_TO_FB
)
&& message.messageStubParameters?.[0]
(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],
verifiedName: message.messageStubParameters?.[0]
})
}
}

View File

@@ -7,10 +7,7 @@ import { extractImageThumb, getHttpStream } from './messages-media'
const THUMBNAIL_WIDTH_PX = 192
/** Fetches an image and generates a thumbnail for it */
const getCompressedJpegThumbnail = async(
url: string,
{ thumbnailWidth, fetchOpts }: URLGenerationOptions
) => {
const getCompressedJpegThumbnail = async (url: string, { thumbnailWidth, fetchOpts }: URLGenerationOptions) => {
const stream = await getHttpStream(url, fetchOpts)
const result = await extractImageThumb(stream, thumbnailWidth)
return result
@@ -39,7 +36,7 @@ export const getUrlInfo = async(
opts: URLGenerationOptions = {
thumbnailWidth: THUMBNAIL_WIDTH_PX,
fetchOpts: { timeout: 3000 }
},
}
): Promise<WAUrlInfo | undefined> => {
try {
// retries
@@ -63,9 +60,9 @@ export const getUrlInfo = async(
}
if (
forwardedURLObj.hostname === urlObj.hostname
|| forwardedURLObj.hostname === 'www.' + urlObj.hostname
|| 'www.' + forwardedURLObj.hostname === urlObj.hostname
forwardedURLObj.hostname === urlObj.hostname ||
forwardedURLObj.hostname === 'www.' + urlObj.hostname ||
'www.' + forwardedURLObj.hostname === urlObj.hostname
) {
retries + 1
return true
@@ -95,20 +92,13 @@ export const getUrlInfo = async(
options: opts.fetchOpts
}
)
urlInfo.jpegThumbnail = imageMessage?.jpegThumbnail
? Buffer.from(imageMessage.jpegThumbnail)
: undefined
urlInfo.jpegThumbnail = imageMessage?.jpegThumbnail ? Buffer.from(imageMessage.jpegThumbnail) : undefined
urlInfo.highQualityThumbnail = imageMessage || undefined
} else {
try {
urlInfo.jpegThumbnail = image
? (await getCompressedJpegThumbnail(image, opts)).buffer
: undefined
urlInfo.jpegThumbnail = image ? (await getCompressedJpegThumbnail(image, opts)).buffer : undefined
} catch (error) {
opts.logger?.debug(
{ err: error.stack, url: previewLink },
'error in generating thumbnail'
)
opts.logger?.debug({ err: error.stack, url: previewLink }, 'error in generating thumbnail')
}
}

View File

@@ -9,7 +9,6 @@ import { hkdf } from './crypto'
const o = 128
class d {
salt: string
constructor(e: string) {
@@ -38,19 +37,19 @@ class d {
async _addSingle(e, t) {
var r = this
const n = new Uint8Array(await hkdf(Buffer.from(t), o, { info: r.salt })).buffer
return r.performPointwiseWithOverflow(await e, n, ((e, t) => e + t))
return r.performPointwiseWithOverflow(await e, n, (e, t) => e + t)
}
async _subtractSingle(e, t) {
var r = this
const n = new Uint8Array(await hkdf(Buffer.from(t), o, { info: r.salt })).buffer
return r.performPointwiseWithOverflow(await e, n, ((e, t) => e - t))
return r.performPointwiseWithOverflow(await e, n, (e, t) => e - t)
}
performPointwiseWithOverflow(e, t, r) {
const n = new DataView(e)
, i = new DataView(t)
, a = new ArrayBuffer(n.byteLength)
, s = new DataView(a)
const n = new DataView(e),
i = new DataView(t),
a = new ArrayBuffer(n.byteLength),
s = new DataView(a)
for (let e = 0; e < n.byteLength; e += 2) {
s.setUint16(e, r(n.getUint16(e, !0), i.getUint16(e, !0)), !0)
}

View File

@@ -24,7 +24,7 @@ export const makeMutex = () => {
// we replace the existing task, appending the new piece of execution to it
// so the next task will have to wait for this one to finish
return task
},
}
}
}

View File

@@ -11,7 +11,20 @@ import { Readable, Transform } from 'stream'
import { URL } from 'url'
import { proto } from '../../WAProto'
import { DEFAULT_ORIGIN, MEDIA_HKDF_KEY_MAPPING, MEDIA_PATH_MAP } from '../Defaults'
import { BaileysEventMap, DownloadableMessage, MediaConnInfo, MediaDecryptionKeyInfo, MediaType, MessageType, SocketConfig, WAGenericMediaMessage, WAMediaPayloadURL, WAMediaUpload, WAMediaUploadFunction, WAMessageContent } from '../Types'
import {
BaileysEventMap,
DownloadableMessage,
MediaConnInfo,
MediaDecryptionKeyInfo,
MediaType,
MessageType,
SocketConfig,
WAGenericMediaMessage,
WAMediaPayloadURL,
WAMediaUpload,
WAMediaUploadFunction,
WAMessageContent
} from '../Types'
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildBuffer, jidNormalizedUser } from '../WABinary'
import { aesDecryptGCM, aesEncryptGCM, hkdf } from './crypto'
import { generateMessageIDV2 } from './generics'
@@ -22,17 +35,11 @@ const getTmpFilesDirectory = () => tmpdir()
const getImageProcessingLibrary = async () => {
const [_jimp, sharp] = await Promise.all([
(async () => {
const jimp = await (
import('jimp')
.catch(() => { })
)
const jimp = await import('jimp').catch(() => {})
return jimp
})(),
(async () => {
const sharp = await (
import('sharp')
.catch(() => { })
)
const sharp = await import('sharp').catch(() => {})
return sharp
})()
])
@@ -55,7 +62,10 @@ export const hkdfInfoKey = (type: MediaType) => {
}
/** generates all the keys required to encrypt/decrypt & sign a media message */
export async function getMediaKeys(buffer: Uint8Array | string | null | undefined, mediaType: MediaType): Promise<MediaDecryptionKeyInfo> {
export async function getMediaKeys(
buffer: Uint8Array | string | null | undefined,
mediaType: MediaType
): Promise<MediaDecryptionKeyInfo> {
if (!buffer) {
throw new Boom('Cannot derive from empty media key')
}
@@ -69,7 +79,7 @@ export async function getMediaKeys(buffer: Uint8Array | string | null | undefine
return {
iv: expandedMediaKey.slice(0, 16),
cipherKey: expandedMediaKey.slice(16, 48),
macKey: expandedMediaKey.slice(48, 80),
macKey: expandedMediaKey.slice(48, 80)
}
}
@@ -78,10 +88,11 @@ const extractVideoThumb = async(
path: string,
destPath: string,
time: string,
size: { width: number, height: number },
) => new Promise<void>((resolve, reject) => {
size: { width: number; height: number }
) =>
new Promise<void>((resolve, reject) => {
const cmd = `ffmpeg -ss ${time} -i ${path} -y -vf scale=${size.width}:-1 -vframes 1 -f image2 ${destPath}`
exec(cmd, (err) => {
exec(cmd, err => {
if (err) {
reject(err)
} else {
@@ -100,16 +111,13 @@ export const extractImageThumb = async(bufferOrFilePath: Readable | Buffer | str
const img = lib.sharp.default(bufferOrFilePath)
const dimensions = await img.metadata()
const buffer = await img
.resize(width)
.jpeg({ quality: 50 })
.toBuffer()
const buffer = await img.resize(width).jpeg({ quality: 50 }).toBuffer()
return {
buffer,
original: {
width: dimensions.width,
height: dimensions.height,
},
height: dimensions.height
}
}
} else if ('jimp' in lib && typeof lib.jimp?.read === 'function') {
const { read, MIME_JPEG, RESIZE_BILINEAR, AUTO } = lib.jimp
@@ -119,10 +127,7 @@ export const extractImageThumb = async(bufferOrFilePath: Readable | Buffer | str
width: jimp.getWidth(),
height: jimp.getHeight()
}
const buffer = await jimp
.quality(50)
.resize(width, AUTO, RESIZE_BILINEAR)
.getBufferAsync(MIME_JPEG)
const buffer = await jimp.quality(50).resize(width, AUTO, RESIZE_BILINEAR).getBufferAsync(MIME_JPEG)
return {
buffer,
original: dimensions
@@ -132,14 +137,8 @@ export const extractImageThumb = async(bufferOrFilePath: Readable | Buffer | str
}
}
export const encodeBase64EncodedStringForUpload = (b64: string) => (
encodeURIComponent(
b64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/\=+$/, '')
)
)
export const encodeBase64EncodedStringForUpload = (b64: string) =>
encodeURIComponent(b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, ''))
export const generateProfilePicture = async (mediaUpload: WAMediaUpload) => {
let bufferOrFilePath: Buffer | string
@@ -154,10 +153,11 @@ export const generateProfilePicture = async(mediaUpload: WAMediaUpload) => {
const lib = await getImageProcessingLibrary()
let img: Promise<Buffer>
if ('sharp' in lib && typeof lib.sharp?.default === 'function') {
img = lib.sharp.default(bufferOrFilePath)
img = lib.sharp
.default(bufferOrFilePath)
.resize(640, 640)
.jpeg({
quality: 50,
quality: 50
})
.toBuffer()
} else if ('jimp' in lib && typeof lib.jimp?.read === 'function') {
@@ -166,16 +166,13 @@ export const generateProfilePicture = async(mediaUpload: WAMediaUpload) => {
const min = Math.min(jimp.getWidth(), jimp.getHeight())
const cropped = jimp.crop(0, 0, min, min)
img = cropped
.quality(50)
.resize(640, 640, RESIZE_BILINEAR)
.getBufferAsync(MIME_JPEG)
img = cropped.quality(50).resize(640, 640, RESIZE_BILINEAR).getBufferAsync(MIME_JPEG)
} else {
throw new Boom('No image processing library available')
}
return {
img: await img,
img: await img
}
}
@@ -209,7 +206,7 @@ export async function getAudioDuration(buffer: Buffer | string | Readable) {
*/
export async function getAudioWaveform(buffer: Buffer | string | Readable, logger?: ILogger) {
try {
const { default: decoder } = await eval('import(\'audio-decode\')')
const { default: decoder } = await eval("import('audio-decode')")
let audioData: Buffer
if (Buffer.isBuffer(buffer)) {
audioData = buffer
@@ -238,12 +235,10 @@ export async function getAudioWaveform(buffer: Buffer | string | Readable, logge
// This guarantees that the largest data point will be set to 1, and the rest of the data will scale proportionally.
const multiplier = Math.pow(Math.max(...filteredData), -1)
const normalizedData = filteredData.map((n) => n * multiplier)
const normalizedData = filteredData.map(n => n * multiplier)
// Generate waveform like WhatsApp
const waveform = new Uint8Array(
normalizedData.map((n) => Math.floor(100 * n))
)
const waveform = new Uint8Array(normalizedData.map(n => Math.floor(100 * n)))
return waveform
} catch (e) {
@@ -251,7 +246,6 @@ export async function getAudioWaveform(buffer: Buffer | string | Readable, logge
}
}
export const toReadable = (buffer: Buffer) => {
const readable = new Readable({ read: () => {} })
readable.push(buffer)
@@ -294,14 +288,14 @@ export async function generateThumbnail(
}
) {
let thumbnail: string | undefined
let originalImageDimensions: { width: number, height: number } | undefined
let originalImageDimensions: { width: number; height: number } | undefined
if (mediaType === 'image') {
const { buffer, original } = await extractImageThumb(file)
thumbnail = buffer.toString('base64')
if (original.width && original.height) {
originalImageDimensions = {
width: original.width,
height: original.height,
height: original.height
}
}
} else if (mediaType === 'video') {
@@ -368,17 +362,10 @@ export const encryptedStream = async(
for await (const data of stream) {
fileLength += data.length
if(
type === 'remote'
&& opts?.maxContentLength
&& fileLength + data.length > opts.maxContentLength
) {
throw new Boom(
`content length exceeded when encrypting "${type}"`,
{
if (type === 'remote' && opts?.maxContentLength && fileLength + data.length > opts.maxContentLength) {
throw new Boom(`content length exceeded when encrypting "${type}"`, {
data: { media, type }
}
)
})
}
sha256Plain = sha256Plain.update(data)
@@ -495,8 +482,8 @@ export const downloadEncryptedContent = async(
const endChunk = endByte ? toSmallestChunkSize(endByte || 0) + AES_CHUNK_SIZE : undefined
const headers: AxiosRequestConfig['headers'] = {
...options?.headers || { },
Origin: DEFAULT_ORIGIN,
...(options?.headers || {}),
Origin: DEFAULT_ORIGIN
}
if (startChunk || endChunk) {
headers.Range = `bytes=${startChunk}-`
@@ -506,15 +493,12 @@ export const downloadEncryptedContent = async(
}
// download the message
const fetched = await getHttpStream(
downloadUrl,
{
...options || { },
const fetched = await getHttpStream(downloadUrl, {
...(options || {}),
headers,
maxBodyLength: Infinity,
maxContentLength: Infinity,
}
)
maxContentLength: Infinity
})
let remainingBytes = Buffer.from([])
@@ -554,7 +538,6 @@ export const downloadEncryptedContent = async(
if (endByte) {
aes.setAutoPadding(false)
}
}
try {
@@ -571,7 +554,7 @@ export const downloadEncryptedContent = async(
} catch (error) {
callback(error)
}
},
}
})
return fetched.pipe(output, { end: true })
}
@@ -580,11 +563,7 @@ export function extensionForMediaMessage(message: WAMessageContent) {
const getExtension = (mimetype: string) => mimetype.split(';')[0].split('/')[1]
const type = Object.keys(message)[0] as MessageType
let extension: string
if(
type === 'locationMessage' ||
type === 'liveLocationMessage' ||
type === 'productMessage'
) {
if (type === 'locationMessage' || type === 'liveLocationMessage' || type === 'productMessage') {
extension = '.jpeg'
} else {
const messageContent = message[type] as WAGenericMediaMessage
@@ -596,13 +575,13 @@ export function extensionForMediaMessage(message: WAMessageContent) {
export const getWAUploadToServer = (
{ customUploadHosts, fetchAgent, logger, options }: SocketConfig,
refreshMediaConn: (force: boolean) => Promise<MediaConnInfo>,
refreshMediaConn: (force: boolean) => Promise<MediaConnInfo>
): WAMediaUploadFunction => {
return async (stream, { mediaType, fileEncSha256B64, timeoutMs }) => {
// send a query JSON to obtain the url & auth token to upload our media
let uploadInfo = await refreshMediaConn(false)
let urls: { mediaUrl: string, directPath: string } | undefined
let urls: { mediaUrl: string; directPath: string } | undefined
const hosts = [...customUploadHosts, ...uploadInfo.hosts]
fileEncSha256B64 = encodeBase64EncodedStringForUpload(fileEncSha256B64)
@@ -615,24 +594,19 @@ export const getWAUploadToServer = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let result: any
try {
const body = await axios.post(
url,
stream,
{
const body = await axios.post(url, stream, {
...options,
headers: {
...options.headers || { },
...(options.headers || {}),
'Content-Type': 'application/octet-stream',
'Origin': DEFAULT_ORIGIN
Origin: DEFAULT_ORIGIN
},
httpsAgent: fetchAgent,
timeout: timeoutMs,
responseType: 'json',
maxBodyLength: Infinity,
maxContentLength: Infinity,
}
)
maxContentLength: Infinity
})
result = body.data
if (result?.url || result?.directPath) {
@@ -651,15 +625,15 @@ export const getWAUploadToServer = (
}
const isLast = hostname === hosts[uploadInfo.hosts.length - 1]?.hostname
logger.warn({ trace: error.stack, uploadResult: result }, `Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}`)
logger.warn(
{ trace: error.stack, uploadResult: result },
`Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}`
)
}
}
if (!urls) {
throw new Boom(
'Media upload failed on all hosts',
{ statusCode: 500 }
)
throw new Boom('Media upload failed on all hosts', { statusCode: 500 })
}
return urls
@@ -673,11 +647,7 @@ const getMediaRetryKey = (mediaKey: Buffer | Uint8Array) => {
/**
* Generate a binary node that will request the phone to re-upload the media & return the newly uploaded URL
*/
export const encryptMediaRetryRequest = async(
key: proto.IMessageKey,
mediaKey: Buffer | Uint8Array,
meId: string
) => {
export const encryptMediaRetryRequest = async (key: proto.IMessageKey, mediaKey: Buffer | Uint8Array, meId: string) => {
const recp: proto.IServerErrorReceipt = { stanzaId: key.id }
const recpBuffer = proto.ServerErrorReceipt.encode(recp).finish()
@@ -708,7 +678,7 @@ export const encryptMediaRetryRequest = async(
tag: 'rmr',
attrs: {
jid: key.remoteJid!,
'from_me': (!!key.fromMe).toString(),
from_me: (!!key.fromMe).toString(),
// @ts-ignore
participant: key.participant || undefined
}
@@ -734,10 +704,10 @@ export const decodeMediaRetryNode = (node: BinaryNode) => {
const errorNode = getBinaryNodeChild(node, 'error')
if (errorNode) {
const errorCode = +errorNode.attrs.code
event.error = new Boom(
`Failed to re-upload media (${errorCode})`,
{ data: errorNode.attrs, statusCode: getStatusCodeForMediaRetry(errorCode) }
)
event.error = new Boom(`Failed to re-upload media (${errorCode})`, {
data: errorNode.attrs,
statusCode: getStatusCodeForMediaRetry(errorCode)
})
} else {
const encryptedInfoNode = getBinaryNodeChild(node, 'encrypt')
const ciphertext = getBinaryNodeChildBuffer(encryptedInfoNode, 'enc_p')
@@ -753,7 +723,7 @@ export const decodeMediaRetryNode = (node: BinaryNode) => {
}
export const decryptMediaRetryData = async (
{ ciphertext, iv }: { ciphertext: Uint8Array, iv: Uint8Array },
{ ciphertext, iv }: { ciphertext: Uint8Array; iv: Uint8Array },
mediaKey: Uint8Array,
msgId: string
) => {
@@ -768,5 +738,5 @@ const MEDIA_RETRY_STATUS_MAP = {
[proto.MediaRetryNotification.ResultType.SUCCESS]: 200,
[proto.MediaRetryNotification.ResultType.DECRYPTION_ERROR]: 412,
[proto.MediaRetryNotification.ResultType.NOT_FOUND]: 404,
[proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418,
[proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418
} as const

View File

@@ -21,13 +21,20 @@ import {
WAMessageContent,
WAMessageStatus,
WAProto,
WATextMessage,
WATextMessage
} from '../Types'
import { isJidGroup, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary'
import { sha256 } from './crypto'
import { generateMessageIDV2, getKeyAuthor, unixTimestampSeconds } from './generics'
import { ILogger } from './logger'
import { downloadContentFromMessage, encryptedStream, generateThumbnail, getAudioDuration, getAudioWaveform, MediaDownloadOptions } from './messages-media'
import {
downloadContentFromMessage,
encryptedStream,
generateThumbnail,
getAudioDuration,
getAudioWaveform,
MediaDownloadOptions
} from './messages-media'
type MediaUploadData = {
media: WAMediaUpload
@@ -51,15 +58,15 @@ const MIMETYPE_MAP: { [T in MediaType]?: string } = {
document: 'application/pdf',
audio: 'audio/ogg; codecs=opus',
sticker: 'image/webp',
'product-catalog-image': 'image/jpeg',
'product-catalog-image': 'image/jpeg'
}
const MessageTypeProto = {
'image': WAProto.Message.ImageMessage,
'video': WAProto.Message.VideoMessage,
'audio': WAProto.Message.AudioMessage,
'sticker': WAProto.Message.StickerMessage,
'document': WAProto.Message.DocumentMessage,
image: WAProto.Message.ImageMessage,
video: WAProto.Message.VideoMessage,
audio: WAProto.Message.AudioMessage,
sticker: WAProto.Message.StickerMessage,
document: WAProto.Message.DocumentMessage
} as const
/**
@@ -69,19 +76,24 @@ const MessageTypeProto = {
*/
export const extractUrlFromText = (text: string) => text.match(URL_REGEX)?.[0]
export const generateLinkPreviewIfRequired = async(text: string, getUrlInfo: MessageGenerationOptions['getUrlInfo'], logger: MessageGenerationOptions['logger']) => {
export const generateLinkPreviewIfRequired = async (
text: string,
getUrlInfo: MessageGenerationOptions['getUrlInfo'],
logger: MessageGenerationOptions['logger']
) => {
const url = extractUrlFromText(text)
if (!!getUrlInfo && url) {
try {
const urlInfo = await getUrlInfo(url)
return urlInfo
} catch(error) { // ignore if fails
} catch (error) {
// ignore if fails
logger?.warn({ trace: error.stack }, 'url generation failed')
}
}
}
const assertColor = async(color) => {
const assertColor = async color => {
let assertedColor
if (typeof color === 'number') {
assertedColor = color > 0 ? color : 0xffffffff + Number(color) + 1
@@ -96,13 +108,10 @@ const assertColor = async(color) => {
}
}
export const prepareWAMessageMedia = async(
message: AnyMediaMessageContent,
options: MediaGenerationOptions
) => {
export const prepareWAMessageMedia = async (message: AnyMediaMessageContent, options: MediaGenerationOptions) => {
const logger = options.logger
let mediaType: typeof MEDIA_KEYS[number] | undefined
let mediaType: (typeof MEDIA_KEYS)[number] | undefined
for (const key of MEDIA_KEYS) {
if (key in message) {
mediaType = key
@@ -119,13 +128,13 @@ export const prepareWAMessageMedia = async(
}
delete uploadData[mediaType]
// check if cacheable + generate cache key
const cacheableKey = typeof uploadData.media === 'object' &&
('url' in uploadData.media) &&
const cacheableKey =
typeof uploadData.media === 'object' &&
'url' in uploadData.media &&
!!uploadData.media.url &&
!!options.mediaCache && (
!!options.mediaCache &&
// generate the key
mediaType + ':' + uploadData.media.url.toString()
)
if (mediaType === 'document' && !uploadData.fileName) {
uploadData.fileName = 'file'
@@ -151,46 +160,37 @@ export const prepareWAMessageMedia = async(
}
const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined'
const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') &&
(typeof uploadData['jpegThumbnail'] === 'undefined')
const requiresThumbnailComputation =
(mediaType === 'image' || mediaType === 'video') && typeof uploadData['jpegThumbnail'] === 'undefined'
const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true
const requiresAudioBackground = options.backgroundColor && mediaType === 'audio' && uploadData.ptt === true
const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation
const {
mediaKey,
encWriteStream,
bodyPath,
fileEncSha256,
fileSha256,
fileLength,
didSaveToTmpPath
} = await encryptedStream(
uploadData.media,
options.mediaTypeOverride || mediaType,
{
const { mediaKey, encWriteStream, bodyPath, fileEncSha256, fileSha256, fileLength, didSaveToTmpPath } =
await encryptedStream(uploadData.media, options.mediaTypeOverride || mediaType, {
logger,
saveOriginalFileIfRequired: requiresOriginalForSomeProcessing,
opts: options.options
}
)
})
// url safe Base64 encode the SHA256 hash of the body
const fileEncSha256B64 = fileEncSha256.toString('base64')
const [{ mediaUrl, directPath }] = await Promise.all([
(async () => {
const result = await options.upload(
encWriteStream,
{ fileEncSha256B64, mediaType, timeoutMs: options.mediaUploadTimeoutMs }
)
const result = await options.upload(encWriteStream, {
fileEncSha256B64,
mediaType,
timeoutMs: options.mediaUploadTimeoutMs
})
logger?.debug({ mediaType, cacheableKey }, 'uploaded media')
return result
})(),
(async () => {
try {
if (requiresThumbnailComputation) {
const {
thumbnail,
originalImageDimensions
} = await generateThumbnail(bodyPath!, mediaType as 'image' | 'video', options)
const { thumbnail, originalImageDimensions } = await generateThumbnail(
bodyPath!,
mediaType as 'image' | 'video',
options
)
uploadData.jpegThumbnail = thumbnail
if (!uploadData.width && originalImageDimensions) {
uploadData.width = originalImageDimensions.width
@@ -218,10 +218,8 @@ export const prepareWAMessageMedia = async(
} catch (error) {
logger?.warn({ trace: error.stack }, 'failed to obtain extra info')
}
})(),
])
.finally(
async() => {
})()
]).finally(async () => {
encWriteStream.destroy()
// remove tmp files
if (didSaveToTmpPath && bodyPath) {
@@ -233,12 +231,10 @@ export const prepareWAMessageMedia = async(
logger?.warn('failed to remove tmp file')
}
}
}
)
})
const obj = WAProto.Message.fromObject({
[`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject(
{
[`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
url: mediaUrl,
directPath,
mediaKey,
@@ -248,8 +244,7 @@ export const prepareWAMessageMedia = async(
mediaKeyTimestamp: unixTimestampSeconds(),
...uploadData,
media: undefined
}
)
})
})
if (uploadData.ptv) {
@@ -285,10 +280,7 @@ export const prepareDisappearingMessageSettingContent = (ephemeralExpiration?: n
* @param message the message to forward
* @param options.forceForward will show the message as forwarded even if it is from you
*/
export const generateForwardMessageContent = (
message: WAMessage,
forceForward?: boolean
) => {
export const generateForwardMessageContent = (message: WAMessage, forceForward?: boolean) => {
let content = message.message
if (!content) {
throw new Boom('no content in message', { statusCode: 400 })
@@ -384,14 +376,14 @@ export const generateWAMessageContent = async(
type: WAProto.Message.ProtocolMessage.Type.REVOKE
}
} else if ('forward' in message) {
m = generateForwardMessageContent(
message.forward,
message.force
)
m = generateForwardMessageContent(message.forward, message.force)
} else if ('disappearingMessagesInChat' in message) {
const exp = typeof message.disappearingMessagesInChat === 'boolean' ?
(message.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) :
message.disappearingMessagesInChat
const exp =
typeof message.disappearingMessagesInChat === 'boolean'
? message.disappearingMessagesInChat
? WA_DEFAULT_EPHEMERAL
: 0
: message.disappearingMessagesInChat
m = prepareDisappearingMessageSettingContent(exp)
} else if ('groupInvite' in message) {
m.groupInviteMessage = {}
@@ -427,33 +419,27 @@ export const generateWAMessageContent = async(
m.templateButtonReplyMessage = {
selectedDisplayText: message.buttonReply.displayText,
selectedId: message.buttonReply.id,
selectedIndex: message.buttonReply.index,
selectedIndex: message.buttonReply.index
}
break
case 'plain':
m.buttonsResponseMessage = {
selectedButtonId: message.buttonReply.id,
selectedDisplayText: message.buttonReply.displayText,
type: proto.Message.ButtonsResponseMessage.Type.DISPLAY_TEXT,
type: proto.Message.ButtonsResponseMessage.Type.DISPLAY_TEXT
}
break
}
} else if ('ptv' in message && message.ptv) {
const { videoMessage } = await prepareWAMessageMedia(
{ video: message.video },
options
)
const { videoMessage } = await prepareWAMessageMedia({ video: message.video }, options)
m.ptvMessage = videoMessage
} else if ('product' in message) {
const { imageMessage } = await prepareWAMessageMedia(
{ image: message.product.productImage },
options
)
const { imageMessage } = await prepareWAMessageMedia({ image: message.product.productImage }, options)
m.productMessage = WAProto.Message.ProductMessage.fromObject({
...message,
product: {
...message.product,
productImage: imageMessage,
productImage: imageMessage
}
})
} else if ('listReply' in message) {
@@ -466,25 +452,21 @@ export const generateWAMessageContent = async(
throw new Boom('Invalid poll values', { statusCode: 400 })
}
if(
message.poll.selectableCount < 0
|| message.poll.selectableCount > message.poll.values.length
) {
throw new Boom(
`poll.selectableCount in poll should be >= 0 and <= ${message.poll.values.length}`,
{ statusCode: 400 }
)
if (message.poll.selectableCount < 0 || message.poll.selectableCount > message.poll.values.length) {
throw new Boom(`poll.selectableCount in poll should be >= 0 and <= ${message.poll.values.length}`, {
statusCode: 400
})
}
m.messageContextInfo = {
// encKey
messageSecret: message.poll.messageSecret || randomBytes(32),
messageSecret: message.poll.messageSecret || randomBytes(32)
}
const pollCreationMessage = {
name: message.poll.name,
selectableOptionsCount: message.poll.selectableCount,
options: message.poll.values.map(optionName => ({ optionName })),
options: message.poll.values.map(optionName => ({ optionName }))
}
if (message.poll.toAnnouncementGroup) {
@@ -506,10 +488,7 @@ export const generateWAMessageContent = async(
} else if ('requestPhoneNumber' in message) {
m.requestPhoneNumberMessage = {}
} else {
m = await prepareWAMessageMedia(
message,
options
)
m = await prepareWAMessageMedia(message, options)
}
if ('viewOnce' in message && !!message.viewOnce) {
@@ -559,7 +538,9 @@ export const generateWAMessageFromContent = (
const { quoted, userJid } = options
if (quoted) {
const participant = quoted.key.fromMe ? userJid : (quoted.participant || quoted.key.participant || quoted.key.remoteJid)
const participant = quoted.key.fromMe
? userJid
: quoted.participant || quoted.key.participant || quoted.key.remoteJid
let quotedMsg = normalizeMessageContent(quoted.message)!
const msgType = getContentType(quotedMsg)!
@@ -595,7 +576,7 @@ export const generateWAMessageFromContent = (
) {
innerMessage[key].contextInfo = {
...(innerMessage[key].contextInfo || {}),
expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL,
expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL
//ephemeralSettingTimestamp: options.ephemeralOptions.eph_setting_ts?.toString()
}
}
@@ -606,7 +587,7 @@ export const generateWAMessageFromContent = (
key: {
remoteJid: jid,
fromMe: true,
id: options?.messageId || generateMessageIDV2(),
id: options?.messageId || generateMessageIDV2()
},
message: message,
messageTimestamp: timestamp,
@@ -617,21 +598,10 @@ export const generateWAMessageFromContent = (
return WAProto.WebMessageInfo.fromObject(messageJSON)
}
export const generateWAMessage = async(
jid: string,
content: AnyMessageContent,
options: MessageGenerationOptions,
) => {
export const generateWAMessage = async (jid: string, content: AnyMessageContent, options: MessageGenerationOptions) => {
// ensure msg ID is with every log
options.logger = options?.logger?.child({ msgId: options.messageId })
return generateWAMessageFromContent(
jid,
await generateWAMessageContent(
content,
options
),
options
)
return generateWAMessageFromContent(jid, await generateWAMessageContent(content, options), options)
}
/** Get the key to access the true type of content */
@@ -668,12 +638,12 @@ export const normalizeMessageContent = (content: WAMessageContent | null | undef
function getFutureProofMessage(message: typeof content) {
return (
message?.ephemeralMessage
|| message?.viewOnceMessage
|| message?.documentWithCaptionMessage
|| message?.viewOnceMessageV2
|| message?.viewOnceMessageV2Extension
|| message?.editedMessage
message?.ephemeralMessage ||
message?.viewOnceMessage ||
message?.documentWithCaptionMessage ||
message?.viewOnceMessageV2 ||
message?.viewOnceMessageV2Extension ||
message?.editedMessage
)
}
}
@@ -683,7 +653,9 @@ export const normalizeMessageContent = (content: WAMessageContent | null | undef
* Eg. extracts the inner message from a disappearing message/view once message
*/
export const extractMessageContent = (content: WAMessageContent | undefined | null): WAMessageContent | undefined => {
const extractFromTemplateMessage = (msg: proto.Message.TemplateMessage.IHydratedFourRowTemplate | proto.Message.IButtonsMessage) => {
const extractFromTemplateMessage = (
msg: proto.Message.TemplateMessage.IHydratedFourRowTemplate | proto.Message.IButtonsMessage
) => {
if (msg.imageMessage) {
return { imageMessage: msg.imageMessage }
} else if (msg.documentMessage) {
@@ -695,9 +667,7 @@ export const extractMessageContent = (content: WAMessageContent | undefined | nu
} else {
return {
conversation:
'contentText' in msg
? msg.contentText
: ('hydratedContentText' in msg ? msg.hydratedContentText : '')
'contentText' in msg ? msg.contentText : 'hydratedContentText' in msg ? msg.hydratedContentText : ''
}
}
}
@@ -726,11 +696,16 @@ export const extractMessageContent = (content: WAMessageContent | undefined | nu
/**
* Returns the device predicted by message ID
*/
export const getDevice = (id: string) => /^3A.{18}$/.test(id) ? 'ios' :
/^3E.{20}$/.test(id) ? 'web' :
/^(.{21}|.{32})$/.test(id) ? 'android' :
/^(3F|.{18}$)/.test(id) ? 'desktop' :
'unknown'
export const getDevice = (id: string) =>
/^3A.{18}$/.test(id)
? 'ios'
: /^3E.{20}$/.test(id)
? 'web'
: /^(.{21}|.{32})$/.test(id)
? 'android'
: /^(3F|.{18}$)/.test(id)
? 'desktop'
: 'unknown'
/** Upserts a receipt in the message */
export const updateMessageWithReceipt = (msg: Pick<WAMessage, 'userReceipt'>, receipt: MessageUserReceipt) => {
@@ -747,8 +722,7 @@ export const updateMessageWithReceipt = (msg: Pick<WAMessage, 'userReceipt'>, re
export const updateMessageWithReaction = (msg: Pick<WAMessage, 'reactions'>, reaction: proto.IReaction) => {
const authorID = getKeyAuthor(reaction.key)
const reactions = (msg.reactions || [])
.filter(r => getKeyAuthor(r.key) !== authorID)
const reactions = (msg.reactions || []).filter(r => getKeyAuthor(r.key) !== authorID)
if (reaction.text) {
reactions.push(reaction)
}
@@ -757,14 +731,10 @@ export const updateMessageWithReaction = (msg: Pick<WAMessage, 'reactions'>, rea
}
/** Update the message with a new poll update */
export const updateMessageWithPollUpdate = (
msg: Pick<WAMessage, 'pollUpdates'>,
update: proto.IPollUpdate
) => {
export const updateMessageWithPollUpdate = (msg: Pick<WAMessage, 'pollUpdates'>, update: proto.IPollUpdate) => {
const authorID = getKeyAuthor(update.pollUpdateMessageKey)
const reactions = (msg.pollUpdates || [])
.filter(r => getKeyAuthor(r.pollUpdateMessageKey) !== authorID)
const reactions = (msg.pollUpdates || []).filter(r => getKeyAuthor(r.pollUpdateMessageKey) !== authorID)
if (update.vote?.selectedOptions?.length) {
reactions.push(update)
}
@@ -787,15 +757,22 @@ export function getAggregateVotesInPollMessage(
{ message, pollUpdates }: Pick<WAMessage, 'pollUpdates' | 'message'>,
meId?: string
) {
const opts = message?.pollCreationMessage?.options || message?.pollCreationMessageV2?.options || message?.pollCreationMessageV3?.options || []
const voteHashMap = opts.reduce((acc, opt) => {
const opts =
message?.pollCreationMessage?.options ||
message?.pollCreationMessageV2?.options ||
message?.pollCreationMessageV3?.options ||
[]
const voteHashMap = opts.reduce(
(acc, opt) => {
const hash = sha256(Buffer.from(opt.optionName || '')).toString()
acc[hash] = {
name: opt.optionName || '',
voters: []
}
return acc
}, {} as { [_: string]: VoteAggregation })
},
{} as { [_: string]: VoteAggregation }
)
for (const update of pollUpdates || []) {
const { vote } = update
@@ -814,9 +791,7 @@ export function getAggregateVotesInPollMessage(
data = voteHashMap[hash]
}
voteHashMap[hash].voters.push(
getKeyAuthor(update.pollUpdateMessageKey, meId)
)
voteHashMap[hash].voters.push(getKeyAuthor(update.pollUpdateMessageKey, meId))
}
}
@@ -825,7 +800,7 @@ export function getAggregateVotesInPollMessage(
/** Given a list of message keys, aggregates them by chat & sender. Useful for sending read receipts in bulk */
export const aggregateMessageKeysNotFromMe = (keys: proto.IMessageKey[]) => {
const keyMap: { [id: string]: { jid: string, participant: string | undefined, messageIds: string[] } } = { }
const keyMap: { [id: string]: { jid: string; participant: string | undefined; messageIds: string[] } } = {}
for (const { remoteJid, id, participant, fromMe } of keys) {
if (!fromMe) {
const uqKey = `${remoteJid}:${participant || ''}`
@@ -860,10 +835,12 @@ export const downloadMediaMessage = async<Type extends 'buffer' | 'stream'>(
options: MediaDownloadOptions,
ctx?: DownloadMediaMessageContext
) => {
const result = await downloadMsg()
.catch(async(error) => {
if(ctx && axios.isAxiosError(error) && // check if the message requires a reupload
REUPLOAD_REQUIRED_STATUS.includes(error.response?.status!)) {
const result = await downloadMsg().catch(async error => {
if (
ctx &&
axios.isAxiosError(error) && // check if the message requires a reupload
REUPLOAD_REQUIRED_STATUS.includes(error.response?.status!)
) {
ctx.logger.info({ key: message.key }, 'sending reupload media request...')
// request reupload
message = await ctx.reuploadRequest(message)
@@ -918,16 +895,14 @@ export const downloadMediaMessage = async<Type extends 'buffer' | 'stream'>(
/** Checks whether the given message is a media message; if it is returns the inner content */
export const assertMediaContent = (content: proto.IMessage | null | undefined) => {
content = extractMessageContent(content)
const mediaContent = content?.documentMessage
|| content?.imageMessage
|| content?.videoMessage
|| content?.audioMessage
|| content?.stickerMessage
const mediaContent =
content?.documentMessage ||
content?.imageMessage ||
content?.videoMessage ||
content?.audioMessage ||
content?.stickerMessage
if (!mediaContent) {
throw new Boom(
'given message is not a media message',
{ statusCode: 400, data: content }
)
throw new Boom('given message is not a media message', { statusCode: 400, data: content })
}
return mediaContent

View File

@@ -1,6 +1,17 @@
import { AxiosRequestConfig } from 'axios'
import { proto } from '../../WAProto'
import { AuthenticationCreds, BaileysEventEmitter, CacheStore, Chat, GroupMetadata, ParticipantAction, RequestJoinAction, RequestJoinMethod, SignalKeyStoreWithTransaction, WAMessageStubType } from '../Types'
import {
AuthenticationCreds,
BaileysEventEmitter,
CacheStore,
Chat,
GroupMetadata,
ParticipantAction,
RequestJoinAction,
RequestJoinMethod,
SignalKeyStoreWithTransaction,
WAMessageStubType
} from '../Types'
import { getContentType, normalizeMessageContent } from '../Utils/messages'
import { areJidsSameUser, isJidBroadcast, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary'
import { aesDecryptGCM, hmacSign } from './crypto'
@@ -25,9 +36,7 @@ const REAL_MSG_STUB_TYPES = new Set([
WAMessageStubType.CALL_MISSED_VOICE
])
const REAL_MSG_REQ_ME_STUB_TYPES = new Set([
WAMessageStubType.GROUP_PARTICIPANT_ADD
])
const REAL_MSG_REQ_ME_STUB_TYPES = new Set([WAMessageStubType.GROUP_PARTICIPANT_ADD])
/** Cleans a received message to further processing */
export const cleanMessage = (message: proto.IWebMessageInfo, meId: string) => {
@@ -52,9 +61,9 @@ export const cleanMessage = (message: proto.IWebMessageInfo, meId: string) => {
// we've to correct the key to be from them, or some other participant
msgKey.fromMe = !msgKey.fromMe
? areJidsSameUser(msgKey.participant || msgKey.remoteJid!, meId)
// if the message being reacted to, was from them
: // if the message being reacted to, was from them
// fromMe automatically becomes false
: false
false
// set the remoteJid to being the same as the chat the message came from
msgKey.remoteJid = message.key.remoteJid
// set participant of the message
@@ -67,33 +76,26 @@ export const isRealMessage = (message: proto.IWebMessageInfo, meId: string) => {
const normalizedContent = normalizeMessageContent(message.message)
const hasSomeContent = !!getContentType(normalizedContent)
return (
!!normalizedContent
|| REAL_MSG_STUB_TYPES.has(message.messageStubType!)
|| (
REAL_MSG_REQ_ME_STUB_TYPES.has(message.messageStubType!)
&& message.messageStubParameters?.some(p => areJidsSameUser(meId, p))
(!!normalizedContent ||
REAL_MSG_STUB_TYPES.has(message.messageStubType!) ||
(REAL_MSG_REQ_ME_STUB_TYPES.has(message.messageStubType!) &&
message.messageStubParameters?.some(p => areJidsSameUser(meId, p)))) &&
hasSomeContent &&
!normalizedContent?.protocolMessage &&
!normalizedContent?.reactionMessage &&
!normalizedContent?.pollUpdateMessage
)
)
&& hasSomeContent
&& !normalizedContent?.protocolMessage
&& !normalizedContent?.reactionMessage
&& !normalizedContent?.pollUpdateMessage
}
export const shouldIncrementChatUnread = (message: proto.IWebMessageInfo) => (
export const shouldIncrementChatUnread = (message: proto.IWebMessageInfo) =>
!message.key.fromMe && !message.messageStubType
)
/**
* Get the ID of the chat from the given key.
* Typically -- that'll be the remoteJid, but for broadcasts, it'll be the participant
*/
export const getChatId = ({ remoteJid, participant, fromMe }: proto.IMessageKey) => {
if(
isJidBroadcast(remoteJid!)
&& !isJidStatusBroadcast(remoteJid!)
&& !fromMe
) {
if (isJidBroadcast(remoteJid!) && !isJidStatusBroadcast(remoteJid!) && !fromMe) {
return participant!
}
@@ -119,22 +121,15 @@ type PollContext = {
*/
export function decryptPollVote(
{ encPayload, encIv }: proto.Message.IPollEncValue,
{
pollCreatorJid,
pollMsgId,
pollEncKey,
voterJid,
}: PollContext
{ pollCreatorJid, pollMsgId, pollEncKey, voterJid }: PollContext
) {
const sign = Buffer.concat(
[
const sign = Buffer.concat([
toBinary(pollMsgId),
toBinary(pollCreatorJid),
toBinary(voterJid),
toBinary('Poll Vote'),
new Uint8Array([1])
]
)
])
const key0 = hmacSign(pollEncKey, new Uint8Array(32), 'sha256')
const decKey = hmacSign(sign, key0, 'sha256')
@@ -150,15 +145,7 @@ export function decryptPollVote(
const processMessage = async (
message: proto.IWebMessageInfo,
{
shouldProcessHistoryMsg,
placeholderResendCache,
ev,
creds,
keyStore,
logger,
options
}: ProcessMessageContext
{ shouldProcessHistoryMsg, placeholderResendCache, ev, creds, keyStore, logger, options }: ProcessMessageContext
) => {
const meId = creds.me!.id
const { accountSettings } = creds
@@ -179,10 +166,7 @@ const processMessage = async(
// unarchive chat if it's a real message, or someone reacted to our message
// and we've the unarchive chats setting on
if(
(isRealMsg || content?.reactionMessage?.key?.fromMe)
&& accountSettings?.unarchiveChats
) {
if ((isRealMsg || content?.reactionMessage?.key?.fromMe) && accountSettings?.unarchiveChats) {
chat.archived = false
chat.readOnly = false
}
@@ -195,12 +179,15 @@ const processMessage = async(
const process = shouldProcessHistoryMsg
const isLatest = !creds.processedHistoryMessages?.length
logger?.info({
logger?.info(
{
histNotification,
process,
id: message.key.id,
isLatest,
}, 'got history notification')
isLatest
},
'got history notification'
)
if (process) {
if (histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND) {
@@ -212,17 +199,11 @@ const processMessage = async(
})
}
const data = await downloadAndProcessHistorySyncNotification(
histNotification,
options
)
const data = await downloadAndProcessHistorySyncNotification(histNotification, options)
ev.emit('messaging-history.set', {
...data,
isLatest:
histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND
? isLatest
: undefined,
isLatest: histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND ? isLatest : undefined,
peerDataRequestSessionId: histNotification.peerDataRequestSessionId
})
}
@@ -232,8 +213,7 @@ const processMessage = async(
const keys = protocolMsg.appStateSyncKeyShare!.keys
if (keys?.length) {
let newAppStateSyncKeyId = ''
await keyStore.transaction(
async() => {
await keyStore.transaction(async () => {
const newKeys: string[] = []
for (const { keyData, keyId } of keys) {
const strKeyId = Buffer.from(keyId!.keyId!).toString('base64')
@@ -244,12 +224,8 @@ const processMessage = async(
newAppStateSyncKeyId = strKeyId
}
logger?.info(
{ newAppStateSyncKeyId, newKeys },
'injecting new app state sync keys'
)
}
)
logger?.info({ newAppStateSyncKeyId, newKeys }, 'injecting new app state sync keys')
})
ev.emit('creds.update', { myAppStateKeyId: newAppStateSyncKeyId })
} else {
@@ -298,9 +274,7 @@ const processMessage = async(
}
case proto.Message.ProtocolMessage.Type.MESSAGE_EDIT:
ev.emit(
'messages.update',
[
ev.emit('messages.update', [
{
// flip the sender / fromMe properties because they're in the perspective of the sender
key: { ...message.key, id: protocolMsg.key?.id },
@@ -315,26 +289,26 @@ const processMessage = async(
: message.messageTimestamp
}
}
]
)
])
break
}
} else if (content?.reactionMessage) {
const reaction: proto.IReaction = {
...content.reactionMessage,
key: message.key,
key: message.key
}
ev.emit('messages.reaction', [{
ev.emit('messages.reaction', [
{
reaction,
key: content.reactionMessage?.key!,
}])
key: content.reactionMessage?.key!
}
])
} else if (message.messageStubType) {
const jid = message.key?.remoteJid!
//let actor = whatsappID (message.participant)
let participants: string[]
const emitParticipantsUpdate = (action: ParticipantAction) => (
const emitParticipantsUpdate = (action: ParticipantAction) =>
ev.emit('group-participants.update', { id: jid, author: message.participant!, participants, action })
)
const emitGroupUpdate = (update: Partial<GroupMetadata>) => {
ev.emit('groups.update', [{ id: jid, ...update, author: message.participant ?? undefined }])
}
@@ -415,7 +389,6 @@ const processMessage = async(
emitGroupRequestJoin(participant, action, method)
break
}
} /* else if(content?.pollUpdateMessage) {
const creationMsgKey = content.pollUpdateMessage.pollCreationMessageKey!
// we need to fetch the poll creation message to get the poll enc key

View File

@@ -1,16 +1,30 @@
import { chunk } from 'lodash'
import { KEY_BUNDLE_TYPE } from '../Defaults'
import { SignalRepository } from '../Types'
import { AuthenticationCreds, AuthenticationState, KeyPair, SignalIdentity, SignalKeyStore, SignedKeyPair } from '../Types/Auth'
import { assertNodeErrorFree, BinaryNode, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildUInt, jidDecode, JidWithDevice, S_WHATSAPP_NET } from '../WABinary'
import {
AuthenticationCreds,
AuthenticationState,
KeyPair,
SignalIdentity,
SignalKeyStore,
SignedKeyPair
} from '../Types/Auth'
import {
assertNodeErrorFree,
BinaryNode,
getBinaryNodeChild,
getBinaryNodeChildBuffer,
getBinaryNodeChildren,
getBinaryNodeChildUInt,
jidDecode,
JidWithDevice,
S_WHATSAPP_NET
} from '../WABinary'
import { DeviceListData, ParsedDeviceInfo, USyncQueryResultList } from '../WAUSync'
import { Curve, generateSignalPubKey } from './crypto'
import { encodeBigEndian } from './generics'
export const createSignalIdentity = (
wid: string,
accountSignatureKey: Uint8Array
): SignalIdentity => {
export const createSignalIdentity = (wid: string, accountSignatureKey: Uint8Array): SignalIdentity => {
return {
identifier: { name: wid, deviceId: 0 },
identifierKey: generateSignalPubKey(accountSignatureKey)
@@ -40,12 +54,11 @@ export const generateOrGetPreKeys = (creds: AuthenticationCreds, range: number)
return {
newPreKeys,
lastPreKeyId,
preKeysRange: [creds.firstUnuploadedPreKeyId, range] as const,
preKeysRange: [creds.firstUnuploadedPreKeyId, range] as const
}
}
export const xmppSignedPreKey = (key: SignedKeyPair): BinaryNode => (
{
export const xmppSignedPreKey = (key: SignedKeyPair): BinaryNode => ({
tag: 'skey',
attrs: {},
content: [
@@ -53,31 +66,26 @@ export const xmppSignedPreKey = (key: SignedKeyPair): BinaryNode => (
{ tag: 'value', attrs: {}, content: key.keyPair.public },
{ tag: 'signature', attrs: {}, content: key.signature }
]
}
)
})
export const xmppPreKey = (pair: KeyPair, id: number): BinaryNode => (
{
export const xmppPreKey = (pair: KeyPair, id: number): BinaryNode => ({
tag: 'key',
attrs: {},
content: [
{ tag: 'id', attrs: {}, content: encodeBigEndian(id, 3) },
{ tag: 'value', attrs: {}, content: pair.public }
]
}
)
})
export const parseAndInjectE2ESessions = async(
node: BinaryNode,
repository: SignalRepository
) => {
const extractKey = (key: BinaryNode) => (
key ? ({
export const parseAndInjectE2ESessions = async (node: BinaryNode, repository: SignalRepository) => {
const extractKey = (key: BinaryNode) =>
key
? {
keyId: getBinaryNodeChildUInt(key, 'id', 3)!,
publicKey: generateSignalPubKey(getBinaryNodeChildBuffer(key, 'value')!),
signature: getBinaryNodeChildBuffer(key, 'signature')!,
}) : undefined
)
signature: getBinaryNodeChildBuffer(key, 'signature')!
}
: undefined
const nodes = getBinaryNodeChildren(getBinaryNodeChild(node, 'list'), 'user')
for (const node of nodes) {
assertNodeErrorFree(node)
@@ -92,8 +100,7 @@ export const parseAndInjectE2ESessions = async(
const chunks = chunk(nodes, chunkSize)
for (const nodesChunk of chunks) {
await Promise.all(
nodesChunk.map(
async node => {
nodesChunk.map(async node => {
const signedKey = getBinaryNodeChild(node, 'skey')!
const key = getBinaryNodeChild(node, 'key')!
const identity = getBinaryNodeChildBuffer(node, 'identity')!
@@ -109,8 +116,7 @@ export const parseAndInjectE2ESessions = async(
preKey: extractKey(key)!
}
})
}
)
})
)
}
}
@@ -120,9 +126,8 @@ export const extractDeviceJids = (result: USyncQueryResultList[], myJid: string,
const extracted: JidWithDevice[] = []
for (const userResult of result) {
const { devices, id } = userResult as { devices: ParsedDeviceInfo, id: string }
const { devices, id } = userResult as { devices: ParsedDeviceInfo; id: string }
const { user } = jidDecode(id)!
const deviceList = devices?.deviceList as DeviceListData[]
if (Array.isArray(deviceList)) {
@@ -169,7 +174,7 @@ export const getNextPreKeysNode = async(state: AuthenticationState, count: numbe
attrs: {
xmlns: 'encrypt',
type: 'set',
to: S_WHATSAPP_NET,
to: S_WHATSAPP_NET
},
content: [
{ tag: 'registration', attrs: {}, content: encodeBigEndian(creds.registrationId) },

View File

@@ -30,13 +30,15 @@ const getFileLock = (path: string): Mutex => {
* Again, I wouldn't endorse this for any production level use other than perhaps a bot.
* Would recommend writing an auth state for use with a proper SQL or No-SQL DB
* */
export const useMultiFileAuthState = async(folder: string): Promise<{ state: AuthenticationState, saveCreds: () => Promise<void> }> => {
export const useMultiFileAuthState = async (
folder: string
): Promise<{ state: AuthenticationState; saveCreds: () => Promise<void> }> => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const writeData = async (data: any, file: string) => {
const filePath = join(folder, fixFileName(file)!)
const mutex = getFileLock(filePath)
return mutex.acquire().then(async(release) => {
return mutex.acquire().then(async release => {
try {
await writeFile(filePath, JSON.stringify(data, BufferJSON.replacer))
} finally {
@@ -50,7 +52,7 @@ export const useMultiFileAuthState = async(folder: string): Promise<{ state: Aut
const filePath = join(folder, fixFileName(file)!)
const mutex = getFileLock(filePath)
return await mutex.acquire().then(async(release) => {
return await mutex.acquire().then(async release => {
try {
const data = await readFile(filePath, { encoding: 'utf-8' })
return JSON.parse(data, BufferJSON.reviver)
@@ -68,7 +70,7 @@ export const useMultiFileAuthState = async(folder: string): Promise<{ state: Aut
const filePath = join(folder, fixFileName(file)!)
const mutex = getFileLock(filePath)
return mutex.acquire().then(async(release) => {
return mutex.acquire().then(async release => {
try {
await unlink(filePath)
} catch {
@@ -76,14 +78,15 @@ export const useMultiFileAuthState = async(folder: string): Promise<{ state: Aut
release()
}
})
} catch{
}
} catch {}
}
const folderInfo = await stat(folder).catch(() => {})
if (folderInfo) {
if (!folderInfo.isDirectory()) {
throw new Error(`found something that is not a directory at ${folder}, either delete it or specify a different location`)
throw new Error(
`found something that is not a directory at ${folder}, either delete it or specify a different location`
)
}
} else {
await mkdir(folder, { recursive: true })
@@ -91,7 +94,7 @@ export const useMultiFileAuthState = async(folder: string): Promise<{ state: Aut
const fixFileName = (file?: string) => file?.replace(/\//g, '__')?.replace(/:/g, '-')
const creds: AuthenticationCreds = await readData('creds.json') || initAuthCreds()
const creds: AuthenticationCreds = (await readData('creds.json')) || initAuthCreds()
return {
state: {
@@ -100,21 +103,19 @@ export const useMultiFileAuthState = async(folder: string): Promise<{ state: Aut
get: async (type, ids) => {
const data: { [_: string]: SignalDataTypeMap[typeof type] } = {}
await Promise.all(
ids.map(
async id => {
ids.map(async id => {
let value = await readData(`${type}-${id}.json`)
if (type === 'app-state-sync-key' && value) {
value = proto.Message.AppStateSyncKeyData.fromObject(value)
}
data[id] = value
}
)
})
)
return data
},
set: async(data) => {
set: async data => {
const tasks: Promise<void>[] = []
for (const category in data) {
for (const id in data[category]) {

View File

@@ -13,7 +13,7 @@ const getUserAgent = (config: SocketConfig): proto.ClientPayload.IUserAgent => {
appVersion: {
primary: config.version[0],
secondary: config.version[1],
tertiary: config.version[2],
tertiary: config.version[2]
},
platform: proto.ClientPayload.UserAgent.Platform.WEB,
releaseChannel: proto.ClientPayload.UserAgent.ReleaseChannel.RELEASE,
@@ -23,13 +23,13 @@ const getUserAgent = (config: SocketConfig): proto.ClientPayload.IUserAgent => {
localeLanguageIso6391: 'en',
mnc: '000',
mcc: '000',
localeCountryIso31661Alpha2: config.countryCode,
localeCountryIso31661Alpha2: config.countryCode
}
}
const PLATFORM_MAP = {
'Mac OS': proto.ClientPayload.WebInfo.WebSubPlatform.DARWIN,
'Windows': proto.ClientPayload.WebInfo.WebSubPlatform.WIN32
Windows: proto.ClientPayload.WebInfo.WebSubPlatform.WIN32
}
const getWebInfo = (config: SocketConfig): proto.ClientPayload.IWebInfo => {
@@ -41,12 +41,11 @@ const getWebInfo = (config: SocketConfig): proto.ClientPayload.IWebInfo => {
return { webSubPlatform }
}
const getClientPayload = (config: SocketConfig) => {
const payload: proto.IClientPayload = {
connectType: proto.ClientPayload.ConnectType.WIFI_UNKNOWN,
connectReason: proto.ClientPayload.ConnectReason.USER_ACTIVATED,
userAgent: getUserAgent(config),
userAgent: getUserAgent(config)
}
payload.webInfo = getWebInfo(config)
@@ -54,7 +53,6 @@ const getClientPayload = (config: SocketConfig) => {
return payload
}
export const generateLoginNode = (userJid: string, config: SocketConfig): proto.IClientPayload => {
const { user, device } = jidDecode(userJid)!
const payload: proto.IClientPayload = {
@@ -62,7 +60,7 @@ export const generateLoginNode = (userJid: string, config: SocketConfig): proto.
passive: false,
pull: true,
username: +user,
device: device,
device: device
}
return proto.ClientPayload.fromObject(payload)
}
@@ -85,7 +83,7 @@ export const generateRegistrationNode = (
const companion: proto.IDeviceProps = {
os: config.browser[0],
platformType: getPlatformType(config.browser[1]),
requireFullSync: config.syncFullHistory,
requireFullSync: config.syncFullHistory
}
const companionProto = proto.DeviceProps.encode(companion).finish()
@@ -102,8 +100,8 @@ export const generateRegistrationNode = (
eIdent: signedIdentityKey.public,
eSkeyId: encodeBigEndian(signedPreKey.keyId, 3),
eSkeyVal: signedPreKey.keyPair.public,
eSkeySig: signedPreKey.signature,
},
eSkeySig: signedPreKey.signature
}
}
return proto.ClientPayload.fromObject(registerPayload)
@@ -111,7 +109,11 @@ export const generateRegistrationNode = (
export const configureSuccessfulPairing = (
stanza: BinaryNode,
{ advSecretKey, signedIdentityKey, signalIdentities }: Pick<AuthenticationCreds, 'advSecretKey' | 'signedIdentityKey' | 'signalIdentities'>
{
advSecretKey,
signedIdentityKey,
signalIdentities
}: Pick<AuthenticationCreds, 'advSecretKey' | 'signedIdentityKey' | 'signalIdentities'>
) => {
const msgId = stanza.attrs.id
@@ -158,7 +160,7 @@ export const configureSuccessfulPairing = (
attrs: {
to: S_WHATSAPP_NET,
type: 'result',
id: msgId,
id: msgId
},
content: [
{
@@ -178,10 +180,7 @@ export const configureSuccessfulPairing = (
const authUpdate: Partial<AuthenticationCreds> = {
account,
me: { id: jid, name: bizName },
signalIdentities: [
...(signalIdentities || []),
identity
],
signalIdentities: [...(signalIdentities || []), identity],
platform: platformNode?.attrs.name
}
@@ -191,10 +190,7 @@ export const configureSuccessfulPairing = (
}
}
export const encodeSignedDeviceIdentity = (
account: proto.IADVSignedDeviceIdentity,
includeSignatureKey: boolean
) => {
export const encodeSignedDeviceIdentity = (account: proto.IADVSignedDeviceIdentity, includeSignatureKey: boolean) => {
account = { ...account }
// set to null if we are not to include the signature key
// or if we are including the signature key but it is empty
@@ -202,7 +198,5 @@ export const encodeSignedDeviceIdentity = (
account.accountSignatureKey = null
}
return proto.ADVSignedDeviceIdentity
.encode(account)
.finish()
return proto.ADVSignedDeviceIdentity.encode(account).finish()
}

File diff suppressed because one or more lines are too long

View File

@@ -9,7 +9,8 @@ const inflatePromise = promisify(inflate)
export const decompressingIfRequired = async (buffer: Buffer) => {
if (2 & buffer.readUInt8()) {
buffer = await inflatePromise(buffer.slice(1))
} else { // nodes with no compression have a 0x00 prefix, we remove that
} else {
// nodes with no compression have a 0x00 prefix, we remove that
buffer = buffer.slice(1)
}
@@ -153,11 +154,7 @@ export const decodeDecompressedBinaryNode = (
const device = readByte()
const user = readString(readByte())
return jidEncode(
user,
domainType === 0 || domainType === 128 ? 's.whatsapp.net' : 'lid',
device
)
return jidEncode(user, domainType === 0 || domainType === 128 ? 's.whatsapp.net' : 'lid', device)
}
const readString = (tag: number): string => {

View File

@@ -1,4 +1,3 @@
import * as constants from './constants'
import { FullJid, jidDecode } from './jid-utils'
import type { BinaryNode, BinaryNodeCodingOptions } from './types'
@@ -38,9 +37,7 @@ const encodeBinaryNodeInner = (
pushBytes([(value >> 8) & 0xff, value & 0xff])
}
const pushInt20 = (value: number) => (
pushBytes([(value >> 16) & 0x0f, (value >> 8) & 0xff, value & 0xff])
)
const pushInt20 = (value: number) => pushBytes([(value >> 16) & 0x0f, (value >> 8) & 0xff, value & 0xff])
const writeByteLength = (length: number) => {
if (length >= 4294967296) {
throw new Error('string too large to encode: ' + length)
@@ -221,9 +218,7 @@ const encodeBinaryNodeInner = (
throw new Error('Invalid node: tag cannot be undefined')
}
const validAttributes = Object.keys(attrs || {}).filter(k => (
typeof attrs[k] !== 'undefined' && attrs[k] !== null
))
const validAttributes = Object.keys(attrs || {}).filter(k => typeof attrs[k] !== 'undefined' && attrs[k] !== null)
writeListStart(2 * validAttributes.length + 1 + (typeof content !== 'undefined' ? 1 : 0))
writeString(tag)
@@ -241,7 +236,8 @@ const encodeBinaryNodeInner = (
writeByteLength(content.length)
pushBytes(content)
} else if (Array.isArray(content)) {
const validContent = content.filter(item => item && (item.tag || Buffer.isBuffer(item) || item instanceof Uint8Array || typeof item === 'string')
const validContent = content.filter(
item => item && (item.tag || Buffer.isBuffer(item) || item instanceof Uint8Array || typeof item === 'string')
)
writeListStart(validContent.length)
for (const item of validContent) {

View File

@@ -62,7 +62,8 @@ export const reduceBinaryNodeToDictionary = (node: BinaryNode, tag: string) => {
(dict, { attrs }) => {
dict[attrs.name || attrs.config_code] = attrs.value || attrs.config_value
return dict
}, { } as { [_: string]: string }
},
{} as { [_: string]: string }
)
return dict
}
@@ -105,7 +106,7 @@ export function binaryNodeToString(node: BinaryNode | BinaryNode['content'], i =
}
if (Array.isArray(node)) {
return node.map((x) => tabs(i + 1) + binaryNodeToString(x, i + 1)).join('\n')
return node.map(x => tabs(i + 1) + binaryNodeToString(x, i + 1)).join('\n')
}
const children = binaryNodeToString(node.content, i + 1)

View File

@@ -17,7 +17,6 @@ export type FullJid = JidWithDevice & {
domainType?: number
}
export const jidEncode = (user: string | number | null, server: JidServer, device?: number, agent?: number) => {
return `${user || ''}${!!agent ? `_${agent}` : ''}${!!device ? `:${device}` : ''}@${server}`
}
@@ -43,27 +42,26 @@ export const jidDecode = (jid: string | undefined): FullJid | undefined => {
}
/** is the jid a user */
export const areJidsSameUser = (jid1: string | undefined, jid2: string | undefined) => (
export const areJidsSameUser = (jid1: string | undefined, jid2: string | undefined) =>
jidDecode(jid1)?.user === jidDecode(jid2)?.user
)
/** is the jid Meta IA */
export const isJidMetaIa = (jid: string | undefined) => (jid?.endsWith('@bot'))
export const isJidMetaIa = (jid: string | undefined) => jid?.endsWith('@bot')
/** is the jid a user */
export const isJidUser = (jid: string | undefined) => (jid?.endsWith('@s.whatsapp.net'))
export const isJidUser = (jid: string | undefined) => jid?.endsWith('@s.whatsapp.net')
/** is the jid a group */
export const isLidUser = (jid: string | undefined) => (jid?.endsWith('@lid'))
export const isLidUser = (jid: string | undefined) => jid?.endsWith('@lid')
/** is the jid a broadcast */
export const isJidBroadcast = (jid: string | undefined) => (jid?.endsWith('@broadcast'))
export const isJidBroadcast = (jid: string | undefined) => jid?.endsWith('@broadcast')
/** is the jid a group */
export const isJidGroup = (jid: string | undefined) => (jid?.endsWith('@g.us'))
export const isJidGroup = (jid: string | undefined) => jid?.endsWith('@g.us')
/** is the jid the status broadcast */
export const isJidStatusBroadcast = (jid: string) => jid === 'status@broadcast'
/** is the jid a newsletter */
export const isJidNewsletter = (jid: string | undefined) => (jid?.endsWith('@newsletter'))
export const isJidNewsletter = (jid: string | undefined) => jid?.endsWith('@newsletter')
const botRegexp = /^1313555\d{4}$|^131655500\d{2}$/
export const isJidBot = (jid: string | undefined) => (jid && botRegexp.test(jid.split('@')[0]) && jid.endsWith('@c.us'))
export const isJidBot = (jid: string | undefined) => jid && botRegexp.test(jid.split('@')[0]) && jid.endsWith('@c.us')
export const jidNormalizedUser = (jid: string | undefined) => {
const result = jidDecode(jid)
@@ -72,5 +70,5 @@ export const jidNormalizedUser = (jid: string | undefined) => {
}
const { user, server } = result
return jidEncode(user, server === 'c.us' ? 's.whatsapp.net' : server as JidServer)
return jidEncode(user, server === 'c.us' ? 's.whatsapp.net' : (server as JidServer))
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,14 @@
import { BinaryInfo } from './BinaryInfo'
import { FLAG_BYTE, FLAG_EVENT, FLAG_EXTENDED, FLAG_FIELD, FLAG_GLOBAL, Value, WEB_EVENTS, WEB_GLOBALS } from './constants'
import {
FLAG_BYTE,
FLAG_EVENT,
FLAG_EXTENDED,
FLAG_FIELD,
FLAG_GLOBAL,
Value,
WEB_EVENTS,
WEB_GLOBALS
} from './constants'
const getHeaderBitLength = (key: number) => (key < 256 ? 2 : 3)
@@ -10,9 +19,7 @@ export const encodeWAM = (binaryInfo: BinaryInfo) => {
encodeEvents(binaryInfo)
console.log(binaryInfo.buffer)
const totalSize = binaryInfo.buffer
.map((a) => a.length)
.reduce((a, b) => a + b)
const totalSize = binaryInfo.buffer.map(a => a.length).reduce((a, b) => a + b)
const buffer = Buffer.alloc(totalSize)
let offset = 0
for (const buffer_ of binaryInfo.buffer) {
@@ -47,12 +54,9 @@ function encodeGlobalAttributes(binaryInfo: BinaryInfo, globals: {[key: string]:
}
function encodeEvents(binaryInfo: BinaryInfo) {
for(const [
name,
{ props, globals },
] of binaryInfo.events.map((a) => Object.entries(a)[0])) {
for (const [name, { props, globals }] of binaryInfo.events.map(a => Object.entries(a)[0])) {
encodeGlobalAttributes(binaryInfo, globals)
const event = WEB_EVENTS.find((a) => a.name === name)!
const event = WEB_EVENTS.find(a => a.name === name)!
const props_ = Object.entries(props)
@@ -67,8 +71,8 @@ function encodeEvents(binaryInfo: BinaryInfo) {
for (let i = 0; i < props_.length; i++) {
const [key, _value] = props_[i]
const id = (event.props)[key][0]
extended = i < (props_.length - 1)
const id = event.props[key][0]
extended = i < props_.length - 1
let value = _value
if (typeof value === 'boolean') {
value = value ? 1 : 0
@@ -80,7 +84,6 @@ function encodeEvents(binaryInfo: BinaryInfo) {
}
}
function serializeData(key: number, value: Value, flag: number): Buffer {
const bufferLength = getHeaderBitLength(key)
let buffer: Buffer
@@ -150,12 +153,7 @@ function serializeData(key: number, value: Value, flag: number): Buffer {
throw 'missing'
}
function serializeHeader(
buffer: Buffer,
offset: number,
key: number,
flag: number
) {
function serializeHeader(buffer: Buffer, offset: number, key: number, flag: number) {
if (key < 256) {
buffer.writeUInt8(flag, offset)
offset += 1

View File

@@ -8,7 +8,7 @@ export class USyncContactProtocol implements USyncQueryProtocol {
getQueryElement(): BinaryNode {
return {
tag: 'contact',
attrs: {},
attrs: {}
}
}
@@ -17,7 +17,7 @@ export class USyncContactProtocol implements USyncQueryProtocol {
return {
tag: 'contact',
attrs: {},
content: user.phone,
content: user.phone
}
}

View File

@@ -26,8 +26,8 @@ export class USyncDeviceProtocol implements USyncQueryProtocol {
return {
tag: 'devices',
attrs: {
version: '2',
},
version: '2'
}
}
}

View File

@@ -12,7 +12,7 @@ export class USyncDisappearingModeProtocol implements USyncQueryProtocol {
getQueryElement(): BinaryNode {
return {
tag: 'disappearing_mode',
attrs: {},
attrs: {}
}
}
@@ -28,7 +28,7 @@ export class USyncDisappearingModeProtocol implements USyncQueryProtocol {
return {
duration,
setAt,
setAt
}
}
}

View File

@@ -12,7 +12,7 @@ export class USyncStatusProtocol implements USyncQueryProtocol {
getQueryElement(): BinaryNode {
return {
tag: 'status',
attrs: {},
attrs: {}
}
}
@@ -37,7 +37,7 @@ export class USyncStatusProtocol implements USyncQueryProtocol {
return {
status,
setAt,
setAt
}
}
}

View File

@@ -35,7 +35,7 @@ export class USyncBotProfileProtocol implements USyncQueryProtocol {
return {
tag: 'bot',
attrs: {},
content: [{ tag: 'profile', attrs: { 'persona_id': user.personaId } }]
content: [{ tag: 'profile', attrs: { persona_id: user.personaId } }]
}
}
@@ -60,7 +60,6 @@ export class USyncBotProfileProtocol implements USyncQueryProtocol {
prompts.push(`${getBinaryNodeChildString(prompt, 'emoji')!} ${getBinaryNodeChildString(prompt, 'text')!}`)
}
return {
isDefault: !!getBinaryNodeChild(profile, 'default'),
jid: node.attrs.jid,

View File

@@ -7,7 +7,7 @@ export class USyncLIDProtocol implements USyncQueryProtocol {
getQueryElement(): BinaryNode {
return {
tag: 'lid',
attrs: {},
attrs: {}
}
}

View File

@@ -2,10 +2,15 @@ import { USyncQueryProtocol } from '../Types/USync'
import { BinaryNode, getBinaryNodeChild } from '../WABinary'
import { USyncBotProfileProtocol } from './Protocols/UsyncBotProfileProtocol'
import { USyncLIDProtocol } from './Protocols/UsyncLIDProtocol'
import { USyncContactProtocol, USyncDeviceProtocol, USyncDisappearingModeProtocol, USyncStatusProtocol } from './Protocols'
import {
USyncContactProtocol,
USyncDeviceProtocol,
USyncDisappearingModeProtocol,
USyncStatusProtocol
} from './Protocols'
import { USyncUser } from './USyncUser'
export type USyncQueryResultList = { [protocol: string]: unknown, id: string }
export type USyncQueryResultList = { [protocol: string]: unknown; id: string }
export type USyncQueryResult = {
list: USyncQueryResultList[]
@@ -45,14 +50,16 @@ export class USyncQuery {
return
}
const protocolMap = Object.fromEntries(this.protocols.map((protocol) => {
const protocolMap = Object.fromEntries(
this.protocols.map(protocol => {
return [protocol.name, protocol.parser]
}))
})
)
const queryResult: USyncQueryResult = {
// TODO: implement errors etc.
list: [],
sideList: [],
sideList: []
}
const usyncNode = getBinaryNodeChild(result, 'usync')
@@ -63,9 +70,12 @@ export class USyncQuery {
const listNode = getBinaryNodeChild(usyncNode, 'list')
if (Array.isArray(listNode?.content) && typeof listNode !== 'undefined') {
queryResult.list = listNode.content.map((node) => {
queryResult.list = listNode.content.map(node => {
const id = node?.attrs.jid
const data = Array.isArray(node?.content) ? Object.fromEntries(node.content.map((content) => {
const data = Array.isArray(node?.content)
? Object.fromEntries(
node.content
.map(content => {
const protocol = content.tag
const parser = protocolMap[protocol]
if (parser) {
@@ -73,7 +83,10 @@ export class USyncQuery {
} else {
return [protocol, null]
}
}).filter(([, b]) => b !== null) as [string, unknown][]) : {}
})
.filter(([, b]) => b !== null) as [string, unknown][]
)
: {}
return { ...data, id }
})
}