@whiskeysockets/baileys
Version:
WhatsApp API
620 lines (619 loc) • 25.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeSocket = void 0;
const boom_1 = require("@hapi/boom");
const crypto_1 = require("crypto");
const url_1 = require("url");
const util_1 = require("util");
const WAProto_1 = require("../../WAProto");
const Defaults_1 = require("../Defaults");
const Types_1 = require("../Types");
const Utils_1 = require("../Utils");
const WABinary_1 = require("../WABinary");
const Client_1 = require("./Client");
/**
* Connects to WA servers and performs:
* - simple queries (no retry mechanism, wait for connection establishment)
* - listen to messages and emit events
* - query phone connection
*/
const makeSocket = (config) => {
var _a, _b;
const { waWebSocketUrl, connectTimeoutMs, logger, keepAliveIntervalMs, browser, auth: authState, printQRInTerminal, defaultQueryTimeoutMs, transactionOpts, qrTimeout, makeSignalRepository, } = config;
let url = typeof waWebSocketUrl === 'string' ? new url_1.URL(waWebSocketUrl) : waWebSocketUrl;
config.mobile = config.mobile || url.protocol === 'tcp:';
if (config.mobile && url.protocol !== 'tcp:') {
url = new url_1.URL(`tcp://${Defaults_1.MOBILE_ENDPOINT}:${Defaults_1.MOBILE_PORT}`);
}
if (!config.mobile && url.protocol === 'wss' && ((_a = authState === null || authState === void 0 ? void 0 : authState.creds) === null || _a === void 0 ? void 0 : _a.routingInfo)) {
url.searchParams.append('ED', authState.creds.routingInfo.toString('base64url'));
}
const ws = config.socket ? config.socket : config.mobile ? new Client_1.MobileSocketClient(url, config) : new Client_1.WebSocketClient(url, config);
ws.connect();
const ev = (0, Utils_1.makeEventBuffer)(logger);
/** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
const ephemeralKeyPair = Utils_1.Curve.generateKeyPair();
/** WA noise protocol wrapper */
const noise = (0, Utils_1.makeNoiseHandler)({
keyPair: ephemeralKeyPair,
NOISE_HEADER: config.mobile ? Defaults_1.MOBILE_NOISE_HEADER : Defaults_1.NOISE_WA_HEADER,
mobile: config.mobile,
logger,
routingInfo: (_b = authState === null || authState === void 0 ? void 0 : authState.creds) === null || _b === void 0 ? void 0 : _b.routingInfo
});
const { creds } = authState;
// add transaction capability
const keys = (0, Utils_1.addTransactionCapability)(authState.keys, logger, transactionOpts);
const signalRepository = makeSignalRepository({ creds, keys });
let lastDateRecv;
let epoch = 1;
let keepAliveReq;
let qrTimer;
let closed = false;
const uqTagId = (0, Utils_1.generateMdTagPrefix)();
const generateMessageTag = () => `${uqTagId}${epoch++}`;
const sendPromise = (0, util_1.promisify)(ws.send);
/** send a raw buffer */
const sendRawMessage = async (data) => {
if (!ws.isOpen) {
throw new boom_1.Boom('Connection Closed', { statusCode: Types_1.DisconnectReason.connectionClosed });
}
const bytes = noise.encodeFrame(data);
await (0, Utils_1.promiseTimeout)(connectTimeoutMs, async (resolve, reject) => {
try {
await sendPromise.call(ws, bytes);
resolve();
}
catch (error) {
reject(error);
}
});
};
/** send a binary node */
const sendNode = (frame) => {
if (logger.level === 'trace') {
logger.trace({ xml: (0, WABinary_1.binaryNodeToString)(frame), msg: 'xml send' });
}
const buff = (0, WABinary_1.encodeBinaryNode)(frame);
return sendRawMessage(buff);
};
/** log & process any unexpected errors */
const onUnexpectedError = (err, msg) => {
logger.error({ err }, `unexpected error in '${msg}'`);
};
/** await the next incoming message */
const awaitNextMessage = async (sendMsg) => {
if (!ws.isOpen) {
throw new boom_1.Boom('Connection Closed', {
statusCode: Types_1.DisconnectReason.connectionClosed
});
}
let onOpen;
let onClose;
const result = (0, Utils_1.promiseTimeout)(connectTimeoutMs, (resolve, reject) => {
onOpen = resolve;
onClose = mapWebSocketError(reject);
ws.on('frame', onOpen);
ws.on('close', onClose);
ws.on('error', onClose);
})
.finally(() => {
ws.off('frame', onOpen);
ws.off('close', onClose);
ws.off('error', onClose);
});
if (sendMsg) {
sendRawMessage(sendMsg).catch(onClose);
}
return result;
};
/**
* Wait for a message with a certain tag to be received
* @param msgId the message tag to await
* @param timeoutMs timeout after which the promise will reject
*/
const waitForMessage = async (msgId, timeoutMs = defaultQueryTimeoutMs) => {
let onRecv;
let onErr;
try {
return await (0, Utils_1.promiseTimeout)(timeoutMs, (resolve, reject) => {
onRecv = resolve;
onErr = err => {
reject(err || new boom_1.Boom('Connection Closed', { statusCode: Types_1.DisconnectReason.connectionClosed }));
};
ws.on(`TAG:${msgId}`, onRecv);
ws.on('close', onErr); // if the socket closes, you'll never receive the message
ws.off('error', onErr);
});
}
finally {
ws.off(`TAG:${msgId}`, onRecv);
ws.off('close', onErr); // if the socket closes, you'll never receive the message
ws.off('error', onErr);
}
};
/** send a query, and wait for its response. auto-generates message ID if not provided */
const query = async (node, timeoutMs) => {
if (!node.attrs.id) {
node.attrs.id = generateMessageTag();
}
const msgId = node.attrs.id;
const wait = waitForMessage(msgId, timeoutMs);
await sendNode(node);
const result = await wait;
if ('tag' in result) {
(0, WABinary_1.assertNodeErrorFree)(result);
}
return result;
};
/** connection handshake */
const validateConnection = async () => {
let helloMsg = {
clientHello: { ephemeral: ephemeralKeyPair.public }
};
helloMsg = WAProto_1.proto.HandshakeMessage.fromObject(helloMsg);
logger.info({ browser, helloMsg }, 'connected to WA');
const init = WAProto_1.proto.HandshakeMessage.encode(helloMsg).finish();
const result = await awaitNextMessage(init);
const handshake = WAProto_1.proto.HandshakeMessage.decode(result);
logger.trace({ handshake }, 'handshake recv from WA');
const keyEnc = noise.processHandshake(handshake, creds.noiseKey);
let node;
if (config.mobile) {
node = (0, Utils_1.generateMobileNode)(config);
}
else if (!creds.me) {
node = (0, Utils_1.generateRegistrationNode)(creds, config);
logger.info({ node }, 'not logged in, attempting registration...');
}
else {
node = (0, Utils_1.generateLoginNode)(creds.me.id, config);
logger.info({ node }, 'logging in...');
}
const payloadEnc = noise.encrypt(WAProto_1.proto.ClientPayload.encode(node).finish());
await sendRawMessage(WAProto_1.proto.HandshakeMessage.encode({
clientFinish: {
static: keyEnc,
payload: payloadEnc,
},
}).finish());
noise.finishInit();
startKeepAliveRequest();
};
const getAvailablePreKeysOnServer = async () => {
const result = await query({
tag: 'iq',
attrs: {
id: generateMessageTag(),
xmlns: 'encrypt',
type: 'get',
to: WABinary_1.S_WHATSAPP_NET
},
content: [
{ tag: 'count', attrs: {} }
]
});
const countChild = (0, WABinary_1.getBinaryNodeChild)(result, 'count');
return +countChild.attrs.value;
};
/** generates and uploads a set of pre-keys to the server */
const uploadPreKeys = async (count = Defaults_1.INITIAL_PREKEY_COUNT) => {
await keys.transaction(async () => {
logger.info({ count }, 'uploading pre-keys');
const { update, node } = await (0, Utils_1.getNextPreKeysNode)({ creds, keys }, count);
await query(node);
ev.emit('creds.update', update);
logger.info({ count }, 'uploaded pre-keys');
});
};
const uploadPreKeysToServerIfRequired = async () => {
const preKeyCount = await getAvailablePreKeysOnServer();
logger.info(`${preKeyCount} pre-keys found on server`);
if (preKeyCount <= Defaults_1.MIN_PREKEY_COUNT) {
await uploadPreKeys();
}
};
const onMessageReceived = (data) => {
noise.decodeFrame(data, frame => {
var _a;
// reset ping timeout
lastDateRecv = new Date();
let anyTriggered = false;
anyTriggered = ws.emit('frame', frame);
// if it's a binary node
if (!(frame instanceof Uint8Array)) {
const msgId = frame.attrs.id;
if (logger.level === 'trace') {
logger.trace({ xml: (0, WABinary_1.binaryNodeToString)(frame), msg: 'recv xml' });
}
/* Check if this is a response to a message we sent */
anyTriggered = ws.emit(`${Defaults_1.DEF_TAG_PREFIX}${msgId}`, frame) || anyTriggered;
/* Check if this is a response to a message we are expecting */
const l0 = frame.tag;
const l1 = frame.attrs || {};
const l2 = Array.isArray(frame.content) ? (_a = frame.content[0]) === null || _a === void 0 ? void 0 : _a.tag : '';
Object.keys(l1).forEach(key => {
anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, frame) || anyTriggered;
anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, frame) || anyTriggered;
anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},${key}`, frame) || anyTriggered;
});
anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},,${l2}`, frame) || anyTriggered;
anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0}`, frame) || anyTriggered;
if (!anyTriggered && logger.level === 'debug') {
logger.debug({ unhandled: true, msgId, fromMe: false, frame }, 'communication recv');
}
}
});
};
const end = (error) => {
if (closed) {
logger.trace({ trace: error === null || error === void 0 ? void 0 : error.stack }, 'connection already closed');
return;
}
closed = true;
logger.info({ trace: error === null || error === void 0 ? void 0 : error.stack }, error ? 'connection errored' : 'connection closed');
clearInterval(keepAliveReq);
clearTimeout(qrTimer);
ws.removeAllListeners('close');
ws.removeAllListeners('error');
ws.removeAllListeners('open');
ws.removeAllListeners('message');
if (!ws.isClosed && !ws.isClosing) {
try {
ws.close();
}
catch (_a) { }
}
ev.emit('connection.update', {
connection: 'close',
lastDisconnect: {
error,
date: new Date()
}
});
ev.removeAllListeners('connection.update');
};
const waitForSocketOpen = async () => {
if (ws.isOpen) {
return;
}
if (ws.isClosed || ws.isClosing) {
throw new boom_1.Boom('Connection Closed', { statusCode: Types_1.DisconnectReason.connectionClosed });
}
let onOpen;
let onClose;
await new Promise((resolve, reject) => {
onOpen = () => resolve(undefined);
onClose = mapWebSocketError(reject);
ws.on('open', onOpen);
ws.on('close', onClose);
ws.on('error', onClose);
})
.finally(() => {
ws.off('open', onOpen);
ws.off('close', onClose);
ws.off('error', onClose);
});
};
const startKeepAliveRequest = () => (keepAliveReq = setInterval(() => {
if (!lastDateRecv) {
lastDateRecv = new Date();
}
const diff = Date.now() - lastDateRecv.getTime();
/*
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
*/
if (diff > keepAliveIntervalMs + 5000) {
end(new boom_1.Boom('Connection was lost', { statusCode: Types_1.DisconnectReason.connectionLost }));
}
else if (ws.isOpen) {
// if its all good, send a keep alive request
query({
tag: 'iq',
attrs: {
id: generateMessageTag(),
to: WABinary_1.S_WHATSAPP_NET,
type: 'get',
xmlns: 'w:p',
},
content: [{ tag: 'ping', attrs: {} }]
})
.catch(err => {
logger.error({ trace: err.stack }, 'error in sending keep alive');
});
}
else {
logger.warn('keep alive called when WS not open');
}
}, keepAliveIntervalMs));
/** i have no idea why this exists. pls enlighten me */
const sendPassiveIq = (tag) => (query({
tag: 'iq',
attrs: {
to: WABinary_1.S_WHATSAPP_NET,
xmlns: 'passive',
type: 'set',
},
content: [
{ tag, attrs: {} }
]
}));
/** logout & invalidate connection */
const logout = async (msg) => {
var _a;
const jid = (_a = authState.creds.me) === null || _a === void 0 ? void 0 : _a.id;
if (jid) {
await sendNode({
tag: 'iq',
attrs: {
to: WABinary_1.S_WHATSAPP_NET,
type: 'set',
id: generateMessageTag(),
xmlns: 'md'
},
content: [
{
tag: 'remove-companion-device',
attrs: {
jid,
reason: 'user_initiated'
}
}
]
});
}
end(new boom_1.Boom(msg || 'Intentional Logout', { statusCode: Types_1.DisconnectReason.loggedOut }));
};
const requestPairingCode = async (phoneNumber) => {
authState.creds.pairingCode = (0, Utils_1.bytesToCrockford)((0, crypto_1.randomBytes)(5));
authState.creds.me = {
id: (0, WABinary_1.jidEncode)(phoneNumber, 's.whatsapp.net'),
name: '~'
};
ev.emit('creds.update', authState.creds);
await sendNode({
tag: 'iq',
attrs: {
to: WABinary_1.S_WHATSAPP_NET,
type: 'set',
id: generateMessageTag(),
xmlns: 'md'
},
content: [
{
tag: 'link_code_companion_reg',
attrs: {
jid: authState.creds.me.id,
stage: 'companion_hello',
// eslint-disable-next-line camelcase
should_show_push_notification: 'true'
},
content: [
{
tag: 'link_code_pairing_wrapped_companion_ephemeral_pub',
attrs: {},
content: await generatePairingKey()
},
{
tag: 'companion_server_auth_key_pub',
attrs: {},
content: authState.creds.noiseKey.public
},
{
tag: 'companion_platform_id',
attrs: {},
content: (0, Utils_1.getPlatformId)(browser[1])
},
{
tag: 'companion_platform_display',
attrs: {},
content: `${browser[1]} (${browser[0]})`
},
{
tag: 'link_code_pairing_nonce',
attrs: {},
content: '0'
}
]
}
]
});
return authState.creds.pairingCode;
};
async function generatePairingKey() {
const salt = (0, crypto_1.randomBytes)(32);
const randomIv = (0, crypto_1.randomBytes)(16);
const key = await (0, Utils_1.derivePairingCodeKey)(authState.creds.pairingCode, salt);
const ciphered = (0, Utils_1.aesEncryptCTR)(authState.creds.pairingEphemeralKeyPair.public, key, randomIv);
return Buffer.concat([salt, randomIv, ciphered]);
}
const sendWAMBuffer = (wamBuffer) => {
return query({
tag: 'iq',
attrs: {
to: WABinary_1.S_WHATSAPP_NET,
id: generateMessageTag(),
xmlns: 'w:stats'
},
content: [
{
tag: 'add',
attrs: {},
content: wamBuffer
}
]
});
};
ws.on('message', onMessageReceived);
ws.on('open', async () => {
try {
await validateConnection();
}
catch (err) {
logger.error({ err }, 'error in validating connection');
end(err);
}
});
ws.on('error', mapWebSocketError(end));
ws.on('close', () => end(new boom_1.Boom('Connection Terminated', { statusCode: Types_1.DisconnectReason.connectionClosed })));
// the server terminated the connection
ws.on('CB:xmlstreamend', () => end(new boom_1.Boom('Connection Terminated by Server', { statusCode: Types_1.DisconnectReason.connectionClosed })));
// QR gen
ws.on('CB:iq,type:set,pair-device', async (stanza) => {
const iq = {
tag: 'iq',
attrs: {
to: WABinary_1.S_WHATSAPP_NET,
type: 'result',
id: stanza.attrs.id,
}
};
await sendNode(iq);
const pairDeviceNode = (0, WABinary_1.getBinaryNodeChild)(stanza, 'pair-device');
const refNodes = (0, WABinary_1.getBinaryNodeChildren)(pairDeviceNode, 'ref');
const noiseKeyB64 = Buffer.from(creds.noiseKey.public).toString('base64');
const identityKeyB64 = Buffer.from(creds.signedIdentityKey.public).toString('base64');
const advB64 = creds.advSecretKey;
let qrMs = qrTimeout || 60000; // time to let a QR live
const genPairQR = () => {
if (!ws.isOpen) {
return;
}
const refNode = refNodes.shift();
if (!refNode) {
end(new boom_1.Boom('QR refs attempts ended', { statusCode: Types_1.DisconnectReason.timedOut }));
return;
}
const ref = refNode.content.toString('utf-8');
const qr = [ref, noiseKeyB64, identityKeyB64, advB64].join(',');
ev.emit('connection.update', { qr });
qrTimer = setTimeout(genPairQR, qrMs);
qrMs = qrTimeout || 20000; // shorter subsequent qrs
};
genPairQR();
});
// device paired for the first time
// if device pairs successfully, the server asks to restart the connection
ws.on('CB:iq,,pair-success', async (stanza) => {
logger.debug('pair success recv');
try {
const { reply, creds: updatedCreds } = (0, Utils_1.configureSuccessfulPairing)(stanza, creds);
logger.info({ me: updatedCreds.me, platform: updatedCreds.platform }, 'pairing configured successfully, expect to restart the connection...');
ev.emit('creds.update', updatedCreds);
ev.emit('connection.update', { isNewLogin: true, qr: undefined });
await sendNode(reply);
}
catch (error) {
logger.info({ trace: error.stack }, 'error in pairing');
end(error);
}
});
// login complete
ws.on('CB:success', async (node) => {
await uploadPreKeysToServerIfRequired();
await sendPassiveIq('active');
logger.info('opened connection to WA');
clearTimeout(qrTimer); // will never happen in all likelyhood -- but just in case WA sends success on first try
ev.emit('creds.update', { me: { ...authState.creds.me, lid: node.attrs.lid } });
ev.emit('connection.update', { connection: 'open' });
});
ws.on('CB:stream:error', (node) => {
logger.error({ node }, 'stream errored out');
const { reason, statusCode } = (0, Utils_1.getErrorCodeFromStreamError)(node);
end(new boom_1.Boom(`Stream Errored (${reason})`, { statusCode, data: node }));
});
// stream fail, possible logout
ws.on('CB:failure', (node) => {
const reason = +(node.attrs.reason || 500);
end(new boom_1.Boom('Connection Failure', { statusCode: reason, data: node.attrs }));
});
ws.on('CB:ib,,downgrade_webclient', () => {
end(new boom_1.Boom('Multi-device beta not joined', { statusCode: Types_1.DisconnectReason.multideviceMismatch }));
});
ws.on('CB:ib,,edge_routing', (node) => {
const edgeRoutingNode = (0, WABinary_1.getBinaryNodeChild)(node, 'edge_routing');
const routingInfo = (0, WABinary_1.getBinaryNodeChild)(edgeRoutingNode, 'routing_info');
if (routingInfo === null || routingInfo === void 0 ? void 0 : routingInfo.content) {
authState.creds.routingInfo = Buffer.from(routingInfo === null || routingInfo === void 0 ? void 0 : routingInfo.content);
ev.emit('creds.update', authState.creds);
}
});
let didStartBuffer = false;
process.nextTick(() => {
var _a;
if ((_a = creds.me) === null || _a === void 0 ? void 0 : _a.id) {
// start buffering important events
// if we're logged in
ev.buffer();
didStartBuffer = true;
}
ev.emit('connection.update', { connection: 'connecting', receivedPendingNotifications: false, qr: undefined });
});
// called when all offline notifs are handled
ws.on('CB:ib,,offline', (node) => {
const child = (0, WABinary_1.getBinaryNodeChild)(node, 'offline');
const offlineNotifs = +((child === null || child === void 0 ? void 0 : child.attrs.count) || 0);
logger.info(`handled ${offlineNotifs} offline messages/notifications`);
if (didStartBuffer) {
ev.flush();
logger.trace('flushed events for initial buffer');
}
ev.emit('connection.update', { receivedPendingNotifications: true });
});
// update credentials when required
ev.on('creds.update', update => {
var _a, _b;
const name = (_a = update.me) === null || _a === void 0 ? void 0 : _a.name;
// if name has just been received
if (((_b = creds.me) === null || _b === void 0 ? void 0 : _b.name) !== name) {
logger.debug({ name }, 'updated pushName');
sendNode({
tag: 'presence',
attrs: { name: name }
})
.catch(err => {
logger.warn({ trace: err.stack }, 'error in sending presence update on name change');
});
}
Object.assign(creds, update);
});
if (printQRInTerminal) {
(0, Utils_1.printQRIfNecessaryListener)(ev, logger);
}
return {
type: 'md',
ws,
ev,
authState: { creds, keys },
signalRepository,
get user() {
return authState.creds.me;
},
generateMessageTag,
query,
waitForMessage,
waitForSocketOpen,
sendRawMessage,
sendNode,
logout,
end,
onUnexpectedError,
uploadPreKeys,
uploadPreKeysToServerIfRequired,
requestPairingCode,
/** Waits for the connection to WA to reach a state */
waitForConnectionUpdate: (0, Utils_1.bindWaitForConnectionUpdate)(ev),
sendWAMBuffer,
};
};
exports.makeSocket = makeSocket;
/**
* map the websocket error to the right type
* so it can be retried by the caller
* */
function mapWebSocketError(handler) {
return (error) => {
handler(new boom_1.Boom(`WebSocket Error (${error === null || error === void 0 ? void 0 : error.message})`, { statusCode: (0, Utils_1.getCodeFromWSError)(error), data: error }));
};
}