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