mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
chore: format everything
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
lib
|
lib
|
||||||
coverage
|
coverage
|
||||||
*.lock
|
*.lock
|
||||||
|
*.json
|
||||||
src/WABinary/index.ts
|
src/WABinary/index.ts
|
||||||
WAProto
|
WAProto
|
||||||
WASignalGroup
|
WASignalGroup
|
||||||
|
|||||||
@@ -30,8 +30,9 @@
|
|||||||
"changelog:update": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
|
"changelog:update": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
|
||||||
"example": "node --inspect -r ts-node/register Example/example.ts",
|
"example": "node --inspect -r ts-node/register Example/example.ts",
|
||||||
"gen:protobuf": "sh WAProto/GenerateStatics.sh",
|
"gen:protobuf": "sh WAProto/GenerateStatics.sh",
|
||||||
|
"format": "prettier --write \"src/**/*.{ts,js,json,md}\"",
|
||||||
"lint": "eslint src --ext .js,.ts",
|
"lint": "eslint src --ext .js,.ts",
|
||||||
"lint:fix": "yarn lint --fix",
|
"lint:fix": "yarn format && yarn lint --fix",
|
||||||
"prepack": "tsc",
|
"prepack": "tsc",
|
||||||
"prepare": "tsc",
|
"prepare": "tsc",
|
||||||
"preinstall": "node ./engine-requirements.js",
|
"preinstall": "node ./engine-requirements.js",
|
||||||
|
|||||||
@@ -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 NOISE_MODE = 'Noise_XX_25519_AESGCM_SHA256\0\0\0\0'
|
||||||
export const DICT_VERSION = 2
|
export const DICT_VERSION = 2
|
||||||
export const KEY_BUNDLE_TYPE = Buffer.from([5])
|
export const KEY_BUNDLE_TYPE = Buffer.from([5])
|
||||||
export const NOISE_WA_HEADER = Buffer.from(
|
export const NOISE_WA_HEADER = Buffer.from([87, 65, 6, DICT_VERSION]) // last is "DICT_VERSION"
|
||||||
[ 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 */
|
/** 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 URL_REGEX = /https:\/\/(?![^:@\/\s]+:[^:@\/\s]+@)[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(:\d+)?(\/[^\s]*)?/g
|
||||||
|
|
||||||
export const WA_CERT_DETAILS = {
|
export const WA_CERT_DETAILS = {
|
||||||
SERIAL: 0,
|
SERIAL: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PROCESSABLE_HISTORY_TYPES = [
|
export const PROCESSABLE_HISTORY_TYPES = [
|
||||||
@@ -32,7 +30,7 @@ export const PROCESSABLE_HISTORY_TYPES = [
|
|||||||
proto.Message.HistorySyncNotification.HistorySyncType.PUSH_NAME,
|
proto.Message.HistorySyncNotification.HistorySyncType.PUSH_NAME,
|
||||||
proto.Message.HistorySyncNotification.HistorySyncType.RECENT,
|
proto.Message.HistorySyncNotification.HistorySyncType.RECENT,
|
||||||
proto.Message.HistorySyncNotification.HistorySyncType.FULL,
|
proto.Message.HistorySyncNotification.HistorySyncType.FULL,
|
||||||
proto.Message.HistorySyncNotification.HistorySyncType.ON_DEMAND,
|
proto.Message.HistorySyncNotification.HistorySyncType.ON_DEMAND
|
||||||
]
|
]
|
||||||
|
|
||||||
export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
|
export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
|
||||||
@@ -57,14 +55,14 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
|
|||||||
linkPreviewImageThumbnailWidth: 192,
|
linkPreviewImageThumbnailWidth: 192,
|
||||||
transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 3000 },
|
transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 3000 },
|
||||||
generateHighQualityLinkPreview: false,
|
generateHighQualityLinkPreview: false,
|
||||||
options: { },
|
options: {},
|
||||||
appStateMacVerification: {
|
appStateMacVerification: {
|
||||||
patch: false,
|
patch: false,
|
||||||
snapshot: false,
|
snapshot: false
|
||||||
},
|
},
|
||||||
countryCode: 'US',
|
countryCode: 'US',
|
||||||
getMessage: async() => undefined,
|
getMessage: async () => undefined,
|
||||||
cachedGroupMetadata: async() => undefined,
|
cachedGroupMetadata: async () => undefined,
|
||||||
makeSignalRepository: makeLibSignalRepository
|
makeSignalRepository: makeLibSignalRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,19 +75,19 @@ export const MEDIA_PATH_MAP: { [T in MediaType]?: string } = {
|
|||||||
'thumbnail-link': '/mms/image',
|
'thumbnail-link': '/mms/image',
|
||||||
'product-catalog-image': '/product/image',
|
'product-catalog-image': '/product/image',
|
||||||
'md-app-state': '',
|
'md-app-state': '',
|
||||||
'md-msg-hist': '/mms/md-app-state',
|
'md-msg-hist': '/mms/md-app-state'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MEDIA_HKDF_KEY_MAPPING = {
|
export const MEDIA_HKDF_KEY_MAPPING = {
|
||||||
'audio': 'Audio',
|
audio: 'Audio',
|
||||||
'document': 'Document',
|
document: 'Document',
|
||||||
'gif': 'Video',
|
gif: 'Video',
|
||||||
'image': 'Image',
|
image: 'Image',
|
||||||
'ppic': '',
|
ppic: '',
|
||||||
'product': 'Image',
|
product: 'Image',
|
||||||
'ptt': 'Audio',
|
ptt: 'Audio',
|
||||||
'sticker': 'Image',
|
sticker: 'Image',
|
||||||
'video': 'Video',
|
video: 'Video',
|
||||||
'thumbnail-document': 'Document Thumbnail',
|
'thumbnail-document': 'Document Thumbnail',
|
||||||
'thumbnail-image': 'Image Thumbnail',
|
'thumbnail-image': 'Image Thumbnail',
|
||||||
'thumbnail-video': 'Video Thumbnail',
|
'thumbnail-video': 'Video Thumbnail',
|
||||||
@@ -98,7 +96,7 @@ export const MEDIA_HKDF_KEY_MAPPING = {
|
|||||||
'md-app-state': 'App State',
|
'md-app-state': 'App State',
|
||||||
'product-catalog-image': '',
|
'product-catalog-image': '',
|
||||||
'payment-bg-image': 'Payment Background',
|
'payment-bg-image': 'Payment Background',
|
||||||
'ptv': 'Video'
|
ptv: 'Video'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MEDIA_KEYS = Object.keys(MEDIA_PATH_MAP) as MediaType[]
|
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
|
SIGNAL_STORE: 5 * 60, // 5 minutes
|
||||||
MSG_RETRY: 60 * 60, // 1 hour
|
MSG_RETRY: 60 * 60, // 1 hour
|
||||||
CALL_OFFER: 5 * 60, // 5 minutes
|
CALL_OFFER: 5 * 60, // 5 minutes
|
||||||
USER_DEVICES: 5 * 60, // 5 minutes
|
USER_DEVICES: 5 * 60 // 5 minutes
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import * as libsignal from 'libsignal'
|
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 { SignalAuthState } from '../Types'
|
||||||
import { SignalRepository } from '../Types/Signal'
|
import { SignalRepository } from '../Types/Signal'
|
||||||
import { generateSignalPubKey } from '../Utils'
|
import { generateSignalPubKey } from '../Utils'
|
||||||
@@ -18,9 +24,15 @@ export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository
|
|||||||
const builder = new GroupSessionBuilder(storage)
|
const builder = new GroupSessionBuilder(storage)
|
||||||
const senderName = jidToSignalSenderKeyName(item.groupId!, authorJid)
|
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])
|
const { [senderName]: senderKey } = await auth.keys.get('sender-key', [senderName])
|
||||||
if(!senderKey) {
|
if (!senderKey) {
|
||||||
await storage.storeSenderKey(senderName, new SenderKeyRecord())
|
await storage.storeSenderKey(senderName, new SenderKeyRecord())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,12 +43,12 @@ export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository
|
|||||||
const session = new libsignal.SessionCipher(storage, addr)
|
const session = new libsignal.SessionCipher(storage, addr)
|
||||||
let result: Buffer
|
let result: Buffer
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'pkmsg':
|
case 'pkmsg':
|
||||||
result = await session.decryptPreKeyWhisperMessage(ciphertext)
|
result = await session.decryptPreKeyWhisperMessage(ciphertext)
|
||||||
break
|
break
|
||||||
case 'msg':
|
case 'msg':
|
||||||
result = await session.decryptWhisperMessage(ciphertext)
|
result = await session.decryptWhisperMessage(ciphertext)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -54,7 +66,7 @@ export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository
|
|||||||
const builder = new GroupSessionBuilder(storage)
|
const builder = new GroupSessionBuilder(storage)
|
||||||
|
|
||||||
const { [senderName]: senderKey } = await auth.keys.get('sender-key', [senderName])
|
const { [senderName]: senderKey } = await auth.keys.get('sender-key', [senderName])
|
||||||
if(!senderKey) {
|
if (!senderKey) {
|
||||||
await storage.storeSenderKey(senderName, new SenderKeyRecord())
|
await storage.storeSenderKey(senderName, new SenderKeyRecord())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +76,7 @@ export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
ciphertext,
|
ciphertext,
|
||||||
senderKeyDistributionMessage: senderKeyDistributionMessage.serialize(),
|
senderKeyDistributionMessage: senderKeyDistributionMessage.serialize()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async injectE2ESession({ jid, session }) {
|
async injectE2ESession({ jid, session }) {
|
||||||
@@ -73,7 +85,7 @@ export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository
|
|||||||
},
|
},
|
||||||
jidToSignalProtocolAddress(jid) {
|
jidToSignalProtocolAddress(jid) {
|
||||||
return jidToSignalProtocolAddress(jid).toString()
|
return jidToSignalProtocolAddress(jid).toString()
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,22 +100,22 @@ const jidToSignalSenderKeyName = (group: string, user: string): string => {
|
|||||||
|
|
||||||
function signalStorage({ creds, keys }: SignalAuthState) {
|
function signalStorage({ creds, keys }: SignalAuthState) {
|
||||||
return {
|
return {
|
||||||
loadSession: async(id: string) => {
|
loadSession: async (id: string) => {
|
||||||
const { [id]: sess } = await keys.get('session', [id])
|
const { [id]: sess } = await keys.get('session', [id])
|
||||||
if(sess) {
|
if (sess) {
|
||||||
return libsignal.SessionRecord.deserialize(sess)
|
return libsignal.SessionRecord.deserialize(sess)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
storeSession: async(id, session) => {
|
storeSession: async (id, session) => {
|
||||||
await keys.set({ 'session': { [id]: session.serialize() } })
|
await keys.set({ session: { [id]: session.serialize() } })
|
||||||
},
|
},
|
||||||
isTrustedIdentity: () => {
|
isTrustedIdentity: () => {
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
loadPreKey: async(id: number | string) => {
|
loadPreKey: async (id: number | string) => {
|
||||||
const keyId = id.toString()
|
const keyId = id.toString()
|
||||||
const { [keyId]: key } = await keys.get('pre-key', [keyId])
|
const { [keyId]: key } = await keys.get('pre-key', [keyId])
|
||||||
if(key) {
|
if (key) {
|
||||||
return {
|
return {
|
||||||
privKey: Buffer.from(key.private),
|
privKey: Buffer.from(key.private),
|
||||||
pubKey: Buffer.from(key.public)
|
pubKey: Buffer.from(key.public)
|
||||||
@@ -118,24 +130,22 @@ function signalStorage({ creds, keys }: SignalAuthState) {
|
|||||||
pubKey: Buffer.from(key.keyPair.public)
|
pubKey: Buffer.from(key.keyPair.public)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
loadSenderKey: async(keyId: string) => {
|
loadSenderKey: async (keyId: string) => {
|
||||||
const { [keyId]: key } = await keys.get('sender-key', [keyId])
|
const { [keyId]: key } = await keys.get('sender-key', [keyId])
|
||||||
if(key) {
|
if (key) {
|
||||||
return new SenderKeyRecord(key)
|
return new SenderKeyRecord(key)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
storeSenderKey: async(keyId, key) => {
|
storeSenderKey: async (keyId, key) => {
|
||||||
await keys.set({ 'sender-key': { [keyId]: key.serialize() } })
|
await keys.set({ 'sender-key': { [keyId]: key.serialize() } })
|
||||||
},
|
},
|
||||||
getOurRegistrationId: () => (
|
getOurRegistrationId: () => creds.registrationId,
|
||||||
creds.registrationId
|
|
||||||
),
|
|
||||||
getOurIdentity: () => {
|
getOurIdentity: () => {
|
||||||
const { signedIdentityKey } = creds
|
const { signedIdentityKey } = creds
|
||||||
return {
|
return {
|
||||||
privKey: Buffer.from(signedIdentityKey.private),
|
privKey: Buffer.from(signedIdentityKey.private),
|
||||||
pubKey: generateSignalPubKey(signedIdentityKey.public),
|
pubKey: generateSignalPubKey(signedIdentityKey.public)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from './types'
|
export * from './types'
|
||||||
export * from './websocket'
|
export * from './websocket'
|
||||||
|
|||||||
@@ -8,12 +8,15 @@ export abstract class AbstractSocketClient extends EventEmitter {
|
|||||||
abstract get isClosing(): boolean
|
abstract get isClosing(): boolean
|
||||||
abstract get isConnecting(): boolean
|
abstract get isConnecting(): boolean
|
||||||
|
|
||||||
constructor(public url: URL, public config: SocketConfig) {
|
constructor(
|
||||||
|
public url: URL,
|
||||||
|
public config: SocketConfig
|
||||||
|
) {
|
||||||
super()
|
super()
|
||||||
this.setMaxListeners(0)
|
this.setMaxListeners(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract connect(): Promise<void>
|
abstract connect(): Promise<void>
|
||||||
abstract close(): 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { DEFAULT_ORIGIN } from '../../Defaults'
|
|||||||
import { AbstractSocketClient } from './types'
|
import { AbstractSocketClient } from './types'
|
||||||
|
|
||||||
export class WebSocketClient extends AbstractSocketClient {
|
export class WebSocketClient extends AbstractSocketClient {
|
||||||
|
|
||||||
protected socket: WebSocket | null = null
|
protected socket: WebSocket | null = null
|
||||||
|
|
||||||
get isOpen(): boolean {
|
get isOpen(): boolean {
|
||||||
@@ -20,7 +19,7 @@ export class WebSocketClient extends AbstractSocketClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async connect(): Promise<void> {
|
async connect(): Promise<void> {
|
||||||
if(this.socket) {
|
if (this.socket) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,20 +28,20 @@ export class WebSocketClient extends AbstractSocketClient {
|
|||||||
headers: this.config.options?.headers as {},
|
headers: this.config.options?.headers as {},
|
||||||
handshakeTimeout: this.config.connectTimeoutMs,
|
handshakeTimeout: this.config.connectTimeoutMs,
|
||||||
timeout: this.config.connectTimeoutMs,
|
timeout: this.config.connectTimeoutMs,
|
||||||
agent: this.config.agent,
|
agent: this.config.agent
|
||||||
})
|
})
|
||||||
|
|
||||||
this.socket.setMaxListeners(0)
|
this.socket.setMaxListeners(0)
|
||||||
|
|
||||||
const events = ['close', 'error', 'upgrade', 'message', 'open', 'ping', 'pong', 'unexpected-response']
|
const events = ['close', 'error', 'upgrade', 'message', 'open', 'ping', 'pong', 'unexpected-response']
|
||||||
|
|
||||||
for(const event of events) {
|
for (const event of events) {
|
||||||
this.socket?.on(event, (...args: any[]) => this.emit(event, ...args))
|
this.socket?.on(event, (...args: any[]) => this.emit(event, ...args))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
if(!this.socket) {
|
if (!this.socket) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,43 +1,46 @@
|
|||||||
import { GetCatalogOptions, ProductCreate, ProductUpdate, SocketConfig } from '../Types'
|
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 { BinaryNode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary'
|
||||||
import { getBinaryNodeChild } from '../WABinary/generic-utils'
|
import { getBinaryNodeChild } from '../WABinary/generic-utils'
|
||||||
import { makeMessagesRecvSocket } from './messages-recv'
|
import { makeMessagesRecvSocket } from './messages-recv'
|
||||||
|
|
||||||
export const makeBusinessSocket = (config: SocketConfig) => {
|
export const makeBusinessSocket = (config: SocketConfig) => {
|
||||||
const sock = makeMessagesRecvSocket(config)
|
const sock = makeMessagesRecvSocket(config)
|
||||||
const {
|
const { authState, query, waUploadToServer } = sock
|
||||||
authState,
|
|
||||||
query,
|
|
||||||
waUploadToServer
|
|
||||||
} = sock
|
|
||||||
|
|
||||||
const getCatalog = async({ jid, limit, cursor }: GetCatalogOptions) => {
|
const getCatalog = async ({ jid, limit, cursor }: GetCatalogOptions) => {
|
||||||
jid = jid || authState.creds.me?.id
|
jid = jid || authState.creds.me?.id
|
||||||
jid = jidNormalizedUser(jid)
|
jid = jidNormalizedUser(jid)
|
||||||
|
|
||||||
const queryParamNodes: BinaryNode[] = [
|
const queryParamNodes: BinaryNode[] = [
|
||||||
{
|
{
|
||||||
tag: 'limit',
|
tag: 'limit',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: Buffer.from((limit || 10).toString())
|
content: Buffer.from((limit || 10).toString())
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'width',
|
tag: 'width',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: Buffer.from('100')
|
content: Buffer.from('100')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'height',
|
tag: 'height',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: Buffer.from('100')
|
content: Buffer.from('100')
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
if(cursor) {
|
if (cursor) {
|
||||||
queryParamNodes.push({
|
queryParamNodes.push({
|
||||||
tag: 'after',
|
tag: 'after',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: cursor
|
content: cursor
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -54,7 +57,7 @@ export const makeBusinessSocket = (config: SocketConfig) => {
|
|||||||
tag: 'product_catalog',
|
tag: 'product_catalog',
|
||||||
attrs: {
|
attrs: {
|
||||||
jid,
|
jid,
|
||||||
'allow_shop_source': 'true'
|
allow_shop_source: 'true'
|
||||||
},
|
},
|
||||||
content: queryParamNodes
|
content: queryParamNodes
|
||||||
}
|
}
|
||||||
@@ -63,7 +66,7 @@ export const makeBusinessSocket = (config: SocketConfig) => {
|
|||||||
return parseCatalogNode(result)
|
return parseCatalogNode(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCollections = async(jid?: string, limit = 51) => {
|
const getCollections = async (jid?: string, limit = 51) => {
|
||||||
jid = jid || authState.creds.me?.id
|
jid = jid || authState.creds.me?.id
|
||||||
jid = jidNormalizedUser(jid)
|
jid = jidNormalizedUser(jid)
|
||||||
const result = await query({
|
const result = await query({
|
||||||
@@ -72,33 +75,33 @@ export const makeBusinessSocket = (config: SocketConfig) => {
|
|||||||
to: S_WHATSAPP_NET,
|
to: S_WHATSAPP_NET,
|
||||||
type: 'get',
|
type: 'get',
|
||||||
xmlns: 'w:biz:catalog',
|
xmlns: 'w:biz:catalog',
|
||||||
'smax_id': '35'
|
smax_id: '35'
|
||||||
},
|
},
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
tag: 'collections',
|
tag: 'collections',
|
||||||
attrs: {
|
attrs: {
|
||||||
'biz_jid': jid,
|
biz_jid: jid
|
||||||
},
|
},
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
tag: 'collection_limit',
|
tag: 'collection_limit',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: Buffer.from(limit.toString())
|
content: Buffer.from(limit.toString())
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'item_limit',
|
tag: 'item_limit',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: Buffer.from(limit.toString())
|
content: Buffer.from(limit.toString())
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'width',
|
tag: 'width',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: Buffer.from('100')
|
content: Buffer.from('100')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'height',
|
tag: 'height',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: Buffer.from('100')
|
content: Buffer.from('100')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -109,14 +112,14 @@ export const makeBusinessSocket = (config: SocketConfig) => {
|
|||||||
return parseCollectionsNode(result)
|
return parseCollectionsNode(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOrderDetails = async(orderId: string, tokenBase64: string) => {
|
const getOrderDetails = async (orderId: string, tokenBase64: string) => {
|
||||||
const result = await query({
|
const result = await query({
|
||||||
tag: 'iq',
|
tag: 'iq',
|
||||||
attrs: {
|
attrs: {
|
||||||
to: S_WHATSAPP_NET,
|
to: S_WHATSAPP_NET,
|
||||||
type: 'get',
|
type: 'get',
|
||||||
xmlns: 'fb:thrift_iq',
|
xmlns: 'fb:thrift_iq',
|
||||||
'smax_id': '5'
|
smax_id: '5'
|
||||||
},
|
},
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@@ -128,23 +131,23 @@ export const makeBusinessSocket = (config: SocketConfig) => {
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
tag: 'image_dimensions',
|
tag: 'image_dimensions',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
tag: 'width',
|
tag: 'width',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: Buffer.from('100')
|
content: Buffer.from('100')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'height',
|
tag: 'height',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: Buffer.from('100')
|
content: Buffer.from('100')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'token',
|
tag: 'token',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: Buffer.from(tokenBase64)
|
content: Buffer.from(tokenBase64)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -155,7 +158,7 @@ export const makeBusinessSocket = (config: SocketConfig) => {
|
|||||||
return parseOrderDetailsNode(result)
|
return parseOrderDetailsNode(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
const productUpdate = async(productId: string, update: ProductUpdate) => {
|
const productUpdate = async (productId: string, update: ProductUpdate) => {
|
||||||
update = await uploadingNecessaryImagesOfProduct(update, waUploadToServer)
|
update = await uploadingNecessaryImagesOfProduct(update, waUploadToServer)
|
||||||
const editNode = toProductNode(productId, update)
|
const editNode = toProductNode(productId, update)
|
||||||
|
|
||||||
@@ -174,12 +177,12 @@ export const makeBusinessSocket = (config: SocketConfig) => {
|
|||||||
editNode,
|
editNode,
|
||||||
{
|
{
|
||||||
tag: 'width',
|
tag: 'width',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: '100'
|
content: '100'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'height',
|
tag: 'height',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: '100'
|
content: '100'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -193,7 +196,7 @@ export const makeBusinessSocket = (config: SocketConfig) => {
|
|||||||
return parseProductNode(productNode!)
|
return parseProductNode(productNode!)
|
||||||
}
|
}
|
||||||
|
|
||||||
const productCreate = async(create: ProductCreate) => {
|
const productCreate = async (create: ProductCreate) => {
|
||||||
// ensure isHidden is defined
|
// ensure isHidden is defined
|
||||||
create.isHidden = !!create.isHidden
|
create.isHidden = !!create.isHidden
|
||||||
create = await uploadingNecessaryImagesOfProduct(create, waUploadToServer)
|
create = await uploadingNecessaryImagesOfProduct(create, waUploadToServer)
|
||||||
@@ -214,12 +217,12 @@ export const makeBusinessSocket = (config: SocketConfig) => {
|
|||||||
createNode,
|
createNode,
|
||||||
{
|
{
|
||||||
tag: 'width',
|
tag: 'width',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: '100'
|
content: '100'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'height',
|
tag: 'height',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: '100'
|
content: '100'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -233,7 +236,7 @@ export const makeBusinessSocket = (config: SocketConfig) => {
|
|||||||
return parseProductNode(productNode!)
|
return parseProductNode(productNode!)
|
||||||
}
|
}
|
||||||
|
|
||||||
const productDelete = async(productIds: string[]) => {
|
const productDelete = async (productIds: string[]) => {
|
||||||
const result = await query({
|
const result = await query({
|
||||||
tag: 'iq',
|
tag: 'iq',
|
||||||
attrs: {
|
attrs: {
|
||||||
@@ -245,19 +248,17 @@ export const makeBusinessSocket = (config: SocketConfig) => {
|
|||||||
{
|
{
|
||||||
tag: 'product_catalog_delete',
|
tag: 'product_catalog_delete',
|
||||||
attrs: { v: '1' },
|
attrs: { v: '1' },
|
||||||
content: productIds.map(
|
content: productIds.map(id => ({
|
||||||
id => ({
|
tag: 'product',
|
||||||
tag: 'product',
|
attrs: {},
|
||||||
attrs: { },
|
content: [
|
||||||
content: [
|
{
|
||||||
{
|
tag: 'id',
|
||||||
tag: 'id',
|
attrs: {},
|
||||||
attrs: { },
|
content: Buffer.from(id)
|
||||||
content: Buffer.from(id)
|
}
|
||||||
}
|
]
|
||||||
]
|
}))
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,62 +1,70 @@
|
|||||||
import { proto } from '../../WAProto'
|
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 { 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'
|
import { makeChatsSocket } from './chats'
|
||||||
|
|
||||||
export const makeGroupsSocket = (config: SocketConfig) => {
|
export const makeGroupsSocket = (config: SocketConfig) => {
|
||||||
const sock = makeChatsSocket(config)
|
const sock = makeChatsSocket(config)
|
||||||
const { authState, ev, query, upsertMessage } = sock
|
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({
|
query({
|
||||||
tag: 'iq',
|
tag: 'iq',
|
||||||
attrs: {
|
attrs: {
|
||||||
type,
|
type,
|
||||||
xmlns: 'w:g2',
|
xmlns: 'w:g2',
|
||||||
to: jid,
|
to: jid
|
||||||
},
|
},
|
||||||
content
|
content
|
||||||
})
|
})
|
||||||
)
|
|
||||||
|
|
||||||
const groupMetadata = async(jid: string) => {
|
const groupMetadata = async (jid: string) => {
|
||||||
const result = await groupQuery(
|
const result = await groupQuery(jid, 'get', [{ tag: 'query', attrs: { request: 'interactive' } }])
|
||||||
jid,
|
|
||||||
'get',
|
|
||||||
[ { tag: 'query', attrs: { request: 'interactive' } } ]
|
|
||||||
)
|
|
||||||
return extractGroupMetadata(result)
|
return extractGroupMetadata(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const groupFetchAllParticipating = async () => {
|
||||||
const groupFetchAllParticipating = async() => {
|
|
||||||
const result = await query({
|
const result = await query({
|
||||||
tag: 'iq',
|
tag: 'iq',
|
||||||
attrs: {
|
attrs: {
|
||||||
to: '@g.us',
|
to: '@g.us',
|
||||||
xmlns: 'w:g2',
|
xmlns: 'w:g2',
|
||||||
type: 'get',
|
type: 'get'
|
||||||
},
|
},
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
tag: 'participating',
|
tag: 'participating',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: [
|
content: [
|
||||||
{ tag: 'participants', attrs: { } },
|
{ tag: 'participants', attrs: {} },
|
||||||
{ tag: 'description', attrs: { } }
|
{ tag: 'description', attrs: {} }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
const data: { [_: string]: GroupMetadata } = { }
|
const data: { [_: string]: GroupMetadata } = {}
|
||||||
const groupsChild = getBinaryNodeChild(result, 'groups')
|
const groupsChild = getBinaryNodeChild(result, 'groups')
|
||||||
if(groupsChild) {
|
if (groupsChild) {
|
||||||
const groups = getBinaryNodeChildren(groupsChild, 'group')
|
const groups = getBinaryNodeChildren(groupsChild, 'group')
|
||||||
for(const groupNode of groups) {
|
for (const groupNode of groups) {
|
||||||
const meta = extractGroupMetadata({
|
const meta = extractGroupMetadata({
|
||||||
tag: 'result',
|
tag: 'result',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: [groupNode]
|
content: [groupNode]
|
||||||
})
|
})
|
||||||
data[meta.id] = meta
|
data[meta.id] = meta
|
||||||
@@ -68,9 +76,9 @@ export const makeGroupsSocket = (config: SocketConfig) => {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
sock.ws.on('CB:ib,,dirty', async(node: BinaryNode) => {
|
sock.ws.on('CB:ib,,dirty', async (node: BinaryNode) => {
|
||||||
const { attrs } = getBinaryNodeChild(node, 'dirty')!
|
const { attrs } = getBinaryNodeChild(node, 'dirty')!
|
||||||
if(attrs.type !== 'groups') {
|
if (attrs.type !== 'groups') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,89 +89,69 @@ export const makeGroupsSocket = (config: SocketConfig) => {
|
|||||||
return {
|
return {
|
||||||
...sock,
|
...sock,
|
||||||
groupMetadata,
|
groupMetadata,
|
||||||
groupCreate: async(subject: string, participants: string[]) => {
|
groupCreate: async (subject: string, participants: string[]) => {
|
||||||
const key = generateMessageIDV2()
|
const key = generateMessageIDV2()
|
||||||
const result = await groupQuery(
|
const result = await groupQuery('@g.us', 'set', [
|
||||||
'@g.us',
|
{
|
||||||
'set',
|
tag: 'create',
|
||||||
[
|
attrs: {
|
||||||
{
|
subject,
|
||||||
tag: 'create',
|
key
|
||||||
attrs: {
|
},
|
||||||
subject,
|
content: participants.map(jid => ({
|
||||||
key
|
tag: 'participant',
|
||||||
},
|
attrs: { jid }
|
||||||
content: participants.map(jid => ({
|
}))
|
||||||
tag: 'participant',
|
}
|
||||||
attrs: { jid }
|
])
|
||||||
}))
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
return extractGroupMetadata(result)
|
return extractGroupMetadata(result)
|
||||||
},
|
},
|
||||||
groupLeave: async(id: string) => {
|
groupLeave: async (id: string) => {
|
||||||
await groupQuery(
|
await groupQuery('@g.us', 'set', [
|
||||||
'@g.us',
|
{
|
||||||
'set',
|
tag: 'leave',
|
||||||
[
|
attrs: {},
|
||||||
{
|
content: [{ tag: 'group', attrs: { id } }]
|
||||||
tag: 'leave',
|
}
|
||||||
attrs: { },
|
])
|
||||||
content: [
|
|
||||||
{ tag: 'group', attrs: { id } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
groupUpdateSubject: async(jid: string, subject: string) => {
|
groupUpdateSubject: async (jid: string, subject: string) => {
|
||||||
await groupQuery(
|
await groupQuery(jid, 'set', [
|
||||||
jid,
|
{
|
||||||
'set',
|
tag: 'subject',
|
||||||
[
|
attrs: {},
|
||||||
{
|
content: Buffer.from(subject, 'utf-8')
|
||||||
tag: 'subject',
|
}
|
||||||
attrs: { },
|
])
|
||||||
content: Buffer.from(subject, 'utf-8')
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
groupRequestParticipantsList: async(jid: string) => {
|
groupRequestParticipantsList: async (jid: string) => {
|
||||||
const result = await groupQuery(
|
const result = await groupQuery(jid, 'get', [
|
||||||
jid,
|
{
|
||||||
'get',
|
tag: 'membership_approval_requests',
|
||||||
[
|
attrs: {}
|
||||||
{
|
}
|
||||||
tag: 'membership_approval_requests',
|
])
|
||||||
attrs: {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
const node = getBinaryNodeChild(result, 'membership_approval_requests')
|
const node = getBinaryNodeChild(result, 'membership_approval_requests')
|
||||||
const participants = getBinaryNodeChildren(node, 'membership_approval_request')
|
const participants = getBinaryNodeChildren(node, 'membership_approval_request')
|
||||||
return participants.map(v => v.attrs)
|
return participants.map(v => v.attrs)
|
||||||
},
|
},
|
||||||
groupRequestParticipantsUpdate: async(jid: string, participants: string[], action: 'approve' | 'reject') => {
|
groupRequestParticipantsUpdate: async (jid: string, participants: string[], action: 'approve' | 'reject') => {
|
||||||
const result = await groupQuery(
|
const result = await groupQuery(jid, 'set', [
|
||||||
jid,
|
{
|
||||||
'set',
|
|
||||||
[{
|
|
||||||
tag: 'membership_requests_action',
|
tag: 'membership_requests_action',
|
||||||
attrs: {},
|
attrs: {},
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
tag: action,
|
tag: action,
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: participants.map(jid => ({
|
content: participants.map(jid => ({
|
||||||
tag: 'participant',
|
tag: 'participant',
|
||||||
attrs: { jid }
|
attrs: { jid }
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}]
|
}
|
||||||
)
|
])
|
||||||
const node = getBinaryNodeChild(result, 'membership_requests_action')
|
const node = getBinaryNodeChild(result, 'membership_requests_action')
|
||||||
const nodeAction = getBinaryNodeChild(node, action)
|
const nodeAction = getBinaryNodeChild(node, action)
|
||||||
const participantsAffected = getBinaryNodeChildren(nodeAction, 'participant')
|
const participantsAffected = getBinaryNodeChildren(nodeAction, 'participant')
|
||||||
@@ -171,63 +159,49 @@ export const makeGroupsSocket = (config: SocketConfig) => {
|
|||||||
return { status: p.attrs.error || '200', jid: p.attrs.jid }
|
return { status: p.attrs.error || '200', jid: p.attrs.jid }
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
groupParticipantsUpdate: async(
|
groupParticipantsUpdate: async (jid: string, participants: string[], action: ParticipantAction) => {
|
||||||
jid: string,
|
const result = await groupQuery(jid, 'set', [
|
||||||
participants: string[],
|
{
|
||||||
action: ParticipantAction
|
tag: action,
|
||||||
) => {
|
attrs: {},
|
||||||
const result = await groupQuery(
|
content: participants.map(jid => ({
|
||||||
jid,
|
tag: 'participant',
|
||||||
'set',
|
attrs: { jid }
|
||||||
[
|
}))
|
||||||
{
|
}
|
||||||
tag: action,
|
])
|
||||||
attrs: { },
|
|
||||||
content: participants.map(jid => ({
|
|
||||||
tag: 'participant',
|
|
||||||
attrs: { jid }
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
const node = getBinaryNodeChild(result, action)
|
const node = getBinaryNodeChild(result, action)
|
||||||
const participantsAffected = getBinaryNodeChildren(node, 'participant')
|
const participantsAffected = getBinaryNodeChildren(node, 'participant')
|
||||||
return participantsAffected.map(p => {
|
return participantsAffected.map(p => {
|
||||||
return { status: p.attrs.error || '200', jid: p.attrs.jid, content: p }
|
return { status: p.attrs.error || '200', jid: p.attrs.jid, content: p }
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
groupUpdateDescription: async(jid: string, description?: string) => {
|
groupUpdateDescription: async (jid: string, description?: string) => {
|
||||||
const metadata = await groupMetadata(jid)
|
const metadata = await groupMetadata(jid)
|
||||||
const prev = metadata.descId ?? null
|
const prev = metadata.descId ?? null
|
||||||
|
|
||||||
await groupQuery(
|
await groupQuery(jid, 'set', [
|
||||||
jid,
|
{
|
||||||
'set',
|
tag: 'description',
|
||||||
[
|
attrs: {
|
||||||
{
|
...(description ? { id: generateMessageIDV2() } : { delete: 'true' }),
|
||||||
tag: 'description',
|
...(prev ? { prev } : {})
|
||||||
attrs: {
|
},
|
||||||
...(description ? { id: generateMessageIDV2() } : { delete: 'true' }),
|
content: description ? [{ tag: 'body', attrs: {}, content: Buffer.from(description, 'utf-8') }] : undefined
|
||||||
...(prev ? { prev } : {})
|
}
|
||||||
},
|
])
|
||||||
content: description ? [
|
|
||||||
{ tag: 'body', attrs: {}, content: Buffer.from(description, 'utf-8') }
|
|
||||||
] : undefined
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
groupInviteCode: async(jid: string) => {
|
groupInviteCode: async (jid: string) => {
|
||||||
const result = await groupQuery(jid, 'get', [{ tag: 'invite', attrs: {} }])
|
const result = await groupQuery(jid, 'get', [{ tag: 'invite', attrs: {} }])
|
||||||
const inviteNode = getBinaryNodeChild(result, 'invite')
|
const inviteNode = getBinaryNodeChild(result, 'invite')
|
||||||
return inviteNode?.attrs.code
|
return inviteNode?.attrs.code
|
||||||
},
|
},
|
||||||
groupRevokeInvite: async(jid: string) => {
|
groupRevokeInvite: async (jid: string) => {
|
||||||
const result = await groupQuery(jid, 'set', [{ tag: 'invite', attrs: {} }])
|
const result = await groupQuery(jid, 'set', [{ tag: 'invite', attrs: {} }])
|
||||||
const inviteNode = getBinaryNodeChild(result, 'invite')
|
const inviteNode = getBinaryNodeChild(result, 'invite')
|
||||||
return inviteNode?.attrs.code
|
return inviteNode?.attrs.code
|
||||||
},
|
},
|
||||||
groupAcceptInvite: async(code: string) => {
|
groupAcceptInvite: async (code: string) => {
|
||||||
const results = await groupQuery('@g.us', 'set', [{ tag: 'invite', attrs: { code } }])
|
const results = await groupQuery('@g.us', 'set', [{ tag: 'invite', attrs: { code } }])
|
||||||
const result = getBinaryNodeChild(results, 'group')
|
const result = getBinaryNodeChild(results, 'group')
|
||||||
return result?.attrs.jid
|
return result?.attrs.jid
|
||||||
@@ -239,8 +213,10 @@ export const makeGroupsSocket = (config: SocketConfig) => {
|
|||||||
* @param invitedJid jid of person you invited
|
* @param invitedJid jid of person you invited
|
||||||
* @returns true if successful
|
* @returns true if successful
|
||||||
*/
|
*/
|
||||||
groupRevokeInviteV4: async(groupJid: string, invitedJid: string) => {
|
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
|
return !!result
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -249,87 +225,90 @@ 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 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
|
* @param inviteMessage the message to accept
|
||||||
*/
|
*/
|
||||||
groupAcceptInviteV4: ev.createBufferedFunction(async(key: string | WAMessageKey, inviteMessage: proto.Message.IGroupInviteMessage) => {
|
groupAcceptInviteV4: ev.createBufferedFunction(
|
||||||
key = typeof key === 'string' ? { remoteJid: key } : key
|
async (key: string | WAMessageKey, inviteMessage: proto.Message.IGroupInviteMessage) => {
|
||||||
const results = await groupQuery(inviteMessage.groupJid!, 'set', [{
|
key = typeof key === 'string' ? { remoteJid: key } : key
|
||||||
tag: 'accept',
|
const results = await groupQuery(inviteMessage.groupJid!, 'set', [
|
||||||
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
|
|
||||||
if(key.id) {
|
|
||||||
// create new invite message that is expired
|
|
||||||
inviteMessage = proto.Message.GroupInviteMessage.fromObject(inviteMessage)
|
|
||||||
inviteMessage.inviteExpiration = 0
|
|
||||||
inviteMessage.inviteCode = ''
|
|
||||||
ev.emit('messages.update', [
|
|
||||||
{
|
{
|
||||||
key,
|
tag: 'accept',
|
||||||
update: {
|
attrs: {
|
||||||
message: {
|
code: inviteMessage.inviteCode!,
|
||||||
groupInviteMessage: inviteMessage
|
expiration: inviteMessage.inviteExpiration!.toString(),
|
||||||
}
|
admin: key.remoteJid!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
}
|
|
||||||
|
|
||||||
// generate the group add message
|
// if we have the full message key
|
||||||
await upsertMessage(
|
// update the invite message to be expired
|
||||||
{
|
if (key.id) {
|
||||||
key: {
|
// create new invite message that is expired
|
||||||
remoteJid: inviteMessage.groupJid,
|
inviteMessage = proto.Message.GroupInviteMessage.fromObject(inviteMessage)
|
||||||
id: generateMessageIDV2(sock.user?.id),
|
inviteMessage.inviteExpiration = 0
|
||||||
fromMe: false,
|
inviteMessage.inviteCode = ''
|
||||||
|
ev.emit('messages.update', [
|
||||||
|
{
|
||||||
|
key,
|
||||||
|
update: {
|
||||||
|
message: {
|
||||||
|
groupInviteMessage: inviteMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate the group add message
|
||||||
|
await upsertMessage(
|
||||||
|
{
|
||||||
|
key: {
|
||||||
|
remoteJid: inviteMessage.groupJid,
|
||||||
|
id: generateMessageIDV2(sock.user?.id),
|
||||||
|
fromMe: false,
|
||||||
|
participant: key.remoteJid
|
||||||
|
},
|
||||||
|
messageStubType: WAMessageStubType.GROUP_PARTICIPANT_ADD,
|
||||||
|
messageStubParameters: [authState.creds.me!.id],
|
||||||
participant: key.remoteJid,
|
participant: key.remoteJid,
|
||||||
|
messageTimestamp: unixTimestampSeconds()
|
||||||
},
|
},
|
||||||
messageStubType: WAMessageStubType.GROUP_PARTICIPANT_ADD,
|
'notify'
|
||||||
messageStubParameters: [
|
)
|
||||||
authState.creds.me!.id
|
|
||||||
],
|
|
||||||
participant: key.remoteJid,
|
|
||||||
messageTimestamp: unixTimestampSeconds()
|
|
||||||
},
|
|
||||||
'notify'
|
|
||||||
)
|
|
||||||
|
|
||||||
return results.attrs.from
|
return results.attrs.from
|
||||||
}),
|
}
|
||||||
groupGetInviteInfo: async(code: string) => {
|
),
|
||||||
|
groupGetInviteInfo: async (code: string) => {
|
||||||
const results = await groupQuery('@g.us', 'get', [{ tag: 'invite', attrs: { code } }])
|
const results = await groupQuery('@g.us', 'get', [{ tag: 'invite', attrs: { code } }])
|
||||||
return extractGroupMetadata(results)
|
return extractGroupMetadata(results)
|
||||||
},
|
},
|
||||||
groupToggleEphemeral: async(jid: string, ephemeralExpiration: number) => {
|
groupToggleEphemeral: async (jid: string, ephemeralExpiration: number) => {
|
||||||
const content: BinaryNode = ephemeralExpiration ?
|
const content: BinaryNode = ephemeralExpiration
|
||||||
{ tag: 'ephemeral', attrs: { expiration: ephemeralExpiration.toString() } } :
|
? { tag: 'ephemeral', attrs: { expiration: ephemeralExpiration.toString() } }
|
||||||
{ tag: 'not_ephemeral', attrs: { } }
|
: { tag: 'not_ephemeral', attrs: {} }
|
||||||
await groupQuery(jid, 'set', [content])
|
await groupQuery(jid, 'set', [content])
|
||||||
},
|
},
|
||||||
groupSettingUpdate: async(jid: string, setting: 'announcement' | 'not_announcement' | 'locked' | 'unlocked') => {
|
groupSettingUpdate: async (jid: string, setting: 'announcement' | 'not_announcement' | 'locked' | 'unlocked') => {
|
||||||
await groupQuery(jid, 'set', [ { tag: setting, attrs: { } } ])
|
await groupQuery(jid, 'set', [{ tag: setting, attrs: {} }])
|
||||||
},
|
},
|
||||||
groupMemberAddMode: async(jid: string, mode: 'admin_add' | 'all_member_add') => {
|
groupMemberAddMode: async (jid: string, mode: 'admin_add' | 'all_member_add') => {
|
||||||
await groupQuery(jid, 'set', [ { tag: 'member_add_mode', attrs: { }, content: mode } ])
|
await groupQuery(jid, 'set', [{ tag: 'member_add_mode', attrs: {}, content: mode }])
|
||||||
},
|
},
|
||||||
groupJoinApprovalMode: async(jid: string, mode: 'on' | 'off') => {
|
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
|
groupFetchAllParticipating
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const extractGroupMetadata = (result: BinaryNode) => {
|
export const extractGroupMetadata = (result: BinaryNode) => {
|
||||||
const group = getBinaryNodeChild(result, 'group')!
|
const group = getBinaryNodeChild(result, 'group')!
|
||||||
const descChild = getBinaryNodeChild(group, 'description')
|
const descChild = getBinaryNodeChild(group, 'description')
|
||||||
let desc: string | undefined
|
let desc: string | undefined
|
||||||
let descId: string | undefined
|
let descId: string | undefined
|
||||||
if(descChild) {
|
if (descChild) {
|
||||||
desc = getBinaryNodeChildString(descChild, 'body')
|
desc = getBinaryNodeChildString(descChild, 'body')
|
||||||
descId = descChild.attrs.id
|
descId = descChild.attrs.id
|
||||||
}
|
}
|
||||||
@@ -355,14 +334,12 @@ export const extractGroupMetadata = (result: BinaryNode) => {
|
|||||||
isCommunityAnnounce: !!getBinaryNodeChild(group, 'default_sub_group'),
|
isCommunityAnnounce: !!getBinaryNodeChild(group, 'default_sub_group'),
|
||||||
joinApprovalMode: !!getBinaryNodeChild(group, 'membership_approval_mode'),
|
joinApprovalMode: !!getBinaryNodeChild(group, 'membership_approval_mode'),
|
||||||
memberAddMode,
|
memberAddMode,
|
||||||
participants: getBinaryNodeChildren(group, 'participant').map(
|
participants: getBinaryNodeChildren(group, 'participant').map(({ attrs }) => {
|
||||||
({ attrs }) => {
|
return {
|
||||||
return {
|
id: attrs.jid,
|
||||||
id: attrs.jid,
|
admin: (attrs.type || null) as GroupParticipant['admin']
|
||||||
admin: (attrs.type || null) as GroupParticipant['admin'],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
),
|
}),
|
||||||
ephemeralDuration: eph ? +eph : undefined
|
ephemeralDuration: eph ? +eph : undefined
|
||||||
}
|
}
|
||||||
return metadata
|
return metadata
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ import { UserFacingSocketConfig } from '../Types'
|
|||||||
import { makeBusinessSocket } from './business'
|
import { makeBusinessSocket } from './business'
|
||||||
|
|
||||||
// export the last socket layer
|
// export the last socket layer
|
||||||
const makeWASocket = (config: UserFacingSocketConfig) => (
|
const makeWASocket = (config: UserFacingSocketConfig) =>
|
||||||
makeBusinessSocket({
|
makeBusinessSocket({
|
||||||
...DEFAULT_CONNECTION_CONFIG,
|
...DEFAULT_CONNECTION_CONFIG,
|
||||||
...config
|
...config
|
||||||
})
|
})
|
||||||
)
|
|
||||||
|
|
||||||
export default makeWASocket
|
export default makeWASocket
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -28,7 +28,7 @@ import {
|
|||||||
getPlatformId,
|
getPlatformId,
|
||||||
makeEventBuffer,
|
makeEventBuffer,
|
||||||
makeNoiseHandler,
|
makeNoiseHandler,
|
||||||
promiseTimeout,
|
promiseTimeout
|
||||||
} from '../Utils'
|
} from '../Utils'
|
||||||
import {
|
import {
|
||||||
assertNodeErrorFree,
|
assertNodeErrorFree,
|
||||||
@@ -61,21 +61,22 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
defaultQueryTimeoutMs,
|
defaultQueryTimeoutMs,
|
||||||
transactionOpts,
|
transactionOpts,
|
||||||
qrTimeout,
|
qrTimeout,
|
||||||
makeSignalRepository,
|
makeSignalRepository
|
||||||
} = config
|
} = config
|
||||||
|
|
||||||
if(printQRInTerminal) {
|
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
|
const url = typeof waWebSocketUrl === 'string' ? new URL(waWebSocketUrl) : waWebSocketUrl
|
||||||
|
|
||||||
|
if (config.mobile || url.protocol === 'tcp:') {
|
||||||
if(config.mobile || url.protocol === 'tcp:') {
|
|
||||||
throw new Boom('Mobile API is not supported anymore', { statusCode: DisconnectReason.loggedOut })
|
throw new Boom('Mobile API is not supported anymore', { statusCode: DisconnectReason.loggedOut })
|
||||||
}
|
}
|
||||||
|
|
||||||
if(url.protocol === 'wss' && authState?.creds?.routingInfo) {
|
if (url.protocol === 'wss' && authState?.creds?.routingInfo) {
|
||||||
url.searchParams.append('ED', authState.creds.routingInfo.toString('base64url'))
|
url.searchParams.append('ED', authState.creds.routingInfo.toString('base64url'))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,28 +111,25 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
const sendPromise = promisify(ws.send)
|
const sendPromise = promisify(ws.send)
|
||||||
/** send a raw buffer */
|
/** send a raw buffer */
|
||||||
const sendRawMessage = async(data: Uint8Array | Buffer) => {
|
const sendRawMessage = async (data: Uint8Array | Buffer) => {
|
||||||
if(!ws.isOpen) {
|
if (!ws.isOpen) {
|
||||||
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
|
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
|
||||||
}
|
}
|
||||||
|
|
||||||
const bytes = noise.encodeFrame(data)
|
const bytes = noise.encodeFrame(data)
|
||||||
await promiseTimeout<void>(
|
await promiseTimeout<void>(connectTimeoutMs, async (resolve, reject) => {
|
||||||
connectTimeoutMs,
|
try {
|
||||||
async(resolve, reject) => {
|
await sendPromise.call(ws, bytes)
|
||||||
try {
|
resolve()
|
||||||
await sendPromise.call(ws, bytes)
|
} catch (error) {
|
||||||
resolve()
|
reject(error)
|
||||||
} catch(error) {
|
|
||||||
reject(error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** send a binary node */
|
/** send a binary node */
|
||||||
const sendNode = (frame: BinaryNode) => {
|
const sendNode = (frame: BinaryNode) => {
|
||||||
if(logger.level === 'trace') {
|
if (logger.level === 'trace') {
|
||||||
logger.trace({ xml: binaryNodeToString(frame), msg: 'xml send' })
|
logger.trace({ xml: binaryNodeToString(frame), msg: 'xml send' })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,15 +139,12 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
/** log & process any unexpected errors */
|
/** log & process any unexpected errors */
|
||||||
const onUnexpectedError = (err: Error | Boom, msg: string) => {
|
const onUnexpectedError = (err: Error | Boom, msg: string) => {
|
||||||
logger.error(
|
logger.error({ err }, `unexpected error in '${msg}'`)
|
||||||
{ err },
|
|
||||||
`unexpected error in '${msg}'`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** await the next incoming message */
|
/** await the next incoming message */
|
||||||
const awaitNextMessage = async<T>(sendMsg?: Uint8Array) => {
|
const awaitNextMessage = async <T>(sendMsg?: Uint8Array) => {
|
||||||
if(!ws.isOpen) {
|
if (!ws.isOpen) {
|
||||||
throw new Boom('Connection Closed', {
|
throw new Boom('Connection Closed', {
|
||||||
statusCode: DisconnectReason.connectionClosed
|
statusCode: DisconnectReason.connectionClosed
|
||||||
})
|
})
|
||||||
@@ -164,14 +159,13 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
ws.on('frame', onOpen)
|
ws.on('frame', onOpen)
|
||||||
ws.on('close', onClose)
|
ws.on('close', onClose)
|
||||||
ws.on('error', onClose)
|
ws.on('error', onClose)
|
||||||
|
}).finally(() => {
|
||||||
|
ws.off('frame', onOpen)
|
||||||
|
ws.off('close', onClose)
|
||||||
|
ws.off('error', onClose)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
|
||||||
ws.off('frame', onOpen)
|
|
||||||
ws.off('close', onClose)
|
|
||||||
ws.off('error', onClose)
|
|
||||||
})
|
|
||||||
|
|
||||||
if(sendMsg) {
|
if (sendMsg) {
|
||||||
sendRawMessage(sendMsg).catch(onClose!)
|
sendRawMessage(sendMsg).catch(onClose!)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,22 +177,20 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
* @param msgId the message tag to await
|
* @param msgId the message tag to await
|
||||||
* @param timeoutMs timeout after which the promise will reject
|
* @param timeoutMs timeout after which the promise will reject
|
||||||
*/
|
*/
|
||||||
const waitForMessage = async<T>(msgId: string, timeoutMs = defaultQueryTimeoutMs) => {
|
const waitForMessage = async <T>(msgId: string, timeoutMs = defaultQueryTimeoutMs) => {
|
||||||
let onRecv: (json) => void
|
let onRecv: (json) => void
|
||||||
let onErr: (err) => void
|
let onErr: (err) => void
|
||||||
try {
|
try {
|
||||||
const result = await promiseTimeout<T>(timeoutMs,
|
const result = await promiseTimeout<T>(timeoutMs, (resolve, reject) => {
|
||||||
(resolve, reject) => {
|
onRecv = resolve
|
||||||
onRecv = resolve
|
onErr = err => {
|
||||||
onErr = err => {
|
reject(err || new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }))
|
||||||
reject(err || new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }))
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ws.on(`TAG:${msgId}`, onRecv)
|
ws.on(`TAG:${msgId}`, onRecv)
|
||||||
ws.on('close', onErr) // if the socket closes, you'll never receive the message
|
ws.on('close', onErr) // if the socket closes, you'll never receive the message
|
||||||
ws.off('error', onErr)
|
ws.off('error', onErr)
|
||||||
},
|
})
|
||||||
)
|
|
||||||
|
|
||||||
return result as any
|
return result as any
|
||||||
} finally {
|
} finally {
|
||||||
@@ -209,19 +201,16 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** send a query, and wait for its response. auto-generates message ID if not provided */
|
/** send a query, and wait for its response. auto-generates message ID if not provided */
|
||||||
const query = async(node: BinaryNode, timeoutMs?: number) => {
|
const query = async (node: BinaryNode, timeoutMs?: number) => {
|
||||||
if(!node.attrs.id) {
|
if (!node.attrs.id) {
|
||||||
node.attrs.id = generateMessageTag()
|
node.attrs.id = generateMessageTag()
|
||||||
}
|
}
|
||||||
|
|
||||||
const msgId = node.attrs.id
|
const msgId = node.attrs.id
|
||||||
|
|
||||||
const [result] = await Promise.all([
|
const [result] = await Promise.all([waitForMessage(msgId, timeoutMs), sendNode(node)])
|
||||||
waitForMessage(msgId, timeoutMs),
|
|
||||||
sendNode(node)
|
|
||||||
])
|
|
||||||
|
|
||||||
if('tag' in result) {
|
if ('tag' in result) {
|
||||||
assertNodeErrorFree(result)
|
assertNodeErrorFree(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +218,7 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** connection handshake */
|
/** connection handshake */
|
||||||
const validateConnection = async() => {
|
const validateConnection = async () => {
|
||||||
let helloMsg: proto.IHandshakeMessage = {
|
let helloMsg: proto.IHandshakeMessage = {
|
||||||
clientHello: { ephemeral: ephemeralKeyPair.public }
|
clientHello: { ephemeral: ephemeralKeyPair.public }
|
||||||
}
|
}
|
||||||
@@ -247,7 +236,7 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
const keyEnc = await noise.processHandshake(handshake, creds.noiseKey)
|
const keyEnc = await noise.processHandshake(handshake, creds.noiseKey)
|
||||||
|
|
||||||
let node: proto.IClientPayload
|
let node: proto.IClientPayload
|
||||||
if(!creds.me) {
|
if (!creds.me) {
|
||||||
node = generateRegistrationNode(creds, config)
|
node = generateRegistrationNode(creds, config)
|
||||||
logger.info({ node }, 'not logged in, attempting registration...')
|
logger.info({ node }, 'not logged in, attempting registration...')
|
||||||
} else {
|
} else {
|
||||||
@@ -255,22 +244,20 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
logger.info({ node }, 'logging in...')
|
logger.info({ node }, 'logging in...')
|
||||||
}
|
}
|
||||||
|
|
||||||
const payloadEnc = noise.encrypt(
|
const payloadEnc = noise.encrypt(proto.ClientPayload.encode(node).finish())
|
||||||
proto.ClientPayload.encode(node).finish()
|
|
||||||
)
|
|
||||||
await sendRawMessage(
|
await sendRawMessage(
|
||||||
proto.HandshakeMessage.encode({
|
proto.HandshakeMessage.encode({
|
||||||
clientFinish: {
|
clientFinish: {
|
||||||
static: keyEnc,
|
static: keyEnc,
|
||||||
payload: payloadEnc,
|
payload: payloadEnc
|
||||||
},
|
}
|
||||||
}).finish()
|
}).finish()
|
||||||
)
|
)
|
||||||
noise.finishInit()
|
noise.finishInit()
|
||||||
startKeepAliveRequest()
|
startKeepAliveRequest()
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAvailablePreKeysOnServer = async() => {
|
const getAvailablePreKeysOnServer = async () => {
|
||||||
const result = await query({
|
const result = await query({
|
||||||
tag: 'iq',
|
tag: 'iq',
|
||||||
attrs: {
|
attrs: {
|
||||||
@@ -279,33 +266,29 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
type: 'get',
|
type: 'get',
|
||||||
to: S_WHATSAPP_NET
|
to: S_WHATSAPP_NET
|
||||||
},
|
},
|
||||||
content: [
|
content: [{ tag: 'count', attrs: {} }]
|
||||||
{ tag: 'count', attrs: {} }
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
const countChild = getBinaryNodeChild(result, 'count')
|
const countChild = getBinaryNodeChild(result, 'count')
|
||||||
return +countChild!.attrs.value
|
return +countChild!.attrs.value
|
||||||
}
|
}
|
||||||
|
|
||||||
/** generates and uploads a set of pre-keys to the server */
|
/** generates and uploads a set of pre-keys to the server */
|
||||||
const uploadPreKeys = async(count = INITIAL_PREKEY_COUNT) => {
|
const uploadPreKeys = async (count = INITIAL_PREKEY_COUNT) => {
|
||||||
await keys.transaction(
|
await keys.transaction(async () => {
|
||||||
async() => {
|
logger.info({ count }, 'uploading pre-keys')
|
||||||
logger.info({ count }, 'uploading pre-keys')
|
const { update, node } = await getNextPreKeysNode({ creds, keys }, count)
|
||||||
const { update, node } = await getNextPreKeysNode({ creds, keys }, count)
|
|
||||||
|
|
||||||
await query(node)
|
await query(node)
|
||||||
ev.emit('creds.update', update)
|
ev.emit('creds.update', update)
|
||||||
|
|
||||||
logger.info({ count }, 'uploaded pre-keys')
|
logger.info({ count }, 'uploaded pre-keys')
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadPreKeysToServerIfRequired = async() => {
|
const uploadPreKeysToServerIfRequired = async () => {
|
||||||
const preKeyCount = await getAvailablePreKeysOnServer()
|
const preKeyCount = await getAvailablePreKeysOnServer()
|
||||||
logger.info(`${preKeyCount} pre-keys found on server`)
|
logger.info(`${preKeyCount} pre-keys found on server`)
|
||||||
if(preKeyCount <= MIN_PREKEY_COUNT) {
|
if (preKeyCount <= MIN_PREKEY_COUNT) {
|
||||||
await uploadPreKeys()
|
await uploadPreKeys()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,10 +302,10 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
anyTriggered = ws.emit('frame', frame)
|
anyTriggered = ws.emit('frame', frame)
|
||||||
// if it's a binary node
|
// if it's a binary node
|
||||||
if(!(frame instanceof Uint8Array)) {
|
if (!(frame instanceof Uint8Array)) {
|
||||||
const msgId = frame.attrs.id
|
const msgId = frame.attrs.id
|
||||||
|
|
||||||
if(logger.level === 'trace') {
|
if (logger.level === 'trace') {
|
||||||
logger.trace({ xml: binaryNodeToString(frame), msg: 'recv xml' })
|
logger.trace({ xml: binaryNodeToString(frame), msg: 'recv xml' })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,7 +316,7 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
const l1 = frame.attrs || {}
|
const l1 = frame.attrs || {}
|
||||||
const l2 = Array.isArray(frame.content) ? frame.content[0]?.tag : ''
|
const l2 = Array.isArray(frame.content) ? frame.content[0]?.tag : ''
|
||||||
|
|
||||||
for(const key of Object.keys(l1)) {
|
for (const key of Object.keys(l1)) {
|
||||||
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, frame) || anyTriggered
|
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, frame) || anyTriggered
|
||||||
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, frame) || anyTriggered
|
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, frame) || anyTriggered
|
||||||
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}`, frame) || anyTriggered
|
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}`, frame) || anyTriggered
|
||||||
@@ -342,7 +325,7 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},,${l2}`, frame) || anyTriggered
|
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},,${l2}`, frame) || anyTriggered
|
||||||
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0}`, frame) || anyTriggered
|
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0}`, frame) || anyTriggered
|
||||||
|
|
||||||
if(!anyTriggered && logger.level === 'debug') {
|
if (!anyTriggered && logger.level === 'debug') {
|
||||||
logger.debug({ unhandled: true, msgId, fromMe: false, frame }, 'communication recv')
|
logger.debug({ unhandled: true, msgId, fromMe: false, frame }, 'communication recv')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -350,16 +333,13 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const end = (error: Error | undefined) => {
|
const end = (error: Error | undefined) => {
|
||||||
if(closed) {
|
if (closed) {
|
||||||
logger.trace({ trace: error?.stack }, 'connection already closed')
|
logger.trace({ trace: error?.stack }, 'connection already closed')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
closed = true
|
closed = true
|
||||||
logger.info(
|
logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed')
|
||||||
{ trace: error?.stack },
|
|
||||||
error ? 'connection errored' : 'connection closed'
|
|
||||||
)
|
|
||||||
|
|
||||||
clearInterval(keepAliveReq)
|
clearInterval(keepAliveReq)
|
||||||
clearTimeout(qrTimer)
|
clearTimeout(qrTimer)
|
||||||
@@ -369,10 +349,10 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
ws.removeAllListeners('open')
|
ws.removeAllListeners('open')
|
||||||
ws.removeAllListeners('message')
|
ws.removeAllListeners('message')
|
||||||
|
|
||||||
if(!ws.isClosed && !ws.isClosing) {
|
if (!ws.isClosed && !ws.isClosing) {
|
||||||
try {
|
try {
|
||||||
ws.close()
|
ws.close()
|
||||||
} catch{ }
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
ev.emit('connection.update', {
|
ev.emit('connection.update', {
|
||||||
@@ -385,12 +365,12 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
ev.removeAllListeners('connection.update')
|
ev.removeAllListeners('connection.update')
|
||||||
}
|
}
|
||||||
|
|
||||||
const waitForSocketOpen = async() => {
|
const waitForSocketOpen = async () => {
|
||||||
if(ws.isOpen) {
|
if (ws.isOpen) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if(ws.isClosed || ws.isClosing) {
|
if (ws.isClosed || ws.isClosing) {
|
||||||
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
|
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,17 +382,16 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
ws.on('open', onOpen)
|
ws.on('open', onOpen)
|
||||||
ws.on('close', onClose)
|
ws.on('close', onClose)
|
||||||
ws.on('error', onClose)
|
ws.on('error', onClose)
|
||||||
|
}).finally(() => {
|
||||||
|
ws.off('open', onOpen)
|
||||||
|
ws.off('close', onClose)
|
||||||
|
ws.off('error', onClose)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
|
||||||
ws.off('open', onOpen)
|
|
||||||
ws.off('close', onClose)
|
|
||||||
ws.off('error', onClose)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const startKeepAliveRequest = () => (
|
const startKeepAliveRequest = () =>
|
||||||
keepAliveReq = setInterval(() => {
|
(keepAliveReq = setInterval(() => {
|
||||||
if(!lastDateRecv) {
|
if (!lastDateRecv) {
|
||||||
lastDateRecv = new Date()
|
lastDateRecv = new Date()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,49 +400,42 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
check if it's been a suspicious amount of time since the server responded with our last seen
|
check if it's been a suspicious amount of time since the server responded with our last seen
|
||||||
it could be that the network is down
|
it could be that the network is down
|
||||||
*/
|
*/
|
||||||
if(diff > keepAliveIntervalMs + 5000) {
|
if (diff > keepAliveIntervalMs + 5000) {
|
||||||
end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }))
|
end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }))
|
||||||
} else if(ws.isOpen) {
|
} else if (ws.isOpen) {
|
||||||
// if its all good, send a keep alive request
|
// if its all good, send a keep alive request
|
||||||
query(
|
query({
|
||||||
{
|
tag: 'iq',
|
||||||
tag: 'iq',
|
attrs: {
|
||||||
attrs: {
|
id: generateMessageTag(),
|
||||||
id: generateMessageTag(),
|
to: S_WHATSAPP_NET,
|
||||||
to: S_WHATSAPP_NET,
|
type: 'get',
|
||||||
type: 'get',
|
xmlns: 'w:p'
|
||||||
xmlns: 'w:p',
|
},
|
||||||
},
|
content: [{ tag: 'ping', attrs: {} }]
|
||||||
content: [{ tag: 'ping', attrs: {} }]
|
}).catch(err => {
|
||||||
}
|
logger.error({ trace: err.stack }, 'error in sending keep alive')
|
||||||
)
|
})
|
||||||
.catch(err => {
|
|
||||||
logger.error({ trace: err.stack }, 'error in sending keep alive')
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
logger.warn('keep alive called when WS not open')
|
logger.warn('keep alive called when WS not open')
|
||||||
}
|
}
|
||||||
}, keepAliveIntervalMs)
|
}, keepAliveIntervalMs))
|
||||||
)
|
|
||||||
/** i have no idea why this exists. pls enlighten me */
|
/** i have no idea why this exists. pls enlighten me */
|
||||||
const sendPassiveIq = (tag: 'passive' | 'active') => (
|
const sendPassiveIq = (tag: 'passive' | 'active') =>
|
||||||
query({
|
query({
|
||||||
tag: 'iq',
|
tag: 'iq',
|
||||||
attrs: {
|
attrs: {
|
||||||
to: S_WHATSAPP_NET,
|
to: S_WHATSAPP_NET,
|
||||||
xmlns: 'passive',
|
xmlns: 'passive',
|
||||||
type: 'set',
|
type: 'set'
|
||||||
},
|
},
|
||||||
content: [
|
content: [{ tag, attrs: {} }]
|
||||||
{ tag, attrs: {} }
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
)
|
|
||||||
|
|
||||||
/** logout & invalidate connection */
|
/** logout & invalidate connection */
|
||||||
const logout = async(msg?: string) => {
|
const logout = async (msg?: string) => {
|
||||||
const jid = authState.creds.me?.id
|
const jid = authState.creds.me?.id
|
||||||
if(jid) {
|
if (jid) {
|
||||||
await sendNode({
|
await sendNode({
|
||||||
tag: 'iq',
|
tag: 'iq',
|
||||||
attrs: {
|
attrs: {
|
||||||
@@ -487,7 +459,7 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }))
|
end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestPairingCode = async(phoneNumber: string): Promise<string> => {
|
const requestPairingCode = async (phoneNumber: string): Promise<string> => {
|
||||||
authState.creds.pairingCode = bytesToCrockford(randomBytes(5))
|
authState.creds.pairingCode = bytesToCrockford(randomBytes(5))
|
||||||
authState.creds.me = {
|
authState.creds.me = {
|
||||||
id: jidEncode(phoneNumber, 's.whatsapp.net'),
|
id: jidEncode(phoneNumber, 's.whatsapp.net'),
|
||||||
@@ -572,10 +544,10 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
ws.on('message', onMessageReceived)
|
ws.on('message', onMessageReceived)
|
||||||
|
|
||||||
ws.on('open', async() => {
|
ws.on('open', async () => {
|
||||||
try {
|
try {
|
||||||
await validateConnection()
|
await validateConnection()
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
logger.error({ err }, 'error in validating connection')
|
logger.error({ err }, 'error in validating connection')
|
||||||
end(err)
|
end(err)
|
||||||
}
|
}
|
||||||
@@ -583,15 +555,17 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
ws.on('error', mapWebSocketError(end))
|
ws.on('error', mapWebSocketError(end))
|
||||||
ws.on('close', () => end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })))
|
ws.on('close', () => end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })))
|
||||||
// the server terminated the connection
|
// 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
|
// QR gen
|
||||||
ws.on('CB:iq,type:set,pair-device', async(stanza: BinaryNode) => {
|
ws.on('CB:iq,type:set,pair-device', async (stanza: BinaryNode) => {
|
||||||
const iq: BinaryNode = {
|
const iq: BinaryNode = {
|
||||||
tag: 'iq',
|
tag: 'iq',
|
||||||
attrs: {
|
attrs: {
|
||||||
to: S_WHATSAPP_NET,
|
to: S_WHATSAPP_NET,
|
||||||
type: 'result',
|
type: 'result',
|
||||||
id: stanza.attrs.id,
|
id: stanza.attrs.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await sendNode(iq)
|
await sendNode(iq)
|
||||||
@@ -604,12 +578,12 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
let qrMs = qrTimeout || 60_000 // time to let a QR live
|
let qrMs = qrTimeout || 60_000 // time to let a QR live
|
||||||
const genPairQR = () => {
|
const genPairQR = () => {
|
||||||
if(!ws.isOpen) {
|
if (!ws.isOpen) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const refNode = refNodes.shift()
|
const refNode = refNodes.shift()
|
||||||
if(!refNode) {
|
if (!refNode) {
|
||||||
end(new Boom('QR refs attempts ended', { statusCode: DisconnectReason.timedOut }))
|
end(new Boom('QR refs attempts ended', { statusCode: DisconnectReason.timedOut }))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -627,7 +601,7 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
})
|
})
|
||||||
// device paired for the first time
|
// device paired for the first time
|
||||||
// if device pairs successfully, the server asks to restart the connection
|
// if device pairs successfully, the server asks to restart the connection
|
||||||
ws.on('CB:iq,,pair-success', async(stanza: BinaryNode) => {
|
ws.on('CB:iq,,pair-success', async (stanza: BinaryNode) => {
|
||||||
logger.debug('pair success recv')
|
logger.debug('pair success recv')
|
||||||
try {
|
try {
|
||||||
const { reply, creds: updatedCreds } = configureSuccessfulPairing(stanza, creds)
|
const { reply, creds: updatedCreds } = configureSuccessfulPairing(stanza, creds)
|
||||||
@@ -641,13 +615,13 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
ev.emit('connection.update', { isNewLogin: true, qr: undefined })
|
ev.emit('connection.update', { isNewLogin: true, qr: undefined })
|
||||||
|
|
||||||
await sendNode(reply)
|
await sendNode(reply)
|
||||||
} catch(error) {
|
} catch (error) {
|
||||||
logger.info({ trace: error.stack }, 'error in pairing')
|
logger.info({ trace: error.stack }, 'error in pairing')
|
||||||
end(error)
|
end(error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// login complete
|
// login complete
|
||||||
ws.on('CB:success', async(node: BinaryNode) => {
|
ws.on('CB:success', async (node: BinaryNode) => {
|
||||||
await uploadPreKeysToServerIfRequired()
|
await uploadPreKeysToServerIfRequired()
|
||||||
await sendPassiveIq('active')
|
await sendPassiveIq('active')
|
||||||
|
|
||||||
@@ -677,7 +651,7 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ws.on('CB:ib,,offline_preview', (node: BinaryNode) => {
|
ws.on('CB:ib,,offline_preview', (node: BinaryNode) => {
|
||||||
logger.info('offline preview received', JSON.stringify(node))
|
logger.info('offline preview received', JSON.stringify(node))
|
||||||
sendNode({
|
sendNode({
|
||||||
tag: 'ib',
|
tag: 'ib',
|
||||||
attrs: {},
|
attrs: {},
|
||||||
@@ -688,7 +662,7 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
ws.on('CB:ib,,edge_routing', (node: BinaryNode) => {
|
ws.on('CB:ib,,edge_routing', (node: BinaryNode) => {
|
||||||
const edgeRoutingNode = getBinaryNodeChild(node, 'edge_routing')
|
const edgeRoutingNode = getBinaryNodeChild(node, 'edge_routing')
|
||||||
const routingInfo = getBinaryNodeChild(edgeRoutingNode, 'routing_info')
|
const routingInfo = getBinaryNodeChild(edgeRoutingNode, 'routing_info')
|
||||||
if(routingInfo?.content) {
|
if (routingInfo?.content) {
|
||||||
authState.creds.routingInfo = Buffer.from(routingInfo?.content as Uint8Array)
|
authState.creds.routingInfo = Buffer.from(routingInfo?.content as Uint8Array)
|
||||||
ev.emit('creds.update', authState.creds)
|
ev.emit('creds.update', authState.creds)
|
||||||
}
|
}
|
||||||
@@ -696,7 +670,7 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
let didStartBuffer = false
|
let didStartBuffer = false
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
if(creds.me?.id) {
|
if (creds.me?.id) {
|
||||||
// start buffering important events
|
// start buffering important events
|
||||||
// if we're logged in
|
// if we're logged in
|
||||||
ev.buffer()
|
ev.buffer()
|
||||||
@@ -712,7 +686,7 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
const offlineNotifs = +(child?.attrs.count || 0)
|
const offlineNotifs = +(child?.attrs.count || 0)
|
||||||
|
|
||||||
logger.info(`handled ${offlineNotifs} offline messages/notifications`)
|
logger.info(`handled ${offlineNotifs} offline messages/notifications`)
|
||||||
if(didStartBuffer) {
|
if (didStartBuffer) {
|
||||||
ev.flush()
|
ev.flush()
|
||||||
logger.trace('flushed events for initial buffer')
|
logger.trace('flushed events for initial buffer')
|
||||||
}
|
}
|
||||||
@@ -724,21 +698,19 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
ev.on('creds.update', update => {
|
ev.on('creds.update', update => {
|
||||||
const name = update.me?.name
|
const name = update.me?.name
|
||||||
// if name has just been received
|
// if name has just been received
|
||||||
if(creds.me?.name !== name) {
|
if (creds.me?.name !== name) {
|
||||||
logger.debug({ name }, 'updated pushName')
|
logger.debug({ name }, 'updated pushName')
|
||||||
sendNode({
|
sendNode({
|
||||||
tag: 'presence',
|
tag: 'presence',
|
||||||
attrs: { name: name! }
|
attrs: { name: name! }
|
||||||
|
}).catch(err => {
|
||||||
|
logger.warn({ trace: err.stack }, 'error in sending presence update on name change')
|
||||||
})
|
})
|
||||||
.catch(err => {
|
|
||||||
logger.warn({ trace: err.stack }, 'error in sending presence update on name change')
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(creds, update)
|
Object.assign(creds, update)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'md' as 'md',
|
type: 'md' as 'md',
|
||||||
ws,
|
ws,
|
||||||
@@ -762,7 +734,7 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
requestPairingCode,
|
requestPairingCode,
|
||||||
/** Waits for the connection to WA to reach a state */
|
/** Waits for the connection to WA to reach a state */
|
||||||
waitForConnectionUpdate: bindWaitForConnectionUpdate(ev),
|
waitForConnectionUpdate: bindWaitForConnectionUpdate(ev),
|
||||||
sendWAMBuffer,
|
sendWAMBuffer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -772,11 +744,6 @@ export const makeSocket = (config: SocketConfig) => {
|
|||||||
* */
|
* */
|
||||||
function mapWebSocketError(handler: (err: Error) => void) {
|
function mapWebSocketError(handler: (err: Error) => void) {
|
||||||
return (error: Error) => {
|
return (error: Error) => {
|
||||||
handler(
|
handler(new Boom(`WebSocket Error (${error?.message})`, { statusCode: getCodeFromWSError(error), data: error }))
|
||||||
new Boom(
|
|
||||||
`WebSocket Error (${error?.message})`,
|
|
||||||
{ statusCode: getCodeFromWSError(error), data: error }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,10 @@ import { makeSocket } from './socket'
|
|||||||
export const makeUSyncSocket = (config: SocketConfig) => {
|
export const makeUSyncSocket = (config: SocketConfig) => {
|
||||||
const sock = makeSocket(config)
|
const sock = makeSocket(config)
|
||||||
|
|
||||||
const {
|
const { generateMessageTag, query } = sock
|
||||||
generateMessageTag,
|
|
||||||
query,
|
|
||||||
} = sock
|
|
||||||
|
|
||||||
const executeUSyncQuery = async(usyncQuery: USyncQuery) => {
|
const executeUSyncQuery = async (usyncQuery: USyncQuery) => {
|
||||||
if(usyncQuery.protocols.length === 0) {
|
if (usyncQuery.protocols.length === 0) {
|
||||||
throw new Boom('USyncQuery must have at least one protocol')
|
throw new Boom('USyncQuery must have at least one protocol')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,15 +18,13 @@ export const makeUSyncSocket = (config: SocketConfig) => {
|
|||||||
// variable below has only validated users
|
// variable below has only validated users
|
||||||
const validUsers = usyncQuery.users
|
const validUsers = usyncQuery.users
|
||||||
|
|
||||||
const userNodes = validUsers.map((user) => {
|
const userNodes = validUsers.map(user => {
|
||||||
return {
|
return {
|
||||||
tag: 'user',
|
tag: 'user',
|
||||||
attrs: {
|
attrs: {
|
||||||
jid: !user.phone ? user.id : undefined,
|
jid: !user.phone ? user.id : undefined
|
||||||
},
|
},
|
||||||
content: usyncQuery.protocols
|
content: usyncQuery.protocols.map(a => a.getUserElement(user)).filter(a => a !== null)
|
||||||
.map((a) => a.getUserElement(user))
|
|
||||||
.filter(a => a !== null)
|
|
||||||
} as BinaryNode
|
} as BinaryNode
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -42,14 +37,14 @@ export const makeUSyncSocket = (config: SocketConfig) => {
|
|||||||
const queryNode: BinaryNode = {
|
const queryNode: BinaryNode = {
|
||||||
tag: 'query',
|
tag: 'query',
|
||||||
attrs: {},
|
attrs: {},
|
||||||
content: usyncQuery.protocols.map((a) => a.getQueryElement())
|
content: usyncQuery.protocols.map(a => a.getQueryElement())
|
||||||
}
|
}
|
||||||
const iq = {
|
const iq = {
|
||||||
tag: 'iq',
|
tag: 'iq',
|
||||||
attrs: {
|
attrs: {
|
||||||
to: S_WHATSAPP_NET,
|
to: S_WHATSAPP_NET,
|
||||||
type: 'get',
|
type: 'get',
|
||||||
xmlns: 'usync',
|
xmlns: 'usync'
|
||||||
},
|
},
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@@ -59,14 +54,11 @@ export const makeUSyncSocket = (config: SocketConfig) => {
|
|||||||
mode: usyncQuery.mode,
|
mode: usyncQuery.mode,
|
||||||
sid: generateMessageTag(),
|
sid: generateMessageTag(),
|
||||||
last: 'true',
|
last: 'true',
|
||||||
index: '0',
|
index: '0'
|
||||||
},
|
},
|
||||||
content: [
|
content: [queryNode, listNode]
|
||||||
queryNode,
|
|
||||||
listNode
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await query(iq)
|
const result = await query(iq)
|
||||||
@@ -76,6 +68,6 @@ export const makeUSyncSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...sock,
|
...sock,
|
||||||
executeUSyncQuery,
|
executeUSyncQuery
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { processSyncAction } from '../Utils/chat-utils'
|
|||||||
import logger from '../Utils/logger'
|
import logger from '../Utils/logger'
|
||||||
|
|
||||||
describe('App State Sync Tests', () => {
|
describe('App State Sync Tests', () => {
|
||||||
|
|
||||||
const me: Contact = { id: randomJid() }
|
const me: Contact = { id: randomJid() }
|
||||||
// case when initial sync is off
|
// case when initial sync is off
|
||||||
it('should return archive=false event', () => {
|
it('should return archive=false event', () => {
|
||||||
@@ -57,7 +56,7 @@ describe('App State Sync Tests', () => {
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
for(const mutations of CASES) {
|
for (const mutations of CASES) {
|
||||||
const events = processSyncAction(mutations, me, undefined, logger)
|
const events = processSyncAction(mutations, me, undefined, logger)
|
||||||
expect(events['chats.update']).toHaveLength(1)
|
expect(events['chats.update']).toHaveLength(1)
|
||||||
const event = events['chats.update']?.[0]
|
const event = events['chats.update']?.[0]
|
||||||
@@ -129,7 +128,7 @@ describe('App State Sync Tests', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
const ctx: InitialAppStateSyncOptions = {
|
const ctx: InitialAppStateSyncOptions = {
|
||||||
@@ -139,7 +138,7 @@ describe('App State Sync Tests', () => {
|
|||||||
accountSettings: { unarchiveChats: true }
|
accountSettings: { unarchiveChats: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
for(const mutations of CASES) {
|
for (const mutations of CASES) {
|
||||||
const events = processSyncActions(mutations, me, ctx, logger)
|
const events = processSyncActions(mutations, me, ctx, logger)
|
||||||
expect(events['chats.update']?.length).toBeFalsy()
|
expect(events['chats.update']?.length).toBeFalsy()
|
||||||
}
|
}
|
||||||
@@ -152,7 +151,7 @@ describe('App State Sync Tests', () => {
|
|||||||
const index = ['archive', jid]
|
const index = ['archive', jid]
|
||||||
const now = unixTimestampSeconds()
|
const now = unixTimestampSeconds()
|
||||||
|
|
||||||
const CASES: { settings: AccountSettings, mutations: ChatMutation[] }[] = [
|
const CASES: { settings: AccountSettings; mutations: ChatMutation[] }[] = [
|
||||||
{
|
{
|
||||||
settings: { unarchiveChats: true },
|
settings: { unarchiveChats: true },
|
||||||
mutations: [
|
mutations: [
|
||||||
@@ -169,7 +168,7 @@ describe('App State Sync Tests', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
settings: { unarchiveChats: false },
|
settings: { unarchiveChats: false },
|
||||||
@@ -187,11 +186,11 @@ describe('App State Sync Tests', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
for(const { mutations, settings } of CASES) {
|
for (const { mutations, settings } of CASES) {
|
||||||
const ctx: InitialAppStateSyncOptions = {
|
const ctx: InitialAppStateSyncOptions = {
|
||||||
recvChats: {
|
recvChats: {
|
||||||
[jid]: { lastMsgRecvTimestamp: now }
|
[jid]: { lastMsgRecvTimestamp: now }
|
||||||
@@ -204,4 +203,4 @@ describe('App State Sync Tests', () => {
|
|||||||
expect(event.archive).toEqual(true)
|
expect(event.archive).toEqual(true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,15 +5,14 @@ import logger from '../Utils/logger'
|
|||||||
import { randomJid } from './utils'
|
import { randomJid } from './utils'
|
||||||
|
|
||||||
describe('Event Buffer Tests', () => {
|
describe('Event Buffer Tests', () => {
|
||||||
|
|
||||||
let ev: ReturnType<typeof makeEventBuffer>
|
let ev: ReturnType<typeof makeEventBuffer>
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const _logger = logger.child({ })
|
const _logger = logger.child({})
|
||||||
_logger.level = 'trace'
|
_logger.level = 'trace'
|
||||||
ev = makeEventBuffer(_logger)
|
ev = makeEventBuffer(_logger)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should buffer a chat upsert & update event', async() => {
|
it('should buffer a chat upsert & update event', async () => {
|
||||||
const chatId = randomJid()
|
const chatId = randomJid()
|
||||||
|
|
||||||
const chats: Chat[] = []
|
const chats: Chat[] = []
|
||||||
@@ -23,14 +22,14 @@ describe('Event Buffer Tests', () => {
|
|||||||
|
|
||||||
ev.buffer()
|
ev.buffer()
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
(async() => {
|
(async () => {
|
||||||
ev.buffer()
|
ev.buffer()
|
||||||
await delay(100)
|
await delay(100)
|
||||||
ev.emit('chats.upsert', [{ id: chatId, conversationTimestamp: 123, unreadCount: 1 }])
|
ev.emit('chats.upsert', [{ id: chatId, conversationTimestamp: 123, unreadCount: 1 }])
|
||||||
const flushed = ev.flush()
|
const flushed = ev.flush()
|
||||||
expect(flushed).toBeFalsy()
|
expect(flushed).toBeFalsy()
|
||||||
})(),
|
})(),
|
||||||
(async() => {
|
(async () => {
|
||||||
ev.buffer()
|
ev.buffer()
|
||||||
await delay(200)
|
await delay(200)
|
||||||
ev.emit('chats.update', [{ id: chatId, conversationTimestamp: 124, unreadCount: 1 }])
|
ev.emit('chats.update', [{ id: chatId, conversationTimestamp: 124, unreadCount: 1 }])
|
||||||
@@ -47,7 +46,7 @@ describe('Event Buffer Tests', () => {
|
|||||||
expect(chats[0].unreadCount).toEqual(2)
|
expect(chats[0].unreadCount).toEqual(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should overwrite a chats.delete event', async() => {
|
it('should overwrite a chats.delete event', async () => {
|
||||||
const chatId = randomJid()
|
const chatId = randomJid()
|
||||||
const chats: Partial<Chat>[] = []
|
const chats: Partial<Chat>[] = []
|
||||||
|
|
||||||
@@ -65,7 +64,7 @@ describe('Event Buffer Tests', () => {
|
|||||||
expect(chats).toHaveLength(1)
|
expect(chats).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should overwrite a chats.update event', async() => {
|
it('should overwrite a chats.update event', async () => {
|
||||||
const chatId = randomJid()
|
const chatId = randomJid()
|
||||||
const chatsDeleted: string[] = []
|
const chatsDeleted: string[] = []
|
||||||
|
|
||||||
@@ -82,7 +81,7 @@ describe('Event Buffer Tests', () => {
|
|||||||
expect(chatsDeleted).toHaveLength(1)
|
expect(chatsDeleted).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should release a conditional update at the right time', async() => {
|
it('should release a conditional update at the right time', async () => {
|
||||||
const chatId = randomJid()
|
const chatId = randomJid()
|
||||||
const chatId2 = randomJid()
|
const chatId2 = randomJid()
|
||||||
const chatsUpserted: Chat[] = []
|
const chatsUpserted: Chat[] = []
|
||||||
@@ -93,41 +92,49 @@ describe('Event Buffer Tests', () => {
|
|||||||
ev.on('chats.update', () => fail('not should have emitted'))
|
ev.on('chats.update', () => fail('not should have emitted'))
|
||||||
|
|
||||||
ev.buffer()
|
ev.buffer()
|
||||||
ev.emit('chats.update', [{
|
ev.emit('chats.update', [
|
||||||
id: chatId,
|
{
|
||||||
archived: true,
|
id: chatId,
|
||||||
conditional(buff) {
|
archived: true,
|
||||||
if(buff.chatUpserts[chatId]) {
|
conditional(buff) {
|
||||||
return true
|
if (buff.chatUpserts[chatId]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}])
|
])
|
||||||
ev.emit('chats.update', [{
|
ev.emit('chats.update', [
|
||||||
id: chatId2,
|
{
|
||||||
archived: true,
|
id: chatId2,
|
||||||
conditional(buff) {
|
archived: true,
|
||||||
if(buff.historySets.chats[chatId2]) {
|
conditional(buff) {
|
||||||
return true
|
if (buff.historySets.chats[chatId2]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}])
|
])
|
||||||
|
|
||||||
ev.flush()
|
ev.flush()
|
||||||
|
|
||||||
ev.buffer()
|
ev.buffer()
|
||||||
ev.emit('chats.upsert', [{
|
ev.emit('chats.upsert', [
|
||||||
id: chatId,
|
{
|
||||||
conversationTimestamp: 123,
|
id: chatId,
|
||||||
unreadCount: 1,
|
|
||||||
muteEndTime: 123
|
|
||||||
}])
|
|
||||||
ev.emit('messaging-history.set', {
|
|
||||||
chats: [{
|
|
||||||
id: chatId2,
|
|
||||||
conversationTimestamp: 123,
|
conversationTimestamp: 123,
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
muteEndTime: 123
|
muteEndTime: 123
|
||||||
}],
|
}
|
||||||
|
])
|
||||||
|
ev.emit('messaging-history.set', {
|
||||||
|
chats: [
|
||||||
|
{
|
||||||
|
id: chatId2,
|
||||||
|
conversationTimestamp: 123,
|
||||||
|
unreadCount: 1,
|
||||||
|
muteEndTime: 123
|
||||||
|
}
|
||||||
|
],
|
||||||
contacts: [],
|
contacts: [],
|
||||||
messages: [],
|
messages: [],
|
||||||
isLatest: false
|
isLatest: false
|
||||||
@@ -144,7 +151,7 @@ describe('Event Buffer Tests', () => {
|
|||||||
expect(chatsSynced[0].archived).toEqual(true)
|
expect(chatsSynced[0].archived).toEqual(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should discard a conditional update', async() => {
|
it('should discard a conditional update', async () => {
|
||||||
const chatId = randomJid()
|
const chatId = randomJid()
|
||||||
const chatsUpserted: Chat[] = []
|
const chatsUpserted: Chat[] = []
|
||||||
|
|
||||||
@@ -152,21 +159,25 @@ describe('Event Buffer Tests', () => {
|
|||||||
ev.on('chats.update', () => fail('not should have emitted'))
|
ev.on('chats.update', () => fail('not should have emitted'))
|
||||||
|
|
||||||
ev.buffer()
|
ev.buffer()
|
||||||
ev.emit('chats.update', [{
|
ev.emit('chats.update', [
|
||||||
id: chatId,
|
{
|
||||||
archived: true,
|
id: chatId,
|
||||||
conditional(buff) {
|
archived: true,
|
||||||
if(buff.chatUpserts[chatId]) {
|
conditional(buff) {
|
||||||
return false
|
if (buff.chatUpserts[chatId]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}])
|
])
|
||||||
ev.emit('chats.upsert', [{
|
ev.emit('chats.upsert', [
|
||||||
id: chatId,
|
{
|
||||||
conversationTimestamp: 123,
|
id: chatId,
|
||||||
unreadCount: 1,
|
conversationTimestamp: 123,
|
||||||
muteEndTime: 123
|
unreadCount: 1,
|
||||||
}])
|
muteEndTime: 123
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
ev.flush()
|
ev.flush()
|
||||||
|
|
||||||
@@ -174,7 +185,7 @@ describe('Event Buffer Tests', () => {
|
|||||||
expect(chatsUpserted[0].archived).toBeUndefined()
|
expect(chatsUpserted[0].archived).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should overwrite a chats.update event with a history event', async() => {
|
it('should overwrite a chats.update event with a history event', async () => {
|
||||||
const chatId = randomJid()
|
const chatId = randomJid()
|
||||||
let chatRecv: Chat | undefined
|
let chatRecv: Chat | undefined
|
||||||
|
|
||||||
@@ -199,7 +210,7 @@ describe('Event Buffer Tests', () => {
|
|||||||
expect(chatRecv?.archived).toBeTruthy()
|
expect(chatRecv?.archived).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should buffer message upsert events', async() => {
|
it('should buffer message upsert events', async () => {
|
||||||
const messageTimestamp = unixTimestampSeconds()
|
const messageTimestamp = unixTimestampSeconds()
|
||||||
const msg: proto.IWebMessageInfo = {
|
const msg: proto.IWebMessageInfo = {
|
||||||
key: {
|
key: {
|
||||||
@@ -235,7 +246,7 @@ describe('Event Buffer Tests', () => {
|
|||||||
expect(msgs[0].status).toEqual(WAMessageStatus.READ)
|
expect(msgs[0].status).toEqual(WAMessageStatus.READ)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should buffer a message receipt update', async() => {
|
it('should buffer a message receipt update', async () => {
|
||||||
const msg: proto.IWebMessageInfo = {
|
const msg: proto.IWebMessageInfo = {
|
||||||
key: {
|
key: {
|
||||||
remoteJid: randomJid(),
|
remoteJid: randomJid(),
|
||||||
@@ -269,7 +280,7 @@ describe('Event Buffer Tests', () => {
|
|||||||
expect(msgs[0].userReceipt).toHaveLength(1)
|
expect(msgs[0].userReceipt).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should buffer multiple status updates', async() => {
|
it('should buffer multiple status updates', async () => {
|
||||||
const key: WAMessageKey = {
|
const key: WAMessageKey = {
|
||||||
remoteJid: randomJid(),
|
remoteJid: randomJid(),
|
||||||
id: generateMessageID(),
|
id: generateMessageID(),
|
||||||
@@ -290,7 +301,7 @@ describe('Event Buffer Tests', () => {
|
|||||||
expect(msgs[0].update.status).toEqual(WAMessageStatus.READ)
|
expect(msgs[0].update.status).toEqual(WAMessageStatus.READ)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should remove chat unread counter', async() => {
|
it('should remove chat unread counter', async () => {
|
||||||
const msg: proto.IWebMessageInfo = {
|
const msg: proto.IWebMessageInfo = {
|
||||||
key: {
|
key: {
|
||||||
remoteJid: '12345@s.whatsapp.net',
|
remoteJid: '12345@s.whatsapp.net',
|
||||||
@@ -316,4 +327,4 @@ describe('Event Buffer Tests', () => {
|
|||||||
|
|
||||||
expect(chats[0].unreadCount).toBeUndefined()
|
expect(chats[0].unreadCount).toBeUndefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,55 +5,44 @@ import { makeMockSignalKeyStore } from './utils'
|
|||||||
logger.level = 'trace'
|
logger.level = 'trace'
|
||||||
|
|
||||||
describe('Key Store w Transaction Tests', () => {
|
describe('Key Store w Transaction Tests', () => {
|
||||||
|
|
||||||
const rawStore = makeMockSignalKeyStore()
|
const rawStore = makeMockSignalKeyStore()
|
||||||
const store = addTransactionCapability(
|
const store = addTransactionCapability(rawStore, logger, {
|
||||||
rawStore,
|
maxCommitRetries: 1,
|
||||||
logger,
|
delayBetweenTriesMs: 10
|
||||||
{
|
})
|
||||||
maxCommitRetries: 1,
|
|
||||||
delayBetweenTriesMs: 10
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
it('should use transaction cache when mutated', async() => {
|
it('should use transaction cache when mutated', async () => {
|
||||||
const key = '123'
|
const key = '123'
|
||||||
const value = new Uint8Array(1)
|
const value = new Uint8Array(1)
|
||||||
const ogGet = rawStore.get
|
const ogGet = rawStore.get
|
||||||
await store.transaction(
|
await store.transaction(async () => {
|
||||||
async() => {
|
await store.set({ session: { [key]: value } })
|
||||||
await store.set({ 'session': { [key]: value } })
|
|
||||||
|
|
||||||
rawStore.get = () => {
|
rawStore.get = () => {
|
||||||
throw new Error('should not have been called')
|
throw new Error('should not have been called')
|
||||||
}
|
|
||||||
|
|
||||||
const { [key]: stored } = await store.get('session', [key])
|
|
||||||
expect(stored).toEqual(new Uint8Array(1))
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
const { [key]: stored } = await store.get('session', [key])
|
||||||
|
expect(stored).toEqual(new Uint8Array(1))
|
||||||
|
})
|
||||||
|
|
||||||
rawStore.get = ogGet
|
rawStore.get = ogGet
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not commit a failed transaction', async() => {
|
it('should not commit a failed transaction', async () => {
|
||||||
const key = 'abcd'
|
const key = 'abcd'
|
||||||
await expect(
|
await expect(
|
||||||
store.transaction(
|
store.transaction(async () => {
|
||||||
async() => {
|
await store.set({ session: { [key]: new Uint8Array(1) } })
|
||||||
await store.set({ 'session': { [key]: new Uint8Array(1) } })
|
throw new Error('fail')
|
||||||
throw new Error('fail')
|
})
|
||||||
}
|
).rejects.toThrowError('fail')
|
||||||
)
|
|
||||||
).rejects.toThrowError(
|
|
||||||
'fail'
|
|
||||||
)
|
|
||||||
|
|
||||||
const { [key]: stored } = await store.get('session', [key])
|
const { [key]: stored } = await store.get('session', [key])
|
||||||
expect(stored).toBeUndefined()
|
expect(stored).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle overlapping transactions', async() => {
|
it('should handle overlapping transactions', async () => {
|
||||||
// promise to let transaction 2
|
// promise to let transaction 2
|
||||||
// know that transaction 1 has started
|
// know that transaction 1 has started
|
||||||
let promiseResolve: () => void
|
let promiseResolve: () => void
|
||||||
@@ -61,32 +50,28 @@ describe('Key Store w Transaction Tests', () => {
|
|||||||
promiseResolve = resolve
|
promiseResolve = resolve
|
||||||
})
|
})
|
||||||
|
|
||||||
store.transaction(
|
store.transaction(async () => {
|
||||||
async() => {
|
await store.set({
|
||||||
await store.set({
|
session: {
|
||||||
'session': {
|
'1': new Uint8Array(1)
|
||||||
'1': new Uint8Array(1)
|
}
|
||||||
}
|
})
|
||||||
})
|
// wait for the other transaction to start
|
||||||
// wait for the other transaction to start
|
await delay(5)
|
||||||
await delay(5)
|
// reolve the promise to let the other transaction continue
|
||||||
// reolve the promise to let the other transaction continue
|
promiseResolve()
|
||||||
promiseResolve()
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
await store.transaction(
|
await store.transaction(async () => {
|
||||||
async() => {
|
await promise
|
||||||
await promise
|
await delay(5)
|
||||||
await delay(5)
|
|
||||||
|
|
||||||
expect(store.isInTransaction()).toBe(true)
|
expect(store.isInTransaction()).toBe(true)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
expect(store.isInTransaction()).toBe(false)
|
expect(store.isInTransaction()).toBe(false)
|
||||||
// ensure that the transaction were committed
|
// ensure that the transaction were committed
|
||||||
const { ['1']: stored } = await store.get('session', ['1'])
|
const { ['1']: stored } = await store.get('session', ['1'])
|
||||||
expect(stored).toEqual(new Uint8Array(1))
|
expect(stored).toEqual(new Uint8Array(1))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { SignalAuthState, SignalDataTypeMap } from '../Types'
|
|||||||
import { Curve, generateRegistrationId, generateSignalPubKey, signedKeyPair } from '../Utils'
|
import { Curve, generateRegistrationId, generateSignalPubKey, signedKeyPair } from '../Utils'
|
||||||
|
|
||||||
describe('Signal Tests', () => {
|
describe('Signal Tests', () => {
|
||||||
|
it('should correctly encrypt/decrypt 1 message', async () => {
|
||||||
it('should correctly encrypt/decrypt 1 message', async() => {
|
|
||||||
const user1 = makeUser()
|
const user1 = makeUser()
|
||||||
const user2 = makeUser()
|
const user2 = makeUser()
|
||||||
|
|
||||||
@@ -12,39 +11,31 @@ describe('Signal Tests', () => {
|
|||||||
|
|
||||||
await prepareForSendingMessage(user1, user2)
|
await prepareForSendingMessage(user1, user2)
|
||||||
|
|
||||||
const result = await user1.repository.encryptMessage(
|
const result = await user1.repository.encryptMessage({ jid: user2.jid, data: msg })
|
||||||
{ jid: user2.jid, data: msg }
|
|
||||||
)
|
|
||||||
|
|
||||||
const dec = await user2.repository.decryptMessage(
|
const dec = await user2.repository.decryptMessage({ jid: user1.jid, ...result })
|
||||||
{ jid: user1.jid, ...result }
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(dec).toEqual(msg)
|
expect(dec).toEqual(msg)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should correctly override a session', async() => {
|
it('should correctly override a session', async () => {
|
||||||
const user1 = makeUser()
|
const user1 = makeUser()
|
||||||
const user2 = makeUser()
|
const user2 = makeUser()
|
||||||
|
|
||||||
const msg = Buffer.from('hello there!')
|
const msg = Buffer.from('hello there!')
|
||||||
|
|
||||||
for(let preKeyId = 2; preKeyId <= 3;preKeyId++) {
|
for (let preKeyId = 2; preKeyId <= 3; preKeyId++) {
|
||||||
await prepareForSendingMessage(user1, user2, preKeyId)
|
await prepareForSendingMessage(user1, user2, preKeyId)
|
||||||
|
|
||||||
const result = await user1.repository.encryptMessage(
|
const result = await user1.repository.encryptMessage({ jid: user2.jid, data: msg })
|
||||||
{ jid: user2.jid, data: msg }
|
|
||||||
)
|
|
||||||
|
|
||||||
const dec = await user2.repository.decryptMessage(
|
const dec = await user2.repository.decryptMessage({ jid: user1.jid, ...result })
|
||||||
{ jid: user1.jid, ...result }
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(dec).toEqual(msg)
|
expect(dec).toEqual(msg)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should correctly encrypt/decrypt multiple messages', async() => {
|
it('should correctly encrypt/decrypt multiple messages', async () => {
|
||||||
const user1 = makeUser()
|
const user1 = makeUser()
|
||||||
const user2 = makeUser()
|
const user2 = makeUser()
|
||||||
|
|
||||||
@@ -52,56 +43,46 @@ describe('Signal Tests', () => {
|
|||||||
|
|
||||||
await prepareForSendingMessage(user1, user2)
|
await prepareForSendingMessage(user1, user2)
|
||||||
|
|
||||||
for(let i = 0;i < 10;i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
const result = await user1.repository.encryptMessage(
|
const result = await user1.repository.encryptMessage({ jid: user2.jid, data: msg })
|
||||||
{ jid: user2.jid, data: msg }
|
|
||||||
)
|
|
||||||
|
|
||||||
const dec = await user2.repository.decryptMessage(
|
const dec = await user2.repository.decryptMessage({ jid: user1.jid, ...result })
|
||||||
{ jid: user1.jid, ...result }
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(dec).toEqual(msg)
|
expect(dec).toEqual(msg)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should encrypt/decrypt messages from group', async() => {
|
it('should encrypt/decrypt messages from group', async () => {
|
||||||
const groupId = '123456@g.us'
|
const groupId = '123456@g.us'
|
||||||
const participants = [...Array(5)].map(makeUser)
|
const participants = [...Array(5)].map(makeUser)
|
||||||
|
|
||||||
const msg = Buffer.from('hello there!')
|
const msg = Buffer.from('hello there!')
|
||||||
|
|
||||||
const sender = participants[0]
|
const sender = participants[0]
|
||||||
const enc = await sender.repository.encryptGroupMessage(
|
const enc = await sender.repository.encryptGroupMessage({
|
||||||
{
|
group: groupId,
|
||||||
group: groupId,
|
meId: sender.jid,
|
||||||
meId: sender.jid,
|
data: msg
|
||||||
data: msg
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
for(const participant of participants) {
|
for (const participant of participants) {
|
||||||
if(participant === sender) {
|
if (participant === sender) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
await participant.repository.processSenderKeyDistributionMessage(
|
await participant.repository.processSenderKeyDistributionMessage({
|
||||||
{
|
item: {
|
||||||
item: {
|
groupId,
|
||||||
groupId,
|
axolotlSenderKeyDistributionMessage: enc.senderKeyDistributionMessage
|
||||||
axolotlSenderKeyDistributionMessage: enc.senderKeyDistributionMessage
|
},
|
||||||
},
|
authorJid: sender.jid
|
||||||
authorJid: sender.jid
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const dec = await participant.repository.decryptGroupMessage(
|
const dec = await participant.repository.decryptGroupMessage({
|
||||||
{
|
group: groupId,
|
||||||
group: groupId,
|
authorJid: sender.jid,
|
||||||
authorJid: sender.jid,
|
msg: enc.ciphertext
|
||||||
msg: enc.ciphertext
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
expect(dec).toEqual(msg)
|
expect(dec).toEqual(msg)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -116,30 +97,24 @@ function makeUser() {
|
|||||||
return { store, jid, repository }
|
return { store, jid, repository }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function prepareForSendingMessage(
|
async function prepareForSendingMessage(sender: User, receiver: User, preKeyId = 2) {
|
||||||
sender: User,
|
|
||||||
receiver: User,
|
|
||||||
preKeyId = 2
|
|
||||||
) {
|
|
||||||
const preKey = Curve.generateKeyPair()
|
const preKey = Curve.generateKeyPair()
|
||||||
await sender.repository.injectE2ESession(
|
await sender.repository.injectE2ESession({
|
||||||
{
|
jid: receiver.jid,
|
||||||
jid: receiver.jid,
|
session: {
|
||||||
session: {
|
registrationId: receiver.store.creds.registrationId,
|
||||||
registrationId: receiver.store.creds.registrationId,
|
identityKey: generateSignalPubKey(receiver.store.creds.signedIdentityKey.public),
|
||||||
identityKey: generateSignalPubKey(receiver.store.creds.signedIdentityKey.public),
|
signedPreKey: {
|
||||||
signedPreKey: {
|
keyId: receiver.store.creds.signedPreKey.keyId,
|
||||||
keyId: receiver.store.creds.signedPreKey.keyId,
|
publicKey: generateSignalPubKey(receiver.store.creds.signedPreKey.keyPair.public),
|
||||||
publicKey: generateSignalPubKey(receiver.store.creds.signedPreKey.keyPair.public),
|
signature: receiver.store.creds.signedPreKey.signature
|
||||||
signature: receiver.store.creds.signedPreKey.signature,
|
},
|
||||||
},
|
preKey: {
|
||||||
preKey: {
|
keyId: preKeyId,
|
||||||
keyId: preKeyId,
|
publicKey: generateSignalPubKey(preKey.public)
|
||||||
publicKey: generateSignalPubKey(preKey.public),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
|
|
||||||
await receiver.store.keys.set({
|
await receiver.store.keys.set({
|
||||||
'pre-key': {
|
'pre-key': {
|
||||||
@@ -156,14 +131,14 @@ function makeTestAuthState(): SignalAuthState {
|
|||||||
creds: {
|
creds: {
|
||||||
signedIdentityKey: identityKey,
|
signedIdentityKey: identityKey,
|
||||||
registrationId: generateRegistrationId(),
|
registrationId: generateRegistrationId(),
|
||||||
signedPreKey: signedKeyPair(identityKey, 1),
|
signedPreKey: signedKeyPair(identityKey, 1)
|
||||||
},
|
},
|
||||||
keys: {
|
keys: {
|
||||||
get(type, ids) {
|
get(type, ids) {
|
||||||
const data: { [_: string]: SignalDataTypeMap[typeof type] } = { }
|
const data: { [_: string]: SignalDataTypeMap[typeof type] } = {}
|
||||||
for(const id of ids) {
|
for (const id of ids) {
|
||||||
const item = store[getUniqueId(type, id)]
|
const item = store[getUniqueId(type, id)]
|
||||||
if(typeof item !== 'undefined') {
|
if (typeof item !== 'undefined') {
|
||||||
data[id] = item
|
data[id] = item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,16 +146,16 @@ function makeTestAuthState(): SignalAuthState {
|
|||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
set(data) {
|
set(data) {
|
||||||
for(const type in data) {
|
for (const type in data) {
|
||||||
for(const id in data[type]) {
|
for (const id in data[type]) {
|
||||||
store[getUniqueId(type, id)] = data[type][id]
|
store[getUniqueId(type, id)] = data[type][id]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUniqueId(type: string, id: string) {
|
function getUniqueId(type: string, id: string) {
|
||||||
return `${type}.${id}`
|
return `${type}.${id}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,38 +31,37 @@ const TEST_VECTORS: TestVector[] = [
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
plaintext: readFileSync('./Media/icon.png')
|
plaintext: readFileSync('./Media/icon.png')
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
describe('Media Download Tests', () => {
|
describe('Media Download Tests', () => {
|
||||||
|
it('should download a full encrypted media correctly', async () => {
|
||||||
it('should download a full encrypted media correctly', async() => {
|
for (const { type, message, plaintext } of TEST_VECTORS) {
|
||||||
for(const { type, message, plaintext } of TEST_VECTORS) {
|
|
||||||
const readPipe = await downloadContentFromMessage(message, type)
|
const readPipe = await downloadContentFromMessage(message, type)
|
||||||
|
|
||||||
let buffer = Buffer.alloc(0)
|
let buffer = Buffer.alloc(0)
|
||||||
for await (const read of readPipe) {
|
for await (const read of readPipe) {
|
||||||
buffer = Buffer.concat([ buffer, read ])
|
buffer = Buffer.concat([buffer, read])
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(buffer).toEqual(plaintext)
|
expect(buffer).toEqual(plaintext)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should download an encrypted media correctly piece', async() => {
|
it('should download an encrypted media correctly piece', async () => {
|
||||||
for(const { type, message, plaintext } of TEST_VECTORS) {
|
for (const { type, message, plaintext } of TEST_VECTORS) {
|
||||||
// check all edge cases
|
// check all edge cases
|
||||||
const ranges = [
|
const ranges = [
|
||||||
{ startByte: 51, endByte: plaintext.length - 100 }, // random numbers
|
{ startByte: 51, endByte: plaintext.length - 100 }, // random numbers
|
||||||
{ startByte: 1024, endByte: 2038 }, // larger random multiples of 16
|
{ startByte: 1024, endByte: 2038 }, // larger random multiples of 16
|
||||||
{ startByte: 1, endByte: plaintext.length - 1 } // borders
|
{ startByte: 1, endByte: plaintext.length - 1 } // borders
|
||||||
]
|
]
|
||||||
for(const range of ranges) {
|
for (const range of ranges) {
|
||||||
const readPipe = await downloadContentFromMessage(message, type, range)
|
const readPipe = await downloadContentFromMessage(message, type, range)
|
||||||
|
|
||||||
let buffer = Buffer.alloc(0)
|
let buffer = Buffer.alloc(0)
|
||||||
for await (const read of readPipe) {
|
for await (const read of readPipe) {
|
||||||
buffer = Buffer.concat([ buffer, read ])
|
buffer = Buffer.concat([buffer, read])
|
||||||
}
|
}
|
||||||
|
|
||||||
const hex = buffer.toString('hex')
|
const hex = buffer.toString('hex')
|
||||||
@@ -73,4 +72,4 @@ describe('Media Download Tests', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import { WAMessageContent } from '../Types'
|
|||||||
import { normalizeMessageContent } from '../Utils'
|
import { normalizeMessageContent } from '../Utils'
|
||||||
|
|
||||||
describe('Messages Tests', () => {
|
describe('Messages Tests', () => {
|
||||||
|
|
||||||
it('should correctly unwrap messages', () => {
|
it('should correctly unwrap messages', () => {
|
||||||
const CONTENT = { imageMessage: { } }
|
const CONTENT = { imageMessage: {} }
|
||||||
expectRightContent(CONTENT)
|
expectRightContent(CONTENT)
|
||||||
expectRightContent({
|
expectRightContent({
|
||||||
ephemeralMessage: { message: CONTENT }
|
ephemeralMessage: { message: CONTENT }
|
||||||
@@ -29,9 +28,7 @@ describe('Messages Tests', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function expectRightContent(content: WAMessageContent) {
|
function expectRightContent(content: WAMessageContent) {
|
||||||
expect(
|
expect(normalizeMessageContent(content)).toHaveProperty('imageMessage')
|
||||||
normalizeMessageContent(content)
|
|
||||||
).toHaveProperty('imageMessage')
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ export function makeMockSignalKeyStore(): SignalKeyStore {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
get(type, ids) {
|
get(type, ids) {
|
||||||
const data: { [_: string]: SignalDataTypeMap[typeof type] } = { }
|
const data: { [_: string]: SignalDataTypeMap[typeof type] } = {}
|
||||||
for(const id of ids) {
|
for (const id of ids) {
|
||||||
const item = store[getUniqueId(type, id)]
|
const item = store[getUniqueId(type, id)]
|
||||||
if(typeof item !== 'undefined') {
|
if (typeof item !== 'undefined') {
|
||||||
data[id] = item
|
data[id] = item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,15 +22,15 @@ export function makeMockSignalKeyStore(): SignalKeyStore {
|
|||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
set(data) {
|
set(data) {
|
||||||
for(const type in data) {
|
for (const type in data) {
|
||||||
for(const id in data[type]) {
|
for (const id in data[type]) {
|
||||||
store[getUniqueId(type, id)] = data[type][id]
|
store[getUniqueId(type, id)] = data[type][id]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUniqueId(type: string, id: string) {
|
function getUniqueId(type: string, id: string) {
|
||||||
return `${type}.${id}`
|
return `${type}.${id}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import type { proto } from '../../WAProto'
|
|||||||
import type { Contact } from './Contact'
|
import type { Contact } from './Contact'
|
||||||
import type { MinimalMessage } from './Message'
|
import type { MinimalMessage } from './Message'
|
||||||
|
|
||||||
export type KeyPair = { public: Uint8Array, private: Uint8Array }
|
export type KeyPair = { public: Uint8Array; private: Uint8Array }
|
||||||
export type SignedKeyPair = {
|
export type SignedKeyPair = {
|
||||||
keyPair: KeyPair
|
keyPair: KeyPair
|
||||||
signature: Uint8Array
|
signature: Uint8Array
|
||||||
keyId: number
|
keyId: number
|
||||||
timestampS?: number
|
timestampS?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProtocolAddress = {
|
export type ProtocolAddress = {
|
||||||
@@ -20,58 +20,58 @@ export type SignalIdentity = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type LTHashState = {
|
export type LTHashState = {
|
||||||
version: number
|
version: number
|
||||||
hash: Buffer
|
hash: Buffer
|
||||||
indexValueMap: {
|
indexValueMap: {
|
||||||
[indexMacBase64: string]: { valueMac: Uint8Array | Buffer }
|
[indexMacBase64: string]: { valueMac: Uint8Array | Buffer }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SignalCreds = {
|
export type SignalCreds = {
|
||||||
readonly signedIdentityKey: KeyPair
|
readonly signedIdentityKey: KeyPair
|
||||||
readonly signedPreKey: SignedKeyPair
|
readonly signedPreKey: SignedKeyPair
|
||||||
readonly registrationId: number
|
readonly registrationId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AccountSettings = {
|
export type AccountSettings = {
|
||||||
/** unarchive chats when a new message is received */
|
/** unarchive chats when a new message is received */
|
||||||
unarchiveChats: boolean
|
unarchiveChats: boolean
|
||||||
/** the default mode to start new conversations with */
|
/** the default mode to start new conversations with */
|
||||||
defaultDisappearingMode?: Pick<proto.IConversation, 'ephemeralExpiration' | 'ephemeralSettingTimestamp'>
|
defaultDisappearingMode?: Pick<proto.IConversation, 'ephemeralExpiration' | 'ephemeralSettingTimestamp'>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuthenticationCreds = SignalCreds & {
|
export type AuthenticationCreds = SignalCreds & {
|
||||||
readonly noiseKey: KeyPair
|
readonly noiseKey: KeyPair
|
||||||
readonly pairingEphemeralKeyPair: KeyPair
|
readonly pairingEphemeralKeyPair: KeyPair
|
||||||
advSecretKey: string
|
advSecretKey: string
|
||||||
|
|
||||||
me?: Contact
|
me?: Contact
|
||||||
account?: proto.IADVSignedDeviceIdentity
|
account?: proto.IADVSignedDeviceIdentity
|
||||||
signalIdentities?: SignalIdentity[]
|
signalIdentities?: SignalIdentity[]
|
||||||
myAppStateKeyId?: string
|
myAppStateKeyId?: string
|
||||||
firstUnuploadedPreKeyId: number
|
firstUnuploadedPreKeyId: number
|
||||||
nextPreKeyId: number
|
nextPreKeyId: number
|
||||||
|
|
||||||
lastAccountSyncTimestamp?: number
|
lastAccountSyncTimestamp?: number
|
||||||
platform?: string
|
platform?: string
|
||||||
|
|
||||||
processedHistoryMessages: MinimalMessage[]
|
processedHistoryMessages: MinimalMessage[]
|
||||||
/** number of times history & app state has been synced */
|
/** number of times history & app state has been synced */
|
||||||
accountSyncCounter: number
|
accountSyncCounter: number
|
||||||
accountSettings: AccountSettings
|
accountSettings: AccountSettings
|
||||||
registered: boolean
|
registered: boolean
|
||||||
pairingCode: string | undefined
|
pairingCode: string | undefined
|
||||||
lastPropHash: string | undefined
|
lastPropHash: string | undefined
|
||||||
routingInfo: Buffer | undefined
|
routingInfo: Buffer | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SignalDataTypeMap = {
|
export type SignalDataTypeMap = {
|
||||||
'pre-key': KeyPair
|
'pre-key': KeyPair
|
||||||
'session': Uint8Array
|
session: Uint8Array
|
||||||
'sender-key': Uint8Array
|
'sender-key': Uint8Array
|
||||||
'sender-key-memory': { [jid: string]: boolean }
|
'sender-key-memory': { [jid: string]: boolean }
|
||||||
'app-state-sync-key': proto.Message.IAppStateSyncKeyData
|
'app-state-sync-key': proto.Message.IAppStateSyncKeyData
|
||||||
'app-state-sync-version': LTHashState
|
'app-state-sync-version': LTHashState
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SignalDataSet = { [T in keyof SignalDataTypeMap]?: { [id: string]: SignalDataTypeMap[T] | null } }
|
export type SignalDataSet = { [T in keyof SignalDataTypeMap]?: { [id: string]: SignalDataTypeMap[T] | null } }
|
||||||
@@ -79,15 +79,15 @@ export type SignalDataSet = { [T in keyof SignalDataTypeMap]?: { [id: string]: S
|
|||||||
type Awaitable<T> = T | Promise<T>
|
type Awaitable<T> = T | Promise<T>
|
||||||
|
|
||||||
export type SignalKeyStore = {
|
export type SignalKeyStore = {
|
||||||
get<T extends keyof SignalDataTypeMap>(type: T, ids: string[]): Awaitable<{ [id: string]: SignalDataTypeMap[T] }>
|
get<T extends keyof SignalDataTypeMap>(type: T, ids: string[]): Awaitable<{ [id: string]: SignalDataTypeMap[T] }>
|
||||||
set(data: SignalDataSet): Awaitable<void>
|
set(data: SignalDataSet): Awaitable<void>
|
||||||
/** clear all the data in the store */
|
/** clear all the data in the store */
|
||||||
clear?(): Awaitable<void>
|
clear?(): Awaitable<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SignalKeyStoreWithTransaction = SignalKeyStore & {
|
export type SignalKeyStoreWithTransaction = SignalKeyStore & {
|
||||||
isInTransaction: () => boolean
|
isInTransaction: () => boolean
|
||||||
transaction<T>(exec: () => Promise<T>): Promise<T>
|
transaction<T>(exec: () => Promise<T>): Promise<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TransactionCapabilityOptions = {
|
export type TransactionCapabilityOptions = {
|
||||||
@@ -96,11 +96,11 @@ export type TransactionCapabilityOptions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type SignalAuthState = {
|
export type SignalAuthState = {
|
||||||
creds: SignalCreds
|
creds: SignalCreds
|
||||||
keys: SignalKeyStore | SignalKeyStoreWithTransaction
|
keys: SignalKeyStore | SignalKeyStoreWithTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuthenticationState = {
|
export type AuthenticationState = {
|
||||||
creds: AuthenticationCreds
|
creds: AuthenticationCreds
|
||||||
keys: SignalKeyStore
|
keys: SignalKeyStore
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
export type WACallUpdateType = 'offer' | 'ringing' | 'timeout' | 'reject' | 'accept' | 'terminate'
|
export type WACallUpdateType = 'offer' | 'ringing' | 'timeout' | 'reject' | 'accept' | 'terminate'
|
||||||
|
|
||||||
export type WACallEvent = {
|
export type WACallEvent = {
|
||||||
|
|||||||
@@ -22,50 +22,58 @@ export type WAPrivacyMessagesValue = 'all' | 'contacts'
|
|||||||
/** set of statuses visible to other people; see updatePresence() in WhatsAppWeb.Send */
|
/** set of statuses visible to other people; see updatePresence() in WhatsAppWeb.Send */
|
||||||
export type WAPresence = 'unavailable' | 'available' | 'composing' | 'recording' | 'paused'
|
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 {
|
export interface PresenceData {
|
||||||
lastKnownPresence: WAPresence
|
lastKnownPresence: WAPresence
|
||||||
lastSeen?: number
|
lastSeen?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BotListInfo = {
|
export type BotListInfo = {
|
||||||
jid: string
|
jid: string
|
||||||
personaId: string
|
personaId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChatMutation = {
|
export type ChatMutation = {
|
||||||
syncAction: proto.ISyncActionData
|
syncAction: proto.ISyncActionData
|
||||||
index: string[]
|
index: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WAPatchCreate = {
|
export type WAPatchCreate = {
|
||||||
syncAction: proto.ISyncActionValue
|
syncAction: proto.ISyncActionValue
|
||||||
index: string[]
|
index: string[]
|
||||||
type: WAPatchName
|
type: WAPatchName
|
||||||
apiVersion: number
|
apiVersion: number
|
||||||
operation: proto.SyncdMutation.SyncdOperation
|
operation: proto.SyncdMutation.SyncdOperation
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Chat = proto.IConversation & {
|
export type Chat = proto.IConversation & {
|
||||||
/** unix timestamp of when the last message was received in the chat */
|
/** unix timestamp of when the last message was received in the chat */
|
||||||
lastMessageRecvTimestamp?: number
|
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
|
* if specified in the update,
|
||||||
* Right now, used to determine when to release an app state sync event
|
* the EV buffer will check if the condition gets fulfilled before applying the update
|
||||||
*
|
* Right now, used to determine when to release an app state sync event
|
||||||
* @returns true, if the update should be applied;
|
*
|
||||||
* false if it can be discarded;
|
* @returns true, if the update should be applied;
|
||||||
* undefined if the condition is not yet fulfilled
|
* false if it can be discarded;
|
||||||
* */
|
* undefined if the condition is not yet fulfilled
|
||||||
conditional: (bufferedData: BufferedEventData) => boolean | undefined
|
* */
|
||||||
}>
|
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
|
* the last messages in a chat, sorted reverse-chronologically. That is, the latest message should be first in the chat
|
||||||
@@ -74,49 +82,50 @@ export type ChatUpdate = Partial<Chat & {
|
|||||||
export type LastMessageList = MinimalMessage[] | proto.SyncActionValue.ISyncActionMessageRange
|
export type LastMessageList = MinimalMessage[] | proto.SyncActionValue.ISyncActionMessageRange
|
||||||
|
|
||||||
export type ChatModification =
|
export type ChatModification =
|
||||||
{
|
| {
|
||||||
archive: boolean
|
archive: boolean
|
||||||
lastMessages: LastMessageList
|
lastMessages: LastMessageList
|
||||||
}
|
}
|
||||||
| { pushNameSetting: string }
|
| { pushNameSetting: string }
|
||||||
| { pin: boolean }
|
| { pin: boolean }
|
||||||
| {
|
| {
|
||||||
/** mute for duration, or provide timestamp of mute to remove*/
|
/** mute for duration, or provide timestamp of mute to remove*/
|
||||||
mute: number | null
|
mute: number | null
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
clear: boolean
|
clear: boolean
|
||||||
} | {
|
}
|
||||||
deleteForMe: { deleteMedia: boolean, key: WAMessageKey, timestamp: number }
|
| {
|
||||||
}
|
deleteForMe: { deleteMedia: boolean; key: WAMessageKey; timestamp: number }
|
||||||
| {
|
}
|
||||||
star: {
|
| {
|
||||||
messages: { id: string, fromMe?: boolean }[]
|
star: {
|
||||||
star: boolean
|
messages: { id: string; fromMe?: boolean }[]
|
||||||
}
|
star: boolean
|
||||||
}
|
}
|
||||||
| {
|
}
|
||||||
markRead: boolean
|
| {
|
||||||
lastMessages: LastMessageList
|
markRead: boolean
|
||||||
}
|
lastMessages: LastMessageList
|
||||||
| { delete: true, lastMessages: LastMessageList }
|
}
|
||||||
// Label
|
| { delete: true; lastMessages: LastMessageList }
|
||||||
| { addLabel: LabelActionBody }
|
// Label
|
||||||
// Label assosiation
|
| { addLabel: LabelActionBody }
|
||||||
| { addChatLabel: ChatLabelAssociationActionBody }
|
// Label assosiation
|
||||||
| { removeChatLabel: ChatLabelAssociationActionBody }
|
| { addChatLabel: ChatLabelAssociationActionBody }
|
||||||
| { addMessageLabel: MessageLabelAssociationActionBody }
|
| { removeChatLabel: ChatLabelAssociationActionBody }
|
||||||
| { removeMessageLabel: MessageLabelAssociationActionBody }
|
| { addMessageLabel: MessageLabelAssociationActionBody }
|
||||||
|
| { removeMessageLabel: MessageLabelAssociationActionBody }
|
||||||
|
|
||||||
export type InitialReceivedChatsState = {
|
export type InitialReceivedChatsState = {
|
||||||
[jid: string]: {
|
[jid: string]: {
|
||||||
/** the last message received from the other party */
|
/** the last message received from the other party */
|
||||||
lastMsgRecvTimestamp?: number
|
lastMsgRecvTimestamp?: number
|
||||||
/** the absolute last message in the chat */
|
/** the absolute last message in the chat */
|
||||||
lastMsgTimestamp: number
|
lastMsgTimestamp: number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InitialAppStateSyncOptions = {
|
export type InitialAppStateSyncOptions = {
|
||||||
accountSettings: AccountSettings
|
accountSettings: AccountSettings
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
export interface Contact {
|
export interface Contact {
|
||||||
id: string
|
id: string
|
||||||
lid?: string
|
lid?: string
|
||||||
/** name of the contact, you have saved on your WA */
|
/** name of the contact, you have saved on your WA */
|
||||||
name?: string
|
name?: string
|
||||||
/** name of the contact, the contact has set on their own on WA */
|
/** name of the contact, the contact has set on their own on WA */
|
||||||
notify?: string
|
notify?: string
|
||||||
/** I have no idea */
|
/** I have no idea */
|
||||||
verifiedName?: string
|
verifiedName?: string
|
||||||
// Baileys Added
|
// Baileys Added
|
||||||
/**
|
/**
|
||||||
* Url of the profile picture of the contact
|
* Url of the profile picture of the contact
|
||||||
*
|
*
|
||||||
* 'changed' => if the profile picture has changed
|
* 'changed' => if the profile picture has changed
|
||||||
* null => if the profile picture has not been set (default profile picture)
|
* null => if the profile picture has not been set (default profile picture)
|
||||||
* any other string => url of the profile picture
|
* any other string => url of the profile picture
|
||||||
*/
|
*/
|
||||||
imgUrl?: string | null
|
imgUrl?: string | null
|
||||||
status?: string
|
status?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,91 +11,97 @@ import { MessageUpsertType, MessageUserReceiptUpdate, WAMessage, WAMessageKey, W
|
|||||||
import { ConnectionState } from './State'
|
import { ConnectionState } from './State'
|
||||||
|
|
||||||
export type BaileysEventMap = {
|
export type BaileysEventMap = {
|
||||||
/** connection state has been updated -- WS closed, opened, connecting etc. */
|
/** connection state has been updated -- WS closed, opened, connecting etc. */
|
||||||
'connection.update': Partial<ConnectionState>
|
'connection.update': Partial<ConnectionState>
|
||||||
/** credentials updated -- some metadata, keys or something */
|
/** credentials updated -- some metadata, keys or something */
|
||||||
'creds.update': Partial<AuthenticationCreds>
|
'creds.update': Partial<AuthenticationCreds>
|
||||||
/** set chats (history sync), everything is reverse chronologically sorted */
|
/** set chats (history sync), everything is reverse chronologically sorted */
|
||||||
'messaging-history.set': {
|
'messaging-history.set': {
|
||||||
chats: Chat[]
|
chats: Chat[]
|
||||||
contacts: Contact[]
|
contacts: Contact[]
|
||||||
messages: WAMessage[]
|
messages: WAMessage[]
|
||||||
isLatest?: boolean
|
isLatest?: boolean
|
||||||
progress?: number | null
|
progress?: number | null
|
||||||
syncType?: proto.HistorySync.HistorySyncType
|
syncType?: proto.HistorySync.HistorySyncType
|
||||||
peerDataRequestSessionId?: string | null
|
peerDataRequestSessionId?: string | null
|
||||||
}
|
}
|
||||||
/** upsert chats */
|
/** upsert chats */
|
||||||
'chats.upsert': Chat[]
|
'chats.upsert': Chat[]
|
||||||
/** update the given chats */
|
/** update the given chats */
|
||||||
'chats.update': ChatUpdate[]
|
'chats.update': ChatUpdate[]
|
||||||
'chats.phoneNumberShare': {lid: string, jid: string}
|
'chats.phoneNumberShare': { lid: string; jid: string }
|
||||||
/** delete chats with given ID */
|
/** delete chats with given ID */
|
||||||
'chats.delete': string[]
|
'chats.delete': string[]
|
||||||
/** presence of contact in a chat updated */
|
/** 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.upsert': Contact[]
|
||||||
'contacts.update': Partial<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.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,
|
* add/update the given messages. If they were received while the connection was online,
|
||||||
* the update will have type: "notify"
|
* the update will have type: "notify"
|
||||||
* if requestId is provided, then the messages was received from the phone due to it being unavailable
|
* 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 */
|
/** 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[]
|
'message-receipt.update': MessageUserReceiptUpdate[]
|
||||||
|
|
||||||
'groups.upsert': GroupMetadata[]
|
'groups.upsert': GroupMetadata[]
|
||||||
'groups.update': Partial<GroupMetadata>[]
|
'groups.update': Partial<GroupMetadata>[]
|
||||||
/** apply an action to participants in a group */
|
/** apply an action to participants in a group */
|
||||||
'group-participants.update': { id: string, author: string, participants: string[], action: ParticipantAction }
|
'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.join-request': {
|
||||||
|
id: string
|
||||||
|
author: string
|
||||||
|
participant: string
|
||||||
|
action: RequestJoinAction
|
||||||
|
method: RequestJoinMethod
|
||||||
|
}
|
||||||
|
|
||||||
'blocklist.set': { blocklist: string[] }
|
'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 */
|
/** Receive an update on a call, including when the call was received, rejected, accepted */
|
||||||
'call': WACallEvent[]
|
call: WACallEvent[]
|
||||||
'labels.edit': Label
|
'labels.edit': Label
|
||||||
'labels.association': { association: LabelAssociation, type: 'add' | 'remove' }
|
'labels.association': { association: LabelAssociation; type: 'add' | 'remove' }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BufferedEventData = {
|
export type BufferedEventData = {
|
||||||
historySets: {
|
historySets: {
|
||||||
chats: { [jid: string]: Chat }
|
chats: { [jid: string]: Chat }
|
||||||
contacts: { [jid: string]: Contact }
|
contacts: { [jid: string]: Contact }
|
||||||
messages: { [uqId: string]: WAMessage }
|
messages: { [uqId: string]: WAMessage }
|
||||||
empty: boolean
|
empty: boolean
|
||||||
isLatest: boolean
|
isLatest: boolean
|
||||||
progress?: number | null
|
progress?: number | null
|
||||||
syncType?: proto.HistorySync.HistorySyncType
|
syncType?: proto.HistorySync.HistorySyncType
|
||||||
peerDataRequestSessionId?: string
|
peerDataRequestSessionId?: string
|
||||||
}
|
}
|
||||||
chatUpserts: { [jid: string]: Chat }
|
chatUpserts: { [jid: string]: Chat }
|
||||||
chatUpdates: { [jid: string]: ChatUpdate }
|
chatUpdates: { [jid: string]: ChatUpdate }
|
||||||
chatDeletes: Set<string>
|
chatDeletes: Set<string>
|
||||||
contactUpserts: { [jid: string]: Contact }
|
contactUpserts: { [jid: string]: Contact }
|
||||||
contactUpdates: { [jid: string]: Partial<Contact> }
|
contactUpdates: { [jid: string]: Partial<Contact> }
|
||||||
messageUpserts: { [key: string]: { type: MessageUpsertType, message: WAMessage } }
|
messageUpserts: { [key: string]: { type: MessageUpsertType; message: WAMessage } }
|
||||||
messageUpdates: { [key: string]: WAMessageUpdate }
|
messageUpdates: { [key: string]: WAMessageUpdate }
|
||||||
messageDeletes: { [key: string]: WAMessageKey }
|
messageDeletes: { [key: string]: WAMessageKey }
|
||||||
messageReactions: { [key: string]: { key: WAMessageKey, reactions: proto.IReaction[] } }
|
messageReactions: { [key: string]: { key: WAMessageKey; reactions: proto.IReaction[] } }
|
||||||
messageReceipts: { [key: string]: { key: WAMessageKey, userReceipt: proto.IUserReceipt[] } }
|
messageReceipts: { [key: string]: { key: WAMessageKey; userReceipt: proto.IUserReceipt[] } }
|
||||||
groupUpdates: { [jid: string]: Partial<GroupMetadata> }
|
groupUpdates: { [jid: string]: Partial<GroupMetadata> }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BaileysEvent = keyof BaileysEventMap
|
export type BaileysEvent = keyof BaileysEventMap
|
||||||
|
|
||||||
export interface BaileysEventEmitter {
|
export interface BaileysEventEmitter {
|
||||||
on<T extends keyof BaileysEventMap>(event: T, listener: (arg: BaileysEventMap[T]) => void): void
|
on<T extends keyof BaileysEventMap>(event: T, listener: (arg: BaileysEventMap[T]) => void): void
|
||||||
off<T extends keyof BaileysEventMap>(event: T, listener: (arg: BaileysEventMap[T]) => void): void
|
off<T extends keyof BaileysEventMap>(event: T, listener: (arg: BaileysEventMap[T]) => void): void
|
||||||
removeAllListeners<T extends keyof BaileysEventMap>(event: T): void
|
removeAllListeners<T extends keyof BaileysEventMap>(event: T): void
|
||||||
emit<T extends keyof BaileysEventMap>(event: T, arg: BaileysEventMap[T]): boolean
|
emit<T extends keyof BaileysEventMap>(event: T, arg: BaileysEventMap[T]): boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { Contact } from './Contact'
|
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'
|
export type ParticipantAction = 'add' | 'remove' | 'promote' | 'demote' | 'modify'
|
||||||
|
|
||||||
@@ -9,51 +13,50 @@ export type RequestJoinAction = 'created' | 'revoked' | 'rejected'
|
|||||||
export type RequestJoinMethod = 'invite_link' | 'linked_group_join' | 'non_admin_add' | undefined
|
export type RequestJoinMethod = 'invite_link' | 'linked_group_join' | 'non_admin_add' | undefined
|
||||||
|
|
||||||
export interface GroupMetadata {
|
export interface GroupMetadata {
|
||||||
id: string
|
id: string
|
||||||
/** group uses 'lid' or 'pn' to send messages */
|
/** group uses 'lid' or 'pn' to send messages */
|
||||||
addressingMode: string
|
addressingMode: string
|
||||||
owner: string | undefined
|
owner: string | undefined
|
||||||
subject: string
|
subject: string
|
||||||
/** group subject owner */
|
/** group subject owner */
|
||||||
subjectOwner?: string
|
subjectOwner?: string
|
||||||
/** group subject modification date */
|
/** group subject modification date */
|
||||||
subjectTime?: number
|
subjectTime?: number
|
||||||
creation?: number
|
creation?: number
|
||||||
desc?: string
|
desc?: string
|
||||||
descOwner?: string
|
descOwner?: string
|
||||||
descId?: string
|
descId?: string
|
||||||
/** if this group is part of a community, it returns the jid of the community to which it belongs */
|
/** if this group is part of a community, it returns the jid of the community to which it belongs */
|
||||||
linkedParent?: string
|
linkedParent?: string
|
||||||
/** is set when the group only allows admins to change group settings */
|
/** is set when the group only allows admins to change group settings */
|
||||||
restrict?: boolean
|
restrict?: boolean
|
||||||
/** is set when the group only allows admins to write messages */
|
/** is set when the group only allows admins to write messages */
|
||||||
announce?: boolean
|
announce?: boolean
|
||||||
/** is set when the group also allows members to add participants */
|
/** is set when the group also allows members to add participants */
|
||||||
memberAddMode?: boolean
|
memberAddMode?: boolean
|
||||||
/** Request approval to join the group */
|
/** Request approval to join the group */
|
||||||
joinApprovalMode?: boolean
|
joinApprovalMode?: boolean
|
||||||
/** is this a community */
|
/** is this a community */
|
||||||
isCommunity?: boolean
|
isCommunity?: boolean
|
||||||
/** is this the announce of a community */
|
/** is this the announce of a community */
|
||||||
isCommunityAnnounce?: boolean
|
isCommunityAnnounce?: boolean
|
||||||
/** number of group participants */
|
/** number of group participants */
|
||||||
size?: number
|
size?: number
|
||||||
// Baileys modified array
|
// Baileys modified array
|
||||||
participants: GroupParticipant[]
|
participants: GroupParticipant[]
|
||||||
ephemeralDuration?: number
|
ephemeralDuration?: number
|
||||||
inviteCode?: string
|
inviteCode?: string
|
||||||
/** the person who added you to group or changed some setting in group */
|
/** the person who added you to group or changed some setting in group */
|
||||||
author?: string
|
author?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface WAGroupCreateResponse {
|
export interface WAGroupCreateResponse {
|
||||||
status: number
|
status: number
|
||||||
gid?: string
|
gid?: string
|
||||||
participants?: [{ [key: string]: {} }]
|
participants?: [{ [key: string]: {} }]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroupModificationResponse {
|
export interface GroupModificationResponse {
|
||||||
status: number
|
status: number
|
||||||
participants?: { [key: string]: {} }
|
participants?: { [key: string]: {} }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,48 @@
|
|||||||
export interface Label {
|
export interface Label {
|
||||||
/** Label uniq ID */
|
/** Label uniq ID */
|
||||||
id: string
|
id: string
|
||||||
/** Label name */
|
/** Label name */
|
||||||
name: string
|
name: string
|
||||||
/** Label color ID */
|
/** Label color ID */
|
||||||
color: number
|
color: number
|
||||||
/** Is label has been deleted */
|
/** Is label has been deleted */
|
||||||
deleted: boolean
|
deleted: boolean
|
||||||
/** WhatsApp has 5 predefined labels (New customer, New order & etc) */
|
/** WhatsApp has 5 predefined labels (New customer, New order & etc) */
|
||||||
predefinedId?: string
|
predefinedId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LabelActionBody {
|
export interface LabelActionBody {
|
||||||
id: string
|
id: string
|
||||||
/** Label name */
|
/** Label name */
|
||||||
name?: string
|
name?: string
|
||||||
/** Label color ID */
|
/** Label color ID */
|
||||||
color?: number
|
color?: number
|
||||||
/** Is label has been deleted */
|
/** Is label has been deleted */
|
||||||
deleted?: boolean
|
deleted?: boolean
|
||||||
/** WhatsApp has 5 predefined labels (New customer, New order & etc) */
|
/** WhatsApp has 5 predefined labels (New customer, New order & etc) */
|
||||||
predefinedId?: number
|
predefinedId?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** WhatsApp has 20 predefined colors */
|
/** WhatsApp has 20 predefined colors */
|
||||||
export enum LabelColor {
|
export enum LabelColor {
|
||||||
Color1 = 0,
|
Color1 = 0,
|
||||||
Color2,
|
Color2,
|
||||||
Color3,
|
Color3,
|
||||||
Color4,
|
Color4,
|
||||||
Color5,
|
Color5,
|
||||||
Color6,
|
Color6,
|
||||||
Color7,
|
Color7,
|
||||||
Color8,
|
Color8,
|
||||||
Color9,
|
Color9,
|
||||||
Color10,
|
Color10,
|
||||||
Color11,
|
Color11,
|
||||||
Color12,
|
Color12,
|
||||||
Color13,
|
Color13,
|
||||||
Color14,
|
Color14,
|
||||||
Color15,
|
Color15,
|
||||||
Color16,
|
Color16,
|
||||||
Color17,
|
Color17,
|
||||||
Color18,
|
Color18,
|
||||||
Color19,
|
Color19,
|
||||||
Color20,
|
Color20
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,35 @@
|
|||||||
/** Association type */
|
/** Association type */
|
||||||
export enum LabelAssociationType {
|
export enum LabelAssociationType {
|
||||||
Chat = 'label_jid',
|
Chat = 'label_jid',
|
||||||
Message = 'label_message'
|
Message = 'label_message'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LabelAssociationTypes = `${LabelAssociationType}`
|
export type LabelAssociationTypes = `${LabelAssociationType}`
|
||||||
|
|
||||||
/** Association for chat */
|
/** Association for chat */
|
||||||
export interface ChatLabelAssociation {
|
export interface ChatLabelAssociation {
|
||||||
type: LabelAssociationType.Chat
|
type: LabelAssociationType.Chat
|
||||||
chatId: string
|
chatId: string
|
||||||
labelId: string
|
labelId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Association for message */
|
/** Association for message */
|
||||||
export interface MessageLabelAssociation {
|
export interface MessageLabelAssociation {
|
||||||
type: LabelAssociationType.Message
|
type: LabelAssociationType.Message
|
||||||
chatId: string
|
chatId: string
|
||||||
messageId: string
|
messageId: string
|
||||||
labelId: string
|
labelId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LabelAssociation = ChatLabelAssociation | MessageLabelAssociation
|
export type LabelAssociation = ChatLabelAssociation | MessageLabelAssociation
|
||||||
|
|
||||||
/** Body for add/remove chat label association action */
|
/** Body for add/remove chat label association action */
|
||||||
export interface ChatLabelAssociationActionBody {
|
export interface ChatLabelAssociationActionBody {
|
||||||
labelId: string
|
labelId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** body for add/remove message label association action */
|
/** body for add/remove message label association action */
|
||||||
export interface MessageLabelAssociationActionBody {
|
export interface MessageLabelAssociationActionBody {
|
||||||
labelId: string
|
labelId: string
|
||||||
messageId: string
|
messageId: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ export type WAMessageKey = proto.IMessageKey
|
|||||||
export type WATextMessage = proto.Message.IExtendedTextMessage
|
export type WATextMessage = proto.Message.IExtendedTextMessage
|
||||||
export type WAContextInfo = proto.IContextInfo
|
export type WAContextInfo = proto.IContextInfo
|
||||||
export type WALocationMessage = proto.Message.ILocationMessage
|
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 WAMessageStubType = proto.WebMessageInfo.StubType
|
||||||
export const WAMessageStatus = proto.WebMessageInfo.Status
|
export const WAMessageStatus = proto.WebMessageInfo.Status
|
||||||
import { ILogger } from '../Utils/logger'
|
import { ILogger } from '../Utils/logger'
|
||||||
@@ -27,235 +32,261 @@ export type WAMediaUpload = Buffer | WAMediaPayloadStream | WAMediaPayloadURL
|
|||||||
/** Set of message types that are supported by the library */
|
/** Set of message types that are supported by the library */
|
||||||
export type MessageType = keyof proto.Message
|
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 = {
|
export type MediaConnInfo = {
|
||||||
auth: string
|
auth: string
|
||||||
ttl: number
|
ttl: number
|
||||||
hosts: { hostname: string, maxContentLengthBytes: number }[]
|
hosts: { hostname: string; maxContentLengthBytes: number }[]
|
||||||
fetchDate: Date
|
fetchDate: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WAUrlInfo {
|
export interface WAUrlInfo {
|
||||||
'canonical-url': string
|
'canonical-url': string
|
||||||
'matched-text': string
|
'matched-text': string
|
||||||
title: string
|
title: string
|
||||||
description?: string
|
description?: string
|
||||||
jpegThumbnail?: Buffer
|
jpegThumbnail?: Buffer
|
||||||
highQualityThumbnail?: proto.Message.IImageMessage
|
highQualityThumbnail?: proto.Message.IImageMessage
|
||||||
originalThumbnailUrl?: string
|
originalThumbnailUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// types to generate WA messages
|
// types to generate WA messages
|
||||||
type Mentionable = {
|
type Mentionable = {
|
||||||
/** list of jids that are mentioned in the accompanying text */
|
/** list of jids that are mentioned in the accompanying text */
|
||||||
mentions?: string[]
|
mentions?: string[]
|
||||||
}
|
}
|
||||||
type Contextable = {
|
type Contextable = {
|
||||||
/** add contextInfo to the message */
|
/** add contextInfo to the message */
|
||||||
contextInfo?: proto.IContextInfo
|
contextInfo?: proto.IContextInfo
|
||||||
}
|
}
|
||||||
type ViewOnce = {
|
type ViewOnce = {
|
||||||
viewOnce?: boolean
|
viewOnce?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type Editable = {
|
type Editable = {
|
||||||
edit?: WAMessageKey
|
edit?: WAMessageKey
|
||||||
}
|
}
|
||||||
type WithDimensions = {
|
type WithDimensions = {
|
||||||
width?: number
|
width?: number
|
||||||
height?: number
|
height?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PollMessageOptions = {
|
export type PollMessageOptions = {
|
||||||
name: string
|
name: string
|
||||||
selectableCount?: number
|
selectableCount?: number
|
||||||
values: string[]
|
values: string[]
|
||||||
/** 32 byte message secret to encrypt poll selections */
|
/** 32 byte message secret to encrypt poll selections */
|
||||||
messageSecret?: Uint8Array
|
messageSecret?: Uint8Array
|
||||||
toAnnouncementGroup?: boolean
|
toAnnouncementGroup?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type SharePhoneNumber = {
|
type SharePhoneNumber = {
|
||||||
sharePhoneNumber: boolean
|
sharePhoneNumber: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type RequestPhoneNumber = {
|
type RequestPhoneNumber = {
|
||||||
requestPhoneNumber: boolean
|
requestPhoneNumber: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MediaType = keyof typeof MEDIA_HKDF_KEY_MAPPING
|
export type MediaType = keyof typeof MEDIA_HKDF_KEY_MAPPING
|
||||||
export type AnyMediaMessageContent = (
|
export type AnyMediaMessageContent = (
|
||||||
({
|
| ({
|
||||||
image: WAMediaUpload
|
image: WAMediaUpload
|
||||||
caption?: string
|
caption?: string
|
||||||
jpegThumbnail?: string
|
jpegThumbnail?: string
|
||||||
} & Mentionable & Contextable & WithDimensions)
|
} & Mentionable &
|
||||||
| ({
|
Contextable &
|
||||||
video: WAMediaUpload
|
WithDimensions)
|
||||||
caption?: string
|
| ({
|
||||||
gifPlayback?: boolean
|
video: WAMediaUpload
|
||||||
jpegThumbnail?: string
|
caption?: string
|
||||||
/** if set to true, will send as a `video note` */
|
gifPlayback?: boolean
|
||||||
ptv?: boolean
|
jpegThumbnail?: string
|
||||||
} & Mentionable & Contextable & WithDimensions)
|
/** if set to true, will send as a `video note` */
|
||||||
| {
|
ptv?: boolean
|
||||||
audio: WAMediaUpload
|
} & Mentionable &
|
||||||
/** if set to true, will send as a `voice note` */
|
Contextable &
|
||||||
ptt?: boolean
|
WithDimensions)
|
||||||
/** optionally tell the duration of the audio */
|
| {
|
||||||
seconds?: number
|
audio: WAMediaUpload
|
||||||
}
|
/** if set to true, will send as a `voice note` */
|
||||||
| ({
|
ptt?: boolean
|
||||||
sticker: WAMediaUpload
|
/** optionally tell the duration of the audio */
|
||||||
isAnimated?: boolean
|
seconds?: number
|
||||||
} & WithDimensions) | ({
|
}
|
||||||
document: WAMediaUpload
|
| ({
|
||||||
mimetype: string
|
sticker: WAMediaUpload
|
||||||
fileName?: string
|
isAnimated?: boolean
|
||||||
caption?: string
|
} & WithDimensions)
|
||||||
} & Contextable))
|
| ({
|
||||||
& { mimetype?: string } & Editable
|
document: WAMediaUpload
|
||||||
|
mimetype: string
|
||||||
|
fileName?: string
|
||||||
|
caption?: string
|
||||||
|
} & Contextable)
|
||||||
|
) & { mimetype?: string } & Editable
|
||||||
|
|
||||||
export type ButtonReplyInfo = {
|
export type ButtonReplyInfo = {
|
||||||
displayText: string
|
displayText: string
|
||||||
id: string
|
id: string
|
||||||
index: number
|
index: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GroupInviteInfo = {
|
export type GroupInviteInfo = {
|
||||||
inviteCode: string
|
inviteCode: string
|
||||||
inviteExpiration: number
|
inviteExpiration: number
|
||||||
text: string
|
text: string
|
||||||
jid: string
|
jid: string
|
||||||
subject: string
|
subject: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WASendableProduct = Omit<proto.Message.ProductMessage.IProductSnapshot, 'productImage'> & {
|
export type WASendableProduct = Omit<proto.Message.ProductMessage.IProductSnapshot, 'productImage'> & {
|
||||||
productImage: WAMediaUpload
|
productImage: WAMediaUpload
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AnyRegularMessageContent = (
|
export type AnyRegularMessageContent = (
|
||||||
({
|
| ({
|
||||||
text: string
|
text: string
|
||||||
linkPreview?: WAUrlInfo | null
|
linkPreview?: WAUrlInfo | null
|
||||||
}
|
} & Mentionable &
|
||||||
& Mentionable & Contextable & Editable)
|
Contextable &
|
||||||
| AnyMediaMessageContent
|
Editable)
|
||||||
| ({
|
| AnyMediaMessageContent
|
||||||
poll: PollMessageOptions
|
| ({
|
||||||
} & Mentionable & Contextable & Editable)
|
poll: PollMessageOptions
|
||||||
| {
|
} & Mentionable &
|
||||||
contacts: {
|
Contextable &
|
||||||
displayName?: string
|
Editable)
|
||||||
contacts: proto.Message.IContactMessage[]
|
| {
|
||||||
}
|
contacts: {
|
||||||
}
|
displayName?: string
|
||||||
| {
|
contacts: proto.Message.IContactMessage[]
|
||||||
location: WALocationMessage
|
}
|
||||||
}
|
}
|
||||||
| { react: proto.Message.IReactionMessage }
|
| {
|
||||||
| {
|
location: WALocationMessage
|
||||||
buttonReply: ButtonReplyInfo
|
}
|
||||||
type: 'template' | 'plain'
|
| { react: proto.Message.IReactionMessage }
|
||||||
}
|
| {
|
||||||
| {
|
buttonReply: ButtonReplyInfo
|
||||||
groupInvite: GroupInviteInfo
|
type: 'template' | 'plain'
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
listReply: Omit<proto.Message.IListResponseMessage, 'contextInfo'>
|
groupInvite: GroupInviteInfo
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
pin: WAMessageKey
|
listReply: Omit<proto.Message.IListResponseMessage, 'contextInfo'>
|
||||||
type: proto.PinInChat.Type
|
}
|
||||||
/**
|
| {
|
||||||
* 24 hours, 7 days, 30 days
|
pin: WAMessageKey
|
||||||
*/
|
type: proto.PinInChat.Type
|
||||||
time?: 86400 | 604800 | 2592000
|
/**
|
||||||
}
|
* 24 hours, 7 days, 30 days
|
||||||
| {
|
*/
|
||||||
product: WASendableProduct
|
time?: 86400 | 604800 | 2592000
|
||||||
businessOwnerJid?: string
|
}
|
||||||
body?: string
|
| {
|
||||||
footer?: string
|
product: WASendableProduct
|
||||||
} | SharePhoneNumber | RequestPhoneNumber
|
businessOwnerJid?: string
|
||||||
) & ViewOnce
|
body?: string
|
||||||
|
footer?: string
|
||||||
|
}
|
||||||
|
| SharePhoneNumber
|
||||||
|
| RequestPhoneNumber
|
||||||
|
) &
|
||||||
|
ViewOnce
|
||||||
|
|
||||||
export type AnyMessageContent = AnyRegularMessageContent | {
|
export type AnyMessageContent =
|
||||||
forward: WAMessage
|
| AnyRegularMessageContent
|
||||||
force?: boolean
|
| {
|
||||||
} | {
|
forward: WAMessage
|
||||||
/** Delete your message or anyone's message in a group (admin required) */
|
force?: boolean
|
||||||
delete: WAMessageKey
|
}
|
||||||
} | {
|
| {
|
||||||
disappearingMessagesInChat: boolean | number
|
/** Delete your message or anyone's message in a group (admin required) */
|
||||||
}
|
delete: WAMessageKey
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
disappearingMessagesInChat: boolean | number
|
||||||
|
}
|
||||||
|
|
||||||
export type GroupMetadataParticipants = Pick<GroupMetadata, 'participants'>
|
export type GroupMetadataParticipants = Pick<GroupMetadata, 'participants'>
|
||||||
|
|
||||||
type MinimalRelayOptions = {
|
type MinimalRelayOptions = {
|
||||||
/** override the message ID with a custom provided string */
|
/** override the message ID with a custom provided string */
|
||||||
messageId?: string
|
messageId?: string
|
||||||
/** should we use group metadata cache, or fetch afresh from the server; default assumed to be "true" */
|
/** should we use group metadata cache, or fetch afresh from the server; default assumed to be "true" */
|
||||||
useCachedGroupMetadata?: boolean
|
useCachedGroupMetadata?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MessageRelayOptions = MinimalRelayOptions & {
|
export type MessageRelayOptions = MinimalRelayOptions & {
|
||||||
/** only send to a specific participant; used when a message decryption fails for a single user */
|
/** 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 */
|
/** additional attributes to add to the WA binary node */
|
||||||
additionalAttributes?: { [_: string]: string }
|
additionalAttributes?: { [_: string]: string }
|
||||||
additionalNodes?: BinaryNode[]
|
additionalNodes?: BinaryNode[]
|
||||||
/** should we use the devices cache, or fetch afresh from the server; default assumed to be "true" */
|
/** should we use the devices cache, or fetch afresh from the server; default assumed to be "true" */
|
||||||
useUserDevicesCache?: boolean
|
useUserDevicesCache?: boolean
|
||||||
/** jid list of participants for status@broadcast */
|
/** jid list of participants for status@broadcast */
|
||||||
statusJidList?: string[]
|
statusJidList?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MiscMessageGenerationOptions = MinimalRelayOptions & {
|
export type MiscMessageGenerationOptions = MinimalRelayOptions & {
|
||||||
/** optional, if you want to manually set the timestamp of the message */
|
/** optional, if you want to manually set the timestamp of the message */
|
||||||
timestamp?: Date
|
timestamp?: Date
|
||||||
/** the message you want to quote */
|
/** the message you want to quote */
|
||||||
quoted?: WAMessage
|
quoted?: WAMessage
|
||||||
/** disappearing messages settings */
|
/** disappearing messages settings */
|
||||||
ephemeralExpiration?: number | string
|
ephemeralExpiration?: number | string
|
||||||
/** timeout for media upload to WA server */
|
/** timeout for media upload to WA server */
|
||||||
mediaUploadTimeoutMs?: number
|
mediaUploadTimeoutMs?: number
|
||||||
/** jid list of participants for status@broadcast */
|
/** jid list of participants for status@broadcast */
|
||||||
statusJidList?: string[]
|
statusJidList?: string[]
|
||||||
/** backgroundcolor for status */
|
/** backgroundcolor for status */
|
||||||
backgroundColor?: string
|
backgroundColor?: string
|
||||||
/** font type for status */
|
/** font type for status */
|
||||||
font?: number
|
font?: number
|
||||||
/** if it is broadcast */
|
/** if it is broadcast */
|
||||||
broadcast?: boolean
|
broadcast?: boolean
|
||||||
}
|
}
|
||||||
export type MessageGenerationOptionsFromContent = MiscMessageGenerationOptions & {
|
export type MessageGenerationOptionsFromContent = MiscMessageGenerationOptions & {
|
||||||
userJid: string
|
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 = {
|
export type MediaGenerationOptions = {
|
||||||
logger?: ILogger
|
logger?: ILogger
|
||||||
mediaTypeOverride?: MediaType
|
mediaTypeOverride?: MediaType
|
||||||
upload: WAMediaUploadFunction
|
upload: WAMediaUploadFunction
|
||||||
/** cache media so it does not have to be uploaded again */
|
/** cache media so it does not have to be uploaded again */
|
||||||
mediaCache?: CacheStore
|
mediaCache?: CacheStore
|
||||||
|
|
||||||
mediaUploadTimeoutMs?: number
|
mediaUploadTimeoutMs?: number
|
||||||
|
|
||||||
options?: AxiosRequestConfig
|
options?: AxiosRequestConfig
|
||||||
|
|
||||||
backgroundColor?: string
|
backgroundColor?: string
|
||||||
|
|
||||||
font?: number
|
font?: number
|
||||||
}
|
}
|
||||||
export type MessageContentGenerationOptions = MediaGenerationOptions & {
|
export type MessageContentGenerationOptions = MediaGenerationOptions & {
|
||||||
getUrlInfo?: (text: string) => Promise<WAUrlInfo | undefined>
|
getUrlInfo?: (text: string) => Promise<WAUrlInfo | undefined>
|
||||||
getProfilePicUrl?: (jid: string, type: 'image' | 'preview') => Promise<string | undefined>
|
getProfilePicUrl?: (jid: string, type: 'image' | 'preview') => Promise<string | undefined>
|
||||||
}
|
}
|
||||||
export type MessageGenerationOptions = MessageContentGenerationOptions & MessageGenerationOptionsFromContent
|
export type MessageGenerationOptions = MessageContentGenerationOptions & MessageGenerationOptionsFromContent
|
||||||
|
|
||||||
@@ -268,16 +299,16 @@ export type MessageUpsertType = 'append' | 'notify'
|
|||||||
|
|
||||||
export type MessageUserReceipt = proto.IUserReceipt
|
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 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 = {
|
export type MediaDecryptionKeyInfo = {
|
||||||
iv: Buffer
|
iv: Buffer
|
||||||
cipherKey: Buffer
|
cipherKey: Buffer
|
||||||
macKey?: Buffer
|
macKey?: Buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MinimalMessage = Pick<proto.IWebMessageInfo, 'key' | 'messageTimestamp'>
|
export type MinimalMessage = Pick<proto.IWebMessageInfo, 'key' | 'messageTimestamp'>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { WAMediaUpload } from './Message'
|
|||||||
|
|
||||||
export type CatalogResult = {
|
export type CatalogResult = {
|
||||||
data: {
|
data: {
|
||||||
paging: { cursors: { before: string, after: string } }
|
paging: { cursors: { before: string; after: string } }
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
data: any[]
|
data: any[]
|
||||||
}
|
}
|
||||||
@@ -82,4 +82,4 @@ export type GetCatalogOptions = {
|
|||||||
limit?: number
|
limit?: number
|
||||||
|
|
||||||
jid?: string
|
jid?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,9 +51,7 @@ type E2ESessionOpts = {
|
|||||||
|
|
||||||
export type SignalRepository = {
|
export type SignalRepository = {
|
||||||
decryptGroupMessage(opts: DecryptGroupSignalOpts): Promise<Uint8Array>
|
decryptGroupMessage(opts: DecryptGroupSignalOpts): Promise<Uint8Array>
|
||||||
processSenderKeyDistributionMessage(
|
processSenderKeyDistributionMessage(opts: ProcessSenderKeyDistributionMessageOpts): Promise<void>
|
||||||
opts: ProcessSenderKeyDistributionMessageOpts
|
|
||||||
): Promise<void>
|
|
||||||
decryptMessage(opts: DecryptSignalProtoOpts): Promise<Uint8Array>
|
decryptMessage(opts: DecryptSignalProtoOpts): Promise<Uint8Array>
|
||||||
encryptMessage(opts: EncryptMessageOpts): Promise<{
|
encryptMessage(opts: EncryptMessageOpts): Promise<{
|
||||||
type: 'pkmsg' | 'msg'
|
type: 'pkmsg' | 'msg'
|
||||||
@@ -65,4 +63,4 @@ export type SignalRepository = {
|
|||||||
}>
|
}>
|
||||||
injectE2ESession(opts: E2ESessionOpts): Promise<void>
|
injectE2ESession(opts: E2ESessionOpts): Promise<void>
|
||||||
jidToSignalProtocolAddress(jid: string): string
|
jidToSignalProtocolAddress(jid: string): string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { AxiosRequestConfig } from 'axios'
|
import { AxiosRequestConfig } from 'axios'
|
||||||
import type { Agent } from 'https'
|
import type { Agent } from 'https'
|
||||||
import type { URL } from 'url'
|
import type { URL } from 'url'
|
||||||
@@ -13,121 +12,124 @@ export type WAVersion = [number, number, number]
|
|||||||
export type WABrowserDescription = [string, string, string]
|
export type WABrowserDescription = [string, string, string]
|
||||||
|
|
||||||
export type CacheStore = {
|
export type CacheStore = {
|
||||||
/** get a cached key and change the stats */
|
/** get a cached key and change the stats */
|
||||||
get<T>(key: string): T | undefined
|
get<T>(key: string): T | undefined
|
||||||
/** set a key in the cache */
|
/** set a key in the cache */
|
||||||
set<T>(key: string, value: T): void
|
set<T>(key: string, value: T): void
|
||||||
/** delete a key from the cache */
|
/** delete a key from the cache */
|
||||||
del(key: string): void
|
del(key: string): void
|
||||||
/** flush all data */
|
/** flush all data */
|
||||||
flushAll(): void
|
flushAll(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PatchedMessageWithRecipientJID = proto.IMessage & {recipientJid?: string}
|
export type PatchedMessageWithRecipientJID = proto.IMessage & { recipientJid?: string }
|
||||||
|
|
||||||
export type SocketConfig = {
|
export type SocketConfig = {
|
||||||
/** the WS url to connect to WA */
|
/** the WS url to connect to WA */
|
||||||
waWebSocketUrl: string | URL
|
waWebSocketUrl: string | URL
|
||||||
/** Fails the connection if the socket times out in this interval */
|
/** Fails the connection if the socket times out in this interval */
|
||||||
connectTimeoutMs: number
|
connectTimeoutMs: number
|
||||||
/** Default timeout for queries, undefined for no timeout */
|
/** Default timeout for queries, undefined for no timeout */
|
||||||
defaultQueryTimeoutMs: number | undefined
|
defaultQueryTimeoutMs: number | undefined
|
||||||
/** ping-pong interval for WS connection */
|
/** ping-pong interval for WS connection */
|
||||||
keepAliveIntervalMs: number
|
keepAliveIntervalMs: number
|
||||||
/** should baileys use the mobile api instead of the multi device api
|
/** should baileys use the mobile api instead of the multi device api
|
||||||
* @deprecated This feature has been removed
|
* @deprecated This feature has been removed
|
||||||
*/
|
*/
|
||||||
mobile?: boolean
|
mobile?: boolean
|
||||||
/** proxy agent */
|
/** proxy agent */
|
||||||
agent?: Agent
|
agent?: Agent
|
||||||
/** logger */
|
/** logger */
|
||||||
logger: ILogger
|
logger: ILogger
|
||||||
/** version to connect with */
|
/** version to connect with */
|
||||||
version: WAVersion
|
version: WAVersion
|
||||||
/** override browser config */
|
/** override browser config */
|
||||||
browser: WABrowserDescription
|
browser: WABrowserDescription
|
||||||
/** agent used for fetch requests -- uploading/downloading media */
|
/** agent used for fetch requests -- uploading/downloading media */
|
||||||
fetchAgent?: Agent
|
fetchAgent?: Agent
|
||||||
/** should the QR be printed in the terminal
|
/** should the QR be printed in the terminal
|
||||||
* @deprecated This feature has been removed
|
* @deprecated This feature has been removed
|
||||||
*/
|
*/
|
||||||
printQRInTerminal?: boolean
|
printQRInTerminal?: boolean
|
||||||
/** should events be emitted for actions done by this socket connection */
|
/** should events be emitted for actions done by this socket connection */
|
||||||
emitOwnEvents: boolean
|
emitOwnEvents: boolean
|
||||||
/** custom upload hosts to upload media to */
|
/** custom upload hosts to upload media to */
|
||||||
customUploadHosts: MediaConnInfo['hosts']
|
customUploadHosts: MediaConnInfo['hosts']
|
||||||
/** time to wait between sending new retry requests */
|
/** time to wait between sending new retry requests */
|
||||||
retryRequestDelayMs: number
|
retryRequestDelayMs: number
|
||||||
/** max retry count */
|
/** max retry count */
|
||||||
maxMsgRetryCount: number
|
maxMsgRetryCount: number
|
||||||
/** time to wait for the generation of the next QR in ms */
|
/** time to wait for the generation of the next QR in ms */
|
||||||
qrTimeout?: number
|
qrTimeout?: number
|
||||||
/** provide an auth state object to maintain the auth state */
|
/** provide an auth state object to maintain the auth state */
|
||||||
auth: AuthenticationState
|
auth: AuthenticationState
|
||||||
/** manage history processing with this control; by default will sync up everything */
|
/** manage history processing with this control; by default will sync up everything */
|
||||||
shouldSyncHistoryMessage: (msg: proto.Message.IHistorySyncNotification) => boolean
|
shouldSyncHistoryMessage: (msg: proto.Message.IHistorySyncNotification) => boolean
|
||||||
/** transaction capability options for SignalKeyStore */
|
/** transaction capability options for SignalKeyStore */
|
||||||
transactionOpts: TransactionCapabilityOptions
|
transactionOpts: TransactionCapabilityOptions
|
||||||
/** marks the client as online whenever the socket successfully connects */
|
/** marks the client as online whenever the socket successfully connects */
|
||||||
markOnlineOnConnect: boolean
|
markOnlineOnConnect: boolean
|
||||||
/** alphanumeric country code (USA -> US) for the number used */
|
/** alphanumeric country code (USA -> US) for the number used */
|
||||||
countryCode: string
|
countryCode: string
|
||||||
/** provide a cache to store media, so does not have to be re-uploaded */
|
/** provide a cache to store media, so does not have to be re-uploaded */
|
||||||
mediaCache?: CacheStore
|
mediaCache?: CacheStore
|
||||||
/**
|
/**
|
||||||
* map to store the retry counts for failed messages;
|
* map to store the retry counts for failed messages;
|
||||||
* used to determine whether to retry a message or not */
|
* used to determine whether to retry a message or not */
|
||||||
msgRetryCounterCache?: CacheStore
|
msgRetryCounterCache?: CacheStore
|
||||||
/** provide a cache to store a user's device list */
|
/** provide a cache to store a user's device list */
|
||||||
userDevicesCache?: CacheStore
|
userDevicesCache?: CacheStore
|
||||||
/** cache to store call offers */
|
/** cache to store call offers */
|
||||||
callOfferCache?: CacheStore
|
callOfferCache?: CacheStore
|
||||||
/** cache to track placeholder resends */
|
/** cache to track placeholder resends */
|
||||||
placeholderResendCache?: CacheStore
|
placeholderResendCache?: CacheStore
|
||||||
/** width for link preview images */
|
/** width for link preview images */
|
||||||
linkPreviewImageThumbnailWidth: number
|
linkPreviewImageThumbnailWidth: number
|
||||||
/** Should Baileys ask the phone for full history, will be received async */
|
/** Should Baileys ask the phone for full history, will be received async */
|
||||||
syncFullHistory: boolean
|
syncFullHistory: boolean
|
||||||
/** Should baileys fire init queries automatically, default true */
|
/** Should baileys fire init queries automatically, default true */
|
||||||
fireInitQueries: boolean
|
fireInitQueries: boolean
|
||||||
/**
|
/**
|
||||||
* generate a high quality link preview,
|
* generate a high quality link preview,
|
||||||
* entails uploading the jpegThumbnail to WA
|
* entails uploading the jpegThumbnail to WA
|
||||||
* */
|
* */
|
||||||
generateHighQualityLinkPreview: boolean
|
generateHighQualityLinkPreview: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns if a jid should be ignored,
|
* Returns if a jid should be ignored,
|
||||||
* no event for that jid will be triggered.
|
* no event for that jid will be triggered.
|
||||||
* Messages from that jid will also not be decrypted
|
* Messages from that jid will also not be decrypted
|
||||||
* */
|
* */
|
||||||
shouldIgnoreJid: (jid: string) => boolean | undefined
|
shouldIgnoreJid: (jid: string) => boolean | undefined
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optionally patch the message before sending out
|
* Optionally patch the message before sending out
|
||||||
* */
|
* */
|
||||||
patchMessageBeforeSending: (
|
patchMessageBeforeSending: (
|
||||||
msg: proto.IMessage,
|
msg: proto.IMessage,
|
||||||
recipientJids?: string[],
|
recipientJids?: string[]
|
||||||
) => Promise<PatchedMessageWithRecipientJID[] | PatchedMessageWithRecipientJID> | PatchedMessageWithRecipientJID[] | PatchedMessageWithRecipientJID
|
) =>
|
||||||
|
| Promise<PatchedMessageWithRecipientJID[] | PatchedMessageWithRecipientJID>
|
||||||
|
| PatchedMessageWithRecipientJID[]
|
||||||
|
| PatchedMessageWithRecipientJID
|
||||||
|
|
||||||
/** verify app state MACs */
|
/** verify app state MACs */
|
||||||
appStateMacVerification: {
|
appStateMacVerification: {
|
||||||
patch: boolean
|
patch: boolean
|
||||||
snapshot: boolean
|
snapshot: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/** options for axios */
|
/** options for axios */
|
||||||
options: AxiosRequestConfig<{}>
|
options: AxiosRequestConfig<{}>
|
||||||
/**
|
/**
|
||||||
* fetch a message from your store
|
* fetch a message from your store
|
||||||
* implement this so that messages failed to send
|
* implement this so that messages failed to send
|
||||||
* (solves the "this message can take a while" issue) can be retried
|
* (solves the "this message can take a while" issue) can be retried
|
||||||
* */
|
* */
|
||||||
getMessage: (key: proto.IMessageKey) => Promise<proto.IMessage | undefined>
|
getMessage: (key: proto.IMessageKey) => Promise<proto.IMessage | undefined>
|
||||||
|
|
||||||
/** cached group metadata, use to prevent redundant requests to WA & speed up msg sending */
|
/** cached group metadata, use to prevent redundant requests to WA & speed up msg sending */
|
||||||
cachedGroupMetadata: (jid: string) => Promise<GroupMetadata | undefined>
|
cachedGroupMetadata: (jid: string) => Promise<GroupMetadata | undefined>
|
||||||
|
|
||||||
makeSignalRepository: (auth: SignalAuthState) => SignalRepository
|
makeSignalRepository: (auth: SignalAuthState) => SignalRepository
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,4 +26,4 @@ export type ConnectionState = {
|
|||||||
* If this is false, the primary phone and other devices will receive notifs
|
* If this is false, the primary phone and other devices will receive notifs
|
||||||
* */
|
* */
|
||||||
isOnline?: boolean
|
isOnline?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,23 +5,23 @@ import { USyncUser } from '../WAUSync'
|
|||||||
* Defines the interface for a USyncQuery protocol
|
* Defines the interface for a USyncQuery protocol
|
||||||
*/
|
*/
|
||||||
export interface USyncQueryProtocol {
|
export interface USyncQueryProtocol {
|
||||||
/**
|
/**
|
||||||
* The name of the protocol
|
* The name of the protocol
|
||||||
*/
|
*/
|
||||||
name: string
|
name: string
|
||||||
/**
|
/**
|
||||||
* Defines what goes inside the query part of a USyncQuery
|
* Defines what goes inside the query part of a USyncQuery
|
||||||
*/
|
*/
|
||||||
getQueryElement: () => BinaryNode
|
getQueryElement: () => BinaryNode
|
||||||
/**
|
/**
|
||||||
* Defines what goes inside the user part of a USyncQuery
|
* Defines what goes inside the user part of a USyncQuery
|
||||||
*/
|
*/
|
||||||
getUserElement: (user: USyncUser) => BinaryNode | null
|
getUserElement: (user: USyncUser) => BinaryNode | null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse the result of the query
|
* Parse the result of the query
|
||||||
* @param data Data from the result
|
* @param data Data from the result
|
||||||
* @returns Whatever the protocol is supposed to return
|
* @returns Whatever the protocol is supposed to return
|
||||||
*/
|
*/
|
||||||
parser: (data: BinaryNode) => unknown
|
parser: (data: BinaryNode) => unknown
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,51 +16,51 @@ import { SocketConfig } from './Socket'
|
|||||||
export type UserFacingSocketConfig = Partial<SocketConfig> & { auth: AuthenticationState }
|
export type UserFacingSocketConfig = Partial<SocketConfig> & { auth: AuthenticationState }
|
||||||
|
|
||||||
export type BrowsersMap = {
|
export type BrowsersMap = {
|
||||||
ubuntu(browser: string): [string, string, string]
|
ubuntu(browser: string): [string, string, string]
|
||||||
macOS(browser: string): [string, string, string]
|
macOS(browser: string): [string, string, string]
|
||||||
baileys(browser: string): [string, string, string]
|
baileys(browser: string): [string, string, string]
|
||||||
windows(browser: string): [string, string, string]
|
windows(browser: string): [string, string, string]
|
||||||
appropriate(browser: string): [string, string, string]
|
appropriate(browser: string): [string, string, string]
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DisconnectReason {
|
export enum DisconnectReason {
|
||||||
connectionClosed = 428,
|
connectionClosed = 428,
|
||||||
connectionLost = 408,
|
connectionLost = 408,
|
||||||
connectionReplaced = 440,
|
connectionReplaced = 440,
|
||||||
timedOut = 408,
|
timedOut = 408,
|
||||||
loggedOut = 401,
|
loggedOut = 401,
|
||||||
badSession = 500,
|
badSession = 500,
|
||||||
restartRequired = 515,
|
restartRequired = 515,
|
||||||
multideviceMismatch = 411,
|
multideviceMismatch = 411,
|
||||||
forbidden = 403,
|
forbidden = 403,
|
||||||
unavailableService = 503
|
unavailableService = 503
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WAInitResponse = {
|
export type WAInitResponse = {
|
||||||
ref: string
|
ref: string
|
||||||
ttl: number
|
ttl: number
|
||||||
status: 200
|
status: 200
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WABusinessHoursConfig = {
|
export type WABusinessHoursConfig = {
|
||||||
day_of_week: string
|
day_of_week: string
|
||||||
mode: string
|
mode: string
|
||||||
open_time?: number
|
open_time?: number
|
||||||
close_time?: number
|
close_time?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WABusinessProfile = {
|
export type WABusinessProfile = {
|
||||||
description: string
|
description: string
|
||||||
email: string | undefined
|
email: string | undefined
|
||||||
business_hours: {
|
business_hours: {
|
||||||
timezone?: string
|
timezone?: string
|
||||||
config?: WABusinessHoursConfig[]
|
config?: WABusinessHoursConfig[]
|
||||||
business_config?: WABusinessHoursConfig[]
|
business_config?: WABusinessHoursConfig[]
|
||||||
}
|
}
|
||||||
website: string[]
|
website: string[]
|
||||||
category?: string
|
category?: string
|
||||||
wid?: string
|
wid?: string
|
||||||
address?: string
|
address?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CurveKeyPair = { private: Uint8Array, public: Uint8Array }
|
export type CurveKeyPair = { private: Uint8Array; public: Uint8Array }
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import NodeCache from '@cacheable/node-cache'
|
import NodeCache from '@cacheable/node-cache'
|
||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
import { DEFAULT_CACHE_TTLS } from '../Defaults'
|
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 { Curve, signedKeyPair } from './crypto'
|
||||||
import { delay, generateRegistrationId } from './generics'
|
import { delay, generateRegistrationId } from './generics'
|
||||||
import { ILogger } from './logger'
|
import { ILogger } from './logger'
|
||||||
@@ -17,11 +25,13 @@ export function makeCacheableSignalKeyStore(
|
|||||||
logger?: ILogger,
|
logger?: ILogger,
|
||||||
_cache?: CacheStore
|
_cache?: CacheStore
|
||||||
): SignalKeyStore {
|
): SignalKeyStore {
|
||||||
const cache = _cache || new NodeCache({
|
const cache =
|
||||||
stdTTL: DEFAULT_CACHE_TTLS.SIGNAL_STORE, // 5 minutes
|
_cache ||
|
||||||
useClones: false,
|
new NodeCache({
|
||||||
deleteOnExpire: true,
|
stdTTL: DEFAULT_CACHE_TTLS.SIGNAL_STORE, // 5 minutes
|
||||||
})
|
useClones: false,
|
||||||
|
deleteOnExpire: true
|
||||||
|
})
|
||||||
|
|
||||||
function getUniqueId(type: string, id: string) {
|
function getUniqueId(type: string, id: string) {
|
||||||
return `${type}.${id}`
|
return `${type}.${id}`
|
||||||
@@ -29,23 +39,23 @@ export function makeCacheableSignalKeyStore(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
async get(type, ids) {
|
async get(type, ids) {
|
||||||
const data: { [_: string]: SignalDataTypeMap[typeof type] } = { }
|
const data: { [_: string]: SignalDataTypeMap[typeof type] } = {}
|
||||||
const idsToFetch: string[] = []
|
const idsToFetch: string[] = []
|
||||||
for(const id of ids) {
|
for (const id of ids) {
|
||||||
const item = cache.get<SignalDataTypeMap[typeof type]>(getUniqueId(type, id))
|
const item = cache.get<SignalDataTypeMap[typeof type]>(getUniqueId(type, id))
|
||||||
if(typeof item !== 'undefined') {
|
if (typeof item !== 'undefined') {
|
||||||
data[id] = item
|
data[id] = item
|
||||||
} else {
|
} else {
|
||||||
idsToFetch.push(id)
|
idsToFetch.push(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(idsToFetch.length) {
|
if (idsToFetch.length) {
|
||||||
logger?.trace({ items: idsToFetch.length }, 'loading from store')
|
logger?.trace({ items: idsToFetch.length }, 'loading from store')
|
||||||
const fetched = await store.get(type, idsToFetch)
|
const fetched = await store.get(type, idsToFetch)
|
||||||
for(const id of idsToFetch) {
|
for (const id of idsToFetch) {
|
||||||
const item = fetched[id]
|
const item = fetched[id]
|
||||||
if(item) {
|
if (item) {
|
||||||
data[id] = item
|
data[id] = item
|
||||||
cache.set(getUniqueId(type, id), item)
|
cache.set(getUniqueId(type, id), item)
|
||||||
}
|
}
|
||||||
@@ -56,8 +66,8 @@ export function makeCacheableSignalKeyStore(
|
|||||||
},
|
},
|
||||||
async set(data) {
|
async set(data) {
|
||||||
let keys = 0
|
let keys = 0
|
||||||
for(const type in data) {
|
for (const type in data) {
|
||||||
for(const id in data[type]) {
|
for (const id in data[type]) {
|
||||||
cache.set(getUniqueId(type, id), data[type][id])
|
cache.set(getUniqueId(type, id), data[type][id])
|
||||||
keys += 1
|
keys += 1
|
||||||
}
|
}
|
||||||
@@ -89,52 +99,45 @@ export const addTransactionCapability = (
|
|||||||
// number of queries made to the DB during the transaction
|
// number of queries made to the DB during the transaction
|
||||||
// only there for logging purposes
|
// only there for logging purposes
|
||||||
let dbQueriesInTransaction = 0
|
let dbQueriesInTransaction = 0
|
||||||
let transactionCache: SignalDataSet = { }
|
let transactionCache: SignalDataSet = {}
|
||||||
let mutations: SignalDataSet = { }
|
let mutations: SignalDataSet = {}
|
||||||
|
|
||||||
let transactionsInProgress = 0
|
let transactionsInProgress = 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get: async(type, ids) => {
|
get: async (type, ids) => {
|
||||||
if(isInTransaction()) {
|
if (isInTransaction()) {
|
||||||
const dict = transactionCache[type]
|
const dict = transactionCache[type]
|
||||||
const idsRequiringFetch = dict
|
const idsRequiringFetch = dict ? ids.filter(item => typeof dict[item] === 'undefined') : ids
|
||||||
? ids.filter(item => typeof dict[item] === 'undefined')
|
|
||||||
: ids
|
|
||||||
// only fetch if there are any items to fetch
|
// only fetch if there are any items to fetch
|
||||||
if(idsRequiringFetch.length) {
|
if (idsRequiringFetch.length) {
|
||||||
dbQueriesInTransaction += 1
|
dbQueriesInTransaction += 1
|
||||||
const result = await state.get(type, idsRequiringFetch)
|
const result = await state.get(type, idsRequiringFetch)
|
||||||
|
|
||||||
transactionCache[type] ||= {}
|
transactionCache[type] ||= {}
|
||||||
Object.assign(
|
Object.assign(transactionCache[type]!, result)
|
||||||
transactionCache[type]!,
|
|
||||||
result
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ids.reduce(
|
return ids.reduce((dict, id) => {
|
||||||
(dict, id) => {
|
const value = transactionCache[type]?.[id]
|
||||||
const value = transactionCache[type]?.[id]
|
if (value) {
|
||||||
if(value) {
|
dict[id] = value
|
||||||
dict[id] = value
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return dict
|
return dict
|
||||||
}, { }
|
}, {})
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
return state.get(type, ids)
|
return state.get(type, ids)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
set: data => {
|
set: data => {
|
||||||
if(isInTransaction()) {
|
if (isInTransaction()) {
|
||||||
logger.trace({ types: Object.keys(data) }, 'caching in transaction')
|
logger.trace({ types: Object.keys(data) }, 'caching in transaction')
|
||||||
for(const key in data) {
|
for (const key in data) {
|
||||||
transactionCache[key] = transactionCache[key] || { }
|
transactionCache[key] = transactionCache[key] || {}
|
||||||
Object.assign(transactionCache[key], data[key])
|
Object.assign(transactionCache[key], data[key])
|
||||||
|
|
||||||
mutations[key] = mutations[key] || { }
|
mutations[key] = mutations[key] || {}
|
||||||
Object.assign(mutations[key], data[key])
|
Object.assign(mutations[key], data[key])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -145,27 +148,27 @@ export const addTransactionCapability = (
|
|||||||
async transaction(work) {
|
async transaction(work) {
|
||||||
let result: Awaited<ReturnType<typeof work>>
|
let result: Awaited<ReturnType<typeof work>>
|
||||||
transactionsInProgress += 1
|
transactionsInProgress += 1
|
||||||
if(transactionsInProgress === 1) {
|
if (transactionsInProgress === 1) {
|
||||||
logger.trace('entering transaction')
|
logger.trace('entering transaction')
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result = await work()
|
result = await work()
|
||||||
// commit if this is the outermost transaction
|
// commit if this is the outermost transaction
|
||||||
if(transactionsInProgress === 1) {
|
if (transactionsInProgress === 1) {
|
||||||
if(Object.keys(mutations).length) {
|
if (Object.keys(mutations).length) {
|
||||||
logger.trace('committing transaction')
|
logger.trace('committing transaction')
|
||||||
// retry mechanism to ensure we've some recovery
|
// retry mechanism to ensure we've some recovery
|
||||||
// in case a transaction fails in the first attempt
|
// in case a transaction fails in the first attempt
|
||||||
let tries = maxCommitRetries
|
let tries = maxCommitRetries
|
||||||
while(tries) {
|
while (tries) {
|
||||||
tries -= 1
|
tries -= 1
|
||||||
//eslint-disable-next-line max-depth
|
//eslint-disable-next-line max-depth
|
||||||
try {
|
try {
|
||||||
await state.set(mutations)
|
await state.set(mutations)
|
||||||
logger.trace({ dbQueriesInTransaction }, 'committed transaction')
|
logger.trace({ dbQueriesInTransaction }, 'committed transaction')
|
||||||
break
|
break
|
||||||
} catch(error) {
|
} catch (error) {
|
||||||
logger.warn(`failed to commit ${Object.keys(mutations).length} mutations, tries left=${tries}`)
|
logger.warn(`failed to commit ${Object.keys(mutations).length} mutations, tries left=${tries}`)
|
||||||
await delay(delayBetweenTriesMs)
|
await delay(delayBetweenTriesMs)
|
||||||
}
|
}
|
||||||
@@ -176,9 +179,9 @@ export const addTransactionCapability = (
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
transactionsInProgress -= 1
|
transactionsInProgress -= 1
|
||||||
if(transactionsInProgress === 0) {
|
if (transactionsInProgress === 0) {
|
||||||
transactionCache = { }
|
transactionCache = {}
|
||||||
mutations = { }
|
mutations = {}
|
||||||
dbQueriesInTransaction = 0
|
dbQueriesInTransaction = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,6 +214,6 @@ export const initAuthCreds = (): AuthenticationCreds => {
|
|||||||
registered: false,
|
registered: false,
|
||||||
pairingCode: undefined,
|
pairingCode: undefined,
|
||||||
lastPropHash: undefined,
|
lastPropHash: undefined,
|
||||||
routingInfo: undefined,
|
routingInfo: undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,15 +16,13 @@ export const captureEventStream = (ev: BaileysEventEmitter, filename: string) =>
|
|||||||
// write mutex so data is appended in order
|
// write mutex so data is appended in order
|
||||||
const writeMutex = makeMutex()
|
const writeMutex = makeMutex()
|
||||||
// monkey patch eventemitter to capture all events
|
// monkey patch eventemitter to capture all events
|
||||||
ev.emit = function(...args: any[]) {
|
ev.emit = function (...args: any[]) {
|
||||||
const content = JSON.stringify({ timestamp: Date.now(), event: args[0], data: args[1] }) + '\n'
|
const content = JSON.stringify({ timestamp: Date.now(), event: args[0], data: args[1] }) + '\n'
|
||||||
const result = oldEmit.apply(ev, args)
|
const result = oldEmit.apply(ev, args)
|
||||||
|
|
||||||
writeMutex.mutex(
|
writeMutex.mutex(async () => {
|
||||||
async() => {
|
await writeFile(filename, content, { flag: 'a' })
|
||||||
await writeFile(filename, content, { flag: 'a' })
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -38,7 +36,7 @@ export const captureEventStream = (ev: BaileysEventEmitter, filename: string) =>
|
|||||||
export const readAndEmitEventStream = (filename: string, delayIntervalMs = 0) => {
|
export const readAndEmitEventStream = (filename: string, delayIntervalMs = 0) => {
|
||||||
const ev = new EventEmitter() as BaileysEventEmitter
|
const ev = new EventEmitter() as BaileysEventEmitter
|
||||||
|
|
||||||
const fireEvents = async() => {
|
const fireEvents = async () => {
|
||||||
// from: https://stackoverflow.com/questions/6156501/read-a-file-one-line-at-a-time-in-node-js
|
// from: https://stackoverflow.com/questions/6156501/read-a-file-one-line-at-a-time-in-node-js
|
||||||
const fileStream = createReadStream(filename)
|
const fileStream = createReadStream(filename)
|
||||||
|
|
||||||
@@ -49,10 +47,10 @@ export const readAndEmitEventStream = (filename: string, delayIntervalMs = 0) =>
|
|||||||
// Note: we use the crlfDelay option to recognize all instances of CR LF
|
// Note: we use the crlfDelay option to recognize all instances of CR LF
|
||||||
// ('\r\n') in input.txt as a single line break.
|
// ('\r\n') in input.txt as a single line break.
|
||||||
for await (const line of rl) {
|
for await (const line of rl) {
|
||||||
if(line) {
|
if (line) {
|
||||||
const { event, data } = JSON.parse(line)
|
const { event, data } = JSON.parse(line)
|
||||||
ev.emit(event, data)
|
ev.emit(event, data)
|
||||||
delayIntervalMs && await delay(delayIntervalMs)
|
delayIntervalMs && (await delay(delayIntervalMs))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,4 +61,4 @@ export const readAndEmitEventStream = (filename: string, delayIntervalMs = 0) =>
|
|||||||
ev,
|
ev,
|
||||||
task: fireEvents()
|
task: fireEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
import { Boom } from '@hapi/boom'
|
import { Boom } from '@hapi/boom'
|
||||||
import { createHash } from 'crypto'
|
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 { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, getBinaryNodeChildString } from '../WABinary'
|
||||||
import { getStream, getUrlFromDirectPath, toReadable } from './messages-media'
|
import { getStream, getUrlFromDirectPath, toReadable } from './messages-media'
|
||||||
|
|
||||||
@@ -11,28 +21,24 @@ export const parseCatalogNode = (node: BinaryNode) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
products,
|
products,
|
||||||
nextPageCursor: paging
|
nextPageCursor: paging ? getBinaryNodeChildString(paging, 'after') : undefined
|
||||||
? getBinaryNodeChildString(paging, 'after')
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parseCollectionsNode = (node: BinaryNode) => {
|
export const parseCollectionsNode = (node: BinaryNode) => {
|
||||||
const collectionsNode = getBinaryNodeChild(node, 'collections')
|
const collectionsNode = getBinaryNodeChild(node, 'collections')
|
||||||
const collections = getBinaryNodeChildren(collectionsNode, 'collection').map<CatalogCollection>(
|
const collections = getBinaryNodeChildren(collectionsNode, 'collection').map<CatalogCollection>(collectionNode => {
|
||||||
collectionNode => {
|
const id = getBinaryNodeChildString(collectionNode, 'id')!
|
||||||
const id = getBinaryNodeChildString(collectionNode, 'id')!
|
const name = getBinaryNodeChildString(collectionNode, 'name')!
|
||||||
const name = getBinaryNodeChildString(collectionNode, 'name')!
|
|
||||||
|
|
||||||
const products = getBinaryNodeChildren(collectionNode, 'product').map(parseProductNode)
|
const products = getBinaryNodeChildren(collectionNode, 'product').map(parseProductNode)
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
products,
|
products,
|
||||||
status: parseStatusInfo(collectionNode)
|
status: parseStatusInfo(collectionNode)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
collections
|
collections
|
||||||
@@ -41,26 +47,24 @@ export const parseCollectionsNode = (node: BinaryNode) => {
|
|||||||
|
|
||||||
export const parseOrderDetailsNode = (node: BinaryNode) => {
|
export const parseOrderDetailsNode = (node: BinaryNode) => {
|
||||||
const orderNode = getBinaryNodeChild(node, 'order')
|
const orderNode = getBinaryNodeChild(node, 'order')
|
||||||
const products = getBinaryNodeChildren(orderNode, 'product').map<OrderProduct>(
|
const products = getBinaryNodeChildren(orderNode, 'product').map<OrderProduct>(productNode => {
|
||||||
productNode => {
|
const imageNode = getBinaryNodeChild(productNode, 'image')!
|
||||||
const imageNode = getBinaryNodeChild(productNode, 'image')!
|
return {
|
||||||
return {
|
id: getBinaryNodeChildString(productNode, 'id')!,
|
||||||
id: getBinaryNodeChildString(productNode, 'id')!,
|
name: getBinaryNodeChildString(productNode, 'name')!,
|
||||||
name: getBinaryNodeChildString(productNode, 'name')!,
|
imageUrl: getBinaryNodeChildString(imageNode, 'url')!,
|
||||||
imageUrl: getBinaryNodeChildString(imageNode, 'url')!,
|
price: +getBinaryNodeChildString(productNode, 'price')!,
|
||||||
price: +getBinaryNodeChildString(productNode, 'price')!,
|
currency: getBinaryNodeChildString(productNode, 'currency')!,
|
||||||
currency: getBinaryNodeChildString(productNode, 'currency')!,
|
quantity: +getBinaryNodeChildString(productNode, 'quantity')!
|
||||||
quantity: +getBinaryNodeChildString(productNode, 'quantity')!
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
|
|
||||||
const priceNode = getBinaryNodeChild(orderNode, 'price')
|
const priceNode = getBinaryNodeChild(orderNode, 'price')
|
||||||
|
|
||||||
const orderDetails: OrderDetails = {
|
const orderDetails: OrderDetails = {
|
||||||
price: {
|
price: {
|
||||||
total: +getBinaryNodeChildString(priceNode, 'total')!,
|
total: +getBinaryNodeChildString(priceNode, 'total')!,
|
||||||
currency: getBinaryNodeChildString(priceNode, 'currency')!,
|
currency: getBinaryNodeChildString(priceNode, 'currency')!
|
||||||
},
|
},
|
||||||
products
|
products
|
||||||
}
|
}
|
||||||
@@ -69,94 +73,92 @@ export const parseOrderDetailsNode = (node: BinaryNode) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const toProductNode = (productId: string | undefined, product: ProductCreate | ProductUpdate) => {
|
export const toProductNode = (productId: string | undefined, product: ProductCreate | ProductUpdate) => {
|
||||||
const attrs: BinaryNode['attrs'] = { }
|
const attrs: BinaryNode['attrs'] = {}
|
||||||
const content: BinaryNode[] = [ ]
|
const content: BinaryNode[] = []
|
||||||
|
|
||||||
if(typeof productId !== 'undefined') {
|
if (typeof productId !== 'undefined') {
|
||||||
content.push({
|
content.push({
|
||||||
tag: 'id',
|
tag: 'id',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: Buffer.from(productId)
|
content: Buffer.from(productId)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if(typeof product.name !== 'undefined') {
|
if (typeof product.name !== 'undefined') {
|
||||||
content.push({
|
content.push({
|
||||||
tag: 'name',
|
tag: 'name',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: Buffer.from(product.name)
|
content: Buffer.from(product.name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if(typeof product.description !== 'undefined') {
|
if (typeof product.description !== 'undefined') {
|
||||||
content.push({
|
content.push({
|
||||||
tag: 'description',
|
tag: 'description',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: Buffer.from(product.description)
|
content: Buffer.from(product.description)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if(typeof product.retailerId !== 'undefined') {
|
if (typeof product.retailerId !== 'undefined') {
|
||||||
content.push({
|
content.push({
|
||||||
tag: 'retailer_id',
|
tag: 'retailer_id',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: Buffer.from(product.retailerId)
|
content: Buffer.from(product.retailerId)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if(product.images.length) {
|
if (product.images.length) {
|
||||||
content.push({
|
content.push({
|
||||||
tag: 'media',
|
tag: 'media',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: product.images.map(
|
content: product.images.map(img => {
|
||||||
img => {
|
if (!('url' in img)) {
|
||||||
if(!('url' in img)) {
|
throw new Boom('Expected img for product to already be uploaded', { statusCode: 400 })
|
||||||
throw new Boom('Expected img for product to already be uploaded', { statusCode: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
tag: 'image',
|
|
||||||
attrs: { },
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
tag: 'url',
|
|
||||||
attrs: { },
|
|
||||||
content: Buffer.from(img.url.toString())
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
return {
|
||||||
|
tag: 'image',
|
||||||
|
attrs: {},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
tag: 'url',
|
||||||
|
attrs: {},
|
||||||
|
content: Buffer.from(img.url.toString())
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if(typeof product.price !== 'undefined') {
|
if (typeof product.price !== 'undefined') {
|
||||||
content.push({
|
content.push({
|
||||||
tag: 'price',
|
tag: 'price',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: Buffer.from(product.price.toString())
|
content: Buffer.from(product.price.toString())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if(typeof product.currency !== 'undefined') {
|
if (typeof product.currency !== 'undefined') {
|
||||||
content.push({
|
content.push({
|
||||||
tag: 'currency',
|
tag: 'currency',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: Buffer.from(product.currency)
|
content: Buffer.from(product.currency)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if('originCountryCode' in product) {
|
if ('originCountryCode' in product) {
|
||||||
if(typeof product.originCountryCode === 'undefined') {
|
if (typeof product.originCountryCode === 'undefined') {
|
||||||
attrs['compliance_category'] = 'COUNTRY_ORIGIN_EXEMPT'
|
attrs['compliance_category'] = 'COUNTRY_ORIGIN_EXEMPT'
|
||||||
} else {
|
} else {
|
||||||
content.push({
|
content.push({
|
||||||
tag: 'compliance_info',
|
tag: 'compliance_info',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
tag: 'country_code_origin',
|
tag: 'country_code_origin',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: Buffer.from(product.originCountryCode)
|
content: Buffer.from(product.originCountryCode)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -164,8 +166,7 @@ export const toProductNode = (productId: string | undefined, product: ProductCre
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof product.isHidden !== 'undefined') {
|
||||||
if(typeof product.isHidden !== 'undefined') {
|
|
||||||
attrs['is_hidden'] = product.isHidden.toString()
|
attrs['is_hidden'] = product.isHidden.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,16 +189,16 @@ export const parseProductNode = (productNode: BinaryNode) => {
|
|||||||
id,
|
id,
|
||||||
imageUrls: parseImageUrls(mediaNode),
|
imageUrls: parseImageUrls(mediaNode),
|
||||||
reviewStatus: {
|
reviewStatus: {
|
||||||
whatsapp: getBinaryNodeChildString(statusInfoNode, 'status')!,
|
whatsapp: getBinaryNodeChildString(statusInfoNode, 'status')!
|
||||||
},
|
},
|
||||||
availability: 'in stock',
|
availability: 'in stock',
|
||||||
name: getBinaryNodeChildString(productNode, 'name')!,
|
name: getBinaryNodeChildString(productNode, 'name')!,
|
||||||
retailerId: getBinaryNodeChildString(productNode, 'retailer_id'),
|
retailerId: getBinaryNodeChildString(productNode, 'retailer_id'),
|
||||||
url: getBinaryNodeChildString(productNode, 'url'),
|
url: getBinaryNodeChildString(productNode, 'url'),
|
||||||
description: getBinaryNodeChildString(productNode, 'description')!,
|
description: getBinaryNodeChildString(productNode, 'description')!,
|
||||||
price: +getBinaryNodeChildString(productNode, 'price')!,
|
price: +getBinaryNodeChildString(productNode, 'price')!,
|
||||||
currency: getBinaryNodeChildString(productNode, 'currency')!,
|
currency: getBinaryNodeChildString(productNode, 'currency')!,
|
||||||
isHidden,
|
isHidden
|
||||||
}
|
}
|
||||||
|
|
||||||
return product
|
return product
|
||||||
@@ -206,10 +207,16 @@ export const parseProductNode = (productNode: BinaryNode) => {
|
|||||||
/**
|
/**
|
||||||
* Uploads images not already uploaded to WA's servers
|
* 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 = {
|
||||||
...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
|
return product
|
||||||
}
|
}
|
||||||
@@ -217,43 +224,37 @@ export async function uploadingNecessaryImagesOfProduct<T extends ProductUpdate
|
|||||||
/**
|
/**
|
||||||
* Uploads images not already uploaded to WA's servers
|
* Uploads images not already uploaded to WA's servers
|
||||||
*/
|
*/
|
||||||
export const uploadingNecessaryImages = async(
|
export const uploadingNecessaryImages = async (
|
||||||
images: WAMediaUpload[],
|
images: WAMediaUpload[],
|
||||||
waUploadToServer: WAMediaUploadFunction,
|
waUploadToServer: WAMediaUploadFunction,
|
||||||
timeoutMs = 30_000
|
timeoutMs = 30_000
|
||||||
) => {
|
) => {
|
||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
images.map<Promise<{ url: string }>>(
|
images.map<Promise<{ url: string }>>(async img => {
|
||||||
async img => {
|
if ('url' in img) {
|
||||||
|
const url = img.url.toString()
|
||||||
if('url' in img) {
|
if (url.includes('.whatsapp.net')) {
|
||||||
const url = img.url.toString()
|
return { url }
|
||||||
if(url.includes('.whatsapp.net')) {
|
|
||||||
return { url }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { stream } = await getStream(img)
|
|
||||||
const hasher = createHash('sha256')
|
|
||||||
const contentBlocks: Buffer[] = []
|
|
||||||
for await (const block of stream) {
|
|
||||||
hasher.update(block)
|
|
||||||
contentBlocks.push(block)
|
|
||||||
}
|
|
||||||
|
|
||||||
const sha = hasher.digest('base64')
|
|
||||||
|
|
||||||
const { directPath } = await waUploadToServer(
|
|
||||||
toReadable(Buffer.concat(contentBlocks)),
|
|
||||||
{
|
|
||||||
mediaType: 'product-catalog-image',
|
|
||||||
fileEncSha256B64: sha,
|
|
||||||
timeoutMs
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return { url: getUrlFromDirectPath(directPath) }
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
const { stream } = await getStream(img)
|
||||||
|
const hasher = createHash('sha256')
|
||||||
|
const contentBlocks: Buffer[] = []
|
||||||
|
for await (const block of stream) {
|
||||||
|
hasher.update(block)
|
||||||
|
contentBlocks.push(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sha = hasher.digest('base64')
|
||||||
|
|
||||||
|
const { directPath } = await waUploadToServer(toReadable(Buffer.concat(contentBlocks)), {
|
||||||
|
mediaType: 'product-catalog-image',
|
||||||
|
fileEncSha256B64: sha,
|
||||||
|
timeoutMs
|
||||||
|
})
|
||||||
|
return { url: getUrlFromDirectPath(directPath) }
|
||||||
|
})
|
||||||
)
|
)
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
@@ -270,6 +271,6 @@ const parseStatusInfo = (mediaNode: BinaryNode): CatalogStatus => {
|
|||||||
const node = getBinaryNodeChild(mediaNode, 'status_info')
|
const node = getBinaryNodeChild(mediaNode, 'status_info')
|
||||||
return {
|
return {
|
||||||
status: getBinaryNodeChildString(node, 'status')!,
|
status: getBinaryNodeChildString(node, 'status')!,
|
||||||
canAppeal: getBinaryNodeChildString(node, 'can_appeal') === 'true',
|
canAppeal: getBinaryNodeChildString(node, 'can_appeal') === 'true'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,32 @@
|
|||||||
import { Boom } from '@hapi/boom'
|
import { Boom } from '@hapi/boom'
|
||||||
import { AxiosRequestConfig } from 'axios'
|
import { AxiosRequestConfig } from 'axios'
|
||||||
import { proto } from '../../WAProto'
|
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 { ChatLabelAssociation, LabelAssociationType, MessageLabelAssociation } from '../Types/LabelAssociation'
|
||||||
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, jidNormalizedUser } from '../WABinary'
|
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, jidNormalizedUser } from '../WABinary'
|
||||||
import { aesDecrypt, aesEncrypt, hkdf, hmacSign } from './crypto'
|
import { aesDecrypt, aesEncrypt, hkdf, hmacSign } from './crypto'
|
||||||
import { toNumber } from './generics'
|
import { toNumber } from './generics'
|
||||||
import { ILogger } from './logger'
|
import { ILogger } from './logger'
|
||||||
import { LT_HASH_ANTI_TAMPERING } from './lt-hash'
|
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>
|
type FetchAppStateSyncKey = (keyId: string) => Promise<proto.Message.IAppStateSyncKeyData | null | undefined>
|
||||||
|
|
||||||
export type ChatMutationMap = { [index: string]: ChatMutation }
|
export type ChatMutationMap = { [index: string]: ChatMutation }
|
||||||
|
|
||||||
const mutationKeys = async(keydata: Uint8Array) => {
|
const mutationKeys = async (keydata: Uint8Array) => {
|
||||||
const expanded = await hkdf(keydata, 160, { info: 'WhatsApp Mutation Keys' })
|
const expanded = await hkdf(keydata, 160, { info: 'WhatsApp Mutation Keys' })
|
||||||
return {
|
return {
|
||||||
indexKey: expanded.slice(0, 32),
|
indexKey: expanded.slice(0, 32),
|
||||||
@@ -25,16 +37,21 @@ 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 = () => {
|
const getKeyData = () => {
|
||||||
let r: number
|
let r: number
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
case proto.SyncdMutation.SyncdOperation.SET:
|
case proto.SyncdMutation.SyncdOperation.SET:
|
||||||
r = 0x01
|
r = 0x01
|
||||||
break
|
break
|
||||||
case proto.SyncdMutation.SyncdOperation.REMOVE:
|
case proto.SyncdMutation.SyncdOperation.REMOVE:
|
||||||
r = 0x02
|
r = 0x02
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
const buff = Buffer.from([r])
|
const buff = Buffer.from([r])
|
||||||
@@ -58,7 +75,7 @@ const to64BitNetworkOrder = (e: number) => {
|
|||||||
return buff
|
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'>) => {
|
const makeLtHashGenerator = ({ indexValueMap, hash }: Pick<LTHashState, 'hash' | 'indexValueMap'>) => {
|
||||||
indexValueMap = { ...indexValueMap }
|
indexValueMap = { ...indexValueMap }
|
||||||
@@ -69,8 +86,8 @@ const makeLtHashGenerator = ({ indexValueMap, hash }: Pick<LTHashState, 'hash' |
|
|||||||
mix: ({ indexMac, valueMac, operation }: Mac) => {
|
mix: ({ indexMac, valueMac, operation }: Mac) => {
|
||||||
const indexMacBase64 = Buffer.from(indexMac).toString('base64')
|
const indexMacBase64 = Buffer.from(indexMac).toString('base64')
|
||||||
const prevOp = indexValueMap[indexMacBase64]
|
const prevOp = indexValueMap[indexMacBase64]
|
||||||
if(operation === proto.SyncdMutation.SyncdOperation.REMOVE) {
|
if (operation === proto.SyncdMutation.SyncdOperation.REMOVE) {
|
||||||
if(!prevOp) {
|
if (!prevOp) {
|
||||||
throw new Boom('tried remove, but no previous op', { data: { indexMac, valueMac } })
|
throw new Boom('tried remove, but no previous op', { data: { indexMac, valueMac } })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,11 +99,11 @@ const makeLtHashGenerator = ({ indexValueMap, hash }: Pick<LTHashState, 'hash' |
|
|||||||
indexValueMap[indexMacBase64] = { valueMac }
|
indexValueMap[indexMacBase64] = { valueMac }
|
||||||
}
|
}
|
||||||
|
|
||||||
if(prevOp) {
|
if (prevOp) {
|
||||||
subBuffs.push(new Uint8Array(prevOp.valueMac).buffer)
|
subBuffs.push(new Uint8Array(prevOp.valueMac).buffer)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
finish: async() => {
|
finish: async () => {
|
||||||
const hashArrayBuffer = new Uint8Array(hash).buffer
|
const hashArrayBuffer = new Uint8Array(hash).buffer
|
||||||
const result = await LT_HASH_ANTI_TAMPERING.subtractThenAdd(hashArrayBuffer, addBuffs, subBuffs)
|
const result = await LT_HASH_ANTI_TAMPERING.subtractThenAdd(hashArrayBuffer, addBuffs, subBuffs)
|
||||||
const buffer = Buffer.from(result)
|
const buffer = Buffer.from(result)
|
||||||
@@ -100,34 +117,31 @@ const makeLtHashGenerator = ({ indexValueMap, hash }: Pick<LTHashState, 'hash' |
|
|||||||
}
|
}
|
||||||
|
|
||||||
const generateSnapshotMac = (lthash: Uint8Array, version: number, name: WAPatchName, key: Buffer) => {
|
const generateSnapshotMac = (lthash: Uint8Array, version: number, name: WAPatchName, key: Buffer) => {
|
||||||
const total = Buffer.concat([
|
const total = Buffer.concat([lthash, to64BitNetworkOrder(version), Buffer.from(name, 'utf-8')])
|
||||||
lthash,
|
|
||||||
to64BitNetworkOrder(version),
|
|
||||||
Buffer.from(name, 'utf-8')
|
|
||||||
])
|
|
||||||
return hmacSign(total, key, 'sha256')
|
return hmacSign(total, key, 'sha256')
|
||||||
}
|
}
|
||||||
|
|
||||||
const generatePatchMac = (snapshotMac: Uint8Array, valueMacs: Uint8Array[], version: number, type: WAPatchName, key: Buffer) => {
|
const generatePatchMac = (
|
||||||
const total = Buffer.concat([
|
snapshotMac: Uint8Array,
|
||||||
snapshotMac,
|
valueMacs: Uint8Array[],
|
||||||
...valueMacs,
|
version: number,
|
||||||
to64BitNetworkOrder(version),
|
type: WAPatchName,
|
||||||
Buffer.from(type, 'utf-8')
|
key: Buffer
|
||||||
])
|
) => {
|
||||||
|
const total = Buffer.concat([snapshotMac, ...valueMacs, to64BitNetworkOrder(version), Buffer.from(type, 'utf-8')])
|
||||||
return hmacSign(total, key)
|
return hmacSign(total, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const newLTHashState = (): LTHashState => ({ version: 0, hash: Buffer.alloc(128), indexValueMap: {} })
|
export const newLTHashState = (): LTHashState => ({ version: 0, hash: Buffer.alloc(128), indexValueMap: {} })
|
||||||
|
|
||||||
export const encodeSyncdPatch = async(
|
export const encodeSyncdPatch = async (
|
||||||
{ type, index, syncAction, apiVersion, operation }: WAPatchCreate,
|
{ type, index, syncAction, apiVersion, operation }: WAPatchCreate,
|
||||||
myAppStateKeyId: string,
|
myAppStateKeyId: string,
|
||||||
state: LTHashState,
|
state: LTHashState,
|
||||||
getAppStateSyncKey: FetchAppStateSyncKey
|
getAppStateSyncKey: FetchAppStateSyncKey
|
||||||
) => {
|
) => {
|
||||||
const key = !!myAppStateKeyId ? await getAppStateSyncKey(myAppStateKeyId) : undefined
|
const key = !!myAppStateKeyId ? await getAppStateSyncKey(myAppStateKeyId) : undefined
|
||||||
if(!key) {
|
if (!key) {
|
||||||
throw new Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { statusCode: 404 })
|
throw new Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { statusCode: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +199,7 @@ export const encodeSyncdPatch = async(
|
|||||||
return { patch, state }
|
return { patch, state }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const decodeSyncdMutations = async(
|
export const decodeSyncdMutations = async (
|
||||||
msgMutations: (proto.ISyncdMutation | proto.ISyncdRecord)[],
|
msgMutations: (proto.ISyncdMutation | proto.ISyncdRecord)[],
|
||||||
initialState: LTHashState,
|
initialState: LTHashState,
|
||||||
getAppStateSyncKey: FetchAppStateSyncKey,
|
getAppStateSyncKey: FetchAppStateSyncKey,
|
||||||
@@ -196,19 +210,20 @@ export const decodeSyncdMutations = async(
|
|||||||
// indexKey used to HMAC sign record.index.blob
|
// indexKey used to HMAC sign record.index.blob
|
||||||
// valueEncryptionKey used to AES-256-CBC encrypt record.value.blob[0:-32]
|
// valueEncryptionKey used to AES-256-CBC encrypt record.value.blob[0:-32]
|
||||||
// the remaining record.value.blob[0:-32] is the mac, it the HMAC sign of key.keyId + decoded proto data + length of bytes in keyId
|
// the remaining record.value.blob[0:-32] is the mac, it the HMAC sign of key.keyId + decoded proto data + length of bytes in keyId
|
||||||
for(const msgMutation of msgMutations) {
|
for (const msgMutation of msgMutations) {
|
||||||
// if it's a syncdmutation, get the operation property
|
// if it's a syncdmutation, get the operation property
|
||||||
// otherwise, if it's only a record -- it'll be a SET mutation
|
// 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 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 key = await getKey(record.keyId!.id!)
|
||||||
const content = Buffer.from(record.value!.blob!)
|
const content = Buffer.from(record.value!.blob!)
|
||||||
const encContent = content.slice(0, -32)
|
const encContent = content.slice(0, -32)
|
||||||
const ogValueMac = content.slice(-32)
|
const ogValueMac = content.slice(-32)
|
||||||
if(validateMacs) {
|
if (validateMacs) {
|
||||||
const contentHmac = generateMac(operation!, encContent, record.keyId!.id!, key.valueMacKey)
|
const contentHmac = generateMac(operation!, encContent, record.keyId!.id!, key.valueMacKey)
|
||||||
if(Buffer.compare(contentHmac, ogValueMac) !== 0) {
|
if (Buffer.compare(contentHmac, ogValueMac) !== 0) {
|
||||||
throw new Boom('HMAC content verification failed')
|
throw new Boom('HMAC content verification failed')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,9 +231,9 @@ export const decodeSyncdMutations = async(
|
|||||||
const result = aesDecrypt(encContent, key.valueEncryptionKey)
|
const result = aesDecrypt(encContent, key.valueEncryptionKey)
|
||||||
const syncAction = proto.SyncActionData.decode(result)
|
const syncAction = proto.SyncActionData.decode(result)
|
||||||
|
|
||||||
if(validateMacs) {
|
if (validateMacs) {
|
||||||
const hmac = hmacSign(syncAction.index!, key.indexKey)
|
const hmac = hmacSign(syncAction.index!, key.indexKey)
|
||||||
if(Buffer.compare(hmac, record.index!.blob!) !== 0) {
|
if (Buffer.compare(hmac, record.index!.blob!) !== 0) {
|
||||||
throw new Boom('HMAC index verification failed')
|
throw new Boom('HMAC index verification failed')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,15 +253,18 @@ export const decodeSyncdMutations = async(
|
|||||||
async function getKey(keyId: Uint8Array) {
|
async function getKey(keyId: Uint8Array) {
|
||||||
const base64Key = Buffer.from(keyId).toString('base64')
|
const base64Key = Buffer.from(keyId).toString('base64')
|
||||||
const keyEnc = await getAppStateSyncKey(base64Key)
|
const keyEnc = await getAppStateSyncKey(base64Key)
|
||||||
if(!keyEnc) {
|
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!)
|
return mutationKeys(keyEnc.keyData!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const decodeSyncdPatch = async(
|
export const decodeSyncdPatch = async (
|
||||||
msg: proto.ISyncdPatch,
|
msg: proto.ISyncdPatch,
|
||||||
name: WAPatchName,
|
name: WAPatchName,
|
||||||
initialState: LTHashState,
|
initialState: LTHashState,
|
||||||
@@ -254,18 +272,24 @@ export const decodeSyncdPatch = async(
|
|||||||
onMutation: (mutation: ChatMutation) => void,
|
onMutation: (mutation: ChatMutation) => void,
|
||||||
validateMacs: boolean
|
validateMacs: boolean
|
||||||
) => {
|
) => {
|
||||||
if(validateMacs) {
|
if (validateMacs) {
|
||||||
const base64Key = Buffer.from(msg.keyId!.id!).toString('base64')
|
const base64Key = Buffer.from(msg.keyId!.id!).toString('base64')
|
||||||
const mainKeyObj = await getAppStateSyncKey(base64Key)
|
const mainKeyObj = await getAppStateSyncKey(base64Key)
|
||||||
if(!mainKeyObj) {
|
if (!mainKeyObj) {
|
||||||
throw new Boom(`failed to find key "${base64Key}" to decode patch`, { statusCode: 404, data: { msg } })
|
throw new Boom(`failed to find key "${base64Key}" to decode patch`, { statusCode: 404, data: { msg } })
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainKey = await mutationKeys(mainKeyObj.keyData!)
|
const mainKey = await mutationKeys(mainKeyObj.keyData!)
|
||||||
const mutationmacs = msg.mutations!.map(mutation => mutation.record!.value!.blob!.slice(-32))
|
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(
|
||||||
if(Buffer.compare(patchMac, msg.patchMac!) !== 0) {
|
msg.snapshotMac!,
|
||||||
|
mutationmacs,
|
||||||
|
toNumber(msg.version!.version),
|
||||||
|
name,
|
||||||
|
mainKey.patchMacKey
|
||||||
|
)
|
||||||
|
if (Buffer.compare(patchMac, msg.patchMac!) !== 0) {
|
||||||
throw new Boom('Invalid patch mac')
|
throw new Boom('Invalid patch mac')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,68 +298,59 @@ export const decodeSyncdPatch = async(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export const extractSyncdPatches = async(
|
export const extractSyncdPatches = async (result: BinaryNode, options: AxiosRequestConfig<{}>) => {
|
||||||
result: BinaryNode,
|
|
||||||
options: AxiosRequestConfig<{}>
|
|
||||||
) => {
|
|
||||||
const syncNode = getBinaryNodeChild(result, 'sync')
|
const syncNode = getBinaryNodeChild(result, 'sync')
|
||||||
const collectionNodes = getBinaryNodeChildren(syncNode, 'collection')
|
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(
|
await Promise.all(
|
||||||
collectionNodes.map(
|
collectionNodes.map(async collectionNode => {
|
||||||
async collectionNode => {
|
const patchesNode = getBinaryNodeChild(collectionNode, 'patches')
|
||||||
const patchesNode = getBinaryNodeChild(collectionNode, 'patches')
|
|
||||||
|
|
||||||
const patches = getBinaryNodeChildren(patchesNode || collectionNode, 'patch')
|
const patches = getBinaryNodeChildren(patchesNode || collectionNode, 'patch')
|
||||||
const snapshotNode = getBinaryNodeChild(collectionNode, 'snapshot')
|
const snapshotNode = getBinaryNodeChild(collectionNode, 'snapshot')
|
||||||
|
|
||||||
const syncds: proto.ISyncdPatch[] = []
|
const syncds: proto.ISyncdPatch[] = []
|
||||||
const name = collectionNode.attrs.name as WAPatchName
|
const name = collectionNode.attrs.name as WAPatchName
|
||||||
|
|
||||||
const hasMorePatches = collectionNode.attrs.has_more_patches === 'true'
|
const hasMorePatches = collectionNode.attrs.has_more_patches === 'true'
|
||||||
|
|
||||||
let snapshot: proto.ISyncdSnapshot | undefined = undefined
|
let snapshot: proto.ISyncdSnapshot | undefined = undefined
|
||||||
if(snapshotNode && !!snapshotNode.content) {
|
if (snapshotNode && !!snapshotNode.content) {
|
||||||
if(!Buffer.isBuffer(snapshotNode)) {
|
if (!Buffer.isBuffer(snapshotNode)) {
|
||||||
snapshotNode.content = Buffer.from(Object.values(snapshotNode.content))
|
snapshotNode.content = Buffer.from(Object.values(snapshotNode.content))
|
||||||
}
|
|
||||||
|
|
||||||
const blobRef = proto.ExternalBlobReference.decode(
|
|
||||||
snapshotNode.content as Buffer
|
|
||||||
)
|
|
||||||
const data = await downloadExternalBlob(blobRef, options)
|
|
||||||
snapshot = proto.SyncdSnapshot.decode(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for(let { content } of patches) {
|
const blobRef = proto.ExternalBlobReference.decode(snapshotNode.content as Buffer)
|
||||||
if(content) {
|
const data = await downloadExternalBlob(blobRef, options)
|
||||||
if(!Buffer.isBuffer(content)) {
|
snapshot = proto.SyncdSnapshot.decode(data)
|
||||||
content = Buffer.from(Object.values(content))
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncd = proto.SyncdPatch.decode(content as Uint8Array)
|
|
||||||
if(!syncd.version) {
|
|
||||||
syncd.version = { version: +collectionNode.attrs.version + 1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
syncds.push(syncd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final[name] = { patches: syncds, hasMorePatches, snapshot }
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
for (let { content } of patches) {
|
||||||
|
if (content) {
|
||||||
|
if (!Buffer.isBuffer(content)) {
|
||||||
|
content = Buffer.from(Object.values(content))
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncd = proto.SyncdPatch.decode(content as Uint8Array)
|
||||||
|
if (!syncd.version) {
|
||||||
|
syncd.version = { version: +collectionNode.attrs.version + 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
syncds.push(syncd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final[name] = { patches: syncds, hasMorePatches, snapshot }
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
return final
|
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 stream = await downloadContentFromMessage(blob, 'md-app-state', { options })
|
||||||
const bufferArray: Buffer[] = []
|
const bufferArray: Buffer[] = []
|
||||||
for await (const chunk of stream) {
|
for await (const chunk of stream) {
|
||||||
@@ -345,16 +360,13 @@ export const downloadExternalBlob = async(
|
|||||||
return Buffer.concat(bufferArray)
|
return Buffer.concat(bufferArray)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const downloadExternalPatch = async(
|
export const downloadExternalPatch = async (blob: proto.IExternalBlobReference, options: AxiosRequestConfig<{}>) => {
|
||||||
blob: proto.IExternalBlobReference,
|
|
||||||
options: AxiosRequestConfig<{}>
|
|
||||||
) => {
|
|
||||||
const buffer = await downloadExternalBlob(blob, options)
|
const buffer = await downloadExternalBlob(blob, options)
|
||||||
const syncData = proto.SyncdMutations.decode(buffer)
|
const syncData = proto.SyncdMutations.decode(buffer)
|
||||||
return syncData
|
return syncData
|
||||||
}
|
}
|
||||||
|
|
||||||
export const decodeSyncdSnapshot = async(
|
export const decodeSyncdSnapshot = async (
|
||||||
name: WAPatchName,
|
name: WAPatchName,
|
||||||
snapshot: proto.ISyncdSnapshot,
|
snapshot: proto.ISyncdSnapshot,
|
||||||
getAppStateSyncKey: FetchAppStateSyncKey,
|
getAppStateSyncKey: FetchAppStateSyncKey,
|
||||||
@@ -365,34 +377,33 @@ export const decodeSyncdSnapshot = async(
|
|||||||
newState.version = toNumber(snapshot.version!.version)
|
newState.version = toNumber(snapshot.version!.version)
|
||||||
|
|
||||||
const mutationMap: ChatMutationMap = {}
|
const mutationMap: ChatMutationMap = {}
|
||||||
const areMutationsRequired = typeof minimumVersionNumber === 'undefined'
|
const areMutationsRequired = typeof minimumVersionNumber === 'undefined' || newState.version > minimumVersionNumber
|
||||||
|| newState.version > minimumVersionNumber
|
|
||||||
|
|
||||||
const { hash, indexValueMap } = await decodeSyncdMutations(
|
const { hash, indexValueMap } = await decodeSyncdMutations(
|
||||||
snapshot.records!,
|
snapshot.records!,
|
||||||
newState,
|
newState,
|
||||||
getAppStateSyncKey,
|
getAppStateSyncKey,
|
||||||
areMutationsRequired
|
areMutationsRequired
|
||||||
? (mutation) => {
|
? mutation => {
|
||||||
const index = mutation.syncAction.index?.toString()
|
const index = mutation.syncAction.index?.toString()
|
||||||
mutationMap[index!] = mutation
|
mutationMap[index!] = mutation
|
||||||
}
|
}
|
||||||
: () => { },
|
: () => {},
|
||||||
validateMacs
|
validateMacs
|
||||||
)
|
)
|
||||||
newState.hash = hash
|
newState.hash = hash
|
||||||
newState.indexValueMap = indexValueMap
|
newState.indexValueMap = indexValueMap
|
||||||
|
|
||||||
if(validateMacs) {
|
if (validateMacs) {
|
||||||
const base64Key = Buffer.from(snapshot.keyId!.id!).toString('base64')
|
const base64Key = Buffer.from(snapshot.keyId!.id!).toString('base64')
|
||||||
const keyEnc = await getAppStateSyncKey(base64Key)
|
const keyEnc = await getAppStateSyncKey(base64Key)
|
||||||
if(!keyEnc) {
|
if (!keyEnc) {
|
||||||
throw new Boom(`failed to find key "${base64Key}" to decode mutation`)
|
throw new Boom(`failed to find key "${base64Key}" to decode mutation`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await mutationKeys(keyEnc.keyData!)
|
const result = await mutationKeys(keyEnc.keyData!)
|
||||||
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey)
|
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey)
|
||||||
if(Buffer.compare(snapshot.mac!, computedSnapshotMac) !== 0) {
|
if (Buffer.compare(snapshot.mac!, computedSnapshotMac) !== 0) {
|
||||||
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name} from snapshot`)
|
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name} from snapshot`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -403,7 +414,7 @@ export const decodeSyncdSnapshot = async(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const decodePatches = async(
|
export const decodePatches = async (
|
||||||
name: WAPatchName,
|
name: WAPatchName,
|
||||||
syncds: proto.ISyncdPatch[],
|
syncds: proto.ISyncdPatch[],
|
||||||
initial: LTHashState,
|
initial: LTHashState,
|
||||||
@@ -420,9 +431,9 @@ export const decodePatches = async(
|
|||||||
|
|
||||||
const mutationMap: ChatMutationMap = {}
|
const mutationMap: ChatMutationMap = {}
|
||||||
|
|
||||||
for(const syncd of syncds) {
|
for (const syncd of syncds) {
|
||||||
const { version, keyId, snapshotMac } = syncd
|
const { version, keyId, snapshotMac } = syncd
|
||||||
if(syncd.externalMutations) {
|
if (syncd.externalMutations) {
|
||||||
logger?.trace({ name, version }, 'downloading external patch')
|
logger?.trace({ name, version }, 'downloading external patch')
|
||||||
const ref = await downloadExternalPatch(syncd.externalMutations, options)
|
const ref = await downloadExternalPatch(syncd.externalMutations, options)
|
||||||
logger?.debug({ name, version, mutations: ref.mutations.length }, 'downloaded external patch')
|
logger?.debug({ name, version, mutations: ref.mutations.length }, 'downloaded external patch')
|
||||||
@@ -441,26 +452,26 @@ export const decodePatches = async(
|
|||||||
getAppStateSyncKey,
|
getAppStateSyncKey,
|
||||||
shouldMutate
|
shouldMutate
|
||||||
? mutation => {
|
? mutation => {
|
||||||
const index = mutation.syncAction.index?.toString()
|
const index = mutation.syncAction.index?.toString()
|
||||||
mutationMap[index!] = mutation
|
mutationMap[index!] = mutation
|
||||||
}
|
}
|
||||||
: (() => { }),
|
: () => {},
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
newState.hash = decodeResult.hash
|
newState.hash = decodeResult.hash
|
||||||
newState.indexValueMap = decodeResult.indexValueMap
|
newState.indexValueMap = decodeResult.indexValueMap
|
||||||
|
|
||||||
if(validateMacs) {
|
if (validateMacs) {
|
||||||
const base64Key = Buffer.from(keyId!.id!).toString('base64')
|
const base64Key = Buffer.from(keyId!.id!).toString('base64')
|
||||||
const keyEnc = await getAppStateSyncKey(base64Key)
|
const keyEnc = await getAppStateSyncKey(base64Key)
|
||||||
if(!keyEnc) {
|
if (!keyEnc) {
|
||||||
throw new Boom(`failed to find key "${base64Key}" to decode mutation`)
|
throw new Boom(`failed to find key "${base64Key}" to decode mutation`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await mutationKeys(keyEnc.keyData!)
|
const result = await mutationKeys(keyEnc.keyData!)
|
||||||
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey)
|
const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey)
|
||||||
if(Buffer.compare(snapshotMac!, computedSnapshotMac) !== 0) {
|
if (Buffer.compare(snapshotMac!, computedSnapshotMac) !== 0) {
|
||||||
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name}`)
|
throw new Boom(`failed to verify LTHash at ${newState.version} of ${name}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -472,38 +483,35 @@ export const decodePatches = async(
|
|||||||
return { state: newState, mutationMap }
|
return { state: newState, mutationMap }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const chatModificationToAppPatch = (
|
export const chatModificationToAppPatch = (mod: ChatModification, jid: string) => {
|
||||||
mod: ChatModification,
|
|
||||||
jid: string
|
|
||||||
) => {
|
|
||||||
const OP = proto.SyncdMutation.SyncdOperation
|
const OP = proto.SyncdMutation.SyncdOperation
|
||||||
const getMessageRange = (lastMessages: LastMessageList) => {
|
const getMessageRange = (lastMessages: LastMessageList) => {
|
||||||
let messageRange: proto.SyncActionValue.ISyncActionMessageRange
|
let messageRange: proto.SyncActionValue.ISyncActionMessageRange
|
||||||
if(Array.isArray(lastMessages)) {
|
if (Array.isArray(lastMessages)) {
|
||||||
const lastMsg = lastMessages[lastMessages.length - 1]
|
const lastMsg = lastMessages[lastMessages.length - 1]
|
||||||
messageRange = {
|
messageRange = {
|
||||||
lastMessageTimestamp: lastMsg?.messageTimestamp,
|
lastMessageTimestamp: lastMsg?.messageTimestamp,
|
||||||
messages: lastMessages?.length ? lastMessages.map(
|
messages: lastMessages?.length
|
||||||
m => {
|
? lastMessages.map(m => {
|
||||||
if(!m.key?.id || !m.key?.remoteJid) {
|
if (!m.key?.id || !m.key?.remoteJid) {
|
||||||
throw new Boom('Incomplete key', { statusCode: 400, data: m })
|
throw new Boom('Incomplete key', { statusCode: 400, data: m })
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isJidGroup(m.key.remoteJid) && !m.key.fromMe && !m.key.participant) {
|
if (isJidGroup(m.key.remoteJid) && !m.key.fromMe && !m.key.participant) {
|
||||||
throw new Boom('Expected not from me message to have participant', { statusCode: 400, data: m })
|
throw new Boom('Expected not from me message to have participant', { statusCode: 400, data: m })
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!m.messageTimestamp || !toNumber(m.messageTimestamp)) {
|
if (!m.messageTimestamp || !toNumber(m.messageTimestamp)) {
|
||||||
throw new Boom('Missing timestamp in last message list', { statusCode: 400, data: m })
|
throw new Boom('Missing timestamp in last message list', { statusCode: 400, data: m })
|
||||||
}
|
}
|
||||||
|
|
||||||
if(m.key.participant) {
|
if (m.key.participant) {
|
||||||
m.key.participant = jidNormalizedUser(m.key.participant)
|
m.key.participant = jidNormalizedUser(m.key.participant)
|
||||||
}
|
}
|
||||||
|
|
||||||
return m
|
return m
|
||||||
}
|
})
|
||||||
) : undefined
|
: undefined
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
messageRange = lastMessages
|
messageRange = lastMessages
|
||||||
@@ -513,7 +521,7 @@ export const chatModificationToAppPatch = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
let patch: WAPatchCreate
|
let patch: WAPatchCreate
|
||||||
if('mute' in mod) {
|
if ('mute' in mod) {
|
||||||
patch = {
|
patch = {
|
||||||
syncAction: {
|
syncAction: {
|
||||||
muteAction: {
|
muteAction: {
|
||||||
@@ -526,7 +534,7 @@ export const chatModificationToAppPatch = (
|
|||||||
apiVersion: 2,
|
apiVersion: 2,
|
||||||
operation: OP.SET
|
operation: OP.SET
|
||||||
}
|
}
|
||||||
} else if('archive' in mod) {
|
} else if ('archive' in mod) {
|
||||||
patch = {
|
patch = {
|
||||||
syncAction: {
|
syncAction: {
|
||||||
archiveChatAction: {
|
archiveChatAction: {
|
||||||
@@ -539,7 +547,7 @@ export const chatModificationToAppPatch = (
|
|||||||
apiVersion: 3,
|
apiVersion: 3,
|
||||||
operation: OP.SET
|
operation: OP.SET
|
||||||
}
|
}
|
||||||
} else if('markRead' in mod) {
|
} else if ('markRead' in mod) {
|
||||||
patch = {
|
patch = {
|
||||||
syncAction: {
|
syncAction: {
|
||||||
markChatAsReadAction: {
|
markChatAsReadAction: {
|
||||||
@@ -552,7 +560,7 @@ export const chatModificationToAppPatch = (
|
|||||||
apiVersion: 3,
|
apiVersion: 3,
|
||||||
operation: OP.SET
|
operation: OP.SET
|
||||||
}
|
}
|
||||||
} else if('deleteForMe' in mod) {
|
} else if ('deleteForMe' in mod) {
|
||||||
const { timestamp, key, deleteMedia } = mod.deleteForMe
|
const { timestamp, key, deleteMedia } = mod.deleteForMe
|
||||||
patch = {
|
patch = {
|
||||||
syncAction: {
|
syncAction: {
|
||||||
@@ -566,7 +574,7 @@ export const chatModificationToAppPatch = (
|
|||||||
apiVersion: 3,
|
apiVersion: 3,
|
||||||
operation: OP.SET
|
operation: OP.SET
|
||||||
}
|
}
|
||||||
} else if('clear' in mod) {
|
} else if ('clear' in mod) {
|
||||||
patch = {
|
patch = {
|
||||||
syncAction: {
|
syncAction: {
|
||||||
clearChatAction: {} // add message range later
|
clearChatAction: {} // add message range later
|
||||||
@@ -576,7 +584,7 @@ export const chatModificationToAppPatch = (
|
|||||||
apiVersion: 6,
|
apiVersion: 6,
|
||||||
operation: OP.SET
|
operation: OP.SET
|
||||||
}
|
}
|
||||||
} else if('pin' in mod) {
|
} else if ('pin' in mod) {
|
||||||
patch = {
|
patch = {
|
||||||
syncAction: {
|
syncAction: {
|
||||||
pinAction: {
|
pinAction: {
|
||||||
@@ -588,7 +596,7 @@ export const chatModificationToAppPatch = (
|
|||||||
apiVersion: 5,
|
apiVersion: 5,
|
||||||
operation: OP.SET
|
operation: OP.SET
|
||||||
}
|
}
|
||||||
} else if('star' in mod) {
|
} else if ('star' in mod) {
|
||||||
const key = mod.star.messages[0]
|
const key = mod.star.messages[0]
|
||||||
patch = {
|
patch = {
|
||||||
syncAction: {
|
syncAction: {
|
||||||
@@ -601,11 +609,11 @@ export const chatModificationToAppPatch = (
|
|||||||
apiVersion: 2,
|
apiVersion: 2,
|
||||||
operation: OP.SET
|
operation: OP.SET
|
||||||
}
|
}
|
||||||
} else if('delete' in mod) {
|
} else if ('delete' in mod) {
|
||||||
patch = {
|
patch = {
|
||||||
syncAction: {
|
syncAction: {
|
||||||
deleteChatAction: {
|
deleteChatAction: {
|
||||||
messageRange: getMessageRange(mod.lastMessages),
|
messageRange: getMessageRange(mod.lastMessages)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
index: ['deleteChat', jid, '1'],
|
index: ['deleteChat', jid, '1'],
|
||||||
@@ -613,7 +621,7 @@ export const chatModificationToAppPatch = (
|
|||||||
apiVersion: 6,
|
apiVersion: 6,
|
||||||
operation: OP.SET
|
operation: OP.SET
|
||||||
}
|
}
|
||||||
} else if('pushNameSetting' in mod) {
|
} else if ('pushNameSetting' in mod) {
|
||||||
patch = {
|
patch = {
|
||||||
syncAction: {
|
syncAction: {
|
||||||
pushNameSetting: {
|
pushNameSetting: {
|
||||||
@@ -623,71 +631,64 @@ export const chatModificationToAppPatch = (
|
|||||||
index: ['setting_pushName'],
|
index: ['setting_pushName'],
|
||||||
type: 'critical_block',
|
type: 'critical_block',
|
||||||
apiVersion: 1,
|
apiVersion: 1,
|
||||||
operation: OP.SET,
|
operation: OP.SET
|
||||||
}
|
}
|
||||||
} else if('addLabel' in mod) {
|
} else if ('addLabel' in mod) {
|
||||||
patch = {
|
patch = {
|
||||||
syncAction: {
|
syncAction: {
|
||||||
labelEditAction: {
|
labelEditAction: {
|
||||||
name: mod.addLabel.name,
|
name: mod.addLabel.name,
|
||||||
color: mod.addLabel.color,
|
color: mod.addLabel.color,
|
||||||
predefinedId : mod.addLabel.predefinedId,
|
predefinedId: mod.addLabel.predefinedId,
|
||||||
deleted: mod.addLabel.deleted
|
deleted: mod.addLabel.deleted
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
index: ['label_edit', mod.addLabel.id],
|
index: ['label_edit', mod.addLabel.id],
|
||||||
type: 'regular',
|
type: 'regular',
|
||||||
apiVersion: 3,
|
apiVersion: 3,
|
||||||
operation: OP.SET,
|
operation: OP.SET
|
||||||
}
|
}
|
||||||
} else if('addChatLabel' in mod) {
|
} else if ('addChatLabel' in mod) {
|
||||||
patch = {
|
patch = {
|
||||||
syncAction: {
|
syncAction: {
|
||||||
labelAssociationAction: {
|
labelAssociationAction: {
|
||||||
labeled: true,
|
labeled: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
index: [LabelAssociationType.Chat, mod.addChatLabel.labelId, jid],
|
index: [LabelAssociationType.Chat, mod.addChatLabel.labelId, jid],
|
||||||
type: 'regular',
|
type: 'regular',
|
||||||
apiVersion: 3,
|
apiVersion: 3,
|
||||||
operation: OP.SET,
|
operation: OP.SET
|
||||||
}
|
}
|
||||||
} else if('removeChatLabel' in mod) {
|
} else if ('removeChatLabel' in mod) {
|
||||||
patch = {
|
patch = {
|
||||||
syncAction: {
|
syncAction: {
|
||||||
labelAssociationAction: {
|
labelAssociationAction: {
|
||||||
labeled: false,
|
labeled: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
index: [LabelAssociationType.Chat, mod.removeChatLabel.labelId, jid],
|
index: [LabelAssociationType.Chat, mod.removeChatLabel.labelId, jid],
|
||||||
type: 'regular',
|
type: 'regular',
|
||||||
apiVersion: 3,
|
apiVersion: 3,
|
||||||
operation: OP.SET,
|
operation: OP.SET
|
||||||
}
|
}
|
||||||
} else if('addMessageLabel' in mod) {
|
} else if ('addMessageLabel' in mod) {
|
||||||
patch = {
|
patch = {
|
||||||
syncAction: {
|
syncAction: {
|
||||||
labelAssociationAction: {
|
labelAssociationAction: {
|
||||||
labeled: true,
|
labeled: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
index: [
|
index: [LabelAssociationType.Message, mod.addMessageLabel.labelId, jid, mod.addMessageLabel.messageId, '0', '0'],
|
||||||
LabelAssociationType.Message,
|
|
||||||
mod.addMessageLabel.labelId,
|
|
||||||
jid,
|
|
||||||
mod.addMessageLabel.messageId,
|
|
||||||
'0',
|
|
||||||
'0'
|
|
||||||
],
|
|
||||||
type: 'regular',
|
type: 'regular',
|
||||||
apiVersion: 3,
|
apiVersion: 3,
|
||||||
operation: OP.SET,
|
operation: OP.SET
|
||||||
}
|
}
|
||||||
} else if('removeMessageLabel' in mod) {
|
} else if ('removeMessageLabel' in mod) {
|
||||||
patch = {
|
patch = {
|
||||||
syncAction: {
|
syncAction: {
|
||||||
labelAssociationAction: {
|
labelAssociationAction: {
|
||||||
labeled: false,
|
labeled: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
index: [
|
index: [
|
||||||
@@ -700,7 +701,7 @@ export const chatModificationToAppPatch = (
|
|||||||
],
|
],
|
||||||
type: 'regular',
|
type: 'regular',
|
||||||
apiVersion: 3,
|
apiVersion: 3,
|
||||||
operation: OP.SET,
|
operation: OP.SET
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Boom('not supported')
|
throw new Boom('not supported')
|
||||||
@@ -716,7 +717,7 @@ export const processSyncAction = (
|
|||||||
ev: BaileysEventEmitter,
|
ev: BaileysEventEmitter,
|
||||||
me: Contact,
|
me: Contact,
|
||||||
initialSyncOpts?: InitialAppStateSyncOptions,
|
initialSyncOpts?: InitialAppStateSyncOptions,
|
||||||
logger?: ILogger,
|
logger?: ILogger
|
||||||
) => {
|
) => {
|
||||||
const isInitialSync = !!initialSyncOpts
|
const isInitialSync = !!initialSyncOpts
|
||||||
const accountSettings = initialSyncOpts?.accountSettings
|
const accountSettings = initialSyncOpts?.accountSettings
|
||||||
@@ -728,20 +729,15 @@ export const processSyncAction = (
|
|||||||
index: [type, id, msgId, fromMe]
|
index: [type, id, msgId, fromMe]
|
||||||
} = syncAction
|
} = syncAction
|
||||||
|
|
||||||
if(action?.muteAction) {
|
if (action?.muteAction) {
|
||||||
ev.emit(
|
ev.emit('chats.update', [
|
||||||
'chats.update',
|
{
|
||||||
[
|
id,
|
||||||
{
|
muteEndTime: action.muteAction?.muted ? toNumber(action.muteAction.muteEndTimestamp) : null,
|
||||||
id,
|
conditional: getChatUpdateConditional(id, undefined)
|
||||||
muteEndTime: action.muteAction?.muted
|
}
|
||||||
? toNumber(action.muteAction.muteEndTimestamp)
|
])
|
||||||
: null,
|
} else if (action?.archiveChatAction || type === 'archive' || type === 'unarchive') {
|
||||||
conditional: getChatUpdateConditional(id, undefined)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
} else if(action?.archiveChatAction || type === 'archive' || type === 'unarchive') {
|
|
||||||
// okay so we've to do some annoying computation here
|
// okay so we've to do some annoying computation here
|
||||||
// when we're initially syncing the app state
|
// when we're initially syncing the app state
|
||||||
// there are a few cases we need to handle
|
// there are a few cases we need to handle
|
||||||
@@ -753,9 +749,7 @@ export const processSyncAction = (
|
|||||||
// 2. if the account unarchiveChats setting is false -- then it doesn't matter,
|
// 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
|
// it'll always take an app state action to mark in unarchived -- which we'll get anyway
|
||||||
const archiveAction = action?.archiveChatAction
|
const archiveAction = action?.archiveChatAction
|
||||||
const isArchived = archiveAction
|
const isArchived = archiveAction ? archiveAction.archived : type === 'archive'
|
||||||
? archiveAction.archived
|
|
||||||
: type === 'archive'
|
|
||||||
// // basically we don't need to fire an "archive" update if the chat is being marked unarchvied
|
// // basically we don't need to fire an "archive" update if the chat is being marked unarchvied
|
||||||
// // this only applies for the initial sync
|
// // this only applies for the initial sync
|
||||||
// if(isInitialSync && !isArchived) {
|
// if(isInitialSync && !isArchived) {
|
||||||
@@ -765,24 +759,28 @@ export const processSyncAction = (
|
|||||||
const msgRange = !accountSettings?.unarchiveChats ? undefined : archiveAction?.messageRange
|
const msgRange = !accountSettings?.unarchiveChats ? undefined : archiveAction?.messageRange
|
||||||
// logger?.debug({ chat: id, syncAction }, 'message range archive')
|
// logger?.debug({ chat: id, syncAction }, 'message range archive')
|
||||||
|
|
||||||
ev.emit('chats.update', [{
|
ev.emit('chats.update', [
|
||||||
id,
|
{
|
||||||
archived: isArchived,
|
id,
|
||||||
conditional: getChatUpdateConditional(id, msgRange)
|
archived: isArchived,
|
||||||
}])
|
conditional: getChatUpdateConditional(id, msgRange)
|
||||||
} else if(action?.markChatAsReadAction) {
|
}
|
||||||
|
])
|
||||||
|
} else if (action?.markChatAsReadAction) {
|
||||||
const markReadAction = action.markChatAsReadAction
|
const markReadAction = action.markChatAsReadAction
|
||||||
// basically we don't need to fire an "read" update if the chat is being marked as read
|
// basically we don't need to fire an "read" update if the chat is being marked as read
|
||||||
// because the chat is read by default
|
// because the chat is read by default
|
||||||
// this only applies for the initial sync
|
// this only applies for the initial sync
|
||||||
const isNullUpdate = isInitialSync && markReadAction.read
|
const isNullUpdate = isInitialSync && markReadAction.read
|
||||||
|
|
||||||
ev.emit('chats.update', [{
|
ev.emit('chats.update', [
|
||||||
id,
|
{
|
||||||
unreadCount: isNullUpdate ? null : !!markReadAction?.read ? 0 : -1,
|
id,
|
||||||
conditional: getChatUpdateConditional(id, markReadAction?.messageRange)
|
unreadCount: isNullUpdate ? null : !!markReadAction?.read ? 0 : -1,
|
||||||
}])
|
conditional: getChatUpdateConditional(id, markReadAction?.messageRange)
|
||||||
} else if(action?.deleteMessageForMeAction || type === 'deleteMessageForMe') {
|
}
|
||||||
|
])
|
||||||
|
} else if (action?.deleteMessageForMeAction || type === 'deleteMessageForMe') {
|
||||||
ev.emit('messages.delete', {
|
ev.emit('messages.delete', {
|
||||||
keys: [
|
keys: [
|
||||||
{
|
{
|
||||||
@@ -792,30 +790,32 @@ export const processSyncAction = (
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
} else if(action?.contactAction) {
|
} else if (action?.contactAction) {
|
||||||
ev.emit('contacts.upsert', [{ id, name: action.contactAction.fullName! }])
|
ev.emit('contacts.upsert', [{ id, name: action.contactAction.fullName! }])
|
||||||
} else if(action?.pushNameSetting) {
|
} else if (action?.pushNameSetting) {
|
||||||
const name = action?.pushNameSetting?.name
|
const name = action?.pushNameSetting?.name
|
||||||
if(name && me?.name !== name) {
|
if (name && me?.name !== name) {
|
||||||
ev.emit('creds.update', { me: { ...me, name } })
|
ev.emit('creds.update', { me: { ...me, name } })
|
||||||
}
|
}
|
||||||
} else if(action?.pinAction) {
|
} else if (action?.pinAction) {
|
||||||
ev.emit('chats.update', [{
|
ev.emit('chats.update', [
|
||||||
id,
|
{
|
||||||
pinned: action.pinAction?.pinned ? toNumber(action.timestamp) : null,
|
id,
|
||||||
conditional: getChatUpdateConditional(id, undefined)
|
pinned: action.pinAction?.pinned ? toNumber(action.timestamp) : null,
|
||||||
}])
|
conditional: getChatUpdateConditional(id, undefined)
|
||||||
} else if(action?.unarchiveChatsSetting) {
|
}
|
||||||
|
])
|
||||||
|
} else if (action?.unarchiveChatsSetting) {
|
||||||
const unarchiveChats = !!action.unarchiveChatsSetting.unarchiveChats
|
const unarchiveChats = !!action.unarchiveChatsSetting.unarchiveChats
|
||||||
ev.emit('creds.update', { accountSettings: { unarchiveChats } })
|
ev.emit('creds.update', { accountSettings: { unarchiveChats } })
|
||||||
|
|
||||||
logger?.info(`archive setting updated => '${action.unarchiveChatsSetting.unarchiveChats}'`)
|
logger?.info(`archive setting updated => '${action.unarchiveChatsSetting.unarchiveChats}'`)
|
||||||
if(accountSettings) {
|
if (accountSettings) {
|
||||||
accountSettings.unarchiveChats = unarchiveChats
|
accountSettings.unarchiveChats = unarchiveChats
|
||||||
}
|
}
|
||||||
} else if(action?.starAction || type === 'star') {
|
} else if (action?.starAction || type === 'star') {
|
||||||
let starred = action?.starAction?.starred
|
let starred = action?.starAction?.starred
|
||||||
if(typeof starred !== 'boolean') {
|
if (typeof starred !== 'boolean') {
|
||||||
starred = syncAction.index[syncAction.index.length - 1] === '1'
|
starred = syncAction.index[syncAction.index.length - 1] === '1'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -825,11 +825,11 @@ export const processSyncAction = (
|
|||||||
update: { starred }
|
update: { starred }
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
} else if(action?.deleteChatAction || type === 'deleteChat') {
|
} else if (action?.deleteChatAction || type === 'deleteChat') {
|
||||||
if(!isInitialSync) {
|
if (!isInitialSync) {
|
||||||
ev.emit('chats.delete', [id])
|
ev.emit('chats.delete', [id])
|
||||||
}
|
}
|
||||||
} else if(action?.labelEditAction) {
|
} else if (action?.labelEditAction) {
|
||||||
const { name, color, deleted, predefinedId } = action.labelEditAction
|
const { name, color, deleted, predefinedId } = action.labelEditAction
|
||||||
|
|
||||||
ev.emit('labels.edit', {
|
ev.emit('labels.edit', {
|
||||||
@@ -839,42 +839,47 @@ export const processSyncAction = (
|
|||||||
deleted: deleted!,
|
deleted: deleted!,
|
||||||
predefinedId: predefinedId ? String(predefinedId) : undefined
|
predefinedId: predefinedId ? String(predefinedId) : undefined
|
||||||
})
|
})
|
||||||
} else if(action?.labelAssociationAction) {
|
} else if (action?.labelAssociationAction) {
|
||||||
ev.emit('labels.association', {
|
ev.emit('labels.association', {
|
||||||
type: action.labelAssociationAction.labeled
|
type: action.labelAssociationAction.labeled ? 'add' : 'remove',
|
||||||
? 'add'
|
association:
|
||||||
: 'remove',
|
type === LabelAssociationType.Chat
|
||||||
association: type === LabelAssociationType.Chat
|
? ({
|
||||||
? {
|
type: LabelAssociationType.Chat,
|
||||||
type: LabelAssociationType.Chat,
|
chatId: syncAction.index[2],
|
||||||
chatId: syncAction.index[2],
|
labelId: syncAction.index[1]
|
||||||
labelId: syncAction.index[1]
|
} as ChatLabelAssociation)
|
||||||
} as ChatLabelAssociation
|
: ({
|
||||||
: {
|
type: LabelAssociationType.Message,
|
||||||
type: LabelAssociationType.Message,
|
chatId: syncAction.index[2],
|
||||||
chatId: syncAction.index[2],
|
messageId: syncAction.index[3],
|
||||||
messageId: syncAction.index[3],
|
labelId: syncAction.index[1]
|
||||||
labelId: syncAction.index[1]
|
} as MessageLabelAssociation)
|
||||||
} as MessageLabelAssociation
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
logger?.debug({ syncAction, id }, 'unprocessable update')
|
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
|
return isInitialSync
|
||||||
? (data) => {
|
? data => {
|
||||||
const chat = data.historySets.chats[id] || data.chatUpserts[id]
|
const chat = data.historySets.chats[id] || data.chatUpserts[id]
|
||||||
if(chat) {
|
if (chat) {
|
||||||
return msgRange ? isValidPatchBasedOnMessageRange(chat, msgRange) : true
|
return msgRange ? isValidPatchBasedOnMessageRange(chat, msgRange) : true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidPatchBasedOnMessageRange(chat: Chat, msgRange: proto.SyncActionValue.ISyncActionMessageRange | null | undefined) {
|
function isValidPatchBasedOnMessageRange(
|
||||||
const lastMsgTimestamp = Number(msgRange?.lastMessageTimestamp || msgRange?.lastSystemMessageTimestamp || 0)
|
chat: Chat,
|
||||||
const chatLastMsgTimestamp = Number(chat?.lastMessageRecvTimestamp || 0)
|
msgRange: proto.SyncActionValue.ISyncActionMessageRange | null | undefined
|
||||||
return lastMsgTimestamp >= chatLastMsgTimestamp
|
) {
|
||||||
|
const lastMsgTimestamp = Number(msgRange?.lastMessageTimestamp || msgRange?.lastSystemMessageTimestamp || 0)
|
||||||
|
const chatLastMsgTimestamp = Number(chat?.lastMessageRecvTimestamp || 0)
|
||||||
|
return lastMsgTimestamp >= chatLastMsgTimestamp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,8 @@ import { KeyPair } from '../Types'
|
|||||||
const { subtle } = globalThis.crypto
|
const { subtle } = globalThis.crypto
|
||||||
|
|
||||||
/** prefix version byte to the pub keys, required for some curve crypto functions */
|
/** prefix version byte to the pub keys, required for some curve crypto functions */
|
||||||
export const generateSignalPubKey = (pubKey: Uint8Array | Buffer) => (
|
export const generateSignalPubKey = (pubKey: Uint8Array | Buffer) =>
|
||||||
pubKey.length === 33
|
pubKey.length === 33 ? pubKey : Buffer.concat([KEY_BUNDLE_TYPE, pubKey])
|
||||||
? pubKey
|
|
||||||
: Buffer.concat([ KEY_BUNDLE_TYPE, pubKey ])
|
|
||||||
)
|
|
||||||
|
|
||||||
export const Curve = {
|
export const Curve = {
|
||||||
generateKeyPair: (): KeyPair => {
|
generateKeyPair: (): KeyPair => {
|
||||||
@@ -26,14 +23,12 @@ export const Curve = {
|
|||||||
const shared = libsignal.curve.calculateAgreement(generateSignalPubKey(publicKey), privateKey)
|
const shared = libsignal.curve.calculateAgreement(generateSignalPubKey(publicKey), privateKey)
|
||||||
return Buffer.from(shared)
|
return Buffer.from(shared)
|
||||||
},
|
},
|
||||||
sign: (privateKey: Uint8Array, buf: Uint8Array) => (
|
sign: (privateKey: Uint8Array, buf: Uint8Array) => libsignal.curve.calculateSignature(privateKey, buf),
|
||||||
libsignal.curve.calculateSignature(privateKey, buf)
|
|
||||||
),
|
|
||||||
verify: (pubKey: Uint8Array, message: Uint8Array, signature: Uint8Array) => {
|
verify: (pubKey: Uint8Array, message: Uint8Array, signature: Uint8Array) => {
|
||||||
try {
|
try {
|
||||||
libsignal.curve.verifySignature(generateSignalPubKey(pubKey), message, signature)
|
libsignal.curve.verifySignature(generateSignalPubKey(pubKey), message, signature)
|
||||||
return true
|
return true
|
||||||
} catch(error) {
|
} catch (error) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,7 +68,7 @@ export function aesDecryptGCM(ciphertext: Uint8Array, key: Uint8Array, iv: Uint8
|
|||||||
decipher.setAAD(additionalData)
|
decipher.setAAD(additionalData)
|
||||||
decipher.setAuthTag(tag)
|
decipher.setAuthTag(tag)
|
||||||
|
|
||||||
return Buffer.concat([ decipher.update(enc), decipher.final() ])
|
return Buffer.concat([decipher.update(enc), decipher.final()])
|
||||||
}
|
}
|
||||||
|
|
||||||
export function aesEncryptCTR(plaintext: Uint8Array, key: Uint8Array, iv: Uint8Array) {
|
export function aesEncryptCTR(plaintext: Uint8Array, key: Uint8Array, iv: Uint8Array) {
|
||||||
@@ -111,7 +106,11 @@ export function aesEncrypWithIV(buffer: Buffer, key: Buffer, IV: Buffer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sign HMAC using SHA 256
|
// 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()
|
return createHmac(variant, key).update(buffer).digest()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,27 +126,17 @@ export function md5(buffer: Buffer) {
|
|||||||
export async function hkdf(
|
export async function hkdf(
|
||||||
buffer: Uint8Array | Buffer,
|
buffer: Uint8Array | Buffer,
|
||||||
expandedLength: number,
|
expandedLength: number,
|
||||||
info: { salt?: Buffer, info?: string }
|
info: { salt?: Buffer; info?: string }
|
||||||
): Promise<Buffer> {
|
): Promise<Buffer> {
|
||||||
// Ensure we have a Uint8Array for the key material
|
// Ensure we have a Uint8Array for the key material
|
||||||
const inputKeyMaterial = buffer instanceof Uint8Array
|
const inputKeyMaterial = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer)
|
||||||
? buffer
|
|
||||||
: new Uint8Array(buffer)
|
|
||||||
|
|
||||||
// Set default values if not provided
|
// Set default values if not provided
|
||||||
const salt = info.salt ? new Uint8Array(info.salt) : new Uint8Array(0)
|
const salt = info.salt ? new Uint8Array(info.salt) : new Uint8Array(0)
|
||||||
const infoBytes = info.info
|
const infoBytes = info.info ? new TextEncoder().encode(info.info) : new Uint8Array(0)
|
||||||
? new TextEncoder().encode(info.info)
|
|
||||||
: new Uint8Array(0)
|
|
||||||
|
|
||||||
// Import the input key material
|
// Import the input key material
|
||||||
const importedKey = await subtle.importKey(
|
const importedKey = await subtle.importKey('raw', inputKeyMaterial, { name: 'HKDF' }, false, ['deriveBits'])
|
||||||
'raw',
|
|
||||||
inputKeyMaterial,
|
|
||||||
{ name: 'HKDF' },
|
|
||||||
false,
|
|
||||||
['deriveBits']
|
|
||||||
)
|
|
||||||
|
|
||||||
// Derive bits using HKDF
|
// Derive bits using HKDF
|
||||||
const derivedBits = await subtle.deriveBits(
|
const derivedBits = await subtle.deriveBits(
|
||||||
@@ -164,7 +153,6 @@ export async function hkdf(
|
|||||||
return Buffer.from(derivedBits)
|
return Buffer.from(derivedBits)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function derivePairingCodeKey(pairingCode: string, salt: Buffer): Promise<Buffer> {
|
export async function derivePairingCodeKey(pairingCode: string, salt: Buffer): Promise<Buffer> {
|
||||||
// Convert inputs to formats Web Crypto API can work with
|
// Convert inputs to formats Web Crypto API can work with
|
||||||
const encoder = new TextEncoder()
|
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)
|
const saltBuffer = salt instanceof Uint8Array ? salt : new Uint8Array(salt)
|
||||||
|
|
||||||
// Import the pairing code as key material
|
// Import the pairing code as key material
|
||||||
const keyMaterial = await subtle.importKey(
|
const keyMaterial = await subtle.importKey('raw', pairingCodeBuffer, { name: 'PBKDF2' }, false, ['deriveBits'])
|
||||||
'raw',
|
|
||||||
pairingCodeBuffer,
|
|
||||||
{ name: 'PBKDF2' },
|
|
||||||
false,
|
|
||||||
['deriveBits']
|
|
||||||
)
|
|
||||||
|
|
||||||
// Derive bits using PBKDF2 with the same parameters
|
// Derive bits using PBKDF2 with the same parameters
|
||||||
// 2 << 16 = 131,072 iterations
|
// 2 << 16 = 131,072 iterations
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
import { Boom } from '@hapi/boom'
|
import { Boom } from '@hapi/boom'
|
||||||
import { proto } from '../../WAProto'
|
import { proto } from '../../WAProto'
|
||||||
import { SignalRepository, WAMessageKey } from '../Types'
|
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 { unpadRandomMax16 } from './generics'
|
||||||
import { ILogger } from './logger'
|
import { ILogger } from './logger'
|
||||||
|
|
||||||
@@ -24,17 +34,20 @@ export const NACK_REASONS = {
|
|||||||
DBOperationFailed: 552
|
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.
|
* Decode the received node as a message.
|
||||||
* @note this will only parse the message, not decrypt it
|
* @note this will only parse the message, not decrypt it
|
||||||
*/
|
*/
|
||||||
export function decodeMessageNode(
|
export function decodeMessageNode(stanza: BinaryNode, meId: string, meLid: string) {
|
||||||
stanza: BinaryNode,
|
|
||||||
meId: string,
|
|
||||||
meLid: string
|
|
||||||
) {
|
|
||||||
let msgType: MessageType
|
let msgType: MessageType
|
||||||
let chatId: string
|
let chatId: string
|
||||||
let author: string
|
let author: string
|
||||||
@@ -47,9 +60,9 @@ export function decodeMessageNode(
|
|||||||
const isMe = (jid: string) => areJidsSameUser(jid, meId)
|
const isMe = (jid: string) => areJidsSameUser(jid, meId)
|
||||||
const isMeLid = (jid: string) => areJidsSameUser(jid, meLid)
|
const isMeLid = (jid: string) => areJidsSameUser(jid, meLid)
|
||||||
|
|
||||||
if(isJidUser(from) || isLidUser(from)) {
|
if (isJidUser(from) || isLidUser(from)) {
|
||||||
if(recipient && !isJidMetaIa(recipient)) {
|
if (recipient && !isJidMetaIa(recipient)) {
|
||||||
if(!isMe(from) && !isMeLid(from)) {
|
if (!isMe(from) && !isMeLid(from)) {
|
||||||
throw new Boom('receipient present, but msg not from me', { data: stanza })
|
throw new Boom('receipient present, but msg not from me', { data: stanza })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,21 +73,21 @@ export function decodeMessageNode(
|
|||||||
|
|
||||||
msgType = 'chat'
|
msgType = 'chat'
|
||||||
author = from
|
author = from
|
||||||
} else if(isJidGroup(from)) {
|
} else if (isJidGroup(from)) {
|
||||||
if(!participant) {
|
if (!participant) {
|
||||||
throw new Boom('No participant in group message')
|
throw new Boom('No participant in group message')
|
||||||
}
|
}
|
||||||
|
|
||||||
msgType = 'group'
|
msgType = 'group'
|
||||||
author = participant
|
author = participant
|
||||||
chatId = from
|
chatId = from
|
||||||
} else if(isJidBroadcast(from)) {
|
} else if (isJidBroadcast(from)) {
|
||||||
if(!participant) {
|
if (!participant) {
|
||||||
throw new Boom('No participant in group message')
|
throw new Boom('No participant in group message')
|
||||||
}
|
}
|
||||||
|
|
||||||
const isParticipantMe = isMe(participant)
|
const isParticipantMe = isMe(participant)
|
||||||
if(isJidStatusBroadcast(from)) {
|
if (isJidStatusBroadcast(from)) {
|
||||||
msgType = isParticipantMe ? 'direct_peer_status' : 'other_status'
|
msgType = isParticipantMe ? 'direct_peer_status' : 'other_status'
|
||||||
} else {
|
} else {
|
||||||
msgType = isParticipantMe ? 'peer_broadcast' : 'other_broadcast'
|
msgType = isParticipantMe ? 'peer_broadcast' : 'other_broadcast'
|
||||||
@@ -82,7 +95,7 @@ export function decodeMessageNode(
|
|||||||
|
|
||||||
chatId = from
|
chatId = from
|
||||||
author = participant
|
author = participant
|
||||||
} else if(isJidNewsletter(from)) {
|
} else if (isJidNewsletter(from)) {
|
||||||
msgType = 'newsletter'
|
msgType = 'newsletter'
|
||||||
chatId = from
|
chatId = from
|
||||||
author = from
|
author = from
|
||||||
@@ -107,7 +120,7 @@ export function decodeMessageNode(
|
|||||||
broadcast: isJidBroadcast(from)
|
broadcast: isJidBroadcast(from)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(key.fromMe) {
|
if (key.fromMe) {
|
||||||
fullMessage.status = proto.WebMessageInfo.Status.SERVER_ACK
|
fullMessage.status = proto.WebMessageInfo.Status.SERVER_ACK
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,19 +145,19 @@ export const decryptMessageNode = (
|
|||||||
author,
|
author,
|
||||||
async decrypt() {
|
async decrypt() {
|
||||||
let decryptables = 0
|
let decryptables = 0
|
||||||
if(Array.isArray(stanza.content)) {
|
if (Array.isArray(stanza.content)) {
|
||||||
for(const { tag, attrs, content } of stanza.content) {
|
for (const { tag, attrs, content } of stanza.content) {
|
||||||
if(tag === 'verified_name' && content instanceof Uint8Array) {
|
if (tag === 'verified_name' && content instanceof Uint8Array) {
|
||||||
const cert = proto.VerifiedNameCertificate.decode(content)
|
const cert = proto.VerifiedNameCertificate.decode(content)
|
||||||
const details = proto.VerifiedNameCertificate.Details.decode(cert.details!)
|
const details = proto.VerifiedNameCertificate.Details.decode(cert.details!)
|
||||||
fullMessage.verifiedBizName = details.verifiedName
|
fullMessage.verifiedBizName = details.verifiedName
|
||||||
}
|
}
|
||||||
|
|
||||||
if(tag !== 'enc' && tag !== 'plaintext') {
|
if (tag !== 'enc' && tag !== 'plaintext') {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!(content instanceof Uint8Array)) {
|
if (!(content instanceof Uint8Array)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,53 +168,52 @@ export const decryptMessageNode = (
|
|||||||
try {
|
try {
|
||||||
const e2eType = tag === 'plaintext' ? 'plaintext' : attrs.type
|
const e2eType = tag === 'plaintext' ? 'plaintext' : attrs.type
|
||||||
switch (e2eType) {
|
switch (e2eType) {
|
||||||
case 'skmsg':
|
case 'skmsg':
|
||||||
msgBuffer = await repository.decryptGroupMessage({
|
msgBuffer = await repository.decryptGroupMessage({
|
||||||
group: sender,
|
group: sender,
|
||||||
authorJid: author,
|
authorJid: author,
|
||||||
msg: content
|
msg: content
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'pkmsg':
|
case 'pkmsg':
|
||||||
case 'msg':
|
case 'msg':
|
||||||
const user = isJidUser(sender) ? sender : author
|
const user = isJidUser(sender) ? sender : author
|
||||||
msgBuffer = await repository.decryptMessage({
|
msgBuffer = await repository.decryptMessage({
|
||||||
jid: user,
|
jid: user,
|
||||||
type: e2eType,
|
type: e2eType,
|
||||||
ciphertext: content
|
ciphertext: content
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'plaintext':
|
case 'plaintext':
|
||||||
msgBuffer = content
|
msgBuffer = content
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown e2e type: ${e2eType}`)
|
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
|
msg = msg.deviceSentMessage?.message || msg
|
||||||
if(msg.senderKeyDistributionMessage) {
|
if (msg.senderKeyDistributionMessage) {
|
||||||
//eslint-disable-next-line max-depth
|
//eslint-disable-next-line max-depth
|
||||||
try {
|
try {
|
||||||
await repository.processSenderKeyDistributionMessage({
|
await repository.processSenderKeyDistributionMessage({
|
||||||
authorJid: author,
|
authorJid: author,
|
||||||
item: msg.senderKeyDistributionMessage
|
item: msg.senderKeyDistributionMessage
|
||||||
})
|
})
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
logger.error({ key: fullMessage.key, err }, 'failed to decrypt message')
|
logger.error({ key: fullMessage.key, err }, 'failed to decrypt message')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(fullMessage.message) {
|
if (fullMessage.message) {
|
||||||
Object.assign(fullMessage.message, msg)
|
Object.assign(fullMessage.message, msg)
|
||||||
} else {
|
} else {
|
||||||
fullMessage.message = msg
|
fullMessage.message = msg
|
||||||
}
|
}
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
logger.error(
|
logger.error({ key: fullMessage.key, err }, 'failed to decrypt message')
|
||||||
{ key: fullMessage.key, err },
|
|
||||||
'failed to decrypt message'
|
|
||||||
)
|
|
||||||
fullMessage.messageStubType = proto.WebMessageInfo.StubType.CIPHERTEXT
|
fullMessage.messageStubType = proto.WebMessageInfo.StubType.CIPHERTEXT
|
||||||
fullMessage.messageStubParameters = [err.message]
|
fullMessage.messageStubParameters = [err.message]
|
||||||
}
|
}
|
||||||
@@ -209,7 +221,7 @@ export const decryptMessageNode = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if nothing was found to decrypt
|
// if nothing was found to decrypt
|
||||||
if(!decryptables) {
|
if (!decryptables) {
|
||||||
fullMessage.messageStubType = proto.WebMessageInfo.StubType.CIPHERTEXT
|
fullMessage.messageStubType = proto.WebMessageInfo.StubType.CIPHERTEXT
|
||||||
fullMessage.messageStubParameters = [NO_MESSAGE_FOUND_ERROR_TEXT]
|
fullMessage.messageStubParameters = [NO_MESSAGE_FOUND_ERROR_TEXT]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
import EventEmitter from 'events'
|
import EventEmitter from 'events'
|
||||||
import { proto } from '../../WAProto'
|
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 { trimUndefined } from './generics'
|
||||||
import { ILogger } from './logger'
|
import { ILogger } from './logger'
|
||||||
import { updateMessageWithReaction, updateMessageWithReceipt } from './messages'
|
import { updateMessageWithReaction, updateMessageWithReceipt } from './messages'
|
||||||
@@ -18,10 +28,10 @@ const BUFFERABLE_EVENT = [
|
|||||||
'messages.delete',
|
'messages.delete',
|
||||||
'messages.reaction',
|
'messages.reaction',
|
||||||
'message-receipt.update',
|
'message-receipt.update',
|
||||||
'groups.update',
|
'groups.update'
|
||||||
] as const
|
] 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
|
* 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 & {
|
type BaileysBufferableEventEmitter = BaileysEventEmitter & {
|
||||||
/** Use to process events in a batch */
|
/** 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
|
* starts buffering events, call flush() to release them
|
||||||
* */
|
* */
|
||||||
buffer(): void
|
buffer(): void
|
||||||
/** buffers all events till the promise completes */
|
/** buffers all events till the promise completes */
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// 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
|
* flushes all buffered events
|
||||||
* @param force if true, will flush all data regardless of any pending buffers
|
* @param force if true, will flush all data regardless of any pending buffers
|
||||||
@@ -68,7 +78,7 @@ export const makeEventBuffer = (logger: ILogger): BaileysBufferableEventEmitter
|
|||||||
|
|
||||||
// take the generic event and fire it as a baileys event
|
// take the generic event and fire it as a baileys event
|
||||||
ev.on('event', (map: BaileysEventData) => {
|
ev.on('event', (map: BaileysEventData) => {
|
||||||
for(const event in map) {
|
for (const event in map) {
|
||||||
ev.emit(event, map[event])
|
ev.emit(event, map[event])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -79,16 +89,16 @@ export const makeEventBuffer = (logger: ILogger): BaileysBufferableEventEmitter
|
|||||||
|
|
||||||
function flush(force = false) {
|
function flush(force = false) {
|
||||||
// no buffer going on
|
// no buffer going on
|
||||||
if(!buffersInProgress) {
|
if (!buffersInProgress) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!force) {
|
if (!force) {
|
||||||
// reduce the number of buffers in progress
|
// reduce the number of buffers in progress
|
||||||
buffersInProgress -= 1
|
buffersInProgress -= 1
|
||||||
// if there are still some buffers going on
|
// if there are still some buffers going on
|
||||||
// then we don't flush now
|
// then we don't flush now
|
||||||
if(buffersInProgress) {
|
if (buffersInProgress) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,8 +107,8 @@ export const makeEventBuffer = (logger: ILogger): BaileysBufferableEventEmitter
|
|||||||
const chatUpdates = Object.values(data.chatUpdates)
|
const chatUpdates = Object.values(data.chatUpdates)
|
||||||
// gather the remaining conditional events so we re-queue them
|
// gather the remaining conditional events so we re-queue them
|
||||||
let conditionalChatUpdatesLeft = 0
|
let conditionalChatUpdatesLeft = 0
|
||||||
for(const update of chatUpdates) {
|
for (const update of chatUpdates) {
|
||||||
if(update.conditional) {
|
if (update.conditional) {
|
||||||
conditionalChatUpdatesLeft += 1
|
conditionalChatUpdatesLeft += 1
|
||||||
newData.chatUpdates[update.id!] = update
|
newData.chatUpdates[update.id!] = update
|
||||||
delete data.chatUpdates[update.id!]
|
delete data.chatUpdates[update.id!]
|
||||||
@@ -106,16 +116,13 @@ export const makeEventBuffer = (logger: ILogger): BaileysBufferableEventEmitter
|
|||||||
}
|
}
|
||||||
|
|
||||||
const consolidatedData = consolidateEvents(data)
|
const consolidatedData = consolidateEvents(data)
|
||||||
if(Object.keys(consolidatedData).length) {
|
if (Object.keys(consolidatedData).length) {
|
||||||
ev.emit('event', consolidatedData)
|
ev.emit('event', consolidatedData)
|
||||||
}
|
}
|
||||||
|
|
||||||
data = newData
|
data = newData
|
||||||
|
|
||||||
logger.trace(
|
logger.trace({ conditionalChatUpdatesLeft }, 'released buffered events')
|
||||||
{ conditionalChatUpdatesLeft },
|
|
||||||
'released buffered events'
|
|
||||||
)
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -132,7 +139,7 @@ export const makeEventBuffer = (logger: ILogger): BaileysBufferableEventEmitter
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emit<T extends BaileysEvent>(event: BaileysEvent, evData: BaileysEventMap[T]) {
|
emit<T extends BaileysEvent>(event: BaileysEvent, evData: BaileysEventMap[T]) {
|
||||||
if(buffersInProgress && BUFFERABLE_EVENT_SET.has(event)) {
|
if (buffersInProgress && BUFFERABLE_EVENT_SET.has(event)) {
|
||||||
append(data, historyCache, event as BufferableEvent, evData, logger)
|
append(data, historyCache, event as BufferableEvent, evData, logger)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -145,7 +152,7 @@ export const makeEventBuffer = (logger: ILogger): BaileysBufferableEventEmitter
|
|||||||
buffer,
|
buffer,
|
||||||
flush,
|
flush,
|
||||||
createBufferedFunction(work) {
|
createBufferedFunction(work) {
|
||||||
return async(...args) => {
|
return async (...args) => {
|
||||||
buffer()
|
buffer()
|
||||||
try {
|
try {
|
||||||
const result = await work(...args)
|
const result = await work(...args)
|
||||||
@@ -157,30 +164,30 @@ export const makeEventBuffer = (logger: ILogger): BaileysBufferableEventEmitter
|
|||||||
},
|
},
|
||||||
on: (...args) => ev.on(...args),
|
on: (...args) => ev.on(...args),
|
||||||
off: (...args) => ev.off(...args),
|
off: (...args) => ev.off(...args),
|
||||||
removeAllListeners: (...args) => ev.removeAllListeners(...args),
|
removeAllListeners: (...args) => ev.removeAllListeners(...args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeBufferData = (): BufferedEventData => {
|
const makeBufferData = (): BufferedEventData => {
|
||||||
return {
|
return {
|
||||||
historySets: {
|
historySets: {
|
||||||
chats: { },
|
chats: {},
|
||||||
messages: { },
|
messages: {},
|
||||||
contacts: { },
|
contacts: {},
|
||||||
isLatest: false,
|
isLatest: false,
|
||||||
empty: true
|
empty: true
|
||||||
},
|
},
|
||||||
chatUpserts: { },
|
chatUpserts: {},
|
||||||
chatUpdates: { },
|
chatUpdates: {},
|
||||||
chatDeletes: new Set(),
|
chatDeletes: new Set(),
|
||||||
contactUpserts: { },
|
contactUpserts: {},
|
||||||
contactUpdates: { },
|
contactUpdates: {},
|
||||||
messageUpserts: { },
|
messageUpserts: {},
|
||||||
messageUpdates: { },
|
messageUpdates: {},
|
||||||
messageReactions: { },
|
messageReactions: {},
|
||||||
messageDeletes: { },
|
messageDeletes: {},
|
||||||
messageReceipts: { },
|
messageReceipts: {},
|
||||||
groupUpdates: { }
|
groupUpdates: {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,305 +200,298 @@ function append<E extends BufferableEvent>(
|
|||||||
logger: ILogger
|
logger: ILogger
|
||||||
) {
|
) {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case 'messaging-history.set':
|
case 'messaging-history.set':
|
||||||
for(const chat of eventData.chats as Chat[]) {
|
for (const chat of eventData.chats as Chat[]) {
|
||||||
const existingChat = data.historySets.chats[chat.id]
|
const existingChat = data.historySets.chats[chat.id]
|
||||||
if(existingChat) {
|
if (existingChat) {
|
||||||
existingChat.endOfHistoryTransferType = chat.endOfHistoryTransferType
|
existingChat.endOfHistoryTransferType = chat.endOfHistoryTransferType
|
||||||
}
|
|
||||||
|
|
||||||
if(!existingChat && !historyCache.has(chat.id)) {
|
|
||||||
data.historySets.chats[chat.id] = chat
|
|
||||||
historyCache.add(chat.id)
|
|
||||||
|
|
||||||
absorbingChatUpdate(chat)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for(const contact of eventData.contacts as Contact[]) {
|
|
||||||
const existingContact = data.historySets.contacts[contact.id]
|
|
||||||
if(existingContact) {
|
|
||||||
Object.assign(existingContact, trimUndefined(contact))
|
|
||||||
} else {
|
|
||||||
const historyContactId = `c:${contact.id}`
|
|
||||||
const hasAnyName = contact.notify || contact.name || contact.verifiedName
|
|
||||||
if(!historyCache.has(historyContactId) || hasAnyName) {
|
|
||||||
data.historySets.contacts[contact.id] = contact
|
|
||||||
historyCache.add(historyContactId)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for(const message of eventData.messages as WAMessage[]) {
|
if (!existingChat && !historyCache.has(chat.id)) {
|
||||||
const key = stringifyMessageKey(message.key)
|
data.historySets.chats[chat.id] = chat
|
||||||
const existingMsg = data.historySets.messages[key]
|
historyCache.add(chat.id)
|
||||||
if(!existingMsg && !historyCache.has(key)) {
|
|
||||||
data.historySets.messages[key] = message
|
|
||||||
historyCache.add(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data.historySets.empty = false
|
absorbingChatUpdate(chat)
|
||||||
data.historySets.syncType = eventData.syncType
|
|
||||||
data.historySets.progress = eventData.progress
|
|
||||||
data.historySets.peerDataRequestSessionId = eventData.peerDataRequestSessionId
|
|
||||||
data.historySets.isLatest = eventData.isLatest || data.historySets.isLatest
|
|
||||||
|
|
||||||
break
|
|
||||||
case 'chats.upsert':
|
|
||||||
for(const chat of eventData as Chat[]) {
|
|
||||||
let upsert = data.chatUpserts[chat.id]
|
|
||||||
if(!upsert) {
|
|
||||||
upsert = data.historySets[chat.id]
|
|
||||||
if(upsert) {
|
|
||||||
logger.debug({ chatId: chat.id }, 'absorbed chat upsert in chat set')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(upsert) {
|
for (const contact of eventData.contacts as Contact[]) {
|
||||||
upsert = concatChats(upsert, chat)
|
const existingContact = data.historySets.contacts[contact.id]
|
||||||
} else {
|
if (existingContact) {
|
||||||
upsert = chat
|
Object.assign(existingContact, trimUndefined(contact))
|
||||||
data.chatUpserts[chat.id] = upsert
|
|
||||||
}
|
|
||||||
|
|
||||||
absorbingChatUpdate(upsert)
|
|
||||||
|
|
||||||
if(data.chatDeletes.has(chat.id)) {
|
|
||||||
data.chatDeletes.delete(chat.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
case 'chats.update':
|
|
||||||
for(const update of eventData as ChatUpdate[]) {
|
|
||||||
const chatId = update.id!
|
|
||||||
const conditionMatches = update.conditional ? update.conditional(data) : true
|
|
||||||
if(conditionMatches) {
|
|
||||||
delete update.conditional
|
|
||||||
|
|
||||||
// if there is an existing upsert, merge the update into it
|
|
||||||
const upsert = data.historySets.chats[chatId] || data.chatUpserts[chatId]
|
|
||||||
if(upsert) {
|
|
||||||
concatChats(upsert, update)
|
|
||||||
} else {
|
} else {
|
||||||
// merge the update into the existing update
|
const historyContactId = `c:${contact.id}`
|
||||||
const chatUpdate = data.chatUpdates[chatId] || { }
|
const hasAnyName = contact.notify || contact.name || contact.verifiedName
|
||||||
data.chatUpdates[chatId] = concatChats(chatUpdate, update)
|
if (!historyCache.has(historyContactId) || hasAnyName) {
|
||||||
}
|
data.historySets.contacts[contact.id] = contact
|
||||||
} else if(conditionMatches === undefined) {
|
historyCache.add(historyContactId)
|
||||||
// condition yet to be fulfilled
|
}
|
||||||
data.chatUpdates[chatId] = update
|
|
||||||
}
|
|
||||||
// otherwise -- condition not met, update is invalid
|
|
||||||
|
|
||||||
// if the chat has been updated
|
|
||||||
// ignore any existing chat delete
|
|
||||||
if(data.chatDeletes.has(chatId)) {
|
|
||||||
data.chatDeletes.delete(chatId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
case 'chats.delete':
|
|
||||||
for(const chatId of eventData as string[]) {
|
|
||||||
if(!data.chatDeletes.has(chatId)) {
|
|
||||||
data.chatDeletes.add(chatId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove any prior updates & upserts
|
|
||||||
if(data.chatUpdates[chatId]) {
|
|
||||||
delete data.chatUpdates[chatId]
|
|
||||||
}
|
|
||||||
|
|
||||||
if(data.chatUpserts[chatId]) {
|
|
||||||
delete data.chatUpserts[chatId]
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if(data.historySets.chats[chatId]) {
|
|
||||||
delete data.historySets.chats[chatId]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
case 'contacts.upsert':
|
|
||||||
for(const contact of eventData as Contact[]) {
|
|
||||||
let upsert = data.contactUpserts[contact.id]
|
|
||||||
if(!upsert) {
|
|
||||||
upsert = data.historySets.contacts[contact.id]
|
|
||||||
if(upsert) {
|
|
||||||
logger.debug({ contactId: contact.id }, 'absorbed contact upsert in contact set')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(upsert) {
|
for (const message of eventData.messages as WAMessage[]) {
|
||||||
upsert = Object.assign(upsert, trimUndefined(contact))
|
const key = stringifyMessageKey(message.key)
|
||||||
} else {
|
const existingMsg = data.historySets.messages[key]
|
||||||
upsert = contact
|
if (!existingMsg && !historyCache.has(key)) {
|
||||||
data.contactUpserts[contact.id] = upsert
|
data.historySets.messages[key] = message
|
||||||
}
|
historyCache.add(key)
|
||||||
|
|
||||||
if(data.contactUpdates[contact.id]) {
|
|
||||||
upsert = Object.assign(data.contactUpdates[contact.id], trimUndefined(contact)) as Contact
|
|
||||||
delete data.contactUpdates[contact.id]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
case 'contacts.update':
|
|
||||||
const contactUpdates = eventData as BaileysEventMap['contacts.update']
|
|
||||||
for(const update of contactUpdates) {
|
|
||||||
const id = update.id!
|
|
||||||
// merge into prior upsert
|
|
||||||
const upsert = data.historySets.contacts[id] || data.contactUpserts[id]
|
|
||||||
if(upsert) {
|
|
||||||
Object.assign(upsert, update)
|
|
||||||
} else {
|
|
||||||
// merge into prior update
|
|
||||||
const contactUpdate = data.contactUpdates[id] || { }
|
|
||||||
data.contactUpdates[id] = Object.assign(contactUpdate, update)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
case 'messages.upsert':
|
|
||||||
const { messages, type } = eventData as BaileysEventMap['messages.upsert']
|
|
||||||
for(const message of messages) {
|
|
||||||
const key = stringifyMessageKey(message.key)
|
|
||||||
let existing = data.messageUpserts[key]?.message
|
|
||||||
if(!existing) {
|
|
||||||
existing = data.historySets.messages[key]
|
|
||||||
if(existing) {
|
|
||||||
logger.debug({ messageId: key }, 'absorbed message upsert in message set')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(existing) {
|
data.historySets.empty = false
|
||||||
message.messageTimestamp = existing.messageTimestamp
|
data.historySets.syncType = eventData.syncType
|
||||||
}
|
data.historySets.progress = eventData.progress
|
||||||
|
data.historySets.peerDataRequestSessionId = eventData.peerDataRequestSessionId
|
||||||
|
data.historySets.isLatest = eventData.isLatest || data.historySets.isLatest
|
||||||
|
|
||||||
if(data.messageUpdates[key]) {
|
break
|
||||||
logger.debug('absorbed prior message update in message upsert')
|
case 'chats.upsert':
|
||||||
Object.assign(message, data.messageUpdates[key].update)
|
for (const chat of eventData as Chat[]) {
|
||||||
delete data.messageUpdates[key]
|
let upsert = data.chatUpserts[chat.id]
|
||||||
}
|
if (!upsert) {
|
||||||
|
upsert = data.historySets[chat.id]
|
||||||
|
if (upsert) {
|
||||||
|
logger.debug({ chatId: chat.id }, 'absorbed chat upsert in chat set')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if(data.historySets.messages[key]) {
|
if (upsert) {
|
||||||
data.historySets.messages[key] = message
|
upsert = concatChats(upsert, chat)
|
||||||
} else {
|
} else {
|
||||||
data.messageUpserts[key] = {
|
upsert = chat
|
||||||
message,
|
data.chatUpserts[chat.id] = upsert
|
||||||
type: type === 'notify' || data.messageUpserts[key]?.type === 'notify'
|
}
|
||||||
? 'notify'
|
|
||||||
: type
|
absorbingChatUpdate(upsert)
|
||||||
|
|
||||||
|
if (data.chatDeletes.has(chat.id)) {
|
||||||
|
data.chatDeletes.delete(chat.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
break
|
break
|
||||||
case 'messages.update':
|
case 'chats.update':
|
||||||
const msgUpdates = eventData as BaileysEventMap['messages.update']
|
for (const update of eventData as ChatUpdate[]) {
|
||||||
for(const { key, update } of msgUpdates) {
|
const chatId = update.id!
|
||||||
const keyStr = stringifyMessageKey(key)
|
const conditionMatches = update.conditional ? update.conditional(data) : true
|
||||||
const existing = data.historySets.messages[keyStr] || data.messageUpserts[keyStr]?.message
|
if (conditionMatches) {
|
||||||
if(existing) {
|
delete update.conditional
|
||||||
Object.assign(existing, update)
|
|
||||||
// if the message was received & read by us
|
// if there is an existing upsert, merge the update into it
|
||||||
// the chat counter must have been incremented
|
const upsert = data.historySets.chats[chatId] || data.chatUpserts[chatId]
|
||||||
// so we need to decrement it
|
if (upsert) {
|
||||||
if(update.status === WAMessageStatus.READ && !key.fromMe) {
|
concatChats(upsert, update)
|
||||||
decrementChatReadCounterIfMsgDidUnread(existing)
|
} else {
|
||||||
|
// merge the update into the existing update
|
||||||
|
const chatUpdate = data.chatUpdates[chatId] || {}
|
||||||
|
data.chatUpdates[chatId] = concatChats(chatUpdate, update)
|
||||||
|
}
|
||||||
|
} else if (conditionMatches === undefined) {
|
||||||
|
// condition yet to be fulfilled
|
||||||
|
data.chatUpdates[chatId] = update
|
||||||
}
|
}
|
||||||
} else {
|
// otherwise -- condition not met, update is invalid
|
||||||
const msgUpdate = data.messageUpdates[keyStr] || { key, update: { } }
|
|
||||||
Object.assign(msgUpdate.update, update)
|
|
||||||
data.messageUpdates[keyStr] = msgUpdate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
// if the chat has been updated
|
||||||
case 'messages.delete':
|
// ignore any existing chat delete
|
||||||
const deleteData = eventData as BaileysEventMap['messages.delete']
|
if (data.chatDeletes.has(chatId)) {
|
||||||
if('keys' in deleteData) {
|
data.chatDeletes.delete(chatId)
|
||||||
const { keys } = deleteData
|
}
|
||||||
for(const key of keys) {
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
case 'chats.delete':
|
||||||
|
for (const chatId of eventData as string[]) {
|
||||||
|
if (!data.chatDeletes.has(chatId)) {
|
||||||
|
data.chatDeletes.add(chatId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove any prior updates & upserts
|
||||||
|
if (data.chatUpdates[chatId]) {
|
||||||
|
delete data.chatUpdates[chatId]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.chatUpserts[chatId]) {
|
||||||
|
delete data.chatUpserts[chatId]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.historySets.chats[chatId]) {
|
||||||
|
delete data.historySets.chats[chatId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
case 'contacts.upsert':
|
||||||
|
for (const contact of eventData as Contact[]) {
|
||||||
|
let upsert = data.contactUpserts[contact.id]
|
||||||
|
if (!upsert) {
|
||||||
|
upsert = data.historySets.contacts[contact.id]
|
||||||
|
if (upsert) {
|
||||||
|
logger.debug({ contactId: contact.id }, 'absorbed contact upsert in contact set')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upsert) {
|
||||||
|
upsert = Object.assign(upsert, trimUndefined(contact))
|
||||||
|
} else {
|
||||||
|
upsert = contact
|
||||||
|
data.contactUpserts[contact.id] = upsert
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.contactUpdates[contact.id]) {
|
||||||
|
upsert = Object.assign(data.contactUpdates[contact.id], trimUndefined(contact)) as Contact
|
||||||
|
delete data.contactUpdates[contact.id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
case 'contacts.update':
|
||||||
|
const contactUpdates = eventData as BaileysEventMap['contacts.update']
|
||||||
|
for (const update of contactUpdates) {
|
||||||
|
const id = update.id!
|
||||||
|
// merge into prior upsert
|
||||||
|
const upsert = data.historySets.contacts[id] || data.contactUpserts[id]
|
||||||
|
if (upsert) {
|
||||||
|
Object.assign(upsert, update)
|
||||||
|
} else {
|
||||||
|
// merge into prior update
|
||||||
|
const contactUpdate = data.contactUpdates[id] || {}
|
||||||
|
data.contactUpdates[id] = Object.assign(contactUpdate, update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
case 'messages.upsert':
|
||||||
|
const { messages, type } = eventData as BaileysEventMap['messages.upsert']
|
||||||
|
for (const message of messages) {
|
||||||
|
const key = stringifyMessageKey(message.key)
|
||||||
|
let existing = data.messageUpserts[key]?.message
|
||||||
|
if (!existing) {
|
||||||
|
existing = data.historySets.messages[key]
|
||||||
|
if (existing) {
|
||||||
|
logger.debug({ messageId: key }, 'absorbed message upsert in message set')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
message.messageTimestamp = existing.messageTimestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.messageUpdates[key]) {
|
||||||
|
logger.debug('absorbed prior message update in message upsert')
|
||||||
|
Object.assign(message, data.messageUpdates[key].update)
|
||||||
|
delete data.messageUpdates[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.historySets.messages[key]) {
|
||||||
|
data.historySets.messages[key] = message
|
||||||
|
} else {
|
||||||
|
data.messageUpserts[key] = {
|
||||||
|
message,
|
||||||
|
type: type === 'notify' || data.messageUpserts[key]?.type === 'notify' ? 'notify' : type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
case 'messages.update':
|
||||||
|
const msgUpdates = eventData as BaileysEventMap['messages.update']
|
||||||
|
for (const { key, update } of msgUpdates) {
|
||||||
const keyStr = stringifyMessageKey(key)
|
const keyStr = stringifyMessageKey(key)
|
||||||
if(!data.messageDeletes[keyStr]) {
|
const existing = data.historySets.messages[keyStr] || data.messageUpserts[keyStr]?.message
|
||||||
data.messageDeletes[keyStr] = key
|
if (existing) {
|
||||||
|
Object.assign(existing, update)
|
||||||
}
|
// if the message was received & read by us
|
||||||
|
// the chat counter must have been incremented
|
||||||
if(data.messageUpserts[keyStr]) {
|
// so we need to decrement it
|
||||||
delete data.messageUpserts[keyStr]
|
if (update.status === WAMessageStatus.READ && !key.fromMe) {
|
||||||
}
|
decrementChatReadCounterIfMsgDidUnread(existing)
|
||||||
|
}
|
||||||
if(data.messageUpdates[keyStr]) {
|
} else {
|
||||||
delete data.messageUpdates[keyStr]
|
const msgUpdate = data.messageUpdates[keyStr] || { key, update: {} }
|
||||||
|
Object.assign(msgUpdate.update, update)
|
||||||
|
data.messageUpdates[keyStr] = msgUpdate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// TODO: add support
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
break
|
||||||
case 'messages.reaction':
|
case 'messages.delete':
|
||||||
const reactions = eventData as BaileysEventMap['messages.reaction']
|
const deleteData = eventData as BaileysEventMap['messages.delete']
|
||||||
for(const { key, reaction } of reactions) {
|
if ('keys' in deleteData) {
|
||||||
const keyStr = stringifyMessageKey(key)
|
const { keys } = deleteData
|
||||||
const existing = data.messageUpserts[keyStr]
|
for (const key of keys) {
|
||||||
if(existing) {
|
const keyStr = stringifyMessageKey(key)
|
||||||
updateMessageWithReaction(existing.message, reaction)
|
if (!data.messageDeletes[keyStr]) {
|
||||||
|
data.messageDeletes[keyStr] = key
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.messageUpserts[keyStr]) {
|
||||||
|
delete data.messageUpserts[keyStr]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.messageUpdates[keyStr]) {
|
||||||
|
delete data.messageUpdates[keyStr]
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
data.messageReactions[keyStr] = data.messageReactions[keyStr]
|
// TODO: add support
|
||||||
|| { key, reactions: [] }
|
|
||||||
updateMessageWithReaction(data.messageReactions[keyStr], reaction)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
break
|
break
|
||||||
case 'message-receipt.update':
|
case 'messages.reaction':
|
||||||
const receipts = eventData as BaileysEventMap['message-receipt.update']
|
const reactions = eventData as BaileysEventMap['messages.reaction']
|
||||||
for(const { key, receipt } of receipts) {
|
for (const { key, reaction } of reactions) {
|
||||||
const keyStr = stringifyMessageKey(key)
|
const keyStr = stringifyMessageKey(key)
|
||||||
const existing = data.messageUpserts[keyStr]
|
const existing = data.messageUpserts[keyStr]
|
||||||
if(existing) {
|
if (existing) {
|
||||||
updateMessageWithReceipt(existing.message, receipt)
|
updateMessageWithReaction(existing.message, reaction)
|
||||||
} else {
|
} else {
|
||||||
data.messageReceipts[keyStr] = data.messageReceipts[keyStr]
|
data.messageReactions[keyStr] = data.messageReactions[keyStr] || { key, reactions: [] }
|
||||||
|| { key, userReceipt: [] }
|
updateMessageWithReaction(data.messageReactions[keyStr], reaction)
|
||||||
updateMessageWithReceipt(data.messageReceipts[keyStr], receipt)
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
case 'groups.update':
|
|
||||||
const groupUpdates = eventData as BaileysEventMap['groups.update']
|
|
||||||
for(const update of groupUpdates) {
|
|
||||||
const id = update.id!
|
|
||||||
const groupUpdate = data.groupUpdates[id] || { }
|
|
||||||
if(!data.groupUpdates[id]) {
|
|
||||||
data.groupUpdates[id] = Object.assign(groupUpdate, update)
|
|
||||||
|
|
||||||
|
break
|
||||||
|
case 'message-receipt.update':
|
||||||
|
const receipts = eventData as BaileysEventMap['message-receipt.update']
|
||||||
|
for (const { key, receipt } of receipts) {
|
||||||
|
const keyStr = stringifyMessageKey(key)
|
||||||
|
const existing = data.messageUpserts[keyStr]
|
||||||
|
if (existing) {
|
||||||
|
updateMessageWithReceipt(existing.message, receipt)
|
||||||
|
} else {
|
||||||
|
data.messageReceipts[keyStr] = data.messageReceipts[keyStr] || { key, userReceipt: [] }
|
||||||
|
updateMessageWithReceipt(data.messageReceipts[keyStr], receipt)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
break
|
break
|
||||||
default:
|
case 'groups.update':
|
||||||
throw new Error(`"${event}" cannot be buffered`)
|
const groupUpdates = eventData as BaileysEventMap['groups.update']
|
||||||
|
for (const update of groupUpdates) {
|
||||||
|
const id = update.id!
|
||||||
|
const groupUpdate = data.groupUpdates[id] || {}
|
||||||
|
if (!data.groupUpdates[id]) {
|
||||||
|
data.groupUpdates[id] = Object.assign(groupUpdate, update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error(`"${event}" cannot be buffered`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function absorbingChatUpdate(existing: Chat) {
|
function absorbingChatUpdate(existing: Chat) {
|
||||||
const chatId = existing.id
|
const chatId = existing.id
|
||||||
const update = data.chatUpdates[chatId]
|
const update = data.chatUpdates[chatId]
|
||||||
if(update) {
|
if (update) {
|
||||||
const conditionMatches = update.conditional ? update.conditional(data) : true
|
const conditionMatches = update.conditional ? update.conditional(data) : true
|
||||||
if(conditionMatches) {
|
if (conditionMatches) {
|
||||||
delete update.conditional
|
delete update.conditional
|
||||||
logger.debug({ chatId }, 'absorbed chat update in existing chat')
|
logger.debug({ chatId }, 'absorbed chat update in existing chat')
|
||||||
Object.assign(existing, concatChats(update as Chat, existing))
|
Object.assign(existing, concatChats(update as Chat, existing))
|
||||||
delete data.chatUpdates[chatId]
|
delete data.chatUpdates[chatId]
|
||||||
} else if(conditionMatches === false) {
|
} else if (conditionMatches === false) {
|
||||||
logger.debug({ chatId }, 'chat update condition fail, removing')
|
logger.debug({ chatId }, 'chat update condition fail, removing')
|
||||||
delete data.chatUpdates[chatId]
|
delete data.chatUpdates[chatId]
|
||||||
}
|
}
|
||||||
@@ -503,15 +503,15 @@ function append<E extends BufferableEvent>(
|
|||||||
// if the message has already been marked read by us
|
// if the message has already been marked read by us
|
||||||
const chatId = message.key.remoteJid!
|
const chatId = message.key.remoteJid!
|
||||||
const chat = data.chatUpdates[chatId] || data.chatUpserts[chatId]
|
const chat = data.chatUpdates[chatId] || data.chatUpserts[chatId]
|
||||||
if(
|
if (
|
||||||
isRealMessage(message, '')
|
isRealMessage(message, '') &&
|
||||||
&& shouldIncrementChatUnread(message)
|
shouldIncrementChatUnread(message) &&
|
||||||
&& typeof chat?.unreadCount === 'number'
|
typeof chat?.unreadCount === 'number' &&
|
||||||
&& chat.unreadCount > 0
|
chat.unreadCount > 0
|
||||||
) {
|
) {
|
||||||
logger.debug({ chatId: chat.id }, 'decrementing chat counter')
|
logger.debug({ chatId: chat.id }, 'decrementing chat counter')
|
||||||
chat.unreadCount -= 1
|
chat.unreadCount -= 1
|
||||||
if(chat.unreadCount === 0) {
|
if (chat.unreadCount === 0) {
|
||||||
delete chat.unreadCount
|
delete chat.unreadCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -519,9 +519,9 @@ function append<E extends BufferableEvent>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function consolidateEvents(data: BufferedEventData) {
|
function consolidateEvents(data: BufferedEventData) {
|
||||||
const map: BaileysEventData = { }
|
const map: BaileysEventData = {}
|
||||||
|
|
||||||
if(!data.historySets.empty) {
|
if (!data.historySets.empty) {
|
||||||
map['messaging-history.set'] = {
|
map['messaging-history.set'] = {
|
||||||
chats: Object.values(data.historySets.chats),
|
chats: Object.values(data.historySets.chats),
|
||||||
messages: Object.values(data.historySets.messages),
|
messages: Object.values(data.historySets.messages),
|
||||||
@@ -534,22 +534,22 @@ function consolidateEvents(data: BufferedEventData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const chatUpsertList = Object.values(data.chatUpserts)
|
const chatUpsertList = Object.values(data.chatUpserts)
|
||||||
if(chatUpsertList.length) {
|
if (chatUpsertList.length) {
|
||||||
map['chats.upsert'] = chatUpsertList
|
map['chats.upsert'] = chatUpsertList
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatUpdateList = Object.values(data.chatUpdates)
|
const chatUpdateList = Object.values(data.chatUpdates)
|
||||||
if(chatUpdateList.length) {
|
if (chatUpdateList.length) {
|
||||||
map['chats.update'] = chatUpdateList
|
map['chats.update'] = chatUpdateList
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatDeleteList = Array.from(data.chatDeletes)
|
const chatDeleteList = Array.from(data.chatDeletes)
|
||||||
if(chatDeleteList.length) {
|
if (chatDeleteList.length) {
|
||||||
map['chats.delete'] = chatDeleteList
|
map['chats.delete'] = chatDeleteList
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageUpsertList = Object.values(data.messageUpserts)
|
const messageUpsertList = Object.values(data.messageUpserts)
|
||||||
if(messageUpsertList.length) {
|
if (messageUpsertList.length) {
|
||||||
const type = messageUpsertList[0].type
|
const type = messageUpsertList[0].type
|
||||||
map['messages.upsert'] = {
|
map['messages.upsert'] = {
|
||||||
messages: messageUpsertList.map(m => m.message),
|
messages: messageUpsertList.map(m => m.message),
|
||||||
@@ -558,41 +558,41 @@ function consolidateEvents(data: BufferedEventData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const messageUpdateList = Object.values(data.messageUpdates)
|
const messageUpdateList = Object.values(data.messageUpdates)
|
||||||
if(messageUpdateList.length) {
|
if (messageUpdateList.length) {
|
||||||
map['messages.update'] = messageUpdateList
|
map['messages.update'] = messageUpdateList
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageDeleteList = Object.values(data.messageDeletes)
|
const messageDeleteList = Object.values(data.messageDeletes)
|
||||||
if(messageDeleteList.length) {
|
if (messageDeleteList.length) {
|
||||||
map['messages.delete'] = { keys: messageDeleteList }
|
map['messages.delete'] = { keys: messageDeleteList }
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageReactionList = Object.values(data.messageReactions).flatMap(
|
const messageReactionList = Object.values(data.messageReactions).flatMap(({ key, reactions }) =>
|
||||||
({ key, reactions }) => reactions.flatMap(reaction => ({ key, reaction }))
|
reactions.flatMap(reaction => ({ key, reaction }))
|
||||||
)
|
)
|
||||||
if(messageReactionList.length) {
|
if (messageReactionList.length) {
|
||||||
map['messages.reaction'] = messageReactionList
|
map['messages.reaction'] = messageReactionList
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageReceiptList = Object.values(data.messageReceipts).flatMap(
|
const messageReceiptList = Object.values(data.messageReceipts).flatMap(({ key, userReceipt }) =>
|
||||||
({ key, userReceipt }) => userReceipt.flatMap(receipt => ({ key, receipt }))
|
userReceipt.flatMap(receipt => ({ key, receipt }))
|
||||||
)
|
)
|
||||||
if(messageReceiptList.length) {
|
if (messageReceiptList.length) {
|
||||||
map['message-receipt.update'] = messageReceiptList
|
map['message-receipt.update'] = messageReceiptList
|
||||||
}
|
}
|
||||||
|
|
||||||
const contactUpsertList = Object.values(data.contactUpserts)
|
const contactUpsertList = Object.values(data.contactUpserts)
|
||||||
if(contactUpsertList.length) {
|
if (contactUpsertList.length) {
|
||||||
map['contacts.upsert'] = contactUpsertList
|
map['contacts.upsert'] = contactUpsertList
|
||||||
}
|
}
|
||||||
|
|
||||||
const contactUpdateList = Object.values(data.contactUpdates)
|
const contactUpdateList = Object.values(data.contactUpdates)
|
||||||
if(contactUpdateList.length) {
|
if (contactUpdateList.length) {
|
||||||
map['contacts.update'] = contactUpdateList
|
map['contacts.update'] = contactUpdateList
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupUpdateList = Object.values(data.groupUpdates)
|
const groupUpdateList = Object.values(data.groupUpdates)
|
||||||
if(groupUpdateList.length) {
|
if (groupUpdateList.length) {
|
||||||
map['groups.update'] = groupUpdateList
|
map['groups.update'] = groupUpdateList
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,15 +600,17 @@ function consolidateEvents(data: BufferedEventData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function concatChats<C extends Partial<Chat>>(a: C, b: Partial<Chat>) {
|
function concatChats<C extends Partial<Chat>>(a: C, b: Partial<Chat>) {
|
||||||
if(b.unreadCount === null && // neutralize unread counter
|
if (
|
||||||
a.unreadCount! < 0) {
|
b.unreadCount === null && // neutralize unread counter
|
||||||
|
a.unreadCount! < 0
|
||||||
|
) {
|
||||||
a.unreadCount = undefined
|
a.unreadCount = undefined
|
||||||
b.unreadCount = undefined
|
b.unreadCount = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
if(typeof a.unreadCount === 'number' && typeof b.unreadCount === 'number') {
|
if (typeof a.unreadCount === 'number' && typeof b.unreadCount === 'number') {
|
||||||
b = { ...b }
|
b = { ...b }
|
||||||
if(b.unreadCount! >= 0) {
|
if (b.unreadCount! >= 0) {
|
||||||
b.unreadCount = Math.max(b.unreadCount!, 0) + Math.max(a.unreadCount, 0)
|
b.unreadCount = Math.max(b.unreadCount!, 0) + Math.max(a.unreadCount, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -616,4 +618,4 @@ function concatChats<C extends Partial<Chat>>(a: C, b: Partial<Chat>) {
|
|||||||
return Object.assign(a, b)
|
return Object.assign(a, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
const stringifyMessageKey = (key: proto.IMessageKey) => `${key.remoteJid},${key.id},${key.fromMe ? '1' : '0'}`
|
const stringifyMessageKey = (key: proto.IMessageKey) => `${key.remoteJid},${key.id},${key.fromMe ? '1' : '0'}`
|
||||||
|
|||||||
@@ -4,26 +4,34 @@ import { createHash, randomBytes } from 'crypto'
|
|||||||
import { platform, release } from 'os'
|
import { platform, release } from 'os'
|
||||||
import { proto } from '../../WAProto'
|
import { proto } from '../../WAProto'
|
||||||
import { version as baileysVersion } from '../Defaults/baileys-version.json'
|
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'
|
import { BinaryNode, getAllBinaryNodeChildren, jidDecode } from '../WABinary'
|
||||||
|
|
||||||
const PLATFORM_MAP = {
|
const PLATFORM_MAP = {
|
||||||
'aix': 'AIX',
|
aix: 'AIX',
|
||||||
'darwin': 'Mac OS',
|
darwin: 'Mac OS',
|
||||||
'win32': 'Windows',
|
win32: 'Windows',
|
||||||
'android': 'Android',
|
android: 'Android',
|
||||||
'freebsd': 'FreeBSD',
|
freebsd: 'FreeBSD',
|
||||||
'openbsd': 'OpenBSD',
|
openbsd: 'OpenBSD',
|
||||||
'sunos': 'Solaris'
|
sunos: 'Solaris'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Browsers: BrowsersMap = {
|
export const Browsers: BrowsersMap = {
|
||||||
ubuntu: (browser) => ['Ubuntu', browser, '22.04.4'],
|
ubuntu: browser => ['Ubuntu', browser, '22.04.4'],
|
||||||
macOS: (browser) => ['Mac OS', browser, '14.4.1'],
|
macOS: browser => ['Mac OS', browser, '14.4.1'],
|
||||||
baileys: (browser) => ['Baileys', browser, '6.5.0'],
|
baileys: browser => ['Baileys', browser, '6.5.0'],
|
||||||
windows: (browser) => ['Windows', browser, '10.0.22631'],
|
windows: browser => ['Windows', browser, '10.0.22631'],
|
||||||
/** The appropriate browser based on your OS & release */
|
/** 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) => {
|
export const getPlatformId = (browser: string) => {
|
||||||
@@ -34,7 +42,7 @@ export const getPlatformId = (browser: string) => {
|
|||||||
export const BufferJSON = {
|
export const BufferJSON = {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
replacer: (k, value: any) => {
|
replacer: (k, value: any) => {
|
||||||
if(Buffer.isBuffer(value) || value instanceof Uint8Array || value?.type === 'Buffer') {
|
if (Buffer.isBuffer(value) || value instanceof Uint8Array || value?.type === 'Buffer') {
|
||||||
return { type: 'Buffer', data: Buffer.from(value?.data || value).toString('base64') }
|
return { type: 'Buffer', data: Buffer.from(value?.data || value).toString('base64') }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +51,7 @@ export const BufferJSON = {
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
reviver: (_, value: any) => {
|
reviver: (_, value: any) => {
|
||||||
if(typeof value === 'object' && !!value && (value.buffer === true || value.type === 'Buffer')) {
|
if (typeof value === 'object' && !!value && (value.buffer === true || value.type === 'Buffer')) {
|
||||||
const val = value.data || value.value
|
const val = value.data || value.value
|
||||||
return typeof val === 'string' ? Buffer.from(val, 'base64') : Buffer.from(val || [])
|
return typeof val === 'string' ? Buffer.from(val, 'base64') : Buffer.from(val || [])
|
||||||
}
|
}
|
||||||
@@ -52,17 +60,13 @@ export const BufferJSON = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getKeyAuthor = (
|
export const getKeyAuthor = (key: proto.IMessageKey | undefined | null, meId = 'me') =>
|
||||||
key: proto.IMessageKey | undefined | null,
|
|
||||||
meId = 'me'
|
|
||||||
) => (
|
|
||||||
(key?.fromMe ? meId : key?.participant || key?.remoteJid) || ''
|
(key?.fromMe ? meId : key?.participant || key?.remoteJid) || ''
|
||||||
)
|
|
||||||
|
|
||||||
export const writeRandomPadMax16 = (msg: Uint8Array) => {
|
export const writeRandomPadMax16 = (msg: Uint8Array) => {
|
||||||
const pad = randomBytes(1)
|
const pad = randomBytes(1)
|
||||||
pad[0] &= 0xf
|
pad[0] &= 0xf
|
||||||
if(!pad[0]) {
|
if (!pad[0]) {
|
||||||
pad[0] = 0xf
|
pad[0] = 0xf
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,23 +75,19 @@ export const writeRandomPadMax16 = (msg: Uint8Array) => {
|
|||||||
|
|
||||||
export const unpadRandomMax16 = (e: Uint8Array | Buffer) => {
|
export const unpadRandomMax16 = (e: Uint8Array | Buffer) => {
|
||||||
const t = new Uint8Array(e)
|
const t = new Uint8Array(e)
|
||||||
if(0 === t.length) {
|
if (0 === t.length) {
|
||||||
throw new Error('unpadPkcs7 given empty bytes')
|
throw new Error('unpadPkcs7 given empty bytes')
|
||||||
}
|
}
|
||||||
|
|
||||||
var r = t[t.length - 1]
|
var r = t[t.length - 1]
|
||||||
if(r > t.length) {
|
if (r > t.length) {
|
||||||
throw new Error(`unpad given ${t.length} bytes, but pad is ${r}`)
|
throw new Error(`unpad given ${t.length} bytes, but pad is ${r}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Uint8Array(t.buffer, t.byteOffset, t.length - r)
|
return new Uint8Array(t.buffer, t.byteOffset, t.length - r)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const encodeWAMessage = (message: proto.IMessage) => (
|
export const encodeWAMessage = (message: proto.IMessage) => writeRandomPadMax16(proto.Message.encode(message).finish())
|
||||||
writeRandomPadMax16(
|
|
||||||
proto.Message.encode(message).finish()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
export const generateRegistrationId = (): number => {
|
export const generateRegistrationId = (): number => {
|
||||||
return Uint16Array.from(randomBytes(2))[0] & 16383
|
return Uint16Array.from(randomBytes(2))[0] & 16383
|
||||||
@@ -96,7 +96,7 @@ export const generateRegistrationId = (): number => {
|
|||||||
export const encodeBigEndian = (e: number, t = 4) => {
|
export const encodeBigEndian = (e: number, t = 4) => {
|
||||||
let r = e
|
let r = e
|
||||||
const a = new Uint8Array(t)
|
const a = new Uint8Array(t)
|
||||||
for(let i = t - 1; i >= 0; i--) {
|
for (let i = t - 1; i >= 0; i--) {
|
||||||
a[i] = 255 & r
|
a[i] = 255 & r
|
||||||
r >>>= 8
|
r >>>= 8
|
||||||
}
|
}
|
||||||
@@ -104,7 +104,8 @@ export const encodeBigEndian = (e: number, t = 4) => {
|
|||||||
return a
|
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 */
|
/** unix timestamp of a date in seconds */
|
||||||
export const unixTimestampSeconds = (date: Date = new Date()) => Math.floor(date.getTime() / 1000)
|
export const unixTimestampSeconds = (date: Date = new Date()) => Math.floor(date.getTime() / 1000)
|
||||||
@@ -124,12 +125,12 @@ export const debouncedTimeout = (intervalMs = 1000, task?: () => void) => {
|
|||||||
timeout && clearTimeout(timeout)
|
timeout && clearTimeout(timeout)
|
||||||
timeout = undefined
|
timeout = undefined
|
||||||
},
|
},
|
||||||
setTask: (newTask: () => void) => task = newTask,
|
setTask: (newTask: () => void) => (task = newTask),
|
||||||
setInterval: (newInterval: number) => intervalMs = newInterval
|
setInterval: (newInterval: number) => (intervalMs = newInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const delay = (ms: number) => delayCancellable (ms).delay
|
export const delay = (ms: number) => delayCancellable(ms).delay
|
||||||
|
|
||||||
export const delayCancellable = (ms: number) => {
|
export const delayCancellable = (ms: number) => {
|
||||||
const stack = new Error().stack
|
const stack = new Error().stack
|
||||||
@@ -140,7 +141,7 @@ export const delayCancellable = (ms: number) => {
|
|||||||
reject = _reject
|
reject = _reject
|
||||||
})
|
})
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
clearTimeout (timeout)
|
clearTimeout(timeout)
|
||||||
reject(
|
reject(
|
||||||
new Boom('Cancelled', {
|
new Boom('Cancelled', {
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
@@ -154,29 +155,33 @@ export const delayCancellable = (ms: number) => {
|
|||||||
return { delay, cancel }
|
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>(
|
||||||
if(!ms) {
|
ms: number | undefined,
|
||||||
|
promise: (resolve: (v: T) => void, reject: (error) => void) => void
|
||||||
|
) {
|
||||||
|
if (!ms) {
|
||||||
return new Promise(promise)
|
return new Promise(promise)
|
||||||
}
|
}
|
||||||
|
|
||||||
const stack = new Error().stack
|
const stack = new Error().stack
|
||||||
// Create a promise that rejects in <ms> milliseconds
|
// Create a promise that rejects in <ms> milliseconds
|
||||||
const { delay, cancel } = delayCancellable (ms)
|
const { delay, cancel } = delayCancellable(ms)
|
||||||
const p = new Promise((resolve, reject) => {
|
const p = new Promise((resolve, reject) => {
|
||||||
delay
|
delay
|
||||||
.then(() => reject(
|
.then(() =>
|
||||||
new Boom('Timed Out', {
|
reject(
|
||||||
statusCode: DisconnectReason.timedOut,
|
new Boom('Timed Out', {
|
||||||
data: {
|
statusCode: DisconnectReason.timedOut,
|
||||||
stack
|
data: {
|
||||||
}
|
stack
|
||||||
})
|
}
|
||||||
))
|
})
|
||||||
.catch (err => reject(err))
|
)
|
||||||
|
)
|
||||||
|
.catch(err => reject(err))
|
||||||
|
|
||||||
promise (resolve, reject)
|
promise(resolve, reject)
|
||||||
})
|
}).finally(cancel)
|
||||||
.finally (cancel)
|
|
||||||
return p as Promise<T>
|
return p as Promise<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,9 +191,9 @@ export const generateMessageIDV2 = (userId?: string): string => {
|
|||||||
const data = Buffer.alloc(8 + 20 + 16)
|
const data = Buffer.alloc(8 + 20 + 16)
|
||||||
data.writeBigUInt64BE(BigInt(Math.floor(Date.now() / 1000)))
|
data.writeBigUInt64BE(BigInt(Math.floor(Date.now() / 1000)))
|
||||||
|
|
||||||
if(userId) {
|
if (userId) {
|
||||||
const id = jidDecode(userId)
|
const id = jidDecode(userId)
|
||||||
if(id?.user) {
|
if (id?.user) {
|
||||||
data.write(id.user, 8)
|
data.write(id.user, 8)
|
||||||
data.write('@c.us', 8 + id.user.length)
|
data.write('@c.us', 8 + id.user.length)
|
||||||
}
|
}
|
||||||
@@ -205,37 +210,30 @@ export const generateMessageIDV2 = (userId?: string): string => {
|
|||||||
export const generateMessageID = () => '3EB0' + randomBytes(18).toString('hex').toUpperCase()
|
export const generateMessageID = () => '3EB0' + randomBytes(18).toString('hex').toUpperCase()
|
||||||
|
|
||||||
export function bindWaitForEvent<T extends keyof BaileysEventMap>(ev: BaileysEventEmitter, event: T) {
|
export function bindWaitForEvent<T extends keyof BaileysEventMap>(ev: BaileysEventEmitter, event: T) {
|
||||||
return async(check: (u: BaileysEventMap[T]) => Promise<boolean | undefined>, timeoutMs?: number) => {
|
return async (check: (u: BaileysEventMap[T]) => Promise<boolean | undefined>, timeoutMs?: number) => {
|
||||||
let listener: (item: BaileysEventMap[T]) => void
|
let listener: (item: BaileysEventMap[T]) => void
|
||||||
let closeListener: (state: Partial<ConnectionState>) => void
|
let closeListener: (state: Partial<ConnectionState>) => void
|
||||||
await (
|
await promiseTimeout<void>(timeoutMs, (resolve, reject) => {
|
||||||
promiseTimeout<void>(
|
closeListener = ({ connection, lastDisconnect }) => {
|
||||||
timeoutMs,
|
if (connection === 'close') {
|
||||||
(resolve, reject) => {
|
reject(
|
||||||
closeListener = ({ connection, lastDisconnect }) => {
|
lastDisconnect?.error || new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
|
||||||
if(connection === 'close') {
|
)
|
||||||
reject(
|
|
||||||
lastDisconnect?.error
|
|
||||||
|| new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ev.on('connection.update', closeListener)
|
|
||||||
listener = async(update) => {
|
|
||||||
if(await check(update)) {
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ev.on(event, listener)
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
.finally(() => {
|
|
||||||
ev.off(event, listener)
|
ev.on('connection.update', closeListener)
|
||||||
ev.off('connection.update', closeListener)
|
listener = async update => {
|
||||||
})
|
if (await check(update)) {
|
||||||
)
|
resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ev.on(event, listener)
|
||||||
|
}).finally(() => {
|
||||||
|
ev.off(event, listener)
|
||||||
|
ev.off('connection.update', closeListener)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,21 +243,18 @@ export const bindWaitForConnectionUpdate = (ev: BaileysEventEmitter) => bindWait
|
|||||||
* utility that fetches latest baileys version from the master branch.
|
* utility that fetches latest baileys version from the master branch.
|
||||||
* Use to ensure your WA connection is always on the latest version
|
* Use to ensure your WA connection is always on the latest version
|
||||||
*/
|
*/
|
||||||
export const fetchLatestBaileysVersion = async(options: AxiosRequestConfig<{}> = { }) => {
|
export const fetchLatestBaileysVersion = async (options: AxiosRequestConfig<{}> = {}) => {
|
||||||
const URL = 'https://raw.githubusercontent.com/WhiskeySockets/Baileys/master/src/Defaults/baileys-version.json'
|
const URL = 'https://raw.githubusercontent.com/WhiskeySockets/Baileys/master/src/Defaults/baileys-version.json'
|
||||||
try {
|
try {
|
||||||
const result = await axios.get<{ version: WAVersion }>(
|
const result = await axios.get<{ version: WAVersion }>(URL, {
|
||||||
URL,
|
...options,
|
||||||
{
|
responseType: 'json'
|
||||||
...options,
|
})
|
||||||
responseType: 'json'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return {
|
return {
|
||||||
version: result.data.version,
|
version: result.data.version,
|
||||||
isLatest: true
|
isLatest: true
|
||||||
}
|
}
|
||||||
} catch(error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
version: baileysVersion as WAVersion,
|
version: baileysVersion as WAVersion,
|
||||||
isLatest: false,
|
isLatest: false,
|
||||||
@@ -272,20 +267,17 @@ export const fetchLatestBaileysVersion = async(options: AxiosRequestConfig<{}> =
|
|||||||
* A utility that fetches the latest web version of whatsapp.
|
* A utility that fetches the latest web version of whatsapp.
|
||||||
* Use to ensure your WA connection is always on the latest version
|
* Use to ensure your WA connection is always on the latest version
|
||||||
*/
|
*/
|
||||||
export const fetchLatestWaWebVersion = async(options: AxiosRequestConfig<{}>) => {
|
export const fetchLatestWaWebVersion = async (options: AxiosRequestConfig<{}>) => {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.get(
|
const { data } = await axios.get('https://web.whatsapp.com/sw.js', {
|
||||||
'https://web.whatsapp.com/sw.js',
|
...options,
|
||||||
{
|
responseType: 'json'
|
||||||
...options,
|
})
|
||||||
responseType: 'json'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const regex = /\\?"client_revision\\?":\s*(\d+)/
|
const regex = /\\?"client_revision\\?":\s*(\d+)/
|
||||||
const match = data.match(regex)
|
const match = data.match(regex)
|
||||||
|
|
||||||
if(!match?.[1]) {
|
if (!match?.[1]) {
|
||||||
return {
|
return {
|
||||||
version: baileysVersion as WAVersion,
|
version: baileysVersion as WAVersion,
|
||||||
isLatest: false,
|
isLatest: false,
|
||||||
@@ -301,7 +293,7 @@ export const fetchLatestWaWebVersion = async(options: AxiosRequestConfig<{}>) =>
|
|||||||
version: [2, 3000, +clientRevision] as WAVersion,
|
version: [2, 3000, +clientRevision] as WAVersion,
|
||||||
isLatest: true
|
isLatest: true
|
||||||
}
|
}
|
||||||
} catch(error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
version: baileysVersion as WAVersion,
|
version: baileysVersion as WAVersion,
|
||||||
isLatest: false,
|
isLatest: false,
|
||||||
@@ -317,9 +309,9 @@ export const generateMdTagPrefix = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_MAP: { [_: string]: proto.WebMessageInfo.Status } = {
|
const STATUS_MAP: { [_: string]: proto.WebMessageInfo.Status } = {
|
||||||
'sender': proto.WebMessageInfo.Status.SERVER_ACK,
|
sender: proto.WebMessageInfo.Status.SERVER_ACK,
|
||||||
'played': proto.WebMessageInfo.Status.PLAYED,
|
played: proto.WebMessageInfo.Status.PLAYED,
|
||||||
'read': proto.WebMessageInfo.Status.READ,
|
read: proto.WebMessageInfo.Status.READ,
|
||||||
'read-self': proto.WebMessageInfo.Status.READ
|
'read-self': proto.WebMessageInfo.Status.READ
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@@ -328,7 +320,7 @@ const STATUS_MAP: { [_: string]: proto.WebMessageInfo.Status } = {
|
|||||||
*/
|
*/
|
||||||
export const getStatusFromReceiptType = (type: string | undefined) => {
|
export const getStatusFromReceiptType = (type: string | undefined) => {
|
||||||
const status = STATUS_MAP[type!]
|
const status = STATUS_MAP[type!]
|
||||||
if(typeof type === 'undefined') {
|
if (typeof type === 'undefined') {
|
||||||
return proto.WebMessageInfo.Status.DELIVERY_ACK
|
return proto.WebMessageInfo.Status.DELIVERY_ACK
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,7 +340,7 @@ export const getErrorCodeFromStreamError = (node: BinaryNode) => {
|
|||||||
let reason = reasonNode?.tag || 'unknown'
|
let reason = reasonNode?.tag || 'unknown'
|
||||||
const statusCode = +(node.attrs.code || CODE_MAP[reason] || DisconnectReason.badSession)
|
const statusCode = +(node.attrs.code || CODE_MAP[reason] || DisconnectReason.badSession)
|
||||||
|
|
||||||
if(statusCode === DisconnectReason.restartRequired) {
|
if (statusCode === DisconnectReason.restartRequired) {
|
||||||
reason = 'restart required'
|
reason = 'restart required'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,28 +353,28 @@ export const getErrorCodeFromStreamError = (node: BinaryNode) => {
|
|||||||
export const getCallStatusFromNode = ({ tag, attrs }: BinaryNode) => {
|
export const getCallStatusFromNode = ({ tag, attrs }: BinaryNode) => {
|
||||||
let status: WACallUpdateType
|
let status: WACallUpdateType
|
||||||
switch (tag) {
|
switch (tag) {
|
||||||
case 'offer':
|
case 'offer':
|
||||||
case 'offer_notice':
|
case 'offer_notice':
|
||||||
status = 'offer'
|
status = 'offer'
|
||||||
break
|
break
|
||||||
case 'terminate':
|
case 'terminate':
|
||||||
if(attrs.reason === 'timeout') {
|
if (attrs.reason === 'timeout') {
|
||||||
status = 'timeout'
|
status = 'timeout'
|
||||||
} else {
|
} else {
|
||||||
//fired when accepted/rejected/timeout/caller hangs up
|
//fired when accepted/rejected/timeout/caller hangs up
|
||||||
status = 'terminate'
|
status = 'terminate'
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
case 'reject':
|
case 'reject':
|
||||||
status = 'reject'
|
status = 'reject'
|
||||||
break
|
break
|
||||||
case 'accept':
|
case 'accept':
|
||||||
status = 'accept'
|
status = 'accept'
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
status = 'ringing'
|
status = 'ringing'
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return status
|
return status
|
||||||
@@ -392,16 +384,17 @@ const UNEXPECTED_SERVER_CODE_TEXT = 'Unexpected server response: '
|
|||||||
|
|
||||||
export const getCodeFromWSError = (error: Error) => {
|
export const getCodeFromWSError = (error: Error) => {
|
||||||
let statusCode = 500
|
let statusCode = 500
|
||||||
if(error?.message?.includes(UNEXPECTED_SERVER_CODE_TEXT)) {
|
if (error?.message?.includes(UNEXPECTED_SERVER_CODE_TEXT)) {
|
||||||
const code = +error?.message.slice(UNEXPECTED_SERVER_CODE_TEXT.length)
|
const code = +error?.message.slice(UNEXPECTED_SERVER_CODE_TEXT.length)
|
||||||
if(!Number.isNaN(code) && code >= 400) {
|
if (!Number.isNaN(code) && code >= 400) {
|
||||||
statusCode = code
|
statusCode = code
|
||||||
}
|
}
|
||||||
} else if(
|
} else if (
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
(error as any)?.code?.startsWith('E')
|
(error as any)?.code?.startsWith('E') ||
|
||||||
|| error?.message?.includes('timed out')
|
error?.message?.includes('timed out')
|
||||||
) { // handle ETIMEOUT, ENOTFOUND etc
|
) {
|
||||||
|
// handle ETIMEOUT, ENOTFOUND etc
|
||||||
statusCode = 408
|
statusCode = 408
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,9 +410,9 @@ export const isWABusinessPlatform = (platform: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function trimUndefined(obj: {[_: string]: any}) {
|
export function trimUndefined(obj: { [_: string]: any }) {
|
||||||
for(const key in obj) {
|
for (const key in obj) {
|
||||||
if(typeof obj[key] === 'undefined') {
|
if (typeof obj[key] === 'undefined') {
|
||||||
delete obj[key]
|
delete obj[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -434,17 +427,17 @@ export function bytesToCrockford(buffer: Buffer): string {
|
|||||||
let bitCount = 0
|
let bitCount = 0
|
||||||
const crockford: string[] = []
|
const crockford: string[] = []
|
||||||
|
|
||||||
for(const element of buffer) {
|
for (const element of buffer) {
|
||||||
value = (value << 8) | (element & 0xff)
|
value = (value << 8) | (element & 0xff)
|
||||||
bitCount += 8
|
bitCount += 8
|
||||||
|
|
||||||
while(bitCount >= 5) {
|
while (bitCount >= 5) {
|
||||||
crockford.push(CROCKFORD_CHARACTERS.charAt((value >>> (bitCount - 5)) & 31))
|
crockford.push(CROCKFORD_CHARACTERS.charAt((value >>> (bitCount - 5)) & 31))
|
||||||
bitCount -= 5
|
bitCount -= 5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(bitCount > 0) {
|
if (bitCount > 0) {
|
||||||
crockford.push(CROCKFORD_CHARACTERS.charAt((value << (5 - bitCount)) & 31))
|
crockford.push(CROCKFORD_CHARACTERS.charAt((value << (5 - bitCount)) & 31))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,7 @@ import { downloadContentFromMessage } from './messages-media'
|
|||||||
|
|
||||||
const inflatePromise = promisify(inflate)
|
const inflatePromise = promisify(inflate)
|
||||||
|
|
||||||
export const downloadHistory = async(
|
export const downloadHistory = async (msg: proto.Message.IHistorySyncNotification, options: AxiosRequestConfig<{}>) => {
|
||||||
msg: proto.Message.IHistorySyncNotification,
|
|
||||||
options: AxiosRequestConfig<{}>
|
|
||||||
) => {
|
|
||||||
const stream = await downloadContentFromMessage(msg, 'md-msg-hist', { options })
|
const stream = await downloadContentFromMessage(msg, 'md-msg-hist', { options })
|
||||||
const bufferArray: Buffer[] = []
|
const bufferArray: Buffer[] = []
|
||||||
for await (const chunk of stream) {
|
for await (const chunk of stream) {
|
||||||
@@ -35,59 +32,58 @@ export const processHistoryMessage = (item: proto.IHistorySync) => {
|
|||||||
const chats: Chat[] = []
|
const chats: Chat[] = []
|
||||||
|
|
||||||
switch (item.syncType) {
|
switch (item.syncType) {
|
||||||
case proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP:
|
case proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP:
|
||||||
case proto.HistorySync.HistorySyncType.RECENT:
|
case proto.HistorySync.HistorySyncType.RECENT:
|
||||||
case proto.HistorySync.HistorySyncType.FULL:
|
case proto.HistorySync.HistorySyncType.FULL:
|
||||||
case proto.HistorySync.HistorySyncType.ON_DEMAND:
|
case proto.HistorySync.HistorySyncType.ON_DEMAND:
|
||||||
for(const chat of item.conversations! as Chat[]) {
|
for (const chat of item.conversations! as Chat[]) {
|
||||||
contacts.push({ id: chat.id, name: chat.name || undefined })
|
contacts.push({ id: chat.id, name: chat.name || undefined })
|
||||||
|
|
||||||
const msgs = chat.messages || []
|
const msgs = chat.messages || []
|
||||||
delete chat.messages
|
delete chat.messages
|
||||||
delete chat.archived
|
delete chat.archived
|
||||||
delete chat.muteEndTime
|
delete chat.muteEndTime
|
||||||
delete chat.pinned
|
delete chat.pinned
|
||||||
|
|
||||||
for(const item of msgs) {
|
for (const item of msgs) {
|
||||||
const message = item.message!
|
const message = item.message!
|
||||||
messages.push(message)
|
messages.push(message)
|
||||||
|
|
||||||
if(!chat.messages?.length) {
|
if (!chat.messages?.length) {
|
||||||
// keep only the most recent message in the chat array
|
// keep only the most recent message in the chat array
|
||||||
chat.messages = [{ message }]
|
chat.messages = [{ message }]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message.key.fromMe && !chat.lastMessageRecvTimestamp) {
|
||||||
|
chat.lastMessageRecvTimestamp = toNumber(message.messageTimestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(message.messageStubType === WAMessageStubType.BIZ_PRIVACY_MODE_TO_BSP ||
|
||||||
|
message.messageStubType === WAMessageStubType.BIZ_PRIVACY_MODE_TO_FB) &&
|
||||||
|
message.messageStubParameters?.[0]
|
||||||
|
) {
|
||||||
|
contacts.push({
|
||||||
|
id: message.key.participant || message.key.remoteJid!,
|
||||||
|
verifiedName: message.messageStubParameters?.[0]
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!message.key.fromMe && !chat.lastMessageRecvTimestamp) {
|
if (isJidUser(chat.id) && chat.readOnly && chat.archived) {
|
||||||
chat.lastMessageRecvTimestamp = toNumber(message.messageTimestamp)
|
delete chat.readOnly
|
||||||
}
|
}
|
||||||
|
|
||||||
if(
|
chats.push({ ...chat })
|
||||||
(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],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isJidUser(chat.id) && chat.readOnly && chat.archived) {
|
break
|
||||||
delete chat.readOnly
|
case proto.HistorySync.HistorySyncType.PUSH_NAME:
|
||||||
|
for (const c of item.pushnames!) {
|
||||||
|
contacts.push({ id: c.id!, notify: c.pushname! })
|
||||||
}
|
}
|
||||||
|
|
||||||
chats.push({ ...chat })
|
break
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
case proto.HistorySync.HistorySyncType.PUSH_NAME:
|
|
||||||
for(const c of item.pushnames!) {
|
|
||||||
contacts.push({ id: c.id!, notify: c.pushname! })
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -99,7 +95,7 @@ export const processHistoryMessage = (item: proto.IHistorySync) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const downloadAndProcessHistorySyncNotification = async(
|
export const downloadAndProcessHistorySyncNotification = async (
|
||||||
msg: proto.Message.IHistorySyncNotification,
|
msg: proto.Message.IHistorySyncNotification,
|
||||||
options: AxiosRequestConfig<{}>
|
options: AxiosRequestConfig<{}>
|
||||||
) => {
|
) => {
|
||||||
@@ -112,4 +108,4 @@ export const getHistoryMsg = (message: proto.IMessage) => {
|
|||||||
const anyHistoryMsg = normalizedContent?.protocolMessage?.historySyncNotification
|
const anyHistoryMsg = normalizedContent?.protocolMessage?.historySyncNotification
|
||||||
|
|
||||||
return anyHistoryMsg
|
return anyHistoryMsg
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ import { extractImageThumb, getHttpStream } from './messages-media'
|
|||||||
const THUMBNAIL_WIDTH_PX = 192
|
const THUMBNAIL_WIDTH_PX = 192
|
||||||
|
|
||||||
/** Fetches an image and generates a thumbnail for it */
|
/** Fetches an image and generates a thumbnail for it */
|
||||||
const getCompressedJpegThumbnail = async(
|
const getCompressedJpegThumbnail = async (url: string, { thumbnailWidth, fetchOpts }: URLGenerationOptions) => {
|
||||||
url: string,
|
|
||||||
{ thumbnailWidth, fetchOpts }: URLGenerationOptions
|
|
||||||
) => {
|
|
||||||
const stream = await getHttpStream(url, fetchOpts)
|
const stream = await getHttpStream(url, fetchOpts)
|
||||||
const result = await extractImageThumb(stream, thumbnailWidth)
|
const result = await extractImageThumb(stream, thumbnailWidth)
|
||||||
return result
|
return result
|
||||||
@@ -34,12 +31,12 @@ export type URLGenerationOptions = {
|
|||||||
* @param text first matched URL in text
|
* @param text first matched URL in text
|
||||||
* @returns the URL info required to generate link preview
|
* @returns the URL info required to generate link preview
|
||||||
*/
|
*/
|
||||||
export const getUrlInfo = async(
|
export const getUrlInfo = async (
|
||||||
text: string,
|
text: string,
|
||||||
opts: URLGenerationOptions = {
|
opts: URLGenerationOptions = {
|
||||||
thumbnailWidth: THUMBNAIL_WIDTH_PX,
|
thumbnailWidth: THUMBNAIL_WIDTH_PX,
|
||||||
fetchOpts: { timeout: 3000 }
|
fetchOpts: { timeout: 3000 }
|
||||||
},
|
}
|
||||||
): Promise<WAUrlInfo | undefined> => {
|
): Promise<WAUrlInfo | undefined> => {
|
||||||
try {
|
try {
|
||||||
// retries
|
// retries
|
||||||
@@ -48,7 +45,7 @@ export const getUrlInfo = async(
|
|||||||
|
|
||||||
const { getLinkPreview } = await import('link-preview-js')
|
const { getLinkPreview } = await import('link-preview-js')
|
||||||
let previewLink = text
|
let previewLink = text
|
||||||
if(!text.startsWith('https://') && !text.startsWith('http://')) {
|
if (!text.startsWith('https://') && !text.startsWith('http://')) {
|
||||||
previewLink = 'https://' + previewLink
|
previewLink = 'https://' + previewLink
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,14 +55,14 @@ export const getUrlInfo = async(
|
|||||||
handleRedirects: (baseURL: string, forwardedURL: string) => {
|
handleRedirects: (baseURL: string, forwardedURL: string) => {
|
||||||
const urlObj = new URL(baseURL)
|
const urlObj = new URL(baseURL)
|
||||||
const forwardedURLObj = new URL(forwardedURL)
|
const forwardedURLObj = new URL(forwardedURL)
|
||||||
if(retries >= maxRetry) {
|
if (retries >= maxRetry) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if(
|
if (
|
||||||
forwardedURLObj.hostname === urlObj.hostname
|
forwardedURLObj.hostname === urlObj.hostname ||
|
||||||
|| forwardedURLObj.hostname === 'www.' + urlObj.hostname
|
forwardedURLObj.hostname === 'www.' + urlObj.hostname ||
|
||||||
|| 'www.' + forwardedURLObj.hostname === urlObj.hostname
|
'www.' + forwardedURLObj.hostname === urlObj.hostname
|
||||||
) {
|
) {
|
||||||
retries + 1
|
retries + 1
|
||||||
return true
|
return true
|
||||||
@@ -75,7 +72,7 @@ export const getUrlInfo = async(
|
|||||||
},
|
},
|
||||||
headers: opts.fetchOpts as {}
|
headers: opts.fetchOpts as {}
|
||||||
})
|
})
|
||||||
if(info && 'title' in info && info.title) {
|
if (info && 'title' in info && info.title) {
|
||||||
const [image] = info.images
|
const [image] = info.images
|
||||||
|
|
||||||
const urlInfo: WAUrlInfo = {
|
const urlInfo: WAUrlInfo = {
|
||||||
@@ -86,7 +83,7 @@ export const getUrlInfo = async(
|
|||||||
originalThumbnailUrl: image
|
originalThumbnailUrl: image
|
||||||
}
|
}
|
||||||
|
|
||||||
if(opts.uploadImage) {
|
if (opts.uploadImage) {
|
||||||
const { imageMessage } = await prepareWAMessageMedia(
|
const { imageMessage } = await prepareWAMessageMedia(
|
||||||
{ image: { url: image } },
|
{ image: { url: image } },
|
||||||
{
|
{
|
||||||
@@ -95,28 +92,21 @@ export const getUrlInfo = async(
|
|||||||
options: opts.fetchOpts
|
options: opts.fetchOpts
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
urlInfo.jpegThumbnail = imageMessage?.jpegThumbnail
|
urlInfo.jpegThumbnail = imageMessage?.jpegThumbnail ? Buffer.from(imageMessage.jpegThumbnail) : undefined
|
||||||
? Buffer.from(imageMessage.jpegThumbnail)
|
|
||||||
: undefined
|
|
||||||
urlInfo.highQualityThumbnail = imageMessage || undefined
|
urlInfo.highQualityThumbnail = imageMessage || undefined
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
urlInfo.jpegThumbnail = image
|
urlInfo.jpegThumbnail = image ? (await getCompressedJpegThumbnail(image, opts)).buffer : undefined
|
||||||
? (await getCompressedJpegThumbnail(image, opts)).buffer
|
} catch (error) {
|
||||||
: undefined
|
opts.logger?.debug({ err: error.stack, url: previewLink }, 'error in generating thumbnail')
|
||||||
} catch(error) {
|
|
||||||
opts.logger?.debug(
|
|
||||||
{ err: error.stack, url: previewLink },
|
|
||||||
'error in generating thumbnail'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return urlInfo
|
return urlInfo
|
||||||
}
|
}
|
||||||
} catch(error) {
|
} catch (error) {
|
||||||
if(!error.message.includes('receive a valid')) {
|
if (!error.message.includes('receive a valid')) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import P from 'pino'
|
import P from 'pino'
|
||||||
|
|
||||||
export interface ILogger {
|
export interface ILogger {
|
||||||
level: string
|
level: string
|
||||||
child(obj: Record<string, unknown>): ILogger
|
child(obj: Record<string, unknown>): ILogger
|
||||||
trace(obj: unknown, msg?: string)
|
trace(obj: unknown, msg?: string)
|
||||||
debug(obj: unknown, msg?: string)
|
debug(obj: unknown, msg?: string)
|
||||||
info(obj: unknown, msg?: string)
|
info(obj: unknown, msg?: string)
|
||||||
warn(obj: unknown, msg?: string)
|
warn(obj: unknown, msg?: string)
|
||||||
error(obj: unknown, msg?: string)
|
error(obj: unknown, msg?: string)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default P({ timestamp: () => `,"time":"${new Date().toJSON()}"` })
|
export default P({ timestamp: () => `,"time":"${new Date().toJSON()}"` })
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { hkdf } from './crypto'
|
|||||||
const o = 128
|
const o = 128
|
||||||
|
|
||||||
class d {
|
class d {
|
||||||
|
|
||||||
salt: string
|
salt: string
|
||||||
|
|
||||||
constructor(e: string) {
|
constructor(e: string) {
|
||||||
@@ -17,7 +16,7 @@ class d {
|
|||||||
}
|
}
|
||||||
add(e, t) {
|
add(e, t) {
|
||||||
var r = this
|
var r = this
|
||||||
for(const item of t) {
|
for (const item of t) {
|
||||||
e = r._addSingle(e, item)
|
e = r._addSingle(e, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +24,7 @@ class d {
|
|||||||
}
|
}
|
||||||
subtract(e, t) {
|
subtract(e, t) {
|
||||||
var r = this
|
var r = this
|
||||||
for(const item of t) {
|
for (const item of t) {
|
||||||
e = r._subtractSingle(e, item)
|
e = r._subtractSingle(e, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,20 +37,20 @@ class d {
|
|||||||
async _addSingle(e, t) {
|
async _addSingle(e, t) {
|
||||||
var r = this
|
var r = this
|
||||||
const n = new Uint8Array(await hkdf(Buffer.from(t), o, { info: r.salt })).buffer
|
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) {
|
async _subtractSingle(e, t) {
|
||||||
var r = this
|
var r = this
|
||||||
|
|
||||||
const n = new Uint8Array(await hkdf(Buffer.from(t), o, { info: r.salt })).buffer
|
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) {
|
performPointwiseWithOverflow(e, t, r) {
|
||||||
const n = new DataView(e)
|
const n = new DataView(e),
|
||||||
, i = new DataView(t)
|
i = new DataView(t),
|
||||||
, a = new ArrayBuffer(n.byteLength)
|
a = new ArrayBuffer(n.byteLength),
|
||||||
, s = new DataView(a)
|
s = new DataView(a)
|
||||||
for(let e = 0; e < n.byteLength; e += 2) {
|
for (let e = 0; e < n.byteLength; e += 2) {
|
||||||
s.setUint16(e, r(n.getUint16(e, !0), i.getUint16(e, !0)), !0)
|
s.setUint16(e, r(n.getUint16(e, !0), i.getUint16(e, !0)), !0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ export const makeMutex = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
mutex<T>(code: () => Promise<T> | T): Promise<T> {
|
mutex<T>(code: () => Promise<T> | T): Promise<T> {
|
||||||
task = (async() => {
|
task = (async () => {
|
||||||
// wait for the previous task to complete
|
// wait for the previous task to complete
|
||||||
// if there is an error, we swallow so as to not block the queue
|
// if there is an error, we swallow so as to not block the queue
|
||||||
try {
|
try {
|
||||||
await task
|
await task
|
||||||
} catch{ }
|
} catch {}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// execute the current task
|
// execute the current task
|
||||||
@@ -24,7 +24,7 @@ export const makeMutex = () => {
|
|||||||
// we replace the existing task, appending the new piece of execution to it
|
// 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
|
// so the next task will have to wait for this one to finish
|
||||||
return task
|
return task
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,11 +35,11 @@ export const makeKeyedMutex = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
mutex<T>(key: string, task: () => Promise<T> | T): Promise<T> {
|
mutex<T>(key: string, task: () => Promise<T> | T): Promise<T> {
|
||||||
if(!map[key]) {
|
if (!map[key]) {
|
||||||
map[key] = makeMutex()
|
map[key] = makeMutex()
|
||||||
}
|
}
|
||||||
|
|
||||||
return map[key].mutex(task)
|
return map[key].mutex(task)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,20 @@ import { Readable, Transform } from 'stream'
|
|||||||
import { URL } from 'url'
|
import { URL } from 'url'
|
||||||
import { proto } from '../../WAProto'
|
import { proto } from '../../WAProto'
|
||||||
import { DEFAULT_ORIGIN, MEDIA_HKDF_KEY_MAPPING, MEDIA_PATH_MAP } from '../Defaults'
|
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 { BinaryNode, getBinaryNodeChild, getBinaryNodeChildBuffer, jidNormalizedUser } from '../WABinary'
|
||||||
import { aesDecryptGCM, aesEncryptGCM, hkdf } from './crypto'
|
import { aesDecryptGCM, aesEncryptGCM, hkdf } from './crypto'
|
||||||
import { generateMessageIDV2 } from './generics'
|
import { generateMessageIDV2 } from './generics'
|
||||||
@@ -19,30 +32,24 @@ import { ILogger } from './logger'
|
|||||||
|
|
||||||
const getTmpFilesDirectory = () => tmpdir()
|
const getTmpFilesDirectory = () => tmpdir()
|
||||||
|
|
||||||
const getImageProcessingLibrary = async() => {
|
const getImageProcessingLibrary = async () => {
|
||||||
const [_jimp, sharp] = await Promise.all([
|
const [_jimp, sharp] = await Promise.all([
|
||||||
(async() => {
|
(async () => {
|
||||||
const jimp = await (
|
const jimp = await import('jimp').catch(() => {})
|
||||||
import('jimp')
|
|
||||||
.catch(() => { })
|
|
||||||
)
|
|
||||||
return jimp
|
return jimp
|
||||||
})(),
|
})(),
|
||||||
(async() => {
|
(async () => {
|
||||||
const sharp = await (
|
const sharp = await import('sharp').catch(() => {})
|
||||||
import('sharp')
|
|
||||||
.catch(() => { })
|
|
||||||
)
|
|
||||||
return sharp
|
return sharp
|
||||||
})()
|
})()
|
||||||
])
|
])
|
||||||
|
|
||||||
if(sharp) {
|
if (sharp) {
|
||||||
return { sharp }
|
return { sharp }
|
||||||
}
|
}
|
||||||
|
|
||||||
const jimp = _jimp?.default || _jimp
|
const jimp = _jimp?.default || _jimp
|
||||||
if(jimp) {
|
if (jimp) {
|
||||||
return { jimp }
|
return { jimp }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,12 +62,15 @@ export const hkdfInfoKey = (type: MediaType) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** generates all the keys required to encrypt/decrypt & sign a media message */
|
/** 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(
|
||||||
if(!buffer) {
|
buffer: Uint8Array | string | null | undefined,
|
||||||
|
mediaType: MediaType
|
||||||
|
): Promise<MediaDecryptionKeyInfo> {
|
||||||
|
if (!buffer) {
|
||||||
throw new Boom('Cannot derive from empty media key')
|
throw new Boom('Cannot derive from empty media key')
|
||||||
}
|
}
|
||||||
|
|
||||||
if(typeof buffer === 'string') {
|
if (typeof buffer === 'string') {
|
||||||
buffer = Buffer.from(buffer.replace('data:;base64,', ''), 'base64')
|
buffer = Buffer.from(buffer.replace('data:;base64,', ''), 'base64')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,49 +79,47 @@ export async function getMediaKeys(buffer: Uint8Array | string | null | undefine
|
|||||||
return {
|
return {
|
||||||
iv: expandedMediaKey.slice(0, 16),
|
iv: expandedMediaKey.slice(0, 16),
|
||||||
cipherKey: expandedMediaKey.slice(16, 48),
|
cipherKey: expandedMediaKey.slice(16, 48),
|
||||||
macKey: expandedMediaKey.slice(48, 80),
|
macKey: expandedMediaKey.slice(48, 80)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Extracts video thumb using FFMPEG */
|
/** Extracts video thumb using FFMPEG */
|
||||||
const extractVideoThumb = async(
|
const extractVideoThumb = async (
|
||||||
path: string,
|
path: string,
|
||||||
destPath: string,
|
destPath: string,
|
||||||
time: string,
|
time: string,
|
||||||
size: { width: number, height: number },
|
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}`
|
new Promise<void>((resolve, reject) => {
|
||||||
exec(cmd, (err) => {
|
const cmd = `ffmpeg -ss ${time} -i ${path} -y -vf scale=${size.width}:-1 -vframes 1 -f image2 ${destPath}`
|
||||||
if(err) {
|
exec(cmd, err => {
|
||||||
reject(err)
|
if (err) {
|
||||||
} else {
|
reject(err)
|
||||||
resolve()
|
} else {
|
||||||
}
|
resolve()
|
||||||
})
|
}
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
export const extractImageThumb = async(bufferOrFilePath: Readable | Buffer | string, width = 32) => {
|
export const extractImageThumb = async (bufferOrFilePath: Readable | Buffer | string, width = 32) => {
|
||||||
if(bufferOrFilePath instanceof Readable) {
|
if (bufferOrFilePath instanceof Readable) {
|
||||||
bufferOrFilePath = await toBuffer(bufferOrFilePath)
|
bufferOrFilePath = await toBuffer(bufferOrFilePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
const lib = await getImageProcessingLibrary()
|
const lib = await getImageProcessingLibrary()
|
||||||
if('sharp' in lib && typeof lib.sharp?.default === 'function') {
|
if ('sharp' in lib && typeof lib.sharp?.default === 'function') {
|
||||||
const img = lib.sharp.default(bufferOrFilePath)
|
const img = lib.sharp.default(bufferOrFilePath)
|
||||||
const dimensions = await img.metadata()
|
const dimensions = await img.metadata()
|
||||||
|
|
||||||
const buffer = await img
|
const buffer = await img.resize(width).jpeg({ quality: 50 }).toBuffer()
|
||||||
.resize(width)
|
|
||||||
.jpeg({ quality: 50 })
|
|
||||||
.toBuffer()
|
|
||||||
return {
|
return {
|
||||||
buffer,
|
buffer,
|
||||||
original: {
|
original: {
|
||||||
width: dimensions.width,
|
width: dimensions.width,
|
||||||
height: dimensions.height,
|
height: dimensions.height
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
} else if('jimp' in lib && typeof lib.jimp?.read === 'function') {
|
} else if ('jimp' in lib && typeof lib.jimp?.read === 'function') {
|
||||||
const { read, MIME_JPEG, RESIZE_BILINEAR, AUTO } = lib.jimp
|
const { read, MIME_JPEG, RESIZE_BILINEAR, AUTO } = lib.jimp
|
||||||
|
|
||||||
const jimp = await read(bufferOrFilePath as string)
|
const jimp = await read(bufferOrFilePath as string)
|
||||||
@@ -119,10 +127,7 @@ export const extractImageThumb = async(bufferOrFilePath: Readable | Buffer | str
|
|||||||
width: jimp.getWidth(),
|
width: jimp.getWidth(),
|
||||||
height: jimp.getHeight()
|
height: jimp.getHeight()
|
||||||
}
|
}
|
||||||
const buffer = await jimp
|
const buffer = await jimp.quality(50).resize(width, AUTO, RESIZE_BILINEAR).getBufferAsync(MIME_JPEG)
|
||||||
.quality(50)
|
|
||||||
.resize(width, AUTO, RESIZE_BILINEAR)
|
|
||||||
.getBufferAsync(MIME_JPEG)
|
|
||||||
return {
|
return {
|
||||||
buffer,
|
buffer,
|
||||||
original: dimensions
|
original: dimensions
|
||||||
@@ -132,20 +137,14 @@ export const extractImageThumb = async(bufferOrFilePath: Readable | Buffer | str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const encodeBase64EncodedStringForUpload = (b64: string) => (
|
export const encodeBase64EncodedStringForUpload = (b64: string) =>
|
||||||
encodeURIComponent(
|
encodeURIComponent(b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, ''))
|
||||||
b64
|
|
||||||
.replace(/\+/g, '-')
|
|
||||||
.replace(/\//g, '_')
|
|
||||||
.replace(/\=+$/, '')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
export const generateProfilePicture = async(mediaUpload: WAMediaUpload) => {
|
export const generateProfilePicture = async (mediaUpload: WAMediaUpload) => {
|
||||||
let bufferOrFilePath: Buffer | string
|
let bufferOrFilePath: Buffer | string
|
||||||
if(Buffer.isBuffer(mediaUpload)) {
|
if (Buffer.isBuffer(mediaUpload)) {
|
||||||
bufferOrFilePath = mediaUpload
|
bufferOrFilePath = mediaUpload
|
||||||
} else if('url' in mediaUpload) {
|
} else if ('url' in mediaUpload) {
|
||||||
bufferOrFilePath = mediaUpload.url.toString()
|
bufferOrFilePath = mediaUpload.url.toString()
|
||||||
} else {
|
} else {
|
||||||
bufferOrFilePath = await toBuffer(mediaUpload.stream)
|
bufferOrFilePath = await toBuffer(mediaUpload.stream)
|
||||||
@@ -153,44 +152,42 @@ export const generateProfilePicture = async(mediaUpload: WAMediaUpload) => {
|
|||||||
|
|
||||||
const lib = await getImageProcessingLibrary()
|
const lib = await getImageProcessingLibrary()
|
||||||
let img: Promise<Buffer>
|
let img: Promise<Buffer>
|
||||||
if('sharp' in lib && typeof lib.sharp?.default === 'function') {
|
if ('sharp' in lib && typeof lib.sharp?.default === 'function') {
|
||||||
img = lib.sharp.default(bufferOrFilePath)
|
img = lib.sharp
|
||||||
|
.default(bufferOrFilePath)
|
||||||
.resize(640, 640)
|
.resize(640, 640)
|
||||||
.jpeg({
|
.jpeg({
|
||||||
quality: 50,
|
quality: 50
|
||||||
})
|
})
|
||||||
.toBuffer()
|
.toBuffer()
|
||||||
} else if('jimp' in lib && typeof lib.jimp?.read === 'function') {
|
} else if ('jimp' in lib && typeof lib.jimp?.read === 'function') {
|
||||||
const { read, MIME_JPEG, RESIZE_BILINEAR } = lib.jimp
|
const { read, MIME_JPEG, RESIZE_BILINEAR } = lib.jimp
|
||||||
const jimp = await read(bufferOrFilePath as string)
|
const jimp = await read(bufferOrFilePath as string)
|
||||||
const min = Math.min(jimp.getWidth(), jimp.getHeight())
|
const min = Math.min(jimp.getWidth(), jimp.getHeight())
|
||||||
const cropped = jimp.crop(0, 0, min, min)
|
const cropped = jimp.crop(0, 0, min, min)
|
||||||
|
|
||||||
img = cropped
|
img = cropped.quality(50).resize(640, 640, RESIZE_BILINEAR).getBufferAsync(MIME_JPEG)
|
||||||
.quality(50)
|
|
||||||
.resize(640, 640, RESIZE_BILINEAR)
|
|
||||||
.getBufferAsync(MIME_JPEG)
|
|
||||||
} else {
|
} else {
|
||||||
throw new Boom('No image processing library available')
|
throw new Boom('No image processing library available')
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
img: await img,
|
img: await img
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** gets the SHA256 of the given media message */
|
/** gets the SHA256 of the given media message */
|
||||||
export const mediaMessageSHA256B64 = (message: WAMessageContent) => {
|
export const mediaMessageSHA256B64 = (message: WAMessageContent) => {
|
||||||
const media = Object.values(message)[0] as WAGenericMediaMessage
|
const media = Object.values(message)[0] as WAGenericMediaMessage
|
||||||
return media?.fileSha256 && Buffer.from(media.fileSha256).toString ('base64')
|
return media?.fileSha256 && Buffer.from(media.fileSha256).toString('base64')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAudioDuration(buffer: Buffer | string | Readable) {
|
export async function getAudioDuration(buffer: Buffer | string | Readable) {
|
||||||
const musicMetadata = await import('music-metadata')
|
const musicMetadata = await import('music-metadata')
|
||||||
let metadata: IAudioMetadata
|
let metadata: IAudioMetadata
|
||||||
if(Buffer.isBuffer(buffer)) {
|
if (Buffer.isBuffer(buffer)) {
|
||||||
metadata = await musicMetadata.parseBuffer(buffer, undefined, { duration: true })
|
metadata = await musicMetadata.parseBuffer(buffer, undefined, { duration: true })
|
||||||
} else if(typeof buffer === 'string') {
|
} else if (typeof buffer === 'string') {
|
||||||
const rStream = createReadStream(buffer)
|
const rStream = createReadStream(buffer)
|
||||||
try {
|
try {
|
||||||
metadata = await musicMetadata.parseStream(rStream, undefined, { duration: true })
|
metadata = await musicMetadata.parseStream(rStream, undefined, { duration: true })
|
||||||
@@ -209,11 +206,11 @@ export async function getAudioDuration(buffer: Buffer | string | Readable) {
|
|||||||
*/
|
*/
|
||||||
export async function getAudioWaveform(buffer: Buffer | string | Readable, logger?: ILogger) {
|
export async function getAudioWaveform(buffer: Buffer | string | Readable, logger?: ILogger) {
|
||||||
try {
|
try {
|
||||||
const { default: decoder } = await eval('import(\'audio-decode\')')
|
const { default: decoder } = await eval("import('audio-decode')")
|
||||||
let audioData: Buffer
|
let audioData: Buffer
|
||||||
if(Buffer.isBuffer(buffer)) {
|
if (Buffer.isBuffer(buffer)) {
|
||||||
audioData = buffer
|
audioData = buffer
|
||||||
} else if(typeof buffer === 'string') {
|
} else if (typeof buffer === 'string') {
|
||||||
const rStream = createReadStream(buffer)
|
const rStream = createReadStream(buffer)
|
||||||
audioData = await toBuffer(rStream)
|
audioData = await toBuffer(rStream)
|
||||||
} else {
|
} else {
|
||||||
@@ -226,10 +223,10 @@ export async function getAudioWaveform(buffer: Buffer | string | Readable, logge
|
|||||||
const samples = 64 // Number of samples we want to have in our final data set
|
const samples = 64 // Number of samples we want to have in our final data set
|
||||||
const blockSize = Math.floor(rawData.length / samples) // the number of samples in each subdivision
|
const blockSize = Math.floor(rawData.length / samples) // the number of samples in each subdivision
|
||||||
const filteredData: number[] = []
|
const filteredData: number[] = []
|
||||||
for(let i = 0; i < samples; i++) {
|
for (let i = 0; i < samples; i++) {
|
||||||
const blockStart = blockSize * i // the location of the first sample in the block
|
const blockStart = blockSize * i // the location of the first sample in the block
|
||||||
let sum = 0
|
let sum = 0
|
||||||
for(let j = 0; j < blockSize; j++) {
|
for (let j = 0; j < blockSize; j++) {
|
||||||
sum = sum + Math.abs(rawData[blockStart + j]) // find the sum of all the samples in the block
|
sum = sum + Math.abs(rawData[blockStart + j]) // find the sum of all the samples in the block
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,20 +235,17 @@ 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.
|
// 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 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
|
// Generate waveform like WhatsApp
|
||||||
const waveform = new Uint8Array(
|
const waveform = new Uint8Array(normalizedData.map(n => Math.floor(100 * n)))
|
||||||
normalizedData.map((n) => Math.floor(100 * n))
|
|
||||||
)
|
|
||||||
|
|
||||||
return waveform
|
return waveform
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
logger?.debug('Failed to generate waveform: ' + e)
|
logger?.debug('Failed to generate waveform: ' + e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const toReadable = (buffer: Buffer) => {
|
export const toReadable = (buffer: Buffer) => {
|
||||||
const readable = new Readable({ read: () => {} })
|
const readable = new Readable({ read: () => {} })
|
||||||
readable.push(buffer)
|
readable.push(buffer)
|
||||||
@@ -259,7 +253,7 @@ export const toReadable = (buffer: Buffer) => {
|
|||||||
return readable
|
return readable
|
||||||
}
|
}
|
||||||
|
|
||||||
export const toBuffer = async(stream: Readable) => {
|
export const toBuffer = async (stream: Readable) => {
|
||||||
const chunks: Buffer[] = []
|
const chunks: Buffer[] = []
|
||||||
for await (const chunk of stream) {
|
for await (const chunk of stream) {
|
||||||
chunks.push(chunk)
|
chunks.push(chunk)
|
||||||
@@ -269,16 +263,16 @@ export const toBuffer = async(stream: Readable) => {
|
|||||||
return Buffer.concat(chunks)
|
return Buffer.concat(chunks)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStream = async(item: WAMediaUpload, opts?: AxiosRequestConfig) => {
|
export const getStream = async (item: WAMediaUpload, opts?: AxiosRequestConfig) => {
|
||||||
if(Buffer.isBuffer(item)) {
|
if (Buffer.isBuffer(item)) {
|
||||||
return { stream: toReadable(item), type: 'buffer' } as const
|
return { stream: toReadable(item), type: 'buffer' } as const
|
||||||
}
|
}
|
||||||
|
|
||||||
if('stream' in item) {
|
if ('stream' in item) {
|
||||||
return { stream: item.stream, type: 'readable' } as const
|
return { stream: item.stream, type: 'readable' } as const
|
||||||
}
|
}
|
||||||
|
|
||||||
if(item.url.toString().startsWith('http://') || item.url.toString().startsWith('https://')) {
|
if (item.url.toString().startsWith('http://') || item.url.toString().startsWith('https://')) {
|
||||||
return { stream: await getHttpStream(item.url, opts), type: 'remote' } as const
|
return { stream: await getHttpStream(item.url, opts), type: 'remote' } as const
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,21 +284,21 @@ export async function generateThumbnail(
|
|||||||
file: string,
|
file: string,
|
||||||
mediaType: 'video' | 'image',
|
mediaType: 'video' | 'image',
|
||||||
options: {
|
options: {
|
||||||
logger?: ILogger
|
logger?: ILogger
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
let thumbnail: string | undefined
|
let thumbnail: string | undefined
|
||||||
let originalImageDimensions: { width: number, height: number } | undefined
|
let originalImageDimensions: { width: number; height: number } | undefined
|
||||||
if(mediaType === 'image') {
|
if (mediaType === 'image') {
|
||||||
const { buffer, original } = await extractImageThumb(file)
|
const { buffer, original } = await extractImageThumb(file)
|
||||||
thumbnail = buffer.toString('base64')
|
thumbnail = buffer.toString('base64')
|
||||||
if(original.width && original.height) {
|
if (original.width && original.height) {
|
||||||
originalImageDimensions = {
|
originalImageDimensions = {
|
||||||
width: original.width,
|
width: original.width,
|
||||||
height: original.height,
|
height: original.height
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if(mediaType === 'video') {
|
} else if (mediaType === 'video') {
|
||||||
const imgFilename = join(getTmpFilesDirectory(), generateMessageIDV2() + '.jpg')
|
const imgFilename = join(getTmpFilesDirectory(), generateMessageIDV2() + '.jpg')
|
||||||
try {
|
try {
|
||||||
await extractVideoThumb(file, imgFilename, '00:00:00', { width: 32, height: 32 })
|
await extractVideoThumb(file, imgFilename, '00:00:00', { width: 32, height: 32 })
|
||||||
@@ -312,7 +306,7 @@ export async function generateThumbnail(
|
|||||||
thumbnail = buff.toString('base64')
|
thumbnail = buff.toString('base64')
|
||||||
|
|
||||||
await fs.unlink(imgFilename)
|
await fs.unlink(imgFilename)
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
options.logger?.debug('could not generate video thumb: ' + err)
|
options.logger?.debug('could not generate video thumb: ' + err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,7 +317,7 @@ export async function generateThumbnail(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getHttpStream = async(url: string | URL, options: AxiosRequestConfig & { isStream?: true } = {}) => {
|
export const getHttpStream = async (url: string | URL, options: AxiosRequestConfig & { isStream?: true } = {}) => {
|
||||||
const fetched = await axios.get(url.toString(), { ...options, responseType: 'stream' })
|
const fetched = await axios.get(url.toString(), { ...options, responseType: 'stream' })
|
||||||
return fetched.data as Readable
|
return fetched.data as Readable
|
||||||
}
|
}
|
||||||
@@ -334,7 +328,7 @@ type EncryptedStreamOptions = {
|
|||||||
opts?: AxiosRequestConfig
|
opts?: AxiosRequestConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export const encryptedStream = async(
|
export const encryptedStream = async (
|
||||||
media: WAMediaUpload,
|
media: WAMediaUpload,
|
||||||
mediaType: MediaType,
|
mediaType: MediaType,
|
||||||
{ logger, saveOriginalFileIfRequired, opts }: EncryptedStreamOptions = {}
|
{ logger, saveOriginalFileIfRequired, opts }: EncryptedStreamOptions = {}
|
||||||
@@ -350,9 +344,9 @@ export const encryptedStream = async(
|
|||||||
let bodyPath: string | undefined
|
let bodyPath: string | undefined
|
||||||
let writeStream: WriteStream | undefined
|
let writeStream: WriteStream | undefined
|
||||||
let didSaveToTmpPath = false
|
let didSaveToTmpPath = false
|
||||||
if(type === 'file') {
|
if (type === 'file') {
|
||||||
bodyPath = (media as WAMediaPayloadURL).url.toString()
|
bodyPath = (media as WAMediaPayloadURL).url.toString()
|
||||||
} else if(saveOriginalFileIfRequired) {
|
} else if (saveOriginalFileIfRequired) {
|
||||||
bodyPath = join(getTmpFilesDirectory(), mediaType + generateMessageIDV2())
|
bodyPath = join(getTmpFilesDirectory(), mediaType + generateMessageIDV2())
|
||||||
writeStream = createWriteStream(bodyPath)
|
writeStream = createWriteStream(bodyPath)
|
||||||
didSaveToTmpPath = true
|
didSaveToTmpPath = true
|
||||||
@@ -368,21 +362,14 @@ export const encryptedStream = async(
|
|||||||
for await (const data of stream) {
|
for await (const data of stream) {
|
||||||
fileLength += data.length
|
fileLength += data.length
|
||||||
|
|
||||||
if(
|
if (type === 'remote' && opts?.maxContentLength && fileLength + data.length > opts.maxContentLength) {
|
||||||
type === 'remote'
|
throw new Boom(`content length exceeded when encrypting "${type}"`, {
|
||||||
&& opts?.maxContentLength
|
data: { media, type }
|
||||||
&& fileLength + data.length > opts.maxContentLength
|
})
|
||||||
) {
|
|
||||||
throw new Boom(
|
|
||||||
`content length exceeded when encrypting "${type}"`,
|
|
||||||
{
|
|
||||||
data: { media, type }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sha256Plain = sha256Plain.update(data)
|
sha256Plain = sha256Plain.update(data)
|
||||||
if(writeStream && !writeStream.write(data)) {
|
if (writeStream && !writeStream.write(data)) {
|
||||||
await once(writeStream, 'drain')
|
await once(writeStream, 'drain')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,7 +402,7 @@ export const encryptedStream = async(
|
|||||||
fileLength,
|
fileLength,
|
||||||
didSaveToTmpPath
|
didSaveToTmpPath
|
||||||
}
|
}
|
||||||
} catch(error) {
|
} catch (error) {
|
||||||
// destroy all streams with error
|
// destroy all streams with error
|
||||||
encWriteStream.destroy()
|
encWriteStream.destroy()
|
||||||
writeStream?.destroy()
|
writeStream?.destroy()
|
||||||
@@ -425,10 +412,10 @@ export const encryptedStream = async(
|
|||||||
sha256Enc.destroy()
|
sha256Enc.destroy()
|
||||||
stream.destroy()
|
stream.destroy()
|
||||||
|
|
||||||
if(didSaveToTmpPath) {
|
if (didSaveToTmpPath) {
|
||||||
try {
|
try {
|
||||||
await fs.unlink(bodyPath!)
|
await fs.unlink(bodyPath!)
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
logger?.error({ err }, 'failed to save to tmp path')
|
logger?.error({ err }, 'failed to save to tmp path')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -451,17 +438,17 @@ const toSmallestChunkSize = (num: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type MediaDownloadOptions = {
|
export type MediaDownloadOptions = {
|
||||||
startByte?: number
|
startByte?: number
|
||||||
endByte?: number
|
endByte?: number
|
||||||
options?: AxiosRequestConfig<{}>
|
options?: AxiosRequestConfig<{}>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getUrlFromDirectPath = (directPath: string) => `https://${DEF_HOST}${directPath}`
|
export const getUrlFromDirectPath = (directPath: string) => `https://${DEF_HOST}${directPath}`
|
||||||
|
|
||||||
export const downloadContentFromMessage = async(
|
export const downloadContentFromMessage = async (
|
||||||
{ mediaKey, directPath, url }: DownloadableMessage,
|
{ mediaKey, directPath, url }: DownloadableMessage,
|
||||||
type: MediaType,
|
type: MediaType,
|
||||||
opts: MediaDownloadOptions = { }
|
opts: MediaDownloadOptions = {}
|
||||||
) => {
|
) => {
|
||||||
const downloadUrl = url || getUrlFromDirectPath(directPath!)
|
const downloadUrl = url || getUrlFromDirectPath(directPath!)
|
||||||
const keys = await getMediaKeys(mediaKey, type)
|
const keys = await getMediaKeys(mediaKey, type)
|
||||||
@@ -473,18 +460,18 @@ export const downloadContentFromMessage = async(
|
|||||||
* Decrypts and downloads an AES256-CBC encrypted file given the keys.
|
* Decrypts and downloads an AES256-CBC encrypted file given the keys.
|
||||||
* Assumes the SHA256 of the plaintext is appended to the end of the ciphertext
|
* Assumes the SHA256 of the plaintext is appended to the end of the ciphertext
|
||||||
* */
|
* */
|
||||||
export const downloadEncryptedContent = async(
|
export const downloadEncryptedContent = async (
|
||||||
downloadUrl: string,
|
downloadUrl: string,
|
||||||
{ cipherKey, iv }: MediaDecryptionKeyInfo,
|
{ cipherKey, iv }: MediaDecryptionKeyInfo,
|
||||||
{ startByte, endByte, options }: MediaDownloadOptions = { }
|
{ startByte, endByte, options }: MediaDownloadOptions = {}
|
||||||
) => {
|
) => {
|
||||||
let bytesFetched = 0
|
let bytesFetched = 0
|
||||||
let startChunk = 0
|
let startChunk = 0
|
||||||
let firstBlockIsIV = false
|
let firstBlockIsIV = false
|
||||||
// if a start byte is specified -- then we need to fetch the previous chunk as that will form the IV
|
// if a start byte is specified -- then we need to fetch the previous chunk as that will form the IV
|
||||||
if(startByte) {
|
if (startByte) {
|
||||||
const chunk = toSmallestChunkSize(startByte || 0)
|
const chunk = toSmallestChunkSize(startByte || 0)
|
||||||
if(chunk) {
|
if (chunk) {
|
||||||
startChunk = chunk - AES_CHUNK_SIZE
|
startChunk = chunk - AES_CHUNK_SIZE
|
||||||
bytesFetched = chunk
|
bytesFetched = chunk
|
||||||
|
|
||||||
@@ -495,33 +482,30 @@ export const downloadEncryptedContent = async(
|
|||||||
const endChunk = endByte ? toSmallestChunkSize(endByte || 0) + AES_CHUNK_SIZE : undefined
|
const endChunk = endByte ? toSmallestChunkSize(endByte || 0) + AES_CHUNK_SIZE : undefined
|
||||||
|
|
||||||
const headers: AxiosRequestConfig['headers'] = {
|
const headers: AxiosRequestConfig['headers'] = {
|
||||||
...options?.headers || { },
|
...(options?.headers || {}),
|
||||||
Origin: DEFAULT_ORIGIN,
|
Origin: DEFAULT_ORIGIN
|
||||||
}
|
}
|
||||||
if(startChunk || endChunk) {
|
if (startChunk || endChunk) {
|
||||||
headers.Range = `bytes=${startChunk}-`
|
headers.Range = `bytes=${startChunk}-`
|
||||||
if(endChunk) {
|
if (endChunk) {
|
||||||
headers.Range += endChunk
|
headers.Range += endChunk
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// download the message
|
// download the message
|
||||||
const fetched = await getHttpStream(
|
const fetched = await getHttpStream(downloadUrl, {
|
||||||
downloadUrl,
|
...(options || {}),
|
||||||
{
|
headers,
|
||||||
...options || { },
|
maxBodyLength: Infinity,
|
||||||
headers,
|
maxContentLength: Infinity
|
||||||
maxBodyLength: Infinity,
|
})
|
||||||
maxContentLength: Infinity,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
let remainingBytes = Buffer.from([])
|
let remainingBytes = Buffer.from([])
|
||||||
|
|
||||||
let aes: Crypto.Decipher
|
let aes: Crypto.Decipher
|
||||||
|
|
||||||
const pushBytes = (bytes: Buffer, push: (bytes: Buffer) => void) => {
|
const pushBytes = (bytes: Buffer, push: (bytes: Buffer) => void) => {
|
||||||
if(startByte || endByte) {
|
if (startByte || endByte) {
|
||||||
const start = bytesFetched >= startByte! ? undefined : Math.max(startByte! - bytesFetched, 0)
|
const start = bytesFetched >= startByte! ? undefined : Math.max(startByte! - bytesFetched, 0)
|
||||||
const end = bytesFetched + bytes.length < endByte! ? undefined : Math.max(endByte! - bytesFetched, 0)
|
const end = bytesFetched + bytes.length < endByte! ? undefined : Math.max(endByte! - bytesFetched, 0)
|
||||||
|
|
||||||
@@ -541,9 +525,9 @@ export const downloadEncryptedContent = async(
|
|||||||
remainingBytes = data.slice(decryptLength)
|
remainingBytes = data.slice(decryptLength)
|
||||||
data = data.slice(0, decryptLength)
|
data = data.slice(0, decryptLength)
|
||||||
|
|
||||||
if(!aes) {
|
if (!aes) {
|
||||||
let ivValue = iv
|
let ivValue = iv
|
||||||
if(firstBlockIsIV) {
|
if (firstBlockIsIV) {
|
||||||
ivValue = data.slice(0, AES_CHUNK_SIZE)
|
ivValue = data.slice(0, AES_CHUNK_SIZE)
|
||||||
data = data.slice(AES_CHUNK_SIZE)
|
data = data.slice(AES_CHUNK_SIZE)
|
||||||
}
|
}
|
||||||
@@ -551,16 +535,15 @@ export const downloadEncryptedContent = async(
|
|||||||
aes = Crypto.createDecipheriv('aes-256-cbc', cipherKey, ivValue)
|
aes = Crypto.createDecipheriv('aes-256-cbc', cipherKey, ivValue)
|
||||||
// if an end byte that is not EOF is specified
|
// if an end byte that is not EOF is specified
|
||||||
// stop auto padding (PKCS7) -- otherwise throws an error for decryption
|
// stop auto padding (PKCS7) -- otherwise throws an error for decryption
|
||||||
if(endByte) {
|
if (endByte) {
|
||||||
aes.setAutoPadding(false)
|
aes.setAutoPadding(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
pushBytes(aes.update(data), b => this.push(b))
|
pushBytes(aes.update(data), b => this.push(b))
|
||||||
callback()
|
callback()
|
||||||
} catch(error) {
|
} catch (error) {
|
||||||
callback(error)
|
callback(error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -568,10 +551,10 @@ export const downloadEncryptedContent = async(
|
|||||||
try {
|
try {
|
||||||
pushBytes(aes.final(), b => this.push(b))
|
pushBytes(aes.final(), b => this.push(b))
|
||||||
callback()
|
callback()
|
||||||
} catch(error) {
|
} catch (error) {
|
||||||
callback(error)
|
callback(error)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
return fetched.pipe(output, { end: true })
|
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 getExtension = (mimetype: string) => mimetype.split(';')[0].split('/')[1]
|
||||||
const type = Object.keys(message)[0] as MessageType
|
const type = Object.keys(message)[0] as MessageType
|
||||||
let extension: string
|
let extension: string
|
||||||
if(
|
if (type === 'locationMessage' || type === 'liveLocationMessage' || type === 'productMessage') {
|
||||||
type === 'locationMessage' ||
|
|
||||||
type === 'liveLocationMessage' ||
|
|
||||||
type === 'productMessage'
|
|
||||||
) {
|
|
||||||
extension = '.jpeg'
|
extension = '.jpeg'
|
||||||
} else {
|
} else {
|
||||||
const messageContent = message[type] as WAGenericMediaMessage
|
const messageContent = message[type] as WAGenericMediaMessage
|
||||||
@@ -596,18 +575,18 @@ export function extensionForMediaMessage(message: WAMessageContent) {
|
|||||||
|
|
||||||
export const getWAUploadToServer = (
|
export const getWAUploadToServer = (
|
||||||
{ customUploadHosts, fetchAgent, logger, options }: SocketConfig,
|
{ customUploadHosts, fetchAgent, logger, options }: SocketConfig,
|
||||||
refreshMediaConn: (force: boolean) => Promise<MediaConnInfo>,
|
refreshMediaConn: (force: boolean) => Promise<MediaConnInfo>
|
||||||
): WAMediaUploadFunction => {
|
): WAMediaUploadFunction => {
|
||||||
return async(stream, { mediaType, fileEncSha256B64, timeoutMs }) => {
|
return async (stream, { mediaType, fileEncSha256B64, timeoutMs }) => {
|
||||||
// send a query JSON to obtain the url & auth token to upload our media
|
// send a query JSON to obtain the url & auth token to upload our media
|
||||||
let uploadInfo = await refreshMediaConn(false)
|
let uploadInfo = await refreshMediaConn(false)
|
||||||
|
|
||||||
let urls: { mediaUrl: string, directPath: string } | undefined
|
let urls: { mediaUrl: string; directPath: string } | undefined
|
||||||
const hosts = [ ...customUploadHosts, ...uploadInfo.hosts ]
|
const hosts = [...customUploadHosts, ...uploadInfo.hosts]
|
||||||
|
|
||||||
fileEncSha256B64 = encodeBase64EncodedStringForUpload(fileEncSha256B64)
|
fileEncSha256B64 = encodeBase64EncodedStringForUpload(fileEncSha256B64)
|
||||||
|
|
||||||
for(const { hostname } of hosts) {
|
for (const { hostname } of hosts) {
|
||||||
logger.debug(`uploading to "${hostname}"`)
|
logger.debug(`uploading to "${hostname}"`)
|
||||||
|
|
||||||
const auth = encodeURIComponent(uploadInfo.auth) // the auth token
|
const auth = encodeURIComponent(uploadInfo.auth) // the auth token
|
||||||
@@ -615,27 +594,22 @@ export const getWAUploadToServer = (
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let result: any
|
let result: any
|
||||||
try {
|
try {
|
||||||
|
const body = await axios.post(url, stream, {
|
||||||
const body = await axios.post(
|
...options,
|
||||||
url,
|
headers: {
|
||||||
stream,
|
...(options.headers || {}),
|
||||||
{
|
'Content-Type': 'application/octet-stream',
|
||||||
...options,
|
Origin: DEFAULT_ORIGIN
|
||||||
headers: {
|
},
|
||||||
...options.headers || { },
|
httpsAgent: fetchAgent,
|
||||||
'Content-Type': 'application/octet-stream',
|
timeout: timeoutMs,
|
||||||
'Origin': DEFAULT_ORIGIN
|
responseType: 'json',
|
||||||
},
|
maxBodyLength: Infinity,
|
||||||
httpsAgent: fetchAgent,
|
maxContentLength: Infinity
|
||||||
timeout: timeoutMs,
|
})
|
||||||
responseType: 'json',
|
|
||||||
maxBodyLength: Infinity,
|
|
||||||
maxContentLength: Infinity,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
result = body.data
|
result = body.data
|
||||||
|
|
||||||
if(result?.url || result?.directPath) {
|
if (result?.url || result?.directPath) {
|
||||||
urls = {
|
urls = {
|
||||||
mediaUrl: result.url,
|
mediaUrl: result.url,
|
||||||
directPath: result.direct_path
|
directPath: result.direct_path
|
||||||
@@ -645,21 +619,21 @@ export const getWAUploadToServer = (
|
|||||||
uploadInfo = await refreshMediaConn(true)
|
uploadInfo = await refreshMediaConn(true)
|
||||||
throw new Error(`upload failed, reason: ${JSON.stringify(result)}`)
|
throw new Error(`upload failed, reason: ${JSON.stringify(result)}`)
|
||||||
}
|
}
|
||||||
} catch(error) {
|
} catch (error) {
|
||||||
if(axios.isAxiosError(error)) {
|
if (axios.isAxiosError(error)) {
|
||||||
result = error.response?.data
|
result = error.response?.data
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLast = hostname === hosts[uploadInfo.hosts.length - 1]?.hostname
|
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) {
|
if (!urls) {
|
||||||
throw new Boom(
|
throw new Boom('Media upload failed on all hosts', { statusCode: 500 })
|
||||||
'Media upload failed on all hosts',
|
|
||||||
{ statusCode: 500 }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return urls
|
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
|
* Generate a binary node that will request the phone to re-upload the media & return the newly uploaded URL
|
||||||
*/
|
*/
|
||||||
export const encryptMediaRetryRequest = async(
|
export const encryptMediaRetryRequest = async (key: proto.IMessageKey, mediaKey: Buffer | Uint8Array, meId: string) => {
|
||||||
key: proto.IMessageKey,
|
|
||||||
mediaKey: Buffer | Uint8Array,
|
|
||||||
meId: string
|
|
||||||
) => {
|
|
||||||
const recp: proto.IServerErrorReceipt = { stanzaId: key.id }
|
const recp: proto.IServerErrorReceipt = { stanzaId: key.id }
|
||||||
const recpBuffer = proto.ServerErrorReceipt.encode(recp).finish()
|
const recpBuffer = proto.ServerErrorReceipt.encode(recp).finish()
|
||||||
|
|
||||||
@@ -698,17 +668,17 @@ export const encryptMediaRetryRequest = async(
|
|||||||
// keeping it here to maintain parity with WA Web
|
// keeping it here to maintain parity with WA Web
|
||||||
{
|
{
|
||||||
tag: 'encrypt',
|
tag: 'encrypt',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: [
|
content: [
|
||||||
{ tag: 'enc_p', attrs: { }, content: ciphertext },
|
{ tag: 'enc_p', attrs: {}, content: ciphertext },
|
||||||
{ tag: 'enc_iv', attrs: { }, content: iv }
|
{ tag: 'enc_iv', attrs: {}, content: iv }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'rmr',
|
tag: 'rmr',
|
||||||
attrs: {
|
attrs: {
|
||||||
jid: key.remoteJid!,
|
jid: key.remoteJid!,
|
||||||
'from_me': (!!key.fromMe).toString(),
|
from_me: (!!key.fromMe).toString(),
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
participant: key.participant || undefined
|
participant: key.participant || undefined
|
||||||
}
|
}
|
||||||
@@ -732,17 +702,17 @@ export const decodeMediaRetryNode = (node: BinaryNode) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const errorNode = getBinaryNodeChild(node, 'error')
|
const errorNode = getBinaryNodeChild(node, 'error')
|
||||||
if(errorNode) {
|
if (errorNode) {
|
||||||
const errorCode = +errorNode.attrs.code
|
const errorCode = +errorNode.attrs.code
|
||||||
event.error = new Boom(
|
event.error = new Boom(`Failed to re-upload media (${errorCode})`, {
|
||||||
`Failed to re-upload media (${errorCode})`,
|
data: errorNode.attrs,
|
||||||
{ data: errorNode.attrs, statusCode: getStatusCodeForMediaRetry(errorCode) }
|
statusCode: getStatusCodeForMediaRetry(errorCode)
|
||||||
)
|
})
|
||||||
} else {
|
} else {
|
||||||
const encryptedInfoNode = getBinaryNodeChild(node, 'encrypt')
|
const encryptedInfoNode = getBinaryNodeChild(node, 'encrypt')
|
||||||
const ciphertext = getBinaryNodeChildBuffer(encryptedInfoNode, 'enc_p')
|
const ciphertext = getBinaryNodeChildBuffer(encryptedInfoNode, 'enc_p')
|
||||||
const iv = getBinaryNodeChildBuffer(encryptedInfoNode, 'enc_iv')
|
const iv = getBinaryNodeChildBuffer(encryptedInfoNode, 'enc_iv')
|
||||||
if(ciphertext && iv) {
|
if (ciphertext && iv) {
|
||||||
event.media = { ciphertext, iv }
|
event.media = { ciphertext, iv }
|
||||||
} else {
|
} else {
|
||||||
event.error = new Boom('Failed to re-upload media (missing ciphertext)', { statusCode: 404 })
|
event.error = new Boom('Failed to re-upload media (missing ciphertext)', { statusCode: 404 })
|
||||||
@@ -752,8 +722,8 @@ export const decodeMediaRetryNode = (node: BinaryNode) => {
|
|||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
export const decryptMediaRetryData = async(
|
export const decryptMediaRetryData = async (
|
||||||
{ ciphertext, iv }: { ciphertext: Uint8Array, iv: Uint8Array },
|
{ ciphertext, iv }: { ciphertext: Uint8Array; iv: Uint8Array },
|
||||||
mediaKey: Uint8Array,
|
mediaKey: Uint8Array,
|
||||||
msgId: string
|
msgId: string
|
||||||
) => {
|
) => {
|
||||||
@@ -768,5 +738,5 @@ const MEDIA_RETRY_STATUS_MAP = {
|
|||||||
[proto.MediaRetryNotification.ResultType.SUCCESS]: 200,
|
[proto.MediaRetryNotification.ResultType.SUCCESS]: 200,
|
||||||
[proto.MediaRetryNotification.ResultType.DECRYPTION_ERROR]: 412,
|
[proto.MediaRetryNotification.ResultType.DECRYPTION_ERROR]: 412,
|
||||||
[proto.MediaRetryNotification.ResultType.NOT_FOUND]: 404,
|
[proto.MediaRetryNotification.ResultType.NOT_FOUND]: 404,
|
||||||
[proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418,
|
[proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,7 @@ export const makeNoiseHandler = ({
|
|||||||
logger = logger.child({ class: 'ns' })
|
logger = logger.child({ class: 'ns' })
|
||||||
|
|
||||||
const authenticate = (data: Uint8Array) => {
|
const authenticate = (data: Uint8Array) => {
|
||||||
if(!isFinished) {
|
if (!isFinished) {
|
||||||
hash = sha256(Buffer.concat([hash, data]))
|
hash = sha256(Buffer.concat([hash, data]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ export const makeNoiseHandler = ({
|
|||||||
const iv = generateIV(isFinished ? readCounter : writeCounter)
|
const iv = generateIV(isFinished ? readCounter : writeCounter)
|
||||||
const result = aesDecryptGCM(ciphertext, decKey, iv, hash)
|
const result = aesDecryptGCM(ciphertext, decKey, iv, hash)
|
||||||
|
|
||||||
if(isFinished) {
|
if (isFinished) {
|
||||||
readCounter += 1
|
readCounter += 1
|
||||||
} else {
|
} else {
|
||||||
writeCounter += 1
|
writeCounter += 1
|
||||||
@@ -57,12 +57,12 @@ export const makeNoiseHandler = ({
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
const localHKDF = async(data: Uint8Array) => {
|
const localHKDF = async (data: Uint8Array) => {
|
||||||
const key = await hkdf(Buffer.from(data), 64, { salt, info: '' })
|
const key = await hkdf(Buffer.from(data), 64, { salt, info: '' })
|
||||||
return [key.slice(0, 32), key.slice(32)]
|
return [key.slice(0, 32), key.slice(32)]
|
||||||
}
|
}
|
||||||
|
|
||||||
const mixIntoKey = async(data: Uint8Array) => {
|
const mixIntoKey = async (data: Uint8Array) => {
|
||||||
const [write, read] = await localHKDF(data)
|
const [write, read] = await localHKDF(data)
|
||||||
salt = write
|
salt = write
|
||||||
encKey = read
|
encKey = read
|
||||||
@@ -71,7 +71,7 @@ export const makeNoiseHandler = ({
|
|||||||
writeCounter = 0
|
writeCounter = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const finishInit = async() => {
|
const finishInit = async () => {
|
||||||
const [write, read] = await localHKDF(new Uint8Array(0))
|
const [write, read] = await localHKDF(new Uint8Array(0))
|
||||||
encKey = write
|
encKey = write
|
||||||
decKey = read
|
decKey = read
|
||||||
@@ -102,7 +102,7 @@ export const makeNoiseHandler = ({
|
|||||||
authenticate,
|
authenticate,
|
||||||
mixIntoKey,
|
mixIntoKey,
|
||||||
finishInit,
|
finishInit,
|
||||||
processHandshake: async({ serverHello }: proto.HandshakeMessage, noiseKey: KeyPair) => {
|
processHandshake: async ({ serverHello }: proto.HandshakeMessage, noiseKey: KeyPair) => {
|
||||||
authenticate(serverHello!.ephemeral!)
|
authenticate(serverHello!.ephemeral!)
|
||||||
await mixIntoKey(Curve.sharedKey(privateKey, serverHello!.ephemeral!))
|
await mixIntoKey(Curve.sharedKey(privateKey, serverHello!.ephemeral!))
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ export const makeNoiseHandler = ({
|
|||||||
|
|
||||||
const { issuerSerial } = proto.CertChain.NoiseCertificate.Details.decode(certIntermediate!.details!)
|
const { issuerSerial } = proto.CertChain.NoiseCertificate.Details.decode(certIntermediate!.details!)
|
||||||
|
|
||||||
if(issuerSerial !== WA_CERT_DETAILS.SERIAL) {
|
if (issuerSerial !== WA_CERT_DETAILS.SERIAL) {
|
||||||
throw new Boom('certification match failed', { statusCode: 400 })
|
throw new Boom('certification match failed', { statusCode: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,13 +125,13 @@ export const makeNoiseHandler = ({
|
|||||||
return keyEnc
|
return keyEnc
|
||||||
},
|
},
|
||||||
encodeFrame: (data: Buffer | Uint8Array) => {
|
encodeFrame: (data: Buffer | Uint8Array) => {
|
||||||
if(isFinished) {
|
if (isFinished) {
|
||||||
data = encrypt(data)
|
data = encrypt(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
let header: Buffer
|
let header: Buffer
|
||||||
|
|
||||||
if(routingInfo) {
|
if (routingInfo) {
|
||||||
header = Buffer.alloc(7)
|
header = Buffer.alloc(7)
|
||||||
header.write('ED', 0, 'utf8')
|
header.write('ED', 0, 'utf8')
|
||||||
header.writeUint8(0, 2)
|
header.writeUint8(0, 2)
|
||||||
@@ -146,7 +146,7 @@ export const makeNoiseHandler = ({
|
|||||||
const introSize = sentIntro ? 0 : header.length
|
const introSize = sentIntro ? 0 : header.length
|
||||||
const frame = Buffer.alloc(introSize + 3 + data.byteLength)
|
const frame = Buffer.alloc(introSize + 3 + data.byteLength)
|
||||||
|
|
||||||
if(!sentIntro) {
|
if (!sentIntro) {
|
||||||
frame.set(header)
|
frame.set(header)
|
||||||
sentIntro = true
|
sentIntro = true
|
||||||
}
|
}
|
||||||
@@ -157,26 +157,26 @@ export const makeNoiseHandler = ({
|
|||||||
|
|
||||||
return frame
|
return frame
|
||||||
},
|
},
|
||||||
decodeFrame: async(newData: Buffer | Uint8Array, onFrame: (buff: Uint8Array | BinaryNode) => void) => {
|
decodeFrame: async (newData: Buffer | Uint8Array, onFrame: (buff: Uint8Array | BinaryNode) => void) => {
|
||||||
// the binary protocol uses its own framing mechanism
|
// the binary protocol uses its own framing mechanism
|
||||||
// on top of the WS frames
|
// on top of the WS frames
|
||||||
// so we get this data and separate out the frames
|
// so we get this data and separate out the frames
|
||||||
const getBytesSize = () => {
|
const getBytesSize = () => {
|
||||||
if(inBytes.length >= 3) {
|
if (inBytes.length >= 3) {
|
||||||
return (inBytes.readUInt8() << 16) | inBytes.readUInt16BE(1)
|
return (inBytes.readUInt8() << 16) | inBytes.readUInt16BE(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inBytes = Buffer.concat([ inBytes, newData ])
|
inBytes = Buffer.concat([inBytes, newData])
|
||||||
|
|
||||||
logger.trace(`recv ${newData.length} bytes, total recv ${inBytes.length} bytes`)
|
logger.trace(`recv ${newData.length} bytes, total recv ${inBytes.length} bytes`)
|
||||||
|
|
||||||
let size = getBytesSize()
|
let size = getBytesSize()
|
||||||
while(size && inBytes.length >= size + 3) {
|
while (size && inBytes.length >= size + 3) {
|
||||||
let frame: Uint8Array | BinaryNode = inBytes.slice(3, size + 3)
|
let frame: Uint8Array | BinaryNode = inBytes.slice(3, size + 3)
|
||||||
inBytes = inBytes.slice(size + 3)
|
inBytes = inBytes.slice(size + 3)
|
||||||
|
|
||||||
if(isFinished) {
|
if (isFinished) {
|
||||||
const result = decrypt(frame)
|
const result = decrypt(frame)
|
||||||
frame = await decodeBinaryNode(result)
|
frame = await decodeBinaryNode(result)
|
||||||
}
|
}
|
||||||
@@ -188,4 +188,4 @@ export const makeNoiseHandler = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
import { AxiosRequestConfig } from 'axios'
|
import { AxiosRequestConfig } from 'axios'
|
||||||
import { proto } from '../../WAProto'
|
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 { getContentType, normalizeMessageContent } from '../Utils/messages'
|
||||||
import { areJidsSameUser, isJidBroadcast, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary'
|
import { areJidsSameUser, isJidBroadcast, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary'
|
||||||
import { aesDecryptGCM, hmacSign } from './crypto'
|
import { aesDecryptGCM, hmacSign } from './crypto'
|
||||||
@@ -25,9 +36,7 @@ const REAL_MSG_STUB_TYPES = new Set([
|
|||||||
WAMessageStubType.CALL_MISSED_VOICE
|
WAMessageStubType.CALL_MISSED_VOICE
|
||||||
])
|
])
|
||||||
|
|
||||||
const REAL_MSG_REQ_ME_STUB_TYPES = new Set([
|
const REAL_MSG_REQ_ME_STUB_TYPES = new Set([WAMessageStubType.GROUP_PARTICIPANT_ADD])
|
||||||
WAMessageStubType.GROUP_PARTICIPANT_ADD
|
|
||||||
])
|
|
||||||
|
|
||||||
/** Cleans a received message to further processing */
|
/** Cleans a received message to further processing */
|
||||||
export const cleanMessage = (message: proto.IWebMessageInfo, meId: string) => {
|
export const cleanMessage = (message: proto.IWebMessageInfo, meId: string) => {
|
||||||
@@ -36,25 +45,25 @@ export const cleanMessage = (message: proto.IWebMessageInfo, meId: string) => {
|
|||||||
message.key.participant = message.key.participant ? jidNormalizedUser(message.key.participant) : undefined
|
message.key.participant = message.key.participant ? jidNormalizedUser(message.key.participant) : undefined
|
||||||
const content = normalizeMessageContent(message.message)
|
const content = normalizeMessageContent(message.message)
|
||||||
// if the message has a reaction, ensure fromMe & remoteJid are from our perspective
|
// if the message has a reaction, ensure fromMe & remoteJid are from our perspective
|
||||||
if(content?.reactionMessage) {
|
if (content?.reactionMessage) {
|
||||||
normaliseKey(content.reactionMessage.key!)
|
normaliseKey(content.reactionMessage.key!)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(content?.pollUpdateMessage) {
|
if (content?.pollUpdateMessage) {
|
||||||
normaliseKey(content.pollUpdateMessage.pollCreationMessageKey!)
|
normaliseKey(content.pollUpdateMessage.pollCreationMessageKey!)
|
||||||
}
|
}
|
||||||
|
|
||||||
function normaliseKey(msgKey: proto.IMessageKey) {
|
function normaliseKey(msgKey: proto.IMessageKey) {
|
||||||
// if the reaction is from another user
|
// if the reaction is from another user
|
||||||
// we've to correctly map the key to this user's perspective
|
// we've to correctly map the key to this user's perspective
|
||||||
if(!message.key.fromMe) {
|
if (!message.key.fromMe) {
|
||||||
// if the sender believed the message being reacted to is not from them
|
// if the sender believed the message being reacted to is not from them
|
||||||
// we've to correct the key to be from them, or some other participant
|
// we've to correct the key to be from them, or some other participant
|
||||||
msgKey.fromMe = !msgKey.fromMe
|
msgKey.fromMe = !msgKey.fromMe
|
||||||
? areJidsSameUser(msgKey.participant || msgKey.remoteJid!, meId)
|
? 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
|
// fromMe automatically becomes false
|
||||||
: false
|
false
|
||||||
// set the remoteJid to being the same as the chat the message came from
|
// set the remoteJid to being the same as the chat the message came from
|
||||||
msgKey.remoteJid = message.key.remoteJid
|
msgKey.remoteJid = message.key.remoteJid
|
||||||
// set participant of the message
|
// set participant of the message
|
||||||
@@ -67,33 +76,26 @@ export const isRealMessage = (message: proto.IWebMessageInfo, meId: string) => {
|
|||||||
const normalizedContent = normalizeMessageContent(message.message)
|
const normalizedContent = normalizeMessageContent(message.message)
|
||||||
const hasSomeContent = !!getContentType(normalizedContent)
|
const hasSomeContent = !!getContentType(normalizedContent)
|
||||||
return (
|
return (
|
||||||
!!normalizedContent
|
(!!normalizedContent ||
|
||||||
|| REAL_MSG_STUB_TYPES.has(message.messageStubType!)
|
REAL_MSG_STUB_TYPES.has(message.messageStubType!) ||
|
||||||
|| (
|
(REAL_MSG_REQ_ME_STUB_TYPES.has(message.messageStubType!) &&
|
||||||
REAL_MSG_REQ_ME_STUB_TYPES.has(message.messageStubType!)
|
message.messageStubParameters?.some(p => areJidsSameUser(meId, p)))) &&
|
||||||
&& 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
|
!message.key.fromMe && !message.messageStubType
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the ID of the chat from the given key.
|
* Get the ID of the chat from the given key.
|
||||||
* Typically -- that'll be the remoteJid, but for broadcasts, it'll be the participant
|
* Typically -- that'll be the remoteJid, but for broadcasts, it'll be the participant
|
||||||
*/
|
*/
|
||||||
export const getChatId = ({ remoteJid, participant, fromMe }: proto.IMessageKey) => {
|
export const getChatId = ({ remoteJid, participant, fromMe }: proto.IMessageKey) => {
|
||||||
if(
|
if (isJidBroadcast(remoteJid!) && !isJidStatusBroadcast(remoteJid!) && !fromMe) {
|
||||||
isJidBroadcast(remoteJid!)
|
|
||||||
&& !isJidStatusBroadcast(remoteJid!)
|
|
||||||
&& !fromMe
|
|
||||||
) {
|
|
||||||
return participant!
|
return participant!
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,22 +121,15 @@ type PollContext = {
|
|||||||
*/
|
*/
|
||||||
export function decryptPollVote(
|
export function decryptPollVote(
|
||||||
{ encPayload, encIv }: proto.Message.IPollEncValue,
|
{ 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(pollMsgId),
|
toBinary(pollCreatorJid),
|
||||||
toBinary(pollCreatorJid),
|
toBinary(voterJid),
|
||||||
toBinary(voterJid),
|
toBinary('Poll Vote'),
|
||||||
toBinary('Poll Vote'),
|
new Uint8Array([1])
|
||||||
new Uint8Array([1])
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
const key0 = hmacSign(pollEncKey, new Uint8Array(32), 'sha256')
|
const key0 = hmacSign(pollEncKey, new Uint8Array(32), 'sha256')
|
||||||
const decKey = hmacSign(sign, key0, 'sha256')
|
const decKey = hmacSign(sign, key0, 'sha256')
|
||||||
@@ -148,17 +143,9 @@ export function decryptPollVote(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const processMessage = async(
|
const processMessage = async (
|
||||||
message: proto.IWebMessageInfo,
|
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 meId = creds.me!.id
|
||||||
const { accountSettings } = creds
|
const { accountSettings } = creds
|
||||||
@@ -166,11 +153,11 @@ const processMessage = async(
|
|||||||
const chat: Partial<Chat> = { id: jidNormalizedUser(getChatId(message.key)) }
|
const chat: Partial<Chat> = { id: jidNormalizedUser(getChatId(message.key)) }
|
||||||
const isRealMsg = isRealMessage(message, meId)
|
const isRealMsg = isRealMessage(message, meId)
|
||||||
|
|
||||||
if(isRealMsg) {
|
if (isRealMsg) {
|
||||||
chat.messages = [{ message }]
|
chat.messages = [{ message }]
|
||||||
chat.conversationTimestamp = toNumber(message.messageTimestamp)
|
chat.conversationTimestamp = toNumber(message.messageTimestamp)
|
||||||
// only increment unread count if not CIPHERTEXT and from another person
|
// only increment unread count if not CIPHERTEXT and from another person
|
||||||
if(shouldIncrementChatUnread(message)) {
|
if (shouldIncrementChatUnread(message)) {
|
||||||
chat.unreadCount = (chat.unreadCount || 0) + 1
|
chat.unreadCount = (chat.unreadCount || 0) + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,63 +166,56 @@ const processMessage = async(
|
|||||||
|
|
||||||
// unarchive chat if it's a real message, or someone reacted to our message
|
// unarchive chat if it's a real message, or someone reacted to our message
|
||||||
// and we've the unarchive chats setting on
|
// and we've the unarchive chats setting on
|
||||||
if(
|
if ((isRealMsg || content?.reactionMessage?.key?.fromMe) && accountSettings?.unarchiveChats) {
|
||||||
(isRealMsg || content?.reactionMessage?.key?.fromMe)
|
|
||||||
&& accountSettings?.unarchiveChats
|
|
||||||
) {
|
|
||||||
chat.archived = false
|
chat.archived = false
|
||||||
chat.readOnly = false
|
chat.readOnly = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const protocolMsg = content?.protocolMessage
|
const protocolMsg = content?.protocolMessage
|
||||||
if(protocolMsg) {
|
if (protocolMsg) {
|
||||||
switch (protocolMsg.type) {
|
switch (protocolMsg.type) {
|
||||||
case proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION:
|
case proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION:
|
||||||
const histNotification = protocolMsg.historySyncNotification!
|
const histNotification = protocolMsg.historySyncNotification!
|
||||||
const process = shouldProcessHistoryMsg
|
const process = shouldProcessHistoryMsg
|
||||||
const isLatest = !creds.processedHistoryMessages?.length
|
const isLatest = !creds.processedHistoryMessages?.length
|
||||||
|
|
||||||
logger?.info({
|
logger?.info(
|
||||||
histNotification,
|
{
|
||||||
process,
|
histNotification,
|
||||||
id: message.key.id,
|
process,
|
||||||
isLatest,
|
id: message.key.id,
|
||||||
}, 'got history notification')
|
isLatest
|
||||||
|
},
|
||||||
|
'got history notification'
|
||||||
|
)
|
||||||
|
|
||||||
if(process) {
|
if (process) {
|
||||||
if(histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND) {
|
if (histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND) {
|
||||||
ev.emit('creds.update', {
|
ev.emit('creds.update', {
|
||||||
processedHistoryMessages: [
|
processedHistoryMessages: [
|
||||||
...(creds.processedHistoryMessages || []),
|
...(creds.processedHistoryMessages || []),
|
||||||
{ key: message.key, messageTimestamp: message.messageTimestamp }
|
{ key: message.key, messageTimestamp: message.messageTimestamp }
|
||||||
]
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await downloadAndProcessHistorySyncNotification(histNotification, options)
|
||||||
|
|
||||||
|
ev.emit('messaging-history.set', {
|
||||||
|
...data,
|
||||||
|
isLatest: histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND ? isLatest : undefined,
|
||||||
|
peerDataRequestSessionId: histNotification.peerDataRequestSessionId
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await downloadAndProcessHistorySyncNotification(
|
break
|
||||||
histNotification,
|
case proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE:
|
||||||
options
|
const keys = protocolMsg.appStateSyncKeyShare!.keys
|
||||||
)
|
if (keys?.length) {
|
||||||
|
let newAppStateSyncKeyId = ''
|
||||||
ev.emit('messaging-history.set', {
|
await keyStore.transaction(async () => {
|
||||||
...data,
|
|
||||||
isLatest:
|
|
||||||
histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND
|
|
||||||
? isLatest
|
|
||||||
: undefined,
|
|
||||||
peerDataRequestSessionId: histNotification.peerDataRequestSessionId
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
case proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE:
|
|
||||||
const keys = protocolMsg.appStateSyncKeyShare!.keys
|
|
||||||
if(keys?.length) {
|
|
||||||
let newAppStateSyncKeyId = ''
|
|
||||||
await keyStore.transaction(
|
|
||||||
async() => {
|
|
||||||
const newKeys: string[] = []
|
const newKeys: string[] = []
|
||||||
for(const { keyData, keyId } of keys) {
|
for (const { keyData, keyId } of keys) {
|
||||||
const strKeyId = Buffer.from(keyId!.keyId!).toString('base64')
|
const strKeyId = Buffer.from(keyId!.keyId!).toString('base64')
|
||||||
newKeys.push(strKeyId)
|
newKeys.push(strKeyId)
|
||||||
|
|
||||||
@@ -244,65 +224,59 @@ const processMessage = async(
|
|||||||
newAppStateSyncKeyId = strKeyId
|
newAppStateSyncKeyId = strKeyId
|
||||||
}
|
}
|
||||||
|
|
||||||
logger?.info(
|
logger?.info({ newAppStateSyncKeyId, newKeys }, 'injecting new app state sync keys')
|
||||||
{ newAppStateSyncKeyId, newKeys },
|
})
|
||||||
'injecting new app state sync keys'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
ev.emit('creds.update', { myAppStateKeyId: newAppStateSyncKeyId })
|
ev.emit('creds.update', { myAppStateKeyId: newAppStateSyncKeyId })
|
||||||
} else {
|
} else {
|
||||||
logger?.info({ protocolMsg }, 'recv app state sync with 0 keys')
|
logger?.info({ protocolMsg }, 'recv app state sync with 0 keys')
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
case proto.Message.ProtocolMessage.Type.REVOKE:
|
|
||||||
ev.emit('messages.update', [
|
|
||||||
{
|
|
||||||
key: {
|
|
||||||
...message.key,
|
|
||||||
id: protocolMsg.key!.id
|
|
||||||
},
|
|
||||||
update: { message: null, messageStubType: WAMessageStubType.REVOKE, key: message.key }
|
|
||||||
}
|
}
|
||||||
])
|
|
||||||
break
|
|
||||||
case proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING:
|
|
||||||
Object.assign(chat, {
|
|
||||||
ephemeralSettingTimestamp: toNumber(message.messageTimestamp),
|
|
||||||
ephemeralExpiration: protocolMsg.ephemeralExpiration || null
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE:
|
|
||||||
const response = protocolMsg.peerDataOperationRequestResponseMessage!
|
|
||||||
if(response) {
|
|
||||||
placeholderResendCache?.del(response.stanzaId!)
|
|
||||||
// TODO: IMPLEMENT HISTORY SYNC ETC (sticker uploads etc.).
|
|
||||||
const { peerDataOperationResult } = response
|
|
||||||
for(const result of peerDataOperationResult!) {
|
|
||||||
const { placeholderMessageResendResponse: retryResponse } = result
|
|
||||||
//eslint-disable-next-line max-depth
|
|
||||||
if(retryResponse) {
|
|
||||||
const webMessageInfo = proto.WebMessageInfo.decode(retryResponse.webMessageInfoBytes!)
|
|
||||||
// wait till another upsert event is available, don't want it to be part of the PDO response message
|
|
||||||
setTimeout(() => {
|
|
||||||
ev.emit('messages.upsert', {
|
|
||||||
messages: [webMessageInfo],
|
|
||||||
type: 'notify',
|
|
||||||
requestId: response.stanzaId!
|
|
||||||
})
|
|
||||||
}, 500)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case proto.Message.ProtocolMessage.Type.MESSAGE_EDIT:
|
break
|
||||||
ev.emit(
|
case proto.Message.ProtocolMessage.Type.REVOKE:
|
||||||
'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
|
||||||
|
},
|
||||||
|
update: { message: null, messageStubType: WAMessageStubType.REVOKE, key: message.key }
|
||||||
|
}
|
||||||
|
])
|
||||||
|
break
|
||||||
|
case proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING:
|
||||||
|
Object.assign(chat, {
|
||||||
|
ephemeralSettingTimestamp: toNumber(message.messageTimestamp),
|
||||||
|
ephemeralExpiration: protocolMsg.ephemeralExpiration || null
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE:
|
||||||
|
const response = protocolMsg.peerDataOperationRequestResponseMessage!
|
||||||
|
if (response) {
|
||||||
|
placeholderResendCache?.del(response.stanzaId!)
|
||||||
|
// TODO: IMPLEMENT HISTORY SYNC ETC (sticker uploads etc.).
|
||||||
|
const { peerDataOperationResult } = response
|
||||||
|
for (const result of peerDataOperationResult!) {
|
||||||
|
const { placeholderMessageResendResponse: retryResponse } = result
|
||||||
|
//eslint-disable-next-line max-depth
|
||||||
|
if (retryResponse) {
|
||||||
|
const webMessageInfo = proto.WebMessageInfo.decode(retryResponse.webMessageInfoBytes!)
|
||||||
|
// wait till another upsert event is available, don't want it to be part of the PDO response message
|
||||||
|
setTimeout(() => {
|
||||||
|
ev.emit('messages.upsert', {
|
||||||
|
messages: [webMessageInfo],
|
||||||
|
type: 'notify',
|
||||||
|
requestId: response.stanzaId!
|
||||||
|
})
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case proto.Message.ProtocolMessage.Type.MESSAGE_EDIT:
|
||||||
|
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 },
|
key: { ...message.key, id: protocolMsg.key?.id },
|
||||||
update: {
|
update: {
|
||||||
message: {
|
message: {
|
||||||
@@ -315,26 +289,26 @@ const processMessage = async(
|
|||||||
: message.messageTimestamp
|
: message.messageTimestamp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
])
|
||||||
)
|
break
|
||||||
break
|
|
||||||
}
|
}
|
||||||
} else if(content?.reactionMessage) {
|
} else if (content?.reactionMessage) {
|
||||||
const reaction: proto.IReaction = {
|
const reaction: proto.IReaction = {
|
||||||
...content.reactionMessage,
|
...content.reactionMessage,
|
||||||
key: message.key,
|
key: message.key
|
||||||
}
|
}
|
||||||
ev.emit('messages.reaction', [{
|
ev.emit('messages.reaction', [
|
||||||
reaction,
|
{
|
||||||
key: content.reactionMessage?.key!,
|
reaction,
|
||||||
}])
|
key: content.reactionMessage?.key!
|
||||||
} else if(message.messageStubType) {
|
}
|
||||||
|
])
|
||||||
|
} else if (message.messageStubType) {
|
||||||
const jid = message.key?.remoteJid!
|
const jid = message.key?.remoteJid!
|
||||||
//let actor = whatsappID (message.participant)
|
//let actor = whatsappID (message.participant)
|
||||||
let participants: string[]
|
let participants: string[]
|
||||||
const emitParticipantsUpdate = (action: ParticipantAction) => (
|
const emitParticipantsUpdate = (action: ParticipantAction) =>
|
||||||
ev.emit('group-participants.update', { id: jid, author: message.participant!, participants, action })
|
ev.emit('group-participants.update', { id: jid, author: message.participant!, participants, action })
|
||||||
)
|
|
||||||
const emitGroupUpdate = (update: Partial<GroupMetadata>) => {
|
const emitGroupUpdate = (update: Partial<GroupMetadata>) => {
|
||||||
ev.emit('groups.update', [{ id: jid, ...update, author: message.participant ?? undefined }])
|
ev.emit('groups.update', [{ id: jid, ...update, author: message.participant ?? undefined }])
|
||||||
}
|
}
|
||||||
@@ -346,76 +320,75 @@ const processMessage = async(
|
|||||||
const participantsIncludesMe = () => participants.find(jid => areJidsSameUser(meId, jid))
|
const participantsIncludesMe = () => participants.find(jid => areJidsSameUser(meId, jid))
|
||||||
|
|
||||||
switch (message.messageStubType) {
|
switch (message.messageStubType) {
|
||||||
case WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER:
|
case WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER:
|
||||||
participants = message.messageStubParameters || []
|
participants = message.messageStubParameters || []
|
||||||
emitParticipantsUpdate('modify')
|
emitParticipantsUpdate('modify')
|
||||||
break
|
break
|
||||||
case WAMessageStubType.GROUP_PARTICIPANT_LEAVE:
|
case WAMessageStubType.GROUP_PARTICIPANT_LEAVE:
|
||||||
case WAMessageStubType.GROUP_PARTICIPANT_REMOVE:
|
case WAMessageStubType.GROUP_PARTICIPANT_REMOVE:
|
||||||
participants = message.messageStubParameters || []
|
participants = message.messageStubParameters || []
|
||||||
emitParticipantsUpdate('remove')
|
emitParticipantsUpdate('remove')
|
||||||
// mark the chat read only if you left the group
|
// mark the chat read only if you left the group
|
||||||
if(participantsIncludesMe()) {
|
if (participantsIncludesMe()) {
|
||||||
chat.readOnly = true
|
chat.readOnly = true
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
case WAMessageStubType.GROUP_PARTICIPANT_ADD:
|
case WAMessageStubType.GROUP_PARTICIPANT_ADD:
|
||||||
case WAMessageStubType.GROUP_PARTICIPANT_INVITE:
|
case WAMessageStubType.GROUP_PARTICIPANT_INVITE:
|
||||||
case WAMessageStubType.GROUP_PARTICIPANT_ADD_REQUEST_JOIN:
|
case WAMessageStubType.GROUP_PARTICIPANT_ADD_REQUEST_JOIN:
|
||||||
participants = message.messageStubParameters || []
|
participants = message.messageStubParameters || []
|
||||||
if(participantsIncludesMe()) {
|
if (participantsIncludesMe()) {
|
||||||
chat.readOnly = false
|
chat.readOnly = false
|
||||||
}
|
}
|
||||||
|
|
||||||
emitParticipantsUpdate('add')
|
emitParticipantsUpdate('add')
|
||||||
break
|
break
|
||||||
case WAMessageStubType.GROUP_PARTICIPANT_DEMOTE:
|
case WAMessageStubType.GROUP_PARTICIPANT_DEMOTE:
|
||||||
participants = message.messageStubParameters || []
|
participants = message.messageStubParameters || []
|
||||||
emitParticipantsUpdate('demote')
|
emitParticipantsUpdate('demote')
|
||||||
break
|
break
|
||||||
case WAMessageStubType.GROUP_PARTICIPANT_PROMOTE:
|
case WAMessageStubType.GROUP_PARTICIPANT_PROMOTE:
|
||||||
participants = message.messageStubParameters || []
|
participants = message.messageStubParameters || []
|
||||||
emitParticipantsUpdate('promote')
|
emitParticipantsUpdate('promote')
|
||||||
break
|
break
|
||||||
case WAMessageStubType.GROUP_CHANGE_ANNOUNCE:
|
case WAMessageStubType.GROUP_CHANGE_ANNOUNCE:
|
||||||
const announceValue = message.messageStubParameters?.[0]
|
const announceValue = message.messageStubParameters?.[0]
|
||||||
emitGroupUpdate({ announce: announceValue === 'true' || announceValue === 'on' })
|
emitGroupUpdate({ announce: announceValue === 'true' || announceValue === 'on' })
|
||||||
break
|
break
|
||||||
case WAMessageStubType.GROUP_CHANGE_RESTRICT:
|
case WAMessageStubType.GROUP_CHANGE_RESTRICT:
|
||||||
const restrictValue = message.messageStubParameters?.[0]
|
const restrictValue = message.messageStubParameters?.[0]
|
||||||
emitGroupUpdate({ restrict: restrictValue === 'true' || restrictValue === 'on' })
|
emitGroupUpdate({ restrict: restrictValue === 'true' || restrictValue === 'on' })
|
||||||
break
|
break
|
||||||
case WAMessageStubType.GROUP_CHANGE_SUBJECT:
|
case WAMessageStubType.GROUP_CHANGE_SUBJECT:
|
||||||
const name = message.messageStubParameters?.[0]
|
const name = message.messageStubParameters?.[0]
|
||||||
chat.name = name
|
chat.name = name
|
||||||
emitGroupUpdate({ subject: name })
|
emitGroupUpdate({ subject: name })
|
||||||
break
|
break
|
||||||
case WAMessageStubType.GROUP_CHANGE_DESCRIPTION:
|
case WAMessageStubType.GROUP_CHANGE_DESCRIPTION:
|
||||||
const description = message.messageStubParameters?.[0]
|
const description = message.messageStubParameters?.[0]
|
||||||
chat.description = description
|
chat.description = description
|
||||||
emitGroupUpdate({ desc: description })
|
emitGroupUpdate({ desc: description })
|
||||||
break
|
break
|
||||||
case WAMessageStubType.GROUP_CHANGE_INVITE_LINK:
|
case WAMessageStubType.GROUP_CHANGE_INVITE_LINK:
|
||||||
const code = message.messageStubParameters?.[0]
|
const code = message.messageStubParameters?.[0]
|
||||||
emitGroupUpdate({ inviteCode: code })
|
emitGroupUpdate({ inviteCode: code })
|
||||||
break
|
break
|
||||||
case WAMessageStubType.GROUP_MEMBER_ADD_MODE:
|
case WAMessageStubType.GROUP_MEMBER_ADD_MODE:
|
||||||
const memberAddValue = message.messageStubParameters?.[0]
|
const memberAddValue = message.messageStubParameters?.[0]
|
||||||
emitGroupUpdate({ memberAddMode: memberAddValue === 'all_member_add' })
|
emitGroupUpdate({ memberAddMode: memberAddValue === 'all_member_add' })
|
||||||
break
|
break
|
||||||
case WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE:
|
case WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE:
|
||||||
const approvalMode = message.messageStubParameters?.[0]
|
const approvalMode = message.messageStubParameters?.[0]
|
||||||
emitGroupUpdate({ joinApprovalMode: approvalMode === 'on' })
|
emitGroupUpdate({ joinApprovalMode: approvalMode === 'on' })
|
||||||
break
|
break
|
||||||
case WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD:
|
case WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD:
|
||||||
const participant = message.messageStubParameters?.[0] as string
|
const participant = message.messageStubParameters?.[0] as string
|
||||||
const action = message.messageStubParameters?.[1] as RequestJoinAction
|
const action = message.messageStubParameters?.[1] as RequestJoinAction
|
||||||
const method = message.messageStubParameters?.[2] as RequestJoinMethod
|
const method = message.messageStubParameters?.[2] as RequestJoinMethod
|
||||||
emitGroupRequestJoin(participant, action, method)
|
emitGroupRequestJoin(participant, action, method)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
} /* else if(content?.pollUpdateMessage) {
|
} /* else if(content?.pollUpdateMessage) {
|
||||||
const creationMsgKey = content.pollUpdateMessage.pollCreationMessageKey!
|
const creationMsgKey = content.pollUpdateMessage.pollCreationMessageKey!
|
||||||
// we need to fetch the poll creation message to get the poll enc key
|
// we need to fetch the poll creation message to get the poll enc key
|
||||||
@@ -466,7 +439,7 @@ const processMessage = async(
|
|||||||
}
|
}
|
||||||
} */
|
} */
|
||||||
|
|
||||||
if(Object.keys(chat).length > 1) {
|
if (Object.keys(chat).length > 1) {
|
||||||
ev.emit('chats.update', [chat])
|
ev.emit('chats.update', [chat])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,39 @@
|
|||||||
import { chunk } from 'lodash'
|
import { chunk } from 'lodash'
|
||||||
import { KEY_BUNDLE_TYPE } from '../Defaults'
|
import { KEY_BUNDLE_TYPE } from '../Defaults'
|
||||||
import { SignalRepository } from '../Types'
|
import { SignalRepository } from '../Types'
|
||||||
import { AuthenticationCreds, AuthenticationState, KeyPair, SignalIdentity, SignalKeyStore, SignedKeyPair } from '../Types/Auth'
|
import {
|
||||||
import { assertNodeErrorFree, BinaryNode, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildUInt, jidDecode, JidWithDevice, S_WHATSAPP_NET } from '../WABinary'
|
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 { DeviceListData, ParsedDeviceInfo, USyncQueryResultList } from '../WAUSync'
|
||||||
import { Curve, generateSignalPubKey } from './crypto'
|
import { Curve, generateSignalPubKey } from './crypto'
|
||||||
import { encodeBigEndian } from './generics'
|
import { encodeBigEndian } from './generics'
|
||||||
|
|
||||||
export const createSignalIdentity = (
|
export const createSignalIdentity = (wid: string, accountSignatureKey: Uint8Array): SignalIdentity => {
|
||||||
wid: string,
|
|
||||||
accountSignatureKey: Uint8Array
|
|
||||||
): SignalIdentity => {
|
|
||||||
return {
|
return {
|
||||||
identifier: { name: wid, deviceId: 0 },
|
identifier: { name: wid, deviceId: 0 },
|
||||||
identifierKey: generateSignalPubKey(accountSignatureKey)
|
identifierKey: generateSignalPubKey(accountSignatureKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPreKeys = async({ get }: SignalKeyStore, min: number, limit: number) => {
|
export const getPreKeys = async ({ get }: SignalKeyStore, min: number, limit: number) => {
|
||||||
const idList: string[] = []
|
const idList: string[] = []
|
||||||
for(let id = min; id < limit;id++) {
|
for (let id = min; id < limit; id++) {
|
||||||
idList.push(id.toString())
|
idList.push(id.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,9 +44,9 @@ export const generateOrGetPreKeys = (creds: AuthenticationCreds, range: number)
|
|||||||
const avaliable = creds.nextPreKeyId - creds.firstUnuploadedPreKeyId
|
const avaliable = creds.nextPreKeyId - creds.firstUnuploadedPreKeyId
|
||||||
const remaining = range - avaliable
|
const remaining = range - avaliable
|
||||||
const lastPreKeyId = creds.nextPreKeyId + remaining - 1
|
const lastPreKeyId = creds.nextPreKeyId + remaining - 1
|
||||||
const newPreKeys: { [id: number]: KeyPair } = { }
|
const newPreKeys: { [id: number]: KeyPair } = {}
|
||||||
if(remaining > 0) {
|
if (remaining > 0) {
|
||||||
for(let i = creds.nextPreKeyId;i <= lastPreKeyId;i++) {
|
for (let i = creds.nextPreKeyId; i <= lastPreKeyId; i++) {
|
||||||
newPreKeys[i] = Curve.generateKeyPair()
|
newPreKeys[i] = Curve.generateKeyPair()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,46 +54,40 @@ export const generateOrGetPreKeys = (creds: AuthenticationCreds, range: number)
|
|||||||
return {
|
return {
|
||||||
newPreKeys,
|
newPreKeys,
|
||||||
lastPreKeyId,
|
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',
|
||||||
tag: 'skey',
|
attrs: {},
|
||||||
attrs: { },
|
content: [
|
||||||
content: [
|
{ tag: 'id', attrs: {}, content: encodeBigEndian(key.keyId, 3) },
|
||||||
{ tag: 'id', attrs: { }, content: encodeBigEndian(key.keyId, 3) },
|
{ tag: 'value', attrs: {}, content: key.keyPair.public },
|
||||||
{ tag: 'value', attrs: { }, content: key.keyPair.public },
|
{ tag: 'signature', attrs: {}, content: key.signature }
|
||||||
{ tag: 'signature', attrs: { }, content: key.signature }
|
]
|
||||||
]
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const xmppPreKey = (pair: KeyPair, id: number): BinaryNode => (
|
export const xmppPreKey = (pair: KeyPair, id: number): BinaryNode => ({
|
||||||
{
|
tag: 'key',
|
||||||
tag: 'key',
|
attrs: {},
|
||||||
attrs: { },
|
content: [
|
||||||
content: [
|
{ tag: 'id', attrs: {}, content: encodeBigEndian(id, 3) },
|
||||||
{ tag: 'id', attrs: { }, content: encodeBigEndian(id, 3) },
|
{ tag: 'value', attrs: {}, content: pair.public }
|
||||||
{ tag: 'value', attrs: { }, content: pair.public }
|
]
|
||||||
]
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const parseAndInjectE2ESessions = async(
|
export const parseAndInjectE2ESessions = async (node: BinaryNode, repository: SignalRepository) => {
|
||||||
node: BinaryNode,
|
const extractKey = (key: BinaryNode) =>
|
||||||
repository: SignalRepository
|
key
|
||||||
) => {
|
? {
|
||||||
const extractKey = (key: BinaryNode) => (
|
keyId: getBinaryNodeChildUInt(key, 'id', 3)!,
|
||||||
key ? ({
|
publicKey: generateSignalPubKey(getBinaryNodeChildBuffer(key, 'value')!),
|
||||||
keyId: getBinaryNodeChildUInt(key, 'id', 3)!,
|
signature: getBinaryNodeChildBuffer(key, 'signature')!
|
||||||
publicKey: generateSignalPubKey(getBinaryNodeChildBuffer(key, 'value')!),
|
}
|
||||||
signature: getBinaryNodeChildBuffer(key, 'signature')!,
|
: undefined
|
||||||
}) : undefined
|
|
||||||
)
|
|
||||||
const nodes = getBinaryNodeChildren(getBinaryNodeChild(node, 'list'), 'user')
|
const nodes = getBinaryNodeChildren(getBinaryNodeChild(node, 'list'), 'user')
|
||||||
for(const node of nodes) {
|
for (const node of nodes) {
|
||||||
assertNodeErrorFree(node)
|
assertNodeErrorFree(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,27 +98,25 @@ export const parseAndInjectE2ESessions = async(
|
|||||||
// It's rare case when you need to E2E sessions for so many users, but it's possible
|
// It's rare case when you need to E2E sessions for so many users, but it's possible
|
||||||
const chunkSize = 100
|
const chunkSize = 100
|
||||||
const chunks = chunk(nodes, chunkSize)
|
const chunks = chunk(nodes, chunkSize)
|
||||||
for(const nodesChunk of chunks) {
|
for (const nodesChunk of chunks) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
nodesChunk.map(
|
nodesChunk.map(async node => {
|
||||||
async node => {
|
const signedKey = getBinaryNodeChild(node, 'skey')!
|
||||||
const signedKey = getBinaryNodeChild(node, 'skey')!
|
const key = getBinaryNodeChild(node, 'key')!
|
||||||
const key = getBinaryNodeChild(node, 'key')!
|
const identity = getBinaryNodeChildBuffer(node, 'identity')!
|
||||||
const identity = getBinaryNodeChildBuffer(node, 'identity')!
|
const jid = node.attrs.jid
|
||||||
const jid = node.attrs.jid
|
const registrationId = getBinaryNodeChildUInt(node, 'registration', 4)
|
||||||
const registrationId = getBinaryNodeChildUInt(node, 'registration', 4)
|
|
||||||
|
|
||||||
await repository.injectE2ESession({
|
await repository.injectE2ESession({
|
||||||
jid,
|
jid,
|
||||||
session: {
|
session: {
|
||||||
registrationId: registrationId!,
|
registrationId: registrationId!,
|
||||||
identityKey: generateSignalPubKey(identity),
|
identityKey: generateSignalPubKey(identity),
|
||||||
signedPreKey: extractKey(signedKey)!,
|
signedPreKey: extractKey(signedKey)!,
|
||||||
preKey: extractKey(key)!
|
preKey: extractKey(key)!
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,14 +126,13 @@ export const extractDeviceJids = (result: USyncQueryResultList[], myJid: string,
|
|||||||
|
|
||||||
const extracted: JidWithDevice[] = []
|
const extracted: JidWithDevice[] = []
|
||||||
|
|
||||||
|
for (const userResult of result) {
|
||||||
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 { user } = jidDecode(id)!
|
||||||
const deviceList = devices?.deviceList as DeviceListData[]
|
const deviceList = devices?.deviceList as DeviceListData[]
|
||||||
if(Array.isArray(deviceList)) {
|
if (Array.isArray(deviceList)) {
|
||||||
for(const { id: device, keyIndex } of deviceList) {
|
for (const { id: device, keyIndex } of deviceList) {
|
||||||
if(
|
if (
|
||||||
(!excludeZeroDevices || device !== 0) && // if zero devices are not-excluded, or device is non zero
|
(!excludeZeroDevices || device !== 0) && // if zero devices are not-excluded, or device is non zero
|
||||||
(myUser !== user || myDevice !== device) && // either different user or if me user, not this device
|
(myUser !== user || myDevice !== device) && // either different user or if me user, not this device
|
||||||
(device === 0 || !!keyIndex) // ensure that "key-index" is specified for "non-zero" devices, produces a bad req otherwise
|
(device === 0 || !!keyIndex) // ensure that "key-index" is specified for "non-zero" devices, produces a bad req otherwise
|
||||||
@@ -145,7 +150,7 @@ export const extractDeviceJids = (result: USyncQueryResultList[], myJid: string,
|
|||||||
* get the next N keys for upload or processing
|
* get the next N keys for upload or processing
|
||||||
* @param count number of pre-keys to get or generate
|
* @param count number of pre-keys to get or generate
|
||||||
*/
|
*/
|
||||||
export const getNextPreKeys = async({ creds, keys }: AuthenticationState, count: number) => {
|
export const getNextPreKeys = async ({ creds, keys }: AuthenticationState, count: number) => {
|
||||||
const { newPreKeys, lastPreKeyId, preKeysRange } = generateOrGetPreKeys(creds, count)
|
const { newPreKeys, lastPreKeyId, preKeysRange } = generateOrGetPreKeys(creds, count)
|
||||||
|
|
||||||
const update: Partial<AuthenticationCreds> = {
|
const update: Partial<AuthenticationCreds> = {
|
||||||
@@ -160,7 +165,7 @@ export const getNextPreKeys = async({ creds, keys }: AuthenticationState, count:
|
|||||||
return { update, preKeys }
|
return { update, preKeys }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getNextPreKeysNode = async(state: AuthenticationState, count: number) => {
|
export const getNextPreKeysNode = async (state: AuthenticationState, count: number) => {
|
||||||
const { creds } = state
|
const { creds } = state
|
||||||
const { update, preKeys } = await getNextPreKeys(state, count)
|
const { update, preKeys } = await getNextPreKeys(state, count)
|
||||||
|
|
||||||
@@ -169,13 +174,13 @@ export const getNextPreKeysNode = async(state: AuthenticationState, count: numbe
|
|||||||
attrs: {
|
attrs: {
|
||||||
xmlns: 'encrypt',
|
xmlns: 'encrypt',
|
||||||
type: 'set',
|
type: 'set',
|
||||||
to: S_WHATSAPP_NET,
|
to: S_WHATSAPP_NET
|
||||||
},
|
},
|
||||||
content: [
|
content: [
|
||||||
{ tag: 'registration', attrs: { }, content: encodeBigEndian(creds.registrationId) },
|
{ tag: 'registration', attrs: {}, content: encodeBigEndian(creds.registrationId) },
|
||||||
{ tag: 'type', attrs: { }, content: KEY_BUNDLE_TYPE },
|
{ tag: 'type', attrs: {}, content: KEY_BUNDLE_TYPE },
|
||||||
{ tag: 'identity', attrs: { }, content: creds.signedIdentityKey.public },
|
{ tag: 'identity', attrs: {}, content: creds.signedIdentityKey.public },
|
||||||
{ tag: 'list', attrs: { }, content: Object.keys(preKeys).map(k => xmppPreKey(preKeys[+k], +k)) },
|
{ tag: 'list', attrs: {}, content: Object.keys(preKeys).map(k => xmppPreKey(preKeys[+k], +k)) },
|
||||||
xmppSignedPreKey(creds.signedPreKey)
|
xmppSignedPreKey(creds.signedPreKey)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const fileLocks = new Map<string, Mutex>()
|
|||||||
// Get or create a mutex for a specific file path
|
// Get or create a mutex for a specific file path
|
||||||
const getFileLock = (path: string): Mutex => {
|
const getFileLock = (path: string): Mutex => {
|
||||||
let mutex = fileLocks.get(path)
|
let mutex = fileLocks.get(path)
|
||||||
if(!mutex) {
|
if (!mutex) {
|
||||||
mutex = new Mutex()
|
mutex = new Mutex()
|
||||||
fileLocks.set(path, mutex)
|
fileLocks.set(path, mutex)
|
||||||
}
|
}
|
||||||
@@ -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.
|
* 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
|
* 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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const writeData = async(data: any, file: string) => {
|
const writeData = async (data: any, file: string) => {
|
||||||
const filePath = join(folder, fixFileName(file)!)
|
const filePath = join(folder, fixFileName(file)!)
|
||||||
const mutex = getFileLock(filePath)
|
const mutex = getFileLock(filePath)
|
||||||
|
|
||||||
return mutex.acquire().then(async(release) => {
|
return mutex.acquire().then(async release => {
|
||||||
try {
|
try {
|
||||||
await writeFile(filePath, JSON.stringify(data, BufferJSON.replacer))
|
await writeFile(filePath, JSON.stringify(data, BufferJSON.replacer))
|
||||||
} finally {
|
} finally {
|
||||||
@@ -45,12 +47,12 @@ export const useMultiFileAuthState = async(folder: string): Promise<{ state: Aut
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const readData = async(file: string) => {
|
const readData = async (file: string) => {
|
||||||
try {
|
try {
|
||||||
const filePath = join(folder, fixFileName(file)!)
|
const filePath = join(folder, fixFileName(file)!)
|
||||||
const mutex = getFileLock(filePath)
|
const mutex = getFileLock(filePath)
|
||||||
|
|
||||||
return await mutex.acquire().then(async(release) => {
|
return await mutex.acquire().then(async release => {
|
||||||
try {
|
try {
|
||||||
const data = await readFile(filePath, { encoding: 'utf-8' })
|
const data = await readFile(filePath, { encoding: 'utf-8' })
|
||||||
return JSON.parse(data, BufferJSON.reviver)
|
return JSON.parse(data, BufferJSON.reviver)
|
||||||
@@ -58,32 +60,33 @@ export const useMultiFileAuthState = async(folder: string): Promise<{ state: Aut
|
|||||||
release()
|
release()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch(error) {
|
} catch (error) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeData = async(file: string) => {
|
const removeData = async (file: string) => {
|
||||||
try {
|
try {
|
||||||
const filePath = join(folder, fixFileName(file)!)
|
const filePath = join(folder, fixFileName(file)!)
|
||||||
const mutex = getFileLock(filePath)
|
const mutex = getFileLock(filePath)
|
||||||
|
|
||||||
return mutex.acquire().then(async(release) => {
|
return mutex.acquire().then(async release => {
|
||||||
try {
|
try {
|
||||||
await unlink(filePath)
|
await unlink(filePath)
|
||||||
} catch{
|
} catch {
|
||||||
} finally {
|
} finally {
|
||||||
release()
|
release()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch{
|
} catch {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const folderInfo = await stat(folder).catch(() => { })
|
const folderInfo = await stat(folder).catch(() => {})
|
||||||
if(folderInfo) {
|
if (folderInfo) {
|
||||||
if(!folderInfo.isDirectory()) {
|
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 {
|
} else {
|
||||||
await mkdir(folder, { recursive: true })
|
await mkdir(folder, { recursive: true })
|
||||||
@@ -91,33 +94,31 @@ export const useMultiFileAuthState = async(folder: string): Promise<{ state: Aut
|
|||||||
|
|
||||||
const fixFileName = (file?: string) => file?.replace(/\//g, '__')?.replace(/:/g, '-')
|
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 {
|
return {
|
||||||
state: {
|
state: {
|
||||||
creds,
|
creds,
|
||||||
keys: {
|
keys: {
|
||||||
get: async(type, ids) => {
|
get: async (type, ids) => {
|
||||||
const data: { [_: string]: SignalDataTypeMap[typeof type] } = { }
|
const data: { [_: string]: SignalDataTypeMap[typeof type] } = {}
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
ids.map(
|
ids.map(async id => {
|
||||||
async id => {
|
let value = await readData(`${type}-${id}.json`)
|
||||||
let value = await readData(`${type}-${id}.json`)
|
if (type === 'app-state-sync-key' && value) {
|
||||||
if(type === 'app-state-sync-key' && value) {
|
value = proto.Message.AppStateSyncKeyData.fromObject(value)
|
||||||
value = proto.Message.AppStateSyncKeyData.fromObject(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
data[id] = value
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
data[id] = value
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
set: async(data) => {
|
set: async data => {
|
||||||
const tasks: Promise<void>[] = []
|
const tasks: Promise<void>[] = []
|
||||||
for(const category in data) {
|
for (const category in data) {
|
||||||
for(const id in data[category]) {
|
for (const id in data[category]) {
|
||||||
const value = data[category][id]
|
const value = data[category][id]
|
||||||
const file = `${category}-${id}.json`
|
const file = `${category}-${id}.json`
|
||||||
tasks.push(value ? writeData(value, file) : removeData(file))
|
tasks.push(value ? writeData(value, file) : removeData(file))
|
||||||
@@ -128,8 +129,8 @@ export const useMultiFileAuthState = async(folder: string): Promise<{ state: Aut
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
saveCreds: async() => {
|
saveCreds: async () => {
|
||||||
return writeData(creds, 'creds.json')
|
return writeData(creds, 'creds.json')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const getUserAgent = (config: SocketConfig): proto.ClientPayload.IUserAgent => {
|
|||||||
appVersion: {
|
appVersion: {
|
||||||
primary: config.version[0],
|
primary: config.version[0],
|
||||||
secondary: config.version[1],
|
secondary: config.version[1],
|
||||||
tertiary: config.version[2],
|
tertiary: config.version[2]
|
||||||
},
|
},
|
||||||
platform: proto.ClientPayload.UserAgent.Platform.WEB,
|
platform: proto.ClientPayload.UserAgent.Platform.WEB,
|
||||||
releaseChannel: proto.ClientPayload.UserAgent.ReleaseChannel.RELEASE,
|
releaseChannel: proto.ClientPayload.UserAgent.ReleaseChannel.RELEASE,
|
||||||
@@ -23,30 +23,29 @@ const getUserAgent = (config: SocketConfig): proto.ClientPayload.IUserAgent => {
|
|||||||
localeLanguageIso6391: 'en',
|
localeLanguageIso6391: 'en',
|
||||||
mnc: '000',
|
mnc: '000',
|
||||||
mcc: '000',
|
mcc: '000',
|
||||||
localeCountryIso31661Alpha2: config.countryCode,
|
localeCountryIso31661Alpha2: config.countryCode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const PLATFORM_MAP = {
|
const PLATFORM_MAP = {
|
||||||
'Mac OS': proto.ClientPayload.WebInfo.WebSubPlatform.DARWIN,
|
'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 => {
|
const getWebInfo = (config: SocketConfig): proto.ClientPayload.IWebInfo => {
|
||||||
let webSubPlatform = proto.ClientPayload.WebInfo.WebSubPlatform.WEB_BROWSER
|
let webSubPlatform = proto.ClientPayload.WebInfo.WebSubPlatform.WEB_BROWSER
|
||||||
if(config.syncFullHistory && PLATFORM_MAP[config.browser[0]]) {
|
if (config.syncFullHistory && PLATFORM_MAP[config.browser[0]]) {
|
||||||
webSubPlatform = PLATFORM_MAP[config.browser[0]]
|
webSubPlatform = PLATFORM_MAP[config.browser[0]]
|
||||||
}
|
}
|
||||||
|
|
||||||
return { webSubPlatform }
|
return { webSubPlatform }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const getClientPayload = (config: SocketConfig) => {
|
const getClientPayload = (config: SocketConfig) => {
|
||||||
const payload: proto.IClientPayload = {
|
const payload: proto.IClientPayload = {
|
||||||
connectType: proto.ClientPayload.ConnectType.WIFI_UNKNOWN,
|
connectType: proto.ClientPayload.ConnectType.WIFI_UNKNOWN,
|
||||||
connectReason: proto.ClientPayload.ConnectReason.USER_ACTIVATED,
|
connectReason: proto.ClientPayload.ConnectReason.USER_ACTIVATED,
|
||||||
userAgent: getUserAgent(config),
|
userAgent: getUserAgent(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
payload.webInfo = getWebInfo(config)
|
payload.webInfo = getWebInfo(config)
|
||||||
@@ -54,7 +53,6 @@ const getClientPayload = (config: SocketConfig) => {
|
|||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const generateLoginNode = (userJid: string, config: SocketConfig): proto.IClientPayload => {
|
export const generateLoginNode = (userJid: string, config: SocketConfig): proto.IClientPayload => {
|
||||||
const { user, device } = jidDecode(userJid)!
|
const { user, device } = jidDecode(userJid)!
|
||||||
const payload: proto.IClientPayload = {
|
const payload: proto.IClientPayload = {
|
||||||
@@ -62,7 +60,7 @@ export const generateLoginNode = (userJid: string, config: SocketConfig): proto.
|
|||||||
passive: false,
|
passive: false,
|
||||||
pull: true,
|
pull: true,
|
||||||
username: +user,
|
username: +user,
|
||||||
device: device,
|
device: device
|
||||||
}
|
}
|
||||||
return proto.ClientPayload.fromObject(payload)
|
return proto.ClientPayload.fromObject(payload)
|
||||||
}
|
}
|
||||||
@@ -85,7 +83,7 @@ export const generateRegistrationNode = (
|
|||||||
const companion: proto.IDeviceProps = {
|
const companion: proto.IDeviceProps = {
|
||||||
os: config.browser[0],
|
os: config.browser[0],
|
||||||
platformType: getPlatformType(config.browser[1]),
|
platformType: getPlatformType(config.browser[1]),
|
||||||
requireFullSync: config.syncFullHistory,
|
requireFullSync: config.syncFullHistory
|
||||||
}
|
}
|
||||||
|
|
||||||
const companionProto = proto.DeviceProps.encode(companion).finish()
|
const companionProto = proto.DeviceProps.encode(companion).finish()
|
||||||
@@ -102,8 +100,8 @@ export const generateRegistrationNode = (
|
|||||||
eIdent: signedIdentityKey.public,
|
eIdent: signedIdentityKey.public,
|
||||||
eSkeyId: encodeBigEndian(signedPreKey.keyId, 3),
|
eSkeyId: encodeBigEndian(signedPreKey.keyId, 3),
|
||||||
eSkeyVal: signedPreKey.keyPair.public,
|
eSkeyVal: signedPreKey.keyPair.public,
|
||||||
eSkeySig: signedPreKey.signature,
|
eSkeySig: signedPreKey.signature
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return proto.ClientPayload.fromObject(registerPayload)
|
return proto.ClientPayload.fromObject(registerPayload)
|
||||||
@@ -111,7 +109,11 @@ export const generateRegistrationNode = (
|
|||||||
|
|
||||||
export const configureSuccessfulPairing = (
|
export const configureSuccessfulPairing = (
|
||||||
stanza: BinaryNode,
|
stanza: BinaryNode,
|
||||||
{ advSecretKey, signedIdentityKey, signalIdentities }: Pick<AuthenticationCreds, 'advSecretKey' | 'signedIdentityKey' | 'signalIdentities'>
|
{
|
||||||
|
advSecretKey,
|
||||||
|
signedIdentityKey,
|
||||||
|
signalIdentities
|
||||||
|
}: Pick<AuthenticationCreds, 'advSecretKey' | 'signedIdentityKey' | 'signalIdentities'>
|
||||||
) => {
|
) => {
|
||||||
const msgId = stanza.attrs.id
|
const msgId = stanza.attrs.id
|
||||||
|
|
||||||
@@ -122,7 +124,7 @@ export const configureSuccessfulPairing = (
|
|||||||
const deviceNode = getBinaryNodeChild(pairSuccessNode, 'device')
|
const deviceNode = getBinaryNodeChild(pairSuccessNode, 'device')
|
||||||
const businessNode = getBinaryNodeChild(pairSuccessNode, 'biz')
|
const businessNode = getBinaryNodeChild(pairSuccessNode, 'biz')
|
||||||
|
|
||||||
if(!deviceIdentityNode || !deviceNode) {
|
if (!deviceIdentityNode || !deviceNode) {
|
||||||
throw new Boom('Missing device-identity or device in pair success node', { data: stanza })
|
throw new Boom('Missing device-identity or device in pair success node', { data: stanza })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,20 +134,20 @@ export const configureSuccessfulPairing = (
|
|||||||
const { details, hmac } = proto.ADVSignedDeviceIdentityHMAC.decode(deviceIdentityNode.content as Buffer)
|
const { details, hmac } = proto.ADVSignedDeviceIdentityHMAC.decode(deviceIdentityNode.content as Buffer)
|
||||||
// check HMAC matches
|
// check HMAC matches
|
||||||
const advSign = hmacSign(details!, Buffer.from(advSecretKey, 'base64'))
|
const advSign = hmacSign(details!, Buffer.from(advSecretKey, 'base64'))
|
||||||
if(Buffer.compare(hmac!, advSign) !== 0) {
|
if (Buffer.compare(hmac!, advSign) !== 0) {
|
||||||
throw new Boom('Invalid account signature')
|
throw new Boom('Invalid account signature')
|
||||||
}
|
}
|
||||||
|
|
||||||
const account = proto.ADVSignedDeviceIdentity.decode(details!)
|
const account = proto.ADVSignedDeviceIdentity.decode(details!)
|
||||||
const { accountSignatureKey, accountSignature, details: deviceDetails } = account
|
const { accountSignatureKey, accountSignature, details: deviceDetails } = account
|
||||||
// verify the device signature matches
|
// verify the device signature matches
|
||||||
const accountMsg = Buffer.concat([ Buffer.from([6, 0]), deviceDetails!, signedIdentityKey.public ])
|
const accountMsg = Buffer.concat([Buffer.from([6, 0]), deviceDetails!, signedIdentityKey.public])
|
||||||
if(!Curve.verify(accountSignatureKey!, accountMsg, accountSignature!)) {
|
if (!Curve.verify(accountSignatureKey!, accountMsg, accountSignature!)) {
|
||||||
throw new Boom('Failed to verify account signature')
|
throw new Boom('Failed to verify account signature')
|
||||||
}
|
}
|
||||||
|
|
||||||
// sign the details with our identity key
|
// sign the details with our identity key
|
||||||
const deviceMsg = Buffer.concat([ Buffer.from([6, 1]), deviceDetails!, signedIdentityKey.public, accountSignatureKey! ])
|
const deviceMsg = Buffer.concat([Buffer.from([6, 1]), deviceDetails!, signedIdentityKey.public, accountSignatureKey!])
|
||||||
account.deviceSignature = Curve.sign(signedIdentityKey.private, deviceMsg)
|
account.deviceSignature = Curve.sign(signedIdentityKey.private, deviceMsg)
|
||||||
|
|
||||||
const identity = createSignalIdentity(jid, accountSignatureKey!)
|
const identity = createSignalIdentity(jid, accountSignatureKey!)
|
||||||
@@ -158,12 +160,12 @@ export const configureSuccessfulPairing = (
|
|||||||
attrs: {
|
attrs: {
|
||||||
to: S_WHATSAPP_NET,
|
to: S_WHATSAPP_NET,
|
||||||
type: 'result',
|
type: 'result',
|
||||||
id: msgId,
|
id: msgId
|
||||||
},
|
},
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
tag: 'pair-device-sign',
|
tag: 'pair-device-sign',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
tag: 'device-identity',
|
tag: 'device-identity',
|
||||||
@@ -178,10 +180,7 @@ export const configureSuccessfulPairing = (
|
|||||||
const authUpdate: Partial<AuthenticationCreds> = {
|
const authUpdate: Partial<AuthenticationCreds> = {
|
||||||
account,
|
account,
|
||||||
me: { id: jid, name: bizName },
|
me: { id: jid, name: bizName },
|
||||||
signalIdentities: [
|
signalIdentities: [...(signalIdentities || []), identity],
|
||||||
...(signalIdentities || []),
|
|
||||||
identity
|
|
||||||
],
|
|
||||||
platform: platformNode?.attrs.name
|
platform: platformNode?.attrs.name
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,18 +190,13 @@ export const configureSuccessfulPairing = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const encodeSignedDeviceIdentity = (
|
export const encodeSignedDeviceIdentity = (account: proto.IADVSignedDeviceIdentity, includeSignatureKey: boolean) => {
|
||||||
account: proto.IADVSignedDeviceIdentity,
|
|
||||||
includeSignatureKey: boolean
|
|
||||||
) => {
|
|
||||||
account = { ...account }
|
account = { ...account }
|
||||||
// set to null if we are not to include the signature key
|
// set to null if we are not to include the signature key
|
||||||
// or if we are including the signature key but it is empty
|
// or if we are including the signature key but it is empty
|
||||||
if(!includeSignatureKey || !account.accountSignatureKey?.length) {
|
if (!includeSignatureKey || !account.accountSignatureKey?.length) {
|
||||||
account.accountSignatureKey = null
|
account.accountSignatureKey = null
|
||||||
}
|
}
|
||||||
|
|
||||||
return proto.ADVSignedDeviceIdentity
|
return proto.ADVSignedDeviceIdentity.encode(account).finish()
|
||||||
.encode(account)
|
|
||||||
.finish()
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -6,10 +6,11 @@ import type { BinaryNode, BinaryNodeCodingOptions } from './types'
|
|||||||
|
|
||||||
const inflatePromise = promisify(inflate)
|
const inflatePromise = promisify(inflate)
|
||||||
|
|
||||||
export const decompressingIfRequired = async(buffer: Buffer) => {
|
export const decompressingIfRequired = async (buffer: Buffer) => {
|
||||||
if(2 & buffer.readUInt8()) {
|
if (2 & buffer.readUInt8()) {
|
||||||
buffer = await inflatePromise(buffer.slice(1))
|
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)
|
buffer = buffer.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ export const decodeDecompressedBinaryNode = (
|
|||||||
const { DOUBLE_BYTE_TOKENS, SINGLE_BYTE_TOKENS, TAGS } = opts
|
const { DOUBLE_BYTE_TOKENS, SINGLE_BYTE_TOKENS, TAGS } = opts
|
||||||
|
|
||||||
const checkEOS = (length: number) => {
|
const checkEOS = (length: number) => {
|
||||||
if(indexRef.index + length > buffer.length) {
|
if (indexRef.index + length > buffer.length) {
|
||||||
throw new Error('end of stream')
|
throw new Error('end of stream')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,7 +55,7 @@ export const decodeDecompressedBinaryNode = (
|
|||||||
const readInt = (n: number, littleEndian = false) => {
|
const readInt = (n: number, littleEndian = false) => {
|
||||||
checkEOS(n)
|
checkEOS(n)
|
||||||
let val = 0
|
let val = 0
|
||||||
for(let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
const shift = littleEndian ? i : n - 1 - i
|
const shift = littleEndian ? i : n - 1 - i
|
||||||
val |= next() << (shift * 8)
|
val |= next() << (shift * 8)
|
||||||
}
|
}
|
||||||
@@ -68,7 +69,7 @@ export const decodeDecompressedBinaryNode = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unpackHex = (value: number) => {
|
const unpackHex = (value: number) => {
|
||||||
if(value >= 0 && value < 16) {
|
if (value >= 0 && value < 16) {
|
||||||
return value < 10 ? '0'.charCodeAt(0) + value : 'A'.charCodeAt(0) + value - 10
|
return value < 10 ? '0'.charCodeAt(0) + value : 'A'.charCodeAt(0) + value - 10
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,26 +77,26 @@ export const decodeDecompressedBinaryNode = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unpackNibble = (value: number) => {
|
const unpackNibble = (value: number) => {
|
||||||
if(value >= 0 && value <= 9) {
|
if (value >= 0 && value <= 9) {
|
||||||
return '0'.charCodeAt(0) + value
|
return '0'.charCodeAt(0) + value
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case 10:
|
case 10:
|
||||||
return '-'.charCodeAt(0)
|
return '-'.charCodeAt(0)
|
||||||
case 11:
|
case 11:
|
||||||
return '.'.charCodeAt(0)
|
return '.'.charCodeAt(0)
|
||||||
case 15:
|
case 15:
|
||||||
return '\0'.charCodeAt(0)
|
return '\0'.charCodeAt(0)
|
||||||
default:
|
default:
|
||||||
throw new Error('invalid nibble: ' + value)
|
throw new Error('invalid nibble: ' + value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const unpackByte = (tag: number, value: number) => {
|
const unpackByte = (tag: number, value: number) => {
|
||||||
if(tag === TAGS.NIBBLE_8) {
|
if (tag === TAGS.NIBBLE_8) {
|
||||||
return unpackNibble(value)
|
return unpackNibble(value)
|
||||||
} else if(tag === TAGS.HEX_8) {
|
} else if (tag === TAGS.HEX_8) {
|
||||||
return unpackHex(value)
|
return unpackHex(value)
|
||||||
} else {
|
} else {
|
||||||
throw new Error('unknown tag: ' + tag)
|
throw new Error('unknown tag: ' + tag)
|
||||||
@@ -106,13 +107,13 @@ export const decodeDecompressedBinaryNode = (
|
|||||||
const startByte = readByte()
|
const startByte = readByte()
|
||||||
let value = ''
|
let value = ''
|
||||||
|
|
||||||
for(let i = 0; i < (startByte & 127); i++) {
|
for (let i = 0; i < (startByte & 127); i++) {
|
||||||
const curByte = readByte()
|
const curByte = readByte()
|
||||||
value += String.fromCharCode(unpackByte(tag, (curByte & 0xf0) >> 4))
|
value += String.fromCharCode(unpackByte(tag, (curByte & 0xf0) >> 4))
|
||||||
value += String.fromCharCode(unpackByte(tag, curByte & 0x0f))
|
value += String.fromCharCode(unpackByte(tag, curByte & 0x0f))
|
||||||
}
|
}
|
||||||
|
|
||||||
if(startByte >> 7 !== 0) {
|
if (startByte >> 7 !== 0) {
|
||||||
value = value.slice(0, -1)
|
value = value.slice(0, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,21 +126,21 @@ export const decodeDecompressedBinaryNode = (
|
|||||||
|
|
||||||
const readListSize = (tag: number) => {
|
const readListSize = (tag: number) => {
|
||||||
switch (tag) {
|
switch (tag) {
|
||||||
case TAGS.LIST_EMPTY:
|
case TAGS.LIST_EMPTY:
|
||||||
return 0
|
return 0
|
||||||
case TAGS.LIST_8:
|
case TAGS.LIST_8:
|
||||||
return readByte()
|
return readByte()
|
||||||
case TAGS.LIST_16:
|
case TAGS.LIST_16:
|
||||||
return readInt(2)
|
return readInt(2)
|
||||||
default:
|
default:
|
||||||
throw new Error('invalid tag for list size: ' + tag)
|
throw new Error('invalid tag for list size: ' + tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const readJidPair = () => {
|
const readJidPair = () => {
|
||||||
const i = readString(readByte())
|
const i = readString(readByte())
|
||||||
const j = readString(readByte())
|
const j = readString(readByte())
|
||||||
if(j) {
|
if (j) {
|
||||||
return (i || '') + '@' + j
|
return (i || '') + '@' + j
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,48 +154,44 @@ export const decodeDecompressedBinaryNode = (
|
|||||||
const device = readByte()
|
const device = readByte()
|
||||||
const user = readString(readByte())
|
const user = readString(readByte())
|
||||||
|
|
||||||
return jidEncode(
|
return jidEncode(user, domainType === 0 || domainType === 128 ? 's.whatsapp.net' : 'lid', device)
|
||||||
user,
|
|
||||||
domainType === 0 || domainType === 128 ? 's.whatsapp.net' : 'lid',
|
|
||||||
device
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const readString = (tag: number): string => {
|
const readString = (tag: number): string => {
|
||||||
if(tag >= 1 && tag < SINGLE_BYTE_TOKENS.length) {
|
if (tag >= 1 && tag < SINGLE_BYTE_TOKENS.length) {
|
||||||
return SINGLE_BYTE_TOKENS[tag] || ''
|
return SINGLE_BYTE_TOKENS[tag] || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (tag) {
|
switch (tag) {
|
||||||
case TAGS.DICTIONARY_0:
|
case TAGS.DICTIONARY_0:
|
||||||
case TAGS.DICTIONARY_1:
|
case TAGS.DICTIONARY_1:
|
||||||
case TAGS.DICTIONARY_2:
|
case TAGS.DICTIONARY_2:
|
||||||
case TAGS.DICTIONARY_3:
|
case TAGS.DICTIONARY_3:
|
||||||
return getTokenDouble(tag - TAGS.DICTIONARY_0, readByte())
|
return getTokenDouble(tag - TAGS.DICTIONARY_0, readByte())
|
||||||
case TAGS.LIST_EMPTY:
|
case TAGS.LIST_EMPTY:
|
||||||
return ''
|
return ''
|
||||||
case TAGS.BINARY_8:
|
case TAGS.BINARY_8:
|
||||||
return readStringFromChars(readByte())
|
return readStringFromChars(readByte())
|
||||||
case TAGS.BINARY_20:
|
case TAGS.BINARY_20:
|
||||||
return readStringFromChars(readInt20())
|
return readStringFromChars(readInt20())
|
||||||
case TAGS.BINARY_32:
|
case TAGS.BINARY_32:
|
||||||
return readStringFromChars(readInt(4))
|
return readStringFromChars(readInt(4))
|
||||||
case TAGS.JID_PAIR:
|
case TAGS.JID_PAIR:
|
||||||
return readJidPair()
|
return readJidPair()
|
||||||
case TAGS.AD_JID:
|
case TAGS.AD_JID:
|
||||||
return readAdJid()
|
return readAdJid()
|
||||||
case TAGS.HEX_8:
|
case TAGS.HEX_8:
|
||||||
case TAGS.NIBBLE_8:
|
case TAGS.NIBBLE_8:
|
||||||
return readPacked8(tag)
|
return readPacked8(tag)
|
||||||
default:
|
default:
|
||||||
throw new Error('invalid string with tag: ' + tag)
|
throw new Error('invalid string with tag: ' + tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const readList = (tag: number) => {
|
const readList = (tag: number) => {
|
||||||
const items: BinaryNode[] = []
|
const items: BinaryNode[] = []
|
||||||
const size = readListSize(tag)
|
const size = readListSize(tag)
|
||||||
for(let i = 0;i < size;i++) {
|
for (let i = 0; i < size; i++) {
|
||||||
items.push(decodeDecompressedBinaryNode(buffer, opts, indexRef))
|
items.push(decodeDecompressedBinaryNode(buffer, opts, indexRef))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,12 +200,12 @@ export const decodeDecompressedBinaryNode = (
|
|||||||
|
|
||||||
const getTokenDouble = (index1: number, index2: number) => {
|
const getTokenDouble = (index1: number, index2: number) => {
|
||||||
const dict = DOUBLE_BYTE_TOKENS[index1]
|
const dict = DOUBLE_BYTE_TOKENS[index1]
|
||||||
if(!dict) {
|
if (!dict) {
|
||||||
throw new Error(`Invalid double token dict (${index1})`)
|
throw new Error(`Invalid double token dict (${index1})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = dict[index2]
|
const value = dict[index2]
|
||||||
if(typeof value === 'undefined') {
|
if (typeof value === 'undefined') {
|
||||||
throw new Error(`Invalid double token (${index2})`)
|
throw new Error(`Invalid double token (${index2})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,44 +214,44 @@ export const decodeDecompressedBinaryNode = (
|
|||||||
|
|
||||||
const listSize = readListSize(readByte())
|
const listSize = readListSize(readByte())
|
||||||
const header = readString(readByte())
|
const header = readString(readByte())
|
||||||
if(!listSize || !header.length) {
|
if (!listSize || !header.length) {
|
||||||
throw new Error('invalid node')
|
throw new Error('invalid node')
|
||||||
}
|
}
|
||||||
|
|
||||||
const attrs: BinaryNode['attrs'] = { }
|
const attrs: BinaryNode['attrs'] = {}
|
||||||
let data: BinaryNode['content']
|
let data: BinaryNode['content']
|
||||||
if(listSize === 0 || !header) {
|
if (listSize === 0 || !header) {
|
||||||
throw new Error('invalid node')
|
throw new Error('invalid node')
|
||||||
}
|
}
|
||||||
|
|
||||||
// read the attributes in
|
// read the attributes in
|
||||||
const attributesLength = (listSize - 1) >> 1
|
const attributesLength = (listSize - 1) >> 1
|
||||||
for(let i = 0; i < attributesLength; i++) {
|
for (let i = 0; i < attributesLength; i++) {
|
||||||
const key = readString(readByte())
|
const key = readString(readByte())
|
||||||
const value = readString(readByte())
|
const value = readString(readByte())
|
||||||
|
|
||||||
attrs[key] = value
|
attrs[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
if(listSize % 2 === 0) {
|
if (listSize % 2 === 0) {
|
||||||
const tag = readByte()
|
const tag = readByte()
|
||||||
if(isListTag(tag)) {
|
if (isListTag(tag)) {
|
||||||
data = readList(tag)
|
data = readList(tag)
|
||||||
} else {
|
} else {
|
||||||
let decoded: Buffer | string
|
let decoded: Buffer | string
|
||||||
switch (tag) {
|
switch (tag) {
|
||||||
case TAGS.BINARY_8:
|
case TAGS.BINARY_8:
|
||||||
decoded = readBytes(readByte())
|
decoded = readBytes(readByte())
|
||||||
break
|
break
|
||||||
case TAGS.BINARY_20:
|
case TAGS.BINARY_20:
|
||||||
decoded = readBytes(readInt20())
|
decoded = readBytes(readInt20())
|
||||||
break
|
break
|
||||||
case TAGS.BINARY_32:
|
case TAGS.BINARY_32:
|
||||||
decoded = readBytes(readInt(4))
|
decoded = readBytes(readInt(4))
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
decoded = readString(tag)
|
decoded = readString(tag)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
data = decoded
|
data = decoded
|
||||||
@@ -268,7 +265,7 @@ export const decodeDecompressedBinaryNode = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const decodeBinaryNode = async(buff: Buffer): Promise<BinaryNode> => {
|
export const decodeBinaryNode = async (buff: Buffer): Promise<BinaryNode> => {
|
||||||
const decompBuff = await decompressingIfRequired(buff)
|
const decompBuff = await decompressingIfRequired(buff)
|
||||||
return decodeDecompressedBinaryNode(decompBuff, constants)
|
return decodeDecompressedBinaryNode(decompBuff, constants)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import * as constants from './constants'
|
import * as constants from './constants'
|
||||||
import { FullJid, jidDecode } from './jid-utils'
|
import { FullJid, jidDecode } from './jid-utils'
|
||||||
import type { BinaryNode, BinaryNodeCodingOptions } from './types'
|
import type { BinaryNode, BinaryNodeCodingOptions } from './types'
|
||||||
@@ -22,14 +21,14 @@ const encodeBinaryNodeInner = (
|
|||||||
const pushByte = (value: number) => buffer.push(value & 0xff)
|
const pushByte = (value: number) => buffer.push(value & 0xff)
|
||||||
|
|
||||||
const pushInt = (value: number, n: number, littleEndian = false) => {
|
const pushInt = (value: number, n: number, littleEndian = false) => {
|
||||||
for(let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
const curShift = littleEndian ? i : n - 1 - i
|
const curShift = littleEndian ? i : n - 1 - i
|
||||||
buffer.push((value >> (curShift * 8)) & 0xff)
|
buffer.push((value >> (curShift * 8)) & 0xff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pushBytes = (bytes: Uint8Array | Buffer | number[]) => {
|
const pushBytes = (bytes: Uint8Array | Buffer | number[]) => {
|
||||||
for(const b of bytes) {
|
for (const b of bytes) {
|
||||||
buffer.push(b)
|
buffer.push(b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,18 +37,16 @@ const encodeBinaryNodeInner = (
|
|||||||
pushBytes([(value >> 8) & 0xff, value & 0xff])
|
pushBytes([(value >> 8) & 0xff, value & 0xff])
|
||||||
}
|
}
|
||||||
|
|
||||||
const pushInt20 = (value: number) => (
|
const pushInt20 = (value: number) => pushBytes([(value >> 16) & 0x0f, (value >> 8) & 0xff, value & 0xff])
|
||||||
pushBytes([(value >> 16) & 0x0f, (value >> 8) & 0xff, value & 0xff])
|
|
||||||
)
|
|
||||||
const writeByteLength = (length: number) => {
|
const writeByteLength = (length: number) => {
|
||||||
if(length >= 4294967296) {
|
if (length >= 4294967296) {
|
||||||
throw new Error('string too large to encode: ' + length)
|
throw new Error('string too large to encode: ' + length)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(length >= 1 << 20) {
|
if (length >= 1 << 20) {
|
||||||
pushByte(TAGS.BINARY_32)
|
pushByte(TAGS.BINARY_32)
|
||||||
pushInt(length, 4) // 32 bit integer
|
pushInt(length, 4) // 32 bit integer
|
||||||
} else if(length >= 256) {
|
} else if (length >= 256) {
|
||||||
pushByte(TAGS.BINARY_20)
|
pushByte(TAGS.BINARY_20)
|
||||||
pushInt20(length)
|
pushInt20(length)
|
||||||
} else {
|
} else {
|
||||||
@@ -59,20 +56,20 @@ const encodeBinaryNodeInner = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const writeStringRaw = (str: string) => {
|
const writeStringRaw = (str: string) => {
|
||||||
const bytes = Buffer.from (str, 'utf-8')
|
const bytes = Buffer.from(str, 'utf-8')
|
||||||
writeByteLength(bytes.length)
|
writeByteLength(bytes.length)
|
||||||
pushBytes(bytes)
|
pushBytes(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
const writeJid = ({ domainType, device, user, server }: FullJid) => {
|
const writeJid = ({ domainType, device, user, server }: FullJid) => {
|
||||||
if(typeof device !== 'undefined') {
|
if (typeof device !== 'undefined') {
|
||||||
pushByte(TAGS.AD_JID)
|
pushByte(TAGS.AD_JID)
|
||||||
pushByte(domainType || 0)
|
pushByte(domainType || 0)
|
||||||
pushByte(device || 0)
|
pushByte(device || 0)
|
||||||
writeString(user)
|
writeString(user)
|
||||||
} else {
|
} else {
|
||||||
pushByte(TAGS.JID_PAIR)
|
pushByte(TAGS.JID_PAIR)
|
||||||
if(user.length) {
|
if (user.length) {
|
||||||
writeString(user)
|
writeString(user)
|
||||||
} else {
|
} else {
|
||||||
pushByte(TAGS.LIST_EMPTY)
|
pushByte(TAGS.LIST_EMPTY)
|
||||||
@@ -84,35 +81,35 @@ const encodeBinaryNodeInner = (
|
|||||||
|
|
||||||
const packNibble = (char: string) => {
|
const packNibble = (char: string) => {
|
||||||
switch (char) {
|
switch (char) {
|
||||||
case '-':
|
case '-':
|
||||||
return 10
|
return 10
|
||||||
case '.':
|
case '.':
|
||||||
return 11
|
return 11
|
||||||
case '\0':
|
case '\0':
|
||||||
return 15
|
return 15
|
||||||
default:
|
default:
|
||||||
if(char >= '0' && char <= '9') {
|
if (char >= '0' && char <= '9') {
|
||||||
return char.charCodeAt(0) - '0'.charCodeAt(0)
|
return char.charCodeAt(0) - '0'.charCodeAt(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`invalid byte for nibble "${char}"`)
|
throw new Error(`invalid byte for nibble "${char}"`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const packHex = (char: string) => {
|
const packHex = (char: string) => {
|
||||||
if(char >= '0' && char <= '9') {
|
if (char >= '0' && char <= '9') {
|
||||||
return char.charCodeAt(0) - '0'.charCodeAt(0)
|
return char.charCodeAt(0) - '0'.charCodeAt(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(char >= 'A' && char <= 'F') {
|
if (char >= 'A' && char <= 'F') {
|
||||||
return 10 + char.charCodeAt(0) - 'A'.charCodeAt(0)
|
return 10 + char.charCodeAt(0) - 'A'.charCodeAt(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(char >= 'a' && char <= 'f') {
|
if (char >= 'a' && char <= 'f') {
|
||||||
return 10 + char.charCodeAt(0) - 'a'.charCodeAt(0)
|
return 10 + char.charCodeAt(0) - 'a'.charCodeAt(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if(char === '\0') {
|
if (char === '\0') {
|
||||||
return 15
|
return 15
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,14 +117,14 @@ const encodeBinaryNodeInner = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const writePackedBytes = (str: string, type: 'nibble' | 'hex') => {
|
const writePackedBytes = (str: string, type: 'nibble' | 'hex') => {
|
||||||
if(str.length > TAGS.PACKED_MAX) {
|
if (str.length > TAGS.PACKED_MAX) {
|
||||||
throw new Error('Too many bytes to pack')
|
throw new Error('Too many bytes to pack')
|
||||||
}
|
}
|
||||||
|
|
||||||
pushByte(type === 'nibble' ? TAGS.NIBBLE_8 : TAGS.HEX_8)
|
pushByte(type === 'nibble' ? TAGS.NIBBLE_8 : TAGS.HEX_8)
|
||||||
|
|
||||||
let roundedLength = Math.ceil(str.length / 2.0)
|
let roundedLength = Math.ceil(str.length / 2.0)
|
||||||
if(str.length % 2 !== 0) {
|
if (str.length % 2 !== 0) {
|
||||||
roundedLength |= 128
|
roundedLength |= 128
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,23 +137,23 @@ const encodeBinaryNodeInner = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const strLengthHalf = Math.floor(str.length / 2)
|
const strLengthHalf = Math.floor(str.length / 2)
|
||||||
for(let i = 0; i < strLengthHalf;i++) {
|
for (let i = 0; i < strLengthHalf; i++) {
|
||||||
pushByte(packBytePair(str[2 * i], str[2 * i + 1]))
|
pushByte(packBytePair(str[2 * i], str[2 * i + 1]))
|
||||||
}
|
}
|
||||||
|
|
||||||
if(str.length % 2 !== 0) {
|
if (str.length % 2 !== 0) {
|
||||||
pushByte(packBytePair(str[str.length - 1], '\x00'))
|
pushByte(packBytePair(str[str.length - 1], '\x00'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isNibble = (str?: string) => {
|
const isNibble = (str?: string) => {
|
||||||
if(!str || str.length > TAGS.PACKED_MAX) {
|
if (!str || str.length > TAGS.PACKED_MAX) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for(const char of str) {
|
for (const char of str) {
|
||||||
const isInNibbleRange = char >= '0' && char <= '9'
|
const isInNibbleRange = char >= '0' && char <= '9'
|
||||||
if(!isInNibbleRange && char !== '-' && char !== '.') {
|
if (!isInNibbleRange && char !== '-' && char !== '.') {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,13 +162,13 @@ const encodeBinaryNodeInner = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isHex = (str?: string) => {
|
const isHex = (str?: string) => {
|
||||||
if(!str || str.length > TAGS.PACKED_MAX) {
|
if (!str || str.length > TAGS.PACKED_MAX) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for(const char of str) {
|
for (const char of str) {
|
||||||
const isInNibbleRange = char >= '0' && char <= '9'
|
const isInNibbleRange = char >= '0' && char <= '9'
|
||||||
if(!isInNibbleRange && !(char >= 'A' && char <= 'F')) {
|
if (!isInNibbleRange && !(char >= 'A' && char <= 'F')) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,25 +177,25 @@ const encodeBinaryNodeInner = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const writeString = (str?: string) => {
|
const writeString = (str?: string) => {
|
||||||
if(str === undefined || str === null) {
|
if (str === undefined || str === null) {
|
||||||
pushByte(TAGS.LIST_EMPTY)
|
pushByte(TAGS.LIST_EMPTY)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenIndex = TOKEN_MAP[str]
|
const tokenIndex = TOKEN_MAP[str]
|
||||||
if(tokenIndex) {
|
if (tokenIndex) {
|
||||||
if(typeof tokenIndex.dict === 'number') {
|
if (typeof tokenIndex.dict === 'number') {
|
||||||
pushByte(TAGS.DICTIONARY_0 + tokenIndex.dict)
|
pushByte(TAGS.DICTIONARY_0 + tokenIndex.dict)
|
||||||
}
|
}
|
||||||
|
|
||||||
pushByte(tokenIndex.index)
|
pushByte(tokenIndex.index)
|
||||||
} else if(isNibble(str)) {
|
} else if (isNibble(str)) {
|
||||||
writePackedBytes(str, 'nibble')
|
writePackedBytes(str, 'nibble')
|
||||||
} else if(isHex(str)) {
|
} else if (isHex(str)) {
|
||||||
writePackedBytes(str, 'hex')
|
writePackedBytes(str, 'hex')
|
||||||
} else if(str) {
|
} else if (str) {
|
||||||
const decodedJid = jidDecode(str)
|
const decodedJid = jidDecode(str)
|
||||||
if(decodedJid) {
|
if (decodedJid) {
|
||||||
writeJid(decodedJid)
|
writeJid(decodedJid)
|
||||||
} else {
|
} else {
|
||||||
writeStringRaw(str)
|
writeStringRaw(str)
|
||||||
@@ -207,9 +204,9 @@ const encodeBinaryNodeInner = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const writeListStart = (listSize: number) => {
|
const writeListStart = (listSize: number) => {
|
||||||
if(listSize === 0) {
|
if (listSize === 0) {
|
||||||
pushByte(TAGS.LIST_EMPTY)
|
pushByte(TAGS.LIST_EMPTY)
|
||||||
} else if(listSize < 256) {
|
} else if (listSize < 256) {
|
||||||
pushBytes([TAGS.LIST_8, listSize])
|
pushBytes([TAGS.LIST_8, listSize])
|
||||||
} else {
|
} else {
|
||||||
pushByte(TAGS.LIST_16)
|
pushByte(TAGS.LIST_16)
|
||||||
@@ -217,37 +214,36 @@ const encodeBinaryNodeInner = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!tag) {
|
if (!tag) {
|
||||||
throw new Error('Invalid node: tag cannot be undefined')
|
throw new Error('Invalid node: tag cannot be undefined')
|
||||||
}
|
}
|
||||||
|
|
||||||
const validAttributes = Object.keys(attrs || {}).filter(k => (
|
const validAttributes = Object.keys(attrs || {}).filter(k => typeof attrs[k] !== 'undefined' && attrs[k] !== null)
|
||||||
typeof attrs[k] !== 'undefined' && attrs[k] !== null
|
|
||||||
))
|
|
||||||
|
|
||||||
writeListStart(2 * validAttributes.length + 1 + (typeof content !== 'undefined' ? 1 : 0))
|
writeListStart(2 * validAttributes.length + 1 + (typeof content !== 'undefined' ? 1 : 0))
|
||||||
writeString(tag)
|
writeString(tag)
|
||||||
|
|
||||||
for(const key of validAttributes) {
|
for (const key of validAttributes) {
|
||||||
if(typeof attrs[key] === 'string') {
|
if (typeof attrs[key] === 'string') {
|
||||||
writeString(key)
|
writeString(key)
|
||||||
writeString(attrs[key])
|
writeString(attrs[key])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(typeof content === 'string') {
|
if (typeof content === 'string') {
|
||||||
writeString(content)
|
writeString(content)
|
||||||
} else if(Buffer.isBuffer(content) || content instanceof Uint8Array) {
|
} else if (Buffer.isBuffer(content) || content instanceof Uint8Array) {
|
||||||
writeByteLength(content.length)
|
writeByteLength(content.length)
|
||||||
pushBytes(content)
|
pushBytes(content)
|
||||||
} else if(Array.isArray(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)
|
writeListStart(validContent.length)
|
||||||
for(const item of validContent) {
|
for (const item of validContent) {
|
||||||
encodeBinaryNodeInner(item, opts, buffer)
|
encodeBinaryNodeInner(item, opts, buffer)
|
||||||
}
|
}
|
||||||
} else if(typeof content === 'undefined') {
|
} else if (typeof content === 'undefined') {
|
||||||
// do nothing
|
// do nothing
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`invalid children for header "${tag}": ${content} (${typeof content})`)
|
throw new Error(`invalid children for header "${tag}": ${content} (${typeof content})`)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { BinaryNode } from './types'
|
|||||||
// some extra useful utilities
|
// some extra useful utilities
|
||||||
|
|
||||||
export const getBinaryNodeChildren = (node: BinaryNode | undefined, childTag: string) => {
|
export const getBinaryNodeChildren = (node: BinaryNode | undefined, childTag: string) => {
|
||||||
if(Array.isArray(node?.content)) {
|
if (Array.isArray(node?.content)) {
|
||||||
return node.content.filter(item => item.tag === childTag)
|
return node.content.filter(item => item.tag === childTag)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ export const getBinaryNodeChildren = (node: BinaryNode | undefined, childTag: st
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getAllBinaryNodeChildren = ({ content }: BinaryNode) => {
|
export const getAllBinaryNodeChildren = ({ content }: BinaryNode) => {
|
||||||
if(Array.isArray(content)) {
|
if (Array.isArray(content)) {
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,37 +21,37 @@ export const getAllBinaryNodeChildren = ({ content }: BinaryNode) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getBinaryNodeChild = (node: BinaryNode | undefined, childTag: string) => {
|
export const getBinaryNodeChild = (node: BinaryNode | undefined, childTag: string) => {
|
||||||
if(Array.isArray(node?.content)) {
|
if (Array.isArray(node?.content)) {
|
||||||
return node?.content.find(item => item.tag === childTag)
|
return node?.content.find(item => item.tag === childTag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getBinaryNodeChildBuffer = (node: BinaryNode | undefined, childTag: string) => {
|
export const getBinaryNodeChildBuffer = (node: BinaryNode | undefined, childTag: string) => {
|
||||||
const child = getBinaryNodeChild(node, childTag)?.content
|
const child = getBinaryNodeChild(node, childTag)?.content
|
||||||
if(Buffer.isBuffer(child) || child instanceof Uint8Array) {
|
if (Buffer.isBuffer(child) || child instanceof Uint8Array) {
|
||||||
return child
|
return child
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getBinaryNodeChildString = (node: BinaryNode | undefined, childTag: string) => {
|
export const getBinaryNodeChildString = (node: BinaryNode | undefined, childTag: string) => {
|
||||||
const child = getBinaryNodeChild(node, childTag)?.content
|
const child = getBinaryNodeChild(node, childTag)?.content
|
||||||
if(Buffer.isBuffer(child) || child instanceof Uint8Array) {
|
if (Buffer.isBuffer(child) || child instanceof Uint8Array) {
|
||||||
return Buffer.from(child).toString('utf-8')
|
return Buffer.from(child).toString('utf-8')
|
||||||
} else if(typeof child === 'string') {
|
} else if (typeof child === 'string') {
|
||||||
return child
|
return child
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getBinaryNodeChildUInt = (node: BinaryNode, childTag: string, length: number) => {
|
export const getBinaryNodeChildUInt = (node: BinaryNode, childTag: string, length: number) => {
|
||||||
const buff = getBinaryNodeChildBuffer(node, childTag)
|
const buff = getBinaryNodeChildBuffer(node, childTag)
|
||||||
if(buff) {
|
if (buff) {
|
||||||
return bufferToUInt(buff, length)
|
return bufferToUInt(buff, length)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const assertNodeErrorFree = (node: BinaryNode) => {
|
export const assertNodeErrorFree = (node: BinaryNode) => {
|
||||||
const errNode = getBinaryNodeChild(node, 'error')
|
const errNode = getBinaryNodeChild(node, 'error')
|
||||||
if(errNode) {
|
if (errNode) {
|
||||||
throw new Boom(errNode.attrs.text || 'Unknown error', { data: +errNode.attrs.code })
|
throw new Boom(errNode.attrs.text || 'Unknown error', { data: +errNode.attrs.code })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,16 +62,17 @@ export const reduceBinaryNodeToDictionary = (node: BinaryNode, tag: string) => {
|
|||||||
(dict, { attrs }) => {
|
(dict, { attrs }) => {
|
||||||
dict[attrs.name || attrs.config_code] = attrs.value || attrs.config_value
|
dict[attrs.name || attrs.config_code] = attrs.value || attrs.config_value
|
||||||
return dict
|
return dict
|
||||||
}, { } as { [_: string]: string }
|
},
|
||||||
|
{} as { [_: string]: string }
|
||||||
)
|
)
|
||||||
return dict
|
return dict
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getBinaryNodeMessages = ({ content }: BinaryNode) => {
|
export const getBinaryNodeMessages = ({ content }: BinaryNode) => {
|
||||||
const msgs: proto.WebMessageInfo[] = []
|
const msgs: proto.WebMessageInfo[] = []
|
||||||
if(Array.isArray(content)) {
|
if (Array.isArray(content)) {
|
||||||
for(const item of content) {
|
for (const item of content) {
|
||||||
if(item.tag === 'message') {
|
if (item.tag === 'message') {
|
||||||
msgs.push(proto.WebMessageInfo.decode(item.content as Buffer))
|
msgs.push(proto.WebMessageInfo.decode(item.content as Buffer))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,7 +83,7 @@ export const getBinaryNodeMessages = ({ content }: BinaryNode) => {
|
|||||||
|
|
||||||
function bufferToUInt(e: Uint8Array | Buffer, t: number) {
|
function bufferToUInt(e: Uint8Array | Buffer, t: number) {
|
||||||
let a = 0
|
let a = 0
|
||||||
for(let i = 0; i < t; i++) {
|
for (let i = 0; i < t; i++) {
|
||||||
a = 256 * a + e[i]
|
a = 256 * a + e[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,20 +93,20 @@ function bufferToUInt(e: Uint8Array | Buffer, t: number) {
|
|||||||
const tabs = (n: number) => '\t'.repeat(n)
|
const tabs = (n: number) => '\t'.repeat(n)
|
||||||
|
|
||||||
export function binaryNodeToString(node: BinaryNode | BinaryNode['content'], i = 0) {
|
export function binaryNodeToString(node: BinaryNode | BinaryNode['content'], i = 0) {
|
||||||
if(!node) {
|
if (!node) {
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
if(typeof node === 'string') {
|
if (typeof node === 'string') {
|
||||||
return tabs(i) + node
|
return tabs(i) + node
|
||||||
}
|
}
|
||||||
|
|
||||||
if(node instanceof Uint8Array) {
|
if (node instanceof Uint8Array) {
|
||||||
return tabs(i) + Buffer.from(node).toString('hex')
|
return tabs(i) + Buffer.from(node).toString('hex')
|
||||||
}
|
}
|
||||||
|
|
||||||
if(Array.isArray(node)) {
|
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)
|
const children = binaryNodeToString(node.content, i + 1)
|
||||||
@@ -118,4 +119,4 @@ export function binaryNodeToString(node: BinaryNode | BinaryNode['content'], i =
|
|||||||
const content: string = children ? `>\n${children}\n${tabs(i)}</${node.tag}>` : '/>'
|
const content: string = children ? `>\n${children}\n${tabs(i)}</${node.tag}>` : '/>'
|
||||||
|
|
||||||
return tag + content
|
return tag + content
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ export const META_AI_JID = '13135550002@c.us'
|
|||||||
export type JidServer = 'c.us' | 'g.us' | 'broadcast' | 's.whatsapp.net' | 'call' | 'lid' | 'newsletter' | 'bot'
|
export type JidServer = 'c.us' | 'g.us' | 'broadcast' | 's.whatsapp.net' | 'call' | 'lid' | 'newsletter' | 'bot'
|
||||||
|
|
||||||
export type JidWithDevice = {
|
export type JidWithDevice = {
|
||||||
user: string
|
user: string
|
||||||
device?: number
|
device?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FullJid = JidWithDevice & {
|
export type FullJid = JidWithDevice & {
|
||||||
@@ -17,14 +17,13 @@ export type FullJid = JidWithDevice & {
|
|||||||
domainType?: number
|
domainType?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const jidEncode = (user: string | number | null, server: JidServer, device?: number, agent?: number) => {
|
export const jidEncode = (user: string | number | null, server: JidServer, device?: number, agent?: number) => {
|
||||||
return `${user || ''}${!!agent ? `_${agent}` : ''}${!!device ? `:${device}` : ''}@${server}`
|
return `${user || ''}${!!agent ? `_${agent}` : ''}${!!device ? `:${device}` : ''}@${server}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const jidDecode = (jid: string | undefined): FullJid | undefined => {
|
export const jidDecode = (jid: string | undefined): FullJid | undefined => {
|
||||||
const sepIdx = typeof jid === 'string' ? jid.indexOf('@') : -1
|
const sepIdx = typeof jid === 'string' ? jid.indexOf('@') : -1
|
||||||
if(sepIdx < 0) {
|
if (sepIdx < 0) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,34 +42,33 @@ export const jidDecode = (jid: string | undefined): FullJid | undefined => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** is the jid a user */
|
/** 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
|
jidDecode(jid1)?.user === jidDecode(jid2)?.user
|
||||||
)
|
|
||||||
/** is the jid Meta IA */
|
/** 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 */
|
/** 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 */
|
/** 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 */
|
/** 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 */
|
/** 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 */
|
/** is the jid the status broadcast */
|
||||||
export const isJidStatusBroadcast = (jid: string) => jid === 'status@broadcast'
|
export const isJidStatusBroadcast = (jid: string) => jid === 'status@broadcast'
|
||||||
/** is the jid a newsletter */
|
/** 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}$/
|
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) => {
|
export const jidNormalizedUser = (jid: string | undefined) => {
|
||||||
const result = jidDecode(jid)
|
const result = jidDecode(jid)
|
||||||
if(!result) {
|
if (!result) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const { user, server } = result
|
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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import * as constants from './constants'
|
|||||||
* to maintain functional code structure
|
* to maintain functional code structure
|
||||||
* */
|
* */
|
||||||
export type BinaryNode = {
|
export type BinaryNode = {
|
||||||
tag: string
|
tag: string
|
||||||
attrs: { [key: string]: string }
|
attrs: { [key: string]: string }
|
||||||
content?: BinaryNode[] | string | Uint8Array
|
content?: BinaryNode[] | string | Uint8Array
|
||||||
}
|
}
|
||||||
export type BinaryNodeAttributes = BinaryNode['attrs']
|
export type BinaryNodeAttributes = BinaryNode['attrs']
|
||||||
export type BinaryNodeData = BinaryNode['content']
|
export type BinaryNodeData = BinaryNode['content']
|
||||||
|
|
||||||
export type BinaryNodeCodingOptions = typeof constants
|
export type BinaryNodeCodingOptions = typeof constants
|
||||||
|
|||||||
4441
src/WAM/constants.ts
4441
src/WAM/constants.ts
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,14 @@
|
|||||||
import { BinaryInfo } from './BinaryInfo'
|
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)
|
const getHeaderBitLength = (key: number) => (key < 256 ? 2 : 3)
|
||||||
|
|
||||||
@@ -10,12 +19,10 @@ export const encodeWAM = (binaryInfo: BinaryInfo) => {
|
|||||||
encodeEvents(binaryInfo)
|
encodeEvents(binaryInfo)
|
||||||
|
|
||||||
console.log(binaryInfo.buffer)
|
console.log(binaryInfo.buffer)
|
||||||
const totalSize = binaryInfo.buffer
|
const totalSize = binaryInfo.buffer.map(a => a.length).reduce((a, b) => a + b)
|
||||||
.map((a) => a.length)
|
|
||||||
.reduce((a, b) => a + b)
|
|
||||||
const buffer = Buffer.alloc(totalSize)
|
const buffer = Buffer.alloc(totalSize)
|
||||||
let offset = 0
|
let offset = 0
|
||||||
for(const buffer_ of binaryInfo.buffer) {
|
for (const buffer_ of binaryInfo.buffer) {
|
||||||
buffer_.copy(buffer, offset)
|
buffer_.copy(buffer, offset)
|
||||||
offset += buffer_.length
|
offset += buffer_.length
|
||||||
}
|
}
|
||||||
@@ -34,11 +41,11 @@ function encodeWAMHeader(binaryInfo: BinaryInfo) {
|
|||||||
binaryInfo.buffer.push(headerBuffer)
|
binaryInfo.buffer.push(headerBuffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
function encodeGlobalAttributes(binaryInfo: BinaryInfo, globals: {[key: string]: Value}) {
|
function encodeGlobalAttributes(binaryInfo: BinaryInfo, globals: { [key: string]: Value }) {
|
||||||
for(const [key, _value] of Object.entries(globals)) {
|
for (const [key, _value] of Object.entries(globals)) {
|
||||||
const id = WEB_GLOBALS.find(a => a?.name === key)!.id
|
const id = WEB_GLOBALS.find(a => a?.name === key)!.id
|
||||||
let value = _value
|
let value = _value
|
||||||
if(typeof value === 'boolean') {
|
if (typeof value === 'boolean') {
|
||||||
value = value ? 1 : 0
|
value = value ? 1 : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,30 +54,27 @@ function encodeGlobalAttributes(binaryInfo: BinaryInfo, globals: {[key: string]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
function encodeEvents(binaryInfo: BinaryInfo) {
|
function encodeEvents(binaryInfo: BinaryInfo) {
|
||||||
for(const [
|
for (const [name, { props, globals }] of binaryInfo.events.map(a => Object.entries(a)[0])) {
|
||||||
name,
|
|
||||||
{ props, globals },
|
|
||||||
] of binaryInfo.events.map((a) => Object.entries(a)[0])) {
|
|
||||||
encodeGlobalAttributes(binaryInfo, globals)
|
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)
|
const props_ = Object.entries(props)
|
||||||
|
|
||||||
let extended = false
|
let extended = false
|
||||||
|
|
||||||
for(const [, value] of props_) {
|
for (const [, value] of props_) {
|
||||||
extended ||= value !== null
|
extended ||= value !== null
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventFlag = extended ? FLAG_EVENT : FLAG_EVENT | FLAG_EXTENDED
|
const eventFlag = extended ? FLAG_EVENT : FLAG_EVENT | FLAG_EXTENDED
|
||||||
binaryInfo.buffer.push(serializeData(event.id, -event.weight, eventFlag))
|
binaryInfo.buffer.push(serializeData(event.id, -event.weight, eventFlag))
|
||||||
|
|
||||||
for(let i = 0; i < props_.length; i++) {
|
for (let i = 0; i < props_.length; i++) {
|
||||||
const [key, _value] = props_[i]
|
const [key, _value] = props_[i]
|
||||||
const id = (event.props)[key][0]
|
const id = event.props[key][0]
|
||||||
extended = i < (props_.length - 1)
|
extended = i < props_.length - 1
|
||||||
let value = _value
|
let value = _value
|
||||||
if(typeof value === 'boolean') {
|
if (typeof value === 'boolean') {
|
||||||
value = value ? 1 : 0
|
value = value ? 1 : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,34 +84,33 @@ function encodeEvents(binaryInfo: BinaryInfo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function serializeData(key: number, value: Value, flag: number): Buffer {
|
function serializeData(key: number, value: Value, flag: number): Buffer {
|
||||||
const bufferLength = getHeaderBitLength(key)
|
const bufferLength = getHeaderBitLength(key)
|
||||||
let buffer: Buffer
|
let buffer: Buffer
|
||||||
let offset = 0
|
let offset = 0
|
||||||
if(value === null) {
|
if (value === null) {
|
||||||
if(flag === FLAG_GLOBAL) {
|
if (flag === FLAG_GLOBAL) {
|
||||||
buffer = Buffer.alloc(bufferLength)
|
buffer = Buffer.alloc(bufferLength)
|
||||||
offset = serializeHeader(buffer, offset, key, flag)
|
offset = serializeHeader(buffer, offset, key, flag)
|
||||||
return buffer
|
return buffer
|
||||||
}
|
}
|
||||||
} else if(typeof value === 'number' && Number.isInteger(value)) {
|
} else if (typeof value === 'number' && Number.isInteger(value)) {
|
||||||
// is number
|
// is number
|
||||||
if(value === 0 || value === 1) {
|
if (value === 0 || value === 1) {
|
||||||
buffer = Buffer.alloc(bufferLength)
|
buffer = Buffer.alloc(bufferLength)
|
||||||
offset = serializeHeader(buffer, offset, key, flag | ((value + 1) << 4))
|
offset = serializeHeader(buffer, offset, key, flag | ((value + 1) << 4))
|
||||||
return buffer
|
return buffer
|
||||||
} else if(-128 <= value && value < 128) {
|
} else if (-128 <= value && value < 128) {
|
||||||
buffer = Buffer.alloc(bufferLength + 1)
|
buffer = Buffer.alloc(bufferLength + 1)
|
||||||
offset = serializeHeader(buffer, offset, key, flag | (3 << 4))
|
offset = serializeHeader(buffer, offset, key, flag | (3 << 4))
|
||||||
buffer.writeInt8(value, offset)
|
buffer.writeInt8(value, offset)
|
||||||
return buffer
|
return buffer
|
||||||
} else if(-32768 <= value && value < 32768) {
|
} else if (-32768 <= value && value < 32768) {
|
||||||
buffer = Buffer.alloc(bufferLength + 2)
|
buffer = Buffer.alloc(bufferLength + 2)
|
||||||
offset = serializeHeader(buffer, offset, key, flag | (4 << 4))
|
offset = serializeHeader(buffer, offset, key, flag | (4 << 4))
|
||||||
buffer.writeInt16LE(value, offset)
|
buffer.writeInt16LE(value, offset)
|
||||||
return buffer
|
return buffer
|
||||||
} else if(-2147483648 <= value && value < 2147483648) {
|
} else if (-2147483648 <= value && value < 2147483648) {
|
||||||
buffer = Buffer.alloc(bufferLength + 4)
|
buffer = Buffer.alloc(bufferLength + 4)
|
||||||
offset = serializeHeader(buffer, offset, key, flag | (5 << 4))
|
offset = serializeHeader(buffer, offset, key, flag | (5 << 4))
|
||||||
buffer.writeInt32LE(value, offset)
|
buffer.writeInt32LE(value, offset)
|
||||||
@@ -118,20 +121,20 @@ function serializeData(key: number, value: Value, flag: number): Buffer {
|
|||||||
buffer.writeDoubleLE(value, offset)
|
buffer.writeDoubleLE(value, offset)
|
||||||
return buffer
|
return buffer
|
||||||
}
|
}
|
||||||
} else if(typeof value === 'number') {
|
} else if (typeof value === 'number') {
|
||||||
// is float
|
// is float
|
||||||
buffer = Buffer.alloc(bufferLength + 8)
|
buffer = Buffer.alloc(bufferLength + 8)
|
||||||
offset = serializeHeader(buffer, offset, key, flag | (7 << 4))
|
offset = serializeHeader(buffer, offset, key, flag | (7 << 4))
|
||||||
buffer.writeDoubleLE(value, offset)
|
buffer.writeDoubleLE(value, offset)
|
||||||
return buffer
|
return buffer
|
||||||
} else if(typeof value === 'string') {
|
} else if (typeof value === 'string') {
|
||||||
// is string
|
// is string
|
||||||
const utf8Bytes = Buffer.byteLength(value, 'utf8')
|
const utf8Bytes = Buffer.byteLength(value, 'utf8')
|
||||||
if(utf8Bytes < 256) {
|
if (utf8Bytes < 256) {
|
||||||
buffer = Buffer.alloc(bufferLength + 1 + utf8Bytes)
|
buffer = Buffer.alloc(bufferLength + 1 + utf8Bytes)
|
||||||
offset = serializeHeader(buffer, offset, key, flag | (8 << 4))
|
offset = serializeHeader(buffer, offset, key, flag | (8 << 4))
|
||||||
buffer.writeUint8(utf8Bytes, offset++)
|
buffer.writeUint8(utf8Bytes, offset++)
|
||||||
} else if(utf8Bytes < 65536) {
|
} else if (utf8Bytes < 65536) {
|
||||||
buffer = Buffer.alloc(bufferLength + 2 + utf8Bytes)
|
buffer = Buffer.alloc(bufferLength + 2 + utf8Bytes)
|
||||||
offset = serializeHeader(buffer, offset, key, flag | (9 << 4))
|
offset = serializeHeader(buffer, offset, key, flag | (9 << 4))
|
||||||
buffer.writeUInt16LE(utf8Bytes, offset)
|
buffer.writeUInt16LE(utf8Bytes, offset)
|
||||||
@@ -150,13 +153,8 @@ function serializeData(key: number, value: Value, flag: number): Buffer {
|
|||||||
throw 'missing'
|
throw 'missing'
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeHeader(
|
function serializeHeader(buffer: Buffer, offset: number, key: number, flag: number) {
|
||||||
buffer: Buffer,
|
if (key < 256) {
|
||||||
offset: number,
|
|
||||||
key: number,
|
|
||||||
flag: number
|
|
||||||
) {
|
|
||||||
if(key < 256) {
|
|
||||||
buffer.writeUInt8(flag, offset)
|
buffer.writeUInt8(flag, offset)
|
||||||
offset += 1
|
offset += 1
|
||||||
buffer.writeUInt8(key, offset)
|
buffer.writeUInt8(key, offset)
|
||||||
@@ -169,4 +167,4 @@ function serializeHeader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return offset
|
return offset
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export * from './constants'
|
export * from './constants'
|
||||||
export * from './encode'
|
export * from './encode'
|
||||||
export * from './BinaryInfo'
|
export * from './BinaryInfo'
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export class USyncContactProtocol implements USyncQueryProtocol {
|
|||||||
getQueryElement(): BinaryNode {
|
getQueryElement(): BinaryNode {
|
||||||
return {
|
return {
|
||||||
tag: 'contact',
|
tag: 'contact',
|
||||||
attrs: {},
|
attrs: {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,16 +17,16 @@ export class USyncContactProtocol implements USyncQueryProtocol {
|
|||||||
return {
|
return {
|
||||||
tag: 'contact',
|
tag: 'contact',
|
||||||
attrs: {},
|
attrs: {},
|
||||||
content: user.phone,
|
content: user.phone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parser(node: BinaryNode): boolean {
|
parser(node: BinaryNode): boolean {
|
||||||
if(node.tag === 'contact') {
|
if (node.tag === 'contact') {
|
||||||
assertNodeErrorFree(node)
|
assertNodeErrorFree(node)
|
||||||
return node?.attrs?.type === 'in'
|
return node?.attrs?.type === 'in'
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export type DeviceListData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ParsedDeviceInfo = {
|
export type ParsedDeviceInfo = {
|
||||||
deviceList?: DeviceListData[]
|
deviceList?: DeviceListData[]
|
||||||
keyIndex?: KeyIndexData
|
keyIndex?: KeyIndexData
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,8 +26,8 @@ export class USyncDeviceProtocol implements USyncQueryProtocol {
|
|||||||
return {
|
return {
|
||||||
tag: 'devices',
|
tag: 'devices',
|
||||||
attrs: {
|
attrs: {
|
||||||
version: '2',
|
version: '2'
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,16 +42,16 @@ export class USyncDeviceProtocol implements USyncQueryProtocol {
|
|||||||
const deviceList: DeviceListData[] = []
|
const deviceList: DeviceListData[] = []
|
||||||
let keyIndex: KeyIndexData | undefined = undefined
|
let keyIndex: KeyIndexData | undefined = undefined
|
||||||
|
|
||||||
if(node.tag === 'devices') {
|
if (node.tag === 'devices') {
|
||||||
assertNodeErrorFree(node)
|
assertNodeErrorFree(node)
|
||||||
const deviceListNode = getBinaryNodeChild(node, 'device-list')
|
const deviceListNode = getBinaryNodeChild(node, 'device-list')
|
||||||
const keyIndexNode = getBinaryNodeChild(node, 'key-index-list')
|
const keyIndexNode = getBinaryNodeChild(node, 'key-index-list')
|
||||||
|
|
||||||
if(Array.isArray(deviceListNode?.content)) {
|
if (Array.isArray(deviceListNode?.content)) {
|
||||||
for(const { tag, attrs } of deviceListNode.content) {
|
for (const { tag, attrs } of deviceListNode.content) {
|
||||||
const id = +attrs.id
|
const id = +attrs.id
|
||||||
const keyIndex = +attrs['key-index']
|
const keyIndex = +attrs['key-index']
|
||||||
if(tag === 'device') {
|
if (tag === 'device') {
|
||||||
deviceList.push({
|
deviceList.push({
|
||||||
id,
|
id,
|
||||||
keyIndex,
|
keyIndex,
|
||||||
@@ -61,7 +61,7 @@ export class USyncDeviceProtocol implements USyncQueryProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(keyIndexNode?.tag === 'key-index-list') {
|
if (keyIndexNode?.tag === 'key-index-list') {
|
||||||
keyIndex = {
|
keyIndex = {
|
||||||
timestamp: +keyIndexNode.attrs['ts'],
|
timestamp: +keyIndexNode.attrs['ts'],
|
||||||
signedKeyIndex: keyIndexNode?.content as Uint8Array,
|
signedKeyIndex: keyIndexNode?.content as Uint8Array,
|
||||||
@@ -75,4 +75,4 @@ export class USyncDeviceProtocol implements USyncQueryProtocol {
|
|||||||
keyIndex
|
keyIndex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export class USyncDisappearingModeProtocol implements USyncQueryProtocol {
|
|||||||
getQueryElement(): BinaryNode {
|
getQueryElement(): BinaryNode {
|
||||||
return {
|
return {
|
||||||
tag: 'disappearing_mode',
|
tag: 'disappearing_mode',
|
||||||
attrs: {},
|
attrs: {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,15 +21,15 @@ export class USyncDisappearingModeProtocol implements USyncQueryProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parser(node: BinaryNode): DisappearingModeData | undefined {
|
parser(node: BinaryNode): DisappearingModeData | undefined {
|
||||||
if(node.tag === 'status') {
|
if (node.tag === 'status') {
|
||||||
assertNodeErrorFree(node)
|
assertNodeErrorFree(node)
|
||||||
const duration: number = +node?.attrs.duration
|
const duration: number = +node?.attrs.duration
|
||||||
const setAt = new Date(+(node?.attrs.t || 0) * 1000)
|
const setAt = new Date(+(node?.attrs.t || 0) * 1000)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
duration,
|
duration,
|
||||||
setAt,
|
setAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export class USyncStatusProtocol implements USyncQueryProtocol {
|
|||||||
getQueryElement(): BinaryNode {
|
getQueryElement(): BinaryNode {
|
||||||
return {
|
return {
|
||||||
tag: 'status',
|
tag: 'status',
|
||||||
attrs: {},
|
attrs: {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,24 +21,24 @@ export class USyncStatusProtocol implements USyncQueryProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parser(node: BinaryNode): StatusData | undefined {
|
parser(node: BinaryNode): StatusData | undefined {
|
||||||
if(node.tag === 'status') {
|
if (node.tag === 'status') {
|
||||||
assertNodeErrorFree(node)
|
assertNodeErrorFree(node)
|
||||||
let status: string | null = node?.content!.toString()
|
let status: string | null = node?.content!.toString()
|
||||||
const setAt = new Date(+(node?.attrs.t || 0) * 1000)
|
const setAt = new Date(+(node?.attrs.t || 0) * 1000)
|
||||||
if(!status) {
|
if (!status) {
|
||||||
if(+node.attrs?.code === 401) {
|
if (+node.attrs?.code === 401) {
|
||||||
status = ''
|
status = ''
|
||||||
} else {
|
} else {
|
||||||
status = null
|
status = null
|
||||||
}
|
}
|
||||||
} else if(typeof status === 'string' && status.length === 0) {
|
} else if (typeof status === 'string' && status.length === 0) {
|
||||||
status = null
|
status = null
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status,
|
status,
|
||||||
setAt,
|
setAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,21 +3,21 @@ import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, getBinaryNodeChi
|
|||||||
import { USyncUser } from '../USyncUser'
|
import { USyncUser } from '../USyncUser'
|
||||||
|
|
||||||
export type BotProfileCommand = {
|
export type BotProfileCommand = {
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BotProfileInfo = {
|
export type BotProfileInfo = {
|
||||||
jid: string
|
jid: string
|
||||||
name: string
|
name: string
|
||||||
attributes: string
|
attributes: string
|
||||||
description: string
|
description: string
|
||||||
category: string
|
category: string
|
||||||
isDefault: boolean
|
isDefault: boolean
|
||||||
prompts: string[]
|
prompts: string[]
|
||||||
personaId: string
|
personaId: string
|
||||||
commands: BotProfileCommand[]
|
commands: BotProfileCommand[]
|
||||||
commandsDescription: string
|
commandsDescription: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export class USyncBotProfileProtocol implements USyncQueryProtocol {
|
export class USyncBotProfileProtocol implements USyncQueryProtocol {
|
||||||
@@ -26,7 +26,7 @@ export class USyncBotProfileProtocol implements USyncQueryProtocol {
|
|||||||
getQueryElement(): BinaryNode {
|
getQueryElement(): BinaryNode {
|
||||||
return {
|
return {
|
||||||
tag: 'bot',
|
tag: 'bot',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: [{ tag: 'profile', attrs: { v: '1' } }]
|
content: [{ tag: 'profile', attrs: { v: '1' } }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,14 +34,14 @@ export class USyncBotProfileProtocol implements USyncQueryProtocol {
|
|||||||
getUserElement(user: USyncUser): BinaryNode {
|
getUserElement(user: USyncUser): BinaryNode {
|
||||||
return {
|
return {
|
||||||
tag: 'bot',
|
tag: 'bot',
|
||||||
attrs: { },
|
attrs: {},
|
||||||
content: [{ tag: 'profile', attrs: { 'persona_id': user.personaId } }]
|
content: [{ tag: 'profile', attrs: { persona_id: user.personaId } }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parser(node: BinaryNode): BotProfileInfo {
|
parser(node: BinaryNode): BotProfileInfo {
|
||||||
const botNode = getBinaryNodeChild(node, 'bot')
|
const botNode = getBinaryNodeChild(node, 'bot')
|
||||||
const profile = getBinaryNodeChild(botNode, 'profile')
|
const profile = getBinaryNodeChild(botNode, 'profile')
|
||||||
|
|
||||||
const commandsNode = getBinaryNodeChild(profile, 'commands')
|
const commandsNode = getBinaryNodeChild(profile, 'commands')
|
||||||
const promptsNode = getBinaryNodeChild(profile, 'prompts')
|
const promptsNode = getBinaryNodeChild(profile, 'prompts')
|
||||||
@@ -49,21 +49,20 @@ export class USyncBotProfileProtocol implements USyncQueryProtocol {
|
|||||||
const commands: BotProfileCommand[] = []
|
const commands: BotProfileCommand[] = []
|
||||||
const prompts: string[] = []
|
const prompts: string[] = []
|
||||||
|
|
||||||
for(const command of getBinaryNodeChildren(commandsNode, 'command')) {
|
for (const command of getBinaryNodeChildren(commandsNode, 'command')) {
|
||||||
commands.push({
|
commands.push({
|
||||||
name: getBinaryNodeChildString(command, 'name')!,
|
name: getBinaryNodeChildString(command, 'name')!,
|
||||||
description: getBinaryNodeChildString(command, 'description')!
|
description: getBinaryNodeChildString(command, 'description')!
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for(const prompt of getBinaryNodeChildren(promptsNode, 'prompt')) {
|
for (const prompt of getBinaryNodeChildren(promptsNode, 'prompt')) {
|
||||||
prompts.push(`${getBinaryNodeChildString(prompt, 'emoji')!} ${getBinaryNodeChildString(prompt, 'text')!}`)
|
prompts.push(`${getBinaryNodeChildString(prompt, 'emoji')!} ${getBinaryNodeChildString(prompt, 'text')!}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isDefault: !!getBinaryNodeChild(profile, 'default'),
|
isDefault: !!getBinaryNodeChild(profile, 'default'),
|
||||||
jid: node.attrs.jid,
|
jid: node.attrs.jid,
|
||||||
name: getBinaryNodeChildString(profile, 'name')!,
|
name: getBinaryNodeChildString(profile, 'name')!,
|
||||||
attributes: getBinaryNodeChildString(profile, 'attributes')!,
|
attributes: getBinaryNodeChildString(profile, 'attributes')!,
|
||||||
description: getBinaryNodeChildString(profile, 'description')!,
|
description: getBinaryNodeChildString(profile, 'description')!,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export class USyncLIDProtocol implements USyncQueryProtocol {
|
|||||||
getQueryElement(): BinaryNode {
|
getQueryElement(): BinaryNode {
|
||||||
return {
|
return {
|
||||||
tag: 'lid',
|
tag: 'lid',
|
||||||
attrs: {},
|
attrs: {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ export class USyncLIDProtocol implements USyncQueryProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parser(node: BinaryNode): string | null {
|
parser(node: BinaryNode): string | null {
|
||||||
if(node.tag === 'lid') {
|
if (node.tag === 'lid') {
|
||||||
return node.attrs.val
|
return node.attrs.val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export * from './USyncDeviceProtocol'
|
export * from './USyncDeviceProtocol'
|
||||||
export * from './USyncContactProtocol'
|
export * from './USyncContactProtocol'
|
||||||
export * from './USyncStatusProtocol'
|
export * from './USyncStatusProtocol'
|
||||||
export * from './USyncDisappearingModeProtocol'
|
export * from './USyncDisappearingModeProtocol'
|
||||||
|
|||||||
@@ -2,14 +2,19 @@ import { USyncQueryProtocol } from '../Types/USync'
|
|||||||
import { BinaryNode, getBinaryNodeChild } from '../WABinary'
|
import { BinaryNode, getBinaryNodeChild } from '../WABinary'
|
||||||
import { USyncBotProfileProtocol } from './Protocols/UsyncBotProfileProtocol'
|
import { USyncBotProfileProtocol } from './Protocols/UsyncBotProfileProtocol'
|
||||||
import { USyncLIDProtocol } from './Protocols/UsyncLIDProtocol'
|
import { USyncLIDProtocol } from './Protocols/UsyncLIDProtocol'
|
||||||
import { USyncContactProtocol, USyncDeviceProtocol, USyncDisappearingModeProtocol, USyncStatusProtocol } from './Protocols'
|
import {
|
||||||
|
USyncContactProtocol,
|
||||||
|
USyncDeviceProtocol,
|
||||||
|
USyncDisappearingModeProtocol,
|
||||||
|
USyncStatusProtocol
|
||||||
|
} from './Protocols'
|
||||||
import { USyncUser } from './USyncUser'
|
import { USyncUser } from './USyncUser'
|
||||||
|
|
||||||
export type USyncQueryResultList = { [protocol: string]: unknown, id: string }
|
export type USyncQueryResultList = { [protocol: string]: unknown; id: string }
|
||||||
|
|
||||||
export type USyncQueryResult = {
|
export type USyncQueryResult = {
|
||||||
list: USyncQueryResultList[]
|
list: USyncQueryResultList[]
|
||||||
sideList: USyncQueryResultList[]
|
sideList: USyncQueryResultList[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export class USyncQuery {
|
export class USyncQuery {
|
||||||
@@ -41,18 +46,20 @@ export class USyncQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parseUSyncQueryResult(result: BinaryNode): USyncQueryResult | undefined {
|
parseUSyncQueryResult(result: BinaryNode): USyncQueryResult | undefined {
|
||||||
if(result.attrs.type !== 'result') {
|
if (result.attrs.type !== 'result') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const protocolMap = Object.fromEntries(this.protocols.map((protocol) => {
|
const protocolMap = Object.fromEntries(
|
||||||
return [protocol.name, protocol.parser]
|
this.protocols.map(protocol => {
|
||||||
}))
|
return [protocol.name, protocol.parser]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
const queryResult: USyncQueryResult = {
|
const queryResult: USyncQueryResult = {
|
||||||
// TODO: implement errors etc.
|
// TODO: implement errors etc.
|
||||||
list: [],
|
list: [],
|
||||||
sideList: [],
|
sideList: []
|
||||||
}
|
}
|
||||||
|
|
||||||
const usyncNode = getBinaryNodeChild(result, 'usync')
|
const usyncNode = getBinaryNodeChild(result, 'usync')
|
||||||
@@ -62,18 +69,24 @@ export class USyncQuery {
|
|||||||
//const resultNode = getBinaryNodeChild(usyncNode, 'result')
|
//const resultNode = getBinaryNodeChild(usyncNode, 'result')
|
||||||
|
|
||||||
const listNode = getBinaryNodeChild(usyncNode, 'list')
|
const listNode = getBinaryNodeChild(usyncNode, 'list')
|
||||||
if(Array.isArray(listNode?.content) && typeof listNode !== 'undefined') {
|
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 id = node?.attrs.jid
|
||||||
const data = Array.isArray(node?.content) ? Object.fromEntries(node.content.map((content) => {
|
const data = Array.isArray(node?.content)
|
||||||
const protocol = content.tag
|
? Object.fromEntries(
|
||||||
const parser = protocolMap[protocol]
|
node.content
|
||||||
if(parser) {
|
.map(content => {
|
||||||
return [protocol, parser(content)]
|
const protocol = content.tag
|
||||||
} else {
|
const parser = protocolMap[protocol]
|
||||||
return [protocol, null]
|
if (parser) {
|
||||||
}
|
return [protocol, parser(content)]
|
||||||
}).filter(([, b]) => b !== null) as [string, unknown][]) : {}
|
} else {
|
||||||
|
return [protocol, null]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(([, b]) => b !== null) as [string, unknown][]
|
||||||
|
)
|
||||||
|
: {}
|
||||||
return { ...data, id }
|
return { ...data, id }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export class USyncUser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
withPersonaId(personaId: string) {
|
withPersonaId(personaId: string) {
|
||||||
this.personaId = personaId
|
this.personaId = personaId
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export * from './Protocols'
|
export * from './Protocols'
|
||||||
export * from './USyncQuery'
|
export * from './USyncQuery'
|
||||||
export * from './USyncUser'
|
export * from './USyncUser'
|
||||||
|
|||||||
Reference in New Issue
Block a user