From 06437e182d095ff171f2a38f79c3bdabb93d1694 Mon Sep 17 00:00:00 2001 From: Adhiraj Singh Date: Sun, 22 May 2022 21:21:35 +0530 Subject: [PATCH] feat: add "multi file auth state" implementation 1. add multi file auth state since it's far more efficient than single state 2. deprecate single file auth state (please don't use it anymore) --- .gitignore | 1 + Example/example.ts | 7 +- README.md | 10 +-- src/Utils/auth-utils.ts | 80 +---------------------- src/Utils/index.ts | 4 +- src/Utils/use-multi-file-auth-state.ts | 78 +++++++++++++++++++++++ src/Utils/use-single-file-auth-state.ts | 85 +++++++++++++++++++++++++ 7 files changed, 177 insertions(+), 88 deletions(-) create mode 100644 src/Utils/use-multi-file-auth-state.ts create mode 100644 src/Utils/use-single-file-auth-state.ts diff --git a/.gitignore b/.gitignore index b96baf0..05fa43d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules auth_info*.json +baileys_auth_info* baileys_store*.json output.csv */.DS_Store diff --git a/Example/example.ts b/Example/example.ts index 2dbc38c..fdaa839 100644 --- a/Example/example.ts +++ b/Example/example.ts @@ -1,5 +1,5 @@ import { Boom } from '@hapi/boom' -import makeWASocket, { AnyMessageContent, delay, DisconnectReason, fetchLatestBaileysVersion, makeInMemoryStore, MessageRetryMap, useSingleFileAuthState } from '../src' +import makeWASocket, { AnyMessageContent, delay, DisconnectReason, fetchLatestBaileysVersion, makeInMemoryStore, MessageRetryMap, useMultiFileAuthState } from '../src' import MAIN_LOGGER from '../src/Utils/logger' const logger = MAIN_LOGGER.child({ }) @@ -21,10 +21,9 @@ setInterval(() => { store?.writeToFile('./baileys_store_multi.json') }, 10_000) -const { state, saveState } = useSingleFileAuthState('./auth_info_multi.json') - // start a connection const startSock = async() => { + const { state, saveCreds } = await useMultiFileAuthState('baileys_auth_info') // fetch latest version of WA Web const { version, isLatest } = await fetchLatestBaileysVersion() console.log(`using WA v${version.join('.')}, isLatest: ${isLatest}`) @@ -94,7 +93,7 @@ const startSock = async() => { console.log('connection update', update) }) // listen for when the auth credentials is updated - sock.ev.on('creds.update', saveState) + sock.ev.on('creds.update', saveCreds) return sock } diff --git a/README.md b/README.md index 8de7ef8..ef37949 100644 --- a/README.md +++ b/README.md @@ -140,12 +140,12 @@ You obviously don't want to keep scanning the QR code every time you want to con So, you can load the credentials to log back in: ``` ts -import makeWASocket, { BufferJSON, useSingleFileAuthState } from '@adiwajshing/baileys' +import makeWASocket, { BufferJSON, useMultiFileAuthState } from '@adiwajshing/baileys' import * as fs from 'fs' -// utility function to help save the auth state in a single file -// it's utility ends at demos -- as re-writing a large file over and over again is very inefficient -const { state, saveState } = useSingleFileAuthState('./auth_info_multi.json') +// utility function to help save the auth state in a single folder +// this function serves as a good guide to help write auth & key states for SQL/no-SQL databases, which I would recommend in any production grade system +const { state, saveState } = useMultiFileAuthState('auth_info_baileys') // will use the given state to connect // so if valid credentials are available -- it'll connect without QR const conn = makeWASocket({ auth: state }) @@ -153,7 +153,7 @@ const conn = makeWASocket({ auth: state }) conn.ev.on ('creds.update', saveState) ``` -**Note**: When a message is received/sent, due to signal sessions needing updating, the auth keys (`authState.keys`) will update. Whenever that happens, you must save the updated keys. Not doing so will prevent your messages from reaching the recipient & other unexpected consequences. The `useSingleFileAuthState` function automatically takes care of that, but for any other serious implementation -- you will need to be very careful with the key state management. +**Note**: When a message is received/sent, due to signal sessions needing updating, the auth keys (`authState.keys`) will update. Whenever that happens, you must save the updated keys (`authState.keys.set()` is called). Not doing so will prevent your messages from reaching the recipient & other unexpected consequences. The `useMultiFileAuthState` function automatically takes care of that, but for any other serious implementation -- you will need to be very careful with the key state management. ## Listening to Connection Updates diff --git a/src/Utils/auth-utils.ts b/src/Utils/auth-utils.ts index db72c0e..5e25a4d 100644 --- a/src/Utils/auth-utils.ts +++ b/src/Utils/auth-utils.ts @@ -1,10 +1,9 @@ import { Boom } from '@hapi/boom' import { randomBytes } from 'crypto' import type { Logger } from 'pino' -import { proto } from '../../WAProto' -import type { AuthenticationCreds, AuthenticationState, SignalDataSet, SignalDataTypeMap, SignalKeyStore, SignalKeyStoreWithTransaction, TransactionCapabilityOptions } from '../Types' +import type { AuthenticationCreds, SignalDataSet, SignalDataTypeMap, SignalKeyStore, SignalKeyStoreWithTransaction, TransactionCapabilityOptions } from '../Types' import { Curve, signedKeyPair } from './crypto' -import { BufferJSON, delay, generateRegistrationId } from './generics' +import { delay, generateRegistrationId } from './generics' /** * Adds DB like transaction capability (https://en.wikipedia.org/wiki/Database_transaction) to the SignalKeyStore, @@ -126,79 +125,4 @@ export const initAuthCreds = (): AuthenticationCreds => { unarchiveChats: false } } -} - -// useless key map only there to maintain backwards compatibility -// do not use in your own systems please -const KEY_MAP: { [T in keyof SignalDataTypeMap]: string } = { - 'pre-key': 'preKeys', - 'session': 'sessions', - 'sender-key': 'senderKeys', - 'app-state-sync-key': 'appStateSyncKeys', - 'app-state-sync-version': 'appStateVersions', - 'sender-key-memory': 'senderKeyMemory' -} -/** stores the full authentication state in a single JSON file */ -export const useSingleFileAuthState = (filename: string, logger?: Logger): { state: AuthenticationState, saveState: () => void } => { - // require fs here so that in case "fs" is not available -- the app does not crash - const { readFileSync, writeFileSync, existsSync } = require('fs') - let creds: AuthenticationCreds - let keys: any = { } - - // save the authentication state to a file - const saveState = () => { - logger && logger.trace('saving auth state') - writeFileSync( - filename, - // BufferJSON replacer utility saves buffers nicely - JSON.stringify({ creds, keys }, BufferJSON.replacer, 2) - ) - } - - if(existsSync(filename)) { - const result = JSON.parse( - readFileSync(filename, { encoding: 'utf-8' }), - BufferJSON.reviver - ) - creds = result.creds - keys = result.keys - } else { - creds = initAuthCreds() - keys = { } - } - - return { - state: { - creds, - keys: { - get: (type, ids) => { - const key = KEY_MAP[type] - return ids.reduce( - (dict, id) => { - let value = keys[key]?.[id] - if(value) { - if(type === 'app-state-sync-key') { - value = proto.AppStateSyncKeyData.fromObject(value) - } - - dict[id] = value - } - - return dict - }, { } - ) - }, - set: (data) => { - for(const _key in data) { - const key = KEY_MAP[_key as keyof SignalDataTypeMap] - keys[key] = keys[key] || { } - Object.assign(keys[key], data[_key]) - } - - saveState() - } - } - }, - saveState - } } \ No newline at end of file diff --git a/src/Utils/index.ts b/src/Utils/index.ts index dee1f9f..0e70011 100644 --- a/src/Utils/index.ts +++ b/src/Utils/index.ts @@ -11,4 +11,6 @@ export * from './chat-utils' export * from './lt-hash' export * from './auth-utils' export * from './legacy-msgs' -export * from './baileys-event-stream' \ No newline at end of file +export * from './baileys-event-stream' +export * from './use-single-file-auth-state' +export * from './use-multi-file-auth-state' \ No newline at end of file diff --git a/src/Utils/use-multi-file-auth-state.ts b/src/Utils/use-multi-file-auth-state.ts new file mode 100644 index 0000000..8d80c05 --- /dev/null +++ b/src/Utils/use-multi-file-auth-state.ts @@ -0,0 +1,78 @@ +import { mkdir, readFile, stat, writeFile } from 'fs/promises' +import { join } from 'path' +import { proto } from '../../WAProto' +import { AuthenticationCreds, AuthenticationState, SignalDataTypeMap } from '../Types' +import { initAuthCreds } from './auth-utils' +import { BufferJSON } from './generics' + +/** + * stores the full authentication state in a single folder. + * Far more efficient than singlefileauthstate + * + * Again, I wouldn't endorse this for any production level use other than perhaps a bot. + * Would recommend writing an auth state for use with a proper SQL or No-SQL DB + * */ +export const useMultiFileAuthState = async(folder: string): Promise<{ state: AuthenticationState, saveCreds: () => Promise }> => { + + const writeData = (data: any, file: string) => { + return writeFile(join(folder, file), JSON.stringify(data, BufferJSON.replacer)) + } + + const readData = async(file: string) => { + try { + const data = await readFile(join(folder, file), { encoding: 'utf-8' }) + return JSON.parse(data, BufferJSON.reviver) + } catch(error) { + return null + } + } + + const folderInfo = await stat(folder).catch(() => { }) + if(folderInfo) { + if(!folderInfo.isDirectory()) { + throw new Error(`found something that is not a directory at ${folder}, either delete it or specify a different location`) + } + } else { + await mkdir(folder, { recursive: true }) + } + + const creds: AuthenticationCreds = await readData('creds.json') || initAuthCreds() + + return { + state: { + creds, + keys: { + get: async(type, ids) => { + const data: { [_: string]: SignalDataTypeMap[typeof type] } = { } + await Promise.all( + ids.map( + async id => { + let value = await readData(`${type}-${id}.json`) + if(type === 'app-state-sync-key') { + value = proto.AppStateSyncKeyData.fromObject(data) + } + + data[id] = value + } + ) + ) + + return data + }, + set: async(data) => { + const tasks: Promise[] = [] + for(const category in data) { + for(const id in data[category]) { + tasks.push(writeData(data[category][id], `${category}-${id}.json`)) + } + } + + await Promise.all(tasks) + } + } + }, + saveCreds: () => { + return writeData(creds, 'creds.json') + } + } +} \ No newline at end of file diff --git a/src/Utils/use-single-file-auth-state.ts b/src/Utils/use-single-file-auth-state.ts new file mode 100644 index 0000000..556185c --- /dev/null +++ b/src/Utils/use-single-file-auth-state.ts @@ -0,0 +1,85 @@ +import type { Logger } from 'pino' +import { proto } from '../../WAProto' +import type { AuthenticationCreds, AuthenticationState, SignalDataTypeMap } from '../Types' +import { initAuthCreds } from './auth-utils' +import { BufferJSON } from './generics' + +// useless key map only there to maintain backwards compatibility +// do not use in your own systems please +const KEY_MAP: { [T in keyof SignalDataTypeMap]: string } = { + 'pre-key': 'preKeys', + 'session': 'sessions', + 'sender-key': 'senderKeys', + 'app-state-sync-key': 'appStateSyncKeys', + 'app-state-sync-version': 'appStateVersions', + 'sender-key-memory': 'senderKeyMemory' +} +/** + * @deprecated use multi file auth state instead please + * stores the full authentication state in a single JSON file + * + * DO NOT USE IN A PROD ENVIRONMENT, only meant to serve as an example + * */ +export const useSingleFileAuthState = (filename: string, logger?: Logger): { state: AuthenticationState, saveState: () => void } => { + // require fs here so that in case "fs" is not available -- the app does not crash + const { readFileSync, writeFileSync, existsSync } = require('fs') + let creds: AuthenticationCreds + let keys: any = { } + + // save the authentication state to a file + const saveState = () => { + logger && logger.trace('saving auth state') + writeFileSync( + filename, + // BufferJSON replacer utility saves buffers nicely + JSON.stringify({ creds, keys }, BufferJSON.replacer, 2) + ) + } + + if(existsSync(filename)) { + const result = JSON.parse( + readFileSync(filename, { encoding: 'utf-8' }), + BufferJSON.reviver + ) + creds = result.creds + keys = result.keys + } else { + creds = initAuthCreds() + keys = { } + } + + return { + state: { + creds, + keys: { + get: (type, ids) => { + const key = KEY_MAP[type] + return ids.reduce( + (dict, id) => { + let value = keys[key]?.[id] + if(value) { + if(type === 'app-state-sync-key') { + value = proto.AppStateSyncKeyData.fromObject(value) + } + + dict[id] = value + } + + return dict + }, { } + ) + }, + set: (data) => { + for(const _key in data) { + const key = KEY_MAP[_key as keyof SignalDataTypeMap] + keys[key] = keys[key] || { } + Object.assign(keys[key], data[_key]) + } + + saveState() + } + } + }, + saveState + } +} \ No newline at end of file